Files
effectcraft/docs/web.md
T
Brandon ThomasandClaude Opus 5.5 31f192e2a9 M15.1: web build (eframe wasm) + xtask web
- apps/effectcraft-web (L6): eframe web runner booting the shared EffectcraftApp with a browser
  session: in-memory file table, <input type=file> pickers and drag-and-drop for open/import,
  downloads for Save and Render Queue outputs (several files as one stored .zip), and the
  window.effectcraft promise API over the control channel. Demo project on start.
- wasm32 runtime fixes: web-time for Instant/SystemTime (render, engine, export, ui-egui);
  Frames renders queued jobs on the UI thread (Frames::pump) instead of a thread pool;
  Render Queue runs inline; relative outputs resolve under / ; control PNGs inline.
- effectcraft-export: per-job output Sink (files to memory instead of disk); host FileExporter
  carries it; test that sink exports touch no file.
- xtask web [--dev] [--serve PORT] (wasm-bindgen 0.2.129, optional wasm-opt, COOP/COEP server);
  effectcraft-web in WEB_CRATES. docs/web.md, headless Chrome smoke test, favicon (+attribution).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-02 00:19:35 -04:00

104 lines
6.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# EffectCraft on the web
`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.
## Build and run
| Tool | Version | Install |
|---|---|---|
| Rust target | `wasm32-unknown-unknown` | `rustup target add wasm32-unknown-unknown` |
| wasm-bindgen CLI | exactly the `wasm-bindgen` crate version (0.2.129) | `cargo install wasm-bindgen-cli --version 0.2.129 --locked` |
| 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 --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
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`.
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 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`.
URL flags: `?empty` (start without the demo project), `?home` (show the start screen).
## 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 |
| `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` |
| 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) |
### Threads
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).
## `window.effectcraft`: the agent / test API
The control channel of the desktop app (`docs/control-protocol.md`) as promises. Results resolve
with the method's `result`, errors reject with the message.
| Call | |
|---|---|
| `effectcraft.request(method, params)` | any control method: `ui.inspect`, `ui.click`, `ui.key`, `ui.playback`, `ui.menu.invoke`, `render.frame`… |
| `effectcraft.execute(command, params)` | `engine.execute`: an engine command (same ids and params as MCP / `effectcraft-cli`) |
| `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.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 |
`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
```
## 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`:
```sh
cargo xtask web --serve 8765 &
node apps/effectcraft-web/tests/smoke.mjs --url http://127.0.0.1:8765/ --out target/web/smoke
```
## 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).
- `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.