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.
positionversusfull. The phone follows the cover's point but keeps its own fade and entrance; had it beenfull, 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¶
- Images have no keyline field. The brief's
extentsbounds name "stroke" as a paint-only addition (Coordinate System & Sizing → Bounds) but no page givesimage(or any non-text layer) astrokeprop. Written here asprops.stroke: { color, width }; needs a home and a statement that it growsextents. - 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.
- Named-animation parameter shapes are undefined.
slideFade'sdistanceis 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).