Canvas 粒子时钟

lxf2023-03-14 07:27:01

我正在参加「码上AdminJS挑战赛」详情请看:码上AdminJS挑战赛来了!

码上AdminJS Demo 展示

目录
  • 问题来了,绘制粒子时钟一共分几步?
    • 第一步,创建一个隐藏的文本canvas
    • 第二步,绘制粒子,%20创建递归延时器
    • 第三步,走两步,没病走两步
  • 核心运动算法
    • 上课啦!!!三角函数的计算,你还记得么?
    • JavaScript%20中三角函数的计算
    • 通过粒子的两个坐标,计算粒子的运动角度
  • 传送门

问题来了,绘制粒子时钟一共分几步?

第一步,创建一个隐藏的文本canvas

因为需要绘制出表示时间的粒子效果。所以我们需要一个隐藏的%20canvas,先绘制出表示当前时间的文本内容。

通过%20Date%20对象获取当前时间的时、分、秒
const%20now%20=%20new%20Date()
const%20h%20=%20now.getHours().toString().padStart(2,%20'0')
const%20m%20=%20now.getMinutes().toString().padStart(2,%20'0')
const%20s%20=%20now.getSeconds().toString().padStart(2,%20'0')
const%20text%20=%20`${h}:${m}:${s}`
创建%20canvas%20绘制时间文本
const%20textCanvas%20=%20document.createElement('canvas')
const%20textCtx%20=%20textCanvas.getContext('2d')
textCanvas.width%20=%20cWdith
textCanvas.height%20=%20cHeight
textCtx.font%20=%20'280px%20SimSun,%20Songti%20SC%20'
textCtx.textAlign%20=%20'center'
textCtx.textBaseline%20=%20'middle'
textCtx.fillStyle%20=%20'rgb(243,%2015,%2015)'
textCtx.fillText(text,%20cWdith%20/%202,%20cHeight%20/%202)
通过%20context.getImageData()%20来获取时间文本的像素点。配置%20distance%20是为了使粒子展示的时候分散一些
const%20imgData%20=%20textCtx.getImageData(0,%200,%20cWdith,%20cHeight).data
//%20粒子点位数组
const%20curDotArr%20=%20[]
for%20(let%20x%20=%200;%20x%20<%20cWdith;%20x%20+=%20distance)%20{
%20%20for%20(let%20y%20=%200;%20y%20<%20cHeight;%20y%20+=%20distance)%20{
%20%20%20%20const%20i%20=%20((y%20*%20cWdith)%20+%20x)%20*%204;
%20%20%20%20//%20243%20对应的是上面设置的字体颜色%20rgb(243,%2015,%2015)
%20%20%20%20if%20(imgData[i]%20===%20243)%20{
%20%20%20%20%20%20curDotArr.push({%20x,%20y%20})
%20%20%20%20}
%20%20}
}

这样通过第一步,我们就拿到了一个时间的所有粒子点位。接下来就是来绘制粒子

第二步,绘制粒子,%20创建递归延时器

所谓的‘粒子’其实不过就是%20canvas%20绘制出来的实心圆而已

封装%20Particle%20粒子对象
function%20Particle(opt)%20{
%20%20this.ctx%20=%20opt.ctx
%20%20this.canvas%20=%20opt.ctx.canvas
%20%20this.x%20=%20opt.x%20//%20坐标
%20%20this.y%20=%20opt.y%20//%20坐标
%20%20this.color%20=%20opt.color%20//%20颜色
%20%20this.radius%20=%20opt.radius%20//%20半径
}

Particle.prototype.draw%20=%20function()%20{
%20%20this.ctx.beginPath()
%20%20this.ctx.arc(this.x,%20this.y,%20this.radius,%200,%20Math.PI%20*%202)
%20%20this.ctx.fillStyle%20=%20this.color
%20%20this.ctx.closePath()
%20%20this.ctx.fill()
}
创建递归延时器,绘制每一秒时间文本的粒子。给粒子设置点位%20Id,可避免一些粒子实例的重复创建
//%20循环粒子集合,创建粒子实例
function%20computed(dotArr)%20{
%20%20const%20arr%20=%20[]
%20%20dotArr.forEach(dot%20=>%20{
%20%20%20%20let%20particle%20=%20null
%20%20%20%20const%20id%20=%20`${dot.x}-${dot.y}`
%20%20%20%20const%20tX%20=%20dot.x%20+%20utils.getRandom(-15,%2015)
%20%20%20%20const%20tY%20=%20dot.y%20+%20utils.getRandom(-15,%2015)
%20%20%20%20if%20(particleList.length)%20{
%20%20%20%20%20%20particle%20=%20particleList.shift()
%20%20%20%20%20%20if%20(particle.id%20!==%20id)%20{
%20%20%20%20%20%20%20%20particle.id%20=%20id
%20%20%20%20%20%20%20%20particle.x%20=%20tX
%20%20%20%20%20%20%20%20particle.y%20=%20tY
%20%20%20%20%20%20}
%20%20%20%20}%20else%20{
%20%20%20%20%20%20particle%20=%20new%20window.Particle({
%20%20%20%20%20%20%20%20ctx,
%20%20%20%20%20%20%20%20x:%20tX,
%20%20%20%20%20%20%20%20y:%20tY,
%20%20%20%20%20%20%20%20color:%20utils.getRandomColor(),
%20%20%20%20%20%20%20%20radius:%202
%20%20%20%20%20%20})
%20%20%20%20%20%20particle.id%20=%20id
%20%20%20%20}
%20%20%20%20arr.push(particle)
%20%20})
%20%20particleList%20=%20arr
}

//%20递归绘制时间,获取时间文本粒子集合
function%20timer()%20{
%20%20setTimeout(()%20=>%20{
%20%20%20%20textCtx.clearRect(0,%200,%20cWdith,%20cHeight)
%20%20%20%20const%20now%20=%20new%20Date()
%20%20%20%20const%20h%20=%20now.getHours().toString().padStart(2,%20'0')
%20%20%20%20const%20m%20=%20now.getMinutes().toString().padStart(2,%20'0')
%20%20%20%20const%20s%20=%20now.getSeconds().toString().padStart(2,%20'0')
%20%20%20%20const%20text%20=%20`${h}:${m}:${s}`
%20%20%20%20textCtx.fillStyle%20=%20'rgb(243,%2015,%2015)';
%20%20%20%20textCtx.fillText(text,%20cWdith%20/%202,%20cHeight%20/%202)
%20%20%20%20const%20imgData%20=%20textCtx.getImageData(0,%200,%20cWdith,%20cHeight).data
%20%20%20%20const%20curDotArr%20=%20[]
%20%20%20%20for%20(let%20x%20=%200;%20x%20<%20cWdith;%20x%20+=%20distance)%20{
%20%20%20%20%20%20for%20(let%20y%20=%200;%20y%20<%20cHeight;%20y%20+=%20distance)%20{
%20%20%20%20%20%20%20%20const%20i%20=%20((y%20*%20cWdith)%20+%20x)%20*%204;
%20%20%20%20%20%20%20%20if%20(imgData[i]%20===%20243)%20{
%20%20%20%20%20%20%20%20%20%20curDotArr.push({%20x,%20y%20})
%20%20%20%20%20%20%20%20}
%20%20%20%20%20%20}
%20%20%20%20}
%20%20%20%20computed(curDotArr)
%20%20%20%20draw()
%20%20%20%20timer()
%20%20},%201000)
}

//%20绘制粒子
function%20draw()%20{
%20%20ctx.clearRect(0,%200,%20cWdith,%20cHeight)
%20%20particleList.forEach(i%20=>%20{
%20%20%20%20i.draw()
%20%20})
}

timer()

这样通过第二步,我们已经实现了绘制时间的粒子文本了

第三步,走两步,没病走两步

让粒子‘走’起来。这一步也是最关键的一步。

扩展粒子钩子函数的能力

function Particle(opt) {
  this.ctx = opt.ctx
  this.canvas = opt.ctx.canvas
  this.x = opt.x // 坐标
  this.y = opt.y // 坐标
  this.color = opt.color // 颜色
  this.radius = opt.radius // 半径
  // ------ 新能力 -------
  this.destroyed = false
  this.directionDeg = opt.directionDeg || Math.random() * 360 // 运动角度
  this.speed = opt.speed || 1 // 速度
  this.speedDis = opt.speedDis || 1 // 速度变化系数
  this.computedDirectionSpeed()
  // -----------------
}

// 计算 x y 加速度
Particle.prototype.computedDirectionSpeed = function() {
  this.speedX = this.speed * Math.cos(Particle.utils.radian(this.directionDeg)) // x轴的加速度
  this.speedY = this.speed * Math.sin(Particle.utils.radian(this.directionDeg)) // y轴的加速度
}

Particle.prototype.draw = function() {
  this.ctx.beginPath()
  this.ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2)
  this.ctx.fillStyle = this.color
  this.ctx.closePath()
  this.ctx.fill()
}

// 更新粒子坐标
Particle.prototype.update = function() {
  this.x += this.speedX
  this.y += this.speedY
  this.radius += this.radiusDis
  this.speedX *= this.speedDis
  this.speedY *= this.speedDis
  if (this.x > this.canvas.width || this.x < 0 || this.y < 0 || this.y > this.canvas.height) {
    this.destroyed = true
  }
  if (this.radius < 1) {
    this.destroyed = true
  }
}

在递归中,给存在的粒子设置目标位置,并计算粒子运动角度

function computed(dotArr) {
  const arr = []
  dotArr.forEach(dot => {
    let particle = null
    const id = `${dot.x}-${dot.y}`
    const tX = dot.x + utils.getRandom(-15, 15)
    const tY = dot.y + utils.getRandom(-15, 15)
    if (particleList.length) {
      particle = particleList.shift()
      if (particle.id !== id) {
        particle.id = id
        // particle.x = tX
        // particle.y = tY
        particle.targetX = tX
        particle.targetY = tY
        // 重新计算运动方向
        particle.directionDeg = utils.pointsToAngle(particle.x, particle.y, tX, tY)
        particle.computedDirectionSpeed()
      }
    } else {
      const cX = dot.x + utils.getRandom(-15, 15)
      const cY = dot.y + utils.getRandom(-15, 15)
      particle = new window.Particle({
        ctx,
        x: cX,
        y: cY,
        color: utils.getRandomColor(),
        radius: 2,
        speed: 5,
        directionDeg: utils.pointsToAngle(cX, cY, tX, tY)
      })
      particle.id = id
      particle.targetX = tX
      particle.targetY = tY
    }
    arr.push(particle)
  })
  particleList = arr
}

在绘画粒子的函数中,通过 requestAnimationFrame()更新粒子坐标,绘制粒子动画

// 绘制粒子
function draw() {
  ctx.clearRect(0, 0, cWdith, cHeight)
  particleList.forEach(i => {
    if (!i.destroyed) {
      i.update()
      if ((i.speedX > 0 && i.x >= i.targetX) || (i.speedX < 0 && i.x <= i.targetX)) {
        i.x = i.targetX
        if ((i.speedY > 0 && i.y >= i.targetY) || (i.speedY < 0 && i.y <= i.targetY)) {
          i.destroyed = true
          i.y = i.targetY
        }
      } else if ((i.speedY > 0 && i.y >= i.targetY) || (i.speedY < 0 && i.y <= i.targetY)) {
        i.y = i.targetY
      }
    }
    i.draw()
  })
  const length = particleList.filter(i => i.destroyed).length
  if (length === particleList.length) {
    window.cancelAnimationFrame(requestId)
    return
  }
  requestId = window.requestAnimationFrame(draw)
}

到这里,粒子时钟的动画已经实现了。

核心运动算法

上面的三大步时粒子的动画的主要逻辑。相信认真观看的同学已经发现了,代码中最关键的粒子运动的计算在上文中并没有讲到。

上课啦!!!三角函数的计算,你还记得么?

canvas 中相对于坐标轴的角度永远是90°,所以三角函数算法是 Canvas 最基础的算法。

Canvas 粒子时钟

Canvas 粒子时钟

JavaScript 中三角函数的计算

Math 对象的 sincos 中只接收弧度,所以需要先计算角度的弧度。

// sin 根据角度计算点位
CanvasUtils.sin = function(deg) {
return Math.sin(CanvasUtils.radian(deg))
}

// cos 根据角度计算点位
CanvasUtils.cos = function(deg) {
return Math.cos(CanvasUtils.radian(deg))
}

// 跟据角度计算弧度
CanvasUtils.radian = function(deg) {
return Math.PI * deg / 180
}

通过粒子的两个坐标,计算粒子的运动角度

// 根据点位 计算角度
CanvasUtils.pointsToAngle = function(x1, y1, x2, y2) {
const a = Math.abs(y2 - y1)
const b = Math.abs(x2 - x1)
let angle = Math.atan(a / b) * 180 / Math.PI

if (x1 > x2) {
  angle = 180 - angle
  if (y1 > y2) {
    angle = 180 - angle + 180
  }
} else if (y1 > y2) {
  angle = 360 - angle
}
return angle
}

// ----------- particle -----
particle.directionDeg = CanvasUtils.pointsToAngle(particle.x, particle.y, tX, tY)

Ending

canvas 的绘制能力十分强大,更多的效果需要深入学习。欢迎感兴趣的同学一起学习交流

传送门

  • Canvas API中文文档
  • 猫十一の纸盒子 -- 欢迎一起交流学习
  • 文章推荐:基于 vue2.x + element-ui 我的自定义表单,你不试试么