Example: Black Friday banner¶
Manifest row 01 · built today as betta-black-friday-banner
Two black panels, BLACK tilted 5° one way and FRIDAY 5° the other, sitting shoulder to shoulder
over a red offer panel that reads UP TO as a small two-line stack beside a huge 50% OFF*. Under
that, a bare black tagline SALE ON NOW with the first word bold. Every panel hugs its own copy, so a
longer headline widens its panel and the rows below re-centre under it. Panels wipe on from the left
in a stagger, the tagline fades up into place, and the three panels breathe on a shared beat.
flowchart TB
subgraph scene["scene-black-friday"]
ROOT["bf-lockup<br/>flowBox column · absolute (120,120)"]
TITLE["bf-title<br/>flowBox row · flow"]
BLACK["bf-black<br/>text · flow · rotation −5 · pulse"]
FRIDAY["bf-friday<br/>text · flow · rotation 5 · pulse"]
OFFER["bf-offer<br/>flowBox row · flow · pulse"]
UPTO["bf-offer-prefix<br/>text 'UP TO' · flow"]
HERO["bf-offer-hero<br/>text '50% OFF*' · flow"]
OFFERBG["bf-offer-bg<br/>background · derived"]
TAG["bf-tagline<br/>text · flow · slideFade"]
BLACK -- flowIn --> TITLE
FRIDAY -- flowIn --> TITLE
TITLE -- flowIn --> ROOT
UPTO -- flowIn --> OFFER
HERO -- flowIn --> OFFER
OFFER -- flowIn --> ROOT
OFFERBG -- of --> OFFER
TAG -- flowIn --> ROOT
end
The document¶
{
"id": "scene-black-friday",
"durationInFrames": 75,
"layers": [
// ── Root column. Content-sized both ways: its box IS the lockup. Yellow canvas background
// from the reference is the scene's own backgroundColor, not part of this component.
{
"id": "bf-lockup",
"type": "flowBox",
"zIndex": 0,
"layout": {
"mode": "absolute",
"16:9": { "x": 120, "y": 120, "w": "content", "h": "content", "anchor": "top-left" },
"9:16": { /* own x/y */ }, "1:1": { /* own x/y */ }
},
// One column gap. The source has three different gaps (6 / −11 / 20) — see Stress points.
"props": { "direction": "column", "alignAcross": "centre", "gap": 20 }
},
// ── Title row: BLACK + FRIDAY side by side, tops aligned.
{
"id": "bf-title",
"type": "flowBox",
"zIndex": 1,
"layout": {
"mode": "flow",
"16:9": { "flowIn": "bf-lockup", "w": "content", "h": "content" },
"9:16": { /* … */ }, "1:1": { /* … */ }
},
"props": { "direction": "row", "alignAcross": "top", "gap": 6 }
},
{
"id": "bf-black",
"type": "text",
"zIndex": 2,
"layout": {
"mode": "flow",
"16:9": { "flowIn": "bf-title", "w": "content", "h": "content", "rotation": -5 },
"9:16": { /* … */ }, "1:1": { /* … */ }
},
"typography": {
"16:9": { "family": "GT Eesti Display", "weight": 700, "size": 197, "color": "#ffffff",
"case": "uppercase", "tracking": 2, "horizontalAlign": "center" }
},
"props": {
"content": [ { "value": "Black" } ],
"background": { "fill": "#000000", "radius": 9,
"padding": { "top": 49, "right": 37, "bottom": 49, "left": 36 } }
},
"animations": [
{ "animation": "wipe", "params": { "direction": "from-left", "durationInFrames": 12 },
"at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 0 } },
{ "animation": "pulse", "params": { "scale": 1.05, "periodInFrames": 40, "delay": 30 },
"at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 0 } }
]
},
{
"id": "bf-friday",
"type": "text",
"zIndex": 2,
"layout": {
"mode": "flow",
// The source nudges FRIDAY 8px down off the row's top edge — see Stress points.
"16:9": { "flowIn": "bf-title", "w": "content", "h": "content", "rotation": 5 },
"9:16": { /* … */ }, "1:1": { /* … */ }
},
"typography": {
"16:9": { "family": "GT Eesti Display", "weight": 700, "size": 196, "color": "#ffffff",
"case": "uppercase", "tracking": 2, "horizontalAlign": "center" }
},
"props": {
"content": [ { "value": "Friday" } ],
"background": { "fill": "#000000", "radius": 6,
"padding": { "top": 48, "right": 39, "bottom": 57, "left": 39 } }
},
"animations": [
{ "animation": "wipe", "params": { "direction": "from-left", "durationInFrames": 12 },
"at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 4 } },
{ "animation": "pulse", "params": { "scale": 1.05, "periodInFrames": 40, "delay": 30 },
"at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 0 } }
]
},
// ── Offer panel: a row of two texts with one red background spanning both. The panel is
// pulled up 11px under the title row in the source (negative gap) — see Stress points.
{
"id": "bf-offer",
"type": "flowBox",
"zIndex": 1,
"layout": {
"mode": "flow",
"16:9": { "flowIn": "bf-lockup", "w": "content", "h": "content" },
"9:16": { /* … */ }, "1:1": { /* … */ }
},
"props": { "direction": "row", "alignAcross": "centre", "gap": 6 },
"animations": [
{ "animation": "wipe", "params": { "direction": "from-left", "durationInFrames": 12 },
"at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 8 } },
{ "animation": "pulse", "params": { "scale": 1.05, "periodInFrames": 40, "delay": 30 },
"at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 0 } }
]
},
{
"id": "bf-offer-bg",
"type": "background",
"zIndex": 1,
"layout": {
"mode": "derived",
"16:9": { "of": ["bf-offer"], "ofBounds": "content" },
"9:16": { /* … */ }, "1:1": { /* … */ }
},
"props": { "fill": "#ed1c24", "radius": 5,
"padding": { "top": 29, "right": 35, "bottom": 42, "left": 27 } }
},
{
"id": "bf-offer-prefix",
"type": "text",
"zIndex": 2,
"layout": {
"mode": "flow",
// Source nudges this stack 23px up off the hero's ink centre — see Stress points.
"16:9": { "flowIn": "bf-offer", "w": "content", "h": "content" },
"9:16": { /* … */ }, "1:1": { /* … */ }
},
"typography": {
"16:9": { "family": "GT Eesti Display", "weight": 700, "size": 42, "color": "#ffffff",
"case": "uppercase", "tracking": 1, "lineHeight": 1, "multiline": true,
"horizontalAlign": "right" }
},
"props": { "content": [ { "value": "Up\nTo" } ] }
},
{
"id": "bf-offer-hero",
"type": "text",
"zIndex": 2,
"layout": {
"mode": "flow",
"16:9": { "flowIn": "bf-offer", "w": "content", "h": "content" },
"9:16": { /* … */ }, "1:1": { /* … */ }
},
"typography": {
"16:9": { "family": "GT Eesti Display", "weight": 700, "size": 201, "color": "#ffffff",
"case": "uppercase", "tracking": 2 }
},
"props": {
"content": [ { "value": "50% off*" } ],
"patterns": [
{ "expression": "%", "typography": { "tracking": -36 } },
{ "expression": "\\*$", "typography": { "size": { "ofRoot": 0.58 }, "baseline": 0.45 } }
]
}
},
// ── Tagline. Bare text, two runs so the first word is bold without a pattern. Fades up into
// place instead of wiping; deliberately no pulse.
{
"id": "bf-tagline",
"type": "text",
"zIndex": 2,
"layout": {
"mode": "flow",
"16:9": { "flowIn": "bf-lockup", "w": "content", "h": "content" },
"9:16": { /* … */ }, "1:1": { /* … */ }
},
"typography": {
"16:9": { "family": "GT Eesti Display", "weight": 400, "size": 150, "color": "#000000",
"case": "uppercase", "tracking": 0 }
},
"props": {
"content": [ { "value": "Sale ", "typography": { "weight": 700 } }, { "value": "on now" } ]
},
"animations": [
{ "animation": "slideFade", "params": { "from": "bottom", "distance": 90, "durationInFrames": 14 },
"at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 14 } }
]
}
]
}
What the notes say a client changes, and where it would bind:
| Note | Field path | Control |
|---|---|---|
| Editable text | bf-black.props.content[0].value, bf-friday…, bf-offer-prefix…, bf-offer-hero…, bf-tagline.props.content[*].value |
text |
| Change colours | *.props.background.fill, bf-offer-bg.props.fill, *.typography[ratio].color |
colour |
| Pulse toggle per panel | bf-black.animations[1], bf-friday.animations[1], bf-offer.animations[1] — on/off |
no bindable leaf exists — see Stress points |
| Tilt | *.layout[ratio].rotation |
degrees |
What the engine does with it¶
Resolve. Every at resolves against this scene's start; the four wipes land at frames 0, 4,
8 and the tagline's slide at 14. Each named animation expands to keyframe tracks — pulse to a
continuous scale track, slideFade to y and opacity tracks — and { ofRoot: 0.58 } on the
asterisk multiplies out against the hero's 201.
Solve. Topological order puts the six text layers first, then bf-title and bf-offer, then
bf-lockup, then bf-offer-bg. Every text is content-sized, so solve calls measure for each with no
width constraint; bf-offer-prefix wraps at its \n. The rows pack on the texts' resting extents —
the tilt is paint, it doesn't widen the slot — and the column centres the three rows. bf-offer-bg
takes the union of its one member's content box, which for a flowBox is its members' extents, and
pads it.
Animate. Each panel's pulse scales it about its own content centre (the built component
breathes about the lockup's shared centre — see Stress points). bf-offer's pulse and wipe reach
bf-offer-prefix and bf-offer-hero through flow membership, and bf-offer-bg re-derives from the
row's animated extents every frame, so the panel breathes with its text. The tagline rides its own
y/opacity tracks.
Place. Nine positioned elements, each with a matrix3d and an opacity; the tilted panels
are rotated text layers whose pills paint around already-placed spans.
What this shows¶
- Panels are text layers with a pill, groups are flowBoxes with a background. A single word
on a panel needs no container at all; a two-text panel becomes a row plus a
derivedbackground. - Nested flow does the two-axis layout. A row inside a column with no absolute coordinate anywhere; a longer headline re-centres everything beneath it for free.
- Animation inheritance is structural. The offer panel's wipe and pulse are authored once on the row; both texts and the background follow without an animation of their own.
Stress points¶
- Pulse pivot. Every pulsing part in the built component scales about the lockup's centre
(
useSharedPulseOrigin), so BLACK and FRIDAY breathe toward one point. Under the brief, rotation and scale always pivot on the layer's own content centre (Coordinate System → Pivot) and thepinPointescape hatch is unavailable to a flow member, because a layer has exactly onelayout.mode. The nearest encoding — onepulseon the rootflowBox— loses the per-panel toggle. Neither the reference's per-panel toggles nor the shared pivot can be had together. - Per-member margin / nudge. The source needs three different column gaps (6, −11, 20), an 8px
drop on FRIDAY, and a 23px lift on the
UP TOstack.flowBoxhas onegap(Positioning → flowBox vocabulary) and a flow member has nomargin/offset. The built code haslockupOffsetSchemafor exactly this reason. Negative gap isn't defined either. - Wipe is a clip animation.
wipereveals with aclip-path, not a transform or opacity. The Motion Graph → What animates admits only position/scale/rotation/opacity as node values, so a named animation can't expand to it. Nearly every note in the manifest says "panels wipe on". - Pulse toggle isn't bindable. Turning an animation on or off per part is neither a numeric
leaf nor a text value (Positioning → Bindings target numeric leaves only).
Binding
params.scaleto1as "off" is a hack. Anenabledboolean on an animation entry, on the binding allow-list, is the missing piece. rotationon a flow/pin member. The brief putsrotation/scalein theabsoluteblock (Positioning → discriminated union) and theFlowMember/PinBoxtypes omit them. Three of this component's tilted panels are flow members. Assumed allowed here; needs stating.- No shared typography base. The built component has one base typography that every part
overrides field by field. Five sibling
textlayers each carry a fulltypographyblock; a client changing the family or colour changes it five times. A component/element system would own this, but the brief has no layer-level inheritance today. - Derived background of a rotated row. If
bf-offerwere tilted (it is, in Boxing Day), its background's union is taken from members' bounds pushed through their matrices (The Motion Graph → Internal representation), which yields an axis-aligned box around a rotated row, not a rotated panel. The background can't inherit the row's rotation becauseofedges carry geometry only. Untilted here, so it only surfaces on 02.