目录
贝塞尔曲线简介
由于用计算机画图大部分时间是操作鼠标来掌握线条的路径,与手绘的感觉和效果有很大的差别。即使是一位精明的画师能轻松绘出各种图形,拿到鼠标想随心所欲的画图也不是一件容易的事。这一点是计算机万万不能代替手工的工作,所以人们只能颇感无奈。使用贝塞尔工具画图很大程度上弥补了这一缺憾。贝塞尔曲线是计算机图形图像造型的基本工具,是图形造型运用得最多的基本线条之一。它通过控制曲线上的四个点(起始点、终止点以及两个相互分离的中间点)来创造、编辑图形。
除此之外,贝塞尔曲线还经常用来做动画,让动画过渡更平滑。本文则记录如何使用贝塞尔曲线定制平滑的动画效果,并使用C++编写了cmd动画和窗口动画示例代码。
一阶贝塞尔
设定图中运动的点为Pt
,t
为运动时间,t∈(0,1)
,可得如下公式:
二阶贝塞尔
在二阶贝塞尔曲线中,已知三点恒定(P0
,P1
,P2
),设定在P0
P1
中的点为Pa
,在P1
P2
中的点为Pb
,Pt
在Pa
Pb
上的点,这三点都在相同时间t内做匀速运动。
由公式(1)可知
将公式(2)(3)代入公式(4)中,可得
三阶贝塞尔
同理,根据以上的推导过程可得
由此可以推导
N阶贝塞尔曲线
四阶贝塞尔曲线:
五阶贝塞尔曲线:
N阶贝塞尔曲线公式:
贝塞尔曲线在动画中的应用
- 贝赛尔曲线广泛应用于绘图软件中,例如Adobe PhotoShop、Adobe Flash。
- Android可以通过自定义的view来实现贝塞尔曲线
- ios则可以使用UIBezierPath类来生成贝塞尔曲线
- 前端,canvas bezierCurveTo,css animation-timing-function: cubic-bezier(x,x,x,x}都有关于贝赛尔曲线的一些应用。
贝塞尔曲线在动画中的应用一般是三阶贝塞尔曲线,通过两个控制点来描述一般的动画曲线。通常以动画完成度为y
轴,时间为x
轴,然后将时间带入动画曲线求得对应的动画完成度
。
但是上述公式描述的是点与点关系,想要分解为x
,y
坐标的关系,则需要继续推导,以三阶为例:
想要直观的感受曲线的效果可以前往: cubic-bezier
得到x
与y
坐标关系后即可写代码进行实践了。
实践
求曲线散点坐标
由上面推导的曲线点的坐标和时间的关系可得:设曲线起点为(0
,0
),终点为(1
,1
),则t
时刻点的位置仅与两个控制点P1
P2
有关。先定义表示一个点的结构体:
struct PointF
{
PointF() : x(0), y(0) {
}
PointF(double x, double y) : x(x), y(y) {
}
double x;
double y;
};
然后传入两个点的坐标进行初始化
void init(double x1, double y1, double x2, double y2)
{
pc.x = 3.0 * x1;
pb.x = 3.0 * (x2 - x1) - pc.x;
pa.x = 1.0 - pc.x - pb.x;
pc.y = 3.0 * y1;
pb.y = 3.0 * (y2 - y1) - pc.y;
pa.y = 1.0 - pc.y - pb.y;
}
计算t
时刻的x
和y
坐标
double calcX(double t)
{
return ((pa.x * t + pb.x) * t + pc.x) * t;
}
double calcY(double t)
{
return ((pa.y * t + pb.y) * t + pc.y) * t;
}
根据给定的采样计算出[0,1]
时刻的n个曲线上的点坐标,即可绘制出曲线图。
for (int i = 0; i < size; ++i) {
// size即为采样个数,然后计算出对应采样时刻的曲线坐标
sample_[i] = PointF(calcX(i * 1.0 / size), calcY(i * 1.0 / size));
}
得到曲线坐标后可绘制曲线图看下:
其中蓝色的曲线是贝塞尔曲线,绿色和红色的曲线分别表示贝塞尔曲线上的y
坐标和x
坐标与t
的关系,即y
随时间先慢,再快,最后慢。x
随时间先快,再变慢,最后变快。
将曲线应用到动画
得到曲线散点坐标后,该怎么将其应用到动画呢?
因为我们已经设了x坐标在[0,1]
之间,而动画一般就是分为动画完成度和时间的关系,而我们设动画的时间也在[0,1]
,那么就可以给定动画的时刻t,然后通过曲线散点坐标求得对应的动画完成度。
即通过x
坐标求y
坐标,因为我们只有散点坐标,时刻t不一定跟已有点的x
坐标相同,因此需要找到最接近的时刻t
的两个点进行插值,即可求得近似的y
坐标,也即动画完成度。
废话不多说,直接上代码,使用二分法查找最近的两点并插值求y
:
double GetYAtX(double x)
{
int head = 0;
int tail = size - 1;
int center;
while (head <= tail) {
center = (head + tail) / 2;
if (sample_[center].x < x) {
head = center + 1;
} else if (sample_[center].x > x) {
tail = center - 1;
} else {
break;
}
}
if (head < size - 1) {
double x0 = sample_[head].x;
double x1 = sample_[head + 1].x;
double y0 = sample_[head].y;
double y1 = sample_[head + 1].y;
return (x - x0) /