Text strokes were always drawn with round joins. The document gets a
Line Join (`lineJoin: miter|round|bevel` in layer.setText / newText),
Miter for new text as in After Effects, Round for documents saved before
it so their look is kept. The 2D and Advanced 3D (extruded) text strokes
use it, expressions' style copies carry it, and the Character panel has
Miter / Round / Bevel buttons under the stroke width.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LayerInfo, sourceRectAtTime and Layer.source now resolve the effective footage item/size through the collapsed instance EvalCtx, matching the rendered pixels. Adds a regression test with different original and replacement footage sizes.
Carry property, Font and Media Replacement overrides into the collapsed nested EvalCtx so nested layers render beyond the precomp frame instead of falling back to a clipped, flattened image. Key the layer cache on instance overrides to keep distinct collapsed instances isolated.
A missing or unusable curve no longer consumes the last value. The long form reads argument 5 and the short form argument 3, with the other arities falling back to linear interpolation. Adds the missing-curve and four-element-array cases reported in review.
- accept only four-element arrays for bezier(): extra values make the curve
malformed, so it falls back to linear like the other invalid forms (#1884);
- stop Newton on the estimated y error (f * dy/dx) instead of a residual in x
alone: near the endpoints dy/dx is in the thousands, so a 1e-9 residual in x
could still move the returned value;
- tests: a five-element curve is linear, and three steep curves match a
bisection reference within 1e-6.
Add bezier(t, tMin, tMax, v1, v2, [x1, y1, x2, y2]) and the short bezier(t, v1, v2, curve) form: a CSS cubic-bezier easing curve solved with Newton-Raphson and a bisection fallback.
Scalars and arrays work like linear/ease; x1/x2 clamp to 0..1, y1/y2 may overshoot, t clamps to tMin..tMax, and a curve that is not four finite numbers falls back to linear instead of throwing.
Tests cover known curves (ease is [1/3, 0, 2/3, 1]), clamps, arrays, the short form, invalid curves and overshoot, plus two fuzz seeds.
Closes#1884.
A Layer is now a callable function with its own prototype, which costs
more to make than the plain object it was. __layer(c, l) makes each
layer once per evaluation (kept in __S, which __begin resets), so an
expression naming the same layer many times builds it once. That covers
thisComp.layer(...) in a loop, .parent, a Layer Control's value and
thisLayer.
The checks for a layer (the result, thisComp.layer(layer, offset), the
property proxy) read __isLayer only on functions. Numbers and other
primitives are no longer boxed to look the property up.
Timing (release, Windows, medians of 2000 evaluations, PR vs main):
- one evaluation building one layer: about 22 vs 16 us;
- thisLayer.transform.opacity: about 200 vs 170 us;
- 20000 thisComp.layer(1) lookups in one evaluation: 95 vs 138 ms.
In After Effects a layer is a property group: thisLayer("transform"),
thisComp.layer("Other")("effects")("Blur"), thisLayer(1). In
EffectCraft's expression runtime a Layer was a plain object, so those
failed with "TypeError: not a callable function", including the
references the pick whip writes with Compact English off
(thisComp.layer("Other")("effects")("ec.noise.fractal")(...)).
A Layer is now callable like a property group (built as __mkGroup
builds groups), looking up its own properties by match name, name or
index, and keeps its methods, name and index. The places that took a
layer for a plain object (the result reading as the layer's index,
thisComp.layer(layer, offset), a Layer Control's value handed through
the property proxy) accept it.
Test: layers_are_callable_property_groups (fails before).
An array replacer now copies each source object once per call, so a cycle stays a cycle and the native check throws TypeError, while shared objects still serialize. Documents which threads evaluate expressions, what the 256 KiB guard covers (tested on a 512 KiB thread), that it does not apply on wasm32, and why eval is indirect.
eval, Function and the generator/async function constructors now check the nesting of their source like the expression text. join, toLocaleString, flat and JSON.stringify check the run's stack use per level, results nested past the detached levels become [object], and Proxy/Reflect are no longer available to expressions. Each of these overflowed the stack and aborted the app.
- scriptui.js: replace the self-comparison NaN check (o.max === o.max) with
the explicit Number.isNaN(o.max) (S1764).
- sw.js: mark the fire-and-forget cache.put() with void and a .catch() so the
promise rejection is handled (S9383).
- prelude.js: return __err(...) from the activeCamera getter so every path
yields a value (S4275); __err always throws, so behavior is unchanged.
The After Effects stagger idiom
id = thisProperty.propertyGroup(2).propertyIndex;
failed with "not a callable function": the expression object model had
no propertyGroup() and property groups had no propertyIndex.
- Property.propertyGroup(countUp = 1) and PropertyGroup.propertyGroup()
return the group countUp levels up; from a top-level group they reach
the layer. A count below 1, not a number, or above the layer is an
error naming propertyGroup, never a crash.
- PropertyGroup.propertyIndex: the group's 1-based index among its
siblings (Child requests now carry it).
- One new host request (Ancestor) walks up from the node's full uid
path, so it also works for the property being evaluated.
- Tests for the stagger expression, effect parameters, reaching the
layer, other layers' properties and groups and out-of-range counts;
the fuzzer seeds and tokens include the new members.
Round nonzero duration frame counts away from zero while retaining
absolute-time rounding, default argument selection and numeric coercion.
Cover fractional durations, absolute controls, defaults and coercion.
Sort expected paths in the existing footage test for temporary-directory portability.
Co-authored-by: Codex <codex@openai.com>
layer.source was undefined in expressions, and a layer from a Layer Control had no transform.
Both now work.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
fanning_out_reads_stop_at_a_budget expects the run-count error, but under heavy load
the 5 s wall-clock budget could fire first (different error). Add a test-only
per-thread time limit and lift it in that test. Wall-clock assertions in the other
new tests are now only generous hang guards (>=10x), the fuzzer's hang deadline is
24x MAX_TIME and the perf bound is 10x looser.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Problem: expression text is input we don't control, and nothing fuzzed the
expression parser and evaluator. A seeded mutation fuzzer found stack
overflows (a few hundred `?:`/`=>`/`if`, a few thousand `.`/`<`/`()` links,
75 nested template substitutions or regex groups, a chain inside one
template), hangs (an expression reading its own layer's transform nested
2^16 times; fanning-out reads; 10M-iteration loops re-run per read; loops
calling looping functions), an i64 overflow panic in wiggle noise, and NaN
path vertices tripping the rasterizer's clipping assertion.
Fix: a dependency-free seeded fuzzer (crates/expr/src/tests/fuzz.rs: real
AE-style seeds, char/token mutations, worker thread with a 2 MiB stack,
catch_unwind, deadline, env vars for long runs); a weighted nesting
estimate covering chains without brackets and template/regex insides;
cycle detection per (property, time, text); a per-evaluation budget of
20 000 script runs and 5 s (runs are interrupted through boa's budgeted
evaluation; native only); loop limit 1M; wrapping noise lattice indices;
non-finite path results rejected. Each fix has a regression test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>