Merge branch 'main' into worktree-agent-a15cecfec57d5290d

# Conflicts:
#	crates/effects/src/ocio.rs
#	crates/engine/src/commands/stubs.rs
#	docs/parity.md
This commit is contained in:
Brandon Thomas
2026-10-03 02:26:19 -04:00
151 changed files with 14085 additions and 2190 deletions
+11 -2
View File
@@ -172,7 +172,8 @@ chain with one upload and one readback. Tests render scenes on both paths and co
(≤ 1/255 at 8 bpc, ≤ 1e-3 at 32 bpc); they skip without an adapter. The desktop viewer builds the
`Gpu` on egui-wgpu's device and shows frames from GPU textures without reading them back
(`ui-egui::frames`); headless renders, the CLI (unless `--gpu`) and CI use the CPU. On the web
(WebGPU) the GPU composites viewer frames; steps that need a readback fall back to the CPU.
(WebGPU) the GPU composites viewer frames; steps that need a readback fall back to the CPU (the
Info panel's pixel readout reads GPU frames back asynchronously).
**Colour and bit depth** (`crates/render/src/color.rs`, `crates/color/src/space.rs`). Pixels are
`f32`, but 8 and 16 bpc projects clamp and quantise each layer after its source and masks and
@@ -315,6 +316,14 @@ inspect and click the interface. See [agents.md](agents.md) and
Settings (`engine::prefs`), keyboard shortcut presets (`engine::shortcuts`) and auto-save /
crash recovery (`engine::autosave`) live in the engine too; they persist through a
`ConfigStore` the frontend provides (a directory on the desktop, `localStorage` on the web).
`ConfigStore` the frontend provides (a directory on the desktop; the Origin Private File System,
or IndexedDB, on the web). Auto-saves go through the store's `FileOps` (the file system by
default, the browser storage on the web).
Renders and analyses run on background threads on the desktop. Where there are no threads (the
browser), `engine::offload` sends them to a second engine instance instead: the session's
`Offload` ships the serialized project, the footage it reads and the job (`WorkerRequest`) to a
Web Worker, and the worker's replies (progress, item status, the analysed property group) are
applied on the UI thread.
The shortcut dispatcher and the menus read the active preset. See
[preferences.md](preferences.md).
+421
View File
@@ -0,0 +1,421 @@
# Effects
<!-- Generated from the effect registry (crates/effects/src/catalog.rs); do not edit by hand.
Regenerate: UPDATE_DOCS=1 cargo test -p effectcraft-effects --lib effects_doc_is_current -->
EffectCraft ships 303 effects with 2434 parameters in total, grouped into the same categories as the Effects & Presets panel. Every effect is our own implementation, written from public behaviour descriptions and standard image-processing literature. Parameter names, order, popup options, units, defaults and ranges follow the reference application so that projects, expressions and muscle memory carry over.
- **GPU**: 24 effects also run on the GPU compositor with identical results.
- **32**: 303 effects process 32-bit float (HDR, overbright) pixels without clamping.
- **Status**: 261 implemented in full, 42 partial (what is missing is listed).
Effects are addressed by id (`ec.<category>.<name>`) or display name in commands, scripts and the control channel, and every parameter by its id (`effects/#1/blurriness`).
## 3D Channel
| Effect | Id | Params | GPU | 32 | Status |
|---|---|---:|:-:|:-:|---|
| 3D Channel Extract | `ec.3d.channelextract` | 5 | | 32 | Partial: no anti-alias option |
| Cryptomatte | `ec.3d.cryptomatte` | 4 | | 32 | Implemented |
| Depth Matte | `ec.3d.depthmatte` | 3 | | 32 | Implemented |
| Depth of Field | `ec.3d.depthoffield` | 4 | | 32 | Implemented |
| EXtractoR | `ec.3d.extractor` | 8 | | 32 | Implemented |
| Fog 3D | `ec.3d.fog3d` | 8 | | 32 | Implemented |
| ID Matte | `ec.3d.idmatte` | 5 | | 32 | Implemented |
| IDentifier | `ec.3d.identifier` | 3 | | 32 | Implemented |
## Audio
| Effect | Id | Params | GPU | 32 | Status |
|---|---|---:|:-:|:-:|---|
| Backwards | `ec.audio.backwards` | 1 | | 32 | Implemented |
| Bass & Treble | `ec.audio.basstreble` | 2 | | 32 | Implemented |
| Compressor | `ec.audio.compressor` | 8 | | 32 | Implemented |
| Delay | `ec.audio.delay` | 5 | | 32 | Implemented |
| Distortion | `ec.audio.distortion` | 7 | | 32 | Implemented |
| Flange & Chorus | `ec.audio.flangechorus` | 9 | | 32 | Implemented |
| Gate | `ec.audio.gate` | 4 | | 32 | Implemented |
| High-Low Pass | `ec.audio.highlowpass` | 4 | | 32 | Implemented |
| Modulator | `ec.audio.modulator` | 4 | | 32 | Implemented |
| Parametric EQ | `ec.audio.parametriceq` | 12 | | 32 | Implemented |
| Reverb | `ec.audio.reverb` | 6 | | 32 | Implemented |
| Stereo Mixer | `ec.audio.stereomixer` | 5 | | 32 | Implemented |
| Tone | `ec.audio.tone` | 7 | | 32 | Implemented |
## Blur & Sharpen
| Effect | Id | Params | GPU | 32 | Status |
|---|---|---:|:-:|:-:|---|
| Bilateral Blur | `ec.blur.bilateral` | 3 | | 32 | Implemented |
| CC Cross Blur | `ec.blur.cccross` | 3 | | 32 | Implemented |
| CC Radial Blur | `ec.blur.ccradial` | 4 | | 32 | Implemented |
| CC Radial Fast Blur | `ec.blur.ccradialfast` | 3 | | 32 | Implemented |
| CC Vector Blur | `ec.blur.ccvector` | 4 | | 32 | Implemented |
| Camera Lens Blur | `ec.blur.cameralens` | 10 | | 32 | Partial: no Diffraction Fringe or Blur Map layer |
| Camera-Shake Deblur | `ec.blur.camerashakedeblur` | 8 | | 32 | Implemented |
| Channel Blur | `ec.blur.channel` | 6 | | 32 | Implemented |
| Compound Blur | `ec.blur.compound` | 4 | | 32 | Implemented |
| Directional Blur | `ec.blur.directional` | 2 | GPU | 32 | Implemented |
| Fast Box Blur | `ec.blur.fastbox` | 4 | GPU | 32 | Implemented |
| Gaussian Blur | `ec.blur.gaussian` | 3 | GPU | 32 | Implemented |
| Radial Blur | `ec.blur.radial` | 4 | | 32 | Implemented |
| Sharpen | `ec.blur.sharpen` | 1 | | 32 | Implemented |
| Smart Blur | `ec.blur.smart` | 3 | | 32 | Implemented |
| Unsharp Mask | `ec.blur.unsharp` | 3 | | 32 | Implemented |
## Channel
| Effect | Id | Params | GPU | 32 | Status |
|---|---|---:|:-:|:-:|---|
| Arithmetic | `ec.channel.arithmetic` | 5 | | 32 | Implemented |
| Blend | `ec.channel.blend` | 4 | | 32 | Implemented |
| CC Composite | `ec.channel.cccomposite` | 3 | | 32 | Implemented |
| Calculations | `ec.channel.calculations` | 9 | | 32 | Implemented |
| Channel Combiner | `ec.channel.combiner` | 6 | | 32 | Partial: no Saturation Multiplied target |
| Compound Arithmetic | `ec.channel.compoundarithmetic` | 6 | | 32 | Implemented |
| Invert | `ec.channel.invert` | 2 | GPU | 32 | Implemented |
| Minimax | `ec.channel.minimax` | 4 | | 32 | Partial: no Don't Shrink Edges option |
| Remove Color Matting | `ec.channel.removecolormatting` | 2 | | 32 | Implemented |
| Set Channels | `ec.channel.setchannels` | 9 | | 32 | Implemented |
| Set Matte | `ec.channel.setmatte` | 6 | | 32 | Implemented |
| Shift Channels | `ec.channel.shiftchannels` | 4 | | 32 | Implemented |
| Solid Composite | `ec.channel.solidcomposite` | 4 | | 32 | Implemented |
## Color Correction
| Effect | Id | Params | GPU | 32 | Status |
|---|---|---:|:-:|:-:|---|
| Auto Color | `ec.color.autocolor` | 6 | | 32 | Implemented |
| Auto Contrast | `ec.color.autocontrast` | 5 | | 32 | Implemented |
| Auto Levels | `ec.color.autolevels` | 5 | | 32 | Implemented |
| Black & White | `ec.color.blackwhite` | 8 | | 32 | Implemented |
| Brightness & Contrast | `ec.color.brightnesscontrast` | 3 | GPU | 32 | Implemented |
| Broadcast Colors | `ec.color.broadcast` | 3 | | 32 | Implemented |
| CC Color Neutralizer | `ec.color.cccolorneutralizer` | 11 | | 32 | Implemented |
| CC Color Offset | `ec.color.cccoloroffset` | 4 | | 32 | Implemented |
| CC Kernel | `ec.color.cckernel` | 12 | | 32 | Implemented |
| CC Toner | `ec.color.cctoner` | 7 | | 32 | Implemented |
| Change Color | `ec.color.changecolor` | 9 | | 32 | Implemented |
| Change to Color | `ec.color.changetocolor` | 9 | | 32 | Implemented |
| Channel Mixer | `ec.color.channelmixer` | 13 | | 32 | Implemented |
| Color Balance | `ec.color.colorbalance` | 10 | | 32 | Implemented |
| Color Balance (HLS) | `ec.color.colorbalancehls` | 3 | | 32 | Implemented |
| Color Link | `ec.color.colorlink` | 6 | | 32 | Implemented |
| Color Stabilizer | `ec.color.colorstabilizer` | 6 | | 32 | Implemented |
| Colorama | `ec.color.colorama` | 5 | | 32 | Partial: no Add Phase, editable Output Cycle, Modify, Pixel Selection or Masking |
| Curves | `ec.color.curves` | 0 | GPU | 32 | Partial: no Channel popup |
| Equalize | `ec.color.equalize` | 2 | | 32 | Implemented |
| Exposure | `ec.color.exposure` | 14 | GPU | 32 | Implemented |
| Gamma/Pedestal/Gain | `ec.color.gammapedestalgain` | 10 | | 32 | Implemented |
| Hue/Saturation | `ec.color.huesaturation` | 7 | GPU | 32 | Partial: no per-range Channel Control |
| Leave Color | `ec.color.leavecolor` | 5 | | 32 | Implemented |
| Levels | `ec.color.levels` | 7 | GPU | 32 | Partial: master channel only (no Channel popup) |
| Levels (Individual Controls) | `ec.color.levelsic` | 27 | | 32 | Implemented |
| Lumetri Color | `ec.color.lumetri` | 38 | | 32 | Partial: no HSL Secondary, hue/saturation curves, HDR mode or look files |
| OCIO CDL Transform | `ec.color.ociocdl` | 12 | | 32 | Implemented |
| OCIO Color Space Transform | `ec.color.ociocolorspace` | 4 | | 32 | Implemented |
| OCIO Display Transform | `ec.color.ociodisplay` | 5 | | 32 | Implemented |
| OCIO File Transform | `ec.color.ociofile` | 4 | | 32 | Implemented |
| OCIO Look Transform | `ec.color.ociolook` | 6 | | 32 | Implemented |
| PS Arbitrary Map | `ec.color.psarbitrarymap` | 2 | | 32 | Implemented |
| Photo Filter | `ec.color.photofilter` | 4 | | 32 | Implemented |
| Selective Color | `ec.color.selectivecolor` | 37 | | 32 | Partial: simplified Colors / Details layout |
| Shadow/Highlight | `ec.color.shadowhighlight` | 14 | | 32 | Implemented |
| Tint | `ec.color.tint` | 3 | GPU | 32 | Implemented |
| Tritone | `ec.color.tritone` | 4 | | 32 | Implemented |
| Vibrance | `ec.color.vibrance` | 2 | | 32 | Implemented |
| Video Limiter | `ec.color.videolimiter` | 5 | | 32 | Implemented |
## Distort
| Effect | Id | Params | GPU | 32 | Status |
|---|---|---:|:-:|:-:|---|
| Bezier Warp | `ec.distort.bezierwarp` | 13 | | 32 | Implemented |
| Bulge | `ec.distort.bulge` | 6 | | 32 | Implemented |
| CC Bend It | `ec.distort.ccbendit` | 4 | | 32 | Implemented |
| CC Bender | `ec.distort.ccbender` | 5 | | 32 | Implemented |
| CC Blobbylize | `ec.distort.ccblobbylize` | 15 | | 32 | Implemented |
| CC Flo Motion | `ec.distort.ccflomotion` | 7 | | 32 | Implemented |
| CC Griddler | `ec.distort.ccgriddler` | 5 | | 32 | Implemented |
| CC Lens | `ec.distort.cclens` | 3 | | 32 | Implemented |
| CC Page Turn | `ec.distort.ccpageturn` | 7 | | 32 | Implemented |
| CC Power Pin | `ec.distort.ccpowerpin` | 10 | | 32 | Implemented |
| CC Ripple Pulse | `ec.distort.ccripplepulse` | 5 | | 32 | Implemented |
| CC Slant | `ec.distort.ccslant` | 4 | | 32 | Implemented |
| CC Smear | `ec.distort.ccsmear` | 4 | | 32 | Implemented |
| CC Split | `ec.distort.ccsplit` | 3 | | 32 | Implemented |
| CC Split 2 | `ec.distort.ccsplit2` | 4 | | 32 | Implemented |
| CC Tiler | `ec.distort.cctiler` | 3 | | 32 | Implemented |
| Corner Pin | `ec.distort.cornerpin` | 4 | | 32 | Implemented |
| Detail-preserving Upscale | `ec.distort.upscale` | 4 | | 32 | Implemented |
| Displacement Map | `ec.distort.displacementmap` | 8 | | 32 | Implemented |
| Liquify | `ec.distort.liquify` | 12 | | 32 | Implemented |
| Magnify | `ec.distort.magnify` | 10 | | 32 | Implemented |
| Mesh Warp | `ec.distort.meshwarp` | 3 | | 32 | Implemented |
| Mirror | `ec.distort.mirror` | 2 | | 32 | Implemented |
| Offset | `ec.distort.offset` | 2 | | 32 | Implemented |
| Optics Compensation | `ec.distort.opticscompensation` | 5 | | 32 | Partial: no Optimal Pixels |
| Polar Coordinates | `ec.distort.polar` | 2 | | 32 | Implemented |
| Puppet | `ec.distort.puppet` | 1 | | 32 | Implemented |
| Reshape | `ec.distort.reshape` | 6 | | 32 | Partial: no correspondence points |
| Ripple | `ec.distort.ripple` | 7 | | 32 | Implemented |
| Rolling Shutter Repair | `ec.distort.rollingshutterrepair` | 5 | | 32 | Partial: Pixel Motion Detail has no effect |
| Smear | `ec.distort.smear` | 8 | | 32 | Implemented |
| Spherize | `ec.distort.spherize` | 2 | | 32 | Implemented |
| Transform | `ec.distort.transform` | 10 | GPU | 32 | Partial: no shutter-angle motion blur |
| Turbulent Displace | `ec.distort.turbulentdisplace` | 11 | | 32 | Partial: no locked pinning variants |
| Twirl | `ec.distort.twirl` | 3 | | 32 | Implemented |
| Twirl (Legacy) | `ec.distort.twirllegacy` | 3 | | 32 | Implemented |
| Warp | `ec.distort.warp` | 5 | | 32 | Implemented |
| Warp Stabilizer | `ec.distort.warpstabilizer` | 20 | | 32 | Implemented |
| Wave Warp | `ec.distort.wavewarp` | 7 | | 32 | Implemented |
## Expression Controls
| Effect | Id | Params | GPU | 32 | Status |
|---|---|---:|:-:|:-:|---|
| 3D Point Control | `ec.control.point3d` | 1 | GPU | 32 | Implemented |
| Angle Control | `ec.control.angle` | 1 | GPU | 32 | Implemented |
| Checkbox Control | `ec.control.checkbox` | 1 | GPU | 32 | Implemented |
| Color Control | `ec.control.color` | 1 | GPU | 32 | Implemented |
| Dropdown Menu Control | `ec.control.dropdown` | 1 | GPU | 32 | Implemented |
| Layer Control | `ec.control.layer` | 1 | GPU | 32 | Implemented |
| Point Control | `ec.control.point` | 1 | GPU | 32 | Implemented |
| Slider Control | `ec.control.slider` | 1 | GPU | 32 | Implemented |
## Generate
| Effect | Id | Params | GPU | 32 | Status |
|---|---|---:|:-:|:-:|---|
| 4-Color Gradient | `ec.generate.fourcolor` | 12 | | 32 | Implemented |
| Advanced Lightning | `ec.generate.advancedlightning` | 16 | | 32 | Partial: no Alpha Obstacle; most Expert Settings missing |
| Audio Spectrum | `ec.generate.audiospectrum` | 23 | | 32 | Implemented |
| Audio Waveform | `ec.generate.audiowaveform` | 17 | | 32 | Implemented |
| Beam | `ec.generate.beam` | 11 | | 32 | Implemented |
| CC Glue Gun | `ec.generate.ccgluegun` | 7 | | 32 | Implemented |
| CC Light Burst 2.5 | `ec.generate.cclightburst` | 6 | | 32 | Implemented |
| CC Light Rays | `ec.generate.cclightrays` | 8 | | 32 | Implemented |
| CC Light Sweep | `ec.generate.cclightsweep` | 9 | | 32 | Implemented |
| CC Threads | `ec.generate.ccthreads` | 8 | | 32 | Implemented |
| Cell Pattern | `ec.generate.cellpattern` | 14 | | 32 | Partial: HQ variants render like the standard ones |
| Checkerboard | `ec.generate.checkerboard` | 10 | | 32 | Implemented |
| Circle | `ec.generate.circle` | 11 | | 32 | Implemented |
| Ellipse | `ec.generate.ellipse` | 8 | | 32 | Implemented |
| Eyedropper Fill | `ec.generate.eyedropperfill` | 5 | | 32 | Implemented |
| Fill | `ec.generate.fill` | 3 | GPU | 32 | Partial: no Fill Mask / All Masks / feather |
| Fractal | `ec.generate.fractal` | 21 | | 32 | Implemented |
| Gradient Ramp | `ec.generate.gradientramp` | 7 | GPU | 32 | Implemented |
| Grid | `ec.generate.grid` | 12 | | 32 | Implemented |
| Lens Flare | `ec.generate.lensflare` | 4 | | 32 | Implemented |
| Paint Bucket | `ec.generate.paintbucket` | 11 | | 32 | Implemented |
| Radio Waves | `ec.generate.radiowaves` | 17 | | 32 | Partial: no Image Contours or Mask wave types, Parameters Are Set At, reflection or stroke profile |
| Scribble | `ec.generate.scribble` | 21 | | 32 | Partial: no caps, joins or Start/End Apply To |
| Stroke | `ec.generate.stroke` | 11 | | 32 | Implemented |
| Vegas | `ec.generate.vegas` | 26 | | 32 | Implemented |
| Write-on | `ec.generate.writeon` | 10 | | 32 | Implemented |
## Immersive Video
| Effect | Id | Params | GPU | 32 | Status |
|---|---|---:|:-:|:-:|---|
| VR Blur | `ec.vr.blur` | 2 | | 32 | Implemented |
| VR Chromatic Aberrations | `ec.vr.chromaticaberrations` | 8 | | 32 | Implemented |
| VR Color Gradients | `ec.vr.colorgradients` | 19 | | 32 | Implemented |
| VR Converter | `ec.vr.converter` | 8 | | 32 | Partial: common layouts only |
| VR De-Noise | `ec.vr.denoise` | 4 | | 32 | Implemented |
| VR Digital Glitch | `ec.vr.digitalglitch` | 8 | | 32 | Partial: reduced control set |
| VR Fractal Noise | `ec.vr.fractalnoise` | 16 | | 32 | Implemented |
| VR Glow | `ec.vr.glow` | 7 | | 32 | Implemented |
| VR Plane to Sphere | `ec.vr.planetosphere` | 6 | | 32 | Implemented |
| VR Rotate Sphere | `ec.vr.rotatesphere` | 5 | | 32 | Implemented |
| VR Sharpen | `ec.vr.sharpen` | 2 | | 32 | Implemented |
| VR Sphere to Plane | `ec.vr.spheretoplane` | 5 | | 32 | Implemented |
## Keying
| Effect | Id | Params | GPU | 32 | Status |
|---|---|---:|:-:|:-:|---|
| Advanced Spill Suppressor | `ec.key.advancedspill` | 5 | | 32 | Partial: no Tolerance, Desaturate or Spill Color Correction |
| CC Simple Wire Removal | `ec.key.ccsimplewireremoval` | 7 | | 32 | Implemented |
| Color Difference Key | `ec.key.colordifference` | 16 | | 32 | Implemented |
| Color Range | `ec.key.colorrange` | 8 | | 32 | Implemented |
| Difference Matte | `ec.key.differencematte` | 6 | | 32 | Implemented |
| Extract | `ec.key.extract` | 6 | | 32 | Implemented |
| Inner/Outer Key | `ec.key.innerouter` | 10 | | 32 | Partial: one additional mask per side; no cleanup strokes |
| Key Cleaner | `ec.key.keycleaner` | 4 | | 32 | Implemented |
| Key Light | `ec.keying.keylight` | 42 | | 32 | Partial: no Source Crops X/Y Method or Edge Colour; no colour suppression / balancing |
| Linear Color Key | `ec.key.linearcolor` | 6 | | 32 | Implemented |
| Screen Key | `ec.key.screen` | 11 | | 32 | Implemented |
| Unmult | `ec.key.unmult` | 6 | | 32 | Implemented |
## Matte
| Effect | Id | Params | GPU | 32 | Status |
|---|---|---:|:-:|:-:|---|
| Matte Choker | `ec.matte.mattechoker` | 7 | | 32 | Implemented |
| Mocha shape | `ec.obsolete.mochashape` | 6 | | 32 | Implemented |
| Refine Hard Matte | `ec.matte.refinehard` | 11 | | 32 | Partial: no Reduce Chatter or motion blur |
| Refine Soft Matte | `ec.matte.refinesoft` | 13 | | 32 | Partial: no Reduce Chatter or motion blur |
| Roto Brush & Refine Edge | `ec.matte.rotobrush` | 24 | | 32 | Implemented |
| Simple Choker | `ec.matte.simplechoker` | 2 | | 32 | Implemented |
## Noise & Grain
| Effect | Id | Params | GPU | 32 | Status |
|---|---|---:|:-:|:-:|---|
| Add Grain | `ec.noise.addgrain` | 22 | | 32 | Partial: no preview region, presets, channel balance or temporal controls |
| Curl Noise | `ec.noise.curlnoise` | 10 | | 32 | Implemented |
| Dust & Scratches | `ec.noise.dustscratches` | 3 | | 32 | Implemented |
| Fractal Noise | `ec.noise.fractal` | 26 | GPU | 32 | Implemented |
| Match Grain | `ec.noise.matchgrain` | 25 | | 32 | Partial: no preview region, presets, sampling or temporal controls |
| Median | `ec.noise.median` | 2 | | 32 | Implemented |
| Median (Legacy) | `ec.noise.medianlegacy` | 2 | | 32 | Implemented |
| Noise | `ec.noise.noise` | 3 | | 32 | Implemented |
| Noise Alpha | `ec.noise.noisealpha` | 8 | | 32 | Implemented |
| Noise HLS | `ec.noise.noisehls` | 6 | | 32 | Implemented |
| Noise HLS Auto | `ec.noise.noisehlsauto` | 6 | | 32 | Implemented |
| Remove Grain | `ec.noise.removegrain` | 7 | | 32 | Partial: no preview region, sampling or temporal filtering |
| Turbulent Noise | `ec.noise.turbulent` | 22 | | 32 | Implemented |
## Obsolete
| Effect | Id | Params | GPU | 32 | Status |
|---|---|---:|:-:|:-:|---|
| Basic 3D | `ec.obsolete.basic3d` | 5 | | 32 | Implemented |
| Basic Text | `ec.obsolete.basictext` | 11 | | 32 | Implemented |
| Color Key | `ec.key.colorkey` | 4 | | 32 | Implemented |
| Gaussian Blur (Legacy) | `ec.obsolete.gaussianlegacy` | 2 | | 32 | Implemented |
| Lightning | `ec.obsolete.lightning` | 25 | | 32 | Implemented |
| Luma Key | `ec.key.luma` | 5 | | 32 | Implemented |
| Path Text | `ec.obsolete.pathtext` | 21 | | 32 | Partial: no kerning, shear/scale, line spacing, fade time or jitter |
| Reduce Interlace Flicker | `ec.blur.reduceflicker` | 1 | | 32 | Implemented |
| Spill Suppressor | `ec.key.spill` | 3 | | 32 | Implemented |
## Paint
| Effect | Id | Params | GPU | 32 | Status |
|---|---|---:|:-:|:-:|---|
| Paint | `ec.paint.paint` | 1 | | 32 | Implemented |
## Perspective
| Effect | Id | Params | GPU | 32 | Status |
|---|---|---:|:-:|:-:|---|
| 3D Camera Tracker | `ec.perspective.cameratracker` | 10 | | 32 | Implemented |
| 3D Glasses | `ec.perspective.3dglasses` | 8 | | 32 | Implemented |
| Bevel Alpha | `ec.perspective.bevelalpha` | 4 | | 32 | Implemented |
| Bevel Edges | `ec.perspective.beveledges` | 4 | | 32 | Implemented |
| CC Cylinder | `ec.perspective.cccylinder` | 12 | | 32 | Implemented |
| CC Environment | `ec.perspective.ccenvironment` | 4 | | 32 | Implemented |
| CC Sphere | `ec.perspective.ccsphere` | 14 | | 32 | Implemented |
| CC Spotlight | `ec.perspective.ccspotlight` | 8 | | 32 | Implemented |
| Drop Shadow | `ec.perspective.dropshadow` | 6 | GPU | 32 | Implemented |
| Radial Shadow | `ec.perspective.radialshadow` | 9 | | 32 | Implemented |
## Simulation
| Effect | Id | Params | GPU | 32 | Status |
|---|---|---:|:-:|:-:|---|
| CC Ball Action | `ec.sim.ccballaction` | 8 | | 32 | Implemented |
| CC Bubbles | `ec.sim.ccbubbles` | 7 | | 32 | Implemented |
| CC Drizzle | `ec.sim.ccdrizzle` | 14 | | 32 | Implemented |
| CC Hair | `ec.sim.cchair` | 20 | | 32 | Implemented |
| CC Mr. Mercury | `ec.sim.ccmrmercury` | 23 | | 32 | Implemented |
| CC Particle Systems II | `ec.sim.ccparticlesystems2` | 21 | | 32 | Implemented |
| CC Particle World | `ec.sim.ccparticleworld` | 30 | | 32 | Implemented |
| CC Pixel Polly | `ec.sim.ccpixelpolly` | 10 | | 32 | Implemented |
| CC Rainfall | `ec.sim.ccrainfall` | 14 | | 32 | Implemented |
| CC Scatterize | `ec.sim.ccscatterize` | 4 | | 32 | Implemented |
| CC Snowfall | `ec.sim.ccsnowfall` | 19 | | 32 | Implemented |
| CC Star Burst | `ec.sim.ccstarburst` | 6 | | 32 | Implemented |
| Card Dance | `ec.sim.carddance` | 35 | | 32 | Partial: no camera system, corner pins or lighting/material controls |
| Caustics | `ec.sim.caustics` | 21 | | 32 | Partial: no Sky group or light type |
| Foam | `ec.sim.foam` | 24 | | 32 | Partial: no wobble, pop velocity, custom bubble texture, environment map or flow map |
| Particle Playground | `ec.sim.particleplayground` | 37 | | 32 | Partial: no Particle Exploder, Ephemeral Property Mapper, Affects groups or text particles |
| Shatter | `ec.sim.shatter` | 26 | | 32 | Partial: no custom shatter map, gradient, textures or extrusion rendering |
| Wave World | `ec.sim.waveworld` | 27 | | 32 | Partial: no wireframe controls, dry-area rendering or ground group |
## Stylize
| Effect | Id | Params | GPU | 32 | Status |
|---|---|---:|:-:|:-:|---|
| Brush Strokes | `ec.stylize.brushstrokes` | 7 | | 32 | Implemented |
| CC Block Load | `ec.stylize.ccblockload` | 5 | | 32 | Implemented |
| CC Burn Film | `ec.stylize.ccburnfilm` | 3 | | 32 | Implemented |
| CC Glass | `ec.stylize.ccglass` | 17 | | 32 | Implemented |
| CC HexTile | `ec.stylize.cchextile` | 5 | | 32 | Implemented |
| CC Kaleida | `ec.stylize.cckaleida` | 4 | | 32 | Implemented |
| CC Mr. Smoothie | `ec.stylize.ccmrsmoothie` | 7 | | 32 | Implemented |
| CC Plastic | `ec.stylize.ccplastic` | 18 | | 32 | Implemented |
| CC RepeTile | `ec.stylize.ccrepetile` | 5 | | 32 | Implemented |
| CC Threshold | `ec.stylize.ccthreshold` | 4 | | 32 | Implemented |
| CC Threshold RGB | `ec.stylize.ccthresholdrgb` | 7 | | 32 | Implemented |
| CC Vignette | `ec.stylize.ccvignette` | 4 | | 32 | Implemented |
| Cartoon | `ec.stylize.cartoon` | 11 | | 32 | Partial: no Edge Enhancement |
| Color Emboss | `ec.stylize.coloremboss` | 4 | | 32 | Implemented |
| Emboss | `ec.stylize.emboss` | 4 | | 32 | Implemented |
| Find Edges | `ec.stylize.findedges` | 2 | | 32 | Implemented |
| Glow | `ec.stylize.glow` | 13 | GPU | 32 | Partial: no Glow Operation or arbitrary colour map |
| Mosaic | `ec.stylize.mosaic` | 3 | | 32 | Implemented |
| Motion Tile | `ec.stylize.motiontile` | 8 | | 32 | Implemented |
| Posterize | `ec.stylize.posterize` | 1 | | 32 | Implemented |
| Roughen Edges | `ec.stylize.roughenedges` | 13 | | 32 | Implemented |
| Scatter | `ec.stylize.scatter` | 3 | | 32 | Implemented |
| Strobe Light | `ec.stylize.strobe` | 7 | | 32 | Implemented |
| Texturize | `ec.stylize.texturize` | 4 | | 32 | Implemented |
| Threshold | `ec.stylize.threshold` | 1 | | 32 | Implemented |
## Text
| Effect | Id | Params | GPU | 32 | Status |
|---|---|---:|:-:|:-:|---|
| Numbers | `ec.text.numbers` | 14 | | 32 | Implemented |
| Timecode | `ec.text.timecode` | 12 | | 32 | Implemented |
## Time
| Effect | Id | Params | GPU | 32 | Status |
|---|---|---:|:-:|:-:|---|
| CC Force Motion Blur | `ec.time.ccforcemotionblur` | 4 | | 32 | Implemented |
| CC Wide Time | `ec.time.ccwidetime` | 3 | | 32 | Implemented |
| Echo | `ec.time.echo` | 5 | | 32 | Implemented |
| Pixel Motion Blur | `ec.time.pixelmotionblur` | 4 | | 32 | Implemented |
| Posterize Time | `ec.time.posterizetime` | 1 | | 32 | Implemented |
| Time Difference | `ec.time.timedifference` | 5 | | 32 | Implemented |
| Time Displacement | `ec.time.timedisplacement` | 4 | | 32 | Implemented |
| Timewarp | `ec.time.timewarp` | 10 | | 32 | Partial: Pixel Motion blends frames (no optical flow); no matte, warp or crop controls |
## Transition
| Effect | Id | Params | GPU | 32 | Status |
|---|---|---:|:-:|:-:|---|
| Block Dissolve | `ec.transition.blockdissolve` | 4 | | 32 | Partial: no Soft Edges option |
| CC Glass Wipe | `ec.transition.ccglasswipe` | 5 | | 32 | Implemented |
| CC Grid Wipe | `ec.transition.ccgridwipe` | 7 | | 32 | Implemented |
| CC Image Wipe | `ec.transition.ccimagewipe` | 7 | | 32 | Implemented |
| CC Jaws | `ec.transition.ccjaws` | 6 | | 32 | Implemented |
| CC Light Wipe | `ec.transition.cclightwipe` | 8 | | 32 | Implemented |
| CC Line Sweep | `ec.transition.cclinesweep` | 5 | | 32 | Implemented |
| CC Radial ScaleWipe | `ec.transition.ccradialscalewipe` | 3 | | 32 | Implemented |
| CC Scale Wipe | `ec.transition.ccscalewipe` | 3 | | 32 | Implemented |
| CC Twister | `ec.transition.cctwister` | 5 | | 32 | Implemented |
| CC WarpoMatic | `ec.transition.ccwarpomatic` | 8 | | 32 | Implemented |
| Card Wipe | `ec.transition.cardwipe` | 12 | | 32 | Partial: 2D flip only; no back layer picker, camera, lighting or jitter |
| Gradient Wipe | `ec.transition.gradientwipe` | 5 | | 32 | Implemented |
| Iris Wipe | `ec.transition.iriswipe` | 7 | | 32 | Implemented |
| Linear Wipe | `ec.transition.linearwipe` | 3 | | 32 | Implemented |
| Radial Wipe | `ec.transition.radialwipe` | 5 | | 32 | Implemented |
| Venetian Blinds | `ec.transition.venetian` | 4 | | 32 | Implemented |
## Utility
| Effect | Id | Params | GPU | 32 | Status |
|---|---|---:|:-:|:-:|---|
| Apply Color LUT | `ec.utility.applylut` | 0 | | 32 | Implemented |
| CC Overbrights | `ec.utility.ccoverbrights` | 2 | | 32 | Implemented |
| Cineon Converter | `ec.utility.cineon` | 7 | | 32 | Implemented |
| Color Profile Converter | `ec.utility.colorprofileconverter` | 7 | | 32 | Implemented |
| Grow Bounds | `ec.utility.growbounds` | 1 | | 32 | Implemented |
| HDR Compander | `ec.utility.hdrcompander` | 3 | | 32 | Implemented |
| HDR Highlight Compression | `ec.utility.hdrcompression` | 1 | | 32 | Implemented |
+28 -10
View File
@@ -114,22 +114,25 @@ the disk cache in the formats wave; the 3D Camera Tracker in M12.6.)
| Audio | 85% | 0.5 | audio to keyframes |
| Import | 88% | 2.0 | AI/EPS/PDF vector footage, PSD smart objects and 3D layers (PSD as footage/composition/retain layer sizes, SVG footage and Create Shapes from Vector Layer landed) |
| Automation | ≈ 90% | 0.8 | scripting covers the documented core object model (AUT-2, M14.4: `app`, project items, comps, layers, properties and keyframes, text documents, markers, render queue, Script Console, `effectcraft-cli script`, MCP `run_script`); still missing: ScriptUI panels/dialogs, `.jsxbin`, sockets |
| Shapes | 68% | 3.9 | taper and wave strokes (pen tool for shape paths and vertex editing landed in M6.5) |
| Shapes | ≈ 80% | 1.5 | Lottie can't carry stroke taper/wave (stroke Taper and Wave, Dash 2/Gap 2/Dash 3/Gap 3 and radial-gradient Highlight Length/Angle landed in M13.5; pen tool for shape paths and vertex editing in M6.5) |
| Compositions | ≈ 80% | 3.5 | Mocha-style planar tracks for templates, Essential Graphics' rare controls (font menus, mirrored properties) (the marker dialog, Composition Flowchart, Essential Graphics with master properties, `.ectemplate` templates and Responsive Design — Time landed: CMP-6, CMP-7) |
| Animation | 65% | 10.4 | puppet, Wiggler/Smoother/Motion Sketch (motion-path handles and the graph editor transform box landed in M5.8) |
| Text | ≈ 86% | 1.3 | vertical type is basic (upright characters, no tate-chu-yoko), no OpenType feature panel, no extruded strokes (extruded, bevelled text landed in M7.6); per-character styles, paragraph settings, on-canvas editing and the `sourceText` style API landed (M9.9–M9.10) |
| Web | 60% | 2.5 | threads, browser storage, audio |
| 3D | 84% | 7.0 | multi-view layouts, collapsed 3D precomps in Advanced 3D, the Extended Viewer; stereo rigs, orbit nulls, lights controlled by the camera, cameras/lights from glTF models, environment backgrounds, Advanced 3D motion blur, blend modes and track mattes landed in M7.7; Advanced 3D (glTF/OBJ models, primitives, extruded text and shapes, PBR, image-based light, shadow maps, GPU rasteriser) landed in M7.4–M7.6 |
| Effects | ≈ 75% | 5.0 | GPU versions of more effects (every After Effects effect exists since M9.11, M12.5 and M12.6) |
| Interface | 70% | 6.0 | Timeline horizontal scrolling of many columns, a richer Learn area (native macOS menu bar, Timeline columns/search/reveal-add, Home screen with recent projects and all AE workspaces landed; viewer rulers/snapping/channels/snapshots landed in M0.13) |
| Animation | 67% | 10.0 | puppet, Wiggler/Smoother/Motion Sketch (motion-path handles and the graph editor transform box landed in M5.8; keyframe colour labels and Select Keyframe Label Group, Graph Editor snapping to markers / layer ends in M13.5) |
| Text | ≈ 91% | 0.8 | no OpenType feature panel, no extruded strokes, variable-axis animation changes outlines but not advances (vertical Roman / Tate-Chu-Yoko, forced LTR paragraphs, caret on animated and path text, Variable Font Axes and Lottie style runs landed in M13.5; extruded, bevelled text in M7.6; per-character styles, paragraph settings, on-canvas editing and the `sourceText` style API in M9.9–M9.10) |
| Web | 85% | 1.0 | viewer frames and Roto Brush propagation still on the page's thread; GPU effects in the browser (browser storage, Web Audio, Web Worker renders/analyses, WebGPU viewer and offline install landed in M15.2) |
| 3D | 85% | 6.5 | multi-view layouts, iris shapes in Advanced 3D's depth of field, collapsed 3D precomps in Advanced 3D, the Extended Viewer; stereo rigs, orbit nulls, lights controlled by the camera, cameras/lights from glTF models, environment backgrounds, Advanced 3D motion blur, blend modes and track mattes landed in M7.7; Classic 3D iris-shaped bokeh with highlights, progressive depth of field on tilted layers and the focus-link commands landed in M13.5; Advanced 3D (glTF/OBJ models, primitives, extruded text and shapes, PBR, image-based light, shadow maps, GPU rasteriser) in M7.4–M7.6 |
| Effects | ≈ 80% | 4.5 | GPU versions of more effects and the missing controls listed as partial in [effects.md](effects.md) (every After Effects effect exists since M9.11, M12.5 and M12.6; parameter names, order, twirl-downs, popups, units and defaults were aligned in M9.12) |
| Interface | 74% | 5.0 | a richer Learn area (Timeline outline and Project panel columns scroll horizontally, the Layer Style dialog, ROI resize handles, Pan Behind snapping and 3D Reference Axes landed in M13.5; native macOS menu bar, Timeline columns/search/reveal-add, Home screen with recent projects and all AE workspaces landed; viewer rulers/snapping/channels/snapshots landed in M0.13) |
| Project | ≈ 68% | 5.5 | auto-save, folder moves, OCIO displays beyond the built-in tone map (Color Engine with OCIO/ACES working spaces, HDR compand/tone mapping, Rec. 2100 PQ/HLG output, Feet + Frames, display colour management, Simulate Output and the locked viewer landed in M7.7; proxies and Interpret Footage fields / pixel aspect / alpha guess landed: PRJ-8, PRJ-3) |
| Masks & roto | 68% | 6.0 | variable-width mask feather points, Roto Brush's learned (3.0) segmentation model (mask tracking and Mask Interpolation landed in M6.6; Roto Brush & Refine Edge with graph-cut segmentation, flow propagation, edge matting, decontamination and Freeze in M6.7) |
| Masks & roto | 74% | 5.0 | Roto Brush's learned (3.0) segmentation model (variable-width mask feather points with the Mask Feather tool landed in M13.5; mask tracking and Mask Interpolation landed in M6.6; Roto Brush & Refine Edge with graph-cut segmentation, flow propagation, edge matting, decontamination and Freeze in M6.7) |
| Preview | 62% | 5.0 | GPU 3D and adjustment layers (persistent disk cache with the blue cache bar landed; region of interest, snapshots, exposure and Fast Previews landed in M0.13) |
| Tracking | ≈ 85% | 2.5 | face tracking, Subspace Warp's mesh warp, lens distortion in the camera solve (Rolling Shutter Repair landed in M9.11; point tracker, mask tracking, Warp Stabilizer and the 3D Camera Tracker landed in M6.x / M12.5 / M12.6) |
| Paint | 0% | 4.0 | Brush, Clone Stamp, Eraser |
## Effects still missing
The full catalogue with GPU / 32-bpc badges and per-effect status is [effects.md](effects.md)
(generated from the registry).
None: the 3D Camera Tracker landed in M12.6. Boris FX
Mocha and Cineware are third-party and not counted.
@@ -138,7 +141,7 @@ Mocha and Cineware are third-party and not counted.
| Category | Effects | Notes |
|---|---|---|
| 3D Channel | 3D Channel Extract, Cryptomatte, Depth Matte, Depth of Field, EXtractoR, Fog 3D, ID Matte, IDentifier | Read auxiliary channels: multi-layer OpenEXR (any `layer.channel`, Cryptomatte manifests from the header) or the compositor's own depth / layer-ID / normal / UV / Cryptomatte pass for precomps of 3D comps. |
| Immersive Video | VR Blur, VR Chromatic Aberrations, VR Color Gradients, VR Converter, VR De-Noise, VR Digital Glitch, VR Fractal Noise, VR Glow, VR Plane to Sphere, VR Rotate Sphere, VR Sharpen, VR Sphere To Plane | Seam-aware equirectangular processing (wrap-around, pole continuation, latitude-scaled filters), mono / over-under / side-by-side layouts; VR Converter between equirectangular, cube maps (4:3, 3:2, 6:1), sphere map and 2D. |
| Immersive Video | VR Blur, VR Chromatic Aberrations, VR Color Gradients, VR Converter, VR De-Noise, VR Digital Glitch, VR Fractal Noise, VR Glow, VR Plane to Sphere, VR Rotate Sphere, VR Sharpen, VR Sphere to Plane | Seam-aware equirectangular processing (wrap-around, pole continuation, latitude-scaled filters), mono / over-under / side-by-side layouts; VR Converter between equirectangular, cube maps (4:3, 3:2, 6:1), sphere map and 2D. |
| Color Correction | OCIO CDL / Color Space / Display / File / Look Transform, Color Stabilizer | Built-in minimal OCIO-style config (ACES2065-1, ACEScg, ACEScct, sRGB, Rec.709, Rec.2020, Display P3, linear variants, XYZ) from published matrices and transfer functions; `.cube` / `.3dl` / `.csp` LUTs and ASC `.cc` / `.ccc` / `.cdl`. Custom `.ocio` config files are read (Configuration ▸ Custom: colorspaces, roles, displays/views; Matrix, File, Exponent, Log, LogAffine, CDL, Range and Group transforms). |
| Distort | CC Flo Motion, Liquify, Rolling Shutter Repair | Liquify strokes are effect data replayed into a displacement mesh (`liquify.stroke` / `liquify.clear` commands; no interactive viewer brush yet). Rolling Shutter Repair uses the KLT tracker. |
| Blur & Sharpen | Camera-Shake Deblur, CC Radial Blur | Deblur substitutes aligned patches from sharper neighbouring frames. |
@@ -146,7 +149,22 @@ Mocha and Cineware are third-party and not counted.
| Simulation | CC Hair, Particle Playground | Particle Playground: cannon, grid, layer exploder, layer map, gravity, repel, wall, persistent property mapper (no Particle Exploder, text particles or ephemeral mapper yet). |
| Keying | Key Light | The full Keylight 1.2 control set under a generic name ("Keylight" is a vendor trademark); `lookup("Keylight (1.2)")` finds it. |
| Utility | Color Profile Converter | Our colour spaces and ACES; rendering intents (perceptual gamut compression, relative / absolute colorimetric, saturation). |
| Obsolete | mocha shape | Mocha's export format is not public: reads a documented JSON shape format instead. |
| Matte | Mocha shape | Mocha's export format is not public: reads a documented JSON shape format instead. |
## Update: M13.5 long-tail polish
Stroke Taper / Wave / multi-segment dashes and the radial gradient highlight (SHP-2);
variable-width mask feather points (MSK-1; mask motion blur verified by tests); the Adaptive
Sample Limit driving per-layer motion-blur samples (LYR-10); Classic 3D depth of field with
the camera's iris shape, rotation, roundness, aspect ratio, diffraction fringe and highlights,
progressive blur on tilted layers, and Link Focus Distance to Point of Interest / to Layer and
Set Focus Distance to Layer (3D-1); Timeline outline and Project panel column scrolling;
keyframe colour labels and Select Keyframe Label Group (ANM-1); vertical Roman text,
Tate-Chu-Yoko, forced LTR paragraphs, the editing caret on animated / path text and Lottie
style runs; the Layer Style dialog (LYR-9); Graph Editor snapping to markers and layer ends
(ANM-4; the reference graph existed); viewer ROI resize handles, Pan Behind snapping and 3D
Reference Axes; Animate Text ▸ Variable Font Axes. Remaining in these rows: Lottie can't carry
taper/wave, Advanced 3D's DOF has no iris shapes, and variable axes don't change advances.
## Highest-value gaps, in order
+138 -30
View File
@@ -2,9 +2,10 @@
`apps/effectcraft-web` runs the same engine and egui UI as the desktop app in the browser: the
workspace compiled to `wasm32-unknown-unknown`, started by eframe's web runner on WebGPU (WebGL2
fallback). It is a static site: one `.wasm`, its `wasm-bindgen` JavaScript glue, `index.html` and an
icon. Nothing is uploaded anywhere; projects, media and renders stay in the page and on the user's
machine.
fallback). It is a static site: one `.wasm`, its `wasm-bindgen` JavaScript glue, `index.html`, a
few small scripts (worker, audio worklet, service worker), a web manifest and icons. Nothing is
uploaded anywhere: projects, media, settings and renders stay in the browser (its private storage)
and on the user's machine.
## Build and run
@@ -15,48 +16,136 @@ machine.
| wasm-opt (optional) | any recent binaryen | `brew install binaryen`; used by `cargo xtask web` when found |
```sh
cargo xtask web # release build → <target>/web/dist (index.html, effectcraft_web.js, effectcraft_web_bg.wasm, favicon.svg)
cargo xtask web # release build → <target>/web/dist
cargo xtask web --dev # unoptimised build (faster to compile, slow to run)
cargo xtask web --serve 8765 # build, then serve dist on http://127.0.0.1:8765/
```
`<target>` is `$CARGO_TARGET_DIR` or `target`. `--serve` is a tiny localhost-only static server that
sends `Cross-Origin-Opener-Policy` / `Cross-Origin-Embedder-Policy`, so the page is cross-origin
`<target>` is `$CARGO_TARGET_DIR` or `target`. `dist` holds `index.html`, `effectcraft_web.js` +
`effectcraft_web_bg.wasm` (+ `snippets/`, the browser glue `js/host.js`), `worker.js`,
`audio-worklet.js`, `sw.js` (its precache list and version filled in by the build),
`manifest.webmanifest`, `favicon.svg` and `icon-256.png` / `icon-512.png` (copied from
`assets/app-icon`). `--serve` is a tiny localhost-only static server that sends
`Cross-Origin-Opener-Policy` / `Cross-Origin-Embedder-Policy`, so the page is cross-origin
isolated like a production deployment should be. Any static server works
(`python3 -m http.server -d target/web/dist 8765`); it must serve `.wasm` as `application/wasm`.
Service workers need a secure context: `https://`, or `http://localhost` / `127.0.0.1`.
The release `.wasm` is about 31 MB before `wasm-opt` (20 MB code: the effects, codecs, the
expression engine and the UI; 8.5 MB data, mostly the bundled fonts; 2 MB function names for
readable panics). Served compressed it is a fraction of that.
The release `.wasm` is about 38 MB before `wasm-opt` (code: the effects, codecs, the expression
engine and the UI; data, mostly the bundled fonts; function names for readable panics). Served
compressed it is a fraction of that, and the service worker caches it after the first visit.
The web crate is empty on non-wasm targets, so `cargo test --workspace` and clippy are unaffected;
`cargo xtask wasm` (part of `cargo xtask ci`) checks it, with every L0–L4 crate and
`effectcraft-ui-egui`, for `wasm32-unknown-unknown`.
On other targets the web crate holds only its portable storage model (`store.rs`, unit-tested by
`cargo test --workspace`); `cargo xtask wasm` (part of `cargo xtask ci`) checks the whole crate,
with every L0–L4 crate and `effectcraft-ui-egui`, for `wasm32-unknown-unknown`.
URL flags: `?empty` (start without the demo project), `?home` (show the start screen).
URL flags: `?empty` (a blank project; the session is not restored or recorded), `?demo` (the demo
project instead of the last session), `?home` (show the start screen), `?storage=indexeddb` /
`?storage=memory` (force a storage backend), `?noworkers` (render and analyse on the page's
thread), `?nosw` (don't register the service worker).
## How the desktop pieces map to the browser
| Desktop | Web |
|---|---|
| frame render thread pool (`ui-egui/src/frames.rs`) | `Frames::pump`: queued frames render on the UI thread after each egui frame, most important first (the viewer's frame, then prefetch), within 40 ms (24 ms while playing) |
| Render Queue on a background thread | `Session::start_render` always runs inline on wasm32: the page waits until the render is done |
| Render Queue on a background thread | a Web Worker running a second engine instance (below); progress streams back, the page never waits |
| tracker / mask tracker / Warp Stabilizer / 3D Camera Tracker / Roto Brush Freeze threads | the same workers; the analysed property group comes back as one undo step |
| `rayon` parallel loops (raster, effects, export batches) | rayon's global pool falls back to the calling thread when threads are unavailable: same code, serial |
| `std::time::Instant` / `SystemTime` (panic on wasm32) | `web-time` (re-exports `std::time` on native) |
| `std::fs` project reads/writes (`FsServices`) | `files::WebServices`: an in-memory file table; saving downloads the `.ecproj` |
| `std::fs` project reads/writes (`FsServices`) | `files::WebServices`: the virtual file table, persisted (below); saving also downloads the `.ecproj` |
| config directory: settings, shortcut presets, recent projects, the recovery sentinel | `store::WebConfig` in browser storage |
| auto-save folder | `/EffectCraft Auto-Save/` in browser storage (`ConfigStore::files`) |
| media reads (`MediaPool`, `probe`) | `files::WebImporter`: bytes from the file table, handed to `MediaPool::add_bytes` / `probe_bytes` |
| export writes (`effectcraft-export`) | the job's `sink` (`effectcraft_host::FileExporter { sink }`): files land in the table and download after the render; several files (an image sequence) download as one stored `.zip` |
| rfd file dialogs | `<input type=file>` for File ▸ Open / Import; drop files anywhere on the page (`.ecproj` opens, everything else imports); "Save As" / "Output To" pick a download name |
| system fonts | not scanned; the bundled fonts (Inter, Noto Serif, JetBrains Mono) are always there |
| TCP control channel / MCP | `window.effectcraft` (below) |
| audio output | none yet (if `cpal` lands for desktop playback it must stay behind a cfg/feature off wasm32; a WebAudio backend would replace it) |
| cpal audio output | Web Audio (below) |
| GPU compositor on the desktop's wgpu device | the same compositor on eframe's WebGPU device (below) |
### Threads
### Persistence
wasm threads need a cross-origin isolated page **and** a wasm build with atomics (a nightly
`build-std` toolchain), so this build is single-threaded: frames, effects and exports run on the UI
thread. A long Render Queue job freezes the page until it finishes; keep web renders short or
small (resolution, time span).
Everything that lives in files on the desktop lives in the browser's **Origin Private File
System** (OPFS); where OPFS can't be written from the page (older Safari), **IndexedDB**; with
neither (some private windows), memory only. `effectcraft.info().storage.backend` says which.
The page asks for persistent storage (`navigator.storage.persist()`), which browsers grant to
installed apps, so the store isn't evicted under storage pressure.
The engine's storage traits are synchronous and the browser's are not, so the app works on an
in-memory mirror (`store.rs`): every stored entry is read before the app starts, reads and writes
hit the mirror, and changes flush in the background (one write per changed key, in order; OPFS
writes are atomic: a swap file replaced on close). Keys:
| Key | What |
|---|---|
| `config/prefs.json`, `config/shortcuts.json` | Settings (with File ▸ Open Recent's list) and keyboard shortcut presets |
| `config/session.lock` | the crash-recovery sentinel |
| `config/session.ecproj`, `config/session.json` | the session snapshot: the open project and editor state (active comp, time, selection), written at most once a second while they change |
| `files/<path>` | the file table: imported media, projects saved with File ▸ Save / Save As or `saveToBrowser`, auto-saves |
On the next visit the app reopens the snapshot (unsaved changes stay unsaved: the project is
dirty), registers every stored media file with the media pool so its footage decodes, and File ▸
Open Recent reopens saved projects from storage. Without a snapshot (first visit) it opens the
demo project. Render outputs are not stored: they download.
### Background jobs: Web Workers
wasm threads need shared memory, which needs a nightly `build-std` toolchain, so this build is
single-threaded per instance. Long jobs run in **Web Workers** instead, each with its own instance
of the same module (the page compiles it once and posts the `WebAssembly.Module`, so nothing is
compiled twice) and its own memory (`crates/engine/src/offload.rs`, `src/worker.rs`):
1. The session's `Offload` serializes a `WorkerRequest`: the project JSON, the footage paths it
reads, and the job (`render` with the resolved queue items and output paths; `warp`, `camera`,
`track`, `maskTrack`, `rotoFreeze` with the target and analysis parameters).
2. `js/host.js` takes an idle worker from its pool (or starts one), sends it the footage bytes it
doesn't have yet, then the request.
3. The worker runs the job on a plain engine session, blocking, and posts `WorkerReply`s: render
progress (at most every 100 ms), item started/finished, rendered files (transferred), analysis
progress, the analysed property group, `failed`, `done`.
4. The page applies them every frame: `Session::poll_render` updates the queue items and the
progress bar, `Session::poll_offload` writes an analysis result as one undo step (the effect /
tracker / mask group is replaced; other edits made meanwhile are kept) and the downloads start.
Stopping a job terminates its worker (a worker can't be interrupted mid-frame): the item being
rendered becomes "User Stopped", an analysis writes nothing. `renderQueue.render {wait: true}` (the
default for agents) and analysis commands with `wait: true` still run on the page's thread and
return when done. Roto Brush propagation fills the session's segmentation cache rather than the
project, so it runs on the page's thread too, as do viewer frames (`Frames::pump`).
### Audio
Audio preview goes through **Web Audio**: an AudioWorklet (`audio-worklet.js`; a
ScriptProcessorNode where worklets are unavailable) plays interleaved stereo blocks. The mixdown
is the desktop's (`AudioPlayback`, `mix_comp`): without a feeder thread, `AudioPlayback::pump`
mixes ahead on every UI frame (≈ 350 ms queued) and the output device takes what keeps it
≈ 150 ms ahead of the playhead. A/V sync follows `AudioContext.currentTime`: the playback clock
is the frames played by the context clock minus its output latency, and the viewer shows the
frame under the audible sample, as on the desktop. The Audio panel's meters read the same feed.
Browsers start an AudioContext suspended until the user interacts with the page; the app resumes
it on the first pointer or key event, so audio plays from the first preview after any click or
key press.
### GPU
eframe starts on WebGPU where the browser has it (WebGL2 otherwise), and the GPU compositor
(`effectcraft-gpu`) runs on the same device: viewer frames are composited by compute shaders and
drawn straight from their texture, with no readback. Steps that need the CPU mid-render (3D runs,
adjustment layers, CPU-only effects) fall back to the CPU renderer for that frame, because the
page's thread can't wait for a GPU readback. The Info panel's pixel readout and the eyedroppers
read GPU frames back asynchronously (`Gpu::read_display_async`: the pixels arrive a frame
later). On WebGL2, or without a usable adapter, everything renders on the CPU.
`effectcraft.info().gpu` reports `{compositor, viewerOnGpu}`.
### Offline and install (PWA)
`manifest.webmanifest` (name, icons, standalone display) makes the app installable; `sw.js`
precaches every file of the build into a cache named after the build's hash, serves them
cache-first (the cached responses keep the server's COOP/COEP headers, so the page stays
cross-origin isolated offline), and drops older caches when a new build activates. After the
first visit the app loads without a network.
## `window.effectcraft`: the agent / test API
@@ -70,34 +159,53 @@ with the method's `result`, errors reject with the message.
| `effectcraft.commands()` / `effectcraft.inspect()` | `engine.commands` / `ui.inspect` |
| `effectcraft.renderFrame(params)` | `render.frame` with `base64: true`: `{comp, time, width, height, png}` |
| `effectcraft.screenshot(params)` | `ui.screenshot`; without `path` the PNG comes back inline as `png` (base64) |
| `effectcraft.addFile(fileOrUrl, name?)` | put a `File`/`Blob` (or fetched URL) into the file table and open (`.ecproj`) or import it; resolves with its path |
| `effectcraft.addFile(fileOrUrl, name?)` | put a `File`/`Blob` (or fetched URL) into the file table (stored) and open (`.ecproj`) or import it; resolves with its path |
| `effectcraft.files()` / `effectcraft.readFile(path)` | the file table `[{path, size}]` / a file's bytes (`Uint8Array`), e.g. a render |
| `effectcraft.info()` | graphics backend, version, `crossOriginIsolated`, load timings |
| `effectcraft.saveToBrowser(path?)` | save the project to browser storage without downloading it (default: its path, or `/<name>.ecproj`); resolves with `{path, bytes}` |
| `effectcraft.listStored()` | `{backend, usage, quota, persisted, pending, files: [{path, size, modified}], config: [name]}` |
| `effectcraft.removeStored(path)` | delete a stored file |
| `effectcraft.flush()` | resolves once every change is written to browser storage |
| `effectcraft.info()` | graphics backend, `gpu`, `storage`, `audio` (`{state, sampleRate, backend, posted, played, underruns}`), `workers`, `restored`, `webgpu`, `serviceWorker`, version, `crossOriginIsolated`, load timings |
`window.effectcraftLoad` holds `{wasmMs, readyMs}` (module fetch + compile, and until the app runs).
```js
await effectcraft.execute("layer.newSolid", {color: "#ff8800"});
await effectcraft.request("ui.menu.invoke", {id: "renderQueue.add", params: {format: "gif", resolution: 0.25}});
await effectcraft.execute("renderQueue.render", {}); // the GIF downloads
await effectcraft.execute("renderQueue.render", {wait: false}); // renders in a worker; the GIF downloads
await effectcraft.saveToBrowser("/my-project.ecproj"); // File ▸ Open Recent has it after a reload
```
## Browser test
`apps/effectcraft-web/tests/smoke.mjs` drives headless Chrome over the DevTools protocol (Node ≥ 22,
no npm packages): load, the demo comp in the viewer, `render.frame`, Render Queue GIF and PNG
sequence (`.zip`) downloads, Save As and re-opening the saved project. It writes screenshots and
`report.json`:
no npm packages): load, the GPU path (WebGPU → GPU compositor, viewer on the GPU), the demo comp
in the viewer, `render.frame`, Render Queue GIF and PNG sequence (`.zip`) downloads, a background
render in a worker while measuring the page's event-loop gaps (must stay under 400 ms) and its
progress, the AudioContext starting on a (simulated) key press and a tone playing through it
with the meters moving, Save As and re-opening the saved project, persistence across a reload
(session snapshot, a stored project, imported media, a setting, Open Recent) and an offline
reload through the service worker. It writes screenshots and `report.json`:
```sh
cargo xtask web --serve 8765 &
node apps/effectcraft-web/tests/smoke.mjs --url http://127.0.0.1:8765/ --out target/web/smoke
```
Native unit tests cover the storage model (`apps/effectcraft-web/src/store.rs`: write
coalescing, file table, `ConfigStore` / `FileOps`, auto-save and crash recovery through the
browser store) and the worker protocol (`crates/engine/src/offload.rs`: serde round trips of
every request and reply, a render through an in-process offload).
## Gaps
- Single-threaded (above); Render Queue renders block the page.
- No audio playback.
- Imported media live in memory for the session only (nothing is persisted; reload = fresh start).
- Viewer frames, RAM preview and Roto Brush propagation still render on the page's thread (one
frame at a time between UI frames).
- GPU effects run only in the desktop GPU path: in the browser, effects run on the CPU (inside
workers for renders) because the page's thread can't wait for mid-render readbacks.
- No disk cache (Settings ▸ Media & Disk Cache) in the browser: the layer cache stays in memory.
- Cancelling an analysis drops its partial result (the worker is terminated).
- Stored media count against the origin's storage quota; `effectcraft.listStored()` shows usage,
`removeStored` frees it (there is no storage manager in the UI yet).
- `engine.execute` rejects the spread Render Settings / Output Module keys of `renderQueue.add`
(`resolution`, `timeSpan`…) through its strict parameter check; use `ui.menu.invoke` as above.