深入理解贝塞尔曲线

canvas中贝塞尔曲线函数

在《深入理解路径(Path)》中,有提到了贝塞尔曲线的相关函数,如下所示:

  • context.quadraticCurveTo(cpx, cpy, x, y)
    添加指定点(x,y)到当前子路径,该点和前一个点通过指定控制点(cpx, cpy)控制的二次贝塞尔曲线连接起来。
  • context.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)
    添加指定点(x,y)到当前子路径,该点和前一个点通过指定控制点(cp1x, cp1y)和(cp2x, cp2y)控制的三次贝塞尔曲线连接起来。

下面的示例绘制一条二次贝赛尔曲线:

ctx.save();
ctx.beginPath();
ctx.moveTo(100, 100);
ctx.quadraticCurveTo(200, 100, 200, 200);
ctx.stroke();

arc(100,100);
arc(200, 100);
arc(200, 200);
ctx.restore();

首先画笔移动到点(100,100), 然后调用二次贝赛尔曲线路径方法:
ctx.quadraticCurveTo(200, 100, 200, 200);
控制点为(200,100),终点为(200,200)。

后面的arc方法是绘制一个小圆形,示例中在曲线的端点和控制点处绘制了小圆,用于示意端点和控制点的位置。
绘制的效果如下:
二次贝塞尔曲线

二次贝塞尔曲线

下面的示例绘制一条三次贝赛尔曲线:

ctx.save();
ctx.beginPath();
ctx.moveTo(100, 100);
ctx.bezierCurveTo(200, 100, 200, 200,300,200);
ctx.stroke();

arc(100, 100);
arc(200, 100);
arc(200, 200);
arc(300, 200);
ctx.restore();

首先画笔移动到点(100,100), 然后调用三次贝赛尔曲线路径方法:
ctx.bezierCurveTo(200, 100, 200, 200,300,200);
控制点为(200,100)和(200,200),终点为(300,200)。
后面的arc方法是绘制一个小圆形,示例中在曲线的端点和控制点处绘制了小圆,用于示意端点和控制点的位置。
绘制的效果如下:
三次贝塞尔曲线

三次贝塞尔曲线

二次贝赛尔曲线

从前面的示例中,可以看出二次贝赛尔曲线包含一个控制点和两个端点。

提示:二次贝塞尔曲线需要两个点。第一个点是用于二次贝塞尔计算中的控制点,第二个点是曲线的结束点。曲线的开始点是当前路径中最后一个点。如果路径不存在,那么请使用 beginPath() 和 moveTo() 方法来指定开始点。

下图标注出了二次贝塞尔曲线的端点和控制点:
二次贝塞尔曲线-标注点

二次贝塞尔曲线-标注点

三次贝赛尔曲线

从前面的示例中,可以看出二次贝赛尔曲线包含两个控制点和两个端点。

提示:三次贝塞尔曲线需要三个点。前两个点是用于三次贝塞尔计算中的控制点,第三个点是曲线的结束点。曲线的开始点是当前路径中最后一个点。如果路径不存在,那么请使用 beginPath() 和 moveTo() 方法来定义开始点。

下图标注出了三次次贝塞尔曲线的端点和控制点:
三次贝塞尔曲线-标注点

三次贝塞尔曲线-标注点

曲线动画

本节源代码参考:animate.html和animate.js与iter.html和iter.js

动画需要计算曲线的任意中间点位,对于直线而言,任意点位都是比较容易计算的,用插值算法很方便就能计算出来。而曲线就不是那么容易了,要计算曲线的中间点位,需要首先理解曲线的原理。此处首先说明贝赛尔曲线的数学方程。

贝塞尔曲线的方程

贝塞尔曲线(Bezier curve)是计算机图形学中相当重要的参数曲线,贝塞尔曲线于1962年,由法国工程师皮埃尔·贝塞尔(Pierre Bézier)所发表,他第一个研究了这种矢量绘制曲线的方法,并运用贝塞尔曲线来为汽车的主体进行设计。

贝塞尔曲线(Bezier curve)通过一个方程来描述一条曲线,根据方程的最高阶数,又分为线性贝赛尔曲线,二次贝塞尔曲线、三次贝塞尔曲线和更高阶的贝塞尔曲线。

贝塞尔曲线的定义:起始点、终止点(也称锚点)、控制点。通过调整控制点,贝塞尔曲线的形状会发生变化。

一次贝塞尔曲线方程

一次贝塞尔曲线,其实就是线段。假设其起始点P0,终止点为P1,线段没有控制点。一次贝塞尔曲线的表达方程:
B(t) = (1 - t) * P0 + t * P1
其中: $t \in $[0,1]

意义:由 P0 至 P1 的连续点, 描述的一条线段。该方程描述了t从0变化到1,线段上的所有点的集合。下图形象表达了上述方程:
线段

线段

二次贝塞尔曲线方程

二次贝塞尔曲线包括了起始点P0,终止点P2,和一个控制点P1。二次贝塞尔曲线的表达方程如下:
B(t) = (1-t)2 * P0 + 2t(1-t) * P1 + t2 * P2
其中: $t \in $[0,1]

三次贝塞尔曲线方程

三次贝塞尔曲线包括了其实点P0,终止点P3,和两个控制点P1和P2。三次贝塞尔曲线的表达方程如下:
B(t) = (1 - t)3 * P0 + 3t(1-t)2 * P1 + 3t2(1-t) * P2 + t3 * P3
其中: $t \in $[0,1]

更高阶贝塞尔曲线

一般来说,最常用的是二次和三次贝塞尔曲线。但是也可能会用到更高阶的贝塞尔曲线, 此处列出贝塞尔曲线的通用公式:

动画示例

本示例会演示如下动画: 一个圆点在一条二次曲线上面从起点移动到终止点。效果如下:
曲线动画

曲线动画

代码如下:

function draw() {
  let p0 = {x:100,y:100},
      p1 = {x:200,y:100},
      p2 = {x:200,y:200};
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.save();
  ctx.strokeStyle = 'red';
  arc(p0,p1,p2);
  ctx.restore();

  ctx.save();
  ctx.strokeStyle = 'blue';
  ctx.beginPath();
  ctx.moveTo(p0.x,p0.y);
  ctx.quadraticCurveTo(p1.x, p1.y, p2.x, p2.y);
  ctx.stroke();
  ctx.restore();

  let pb = {};
  pb.x = computeCurvePoint(p0.x,p1.x,p2.x,t);
  pb.y = computeCurvePoint(p0.y,p1.y,p2.y,t);
  ctx.save();
  ctx.strokeStyle = 'green';
  ctx.lineWidth = 4;
  arc(pb);
  ctx.restore();
  t += 0.01;

  if(t > 1){
    t = 0;
  }
  requestAnimationFrame(draw);
}

function  arc(...ps) {
  ps.forEach(p => {
     ctx.beginPath();
     ctx.arc(p.x, p.y, 3, 0, Math.PI * 2);
     ctx.stroke();
  });
}

function computeCurvePoint(a0,a1,a2,t){
  let b =  (1 -t)*(1-t) * a0 + 2 *(1-t) * t * a1 + t * t * a2;
  return b;
}
  1. 代码首先定义了二次贝赛尔曲线三个点p0、p1、p2。其中p0、p2是起始点和终止点,p1是控制点。
  2. 调用arc函数在p0、p1、p2处绘制三个半径为3的圈。
  3. 调用moveTo和quadraticCurveTo绘制二次贝塞尔曲线。quadraticCurveTo函数在前面已经讲解过,此处不赘述。
  4. 之后是通过二次贝塞尔曲线方程来计算曲线上面的点pb。根据t值进行计算,计算的公式参考computeCurvePoint函数,正是二次贝塞尔曲线方程。分别调用该公式计算得出点pb的x坐标和y坐标。
    let b = (1 -t)*(1-t) * a0 + 2 *(1-t) * t * a1 + t * t * a2;
  5. 然后调用arc函数在pb点处绘制一个圆点。
  6. 要起到动画效果,就要不断改变t的数值 并不断重复上面1~5步骤。所以每次t都增加0.01。然后调用requestAnimationFrame函数不断调用draw函数。

读者可以使用同样的思路 绘制点在三次贝塞尔曲线上面运动的效果。

迭代(分片)

上面的示例演示了一个点在贝塞尔曲线上面的运动。还有的时候,我们期望只绘制一曲线的一部分,而不是全部,如下图所示,那么应该如何绘制呢。
曲线动画2

曲线动画2

其中一种比较容易想到的方式就是分片,通过前面介绍的公式,我们知道,对于给定的t值(其中: $t \in $[0,1]),可以获取曲线上面对应的点,如果t值遍历0~1之间足够多的值,那么就可以获取曲线上面足够多的点。通过贝赛尔曲线的方程计算出一系列点,然后把这一系列的点按照先后顺序连接起来,可以模拟曲线。下面的iter函数实现了迭代分片:

function iter(t,p0,p1,p2){
  ctx.beginPath();
  ctx.moveTo(p0.x,p0.y);
  for(var i = 1;i < 100; i ++){
    var tt = t/100 * i;
    var pb = {};
    pb.x = computeCurvePoint(p0.x,p1.x,p2.x,tt);
    pb.y = computeCurvePoint(p0.y,p1.y,p2.y,tt);
    ctx.lineTo(pb.x,pb.y);
  }
  ctx.stroke();
}

iter函数的第一个参数是t值(t值的意义:表示我们要绘制的曲线是0~t之间的一段,如果t=1,说明要绘制的是完整的贝赛尔曲线),p0、p1、p2是二次贝赛尔曲线的三个点。下面说明该函数的细节:

  • 首先moveTo函数,把起始点移动到p0点
  • 然后是一个for循环,迭代次数是100。
  • 在循环体内,首先计算出一个新的比例值tt,在0~t之间平均计算出100个tt值。然后通过tt值计算出一个点pb,并lineTo该点。由于迭代次数是100,所以实际上会计算出100个点。
    上述过程一直使用的是画直线的函数lineTo,但是只要迭代次数够大,最终用户感知的会是一条曲线,效果参考上面的图片。
    使用上述办法,对于给定的任意t值,可以绘制曲线的任意部分。

如果希望绘制曲线从0到1的动画过程,只要使用上面的方法,并不断改变t值即可以。代码可以参考前面的动画示例:

 t += 0.01;
  if(t > 1){
    t = 0;
  }
  requestAnimationFrame(draw);

动画绘制效果参考前面的动画图片。

任意次贝赛尔曲线

本节代码参考algorithm.html和algorithm.js

前面学习了二次、三次和任意次贝塞尔曲线的方程式。并应用方程式实现了曲线动画效果。本节将进一步解释贝塞尔曲线的图形化应用原理,并由此得出任意次贝塞尔曲线的计算方法,并改进前面的曲线迭代分片算法。
贝塞尔曲线的通用公式:

贝塞尔曲线的前世今生

top Created with Sketch.