使用Canvas给DOM元素添加粒子效果

发布于 大漠

特别声明,本文根据@ZACH SAUCIER的《Adding Particle Effects to DOM Elements with Canvas》一文整理。

让我们来看看如何将<canvas>的自由度与HTML元素结合起来,使Web页面在视觉上有更好的效果。具体地说,我们将创建一个基于**HTML-to-particle**的效果,但同样的技术也可以用于实现很多类型的效果。

在开始之前,可以通过Repo获取源代码

创建初始的元素

首先,让我们创建一个HTML元素。这里创建了一个带有简单样式效果的按钮,事实上它可以是任何一个HTML元素。

建议使用Chrome、Firefox或Edge等现代浏览器查看这些Demo效果。

但是,如何让canvas“看到”这个元素,以便使用canvas操作每个像素 呢?为了实现这一点,需要对HTML元素进行快照 —— 就像“打印屏幕”一样,但只对希望在canvas中操作的特定元素(比如说按钮)进行快照。

创建canvas版本的元素

尽管浏览器到目前还无法实现这一点,但庆幸的是允许我们通过JavaScript来操作它,较为出名的html2canvas就可以。我们所要做的就是加载这个库,然后调用html2canvas(element),它将返回一个Promise以及一个canvas版本的元素。是不是很棒。

上面这个示例,我们有一个HTML版本和Canvas版本的按钮。我们可以使用Canvas版本作为我们的“屏幕快照”,并将其作为信息的来涨势,例如特定像素位置的颜色。

从Canvas中获取数据

为此,我们创建一个新函数来获取Canvas中特定位置的像素信息。我们也不需要显示从Canvas中得到的颜色数据,因为我们显示原始的HTML元素。

function getColorAtPoint(e) {
    // 获取鼠标点击位置的坐标
    let x = e.offsetX;
    let y = e.offsetY;
    
    // 获取该位置上颜色数据
    let rgbaColorArr = ctx.getImageData(x, y, 1, 1).data;

    // rgbaColorArr相关的处理
}

现在我们需要使用这些信息创建一个Canvas的粒子效果。

创建Canvas粒子效果

我们还没有使用Canvas来放置粒子,因为我们想要保留html2canvascanvas用于访问颜色信息。接下来创建粒子效果的函数。

var particleCanvas, particleCtx;
function createParticleCanvas() {
    // 创建用于粒子效果的Canvas
    particleCanvas = document.createElement("canvas");
    particleCtx = particleCanvas.getContext("2d");
    
    // Canvas的大小
    particleCanvas.width = window.innerWidth;
    particleCanvas.height = window.innerHeight;
    
    // Canvas的位置 
    particleCanvas.style.position = "absolute";
    particleCanvas.style.top = "0";
    particleCanvas.style.left = "0";
    
    // 设置Canvas在元素的上面
    particleCanvas.style.zIndex = "1001";
    
    // 确保它下面的其他元素是可点击的
    particleCanvas.style.pointerEvents = "none";
    
    // 把Canvas添加到页面中
    document.body.appendChild(particleCanvas);
}

获取坐标数据

我们还需要从本地坐标系统得到颜色相关的数据 —— 不仅仅是按钮的左上角,而且全局坐标的位置(相对整个Web页面),这样做是为了在Canvas的正确地方创建粒子。

我们可以这样做:

btn.addEventListener("click", e => {
    // 获取颜色数据
    let localX = e.offsetX;
    let localY = e.offsetY;
    let rgbaColorArr = ctx.getImageData(localX, localY, 1, 1).data;
    
    // 根据window获取按钮的位置
    let bcr = btn.getBoundingClientRect();
    let globalX = bcr.left + localX;
    let globalY = bcr.top + localY;
    
    // 使用window的位置获取颜色数据,创建一个粒子
    createParticleAtPoint(globalX, globalY, rgbaColorArr);
});

创建一个粒子原型

我们还可以创建一个基本粒子,是一个带有参数的绘图函数:

/* 圆形爆炸的粒子效果 */
var ExplodingParticle = function() {

    // 设置想要的粒子动画的时长
    this.animationDuration = 1000; // in ms

    // 设置粒子的速度
    this.speed = {
        x: -5 + Math.random() * 10,
        y: -5 + Math.random() * 10
    };
    
    // 粒子大小
    this.radius = 5 + Math.random() * 5;
    
    // 为粒子设定一个最大的生存时间
    this.life = 30 + Math.random() * 10;
    this.remainingLife = this.life;
    
    // 这个函数稍后将会调用动画相关的逻辑
    this.draw = ctx => {
        let p = this;

        if(this.remainingLife > 0
        && this.radius > 0) {
            // 在当前位置绘制一个圆
            ctx.beginPath();
            ctx.arc(p.startX, p.startY, p.radius, 0, Math.PI * 2);
            ctx.fillStyle = "rgba(" + this.rgbArray[0] + ',' + this.rgbArray[1] + ',' + this.rgbArray[2] + ", 1)";
            ctx.fill();
            
            // 更新粒子的位置和生命
            p.remainingLife--;
            p.radius -= 0.25;
            p.startX += p.speed.x;
            p.startY += p.speed.y;
        }
    }
}

创建一个粒子工厂

我们还需要一个基于一些坐标和颜色信息的函数,这个函数主要用来创建这些粒子,确保我们将它们添加到创建的粒子数组中:

var particles = [];
function createParticleAtPoint(x, y, colorData) {
    let particle = new ExplodingParticle();
    particle.rgbArray = colorData;
    particle.startX = x;
    particle.startY = y;
    particle.startTime = Date.now();
    
    particles.push(particle);
}

添加动画逻辑

我们还需要一种方法,让创建出来的粒子动起来。

function update() {
    // 清除旧粒子
    if(typeof particleCtx !== "undefined") {
        particleCtx.clearRect(0, 0, window.innerWidth, window.innerHeight);
    }

    // 在新位置绘制所有粒子
    for(let i = 0; i < particles.length; i++) {
        particles[i].draw(particleCtx);
        
        // 如果最后一个粒子完成动画,清除旧的粒子
        if(i === particles.length - 1) {
            let percent = (Date.now() - particles[i].startTime) / particles.animationDuration;
            
            if(percent > 1) {
                particles = [];
            }
        }
    }
    
    // 动画性能
    window.requestAnimationFrame(update);
}

window.requestAnimationFrame(update);

把这几个部分放在一起,现在可以基于HTML元素创建粒子。当我们点击它时,可以看到效果。

如果我们想要点击整个按钮会爆炸,而不仅是一个像素。我们只需要修改我们的点击函数。

let reductionFactor = 17;
btn.addEventListener("click", e => {
    // 获取按钮的颜色数据
    let width = btn.offsetWidth;
    let height = btn.offsetHeight
    let colorData = ctx.getImageData(0, 0, width, height).data;
    
    // 跟踪重复了多少次(为了减少创建的粒子总数)
    let count = 0;
    
    // 遍历按钮的每个位置并创建一个粒子
    for(let localX = 0; localX < width; localX++) {
        for(let localY = 0; localY < height; localY++) {
            if(count % reductionFactor === 0) {
                let index = (localY * width + localX) * 4;
                let rgbaColorArr = colorData.slice(index, index + 4);

                let bcr = btn.getBoundingClientRect();
                let globalX = bcr.left + localX;
                let globalY = bcr.top + localY;

                createParticleAtPoint(globalX, globalY, rgbaColorArr);
            }
            count++;
        }
    }
});

现在Web方面的限制相对较少,因为我们知道我们可以添加像canvas来创建更多的自由性。

通过使用边缘检测来判断元素是否超出容器边界(即隐藏在视图之外),并在元素超出边界时创建粒子。这样可以有更多的创意。

这样的一个过程可以写一篇文章来阐述,因为在Web浏览器中对元素的定位是复杂的,但是我写了一个叫做Disintegration的小插件,其中包括对这类事情的处理。

Disintegrate插件

Disintegrate是开源的,它处理了一些Web浏览器兼容这个效果的代码。它还允许多个元素在同一个页面上应用此效果,如果需要,可以忽略指定的颜色,以及处理过程中不同重要时刻的事件。

我们只需要在按钮上声明data-distype="contains",将使我们的元素超出容器边界时,粒子将被创建。比如下面这个Demo效果

使用Disintegrate的另一个效果。允许使用self-contained的粒子动画,比如我们之前所做的按钮效果。通过动画容器和主元素本身,可以创建更有趣的粒子效果。

然而,这种方法确实有其局限性(Disintegrate也是如此)。例如,它只能在IE11运行,因此在此之前缺乏ponter-events事件的支持。由于html2canvas的限制,Disintegration也不支持我们想要的所有DOM元素。我发现最受限的是CSS的transformclip-path

如果你想要安装Disintegrate,可以使用npm install disintegrate进行安装。或者可以在disintegrate.init()之前手动引用html2canvas.jsdisintegrate.js

Disintegrate是一个新写的插件,还有一些需要进行改进。如果你愿意,可以给Disintegrate和html2canvas提供相关的建议。

如何在你的项目中使用这个功能,或者说使用它们来扩展Web的功能,有较好的建议或想法的话,欢迎在下面的评论中与我们一起分享。air max 90 essential purple