Skip to content

Example: Product description and code

Manifest row 17 · built today as betta-product-description

A white brand mark (invisible on the reference's white ground) over a two-line uppercase product description in navy, over the SKU in a lighter weight, all left aligned and hugging their own content. Each of the three slides in from the left while fading, staggered by four frames.

flowchart TB
    subgraph scene["scene-product-description  (one motion graph per ratio)"]
        ROOT["pd-root<br/>flowBox column · absolute · content-sized · alignAcross left"]
        LOGO["pd-logo<br/>image · flow · w 380, h content · slideFade"]
        DESC["pd-description<br/>text · flow · slideFade"]
        CODE["pd-code<br/>text · flow · slideFade"]
        LOGO -- flowIn --> ROOT
        DESC -- flowIn --> ROOT
        CODE -- flowIn --> ROOT
    end

The document

{
  "id": "scene-product-description",
  "durationInFrames": 75,
  "layers": [

    // ── Content-sized column: exactly as wide as its widest member, no fixed length here.
    {
      "id": "pd-root",
      "type": "flowBox",
      "zIndex": 1,
      "layout": {
        "mode": "absolute",
        "16:9": { "x": 120, "y": 120, "w": "content", "h": "content", "anchor": "top-left" },
        "9:16": { /* … */ }, "1:1": { /* … */ }
      },
      // The preset wants 23px above the description and 21px above the code. One `gap`.
      "props": { "direction": "column", "alignAcross": "left", "alignAlong": "top", "gap": 22 }
    },

    // ── The logo: fixed width, height follows the file's aspect ratio.
    {
      "id": "pd-logo",
      "type": "image",
      "zIndex": 2,
      "layout": { "mode": "flow", "16:9": { "flowIn": "pd-root", "w": 380, "h": "content" },
                  "9:16": { /* … */ }, "1:1": { /* … */ } },
      "props": { "src": { "kind": "bundled", "path": "effects/betta-product-description/logo.png" } },
      "animations": [
        { "animation": "slideFade",
          "params": { "from": "left", "distance": 76, "durationInFrames": 12 },
          "at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 0 } }
      ]
    },

    // ── Description: content-sized on both axes, so it never wraps by measure; the hard `\n`
    //    is the only break, and the box is as wide as the longer of the two lines.
    {
      "id": "pd-description",
      "type": "text",
      "zIndex": 2,
      "layout": { "mode": "flow", "16:9": { "flowIn": "pd-root", "w": "content", "h": "content" },
                  "9:16": { /* … */ }, "1:1": { /* … */ } },
      "typography": { "16:9": { "family": "GT Eesti Display", "weight": 700, "size": 56.5,
                                "color": "#054F8E", "tracking": 0.6, "lineHeight": 0.93,
                                "case": "uppercase", "horizontalAlign": "left" } },
      "props": { "content": [ { "value": "509L French\nDoor Fridge" } ] },
      "animations": [
        { "animation": "slideFade",
          "params": { "from": "left", "distance": 60, "durationInFrames": 12 },
          "at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 4 } }
      ]
    },

    // ── The SKU.
    {
      "id": "pd-code",
      "type": "text",
      "zIndex": 2,
      "layout": { "mode": "flow", "16:9": { "flowIn": "pd-root", "w": "content", "h": "content" },
                  "9:16": { /* … */ }, "1:1": { /* … */ } },
      "typography": { "16:9": { "family": "GT Eesti Display", "weight": 400, "size": 35,
                                "color": "#054F8E", "tracking": 0.4 } },
      "props": { "content": [ { "value": "836332" } ] },
      "animations": [
        { "animation": "slideFade",
          "params": { "from": "left", "distance": 22, "durationInFrames": 12 },
          "at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 8 } }
      ]
    }
  ]
}

What a campaign would bind:

Path Control
pd-logo.props.src image (the brand mark)
pd-logo.layout.16:9.w (and per ratio) number
pd-description.props.content[0].value multiline text, from product data
pd-code.props.content[0].value text, from product data
each typography.*.color colour

What the engine does with it

Resolve. No prices, no relative sizes. Three at references resolve to frames 0, 4 and 8. Each slideFade expands into an x track and an opacity track.

Solve. The logo's intrinsic size is the PNG's pixel size, scaled to 380 wide with height following. The description is measured with no width constraint: two lines, hard-broken, the box as wide as "DOOR FRIDGE". The code likewise. The column packs the three left-aligned with 22px between; its own box is the widest member by the sum of heights.

Animate. Three independent slide-fades on three nodes with no edges between them beyond flow membership, which composes nothing extra here since the column itself is still.

Place. Three elements, one an <img> sized by layout.

What this shows

  • A content-sized column with mixed primitives — image and text — each supplying its own intrinsic size (Coordinate System → Intrinsic size).
  • w: 380, h: "content" on an image is the aspect-preserving auto-height that page promised.
  • Product data flows straight into runs: description and SKU are plain text bindings.

Stress points

  • No per-member spacing in a flowBox. The design wants 23px then 21px; gap is one number, there is no member margin, and the built component uses per-part marginTop for exactly this. Encoded here as gap: 22, a 1px lie each way. Every component in the catalogue with more than two rows has this (Positioning → flowBox vocabulary).
  • Named-animation distances relative to the layer's own size have no unit. The built entrance is distancePercent: 20 — a fifth of the part's own width. targetFraction is relative to a target or container box, never to self, so the px values above (76, 60, 22) are 20% of each layer's expected width worked out by hand and go stale the moment a binding changes the copy (Coordinate System → targetFraction, Keyframe Animation → named animations). A selfFraction unit, or letting a named animation's expansion read its layer's solved box, would close this — the second is already implied by "a named animation can read the layer's resolved box" on that page, so it may only need stating as the way slideFade gets its distance.
  • Hard line breaks in content-sized text are unstated. Point text "never wraps"; this layer is content/content and carries a \n. It obviously should break, and the width should be the widest line, but the three-shape table doesn't say so (Text Foundation → point/box consolidation).
  • Bindable w on a flow member is a numeric leaf in layout, not props. Fine under the rule as written, worth one example in the brief since every other binding shown targets props.