canvas 二次贝塞尔曲线 匀速 均匀的点

整理贝塞尔曲线的各种公式,以及解决如何来获得一个二次贝塞尔曲线上的匀速的点的问题。

线性贝塞尔曲线

给定点$P_0$、$P_1$,线性贝塞尔曲线只是一条两点之间的直线。公式为:

$ B(t) = P_0 + (P_1 - P0)t = (1 - t)P_0 + tP_1, t ∈ [0, 1]$

二次贝塞尔曲线

二次贝塞尔曲线的路径由给定的点$P_0$、$P_1$、$P_2$的函数$B(t)$。公式:

$B(t) = (1 - t)^2P_0 + 2t(1 - t)P_1 + t^2P_2, t ∈ [0, 1]$

三次贝塞尔曲线

$P_0$、 $P_1$、 $P_2$、 $P_3$四个点在平面或三维空间中定义了三次贝塞尔曲线,曲线起始于$P_0$走向$P_1$,并从$P_2$的方向来到$P_3$。一般不会经过$P_1$或$P_2$,这两个点只是提供方向。公式:

$B(t) = P_0(1 - t)^3 + 3P_1t(1 - t)^2 + 3P_2t^2(1 - t) + P_3t^3, t ∈ [0, 1]$

为什么要了解贝塞尔曲线的方程?因为虽然在canvas里面画贝塞尔曲线的时候是有曲线函数的,但是当我们要用到贝塞尔曲线轨迹的时候就没法子了,我们还是得求贝塞尔曲线上的点,用点来做轨迹。

匀速贝塞尔曲线运动

通过在canvas上画图我们可以看到,二次贝塞尔曲线的特性是两端比较稀疏,中间比较密集,而且曲线拐弯角度比较大的话中间更密集,两端更稀疏,这样在做轨迹运动的时候,表现为两端快,中间慢。

我们需要的是在曲线上做匀速运动,有什么法子呢?

搜了一篇文章:二次贝塞尔曲线长度,具体如何推导出来的公式我看不太懂,但是我根据里面的C++的代码以JavaScript的代码写出来了,而且实际应用了发现效果甚好😳。算了,就这样吧,直接看代码。

首先是点的定义:

1
2
3
4
5
6
7
8
export default class Point {
public x: number;
public y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}

然后定义一个贝塞尔曲线的类:

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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
import Point from './point';

export default class BezierLine {
middle: Point;
start: Point;
end: Point;
length: number;
private A: number;
private B: number;
private C: number;
constructor(start: Point, end: Point) {
this.start = start;
this.end = end;
this.getMiddlePoint();
}

/**
* 获得平方贝塞尔曲线的控制点
*/
private getMiddlePoint() {
// 平方贝塞尔曲线的点
this.middle = new Point(
(this.start.x + this.end.x) / 2 - (this.start.y - this.end.y) * 0.4,
(this.start.y + this.end.y) / 2 - (this.end.x - this.start.x) * 0.4,
);
// 直接计算好多项式,留后用
const ax = this.start.x - 2 * this.middle.x + this.end.x;
const ay = this.start.y - 2 * this.middle.y + this.end.y;
const bx = 2 * this.middle.x - 2 * this.start.x;
const by = 2 * this.middle.y - 2 * this.start.y;
this.A = 4 * (ax * ax + ay * ay);
this.B = 4 * (ax * bx + ay * by);
this.C = bx * bx + by * by;
}

/**
* 获得贝塞尔曲线上的点
* B(t) = (1 - t)^2 * P0 + 2t * (1 - t) * P1 + t^2 * P2, t ∈ [0,1]
* @param t 曲线长度比例
*/
public calculateBezierPointForQuadratic(t): Point {
const temp = 1 - t;
return new Point(
temp * temp * this.start.x + 2 * t * temp * this.middle.x + t * t * this.end.x,
temp * temp * this.start.y + 2 * t * temp * this.middle.y + t * t * this.end.y
);
}

public getLength(t: number): number {

const temp1 = Math.sqrt(this.C + t * (this.B + this.A * t));

const temp2 = (2 * this.A * t * temp1 + this.B * (temp1 - Math.sqrt(this.C)));

const temp3 = Math.log(this.B + 2 * Math.sqrt(this.A) * Math.sqrt(this.C));

const temp4 = Math.log(this.B + 2 * this.A * t + 2 * Math.sqrt(this.A) * temp1);

const temp5 = 2 * Math.sqrt(this.A) * temp2;

const temp6 = (this.B * this.B - 4 * this.A * this.C) * (temp3 - temp4);

return (temp5 + temp6) / (8 * Math.pow(this.A, 1.5));

}

/**
* 根据反函数求得对应的t值
* @param t
* @param l
*/
public invertL(t, l) {

let t1 = t, t2;

do {
t2 = t1 - (this.getLength(t1) - l) / this.s(t1);

if (Math.abs(t1 - t2) < 0.000001) {
break;
}
t1 = t2;

} while (true) ;

return t2;
}

private s(t) {
return Math.sqrt(this.A * t * t + this.B * t + this.C);
}
}

通过场面的类来创建贝塞尔曲线并获取曲线上的均匀的点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const begin: Point = new Point(20, 280);
const end: Point = new Point(680, 280);
const line = new BezierLine(begin, end);
// 贝塞尔曲线的长度
const len = Math.floor(line.getLength(1));
// 分割份数为曲线的长度
for (let j = 0; j < 100; j += 1) {
let t = j / 100;
const l = t * len;
t = line.invertL(t, l);

const point = line.calculateBezierPointForQuadratic(t);
const color = 'rgb(' + Math.floor(Math.random() * 255) + ',' + Math.floor(Math.random() * 255) + ',' + Math.floor(Math.random() * 255) + ')';
Canvas.drawCycle(this.contextFour, point, 8, color);
}

可以看下对比的图片:

贝塞尔曲线和匀速贝塞尔曲线的差别


源自于一个页面的特效,需要画线,按照贝塞尔曲线的路径来完成动画,本身是直接用贝塞尔曲线上的点构造的路径,导致点密集的地方会显得很高亮,使用这个匀速贝塞尔曲线上的点就完全避免了这个问题。

还是得好好学习下数学啊。

参考网站:二次贝塞尔曲线长度

码字辛苦,打赏个咖啡☕️可好?💘