1 个月前 Web canvas

2D图文粒子动画

步骤分解

实现 getPixels 方法

用于将目标转换成像素点,该方法返回一个像素点数组[{x, y, rgba}],xy 为像素点的位置,rgba 为对应颜色。实现逻辑主要通过 canvas getImageData 方法获取像素数据然后遍历找出有颜色的像素点。

// 获取目标对象的像素点,space 用于稀释像素点,值越大返回的像素点越少
function getPixels(target, space = 5) {
  const canvas = document.createElement('canvas')
  const ctx = canvas.getContext('2d')
  const viewWidth = window.innerWidth
  const viewHeight = window.innerHeight

  canvas.width = viewWidth
  canvas.height = viewHeight

  if (typeof target === 'string') {
    // 绘制文字
    ctx.font = '150px bold'
    ctx.fillStyle = '#fff'
    ctx.textBaseline = 'middle'
    ctx.textAlign = 'center'
    ctx.fillText(target, viewWidth / 2, viewHeight / 2)
  } else {
    // 绘制图片
    ctx.drawImage(
      target,
      (viewWidth - target.width) / 2,
      (viewHeight - target.height) / 2,
      target.width,
      target.height
    )
  }

  // 获取像素数据
  const { data, width, height } = ctx.getImageData(0, 0, viewWidth, viewHeight)
  const pixeles = []
  // 遍历像素数据,用space减少取到的像素数据
  for (let x = 0; x < width; x += space) {
    for (let y = 0; y < height; y += space) {
      const pos = (y * width + x) * 4 // 每个像素点由 rgba 四个值组成,所以需要乘以4才能得到正确的位置
      // 只提取 rgba 中透明度大于0.5的像素,imageData 里 aplha 128等于 rgba 中 alpha 的 0.5
      if (data[pos + 3] > 128) {
        pixeles.push({
          x,
          y,
          rgba: [data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]
        })
      }
    }
  }

  return pixeles
}

实现粒子类

用于将像素点数据转换成粒子对象,在后面动画帧里会操作粒子对象移动完成动画成像,粒子对象包含 x, y, tx, ty, color, radius 等属性,xy 为粒子当前位置,tx ty 为成像的目标位置。

class Particle {
constructor({ x = 0, y = 0, tx = 0, ty = 0, radius = 2, color = '#F00000' }) {
  // 当前坐标
  this.x = x
  this.y = y
  // 目标点坐标
  this.tx = tx
  this.ty = ty
  this.radius = radius
  this.color = color
}
draw(ctx) {
  ctx.save()
  ctx.translate(this.x, this.y)
  ctx.fillStyle = this.color
  // ctx.fillRect(0, 0, this.radius * 2, this.radius * 2)
  ctx.beginPath()
  ctx.arc(0, 0, this.radius, 0, Math.PI * 2, true)
  ctx.closePath()
  ctx.fill()
  ctx.restore()
  return this
}

实现动画逻辑

通过 requestAnimationFrame 循环绘制画布,遍历所有的粒子对象,通过缓动动画算法操控它们的位置,直到所有粒子移动到目标位置完成成像才取消绘制。实现一个 drawFrame 方法来完成动画的逻辑,该方法接收粒子对象集合以及一个完成动画的回调函数。

// 传入粒子对象绘制动画帧,并接受一个动画结束的回调
function drawFrame(particles, finished) {
  const timer = window.requestAnimationFrame(() => {
    drawFrame(particles, finished)
  })
  ctx.clearRect(0, 0, canvas.width, canvas.height)
  // 缓动系数
  const easing = 0.06
  const finishedParticles = particles.filter(particle => {
    // 当前坐标和目标点之间的距离
    const dx = particle.tx - particle.x
    const dy = particle.ty - particle.y
    // 速度
    let vx = dx * easing
    let vy = dy * easing

    // 当距离小于0.1表示粒子已完成动画
    if (Math.abs(dx) < 0.1 && Math.abs(dy) < 0.1) {
      particle.finished = true
      particle.x = particle.tx
      particle.y = particle.ty
    } else {
      particle.x += vx
      particle.y += vy
    }
    particle.draw(ctx)
    return particle.finished
  })

  if (finishedParticles.length === particles.length) {
    window.cancelAnimationFrame(timer)
    finished && finished()
  }
}

创建粒子

实现 createParticles 方法创建粒子,对传入的文字提取粒子,返回粒子集合。

// 创建粒子
function createParticles({ text, radius, space }) {
  const pixeles = getPixels(text, space)
  return pixeles.map(({ x, y, rgba: color }) => {
    return new Particle({
      x: Math.random() * window.innerWidth,
      y: Math.random() * window.innerHeight,
      tx: x,
      ty: y,
      radius,
      color: `rgba(${color})`
    })
  })
}

调用

最后通过组合调用这些函数就能完成粒子动画效果:

const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')

canvas.width = window.innerWidth
canvas.height = window.innerHeight

const particles = createParticles({ text: 'JS', radius: 2, space: 5 }

drawFrame(particles, () => { console.log('动画已完成') })

优化随机位置

在 createParticles 方法里初始粒子的随机位置使用的是基于视图的大小随机的,效果不是很好。可以改用基于圆形分布的随机位置,让效果更加自然。创建一个获取圆形随机位置的方法 getRandomPos,该方法接受一个圆形半径为参数,并返回随机 x,y 坐标。

// 获取圆形的随机分布位置
function getRandomPos(maxRadius = canvas.width / 1.3) {
    const radius = Math.sqrt(Math.random()) * maxRadius
    const angle = Math.PI * 2 * Math.random()
    const x = canvas.width / 2 + Math.cos(angle) * radius
    const y = canvas.height / 2 + Math.sin(angle) * radius
    return { x,y }
}

// 修改 createParticles 方法中 xy 赋值逻辑
function createParticles (...) {
    // ...
    const randomPos = getRandomPos()
    return { x: randomPos.x, y: randomPos.y }
}

基于圆形随机分布:http://jsrun.net/PkIKp

基于视图大小随机分布:http://jsrun.net/SAwKp