新春快乐
我正在参加「兔了个兔」创意投稿大赛,详情请看:「兔了个兔」创意投稿大赛
废话不多说,咱直接上代码。首先准备一下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 };
}
}
...
那我来说一下为什么要声明这些属性:
- 位置信息:
X、Y,无论什么形式,你都要确定你烟花的起始爆炸点。 - 半径:
radius,烟花爆炸的过程中,通过计算半径来起到动画的效果。 - 透明度:
alpha,通过合理的计算,让烟花慢慢的消失。 - 颜色:
color,这个就很好理解了,礼花的颜色嘛。 - 层级:
level,这个可就有讲究了,因为加了这个属性,烟花直接变成礼花了。因为烟花只回爆一下,而好看的礼花可以爆2下以上(不要设置太多,否则你会闻到显卡的芳香~)。 - 颗粒度:
size,烟花的颗粒,在多层爆炸过程中可以自己改变想要的效果。
OK,那我们继续来看定义的两个方法:
- run():简单的运动计算,如果要实现复杂的运动可以扩展。
- 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();
};
来讲解下:
- globalCompositeOperation:其实就是你要渲染的模式,里面有很多种,采用
destination-out+lighter可以实现模糊的拖尾感(我百度来的,详细也不是很清楚,请不要打我)。 - 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,到这里就是完整的代码啦,接下来让我们来揭开它神秘的面纱吧!