Example: Betta Buys Cashback¶
Manifest row 12 · built today as betta-buys-cashback
Three flush, equal-width blocks stacked: the yellow Betta wordmark, a yellow BUYS panel with
blue text, and a red panel carrying three rows of white copy — PLUS beside a small UP/TO beside
$300, then Cashback, then on selected models*. Every block is exactly as wide as the widest,
so the stack stays a clean column however the copy changes. The blocks wipe on in turn; the two
panels pulse.
flowchart TB
subgraph scene["scene (16:9 graph)"]
ROOT["bb-stack<br/>flowBox · column · alignAcross stretch · absolute"]
LOGO["bb-logo<br/>image · flow · alignAcrossSelf centre"]
BUYS["bb-buys<br/>text 'BUYS' on yellow · flow (stretched) · pulse"]
RED["bb-red<br/>flowBox · column · flow (stretched) · pulse"]
REDBG["bb-red-bg<br/>background · derived of bb-red"]
R1["bb-red-line1<br/>flowBox · row · flow"]
PLUS["bb-plus<br/>text · flow"]
UPTO["bb-up-to<br/>text · flow"]
AMT["bb-amount<br/>text price $300 · flow"]
HERO["bb-hero<br/>text 'Cashback' · flow"]
FOOT["bb-footnote<br/>text · flow"]
LOGO -- flowIn --> ROOT
BUYS -- flowIn --> ROOT
RED -- flowIn --> ROOT
REDBG -- of --> RED
R1 -- flowIn --> RED
PLUS -- flowIn --> R1
UPTO -- flowIn --> R1
AMT -- flowIn --> R1
HERO -- flowIn --> RED
FOOT -- flowIn --> RED
end
The document¶
{
"id": "scene-betta-buys",
"durationInFrames": 75,
"layers": [
// ── The red panel: a derived background under the inner column. Because the column is a
// stretched flow member, its extents are the stack's full width, so the panel is too.
{
"id": "bb-red-bg",
"type": "background",
"zIndex": 0,
"layout": {
"mode": "derived",
"16:9": { "of": ["bb-red"], "ofBounds": "extents" }
// "9:16": …, "1:1": …
},
"props": { "fill": "#ed1b24", "radius": 0, "padding": { "top": 21, "right": 12, "bottom": 21, "left": 12 } },
"animations": [
{ "animation": "wipe", "params": { "direction": "from-left", "durationInFrames": 12 },
"at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 8 } }
]
},
// ── Root column. `stretch` gives every member the widest member's width — the "equal
// rectangles" the source draws — while the column itself stays content-sized.
{
"id": "bb-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": "stretch", "gap": 29 }
},
// ── Wordmark: fixed width, aspect height. Opts out of stretch so it isn't distorted.
{
"id": "bb-logo",
"type": "image",
"zIndex": 2,
"layout": { "mode": "flow", "16:9": { "flowIn": "bb-stack", "w": 660, "h": "content", "alignAcrossSelf": "centre" } },
"props": { "src": { "kind": "bundled", "path": "effects/betta-buys-cashback/betta-wordmark.png" } },
"animations": [
{ "animation": "wipe", "params": { "direction": "from-left", "durationInFrames": 12 },
"at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 0 } }
]
},
// ── BUYS: a stretched text layer whose paint-only background fills the stretched box.
{
"id": "bb-buys",
"type": "text",
"zIndex": 2,
"layout": { "mode": "flow", "16:9": { "flowIn": "bb-stack", "w": "content", "h": "content" } },
"typography": { "16:9": { "family": "GT Eesti Display", "weight": 700, "size": 248, "color": "#054f8e", "case": "uppercase", "horizontalAlign": "center" } },
"props": {
"content": [ { "value": "Buys" } ],
"background": { "fill": "#ffd007", "radius": 0, "padding": { "top": 53, "right": 24, "bottom": 52, "left": 23 } }
},
"animations": [
{ "animation": "wipe", "params": { "direction": "from-left", "durationInFrames": 12 },
"at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 4 } },
{ "animation": "pulse", "params": { "scale": 1.05, "periodInFrames": 40 },
"at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 30 } }
]
},
// ── Inner red column: three rows of copy, centred, stretched to the stack's width.
{
"id": "bb-red",
"type": "flowBox",
"zIndex": 2,
"layout": { "mode": "flow", "16:9": { "flowIn": "bb-stack", "w": "content", "h": "content" } },
"props": { "direction": "column", "alignAcross": "centre", "gap": 20 },
"animations": [
{ "animation": "pulse", "params": { "scale": 1.05, "periodInFrames": 40 },
"at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 30 } }
]
},
{
"id": "bb-red-line1",
"type": "flowBox",
"zIndex": 3,
"layout": { "mode": "flow", "16:9": { "flowIn": "bb-red", "w": "content", "h": "content" } },
"props": { "direction": "row", "alignAcross": "centre", "gap": 24 }
},
{
"id": "bb-plus",
"type": "text",
"zIndex": 4,
"layout": { "mode": "flow", "16:9": { "flowIn": "bb-red-line1", "w": "content", "h": "content" } },
"typography": { "16:9": { "family": "GT Eesti Display", "weight": 700, "size": 116, "color": "#ffffff", "case": "uppercase" } },
"props": { "content": [ { "value": "Plus" } ] }
},
{
"id": "bb-up-to",
"type": "text",
"zIndex": 4,
"layout": { "mode": "flow", "16:9": { "flowIn": "bb-red-line1", "w": "content", "h": "content" } },
"typography": { "16:9": { "family": "GT Eesti Display", "weight": 700, "size": 41, "color": "#ffffff", "case": "uppercase", "lineHeight": 0.83 } },
"props": { "content": [ { "value": "Up\nTo" } ] }
},
{
"id": "bb-amount",
"type": "text",
"zIndex": 4,
"layout": { "mode": "flow", "16:9": { "flowIn": "bb-red-line1", "w": "content", "h": "content" } },
"typography": { "16:9": { "family": "GT Eesti Display", "weight": 700, "size": 116, "color": "#ffffff" } },
"props": { "content": [ { "price": { "amount": 300, "currency": "AUD", "locale": "en-AU" } } ] }
},
{
"id": "bb-hero",
"type": "text",
"zIndex": 3,
"layout": { "mode": "flow", "16:9": { "flowIn": "bb-red", "w": "content", "h": "content" } },
"typography": { "16:9": { "family": "GT Eesti Display", "weight": 700, "size": 116, "color": "#ffffff" } },
"props": { "content": [ { "value": "Cashback" } ] }
},
{
"id": "bb-footnote",
"type": "text",
"zIndex": 3,
"layout": { "mode": "flow", "16:9": { "flowIn": "bb-red", "w": "content", "h": "content" } },
"typography": { "16:9": { "family": "GT Eesti Display", "weight": 700, "size": 40, "color": "#ffffff" } },
"props": {
"content": [ { "value": "on selected models*" } ],
"patterns": [ { "expression": "\\*$", "typography": { "size": { "ofRoot": 0.58 }, "baseline": 0.45 } } ]
}
}
]
}
| What the notes want editable | Bound field | Control |
|---|---|---|
| Every run's text | bb-*.props.content[0].value |
text |
| Amount | bb-amount.props.content[0].price.amount |
number |
| Panel colours | bb-buys.props.background.fill, bb-red-bg.props.fill |
colour |
| "UP TO" on/off | — | see Stress points |
| Pulse toggle per panel | bb-{buys,red}.animations[pulse].params.scale |
see Stress points |
What the engine does with it¶
Resolve. $300 formats to parts; the footnote's asterisk shrinks and lifts via its pattern;
four wipes stagger 0/4/8; two pulses start at frame 30.
Solve. Leaves first, then bb-red-line1, bb-red, bb-red-bg, and bb-stack last. The
interesting step is stretch: bb-stack is "content" wide, so it first takes the widest
member's intrinsic width (the 660px wordmark), then hands that width back to bb-buys and bb-red
as their resolved w. bb-buys's yellow fill paints across the full stretched box with the word
centred inside it; bb-red-bg takes the stretched column's extents, so the red is flush with the
yellow. bb-logo opted out with alignAcrossSelf: "centre" and keeps its 660.
Animate. bb-buys pulses about its own centre. bb-red's pulse reaches all five text layers
through flowIn and the red panel through of; the panel follows the scaled union.
Place. Eleven elements, three of them non-visual containers that emit nothing.
What this shows¶
stretchin a content-sized container is the equal-width column. No grid, no measuredmax-contentpass in component code — the sizing-collapse rule gives it for free.- Two ways to make a panel, again.
BUYSis text with a background; the red block is a background under a column. Same look, chosen by how many layers the panel holds. - Deep but legal.
bb-plusis four flow links from the scene (line1 → red → stack), under the cap of five; a fourth row of copy insideline1would not be.
Stress points¶
stretchinside a"content"-sized container is a two-pass resolve the brief doesn't state. The sizing-collapse cases cover content-in-content and fraction-in-content; a stretched member in a content-sized container is a third case (intrinsic pass, then distribute) and it's the one this whole component rests on. It works the CSS way, but it should be written down.- Removing a run needs a layout-affecting "collapsed" state, and it has to be bindable. The built
upTo.enabled: falseremoves the stack and re-centres the row. The brief has no per-layer "not rendered, don't reserve space" flag,opacity: 0keeps the slot, and bindings can't add or remove layers (Positioning → Bindings). This is a real, common authoring switch with no home. - Depth cap is close.
bb-plussits at four links; a common lockup shape is one nesting away from the five-link ceiling (Positioning → Depth cap). Not a hole, but the cap's rationale ("should be a component") was written before flow-inside-flow-inside-flow was the normal way to draw a three-row panel. wipeis not transform/opacity, no per-member spacing (24 vs 20 inside the red panel, and the builtupToOffsetYnudge), pulse pivot, per-part pulse toggle — as on the Home Appliance page.