- Cryptomatte (#1183): planes shorter than the image read as empty instead of panicking.
- layer.addMarker (#1180): a comp marker no longer replaces one at the same time.
- Desktop (#1005): ignore macOS's -psn_ argument instead of refusing to launch.
- Doc comments put back on their own items (#1153, #1037).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A single selected_layers list kept ids from every comp, so the active comp looked selected whenever another comp was. Opening a comp and running a script against a different comp now park and restore that selection.
uniform_parent_pose took the Y angle from asin, which loses precision within about 1e-6 degrees of
+-90. The Y angle now comes from atan2 on cos(Y), as the X and Z angles already do. The gimbal
branch and the principal-range output are unchanged.
A new test parents a static Orientation with Y 1e-6 degrees off gimbal lock under a uniform
parent. Its largest corner displacement was 3.3e-8 px before this change and 1.5e-14 px after.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
keys.quickOffset and keys.move stored the drag's new total before the
edit ran. When the edit refused the step (keys.quickOffset with a key
landing past the end of time), the project stayed as it was but the
total kept the refused delta, and with incremental steps (merge without
fromStart) the next step moved by it too.
The drag state is now stored only after the edit applies.
Test: a_refused_step_does_not_count_towards_the_drag (+1 s, a refused
step, -1 s: the keys end where they started; fails before: the last key
ends near the end of time).
keys.move and the nudges snapped each key's destination to the nearest
frame, so moving keys a Quick Offset had staggered onto subframes put
them back on frames: four keys at 0, 1/3, 2/3 and 1 frame, moved one
frame, came out at 1, 1, 2 and 2. Layer bars kept theirs, because
layer.timing snaps the move, not the times.
The move is now rounded to whole frames and every key translated by it.
layer_shift turns a comp-time move into a layer-time move through the
layer's start and stretch (reversed too) without a round trip through
comp time; keys.quickOffset uses it as well.
Test: moving_staggered_keys_keeps_their_subframe_spacing (Quick Offset,
then keys.move and keys.nudgeForward; fails before: 1/3 frame lands on
a frame).
prop.reset cleared every keyframe before setting the default value. As in
After Effects (and as effect.reset already does for effect parameters), an
animated property now keeps its keyframes and gets the default as a key at
the current time; a static property just takes the default. The expression
is still removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Validate saved stroke frames against the current layer limits before clamping a span edit. Keep project and undo state intact when trimming has left strokes outside the layer.
Co-Authored-By: OpenAI Codex <noreply@openai.com>
After Effects 25.4's Quick Offset staggers selected layers, or each
layer's selected keyframes, in the order they were selected: the first
stays, the last moves by the drag and the ones between by equal steps,
landing on subframes.
layer.quickOffset {delta, layers?} moves each layer's start, in and out
point by total * i / (n - 1), worked out once in ticks with integer
arithmetic, so 1 frame over 4 layers gives 0, 1/3, 2/3 and 1 frame
exactly. keys.quickOffset {delta} does the same per layer for the
selected keys, the layers in the order their first key was selected;
every selected key of a layer, across its properties, moves by that
layer's comp-time offset through its start and stretch (reversed too),
keeping values, interpolation, easing and labels. Locked layers are
skipped and at least two layers are needed. With merge and fromStart,
delta is the whole drag and each step starts again from before it, so
the drag is one undo step and a key passed over survives. A delta or a
destination out of the range of times is an error, not a clamped move.
Docs: agents.md; architecture.md notes Quick Offset as the exception to
times snapping to frames.
keys.move (and nudging) rebuilt the key selection in layer and property
order, so the order the keys were selected in was lost after a move. A
Quick Offset that follows staggers layers in that order (#1012).
The move now goes through move_key_refs, which maps each selected key to
its new time and returns the selection in its original order, with the
same collisions (a key landing on another replaces it), roving retiming
and locked-layer handling as before. key_gesture is the drag-step logic
(start again from the keys and project as they were before the drag)
split out of shift_keys so other key drags can share it.
Test: moving_keys_keeps_the_selection_order (fails before: the keys come
back sorted by layer).
CameraLayer gets the transform and camera-options shortcuts (position, xRotation, zoom...) that AVLayer already had, and Convert Expression to Keyframes run without a property converts the selected ones, as app.executeCommand does it.
Problem: the built-in animation preset library had no Backgrounds category
(gap 7: 31 presets against After Effects' 621).
Fix: 23 original Backgrounds presets in crates/engine/presets/backgrounds.json,
built from our own generator, noise, colour and stylize effects: Soft Gradient
Drift, Aurora Veil, Slow Fog, Cloud Bank, Dark Smoke, Ember Haze, Magma Flow,
Ice Flow, Pastel Plasma, Cell Drift, Crystal Facets, Hex Mosaic, Mirrored Silk,
Water Caustics, Blueprint Grid, Checker Scroll, Scrolling Stripes, Pulse Rings,
Bokeh Drift, Spotlight Floor, Dawn Sky, Sweeping Light, Grainy Wash.
The preset format gains `expr` (an expression per property, so backgrounds keep
moving for the whole layer) and point parameters written as fractions of the
layer size. Effects & Presets lists them under * Animation Presets > Backgrounds.
Tests: catalogue uniqueness and categories, apply/undo/redo of every preset
(engine), render of every preset with the expression engine: finite,
non-flat, deterministic, moving (host), panel listing and apply (ui-egui).
Sidecar, ATTRIBUTION.md, gaps.md row 7 and effects-parity.md updated.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Guides can store a percent of the comp so they follow comp-size changes
(View > Add Percentage Guide, Convert Guides to Percentage / Pixels); an
effect instance has an optional colour label (effect.setLabel, Effect Controls
header bar and Label menu). Both are serde-defaulted, so old projects load.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
keys.deselectAll (Shift+F2, Cmd+Alt+Shift+A) clears the selected
keyframes, properties and groups and leaves the layers selected.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
comp.workAreaToSelection (Cmd+Alt+B) spans the selected layers, or the
whole composition with none selected. comp.activateRecent (Shift+Esc)
goes back to the composition that was active most recently within the
active one's nesting hierarchy; EditorState keeps recent_comps for it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
time.workAreaStart / time.workAreaEnd (Shift+Home / End) and
time.previousInOut / time.nextInOut (Cmd+Alt+Shift+Left / Right): the In
and Out points of the selected layers, of every layer with none selected.
time.go takes nextInOut and prevInOut.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The media layer keys decoded frames by path, and nothing dropped them when a
new project (or a new import) named a file that another app had overwritten
since, so a same-size overwrite showed the old pixels. Importing footage and
replacing the project now make the media layer and the layer cache let go of
the files involved.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
File > Import > EffectCraft Project (and .ecproj/.ecprojx given to File > Import)
merges the project into a folder named after the file, with every id shifted past
the open project's, in one undo step. Relative footage resolves next to the file;
the result lists comps, root comps, primaryComp and missing media.
offset_ids now shifts tracker Motion Targets and saturates instead of overflowing;
fix_next_id saturates too, so a damaged file can't overflow on open or import.
Menu entry translated in all ten menu catalogues.
Rebased onto current upstream main for the split of #858 (test module list in
engine lib.rs; es/pt-br/ru/fr/de/cs menu translations).
(cherry picked from commit 19c653fccc95133fb35e978a7f7f20a18c0e8c89)
(cherry picked from commit 4e751ad2)
(cherry picked from commit ceed706cc3b5f3755769738a1ddb3d965a94ab6b)
footage.relink {item, path} relinks one item and every other missing item that moved with it (one undo
step); Replace Footage on a missing item does the same, as in After Effects. File > Dependencies >
Relink Missing Footage in Folder... (footage.relinkFolder) finds missing files by name in a folder;
footage.missing lists them. The footage check after open finds files that moved along with the
project's folder, without marking the project modified. Paths compare by parts, so Windows and macOS
projects relink either way.
Checks: engine relink:: / footage_check:: (13 passed with replay::), host relink:: (1 passed),
clippy -D warnings (engine, host, ui-egui, script, cli, xtask). Not run: ui-egui i18n tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEytG1Qum4TJrMTLKjgwDa
Upstream port: the folder search (footage.relinkFolder) is left out, since upstream's file.relinkFootage already scans a folder; the module is relink_moved.rs next to upstream's relink.rs.
(cherry picked from commit ea7923535fb81693ed95aa0fe768b850b347b2a9)
(cherry picked from commit d15e853a720304c1032e21974d84e1501c6edfc3)
(cherry picked from commit 0b8a328dda1fb727488e912a13f94a2ef4147d03)
(cherry picked from commit 9d85bfde72)
Alt+1 after Alt+Shift+1 shows the red channel in grayscale instead of going back
to RGB (and the other way round). The same key again is still RGB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
layer.sampling takes `toggle`: layers that all have the sampling already get the
other one. The Bilinear and Bicubic menu entries, which carry the keys, pass it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>