『 canvas 特效』一文教你绘制绚丽的星空背景🌃 TS + ES6

4,230 阅读5分钟

本文正在参加「金石计划 . 瓜分6万现金大奖」

介绍

大家好,我是爱吃鱼的桶哥Z,很久没有写关于 canvas 效果的文章了,刚好最近又学到了一个新的特效,使用 canvas 绘制多层次动态星空背景,今天就分享给大家。首先我们依旧来看一下最终实现的效果,如图所示:

demo1.gif

由于录制 GIF 造成失帧,因此图片可能看不出完整的动画,完整的效果及代码可以拉到文章最底部来进行观赏。

在上图中可以简单的看出有层次的星空图在不断的变换,一会儿向右移动,一会儿又向左移动,最后还会进行旋转,那么这样的效果是如何实现的呢?咱们就一起来学习一下吧!

绘制动态星空图

因为这个效果是使用 canvas 来实现的,因此咱们就需要做一些基础的准备工作,由于这次是做背景效果,因此不需要在 html 中添加默认的标签,我们会通过代码来动态插入 canvas 标签。

首先还是需要准备好相关的 css 样式代码,涉及到的样式很简单,如下所示:

*{
    margin: 0; 
    padding: 0;
}
body {
    background: #000; 
    overflow: hidden;
}

简单的设置好 css 代码后,接下来就需要添加基础的 TS 准备代码了同鞋悉的童鞋们可能都知道这里为啥要使用 TS 了吧,如果还不清楚的,可以去查看我之前写的文章,这里咱们依旧使用 TS + ES6 的语法来进行编写,基础的准备代码如下所示:

class StarrySky {
    canvas: HTMLCanvasElement;
    ctx: CanvasRenderingContext2D;
    constructor() {
        this.canvas = document.createElement('canvas') as HTMLCanvasElement;
        this.canvas.width = innerWidth;
        this.canvas.height = innerHeight;
        this.canvas.style.zIndex = '-1';
        this.ctx = this.canvas.getContext('2d');
        document.body.appendChild(this.canvas);
    }
}

在基础的准备代码中,我们通过 document.createElement 创建了一个 canvas 标签,并给它设置了相关的宽高和层级,最后通过 document.body.appendChild 将 canvas 插入到 body 中。有了以上准备代码后,接下来咱们还需要把动画相关的代码也编写好,相关代码如下所示:

class StarrySky {
    ...other code 
    constructor () {
        ...other code 
    }
    draw() {
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    }
    animate() {
        requestAnimationFrame(() => this.animate());
        this.draw();
    }
}

其实现在 canvas 上面已经有了动画了,只是因为还没有绘制内容,因此看不到效果,接下来咱们就该添加星空效果了。

星空效果其实就是很多不同的粒子展示在 canvas 中,因此咱们需要先创建一个 Particle 粒子类,然后通过生成N个粒子,从而在 canvas 中展示,一起来看一下 Particle 粒子类的相关代码吧,如下:

class Particle {
    x: number;
    y: number;
    vx: number;
    w: number;
    h: number;
    ctx: CanvasRenderingContext2D;
    constructor(width: number, height: number, ctx: CanvasRenderingContext2D) {
        this.w = width;
        this.h = height;
        this.ctx = ctx;
        this.x = Math.random() * width;
        this.y = Math.random() * height;
        this.vx = Math.random();
    }
    update() {
        this.x += this.vx * 3;
        if (this.x > this.w) {
            this.x = 0;
        }
    }
    draw() {
        this.ctx.beginPath();
        this.ctx.arc(this.x, this.y, 1 + this.vx, 0, Math.PI * 2);
        this.ctx.fillStyle = `rgba(255, 255, 255, ${this.vx})`;
        this.ctx.fill();
    }
}

在 Particle 粒子类中,我们添加了三个随机值,并在 update 方法中不断更新 x 值的数值,在前面的文章中讲过,我们要改变某个元素的位置时,只需要改变它沿 x轴 或 y轴 的属性值即可,因此这里也是一样的;而在 draw 方法中,我们通过 ctx.arc() 方法来绘制每一个粒子圆,并通过 fillStyle 设置粒子的颜色,当 this.vx 值越小时,这个粒子的透明度越低,这样看起来就会显得非常有层次感。

有了 Particle 粒子类,接下来咱们就需要在前面的 StarrySky 类中进行使用了。首先咱们还要对 StarrySky 类中的 constructor 添加更多的属性,代码如下:

class StarrySky {
    canvas: HTMLCanvasElement;
    ctx: CanvasRenderingContext2D;
    particles: Particle[];
    count: number;
    actions: string[];
    action: number;
    constructor() {
        ...other code
        
        this.particles = [];
        this.count = 1000;
        
        this.animate();
    }
}

可以看到我们在 constructor 中新增了一个 particles 数组,它主要用于存储生成的粒子类,然后我们需要在 StarrySky 类的 draw 方法中生成粒子,一起来看代码:

class StarrySky {
    ...other code
    
    draw() {
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
        if (this.particles.length < this.count) {
            this.particles.push(
                new Particle(this.canvas.width, this.canvas.height, this.ctx)
            );
        }
        for (let i in this.particles) {
            const p = this.particles[i];
            p.update();
            p.draw();
        }
    }
}

当我们在 StarrySky 类的 draw 方法中动态生成粒子,并调用对应的 update 和 draw 方法时,最终可以看到如下效果:

demo2.gif

通过上图咱们可以看到一幅有层次的星空图已经绘制回来了,但是目前的星空只会向右进行移动,效果还是有些单调,因此咱们接下来通过代码实现点击任何区域让星空的移动能够变换方向,那么该如何实现呢?

变幻的星空图

在前面咱们已经通过代码实现了有层次的星空背景绘制了,但是目前可以看到星空的移动只有一个方法,那么该如何修改星空的移动方法呢?还记得在 Particle 星空类 中的 update 方法吗?在 update 方法中,我们是通过修改当前 this.x 的值从而让粒子实现移动的,因此咱们可以从 update 方法入手,代码如下:

class Particle {
    ...other code
    
    update(direction = 'right') {
        switch (direction) {
            case 'right':
                this.x += this.vx * 3;
                if (this.x > this.w) this.x = 0;
                break;
            case 'left':
                this.x -= this.vx * 3;
                if (this.x < 0) this.x = this.w;
                break;
            case 'up':
                this.y -= this.vx * 3;
                if (this.y < 0) this.y = this.h;
                break;
            case 'down':
                this.y += this.vx * 3;
                if (this.y > this.h) this.y = 0;
                break;
        }
    }
}

通过修改 Particle 类的 update 方法可以看出,我们通过一个变量来判断当前移动的方向,并且设置每个方法不同的值,接下来我们还需要继续修改 StarrySky 类,这样咱们才能实现星空方法的不断变换,一起来看代码:

class StarrySky {
    ...other code
    
    actions: string[];
    action: number;
    constructor() {
        ...other code
    
        this.actions = ['right', 'left', 'up', 'down', 'around'];
        this.action = 0;

        this.animate();
        this.event();
    }
    
    event() {
        document.body.addEventListener('click', () => {
            this.action++;
            this.action = this.action % this.actions.length;
        });
    }
    
    draw() {
        ...other code
        
        for (let i in this.particles) {
            const p = this.particles[i];
            p.update(this.actions[this.action]);
            p.draw();
        }
    }
}

在 StarrySky 类的 constructor 方法中,我们添加了一个 actions 数组,里面包含了上下左右以及旋转五个值,然后还添加了一个 action 变量,用于表示当前是第几个,并且通过 document.body.addEventListener 给 body 添加了一个 click 事件,当 body 被点击时,修改当前的 action,从而改变星空的移动方向。最后在 StarrySky 类的 draw 方法中,调用 Particle 类的 update 方法时,将当前的移动方向传入即可实现改变星空方向变换的效果了。

当然,咱们还剩下最后一步,还记得上面的 actions 中咱们总共写了五个值,但是在 Particle 类的 update 方法中,咱们只添加了四个 case,因此还剩下最后一个旋转的 case 需要编写,代码如下:

class Particle {
    ...other code
    
    update(direction = 'right') {
        switch (direction) {
            ...other code
            
            case 'around':
                let deg = Math.atan2(
                    this.y - this.h / 2,
                    this.x - this.w / 2
                );
                let r = Math.sqrt(
                    Math.pow(this.x - this.w / 2, 2) + Math.pow(this.y - this.h / 2, 2)
                )
                this.x = r * Math.cos(deg + this.vx / 200) + this.w / 2;
                this.y = r * Math.sin(deg + this.vx / 200) + this.h / 2;
                break;
        }
    }
}

当 case 为 around 时,星空需要变换为渲染的形式,咱们就需要找到旋转的角度和半径,根据勾股定理和三角函数相关的知识,最终获取到当前移动的 x 和 y 的值。完整的代码及实现效果如下所示:

总结

总的来说,这个效果还是很不错的,并且还添加了相关的交互事件,实现的原理也很简单,有兴趣的童靴可以自己实现一下,本期的内容到此结束。

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

往期回顾

我是如何用 canvas 实现一个自定义仙女棒的?

又用 canvas 实现了一个旋转的伪3D球体,来瞧瞧?

canvas 实现多彩的圆环数字时钟

不得不说,这个 canvas 的旋转半圆效果可能会闪瞎你的双眼🐶

canvas 实现燃烧的线段,原来线段也能玩的这么出彩

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

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