Pattern · 25
Layout function
布局函数 · layout-functionMap an index to a position and orientation; swap the function to change the arrangement.
When cards switch from a row to an arc, cylinder or helix, the real question is “where should item i be?” Express that as layout(i,n,options). The animation follows the new targets, and dragging an offset simply changes an input. No separate transition is needed for each arrangement.
Formula
pose[i] = layout(i, count, options) for each pose field: current ← damp(current, target, λ, dt) render translate3d(x,y,z) · rotateY · rotate
Parameters
| This page | Mature libraries | |
|---|---|---|
| layout | row / arc / cylinder / helix / grid | motion-kit onCylinder / onHelix |
| count | 14 | —The same cards pass through all five layouts. |
| offset | 0 → 2π | —Rotates cylinder and helix. |
| λ | 8 /s | —The demo's transition follow rate. |
Doing it well
- Return a full pose, not just x. Cylinder and helix also need z and rotateY, as in the site's
motion-kit.www-motion/rewrite/lib/motion-kit.js:133–141 - A layout switch changes targets; time-based damping brings current poses toward them at a consistent speed across refresh rates.lenis packages/core/src/maths.ts:46
- Check whether visible cards turn edge-on in depth layouts. The site's helix variant bounds yaw by screen position to avoid thin slivers.www-motion/rewrite/lib/motion-kit.js:144–149
- Even complex choreography fits this interface:
rifflechooses formations by progress, calculates each card's pose and blends between layouts.www-motion/rewrite/riffle/layouts.js:20–31
In our cases
53 studies use a layout computed from item index or state. riffle blends several formations; motion-kit defines cylinder and helix position functions. Both separate the target layout from the motion toward it.
Used in 53 cases
01Momentum Slider02Drag Gallery03Formation04Cardwall05Riffle06Revolver07Rotunda08Iris09Lens Bloom10Frieze11Aperture12Folio13Cinema Reel14Stream Showcase15Hardback16Picasso Ecommerce17Assembly18Fathom
+35 more
19Emulsion20Portfolio Photographer21Reverie22Boutique23Nova25Shape Trail26Membrane28Filament29Pixels30Cipher32Cutout36Benchmark37Amour38Fluid Gallery39Marathon43Pantheon45Aurora46Rooster Rush47Overworld48Moments49Burst50Dimension51Morph54Lumen56Fleur59Helix60Crate61Prism62Flashlight63Viewfinder64Sand65Arrange66Tunnel68Type69Loupe
Reference implementation · basics.js
/** Index → position. Swapping the function swaps the whole effect; everything else stays. */
export const layouts = {
row: (i, n, { gap = 120 } = {}) => ({ x: (i - (n - 1) / 2) * gap, y: 0, z: 0, rotateY: 0 }),
arc: (i, n, { radius = 500, spread = 1.2 } = {}) => {
const a = (i / Math.max(1, n - 1) - 0.5) * spread;
return { x: radius * Math.sin(a), y: radius * (1 - Math.cos(a)) * 0.35, z: 0, rotateY: 0, rotate: a * 25 };
},
cylinder: (i, n, { radius = 420, offset = 0 } = {}) => {
const a = (i / n) * Math.PI * 2 + offset;
return { x: radius * Math.sin(a), y: 0, z: radius * (Math.cos(a) - 1), rotateY: (a * 180) / Math.PI };
},
helix: (i, n, { radius = 300, turns = 2, rise = 520, offset = 0 } = {}) => {
const t = i / n, a = t * turns * Math.PI * 2 + offset;
return { x: radius * Math.sin(a), y: (t - 0.5) * rise, z: radius * (Math.cos(a) - 1), rotateY: (a * 180) / Math.PI };
},
grid: (i, n, { cols = 5, gap = 110 } = {}) => {
const rows = Math.ceil(n / cols);
return { x: ((i % cols) - (cols - 1) / 2) * gap, y: (Math.floor(i / cols) - (rows - 1) / 2) * gap, z: 0, rotateY: 0 };
},
};