持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第 8 天,点击查看活动详情
前言
大家好,我是爱吃鱼的桶哥Z,最近比较忙,一直都没空更新文章。刚好周末了,可以好好的放松一下,顺便在小破站上学习学习。今天又学到了一个动感激光效果,还是很酷炫的,顺便就分享给大家一起学习一下。首先我们还是先看一下最终实现的效果,如图:
可以看到这个效果就是一个不断旋转的激光图,并且中间还生成了很多粒子效果,下面就让我们一起来看一下这个效果是如何实现的吧!
激光线条的生成
我们通过上图可以看出这个动感激光主要是由两部分组成,其中包括了N多线条的生成和N多粒子的生成,通过这两种元素组成了我们最终的这个效果。那么我们就先从生成线段着手。
首先还是老规矩,先编写相应的 html
和 css
,这两块的内容在前面的文章中已经写了很多次了,这里就不写了,不知道具体 html
和 css
的童鞋,可以看前面的文章。我们主要还是看一下 js
相关的内容。
这次我们就采用 ES6 + TS
的来编写这个动感激光的效果,为什么要用 TS
呢?因为在前面的文章中,我们编写 canvas
相关代码时,一直都没有提示信息,有时候会因为写错了单词而查找半天,而使用了 TS
后,编辑器能够知道当前的 ctx
对象是一个 CanvasRenderingContext2D
,这时候再调用相关的 API
时,就会有很多提示信息,编写代码也就不会那么容易出错了。
我们还是采用 ES6
中的 class
的语法来编写代码,首先定义一个 Laser
类,并完成 constructor
相关的初始代码,如下:
class Laser {
viewport: HTMLCanvasElement;
ctx: CanvasRenderingContext2D;
lines: Line[];
particles: Particles[];
deg: number;
w: number;
h: number;
constructor(viewport: HTMLCanvasElement) {
this.viewport = viewport;
this.ctx = this.viewport.getContext('2d');
this.w = innerWidth;
this.h = innerHeight;
this.viewport.width = this.w;
this.viewport.height = this.h;
this.lines = []; // 存放创建的激光线条
this.particles = []; // 存放创建的粒子
this.deg = 0; // 初始角度
}
}
因为我们这次采用的是 TS
的写法,因此需要给每个属性添加相关的定义,这样编辑器才知道每个变量是什么类型。在 Laser
类中,还定义了 lines
属性和 particles
属性,这两个属性主要是用于存放生成的线段和粒子,而它们也有自己的类型,因此我们需要先定义出 Line
类和 Particles
类。
有了 Laser
后,接下来我们就需要先完成 Line
类,因为我们是通过 Line
这个类来实例化出N多条线段,因此我们就先来实现一下这个类,相关代码如下:
class Line {
d: number;
r: number;
l: number;
color: string;
speed: number;
ctx: CanvasRenderingContext2D;
constructor(d: number, ctx: CanvasRenderingContext2D) {
this.d = d; // 初始旋转角度
this.r = 0; // 存放线段顶点到中心的距离
this.l = Math.random() * 50 + 50 + this.r; // 线段的长度
this.color = `hsl(${Math.random() * 360}, 80%, 70%)`;
this.speed = Math.random() * 2 + 2; // 随机 2 - 4 速度
this.ctx = ctx;
}
}
在 Line
的 constructor
中,我们定义了 ctx
的类型为 CanvasRenderingContext2D
,这使得我们在后续调用 this.ctx
时,编辑器能够知道 this.ctx
的具体类型是什么,并且当我们在编辑器中输入 this.ctx
时,会有非常丰富的 API
提示,这样我们的编写准确率和编写速度都会有非常大的提升。
在前面的文章中也编写了很多次,我们一般定义一个实体类时,会给它添加 draw
方法和 update
方法,这两个方法主要是用于绘制和更新图形,编写 Line
也不例外,下面我们一起来看一下 Line
中剩余的代码,如下:
class Line {
...other code
update() {
// 同步移动线段的起点和终点
this.r += this.speed;
this.l += this.speed;
}
draw() {
// 根据角度和半径值绘制线段
const w = innerWidth;
const h = innerHeight;
let x1 = this.r * Math.cos(this.d * Math.PI / 180) + w / 2;
let y1 = this.r * Math.sin(this.d * Math.PI / 180) + h / 2;
let x2 = this.l * Math.cos(this.d * Math.PI / 180) + w / 2;
let y2 = this.l * Math.sin(this.d * Math.PI / 180) + h / 2;
this.ctx.beginPath();
this.ctx.moveTo(x1, y1);
this.ctx.lineTo(x2, y2);
this.ctx.strokeStyle = this.color;
this.ctx.lineWidth = 2;
this.ctx.stroke();
}
}
在 Line
的 update
方法中,通过不断的更新 this.r
和 this.l
来改变当前线段的起点和终点,这样就能让线段在 canvas
中不断的移动;而在 draw
中,则通过三角函数相关的内容计算出线段的开始和结束的点,并通过 this.ctx.moveTo
和 this.ctx.lineTo
来绘制出这条线段。
现在已经有了线段的生成方法了,接下来我们就需要在 Laser
类中通过 Line
类来生成N多的线段,并让它们旋转起来,该如何实现呢?让我们一起来看一下相关的代码,如下:
class Laser {
...other code
draw() {
this.ctx.clearRect(0, 0, this.w, this.h);
this.deg += 2;
this.lines.push(new Line(this.deg, this.ctx));
this.lines.push(new Line(this.deg + 180, this.ctx));
// 生成线段
for (const i in this.lines) {
const l = this.lines[i];
l.update();
l.draw();
// 如果线段的距离超过了页面对角线的一半,则删除这条线段
if (l.r > Math.sqrt(Math.pow(this.w / 2, 2) + Math.pow(this.h / 2, 2))) {
this.lines.splice(Number(i), 1);
}
}
}
}
在 Laser
中,我们定义了一个 draw
方法,主要用于绘制线段和粒子。可以看到在最开始向 this.lines
数组中生成了两条不同的线段,它们主要的区别是初始的旋转角度不同,这样就能够实现沿中心点生成两条对等的线段;而在后面代码中,我们通过执行 Line
中的 update
和 draw
方法来实现线段的绘制和更新,最后需要判断当生成的线段已经超过了页面对角线的一半时,需要删除当前线段,这样主要是为了减少浏览器内存的消耗。
接下来我们还需要让这些生成的线段动起来,我们已经实现了很多次 canvas
中的动画效果了,这里就直接来看一下代码吧,如下:
class Laser {
...other code
animate() {
requestAnimationFrame(() => this.animate());
this.draw();
}
}
我们定义了一个 animate
方法,并通过 requestAnimationFrame
方法不断的调用 animate
,从而让整个 canvas
不断的重复渲染,让我们看到最终的动画效果。最后不要忘了实例化 Laser
类,它有一个参数,最终实例化的代码如下:
new Laser(document.getElementById('canvas') as HTMLCanvasElement);
我们一起来看一下线段的生成效果吧,如下:
可以看到上述的代码已经帮我们在 canvas
中生成了N多条线段,并且通过不断的改变 this.deg
的值,从而让整个线段都旋转起来。
到这里线段的效果已经实现了,还剩下粒子的生成了,接下来一起看一下剩余的部分该如何实现吧!
动感粒子的生成
在 canvas
中生成粒子效果,我们在前面的文章中也实现过很多次了,这里依旧跟前面的套路一样,我们先来看一下 Particles
类中 constructor
相关的代码,如下:
class Particles {
x: number;
y: number;
ctx: CanvasRenderingContext2D;
vx: number;
vy: number;
age: number;
color: string;
size: number;
constructor(x: number, y: number, ctx: CanvasRenderingContext2D) {
this.x = x;
this.y = y;
this.ctx = ctx;
this.vx = Math.random() - 0.5;
this.vy = Math.random() - 0.5;
this.age = Math.random() * innerWidth / 2; // 显示的区域为页面宽度的一半
this.color = `hsl(${Math.random() * 360}, 80%, 70%)`;
this.size = Math.random() * 5;
}
}
在 Particles
的 constructor
中,我们定义了生成粒子的相关属性,通过这些属性来绘制一个基础的粒子,下面我们来看一下相关的 update
和 draw
方法,代码如下:
class Particles {
...other code
update() {
this.age--;
this.x += this.vx;
this.y += this.vy;
}
draw() {
this.ctx.beginPath();
this.ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2, false);
this.ctx.fillStyle = this.color;
this.ctx.fill();
}
}
在之前也介绍过,如果我们希望一个物体能够移动或者发现变换,则需要让它的属性值发生变换,因此我们只需要在 Particles
类的 update
方法中不断的更新 this.age
以及 this.x
和 this.y
的值,这样绘制出来的粒子就会不断的进行移动;而在 draw
方法中,则通过调用 this.ctx.arc
方法来绘制出一个圆,也就是粒子。
最后我们依旧需要在 Laser 类中通过实例化 Particles 类从而生成N多个粒子,让我们一起来看一下最后的代码,如下:
class Laser {
...other code
draw() {
...other code
// 生成粒子
for (const i in this.particles) {
const p = this.particles[i];
p.update();
p.draw();
if (p.age < 0) {
this.particles.splice(Number(i), 1);
}
}
this.particles.push(new Particles(this.w / 2, this.h / 2, this.ctx));
}
}
在 Laser
的 draw
方法中,我们通过实例化 Particles
类,将生成的粒子全部添加到 this.particles
数组中,然后遍历 this.particles
数组,从而实现粒子的渲染和更新,最终的完整实现效果和代码可以在这里进行查看:
总结
通过 ES6 + TS
,让我们编写 canvas
相关的代码时能够更加的得心应手,并且有了编辑器的代码提示后,不论是编写的速度还是代码的准确率,都有很大的提升。虽然定义相关的 TS
类型会花费我们一些时间,但是这些都是很值得的,因此强烈建议大家在后续的学习和开发中都能用到 TS
。
最后,如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者,谢谢大家