Content r from Center smears out to r / (1 - s), so the buffer is padded by
the farthest corner's reach (capped at 4096, skipped on adjustment layers),
on the CPU and GPU paths alike.
On an adjustment layer Motion Tile kept the comp-sized buffer whatever the
Output Width / Height, and the adjustment draw dropped any stack result whose
size differed from the canvas. After Effects grows (or crops) the comp
rectangle there as on any layer, so a Transform after it brings the extra
tiles into view, as in the frames attached to #1727 and #1409.
- motion_tile_frame: one output frame for the CPU and GPU effect; on an
adjustment it grows about the comp rectangle on whole pixels, keeps the
buffer at 100 % and past 16 384 pixels a side.
- adjustment_crop: the CPU and GPU adjustment draws put a stack result whose
frame changed back onto the canvas instead of dropping it (Enc::crop on
the GPU).
Tests: the #1727 After Effects frames (910 x 340 % and 90 x 340 % then
Scale 12 %), tiles past the frame and cropping on CPU, GPU vs CPU for
adjustment and full-frame layers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WGSL cannot read Rust constants, so the Rec. 601 luma (Tint, Tritone, Luma Key /
Extract), Black & White's tint weights and Exposure's 2.2 power stay literals in the
kernels; they now name their Rust source (effects::LUMA_601, BW_TINT_LUMA,
EXPOSURE_LINEAR_GAMMA, newly exported) and the test shader_constants_match_the_cpu
(no GPU needed) fails if a literal drifts. gpu_tint_defaults_match_after_effects_greys
says "skipped: no GPU adapter" when it skips (EFFECTCRAFT_REQUIRE_GPU=1 still makes it
fail). docs/effects-parity.md: Brightness & Contrast is close to, not the same as,
After Effects (up to 6.2/255), and the summary row lists what is measured.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After Effects weighs each of the four points by its inverse squared
distance softened by Blend squared, 1 / (d^2 + Blend^2) in pixels: fitted to
AE 2026 renders of the default gradient at Blend 10, 100 and 300 and case
904 E16 at Blend 50 (power 2.00 at every Blend, softening 0 / 2367 / 9817 /
90127 = about Blend^2; mean error 0.27/255, max 0.9). EffectCraft used
1 / (d^2 / (100 Blend))^1.5, in which Blend cancels out: E16 was 34/255 off
on 99 % of the grid, now 0 grid points off, max 1/255, CPU and GPU.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After Effects 2026 in a 32 bpc project, Levels (Individual Controls) with RGB
(0.1, 0.9, gamma 1.5, 0.05, 0.95), Red Gamma 0.8, Green Input White 0.8 and
Blue Output Black 0.2 on grey steps 0..255 (case 904 E07): every step matches
"each channel's controls, then the RGB controls" with nothing clipped
(values below Input Black end below Output Black, above Input White above
Output White). EffectCraft ran RGB first and clipped "Off for 32 bpc Color"
(it could not see the bit depth): up to 27/255 off on 95 % of the grid; now
0 grid points off, max 1/255, CPU and GPU.
EffectEnv gains float_depth (the renderer sets it for 32 bpc projects), so
"Off for 32 bpc Color" clips in 8 / 16 bpc projects as before. Unclipped
values below 0 take the gamma mirrored through 0. Plain Levels keeps its
order (its channel controls were not measured); its clipping follows the same
rule. EffectCraft's own projects default to 8 bpc and are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At preview scales from 0.75 to 1 footage keeps native pixels, a proxy sets
its own buffer scale at any render scale, a Preserve Resolution precomp is
drawn at full size, and earlier effects can pad or offset the buffer, so
snapping to the preview grid could pick a different ID than the rendered
matte. Use the scale and offset of the buffer the Cryptomatte effect
receives (layer_input, rendered once and then cached).
The pixel centre goes to an aux point through effectcraft_effects::aux_point,
which index_map_at now calls too, so the two round identically (a division
vs the reciprocal picked the other ID on a 104 px proxy of 1920 px footage).
An unknown effect uid errors "no such effect" and a layer without pixels
"this layer has no pixels", as effect.pickColor does.
Engine part of viewer picking: replace / add / remove the top-coverage ID at a
layer-space point in one undo step, at the time the Selection was read. The
aux channels come from the render's own path (Renderer::layer_aux, shared with
the effect host; footage at its source time, 3D precomps) through a renderer
set up like Session::render (expressions, render prefs, layer cache). An
optional scale snaps the point to the pixel a preview at that resolution
samples. New effects::crypto_select quotes names and falls back to <hex> for
unnamed, wildcard-looking or ambiguous names. effect.cryptomatteAt is the
read-only hover query.
Echo, Posterize Time and the other Time effects read the layer at other times with none of
its effects. After Effects renders them with the effects before them: SceneYard "Task
Preparation UI" applies Echo after CC Particle Systems II on black solids, and AE shows the
echoed fireworks over a white page where we echoed the black solids and blacked it out
(Posterize Time after Turbulent Displace, the usual stop-motion setup, depends on the same).
The host already bounds the count to the effects before the one rendering.
Mean abs error vs AE: Task Preparation UI 0.1855 -> 0.0071, Lightbulb Idea 0.0126 -> 0.0102;
the 15 other corpus projects with a Time effect after another effect are pixel-identical.
(cherry picked from commit 46870452db2656c72406f63245e8b1609ceb04d4)
(cherry picked from commit 334d102582e5a64db32e2f3e55836d55ac96bdaa)
(cherry picked from commit 734da3e35dc57c01b73823458bdd87611ac1bf99)
(cherry picked from commit 87a733e076)
AuxChannels::crypto_rank_planes resolves a layer's (ID, coverage) planes once; crypto_ranks keeps its signature on top of it. The CPU effect and the GPU effect's aux image (effectcraft_effects::crypto_aux_image, rayon) index the planes per pixel. Output is bit-identical.
Problem: After Effects 26.2's Object Matte (pick an object with a click,
optionally a box, get a matte that follows it) had no counterpart; only
stroke-driven Roto Brush existed.
Fix: an Object Matte effect (Effect > Matte, ec.matte.objectmatte) turns
the Object Point (and optional box) on a Reference Frame into Roto Brush
strokes and segments them with Roto Brush's engine, content-keyed cache
and the model chosen in Settings > Roto Brush (MobileSAM, classical graph
cut without it). Track Object carries the matte frame by frame up to 600
frames each way; otherwise every frame is segmented on its own. Feather,
Contrast, Shift Edge, Invert and a Matte view. The engine keeps its Input
Key current so upstream edits recompute it. objectMatte.apply creates one
from agents / the CLI. Unusable prompts pass the layer through.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Time > Pixel Motion Blur fetched the layer at its shutter times and averaged the decoded
frames as they were, so fast motion rendered as doubled copies and the accepted Vector
Detail parameter changed nothing. Build a TimewarpPlan instead: every shutter instant is
decoded between the two source frames it falls between along a measured motion field — the
same block-matching and in-between synthesis Time > Timewarp uses, with a search radius
wide enough for full-frame displacements and the vector grid tightened from Vector Detail
0 (one vector per 32 px) to 100 (one per 2 px) — and the instants average into the blur.
CPU and GPU share the plan (fx_time renders it with its Timewarp step loop, declining to
the CPU when there is no host), so the existing GPU parity cases for the effect compare
the new path. Falling back to the old average is kept for layers without a time host.
A render integration test moves a textured block 12 px between source frames, renders
the straddling shutter, and asserts the output is neither the still nor the plain frame
overlay, lays trail coverage along the block's path, and changes with Vector Detail;
without the synthesis it fails at the overlay assertion.
An authored layer's effect buffer spanned only its painted content — the glyph
bounding box for text, the drawn bounds for shapes — while the size the stack
measures from (effect_bounds) is the whole comp-sized layer rectangle. Tiling
and displacement effects computed correct output for pixels outside the painted
rectangle and then wrote them into the buffer that was too small, so CC Tiler
repeated only within the text box and CC Smear lost its liquid trail at the
glyph edge (#658). Buf::cover now grows the stack's input to the layer's
rectangle with transparent margins — what a sized source's buffer already is —
applied by run_effect_stack whenever an enabled video effect will run, and
natively on the GPU by reallocating and copying the texture. The placement and
the sized-source paths are unchanged; two regression tests (CC Tiler spans the
region, CC Smear trails past the text bounds) fail without the growth.
A text or shape layer's effect bounds are comp-sized (as in After Effects),
but its buffer surrounds only its content, and each effect grows the buffer
itself when it needs room. CC Smear and CC Tiler never did, so a smear was
cut off where the glyphs end and tiles filled only the text's box.
CC Tiler now covers the whole layer rectangle; CC Smear grows by how far it
moves pixels (Reach x From->To), at most to the layer bounds, so footage
layers, whose buffer is their bounds already, are unchanged. CPU and GPU
alike.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
Cause: Apply Color LUT's only parameter, `lut`, was ParamUi::Hidden (set only
through commands), so the effect could not be used from the UI. Effect
Controls also had no widget for any ParamUi::Text parameter (only EXtractoR's
channels had one), so Cryptomatte's Selection, the OCIO effects' config file /
file / config names and Colorama's Output Cycle could not be set either.
Fix: a new ParamUi::File { filters } for paths. Effect Controls shows the
file's name (None when empty) and a Choose... button that opens the host's
file dialog with those extensions and sets the path (prop.set, so agents use
the same command). As in After Effects, applying an effect with an empty file
parameter from the UI (Effect menu, Last Effect, drops, Effects & Presets)
opens the file dialog straight away; cancelling keeps the effect without a
file. Apply Color LUT's LUT (.cube) and the OCIO Config File (.ocio) and File
(.cube/.3dl/.csp/.cc/.ccc/.cdl/.spi1d/.spi3d/.spimtx) use it. Text
parameters get a single-line field that sets the value when it loses the
focus (Esc keeps the old text). Saved projects are upgraded on open: a
Hidden or Text parameter whose spec is now a File parameter takes the File
UI and keeps its path (effects::migrate). docs/effects.md regenerated.
This covers #473's first part (the Selection field); picking mattes in the
viewer, the layer popup from the file's metadata, the GPU path and quoted
commas are separate.
Tests: effects hidden_and_text_paths_become_file_parameters (migration); ui
file_and_text_parameters_have_widgets (the LUT is asked for on apply and
inverts red to cyan, the Choose... button picks another LUT, typing into
Cryptomatte's Selection sets it).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
(cherry picked from commit 403863f703999d19d3a83d9976f76bdc7577e682)
Cause: CC Particle World projected its particles through a fixed camera
(distance 2 layer widths, looking at the layer centre) and never asked the
host for the comp camera, so moving or orbiting a camera layer left the
particles flat on the layer. It also had no Extras > Effect Camera. Separately,
the layer cache key ignored the comp camera and lights, so the effects that
read them (Card Dance / Shatter / Card Wipe Comp Camera, First Comp Light,
Caustics) kept a stale frame when the camera, a light or the layer moved.
Fix: as the Cycore manual describes, Particle World sees its particles through
the comp's active camera layer when there is one (EffectHost::comp_scene now
says whether its camera is a camera layer's, CompScene::camera_layer), through
card3d's comp-camera projection (factored out of Card Dance as comp_proj, with
Proj::view for depth, size scale and screen velocity). Otherwise it uses the
new Extras > Effect Camera: Distance (2), Rotation X / Y / Z (0) and FOV (50)
orbit the world origin; FOV moves the camera so the origin's plane keeps the
scale Distance gives it, so the defaults reproduce the old fixed camera bit for
bit (golden hashes unchanged) and old projects render as before. The Effect
Camera is left out of the simulation cache key, so animating it doesn't
restart the simulation. The plan is shared, so the GPU compositor follows too.
The layer cache folds the comp scene into the key of layers running an effect
in effects::READS_COMP_SCENE (render::comp_scene is shared with the host).
Tests: sim particle_world_follows_the_comp_camera (panning a camera layer moves
the particles, zooming spreads them, the Effect Camera is ignored then, the
default view keeps the Effect Camera, cameras facing away / zero / NaN draw
nothing) and particle_world_effect_camera (missing params = defaults, rotation,
distance and FOV change the view, extremes stay finite, the sim key ignores the
camera); render particle_world_follows_the_comp_camera (a real camera layer
panned 40 px moves the particles left, and the cached render after the move
equals an uncached one: it failed with the old cache key); gpu
particle_world_through_the_comp_camera (CPU vs GPU at 8 / 32 bpc, full and
half resolution). particle_golden / sim_golden unchanged. effects.md
regenerated; parity.md and gaps.md describe the fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cause: the config reader didn't know ColorSpaceTransform or
ExponentWithLinearTransform and passed colours through them silently.
Blender's Linear Rec.709 -> sRGB therefore stayed in its E-white XYZ
reference space (the blue cast) without the sRGB curve. Display
colorspaces, view transforms, v2 views, looks, AllocationTransform,
BuiltinTransform and .spi1d / .spimtx files were missing too, and
nothing told the user.
Fix (from the OCIO v2 documentation and the CLF "moncurve", not OCIO's
source): ExponentWithLinear, ColorSpace, Look, DisplayView, Allocation
and Builtin transforms (identity, ACES AP0/AP1 matrices, ACEScct /
ACEScc, the sRGB / P3 / Rec.1886 / gamma / PQ displays). RangeTransform
now clamps and takes one-sided ranges. Display colorspaces meet the
scene reference through the default view transform. Added v2 views,
view looks, the looks section and .spi1d / .spi3d / .spimtx files. A
conversion is built once as a flat list of steps (Config::path /
display_path) for both the CPU effect and the GPU colour program (new
moncurve and clamp ops and the ACEScc curve; a program over the
kernel's 255 ops renders on the CPU). Named-transform resolution is
limited to 16 levels and 4096 steps, and YAML nesting to 64 levels, so
hostile or looping configs stop. Transforms that are still unsupported
(Grading Primary / Tone, ACES output, HLG…), unreadable configs and
unknown spaces are now reported: a warning under the effect in Effect
Controls, and the new effect.warning query.
Tests: ocio_config::blender_like_config_converts_linear_rec709_to_srgb
(CST, display colorspaces, v1 / v2 views, allocation, looks, builtin,
range, loops), moncurve_matches_srgb_and_inverts, builtin_styles,
yaml_nesting_is_bounded, ocio::spi_files_parse,
tests_effects::ocio_effect_warns_about_unsupported_config_transforms,
gpu tests_fx_lut::ocio_custom_config_v2 (GPU = CPU).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Both visualisers read their Audio Layer's samples around the frame
time, but they weren't in TIME_DEPENDENT, so the viewer's layer cache
gave the layer one key for every frame and showed the first frame's
spectrum for the whole preview. Export doesn't use that cache, which is
why it rendered correctly.
Test: audio_spectrum_follows_time_through_the_layer_cache renders a
tone that starts at 0.5 s through a shared layer cache after t = 0 and
compares it with an uncached render (it showed the stale baseline).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018gnTnH3Jmk5zr4tMq1Mc9f
- 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>
Effects without expect(): nested parameter groups are looked up with ? (falling back to the
effect's top level), the OCIO effects use named constants for the built-in ACES spaces instead
of expect()ing a table lookup, curve fitting and Path Text no longer expect on just-pushed
vectors. Test-only unreachable!() becomes panic!(). Denies the panic lints in the crate.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The geometry stays on the CPU as per-frame plans shared with the CPU effects (bolts, mask
strokes, scribbles, contour segments, audio marks, glyph strokes, waves, flood-fill region,
eyedropper sample); segments and convex polygons are tile-binned and rasterised on the GPU
with the CPU's coverage functions. CPU output unchanged (gen_golden). CC Threads' exact ties
at round parameter values are a documented exception; Fractal stays on the CPU (needs f64).
Regenerated docs/effects.md, docs/parity.md EFF-5.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GPU render passes for CC Particle World / Systems II, Particle Playground,
CC Ball Action, CC Pixel Polly, CC Scatterize and Curl Noise (fx_particles,
chunked multi-pass raster); Shatter wireframe views, Foam extras, Timewarp
with a Matte Layer, strong Warp Fisheye/Twist (CPU f64 inverse map) and
custom .ocio configs no longer fall back to the CPU. CPU refactors pinned by
particle_golden / timewarp_golden / sim_golden hashes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Inner/Outer Key's trimap and Cleanup strokes, Color Link's sample and CC Glass/Plastic's
light are shared with the CPU effects (CPU output unchanged: gen_golden). CC Plastic now
reads its Bump Layer (it read the nonexistent bumpMap id).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CC Rainfall, Snowfall, Star Burst, Bubbles, Drizzle, Hair, Mr. Mercury,
Caustics, Wave World, Foam, Shatter, Card Dance and Card Wipe render on the GPU
(fx_sim.rs / fx_sim.wgsl). The per-frame simulation / layout stays on the CPU
and is shared with the CPU effect through plans (effects::sprite_plan,
piece_plan, bubble_list, mercury_blobs, drizzle_drops, wave_world_plan,
caustics_setup); a tiled item rasteriser composites sprites, bubbles, blob
fields and textured pieces in the CPU's order and shading. Layer-dependent
sprite colours (refracting rain, star-burst and hair roots) are sampled per
pixel; piece plans read the frame back once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Timewarp without a Matte Layer is now planned once (time_fx::timewarp_plan: frames, shutter
instants, motion fields) and built by the CPU or the GPU from the plan; with a Matte Layer it
renders on the CPU (catalog::gpu_supported).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
VR Blur, Chromatic Aberrations, Color Gradients, Converter (all projections and
cube layouts), De-Noise, Digital Glitch, Fractal Noise, Glow, Plane to Sphere,
Rotate Sphere, Sharpen and Sphere to Plane run on the GPU (fx_vr.rs /
fx_vr.wgsl) with the CPU's equirectangular maths: seam-wrapping, pole-continuing
sampling and latitude-widened box blurs, per eye for stereo layouts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CC Environment now fades nearly transparent environment texels to black continuously
(colour / max(alpha, 1e-3)) instead of a hard 1e-6 un-premultiply cut-off, which turned the
filtered map's float noise into colour.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Apply Color LUT, OCIO CDL / Color Space / Display / File / Look Transform and
Color Profile Converter compile to colour programs (effects::color_program,
built next to the CPU effects) that fx_lut.wgsl interprets per pixel; LUTs
(1D, 3D, cineSpace shapers, inverses) are read from the storage buffer with the
CPU's nearest / trilinear / tetrahedral interpolation. Lumetri's Input LUT and
Look no longer force the CPU. Custom .ocio configs stay on the CPU.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3D Channel Extract, Cryptomatte, Depth Matte, Depth of Field, EXtractoR, Fog 3D,
ID Matte and IDentifier run on the GPU (fx_depth.rs / fx_depth.wgsl): aux planes
upload as a texture and are resampled onto the buffer grid with the CPU's index
arithmetic; tests compare against the CPU at 1x / 2x aux resolution, padded
buffers, half resolution and 8 / 32 bpc composites.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- gpu: pooled working textures (released once every encoder that could read
them is submitted), shared zero images, reused readback staging buffers,
direct readback into Image; NaN-poisoned scratch images under test and
NaN-aware diffs; validation errors panic under test
- render: AutoPick (per comp / scale / path EWMA of CPU vs GPU frame times,
warm-up and periodic re-probe) behind Backend::Auto; viewer uses it too;
cached adjustment footprints (uploaded once)
- gpu fx_warp: Warp, Bezier Warp, CC Bend It, CC Page Turn, Smear, Reshape,
Color Emboss, Cartoon; sliding-histogram median for large radii; CC Power
Pin perspective < 100
- cli: bench --gpu reports Auto and per-frame upload / readback MB
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Effect Controls: Lumetri RGB and hue/saturation curve graphs, Colorama's
palette wheel, Glow's colour map strip and Arbitrary Map curves, Reshape
correspondence buttons and viewer handles (all prop.set, one undo step per
gesture, automation ids). Preview panel scrolls; Composition/Timeline tabs
get close, label swatch and viewer lock; Effect Controls tab the layer
swatch; dropdowns elide long text. Fix the browser start (no default disk
cache folder on wasm32). Browser smoke test covers frame workers, waiting
renders and the Media Browser.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>