让我们看看如何通过结合 <canvas>
的自由度和 HTML 元素来使网页在视觉上更具功能性。具体来说,我们将创建一个基本的**HTML 到粒子效果**,但同样的技术可以用于多种效果。
在开始之前,请随时从代码库中获取源代码。
创建初始元素
首先,让我们创建一个 HTML 元素作为基础。我使用了一个简单的样式化按钮,但它实际上可以是任何 HTML 元素。
查看 CodePen 上 Zach Saucier (@Zeaklous) 编写的作品 DOM 到 Canvas #1。
需要使用 Chrome、Firefox 或 Edge 等现代浏览器才能查看这些演示。
但是,我们如何让画布“看到”此元素,以便我们可以使用画布**操作每个像素**?为了实现这一点,我们基本上需要**截取**我们的 HTML 元素的快照——就像“屏幕截图”,但仅针对我们想要在画布中操作的特定元素(按钮)。
创建元素的画布版本
尽管浏览器没有提供原生方法来执行此操作并允许我们在 JavaScript 中对其进行操作,但有一个非常方便的库名为 html2canvas 可以帮助我们。我们只需要加载库,然后调用 html2canvas(element)
,它将返回一个 Promise
以及我们元素的画布版本!太棒了。
查看 CodePen 上 Zach Saucier (@Zeaklous) 编写的作品 DOM 到 Canvas #2。
这里我们有一个 HTML 版本和一个画布版本的按钮并排放置。我们可以使用画布版本作为我们的“屏幕截图”和信息来源,例如特定位置像素的颜色。
获取画布数据
为此,让我们创建一个新函数来**获取特定位置的像素信息**。我们也不需要显示获取颜色数据的画布,因为我们希望显示原始的 HTML 元素。
function getColorAtPoint(e) {
// Get the coordinate of the click
let x = e.offsetX;
let y = e.offsetY;
// Get the color data of the canvas version of our element at that location
let rgbaColorArr = ctx.getImageData(x, y, 1, 1).data;
// Do something with rgbaColorArr
}
查看 CodePen 上 Zach Saucier (@Zeaklous) 编写的作品 DOM 到 Canvas #3。
现在我们需要使用这些信息创建一个画布粒子。
创建用于显示粒子的画布
我们还没有用于放置粒子的画布,因为我们希望保留从 html2canvas 获取的画布,仅用于访问颜色信息。所以让我们再创建一个
var particleCanvas, particleCtx;
function createParticleCanvas() {
// Create our canvas
particleCanvas = document.createElement("canvas");
particleCtx = particleCanvas.getContext("2d");
// Size our canvas
particleCanvas.width = window.innerWidth;
particleCanvas.height = window.innerHeight;
// Position out canvas
particleCanvas.style.position = "absolute";
particleCanvas.style.top = "0";
particleCanvas.style.left = "0";
// Make sure it's on top of other elements
particleCanvas.style.zIndex = "1001";
// Make sure other elements under it are clickable
particleCanvas.style.pointerEvents = "none";
// Add our canvas to the page
document.body.appendChild(particleCanvas);
}
获取坐标数据
我们还需要继续从本地坐标获取颜色数据——不仅是按钮的顶部和左侧,还有全局坐标中的位置(相对于整个网页),以便在画布上的正确位置创建粒子。
我们可以使用以下方法做到这一点
btn.addEventListener("click", e => {
// Get our color data like before
let localX = e.offsetX;
let localY = e.offsetY;
let rgbaColorArr = ctx.getImageData(localX, localY, 1, 1).data;
// Get the button's positioning in terms of the window
let bcr = btn.getBoundingClientRect();
let globalX = bcr.left + localX;
let globalY = bcr.top + localY;
// Create a particle using the color we obtained at the window location
// that we calculated
createParticleAtPoint(globalX, globalY, rgbaColorArr);
});
创建粒子原型
并且让我们也创建一个使用变量的基本粒子,该粒子具有**绘制**函数
/* An "exploding" particle effect that uses circles */
var ExplodingParticle = function() {
// Set how long we want our particle to animate for
this.animationDuration = 1000; // in ms
// Set the speed for our particle
this.speed = {
x: -5 + Math.random() * 10,
y: -5 + Math.random() * 10
};
// Size our particle
this.radius = 5 + Math.random() * 5;
// Set a max time to live for our particle
this.life = 30 + Math.random() * 10;
this.remainingLife = this.life;
// This function will be called by our animation logic later on
this.draw = ctx => {
let p = this;
if(this.remainingLife > 0
&& this.radius > 0) {
// Draw a circle at the current location
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();
// Update the particle's location and life
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() {
// Clear out the old particles
if(typeof particleCtx !== "undefined") {
particleCtx.clearRect(0, 0, window.innerWidth, window.innerHeight);
}
// Draw all of our particles in their new location
for(let i = 0; i < particles.length; i++) {
particles[i].draw(particleCtx);
// Simple way to clean up if the last particle is done animating
if(i === particles.length - 1) {
let percent = (Date.now() - particles[i].startTime) / particles[i].animationDuration[i];
if(percent > 1) {
particles = [];
}
}
}
// Animate performantly
window.requestAnimationFrame(update);
}
window.requestAnimationFrame(update);
将这些部分组合在一起,我们现在可以在点击 HTML 元素时创建基于该元素的粒子!
查看 CodePen 上 Zach Saucier (@Zeaklous) 编写的作品 DOM 到 Canvas #4。

不错!
如果我们希望在点击时“爆炸”整个按钮,而不是只有一个像素,我们只需要修改我们的点击函数
let reductionFactor = 17;
btn.addEventListener("click", e => {
// Get the color data for our button
let width = btn.offsetWidth;
let height = btn.offsetHeight
let colorData = ctx.getImageData(0, 0, width, height).data;
// Keep track of how many times we've iterated (in order to reduce
// the total number of particles create)
let count = 0;
// Go through every location of our button and create a particle
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++;
}
}
});
查看 CodePen 上 Zach Saucier (@Zeaklous) 编写的作品 DOM 到 Canvas #5。

希望现在网页不再像本文开头那样具有限制性,因为我们知道可以使用附加功能(如画布)来发挥更多创意自由。
我们可以通过使用边缘检测来判断我们的元素是否超出容器边界(即隐藏在视图之外)并在元素超出这些边界时创建粒子,从而变得更有创意。
可以围绕此过程撰写整篇文章,因为在 Web 浏览器中定位元素非常复杂,但我创建了一个名为 Disintegrate 的小型插件,其中包含对此类事物的处理。
Disintegrate:一个为您创建此效果的插件
Disintegrate 是开源的,并处理了使此技术的代码达到生产就绪状态所需的许多混乱之处。它还允许在同一页面上为多个元素应用此效果,指定要使用的容器,如果需要,忽略指定颜色,以及过程中不同重要时刻的事件。
使用 Disintegrate,我们只需要在按钮上声明 data-dis-type="contained"
,它就会使我们的元素超出其容器边界时创建粒子!查看演示。

我们可以使用 Disintegrate 创建的另一种类型的效果是容器直接围绕我们的元素。这允许像我们之前创建的按钮效果一样进行 自包含粒子动画。通过动画化容器和我们的主要元素本身,我们可以以更有趣的方式创建粒子。

然而,这种方法确实有其局限性(Disintegrate 也是如此)。例如,由于在 IE11 之前缺乏 pointer-events
支持(前提是 Disintegrate 编译为 ES5),它只能向后兼容到 IE11。由于 html2canvas 的限制,Disintegrate 也不支持我们能想到的每个 DOM 元素。我发现最具限制性的一个是 CSS 变换支持不完整,以及缺乏剪辑路径支持。
要安装 Disintegrate,如果使用 npm,可以使用 npm install disintegrate
。或者,可以在调用 disintegrate.init()
之前手动包含 html2canvas.js 和 disintegrate.js。
Disintegrate 非常新,可以进行一些改进。如果您想贡献代码,Disintegrate 和 html2canvas 都是开源的,欢迎提出建议!
您认为此功能如何在您的项目中使用?您如何看待这种方法扩展网络上的可能性?请在评论中告诉我。
不错的演示!
嗨!非常感谢你的帖子。这正是我正在开发的一个应用程序中需要的!
但是,我注意到
update
函数中有一个错误。您在particles.animationDuration
中缺少[i]
。后面的 if 语句永远不会结束!感谢你的提醒!
现在应该修复了:) 感谢通知。
我简直惊呆了!
就像那些按钮一样。
很酷的演示,但有一个问题会影响一小部分用户,并且将来可能会影响更多用户。
动画的时间取决于帧率,这归因于 rAF,这意味着在远高于 60hz 的帧率下,动画中的运动会大大加速。在我的配备 240hz 游戏显示器的机器上,粒子以非常快的速度移动,难以欣赏动画。虽然 240hz 游戏显示器在今天是一个极端情况,但 120hz 和 144hz 显示器正变得越来越普遍。尤其值得注意的是,iPad Pro 以以前从未见过的市场规模将 120hz 显示器引入非游戏玩家,并且我们可以预计在未来几年内,120hz 将在消费级手机、平板电脑和笔记本电脑上变得越来越普遍。
相关主题:https://stackoverflow.com/questions/19764018/controlling-fps-with-requestanimationframe
捕捉得很好,谢谢!我需要更新演示和 Disintegrate 以纠正此问题。
即使包含相同的代码,它对我也不起作用!请帮忙
当只有一个粒子发射时,点击按钮的右侧边缘不会创建相同颜色的粒子,而是创建代表按钮渐变中间区域的颜色。您知道是什么导致这种情况吗?