canvas绘制玫瑰曲线
玫瑰曲线是指以一点为中心均匀分布花瓣的图形,是数学之美的完美体现 要想在程序中画出玫瑰曲线,这里有几个基础概念要先了解
1、玫瑰曲线表达式
x = r * Math.sin(n * angle) * Math.cos(angle)
y = r * Math.sin(n * angle) * Math.sin(angle)
1
2
2
表达式中存在两个常量r和n,r代表花瓣分布半径、n代表花瓣系数需为正数,angle代表当前曲线角度
2、闭合周期 闭合周期指绘制完整图形需要的角度,例如圆的闭合周期是2*PI,玫瑰曲线的闭合周期与它的系数n相关,具体如下
- n为奇数:Math.PI
- n为偶数:Math.PI * 2
- n为分数L/M(L和M不能同时为偶数,需为最简分数):
- L和M同时为奇数时闭合周期: Math.PI * M
- L和M存在偶数时闭合周期: Math.PI * M * 2 有了以上两个为基础,我们就可以在程序中使用canvas来绘制玫瑰曲线了
# 第一步:获取x、y坐标
const getX = (t: number) => { //获取玫瑰线的X坐标
return 100 * Math.sin(L.value/M.value*t)*Math.cos(t);
}
const getY = (t: number) => { //获取玫瑰线的Y坐标
return 100 * Math.sin(L.value/M.value*t)*Math.sin(t);
}
1
2
3
4
5
6
7
2
3
4
5
6
7
# 第二步:计算闭合周期
根据上面闭合周期规则计算完整玫瑰曲线的闭合周期,整数情况下默认分母为1,代码如下:
const getFactor = () => {
let lmPurify = purify(L.value, M.value)
console.log(lmPurify)
if (lmPurify[1] == 1) {
// 正整数
if (lmPurify[0] % 2 == 0) {
// 偶数
period.value = Math.PI * 2
} else {
// 奇数
period.value = Math.PI
}
return lmPurify[0]
} else {
if (lmPurify[0] % 2 == 0 || lmPurify[1] % 2 == 0) {
//
period.value = Math.PI * lmPurify[1] * 2
} else {
period.value = Math.PI * lmPurify[1]
}
return lmPurify[0] + '/' + lmPurify[1]
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
*注意:*计算闭合周期前需要将分子分母最简化,所以这里首先执行了purify函数
/* 最简分数 */
const purify = (l: number, m: number) => {
let x, y;
let son = l,mother = m;
let j = 2;
while (j <= son && j <= mother) {
x = son / j;
y = mother / j;
++j;
if ((x + '').indexOf('.') == -1 && (y + '').indexOf('.') == -1) {
son = x;
mother = y;
j = 2;
}
}
return [son, mother];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 第三步:使用canvas绘制
- 绘制前调用
getFactor计算闭合周期 - 设定每次移动举例
Math.PI/180(越小越清晰)并清空画布、移动到初始位置 - 判断已绘制的弧度是否达到闭合周期
period,达到则停止绘制,未到达到则重复步骤
const draw = () => {
getFactor()
let drawing = document.getElementById("drawing") as HTMLCanvasElement; //获取canvas元素
drawing.width = 500; //设置画布大小
drawing.height = 500;
if (drawing.getContext){ //获取绘图上下文
let context = drawing.getContext("2d"),
radian = 0, //设置初始弧度
radian_add = Math.PI/180; //设置弧度增量
if (context != null) {
context.clearRect(0, 0, 500, 500);
context.beginPath(); //开始绘图
context.translate(250,250); //设置绘图原点
context.moveTo(getX(radian),getY(radian)); //移动绘图游标至原点
while(radian <= period.value){ //每增加一次弧度,绘制一条线
radian += radian_add;
let X = getX(radian);
let Y = getY(radian);
context.lineTo(X,Y);
}
context.strokeStyle = "red"; //设置描边样式
context.stroke(); //对路径描边
}
}
}
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
效果如下:

# 优化:使用计时器实现动态绘制
修改上面绘制顺序,每移动一步就绘制颜色并延迟下一步绘制,代码如下:
const drawAnim = () => {
getFactor()
let drawing = document.getElementById("drawing") as HTMLCanvasElement; //获取canvas元素
drawing.width = 500; //设置画布大小
drawing.height = 500;
if (drawing.getContext){ //获取绘图上下文
let context = drawing.getContext("2d"),
radian = 0, //设置初始弧度
radian_add = Math.PI/360; //设置弧度增量
if (context != null) {
context.beginPath(); //开始绘图
context.translate(250 + getX(radian),250 + getY(radian)); //设置绘图原点
context.strokeStyle = "red"; //设置描边样式
let timer: number;
let lineFunc = () => {
radian += radian_add;
let X = getX(radian);
let Y = getY(radian);
context?.lineTo(X,Y);
context?.stroke(); //对路径描边
if (radian > period.value) {
clearInterval(timer)
}
}
timer = setInterval(lineFunc, 5)
}
}
}
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
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

上次更新: 2025/09/05, 8:09:00