『纯canvas实现』你可能想象不到,粒子效果配合时钟还能这么玩?


theme: scrolls-light
highlight: atom-one-dark

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

前言

大家好,我们知道一般学习 canvas 时,做的最多的莫过于各种时钟,像下面这样的:

2.gif

亦或是这样的:

3.gif

或是这样的:

4.gif

上面给大家展示了三种风格各异的时钟效果,但都没有让人眼前一亮的感觉,因此今天给大家介绍一种更加炫酷的粒子圆环时钟。我们都知道粒子效果在很多特效里面都会用到,像烟花的燃放,星空的绘制之类的,一旦有粒子的存在,那这个效果肯定是很炫酷的,下面我们就一起来看一下今天要实现的效果,如图所示:

1.gif

通过上图可以明显的看到,一旦粒子出厂,其它的效果在它面前都黯然失色了,那么这个炫酷的粒子圆环时钟效果是如何实现的呢?下面我们就一起来学习一下吧!

外层粒子圆环

上面这个效果首先接触的时候可能无从下手,但是我们可以通过分解动作来实现。首先我们先看一下这个效果包含的内容,它包含一个外部的粒子圆环,以及内部的电子时钟,因此我们可以先从外部的粒子圆环着手。

首先我们还是先来做一下基础的准备工作,这里依旧使用 TS + ES6 来实现。

先准备好基础的 canvas 标签和简单的 css 样式,因为代码较为简单,这里就不贴代码了,在文章的最后会提供完整的实现代码和效果,我们主要还是看 TS 相关的代码。

按照之前文章的老规矩,我们先定义一个 Clock 类,并准备好基础的代码,如下所示:

class Clock {
    canvas: HTMLCanvasElement;
    ctx: CanvasRenderingContext2D;
    rings: Ring[];
    priticles: Priticle[];
    numbers: {}[];
    last: string;
    colors: string[];
    constructor() {
            this.canvas = document.getElementById('canvas') as HTMLCanvasElement;
            this.ctx = this.canvas.getContext('2d');
            this.canvas.width = innerWidth;
            this.canvas.height = innerHeight;
            this.rings = [];
            this.priticles = [];
            this.numbers = [];
            this.last = '';
            this.colors = [];

            this.init();
    }
    init() {
        this.animate();
    }
    draw() {
        
    }
    animate() {
        requestAnimationFrame(() => this.animate());
        this.draw();
    }
}

new Clock();

上述基础的准备代码中,我们实现了 Clock 类的几个基本的方法,这些在后续会用到,这里先列出来。接下来我们就需要实现外部的粒子圆环了,因为这个粒子圆环其实也是通过很多小粒子组成的,因此我们可以通过定义一个 Ring 类来动态创建这个粒子圆环,我们一起来看一下 Ring 这个类是如何定义的,代码如下:

class Ring {
    x: number;
    y: number;
    deg: number;
    r: number;
    vd: number;
    color: string;
    dx: number;
    dy: number;
    ctx: CanvasRenderingContext2D;
    constructor(w: number, h: number, ctx: CanvasRenderingContext2D) {
        this.x = w / 2;
        this.y = h / 2;
        this.deg = Math.random() * Math.PI * 2;
        this.r = 200 + Math.random() * 10 | 0;
        this.vd = Math.random() * Math.PI * 2 / 360 + 0.01;
        this.color = `hsl(${Math.random() * 360 | 0}, 80%, 50%)`;
        this.dx = this.r * Math.cos(this.deg) + this.x;
        this.dy = this.r * Math.sin(this.deg) + this.y;
        this.ctx = ctx;
    }
    update() {
        this.deg += this.vd;
        this.deg = this.deg % (Math.PI * 2);
        this.dx = this.r * Math.cos(this.deg) + this.x;
        this.dy = this.r * Math.sin(this.deg) + this.y;
    }
    draw() {
        this.ctx.beginPath();
        this.ctx.arc(this.dx, this.dy, 1, 0, Math.PI * 2);
        this.ctx.fillStyle = this.color;
        this.ctx.fill();
    }
}

上述代码可以看出,这就是一个很基础的粒子生成类,通过不断的改变当前的角度,从而生成一个圆环。有了这个 Ring 类,接下来就需要在 Clock 中进行调用,从而动态生成一个粒子圆环,我们一起来看一下修改后的 Clock 类的代码,如下:

class Clock {
    ...other code
    constructor() {
        ...other code
        
        this.init();
    }
    init() {
        for (let i = 0; i < 200; i++) {
            this.rings.push(new Ring(this.canvas.width, this.canvas.height, this.ctx));
        }
        this.animate();
    }
    draw() {
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
        for (let i in this.rings) {
            const r = this.rings[i];
            r.update();
            r.draw();
        }
    }
}

我们通过在 Clock 类的 init 方法中,动态创建 200 个粒子,并将生成的粒子插入到 rings 数组中,并且在 draw 方法中进行调用,最终通过上述代码实现的效果如下所示:

1.gif

可以看到我们已经完成了第一步了,接下来就需要根据当前的时间绘制出中间的数字时钟了,那么该如何做呢?一起接着看~

动态绘制数字时钟

在上述我们已经实现了外层的粒子圆环,接下来我们就需要渲染出中间的粒子时钟了,我们在最前面的效果可以看到,中间的时钟其实也是由一个个点动态组装在一起,从而展示当前的时间,因此我们接下来就需要创建这个时钟的粒子类了。

不过在此之前,我们还需要先将文字渲染出来,我们一起来看一下如何在 canvas 中渲染文字,代码如下:

class Clock {
    ...other code
    
    init() {
        ...other code
        
        this.initText();
    }
    initText() {
        for (let i = 0; i < 10; i++) { //循环0-9
            this.ctx.font = "24px Arial"; 
            let span = document.createElement("span") as HTMLElement; 
            span.style.fontSize = "24px"; 
            span.style.fontFamily = "Arial";
            span.innerHTML = i.toString();
            document.body.appendChild(span); 
            let width = span.offsetWidth; 
            let height = span.offsetHeight;
            span.remove(); 
            this.ctx.fillText(i.toString(), 0, height);
            let data = this.ctx.getImageData(0, 0, width, height).data; 
            let len = data.length; 
            let tdata = []; 
            for (let j = 0; j < len / 4; j++) { 
                if (data[j * 4 + 3] != 0) { 
                    let x = j % width | 0;
                    let y = j / width | 0;
                    tdata.push({
                        x: x,
                        y: y
                    });
                }
            }
            this.numbers.push(tdata);
            this.ctx.clearRect(0, 0, width, height); 
        }
    }
}

上述 initText 方法中,我们动态创建了文字的模型,但是目前还不会在 canvas 中展示出来,因为它目前只是一个“壳”,我们需要往上面填充对应的内容,它才会展示出来,因此我们还需要继续完成中间的粒子 Priticle 类,让我们一起来看一下代码:

class Priticle {
    x: number;
    y: number;
    sx: number;
    sy: number;
    tx: number;
    ty: number;
    color: string;
    age: number;
    ctx: CanvasRenderingContext2D;
    constructor(x: number, y: number, tx: number, ty: number, ctx: CanvasRenderingContext2D, color = 'gray',) {
        this.x = x;
        this.y = y;
        this.sx = (tx - x) / 100;
        this.sy = (ty - y) / 100;
        this.tx = tx;
        this.ty = ty;
        this.color = color;
        this.age = 100;
        this.ctx = ctx;
    }
    update() {
        this.age--;
        if (this.age >= 0) {
            this.x += this.sx;
            this.y += this.sy;
        }
    }
    draw() {
        this.ctx.beginPath();
        this.ctx.fillStyle = this.color;
        this.ctx.arc(this.x, this.y, 1, 0, Math.PI * 2);
        this.ctx.fill();
    }
}

Priticle 类和 Ring 类很类似,因为它们都是用于生成粒子效果的,而我们要生成中间的时钟,其实就是通过生成 N个 粒子来组成的。有了 Priticle 类,我们还需要获取到当前的时间,用于生成需要展示的文字,让我们一起来看一下如何获取到当前的时间,代码如下:

class Clock {
    ...other code
    
    currentClock() {
        const d = new Date();
        return ('0' + d.getHours()).slice(-2) + ('0' + d.getMinutes()).slice(-2) + ('0' + d.getSeconds()).slice(-2);
    }
}

Clock 中,我们定义了一个辅助方法 currentClock,它主要用于获取当前的时分秒,并通过往前补零切割的方式来组成一个新的时间,最后我们还需要定义一个 initClock 方法用于生成中间的粒子时钟,让我们一起来看代码:

class Clock {
    ...other code
    
    initClock() {
        let now: string = this.currentClock();
        for (let i in now) {
            for (let j in this.numbers[now[i]]) {
                let n = this.numbers[now[i]][j];
                let r = this.rings[j];
                if (now[i] !== this.last[i]) {
                    this.priticles.push(
                        new Priticle(
                            r.dx,
                            r.dy,
                            n.x * 4 + (this.canvas.width / 2 - 180 + 60 * +i),
                            n.y * 4 + this.canvas.height / 2 - 60,
                            this.ctx
                        )
                    )
                } else {
                    this.priticles.push(
                        new Priticle(
                            n.x * 4 + (this.canvas.width / 2 - 180 + 60 * +i),
                            n.y * 4 + this.canvas.height / 2 - 60,
                            n.x * 4 + (this.canvas.width / 2 - 180 + 60 * +i),
                            n.y * 4 + this.canvas.height / 2 - 60,
                            this.ctx
                        )
                    )
                }
            }
        }
        this.last = now;
    }
}

可以看到在 initClock 方法中,我们通过 currentClock 方法获取到当前的时分秒,它是一个字符串,类似这样的“172231”;接下来我们通过遍历当前的时间来动态创建粒子,在创建的过程中,我们需要判断当前一个粒子和最后一个粒子是否一致,如果不一致就生成新的,否则当前的粒子位置不做改变。通过上述代码可以实现一个静态的时钟,它还不会走动,因此我们还需要通过添加定时器来获取每一秒的时间,相关代码如下:

class Clock {
    ...other code
    
    init() {
        ...other code
        
        this.startTime();
    }
    
    draw() {
        ...other code
        for (let i in this.priticles) {
            const p = this.priticles[i];
            p.update();
            p.draw();
            if (p.age === -50) {
                this.priticles.splice(+i, 1);
            }
        }
    }
    
    startTime() {
        setInterval(() => this.initClock(), 1000);
    }
}

通过修改上述代码,最终实现的效果如下所示:

2.gif

可以看到目前效果已经出来了,但是中间时钟的颜色还是白色的,因此我们还需要添加对应时分秒的颜色,还记得我们在 Clock 类的 constructor 中预留的 this.colors 数组吗?它就是用于存放时分秒文字对应的颜色的,我们只需要添加上对应的颜色值即可,然后在 Clock 类的 initClock 方法中传入对应的颜色即可,最终实现的完整代码及效果可以在这里进行查看:

代码片段

总结

通过借助 canvas 的粒子效果,可以实现更多有趣炫酷的内容,当然要想学好粒子效果,三角函数相关的知识点还是必不可少的。

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

往期回顾

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

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

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

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

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

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

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

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

© 版权声明
THE END
喜欢就支持一下吧
点赞9 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容