renderbox-motion · rust · cpu
A deterministic renderer for motion graphics.
It reads one composition document and writes raw frames. It draws on the CPU, with no browser, no GPU and no screen. A document gives the same bytes on a laptop, a render host and in a browser tab, as long as their CPUs are in the same hash class.
Everyframe uses this API for its animations today. The renderer is built and tested. It is not on the servers yet.
Made with it
Everyframe
Everyframe makes its animations with this API and renderer.
Everyframe's assistant writes each animation as code with this API. Renderbox renders it. Everyframe makes 11 of the 16 examples on this page today. The other 5 need the new renderer, which is not on the servers yet.
- Exact text and numbers
- Text and numbers come out exactly as typed.
- Checked
- Text too small to read on a phone stops the build.
- Every size and language
- One animation makes all of them. Example: 9 ads from one brief.
- No AI generation
- No image or video model is used, so there is no generation cost.
- Same frames every time
- The new renderer draws the same frames on every machine of the same CPU type.
The renderer
The renderer turns one document into raw frames.
renderbox-motion is a Rust workspace of three crates. The core draws the frames. The CLI crate builds the renderbox-motion binary. The third compiles the same core to WebAssembly (wasm32), behind a small C interface.
It reads CompDoc, a strict JSON document. It uses the same reader as the compiler, pinned to an exact revision.
No other program links the crate. Each plan is sealed for one epoch, a numbered version of the renderer. The runtime starts the binary for that epoch as its own process. It checks the binary's self-test and contract number, and checks that the document matches its sealed hash. Then it pipes the frames into FFmpeg. On Linux the pipe is grown to 16 MiB, and the renderer and FFmpeg run on separate sets of CPU cores.
written in
TypeScript,
@jtdigital/renderbox-sdk/motionLayout, keyframes, themes and checks. The build writes one document.
document
CompDoc,
*.comp.jsonThe reader refuses unknown fields, duplicate keys,
nulland newer schemas, each with a named code. From epoch 2 it also refuses the old v0 format.renderer
renderbox-motion
Opens the scene once. It hashes every file and checks every size limit. Then, for each frame, it computes each element's values on that element's own clock, paints the frame and hashes it.
text
renderbox-text
Our text shaping crate, Apache-2.0. The same code measures text in the SDK and draws it in the renderer.
footage
A decode-only FFmpeg 8.1.2, shipped beside the binary
Each clip is hashed and probed when the scene opens. The probe runs in its own process, with a time limit and a memory limit. Each source frame is decoded once and kept only until the last frame that needs it. Decoded frames are matched by presentation time, so a phone's variable-rate clip plays by each frame's own time and a rate declared for it is nominal. A constant-rate file is still held to its declared rate. On Linux the decoder links musl statically, so every host runs the exact bytes the build hashed. A decode that runs out of memory is retried once from the same keyframe.
out
Raw frames on stdout, PNGs, or frame hashes
For the fleet:
yuv420pin BT.709 limited range. Frames are drawn in 32-bit floats, the canonicalf32pipeline.
The fleet runs it with one command.
The binary checks its arguments first, then the epoch, then the document's hash. Only then does it open the document. It writes the frames to stdout, one after another.
renderbox-motion render <doc> \
--frames a:b --class unfused --pipeline f32 --pix-fmt yuv420p \
--epoch <n> --doc-sha256 <hex> … \
--out raw --output -exit codes · append-only
- 2
- bad arguments
- 3
- bad document
- 4
- hash mismatch
- 5
- wrong epoch
- 6
- missing asset
- 7
- render error
- 8
- broken pipe
- 9
- io
- 10
- no decoder
8 means the reader quit first
Its dependencies are pinned to exact versions.
Every crate that does float maths uses libm, never std. So no platform maths library can change a pixel.
| crate | version | what it does here |
|---|---|---|
| vello_cpu | 0.3.0 | Draws paths, fills, strokes and blends on the CPU, in f32 (canonical) or u8 (draft) |
| fearless_simd | 0.7.0 | Runs SIMD code at the level we pass in: scalar, SSE4.2, AVX2, NEON or wasm simd128. It never picks a level by detecting the CPU |
| kurbo | 0.13.1 | Curves and geometry: shapes, glyph outlines and the paths that elements move along |
| peniko | 0.6.1 | The brushes, colours and gradients passed to the rasterizer |
| parley | 0.12.0 | Text layout inside renderbox-text. Shaping uses HarfRust. Line breaks and segmentation use ICU4X |
| skrifa | 0.44.0 | Reads the font files: metrics, cap heights, outlines |
| libm | 0.2.16 | Sines, powers and logs written in Rust, with the same result on every target |
| serde_json | 1.0.153 | Built with float_roundtrip, so every number reads back as the exact double the SDK wrote |
| jpeg-decoder | 0.3.2 | Decodes JPEG stills with the same scalar integer code on every target. Its SIMD code is compiled out |
| renderbox-ir | pinned rev | Our reader for the composition document. The compiler uses the same code, so the renderer and the compiler refuse the same documents |
The SDK measures text with the renderer's own code.
Layout runs in TypeScript. Rows, columns, lines stacked by cap height and the safe area are plain numbers you can read back. To place a word, layout needs its width. That width comes from renderbox-text, compiled to WebAssembly from the renderer's own lockfile.
The document records the shaper epoch that measured it. A renderer of another epoch refuses the document, because its text might not fit.
- 1 Text("TYPE THAT MOVES", { style: t.type.hook })
- 2 renderbox-text.wasm → cap band 860 × 180 px
- 3 Row / Column / Safe → where each line goes
- 4 comp.json · render.shaperEpoch
- 5 renderbox-motion → same shaper, same glyphs
The same frame on every machine
Machines in one hash class draw identical frames.
The renderer is versioned in epochs. A sealed plan names its epoch. A job goes only to a host that has that epoch installed and passes its self-test.
The rest is enforced when the renderer compiles. Sines, powers and exponentials go through libm. The renderer never calls mul_add. An x86 build whose baseline includes AVX2 or FMA fails to compile. With either one, the SIMD library would run fused multiply-adds when SSE4.2 is asked for.
A fused multiply-add rounds once instead of twice, so it can change the last bit of a result. Frames therefore fall into two hash classes. Fused: NEON and AVX2. Unfused: scalar, SSE4.2 and simd128. Within a class, frames are identical on every machine.
- self-test
- Before a worker runs a plan, it asks the binary for its epoch and contract number. Then the binary runs a self-test. It renders a built-in frame and checks it against five stored hashes.
- parity
- 150 of the 170 programs in the SDK's test corpus build for this renderer. Each one is also built for today's FFmpeg path. Both give the same layers and timing.
- golden frames
- 46 draw-model cases and 150 document cases. Each is hashed at every SIMD level and compared with a stored hash.
- colour
- HLG (HDR phone) footage is converted to SDR, tuned to match Apple's rendering. Video frames convert to RGBA with integer maths only.
- routing
- Plans are routed by epoch. If no host lists a plan's epoch, the plan is refused at intake. It never runs on a host with another epoch.
The composition API today
An animation is a TypeScript program.
A composition is a tree of values. The leaves are text, shapes and footage. Layouts arrange them. Key tracks say how each one moves. There is no timeline to drag. Each frame's values are computed from its time.
title-storm and drop-measured are written in 0.16's API, which 0.17 moved to /motion/legacy. The kinetic-type theme below is written in /motion.
title-storm · storm.ts · shortened
import { EASE, along, effect, text }
from "@jtdigital/renderbox-sdk/motion/legacy";
// each of 24 letters flies in on its own arc,
// turning, motion-blurred, landing on a kick of the song
light(12 * u).show({
pose: along(d, { at, dur, ease: EASE.inOutSine }),
effects: [
effect.filter("lagfun", { decay: 0.965 }, "rgba"),
effect.glow({ blur: 36 * u, color: cyan }),
],
});
Every landing time comes from the SDK's analysis of the song. No time in the program is typed by hand.
drop-measured · cut.ts · simplified
import { planDropEdit, ad, lintAd, reviewSheet }
from "@jtdigital/renderbox-sdk/motion/legacy";
// the tempo, the bars and the drop come from the analysis;
// the only written time is the one it is checked against
const plan = planDropEdit(song, inserts, { drop: { at } });
One program makes a 25.8 s drop edit at 9:16 and at 16:9. The drop lands at 5.6 s. A strip at the bottom shows the measured beat grid under a playhead.
kinetic-type · the theme · shortened
const t = defineTheme({
name: "kinetic",
colors: { night: "#0E0A17", bone: "#F3EEE4", dust: "#A79FB4", volt: "#D8FF3C" },
type: {
hook: { face: "archivo-black", caps: true, capPx: 180,
role: "headline", color: "bone" },
kicker: { face: "ibm-plex-mono-bold", caps: true, capPx: 34,
role: "support", color: "dust" },
// outlines nobody reads: no role, so no legibility check
ghost: { face: "anton", caps: true, capPx: 780, color: "night" },
},
});
const pop = (deg: number) => ENTER.rise.with({
enter: { opacity: 0, scale: 0.2, rotate: deg, dur: 0.6, ease: EASE.outBack },
});what a role is for
A type style with a role is checked at build time against the display it is shown on. A headline too small to read on a phone fails the build.
Decorative text, like a 780 px outline behind the words, has no role and is not checked.
Checks
Checks run before any frame is drawn.
compile time
A fill at the root has nothing to fill, so it does not compile. Every marker a layer anchors to must be set on some layer.
build time
Text with a role is checked for legibility at its size. A layer with more changes than the cap allows is refused by name.
named refusals
A raw FFmpeg colour key is refused. The error points to the keyer the renderer supports. So no program depends on a filter the fleet will not run.
lint for ads
Before delivery, an ad is linted for its target platform, at every ratio the program declares.
$ rbv check showcase/title-storm/storm
refused kick-one: 24 changes on one layer, the cap is 12
help: split the kicks across layers
refused key-filter: effect.filter("colorkey")
help: effect.key() keys a colour on every backend
ok hero: 8.00 s · 240 frames · 0 findings
The output format is an example. The two refusals are real probes in title-storm.
rbv
rbv previews, checks, renders and delivers a program.
01
rbv still
Renders one frame at a given second.
02
rbv sheet
Renders a contact sheet of the key moments.
03
rbv check
Runs the legibility checks, refusals and lint without rendering.
04
rbv render
Renders the whole piece locally.
05
rbv deliver
Renders every ratio and target the project declares.
Next preview · in design
The builder in design animates fluent-html components.
The studio's products render every screen as a fluent-html component. The builder takes such a component unchanged. Taffy, a Rust layout crate for flexbox and grid, lays it out. It measures text with renderbox-text. Both are compiled to WebAssembly. No browser is involved. This renderer draws it, with no change to the renderer. Each part is animated through the typed ids the app already declares.
The design follows plans N20 and N21 in renderbox-sdk. The names below are a proposal.
a Composer card, moved between two states by id
import { Html, Project, DISPLAYS, EASE } from "@jtdigital/renderbox-sdk/motion";
import { CoverCard, coverOf, ids } from "everyframe-composer/runs";
// the function the app renders, two states of one run
const waiting = CoverCard(coverOf(planRun)); // Plan · 6 shots
const finished = CoverCard(coverOf(doneRun)); // 3 versions · 2 languages
export default Project(
Html(waiting, { width: 600, sheet: "styles.compiled.css" })
.hold(1.2)
.morph(finished, { dur: 0.8, ease: EASE.inOutCubic }) // every shared id moves
.part(ids.coverBadge, (b) => b.enter("pop").at(1.4)),
{ name: "run-done", display: DISPLAYS.square, fps: 30 },
);
Layout runs once per state, at build time. Between two states, each element whose id is in both moves. Its position, size, colour and corner radius animate. No frame runs layout, so the renderer and its guarantees stay as they are.
the layout half, tested in a spike
- style
- 0 of 2,545 values differ
- layout
- 0.0125 px, the largest box offset
- text
- 15 of 15 line breaks match
- coverage
- 86.6 % of Composer's styling calls
One real run card from Everyframe Composer. Taffy laid it out, and today's renderer drew it with no change to the renderer. Layout takes 3 to 5 ms. A frame takes 13 ms.
real today
Every video on this page, made with the SDK as it is today.
proven
A product card laid out by Taffy, drawn by this renderer and compared with Chrome.
in design
The builder above. It animates components and moves elements between two states by id. Names may change before it is built.
Showcases
Each of these sixteen pieces is a TypeScript program.
The programs are in the SDK. Everyframe shows the same sixteen on its Motion page. Its agent makes eleven of them from a brief today. The five drawn by renderbox-motion wait until the renderer is on the fleet.
Drawn by renderbox-motion
Launch film
Glass cards, feature tiles that slide in, counters and an area chart that is drawn on.
blur · box shadows · along() · camera push-in
Type festival
Words pop in, turned and scaled. A counter runs to 48. The city's name is set in a gradient.
kinetic type · strokes · glow · motion blur
Data story
Bars race and change rank. A line climbs with a dot on its tip.
Row and Column · counters · Drawn · along()
Ten charts
Line charts, bars that re-sort, a heatmap, a radar chart that morphs and a function plot. Each one is drawn on.
Drawn closures · Arc · wipes and xfades
Looks and transitions
Eleven colour looks on one shot, a chroma key and fourteen transitions.
grades · keys · shadows and glows
Rendered through today's API
Trailer cuts
Eight shots joined by a flash, a whip pan, a zoom through and an iris.
custom transitions · frame-exact labels
Live board
Bars grow and change places as the figures move. The total counts up.
data-driven layers · eased swaps
Nine ads from one brief
Three languages in three shapes make nine ads, all from one program.
copy fitting · multi-ratio layout
Every shape in one
A 16:9 ad contains its 1:1 version, which contains its 9:16. The last frame leads into the first.
nested precomps · bit-exact loop
Logo stings
Short logo animations from one wordmark outline, plus a version with an alpha channel.
batch generation · alpha output
Name cards
A card for every name on a list, each name sized to fit.
per-row glyph check · font fallback
Lens look
A glow, a light leak, a slow push in and a focus pull on one shot.
lens effects over footage
Title storm
Each letter flies in on its own arc and lands on a beat.
along() · motion blur · beat sync
Cut on the drop
Waits for the drop, then cuts on every kick. No time is typed by hand.
audio analysis · planDropEdit()
Talking reel
The music dips under the voice. Each word lights up as it is said.
ducking · word-level captions
Living photo
One photo turned into a slow camera move with depth and haze.
plane cuts · focus pull · grain
Status
The renderer is built but not on the fleet yet.
No host lists a motion epoch yet. So today a plan with a motion segment is refused at intake. This is how the routing is designed to work.
built
- The composition API,
@jtdigital/renderbox-sdk/motion, with a generated reference renderbox-motion, epoch 2, built for Linuxrbv: still, sheet, check, render, deliver- Plans routed by epoch. Workers prove which epochs they have and list them
- The epoch deploy: one gated command builds an epoch once and checks each file's SHA-256 on every host. Each host install holds a lock, keeps the build it replaces for rollback and refuses a rollback to a lower fleet contract
rolling out
- Epoch 2 installed on the fleet's hosts, one host at a time
- The WebAssembly build of the renderer, for previews
- The fluent builder, which animates components and moves them between states. It is in design. Its layout half is proven
Access is by request.
There is no sign-up. To run work on Renderbox, or to ask how a part of it works, write to us.