Example: 50% Off — Audio¶
Manifest row 13 · built today as betta-category-discount
One tilted red panel with a soft shadow: a small two-line UP/TO beside a huge 50, then a column
of % over a small OFF*, the whole thing leaning −5.6°. Under it, AUDIO in blue with no panel.
The panel wipes on and pulses; AUDIO fades on and slides down, and pulses too.
flowchart TB
subgraph scene["scene (16:9 graph)"]
ROOT["cd-stack<br/>flowBox · column · absolute"]
OFFER["cd-offer<br/>flowBox · row · alignAcross capHeight · flow · rotation -5.6 · pulse"]
BG["cd-offer-bg<br/>background · derived of cd-offer"]
UPTO["cd-up-to<br/>text 'UP / TO' · flow"]
HERO["cd-hero<br/>text '50' · flow"]
UNIT["cd-unit-col<br/>flowBox · column · flow"]
PCT["cd-percent<br/>text '%' · flow"]
OFF["cd-off<br/>text 'OFF*' · flow"]
CAT["cd-category<br/>text 'AUDIO' · flow · pulse"]
OFFER -- flowIn --> ROOT
BG -- of --> OFFER
UPTO -- flowIn --> OFFER
HERO -- flowIn --> OFFER
UNIT -- flowIn --> OFFER
PCT -- flowIn --> UNIT
OFF -- flowIn --> UNIT
CAT -- flowIn --> ROOT
end
The document¶
{
"id": "scene-category-discount",
"durationInFrames": 75,
"layers": [
// ── The red panel, derived from the offer row. Shadow is paint-only and widens `extents`.
{
"id": "cd-offer-bg",
"type": "background",
"zIndex": 0,
"layout": { "mode": "derived", "16:9": { "of": ["cd-offer"], "ofBounds": "extents" } },
"props": {
"fill": "#e00711", "radius": 4,
"padding": { "top": 36, "right": 20, "bottom": 29, "left": 26 }
},
"animations": [
{ "animation": "wipe", "params": { "direction": "from-left", "durationInFrames": 12 },
"at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 0 } }
]
},
{
"id": "cd-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": 60 }
},
// ── Offer row. capHeight alignment puts the top of "UP" on the cap line of "50", which is
// exactly where the PSD has it (the built code nudges the stack up 35px to get there).
{
"id": "cd-offer",
"type": "flowBox",
"zIndex": 2,
"layout": { "mode": "flow", "16:9": { "flowIn": "cd-stack", "w": "content", "h": "content", "rotation": -5.6 } },
"props": { "direction": "row", "alignAcross": "capHeight", "gap": 24 },
"animations": [
{ "animation": "pulse", "params": { "scale": 1.05, "periodInFrames": 40 },
"at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 30 } }
]
},
{
"id": "cd-up-to",
"type": "text",
"zIndex": 3,
"layout": { "mode": "flow", "16:9": { "flowIn": "cd-offer", "w": "content", "h": "content" } },
"typography": { "16:9": { "family": "GT Eesti Display", "weight": 400, "size": 60, "color": "#ffffff", "case": "uppercase", "lineHeight": 0.95 } },
"props": { "content": [ { "value": "Up\nTo" } ] }
},
{
"id": "cd-hero",
"type": "text",
"zIndex": 3,
"layout": { "mode": "flow", "16:9": { "flowIn": "cd-offer", "w": "content", "h": "content" } },
"typography": { "16:9": { "family": "GT Eesti Display", "weight": 700, "size": 345, "color": "#ffffff" } },
"props": { "content": [ { "value": "50" } ] } // a bound integer typed as text — see Stress points
},
// ── "%" over "OFF*": a tight column. Its top edge stands in for a cap line (non-text fallback).
{
"id": "cd-unit-col",
"type": "flowBox",
"zIndex": 3,
"layout": { "mode": "flow", "16:9": { "flowIn": "cd-offer", "w": "content", "h": "content" } },
"props": { "direction": "column", "alignAcross": "centre", "gap": 10 }
},
{
"id": "cd-percent",
"type": "text",
"zIndex": 4,
"layout": { "mode": "flow", "16:9": { "flowIn": "cd-unit-col", "w": "content", "h": "content" } },
"typography": { "16:9": { "family": "GT Eesti Display", "weight": 700, "size": 225, "color": "#ffffff" } },
"props": { "content": [ { "value": "%" } ] }
},
{
"id": "cd-off",
"type": "text",
"zIndex": 4,
"layout": { "mode": "flow", "16:9": { "flowIn": "cd-unit-col", "w": "content", "h": "content" } },
"typography": { "16:9": { "family": "GT Eesti Display", "weight": 700, "size": 86, "color": "#ffffff", "case": "uppercase" } },
"props": {
"content": [ { "value": "Off*" } ],
"patterns": [ { "expression": "\\*$", "typography": { "size": { "ofRoot": 0.45 }, "baseline": 0.6 } } ]
}
},
// ── Category word: bare blue, its own entrance, its own pulse.
{
"id": "cd-category",
"type": "text",
"zIndex": 2,
"layout": { "mode": "flow", "16:9": { "flowIn": "cd-stack", "w": "content", "h": "content" } },
"typography": { "16:9": { "family": "GT Eesti Display", "weight": 700, "size": 150, "color": "#054f8e", "case": "uppercase" } },
"props": { "content": [ { "value": "Audio" } ] },
"animations": [
{ "animation": "slideFade", "params": { "from": "top", "distancePercent": 60, "durationInFrames": 12 },
"at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 10 } },
{ "animation": "pulse", "params": { "scale": 1.05, "periodInFrames": 40 },
"at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 30 } }
]
}
]
}
| What the notes want editable | Bound field | Control |
|---|---|---|
| Percentage | cd-hero.props.content[0].value |
text today; should be a number — see Stress points |
| Every run's text | cd-*.props.content[0].value |
text |
| Panel colour | cd-offer-bg.props.fill |
colour |
| Category colour | cd-category.typography[ratio].color |
colour |
| "UP TO" on/off | — | see Stress points |
| Pulse toggle per part | cd-{offer,category}.animations[pulse].params.scale |
see Stress points |
Panel shadows are layer effects
Each panel above carries a drop shadow in the source artwork. A shadow is a per-layer
effect, applied to the whole layer (glyphs and pill together), not a field on
background — see Coordinate System & Sizing.
Where text and its panel need different shadows, they become two layers: a text layer
and a background layer spanning it, each with its own
effects.
What the engine does with it¶
Resolve. Two ats on start (frames 0 and 10), two pulses from frame 30; the OFF* pattern
resolves its ofRoot.
Solve. Texts, then cd-unit-col, then cd-offer, then cd-offer-bg, then cd-stack. The
row aligns UP/TO's first-line cap and 50's cap on one line; cd-unit-col is a non-text member,
so its top edge stands in for a cap line and %'s ink top lands on 50's cap line, which is where
the PSD puts it. The background wraps the row's extents plus padding; the shadow spread is part of
those extents.
Animate. The row's −5.6° and pulse propagate to its five descendants and the derived panel.
AUDIO slides and pulses on its own.
Place. Eleven elements; the panel's shadow paints under everything else in the stack.
What this shows¶
- Three nested containers, one authored tilt. The whole panel leans because one
flowBoxhasrotation: -5.6; nothing inside it knows. capHeightearns its place twice — once for text (UPto50) and once through the non-text fallback (%'s column top to50's cap).- A "component" in this system is a subtree, not a file: cut
cd-offerand its descendants out and they are a reusable offer panel on their own.
Stress points¶
- No number run.
50is a percentage the client will change per campaign; the content-item union has text runs and price runs only, so it's a typed string here and would be bound as text. Anumberrun (Intlpercent/unit/ plain decimal, with the same role-typography treatment as price parts) is the obvious third variant (Price already caches one formatter per locale). - Removing a run needs a layout-affecting "collapsed" state — see Betta Buys.
wipeis not transform/opacity, no per-member spacing (unitGap: 0vs rowgap: 24— the%column should touch50, it doesn't here), pulse pivot, per-part pulse toggle,background.shadowunspecified — as on the Home Appliance page.