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

3,064 阅读9分钟

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

前言

前面一节中,我们使用 canvas 实现了一个炫酷的全屏旋转六边形。通过学习三角函数相关的知识点,我发现 canvas 的动画是真的好玩,刚好今天又在小破站学到了一个更加炫酷的三角形旋转效果,那就让我们一起来学习一下吧!

首先还是先看一下最终实现的效果,如下图所示:

demo1.gif

这个效果中运用到的很多知识点,包括获取三角形的坐标三角函数相关的知识点,在前面一节中也都有介绍过,接下来咱们就一起来实现这个效果吧!

旋转的大三角形

看到这个效果,我们首先应该想到的是,如何定位到外部的三角形的三个顶点,以及如何定位到生成的小三角形的顶点。如果你能想到这里,那就证明你已经懂了一点关于 canvas 绘图的原理。其实跟上一节一样,我们只需要找到外部三角形的中心点,以及它的三个顶点,就能够画出外部的三角形了。

我们先来画出外面的三角形,并实现旋转的效果。这里需要的 htmlcss前面一节是一样的,就不做过多的描述了,我们主要还是关注 js 相关的内容,我们依旧使用 ES6 中的 class 语法糖,相关代码如下:

class RotationTriangle {
    constructor() {
        /** @type {HTMLCanvasElement} */
        this.canvas = document.getElementById('canvas');
        this.ctx = this.canvas.getContext('2d');
        this.canvas.width = 800;
        this.canvas.height = 800;
        this.tris = [];  // 保存所有的三角形对象
    }
}

首先定义一个 RotationTriangle 类,并在 constructor 函数中定义好初始化的相关数据,因为我们后面要生成很多三角形,因此我们继续定义一个新的 Triangle 类,通过实例化 Triangle 类,实现三角形的绘制,Triangle 类的相关代码如下:

class Triangle {
    constructor(x, y, r, d, ctx) {
        this.x = x; // 三角形中心的x坐标
        this.y = y; // 三角形中心的y坐标
        this.r = r; // 中心到顶点的距离
        this.d = d; // 初始角度
        this.td = this.d; // 旋转角度
        this.ctx = ctx;
    }
    // 更新方法
    update() {
        // 增加旋转角度
        this.td++;
    }
    // 绘制方法
    draw() {
        this.drawTri(this.x, this.y, this.r, this.d);
        this.drawTri(this.x, this.y, this.r, this.td);
    }
    // 绘制三角形
    drawTri(x, y, r, d) {
        this.ctx.beginPath();
        for (let i = 0; i < 3; i++) {
            const { x: x1, y: y1 } = this.getCoordinate(x, y, r, d + i * 120);
            this.ctx.lineTo(x1, y1);
        }
        this.ctx.closePath();
        this.ctx.stroke();
    }
    // 根据三角函数计算出对应的坐标点
    getCoordinate(x, y, r, d) {
        let x1 = r * Math.cos(d * Math.PI / 180) + x;
        let y1 = r * Math.sin(d * Math.PI / 180) + y;
        return { x: x1, y: y1 };
    }
}

Triangle 类主要是用于生成三角形,其中我们需要传入五个参数,这五个参数在代码中已经添加了相关的注释。接下来我们就需要在 RotationTriangle 类中通过实例化 Triangle 类来渲染外部的三角形,并让它们旋转起来,我们一起来给 RotationTriangle 类添加几个方法,相关代码如下:

class RotationTriangle {
    constructor() {
        ...other code
        
        this.init();
        this.animate();
    }
    init() {
        this.tris.push(
            new Triangle(this.canvas.width / 2, this.canvas.height / 2, this.canvas.width / 2, -90, this.ctx)
        );
    }
    draw() {
        for (const n in this.tris) {
            const tris = this.tris[n];
            tris.update();
            tris.draw();
        }
    }
    animate() {
        requestAnimationFrame(() => this.animate());
        this.draw();
    }
}

new RotationTriangle();

RotationTriangle 类中,我们添加了一个 init 方法,主要用于初始化生成的三角形,最后不要忘了实例化 RotationTriangle 类,实现的效果如下图所示:

demo2.gif

通过上图可以看到,我们的外部的三角形已经绘制出来了,并且也在旋转,但是我们这里没有加 clearRect ,因此会绘制无限个三角形,我们在 RotationTriangle 类的 draw 方法中添加一下 clearRect,效果如下所示:

demo3.gif

当我们添加了 clearRect 方法后,可以看到多余的三角形都已经被清除了,但是在 canvas 中的三角形却一直旋转不会停下来,我们需要让它旋转到一定的角度后就停止下来,然后继续下一步的操作。

再说这个旋转的三角形停下来之前,我们先来看一下它是如何旋转起来的。还记得在前面定义的 Triangle 类吗?在 Triangle 类中,我们定义了一个 this.d 的变量,这个变量主要用于保存初始的旋转角度,并且还定义了一个 this.td 的变量,这个变量主要用于旋转时角度的改变。

Triangle 类的 update 方法中,我们通过 this.td++ 不断的改变旋转角度,然后执行 this.draw 方法来绘制三角形,就实现了三角形的旋转。那么我们该如何让这个旋转的三角形到达一定角度后就停下来呢?其实也很简单,我们只需要修改 Triangle 类中的 update 方法即可,让我们一起来看一下修改后的代码,如下:

class Triangle {
    ...other code 
    update() {
        // 限制旋转角度
        if (this.td < this.d + 60) {
            // 增加旋转角度
            this.td++;
        }
    }
    ...other code
}

我们只需要在 Triangle 类的 update 方法中添加一个简单的逻辑判断,当旋转的角度小于初始角度加60度时,旋转的角度才不断的变化,当超过这个值时,旋转的角度就不会变化了,这样整个旋转的三角形就会停止了,让我们一起来看一下实现的效果:

demo4.gif

在上图中可以看到,当旋转到距离初始角度的60度后,三角形就会停止旋转了。这里在GIF图中没有停下来是因为图片会反复的播放,大家可以将代码拷到本地跑跑就知道了。

外部的大三角实现了,接下来我们就需要实现两个大三角形的六个顶角出生成N个小三角形的效果了,该如何实现呢?让我们一起来看一下吧!

旋转的小三角

在上面我们已经实现了一个旋转的大三角形,那么要实现在这两个三角形六个顶点出生成新的 N个 小三角形,我们只需要找到这六个顶点,然后再使用跟上面一样生成大三角形的方法,就可以生成 N个 小三角形了,让我们一起来看代码吧,这里只需要修改 RotationTriangle 类中的 draw 方法即可,相关代码如下:

class RotationTriangle {
    ...other code
    
    draw() {
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
        for (const n in this.tris) {
            const tris = this.tris[n];
            tris.update();
            tris.draw();
            
            // 渲染两个大三角形的六个顶点周围的小三角形
            // 首先需要判断一下当前大三角形的旋转是否已经停止了
            if (tris.td === tris.d + 60) {
                // 因为是两个大三角形,因此有6个顶点
                for (let m = 0; m < 6; m++) {
                    // 根据三角函数,小三角的中心到外部三角的中心距离相当于外部三角距离顶点的2/3
                    const nr = tris.r * 2 / 3;
                    // 新的三角形的旋转角度,默认为外部三角形的角度
                    const d = tris.d + m * 60;
                    // 计算坐标,根据外部三角行的角度旋转六次,即可获得新的三角形中心的坐标
                    const x = nr * Math.cos(d * Math.PI / 180) + tris.x;
                    const y = nr * Math.sin(d * Math.PI / 180) + tris.y;
                    // 计算新的三角形的顶点,距离相当于外部三角形距离顶点的1/3
                    const r = tris.r / 3;
                    this.tris.push(new Triangle(x, y, r, d, this.ctx));
                }
            }
        }
    }
}

我们在 RotationTriangle 类的 draw 方法中,首先要在两个大三角形的六个顶点周围找到 N个 小三角形的坐标点,然后通过实例化 Triangle 类来生成 N个 小三角形,最终实现的效果如下所示:

demo5.gif

还是跟前面一样,我们也需要限制一下小三角形的生成,否则它会无限的生成,产生的效果就如上图所示。我们需要在 Triangle 类中定义一个旋转的开关,用它来判断当前的N个三角形是否已经旋转到指定的位置了,这样就能防止三角形无限的旋转下去,我们先来看一下 Triangle 相关的代码,如下:

class Triangle {
    constructor(x, y, r, d, ctx) {
        ...other code
        // 添加一个开关即可
        this.act = true; // 旋转开关,当旋转到指定的位置后,才开始生成新的小三角形
    }
}

Triangle 类中有了旋转的开关后,我们就需要修改一下 RotationTriangle 类中的 draw 方法内的判断条件了,相关代码如下:

class RotationTriangle {
    ... other code
    draw() {
        ... other code 
        
        // 判断一下当前大三角是否旋转到了指定的角度,并且需要限制一下生成的三角形尺寸,不能无限制的生成下去
        if (tris.td === tris.d + 60 && tris.r > 10 && tris.act) {
            ...other code
            
            // 开关设置为false,停止旋转
            tris.act = false;
        }
    }
}

我们在 RotationTriangle 类的 draw 方法中更新了生成三角形的条件后,当旋转到一定的角度时,就不会再继续旋转了,让我们一起来看一下修改后的效果:

demo6.gif

到这里,这个旋转的三角形的效果已经完成的差不多了,但是目前这个效果只会执行一遍就自动停止下来,并且这个颜色也很丑。接下来我们就让整个旋转的三角形变得更加炫丽一些,并且可以无限的动起来。

炫丽的旋转三角形

要给一个图像添加颜色,我们有可以通过 rgba 或者 hsl ,这里我依旧使用 hsl,在上一节中也使用过这个属性,我们只需要在 Triangle 类的 drawTri 方法中添加 hsl 属性,并根据旋转的角度自动来变换颜色以及自动淡出即可,让我们看一下修改后的代码,如下:

class Triangle {
    constructor(x, y, r, d, ctx) {
        ...other code 
        
        // 旋转180度后,“清空” canvas
        this.opt = 180; // 当旋转到对应角度时,画布中的三角形淡出画布
    }
    ...other code
    
    drawTri(x, y, r, d) {
        this.ctx.beginPath();
        for (let i = 0; i < 3; i++) {
            const { x: x1, y: y1 } = this.getCoordinate(x, y, r, d + i * 120);
            this.ctx.lineTo(x1, y1);
            // 添加 strokeStyle 即可,颜色值属性为 hsl
            this.ctx.strokeStyle = `hsl(${this.td}deg, 30%, 50%, ${this.opt / 60})`;
        }
        this.ctx.closePath();
        this.ctx.stroke();
    }
}

Triangle 类的 drawTri 方法中,我们添加了 strokeStyle 属性值,并且使用 hsl 作为颜色的变换属性,如果不了解 hsl 相关的参数,可以点击这里进行查看。hsl 前三个参数分别就对于了颜色的色调值饱和度以及颜色的亮度,而最后一个参数则代表当前的透明度,当我们在前面定义的 this.opt 遍历值除以 60 后,canvas 中的三角形就会逐渐淡出画布,看起来就像被“清除”了一样。

然后我们还要让整个旋转的效果一直持续下去,接下来就需要在 RotationTriangle 类中添加一个 this.count 变量,它主要用于记录当前的旋转次数,当旋转次数小于0时,就重新绘制全屏的三角形,这样就可以无限的旋转下去了,让我们一起来看一下最后的代码:

class RotationTriangle {
    constructor() {
        ...other code
        this.count = 0;
        
        this.init();
    }
    init() {
        this.count = 480;
        ...other code
    }
    draw() {
        ...other code
        
        this.count--;
        if (this.count < 0) {
            this.tris.length = 0;
            this.init();
        }
    }
}

draw 方法中,通过不断的减小 this.count 的值,当它小于 0 时,我们就将 this.tris 数组的长度重置为 0,这样就减少 canvas 绘制时浏览器内存的消耗;其次我们重新执行 this.init() 方法,这样就能够让整个效果无限的执行下去了,最终完整的代码及执行效果可以在这里进行查看:

总结

在这一节总,我们又对前面学到的关于三角函数相关的知识点进行了复习,并且实现的这个效果中,用到的获取图像坐标的方法,也是在前面一节中学到过的,如果对这一节中获取图像坐标点的方法还不了解,可以去看一下前一篇的文章。

不得不说的是,学好三角函数,真的可以畅快淋漓的把玩 canvas 的动画。那么,你心动了吗?快来跟我一起学习吧!

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

往期回顾

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

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

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

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