Adding a marker replaced the comp, so every cached frame stopped matching and the preview had to
re-render before its audio could carry on. Plain markers change no pixels; protected time markers
with a duration still do (Responsive Design). Markers count in full again when a project uses
expressions, which can read them.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
A script asks a light's options for "ADBE Casts Shadows", which returned null. The light's option
is named "Casts Shadows" here, so the lookup never matched. The name is now an alias for the
castsShadows property, and a regression test checks the lookup returns the property.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Inside a function wrapper such as (function () { ... })(), boa places a ReferenceError at the start of
the function, so a script's error was reported at the wrapper's first line rather than the line that
failed. The first use of the undefined name in the code is now the position, and boa's frames remain
the fallback. Thrown errors and object-model errors already report their own lines. A regression
test covers a thrown error, an undefined name, and an object-model error inside a wrapper.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
The Gradient Ramp wrote alpha 1.0 to every pixel, so a shape layer's transparent area filled up with
the ramp and the layer's bounds showed instead of the shape. The ramp now uses the pixel's own alpha
on the CPU and in the GPU kernel, as the #939 fix does for the noise effects. A regression test
checks that transparent pixels stay transparent at two Blend With Original values.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Every key call (removeKey, keyTime, keyValue, setInterpolationTypeAtKey…)
looked its key up through `__keys()`, which serialised the whole key list,
eases and tangents included, on each call. Removing keys one by one was
therefore quadratic: about 100 ms per removeKey on a 2,000-key property.
The `keys` query now takes an optional `index` and returns just that key
with the count; `__key` uses it. removeKey on 2,000 keys drops from
103.7 ms to 0.4 ms (release, Windows 11 aarch64).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The layer bar Quick Offset was followed and ended in its row's code, and
rows out of view aren't drawn. Released with its row scrolled away, it
stayed active: the next drag of that bar, even a plain one, ran as a
Quick Offset from the old origin, into the old undo step.
Like the key drag, it now follows the pointer below the rows and ends
when the button is released, wherever the bar's row is.
Test: a_quick_offset_released_off_screen_ends (release with the row
scrolled away; a plain drag after it moves both layers together as its
own undo step; fails before).
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).
In After Effects a marquee selection staggers in the comp's layer order,
not in the direction the box was drawn. A Shift-click range of layer
bars and a box of keys already come out top layer first; this test makes
both with the pointer (the range from the bottom layer up, the box from
the bottom right to the top left) and checks the Quick Offset that
follows: the top layer stays and the bottom one moves the whole delta.
The Chinese UI fallback was chosen by probing 文, which Japanese faces
cover too. Fallback takes a face already loaded first, so after the
English interface had loaded a Japanese face for kana, switching to
Simplified Chinese reused it, and the simplified-only hanzi (选, 删,
层, 设, 页 ...) drew as boxes. A Simplified Chinese interface now probes
页, as wordcraft does for its interface font (storytold/wordcraft#248).
Ctrl/Cmd+Alt dragging a selected layer bar or a selected key now runs a
Quick Offset (layer.quickOffset / keys.quickOffset with fromStart, one
undo step). The mode is chosen when the drag starts and needs at least
two unlocked layers; the last selected layer moves by whole frames with
the pointer. A readout by the pointer shows the Total Offset and the
offset Per Layer, translated.
A plain drag still moves the selection together, Alt alone still scales
a key group in time (it now leaves Ctrl/Cmd+Alt to Quick Offset), and
Ctrl/Cmd+Alt+click still toggles Hold. The key drag's readout is drawn
by a shared drag_tip. Keys also register timeline.keyAt.<uid>.<ticks>,
exact where keys on subframes can share a frame.
Tests (ui_quick_offset): staggering layer bars in selection order and
keys per layer, one undo each; plain drags and Alt scaling unchanged; the
readout while dragging.
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).
The 2^28 pixel limit applied per image; one composite or layer decode kept
the ZIP output, a Vec<f32> per channel and the result alive together, and the
bake size came from the file's quad. A decode now charges every buffer
against a byte budget (4 GiB, 1 GiB on 32-bit), checks its whole expected peak
before allocating, writes channels straight into the result and returns an
error when over budget. Bakes are at most 4x the document across and 2^24
pixels.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The layer context menu gains an Effects submenu between Blending Mode and Arrange, mirroring the After Effects layer menu: the effect registry grouped by category (sorted), synced with the Effect menu; picking one applies it to the selected layers.
Raw channels are borrowed, ZIP planes are un-predicted in place in the inflated buffer,
the 32-bit un-predictor uses one scratch row, the layer mask tests the rect per row
instead of per pixel (and skips a constant factor of 1.0), and a placed-layer bake
solves the quad once and hoists per-triangle invariants. Output is bit-identical;
tests compare against the replaced implementations and count the avoided work.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Psd::parse drops the layers, records Psd::warnings and keeps the header and
merged image; the importer then imports the flattened image as one layer and
reports 'Layers could not be read'. Header damage and a missing merged image
stay hard errors.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
One decode_plane for raw/RLE/ZIP (decode_channel and composite), placed::eight and
quad_from for the SoLd/PlLd transforms, the writer's big-endian and UTF-16 helpers
from descriptor.rs, one delimiter const in engine_data.rs, and the dead binding in
write.rs. No behaviour change; adds ZIP and RLE composite tests.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Psd::tree flattens groups nested deeper than MAX_GROUP_DEPTH into their depth-64 parent, keeping every layer, so the importer recursion and the tree's Drop/Clone/PartialEq are bounded. The import result reports how many groups were flattened.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
On Windows $.fileName is the full path with backslashes, so split('/') returned the whole path
and includes_are_inlined_relative_to_the_script failed. The test now takes the last part after a
backslash and then after a slash, with plain string splits: a regex split did not split the path
in the script engine.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
The handle was filled with the colour of its own track (both the field
background), so it only showed while hovered or dragged. It now takes the text
colour, in every scroll area.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Enter closes the editor like its OK button, also while the search field has
focus. It stays with the shortcut recorder (Enter can be recorded), the preset
rename field and an open preset menu.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The floating group's title strip used the same id as its Area's own
background widget, and the Area was shifted back into the window while
the group's rect kept moving, so the two no longer overlapped and egui
painted "Double use of widget ID" over the panel. The strip has its own
id and the group is drawn where its rect says.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The editor's content was a little taller than its dialog, so the outer scroll
area clipped the OK button and its scrollbar covered the right edge. The dialog
now has no outer scroll area: the command list takes whatever height is left,
so the button row always fits, in small windows too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Undoes the 800 px Project Settings width from #980: the form is back at the
shared 480 px.
A form's choice popup keeps its 240 px column and ends a long choice (a GPU
adapter's name) in an ellipsis instead of widening the grid past the dialog.
Only the fields scroll, and only vertically; OK and Cancel sit under the
scroll area, so they stay in view.
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>
Both returned their input unchanged. They now go through the layer's transform and its parents'
at comp time, and the inverse.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.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>
The menus have been complete in Japanese since the menu catalog landed, but the panel
catalog below the menu bar stopped at 410 of the 506 source strings the Chinese catalogs
define. Settings, the Home screen, the viewer tools and the Fast Previews and channel
popups therefore still drew in English for a Japanese interface.
This adds the 96 missing rows. Existing rows are untouched and keep their order; the new
ones follow the canonical (Simplified Chinese) order, which the catalog tests require, and
no row changes the number of `{}` values its English source carries.
Terms follow the Japanese already in the catalog and, wherever a control exists in both,
the wording After Effects 2026 (26.5) itself shows in Japanese: 平面 (Solid), 関心領域
(Region of Interest), フル画質 / 1/2 画質 / 1/3 画質 / 1/4 画質 (viewer resolution),
ドラフト 3D (Fast Previews), 透明グリッド (Transparency Grid), マスクとシェイプのパスを表示
(Toggle Mask and Shape Path Visibility), チャンネルとカラーマネジメント設定を表示 (Show
Channel and Color Management Settings).
The crate's `i18n` unit tests pass (10/10) and rustfmt is clean.
First in the menu, as in After Effects, and for the right-clicked comp rather than the one in the viewer: the dialog keeps the comp it edits and OK passes it to comp.settings. comp.settings with only {"comp": id} opens the dialog on that comp.
The shared release workflow installs trunk, but EffectCraft's web build
runs the wasm-bindgen CLI directly (cargo xtask web), so the Web job
fails with "wasm-bindgen CLI not found". In CI, package.sh now installs
the CLI at xtask's WASM_BINDGEN version, as the per-app release.yml did
before the move to craft-common-actions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
F2 Deselect All, Cmd+Shift+T Effect Controls, Cmd+Alt+Left / Right Go to Start /
End, Cmd+Shift+/ Add to Render Queue.
Effect Controls is a menu entry with bound parameters, which could hold one key:
a menu line can now give further default keys after a comma
(F3, Cmd+Shift+T); the menus show the first. MenuEntry gains alt_shortcuts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both were a single command taking the mode as a parameter, with no menu entry,
so nothing could carry a default key. Each mode is now its own command:
view.fastPreviews.off / adaptive / draft / fastDraft / wireframe (Cmd+Alt+1-5)
and view.channel.red / green / blue / alpha (Alt+1-4), redColorized /
greenColorized / blueColorized (Alt+Shift+1-3) and rgbStraight (Alt+Shift+4).
A channel's key again returns to RGB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The previous release build failed in craft-common-actions, not in this
repo: riscv64 packaging got --formats deb,rpm,tar (fixed in
craft-common-actions#3), Linux jobs lacked the ALSA headers
(craft-common-actions#5), and Windows signing needed the AZURE_* secrets
listed in the caller.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
setInterpolationTypeAtKey and setTemporalEaseAtKey act on effect parameters
(a Slider Control's slider and a 3D Point Control), not just transform
properties. Verified on current main; lock it in with a script regression
test driving the per-key methods through the property object.
Drop the 6 px added to expression editor rows (and the matching offset in
the drag-to-resize maths), so the row is a whole number of row heights.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rows stripe by their index among the non-expression rows, so adding or
removing an expression leaves every other row's stripe unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
setParent re-expressed Position for 3D layers but skipped cameras and lights
entirely, so parenting a camera to a (default) null moved its eye by the
null's world transform and the framing jumped. Cameras and lights now get the
same parent-space Position fix, plus Point of Interest for two-node rigs
(values, keys, spatial tangents and ease speeds). They have no Scale, so
Orientation stays as authored.
anim.reveal keyframes/animation only matched keyframed top-level properties,
so Time Remap vanished from U once its keys were deleted even though enabling
remapping is itself the modification (and freshly enabled remaps, which get
two keys, kept showing). Match the bare timeRemap prop for both kinds, as
modified already did.
The Properties panel kept its own ephemeral per-layer link toggle, so
unlinking Scale there left the Timeline linked and vice versa, and its edit
math differed from the Timeline's. Use the shared per-property unlinked set
and widgets::constrained, with the stable match id instead of the display
name, plus Alt-edits-one-axis like the Timeline.
The message box's only button is OK, the default; Enter now dismisses it
(back to Settings when opened from there), as Escape already does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>