Canvas 2D 水波纹:涟漪扩散的数学与优化
怎么让点击有水的感觉
鼠标点击屏幕,以点击位置为圆心,一个半透明圆环向外扩散——越扩越大、越来越淡,直到消失。这就是"水波纹"的视觉效果。
实现上就是 Canvas 2D 画圆。每个涟漪是一个对象:
{
x, y, // 圆心
radius: 0, // 当前半径
maxRadius: 80, // 最大半径(扩散到这就消失)
opacity: 0.6, // 当前透明度
speed: 1.5, // 扩散速度(px/帧)
}
每帧做的事情:
1. radius += speed — 扩散
2. opacity = 0.6 × (1 - radius / maxRadius) — 越远越淡
3. ctx.arc(x, y, radius) — 画圆
4. ctx.stroke() — 只描边,不填充
消除前先按半径从大到小排序,大的先画(因为大的更淡,被小的覆盖也看不出来)。
DPR 缩放
window.devicePixelRatio 把 Canvas 的物理像素和 CSS 像素对齐:
canvas.width = window.innerWidth * dpr
canvas.height = window.innerHeight * dpr
ctx.scale(dpr, dpr)
不处理 DPR 的话,在 Retina 屏幕上 Canvas 相当于被放大 2~3 倍,圆环边缘锯齿明显。scale(dpr, dpr) 后,所有坐标用 CSS 像素值,Canvas 自动渲染到对应的物理分辨率。
避免 DOM 拖慢
水波纹 Canvas 放在独立层,不跟页面 DOM 树重合。用 position: fixed + z-index: 95 盖在全页面之上,但 pointer-events: none 让它不拦截任何点击。用户点击照常穿透到下面的按钮/链接。
多点触控
touchstart 事件中,e.touches 能拿到所有手指的坐标。每个手指创建一个涟漪——五指同时按就同时扩散五个圈。pointercancel 和 touchcancel 时做清理,防止遗留半截涟漪。
上限控制
Canvas 不是零成本的。每帧画 200 个涟漪会掉帧。设置上限 50 个,超过时 shift() 移除最老的。
整个动画用一个 requestAnimationFrame 循环驱动,60fps。涟漪列表为空时自动暂停循环——没东西画就不浪费 CPU。
---
水波纹是一个低成本但高感知的反馈:用户点击任何一个地方都能"看到"自己的操作,不需要 loading spinner 或 toast。它的存在感来自视觉,存在成本来自数学。