Example: the catalogue flag¶
Manifest row 19 · built today as betta-catalogue-flag
A yellow pennant hanging from the top edge of the canvas in the top-left corner: CATALOGUE in
brand blue over a small, slightly tilted blue panel reading OUT NOW in white. The pennant's top is
flat against the canvas edge, its bottom corners are rounded, and its bottom edge slopes up from the
deep left corner to the right. The whole flag drops in from above the frame; the OUT NOW panel
pulses on the shared beat.
flowchart TB
subgraph scene["scene-flag (one motion graph per ratio)"]
CONTENT["flag-content<br/>flowBox · column · absolute<br/>slideFade from top"]
HEAD["flag-headline<br/>text CATALOGUE · flow"]
BANNER["flag-banner<br/>text OUT NOW · flow · pulse"]
BG["flag-pennant<br/>background · derived · fade"]
HEAD -- "flowIn" --> CONTENT
BANNER -- "flowIn" --> CONTENT
BG -- "of" --> CONTENT
end
The document¶
{
"id": "scene-flag",
"durationInFrames": 75,
"layers": [
// ── The pennant. Derived from the copy's extents plus the flag's own inset; the top
// padding is the drop from the canvas edge to the headline. Radius 14 rounds every
// corner, so the content sits at y: 72 to push the pennant's top (72 - 86 = -14) off
// the canvas and hide the two top corners — see Stress points for the slant.
{
"id": "flag-pennant",
"type": "background",
"zIndex": 0,
"layout": {
"mode": "derived",
"16:9": { "of": ["flag-content"], "ofBounds": "extents" },
"9:16": { "of": ["flag-content"], "ofBounds": "extents" },
"1:1": { "of": ["flag-content"], "ofBounds": "extents" }
},
"props": {
"fill": "#FFD100",
"padding": { "top": 86, "right": 30, "bottom": 93, "left": 30 },
"radius": 14
},
"animations": [
{ "animation": "fade", "params": { "from": 0, "durationInFrames": 14 },
"at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 0 } }
]
},
// ── The copy column. Content-sized both ways; its top-centre anchor is what the
// slide-down animation moves.
{
"id": "flag-content",
"type": "flowBox",
"zIndex": 1,
"layout": {
"mode": "absolute",
"16:9": { "x": 300, "y": 72, "w": "content", "h": "content", "anchor": "top-centre" },
"9:16": { "x": 200, "y": 72, "w": "content", "h": "content", "anchor": "top-centre" },
"1:1": { "x": 200, "y": 72, "w": "content", "h": "content", "anchor": "top-centre" }
},
"props": { "direction": "column", "alignAcross": "centre", "gap": 19 },
"animations": [
{ "animation": "slideFade", "params": { "from": "top", "distance": 260, "durationInFrames": 14 },
"at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 0 } }
]
},
// ── First member: the bare headline.
{
"id": "flag-headline",
"type": "text",
"zIndex": 2,
"layout": {
"mode": "flow",
"16:9": { "flowIn": "flag-content", "w": "content", "h": "content" },
"9:16": { "flowIn": "flag-content", "w": "content", "h": "content" },
"1:1": { "flowIn": "flag-content", "w": "content", "h": "content" }
},
"typography": {
"16:9": { "family": "GT Eesti Display", "weight": 700, "size": 54, "color": "#054F8E",
"case": "uppercase", "horizontalAlign": "center" }
// "9:16": …, "1:1": …
},
"props": { "content": [ { "value": "Catalogue" } ] }
},
// ── Second member: the tilted blue panel. Rotation is paint-level, so the tilt never
// reflows the column; the pulse scales it about its own content centre.
{
"id": "flag-banner",
"type": "text",
"zIndex": 2,
"layout": {
"mode": "flow",
"16:9": { "flowIn": "flag-content", "w": "content", "h": "content", "rotation": 3.1 },
"9:16": { "flowIn": "flag-content", "w": "content", "h": "content", "rotation": 3.1 },
"1:1": { "flowIn": "flag-content", "w": "content", "h": "content", "rotation": 3.1 }
},
"typography": {
"16:9": { "family": "GT Eesti Display", "weight": 700, "size": 49, "color": "#FFFFFF",
"case": "uppercase", "horizontalAlign": "center" }
},
"props": {
"content": [ { "value": "Out Now" } ],
"background": { "fill": "#054F8E", "padding": { "top": 14, "right": 24, "bottom": 13, "left": 30 }, "radius": 2 }
},
"animations": [
{ "animation": "pulse", "params": { "scale": 1.05, "periodInFrames": 40 },
"at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 30 } }
]
}
]
}
| Bound field | Control |
|---|---|
flag-headline.props.content[0].value |
text |
flag-banner.props.content[0].value |
text |
flag-pennant.props.fill, flag-banner.props.background.fill |
colour |
flag-headline.typography.*.color, flag-banner.typography.*.color |
colour |
flag-banner.layout.*.rotation |
degrees |
What the engine does with it¶
Resolve. Both at references are this scene's own start. slideFade expands into a y
track (−260 → 0) and an opacity track; fade into opacity alone; pulse into a looping
scale track (Keyframe Animation).
Solve. Order: flag-headline, flag-banner, flag-content, flag-pennant. Both text layers
are content-sized so measure runs with no width constraint. The column packs them centred with a
19 px gap and its top-centre lands at (300, 72). The pennant takes the column's extents and adds
its four paddings, so its top edge sits 14 px above the canvas and its width follows whatever the
longer of the two words measures (Coordinate System & Sizing).
Animate. During the drop the column's matrix carries a translate; both members inherit it
through flow membership, and the pennant re-derives from the column's animated extents so it
drops too. Its opacity is its own fade track on the same timing. From frame 30 the banner
scales about its own content centre; nothing else moves (The Motion Graph).
Place. Four elements. The canvas edge clips the pennant's off-canvas top.
What this shows¶
- Width follows copy. Change
CataloguetoWinter Catalogueand the pennant widens by the measured difference in every ratio, with no coordinate edited. - One animation, two inheritance paths. The drop is authored once on the column; members ride its matrix, the pennant tracks its box.
- Paint-level tilt. The banner's
rotationnever reflows the column above or below it.
Stress points¶
- The slanted bottom edge can't be expressed. The reference pennant is a parallelogram with a
flat top and a bottom edge sloping up 9.5° (the built component uses CSS
skewYon an over-tall fill).backgroundis a rounded rectangle; the transform vocabulary is position/scale/rotation only (Coordinate System & Sizing → Rotation and scale); ashape: polygonpinned withw: targetFraction 1would track width but has no corner radius. Askew: { x, y }transform field is the natural fix — a 4×4 matrix holds it for free — and with the top edge already off-canvas the skewed rectangle is the reference shape. - A derived background cannot inherit the opacity of the group it wraps. The pennant follows the
column's geometry through
ofbut not its fade, so the same timing is authored twice (The Motion Graph → Opacity along edges). GivingflowBoxa paint-onlybackground(fill/padding/radius, like text's pill) would make the common "panel around a group" case one layer that inherits everything, and would also give containers the padding they currently lack. radiusis uniform. A flag wants square top corners and round bottom ones; the workaround here is hiding the top corners above the canvas edge. Per-corner radius onbackground(Background) is a small addition.- Corner placement via
targetFractionneeds fraction + px. A top-right variant wantsx = canvas width − 120;{ value: 1, unit: "targetFraction" }gives the edge and there is no way to add a px offset to it (Coordinate System & Sizing → targetFraction). The same gap appears whenever a pinned layer wants "target width plus padding". - Naming:
absoluteusesx/y,pinusesoffsetX/offsetY. The targetFraction section says it applies tooffsetX/offsetY/w/h; anabsolutelayer has no such fields. The brief should sayx/ytake it too, or unify the names. rotationon a flow member's layout block — theFlowMembertype in Positioning listsflowIn/w/h/alignAcrossSelf/growonly; rotation/scale are described as universal per-ratio fields elsewhere. Assumed present here.