张春成
2022/09/30阅读:22主题:默认主题
雨滴打在窗户上
雨滴打在窗户上
本文使用 Canvas 模拟了雨滴打在窗户上的视觉效果。
实现这个玩意需要在前端实现三个功能,分别是图像的径向模糊、Canvas 绘图的遮罩方式和动态帧动画。
实现过程可见我的前端笔记本
Rain drop simulation[1]
图像的径向模糊
用于模拟雨天透过窗户看世界的模糊感觉。其中,模糊的图像用来模拟雨天玻璃起雾时的场景,而清晰的图像用来模拟雨滴划过时的透亮图像。

清晰图像

模糊图像
实现方式为
blurImage = ((rawImg) => {
const context = DOM.context2d(width, height);
const img = new Image();
img.src = rawImg.src;
img.onload = () => {
context.drawImage(img, 0, 0, width, height);
const data = context.getImageData(0, 0, width, height);
d3.blurImage(data, blurDense);
context.putImageData(data, 0, 0);
};
return { context, img };
})(image1);
遮罩效果
接下来进行双层绘图,首先在底层绘制模糊图像,之后在上层绘制形似雨滴的圆周,最后在圆周中填充清晰图像。填充过程中,为了模拟雨滴的球形透视效果,将图像进行局部翻转。

const context = DOM.context2d(width, height);
// Background is the blur image
// Foreground is the clear image
// Draw background
const data = clearImage.context.getImageData(0, 0, width, height);
d3.blurImage(data, blurDense);
context.putImageData(data, 0, 0);
// Draw water drops
if (true) {
var x, y, s;
for (let index = 0; index < 30; ++index) {
(x = Math.round(d3.randomUniform(0.1, 0.9)() * width)),
(y = Math.round(d3.randomUniform(0.1, 0.9)() * height)),
(s = Math.round(Math.random() * 20));
// context.save();
context.save();
// Draw the masks
{
context.beginPath();
context.save(),
context.translate(x, y),
context.rotate(0.2),
context.rotate(Math.PI),
context.scale(1, 1.5),
context.arc(0, 0, s, 0, Math.PI * 2, true),
context.restore();
context.closePath();
}
// Draw inside the masks
// The image flips by 180 degrees
{
context.clip(),
context.save(),
context.translate(x, y),
context.scale(1, -1),
context.drawImage(clearImage.img, -x, -y, width, height),
context.restore();
}
context.restore();
// context.restore();
}
}
return context.canvas;
动态帧
最后,为了实现动态效果,我用到了动态帧的刷新方法。它是前端常用的动画渲染方法,大致可以保证动画帧的刷新频率为 60 帧。当然,如果计算能力不足,前端程序会自行确定合适的刷新频率。
简单来说,这个东西是通过循环调用某个渲染程序来实现动画绘制的,但它涉及一些复杂的程序堆栈操作,这导致它在接受外部控制的时候,容易忽略某些变量的实时变化,变得不可控。
比如 animation2Toggle 这个变量是即时的布尔型数值,它并没有申请静态内存指针,因此如果直接使用它来控制 loop 程序的起停,就会导致 loop 程序在启动时对它进行初始化,而初始化过程中会为“当时”的 true 值申请一段静态内存,但问题是这段内存并不受 animation2Toggle 这个变量的控制。因此,就会产生 animation2Toggle 变量能够用来启动 loop 程序,却无法让它停止的异常现象。
function loop() {
// DO NOT directly use the animationToggle here,
// Because it will be snaped and keep being true forever.
if (animation2State.animation) {
requestAnimationFrame(loop);
}
updateDrops();
draw();
}
// Start loop or draw once
if (animation2Toggle) {
// Draw loop
loop();
} else {
// Draw once
updateDrops();
draw();
}
// Update state
{
animation2State.animation = animation2Toggle;
return animation2State;
}
// Init state
const animation2State = {
const animation = false;
return { animation };
}
参考资料
Rain drop simulation: https://observablehq.com/@listenzcc/rain-drop-simulation
作者介绍