Renderbox

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.

launch-film · 16.1 s · glass cards, feature tiles, counters and an animated area chart, drawn by renderbox-motion

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.

  1. written in

    TypeScript, @jtdigital/renderbox-sdk/motion

    Layout, keyframes, themes and checks. The build writes one document.

  2. document

    CompDoc, *.comp.json

    The reader refuses unknown fields, duplicate keys, null and newer schemas, each with a named code. From epoch 2 it also refuses the old v0 format.

  3. 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.

  4. text

    renderbox-text

    Our text shaping crate, Apache-2.0. The same code measures text in the SDK and draws it in the renderer.

  5. 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.

  6. out

    Raw frames on stdout, PNGs, or frame hashes

    For the fleet: yuv420p in BT.709 limited range. Frames are drawn in 32-bit floats, the canonical f32 pipeline.

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. 1 Text("TYPE THAT MOVES", { style: t.type.hook })
  2. 2 renderbox-text.wasm → cap band 860 × 180 px
  3. 3 Row / Column / Safe → where each line goes
  4. 4 comp.json · render.shaperEpoch
  5. 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.

  1. 01

    rbv still

    Renders one frame at a given second.

  2. 02

    rbv sheet

    Renders a contact sheet of the key moments.

  3. 03

    rbv check

    Runs the legibility checks, refusals and lint without rendering.

  4. 04

    rbv render

    Renders the whole piece locally.

  5. 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

One Everyframe Composer run card three times: rendered by Chrome, rendered by renderbox-motion from a Taffy layout, and a heat map of their difference.
Chrome · renderbox-motion · the difference
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 Linux
  • rbv: 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.

Write to us →