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

2,020 阅读7分钟

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

前言

前面一节中,我熬夜复习了三角函数相关的知识点。既然我都熬夜了,那要是不来点实际的效果,也对不起自己熬的夜,刚好在小破站又学到了一个新的效果。那么今天就来继续加强一下对三角函数的学习和使用,通过三角函数相关的知识点在 canvas 中绘制全屏旋转的六边形。首先还是先看一下最终实现的效果:

demo1.gif

可以看到这个效果还是比较炫酷的,那么接下来我们就一步一步实现一下这个效果吧!

渲染一个六边形

看到这个效果,如果不知道如何下手,那么我们可以先从渲染一个六边形开始,只要渲染出一个,然后再根据不同的位置,将整个屏幕用六边形铺满,并加上相关的角度旋转,就能实现上面的这个效果了。

接下来还是先实现最基础的 htmlcss,这个效果用到的 htmlcss 都很简单,代码如下:

<canvas id="canvas"></canvas>

跟前面的文章一样,html 中只需要一个 canvas 标签即可,css 相关的代码也及其简单,如下:

*{margin: 0; padding: 0;}
body{
    width: 100%;
    height: 100vh;
    overflow: hidden;
    background: #000;
}

相信这么几行 css 代码,也不用过多的解释了,JS 才是我们的重点,接下来就看看如何通过 JScanvas 中绘制出如上的效果吧!

还是跟前面一样,这里依旧基于 ES6 中的 class 来构造一个类,并且执行它。

首先我们定义一个 Hexagon 类,依旧是需要准备好基础内容,相关代码如下:

class Hexagon {
    constructor() {
        /** @type {HTMLCanvasElement} */
        this.canvas = document.getElementById('canvas');
        this.ctx = this.canvas.getContext('2d');
        this.canvas.width = innerWidth;
        this.canvas.height = innerHeight;
        // 装载六边形的数组
        this.hexs = [];
        // 六边形半径
        this.radius = 60;
    }
}

我们在 Hexagon 类的 constructor 中定义好相关的属性,方便我们后续的使用。接下来我们还需要再创建一个 Hex 类,它主要用于生成六边形,在上述的代码中,我们添加了一个 this.hexs 属性,这个属性主要用于存放动态生成的六边形。接下来让我们一起来实现一些 Hex 类,相关代码如下:

class Hex {
    constructor(x, y, radius, ctx) {
        this.x = x;
        this.y = y;
        this.deg = 0;
        this.radius = radius;
        this.ctx = ctx;
    }
    draw() {
        // 开始绘制
        this.ctx.beginPath();
        // 获取坐标点
        const { x, y } = this.getCoordinate(this.x, this.y, this.radius, this.deg);
        // 移动到指定坐标点
        this.ctx.moveTo(x, y);
        // 动态生成六边形,每个角是60度
        for (let i = 0; i <= 6; i++) {
            // 获取每一个角
            const deg = i * 60 + this.deg;
            // 再次获取对于的坐标点
            const { x, y } = this.getCoordinate(this.x, this.y, this.radius, deg);
            // 连接每一个点
            this.ctx.lineTo(x, y);
        }
        // 根据角度不同动态生成颜色
        this.ctx.fillStyle = `hsl(${this.deg}deg, 30%, 50%)`;
        this.ctx.fill();
    }
    // 根据角度与弧度互换的方式获取指定的坐标点
    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 };
    }
}

Hex 类的 constructor 中,我们接收从外面传入的四个参数,其中 x 表示在 canvas 画布中的 x轴 坐标;y 表示在 canvas 画布中的 y轴 坐标;radius 表示当前六边形的半径,ctx 就不用过多介绍了,因为需要用到 ctx 来绘制,因此需要将 ctx 也传到 Hex 中来。

接下来我们只需要在 Hexagon 类中定义初始的 init 方法和 draw 方法,然后执行它就可以绘制出一个六边形了,让我们一起来看代码,如下:

class Hexagon {
    constructor() {
        ...other code
        
        this.init()
    }
    
    init() {
        // 在canvas的中心绘制一个六边形
        this.hexs.push(new Hex(this.canvas.width / 2, this.canvas.height / 2, 0, this.radius, this.ctx));
        
        this.draw();
    }
    
    draw() {
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
        for (let i in this.hexs) {
            let h = this.hexs[i];
            h.draw();
        }
    }
}

new Hexagon();

通过实例化 Hexagon 类,我们在 canvas 中就能得到一个六边形了,绘制出来的效果如下所示:

image.png

还记得咱们在前面定义的 this.radius 吗?通过它可以改变六边形的半径,大家可以尝试修改一下,就可以得到不同大小的六边形。

现在咱们已经成功的在 canvas 中绘制出一个六边形了,那么要实现全屏的六边形该怎么做呢?

渲染全屏六边形

Hexagon 类的 init 方法中,我们通过实例化 Hex 类,并将它存在 this.hexs 数组中,然后在 Hexagon 类的 draw 方法中,通过遍历的方式获取到每一个 Hex 实例对象,然后执行它上面的 draw 方法,最后就得到了一个完整的六边形。那么我们要渲染全屏的六边形,当然也是从 Hexagon 类的 init 方法下手了。

首先我们在 init 方法中通过双重循环来渲染当前 canvas 正中心的六边形的六条边的延伸效果,可以先看代码,然后就明白为什么要使用双重循环了,代码如下:

class Hexagon {
    ...other code
    
    init() {
        ...other code
        
        // 在前面已经渲染了一个六边形,这一层的循环是为了找到当前六边形外面的六条边的点
        for (let i = 0; i < 6; i++) {
            // 这里的一层循环是通过六边形的半径来创建点,因为半径只是一般,因此需要乘以2得到直径
            for (let j = this.radius * 2; j < this.canvas.width; j += this.radius * 2) {
                // 这里就是获取当前六边形外部六条边的点的位置
                const x = j * Math.cos(i * 60 * Math.PI / 180) + this.canvas.width / 2;
                const y = j * Math.sin(i * 60 * Math.PI / 180) + this.canvas.height / 2;
                // 将上面找到的点用于创建新的六边形
                this.hexs.push(new Hex(x, y, this.radius, this.ctx));
            }
        }
    }
}

在上述代码中,添加了相关的注释信息,并且说明了为什么要使用双重循环,其中用到了 Math.cos()Math.sin() ,在上一节中介绍了弧度和角度的互换公式,这里就用到了,也算是复习了昨天学习的内容。通过这部分的内容,最终实现的效果如下:

image.png

看到这个效果,我想你应该就明白为什么要使用双重循环了吧!

在第一层的循环中,是为了找到正中心六边形外面的六个边,因此只循环了6次;而在第二层循环中,我们就需要通过当前定义的六边形的半径来获取它外围的每一个六边形的距离,最后再通过三角函数相关的知识点,从而计算出在这个正中心六边形外部六条边对应的坐标点,并生成对应的六边形。

现在已经有了这么一个效果,那么下一步就是需要铺满全屏了,该如何实现呢?其实还需要继续在 init 方法中做第三层的循环,通过第三层的循环,找到其它六边形的坐标点,让我们先来看代码,如下:

class Hexagon {
    ...other code
    
    init() {
        ...other code
        
        // 第一层循环
        for (let i = 0; i < 6; i++) {
            // 第二层循环 
            for (let j = this.radius * 2; j < this.canvas.width; j += this.radius * 2) {
                ...other code
                
                // 第三层循环,循环的次数为外层次数处于当前六边形的直径
                for (let k = 1; k < j / (this.radius * 2); k++) {
                    // 这里用到最外层的 i + 2,是为了实现间隔的六边形
                    const x1 = k * this.radius * 2 * Math.cos((i + 2) * 60 * Math.PI / 180) + x;
                    const y1 = k * this.radius * 2 * Math.sin((i + 2) * 60 * Math.PI / 180) + y;
                    this.hexs.push(new Hex(x1, y1, this.radius, this.ctx));
                }
                
                ...other code
            }
        }
    }
}

我们在第三层循环中,通过每一个六边形的间隔来找到它旁边的坐标,而我们知道一个圆是360度,六边形就是六个角,每个角60度,因此需要乘以60度,然后通过 Math.cos() 和 Math.sin() 方法将弧度转换为角度,最终计算出所有的坐标点。

有了所有的六边形的坐标点,我们就可以继续创建新的六边形了,因此在第三层循环中,我们继续实例化 Hex 类。最终实现的效果如下图所示:

image.png

现在整个屏幕都被六边形挤满了,并且它们的颜色都是一样的,非常难看。还记得咱们最开始看到的效果吗?所有的六边形都是可以旋转的,并且根据它的旋转颜色还会发生改变,那该如何实现呢?

旋转的多彩六边形

canvas 中,我们要让一个图像动起来,无非就是改变它的坐标值或者是它的角度,这样就能让一个图像动起来,那么首先我们就需要先添加动画相关的内容,在 Hexagon 类中定义一个 animate 方法,并使用 requestAnimationFrame 来执行这个动画,相关代码如下:

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

Hexagon 类的 init 方法中,我们执行 this.animate() 方法,目前这样执行还只会让画布不断的重新渲染,六边形并不会旋转和变色,要想让所有的六边形都能够旋转并根据旋转不断变换颜色,我们还需要继续对 Hex 类进行改造,让我们一起来看相关的代码:

class Hex {
    constructor(x, y, deg, radius, ctx) {
        this.x = x;
        this.y = y;
        this.deg = deg;
        this.radius = radius;
        this.angle = 0; // 当前的角度
        this.ctx = ctx;
    }
    update() {
        this.angle++
        if (this.angle <= 30) {
            this.deg++;
        }
        if (this.angle == 90) {
            this.angle = 0;
        }
    }
    
    ...other code
}

我们只需要再传一个参数给 Hex 类,然后添加一个 update 方法,并通过设置一个角度值,不断的改变这个角度,随着角度的改变,六边形的旋转角度 this.deg 也在不断的变换,这样就能够让所有的六边形旋转了。实现的效果如下所示:

demo2.gif

我们还能通过修改最初定义的 this.radius 来改变六边形的大小,让我们将最初的 60 改变为 30,实现的效果如下图所示:

demo3.gif

怎么样,是不是很炫酷呢?我们还能添加一个 resize 事件,监听屏幕的改变,当屏幕大小发生改变时,重新绘制全屏的六边形,让我们一起来实现 resize 事件,代码如下:

class Hexagon {
    ...other code
    
    resize() {
        addEventListener('resize', () => {
            this.canvas.width = innerWidth;
            this.canvas.height = innerHeight;
            this.hexs.length = 0;
            this.formatData();
        });
    }
}

这里因为要监听 windowresize,因此使用到了 addEventListener 方法,而我们一开始定义的 init 方法中是在不断的向 this.hexs 中添加实例化后的 Hex 类,因此也需要修改一下 init 方法,并且在每次执行 resize 的时候,需要将前面创建的所有六边形都清除掉,我们直接让 this.hexs.length 等于 0 即可快速的清空数组,最终实现的完整代码可以在这里进行查看:

总结

我们通过实现一个六边形学习了如何在 canvas 中画出一个静态的六边形图案,并且还复习了前一节中学习的弧度和角度的互相转换,以及三角函数相关的知识;其次我们继续学习了如何实现全屏的静态六边形。我们知道如何绘制出一个六边形,其它的六边形只需要根据最初绘制的六边形找到它旁边的点就可以绘制出来了;最后我们还学习了如何让一个静态的图像动起来,通过 requestAnimationFrame 方法执行一个动画来执行一个动画,通过不断改变图像的坐标值让它动起来。

当然实现这个效果也是参考了小破站的视频,如果大家有更好的实现方法,可以在评论区进行讨论。

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

往期回顾

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

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

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

参考内容

canvas 绘制多边形