☰
水彩颜料的流体扩散:用 Canvas 2D 实现带有阻尼感的点击涟漪动效
2026/10/11 8:04:03 网站建设 项目流程

在现代网页交互中,点击反馈几乎已经成了一种标准配置。但每当你点开许多现成的组件库,所见的大多是整齐划一、机械扩张的标准正圆形。那种毫无波澜的匀速放大与线性渐隐,虽然规矩高效,却冰冷得像一台流水线上的冲压机。

而在真实的物理世界里,当一滴饱蘸水分的秋日赭石颜料轻轻触碰浸润过的棉浆水彩纸时,那一瞬间的扩散是极其温柔而富有生命的。颜料带着液体的黏性与惯性向外推移,受到纸张微观纤维的天然阻滞,速度由快到慢缓缓止息;它的外缘绝不是一条光滑如手术刀般的正圆弧线,而是随着纸张吸水肌理生出细微的起伏与墨迹积聚。

为了给手账单页增添一份会呼吸的手工温度,我们可以用不到一百行的原生 Canvas 2D 代码,模拟这种具有流体阻尼感的水彩晕染涟漪。

物理通感:解构一滴水彩的扩散密码

要想把生硬的数学图形转化为具有水彩质感的视觉语言,我们需要在渲染管线中植入三个核心的感官维度:

第一是运动阻尼感(Fluid Damping)。液体在纤维介质中扩散时,初始阶段在表面张力与重力作用下迅速展开,随后迅速被毛细孔隙吸附,呈现出强烈的非线性减速。我们不能使用简单的线性插值(Linear),而应当采用三次缓出曲线(Ease-Out Cubic)或衰减弹簧模型。

第二是边缘的不规则微扰(Organic Wobble)。纯粹的圆给人工业感,而大自然的本质是不规则的。通过在极坐标系下将圆周角度分解,利用几个不同频率的正弦与余弦谐波进行叠加微扰,可以让圆弧边缘产生类似手工撕边纸或湿画晕开的自然波动。

第三是色素密度的层次分布(Pigment Falloff)。水彩最迷人的特质是“水色交融”。在水滴中心,色素浓度相对聚集;在向外晕染的过程中,水分稀释色素,透明度呈现非线性跌落;而在波纹即将终结的最外缘,又往往会因为毛细管边缘效应(Coffee-ring effect)形成一层极浅的墨色收口。

Canvas 2D 核心实现

我们不需要引入笨重的 WebGL 渲染管线,纯粹的原生 Canvas 2D API 在处理局部的点击反馈时,其性能与能耗表现最为轻巧。

下面是封装好的水彩扩散触发器:

class WatercolorRipple { constructor(canvas) { this.canvas = canvas; this.ctx = canvas.getContext('2d'); this.ripples = []; this.animating = false; this.resize(); window.addEventListener('resize', () => this.resize()); } resize() { // 适配高清屏幕像素比,确保水彩边缘细腻不模糊 const dpr = window.devicePixelRatio || 1; this.width = this.canvas.clientWidth; this.height = this.canvas.clientHeight; this.canvas.width = this.width * dpr; this.canvas.height = this.height * dpr; this.ctx.scale(dpr, dpr); } // 外部调用:在指定坐标滴落一滴水彩 drop(x, y, colorRgb = [190, 110, 80]) { this.ripples.push({ x, y, rgb: colorRgb, progress: 0.0, maxRadius: 40 + Math.random() * 25, // 为每个水滴生成随机的边缘扰动相位 seed: Math.random() * 10 }); if (!this.animating) { this.animating = true; requestAnimationFrame(() => this.render()); } } render() { this.ctx.clearRect(0, 0, this.width, this.height); for (let i = this.ripples.length - 1; i >= 0; i--) { const rip = this.ripples[i]; rip.progress += 0.022; // 控制扩散速度 if (rip.progress >= 1.0) { this.ripples.splice(i, 1); continue; } // 三次缓出曲线模拟流体阻尼: 1 - (1 - t)^3 const ease = 1 - Math.pow(1 - rip.progress, 3); const currentRadius = rip.maxRadius * ease; const alpha = (1 - rip.progress) * 0.35; // 整体柔和半透 this.drawOrganicDrop(rip.x, rip.y, currentRadius, rip.rgb, alpha, rip.seed); } if (this.ripples.length > 0) { requestAnimationFrame(() => this.render()); } else { this.animating = false; // 扩散完毕后彻底休眠,零 CPU 占用 } } drawOrganicDrop(cx, cy, radius, rgb, baseAlpha, seed) { const ctx = this.ctx; ctx.save(); ctx.beginPath(); // 在 360 度圆周上进行谐波扰动,生成有机边缘 const segments = 48; for (let step = 0; step <= segments; step++) { const theta = (step / segments) * Math.PI * 2; // 利用双重谐波构造手绘边缘起伏 const wobble = Math.sin(theta * 3 + seed) * 2.5 + Math.cos(theta * 5 - seed) * 1.5; const r = Math.max(1, radius + wobble); const px = cx + Math.cos(theta) * r; const py = cy + Math.sin(theta) * r; if (step === 0) { ctx.moveTo(px, py); } else { ctx.lineTo(px, py); } } ctx.closePath(); // 构建非对称水彩径向渐变 const grad = ctx.createRadialGradient(cx, cy, 0, cx, cy, radius); const [r, g, b] = rgb; grad.addColorStop(0.0, `rgba(${r}, ${g}, ${b}, ${baseAlpha * 1.2})`); grad.addColorStop(0.6, `rgba(${r}, ${g}, ${b}, ${baseAlpha * 0.7})`); grad.addColorStop(0.9, `rgba(${r}, ${g}, ${b}, ${baseAlpha * 0.4})`); grad.addColorStop(1.0, `rgba(${r}, ${g}, ${b}, 0)`); ctx.fillStyle = grad; ctx.fill(); ctx.restore(); } }

与手账交互界面的优雅缝合

这套实现的美妙之处在于它是非侵入式的。我们只需要在整个界面的最顶层挂载一个轻量且具备穿透属性(pointer-events: none)的透明 Canvas 覆盖层:

<canvas id="water-layer" style="position: fixed; inset: 0; width: 100%; height: 100%; pointer-events: none; z-index: 999;"></canvas>

随后在全局监听点击或者指针释放事件:

const canvas = document.getElementById('water-layer'); const rippleEffect = new WatercolorRipple(canvas); document.addEventListener('pointerdown', (e) => { // 根据点击元素的心情属性,映射出不同的秋季水彩原色 // 如:赭石色 [180, 105, 75]、群青色 [90, 120, 150]、若竹色 [110, 145, 120] rippleEffect.drop(e.clientX, e.clientY, [180, 110, 80]); });

颜料交融:用正片叠底实现多次点击的自然混色

在传统的水彩作画中,当两滴不同颜色的湿润颜料在同一片区域相遇时,它们并不会简单地覆盖彼此,而是会在交界处发生温润的渗色与混色。

在 Canvas 2D 中,要还原这种真实的物理触感,我们可以巧妙地利用复合操作模式(Global Composite Operation):

// 在绘制水滴前设置混合模式为正片叠底 ctx.globalCompositeOperation = 'multiply';

当开启了multiply(正片叠底)之后,如果你在同一个位置快速连续点击两次——比如第一次滴落了一抹淡淡的秋菊黄,紧接着又滴落了一滴浅浅的若竹绿,两次波纹在交叠扩散的瞬间,重叠区域会自动通过色素相乘计算出通透而富有层次感的橄榄绿。颜色的饱和度随着水分交融自然加深,边缘泛起深色的色素积聚圈,完全重现了水彩在画纸上干湿交替时那种灵动的质感。

与此同时,为了确保在各种移动设备和高分辨率 Retina 屏幕上不出现像素颗粒模糊,我们在初始化画布时动态绑定了设备的硬件像素比(devicePixelRatio),并将离屏缓冲与物理渲染严格对应。在保持视觉极致柔和的同时,整个计算管线完全运行在主线程的微任务间隙,绝不挤占滚动与打字的流畅度。

融入画境的触感

当你在屏幕上轻按鼠标或触控板,光标下方并不会弹出一块冷硬突兀的几何高亮,而是一滴温润的秋日水彩顺着纤维微微震颤、徐徐弥散,最后悄无声息地隐没在米白色的背景底色里。动画结束的瞬间,requestAnimationFrame循环彻底归零,整个系统重回静默,不耗费一分一毫多余的电量。

好的前端交互,不应该用刺眼的转场和喧闹的弹跳去争抢用户的注意力。它应当像秋水入池,在指尖落下的一刻泛起恰到好处的涟漪,随后归于平静,只留下一份温润而舒适的从容。在这个被工业化组件模板充斥的数字世界里,为自己的手账单页亲手调配一抹灵动的水色,能让每一次点击都变成一次轻盈而愉悦的指尖旅行。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询