Example: Home Appliance Cashback¶
Manifest row 10 · built today as betta-home-appliance-cashback
A category tab: HOME in white beside a tilted yellow APPLIANCE panel with blue text and a soft
shadow, CASHBACK in white below, then two lines of small print. Everything but the panel is white
on a coloured scene background, which is why the reference composite shows only the panel. The
panel wipes on; every white line fades on and slides down on its own delay; HOME, APPLIANCE
and CASHBACK each pulse.
flowchart TB
subgraph scene["scene (16:9 graph)"]
ROOT["hac-stack<br/>flowBox · column · absolute"]
ROW["hac-title-row<br/>flowBox · row · alignAcross capHeight · flow"]
HOME["hac-home<br/>text 'HOME' · flow · pulse"]
APP["hac-appliance<br/>text 'APPLIANCE' on yellow · flow · rotation -5 · pulse"]
CB["hac-cashback<br/>text 'CASHBACK' · flow · pulse"]
FINE["hac-fine-print<br/>flowBox · column · flow"]
FOOT["hac-footnote<br/>text · flow"]
TERMS["hac-terms<br/>text · flow"]
ROW -- flowIn --> ROOT
HOME -- flowIn --> ROW
APP -- flowIn --> ROW
CB -- flowIn --> ROOT
FINE -- flowIn --> ROOT
FOOT -- flowIn --> FINE
TERMS -- flowIn --> FINE
end
The document¶
{
"id": "scene-home-appliance",
"durationInFrames": 75,
"layers": [
// ── Root column. Content-sized both ways; its box IS the lockup.
// One gap for the whole column (the built component uses 74 / 58 between rows — see Stress points).
{
"id": "hac-stack",
"type": "flowBox",
"layout": {
"mode": "absolute",
"16:9": { "x": 120, "y": 120, "w": "content", "h": "content", "anchor": "top-left" }
// "9:16": …, "1:1": …
},
"props": { "direction": "column", "alignAcross": "centre", "gap": 66 }
},
// ── Title row: HOME beside the panel, aligned on their cap lines. The built code nudges HOME
// down 34px by hand; capHeight alignment says the same thing without a number.
{
"id": "hac-title-row",
"type": "flowBox",
"layout": {
"mode": "flow",
"16:9": { "flowIn": "hac-stack", "w": "content", "h": "content" }
},
"props": { "direction": "row", "alignAcross": "capHeight", "gap": 38 }
},
{
"id": "hac-home",
"type": "text",
"layout": { "mode": "flow", "16:9": { "flowIn": "hac-title-row", "w": "content", "h": "content" } },
"typography": { "16:9": { "family": "GT Eesti Display", "weight": 700, "size": 142, "color": "#ffffff", "case": "uppercase" } },
"props": { "content": [ { "value": "Home" } ] },
"animations": [
{ "animation": "slideFade", "params": { "from": "top", "distancePercent": 40, "durationInFrames": 12 },
"at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 4 } },
{ "animation": "pulse", "params": { "scale": 1.05, "periodInFrames": 40 },
"at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 30 } }
]
},
{
"id": "hac-appliance",
"type": "text",
"layout": {
"mode": "flow",
"16:9": { "flowIn": "hac-title-row", "w": "content", "h": "content", "rotation": -5 }
},
"typography": { "16:9": { "family": "GT Eesti Display", "weight": 700, "size": 137, "color": "#054f8e", "case": "uppercase" } },
"props": {
"content": [ { "value": "Appliance" } ],
"background": {
"fill": "#ffd100", "radius": 4,
"padding": { "top": 35, "right": 19, "bottom": 31, "left": 26 }, // see Stress points
}
},
"effects": [ { "type": "dropShadow", "color": "#000000", "opacity": 0.26, "blurPx": 4, "offsetX": 5, "offsetY": 6 } ],
"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 } }
]
},
// ── CASHBACK, bare white, the base size.
{
"id": "hac-cashback",
"type": "text",
"layout": { "mode": "flow", "16:9": { "flowIn": "hac-stack", "w": "content", "h": "content" } },
"typography": { "16:9": { "family": "GT Eesti Display", "weight": 700, "size": 173, "color": "#ffffff", "case": "uppercase" } },
"props": { "content": [ { "value": "Cashback" } ] },
"animations": [
{ "animation": "slideFade", "params": { "from": "top", "distancePercent": 40, "durationInFrames": 12 },
"at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 10 } },
{ "animation": "pulse", "params": { "scale": 1.05, "periodInFrames": 40 },
"at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 30 } }
]
},
// ── Small print as its own tight column so the two lines sit 15px apart, not 66.
{
"id": "hac-fine-print",
"type": "flowBox",
"layout": { "mode": "flow", "16:9": { "flowIn": "hac-stack", "w": "content", "h": "content" } },
"props": { "direction": "column", "alignAcross": "centre", "gap": 15 }
},
{
"id": "hac-footnote",
"type": "text",
"layout": { "mode": "flow", "16:9": { "flowIn": "hac-fine-print", "w": "content", "h": "content" } },
"typography": { "16:9": { "family": "GT Eesti Display", "weight": 400, "size": 69, "color": "#ffffff", "tracking": -0.7 } },
"props": {
"content": [ { "value": "With Selected Products*" } ],
"patterns": [ { "expression": "\\*$", "typography": { "size": { "ofRoot": 0.58 }, "baseline": 0.45 } } ]
},
"animations": [
{ "animation": "slideFade", "params": { "from": "top", "distancePercent": 40, "durationInFrames": 12 },
"at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 16 } }
]
},
{
"id": "hac-terms",
"type": "text",
"layout": { "mode": "flow", "16:9": { "flowIn": "hac-fine-print", "w": "content", "h": "content" } },
"typography": { "16:9": { "family": "GT Eesti Display", "weight": 400, "size": 29, "color": "#ffffff", "tracking": -0.3 } },
"props": { "content": [ { "value": "*Via Digital Visa Gift Card. Via redemption. T&Cs apply." } ] },
"animations": [
{ "animation": "slideFade", "params": { "from": "top", "distancePercent": 40, "durationInFrames": 12 },
"at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 18 } }
]
}
]
}
| What the notes want editable | Bound field | Control |
|---|---|---|
| Text values | hac-*.props.content[0].value |
text |
| Text colours | hac-*.typography[ratio].color |
colour |
| Panel colour | hac-appliance.props.background.fill |
colour |
| Pulse toggle per part | hac-{home,appliance,cashback}.animations[pulse].params.scale — 1 is "off" |
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. Every at resolves against this scene's start. The four slideFades and the wipe
expand into keyframe tracks staggered 0/4/10/16/18 frames; the three pulses expand into
continuous scale tracks from frame 30. The footnote's asterisk pattern resolves its ofRoot size.
Solve. Topological order: the five text layers, then hac-title-row and hac-fine-print, then
hac-stack. Each text measures at content size; the row packs HOME and APPLIANCE on their cap
lines (the panel's padding is paint-only, so it's the text's cap line, not the yellow box's top);
the columns stack centred. APPLIANCE's −5° is not in the packing.
Animate. Each pulsing layer scales about its own content centre. APPLIANCE composes rotation,
pulse scale and the wipe's clip. Flow membership carries the root's transform (identity here).
Place. Seven positioned elements; the panel's shadow and fill paint around its solved box.
What this shows¶
- capHeight alignment replaces a hand-tuned nudge. The built
offset.y: 34onHOMEexists only because flex has no cap-line alignment;alignAcross: "capHeight"is the intent. - Paint-only padding works as designed. The gap between
HOMEand the panel is measured panel-edge to ink, exactly as the PSD shows, with no double-counting. - Invisible-in-the-reference copy is just white text, which the notes call out; nothing in the schema needs to know the scene background.
Stress points¶
- No per-member spacing in a
flowBox(Positioning →flowBox's layout vocabulary). The source has three distinct row gaps (74, 58, 15). A singlegapforces either a nestedflowBoxper distinct gap or an averaged value; this page uses 66 for two of them. A per-membermargin(or a per-gap list) is the obvious fix. wipeis not a transform or opacity animation (Motion Graph → What animates). A left-to-right reveal is a clip-path animation. The transform-only rule cannot express it, yet every component in this catalogue uses it.- Pulse pivot. The built lockup scales every pulsing part about the lockup's shared centre; each member here pulses about its own content centre (Coordinate System → Pivot). A flow member cannot pin to a
pinPoint(one mode per layer), so the escape hatch is unavailable to exactly the layers that need it. - Per-part pulse toggle has no bindable home (Positioning → Bindings target numeric leaves only). Turning an animation on and off is structural;
params.scale = 1as "off" is a hack, not a design. background.shadowdoesn't exist —backgroundSchemahasfill/opacity/radius/paddingonly. The brief'sextentsdefinition already names "shadow spread" as a paint-only contributor (Coordinate System → Bounds), so the field is implied but unspecified.