Skip to content

Example: Spend and Get

Manifest row 11 · built today as betta-spend-and-get

A partner lockup: a swappable logo (Hisense) over a tilted blue SPEND & GET panel, a red panel tilted the other way carrying a two-line UP TO beside a big $500, Cashback* in blue below, and a one-line blue disclaimer. Panels wipe on; the logo, Cashback* and the disclaimer each fade on and slide down on their own timing; both panels pulse.

flowchart TB
    subgraph scene["scene (16:9 graph)"]
        ROOT["sag-stack<br/>flowBox · column · absolute"]
        LOGO["sag-logo<br/>image · flow · w 308 · h content"]
        HEAD["sag-headline<br/>text 'SPEND & GET' on blue · flow · rotation 3.2 · pulse"]
        OFFER["sag-offer<br/>flowBox · row · alignAcross centre · flow · rotation -3 · pulse"]
        UPTO["sag-up-to<br/>text 'UP / TO' · flow"]
        AMT["sag-amount<br/>text price $500 · flow"]
        BG["sag-offer-bg<br/>background · derived of sag-offer"]
        CB["sag-cashback<br/>text 'Cashback*' · flow"]
        DIS["sag-disclaimer<br/>text · flow"]
        LOGO -- flowIn --> ROOT
        HEAD -- flowIn --> ROOT
        OFFER -- flowIn --> ROOT
        UPTO -- flowIn --> OFFER
        AMT -- flowIn --> OFFER
        BG -- of --> OFFER
        CB -- flowIn --> ROOT
        DIS -- flowIn --> ROOT
    end

The document

{
  "id": "scene-spend-and-get",
  "durationInFrames": 75,
  "layers": [

    // ── The red offer panel is a derived background under a row flowBox, because a panel
    //    holding TWO text layers can't be one text layer's `props.background`.
    {
      "id": "sag-offer-bg",
      "type": "background",
      "zIndex": 0,
      "layout": {
        "mode": "derived",
        "16:9": { "of": ["sag-offer"], "ofBounds": "extents" }
        // "9:16": …, "1:1": …
      },
      "props": { "fill": "#fe2121", "radius": 6, "padding": { "top": 18, "right": 6, "bottom": 12, "left": 5 } },
      "animations": [
        { "animation": "wipe", "params": { "direction": "from-left", "durationInFrames": 12 },
          "at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 8 } }
      ]
    },

    // ── Root column, content-sized, centred.
    {
      "id": "sag-stack",
      "type": "flowBox",
      "zIndex": 1,
      "layout": {
        "mode": "absolute",
        "16:9": { "x": 120, "y": 120, "w": "content", "h": "content", "anchor": "top-left" }
      },
      "props": { "direction": "column", "alignAcross": "centre", "gap": 24 }   // source: 32 / 13 / 23 / 26
    },

    // ── Swappable partner logo, width-driven, aspect-preserving height.
    {
      "id": "sag-logo",
      "type": "image",
      "zIndex": 2,
      "layout": { "mode": "flow", "16:9": { "flowIn": "sag-stack", "w": 308, "h": "content" } },
      "props": { "src": { "kind": "bundled", "path": "effects/betta-spend-and-get/logo.png" } },
      "animations": [
        { "animation": "slideFade", "params": { "from": "top", "distancePercent": 60, "durationInFrames": 12 },
          "at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 0 } }
      ]
    },

    // ── Blue headline panel: one text layer, its own paint-only background.
    {
      "id": "sag-headline",
      "type": "text",
      "zIndex": 2,
      "layout": { "mode": "flow", "16:9": { "flowIn": "sag-stack", "w": "content", "h": "content", "rotation": 3.2 } },
      "typography": { "16:9": { "family": "GT Eesti Display", "weight": 700, "size": 82, "color": "#ffffff", "case": "uppercase", "tracking": 2 } },
      "props": {
        "content": [ { "value": "Spend & Get" } ],
        "background": { "fill": "#054f8e", "radius": 5, "padding": { "top": 30, "right": 13, "bottom": 31, "left": 12 } }
      },
      "animations": [
        { "animation": "wipe", "params": { "direction": "from-left", "durationInFrames": 12 },
          "at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 4 } },
        { "animation": "pulse", "params": { "scale": 1.05, "periodInFrames": 40 },
          "at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 30 } }
      ]
    },

    // ── Offer row: the UP/TO stack beside the amount. Tilt and pulse live on the row; the two
    //    members and the derived background all follow it through the graph.
    {
      "id": "sag-offer",
      "type": "flowBox",
      "zIndex": 2,
      "layout": { "mode": "flow", "16:9": { "flowIn": "sag-stack", "w": "content", "h": "content", "rotation": -3 } },
      "props": { "direction": "row", "alignAcross": "centre", "gap": 11 },
      "animations": [
        { "animation": "pulse", "params": { "scale": 1.05, "periodInFrames": 40 },
          "at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 30 } }
      ]
    },
    {
      "id": "sag-up-to",
      "type": "text",
      "zIndex": 3,
      "layout": { "mode": "flow", "16:9": { "flowIn": "sag-offer", "w": "content", "h": "content" } },
      "typography": { "16:9": { "family": "GT Eesti Display", "weight": 400, "size": 50, "color": "#ffffff", "case": "uppercase", "tracking": 1.3, "lineHeight": 0.97 } },
      "props": { "content": [ { "value": "Up\nTo" } ] }   // hard line break in content-width text — see Stress points
    },
    {
      "id": "sag-amount",
      "type": "text",
      "zIndex": 3,
      "layout": { "mode": "flow", "16:9": { "flowIn": "sag-offer", "w": "content", "h": "content" } },
      "typography": { "16:9": { "family": "GT Eesti Display", "weight": 700, "size": 150, "color": "#ffffff", "tracking": 3.7 } },
      "props": {
        "content": [ { "price": { "amount": 500, "currency": "AUD", "locale": "en-AU" } } ]
      }
    },

    // ── Cashback label and disclaimer, bare blue text, separately timed.
    {
      "id": "sag-cashback",
      "type": "text",
      "zIndex": 2,
      "layout": { "mode": "flow", "16:9": { "flowIn": "sag-stack", "w": "content", "h": "content" } },
      "typography": { "16:9": { "family": "GT Eesti Display", "weight": 700, "size": 95, "color": "#054f8e", "tracking": 2.4 } },
      "props": {
        "content": [ { "value": "Cashback*" } ],
        "patterns": [ { "expression": "\\*$", "typography": { "size": { "ofRoot": 0.58 }, "baseline": 0.45 } } ]
      },
      "animations": [
        { "animation": "slideFade", "params": { "from": "top", "distancePercent": 60, "durationInFrames": 12 },
          "at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 14 } }
      ]
    },
    {
      "id": "sag-disclaimer",
      "type": "text",
      "zIndex": 2,
      "layout": { "mode": "flow", "16:9": { "flowIn": "sag-stack", "w": "content", "h": "content" } },
      "typography": { "16:9": { "family": "GT Eesti Display", "weight": 400, "size": 17, "color": "#054f8e", "tracking": 0.4 } },
      "props": { "content": [ { "value": "*Via Redemption Via Digital Visa Gift Card. T&Cs Apply." } ] },
      "animations": [
        { "animation": "slideFade", "params": { "from": "top", "distancePercent": 60, "durationInFrames": 12 },
          "at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 20 } }
      ]
    }
  ]
}
What the notes want editable Bound field Control
Logo sag-logo.props.src asset — see Stress points
Cashback figure sag-amount.props.content[0].price.amount number
Text values sag-*.props.content[0].value text
Panel colours sag-headline.props.background.fill, sag-offer-bg.props.fill colour
Pulse toggle per panel sag-{headline,offer}.animations[pulse].params.scale see Stress points

What the engine does with it

Resolve. $500 becomes $ and 500 parts through Intl.NumberFormat("en-AU"). Every at lands on start plus its stagger. pulse on sag-offer expands once; sag-up-to, sag-amount and sag-offer-bg will inherit it rather than carrying copies.

Solve. Order: the texts and the image, then sag-offer, then sag-offer-bg (its of edge points at the row), then sag-stack. The logo measures 308 wide and takes its height from the file's aspect. The row packs UP/TO and $500 centred on the cross axis; the background takes the row's extents plus its own padding. The root column stacks five members.

Animate. sag-offer's −3° and its pulse become one matrix; both members inherit it through flowIn, and sag-offer-bg re-derives from the row's animated box every frame so the red panel tilts and breathes with the text without an animation of its own. Its wipe is the one thing that doesn't fit — see Stress points.

Place. Nine elements. The panel is painted from a derived box; the badge-style asterisk is a pattern-styled run on the same baseline as Cashback.

What this shows

  • A multi-text panel is a background, a single-text panel is props.background. The blue panel and the red panel look identical to an author and are two different mechanisms.
  • Animate the container, not the parts. Tilt and pulse on sag-offer reach both runs and the derived panel through edges; the built code has to thread rotation and pulse through every child by hand.
  • The amount is a number. price.amount: 500 is bindable to campaign data; the built lockupTextSchema hero is a typed string "$500".

Stress points

  • wipe is not a transform or opacity animation — see the Home Appliance page; this component wipes a derived background too, so the same gap applies to a layer that has no intrinsic box of its own to clip against.
  • No per-member spacing in a flowBox — four distinct gaps (32 / 13 / 23 / 26) collapsed to one gap: 24, and the built offset.x: -19 on the offer panel has no home at all (Positioning → flowBox).
  • Swapping an image is not a numeric or text leaf. The notes say "logo is changeable"; the bindings rule (Positioning → Bindings target numeric leaves only) admits numbers and text values only, so props.src (an asset ref) is unbindable as written. Asset refs need to be an explicitly allowed leaf kind.
  • Hard line breaks in content-sized text are undefined. "Up\nTo" at w: "content" relies on \n forcing a break in a layer that "never wraps" (Text Foundation → Point/box consolidation). Authored breaks and wrap-driven breaks need to be stated as separate things.
  • Pulse pivot and per-part pulse toggle — same as Home Appliance. Here the pivot problem is milder because the pulse is authored on the offer row itself, but sag-headline still pulses about its own centre rather than the lockup's.
  • Opacity inheritance through of. The red panel wipes on 4 frames after the headline; if an author instead faded the row, the derived background would not fade with it (Motion Graph → Opacity along edges). Correct per the rule, but an author will expect a panel to fade with its contents — worth a line in the brief.