使用canvas给html元素添加粒子效果

原文: Adding Particle Effects to DOM Elements with Canvas | CSS-Tricks

创建初始DOM元素

首先,我们先创建基础HTML元素。这里使用一个简单的有一些样式的按钮,当然也可以使用其他任何HTML元素。

请使用Chrome,Firefox或者Edge浏览器

但是我们如何把它放在画布(canvas)中以便操作里面的像素?这就需要对HTML元素创建快照——就像是对我们想要操作的特定元素(这个按钮)的屏幕截图。

创建元素的canvas版本

虽然这里没有原生的实现方法,但是有一个很好用的插件html2canvas可以来帮助我们。我们需要做的只是加载这个库,调用html2canvas(element),它将返回带有元素的canvas版本值的Promise

这里我们有一个HTML版的按钮和一个Canvas的按钮。我们将把canvas的按钮作为“快照”和信息源,来获取类似指定区域像素颜色的值。

从画布获得数据

首先创建一个获得指定位置像素颜色的函数。因为我们想要展示原始的HTML元素,所以不需要显示作为颜色值来源的canvas。

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

  // 使用rgbaColorArr...
}

现在,我们需要用获得的信息创建一个cavas粒子。

创建用于展示粒子的画布

因为想保留使用html2canvas获得的画布,用来保存颜色信息,所以现在还没有用于放置粒子的画布,让我们再创建一个画布:

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";
  
  // 保证它在其他元素之上
  particleCanvas.style.zIndex = "1001";
  
  // 确保它下面的其他元素可点击
  particleCanvas.style.pointerEvents = "none";
  
  // 添加cavas到页面
  document.body.appendChild(particleCanvas);
}

获取坐标信息

我们需要继续从坐标上获取颜色信息——不限于按钮上边和左边(形成的坐标轴),也包括整个页面的坐标——来在画布的合适位置上创建粒子。

btn.addEventListener("click", e => {
  // 像上面那样获得颜色信息
  let localX = e.offsetX;
  let localY = e.offsetY;
  let rgbaColorArr = ctx.getImageData(localX, localY, 1, 1).data;
  
  // 获得按钮相对视窗的位置
  let bcr = btn.getBoundingClientRect();
  let globalX = bcr.left + localX;
  let globalY = bcr.top + localY;
  
  // 用获得的颜色创建一个粒子
  createParticleAtPoint(globalX, globalY, rgbaColorArr);
});

创建粒子原型函数

让我们创建一个使用变量绘制函数的基础粒子。

/* 使用circle实现“爆炸”效果粒子 */
var ExplodingParticle = function() {
  // 粒子动画时间
  this.animationDuration = 1000; // 单位毫秒 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[i].animationDuration[i];
      
      if(percent > 1) {
        particles = [];
      }
    }
  }
  
  // 执行动画
  window.requestAnimationFrame(update);
}

window.requestAnimationFrame(update);

好极了!

如果想让整个按钮都有这个效果而不只是在一个像素上,需要修改点击函数:

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;
  
  //  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++;
    }
  }
});

文章最后作者还分享了写的插件GitHub - ZachSaucier/Disintegrate: A small JS library to break DOM elements into animated Canvas particles.,可以方便的创建元素在到达容器边缘时的粒子效果

Comments
Write a Comment