mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 21:16:52 -06:00
Clipboard: keyframe text for effect properties (#1908)
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
//! number in comp time, then the value) and static ones as a single row with no frame. Easing
|
||||
//! and tangents aren't part of the format; EffectCraft's own paste keeps them.
|
||||
|
||||
use effectcraft_project::{AutoOrient, Comp, ItemKind, Layer, LayerId, LayerSource, Project, Property, Uid};
|
||||
use effectcraft_project::{AutoOrient, Comp, GroupKind, ItemKind, Layer, LayerId, LayerSource, Node, ParamUi, Project, PropGroup, Property, Uid};
|
||||
|
||||
use crate::KeyRef;
|
||||
|
||||
@@ -39,14 +39,25 @@ enum Values {
|
||||
/// A 2D layer's Transform vector: the third component is the one the layer shows (it may
|
||||
/// keep another from pasted 3D keys).
|
||||
Flat(f64),
|
||||
/// RGBA 0–1 as alpha, red, green, blue 0–255.
|
||||
Argb,
|
||||
/// A popup's 0-based choice as After Effects' 1-based one.
|
||||
OneBased,
|
||||
}
|
||||
|
||||
impl Values {
|
||||
fn row(self, mut v: Vec<f64>) -> Vec<f64> {
|
||||
if let (Values::Flat(z), Some(c)) = (self, v.get_mut(2)) {
|
||||
*c = z;
|
||||
match (self, v.as_slice()) {
|
||||
(Values::Flat(z), _) => {
|
||||
if let Some(c) = v.get_mut(2) {
|
||||
*c = z;
|
||||
}
|
||||
v
|
||||
}
|
||||
(Values::Argb, [r, g, b, a]) => [a, r, g, b].map(|c| c * 255.0).to_vec(),
|
||||
(Values::OneBased, _) => v.into_iter().map(|c| c + 1.0).collect(),
|
||||
_ => v,
|
||||
}
|
||||
v
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,10 +129,42 @@ fn channel(layer: &Layer, pr: &Property) -> Option<Channel> {
|
||||
match chain.first().and_then(|n| n.as_group()).map(|g| g.match_id.as_str()) {
|
||||
None if pr.match_id == "timeRemap" => Some(Channel { header: "Time Remap".into(), labels: labels("seconds", 1), values: Values::Plain }),
|
||||
Some("transform") => transform(layer, pr),
|
||||
Some("effects") => effect(&chain, pr),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// An effect property: `Effects`, the effect's name (not a custom one) and place on the layer,
|
||||
/// then the property's name and place, counting the effect's hidden input layer as the first.
|
||||
fn effect(chain: &[&Node], pr: &Property) -> Option<Channel> {
|
||||
let [Node::Group(effects), Node::Group(fx), _] = chain else { return None };
|
||||
let GroupKind::Effect { effect } = &fx.kind else { return None };
|
||||
let name = effectcraft_effects::find(effect).map_or(fx.name.as_str(), |spec| spec.name);
|
||||
let (labels, values) = by_ui(pr)?;
|
||||
let header = format!("Effects\t{name} #{}\t{} #{}", place(effects, fx.uid)?, pr.name, place(fx, pr.uid)? + 1);
|
||||
Some(Channel { header, labels, values })
|
||||
}
|
||||
|
||||
/// 1-based position of a child in its group.
|
||||
fn place(group: &PropGroup, uid: Uid) -> Option<usize> {
|
||||
group.children.iter().position(|c| c.uid() == uid).map(|i| i + 1)
|
||||
}
|
||||
|
||||
/// Column labels and values for an effect or text property, by how it's edited (After Effects
|
||||
/// labels sliders, checkboxes and popups with no unit).
|
||||
fn by_ui(pr: &Property) -> Option<(Vec<String>, Values)> {
|
||||
let dims = pr.value.dims();
|
||||
Some(match &pr.ui {
|
||||
ParamUi::Color => (["alpha ", "red ", "green ", "blue "].map(String::from).to_vec(), Values::Argb),
|
||||
ParamUi::Popup { .. } => (labels("", dims), Values::OneBased),
|
||||
ParamUi::Percent => (labels("percent", dims), Values::Plain),
|
||||
ParamUi::Angle => (labels("degrees", dims), Values::Plain),
|
||||
ParamUi::Point | ParamUi::Point3 => (labels("pixels", dims), Values::Plain),
|
||||
ParamUi::Number | ParamUi::Slider { .. } | ParamUi::Pixels | ParamUi::Checkbox => (labels("", dims), Values::Plain),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn transform(layer: &Layer, pr: &Property) -> Option<Channel> {
|
||||
let (id, unit, z) = TRANSFORM.iter().find(|(id, ..)| *id == pr.match_id)?;
|
||||
if !active(layer, id) || (pr.three_d_only && !layer.is_3d()) {
|
||||
|
||||
@@ -175,6 +175,47 @@ fn time_remap_writes_source_seconds_at_comp_frames() {
|
||||
assert!(text.contains("Layer\nTime Remap\n\tFrame\tseconds\t\n\t10\t0\t\n\t16\t2.5\t\n"), "{text}");
|
||||
}
|
||||
|
||||
/// Selects the layer's properties at `paths` and copies them.
|
||||
fn copy_props(s: &mut Session, lid: LayerId, paths: &[&str]) -> String {
|
||||
for (i, path) in paths.iter().enumerate() {
|
||||
s.execute("prop.select", json!({"layer": lid.0, "path": path, "add": i > 0, "selectKeys": false})).unwrap();
|
||||
}
|
||||
s.execute("edit.copy", json!({})).unwrap();
|
||||
s.state.keyframe_text.clone().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effect_properties_are_written_as_after_effects_does() {
|
||||
// The blocks After Effects 2019 wrote for the same effects and values.
|
||||
let (mut s, l) = session();
|
||||
for fx in ["Fill", "Checkbox Control", "Dropdown Menu Control", "Slider Control", "Point Control"] {
|
||||
s.execute("effect.apply", json!({"layer": l.0, "effect": fx})).unwrap();
|
||||
}
|
||||
let t = frame(&s, 12);
|
||||
add_key(&mut s, l, "effects/#1/color", Tick::ZERO, json!([1, 0.5, 0.25, 1]));
|
||||
add_key(&mut s, l, "effects/#1/color", t, json!([0, 0, 1, 1]));
|
||||
add_key(&mut s, l, "effects/#2/checkbox", Tick::ZERO, json!(true));
|
||||
add_key(&mut s, l, "effects/#2/checkbox", t, json!(false));
|
||||
s.execute("prop.set", json!({"layer": l.0, "path": "effects/#3/menu", "value": 1})).unwrap();
|
||||
s.execute("prop.set", json!({"layer": l.0, "path": "effects/#4/slider", "value": 20.5})).unwrap();
|
||||
s.execute("prop.set", json!({"layer": l.0, "path": "effects/#5/point", "value": [10, 20]})).unwrap();
|
||||
// After Effects writes a renamed effect under the effect's own name.
|
||||
let slider = s.active_comp().unwrap().layer(l).unwrap().props.group("effects/#4").unwrap().uid;
|
||||
s.execute("prop.renameGroup", json!({"layer": l.0, "prop": slider, "name": "My Slider"})).unwrap();
|
||||
let text = copy_props(&mut s, l, &["effects/#1/color", "effects/#2/checkbox", "effects/#3/menu", "effects/#4/slider", "effects/#5/point"]);
|
||||
assert!(
|
||||
text.contains(
|
||||
"Layer\n\
|
||||
Effects\tFill #1\tColor #4\n\tFrame\talpha \tred \tgreen \tblue \t\n\t0\t255\t255\t127.5\t63.75\t\n\t12\t255\t0\t0\t255\t\n\n\
|
||||
Effects\tCheckbox Control #2\tCheckbox #2\n\tFrame\t\t\n\t0\t1\t\n\t12\t0\t\n\n\
|
||||
Effects\tDropdown Menu Control #3\tMenu #2\n\tFrame\t\t\n\t\t2\t\n\n\
|
||||
Effects\tSlider Control #4\tSlider #2\n\tFrame\t\t\n\t\t20.5\t\n\n\
|
||||
Effects\tPoint Control #5\tPoint #2\n\tFrame\tX pixels\tY pixels\t\n\t\t10\t20\t\n\n"
|
||||
),
|
||||
"{text}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_2d_layer_writes_the_z_it_shows() {
|
||||
// Keys pasted from a 3D layer keep their Z on a 2D one, which ignores it.
|
||||
|
||||
Reference in New Issue
Block a user