噪声:无尽的微动
插值与弹簧都在演化到一个目标:有明确的起点、终点,到达后静止。待机微动恰恰相反——漂浮的图标、呼吸的光晕、列表里轻轻晃动的卡片,它们没有终点、永不停止,也不能有肉眼可辨的重复周期。
驱动这类动画的不是缓动曲线,而是 value noise:一条平滑、有界、却又不重复的随机信号。
1 · 群体微动
每个方块分到一段独立相位的噪声,取四路分别驱动位移、旋转与缩放。相位错开是群体不同步的全部机制——共用同一个 fbm 函数,只是采样位置差一个常数。
2 · 为什么不是白噪声
白噪声每个采样点都独立随机,相邻值毫无关联,信号尖锐跳变,直接拿来驱动 transform 只会得到剧烈抖动。
value noise 先在整数格点上放随机值,格点之间用平滑插值填充。本页用的 fade 曲线是 ,即 smoothstep:它在 与 处导数为 0,因此拼接处不仅值连续、斜率也连续,信号看起来是缓慢游移而非折线转折。
注 · 格点表只有 512 项且用 i & 511 取模,所以这条噪声的周期就是 512。以默认基频 1 与流速 1 计,
每秒推进 1,走完一个周期要 512 秒——八分半钟。「无可感知的重复」靠的是周期远长于观看时长,不是真正的非周期。要真正无限,得换成基于哈希的噪声(Perlin、Simplex 都属此列),格点值由坐标现算而非查表。
3 · 倍频叠加
单层噪声只有一个尺度的起伏,看着像匀速摇摆。把若干倍频叠起来——频率逐层翻倍、幅度逐层减半——就得到 fractal noise:低频负责大尺度的缓慢漂移,高频负责细小的颤动。层数就是控件里的 octaves。
警示 · 幅度按 递减,总和收敛到 2,所以叠加结果必须除以实际累计的幅度和才回到 。漏掉这一步的表现很隐蔽:层数越多整体幅度越小(而不是越界),看上去像是「octaves 调大反而变安静」——很容易误判成频率的锅。
// 整数格点放随机值, 格点间用 smoothstep 平滑插值 → 连续 1D 信号
const fade = (t) => t * t * (3 - 2 * t);
function valueNoise(x) {
const i = Math.floor(x), f = x - i;
return lerp(PERM[i & 511], PERM[(i + 1) & 511], fade(f)) * 2 - 1;
}
// 倍频叠加: 频率逐层翻倍、幅度逐层减半 → fractal noise
function fbm(x, octaves) {
let sum = 0, amp = 1, freq = 1, norm = 0;
for (let o = 0; o < octaves; o++) {
sum += amp * valueNoise(x * freq);
norm += amp; amp *= 0.5; freq *= 2;
}
return sum / norm;
}
// rAF 持续推进 t —— 无起点终点, 永不停止
function loop(now) {
t += (now - last) / 1000 * speed; last = now;
const dx = fbm(t * freq, octaves) * amplitude;
el.style.transform = `translate(${dx}px, 0)`;
requestAnimationFrame(loop);
}
演化到固定目标、有明确时长的塑形见缓动;把目标交给物理系统自己收敛的见弹簧。噪声补上第三种范式——不收敛、不重复、为持续在场而生。规则的周期信号则见三角函数。