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

1,935 阅读8分钟

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

前言

大家好,我是爱吃鱼的桶哥Z,最近在小破站看了很多炫酷的 canvas 效果,发现了一个原理,那就是不管简单的或是复杂的效果,都需要用到数学中的三角函数,深感当年没有好好学习而导致现在很多知识点都遗忘了。每当想要实现一个炫酷的效果时都力不从心,因此今天咱们就通过实现一个环绕圆心的正弦曲线动画来回顾一下三角函数相关的知识点,并为后续实现更多的效果打下基础。

弧度与角度之间的互相转换

先来看一下弧度相关的介绍,如下:

弧度的定义:半径为1的圆,不同的角度所对应的弧形的长度,根据圆周率的定义,一个完整的圆的周长是2PI半径,由于半径为1,所以弧度的取值范围就是0~2*PI。

再来看一下角度相关的介绍,如下:

角度值的取值范围是-0° 到360°,那么每一度对应的弧度值就是2*PI/360,简化一下就是PI/180

因此角度弧度的相互转化方法如下:

let radian //弧度

let angle //角度

radian = angle * Math.PI / 180

angle = radian / Math.PI * 180

有了上面的这些知识点,接下来就来实现一个正弦曲线吧!

正弦曲线

首先咱们先来实现一个正弦曲线,然后再逐步来实现最终的整个效果。

老规矩,还是先来编写一下 htmlcss,也都很简单,具体代码如下:

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

html 中只有一个 canvas 标签;css 也很简单,如下:

*{margin: 0; padding: 0;}
body {
    display: flex;
    justify-content: center;
    align-items: center;
    width: 100%;
    height: 100vh;
    overflow: hidden;
}

就这几行样式,我想大家应该都已经很熟悉了,接下来 JS 才是我们的重点,这里依旧采用面向对象的方式来编写代码,这样让我们的代码看起来更有条理性,首先还是相关的基础准备代码,如下:

class SineAnimate {
    constructor() {
        /** @type {HTMLCanvasElement} */
        this.canvas = document.getElementById('canvas');
        this.ctx = this.canvas.getContext('2d');
        this.canvas.width = 800;
        this.canvas.height = 800;
        this.dots = [];
    }
}

前面几行就是基本的获取 canvas 以及 ctx,并设置 canvas 的宽和高,最后一行主要是用于存放我们后续生成的点,这里用一个个小点来画出正弦效果。

接下来我们需要先准备一下画点的方法,我们重新定义一个新的类,方便我们后续的使用,具体代码如下:

class Dots {
    constructor(x, canvas, ctx) {
        this.x = x;
        this.y = 0;
        this.canvas = canvas;
        this.ctx = ctx;
    }
    update() {
        this.y = 20 * Math.sin(this.x * Math.PI / 36) + this.canvas.height / 2;
    }
    draw() {
        this.ctx.beginPath();
        this.ctx.arc(this.x, this.y, 1, 0, Math.PI * 2, 0);
        this.ctx.fill();
    }
}

Dots 这个类中,我们只需要关注 update() 方法中的 this.y 的设置。在这个方法里面,我们使用到了 Math.sin() ,这个方法会返回一个 -11 之间的数值,表示给定角度(单位:弧度)的正弦值,然后我们还使用到了 Math.PI,这个方法是获取的圆周率,读过书的童鞋们都知道圆周率是什么,这里就不做过多的赘述了。在 JS 中我们要获取一个夹角的弧度就需要用到 Math.PI 方法,通过一个角度 x 乘以 Math.PI 并除以一个角度 36,最终就能够我们需要的正弦角度。

有了上述的 Dots 类后,我们就需要在 SineAnimate 类中添加一个初始化方法,并将整个正弦画在页面中,相关的代码如下:

class SineAnimate {
    constructor() {
        //...other code
        
        this.init();
    }
    
    init() {
        for (let i = 0; i < this.canvas.width; i++) {
            this.dots.push(new Dots(i, this.canvas, this.ctx));
        }

        this.draw();
    }

    draw() {
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
        for (const key in this.dots) {
            let dot = this.dots[key];
            dot.update();
            dot.draw();
        }
    }
}

new SineAnimate();

SineAnimate 类中,定义了一个 init 方法,并通过当前 canvas 的宽度动态生成了 800 个点,然后还定义了一个 draw 方法,在这个方法中将前面生成的 800 个点渲染在页面中,最后不要忘记实例化 SineAnimate 类,最终实现的正弦效果如下图所示:

image.png

你知道为什么上面是 800 个点吗?因为我们在前面定义的 canvas 宽度为 800,然后在 init 方法中通过循环遍历的方式不断生成小点,最大值就是 canvas 的宽度,因此最终生成的小圆点有 800 个。

接着我们绘制一条辅助线,只需要添加一行代码即可,如下:

class SineAnimate {
    //...other code
    
    draw() {
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
        this.ctx.fillRect(0, this.canvas.height / 2, this.canvas.width, 1);
        for (const key in this.dots) {
            let dot = this.dots[key];
            dot.update();
            dot.draw();
        }
    }
}

SineAnimate 类的 draw 方法中通过 canvasfillRect 方法,绘制一条辅助线,实现出来的效果如下所示:

image.png

通过上面的示例,我们可以看出正弦曲线是由多个离参考线不同距离的点组成的,我们还可以通过 fillRect 方法实现点到直线的垂直线段,只需要修改 Dots 类中的 draw 方法即可,代码如下:

class Dots {
    //...other code
    
    draw() {
        //...other code
        this.ctx.fillRect(this.x, this.y, 1, this.canvas.height / 2 - this.y);
    }
}

Dots 类的 draw 方法中,通过 fillRect 方法来绘制点到垂直距离的线段,实现的效果如下所示:

image.png

有了上面的这些基础知识点,如果我们将距离改为沿圆轴排列,也就是用来定义圆的半径,那么是不是就可以实现一条环绕圆心的正弦曲线了?让我们一起来实现一下吧!

环绕圆心的正弦曲线

在上面咱们已经实现了一个基本的正弦曲线,接下来通过前面的描述,咱们就一起来改造一下前面的代码,让它能够环绕一个圆,首先要改造的就是 SineAnimate 类中的 init 方法,之前咱们生成的点是按 canvas 的宽度来,现在修改为 360 即可,因为一个圆就是 360 度,所以只需要改成 360 即可,相关修改代码如下:

class SineAnimate {
    ...other code
    
    init() {
        for (let i = 0; i < 360; i++) {
            this.dots.push(new Dots(i, this.canvas, this.ctx));
        }

        this.draw();
    }
}

修改完 SineAnimate 类,接下来就需要修改 Dots 类了,让我们一起来看相关的代码,如下:

class Dots {
    constructor(d, canvas, ctx) {
        this.d = d;
        this.x = 0;
        this.y = 0;
        this.r = 0;
        this.w = 0;
        this.canvas = canvas;
        this.ctx = ctx;
    }
    update() {
        this.r = this.w * Math.sin(this.d * Math.PI / 4) + 350;
        this.x = this.r * Math.cos(this.d * Math.PI / 180) + this.canvas.width / 2;
        this.y = this.r * Math.sin(this.d * Math.PI / 180) + this.canvas.height / 2;
    }
    draw() {
        this.ctx.beginPath();
        this.ctx.arc(this.x, this.y, 1, 0, Math.PI * 2, 0);
        this.ctx.fill();
    }
}

Dots 类中,重新定义了圆的半径和宽度,然后在 update 方法中,按照 360 度的角度来生成点,根据上面正弦曲线公式生成不同角度下的半径值,然后再根据最前面的知识点,通过圆的半径与角度得到最终确定的坐标点,最终实现了将所有的点连接成一个圆,如下图所示:

image.png

通过上面的计算可以得知,当正弦函数的振幅(this.w)为 0 时,就实现了如上完整的圆。只需要修改 this.w 也就是振幅,就可以看到正弦函数的轮廓,这里我们可以随意修改,例如将 this.w 修改为 10,实现的效果如下所示:

image.png

接着我们可以调整一下角度的间隙,直到所有的点组成一条连续的曲线,只修改要修改 SineAnimate 类中 init 方法的循环即可,修改代码如下:

class SineAnimate {
    ...other code
    
    init() {
        for (let i = 0; i < 360; i += 0.2) {
            this.dots.push(new Dots(i, this.canvas, this.ctx));
        }
    }
    
    ...other code
}

修改 init 中循环的最后一个参数,实现出来的效果如下所示:

image.png

同样的,这里我们也把这个圆的正弦参考线画出来,修改 SineAnimate 中的 draw 方法,代码如下:

class SineAnimate {
    ...other code
    
    draw() {
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
        this.ctx.beginPath();
        this.ctx.arc(this.canvas.width/2, this.canvas.height/2, 350, 0, Math.PI * 2, 0);
        this.ctx.stroke();
        for (const key in this.dots) {
            let dot = this.dots[key];
            dot.update();
            dot.draw();
        }
    }
}

跟前面的正弦辅助线一样的,这里只是画在圆上,实现的效果如下图所示:

image.png

到这里,咱们这个环绕圆心的正弦曲线已经有了一个大概的模样了,但是光静态的效果还不够,咱们要实现一个动态的环绕圆心的正弦曲线效果,看看怎么实现吧!

动态环绕圆心的正弦曲线

在前面咱们是直接在 SineAnimate 类的 init 方法中调用 draw 方法来进行绘制,这里我们通过 requestAnimationFrame 方法来实现动画的效果,它比 setInterval 方法要更好一些,前面介绍过,这里也不做赘述。

接下来只需要修改 Dots 类中的振幅值,让它不断的变换,就能让这个环绕圆心的正弦曲线先动起来,修改代码如下:

class Dots {
    constructor(d, canvas, ctx) {
        ...other code
        this.w = 10;
        this.s = 0.2;
        ...other code
    }
    update() {
        if (Math.abs(this.w) > 10) {
            this.s = -this.s;
        }
        this.w += this.s;
        ...other code
    }
}

通过 Math.abs 获取当前振幅的绝对值,然后不断的修改 this.w 的值,这样就能让正弦曲线动起来,并将圆的辅助线去掉,实现的效果如下所示:

demo1.gif

当然,除了上面修改振幅改变正弦曲线,咱们还可以通过修改圆的角度,让它转动起来,只需要修改 Dots 类即可,修改的代码如下:

class Dots {
    constructor(d, canvas, ctx) {
        ...other code
        this.w = 10;
        this.s = 0.2;
        this.deg = d;
        ...other code
    }
    update() {
        if (Math.abs(this.w) > 10) {
            this.s = -this.s;
        }
        this.w += this.s;
        this.r = this.w * Math.sin(this.d * Math.PI / 4) + 350;
        this.x = this.r * Math.cos(this.deg * Math.PI / 180) + this.canvas.width / 2;
        this.y = this.r * Math.sin(this.deg * Math.PI / 180) + this.canvas.height / 2;
        this.deg++;
        this.deg = this.deg % 360;
    }
}

当我们不断的修改当前的圆的角度时,它就会滚动起来了,效果如下所示:

demo2.gif

最后我们再来修饰一下这个效果,只需要修改 Dots 类中的正弦曲线的相关值,以及圆的半径,就能得到一个动态环绕圆心的正弦曲线,最终的实现和完整的代码可以在这里进行查看:

总结

写这篇文章花了三个小时,因为对于三角函数以及弧度和角度之间的知识都很模糊了,因此在查阅教程的情况下才完成了这个效果,虽然这个效果不是特别的惊艳,但是它其中包含的知识点,在后续我们制作 canvas 相关的其它效果是都是必不可少的知识点,因此这一节的内容真的值得大家好好的去学习一下。

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

往期回顾

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

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