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
won 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
savingfield, 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. $1376at 152px won't fit the library template's 219px-wide placement. The built component shrinks its wholeLockupRootto 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.