mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-11 16:33:01 -06:00
M0.9: AE-style Properties panel, context tab titles, AE default workspace; headless UI snapshot tool
- Properties panel (panels/properties.rs): Layer Transform with keyframe navigator or stopwatch, AE value formatting (integers, %, Nx+N° rotation), linked Scale, Reset; Text (font/style/size/leading/tracking/stroke, fill/stroke toggles); Paragraph (7 alignments); Text Animation > Add Animator; scrolling. - Tabs name what they show: Composition <comp>, <comp> timeline, Effect Controls <layer>, Properties: <layer>. - Default workspace: full-height right column, Preview over Properties. - engine: time.go nextKey/prevKey accept prop uid; layer.setText supports all seven justify modes, applyFill/applyStroke and Auto leading (fixes the Paragraph 'justify' button mapping to Justify All). - examples/snapshot: render the real UI headless (egui_kittest + wgpu) with scripted control-channel steps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
214345df13
commit
4f47a01fed
@@ -95,8 +95,17 @@ fn text_doc_from(p: &Value, base: TextDoc) -> TextDoc {
|
||||
if let Some(v) = f_p(p, "tracking") {
|
||||
d.tracking = v;
|
||||
}
|
||||
if let Some(v) = f_p(p, "leading") {
|
||||
d.leading = Some(v);
|
||||
match p.get("leading") {
|
||||
Some(Value::Number(n)) => d.leading = n.as_f64(),
|
||||
// "auto" or null: Auto Leading (120% of the font size).
|
||||
Some(Value::String(_) | Value::Null) => d.leading = None,
|
||||
_ => {}
|
||||
}
|
||||
if let Some(v) = b_p(p, "applyFill") {
|
||||
d.apply_fill = v;
|
||||
}
|
||||
if let Some(v) = b_p(p, "applyStroke") {
|
||||
d.apply_stroke = v;
|
||||
}
|
||||
if let Some(v) = b_p(p, "allCaps") {
|
||||
d.all_caps = v;
|
||||
@@ -108,10 +117,14 @@ fn text_doc_from(p: &Value, base: TextDoc) -> TextDoc {
|
||||
d.faux_italic = v;
|
||||
}
|
||||
if let Some(j) = str_p(p, "justify") {
|
||||
// AE's seven Paragraph alignment buttons.
|
||||
d.justify = match j.to_ascii_lowercase().as_str() {
|
||||
"center" | "centre" => Justify::Center,
|
||||
"right" => Justify::Right,
|
||||
"justify" | "justifyall" => Justify::JustifyAll,
|
||||
"justify" | "justifyleft" | "justifylastleft" => Justify::JustifyLastLeft,
|
||||
"justifycenter" | "justifylastcenter" => Justify::JustifyLastCenter,
|
||||
"justifyright" | "justifylastright" => Justify::JustifyLastRight,
|
||||
"justifyall" => Justify::JustifyAll,
|
||||
_ => Justify::Left,
|
||||
};
|
||||
}
|
||||
@@ -927,7 +940,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Edit Text",
|
||||
[],
|
||||
None,
|
||||
"{layer?, text?, size?, font?, style?, fill?, stroke?, strokeWidth?, tracking?, leading?, justify?, allCaps?, fauxBold?, fauxItalic?}",
|
||||
"{layer?, text?, size?, font?, style?, fill?, stroke?, applyFill?, applyStroke?, strokeWidth?, tracking?, leading?: px|\"auto\", justify?: left|center|right|justifyLeft|justifyCenter|justifyRight|justifyAll, allCaps?, fauxBold?, fauxItalic?}",
|
||||
has_layers,
|
||||
set_text
|
||||
),
|
||||
|
||||
@@ -56,13 +56,23 @@ fn go(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
} else {
|
||||
comp.layers.iter().filter(|l| s.state.selected_layers.contains(&l.id)).collect()
|
||||
};
|
||||
// `prop` (uid) narrows the search to one property: the Properties panel's ◀ ▶ arrows.
|
||||
let only = p.get("prop").and_then(Value::as_u64);
|
||||
if only.is_some() {
|
||||
times.clear();
|
||||
}
|
||||
for l in layers {
|
||||
l.props.walk("", &mut |_, pr| {
|
||||
if only.is_some_and(|u| u != pr.uid) {
|
||||
return;
|
||||
}
|
||||
for k in &pr.keys {
|
||||
times.push(l.comp_time(k.time));
|
||||
}
|
||||
});
|
||||
times.extend(l.markers.iter().map(|m| l.comp_time(m.time)));
|
||||
if only.is_none() {
|
||||
times.extend(l.markers.iter().map(|m| l.comp_time(m.time)));
|
||||
}
|
||||
}
|
||||
times.sort();
|
||||
times.dedup();
|
||||
@@ -93,6 +103,6 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
cmd!("time.layerOut", "Go to Layer Out Point", [], Some("O"), "{}", has_comp, |s, _| go(s, &json!({"to": "layerOut"}))),
|
||||
cmd!("time.nextKey", "Go to Next Keyframe or Marker", [], Some("K"), "{}", has_comp, |s, _| go(s, &json!({"to": "nextKey"}))),
|
||||
cmd!("time.previousKey", "Go to Previous Keyframe or Marker", [], Some("J"), "{}", has_comp, |s, _| go(s, &json!({"to": "prevKey"}))),
|
||||
cmd!("time.go", "Go To", [], None, "{to: start|end|workStart|workEnd|layerIn|layerOut|nextKey|prevKey}", has_comp, go),
|
||||
cmd!("time.go", "Go To", [], None, "{to: start|end|workStart|workEnd|layerIn|layerOut|nextKey|prevKey, prop?: uid}", has_comp, go),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -114,3 +114,58 @@ fn time_navigation() {
|
||||
s.execute("time.set", json!({"frame": 45})).unwrap();
|
||||
assert_eq!(s.execute("time.step", json!({"frames": 5})).unwrap()["frame"], 50);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn time_navigation_per_property() {
|
||||
let mut s = demo();
|
||||
// An animated property and the comp times of its keys.
|
||||
let comp = s.active_comp().unwrap().clone();
|
||||
let mut found = None;
|
||||
for l in &comp.layers {
|
||||
l.props.walk("", &mut |_, pr| {
|
||||
if found.is_none() && pr.keys.len() >= 2 {
|
||||
found = Some((pr.uid, pr.keys.iter().map(|k| l.comp_time(k.time)).collect::<Vec<_>>()));
|
||||
}
|
||||
});
|
||||
}
|
||||
let (uid, times) = found.expect("demo has an animated property");
|
||||
s.set_time(effectcraft_time::Tick::ZERO);
|
||||
let half = comp.frame_duration().0 / 2;
|
||||
let expect = times.iter().copied().filter(|t| t.0 > half).min().unwrap();
|
||||
s.execute("time.go", json!({"to": "nextKey", "prop": uid})).unwrap();
|
||||
let snap = |t| comp.frame_rate.snap(t);
|
||||
assert_eq!(s.time(), snap(expect));
|
||||
// Going back from past the last key lands on the last key.
|
||||
s.set_time(comp.duration);
|
||||
s.execute("time.go", json!({"to": "prevKey", "prop": uid})).unwrap();
|
||||
assert_eq!(s.time(), snap(*times.iter().max().unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_text_paragraph_fill_and_leading() {
|
||||
use effectcraft_keyframe::{Justify, Value as KValue};
|
||||
let mut s = demo();
|
||||
let t = s.execute("layer.newText", json!({"text": "Hi"})).unwrap()["layer"].as_u64().unwrap();
|
||||
let doc = |s: &Session| {
|
||||
let l = s.active_comp().unwrap().layer(crate::project::LayerId(t)).unwrap().clone();
|
||||
match &l.props.prop("text/sourceText").unwrap().value {
|
||||
KValue::Text(d) => (**d).clone(),
|
||||
v => panic!("{v:?}"),
|
||||
}
|
||||
};
|
||||
for (key, j) in [
|
||||
("justifyLeft", Justify::JustifyLastLeft),
|
||||
("justifyCenter", Justify::JustifyLastCenter),
|
||||
("justifyRight", Justify::JustifyLastRight),
|
||||
("justifyAll", Justify::JustifyAll),
|
||||
("center", Justify::Center),
|
||||
] {
|
||||
s.execute("layer.setText", json!({"layer": t, "justify": key})).unwrap();
|
||||
assert_eq!(doc(&s).justify, j, "{key}");
|
||||
}
|
||||
s.execute("layer.setText", json!({"layer": t, "leading": 50, "applyFill": false, "applyStroke": true})).unwrap();
|
||||
let d = doc(&s);
|
||||
assert_eq!((d.leading, d.apply_fill, d.apply_stroke), (Some(50.0), false, true));
|
||||
s.execute("layer.setText", json!({"layer": t, "leading": "auto"})).unwrap();
|
||||
assert_eq!(doc(&s).leading, None);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user