Skip to content

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

  1. Rotated group with a spanning background (this component's main finding). bd-offer is a tilted row; its background is derived from 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. A background has no layout fields to carry a rotation, and of edges carry geometry only (The Motion Graph → Opacity along edges), so it can't inherit the row's tilt. Either a derived layer 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).
  2. Per-member nudge / uneven gaps. Source gaps 0 and 22 in one column; the offer panel shifted 44px right of centre (offset: { x: 44 }); the UP TO stack pushed 18px down. Same hole as 01 — no margin or offset on a flow member (Positioning → flowBox vocabulary).
  3. Pulse pivot — shared lockup centre in the built code vs. own content centre in the brief, with pinPoint unavailable to a flow member. Same as 01.
  4. Wipe is a clip animation, not a transform/opacity change. Same as 01.
  5. Pulse toggle isn't bindable. Same as 01.
  6. rotation on a flow member is assumed allowed but isn't in the FlowMember block. Same as 01.