How a frame is rendered
You do not need this page to use Premation. You need it to understand why certain classes of bug do not happen here, and to predict what a change will do.
The pipeline
Scene graph + animation engine
│ buildSnapshot(graph, animation, time, …)
▼
RenderSnapshot a flat, immutable description of one frame
│ snapshotToFrameScene()
▼
FrameScene renderer-native draw list
│ Renderer.render(viewport, scene)
▼
Render graph passes clear → background → composition → selection → overlay
│
▼
WebGPU or WebGL2 backendThe snapshot is the seam
buildSnapshot samples the animation engine at one time and produces a plain
data structure describing that frame. Everything downstream draws from it:
- the viewport,
- the secondary view panes in 2-up and 4-up layouts,
- library and template card previews,
- the export preview in the export dialog,
- the offline export loop itself,
- the pixel-comparison test harness.
This is why “the export doesn’t match the preview” is not a class of bug that can exist here. They are not two renderers that have been kept in sync — they are one renderer called from six places.
Backend tiers
The engine chooses its tier at startup, walking the whole
adapter → device → configure path with a delayed retry at each rung:
- WebGPU if the full path succeeds.
- WebGL2 otherwise.
- If both fail, a visible error — never a blank canvas.
The tier it landed on is shown in the viewport header. There is also a Canvas2D backend, used for library and template card previews, which is why those cards show real frames rather than static images.
Canvas2D previews skip GPU-only effects. A library card is an accurate preview of geometry and timing, not necessarily of every filter.
Alpha is premultiplied
Past the decode, every colour in the pipeline is premultiplied. There is one alpha space, no per-draw flag selecting between two, and no shader variant that handles the other case.
A file’s alpha mode never reaches the fragment stage. It is consumed at the decode, which is the only boundary that can answer the question once per file rather than once per draw:
| The file says | What happens at decode |
|---|---|
| Straight | premultiplyAlpha: 'premultiply' — the browser multiplies |
| Premultiplied | premultiplyAlpha: 'none' — the bytes already are |
Straight and Premultiplied in Interpret Footage ▸ Alpha still describe the file correctly; they now change how it is decoded rather than how it is shaded.
Why it matters
A bilinear sampler averages in whatever space it is handed, and straight is the wrong one. Measured on a hard alpha edge magnified 30×, the half-covered column read 181 where correct filtering predicts 243.8 — a 63-of-255-level dark halo around every soft edge. After the flip the same column reads 244 against a prediction of 243.9, identically on both backends.
If you are working on the renderer: both upload flags mean the destination shall be premultiplied — not “multiply the source”. Reading either as pass-through un-premultiplies the texture and hands the shader something it then divides as though it were premultiplied. That error cancels almost perfectly: 50% fill opacity rendered at 97.3% of full, which an inequality-based assertion happily accepts. Assert on the ratio.
Determinism
Frame time is exactly index / fps — never wall-clock. Two renders of the same
project produce identical frames, and a re-render after a crash resumes
identical work. Particles are deterministic for the same reason: the same frame
always produces the same field.
See Determinism.
Design rules you can rely on
These are enforced in the codebase and they show up in how the app behaves:
- The scene graph is the source of truth. A node’s transform lives in its transform component; nothing else holds a shadow copy.
- Every mutation is a command. Direct writes would bypass undo, and undo is the feature people notice being broken first.
- Pure where it can be. Math, interpolation, geometry, muxing and encoding are pure functions with unit tests. Only the parts that genuinely need a canvas or a GPU touch one.
- No silent degradation. If a code path cannot do what was asked, it raises an error with a reason you can act on. Producing a plausible-looking wrong result is treated as worse than failing.
That last rule is why video export refuses to write a zero-frame file, why the renderer fails loudly when the GPU backend did not initialise, and why the info readout shows a placeholder instead of an invented colour.