算法与数据结构 / 动画引擎原理 · 从插值到播放控制 / 高尔顿钉板:随机过程与确定性播放器 待审核 16 / 47
galton · 预定关键帧路径 · 交错投放

高尔顿钉板:随机过程与确定性播放器

小球从顶部落下,每撞一排钉子就左右各半概率偏一格,穿过 NN 排后落入底部某个槽。大量小球累积出二项分布,随排数增大逼近正态曲线。

它看上去是一台需要碰撞检测的物理玩具,实际不必。落点只取决于「这一路向左还是向右」的一串抉择;抉择一旦抽定,整条下落轨迹就是一条确定的折线。随机性被收进开局那一次抽样,播放本身仍是时间到值的纯函数——本系列反复用到的正是这条主线。

1 · 连续投放与统计涌现

持续向钉板倾泻小球,底部直方图实时累积。偏置 pp 让左右概率不再对半时,分布随之偏斜,均值移到 NpN p;叠加的细线为理论二项分布。

图 1-1 · 连续投放的钉板与实时直方图。可调排数、投放速率与偏置 p,开关切换叠加的理论分布曲线。

每排是一次独立的伯努利试验,落点是 NN 次之和,故服从 B(N,p)B(N, p),均值 NpNp、方差 Np(1p)Np(1-p);由中心极限定理,NN 增大时逼近正态。直方图的钟形不是任何单个球的属性,而是大量独立抉择串的统计涌现。

2 · 单球可回退

把镜头收到一个球上。它的整条轨迹由开局那串抉择唯一决定,位置只是 currentTime 的函数,因此 seekreverse 都零成本——不需要记录历史,也不需要反向积分。

图 2-1 · 单球轨迹与它的抉择串。可拖播放头在轨迹上任意 scrub,点反向让球倒着爬回顶部,或重抽一串抉择换一条路径。

注 · 折线的段内插值分了两个方向:横向走 easeInOut,纵向走线性再叠一个 hopsin(πt)\text{hop} \cdot \sin(\pi t) 的小跳。这不是物理,是补上视觉缺口——纯线性折线在拐点处会显出尖角,而横向缓动让球在每颗钉子附近减速、小跳让它看起来是被弹开而非贴着线滑行。真实的自由落体纵向应该是加速的,本页刻意没做:NN 排匀速下落才能让整块板上的球看起来节奏一致。

3 · 一个球就是一个 Animation

每个球是一个 Animationkeyframes 是那条预抽的路径,onUpdate 沿折线采样位置,sink 换成 canvas 就能同时画几十个球,投放本身用交错的思路错开起点。

import { Animation } from '@vega/anim';

function drop(dirs) {                       // dirs: 开局抽好的左右序列
  const pts = pathFromDirs(dirs);           // 落点折线 = 一条确定的路径
  const bin = dirs.filter((d) => d > 0).length;
  return new Animation({
    keyframes: [0, 1], duration: 1800, ease: 'linear',
    onUpdate: (p) => {                       // p: 0→1 线性时钟
      const s = sampleAlong(pts, p);         // 沿折线取点 + 逐段 easeInOut 摆动
      ball.x = s.x; ball.y = s.y;            // sink: 画到 <canvas>
    },
    onComplete: () => { bins[bin]++; },      // 落定 → 直方图 +1
  });
}

同属「非补间的信号源」的还有噪声三角函数,只是那里的信号是连续的周期或噪声,本页则是一串离散的随机抉择。播放器为何能在纯函数之上免费得到 seek 与反向,见播放控制