Skip to content

Example: Sale — Up To 30% Off

Manifest row 14 · built today as betta-sale-percent-off

Two red panels tilted apart: SALE leaning −2° on top, and a wider panel leaning +2° below with a small two-line UP/TO beside 30% OFF*. White type throughout. Both panels wipe on and pulse.

flowchart TB
    subgraph scene["scene (16:9 graph)"]
        ROOT["spo-stack<br/>flowBox · column · absolute"]
        SALE["spo-sale<br/>text 'SALE' on red · flow · rotation -2 · pulse"]
        OFFER["spo-offer<br/>flowBox · row · alignAcross centre · flow · rotation 2 · pulse"]
        BG["spo-offer-bg<br/>background · derived of spo-offer"]
        UPTO["spo-up-to<br/>text 'UP / TO' · flow"]
        HERO["spo-hero<br/>text '30% OFF*' · flow"]
        SALE -- flowIn --> ROOT
        OFFER -- flowIn --> ROOT
        BG -- of --> OFFER
        UPTO -- flowIn --> OFFER
        HERO -- flowIn --> OFFER
    end

The document

{
  "id": "scene-sale-percent-off",
  "durationInFrames": 75,
  "layers": [
    {
      "id": "spo-offer-bg",
      "type": "background",
      "zIndex": 0,
      "layout": { "mode": "derived", "16:9": { "of": ["spo-offer"], "ofBounds": "extents" } },
      "props": { "fill": "#ed1c24", "radius": 6, "padding": { "top": 19, "right": 13, "bottom": 20, "left": 15 } },
      "animations": [
        { "animation": "wipe", "params": { "direction": "from-left", "durationInFrames": 12 },
          "at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 4 } }
      ]
    },
    {
      "id": "spo-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": 28 }
    },

    // ── SALE: a single text layer whose paint-only background is the panel.
    {
      "id": "spo-sale",
      "type": "text",
      "zIndex": 2,
      "layout": { "mode": "flow", "16:9": { "flowIn": "spo-stack", "w": "content", "h": "content", "rotation": -2 } },
      "typography": { "16:9": { "family": "GT Eesti Display", "weight": 700, "size": 192, "color": "#ffffff", "case": "uppercase", "tracking": 4.8 } },
      "props": {
        "content": [ { "value": "Sale" } ],
        "background": { "fill": "#ed1c24", "radius": 6, "padding": { "top": 38, "right": 18, "bottom": 36, "left": 20 } }
      },
      "animations": [
        { "animation": "wipe", "params": { "direction": "from-left", "durationInFrames": 12 },
          "at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 0 } },
        { "animation": "pulse", "params": { "scale": 1.05, "periodInFrames": 40 },
          "at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 30 } }
      ]
    },

    // ── Offer row: two text layers under one derived panel.
    {
      "id": "spo-offer",
      "type": "flowBox",
      "zIndex": 2,
      "layout": { "mode": "flow", "16:9": { "flowIn": "spo-stack", "w": "content", "h": "content", "rotation": 2 } },
      "props": { "direction": "row", "alignAcross": "centre", "gap": 9 },
      "animations": [
        { "animation": "pulse", "params": { "scale": 1.05, "periodInFrames": 40 },
          "at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 30 } }
      ]
    },
    {
      "id": "spo-up-to",
      "type": "text",
      "zIndex": 3,
      "layout": { "mode": "flow", "16:9": { "flowIn": "spo-offer", "w": "content", "h": "content" } },
      "typography": { "16:9": { "family": "GT Eesti Display", "weight": 400, "size": 39, "color": "#ffffff", "case": "uppercase", "tracking": 1, "lineHeight": 0.95 } },
      "props": { "content": [ { "value": "Up\nTo" } ] }
    },
    {
      "id": "spo-hero",
      "type": "text",
      "zIndex": 3,
      "layout": { "mode": "flow", "16:9": { "flowIn": "spo-offer", "w": "content", "h": "content" } },
      "typography": { "16:9": { "family": "GT Eesti Display", "weight": 700, "size": 111, "color": "#ffffff", "case": "uppercase", "tracking": -0.6 } },
      "props": {
        // Three runs so the figure is a bindable value and the words stay words.
        "content": [ { "value": "30" }, { "value": "% off" }, { "value": "*", "typography": { "size": { "ofRoot": 0.55 }, "baseline": 0.5 } } ]
      }
    }
  ]
}
What the notes want editable Bound field Control
Percentage spo-hero.props.content[0].value text today; should be a number — see Stress points
Text values spo-sale.props.content[0].value, spo-up-to…, spo-hero.props.content[1].value text
Panel colours spo-sale.props.background.fill, spo-offer-bg.props.fill colour
"UP TO" on/off see Stress points
Pulse toggle per panel spo-{sale,offer}.animations[pulse].params.scale see Stress points

What the engine does with it

Resolve. Two wipes at frames 0 and 4, two pulses from 30; the asterisk run's ofRoot resolves against the hero's 111.

Solve. spo-sale, spo-up-to, spo-hero, then spo-offer, spo-offer-bg, spo-stack. The offer row centres the small stack on the hero's cross axis; the panel takes the row's extents plus padding. The column centres SALE over the wider offer panel.

Animate. Each panel tilts and pulses about its own content centre; the derived panel follows the row's animated box.

Place. Six elements.

What this shows

  • The minimal lockup. Two panels, one of each kind: a text-with-background and a background-under-a-row. If the brief handles this one cleanly, the family above it follows.
  • Runs instead of patterns for structure. 30 / % off / * as three runs makes the figure addressable without a regex; the pattern mechanism is kept for styling within a run.

Stress points

  • No number run for 30 — see 50% Off.
  • Removing a run needs a layout-affecting "collapsed" state — see Betta Buys.
  • wipe is not transform/opacity, no per-member spacing (the built offerPrefixOffsetY: -8 has no home), pulse pivot, per-part pulse toggle — as on the Home Appliance page.
  • Inline runs at mixed sizes need a stated baseline rule. The hero's three runs are 111 / 111 / 61px and the asterisk lifts by baseline: 0.5; the brief's runs model never says whether runs share a baseline by default or what baseline is a fraction of (Text Foundation → Content-item text runs). Nothing awkward otherwise.