Example: Boxing Day banner¶
Manifest row 02 · built today as betta-boxing-day-banner
Three stacked panels, each hugging its own copy and centred on the others: BOXING in red on a white
panel tilted 4.8° anticlockwise, DAY on a second white panel tilted 5.3° the other way and tucked
straight under it, and a blue offer panel tilted 3.4° reading UP TO as a small two-line stack
beside 50% OFF*. Panels wipe on from the left in a stagger and all three breathe on one beat. The
red canvas in the reference is the scene's background, not the component.
flowchart TB
subgraph scene["scene-boxing-day"]
ROOT["bd-lockup<br/>flowBox column · absolute (120,120)"]
BOXING["bd-boxing<br/>text · flow · rotation −4.8 · pulse"]
DAY["bd-day<br/>text · flow · rotation 5.3 · pulse"]
OFFER["bd-offer<br/>flowBox row · flow · rotation −3.4 · pulse"]
UPTO["bd-offer-prefix<br/>text 'UP TO' · flow"]
HERO["bd-offer-hero<br/>text '50% OFF*' · flow"]
OFFERBG["bd-offer-bg<br/>background · derived"]
BOXING -- flowIn --> ROOT
DAY -- flowIn --> ROOT
UPTO -- flowIn --> OFFER
HERO -- flowIn --> OFFER
OFFER -- flowIn --> ROOT
OFFERBG -- of --> OFFER
end
The document¶
{
"id": "scene-boxing-day",
"durationInFrames": 75,
"layers": [
{
"id": "bd-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 */ }
},
// Source gaps are 0 (BOXING→DAY) then 22 (DAY→offer). One gap here — see Stress points.
"props": { "direction": "column", "alignAcross": "centre", "gap": 11 }
},
{
"id": "bd-boxing",
"type": "text",
"zIndex": 1,
"layout": {
"mode": "flow",
"16:9": { "flowIn": "bd-lockup", "w": "content", "h": "content", "rotation": -4.8 },
"9:16": { /* … */ }, "1:1": { /* … */ }
},
"typography": {
"16:9": { "family": "GT Eesti Display", "weight": 700, "size": 192, "color": "#ed1c24",
"case": "uppercase", "tracking": 4.8, "horizontalAlign": "center" }
},
"props": {
"content": [ { "value": "Boxing" } ],
"background": { "fill": "#ffffff", "radius": 18,
"padding": { "top": 36, "right": 44, "bottom": 37, "left": 39 } }
},
"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": "bd-day",
"type": "text",
"zIndex": 2,
"layout": {
"mode": "flow",
"16:9": { "flowIn": "bd-lockup", "w": "content", "h": "content", "rotation": 5.3 },
"9:16": { /* … */ }, "1:1": { /* … */ }
},
"typography": {
"16:9": { "family": "GT Eesti Display", "weight": 700, "size": 192, "color": "#ed1c24",
"case": "uppercase", "tracking": 4.8, "horizontalAlign": "center" }
},
"props": {
"content": [ { "value": "Day" } ],
"background": { "fill": "#ffffff", "radius": 12,
"padding": { "top": 38, "right": 45, "bottom": 42, "left": 45 } }
},
"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: tilted row of two texts with one blue background. The source also shoves the
// whole panel 44px right of the column's centre line — see Stress points.
{
"id": "bd-offer",
"type": "flowBox",
"zIndex": 1,
"layout": {
"mode": "flow",
"16:9": { "flowIn": "bd-lockup", "w": "content", "h": "content", "rotation": -3.4 },
"9:16": { /* … */ }, "1:1": { /* … */ }
},
"props": { "direction": "row", "alignAcross": "centre", "gap": 27 },
"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": "bd-offer-bg",
"type": "background",
"zIndex": 1,
"layout": {
"mode": "derived",
"16:9": { "of": ["bd-offer"], "ofBounds": "content" },
"9:16": { /* … */ }, "1:1": { /* … */ }
},
"props": { "fill": "#054f8e", "radius": 10,
"padding": { "top": 44, "right": 42, "bottom": 24, "left": 41 } }
},
{
"id": "bd-offer-prefix",
"type": "text",
"zIndex": 2,
"layout": {
"mode": "flow",
// Source nudges the stack 18px DOWN off the hero's ink centre — see Stress points.
"16:9": { "flowIn": "bd-offer", "w": "content", "h": "content" },
"9:16": { /* … */ }, "1:1": { /* … */ }
},
"typography": {
"16:9": { "family": "GT Eesti Display", "weight": 700, "size": 72, "color": "#ffffff",
"case": "uppercase", "tracking": 1.8, "lineHeight": 1, "multiline": true,
"horizontalAlign": "center" }
},
"props": { "content": [ { "value": "Up\nTo" } ] }
},
{
"id": "bd-offer-hero",
"type": "text",
"zIndex": 2,
"layout": {
"mode": "flow",
"16:9": { "flowIn": "bd-offer", "w": "content", "h": "content" },
"9:16": { /* … */ }, "1:1": { /* … */ }
},
"typography": {
"16:9": { "family": "GT Eesti Display", "weight": 700, "size": 175, "color": "#ffffff",
"case": "uppercase", "tracking": 1.75 }
},
"props": {
"content": [ { "value": "50% off*" } ],
"patterns": [
{ "expression": "%", "typography": { "tracking": -31 } },
{ "expression": "\\*$", "typography": { "size": { "ofRoot": 0.58 }, "baseline": 0.45 } }
]
}
}
]
}
| Note | Field path | Control |
|---|---|---|
| Editable text | bd-boxing.props.content[0].value, bd-day…, bd-offer-prefix…, bd-offer-hero… |
text |
| Change colours | *.props.background.fill, bd-offer-bg.props.fill, *.typography[ratio].color |
colour |
| Pulse toggle per panel | bd-boxing.animations[1], bd-day.animations[1], bd-offer.animations[1] — on/off |
no bindable leaf — see Stress points |
| Tilt | *.layout[ratio].rotation |
degrees |
What the engine does with it¶
Resolve. Wipes at frames 0, 4, 8; pulse expands to a continuous scale track on each of the
three panels; the asterisk's { ofRoot: 0.58 } resolves against 175.
Solve. Texts first, then bd-offer, then bd-lockup, then bd-offer-bg. All texts are
content-sized, so measure runs unconstrained; the prefix wraps at its \n. The row packs the two
texts on resting extents with a 27px gap; the column stacks its three members on their resting
boxes and centres them, so the tilts never change the packing.
Animate. bd-offer's rotation, wipe and pulse compose into one matrix that its two texts
inherit. bd-offer-bg re-derives from the row's animated extents — and here the row is rotated, so
the union it takes is the axis-aligned box around a tilted row, not the tilted blue panel the
reference shows (Stress points, 6).
Place. Seven elements. The two single-word panels are rotated text layers with pills; the
offer panel is three placed elements under one rotated parent.
What this shows¶
- Same shape as Black Friday, one fewer level. With no side-by-side title, the column holds the three panels directly; only the offer needs a nested row.
- Rotation is a per-member field with no layout cost. Three different tilts, one packed column.
- Where the derived-background model runs out. A rotated group with a spanning background is the one thing this composition can't express faithfully today.
Stress points¶
- Rotated group with a spanning background (this component's main finding).
bd-offeris a tilted row; its background isderivedfrom it. The union is computed from bounds pushed through member matrices (The Motion Graph → Internal representation), giving an axis-aligned box around the tilted row. Abackgroundhas nolayoutfields to carry a rotation, andofedges carry geometry only (The Motion Graph → Opacity along edges), so it can't inherit the row's tilt. Either aderivedlayer needs to derive in its members' local frame and then adopt their composed transform, or the brief needs a rule for which frame the union is taken in (Background says neither). - Per-member nudge / uneven gaps. Source gaps 0 and 22 in one column; the offer panel shifted
44px right of centre (
offset: { x: 44 }); theUP TOstack pushed 18px down. Same hole as 01 — nomarginoroffseton a flow member (Positioning → flowBox vocabulary). - Pulse pivot — shared lockup centre in the built code vs. own content centre in the brief,
with
pinPointunavailable to a flow member. Same as 01. - Wipe is a clip animation, not a transform/opacity change. Same as 01.
- Pulse toggle isn't bindable. Same as 01.
rotationon a flow member is assumed allowed but isn't in theFlowMemberblock. Same as 01.