Skip to content

Example: Price + Product v2, price and saving

Manifest row 16 · built today as betta-price-save-stack

A navy block reading "$1376" in white, over a white block with "SAVE" in red above "$223" in red. The white block is invisible against the reference's white background but it is there. Both blocks are the same fixed width; the saving line being shorter never narrows the block. A straight-discount module: no cashback maths, two parts.

flowchart TB
    subgraph scene["scene-price-saving  (one motion graph per ratio)"]
        ROOT["pss-root<br/>flowBox column · absolute · w 293 · alignAcross stretch"]
        PRICE["pss-price<br/>text (price run) + own pill · flow · wipe, pulse"]
        SAVE["pss-save<br/>flowBox column · flow (stretched) · wipe, pulse"]
        SL["pss-save-label<br/>text · flow"]
        SA["pss-save-amount<br/>text (price run) · flow"]
        SBG["pss-save-bg<br/>background · derived · wipe"]
        PRICE -- flowIn --> ROOT
        SAVE -- flowIn --> ROOT
        SL -- flowIn --> SAVE
        SA -- flowIn --> SAVE
        SBG -- of --> SAVE
    end

The document

16:9 shown in full. Panel width in the design is 349; the root is authored 293 so the 28px side padding every panel carries outward brings the pills to 349 starting at x: 120.

{
  "id": "scene-price-saving",
  "durationInFrames": 75,
  "layers": [

    // ── The column. Explicit width = fixed length. `stretch` gives it to both members.
    {
      "id": "pss-root",
      "type": "flowBox",
      "zIndex": 1,
      "layout": {
        "mode": "absolute",
        "16:9": { "x": 148, "y": 120, "w": 293, "h": "content", "anchor": "top-left" },
        "9:16": { /* … */ }, "1:1": { /* … */ }
      },
      "props": { "direction": "column", "alignAcross": "stretch", "alignAlong": "top", "gap": 21 }
    },

    // ── Price block. A single price run; the navy panel is the layer's own paint-only pill.
    {
      "id": "pss-price",
      "type": "text",
      "zIndex": 3,
      "layout": { "mode": "flow", "16:9": { "flowIn": "pss-root", "w": "content", "h": "content" },
                  "9:16": { /* … */ }, "1:1": { /* … */ } },
      "typography": { "16:9": { "family": "GT Eesti Display", "weight": 700, "size": 152,
                                "color": "#FFFFFF", "tracking": 4.6, "horizontalAlign": "left" } },
      "props": {
        "content": [
          {
            "price": { "amount": 1376, "currency": "AUD", "locale": "en-AU", "grouping": false },
            "typography": {
              "currencySymbol": { "size": { "ofRoot": 0.585 }, "baseline": "super", "tracking": 11.5 }
            }
          }
        ],
        "background": { "fill": "#054F8E", "radius": 8,
                        "padding": { "top": 19, "right": 28, "bottom": 16, "left": 28 } }
      },
      "animations": [
        { "animation": "wipe", "params": { "direction": "from-left", "durationInFrames": 12 },
          "at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 0 } },
        { "animation": "pulse", "params": { "scale": 1.04, "periodInFrames": 40, "delay": 30 } }
      ]
    },

    // ── Save block: "SAVE" over "$223", 4px apart, on a derived white background.
    {
      "id": "pss-save",
      "type": "flowBox",
      "zIndex": 2,
      "layout": { "mode": "flow", "16:9": { "flowIn": "pss-root", "w": "content", "h": "content" },
                  "9:16": { /* … */ }, "1:1": { /* … */ } },
      "props": { "direction": "column", "alignAcross": "left", "alignAlong": "top", "gap": 4 },
      "animations": [
        { "animation": "wipe", "params": { "direction": "from-left", "durationInFrames": 12 },
          "at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 4 } },
        { "animation": "pulse", "params": { "scale": 1.04, "periodInFrames": 40, "delay": 30 } }
      ]
    },
    {
      "id": "pss-save-label",
      "type": "text",
      "zIndex": 3,
      "layout": { "mode": "flow", "16:9": { "flowIn": "pss-save", "w": "content", "h": "content" },
                  "9:16": { /* … */ }, "1:1": { /* … */ } },
      "typography": { "16:9": { "family": "GT Eesti Display", "weight": 700, "size": 71,
                                "color": "#FE2121", "case": "uppercase", "tracking": 0.7 } },
      "props": { "content": [ { "value": "Save" } ] }
    },
    {
      "id": "pss-save-amount",
      "type": "text",
      "zIndex": 3,
      "layout": { "mode": "flow", "16:9": { "flowIn": "pss-save", "w": "content", "h": "content" },
                  "9:16": { /* … */ }, "1:1": { /* … */ } },
      "typography": { "16:9": { "family": "GT Eesti Display", "weight": 700, "size": 152,
                                "color": "#FE2121", "tracking": 1.5 } },
      "props": {
        "content": [
          {
            "price": { "amount": 223, "currency": "AUD", "locale": "en-AU", "grouping": false },
            "typography": {
              "currencySymbol": { "size": { "ofRoot": 0.585 }, "baseline": "super", "tracking": 8 }
            }
          }
        ]
      }
    },
    {
      "id": "pss-save-bg",
      "type": "background",
      "zIndex": 1,
      "layout": { "mode": "derived", "16:9": { "of": ["pss-save"], "ofBounds": "extents" },
                  "9:16": { /* … */ }, "1:1": { /* … */ } },
      "props": { "fill": "#FFFFFF", "radius": 8,
                 "padding": { "top": 15, "right": 28, "bottom": 13, "left": 28 } },
      // Geometry follows `pss-save` (and its pulse) for free; the wipe does not, so it's restated.
      "animations": [
        { "animation": "wipe", "params": { "direction": "from-left", "durationInFrames": 12 },
          "at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 4 } }
      ]
    }
  ]
}

What a campaign would bind:

Path Control
pss-price.props.content[0].price.amount number, the connected product's price
pss-save-amount.props.content[0].price.amount number — typed or supplied separately, see below
pss-save-label.props.content[0].value text
pss-price.props.background.fill, pss-save-bg.props.fill colour

What the engine does with it

Resolve. Two price runs format to $1376 and $223 — whole amounts, so no cents, and grouping off. The at references resolve to frames 0 and 4 of this scene.

Solve. Order: pss-save-label, pss-save-amount, pss-price, pss-save, pss-root, pss-save-bg. The inner column packs "SAVE" over "$223" with a 4px gap, left aligned; the root packs the price block over it with 21px between and stretches both to 293. The background takes the inner column's extents plus padding.

Animate. The price block's pill wipes and pulses with its text because it is the text's own paint. pss-save's pulse reaches its two texts through flow membership and reaches the background geometrically through of; the background's wipe is its own.

Place. Six elements, the background under the two red texts.

What this shows

  • The same fixed-length rule as row 15, one explicit w on a stretched column.
  • A price with no label is one layer; a label over a price with an exact gap is a column of two.
  • Pulse inherits, wipe doesn't — visible in which layers had to carry which animation.

Stress points

Shares every point on the row 15 page — wipe isn't a node value, derived backgrounds restate entrances, authored width ≠ pill width, container bounds miss the pills, stretch vs member w, no single-line shrink, price grouping, no flowBox padding. Two more of its own:

  • A saving is a derived value and bindings can't derive. "Save $223" is was − now, but bindings write numeric leaves only and there is no computed or expression binding (Positioning → bindings target numeric leaves only, Price). Either product data supplies a saving field, or an author types it, and either way it can drift from the price above it. The brief should say whether a derived-number binding kind is ever coming or that this stays a data-source concern.
  • $1376 at 152px won't fit the library template's 219px-wide placement. The built component shrinks its whole LockupRoot to the layer box; here nothing shrinks, so this example widened the module to the row-15 width instead. Same missing single-line fit-to-width as row 15, seen from the other side: the authored typography and the authored width can disagree, and the model has no reconciliation except wrap (irrelevant for a number) or overflow.