输入 · 13
虚拟滚动
Virtual scroll · virtual-scroll把滚轮、拖动和键盘输入汇成一个有边界的进度,再平滑显示。
横向叙事区常用纵向滚轮推动横向内容。事件可能用像素、行或页计量;若直接累加原始数字,设备之间会差得很远。到达边界后必须把继续向外的滚轮还给页面,避免困住读者。
公式
deltaPx = wheelPixels(event) if shouldCapture(deltaPx): preventDefault; push(deltaPx) current ← damp(current, target, λ, dt) progress = (current − min)/(max − min)
参数
| 本页默认 | 成熟实现 | |
|---|---|---|
| deltaMode | 0 / 1 / 2 | Lenis 按像素、行、页换算 |
| λ | 10 /s | Lenis lerp 0.1 |
怎样做好
- 先换算 wheel 的
deltaMode,再推同一个目标值;Lenis 对行和页分别使用行高与视口尺寸。lenis packages/core/src/virtual-scroll.ts:4–10,142–153 - 目标值可立即钳在边界,显示值用按 dt 的阻尼跟随。这样输入与画面不会互相覆盖。lenis packages/core/src/lenis.ts:115; animate.ts:43
- 到端点时只放行继续向外的滚轮;向内滚动仍由组件接收。
在我们的案例里
primitives.json 没有单列 virtual-scroll;另一次滚轮模型核查把 marathon、boutique 等归为连续输入。marathon 把滚轮与触摸位移推入同一目标,正是这里统一的模式。
出现在 18 个案例
参考实现 · basics.js
/** Wheel deltas arrive in pixels, lines or pages depending on device. Normalise to pixels. */
export const wheelPixels = e => e.deltaY * (e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? innerHeight : 1);
/**
* A section that owns its own scroll position: inputs move `target`, `current` follows with damp.
* shouldCapture(): hand the wheel back to the page at the ends instead of trapping the user.
*/
export class VirtualScroll {
constructor({ min = 0, max = 1000, lambda = 10, multiplier = 1 } = {}) {
Object.assign(this, { min, max, lambda, multiplier }); this.current = this.target = min;
}
shouldCapture(delta) {
return !((delta < 0 && this.target <= this.min) || (delta > 0 && this.target >= this.max));
}
push(delta) { this.target = clamp(this.target + delta * this.multiplier, this.min, this.max); }
step(dt) { this.current = damp(this.current, this.target, this.lambda, dt); return this.current; }
get progress() { return normalize(this.current, this.min, this.max); }
}