lzwcc/ ui-motion-lab
No.002 · Motion · 2026.10.08刷到自 adixcode.com
作品 / 动画

抖动像素波纹头像

指针划过时像素被推开再弹回,点击激起向外扩散的波环。每个像素都是一个带弹簧的粒子。

复刻canvas

把指针移过头像,像素会被推开,移走后再弹回原位。点击,或用手指轻触,会激起一圈向外扩散的波纹。头像停在视口里时,每秒还会自己泛起一圈。

Demo · 动手试试

灵感来自 adixcode.com 的头像卡片,但实现是重新写的。头像被缩成约 100 列,每个不透明像素变成一个粒子,保留原色。每个粒子有基准位置和位移,位移每帧向目标值靠近 12%,所以推开和回弹都带惯性。

指针是一个半径 100px 的排斥场,推力按三次方衰减。波纹是一个以 225px/s 扩散的环,带宽 ±37px,675ms 内线性消失;同时存在多个波纹时推力会叠加。动画只在有东西在动时才跑,全部静止后自动停止。

系统开启「减少动态效果」时,推力降到四分之一,并关闭自动波纹。

提示词

复制给 Claude、ChatGPT 或编码 agent,可以复现本页的头像。需要准备一张透明背景的头像 PNG。

用 Canvas 2D 把一张透明背景的头像做成可交互的像素粒子:指针划过时像素被推开再弹回,点击激起向外扩散的波环。不依赖外部库。

1. 采样:把图片中间最大的正方形画到一个 N×N 的离屏 canvas 上(N = 容器边长 / 4.5,限制在 56–128 之间,imageSmoothingQuality 设为 "high"),用 getImageData 读回。透明度低于 128 的像素丢掉,其余每个像素变成一个粒子,保留原色。粒子的基准位置取像素中心,按「容器边长 / N」放大,整体在容器里居中。
2. 数据:粒子有上万个,用 Float32Array 分别存基准 x、y 和位移 dx、dy,颜色打包成一个 Uint32(小端序写成 A<<24 | B<<16 | G<<8 | R)。不要用对象数组。
3. 受力:每帧给每个粒子算一个目标位移,指针和所有波纹的力相加。
   - 指针排斥:半径 100px。t = 1 − 距离 / 100,推力 = t³ × 40px,方向从指针指向粒子的当前位置(基准 + 位移)。先比较距离的平方,在范围内才开方。
   - 波纹:点击或触摸抬起时在该点生成。环的半径 = 225px/s × 经过时间;离环中心线 37px 以内的粒子被向外推,推力 = (1 − 离环距离 / 37) × (1 − 经过时间 / 675ms) × 20px。675ms 后删除。最多同时 12 个;有 n 个波纹时,波纹推力整体乘 1 + 0.5 × (n − 1)。
4. 弹簧:位移每帧向目标位移靠近 12%(按 60fps 定义)。换算成与帧率无关:ease = 1 − 0.88^(dt × 60),dt 最多按 0.05 秒算。位移小于 0.01px 时直接归零。
5. 绘制:复用一个 ImageData,每帧清零后把每个粒子画成边长等于放大倍数的实心方块(乘 devicePixelRatio,四舍五入对齐,相邻方块不留缝),最后 putImageData 一次。不要对每个粒子调用 fillRect。
6. 循环按需运行:有粒子还在动、有波纹、或指针在上面时才请求下一帧,全部静止就停止。容器在视口内时,每秒在随机位置自动激起一个波纹;滚出视口就停止。
7. 触屏:canvas 设 touch-action: none。pointerup 时激起波纹;非鼠标指针抬起后取消排斥;pointercancel 也取消排斥。容器尺寸变化后防抖 150ms 再重新采样。
8. prefers-reduced-motion:所有力乘 0.25,关闭自动波纹。