Example: Spend and Get¶
Manifest row 11 · built today as betta-spend-and-get
A partner lockup: a swappable logo (Hisense) over a tilted blue SPEND & GET panel, a red panel
tilted the other way carrying a two-line UP TO beside a big $500, Cashback* in blue below,
and a one-line blue disclaimer. Panels wipe on; the logo, Cashback* and the disclaimer each fade
on and slide down on their own timing; both panels pulse.
flowchart TB
subgraph scene["scene (16:9 graph)"]
ROOT["sag-stack<br/>flowBox · column · absolute"]
LOGO["sag-logo<br/>image · flow · w 308 · h content"]
HEAD["sag-headline<br/>text 'SPEND & GET' on blue · flow · rotation 3.2 · pulse"]
OFFER["sag-offer<br/>flowBox · row · alignAcross centre · flow · rotation -3 · pulse"]
UPTO["sag-up-to<br/>text 'UP / TO' · flow"]
AMT["sag-amount<br/>text price $500 · flow"]
BG["sag-offer-bg<br/>background · derived of sag-offer"]
CB["sag-cashback<br/>text 'Cashback*' · flow"]
DIS["sag-disclaimer<br/>text · flow"]
LOGO -- flowIn --> ROOT
HEAD -- flowIn --> ROOT
OFFER -- flowIn --> ROOT
UPTO -- flowIn --> OFFER
AMT -- flowIn --> OFFER
BG -- of --> OFFER
CB -- flowIn --> ROOT
DIS -- flowIn --> ROOT
end
The document¶
{
"id": "scene-spend-and-get",
"durationInFrames": 75,
"layers": [
// ── The red offer panel is a derived background under a row flowBox, because a panel
// holding TWO text layers can't be one text layer's `props.background`.
{
"id": "sag-offer-bg",
"type": "background",
"zIndex": 0,
"layout": {
"mode": "derived",
"16:9": { "of": ["sag-offer"], "ofBounds": "extents" }
// "9:16": …, "1:1": …
},
"props": { "fill": "#fe2121", "radius": 6, "padding": { "top": 18, "right": 6, "bottom": 12, "left": 5 } },
"animations": [
{ "animation": "wipe", "params": { "direction": "from-left", "durationInFrames": 12 },
"at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 8 } }
]
},
// ── Root column, content-sized, centred.
{
"id": "sag-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": 24 } // source: 32 / 13 / 23 / 26
},
// ── Swappable partner logo, width-driven, aspect-preserving height.
{
"id": "sag-logo",
"type": "image",
"zIndex": 2,
"layout": { "mode": "flow", "16:9": { "flowIn": "sag-stack", "w": 308, "h": "content" } },
"props": { "src": { "kind": "bundled", "path": "effects/betta-spend-and-get/logo.png" } },
"animations": [
{ "animation": "slideFade", "params": { "from": "top", "distancePercent": 60, "durationInFrames": 12 },
"at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 0 } }
]
},
// ── Blue headline panel: one text layer, its own paint-only background.
{
"id": "sag-headline",
"type": "text",
"zIndex": 2,
"layout": { "mode": "flow", "16:9": { "flowIn": "sag-stack", "w": "content", "h": "content", "rotation": 3.2 } },
"typography": { "16:9": { "family": "GT Eesti Display", "weight": 700, "size": 82, "color": "#ffffff", "case": "uppercase", "tracking": 2 } },
"props": {
"content": [ { "value": "Spend & Get" } ],
"background": { "fill": "#054f8e", "radius": 5, "padding": { "top": 30, "right": 13, "bottom": 31, "left": 12 } }
},
"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 } }
]
},
// ── Offer row: the UP/TO stack beside the amount. Tilt and pulse live on the row; the two
// members and the derived background all follow it through the graph.
{
"id": "sag-offer",
"type": "flowBox",
"zIndex": 2,
"layout": { "mode": "flow", "16:9": { "flowIn": "sag-stack", "w": "content", "h": "content", "rotation": -3 } },
"props": { "direction": "row", "alignAcross": "centre", "gap": 11 },
"animations": [
{ "animation": "pulse", "params": { "scale": 1.05, "periodInFrames": 40 },
"at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 30 } }
]
},
{
"id": "sag-up-to",
"type": "text",
"zIndex": 3,
"layout": { "mode": "flow", "16:9": { "flowIn": "sag-offer", "w": "content", "h": "content" } },
"typography": { "16:9": { "family": "GT Eesti Display", "weight": 400, "size": 50, "color": "#ffffff", "case": "uppercase", "tracking": 1.3, "lineHeight": 0.97 } },
"props": { "content": [ { "value": "Up\nTo" } ] } // hard line break in content-width text — see Stress points
},
{
"id": "sag-amount",
"type": "text",
"zIndex": 3,
"layout": { "mode": "flow", "16:9": { "flowIn": "sag-offer", "w": "content", "h": "content" } },
"typography": { "16:9": { "family": "GT Eesti Display", "weight": 700, "size": 150, "color": "#ffffff", "tracking": 3.7 } },
"props": {
"content": [ { "price": { "amount": 500, "currency": "AUD", "locale": "en-AU" } } ]
}
},
// ── Cashback label and disclaimer, bare blue text, separately timed.
{
"id": "sag-cashback",
"type": "text",
"zIndex": 2,
"layout": { "mode": "flow", "16:9": { "flowIn": "sag-stack", "w": "content", "h": "content" } },
"typography": { "16:9": { "family": "GT Eesti Display", "weight": 700, "size": 95, "color": "#054f8e", "tracking": 2.4 } },
"props": {
"content": [ { "value": "Cashback*" } ],
"patterns": [ { "expression": "\\*$", "typography": { "size": { "ofRoot": 0.58 }, "baseline": 0.45 } } ]
},
"animations": [
{ "animation": "slideFade", "params": { "from": "top", "distancePercent": 60, "durationInFrames": 12 },
"at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 14 } }
]
},
{
"id": "sag-disclaimer",
"type": "text",
"zIndex": 2,
"layout": { "mode": "flow", "16:9": { "flowIn": "sag-stack", "w": "content", "h": "content" } },
"typography": { "16:9": { "family": "GT Eesti Display", "weight": 400, "size": 17, "color": "#054f8e", "tracking": 0.4 } },
"props": { "content": [ { "value": "*Via Redemption Via Digital Visa Gift Card. T&Cs Apply." } ] },
"animations": [
{ "animation": "slideFade", "params": { "from": "top", "distancePercent": 60, "durationInFrames": 12 },
"at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 20 } }
]
}
]
}
| What the notes want editable | Bound field | Control |
|---|---|---|
| Logo | sag-logo.props.src |
asset — see Stress points |
| Cashback figure | sag-amount.props.content[0].price.amount |
number |
| Text values | sag-*.props.content[0].value |
text |
| Panel colours | sag-headline.props.background.fill, sag-offer-bg.props.fill |
colour |
| Pulse toggle per panel | sag-{headline,offer}.animations[pulse].params.scale |
see Stress points |
What the engine does with it¶
Resolve. $500 becomes $ and 500 parts through Intl.NumberFormat("en-AU"). Every at
lands on start plus its stagger. pulse on sag-offer expands once; sag-up-to, sag-amount
and sag-offer-bg will inherit it rather than carrying copies.
Solve. Order: the texts and the image, then sag-offer, then sag-offer-bg (its of edge
points at the row), then sag-stack. The logo measures 308 wide and takes its height from the
file's aspect. The row packs UP/TO and $500 centred on the cross axis; the background takes the
row's extents plus its own padding. The root column stacks five members.
Animate. sag-offer's −3° and its pulse become one matrix; both members inherit it through
flowIn, and sag-offer-bg re-derives from the row's animated box every frame so the red panel
tilts and breathes with the text without an animation of its own. Its wipe is the one thing that
doesn't fit — see Stress points.
Place. Nine elements. The panel is painted from a derived box; the badge-style asterisk is a
pattern-styled run on the same baseline as Cashback.
What this shows¶
- A multi-text panel is a
background, a single-text panel isprops.background. The blue panel and the red panel look identical to an author and are two different mechanisms. - Animate the container, not the parts. Tilt and pulse on
sag-offerreach both runs and the derived panel through edges; the built code has to threadrotationandpulsethrough every child by hand. - The amount is a number.
price.amount: 500is bindable to campaign data; the builtlockupTextSchemahero is a typed string"$500".
Stress points¶
wipeis not a transform or opacity animation — see the Home Appliance page; this component wipes aderivedbackground too, so the same gap applies to a layer that has no intrinsic box of its own to clip against.- No per-member spacing in a
flowBox— four distinct gaps (32 / 13 / 23 / 26) collapsed to onegap: 24, and the builtoffset.x: -19on the offer panel has no home at all (Positioning →flowBox). - Swapping an image is not a numeric or text leaf. The notes say "logo is changeable"; the bindings rule (Positioning → Bindings target numeric leaves only) admits numbers and text values only, so
props.src(an asset ref) is unbindable as written. Asset refs need to be an explicitly allowed leaf kind. - Hard line breaks in content-sized text are undefined.
"Up\nTo"atw: "content"relies on\nforcing a break in a layer that "never wraps" (Text Foundation → Point/box consolidation). Authored breaks and wrap-driven breaks need to be stated as separate things. - Pulse pivot and per-part pulse toggle — same as Home Appliance. Here the pivot problem is milder because the pulse is authored on the offer row itself, but
sag-headlinestill pulses about its own centre rather than the lockup's. - Opacity inheritance through
of. The red panel wipes on 4 frames after the headline; if an author instead faded the row, the derived background would not fade with it (Motion Graph → Opacity along edges). Correct per the rule, but an author will expect a panel to fade with its contents — worth a line in the brief.