* 3D: Orientation and X/Y/Z Rotation turn Z first, keyed Orientation slerps, as After Effects
After Effects composes a 3D layer's (and a camera's) Orientation and its X/Y/Z
Rotation as Rx * Ry * Rz each, Orientation before the axis rotations; EffectCraft
used Rz * Ry * Rx. Measured on synthetic AE 2026 cases: Orientation [10,20,30]
(488) was 5.2 % of the frame off and now matches to the anti-aliased edge (0.03 %);
X/Y/Z Rotation 10/20/30 (800), Orientation + X Rotation (801) and one-node
cameras (804, 805) likewise.
Keyed Orientation moves along the shortest arc between the keys (quaternion
slerp, eased in time, angles written in [0, 360)): keys [0,0,0] -> [10,20,30]
read back [6.2643, 9.3531, 15.2190] half-way where EffectCraft gave [5,10,15];
[0,0,0] -> [60,80,40] linear and eased match AE's read-backs within 0.002 deg.
lookAt() returns AE's angles (z axis on the target, x axis level; two AE
read-backs within 1e-3 deg). The camera tracker, VR editor, Look At and orbit
commands use the new decomposition.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keyframes: a linear side next to an eased side has 16.67 % influence, as After Effects
In a segment that is Bezier on one side and linear on the other, the linear
side keeps the segment's average speed, but with the 16.67 % influence After
Effects shows for linear keys in the Keyframe Velocity dialog (and reports to
scripts), not 33.3 %. Measured on an After Effects 2026 time-remap render (a
square at x = 50 + 455 * remap): out of a key at 2 s (3.5, speed 0, influence
50 %) into a linear key at 4 s (0.5), AE plays 3.290 at 2.5 s and 2.657 at
3 s; EffectCraft played 3.308 and 2.629 (8 and 13 px off), now the same
square positions to the pixel.
LINEAR_SIDE_INFLUENCE is used by evaluation (scalar, per-dimension, paths and
spatial), side_ease (Graph Editor, velocity dialog, keyIn/OutTemporalEase) and
the Lottie export, so exported easing plays as EffectCraft does. Segments
linear on both sides stay straight lines.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Timewarp: retime the layer's source without the effects above it, as After Effects
After Effects' Timewarp reads the layer's source (with its masks) at the
retimed times and ignores the effects applied before it: a red solid with a
Gradient Ramp and then Timewarp (Speed 25) renders as the red solid in AE
2026 (EXPORTER e2 read-back). EffectCraft retimed the layer with its
effects (mean difference 127.5/255 over the comp), now identical (0, CPU and
GPU). Echo, Posterize Time and the other time effects keep reading the
effects before them, as measured before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Image sequences: a gap in the numbering holds the previous frame, as After Effects
After Effects 2026 shows the last existing file across a gap in a sequence's
numbering (case 860: files 1, 2, 4, 5 show frame 2 at frame 3; VERIFY
read-back). EffectCraft drew colour bars there. The media pool now holds the
previous file (Footage::sequence_file_held); colour bars remain only where no
file precedes the frame. Missing-frame reports on import are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Review: keyed Orientation uses LINEAR_SIDE_INFLUENCE for linear sides; real NaN assert
evaluate_orientation still gave a linear side (and the unreachable missing-key fallback)
1/3 influence while segment_value, side_ease and the Lottie export use
LINEAR_SIDE_INFLUENCE (1/6), so a keyed Orientation next to an eased key moved on a
different curve than every other property (CodeRabbit on #1876). New test:
orientation_linear_side_next_to_an_eased_side_matches_a_scalar (both directions).
The always-true NaN assert in keyed_orientation_slerps_as_after_effects now checks that
a non-finite key comes back as given; Property::value_at says why the match_id
"orientation" routing is safe.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Format keyframe orientation test (cargo fmt)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Move the image-sequence gap change out of this branch
The gap change (hold the previous file instead of colour bars) reverses
#297's documented behaviour and goes to its own PR, as requested in review.
pool.rs, sequence.rs, the footage test and docs/footage.md / docs/agents.md
are back to main.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* #3113: use After Effects' effect match names in scripting
Checked every row of crates/script/src/matchnames.rs against After Effects
26.2 (app.effects, canAddProperty and the layer property trees). Fourteen
effect names were wrong: twelve did not exist (Pixel Motion Blur is
ADBE OFMotionBlur, Shadow/Highlight is ADBE ShadowHighlight, Ellipse is
ADBE ELLIPSE, ...), and Noise and Photo Filter pointed at After Effects'
hidden obsolete versions (now ADBE Noise2 and ADBE PhotoFilterPS). A
light's Casts Shadows is ADBE Casts Shadows.
The old names stay accepted for lookup (addProperty, property(), and
parameters such as "ADBE Noise-0001"), so existing scripts keep working;
.matchName emits only the After Effects name. A names-only fixture of
After Effects' first-party match names backs the new test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Scripting: match names that changed only in case still find effects and parameters
property("ADBE Ellipse") and "ADBE Grow Bounds-0001" found nothing after the names became
ADBE ELLIPSE / ADBE GROW BOUNDS.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Offset Paths' Amount, Line Join, Miter Limit, Copies and Copy Offset,
and Zig Zag's Size, Ridges per segment and Points reported their
internal ids as matchName, so property("ADBE Vector Offset Amount") and
the other documented names returned null. They now report and resolve
by ADBE Vector Offset Amount, Line Join, Miter Limit, Copies and Copy
Offset, and ADBE Vector Zigzag Size, Detail and Points. Lookups by
display name and internal id still work.
A docked ScriptUI panel only ever saw its preferredSize: the host drew
it in a scroll area at max(dock, preferred) and the script's window never
learned the dock. scriptui.resize now gives a window a size: the script's
window takes it, lays itself out again and runs onResizing / onResize.
The dock host sends it whenever the panel's dock size changes, so
window.size is the dock's, as in After Effects.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A drag that started on a script control stopped at the window edge:
moves outside were dropped and a release outside never reached the
control. While a button is held, the control it was pressed on now gets
the moves (with screen and control-relative coordinates) and the
release, wherever the pointer is, as in After Effects. Every MouseEvent
also carries buttons (held: 1 left, 2 right, 4 middle), so a handler can
tell a drag from a hover; scriptui.mouse accepts it too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since #2107, Replace Footage keeps an item's kind by default, so
replace() on a sequence item brought the new file's whole numbered run.
In After Effects, replace() swaps in the one file and replaceWithSequence()
brings a run; replace() now asks for exactly that.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
The tests now sit with the crate's other test modules under crates/script/src/tests/ and are declared in tests.rs. docs/scripting-parity.md points at the new path.
`backpressure` stopped writing at the first WouldBlock. On macOS the
accepted stream does not keep the listener's 4 KiB receive buffer (it
gets about 114 KiB), so that WouldBlock only meant the 4 KiB send buffer
was full; it drained into the peer, and the 64 KiB write in
a_write_to_a_peer_that_stops_reading_fails_after_the_timeout and
a_blocked_socket_doesnt_hold_up_the_others_and_close_interrupts_it then
succeeded instead of blocking. Both tests failed on every run.
The helper now stops only after writes have stayed blocked for 200 ms,
still within its 16 MiB and 5 s limits. The socket code is unchanged.
Upstream moved crates/ui-egui/src/menus.rs into menus/*.rs; the dynamic-menu change that
draws script folders as submenus now lives in menus/bar.rs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Wiggle Paths had Size, Detail, Points, Wiggles/Second, Correlation,
Temporal Phase and Random Seed, but no Spatial Phase, which After
Effects lists among its properties (ADBE Vector Spatial Phase). Spatial
Phase now moves each point's own displacement along its subpath: 360
degrees is once around, and a fraction of a point is interpolated
between neighbors. At 0 degrees a path draws as before.
A project saved without the property gets it at 0 degrees, after
Temporal Phase, when it opens. Scripts can read and set every Wiggle
Paths property by its match name.
A shape layer's Contents > Add menu had every After Effects path operation
except Wiggle Transform. It is now a shape item (layer.addShapeItem kind
wiggler, "ADBE Vector Filter - Wiggler" for scripts) with Wiggles/Second,
Correlation, Temporal Phase, Spatial Phase, Random Seed and a Transform
group whose Anchor Point, Position, Scale and Rotation set how far each
transformation wiggles.
The values come from the Wiggly selector's noise, with each Repeater copy
as one unit: the copies of a Repeater above the operation, in its group or
a nested one, wiggle independently as far as Correlation allows, and the
copies of a Repeater below it wiggle the same way. A paint above it whose
paths all move by one matrix moves its gradient and stroke scale with them.
The layer cache keys a layer with a Wiggle Transform by time.
An extension that ships several panels no longer adds one Window menu entry per panel:
`Scripts/ScriptUI Panels/Motion Kit/Animate.jsx` shows as Window > Motion Kit > Animate.jsx,
and a subfolder of `Scripts/` is a submenu of File > Scripts. Top-level scripts are unchanged.
- Script names may be `Folder/File.jsx`: exactly one folder, each part a bare name (no `..`,
`\`, NUL or empty part), never `ScriptUI Panels/...` under `Scripts/`.
- window.scriptPanel, file.runScript, file.uninstallScript, file.scripts.list and
scriptui_panels.json take such names; a panel's tab is titled after its file.
- ConfigStore::list_dirs (DirConfig, MemoryConfig) lists the subfolders.
- menus::dynamic_nodes groups a folder's entries into a MenuNode::Submenu; the egui and native
menus draw it with their submenu code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Scripts in Scripts/Startup and Scripts/Shutdown in the settings folder
were never run. The desktop and web apps now run the Startup scripts at
launch, before a project opens, and the desktop app runs the Shutdown
scripts as it quits, after the project is closed. Each folder runs in
alphabetical order with case ignored, and files larger than 16 MiB are
not run. A Startup script that fails is reported in a toast and in the
log, a Shutdown script that fails is reported in the log, and the next
one still runs.
A Startup script that waits in a modal dialog goes on when the dialog
closes, while the next scripts run. A Shutdown script's dialogs close
as if canceled. With the settings folder on disk, each script runs as
its file, so #include, $.fileName and $.evalFile resolve against the
script's own folder.
The new commands file.runStartupScripts and file.runShutdownScripts
return the scripts that ran, those still waiting in a dialog, and those
that failed. Like script.run, both are refused in confined automation
sessions, in batches and in script steps, which now read one list of
script commands (RUNS_SCRIPTS). The list also holds window.scriptPanel
and window.restoreScriptPanels, which run ScriptUI panel scripts and
were not refused on main either. The apps still call
window.restoreScriptPanels at launch. plugins.md, scripting-parity.md,
gaps.md, target-app-parity.md and agents.md describe the folders. Each script
still runs in its own global scope, so a Startup script's variables are
not available to later scripts.
Poison recovery, OsStr::to_str and char predicates only; S4325 casts are
f64 to f32 and stay.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
file.projectSettings takes frameStart (0|1), the Project Settings dialog shows Frame Count, and scripts get app.project.framesCountType and displayStartFrame. The setting was already read by the Frames and Feet + Frames time display; nothing could change it.
The setters go through `comp.settings`, which defaults to the Composition Settings dialog's
centre anchor, so `comp.width = 800` on a 400-wide comp shifted every unparented layer — and
all of its position keys — by 200 px. A script that resized a precomp then found its pixel
coordinates had moved under it.
After Effects' width and height setters only change the frame; moving the contents is what
"resize from centre" scripts do for themselves. The setters now pass `anchor: 0` (top left).
The dialog, which is where the anchor belongs, is untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- 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.
A 2-D point property took any array: `[1, 2, 3]` and `[1, 2, 3, 4]` were cut to `[1, 2]` and
`[1]` was broadcast to `[1, 1]`, each reported as success, so a script holding a wrong value
silently did something else instead of failing. After Effects raises "Array is not of the
correct dimension" and leaves the property unchanged.
The length must now match the property's dimension, with the one exception After Effects also
makes: two values for a 3-D property, where z stays 0, so `position.setValue([x, y])` keeps
working.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Alt/Option-drag a Project item onto a Timeline layer, or press Ctrl+Alt+/ (Cmd+Opt+/) with layers and an item selected, to swap the layer's source. The layer keeps its name, timing, transforms, keyframes, expressions, effects, masks, switches, parenting and track matte; a source with audio adds Audio properties when missing. Precompositions can't be nested inside themselves.
AVLayer.replaceSource now runs the same engine command and closes the replaceSource part of #813.