又实现了一个酷炫的动感激光,不来看看?

2,057 阅读5分钟

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

前言

大家好,我是爱吃鱼的桶哥Z,最近比较忙,一直都没空更新文章。刚好周末了,可以好好的放松一下,顺便在小破站上学习学习。今天又学到了一个动感激光效果,还是很酷炫的,顺便就分享给大家一起学习一下。首先我们还是先看一下最终实现的效果,如图:

demo1.gif

可以看到这个效果就是一个不断旋转的激光图,并且中间还生成了很多粒子效果,下面就让我们一起来看一下这个效果是如何实现的吧!

激光线条的生成

我们通过上图可以看出这个动感激光主要是由两部分组成,其中包括了N多线条的生成和N多粒子的生成,通过这两种元素组成了我们最终的这个效果。那么我们就先从生成线段着手。

首先还是老规矩,先编写相应的 htmlcss,这两块的内容在前面的文章中已经写了很多次了,这里就不写了,不知道具体 htmlcss 的童鞋,可以看前面的文章。我们主要还是看一下 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;
    }
}

Lineconstructor 中,我们定义了 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();
    }
}

Lineupdate 方法中,通过不断的更新 this.rthis.l 来改变当前线段的起点和终点,这样就能让线段在 canvas 中不断的移动;而在 draw 中,则通过三角函数相关的内容计算出线段的开始和结束的点,并通过 this.ctx.moveTothis.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 中的 updatedraw 方法来实现线段的绘制和更新,最后需要判断当生成的线段已经超过了页面对角线的一半时,需要删除当前线段,这样主要是为了减少浏览器内存的消耗。

接下来我们还需要让这些生成的线段动起来,我们已经实现了很多次 canvas 中的动画效果了,这里就直接来看一下代码吧,如下:

class Laser {
    ...other code
    
    animate() {
        requestAnimationFrame(() => this.animate());
        this.draw();
    }
}

我们定义了一个 animate 方法,并通过 requestAnimationFrame 方法不断的调用 animate,从而让整个 canvas 不断的重复渲染,让我们看到最终的动画效果。最后不要忘了实例化 Laser 类,它有一个参数,最终实例化的代码如下:

new Laser(document.getElementById('canvas') as HTMLCanvasElement);

我们一起来看一下线段的生成效果吧,如下:

demo2.gif

可以看到上述的代码已经帮我们在 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;
    }
}

Particlesconstructor 中,我们定义了生成粒子的相关属性,通过这些属性来绘制一个基础的粒子,下面我们来看一下相关的 updatedraw 方法,代码如下:

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.xthis.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));
    }
}

Laserdraw 方法中,我们通过实例化 Particles 类,将生成的粒子全部添加到 this.particles 数组中,然后遍历 this.particles 数组,从而实现粒子的渲染和更新,最终的完整实现效果和代码可以在这里进行查看:

总结

通过 ES6 + TS ,让我们编写 canvas 相关的代码时能够更加的得心应手,并且有了编辑器的代码提示后,不论是编写的速度还是代码的准确率,都有很大的提升。虽然定义相关的 TS 类型会花费我们一些时间,但是这些都是很值得的,因此强烈建议大家在后续的学习和开发中都能用到 TS

最后,如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者,谢谢大家

往期回顾

canvas 动画真好玩,快来学一下这炫酷的效果吧!

妙啊,canvas 还能实现这么酷炫的旋转六边形

为了学会更多炫酷的 canvas 效果,我熬夜复习了三角函数相关的知识点

嚯,五角星还能这么玩?快摘下来送给你的她/他/ta😁

这个国庆,带老婆去看一场烟花雨