Commit Graph
817 Commits
Author SHA1 Message Date
bflatasticandClaude Opus 5.5 940bea3ba7 Effects: effect points on shape layers start at the layer centre (#227)
Shape and text layers have comp-sized effect bounds centred on their
origin, but effects measured positions from the origin: a point at its
default "layer centre" sat at the comp's bottom-right corner, so Twirl
on a centred shape changed nothing and Bulge only its bottom-right
quarter. As in After Effects, effects now work in effect space,
measured from the top-left of the layer's effect bounds.

The renderer moves the layer buffer into effect space for the whole
stack (Buf::rebase, CPU and GPU alike) and hands effects everything
else in it: masks, other layers' pixels for layer parameters (each in
its own effect space, so Set Matte and displacement maps see a whole
shape layer), the layer's own frames (layer_input, also what Puppet,
Roto Brush and the Layer panel read) and the comp camera
(CompScene::rebased). Layer bounds are now right for every effect, so
pin_rect and EffectEnv::bounds_origin go and layer_rect serves edge
pinning too. Adjustment layers still run on the comp below as it is.

effect_bounds / EvalCtx::effect_to_comp replace the repeated "comp size
when there is no source" code; the viewer's effect point handles and
eyedroppers, Puppet pins, the Layer panel (paint, Roto Brush) and the
tracker's Corner Pin output use them.

Project schema 2: opening a schema 1 project moves positions held by
effects on shape and text layers by half the comp size, so set points
keep their place (point parameters with keyframes, Paint paths and
clone positions, Puppet seeds and pins, Liquify and Roto Brush
strokes). Points still at their default become the layer centre;
expression controls, adjustment layers and expressions are left as
they are (docs/architecture.md). The disk cache's render version is
bumped.

Tests: default_effect_points_sit_at_the_centre_of_a_shape_layer and
layer_parameters_see_a_whole_shape_layer (render, both fail before),
effect_points_on_a_shape_layer (GPU vs CPU),
opening_a_schema_1_project_moves_shape_layer_effect_points_into_effect_space
(engine), and the effects migration tests.

Part of #227

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 20:33:03 +07:00
BFlat 52ab38fcff Merge pull request #276 from storytold/fix/262-gpu-why
Render: render.backend says why it renders on the CPU; mcp --gpu documented (#262)
2026-10-08 18:09:33 +07:00
bflatasticandClaude Opus 5.5 4eb69e7577 Render: render.backend says why it renders on the CPU; mcp --gpu documented (#262)
get_project / render.backend reported "gpuAcceleration": true with
"active": "cpu" and no reason, which read as a broken GPU. The
headless MCP server only attaches the GPU compositor when started with
--gpu, which docs/agents.md didn't mention, and a failed --gpu said
only "no usable GPU adapter" (the adapter's error went to a log no
one sees). The session now keeps the host's reason there is no GPU
compositor (accel_note): the CLI's "headless: renders on the CPU
unless started with --gpu", or the desktop app's compositor setup or
retirement error. The status reports it as `why` whenever it renders
on the CPU (or that the project's renderer is Software Only). --gpu
failures include the adapter's error (Gpu::try_headless).
docs/agents.md documents `mcp --gpu`.

Test: render_backend_says_why_it_renders_on_the_cpu (engine).
Checked with effectcraft-cli mcp and mcp --gpu.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 18:09:01 +07:00
BFlat 0d3b5ed286 Merge pull request #267 from storytold/feat/254-ease-presets
Keyframes: ease presets apply saved curves to keyframe pairs (#254)
2026-10-08 17:44:02 +07:00
bflatasticandClaude Opus 5.5 db5adfa62a Docs: parity.md and gaps.md G8 describe ease presets (#254)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 17:25:00 +07:00
bflatasticandClaude Opus 5.5 1c22615553 UI: Window > Ease Presets panel draws, edits and applies ease curves (#254)
A dockable panel (our own design) for the keys.easePreset commands. A
value graph shows the working curve with its two handles, which drag
to edit it; Apply eases the selected keyframes with it and From Keys
loads the curve between the first selected pair. Every preset is a
thumbnail drawn in code; a click loads it and applies it to the
selected keys. Save Current keeps the working curve under the name in
the field, and Rename and Delete act on the selected user preset.
Wide panels put the presets beside the graph; narrow ones below it,
the graph shrinking to leave a row of presets, scrolled with the wheel.

The panel state (working curve, selection, name) is serde in the UI
state, and every control registers an automation id (easePresets.*).
widgets::text_field is the search field without its magnifier.

Test: clicking_an_ease_preset_eases_the_selected_keys_and_handles_edit_the_curve
(ui_keyframes); every_panel_registers_automation_ids_for_its_widgets
covers the new panel.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 17:25:00 +07:00
bflatasticandClaude Opus 5.5 0717f12246 Keyframes: ease presets apply a saved curve to keyframe pairs (#254)
keys.easePreset.apply {preset | curve} eases every pair of neighbouring
selected keyframes of each selected property in one undo step: the out
side of the earlier key and the in side of the later one. A curve is
given as {outInfluence, outSpeed, inInfluence, inSpeed} or as handles
[x1, y1, x2, y2]. keys.easePreset.capture reads the curve between the
first selected pair; keys.easePreset.save {name, curve?} keeps it (or a
given curve), and list, rename and delete manage the user presets.

Twelve built-in curves are our own numbers (Linear, Smooth, Ease In-Out
Soft / plain / Strong, Accelerate and Decelerate with Strong variants,
Expo In-Out / Accelerate / Decelerate) and can't be renamed, deleted or
overwritten. User presets live in the config store (ease_presets.json),
loaded with the other settings. A file that isn't JSON gives no user
presets, and entries that don't parse, have no usable name or repeat a
name are skipped and logged. A failed write leaves the presets as they
were.

Test: a_preset_eases_every_selected_pair_in_one_undo_step,
a_curve_scales_to_each_dimension_and_follows_motion_paths,
apply_needs_neighbouring_keys_and_a_known_curve,
user_presets_save_rename_delete_and_persist,
a_corrupt_presets_file_loads_what_it_can (effectcraft-engine).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 17:25:00 +07:00
bflatasticandClaude Opus 5.5 cc594a7385 Keyframes: ease curves measure and apply the ease of a keyframe pair (#254)
An EaseCurve is the temporal ease of one segment independent of its
duration and values: the out side of the first key and the in side of
the second, as influence (%) and speed relative to the segment's
average speed (1 = linear, 0 = at rest). Its handles in the normalised
value graph are cubic-bezier points. apply() eases a pair of keys per
dimension, along the motion path for spatial values and as overall
progress for paths and gradients, the way Easy Ease shapes them; an
Auto Bezier key keeps its other side as it plays. measure() reads the
curve a segment plays back. Non-finite numbers are rejected and
influences and speeds clamped.

Test: handles_round_trip_and_reject_non_finite_numbers,
applied_curve_scales_to_each_dimension_and_measures_back,
spatial_curve_follows_the_motion_path,
linear_and_auto_bezier_sides_measure_as_they_play (effectcraft-keyframe).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 17:25:00 +07:00
BFlat 6fa047c2ee Merge pull request #265 from storytold/fix/253-snapping
Snapping: layers, scale handles and the work area snap as in After Effects (#253, #252)
2026-10-08 17:24:10 +07:00
bflatasticandClaude Opus 5.5 25e5d0f9c0 Viewer: wrap the snap() doc comment (#253)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 17:14:18 +07:00
bflatasticandClaude Opus 5.5 631c069557 Viewer: scale handles snap to the comp's and other layers' edges (#252)
Scaling a layer by a bounding-box handle didn't snap, so it couldn't
be scaled exactly to the comp. With snapping on (Cmd/Ctrl toggles it
during the drag), the dragged handle now snaps like a dragged layer's
feature: to the comp's edges, corners and centre and to other layers'
edges, corners, centres and anchor points, as the Snapping options
allow. A corner snaps both axes, an edge its own axis; with Shift the
corner slides along its line from the anchor point to the nearer
snapped edge, so the proportions stay and the layer lands on, say, the
comp's size.

Test: handle_drags_snap_to_the_comp_edges.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 17:14:05 +07:00
bflatasticandClaude Opus 5.5 0ac5f9fb9b Timeline: Shift-dragging the work area snaps to the current time (#252)
Shift-dragging the work area's begin or end handle now snaps it to the
current-time indicator, keyframes, layer in and out points and markers
within 8 px, with the snap candidates the CTI and keyframe drags use.
The work area bar itself drags too, as in After Effects: it moves the
work area and keeps its length, and with Shift the nearer end snaps.

Test: shift_dragging_the_work_area_snaps_to_the_current_time.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 17:14:05 +07:00
bflatasticandClaude Opus 5.5 41be200749 Viewer: Snapping options pick which layer features snap (#253)
Dragging a layer already snapped its feature nearest the pointer to
other layers' edges, corners, centres, anchor points and path points
and to the comp's edges and centre, and Cmd/Ctrl held during the drag
already turned snapping on or off against the checkbox. What was
missing were After Effects' Snapping options beside the Tools bar's
checkbox. The new menu turns Snap Edges Extended off (a layer's edges
then snap only along the layer, not across the comp) and each feature
family (edges, corners, centres, anchor points, mask and shape path
points) on or off, for the dragged layer's snap features and for the
targets alike. Agents set them with view.snappingOptions.

Layers whose eye is off (or that a solo hides) are no longer snap
targets, mask and shape path points of the dragged layer snap too, and
while snapping is on the feature that snaps is boxed during the drag.

Test: layer_drag_snaps_to_other_layers_edges_ctrl_toggles_and_options_apply
(UI), snapping_options_pick_the_targets and
snapping_options_command_sets_and_toggles (engine).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 17:14:05 +07:00
BFlat c486130a9b Merge pull request #255 from storytold/fix/252-key-selection-props
Timeline: a selected key selects its property and layer (#252)
2026-10-08 16:20:15 +07:00
bflatasticandClaude Opus 5.5 8d5e4ae0ef Timeline: a selected key selects its property and layer (#252)
Clicking (or marquee-selecting) keyframes in the Timeline selected only
the keys. Their property and layer stayed unselected, so the Graph
Editor, which shows the selected properties or else the animated
properties of the selected layers, came up empty until the property
was selected separately. As in After Effects, the Timeline's key
selection now selects the keys' properties and layers too (Shift adds
to them). keys.select takes this as `selectProperties`; the Graph
Editor's own key clicks leave what it shows alone.

Test: a_key_clicked_in_the_timeline_shows_in_the_graph_editor
(ui_keyframes).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 16:19:53 +07:00
BFlat 72a2d47131 Merge pull request #249 from storytold/docs/248-py-aep-g4
Docs: gaps.md G4 lists py-aep as a candidate for .aep import (#248)
2026-10-08 14:40:58 +07:00
bflatasticandClaude Opus 5.5 9fc4407a37 Docs: gaps.md G4 lists py-aep as a candidate for .aep import (#248)
A user suggested py-aep (MIT, Python), which reads and writes .aep
files. G4 now records it for the clean-room decision, with the two
ways it could be used: as an external converter, or ported, which
means reading another implementation of the format.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 14:40:30 +07:00
BFlat 55defcb167 Merge pull request #247 from storytold/fix/243-gl-fallback
App: a launch whose window never drew makes the next use OpenGL (#243)
2026-10-08 13:59:08 +07:00
bflatasticandClaude Opus 5.5 d554724aa4 App: a launch whose window never drew makes the next use OpenGL (#243)
On some older Intel integrated GPUs (UHD 600, UHD with 2020 drivers)
the graphics driver crashes EffectCraft as its window's DirectX 12 or
Vulkan device is created (exit code 0xC0000005): the process
disappears with no message, and only WGPU_BACKEND=gl helped. A driver
crash can't be caught, so each launch now leaves a marker in the
settings folder until its window draws. A launch that finds the
previous one's marker switches the new Settings ▸ Startup & Repair ▸
Window Graphics (Automatic / OpenGL) to OpenGL and says so in the
status bar. A window that fails to open says the next launch will try
OpenGL. WGPU_BACKEND still wins; macOS has no OpenGL backend, so it
never switches there.

Tests: a_launch_that_never_drew_switches_the_next_to_opengl (app),
window_graphics_is_validated (engine). Checked end to end with a
leftover marker: the app opened on OpenGL with the notice and cleared
the marker.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 13:57:21 +07:00
bflatasticandClaude Opus 5.5 fdb655e78e GPU: the compositor declines OpenGL adapters at once (#243)
On OpenGL the compositor translated its compute kernels for tens of
seconds (22 s on an RTX 4090) before one failed validation and it fell
back to CPU compositing, all before the window's first frame. It now
declines GL adapters up front, as it declines adapters without compute
shaders. OpenGL is what Window Graphics falls back to for drivers that
crash with DirectX 12 or Vulkan.

Test: opengl_adapters_get_cpu_compositing_at_once (gpu tests_init;
skips without an OpenGL adapter).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 13:57:08 +07:00
BFlat 4d5765f158 Merge pull request #233 from ulanch/fix-variable-font-axis-test
Fix variable-font animator test axis selection and endpoint delta
2026-10-08 13:38:03 +07:00
BFlat 06aa138c94 Merge pull request #245 from storytold/fix/232-golden-libm-variant
Effects: the sim golden test accepts macOS 27's libm hash for Mr. Mercury (#232)
2026-10-08 13:36:30 +07:00
bflatasticandClaude Opus 5.5 e61f0e5560 Effects: the sim golden test accepts macOS 27's libm hash for Mr. Mercury (#232)
On macOS 27.2 (rustc 1.98) one of the 404 pinned simulation hashes,
CC Mr. Mercury at non-default settings, 32 bpc, t = 2.2 s, came out
different. The reporter rendered every case at the commit that pinned
the hashes and at current main on that machine, and the frames were
byte-identical. So the code didn't change: the platform's libm did
(the hashes are exact f32 bits through sin, cos and powf). The test
now also accepts listed per-case libm variants, with that hash as the
first, and every other case stays exact. Its docs say how to tell a
libm difference from a code change.

Test: libm_variants_are_accepted_for_their_case_only (sim_golden; runs
on every platform).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 13:36:09 +07:00
BFlat f4319c1917 Merge pull request #244 from storytold/fix/234-startup-logging
Logging: RUST_LOG prints to stderr, and start-up logs its milestones (#234)
2026-10-08 13:32:44 +07:00
bflatasticandClaude Opus 5.5 468ed0b297 Logging: RUST_LOG prints to stderr, and start-up logs its milestones (#234)
The logger kept warnings in memory and wrote to a file only after Help
▸ Enable Logging, and it ignored RUST_LOG. An app that never opens its
window (macOS 27 on Apple Silicon, #234) therefore left no trace. Now
RUST_LOG (a level, or env_logger-style `target=level` directives)
also prints what it lets through to stderr, including eframe, winit
and wgpu. The desktop app logs its start-up milestones: opening the
window, the graphics adapter, the app created, the first frame drawn,
the window being asked to close, and the window closed.

Test: rust_log_filters_by_level_and_target (engine logging). Checked
by running the app with RUST_LOG=info and quitting it over the control
channel.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 13:32:16 +07:00
BFlat 5a7119d5fa Merge pull request #242 from storytold/docs/gaps-reports-8-oct
Docs: gaps.md records the user reports of 8 October
2026-10-08 13:11:12 +07:00
bflatasticandClaude Opus 5.5 6a8d339316 Docs: gaps.md records the user reports of 8 October
The reports of 8 October (#225-#227, #229-#231), their fixes (#235,
#237-#241), what remains of #227 (multi-monitor support, the Project
panel's empty-area menu, effect points on shape and text layers) and
the regression tests for each.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 13:10:57 +07:00
BFlat 49af6472bc Merge pull request #240 from storytold/fix/227-shape-contents
Shapes: draw into, copy, paste and duplicate shape contents (#227)
2026-10-08 13:09:50 +07:00
bflatasticandClaude Opus 5.5 25c01aa5a0 Docs: gaps.md lists the regression tests for the #227 shape fixes
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 13:03:46 +07:00
bflatasticandClaude Opus 5.5 89e075d1b1 Shapes: Tools bar Tool Creates Shape / Mask and Fill / Stroke Options (#227)
The Tools bar had Fill and Stroke swatches and a Stroke Width, kept in
the interface only, so agents couldn't set them and new shapes could
only be a solid fill with a solid stroke. As in After Effects:

- With a shape layer selected and a shape tool or the Pen active, Tool
  Creates Shape / Tool Creates Mask choose whether drawing adds shapes
  to the layer or masks on it (Fill and Stroke hide while drawing
  masks).
- Clicking the word "Fill" or "Stroke" opens Fill Options / Stroke
  Options: None, Solid Color, Linear Gradient or Radial Gradient, the
  blend mode and the opacity. The swatches show the kind; Alt-click
  steps through the kinds.
- New shapes and Pen paths are painted with them: a Gradient Fill or
  Gradient Stroke across the shape for the gradient kinds.

The options are engine state (EditorState.shape_tool) set with the new
shape.toolOptions command; shape.newShape and shape.newPath use them
unless given paint parameters (which now include fillType, fillBlend,
fillOpacity and their stroke twins; layer.newShape takes them too). A
Pen path without paint parameters now takes the Tools bar's stroke
(none by default) instead of a fixed 2 px white one. layer.addMask also
draws rounded rectangle, polygon and star masks, so every shape tool
makes a mask on a selected footage or solid layer (rounded rectangles,
polygons and stars made a new shape layer).

Test: shape_tool_options_paint_new_shapes,
masks_of_every_shape_tool_kind,
tool_creates_mask_draws_a_mask_on_the_selected_shape_layer,
fill_options_paint_the_next_shape.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 13:03:38 +07:00
bflatasticandClaude Opus 5.5 0048569682 Shapes: Duplicate duplicates the selected shape items, not the layer (#227)
Edit > Duplicate (Ctrl+D) with a shape group or other shape item
selected in the Timeline duplicated the whole shape layer. As in After
Effects it now duplicates the selected items inside their group, each
copy above its original and numbered on ("Rectangle 1" gives
"Rectangle 2"), and selects the copies. With only layers selected the
layers are duplicated as before.

Test: duplicate_with_shape_items_selected_duplicates_them_in_place,
ctrl_d_duplicates_the_selected_shape_group_in_its_layer.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 13:03:38 +07:00
bflatasticandClaude Opus 5.5 95bb346c89 Shapes: Copy, Cut and Paste shape items between shape layers (#227)
Edit > Copy with a shape group (or a path, fill, stroke or path
operation) selected in a shape layer's Contents copied the whole layer,
and Paste made a new layer. As in After Effects, Copy now copies the
selected items, Paste puts them into each selected shape layer's
Contents (above its selected item, in that item's group, else on top,
with names kept unique there: "Rectangle 2"), and Cut removes them, so
Cut + Paste moves them. Pasting with no shape layer selected is an
error rather than a new layer. Copying anything else clears the shape
item clipboard, as it does the other clipboards.

Test: shape_contents_copy_cut_and_paste_between_shape_layers,
ctrl_c_and_ctrl_v_copy_a_shape_group_into_another_shape_layer.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 13:03:38 +07:00
bflatasticandClaude Opus 5.5 9968950b71 Shapes: drawing with a shape tool adds to the selected shape layer (#227)
With a shape layer selected, the Rectangle, Rounded Rectangle, Ellipse,
Polygon and Star tools made a new shape layer for every shape. After
Effects draws into the selected shape layer instead: the shape becomes a
new group on top of its Contents ("Rectangle 2", ...), placed by the
group's Transform. The new shape.newShape command does that (in the
layer's space, so a moved or scaled layer still gets the shape where it
was drawn), and makes a new shape layer only when no unlocked shape
layer is selected. The viewer's shape tools use it; masks on other
selected layers are unchanged. layer.newShape and the Pen's
shape.newPath share its layer and Contents helpers.

Test: shape_tools_draw_into_the_selected_shape_layer,
shape_tool_draws_into_the_selected_shape_layer.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 13:03:38 +07:00
BFlat 7e4a7cd863 Merge pull request #238 from storytold/fix/227-interface
Spacebar hand tool, Effect Controls menu, New Comp drop, switch drags (#227)
2026-10-08 13:02:49 +07:00
bflatasticandClaude Opus 5.5 764eee409b Gaps: regression evidence for the #227 interface fixes
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 12:59:40 +07:00
bflatasticandClaude Opus 5.5 c4c28682eb Timeline: dragging over layer switches sets them all (#227)
Each layer switch toggled on its own click only, so hiding or locking
many layers took a click per layer. As in After Effects, pressing a
switch in the A/V Features or Switches column now sets it at once, and
dragging up or down gives the same switch of every layer the pointer
passes the state the first one got, whatever their own state was
(rows passed in a quick drag included). The whole drag is one undo
step: layer.setSwitch takes a `merge` key like other gesture edits.
A single click still changes only that layer, so clicking the eye of
one selected layer doesn't change the others (Layer > Switches still
sets all selected layers).

Test: dragging_over_layer_switches_sets_them_all.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 12:59:40 +07:00
bflatasticandClaude Opus 5.5 6b95b7a182 Project panel: items dropped on New Composition make a comp (#227)
Dragging Project items onto the Create a new Composition button at the
bottom of the Project panel did nothing. As in After Effects, the
button now takes the drop and runs File > New Comp from Selection with
the dragged items (the selection when the dragged item is in it): one
item becomes a comp of its size, frame rate and duration holding it,
and several open the New Composition from Selection dialog. The button
is outlined while items are dragged over it.

Test: project_items_dropped_on_new_comp_make_a_composition.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 12:59:40 +07:00
bflatasticandClaude Opus 5.5 fab6dedd7c Effect Controls: right-click shows the Effect menu (#227)
Right-clicking the Effect Controls panel did nothing outside an
effect's header. As in After Effects, a right-click anywhere a control
has no menu of its own (the empty area, property names, buttons such
as Reset) now opens the Effect menu: Effect Controls, Last Effect,
Remove All, Manage Effects and every effect category, applying the
chosen effect to the selected layers. It is the menu bar's own Effect
menu tree and drawing code; an effect header keeps its own menu.

Test: right_click_in_effect_controls_shows_the_effect_menu.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 12:59:40 +07:00
bflatasticandClaude Opus 5.5 e008085a93 Spacebar: a tap previews on release, held it is the Hand tool (#227)
Spacebar started or stopped the preview on its press, so holding it to
pan the Composition panel (Space+drag already panned there) also
toggled playback. As in After Effects, plain Spacebar now runs its
shortcut when it is released, and only if no mouse button went down
while it was held and the press didn't go to a text field; key
auto-repeat toggles nothing more. Shift+Spacebar and the numpad
preview shortcuts still run on the press.

Holding Spacebar over the Timeline's ruler and time graph is the Hand
tool there too: dragging scrolls the time graph (when zoomed in, never
before the comp's start or past its end) instead of moving bars, keys
or markers.

Test: spacebar_taps_preview_and_held_spacebar_pans_the_viewer,
spacebar_drag_scrolls_the_time_graph.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 12:59:40 +07:00
BFlat e1062ab81b Merge pull request #241 from storytold/feat/229-system-language
Settings: Language ▸ Match System follows the system language (#229)
2026-10-08 12:58:55 +07:00
bflatasticandClaude Opus 5.5 fd572949f3 Settings: Language ▸ Match System follows the system language (#229)
The interface language was English unless Japanese was chosen in
Settings ▸ General ▸ Language. Language now has a Match System choice
(`general.language` = `system`), the new default: the operating
system's interface language where EffectCraft has a translation
(Japanese), else English, as After Effects installs in the system's
language. English and Japanese can still be chosen; a language chosen
before stays. The browser build stays in English with Match System,
since it has no Japanese font of its own.

The system language comes from sys-locale (MIT OR Apache-2.0, no
dependencies on the desktop platforms), on desktop builds only.

Tests: match_system_uses_the_system_language_where_there_is_a_catalog
(i18n), interface_language_is_validated_persisted_and_backward_compatible
(engine).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 12:58:11 +07:00
BFlat bb3e63e8f3 Merge pull request #239 from storytold/fix/230-viewer-selected-layer-first
Viewer: a press on a selected layer behind others acts on it (#230)
2026-10-08 12:50:11 +07:00
bflatasticandClaude Opus 5.5 2c7eca7ef2 Viewer: a press on a selected layer behind others acts on it (#230)
A press in the Composition panel always acted on the topmost layer
under the pointer, so a selected layer could not be dragged where
another layer covered it: the press selected the layer in front. As in
After Effects, a selected layer under the pointer now comes first: it
stays selected and a drag moves it. With no selected layer there the
topmost layer is picked as before, and Shift still toggles the topmost
one. The context menu's list of layers under the pointer shares the
same hit test.

Test: viewer_press_prefers_a_selected_layer_under_the_pointer
(ui_viewer).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 12:47:30 +07:00
BFlat 5d1b818fa1 Merge pull request #237 from storytold/fix/225-226-231-web
Web: short Project panel, unreadable picked files, lost graphics device (#231, #226, #225)
2026-10-08 12:46:13 +07:00
bflatasticandClaude Opus 5.5 db3b34b134 Web: a lost graphics device says so and offers a reload (#225)
When the page's WebGPU device was lost, the canvas kept showing its
last frame: edits still went through and rendered frames were right,
but the editor never showed them and nothing said why (the status
message about it is drawn in the canvas that can't present any more).
The page now covers the canvas with a notice that offers Save Project
(a download) and Reload. Reload first writes pending changes to
browser storage, so the session snapshot brings the project and its
unsaved changes back; if that write fails it says so and offers
Reload Anyway. Without a kept session (?empty, memory storage) the
notice says to save first.

Test: apps/effectcraft-web/tests/page.mjs (Node, fake DOM).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 12:38:11 +07:00
bflatasticandClaude Opus 5.5 82a1862fa1 Web: Media Browser reports a picked file it can't read (#226)
Add Files... and Upload Folder... read every picked file in one go and
rejected on the first file the browser couldn't read; the Media
Browser dropped that rejection, so nothing was added and nothing said
why. Each file is now read on its own: the readable ones are added and
the status bar names the unreadable ones with the browser's reason and
suggests picking them again. Cancelling the picker is not reported.

Test: apps/effectcraft-web/tests/page.mjs (Node, fake DOM).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 12:38:11 +07:00
bflatasticandClaude Opus 5.5 7612b220ef Project panel: a panel too short for the scroll bar draws (#231)
The Project panel's vertical scroll bar clamped its thumb between 16
points and the track's height. When the track was shorter than 16
points (an empty project in a 1280 x 800 browser window at device pixel
ratio 2, or any short panel), that clamp panicked on the first frame:
the browser canvas went blank and the app stopped responding. The
thumb is now at most the track's length, and both scroll bars share
the calculation.

Test: a_short_project_panel_draws (ui_project_rows).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 12:38:10 +07:00
BFlat 543bff29dc Merge pull request #235 from storytold/fix/227-turbulent-displace
Effects: Turbulent Displace displaces the whole layer, not one quadrant (#227)
2026-10-08 12:36:29 +07:00
bflatasticandClaude Opus 5.5 754e80bdf9 Effects: Turbulent Displace displaces the whole layer, not one quadrant (#227)
Edge pinning (Turbulent Displace's default Pin All, Wave Warp's
Pinning, Bulge's Pin All Edges) holds the layer bounds still: a
comp-sized rectangle for shape and text layers. That rectangle started
at the layer's origin, but a shape layer's content surrounds its origin
(anchor 0,0), so everything left of or above the origin counted as
outside the layer and was pinned: only the bottom-right quadrant of a
centred shape moved.

The renderer now passes the bounds' top-left in layer coordinates
(EffectEnv::bounds_origin): (-w/2, -h/2) for layers without a source
rectangle, (0, 0) otherwise. util::pin_rect places the bounds there and
the three effects use it on the CPU and the GPU. Effect point
parameters keep their current layer coordinates.

Test: edge_pinning_displaces_all_of_a_centred_shape_layer,
edge_pinning_on_a_shape_layer (GPU vs CPU).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 12:20:10 +07:00
Alex Ulanch d506a80639 Fix variable-font animator test axis and endpoint selection 2026-10-07 21:38:19 -07:00