一看就懂的canvas粒子系统

先看效果

说干就干

1.先定义个粒子类 Particle

class Parcicle {
  constructor(props) {

    // 粒子的横坐标初始位置
    this.x = 0; 

    // 粒子的纵坐标初始位置
    this.y = 0;

    // 粒子的横向速度
    this.vx = 0;

    // 粒子的纵向速度
    this.vy = 0;

    // 粒子的运动终点横坐标
    this.finalX = 0;

    // 粒子的运动终点纵坐标
    this.finalY = 0;

    // 粒子的半径
    this.r = 1;

    // 粒子的填充颜色
    this.fillStyle = '#000';

    // 粒子的描边颜色
    this.strokeStyle = '#000';
    Object.assign(this, props);
    }
  render(ctx) {
    const { x, y, r, fillStyle, strokeStyle } = this;
    ctx.save();
    ctx.translate(x, y);
    ctx.fillStyle = fillStyle;
    ctx.strokeStyle = strokeStyle;
    ctx.beginPath();
    ctx.arc(0, 0, r, 0, 2 * Math.PI);
    ctx.stroke();
    ctx.fill();
    ctx.restore();
    return this;
  }
}
复制代码

2.定义粒子系统类

先确定下实例化参数

let fontList = [
  {
    // 要显示的文字
    font: '3',

    // 字体颜色
    color: 'green',

    // 字体显示时间
    lifetime: 2000,

    // 字体占比画布大小比例
    size: 0.8,

    // 粒子密度
    density: 5
  }
];
new ParticleSystem({
  fontDataList: fontList,

  // 循环显示
  loop: true,

  // canvas宽度
  width: 200,

  // canvas 高度
  height: 200,

  // 字体占比画布大小比例
  size: 0.8,

  // 画布容器
  container
});
复制代码

3.设置默认值

constructor(props) {
  // 显示的字体样式
  this.font = '?';

  // 当前显示字体的颜色
  this.color = 'red';

  //  canvas容器
  this.container = null;

  // canvas 的宽度
  this.width = 200;

  // canvas 的高度
  this.height = 200;

  this.size = 0.8;

  //粒子未扩散之前的范围
  this.range = 0.5;

  //粒子密度
  this.density = 4;
  Object.assign(this, props);
  this.init();
  return this;
}
复制代码

4.初始化调用

init() {
  // 设置画布尺寸
  this.setCanvasSize();

  // 执行动画
  this.animate();

  // 添加画布到页面元素
  this.appendCanvas();
}
复制代码

5.动画执行方法

animate() {
  const _this = this;
  // 获取当前时间戳
  let startTime = +new Date();

  (function move() {
    const { currentFontIndex, fontDataList } = _this;

    // 获取执行动画时的时间
    let endTime = +new Date();
    let interTime = endTime - startTime;
    let lifetime = fontDataList[currentFontIndex] && fontDataList[currentFontIndex].lifetime;

    // 判断当前时间差是否达到预设的lifeTime值来判断是否执行下一个文字
    if (interTime >= lifetime) {
      _this.nextFont();
      startTime = +new Date();
    }
    _this.drawParticle();
    requestAnimationFrame(move);
  })();
}
复制代码

6.文字切换

nextFont() {
  const { fontDataList, loop } = this;
  const _this = this;

  // 设置当前字体的配置
  Object.entries(fontDataList[this.currentFontIndex]).forEach(([ key, value ]) => {
    _this[key] = value || _this.defaultValue(key);
  });
  if (typeof _this.font == 'string') _this.setFont();
  this.currentFontIndex += 1;
  if (fontDataList.length <= this.currentFontIndex) {
    if (!loop) return false;
    this.currentFontIndex = 0;
  }
}

setFont() {
  const { height, width, size, font, ctx } = this;
  ctx.clearRect(0, 0, width, height);
  ctx.fillStyle = '#000';

  // 预设10px的字体
  ctx.font = 'blod 10px Arial';

  // 测量10px字体的宽高
  const measure = ctx.measureText(font);

  // 10px的字体除行高7得到字体的高度
  const lineHeight = 7;

  // 通过画布的尺寸计算出最大的字体尺寸
  const fSize = Math.min(height * size * 10 / lineHeight, width * size * 10 / measure.width);
  ctx.save();
  ctx.font = `bold ${fSize}px Arial`;
  const measureResize = ctx.measureText(font);
  const left = (width - measureResize.width) / 2;
  const bottom = (height + fSize / 10 * lineHeight) / 2;
  ctx.fillText(font, left, bottom);

  // 获取回执好的图片信息
  this.getImageData();
  ctx.restore();
}
复制代码

7.获取图片文字信息

getImageData() {
  const { ctx, color, range, width, height, density } = this;

  // 获取图片信息
  const data = ctx.getImageData(0, 0, width, height);
  const particleList = [];
  for (let x = 0, width = data.width, height = data.height; x < width; x += density) {
    for (let y = 0; y < height; y += density) {

      // 获取粒子的初始位置
      const currentX = (width - width * range) / 2 + Math.random() * width * range;
      const currentY = (height - height * range) / 2 + Math.random() * height * range;
      const i = (y * width + x) * 4;

      // 如果当前的像素点有色值则push当前的像素点到particleList
      if (data.data[i + 3]) {
        particleList.push(
          new Particle({
            x: currentX,
            y: currentY,
            finalX: x,
            finalY: y,
            fillStyle: color,
            strokeStyle: color
          })
        );
      }
    }
  }
  this.particleList = particleList;
  this.drawParticle();
}
复制代码

8.绘制粒子执行动画

drawParticle() {
  const { particleList, ctx, width, height } = this;
  if (!particleList) return false;
  ctx.clearRect(0, 0, width, height);

  // 设置加速度系数,参数越大动画执行速度越快
  const spring = 0.01;

  // 设置摩擦系数,让粒子最后能停下来(偷了个懒没做终点位置校正)
  const FRICTION = 0.88;

  // 让所有的粒子都动起来吧
  particleList.forEach((item, index) => {
    item.vx += (item.finalX - item.x) * spring;
    item.vy += (item.finalY - item.y) * spring;
    item.x += item.vx;
    item.y += item.vy;
    item.vx *= FRICTION;
    item.vy *= FRICTION;
    item.render(ctx);
  });
}
复制代码

至此粒子已经完成了动画

8.添加图片支持

别着急,为了让粒子系统支持图片转化粒子还得再加点东西

setImage() {
  const { height, width, font, ctx } = this;
  ctx.clearRect(0, 0, width, height);
  ctx.drawImage(font, 0, 0, width, height);
  this.getImageData();
}
复制代码

第六步的nextFont也需要再添加个判断

if (typeof _this.font == 'object') _this.setImage();
复制代码

加载图片是异步行为,但是配合 es7 的async,await 又能省了不少麻烦。不说了直接上代码。

function loadImg(src) {
  return new Promise((res, rej) => {
    let oImg = new Image();
    oImg.src = src;
    oImg.onload = () => res(oImg);
    oImg.onerror = (err) => rej(err);
  });
}

(async () => {
  const oImg4 = await loadImg('./1.png');
  let fontList = [
    {
      font: '3',
      color: 'green',
      lifetime: 2000,
      size: 0.8,
      density: 5
    },
    {
      font: oImg4,
      lifetime: 2000,
      density: 5,
      color: "red"
    }
  ];
  const container = document.getElementById('canvasContainer');
  new ParticleSystem({
    fontDataList: fontList,
    loop: true,
    width: 200,
    height: 200,
    size: 0.8,
    container
  });
})();
复制代码

源码地址: github.com/tomatoKnigh…

参考

炫酷粒子表白,双十一脱单靠它了

转载于:https://juejin.im/post/5c866d706fb9a049d132f700

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值