Example: Sale — Up To 30% Off¶
Manifest row 14 · built today as betta-sale-percent-off
Two red panels tilted apart: SALE leaning −2° on top, and a wider panel leaning +2° below with a
small two-line UP/TO beside 30% OFF*. White type throughout. Both panels wipe on and pulse.
flowchart TB
subgraph scene["scene (16:9 graph)"]
ROOT["spo-stack<br/>flowBox · column · absolute"]
SALE["spo-sale<br/>text 'SALE' on red · flow · rotation -2 · pulse"]
OFFER["spo-offer<br/>flowBox · row · alignAcross centre · flow · rotation 2 · pulse"]
BG["spo-offer-bg<br/>background · derived of spo-offer"]
UPTO["spo-up-to<br/>text 'UP / TO' · flow"]
HERO["spo-hero<br/>text '30% OFF*' · flow"]
SALE -- flowIn --> ROOT
OFFER -- flowIn --> ROOT
BG -- of --> OFFER
UPTO -- flowIn --> OFFER
HERO -- flowIn --> OFFER
end
The document¶
{
"id": "scene-sale-percent-off",
"durationInFrames": 75,
"layers": [
{
"id": "spo-offer-bg",
"type": "background",
"zIndex": 0,
"layout": { "mode": "derived", "16:9": { "of": ["spo-offer"], "ofBounds": "extents" } },
"props": { "fill": "#ed1c24", "radius": 6, "padding": { "top": 19, "right": 13, "bottom": 20, "left": 15 } },
"animations": [
{ "animation": "wipe", "params": { "direction": "from-left", "durationInFrames": 12 },
"at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 4 } }
]
},
{
"id": "spo-stack",
"type": "flowBox",
"zIndex": 1,
"layout": { "mode": "absolute", "16:9": { "x": 120, "y": 120, "w": "content", "h": "content", "anchor": "top-left" } },
"props": { "direction": "column", "alignAcross": "centre", "gap": 28 }
},
// ── SALE: a single text layer whose paint-only background is the panel.
{
"id": "spo-sale",
"type": "text",
"zIndex": 2,
"layout": { "mode": "flow", "16:9": { "flowIn": "spo-stack", "w": "content", "h": "content", "rotation": -2 } },
"typography": { "16:9": { "family": "GT Eesti Display", "weight": 700, "size": 192, "color": "#ffffff", "case": "uppercase", "tracking": 4.8 } },
"props": {
"content": [ { "value": "Sale" } ],
"background": { "fill": "#ed1c24", "radius": 6, "padding": { "top": 38, "right": 18, "bottom": 36, "left": 20 } }
},
"animations": [
{ "animation": "wipe", "params": { "direction": "from-left", "durationInFrames": 12 },
"at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 0 } },
{ "animation": "pulse", "params": { "scale": 1.05, "periodInFrames": 40 },
"at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 30 } }
]
},
// ── Offer row: two text layers under one derived panel.
{
"id": "spo-offer",
"type": "flowBox",
"zIndex": 2,
"layout": { "mode": "flow", "16:9": { "flowIn": "spo-stack", "w": "content", "h": "content", "rotation": 2 } },
"props": { "direction": "row", "alignAcross": "centre", "gap": 9 },
"animations": [
{ "animation": "pulse", "params": { "scale": 1.05, "periodInFrames": 40 },
"at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 30 } }
]
},
{
"id": "spo-up-to",
"type": "text",
"zIndex": 3,
"layout": { "mode": "flow", "16:9": { "flowIn": "spo-offer", "w": "content", "h": "content" } },
"typography": { "16:9": { "family": "GT Eesti Display", "weight": 400, "size": 39, "color": "#ffffff", "case": "uppercase", "tracking": 1, "lineHeight": 0.95 } },
"props": { "content": [ { "value": "Up\nTo" } ] }
},
{
"id": "spo-hero",
"type": "text",
"zIndex": 3,
"layout": { "mode": "flow", "16:9": { "flowIn": "spo-offer", "w": "content", "h": "content" } },
"typography": { "16:9": { "family": "GT Eesti Display", "weight": 700, "size": 111, "color": "#ffffff", "case": "uppercase", "tracking": -0.6 } },
"props": {
// Three runs so the figure is a bindable value and the words stay words.
"content": [ { "value": "30" }, { "value": "% off" }, { "value": "*", "typography": { "size": { "ofRoot": 0.55 }, "baseline": 0.5 } } ]
}
}
]
}
| What the notes want editable | Bound field | Control |
|---|---|---|
| Percentage | spo-hero.props.content[0].value |
text today; should be a number — see Stress points |
| Text values | spo-sale.props.content[0].value, spo-up-to…, spo-hero.props.content[1].value |
text |
| Panel colours | spo-sale.props.background.fill, spo-offer-bg.props.fill |
colour |
| "UP TO" on/off | — | see Stress points |
| Pulse toggle per panel | spo-{sale,offer}.animations[pulse].params.scale |
see Stress points |
What the engine does with it¶
Resolve. Two wipes at frames 0 and 4, two pulses from 30; the asterisk run's ofRoot resolves
against the hero's 111.
Solve. spo-sale, spo-up-to, spo-hero, then spo-offer, spo-offer-bg, spo-stack. The
offer row centres the small stack on the hero's cross axis; the panel takes the row's extents plus
padding. The column centres SALE over the wider offer panel.
Animate. Each panel tilts and pulses about its own content centre; the derived panel follows the row's animated box.
Place. Six elements.
What this shows¶
- The minimal lockup. Two panels, one of each kind: a text-with-background and a background-under-a-row. If the brief handles this one cleanly, the family above it follows.
- Runs instead of patterns for structure.
30/% off/*as three runs makes the figure addressable without a regex; the pattern mechanism is kept for styling within a run.
Stress points¶
- No number run for
30— see 50% Off. - Removing a run needs a layout-affecting "collapsed" state — see Betta Buys.
wipeis not transform/opacity, no per-member spacing (the builtofferPrefixOffsetY: -8has no home), pulse pivot, per-part pulse toggle — as on the Home Appliance page.- Inline runs at mixed sizes need a stated baseline rule. The hero's three runs are 111 / 111 / 61px and the asterisk lifts by
baseline: 0.5; the brief's runs model never says whether runs share a baseline by default or whatbaselineis a fraction of (Text Foundation → Content-item text runs). Nothing awkward otherwise.