贝赛尔曲线知识详解

这些天pop 动画源码,里面涉及到贝塞尔曲线。对贝塞尔曲线以前只是局限在会用的阶段,没仔细详细研究过,今天准备仔细看看,搞明白。

贝赛尔曲线 概念

贝塞尔曲线(Bézier curve),又称贝兹曲线或贝济埃曲线,是应用于二维图形应用程序的数学曲线。一般的矢量图形软件通过它来精确画出曲线,贝兹曲线由线段与节点组成,节点是可拖动的支点,线段像可伸缩的皮筋。

贝塞尔曲线的公式

贝塞尔曲线通用公式

术语

一些关于参数曲线的术语,有


image.png

即多项式


image.png

又称作n阶的伯恩斯坦基底多项式,定义00 = 1。
Pi称作贝济埃曲线的控制点。多边形以带有线的贝济埃点连接而成,起始于P0并以Pn终止,称作贝济埃多边形(或控制多边形)。贝济埃多边形的凸包包含有贝济埃曲线。

二项式定理

这里我看到


image.png

一脸懵逼,虽然以前学过,但是忘记了,因此这里查了半天才知道这么写法是什么意思了。

image.png

看懂这个图就明白二项式前面的项了

注解

  • 开始于P0并结束于Pn的曲线,即所谓的端点插值法属性。
  • 曲线是直线的充分必要条件是所有的控制点都位在曲线上。同样的,贝济埃曲线是直线的充分必要条件是控制点共线。
  • 曲线的起始点结束点相切于贝济埃多边形。
  • 一条曲线可在任意点切割成两条或任意多条子曲线,每一条子曲线仍是贝济埃曲线。
  • 一些看似简单的曲线(如圆)无法以贝济埃曲线精确的描述,或分段成贝济埃曲线。
  • 位于固定偏移量的曲线(来自给定的贝济埃曲线),又称作偏移曲线(假平行于原来的曲线,如两条铁轨之间的偏移)无法以贝济埃曲线精确的形成(某些平凡实例除外)。无论如何,现存的启发法通常可为实际用途中给出近似值。

基本公式的低阶公式

线性公式 (当n=1)时候

一阶

给定点P0、P1,线性贝兹曲线只是一条两点之间的直线。

二次方公式

image.png

三次方公式

image.png

构建贝塞尔曲线

线型曲线

线性贝塞尔曲线函数中的t会经过由P0至P1的B(t)所描述的曲线。例如当t=0.25时,B(t)即一条由点P0至P1路径的四分之一处。就像由0至1的连续t,B(t)描述一条由P0至P1的直线。

线型贝塞尔曲线

二次曲线

为建构二次贝塞尔曲线,可以中介点Q0和Q1作为由0至1的t:
由P0至P1的连续点Q0,描述一条线性贝塞尔曲线。
由P1至P2的连续点Q1,描述一条线性贝塞尔曲线。
由Q0至Q1的连续点B(t),描述一条二次贝塞尔曲线。


三阶贝塞尔曲线

三次曲线,可由线性贝济埃曲线描述的中介点Q0、Q1、Q2,和由二次曲线描述的点R0、R1所建构



高阶贝塞尔曲线

对于四次曲线,可由线性贝济埃曲线描述的中介点Q0、Q1、Q2、Q3,由二次贝济埃曲线描述的点R0、R1、R2,和由三次贝济埃曲线描述的点S0、S1所建构


升阶

n次贝济埃曲线可以转换为一个形状完全相同的n+1次贝济埃曲线。 这在软件只支援特定阶次的贝济埃曲线时很有用。 例如,Cairo只支援三次贝济埃曲线,你就可以用升阶的方法在Cairo画出二次贝济埃曲线。

我们利用

这个特性来做升阶。我们把曲线方程中每一项
都乘上 (1 − t) 或 t,让每一项都往上升一阶。以下是将二阶升为三阶的范例

这里我估计好多很看着很懵,我也懵,仔细看就明白了,将上述公式的省略项补齐就好看了。
(1-t)2P0 + 2(1-t)tP1 + t2P2 这是二阶的公式
推导出三阶如下
   (1-t)2P0        +     2(1-t)tP1        +    t2P2
= (1-t)2P0[(1-t)+t]     +   2(1-t)tP1[(1-t)+t]      +  t2P2[(1-t)+t]
= (1-t)2P0(1-t) + (1-t)2P0t +  2(1-t)tP1(1-t) + 2(1-t)tP1t  +  t2P2(1-t) + t2P2t
=(1-t)3P0 + (1-t)2tP0    +  2(1-t)2tP1 + 2(1-t)t2P1    +  (1-t)t2P2 + t3P2
整理
=(1-t)3P0  +  [(1-t)2tP0 + 2(1-t)2tP1]  +  [2(1-t)t2P1 + (1-t)t2P2]  +  t3P2
=(1-t)3P0  +  (1-t)2t(P0+2P1)    + (1-t)t2(2P1+P2)        +  t3P2
=(1-t)3P0  +  [(1-t)2tP0 + 2(1-t)2tP1]  +  [2(1-t)t2P1 + (1-t)t2P2]  +  t3P2
=(1-t)3P0  +  3(1-t)2t(P0+2P1)/3    + 3(1-t)t2(2P1+P2)/3     +  t3P2

通过上面的规律下面的也是一样的计算获取高阶
对任何的n值,我们都可以使用以下等式




式中P-1 和Pn+1 可以任意挑选。
因此,新的控制点为

程序范例

三阶贝塞尔曲线

namespace WebCore {
  struct UnitBezier {
    UnitBezier(double p1x, double p1y, double p2x, double p2y)
    {
    // Calculate the polynomial coefficients, implicit first and last control points are (0,0) and (1,1).
/// 三阶贝塞尔曲线系数计算,推导在pop动画分析里讲解的
      cx = 3.0 * p1x;
      bx = 3.0 * (p2x - p1x) - cx;
      ax = 1.0 - cx -bx;
      cy = 3.0 * p1y;
      by = 3.0 * (p2y - p1y) - cy;
      ay = 1.0 - cy - by;
    }
    
    /// 时间对应的点x
    double sampleCurveX(double t)
    {
      // `ax t^3 + bx t^2 + cx t' expanded using Horner's rule.
      return ((ax * t + bx) * t + cx) * t;
    }
      ///时间对应的点y
    double sampleCurveY(double t)
    {
      return ((ay * t + by) * t + cy) * t;
    }
    
      ///x的倒数
    double sampleCurveDerivativeX(double t)
    {
      return (3.0 * ax * t + 2.0 * bx) * t + cx;
    }
    
    // Given an x value, find a parametric value it came from.
    double solveCurveX(double x, double epsilon)
    {
      double t0;
      double t1;
      double t2;
      double x2;
      double d2;
      int i;
      
      // First try a few iterations of Newton's method -- normally very fast.
        ///
      for (t2 = x, i = 0; i < 8; i++) {
          ///获取t2 时间x 周的坐标
        x2 = sampleCurveX(t2) - x;
          NSLog(@"%lf %lf  %lf",t2 , x ,x2);
        if (fabs (x2) < epsilon)
          return t2;
          NSLog(@"====%lf",x2);
          ///计算斜率
        d2 = sampleCurveDerivativeX(t2);
          NSLog(@"d2====%lf",d2);

        if (fabs(d2) < 1e-6)
          break;
          ///
        t2 = t2 - x2 / d2;
      }
       NSLog(@"error");
      // Fall back to the bisection method for reliability.
      t0 = 0.0;
      t1 = 1.0;
      t2 = x;
      
      if (t2 < t0)
        return t0;
      if (t2 > t1)
        return t1;
      
      while (t0 < t1) {
        x2 = sampleCurveX(t2);
        if (fabs(x2 - x) < epsilon)
          return t2;
        if (x > x2)
          t0 = t2;
        else
          t1 = t2;
        t2 = (t1 - t0) * .5 + t0;
      }
      
      // Failure.
      return t2;
    }
    
    double solve(double x, double epsilon)
    {
      return sampleCurveY(solveCurveX(x, epsilon));
    }
    
  private:
    double ax;
    double bx;
    double cx;
    double ay;
    double by;
    double cy;
  };
}

ios的三阶贝塞尔曲线

苹果提供了关于三阶贝塞尔曲线的类CAMediaTimingFunction
从这个类中我们读取到苹果规定的

CA_EXTERN NSString * const kCAMediaTimingFunctionLinear
    CA_AVAILABLE_STARTING (10.5, 2.0, 9.0, 2.0);
CA_EXTERN NSString * const kCAMediaTimingFunctionEaseIn
    CA_AVAILABLE_STARTING (10.5, 2.0, 9.0, 2.0);
CA_EXTERN NSString * const kCAMediaTimingFunctionEaseOut
    CA_AVAILABLE_STARTING (10.5, 2.0, 9.0, 2.0);
CA_EXTERN NSString * const kCAMediaTimingFunctionEaseInEaseOut
    CA_AVAILABLE_STARTING (10.5, 2.0, 9.0, 2.0);
CA_EXTERN NSString * const kCAMediaTimingFunctionDefault

样式的贝塞尔曲线的顶点值。

- (void)getControlPointAtIndex:(size_t)idx values:(float[_Nonnull 2])ptr;

该函数是获取顶点的api

该类使用在layer动画中

- (void)viewDidLoad {
    [super viewDidLoad];


    // 初始化layer
    CALayer *layer        = [CALayer layer];
    layer.frame           = CGRectMake(50, 50, 200, 2);
    layer.backgroundColor = [UIColor blackColor].CGColor;
    
    
    // 终点位置
    CGPoint endPosition = CGPointMake(layer.position.x, layer.position.y + 200);
    
    
    // 动画
    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];
    animation.fromValue         = [NSValue valueWithCGPoint:layer.position];
    animation.toValue           = [NSValue valueWithCGPoint:endPosition];
    animation.timingFunction    = [CAMediaTimingFunction functionWithControlPoints:0.20 :0.03 :0.13 :1.00];
    layer.position              = endPosition;
    animation.duration          = 1.f;
   
    // 添加动画
    [layer addAnimation:animation forKey:nil];
    // 添加layer
    [self.view.layer addSublayer:layer];
}

绘制贝塞尔曲线与时间的关系

坐标系没有进行转换,坐标原点再左上角

float  cx;
float  bx ;
float  ax;

float   cy ;
float   by ;
float   ay ;
@interface DrawPoint()
{

}


@property (nonatomic,assign) CGPoint control1;
@property (nonatomic,assign) CGPoint control2;


@end

@implementation DrawPoint

- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        self.backgroundColor = [UIColor redColor];
        CAMediaTimingFunction *function = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
        float point[2];
      for (int i =0;i<4;i++) {
            float point[2];
            [function getControlPointAtIndex:i values:point];
            self.control1 = CGPointMake(point[0], point[1]);
            NSLog(@"%@",NSStringFromCGPoint(self.control1));
        }
        [function getControlPointAtIndex:1 values:point];
        self.control1 = CGPointMake(point[0], point[1]);
        [function getControlPointAtIndex:2 values:point];
        self.control2 = CGPointMake(point[0], point[1]);
        cx = 3.0 * self.control1.x;
        bx = 3.0 * (self.control2.x - self.control1.x) - cx;
        ax = 1.0 - cx -bx;
        cy = 3.0 * self.control1.y;
        by = 3.0 * (self.control2.y - self.control1.y) - cy;
        ay = 1.0 - cy - by;
    }
    return self;
}
double sampleCurveX(double t)
{
    // `ax t^3 + bx t^2 + cx t' expanded using Horner's rule.
    return ((ax * t + bx) * t + cx) * t;
}
///时间对应的点y
double sampleCurveY(double t)
{
    return ((ay * t + by) * t + cy) * t;
}
- (void)drawRect:(CGRect)rect {
    UIColor *color = [UIColor colorWithRed:0 green:0 blue:0.7 alpha:1];
    [color set];
    int n = 1000;
    for (int i=0; i<n; i++) {
        UIBezierPath* aPath = [UIBezierPath bezierPathWithRect:CGRectMake(sampleCurveX(i*1.0/n)*self.bounds.size.width, sampleCurveY(i*1.0/n)*self.bounds.size.height, 1, 1)]; // 2.创建图形相应的
        [aPath fill];
    }
}



/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.

*/

@end

上面是测试代码

kCAMediaTimingFunctionLinear

image.png
2018-09-18 16:40:56.085033+0800 draw[16389:15584186] {0, 0}
2018-09-18 16:40:56.086905+0800 draw[16389:15584186] {0, 0}
2018-09-18 16:40:56.087094+0800 draw[16389:15584186] {1, 1}
2018-09-18 16:40:56.087281+0800 draw[16389:15584186] {1, 1}

kCAMediaTimingFunctionEaseIn

image.png
2018-09-18 16:40:12.168905+0800 draw[16362:15583293] {0, 0}
2018-09-18 16:40:12.169112+0800 draw[16362:15583293] {0.41999998688697815, 0}
2018-09-18 16:40:12.169238+0800 draw[16362:15583293] {1, 1}
2018-09-18 16:40:12.169362+0800 draw[16362:15583293] {1, 1}

kCAMediaTimingFunctionEaseOut

image.png
2018-09-18 16:39:31.150050+0800 draw[16336:15582505] {0, 0}
2018-09-18 16:39:31.150593+0800 draw[16336:15582505] {0, 0}
2018-09-18 16:39:31.151359+0800 draw[16336:15582505] {0.57999998331069946, 1}
2018-09-18 16:39:31.151518+0800 draw[16336:15582505] {1, 1}

kCAMediaTimingFunctionEaseInEaseOut

image.png
2018-09-18 16:38:36.048302+0800 draw[16305:15581503] {0, 0}
2018-09-18 16:38:36.048784+0800 draw[16305:15581503] {0.41999998688697815, 0}
2018-09-18 16:38:36.049117+0800 draw[16305:15581503] {0.57999998331069946, 1}
2018-09-18 16:38:36.049575+0800 draw[16305:15581503] {1, 1}

kCAMediaTimingFunctionDefault

image.png
2018-09-18 16:38:05.253326+0800 draw[16284:15580826] {0, 0}
2018-09-18 16:38:05.253511+0800 draw[16284:15580826] {0.25, 0.10000000149011612}
2018-09-18 16:38:05.253643+0800 draw[16284:15580826] {0.25, 1}
2018-09-18 16:38:05.253774+0800 draw[16284:15580826] {1, 1}

自定义几个值看看

        CAMediaTimingFunction *function = [CAMediaTimingFunction functionWithControlPoints:0.25 :0.5 :0.75 :0.5];
image.png

推荐阅读更多精彩内容