Example: the discount lockup¶
The hard case that touches the most of the system at once, written as the JSON an author would actually save — imagining every Feature in this brief built exactly as designed. Five layers, no React, no bespoke component.
What it looks like: a red pill sitting at the bottom-centre of the frame, SAVE in bold beside a
large $200*, the top of SAVE level with the top of the digits, a small yellow LIMITED TIME
badge hanging off the pill's top-right corner. The whole assembly pops in together six frames after
the scene starts.
flowchart TB
subgraph scene["scene-offer (one motion graph per ratio)"]
ROW["lockup-row<br/>flowBox · absolute · anchor bottom-centre<br/>pop animation"]
PRE["lockup-prefix<br/>text 'SAVE' · flow"]
HERO["lockup-hero<br/>text $200* · flow"]
BG["lockup-bg<br/>background · derived · fade animation"]
BADGE["lockup-badge<br/>text · pin full"]
PRE -- "flowIn" --> ROW
HERO -- "flowIn" --> ROW
BG -- "of" --> ROW
BADGE -- "pinTo" --> BG
end
The document¶
Per-ratio blocks are shown in full for 16:9; 9:16 and 1:1 carry the same shape with their own
numbers.
{
"id": "scene-offer",
"durationInFrames": 90,
"layers": [
// ── The pill behind everything. Its box is never authored: it's the union of
// lockup-row's extents plus its own paint-time padding.
{
"id": "lockup-bg",
"type": "background",
"zIndex": 0,
"layout": {
"mode": "derived",
"16:9": { "of": ["lockup-row"], "ofBounds": "extents" },
"9:16": { "of": ["lockup-row"], "ofBounds": "extents" },
"1:1": { "of": ["lockup-row"], "ofBounds": "extents" }
},
"props": { "fill": "#E4002B", "padding": { "x": 40, "y": 24 }, "radius": 16 },
"animations": [
{ "animation": "fade", "params": { "from": 0, "durationInFrames": 18 },
"at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 6 } }
]
},
// ── The row. Content-sized on both axes, so it's exactly as wide as SAVE + gap + $200*.
// x/y place its bottom-centre anchor; it grows upward and outward from there.
{
"id": "lockup-row",
"type": "flowBox",
"zIndex": 1,
"layout": {
"mode": "absolute",
"16:9": { "x": 960, "y": 940, "w": "content", "h": "content", "anchor": "bottom-centre" },
"9:16": { "x": 540, "y": 1560, "w": "content", "h": "content", "anchor": "bottom-centre" },
"1:1": { "x": 540, "y": 940, "w": "content", "h": "content", "anchor": "bottom-centre" }
},
"props": { "direction": "row", "alignAcross": "capHeight", "gap": 20 },
"animations": [
{ "animation": "pop", "params": { "from": 0.6, "durationInFrames": 18 },
"at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 6 } }
]
},
// ── First member. Array order is flow order: SAVE comes first.
{
"id": "lockup-prefix",
"type": "text",
"zIndex": 2,
"layout": {
"mode": "flow",
"16:9": { "flowIn": "lockup-row", "w": "content", "h": "content" },
"9:16": { "flowIn": "lockup-row", "w": "content", "h": "content" },
"1:1": { "flowIn": "lockup-row", "w": "content", "h": "content" }
},
"typography": {
"16:9": { "family": "Brand Sans", "weight": 800, "size": 72, "colour": "#FFFFFF" }
// "9:16": …, "1:1": …
},
"props": { "content": [ { "value": "SAVE" } ] }
},
// ── Second member. One text layer, three runs: a price run and an inline suffix run.
// The suffix is not a pinned layer — it's just the next run on the same line.
{
"id": "lockup-hero",
"type": "text",
"zIndex": 2,
"layout": {
"mode": "flow",
"16:9": { "flowIn": "lockup-row", "w": "content", "h": "content" },
"9:16": { "flowIn": "lockup-row", "w": "content", "h": "content" },
"1:1": { "flowIn": "lockup-row", "w": "content", "h": "content" }
},
"typography": {
"16:9": { "family": "Brand Sans", "weight": 900, "size": 180, "colour": "#FFFFFF" }
},
"props": {
"content": [
{
"price": { "amount": 200, "currency": "AUD", "locale": "en-AU" },
"typography": {
"currencySymbol": { "size": { "ofRoot": 0.55 } },
"cents": { "size": { "ofRoot": 0.5 } }
}
},
{ "value": "*", "typography": { "size": { "ofRoot": 0.4 } } }
]
}
},
// ── The badge. Pinned "full" to the pill's top-right, centred on that corner,
// so it rides the pill's box and fades with it.
{
"id": "lockup-badge",
"type": "text",
"zIndex": 3,
"layout": {
"mode": "pin",
"16:9": {
"pinTo": "lockup-bg", "pinToBounds": "extents", "pinToAnchor": "top-right",
"anchor": "centre-centre", "offsetX": 0, "offsetY": 0,
"w": "content", "h": "content", "pinType": "full"
},
"9:16": { /* same, own offsets */ },
"1:1": { /* same, own offsets */ }
},
"typography": {
"16:9": { "family": "Brand Sans", "weight": 700, "size": 28, "colour": "#1A1A1A" }
},
"props": {
"content": [ { "value": "LIMITED TIME" } ],
"background": { "fill": "#FFD100", "padding": { "x": 16, "y": 8 }, "radius": 999 }
}
}
]
}
What the engine does with it¶
Resolve. The price run becomes role-tagged parts through one Intl.NumberFormat("en-AU",
{ currency: "AUD" }) — $, 200 — with no cents part because the amount is whole
(Price). Every { ofRoot } multiplies out against the hero's root size
(0.55 × 180 = 99 for the symbol, 72 for the asterisk). Both at references resolve against this
scene's own start cue: frame 6 (Cues). Both named animations expand into
keyframe tracks — pop into a scale track from 0.6 to 1 and an opacity track from 0 to
1, fade into opacity alone (Keyframe Animation).
Solve. One topological sort over the graph above gives the order: lockup-prefix,
lockup-hero, lockup-row, lockup-bg, lockup-badge. The two text layers are "content" on
both axes, so solve calls measure for each with no width constraint: canvas measureText returns
each run's ink extents, and the font-table reader supplies the cap-height and baseline for Brand
Sans at each size (Text Foundation). The hero's three runs share
one baseline. lockup-row then packs its two members left to right with a 20px gap, aligning
them on capHeight — the top of S and the top of 2 land on the same line, whatever the ascender
space above them — and its own content box is exactly that packed extent, positioned so its
bottom-centre sits at (960, 940). lockup-bg takes the row's extents and grows them by its
padding. lockup-badge measures its own text, then places its centre on the pill's top-right
corner. Every box here is frame-invariant (Positioning,
Background).
Animate. Frame 10, say. The pop track puts lockup-row at scale 0.78, opacity 0.42, about
its content centre, composed with the scene's own motion into the row's matrix. lockup-prefix and
lockup-hero are flow members, so each asks the graph for its transform and gets the row's matrix
composed with its own (identity) — they scale and fade with the row without an animation of their
own. lockup-bg re-derives its box from the row's animated extents, so the pill shrinks with the
text; its opacity is its own fade track, 0.42 at this frame by construction of the same timing.
lockup-badge is pinned full, so it takes the pill's matrix and multiplies in the pill's opacity:
it rides the corner inward as the pill shrinks and fades with it (The Motion Graph).
Place. Five positioned elements, each with a transform: matrix3d(…) and an opacity, text
runs emitted as absolutely-positioned spans at their solved coordinates. The browser rasterises
glyphs and paints fills; it lays out nothing.
What this shows¶
- Five layers, zero code. Today this assembly is
betta-discount-box, a bespoke React component with its own padding, clipping, and price-formatting logic. Here every rule it hard-codes is a schema field. - One animation, three inheritance paths. The
popis authored once on the row. Its members inherit it through flow membership, the pill tracks it geometrically throughof, and the badge inherits both geometry and opacity through afullpin. Nothing else was told to move. - Content-driven, ratio by ratio. Change the amount to
$1,299.95through a manifest binding and the hero widens, the row re-packs, the pill re-derives, the badge follows the new corner — in every ratio, with no coordinate edited.
Where a Feature page hasn't yet pinned a field this example uses (the inner shape of a price run,
the parameter names of pop and fade), the example uses the simplest shape that fits and that
page should adopt it.