Problem: After Effects 26.2 spreads a Ctrl+Alt (Cmd+Opt) hot-text scrub
over the selected layers by selection order (first unchanged, last the
whole change, the ones between even shares). Our multi-layer scrub
(#491) always moved every selected layer by the same change.
Fix: prop.set takes `proportional` (with `selected` + `offset`); each
participating layer, the scrubbed one included, gets index/(n-1) of the
change in selection order, in one undo step; the first layer is left
alone (no key added). The Timeline value editor sends it while Ctrl+Alt
(Cmd+Opt) is held; that chord no longer fine-scrubs or unlinks Scale.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With several layers selected, After Effects applies a property edit to the same property of
every selected layer; we changed only the layer whose row was edited (the Properties panel
showed only the first selected layer), so a stopwatch switched on one layer and later moves
of the others had nothing to keyframe (#355), and scaling or rotating a selection changed
one layer (#491).
- Engine: `prop.set`, `prop.toggleAnimation` and `prop.toggleKey` take `selected: true`:
the change also goes to the same property (by match path, `transform/scale`,
`effects/<effect>#n/<param>`) of every other selected, unlocked layer whose value has the
same kind, in one undo step (a scrub keeps folding into it through its `merge` key). With
`offset: true` (a scrub) each layer changes by the same amount from its own value; a typed
value sets them all. Position with Separate Dimensions is handled per layer.
- The Timeline, Properties and Effect Controls panels pass `selected` (and `offset` while a
value is dragged). The Properties panel heads "(N) Selected Objects" (zh-CN / zh-TW / ja /
uk rows).
- Viewer: a bounding-box handle drag scales every selected layer by the same ratio about its
own anchor point; the Rotation tool turns them all by the same angle (one undo step).
Tests: engine test (stopwatch, scrub offset, typed value, keyframe button, one undo step,
unselected layers untouched); kittests: the Properties panel with three layers selected
(header, stopwatch on all three undone in one step, a scrub moving all three by the same
amount) and a handle drag scaling two selected layers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
Measured against After Effects 26.3 with the original
examples/ae-spatial-tangents.jsx (three paths, ten keys, even and uneven
timing): an auto-Bezier key's out tangent is (next - previous) / 6 and
the in tangent its mirror, whatever the key times; an end key uses its
one neighbour, (neighbour - key) / 6. EffectCraft gave end keys none and
scaled middle tangents by segment length, so paths with auto-Bezier keys
ran up to 28 px from AE's. All ten tangents now match; path samples are
within 0.05 px.
Tests that relied on end keys having no tangent now expect AE's: Spatial
Bezier keeps auto keys' handles (linear keys still get a third), the
motion-path handle is drawn at a sixth, and a straight loopOut path is
compared within 1e-9. docs/fidelity records the method and scores.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Timecode and Frames entries were parsed as absolute counts without the composition Start Timecode or Start Numbering Frames At offset, so committing the displayed text jumped to another frame.
Right-clicking a property's name in the Timeline did nothing, so
Position's dimensions could be separated only from the Graph Editor.
As in After Effects, it now opens the property's menu: Reset, Edit
Value…, Separate Dimensions (Position and, once separated, X / Y / Z
Position, checked), Add / Remove Expression and Add Property to
Essential Graphics. Only Position separates, as in After Effects
(Scale stays one property).
`prop.reset` without `default` used to leave the value as it was: it
now gives the property its own default (an effect parameter's, or a
Transform property's, shared with Layer ▸ Transform ▸ Reset), which
also makes the Properties panel's Reset reset the values. `prop.set
{layer, prop}` without `value` opens the value dialog, like
`keys.set`.
Tests: timeline_property_names_have_the_property_menu
(ui_blank_context_menus), reset_gives_a_property_its_default (engine).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dropping the expression pick whip on one of a property's values (the X
of Position) referenced the whole property. As in After Effects, a
drop on one value now picks that dimension: `transform.position[0]`;
a drop on the name still picks the whole property.
`prop.pickWhip` takes `target.dimension`.
The rest of #363 is unchanged: inserting at the cursor landed with
#284, and After Effects' property pick whip in the Parent & Link
column is left for later.
Tests: pick_whip_picks_one_dimension_of_the_target (engine),
the_expression_pick_whip_picks_the_dimension_it_is_dropped_on
(ui_inline_edit).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Position motion path drew a key's spatial handles only where its
tangent wasn't zero, and auto-Bezier keys have none at the path's ends,
so a two-key path (the usual start) had no handles to pull it into a
curve. Auto-Bezier keys now show a handle on such a side too, pointing
where the path leaves the key, a third of the way to the next key;
dragging it makes the key Bezier with that tangent. Linear keys still
show none.
Keyframe Interpolation > Spatial Bezier copied those zero tangents, so
the keys stayed straight with no handles (and read back as Linear); it
now gives them the same handles. The path keeps its shape and timing.
The handles come from the new keyframe::spatial_handles.
Tests: spatial_bezier_gives_a_straight_path_handles (engine),
a_straight_motion_path_has_handles_to_curve_it (ui_viewer).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A footage or precomp layer dragged out past its source's end holds its
last frame, but its bar looked the same all the way, so the source's
end couldn't be seen. That part of the bar (and any part before the
source's start) is now darkened and striped in the label colour, with
an automation id (timeline.layer.<id>.bar.afterSource / beforeSource);
with time remapping on, the remap decides what shows there, so it isn't.
Enable Time Remapping on such a layer put its end key at the extended
out point; it now ends at the source's end, where the last frame starts
to hold (After Effects). Trimmed and reversed layers keep their keys.
Tests: time_remap_on_an_extended_layer_ends_at_the_source_end (engine),
the_bar_past_the_source_end_is_striped (ui_timeline_switches).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Selecting Time Remap in the Timeline (its name, which selects all its
keys) and pressing Delete removed the keys, and a second Delete then
deleted the whole layer. As in After Effects, Edit > Clear with Time
Remap selected now turns time remapping off (Layer > Time > Enable Time
Remapping off), in one undo step, and the layer stays; one selected
Time Remap key on its own is still deleted as a key.
Test: deleting_time_remap_turns_time_remapping_off (engine).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dragging an expression's pick whip always replaced the whole
expression, also while it was being edited (and the editor then wrote
its old text back over the reference). As in After Effects, a pick
whip pressed while the expression is being edited now puts the
reference at the cursor, replacing the selected text, and the editing
goes on with the cursor after the reference. Without an edit in
progress it replaces the expression as before.
`prop.pickWhip` takes the text being edited (`expression`) and the
selection in it (`range`, in characters) to insert there.
Tests: the_expression_pick_whip_inserts_at_the_cursor_while_editing
(ui_inline_edit), pick_whip_inserts_into_the_expression_being_edited
(engine).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
As in After Effects, pressing a property's stopwatch toggles it, and
dragging on over the stopwatches above or below gives every property
the pointer passes the state the first one got (animated or not),
whatever its own, in one undo step. Alt+click still adds an
expression.
The gesture is the layer switches' press-and-drag from #238, moved
into `switch_gesture` and shared by both. `prop.toggleAnimation` takes
`merge` so the drag folds into one undo step.
Tests: dragging_over_stopwatches_sets_them_all (ui_timeline_switches),
stopwatch_toggles_with_a_merge_key_are_one_undo_step (engine).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Comp durations (comp.new / comp.settings) are whole frames.
- layer.addItem start/out, layer.timing in/out/start/delta and time-stretch in/out snap to
the nearest comp frame; prop.addKey / prop.set with an explicit time key the nearest frame.
- The demo project builds every time on 29.97 fps frames (Lower Third in = frame 150 =
0:00:05:00, comp = 300 frames).
- Test: demo + commands stay frame-aligned.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Engine (mask.rs, tests in tests_timeline.rs incl. render + undo/redo):
mask.new, mask.addVertex, mask.setVertex, mask.setClosed (Layer > Mask and Shape Path >
Closed), mask.selectVertices, mask.moveVertices (merged drags), mask.deleteVertices
(Delete with vertices selected), mask.remove / mask.removeAll (Layer > Mask). Edits of an
animated Mask Path key the current time.
Viewer: Pen tool on the selected layer - click = corner vertex, drag = Bezier vertex,
click the first vertex to close, Esc/Enter or another tool ends the path; rubber band to
the pointer. Selection tool: click/shift-click mask vertices, drag selected vertices,
drag tangent handles (symmetric; Alt breaks). Vertices register automation ids
viewer.mask.<uid>.vertex.<i>. Mode/inverted/feather/opacity/expansion stay in the
timeline mask rows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Properties with an expression get an editor row in the timeline: Expression label,
enable (=) switch, pick whip, syntax-error warning, and a multiline code editor in the
time-graph area (commits on blur or Cmd/Ctrl+Enter via prop.setExpression).
- Expression pick whip: drag onto another property row -> prop.pickWhip (AE reference
expression). Parent pick whip in Parent & Link: drag onto a layer -> layer.setParent.
- Alt-click on a stopwatch starts with the property's own reference (transform.opacity).
- Timeline rows have variable heights; the empty-graph box-select area sits under the rows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Current time shown as AE does: 0:00:02:15 (hours unpadded; time::format_timecode_ae),
frame count zero-padded to 5 digits (00075 (29.97 fps)).
- Time ruler labels in seconds:frames (00:15f, 01:00f) with frame-based adaptive spacing.
- time.set snaps seconds to the nearest frame (2.5 s at 29.97 -> frame 75).
- Mode / Track Matte columns hidden until Toggle Switches/Modes (F4), as in AE's default.
- Timeline outline pane sized to its columns and clipped so Parent & Link never draws
over the time graph in narrow timelines.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Engine (all commands with undo/redo + tests in engine/src/tests_timeline.rs):
- keys.copy/paste (Edit Copy/Cut/Paste route to keys when keys are selected;
relative timing kept, paste at CTI, cross-property paste of same type),
keys.selectAll (Cmd+Alt+A), keys.nudge* (Alt+Arrow, Alt+Shift+Arrow),
keys.set / keys.setEase (graph editor drags), keys.info (dialog data),
keys.timeReverse; keys.interpolation gains continuous/auto Bezier, spatial
interpolation and roving; keys.velocity takes per-dimension values and
Continuous.
- Roving keys (keyframe::retime_roving): constant speed along the motion path.
- Layer > Time: layer.timeStretch (percent|duration, hold in/current/out),
layer.timeReverse (Cmd+Alt+R), layer.enableTimeRemap (Cmd+Alt+T),
layer.freezeFrame, layer.freezeOnLastFrame; render uses Time Remap as the
source time for precomp/footage layers.
- prop.separateDimensions (X/Y/Z Position; prop.set on Position writes them).
- layer.setParent compensates so the layer stays in place (compensate:false
to opt out).
- prop.pickWhip writes AE-style reference expressions (thisComp.layer(..),
effect(..)(..), mask(..).maskFeather, temp=…;[temp,temp], …[0]).
- prop.setExpression: syntax errors keep the text but disable the expression.
- expr: arrays of properties returned by an expression are resolved.
UI: Keyframe Velocity, Keyframe Interpolation and Time Stretch dialogs;
Graph Editor (value + speed graphs, draggable keys and Bezier/influence
handles, auto-zoom, fit, show selected, separate dimensions, hold/linear/
auto Bezier/Easy Ease buttons); key + layer context menus. Clears the
workspace clippy -D warnings failures.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>