张春成

V2

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, 00, width, height);
        const data = context.getImageData(00, width, height);
        d3.blurImage(data, blurDense);
        context.putImageData(data, 00);
    };

    return { context, img };
})(image1);

遮罩效果

接下来进行双层绘图,首先在底层绘制模糊图像,之后在上层绘制形似雨滴的圆周,最后在圆周中填充清晰图像。填充过程中,为了模拟雨滴的球形透视效果,将图像进行局部翻转。

Untitled
Untitled
const context = DOM.context2d(width, height);

// Background is the blur image
// Foreground is the clear image

// Draw background
const data = clearImage.context.getImageData(00, width, height);
d3.blurImage(data, blurDense);
context.putImageData(data, 00);

// Draw water drops
if (true) {
    var x, y, s;

    for (let index = 0; index < 30; ++index) {
        (x = Math.round(d3.randomUniform(0.10.9)() * width)),
            (y = Math.round(d3.randomUniform(0.10.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(11.5),
                context.arc(00, s, 0Math.PI * 2true),
                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 };
}

参考资料

[1]

Rain drop simulation: https://observablehq.com/@listenzcc/rain-drop-simulation

分类:

后端

标签:

后端

作者介绍

张春成
V2