使用canvas实现可个性化配置的随机粒子动画!

1,811 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情

使用canvas能够让我们实现一些好看的特效和动画,本篇文章就带你用canvas实现一个可以根据你的喜好进行个性化配置的随机粒子动画,就像下面这些这样

可以是小小的粒子 粒子动画.gif 粒子动画.gif 还可以加快粒子的移动速度 粒子动画.gif 也可以改变粒子的大小和颜色,从粒子变成“泡泡” 粒子动画.gif 粒子动画.gif

粒子动画.gif

怎么样是不是很酷炫!快跟着看完并动手实现吧! 项目源码地址:github.com/Plasticine-…

1. 项目搭建

使用vite的原生脚手架搭建项目

pnpm create vite

选择vanilla也就是原生js进行开发


2. 设计主函数

为了方便使用,我的设计方案是实现一个useParticles的函数,用户直接调用它,传入canvas元素即可使用到粒子动画背景了,并且还支持进行个性化配置,比如配置粒子的数量,颜色等 创建src/hooks/useParticles.ts,并且只默认导出一个函数,该函数就是实现粒子动画背景的主函数,其接收用户传入的canvas元素,以及可选的配置对象

/**
 * @description 粒子动画背景
 * @param canvasEl canvas 元素 如果没有则会自动创建一个
 * @param options 个性化配置项
 */
export default (
  canvasEl: HTMLCanvasElement,
  options?: ParticleOptions
): void => {
  const particleCount = options?.particleCount ?? 100;
};

options用一个接口去定义,目前我们先实现一个配置粒子数量的配置项

/**
 * @description 粒子动画背景配置项
 */
interface ParticleOptions {
  /**
   * @description 粒子数量
   */
  particleCount?: number;
}

用户如果没有配置particleCount配置项的话,则默认为100个粒子


3. 初始化粒子画布

实现一个setupCanvasEl函数,用于初始化用户传入的画布,主要要做以下两件事:

  • 设置画布大小为documentElement的大小,即铺满整个浏览器当前文档视窗大小
  • 获取上下文对象,并存储到全局变量ctx中,以便后续给其他函数直接使用,避免多次传参
/**
 * @description 初始化 canvas 画布
 * @param canvasEl canvas 元素
 */
const setupCanvasEl = (canvasEl: HTMLCanvasElement) => {
  // 获取上下文对象
  ctx = canvasEl.getContext('2d')!;
  canvasEl.height = HEIGHT;
  canvasEl.width = WIDTH;
};

这里HEIGHTWIDTH是两个全局变量,正是documentElement的高度和宽度

const HEIGHT = document.documentElement.clientHeight;
const WIDTH = document.documentElement.clientWidth;

4. 粒子类

4.1 粒子属性

由于我们需要创建多个粒子对象,因此统一封装成一个粒子类,然后再多次实例化它们会比较合适 首先思考一下粒子会有哪些属性:首先,xy坐标是肯定要有的,其次由于粒子是用圆形绘制的,因此肯定需要有半径,以及粒子的颜色,综上我们可以得出粒子需要具有如下属性:

  • xy坐标
  • 半径
  • 颜色

对应的ts代码如下:

class Particle {
  id: number;
  x: number;
  y: number;
  radius: number;
  color: string;

  /**
   * @param id 粒子的 id,用于标识粒子
   * @param x 粒子的 x 坐标
   * @param y 粒子的 y 坐标
   * @param radius 粒子的半径大小
   */
  constructor(id: number, x: number, y: number, radius: number, color: string) {
    this.id = id;
    this.x = x;
    this.y = y;
    this.radius = radius;
    this.color = color;
  }
}

4.2 粒子方法

首先能想到的肯定是绘制粒子,调用每个粒子的绘制方法,就可以将粒子绘制在canvas画布上; 其次粒子需要往上飘,因此还需要一个粒子移动的方法,所以我们需要实现以下两个方法:

  • draw -- 将粒子画出来
  • move -- 让粒子移动

4.2.1 draw方法

主要用到canvas APIarc绘制一个圆形

/**
 * @description 绘制粒子
 * @param ctx canvas 2d 上下文对象
 */
draw() {
  ctx.fillStyle = this.color; // 设置画笔颜色
  ctx.shadowBlur = this.radius * 2; // 设置模糊半径
  ctx.beginPath();
  ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI, true);
  ctx.closePath();
  ctx.fill();
}

4.2.2 move方法

我的设计是让粒子匀速向上移动,之后渲染动画的时候会使用requestAnimationFrame这个APImove方法中只需要改变粒子的位置,然后调用draw方法绘制出移动后的粒子即可

/**
 * @description 粒子移动 只在垂直方向上移动
 * @param boundaryHeight 垂直方向的边界 用于超出边界时移动到边界另一边
 */
move() {
  this.y -= 0.15;
  if (this.y <= -10) {
    this.y = HEIGHT + 10;
  }

  this.draw();
}

5. 批量生成随机粒子

粒子类已经实现好了,接下来要做的就是批量去实例化粒子对象,然后调用它们的draw方法,即可将它们绘制到canvas画布上

/**
 * @description 生成粒子
 * @param ctx canvas 2d 上下文对象
 * @param particleCount 要生成的粒子数量
 */
const generateParticles = (particleCount: number) => {
  for (let i = 0; i < particleCount; i++) {
    // 粒子 x, y 坐标
    const x = Math.random() * WIDTH;
    const y = Math.random() * HEIGHT;

    // 粒子半径的大小在 1 ~ 3 个单位之间随机生成
    const radius = Math.random() * 2 + 1;
    const color = '#fff';

    const particle = new Particle(i, x, y, radius, color);

    particle.draw();
  }
};

在主函数中调用该函数即可看到效果

/**
 * @description 粒子动画背景
 * @param canvasEl canvas 元素 如果没有则会自动创建一个
 * @param options 个性化配置项
 */
export default (
  canvasEl: HTMLCanvasElement,
  options?: ParticleOptions
): void => {
  const particleCount = options?.particleCount ?? 100;

  // 初始化 canvas 信息
  setupCanvasEl(canvasEl);

  // 生成粒子
  generateParticles(particleCount);
};

还要在main.ts中调用useParticles函数

import useParticles from './hooks/useParticles';
import './style.css';

const oParticleCanvas =
  document.querySelector<HTMLCanvasElement>('#particles')!;

useParticles(oParticleCanvas, { particleCount: 666 });

效果图如下 image.png 每次刷新,生成的粒子位置都是不一样的


6. 让粒子动起来!

前面说了,我们主要会用requestAnimationFrame去实现动画的效果,动画的原理就是不断的画出下一个画面,当这个绘制的速度足够快时,看起来就像动画一样,最理想的情况是当刷新的频率达到屏幕的刷新率的时候,如果通过setInterval去实现的话,并不能保证真的会在延迟指定时间后执行我们的渲染逻辑,而使用requestAnimationFrame则不需要担心这一点,如果感兴趣的话可以自行了解具体原因,这里不过多介绍 由于是通过刷新的方式实现的动画,因此我们需要在每次刷新的时候先清空上一次画布中的内容,再画出这一次需要显示的内容

/**
 * @description 开启粒子动画
 */
const showParticleAnimation = () => {
  // 清屏 用于刷新下一帧
  ctx.clearRect(0, 0, WIDTH, HEIGHT);
};

然后我们还需要遍历每一个粒子,调用它们的move方法,生成下一帧的画面内容,因此我们需要在生成随机粒子的时候将它们存到一个数组中,然后在渲染动画的时候去遍历这个数组逐一调用 首先定义一个全局变量particles数组,然后修改生成随机粒子的代码,将生成的粒子放入该数组中

+ const particles: Particle[] = [];

/**
 * @description 生成粒子
 * @param ctx canvas 2d 上下文对象
 * @param particleCount 要生成的粒子数量
 */
const generateParticles = (particleCount: number) => {
  for (let i = 0; i < particleCount; i++) {
    // 粒子 x, y 坐标
    const x = Math.random() * WIDTH;
    const y = Math.random() * HEIGHT;

    // 粒子半径的大小在 1 ~ 3 个单位之间随机生成
    const radius = Math.random() * 2 + 1;
    const color = '#fff';

+   particles.push(particle);
    const particle = new Particle(i, x, y, radius, color);

    particle.draw();
  }
};

然后遍历该数组,调用粒子对象的move方法

/**
 * @description 开启粒子动画
 */
const showParticleAnimation = () => {
  // 清屏 用于刷新下一帧
  ctx.clearRect(0, 0, WIDTH, HEIGHT);

+ for (const particle of particles) {
+   particle.move();
+ }

+ requestAnimationFrame(showParticleAnimation);
};

最后别忘了调用requestAnimationFrame去绘制动画,将showParticleAnimation函数自身作为回调传入,当到了浏览器渲染动画的时机,就会执行该回调去渲染移动之后的粒子的画面 最后我们在主函数中调用showParticleAnimation函数就可以啦!

/**
 * @description 粒子动画背景
 * @param canvasEl canvas 元素 如果没有则会自动创建一个
 * @param options 个性化配置项
 */
export default (
  canvasEl: HTMLCanvasElement,
  options?: ParticleOptions
): void => {
  const particleCount = options?.particleCount ?? 100;

  // 初始化 canvas 信息
  setupCanvasEl(canvasEl);

  // 生成粒子
  generateParticles(particleCount);

+ // 开启粒子动画
+ showParticleAnimation();
};

粒子动画.gif


7. 给粒子加点色彩

我们可以通过主函数的options去配置粒子的颜色,当用户传入了想要渲染的颜色的时候,就会渲染相应的颜色,而如果没有传入时,则渲染默认的白色粒子,由于粒子的颜色可以有多种,所以我们应当用数组去存放

/**
 * @description 粒子动画背景配置项
 */
interface ParticleOptions {
  /**
   * @description 粒子数量
   */
  particleCount?: number;
+ /**
+  * @description 粒子颜色
+  */
+ particleColors?: string[];
}

然后修改生成粒子的代码

/**
 * @description 生成粒子
 * @param ctx canvas 2d 上下文对象
 * @param particleCount 要生成的粒子数量
 */
- const generateParticles = (particleCount: number) => {
+ const generateParticles = (particleCount: number, paricleColors: string[]) => {
  for (let i = 0; i < particleCount; i++) {
    // 粒子 x, y 坐标
    const x = Math.random() * WIDTH;
    const y = Math.random() * HEIGHT;

    // 粒子半径的大小在 1 ~ 3 个单位之间随机生成
    const radius = Math.random() * 2 + 1;
+   const color =
+     paricleColors[Math.floor(Math.random() * paricleColors.length)];

    const particle = new Particle(i, x, y, radius, color);

    particles.push(particle);
    particle.draw();
  }
};

调用的时候传入配置项即可

useParticles(oParticleCanvas, {
  particleCount: 666,
  prticleColors: ['#90AEFF', '#CEFC86', '#60EFB8'],
});

粒子动画.gif


8. 控制粒子大小

再增加一个配置项,用于控制粒子的最大尺寸

/**
 * @description 粒子动画背景配置项
 */
interface ParticleOptions {
  /**
   * @description 粒子数量
   */
  particleCount?: number;
  /**
   * @description 粒子颜色
   */
  particleColors?: string[];
+  /**
+   * @description 粒子最大尺寸
+   */
+  particleMaxSize?: number;
}
/**
 * @description 生成粒子
 * @param ctx canvas 2d 上下文对象
 * @param particleCount 要生成的粒子数量
 */
const generateParticles = (
  particleCount: number,
  paricleColors: string[],
  particleMaxSize: number
) => {
  for (let i = 0; i < particleCount; i++) {
    // 粒子 x, y 坐标
    const x = Math.random() * WIDTH;
    const y = Math.random() * HEIGHT;

-   // 粒子半径的大小在 1 ~ 3 个单位之间随机生成
-   const radius = Math.random() * 2 + 1;
+   // 粒子半径的大小在 1 ~ particleMaxSize 个单位之间随机生成
+   const radius = Math.random() * (particleMaxSize - 1) + 1;
    const color =
      paricleColors[Math.floor(Math.random() * paricleColors.length)];

    const particle = new Particle(i, x, y, radius, color);

    particles.push(particle);
    particle.draw();
  }
};

将最大尺寸设置大一些再看看效果

useParticles(oParticleCanvas, {
  particleCount: 100,
  particleColors: ['#EFFFFD', '#B8FFF9', '#85F4FF', '#42C2FF'],
  particleMaxSize: 30,
});

粒子动画.gif 哈哈,粒子变成了“泡泡”了~


9. 让粒子跑快点!

现在的粒子移动太慢了,我们可以扩展一下,让用户自行决定粒子的移动速度

/**
 * @description 粒子动画背景配置项
 */
interface ParticleOptions {
  /**
   * @description 粒子数量
   */
  particleCount?: number;
  /**
   * @description 粒子颜色
   */
  particleColors?: string[];
  /**
   * @description 粒子最大尺寸
   */
  particleMaxSize?: number;
+  /**
+   * @description 粒子移动速度
+   */
+  particleMoveRate?: number;
}

由于移动速度是动画相关的配置了,因此我们这次不是修改生成粒子的函数了,应当修改粒子动画的函数,所以particleMoveRate应当传给showParticleAnimation函数

/**
 * @description 开启粒子动画
 */
const showParticleAnimation = (particleMoveRate: number) => {
  // 清屏 用于刷新下一帧
  ctx.clearRect(0, 0, WIDTH, HEIGHT);

  for (const particle of particles) {
    particle.move(particleMoveRate);
  }

  requestAnimationFrame(showParticleAnimation.bind(null, particleMoveRate));
};

然后再传递给move函数

  /**
   * @description 粒子移动 只在垂直方向上移动
   * @param boundaryHeight 垂直方向的边界 用于超出边界时移动到边界另一边
   */
  move(particleMoveRate: number) {
    this.y -= particleMoveRate;
    if (this.y <= -this.radius) {
      this.y = HEIGHT + this.radius;
    }

    this.draw();
  }

由于showParticleAnimation函数现在有参数了,直接作为回调传递给requestAnimationFrame的话会导致particleMoveRate参数丢失,所以我们可以使用bind将参数绑定上去再传入 而move函数中将原本的越界判断逻辑修改了一下,原先写死了判断的边界为10,导致粒子越界出现在另一头的时候是突然出现的,看起来会很生硬,现在改为了粒子的半径作为越界条件,效果就自然了 调用的时候配置一下运动速度,稍微调快一些

useParticles(oParticleCanvas, {
  particleCount: 100,
  particleColors: ['#EFFFFD', '#B8FFF9', '#85F4FF', '#42C2FF'],
  particleMaxSize: 30,
  particleMoveRate: 6,
});

粒子动画.gif