As in After Effects, the Delete key and the Project panel's trash button ask before deleting
items that layers in other compositions use, saying how many layers in how many compositions go
with them (Delete / Cancel; Escape cancels). Unused items are deleted at once; agents running
project.delete directly are not asked. project.usage {items?} reports the count, sharing the
deletion's item and usage helpers. Follow-up to #72.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
As in After Effects, a double-click below the items in the Project panel opens File ▸ Import ▸
File... (rows and the scroll bar keep their own clicks). Dropping files on the window already
imported them; the test now covers both. (On Wayland, winit delivers no file drops at all, so
dragging files in works on X11, macOS and Windows.)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The details header (item name, size, duration, codec) and the "Select an item" hint were painted
with no width limit, so in a narrow Project panel they ran past its right edge. They now stop
10 px short of it and end in "…" when they don't fit (widgets::text_fit, a shared helper), and
are registered for automation (project.details.*).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While paused, the app prefetched the frames around the current one as soon as the viewer's
frame finished. During a drag every step changes the project, so those frames were stale at the
next step, and they held the CPU and GPU the next viewer frame needed: on the demo project a
drag step took 102 ms on average to show (up to 413 ms) though a frame renders in 20 ms. Paused
prefetch now waits until edits pause for 0.3 s: 16 ms a step (11 ms render).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two implementations of Show 3D Reference Axes had landed (M13.5 and M14.5) and both drew,
offset by a few pixels: lines, and arrows. Keep the arrowed one (perspective-correct, shown
while a 3D layer is active) and test that only one is drawn.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The handle drag rescaled incrementally from the current scale every frame: errors built up,
edge handles scaled both axes, and once a factor reached ~0 the drag could never bring it
back. It also tested the handle at the point where egui recognised the drag (past the drag
threshold), so a quick grab often missed the handle and moved the layer instead.
Now, as in After Effects, the drag scales about the anchor point relative to the layer as the
drag began, so the grabbed point stays under the pointer: corners scale both axes, edges one,
Shift keeps the proportions, and dragging past the anchor flips. Selection-tool gestures start
at the press point.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- effectcraft-segment: a FaceModel interface (find a face in a region, follow it; a Topology
maps the model's points to the tracker's landmarks and outline), a TensorFlow Lite reader and
interpreter (float ops, float16 weights folded; matches ai-edge-litert to ~1e-6), and
MediaPipe Face Landmarker (Google, Apache-2.0: BlazeFace + Face Mesh V2, 3.8 MB, pinned
SHA-256). The registry gains a task and authors per model; installed weights get a NOTICE.
- effectcraft-track: FaceTracker::new_with uses the model, falls back to the classical fitter
when it finds no face; chin and jaw come from the outline either way so measurements agree.
- Engine: roto_models becomes models (both tasks); face.models / face.model.* commands,
Settings ▸ Face Tracking, `wait` on select/install/download for scripts; track.mask reports
faceModel.
- Credits: ATTRIBUTION.md lists the optional models and the Apache-2.0 specs the code follows.
- Docs: README, ROADMAP (G7), gaps, parity (TRK-3 score unchanged pending G1), agents,
preferences, architecture, segment README.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- New L1 crate effectcraft-segment: the MaskModel interface (prompts → foreground probability),
a registry of open-licensed models (licence must be MIT/Apache/BSD/…; URL, size, SHA-256),
a PyTorch checkpoint reader (zip + pickle subset), dependency-free SHA-256, and MobileSAM
(TinyViT-5M + SAM prompt encoder / two-way transformer / mask decoder) on rayon and ndarray's
safe GEMM. Loads the official mobile_sam.pt as published; checked against candle
(embedding 0.3% RMS, 99.9% equal mask pixels); ~0.7 s/frame encoder at 32 threads, cached
per frame, ~0.1 s decoder.
- track::roto: segment_with / propagate_with prompt a model with the strokes or the flow-warped
matte (box, core points, mask); its probability is a strong, unblurred prior for the same
graph cut (strokes stay hard constraints); a failing model or one disagreeing with the flow
(IoU < 0.5) is ignored. Prior gets a weight.
- effects::roto: Version 2.0 / 3.0 use the model chosen in Settings (set_model / model_for);
the model id is part of the chain seed.
- engine: roto_models (roto.models, roto.model.select / install / download / remove), weights in
the models folder (host::config_dir()/models, shared by the app, CLI and MCP), background
loading, downloads through the system curl, verified; Settings ▸ Roto Brush page (prefs
roto.model). host::config_dir moved from the desktop app.
- Tests: segment (kernels vs direct sums, checkpoint reader, SHA-256 vectors, registry), track
(fake model: prior shapes the cut, failing / disagreeing models ignored; real weights with
EFFECTCRAFT_MOBILESAM: base IoU 0.989, propagated ≥ 0.980 vs classic 0.973), engine (registry,
verification, selection; real install + load), MCP.
- Docs: README, ROADMAP, parity (MSK-4 done: 90/2/0), preferences, agents, architecture,
crates/segment/README.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- audio::AudioScrub: an open output that plays a faded snippet (one frame, 30–100 ms) of the
comp's mix at each new frame, replacing what is still queued; holding still repeats nothing.
The mixing is shared with the preview feeder (mix_block); AudioFeed::clear.
- The Timeline ruler: Ctrl/Cmd-drag scrubs audio (EffectcraftApp::scrub_audio opens the output
through the audio device hook on first use). The Audio panel meters follow the snippets; an
idle second closes the output.
- playback.scrubAudio {time?} for agents.
- Tests: ui_audio_scrub (snippet length, fades, same-frame no-op, replace not pile up, silent
comps, Cmd-drag in the ruler, idle close) with a fake device. Docs: parity.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- commands::markers::nested_markers: a precomp layer's comp markers at their times in the
containing comp (start and stretch in closed form; with time remapping or a stretch over
protected regions, the first frame whose source time reaches them), within In–Out.
- markers.nested {layer}: the same list for agents.
- Timeline: drawn on the precomp bar as outlined read-only markers (positions cached per
revision), hover names them, double-click opens the nested comp at the marker; automation id
timeline.layer.<id>.nestedMarker.<i>.
- Tests: engine (start, stretch, trim, time remap, non-precomp error), UI (draw, label, open).
Docs: parity, control protocol.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- PanelKind::Viewer(n): more Composition viewers (panels::viewers). One is active (the full
interactive viewer, showing the active comp); the others show their own comp's frame at that
comp's current time (CPU or GPU frames, their own textures) and become active on a click or
their tab, their comp becoming the active comp.
- view.newViewer (View menu, the Composition panel menu, Alt+Shift+N): a new viewer of the
active comp next to the one in use, which is locked. Opening a comp while the active viewer
is locked shows it in an unlocked viewer, or a new one when every viewer is locked. Closing
the active viewer hands over to another with its comp. Tabs are named after their comp, with
their own lock and swatch; Preview full screen maximizes the active viewer.
- UI state: viewers (comp per viewer) and active_viewer (serde).
- Tests: ui_viewers (lock on New Viewer, routing, click to activate, all locked → new viewer,
close hand-over, tab titles; an ignored snapshot). The bounded-drag frames test asserts what
holds when freed addresses are reused. Docs: parity, control protocol.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- FrameKey's identity is the comp's content (frames::comp_content), not the project revision:
an edit keeps the cached frames of comps it doesn't touch and undo finds the frames of the
state it returns to. The identity hashes the addresses of the comps a comp draws (~0.1 ms on
the 1500-layer test project; the full content hash took ~117 ms), by value the footage, solids,
proxies and project settings; with expressions, every item. Frames::content_of keeps a
project snapshot per identity with frames (or in flight), so Arc::make_mut always copies an
edited comp and its address can't be reused meanwhile; past 64 identities the least recently
used go with their frames (long drags).
- drop_stale is gone (frames of other contents are evicted least-recently-shown first).
- disk_cache::comp_content_key covers items' proxies and Use Proxy (it served stale frames).
- Tests: frames (content keys, edits elsewhere and undo, bounded drags, in-place edit safety),
ui_ram_preview (end to end), the disk cache proxy key. Docs: roadmap (M4 done), parity.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Home covers the whole workspace below the Tools bar (the panels wait behind it) instead of
the Composition panel.
- Left rail: New Project (primary) and Open Project, the Home / Templates / Learn pages with
icons, and the community links and ArtCraft apps at its foot.
- Home page: "Welcome to EffectCraft", quick-start tiles (New Composition, Open Demo Project,
Import Footage, New from Template) and Recent as a table — thumbnail, name and folder,
Opened (relative), Size, Kind — with a filter field, Clear list and an empty state.
- Theme tokens throughout (light themes follow); web_time for "now" (works on the web).
- Tests: formatters (opened / size / kind) and the new automation ids. Docs: parity.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- paint_logo draws docs/brand/artcraft-mark.png (the README's / getartcraft.com's ArtCraft
mark) as supplied, decoded once into a mipmapped texture, instead of a drawn placeholder.
- The "More ArtCraft apps" sibling and help.sibling named DrawCraft (storytold/drawcraft, which
doesn't exist): it is VectorCraft (storytold/vectorcraft); provenance comments in the path
crate follow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The Timeline shows one tab per open comp (dock DocTab). Clicking a tab shows that comp; ×
or middle-click closes that comp's Timeline (comp.close), not the panel. Opening a comp brings
its viewer and Timeline tab to the front.
- Project panel: a double-click on an item's name opens it (a comp gets its own tab) instead of
renaming; Enter and the context menu rename.
- Tab drags: over a tab strip an insertion mark shows where the tab lands, and it lands there
(Layout::insert_tab), which also reorders a group's own tabs; side zones are unchanged.
- Tests: ui_tabs (double-click, tab switch / close, drag reorder and cross-group insert; an
ignored mid-drag snapshot); the Timeline close test follows the new behaviour.
- Docs: parity, control protocol.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- effect.pickColor {effect, param|prop, x, y (layer px), average?}: the colour of the effect's
input at a point, set on the parameter (one undo step). Keying effects' Effect Controls
eyedroppers use it, since the shown frame is already keyed; Ctrl/Cmd+click averages 5x5.
- effects::aliases / name_matches: Effects & Presets and effect.list (MCP list_effects) find
Key Light by After Effects' name ("Keylight (1.2)").
- End-to-end test: a green-screen plate with an uneven screen, spill and soft edges keyed via
effect.apply "Keylight (1.2)", pick, Clip Black / White and a render; UI eyedropper test.
- Docs: parity, agents (green-screen recipe).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- J / K (Go to Previous / Next Keyframe) and Ctrl+Alt+A (Select All Keyframes) act on what the
Timeline shows, as in After Effects: the keys of its revealed properties, layer and comp
markers and the work area (they used every property of the selected or all layers, revealed
or not). The commands take `visible: [{layer, prop}]`, which the Timeline fills in; agents
calling them without it keep the previous behaviour.
- Graph Editor ▸ Auto-Select Graph Type (an "Auto" button, on by default as in After Effects):
the speed graph when every property shown is spatial (Position, Anchor Point…), else the
value graph; choosing Value or Speed turns it off.
- Docs: parity, agents.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Go to Next / Previous Keyframe (K / J) also stops at the work area's start and end, as in
After Effects.
- With one keyframe selected, the Info panel shows its property, time and value instead of
"1 keyframes selected".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Double-clicking a keyframe of a numeric property (or its context menu ▸ Edit Value…) opens
the property's value dialog for that key, as in After Effects (it only moved the current
time); other keys still move the current time to them.
- The keyframe context menu has Select Equal Keyframes, Select Previous Keyframes and Select
Following Keyframes (`keys.selectEqual` / `selectPrevious` / `selectFollowing`).
- Roving keys draw as small dots in the Timeline, like After Effects.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `keys.transform {fromStart}`: each step of a merged drag gives the whole transform since the
drag started and applies to the keys as they were then. Keys squeezed onto one frame on the
way (Alt-drag scaling, the transform box) are no longer merged for good, and a slow
transform-box move, whose per-frame offsets rounded to nothing, now moves the keys.
- Dragging a key in the Graph Editor moves every selected key in time and value (it moved only
the grabbed one), by how far the pointer went from where the button went down (the key no
longer jumps under the pointer); Shift keeps the drag to the time or the value axis.
- Shift+click toggles a key in or out of the selection; Ctrl+click switches Linear ↔ Auto
Bezier and Ctrl+Alt+click toggles Hold, as in the Timeline.
- The transform box and the Timeline's Alt-drag scale send whole-gesture values.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A drag (keys.move steps sharing a merge key) applies its total offset to the keys as they
were before it (`Session::key_move`, `prop::shift_keys`): dragging over another key of the
property no longer deletes it; only the drop position replaces a key, as in After Effects.
- Keys of time-stretched layers move with the pointer: offsets are applied in comp time and
snapped to comp frames (drags and Alt+Arrow nudges; they moved by the layer-time distance).
- Shift+click toggles a key in or out of the selection (`keys.select {toggle}`); Ctrl+click
switches Linear ↔ Auto Bezier and Ctrl+Alt+click toggles Hold.
- Keys of locked layers can't be selected (clicks, marquee, Select All Keyframes), so nothing
edits them.
- Toggle Hold off returns an eased key to Bezier (it always restored Linear).
- Shift-dragging a key snaps to everything the current time snaps to — other keys, layer in /
out points, layer and comp markers, the work area — and a tooltip shows the key's time and how
many frames it moved.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each reading starts PowerShell on Windows (in the background since the previous commit); the
cache budgets only need to follow memory pressure, not track it closely.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The windowing layer delivers Ctrl+C, Ctrl+X and Ctrl+V as clipboard events instead of key
presses, and the shortcut dispatcher only read key presses: in the app, copying and pasting
keyframes, layers and effects with the keyboard did nothing (tests passed because they inject
key presses). Clipboard events now map back to their keys with the modifiers held (so Ctrl+Alt+C
and the other variants work too), except while text is edited in the viewer, which handles them
itself. Ctrl+V sends no event while the system clipboard is empty, so copying now also puts a
line naming what was copied on it ("EffectCraft: 3 keyframes").
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dragging a keyframe moved it a frame or two and then let go: the key's widget id includes its
time, so after the first move it was a new widget and egui ended the drag. The Timeline now
follows the whole gesture itself (`KeyDrag`: from where the button went down, by whole frames,
every selected key, one undo step), like After Effects. Holding Shift snaps the grabbed key to
the current time, the work area and composition markers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Following AGENTS.md's Never crash rules for the M3 / M4 additions:
- Cache Frames When Idle panicked (`clamp` with min > max) when the work area was inverted
(start set past the end); an empty work area now has nothing to cache.
- `file.newCompFromSelection`: the sequence overlap times the layer count overflowed for a huge
`overlapDuration` (wrapping to a wrong comp length without overflow checks); negative or
non-finite values fall back to 1 s and the product saturates. The lead item is read with
`get()` instead of indexing.
- Layer Settings' enablement reads the first selected layer with `first()`.
- Regression tests for both (each failed before the fix).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ui-egui without expect()/unreachable!(): the frame pool is optional (rayon's global pool if the
OS refuses threads) and gets a panic_handler, because a panic in a rayon spawn() job aborts the
whole process by default; render-queue progress, the text-edit anchor and the GPU viewer
texture are bound where they're checked; the settings panel's Bool arm no longer panics.
Denies the panic lints in the crate.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Composition Settings ▸ Advanced has both options (they were missing from the model, the
renderer and the dialog):
- Preserve frame rate: nested in another comp, the comp shows only its own frames
(`EvalCtx::nested_time` floors the precomp layer's source time to the nested comp's frame
rate; used for the nested frame, its proxy, aux channels, collapsed transformations and the
viewer's internal wireframes), and the Render Queue renders it at its own rate whatever the
Frame Rate override (`RenderSettings::rate`).
- Preserve resolution: nested in a comp rendered at Half / Third / Quarter, it renders at full
size and the buffer's scale places it.
- `comp.settings {preserveFrameRate, preserveResolution}`, `comp.info` reports them, scripts
read and set `CompItem.preserveNestedFrameRate` / `preserveNestedResolution`, Pre-compose
copies them, and the dialog registers `dialog.comp.preserveFrameRate` /
`dialog.comp.preserveResolution`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Composition ▸ Preview ▸ Cache Frames When Idle was only a saved checkbox. After a second
without input or edits it now renders the work area's frames (from the current time on, then
from the start) into the RAM preview, a few at a time behind the viewer's own frame, until the
work area is cached or the RAM preview budget is full.
Docs: preferences.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Viewer Resolution Auto rendered at half its scale while a preview played, so frames cached
while scrubbing were not played and frames cached by the preview were re-rendered once it
stopped. Auto now follows the zoom alike playing and paused (Fast Previews still lowers it
while dragging).
- Cache Before Playback waited for every frame of the range: a range larger than the RAM
preview budget evicted its first frames and never started. The range is now cut to the
frames that fit (90 % of Settings ▸ Memory & CPU's budget) with a message, then cached and
played.
- The Info panel shows the frame rate a playing preview achieves and whether it is real time
(`Playback::achieved_fps` / `real_time`, automation id `info.previewFps`).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `FrameKey` carries a hash of the render options (`frames::opts_hash`): switching Fast
Previews (Draft / Fast Draft) or Realtime Shadows no longer shows frames rendered the other
way.
- Eviction drops the least recently shown frame (`Frames::get` touches it), not the oldest
inserted: a looping preview a little over budget no longer evicts the frame it shows next.
- Frames of earlier project revisions are dropped on every change (`Frames::drop_stale`); they
could never be shown again but held memory (and GPU textures) until evicted.
- The green cache bars (Timeline, Preview panel) count the frames of exactly what the viewer
shows — scale, 3D view / region of interest and options (`EffectcraftApp::shown_series`) —
instead of any frame at that scale.
- Playback loops compute the key's fixed part once (`EffectcraftApp::frame_series`).
- Docs: architecture.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Pre-compose makes the new composition with the original's pixel aspect, background, motion
blur and frame blending settings and 3D renderer (`Comp::nested_like`); it kept only size,
rate and background.
- File ▸ New Comp from Selection records one undo step (two per item before) and takes each
item's pixel aspect. Several items selected open the New Composition from Selection dialog:
Single / Multiple Compositions, Use Dimensions From, Still Duration (default from Settings ▸
Import ▸ Still Footage), Add to Render Queue, Sequence Layers with Overlap, Duration and
Transition. `file.newCompFromSelection {single, dimensionsFrom, duration, addToRenderQueue,
sequence, overlap, overlapDuration, transition}`; `layer.addItem {duration}` for stills.
- Docs: parity.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Locked layers can't be selected (Timeline click, Select All, Ctrl+Up/Down; shy layers while
hidden too) and are left alone by Clear/Delete, Arrange, Layer ▸ Transform and Center Anchor
Point, which fail when every target is locked (`commands::unlocked`, `commands::selectable`).
- Deleting a parent unparents its children where they are, in the same undo step: the
pick-whip compensation moved out of `layer.setParent` into `layer::parent_fixes` /
`ParentFix::apply`, which now also handles separated X/Y Position.
- Bring Forward / Send Backward move each selected layer one step (a non-contiguous selection
was squeezed into one block).
- Center In View / Fit to Comp keep a 3D layer's Z (and Fit keeps Scale/Anchor Z); Reset /
Center / Fit / Center Anchor Point work with separated dimensions (`layer::update_position`).
- Duplicate and paste keep parents and track mattes, following the copies when both were
copied; paste dropped them always.
- Docs: agents, parity.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Layer ▸ Layer Settings on a solid or adjustment layer opens Solid Settings on the layer's
solid (it ran an empty command before): name (renames the layer and the solid), size, Pixel
Aspect Ratio, colour, and "Affect all layers that use this solid". It starts off, as in After
Effects, so changing a solid other layers share gives this layer its own copy.
`layer.settings {pixelAspect?, affectAll?}` → `{layer, solid?}`; `layer.newSolid {pixelAspect?}`.
- Nulls ask for their name; text and shape layers have no settings (the entry is disabled).
- Composition Settings / New Composition and Solid Settings register an automation id for
every control (`dialog.comp.*`, `dialog.solid.*`). Composition Settings takes any typed frame
rate besides the list, and New Composition starts from the last settings used.
- Colour buttons in both dialogs show the stored sRGB colour (`widgets::srgb_color_button`);
egui's linear picker showed it lighter.
- Docs: agents, parity.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Quit and the window's close button, File ▸ New Project, Open Project, Open Recent, Close
Project, the demo project and a Home template show Save / Don't Save / Cancel when the
project has unsaved changes (Save asks for a path when untitled; cancelling that keeps the
project open). File ▸ Revert asks to discard. Before, all of them dropped unsaved work
without asking.
- The window title shows the project's path and `*` while it is modified.
- The modified mark follows undo: the session keeps the snapshot it last opened or saved
(`Session::saved_project`), so undoing, jumping in the History panel or rolling back a batch
to the saved state clears it.
- An edit that changes nothing (a value set to what it already is) records no undo step and
doesn't mark the project modified (`Project::same_content`, comparing untouched comps by
pointer).
- Agents' `engine.execute` / MCP calls are never asked; `ui.menu.invoke` shows the prompt like a
click (automation ids `dialog.unsaved.save` / `dontSave` / `revert` / `cancel`), and the
control channel's `app.quit {force: true}` skips it.
- Docs: architecture, agents, control protocol, parity.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Off macOS `Cmd` maps to Ctrl, so Enter Full Screen's Ctrl+Cmd+F parsed
to the same keys as Find's Cmd+F and both commands were bound to Ctrl+F
(ui_and_menu_shortcuts_do_not_collide fails on Linux and Windows).
Shortcuts that use both Ctrl and Cmd can't be typed distinctly there,
so they are skipped when the key bindings are built off macOS.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- GPU in browser job workers: each job worker opens its own WebGPU device
(deferred readbacks); the Render Queue export and the analyses' frame loops
are futures (render::passes, offload::run_request_async) whose frames
render in passes, awaiting the device between them. GPU particles and
Advanced 3D read back under keys too. workers.jobs reports passes and
readbacks; the smoke test measures them.
- Layer buffers in the browser's OPFS disk cache: frame workers back their
layer caches with a PrefetchStore; the page plans what to fetch before a
request from recent misses (LayerPrefetch over the shared DiskIndex).
- Templates embed footage (default on, size-capped with a warning) and
creating from a template extracts it next to the new project / into
browser storage.
- ICC LUT profiles (lut8/lut16/lutAtoB/lutBtoA, from the public ICC spec) in
Simulate Output > My Custom RGB, baked into a 3D LUT for the viewer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- GPU in the browser: each frame worker opens its own WebGPU device (Gpu::request_deferred) and
renders the GPU compositor and GPU effects with deferred readbacks (effectcraft-gpu deferred):
readbacks keyed by content start with map_async and miss, the pass returns placeholders, the
layer cache's gate drops inserts, and FrameServer::resume renders again once the device
delivered. Backend Auto times whole frames per comp in the workers. Frames with effects go to
GPU workers instead of the page. CPU fallback without WebGPU in workers (?nogpuworkers).
- Fix: a WGSL constant (3.40282347e38) that Chrome rejects invalidated every GPU kernel in Chrome
(the page's compositor too); a test checks every float literal fits f32.
- Disk cache in the Origin Private File System: workers write sealed frame entries with sync
access handles (temp file + move), the page keeps the DiskIndex (LRU under the settings' limit,
at most half the quota) and reads hits instead of rendering; survives reloads; blue cache bar.
- Storage manager: Settings ▸ Disk ▸ Browser Storage (usage/quota, persistent storage, Clear
buttons, automation ids) and storage.info / storage.persist / storage.clear (StorageHost).
- Docs: web.md (GPU workers, deferred passes, disk cache, storage manager, threading design:
one engine instance per worker), architecture, preferences, parity (WEB-1), roadmap.
- Tests: deferred passes vs CPU on a real device, placeholder gating, frame server passes and
Auto, GPU worker routing + disk cache across a reload, storage commands and widgets, store
usage by kind; smoke test: GPU effect frames in workers with pixel parity, disk hits after a
reload, storage manager.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>