去年的五一,计划是带着我宝回老家见父母的,虽然我和我宝在一起好几个月了,但是还没有给我宝一场浪漫的告白,之前就有计划给我宝放一场专属的烟花,在烟花下去告白。五一是一次难得的机会,因为老家是农村,没有限制放烟花,门口也有很大的空地,很适合放烟花。然而不久便有了个不好的消息,五一前我宝可能要去海南出差,因为太远了,不一定回来,原本的计划有了不确定性。然后我便想着,如果我宝五一去了海南,那么我就给我宝准备一场线上烟花秀。有了想法,便立即行动。
一开始的时候,设想的是有几种烟花,比如只有一种颜色的烟花,有多种颜色的烟花,有图片烟花,有文字烟花等等。所以需要有一个基类Firework,这个基类中定义了烟花的位置,速度,透明度,持续时间,角度。然后就是update和render两个函数,update函数用来更新这些属性的值,当持续时间为0的时候,从烟花数组中移除掉自己,render函数用来渲染烟花。
js
import type Vector from "../statute/Vector";
export default class Firework {
position: Vector;
speed: Vector;
color: string = 'fffff';
alpha: number = 255;
duration: number = 1000;
durationLeft: number = 1000;
radius: number = 2;
constructor(position: Vector, speed: Vector, color: string, duration: number) {
this.position = position;
this.speed = speed;
this.color = color;
this.duration = duration;
this.durationLeft = duration;
}
getColor(): string {
return `#${this.color}${this.alpha.toString(16).padStart(2, '0')}`
}
update(fires: Array<Firework>): void {
this.durationLeft -= 1;
if (this.durationLeft <= 0) {
fires.splice(fires.indexOf(this), 1);
return;
}
this.position.x += (this.speed.x * Math.sin(this.durationLeft / this.duration * Math.PI / 2));
this.position.y += (this.speed.y * Math.sin(this.durationLeft / this.duration * Math.PI / 2));
this.alpha = Math.floor(255 * Math.sin(this.durationLeft / this.duration * Math.PI / 2));
}
render(context: CanvasRenderingContext2D | null | undefined): void {
if (!context) {
return;
}
const x = this.position.x;
const y = this.position.y;
context.beginPath();
context.fillStyle = this.getColor();
context.arc(x, y, this.radius, 0, Math.PI * 2, false);
context.fill();
context.closePath();
}
}
然后是具体到每一种类型的烟花。
第一种是单色烟花,说是单色,其实也支持多色。首先是发射的烟花,就是从地面升起到爆炸这一阶段的烟花,这个类只是重写了update函数,让它快速往上跑,当要爆炸的时候,就是往烟花数组里插入很多个烟花(爆炸散射出来的烟花),然后销毁自己。这里其实也挺麻烦,爆炸开的烟花要多均匀一些,但是不能太过均匀,否则看起来就太假了,每个角度的烟花需要在一定的范围内随机,尝试了很多次,才得到一个自己比较满意的效果。除此之外呢,爆炸开的烟花速度也不一样,最外面的烟花速度要快一点,越靠近中心,速度越慢,这样爆炸开的烟花就会呈现一个圆形,比较好看。
js
static createSingleColorFirework(px: number, py: number, color?: string): Array<Firework> {
const fires: Array<Firework> = [];
const r = 5;
let fireworkColor = color ?? FireworksUtil.colors[FireworksUtil.randomInt(0, FireworksUtil.colors.length)];
const speeds = [5, 4.2, 3.4, 2.6, 1.8, 1];
speeds.forEach(speed => {
const count = speed * 4;
for (let i = 1; i <= count; i++) {
const s = speed * this.random(0.9, 1);
const angle = i * Math.PI * 2 / count + this.random(-1 / (Math.PI * 2), 1 / (Math.PI * 2));
const sx = s * Math.cos(angle);
const sy = s * Math.sin(angle);
const firework = new LoveFirework({ x: px, y: py }, { x: sx, y: sy }, fireworkColor, FireworksUtil.randomInt(30, 50));
firework.type = 'single';
fires.push(firework);
}
});
return fires;
}
多色烟花其实和单色烟花差不多,只是把颜色随机一下,也随机一点,均匀一点就行。
第二种烟花是图片烟花,图片烟花关键就是先把图片画到画布上,然后用getImageData把图片的像素信息提取出来生成烟花即可,因为图片的像素比较高,如果都取出来生成烟花会很影响性能,所以这里的图片大小是调整过的,比较小,烟花在屏幕上也没有那么大,所以用大图片没必要。然后就是取像素的时候隔几个像素取就行了。
js
static createImageFirework(path: string, px: number, py: number): Promise<Array<Firework>> {
return new Promise((resolve, reject) => {
const image = new Image();
image.src = path;
image.onload = function () {
const fires: Array<Firework> = [];
const canvas = document.createElement('canvas');
canvas.width = image.width;
canvas.height = image.height;
const ctx = canvas.getContext('2d');
ctx?.drawImage(image, 0, 0);
const imageData = ctx?.getImageData(0, 0, canvas.width, canvas.height);
const radius = Math.floor((canvas.width >= canvas.height ? canvas.height : canvas.width) / 2);
for (let r = 1; r <= radius; r = r + 4) {
const speed = r * 0.1;
const count = r * 3;
for (let a = 0; a < Math.PI * 2; a = a + Math.PI * 2 / count) {
const s = speed * FireworksUtil.random(0.9, 1);
const x = Math.floor(radius + r * Math.cos(a));
const y = Math.floor(radius - r * Math.sin(a));
const sx = s * Math.cos(a);
const sy = -s * Math.sin(a);
const index = (y * image.width + x) * 4;
const red = imageData?.data[index] ?? 0;
const green = imageData?.data[index + 1] ?? 0;
const blue = imageData?.data[index + 2] ?? 0;
const color = FireworksUtil.convertColor(red, green, blue, 255);
const firework = new ImageFirework({ x: px, y: py }, { x: sx, y: sy }, color, FireworksUtil.randomInt(30, 50));
firework.white = false;
fires.push(firework);
}
}
resolve(fires);
}
});
}
第三种是文字烟花,文字烟花和图片烟花其实是差不多的,只是一开始需要将文字绘制到画布上,然后从画布上获取像素信息,和图片烟花一样的处理逻辑。
效果如下
因为时间的关系,最后就做出来这几种烟花。然后放到了我宝和我的网站上,设置好触发条件,等到5月20号的时候,我宝登录我们的网站,就会进入烟花的页面,给她来一场烟花秀。
然而事情出现了变化。出差延迟了,我宝可以和我一起回老家了,线上烟花用不上了。然而事情又出现了变化。我宝觉得烟花虽美,然而转瞬即逝。拗不过我宝,只能打消了,所以我的线上烟花又能派上用场了