0%

博客新背景

之前刷视频看见一个有意思的东西,就是用两根转速不同的线一直旋转画图,我见有点意思而且画出来挺好看的,所以就自己写了一个。

原理

其实我也不太懂原理,大概就是两个杆画圆,设中间的杆旋转的速率为1,外杆旋转的速率为k,也就是中间的圆转了1圈的时候外面的圆转了k圈。

如果两个杆在某一时间转的圈数能够整除了,那就会开始循环,视频中用的是$\pi$,所以每次在差一点点准备连上的时候又错过了然后继续画圆。

Python版

最早写的版本,用来测试的,用python是因为有简单易用的图形库

1
2
3
4
5
6
7
8
9
10
11
12
13
import 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为总时间,然后就一直循环画圆,说实话这样画挺慢的。

三角函数打表优化版

最开始我认为耗时的部分有两个

  1. 反复计算三角函数值耗时多
  2. 时间分段设置小了,循环次数多

对于第一点可以把三角函数计算优化掉,即把三角函数值打表,然后按照时间查表。

第二点就需要计算了,设置最小时间也是某种化曲为直的思想或者说积分的思想,用小段的直线表示圆弧,所以最小时间设置的越小越精确,但是需要的时间越多。但是这只是理论上的,因为实际绘制的时候会受到图片分辨率限制,不可能无限精确,所以为了减少在同一个像素上面重复渲染,可以根据分辨率计算出一个合适的分段数。

结合以上两点,我们就以弧度来划分并打表。像素是方的,圆是圆的,但是我们可以做一些近似的假设,比如计算出大圆的周长,以周长的长度来划分弧度,这样就近似于一个弧度分段一个像素了,虽然还是很不准确。

但是假设大圆的半径是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
27
import 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
78
class 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
2
3
4
5
6
7
8
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();

最后,看到这篇文章的,打开我博客主页应该已经能顾看到这个画圆的效果了。