使用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来放置粒子,因为我们想要保留html2canvas
的canvas
用于访问颜色信息。接下来创建粒子效果的函数。
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的transform
和clip-path
。
如果你想要安装Disintegrate
,可以使用npm install disintegrate
进行安装。或者可以在disintegrate.init()
之前手动引用html2canvas.js
和disintegrate.js
。
Disintegrate是一个新写的插件,还有一些需要进行改进。如果你愿意,可以给Disintegrate和html2canvas
提供相关的建议。
如何在你的项目中使用这个功能,或者说使用它们来扩展Web的功能,有较好的建议或想法的话,欢迎在下面的评论中与我们一起分享。air max 90 essential purple