Skip to content

Example: 50% Off — Audio

Manifest row 13 · built today as betta-category-discount

One tilted red panel with a soft shadow: a small two-line UP/TO beside a huge 50, then a column of % over a small OFF*, the whole thing leaning −5.6°. Under it, AUDIO in blue with no panel. The panel wipes on and pulses; AUDIO fades on and slides down, and pulses too.

flowchart TB
    subgraph scene["scene (16:9 graph)"]
        ROOT["cd-stack<br/>flowBox · column · absolute"]
        OFFER["cd-offer<br/>flowBox · row · alignAcross capHeight · flow · rotation -5.6 · pulse"]
        BG["cd-offer-bg<br/>background · derived of cd-offer"]
        UPTO["cd-up-to<br/>text 'UP / TO' · flow"]
        HERO["cd-hero<br/>text '50' · flow"]
        UNIT["cd-unit-col<br/>flowBox · column · flow"]
        PCT["cd-percent<br/>text '%' · flow"]
        OFF["cd-off<br/>text 'OFF*' · flow"]
        CAT["cd-category<br/>text 'AUDIO' · flow · pulse"]
        OFFER -- flowIn --> ROOT
        BG -- of --> OFFER
        UPTO -- flowIn --> OFFER
        HERO -- flowIn --> OFFER
        UNIT -- flowIn --> OFFER
        PCT -- flowIn --> UNIT
        OFF -- flowIn --> UNIT
        CAT -- flowIn --> ROOT
    end

The document

{
  "id": "scene-category-discount",
  "durationInFrames": 75,
  "layers": [

    // ── The red panel, derived from the offer row. Shadow is paint-only and widens `extents`.
    {
      "id": "cd-offer-bg",
      "type": "background",
      "zIndex": 0,
      "layout": { "mode": "derived", "16:9": { "of": ["cd-offer"], "ofBounds": "extents" } },
      "props": {
        "fill": "#e00711", "radius": 4,
        "padding": { "top": 36, "right": 20, "bottom": 29, "left": 26 }
      },
      "animations": [
        { "animation": "wipe", "params": { "direction": "from-left", "durationInFrames": 12 },
          "at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 0 } }
      ]
    },

    {
      "id": "cd-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": 60 }
    },

    // ── Offer row. capHeight alignment puts the top of "UP" on the cap line of "50", which is
    //    exactly where the PSD has it (the built code nudges the stack up 35px to get there).
    {
      "id": "cd-offer",
      "type": "flowBox",
      "zIndex": 2,
      "layout": { "mode": "flow", "16:9": { "flowIn": "cd-stack", "w": "content", "h": "content", "rotation": -5.6 } },
      "props": { "direction": "row", "alignAcross": "capHeight", "gap": 24 },
      "animations": [
        { "animation": "pulse", "params": { "scale": 1.05, "periodInFrames": 40 },
          "at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 30 } }
      ]
    },
    {
      "id": "cd-up-to",
      "type": "text",
      "zIndex": 3,
      "layout": { "mode": "flow", "16:9": { "flowIn": "cd-offer", "w": "content", "h": "content" } },
      "typography": { "16:9": { "family": "GT Eesti Display", "weight": 400, "size": 60, "color": "#ffffff", "case": "uppercase", "lineHeight": 0.95 } },
      "props": { "content": [ { "value": "Up\nTo" } ] }
    },
    {
      "id": "cd-hero",
      "type": "text",
      "zIndex": 3,
      "layout": { "mode": "flow", "16:9": { "flowIn": "cd-offer", "w": "content", "h": "content" } },
      "typography": { "16:9": { "family": "GT Eesti Display", "weight": 700, "size": 345, "color": "#ffffff" } },
      "props": { "content": [ { "value": "50" } ] }   // a bound integer typed as text — see Stress points
    },
    // ── "%" over "OFF*": a tight column. Its top edge stands in for a cap line (non-text fallback).
    {
      "id": "cd-unit-col",
      "type": "flowBox",
      "zIndex": 3,
      "layout": { "mode": "flow", "16:9": { "flowIn": "cd-offer", "w": "content", "h": "content" } },
      "props": { "direction": "column", "alignAcross": "centre", "gap": 10 }
    },
    {
      "id": "cd-percent",
      "type": "text",
      "zIndex": 4,
      "layout": { "mode": "flow", "16:9": { "flowIn": "cd-unit-col", "w": "content", "h": "content" } },
      "typography": { "16:9": { "family": "GT Eesti Display", "weight": 700, "size": 225, "color": "#ffffff" } },
      "props": { "content": [ { "value": "%" } ] }
    },
    {
      "id": "cd-off",
      "type": "text",
      "zIndex": 4,
      "layout": { "mode": "flow", "16:9": { "flowIn": "cd-unit-col", "w": "content", "h": "content" } },
      "typography": { "16:9": { "family": "GT Eesti Display", "weight": 700, "size": 86, "color": "#ffffff", "case": "uppercase" } },
      "props": {
        "content": [ { "value": "Off*" } ],
        "patterns": [ { "expression": "\\*$", "typography": { "size": { "ofRoot": 0.45 }, "baseline": 0.6 } } ]
      }
    },

    // ── Category word: bare blue, its own entrance, its own pulse.
    {
      "id": "cd-category",
      "type": "text",
      "zIndex": 2,
      "layout": { "mode": "flow", "16:9": { "flowIn": "cd-stack", "w": "content", "h": "content" } },
      "typography": { "16:9": { "family": "GT Eesti Display", "weight": 700, "size": 150, "color": "#054f8e", "case": "uppercase" } },
      "props": { "content": [ { "value": "Audio" } ] },
      "animations": [
        { "animation": "slideFade", "params": { "from": "top", "distancePercent": 60, "durationInFrames": 12 },
          "at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 10 } },
        { "animation": "pulse", "params": { "scale": 1.05, "periodInFrames": 40 },
          "at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 30 } }
      ]
    }
  ]
}
What the notes want editable Bound field Control
Percentage cd-hero.props.content[0].value text today; should be a number — see Stress points
Every run's text cd-*.props.content[0].value text
Panel colour cd-offer-bg.props.fill colour
Category colour cd-category.typography[ratio].color colour
"UP TO" on/off see Stress points
Pulse toggle per part cd-{offer,category}.animations[pulse].params.scale see Stress points

Panel shadows are layer effects

Each panel above carries a drop shadow in the source artwork. A shadow is a per-layer effect, applied to the whole layer (glyphs and pill together), not a field on background — see Coordinate System & Sizing. Where text and its panel need different shadows, they become two layers: a text layer and a background layer spanning it, each with its own effects.

What the engine does with it

Resolve. Two ats on start (frames 0 and 10), two pulses from frame 30; the OFF* pattern resolves its ofRoot.

Solve. Texts, then cd-unit-col, then cd-offer, then cd-offer-bg, then cd-stack. The row aligns UP/TO's first-line cap and 50's cap on one line; cd-unit-col is a non-text member, so its top edge stands in for a cap line and %'s ink top lands on 50's cap line, which is where the PSD puts it. The background wraps the row's extents plus padding; the shadow spread is part of those extents.

Animate. The row's −5.6° and pulse propagate to its five descendants and the derived panel. AUDIO slides and pulses on its own.

Place. Eleven elements; the panel's shadow paints under everything else in the stack.

What this shows

  • Three nested containers, one authored tilt. The whole panel leans because one flowBox has rotation: -5.6; nothing inside it knows.
  • capHeight earns its place twice — once for text (UP to 50) and once through the non-text fallback (%'s column top to 50's cap).
  • A "component" in this system is a subtree, not a file: cut cd-offer and its descendants out and they are a reusable offer panel on their own.

Stress points

  • No number run. 50 is a percentage the client will change per campaign; the content-item union has text runs and price runs only, so it's a typed string here and would be bound as text. A number run (Intl percent / unit / plain decimal, with the same role-typography treatment as price parts) is the obvious third variant (Price already caches one formatter per locale).
  • Removing a run needs a layout-affecting "collapsed" state — see Betta Buys.
  • wipe is not transform/opacity, no per-member spacing (unitGap: 0 vs row gap: 24 — the % column should touch 50, it doesn't here), pulse pivot, per-part pulse toggle, background.shadow unspecified — as on the Home Appliance page.