模式 · 25
布局函数
Layout function · layout-function把序号映射成位置和朝向;换函数,就换一整套编排。
一组卡片从横排变圆弧、圆柱或螺旋时,真正变化的是“第 i 张该在哪里”。把目标姿态写成 layout(i,n,options),动画只需跟随新目标;拖动偏移也只是更新函数的输入。这样切布局时无需重写每张卡片的过渡。
公式
pose[i] = layout(i, count, options) for each pose field: current ← damp(current, target, λ, dt) render translate3d(x,y,z) · rotateY · rotate
参数
| 本页默认 | 成熟实现 | |
|---|---|---|
| layout | row / arc / cylinder / helix / grid | motion-kit onCylinder / onHelix |
| count | 14 | —同一批卡片贯穿五种布局。 |
| offset | 0 → 2π | —圆柱与螺旋按此转动。 |
| λ | 8 /s | —仅演示过渡用的阻尼速度。 |
怎样做好
- 让布局函数返回完整姿态,不只返回 x;圆柱与螺旋还需要 z 和 rotateY。站内
motion-kit就这样组织。www-motion/rewrite/lib/motion-kit.js:133–141 - 切布局只改目标;当前位置用按时间计算的阻尼靠近目标,刷新率变化时过渡速度仍一致。lenis packages/core/src/maths.ts:46
- 深度布局要检查可见卡片是否侧对镜头。站内 helix 变体用屏幕位置约束偏航,避免卡片变成细条。www-motion/rewrite/lib/motion-kit.js:144–149
- 复杂编排也可以是同一个接口:
riffle按进度选择不同 formation,算出每张卡片的完整姿态,再在两套结果之间混合。www-motion/rewrite/riffle/layouts.js:20–31
在我们的案例里
53 个案例有按序号或状态计算布局的模式。riffle 在多套 formation 间插值;motion-kit 给 burst 的圆柱和 helix 的螺旋各写了一个位置函数。两者都把“布局目标”和“怎么动过去”分开。
出现在 53 个案例
01Momentum Slider02Drag Gallery03Formation04Cardwall05Riffle06Revolver07Rotunda08Iris09Lens Bloom10Frieze11Aperture12Folio13Cinema Reel14Stream Showcase15Hardback16Picasso Ecommerce17Assembly18Fathom
+35 更多
19Emulsion20Portfolio Photographer21Reverie22Boutique23Nova25Shape Trail26Membrane28Filament29Pixels30Cipher32Cutout36Benchmark37Amour38Fluid Gallery39Marathon43Pantheon45Aurora46Rooster Rush47Overworld48Moments49Burst50Dimension51Morph54Lumen56Fleur59Helix60Crate61Prism62Flashlight63Viewfinder64Sand65Arrange66Tunnel68Type69Loupe
参考实现 · 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 };
},
};