之前刷视频看见一个有意思的东西,就是用两根转速不同的线一直旋转画图,我见有点意思而且画出来挺好看的,所以就自己写了一个。
原理
其实我也不太懂原理,大概就是两个杆画圆,设中间的杆旋转的速率为1,外杆旋转的速率为k,也就是中间的圆转了1圈的时候外面的圆转了k圈。
如果两个杆在某一时间转的圈数能够整除了,那就会开始循环,视频中用的是$\pi$,所以每次在差一点点准备连上的时候又错过了然后继续画圆。
Python版
最早写的版本,用来测试的,用python是因为有简单易用的图形库1
2
3
4
5
6
7
8
9
10
11
12
13import turtle as t
import numpy as np
d=0.01
time=0
t.penup()
t.goto(300,0)
t.pendown()
while True:
pos=(150*(np.cos(time)+np.cos(np.e*time)),
150*(np.sin(time)+np.sin(np.e*time)))
t.goto(pos)
time+=d
pass
非常简单就能有不错的效果,然后来看看代码。
学过高中数学我们知道,一个点的坐标可以表示为$(r\cos(t),r\sin(t))$,t为时间,r为半径,这样就是一个绕着原点旋转的点了。
第二个圆的圆心是第一个圆的轨迹,先用同样的方法表示第二个杆的位置$(r\cos(kt),r\sin(kt))$这里乘个常数k,然后加上第一个杆的位置,就是我们最终需要的画的轨迹的位置了。
然后我用一个变量d表示最小时间单位,time为总时间,然后就一直循环画圆,说实话这样画挺慢的。
三角函数打表优化版
最开始我认为耗时的部分有两个
- 反复计算三角函数值耗时多
- 时间分段设置小了,循环次数多
对于第一点可以把三角函数计算优化掉,即把三角函数值打表,然后按照时间查表。
第二点就需要计算了,设置最小时间也是某种化曲为直的思想或者说积分的思想,用小段的直线表示圆弧,所以最小时间设置的越小越精确,但是需要的时间越多。但是这只是理论上的,因为实际绘制的时候会受到图片分辨率限制,不可能无限精确,所以为了减少在同一个像素上面重复渲染,可以根据分辨率计算出一个合适的分段数。
结合以上两点,我们就以弧度来划分并打表。像素是方的,圆是圆的,但是我们可以做一些近似的假设,比如计算出大圆的周长,以周长的长度来划分弧度,这样就近似于一个弧度分段一个像素了,虽然还是很不准确。
但是假设大圆的半径是300,$2300\pi\approx 1900$,如果sin和cos的都要的话就有3800个分段了,虽然这个数据量也不大,但是也存在压缩的方法,总所周知sin是周期函数,其实只用计算前四分之一周期就够了,sin
和cos的都可以用那四分之一经过变换得到。
所以得到改进版代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27import turtle as t
import numpy as np
N=int(np.ceil(np.pi*150))
table=[np.sin(i*np.pi/(2*N)) for i in range(N)]
def sin_table(i):
match np.floor(i/N)%4:
case 0:return table[i%N]
case 1:return table[N-1-i%N]
case 2:return -table[i%N]
case 3:return -table[N-1-i%N]
def cos_table(i):
match np.floor(i/N)%4:
case 0:return table[N-1-i%N]
case 1:return -table[i%N]
case 2:return -table[N-1-i%N]
case 3:return table[i%N]
time=0
t.penup()
t.goto(300,0)
t.pendown()
while True:
pos=(150*(cos_table(time)+cos_table(round(np.e*time))),
150*(sin_table(time)+sin_table(round(np.e*time))))
t.goto(pos)
time+=1
pass
移植到js
之后我通过现在流行的DeepSeek把这段代码转换成了js代码,并让AI把它封装成了一个类,然后自己修改了一下,最终版本1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78class TrigTable {
constructor(canvas) {
this.canvas = canvas; // 直接传入 canvas 对象
this.ctx = this.canvas.getContext('2d');
this.width = canvas.width;
this.height = canvas.height;
this.R = Math.floor(Math.min(this.width, this.height) / 4);
this.N = Math.ceil(Math.PI * this.R);
this.table = (()=>{
const table = new Array(this.N);
for (let i = 0; i < this.N; i++) {
table[i] = Math.sin((i * Math.PI) / (this.N * 2));
}
return table;
})();
this.count = 0;
this.centerX = this.width / 2;
this.centerY = this.height / 2;
// Initialize canvas
this.canvas.width = this.width;
this.canvas.height = this.height;
this.ctx.beginPath();
this.ctx.strokeStyle = "#888888";
this.ctx.moveTo(this.centerX+2*this.R, this.height / 2); // Start from center
}
// Sine table function
sin_table(i) {
const index = Math.floor(i / this.N) % 4;
switch (index) {
case 0:
return this.table[i % this.N];
case 1:
return this.table[this.N - 1 - (i % this.N)];
case 2:
return -this.table[i % this.N];
case 3:
return -this.table[this.N - 1 - (i % this.N)];
}
}
// Cosine table function
cos_table(i) {
const index = Math.floor(i / this.N) % 4;
switch (index) {
case 0:
return this.table[this.N - 1 - (i % this.N)];
case 1:
return -this.table[i % this.N];
case 2:
return -this.table[this.N - 1 - (i % this.N)];
case 3:
return this.table[i % this.N];
}
}
// Draw function
draw() {
// Draw line
for(let i=0;i!=8;i++){
const x = this.R * (this.cos_table(this.count+i) + this.cos_table(Math.round(Math.E * (this.count+i))));
const y = this.R * (this.sin_table(this.count+i) + this.sin_table(Math.round(Math.E * (this.count+i))));
this.ctx.lineTo(this.centerX + x, this.centerY + y);
}
// Increment count
this.count+=8;
this.ctx.stroke();
// Request next frame
requestAnimationFrame(() => this.draw());
}
// Start drawing
start() {
this.draw();
}
}
慢速问题的解决
一直到js之后我还是觉得速度太慢了,之后我发现,慢的地方不是计算,而是画线,所以正确的加速方式是一帧用lineTo
画多段线然后一次性stroke
和渲染,也就是代码中的这一段
1 | for(let i=0;i!=8;i++){ |
最后,看到这篇文章的,打开我博客主页应该已经能顾看到这个画圆的效果了。