为什么要讲贝塞尔曲线,实际上 Android 中很多效果都有用到贝塞尔曲线。
可以先对贝塞尔曲线有一个大概的认识。
历史
贝塞尔曲线的数学基础是早在 1912 年就广为人知的 伯恩斯坦多项式 。但直到 1959 年,当时就职于雪铁龙的法国数学家 Paul de Casteljau 才开始对它进行图形化应用的尝试,并提出了一种数值稳定的 de Casteljau 算法。然而贝塞尔曲线的得名,却是由于 1962 年另一位就职于雷诺的法国工程师 Pierre Bézier 的广泛宣传。他使用这种只需要很少的控制点就能够生成复杂平滑曲线的方法,来辅助汽车车体的工业设计。
正是因为控制简便却具有极强的描述能力,贝塞尔曲线在工业设计领域迅速得到了广泛的应用。不仅如此,在计算机图形学领域,尤其是矢量图形学,贝塞尔曲线也占有重要的地位。今天我们最常见的一些矢量绘图软件,如 Flash、Illustrator、CorelDraw 等,无一例外都提供了绘制贝塞尔曲线的功能。甚至像 Photoshop 这样的位图编辑软件,也把贝塞尔曲线作为仅有的矢量绘制工具(钢笔工具)包含其中。
贝塞尔曲线在 Web 开发领域同样占有一席之地。CSS3 新增了 transition-timing-function 属性,它的取值就可以设置为一个三次贝塞尔曲线方程。在此之前,也有不少 JavaScript 动画库使用贝塞尔曲线来实现美观逼真的缓动效果。
公式
线性贝塞尔曲线
给定点P0、P1,线性贝塞尔曲线只是一条两点之间的直线。这条线由下式给出:
二次方贝塞尔曲线
二次方贝塞尔曲线的路径由给定点P0、P1、P2的函数B(t)追踪:
三次方贝塞尔曲线
P0、P1、P2、P3 四个点在平面或在三维空间中定义了三次方贝塞尔曲线。曲线起始于 P0 走向 P1 ,并从 P2 的方向来到 P3 。一般不会经过 P1 或 P2 ;这两个点只是在那里提供方向资讯。 P0 和 P1 之间的间距,决定了曲线在转而趋进 P2 之前,走向 P1 方向的“长度有多长”。
曲线的参数形式为:
n阶贝塞尔曲线
n阶贝塞尔曲线可如下判断,给定点P0、p1、...、Pn,其贝塞尔曲线即
看公式还是有些抽象,接下来可以看一下图解,具像的了解一下什么是贝塞尔曲线:以二阶贝塞尔曲线和三阶贝塞尔曲线为例。
有一篇文章写的极好
贝塞尔曲线不仅能画直线,也能画曲线。即便是更复杂的曲线,控制点的增加也只是线性的。这一特点使其不光在工业设计领域大展拳脚,就连数学基础不好的人也可以比较容易地掌握,比如大多数平面美术设计师们。
简单来说,贝塞尔曲线就是将任意一条曲线转化为准确的数学公式。 Bezier 曲线是用一系列点来控制曲线状态的。
一些关键的名词
- 数据点:通常是指一条路径的起始点和终止点
- 控制点:控制点决定了一条路径的弯曲轨迹,根据控制点的个数,贝塞尔曲线被分为一阶贝塞尔曲线(0个控制点)、二阶贝塞尔曲线(1个控制点)、三阶贝塞尔曲线(2个控制点),以此类推。
在Android中的应用
在 Android 开发中,我们只考虑二阶贝塞尔曲线和三阶贝塞尔曲线, SDK 也是只提供了二阶和三阶的 API 调用。
当然,对于再高阶的贝塞尔曲线,通常可以拆分成多个低阶的贝塞尔曲线。
推荐一个好的贝塞尔曲线的动态演示,可以很直观的感受一下:
http://myst729.github.io/bezier-curve/
这里推荐 医生 的一片博文,已经非常全面的介绍了贝塞尔曲线在 Android 中的一些用法及实例,写的很好。
下面的内容都基于这篇文章。
二阶贝塞尔曲线在 Android 中的 API 为:
quadTo 是基于绝对坐标,而 rQuadTo 是基于相对坐标。
mPath.moveTo(mStartPointX, mStartPointY);
//二阶贝塞尔曲线
mPath.quadTo(mAuxiliaryX, mAuxiliaryY, mEndPointX, mEndPointY);
canvas.drawPath(mPath, mPaintBezier);
三阶贝塞尔曲线在 Android 中的 API 为:
这两个API在原理上也是可以互相转换的。
两个点的话涉及到多点触摸,有兴趣可以看看下面这篇文章。
cubicTo是基于绝对坐标,而rCubicTo是基于相对坐标。
mPath.moveTo(mStartPointX, mStartPointY);
// 三阶贝塞尔曲线
mPath.cubicTo(mAuxiliaryOneX, mAuxiliaryOneY, mAuxiliaryTwoX, mAuxiliaryTwoY, mEndPointX, mEndPointY);
canvas.drawPath(mPath, mPaintBezier);
三阶贝塞尔曲线其实也没什么,就是多了一个控制点,记录下他的位置就可以了。
圆滑绘图
当在屏幕上绘制路径的时候,我们通常会通过 Path.lineTo 将各个触点连接起来,但是这样肯定会很生硬。因为是通过直线来连接的。如果通过二阶贝塞尔曲线,就会圆滑很多。不会出现太多的生硬连接。
if (dx >= offset || dy >= offset) {
// 贝塞尔曲线的控制点为起点和终点的中点
float cX = (x1 + preX) / 2;
float cY = (y1 + preY) / 2;
mPath.quadTo(preX, preY, cX, cY);
// mPath.lineTo(x1, y1);
mX = x1;
mY = y1;
}
通过纪录 Move 过程中点位置的变化,然后传入 quadTo 中的参数,就可以实现圆滑的绘图。
根据公式我们可以模仿着写一个工具类:
public class BezierUtil {
/**
* B(t) = (1 - t)^2 * P0 + 2t * (1 - t) * P1 + t^2 * P2, t ∈ [0,1]
*
* @param t 曲线长度比例
* @param p0 起始点
* @param p1 控制点
* @param p2 终止点
* @return t对应的点
*/
public static PointF CalculateBezierPointForQuadratic(float t, PointF p0, PointF p1, PointF p2) {
PointF point = new PointF();
float temp = 1 - t;
point.x = temp * temp * p0.x + 2 * t * temp * p1.x + t * t * p2.x;
point.y = temp * temp * p0.y + 2 * t * temp * p1.y + t * t * p2.y;
return point;
}
/**
* B(t) = P0 * (1-t)^3 + 3 * P1 * t * (1-t)^2 + 3 * P2 * t^2 * (1-t) + P3 * t^3, t ∈ [0,1]
*
* @param t 曲线长度比例
* @param p0 起始点
* @param p1 控制点1
* @param p2 控制点2
* @param p3 终止点
* @return t对应的点
*/
public static PointF CalculateBezierPointForCubic(float t, PointF p0, PointF p1, PointF p2, PointF p3) {
PointF pointF = new PointF();
float temp = 1- t;
pointF.x = p0.x * temp * temp * temp + 3 * p1.x * t * temp * temp + 3 * p2.x * t * t * temp + p3.x * t * t * t;
pointF.y = p0.y * temp * temp * temp + 3 * p1.y * t * temp * temp + 3 * p2.y * t * t * temp + p3.y * t * t * t;
return pointF;
}
}
推荐一个关于贝塞尔曲线的开源项目:
https://github.com/venshine/BezierMaker ( 通过 de Casteljau 算法绘制贝塞尔曲线,并计算它的切线,实现 1-7 阶贝塞尔曲线的形成动画 )