Skip to content

Example: the catalogue cover and phone

Manifest row 20 · built today as betta-catalogue-cover

A catalogue front page, upright, with a phone showing the same catalogue's mobile view overlapping its lower-right corner and hanging past its edge. Both images carry a thin white keyline. The cover slides in from the left and fades up; the phone slides up from below a few frames later. Both images are swappable.

flowchart TB
    subgraph scene["scene-catalogue  (one motion graph per ratio)"]
        COVER["cat-cover<br/>image · absolute<br/>slideFade from left"]
        PHONE["cat-phone<br/>image · pin position<br/>slideFade from bottom"]
        PHONE -- "pinTo (top-left, 0.84 / 0.72 of cover)" --> COVER
    end

The document

{
  "id": "scene-catalogue",
  "durationInFrames": 75,
  "layers": [

    // ── The cover. Fixed width, aspect-preserving auto height from the file's own pixels.
    {
      "id": "cat-cover",
      "type": "image",
      "zIndex": 0,
      "layout": {
        "mode": "absolute",
        "16:9": { "x": 120, "y": 120, "w": 463, "h": "content", "anchor": "top-left" },
        "9:16": { "x": 120, "y": 320, "w": 463, "h": "content", "anchor": "top-left" },
        "1:1":  { "x": 120, "y": 120, "w": 463, "h": "content", "anchor": "top-left" }
      },
      "props": {
        "src": { "kind": "bundled", "path": "effects/betta-catalogue-cover/cover.png" },
        "stroke": { "color": "#FFFFFF", "width": 3 }        // see Stress points
      },
      "animations": [
        { "animation": "slideFade", "params": { "from": "left", "distance": 140, "durationInFrames": 14 },
          "at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 0 } }
      ]
    },

    // ── The phone. Pinned to the cover's top-left, offset by fractions of the cover's own
    //    box, so swapping in a taller cover keeps the phone on the same corner. `position`
    //    pin: it rides the cover's slide but keeps its own fade and its own entrance.
    {
      "id": "cat-phone",
      "type": "image",
      "zIndex": 1,
      "layout": {
        "mode": "pin",
        "16:9": {
          "pinTo": "cat-cover", "pinToBounds": "content", "pinToAnchor": "top-left",
          "anchor": "top-left",
          "offsetX": { "value": 0.84, "unit": "targetFraction" },
          "offsetY": { "value": 0.72, "unit": "targetFraction" },
          "w": 257, "h": "content", "pinType": "position"
        },
        "9:16": { /* same, own offsets */ },
        "1:1":  { /* same, own offsets */ }
      },
      "props": {
        "src": { "kind": "bundled", "path": "effects/betta-catalogue-cover/phone.png" },
        "stroke": { "color": "#FFFFFF", "width": 3 }
      },
      "animations": [
        { "animation": "slideFade", "params": { "from": "bottom", "distance": 120, "durationInFrames": 14 },
          "at": { "mode": "cueOffset", "cue": "start", "offsetFrames": 6 } }
      ]
    }
  ]
}
Bound field Control
cat-cover.props.src, cat-phone.props.src asset
cat-cover.layout.*.w, cat-phone.layout.*.w number
*.props.stroke.color colour

What the engine does with it

Resolve. Two slideFade expansions: the cover's into an x track (−140 → 0) plus opacity, the phone's into a y track (+120 → 0) plus opacity, six frames later.

Solve. cat-cover first: width 463, height from the PNG's aspect. cat-phone next: both targetFraction offsets multiply out against the cover's resolved box (0.84 × 463 ≈ 389, 0.72 × ~400 ≈ 288) and become plain px before the pin resolves; its own height comes from its file (Positioning → pin).

Animate. The cover's matrix carries the slide. The phone's position pin tracks the cover's animated top-left, so the phone drifts left-to-right with the cover while rising on its own track; it does not take the cover's opacity — its own slideFade supplies one.

Place. Two <img> elements with keylines, each under its own matrix3d and opacity.

What this shows

  • Swap-safe placement. Fractional offsets keep the phone on the cover's corner whatever the replacement image's aspect; a px offset would drift.
  • position versus full. The phone follows the cover's point but keeps its own fade and entrance; had it been full, it would also have inherited the cover's fade and any tilt.
  • The "effects library" isn't where motion lives. Today's effects (blur, glow, shadow, tint…) are paint filters on a layer node; a slide-in is a named animation on the same node.

Stress points

  1. Images have no keyline field. The brief's extents bounds name "stroke" as a paint-only addition (Coordinate System & Sizing → Bounds) but no page gives image (or any non-text layer) a stroke prop. Written here as props.stroke: { color, width }; needs a home and a statement that it grows extents.
  2. The phone overhangs the cover. A background or a container-relative sizing that used the pair's "extents" would want the union of both; nothing here does, but selection-ring geometry for a future component wrapping these two must take the union of both members' extents rather than the first layer's box — worth a sentence in Positioning → Frontend impact.
  3. Named-animation parameter shapes are undefined. slideFade's distance is written as px; the built transition uses a percentage of the element's own size, which is the more portable default. The library's parameter vocabulary needs to be pinned once (Keyframe Animation).