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

Avatar of Zach Saucier
Zach Saucier 发布

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

让我们看看如何通过结合 <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 创建的另一种类型的效果是容器直接围绕我们的元素。这允许像我们之前创建的按钮效果一样进行 自包含粒子动画。通过动画化容器和我们的主要元素本身,我们可以以更有趣的方式创建粒子。

滑动解锁动画在结束时触发粒子爆炸。

然而,这种方法确实有其局限性(Disintegrate 也是如此)。例如,由于在 IE11 之前缺乏 pointer-events 支持(前提是 Disintegrate 编译为 ES5),它只能向后兼容到 IE11。由于 html2canvas 的限制,Disintegrate 也不支持我们能想到的每个 DOM 元素。我发现最具限制性的一个是 CSS 变换支持不完整,以及缺乏剪辑路径支持。

要安装 Disintegrate,如果使用 npm,可以使用 npm install disintegrate。或者,可以在调用 disintegrate.init() 之前手动包含 html2canvas.jsdisintegrate.js

Disintegrate 非常新,可以进行一些改进。如果您想贡献代码,Disintegrate 和 html2canvas 都是开源的,欢迎提出建议!

您认为此功能如何在您的项目中使用?您如何看待这种方法扩展网络上的可能性?请在评论中告诉我。