mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
M9.6: text animation depth (selectors, animator properties, per-char 3D, path text, presets)
Range selector complete (units, based on, modes, shapes, smoothness, ease high/low, randomize), Wiggly and Expression selectors combined by mode; all AE animator properties incl. line anchor, character offset/value, skew axis, tracking type, fill/stroke HSB+opacity, blur; More Options (anchor grouping, fill & stroke order, inter-character blending); Path Options (text on a mask path); per-character 3D rendered as planes in the 3D compositor; text animation commands and original presets. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
d73f199f38
commit
c5cf189e1a
24 files changed
+2912
-284
No files matched your search
@@ -25,3 +25,4 @@ boa_engine = { workspace = true, features = ["js"] }
|
||||
[dev-dependencies]
|
||||
effectcraft-color = { workspace = true }
|
||||
effectcraft-effects = { workspace = true }
|
||||
effectcraft-text = { workspace = true }
|
||||
+45
-1
@@ -53,6 +53,31 @@ impl ExprHost for Expressions {
|
||||
None => Ok(value.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn eval_text_selector(&self, ctx: &EvalCtx, layer: &Layer, prop: &Property, index: usize, total: usize, selector: [f64; 3]) -> Result<[f64; 3], String> {
|
||||
let Some(e) = prop.expr.as_ref().filter(|_| prop.has_expression()) else {
|
||||
let v = prop.value_at(layer.layer_time(ctx.time)).components();
|
||||
return Ok([v.first().copied().unwrap_or(100.0), v.get(1).copied().unwrap_or(100.0), v.get(2).copied().unwrap_or(100.0)]);
|
||||
};
|
||||
let value = prop.value_at(layer.layer_time(ctx.time));
|
||||
let out = evaluate_out(self, ctx, layer, prop, &value, &e.text, [index as f64 + 1.0, total as f64], selector)?;
|
||||
let num = |o: &runtime::Out| match o {
|
||||
runtime::Out::Num(x) => Some(*x),
|
||||
runtime::Out::Bool(b) => Some(if *b { 100.0 } else { 0.0 }),
|
||||
_ => None,
|
||||
};
|
||||
let r = match &out {
|
||||
runtime::Out::Arr(items) => {
|
||||
let g = |i: usize| items.get(i).and_then(num).or_else(|| items.first().and_then(num));
|
||||
[g(0), g(1), g(2)]
|
||||
}
|
||||
o => [num(o); 3],
|
||||
};
|
||||
match r {
|
||||
[Some(a), Some(b), Some(c)] if a.is_finite() && b.is_finite() && c.is_finite() => Ok([a, b, c]),
|
||||
_ => Err("Error: expression selector amount must be a number or an array of numbers".into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pops the evaluation frame on every exit path.
|
||||
@@ -66,6 +91,23 @@ impl Drop for FrameGuard {
|
||||
/// Evaluate `text` as the expression of `prop` on `layer` at `ctx.time`, given its keyframed
|
||||
/// `value`. Other properties' expressions are evaluated through `host`.
|
||||
pub fn evaluate(host: &dyn ExprHost, ctx: &EvalCtx, layer: &Layer, prop: &Property, value: &Value, text: &str) -> Result<Value, String> {
|
||||
let out = evaluate_out(host, ctx, layer, prop, value, text, [1.0, 1.0], [100.0; 3])?;
|
||||
runtime::to_value(&out, value)
|
||||
}
|
||||
|
||||
/// Run an expression and return the raw script result. `text_sel` / `selector` are the text
|
||||
/// Expression Selector's `[textIndex, textTotal]` and `selectorValue`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn evaluate_out(
|
||||
host: &dyn ExprHost,
|
||||
ctx: &EvalCtx,
|
||||
layer: &Layer,
|
||||
prop: &Property,
|
||||
value: &Value,
|
||||
text: &str,
|
||||
text_sel: [f64; 2],
|
||||
selector: [f64; 3],
|
||||
) -> Result<runtime::Out, String> {
|
||||
let depth = FRAMES.with(|f| f.borrow().len());
|
||||
if depth >= MAX_DEPTH {
|
||||
return Err("Error: expressions reference each other too deeply (circular reference?)".into());
|
||||
@@ -84,6 +126,8 @@ pub fn evaluate(host: &dyn ExprHost, ctx: &EvalCtx, layer: &Layer, prop: &Proper
|
||||
uid: prop.uid,
|
||||
index: ctx.comp.index_of(layer.id).unwrap_or(0) as f64,
|
||||
frame_duration: ctx.comp.frame_duration().seconds(),
|
||||
text_sel,
|
||||
selector,
|
||||
};
|
||||
// Answer the requests nearly every expression makes up front.
|
||||
let mut frame = Frame::default();
|
||||
@@ -95,7 +139,7 @@ pub fn evaluate(host: &dyn ExprHost, ctx: &EvalCtx, layer: &Layer, prop: &Proper
|
||||
let _guard = FrameGuard;
|
||||
for _ in 0..MAX_PASSES {
|
||||
match runtime::run(text, &begin) {
|
||||
Run::Done(r) => return r.and_then(|o| runtime::to_value(&o, value)),
|
||||
Run::Done(r) => return r,
|
||||
Run::Pending => {
|
||||
let misses = FRAMES.with(|f| f.borrow_mut().get_mut(depth).map(|fr| std::mem::take(&mut fr.misses)).unwrap_or_default());
|
||||
for m in misses {
|
||||
|
||||
@@ -11,13 +11,20 @@ var __S = null;
|
||||
var __rc = 0;
|
||||
var __seed = 0;
|
||||
var __timeless = false;
|
||||
// Text Expression Selector inputs (1, 1, [100, 100, 100] outside a selector).
|
||||
var textIndex = 1;
|
||||
var textTotal = 1;
|
||||
var selectorValue = [100, 100, 100];
|
||||
|
||||
// Request ops (must match runtime.rs).
|
||||
var __COMP = 0, __COMPNAME = 1, __LAYER = 2, __LINFO = 3, __CHILD = 4, __PINFO = 5, __VALUE = 6, __XFORM = 7, __RECT = 8, __MARKERS = 9;
|
||||
|
||||
function __begin(t, v, c, l, path, uid, idx, fd) {
|
||||
function __begin(t, v, c, l, path, uid, idx, fd, ti, tt, sv) {
|
||||
time = t;
|
||||
value = v;
|
||||
textIndex = ti;
|
||||
textTotal = tt;
|
||||
selectorValue = sv;
|
||||
__S = { c: c, l: l, path: path, uid: uid, idx: idx, fd: fd, comp: null, layer: null, prop: null, vars: {} };
|
||||
__rc = 0;
|
||||
__seed = 0;
|
||||
|
||||
@@ -49,6 +49,9 @@ pub struct Begin<'a> {
|
||||
pub uid: u64,
|
||||
pub index: f64,
|
||||
pub frame_duration: f64,
|
||||
/// Text Expression Selector `[textIndex, textTotal]` and `selectorValue` (percent).
|
||||
pub text_sel: [f64; 2],
|
||||
pub selector: [f64; 3],
|
||||
}
|
||||
|
||||
/// Outcome of one script run.
|
||||
@@ -228,6 +231,9 @@ impl Runtime {
|
||||
JsValue::from(b.uid as f64),
|
||||
JsValue::from(b.index),
|
||||
JsValue::from(b.frame_duration),
|
||||
JsValue::from(b.text_sel[0]),
|
||||
JsValue::from(b.text_sel[1]),
|
||||
to_js(&crate::host::Resp::List(b.selector.iter().map(|x| crate::host::Resp::Num(*x)).collect()), ctx),
|
||||
];
|
||||
if let Err(e) = self.begin.call(&JsValue::undefined(), &args, ctx) {
|
||||
return Run::Done(Err(format_error(&e.to_string(), line_count(text))));
|
||||
|
||||
@@ -942,3 +942,60 @@ fn error_messages_point_at_the_users_line() {
|
||||
// An unfinished last line is reported on that line, not on the hidden wrapper.
|
||||
assert!(f.err("5 +").starts_with("Error at line 1: SyntaxError"));
|
||||
}
|
||||
|
||||
/// Text Expression Selector: Amount evaluated per unit with textIndex / textTotal /
|
||||
/// selectorValue.
|
||||
#[test]
|
||||
fn text_expression_selector_values() {
|
||||
let mut p = Project::default();
|
||||
let comp = Comp::new(400, 200, FrameRate::FPS_30, Tick::from_seconds_f64(2.0));
|
||||
let cid = p.add_item("Main", Label::Sandstone, None, ItemKind::Comp(comp.clone().into()));
|
||||
let mut l = build::layer(&mut p, &comp, "T", LayerSource::Text, (400, 200), None);
|
||||
l.props.prop_mut("text/sourceText").unwrap().value = Value::Text(Box::new(TextDoc { text: "ABCD".into(), size: 40.0, ..Default::default() }));
|
||||
let mut next = p.next_id;
|
||||
let mut ids = Ids(&mut next);
|
||||
let props = build::text_anim_props(&mut ids, "opacity", false);
|
||||
let mut a = build::text_animator(&mut ids, "Animator 1", props);
|
||||
let sel = build::expression_selector(&mut ids, "Expression Selector 1");
|
||||
a.sub_mut("selectors").unwrap().children.push(sel.into());
|
||||
p.next_id = next;
|
||||
l.props.group_mut("text/animators").unwrap().children.push(a.into());
|
||||
let lid = l.id;
|
||||
p.comp_mut(cid).unwrap().layers.push(l);
|
||||
let sel_of = |p: &Project| {
|
||||
let c = p.comp(cid).unwrap();
|
||||
let l = c.layer(lid).unwrap();
|
||||
let ctx = EvalCtx { project: p, comp_id: cid, comp: c, time: Tick::ZERO, expr: Some(&Expressions) };
|
||||
let doc = effectcraft_render::text::source_text(&ctx, l).unwrap();
|
||||
let lay = effectcraft_text::layout_doc(&doc);
|
||||
let anim = l.props.group("text/animators/#1").unwrap();
|
||||
effectcraft_render::text::animator_selection(&ctx, l, anim, &lay).into_iter().map(|v| (v[0] * 1000.0).round() / 1000.0).collect::<Vec<_>>()
|
||||
};
|
||||
// Default: selectorValue * textIndex / textTotal, intersected with the full range selector.
|
||||
assert_eq!(sel_of(&p), vec![0.25, 0.5, 0.75, 1.0]);
|
||||
let set_expr = |p: &mut Project, text: &str| {
|
||||
let l = p.comp_mut(cid).unwrap().layer_mut(lid).unwrap();
|
||||
l.props.prop_mut("text/animators/#1/selectors/#2/amount").unwrap().expr = Some(Expression { text: text.into(), enabled: true });
|
||||
};
|
||||
// Range selector covering the first half → selectorValue 0 for the second half.
|
||||
p.comp_mut(cid).unwrap().layer_mut(lid).unwrap().props.prop_mut("text/animators/#1/selectors/#1/end").unwrap().value = Value::Scalar(50.0);
|
||||
assert_eq!(sel_of(&p), vec![0.25, 0.5, 0.0, 0.0]);
|
||||
// Plain numbers, conditionals on textIndex (1-based) and arrays all work.
|
||||
p.comp_mut(cid).unwrap().layer_mut(lid).unwrap().props.prop_mut("text/animators/#1/selectors/#1/end").unwrap().value = Value::Scalar(100.0);
|
||||
set_expr(&mut p, "textIndex % 2 == 0 ? 100 : 0");
|
||||
assert_eq!(sel_of(&p), vec![0.0, 1.0, 0.0, 1.0]);
|
||||
set_expr(&mut p, "[50, 0, 0]");
|
||||
assert_eq!(sel_of(&p), vec![0.5; 4]);
|
||||
// Mode Add after the range: 1 + v.
|
||||
let l = p.comp_mut(cid).unwrap().layer_mut(lid).unwrap();
|
||||
l.props.prop_mut("text/animators/#1/selectors/#2/mode").unwrap().value = Value::Enum(0);
|
||||
set_expr(&mut p, "-textIndex / textTotal * 100");
|
||||
assert_eq!(sel_of(&p), vec![0.75, 0.5, 0.25, 0.0]);
|
||||
// Errors fall back to the static amount (100%).
|
||||
set_expr(&mut p, "nope(");
|
||||
assert_eq!(sel_of(&p), vec![1.0; 4]);
|
||||
// Outside a selector the expression still evaluates (textIndex = textTotal = 1).
|
||||
set_expr(&mut p, build::EXPRESSION_SELECTOR_DEFAULT);
|
||||
let v = eval_property(&p, cid, lid, "text/animators/#1/selectors/#2/amount", 0.0).unwrap();
|
||||
assert_eq!(v, Value::Vec3([100.0; 3]));
|
||||
}
|
||||
Reference in new issue
Block a user