2023年了,写个礼花祝大家新年快乐!!!

277 阅读2分钟

新春快乐

我正在参加「兔了个兔」创意投稿大赛,详情请看:「兔了个兔」创意投稿大赛

废话不多说,咱直接上代码。首先准备一下canvas环境:

<template>
  <div>
    <canvas id="canvas"></canvas>
  </div>
</template>
<script setup lang="ts">
import { onMounted } from "vue";
let ctx: CanvasRenderingContext2D;
let canvas: HTMLCanvasElement;
onMounted(() => {
  init();
});
let init = () => {
  canvas = document.querySelector("#canvas") as HTMLCanvasElement;
  ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
  resizeCanvas();
};
let resizeCanvas = () => {
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
  clearCanvas();
};
let clearCanvas = () => {
  ctx.fillStyle = "#000000";
  ctx.fillRect(0, 0, canvas.width, canvas.height);
};
window.addEventListener("resize", resizeCanvas);
</script>

OK,接下来让我们做点什么,比如准备一个礼花类?说干就干:

...
interface color {
  R: number;
  G: number;
  B: number;
}
interface Firework {
  x: number;
  Y: number;
  radius: number;
  alpha?: number;
  color?: color;
  level?: number;
  size?: number;
}
class Fireworks {
  public x; //烟花的爆炸点X
  public Y; //烟花的爆炸点Y
  public radius; // 烟花爆炸的半径
  public alpha; // 烟花透明度(默认为1)
  public color; //烟花颜色(默认随机)
  public level; //爆炸几层(默认为2)
  public size; //烟花颗粒大小
  constructor({ x, Y, radius, alpha, color, level, size }: Firework) {
    this.x = x;
    this.Y = Y;
    this.radius = radius;
    this.alpha = alpha || 1;
    this.color = color || this.randomColor();
    this.level = level || 2;
    this.size = size || 2;
  }
  public run() {
    this.radius += 2; //烟花半径运动
    this.Y += this.radius / 50; //简单的引力效果
    this.alpha -= 0.01; //简单的渐变效果
  }
  private randomColor() {
    let R = Math.floor(Math.random() * 256);
    let G = Math.floor(Math.random() * 256);
    let B = Math.floor(Math.random() * 256);
    return { R, G, B };
  }
}
...

那我来说一下为什么要声明这些属性:

  1. 位置信息:XY,无论什么形式,你都要确定你烟花的起始爆炸点。
  2. 半径:radius,烟花爆炸的过程中,通过计算半径来起到动画的效果。
  3. 透明度: alpha,通过合理的计算,让烟花慢慢的消失。
  4. 颜色:color,这个就很好理解了,礼花的颜色嘛。
  5. 层级:level,这个可就有讲究了,因为加了这个属性,烟花直接变成礼花了。因为烟花只回爆一下,而好看的礼花可以爆2下以上(不要设置太多,否则你会闻到显卡的芳香~)。
  6. 颗粒度:size,烟花的颗粒,在多层爆炸过程中可以自己改变想要的效果。

OK,那我们继续来看定义的两个方法:

  1. run():简单的运动计算,如果要实现复杂的运动可以扩展。
  2. randomColor():为了给礼花多一些惊喜,所以定义并默认了随机颜色。

OK,接下来让我们来尝试渲染一下:

let fireArr: Fireworks[] = []; //烟花的数组
let isRun: boolean = false;
//选择点击的方式获取位置坐标
let clickHandler = (e: MouseEvent) => {
  let x = e.clientX;
  let Y = e.clientY;
  fireArr.push(
    new Fireworks({
      x,
      Y,
      radius: 10,
      alpha: 1,
      color: { R: 240, G: 239, B: 158 },
    })
  );  
  if (!isRun) {
   //节约下性能,只在点击时开始渲染
    RunFire(fireArr);//动画函数
    isRun = true;
  }
};

OK,接下来让我们看一下RunFire函数:

let RunFire = (arr: Fireworks[]) => {
  let tick = () => {    
    ctx.globalCompositeOperation = "destination-out";
    ctx.fillStyle = `rgba(0,0,0, ${0.2})`;
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.globalCompositeOperation = "lighter";
    clearCanvas();
    arr.forEach((i, index) => {
      i.run();
      if (i.alpha <= 0) {
        arr.splice(index, 1);
      }
      drawFireworks(i);//渲染函数
    });
    requestAnimationFrame(tick);
  };
  tick();
};

来讲解下:

  1. globalCompositeOperation:其实就是你要渲染的模式,里面有很多种,采用destination-out+lighter可以实现模糊的拖尾感(我百度来的,详细也不是很清楚,请不要打我)。
  2. requestAnimationFrame:是一种动画渲染的方式,它可以切合你屏幕的刷新率来做同等频率的渲染,来达到完美的性能应用和优雅的用户体验。(且不占用js线程,就很顺畅)

OK,那接下来就是最后一步了:

let drawFireworks = ({
  x,
  Y,
  radius,
  alpha,
  color,
  level,
  size,
}: Fireworks) => {
  let count = 20;
  for (let i = 0; i < count; i++) {  
    //计算你每颗烟花粒子的位置
    let angle = (360 / count) * i; //烟花粒子角度
    let radians = (angle * Math.PI) / 180; //烟花粒子弧度
    let vx = x + Math.cos(radians) * radius;
    let vy = Y + Math.sin(radians) * radius;
    if (alpha <= 0 && level > 1) {
    //一个小小的条件,来提现多层爆炸的效果
      fireArr.push(
        new Fireworks({
          x: vx,
          Y: vy,
          radius: 1,
          alpha: 0.8,
          level: level - 1,
          size: size / 2,
        })
      );
    }
    ctx.beginPath();
    ctx.arc(vx, vy, size, 0, Math.PI * 2, false);
    ctx.closePath();
    ctx.fillStyle = `rgba(${color.R},${color.G},${color.B}, ${alpha})`;
    ctx.fill();
  }
};

OK,到这里就是完整的代码啦,接下来让我们来揭开它神秘的面纱吧!