The Japanese data point in #821 (zitann2020) offered to write its rows against whatever mechanism
this PR lands, so both languages share one catalog instead of inventing two. This makes adding a
language a data change and reserves `ja`:
- `Catalog` holds a language's effect rows and its category labels, and `catalog()` resolves a
language code to one; adding a language is two tables and one arm, with no change to the lookups
or to any panel.
- `JAPANESE_EFFECTS` and `JAPANESE_CATEGORIES` are declared, keyed by the same effect ids and by
the registry's English category names, and `ja` resolves to them. Both are empty, so a Japanese
interface draws exactly what it drew before — the slot changes no behaviour on its own.
- `a_pending_language_only_uses_known_ids` guards the pending tables (Japanese and Traditional
Chinese) as rows arrive: an id Simplified Chinese does not know, a different English name for one
of them, an empty translation or a repeated id fails. `languages_without_rows_draw_the_registry_name`
asserts each pending language has an arm in `catalog()` (a table with no arm would leave a
translator's rows dead) and is still row-less, so the day one gets rows the test says where its
coverage moved.
- `only_the_brand_names_stay_in_english` now documents that it pins *Simplified Chinese's* five
brand names and is deliberately not shared: AE's Japanese interface keeps the whole `CC`, `VR`
and `OCIO` sets in English while its Chinese one translates them, so repeating the English name
is a language's choice and not an unfinished row. The row check reflects that (a repeated English
name is accepted, an empty one is not).
docs/localization-parity.md records the two empty tables and that each language decides for itself
which names stay in English.
Refs #821
The interface below the menu bar is localized (618 fixed menu entries, ~490 panel rows) but an
effect's name still rendered in English everywhere it appeared: the generated Effect menu, Effect
Controls, the Timeline and Effects & Presets.
The registry's English name is the effect's identity: `effect.list`, `effect.apply {"effect": …}`,
scripts, expressions and the property tree's group name all match on it. Only the drawing changes.
`crates/ui-egui/src/i18n/effects.rs` keys the names by effect **id** rather than by the English
string — a repeat is stored as `Gaussian Blur 2` (an English key would miss it) and upstream
renames a display name now and then, while the id is stable. It carries 306 rows for Simplified
Chinese, one per effect in the registry, taken from the installed After Effects 2026 (26.5.0.89)
Simplified Chinese interface, with the `CC` series following Adobe's Chinese documentation (which
names 68 of the 69 that its interface leaves in English). `Cryptomatte`, `EXtractoR`, `IDentifier`,
`Key Light` and `Unmult` stay in English as brand names; `Face Measurements` and `Face Track
Points` are EffectCraft's own effects. The 21 category folder names are in the same file, worded
after the effect names (channel → 通道, noise → 杂色, matte → 遮罩 …).
What draws the catalog now:
- **Effect menu:** the entries and the category submenus. The command and its `{"effect": <id>}`
params are untouched, so the menu still applies by id.
- **Effect Controls:** the header, its Rename field, the accessible name and the About popup.
- **Timeline:** effect group rows, and the search (a Chinese interface is searchable in Chinese).
- **Effects & Presets:** the rows, the drag ghost, the folder rows and the search.
Kept as they are: the effect id, the property tree's `PropGroup.name`, every automation id and
label, `effect.list`'s `name`/`category`, and the undo label (`Apply Gaussian Blur`). A name the
user typed in Effect Controls stays verbatim, and a repeated instance keeps the index the engine
appends (`Gaussian Blur 2` → `高斯模糊 2`). No other language has rows yet, Traditional Chinese
included, so those draw the registry's English name.
Tests: nine unit tests under `cargo test -p effectcraft-ui-egui --lib i18n` cover the data (every
effect has a row, the row still carries the registry's English name, ids are unique, only the five
brand names repeat their English name, every category has a row, a row names an effect the registry
knows) and the behaviour (the instance index, user renames, English and untranslated languages,
search) plus a scan pinning the drawing sites. `crates/ui-egui/tests/ui_effect_names.rs` checks the
Effect Controls header, the generated Effect menu and that the ids, automation labels and
`effect.list` stay English.
`docs/localization-parity.md` and `docs/gaps.md` no longer record effect names as deliberately
English; effect *parameter* names are still untranslated and stay the next piece.
Refs #821
Shift+Spacebar now plays every frame by default, so it is no longer a gap in docs/ui-parity.md.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
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>
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.
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>
Shortcut presets accept Physical+<key> (egui US-position names) so layouts that report the
same logical key for two physical keys (AZERTY's - and )) can bind them separately; physical
bindings win for that press, and a focused panel's binding wins over an application command on
the same chord. ui.key accepts the same notation. Adds time.workAreaStart / time.workAreaEnd.
(cherry picked from commit 209543e9e0ba32f809f9ff333abb8b8ec4125cea)
(cherry picked from commit f8301a9e3e0b60e200e8a233455398741658481e)
(cherry picked from commit 4d002fabf992934e1bb73a0c32bc9f3fd3ae5447)
(cherry picked from commit 5d466c5955ee934d5347c194ec7d15fb5f58bba4)
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)
(cherry picked from commit 40b232d02069925e9c72677f52ee6f356495a736)
(cherry picked from commit a2d89bda11b7217c83c98ab681b8f9b4a334786d)
(cherry picked from commit 63951595222207b6bfd0ecde72f991628c6f12d9)
(cherry picked from commit 20c2bb0dcb)
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)
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.
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>
Right-clicking a footage item or solid in the Project panel offers
"Replace with Precomposition (used N times)": the item goes into a new
composition of its size, pixel aspect and frame rate, in the same folder, and
every layer in every composition that used it switches to that composition,
keeping timing, transforms, effects and masks. One undo step. Agents run it as
`file.replaceWithPrecomp`.
The Project panel's details header also shows how often the selected item is
used ("Name ▾, used 3 times"); the ▾ lists and opens the comps that use it.
`Project::item_users` and the `project.usedIn` query back both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The desktop app imported any argument it didn't know as a file (writing it to Recent Footage)
and dropped a missing or non-numeric --control value, starting without a control server.
parse_args now returns an error for an unknown option, a --control value that isn't a port from
1 to 65535, or a bad EFFECTCRAFT_CONTROL_PORT (checked only when --control isn't given); main
prints it and exits with status 1 before anything starts. --control=<port> works, and -- ends
the options so a file named -x can still be opened.
Tests: parse_args unit tests, and tests/args.rs runs the binary with its own settings folder
and checks the exit, the message and that nothing was written.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>