Categories
程式開發

貝塞爾曲線在iOS端的繪圖實踐


1 前言

在這個大數據的時代,很多信息只有通過圖形才能更好的展示給用戶。 例如:房屋的歷史價格、基金股票的歷史增長、數據佔比分析圖等。 如何做圖形? 需要用到什麼知識? 本文將從建模、顯示兩方面來展開介紹。

貝塞爾曲線在iOS端的繪圖實踐 1

2 建模

建模是一切圖形的基礎,其他內容的前提,要用代碼展示一個圖形,首先要有它的幾何模型表達。 目前在客戶端二維圖形建模上,Bézier curve(貝塞爾曲線)可以稱為經典和主流並重的數學曲線。

對於貝塞爾曲線來說,最重要的是起始點、終止點(也稱錨點)、控制點。 控制點決定了一條路徑的彎曲軌跡,根據控制點的個數,貝塞爾曲線被分為:一階貝塞爾曲線(0個控制點)、二階貝塞爾曲線(1個控制點) 、三階貝塞爾曲線(2個控制點)、N階貝塞爾曲線(n – 1個控制點)。

2.1 貝塞爾曲線原理

以二階貝塞爾曲線為例解釋說明:

起始點:P0 ; 控制點:P1 ; 終止點:P2

貝塞爾曲線在iOS端的繪圖實踐 2

  1. 連接P0P1線和P1P2線。
  2. 在P0P1線上找到點A,在P1P2線上找到點B,使得P0A/AP1 = P1B/BP2
  3. 連接AB,在AB上找到點X,X點滿足:AX/XB = P0A/AP1 = P1B/BP2
  4. 找出所有滿足公式:AX/XB = P0A/AP1 = P1B/BP2 的X點。 (從P0 到P2的紅色曲線點為所有X點的連線)這條由所有X點組成的連線即為貝塞爾曲線。

二階貝塞爾曲線起始點:P0 ; 控制點:P1 ; 終止點:P2

貝塞爾曲線在iOS端的繪圖實踐 3

三階貝塞爾曲線起始點:P0 ; 控制點:P1、P2; 終止點:P3

貝塞爾曲線在iOS端的繪圖實踐 4

四階貝塞爾曲線起始點:P0 ; 控制點:P1、P2、P3 ; 終止點:P4

貝塞爾曲線在iOS端的繪圖實踐 5

2.2 UIBezierPath類

系統給我們提供了一個叫做UIBezierPath類,用它可以畫簡單的圓形,橢圓,矩形,圓角矩形,也可以通過添加點去生成任意的圖形,還可以簡單的創建一條二階貝塞爾曲線和三階貝塞爾曲線。 我們來了解一下它的常用方法:

2.2.1 初始化方法

// 创建UIBezierPath对象
+ (instancetype)bezierPath;
// 创建在rect内的矩形
+ (instancetype)bezierPathWithRect:(CGRect)rect;
// 设定特定的角为圆角的矩形,corners:指定的角为圆角,其他角不变,cornerRadii:圆角的大小
+ (instancetype)bezierPathWithRoundedRect:(CGRect)rect byRoundingCorners:(UIRectCorner)corners cornerRadii:(CGSize)cornerRadii;
// 创建圆弧
+ (instancetype)bezierPathWithArcCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise;
// 通过已有路径创建路径
+ (instancetype)bezierPathWithCGPath:(CGPathRef)CGPath;
// 创建三次贝塞尔曲线 endPoint:终点 controlPoint1:控制点1 controlPoint2:控制点2
- (void)addCurveToPoint:(CGPoint)endPoint controlPoint1:(CGPoint)controlPoint1 controlPoint2:(CGPoint)controlPoint2;
- // 创建二次贝塞尔曲线 endPoint:终点 controlPoint:控制点
- (void)addQuadCurveToPoint:(CGPoint)endPoint controlPoint:(CGPoint)controlPoint;

2.2.2 使用方法

// 移动到某一点
- (void)moveToPoint:(CGPoint)point;
// 绘制一条线
- (void)addLineToPoint:(CGPoint)point;
// 闭合路径,即在终点和起点连一根线
- (void)closePath;
// 清空路径
- (void)removeAllPoints;
// 填充
- (void)fill;
// 描边,路径创建需要描边才能显示出来
- (void)stroke;

2.2.3 常用屬性

// 将UIBezierPath类转换成CGPath,类似于UIColor的CGColor
@property(nonatomic) CGPathRef CGPath;
// path线的宽度
@property(nonatomic) CGFloat lineWidth;
// path端点样式
@property(nonatomic) CGLineCap lineCapStyle;
// 拐角样式
@property(nonatomic) CGLineJoin lineJoinStyle;

2.2.4 舉個栗子🌰

先看效果👇:

貝塞爾曲線在iOS端的繪圖實踐 6

代碼如下:

- (void)drawRect:(CGRect)rect {

    [[UIColor redColor] set];

    // 右边第一个图
    UIBezierPath* maskPath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(50, 50, 100, 100) byRoundingCorners:UIRectCornerTopLeft cornerRadii:CGSizeMake(30, 30)];
    maskPath.lineWidth     = 20.f;
    maskPath.lineJoinStyle = kCGLineJoinBevel;
    [maskPath stroke];
    // 中间第二个图
    UIBezierPath* maskFillPath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(200, 50, 100, 100) byRoundingCorners:UIRectCornerTopLeft cornerRadii:CGSizeMake(30, 30)];
    maskFillPath.lineWidth     = 20.f;
    maskFillPath.lineJoinStyle = kCGLineJoinBevel;
    [maskFillPath fill];
    [maskFillPath stroke];
    // 右边第三个图
    UIBezierPath *maskLinePath = [UIBezierPath bezierPath];
    maskLinePath.lineWidth     = 20.f;
    maskLinePath.lineCapStyle  = kCGLineCapRound;
    [maskLinePath moveToPoint:CGPointMake(250.0, 50)];
    [maskLinePath addLineToPoint:CGPointMake(300.0, 100.0)];
    [maskLinePath stroke];
}

上圖中:

1)圖一和圖二唯一的不同是[maskFillPath fill]方法,fill方法要在封閉的曲線調用。

2)圖一和圖二為設定特定的角為圓角的矩形,corners為UIRectCornerTopLeft左上角,cornerRadii圓角大小為30,綠色的箭頭表示的設定的這個角。

corners 为下面五种类型
typedef NS_OPTIONS(NSUInteger, UIRectCorner) {
    UIRectCornerTopLeft     = 1 << 0,    // 左上角
    UIRectCornerTopRight    = 1 << 1,    // 右上角
    UIRectCornerBottomLeft  = 1 << 2,    // 左下角
    UIRectCornerBottomRight = 1 << 3,    // 右下角
    UIRectCornerAllCorners  = ~0UL        // 全部
};

3)圖一和圖二黃色的箭頭設置的屬性拐角樣式:lineJoinStyle kCGLineJoinBevel(缺角)

lineJoinStyle 为下面三种类型
typedef CF_ENUM(int32_t, CGLineJoin) {
    kCGLineJoinMiter,    // 尖角
    kCGLineJoinRound,    // 圆角
    kCGLineJoinBevel    // 缺角
};

4)圖三白色的箭頭設置的屬性path端點樣式:lineCapStyle kCGLineCapRound(圓形端點)

lineCapStyle 为下面三种类型
typedef CF_ENUM(int32_t, CGLineCap) {
    kCGLineCapButt,        // 无端点
    kCGLineCapRound,    // 圆形端点
    kCGLineCapSquare    // 方形端点
};

有興趣的可以試試別的方法屬性~

2.3 波浪曲線實現

如何實現N階波浪式曲線? 如何找到N-1 個對應的控制點?

有兩個方法,下圖為同數據,方案一和方案二分別所得曲線圖。

方案一為左邊(三階貝塞爾)圖其中第二條的紅點為數據的位置

方案二為右邊(CatmullRom)圖其中第二條的紅點為數據的位置

貝塞爾曲線在iOS端的繪圖實踐 7

方案一:根據創建三次貝塞爾曲線方法實現波浪曲線

控制點的選取方案不唯一,以下為我選擇控制點的方案:

控制點P1:CGPointMake((PrePonit.x+NowPoint.x)/2, PrePonit.y)

控制點P2:CGPointMake((PrePonit.x + NowPoint.x)/ 2,NowPoint.y)

可以根據前一個點PrePonit 和現在的點NowPoint 進行計算控制點。

主要代碼如下:

UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:[self pointAtIndex:0]];
NSInteger count = self.points.count;
CGPoint PrePonit;

for (NSInteger i = 0; i < count; i++) {

    CGPoint NowPoint = [self pointAtIndex:i];
    if(i == 0) {
        PrePonit = NowPoint;
    } else {
          // 利用三次曲线 形成波浪曲线
        [path addCurveToPoint:point controlPoint1:CGPointMake((PrePonit.x+NowPoint.x)/2, PrePonit.y) controlPoint2:CGPointMake((PrePonit.x+NowPoint.x)/2, NowPoint.y)];
        PrePonit = NowPoint;
    }
}
return path;

方案二:使用CatmullRom 插值樣條。 (有興趣的可以百度一下~這裡只簡單介紹)

要點分析:

1)給定一組控制點而得到一條曲線,曲線經過給定所有數據點。

2)Catmull-Rom公式:P(t)= 0.5 *(2 * p1 +(p2-p0)* t +(2 * p0-5 * p1 + 4 * p2-p3)* t * t +(3 * p1-p0-3 * p2 + p3)* t * t * t);

注:t為分割的最小粒尺寸,根據P0 P1 P2 P3 順序的四個點求得P1P2 曲線公式。

主要代碼如下:

void getPointsFromBezier(void *info, const CGPathElement *element) {
    NSMutableArray *bezierPoints = (__bridge NSMutableArray *)info;
    CGPathElementType type = element->type;
    CGPoint *points = element->points;

    if (type != kCGPathElementCloseSubpath) {
        [bezierPoints addObject:VALUE(0)];
        if ((type != kCGPathElementAddLineToPoint) &&
            (type != kCGPathElementMoveToPoint))
            [bezierPoints addObject:VALUE(1)];
    }
    if (type == kCGPathElementAddCurveToPoint)
        [bezierPoints addObject:VALUE(2)];
}

NSArray *pointsFromBezierPath(UIBezierPath *bpath) {
    NSMutableArray *points = [NSMutableArray array];
    // 获取贝塞尔曲线上所有的点
    CGPathApply(bpath.CGPath, (__bridge void *)points, getPointsFromBezier);
    return points;
}

- (UIBezierPath*)smoothedPathWithGranularity:(NSInteger)granularity path:(UIBezierPath *)path {
    NSMutableArray *points = [pointsFromBezierPath(path) mutableCopy];

    if (points.count < 4) return [path copy];
    [points insertObject:[points objectAtIndex:0] atIndex:0];
    [points addObject:[points lastObject]];

    UIBezierPath *smoothedPath = [path copy];
    [smoothedPath removeAllPoints];
    [smoothedPath moveToPoint:POINT(0)];

    for (NSUInteger index = 1; index < points.count - 2; index++) {
        CGPoint p0 = POINT(index - 1);
        CGPoint p1 = POINT(index);
        CGPoint p2 = POINT(index + 1);
        CGPoint p3 = POINT(index + 2);

        for (int i = 1; i < granularity; i++) {
              // granularity 这里按照 20 粒度划分的
            float t = (float) i * (1.0f / (float) granularity);
            float tt = t * t;
            float ttt = tt * t;

            CGPoint pi;
            // 根据 CatmullRom 公式 根据 P0 P1 P2 P3 获取点的坐标
            pi.x = 0.5 * (2*p1.x+(p2.x-p0.x)*t + (2*p0.x-5*p1.x+4*p2.x-p3.x)*tt + (3*p1.x-p0.x-3*p2.x+p3.x)*ttt);
            pi.y = 0.5 * (2*p1.y+(p2.y-p0.y)*t + (2*p0.y-5*p1.y+4*p2.y-p3.y)*tt + (3*p1.y-p0.y-3*p2.y+p3.y)*ttt);
            if (pi.x <= self.width) {
                [smoothedPath addLineToPoint:pi];
            }
        }

        if (p2.x <= self.width) {
            [smoothedPath addLineToPoint:p2];
        }
    }

    return smoothedPath;
}

對比總結:

方案一控制點的選取比較難,曲線的彎度也取決於控制點,操作簡單,易理解。

方案二曲線更順滑,但實現更複雜,不易理解。

這裡推薦兩個好用的網站:

a 這個網站提供了豐富的曲線類型可供選擇,非常直觀。

http://easings.net

b 這個網站提供了可視化的修改兩個控制點,來生成一條三階貝塞爾曲線,並切右邊還可以看到這條曲線產生的動畫會做怎樣的速度改變。

http://www.roblaplaca.com/examples/bezierBuilder/#

3 顯示

當layer與貝塞爾曲線相遇,會發生什麼樣的神奇反應?

3.1 CALayer

蘋果官網註釋:“An object that manages image-based content and allows you to perform animations on that content.” 管理基於圖像的內容並允許您對該內容執行動畫的對象。

CALayer 主要就兩方面作用:

1) 管理展示內容

2)內容可執行動畫

CALayer 自身有很多情況下自帶隱式動畫,但是UIView的根Layer是沒有隱式動畫的。

3.1.1 常用屬性

// 图层大小 支持隐式动画
@property CGRect bounds;
// 图层位置 支持隐式动画
@property CGPoint position;
// 在z轴上的位置 支持隐式动画
@property CGFloat zPosition;
// 沿z轴位置的锚点 支持隐式动画
@property CGFloat anchorPointZ;
// 锚点 默认在layer的中心点 取值范围(0~1) 支持隐式动画
@property CGPoint anchorPoint;
// 图层变换 支持隐式动画
@property CATransform3D transform;
// 图层大小和位置 不支持隐式动画
@property CGRect frame;
// 是否隐藏 支持隐式动画
@property(getter=isHidden) BOOL hidden;
// 图层背景是否显示 支持隐式动画
@property(getter=isDoubleSided) BOOL doubleSided;
// 父图层 支持隐式动画
@property(nullable, readonly) CALayer *superlayer;
// 子图层 支持隐式动画
@property(nullable, copy) NSArray *sublayers;
// 子图层变换 支持隐式动画
@property CATransform3D sublayerTransform;
// 图层蒙版 支持隐式动画
@property(nullable, strong) __kindof CALayer *mask;
// 子图层是否裁切超出父图层的部分,默认为NO
@property BOOL masksToBounds;
// 图层显示内容 设置layer的contents可以为layer添加显示内容 支持隐式动画
@property(nullable, strong) id contents;
// 图层显示内容的大小和位置 支持隐式动画
@property CGRect contentsRect;
// 用于指定层的内容如何在其范围内定位或缩放
@property(copy) CALayerContentsGravity contentsGravity;
// 是否包含完全不透明内容的布尔值
@property(getter=isOpaque) BOOL opaque;
// 背景色 支持隐式动画
@property(nullable) CGColorRef backgroundColor;
// 圆角半径 支持隐式动画
@property CGFloat cornerRadius;
// 边框宽度 支持隐式动画
@property CGFloat borderWidth;
// 边框颜色 支持隐式动画
@property(nullable) CGColorRef borderColor;
// 透明度 支持隐式动画
@property float opacity;
// 阴影颜色 支持隐式动画
@property(nullable) CGColorRef shadowColor;
// 阴影透明度 默认为0 需要显示阴影 必须设置值 支持隐式动画
@property float shadowOpacity;
// 阴影偏移量 支持隐式动画
@property CGSize shadowOffset;
// 阴影半径 支持隐式动画
@property CGFloat shadowRadius;
// 阴影形状 支持隐式动画
@property(nullable) CGPathRef shadowPath;

3.1.2 子類

貝塞爾曲線在iOS端的繪圖實踐 8

CALayer的子類有很多,下面說幾個比較常用的。

3.2 CAShapeLayer

蘋果官網註釋:“A layer that draws a cubic Bezier spline in its coordinate space.” 專門用於繪製貝塞爾曲線的layer。

3.2.1 看一下它獨特的屬性:

// path属性是曲线的路径,也是它和贝塞尔曲线紧密连接一个入口,决定了图层上画的是什么形状。
@property(nullable) CGPathRef path;
// 填充颜色
@property(nullable) CGColorRef fillColor;
// 曲线 指定哪块区域为内部,内部会被填充颜色
@property(copy) CAShapeLayerFillRule fillRule;
// 线的颜色
@property(nullable) CGColorRef strokeColor;
// strokeStart 和 strokeEnd 两者的取值都是0~1,决定贝塞尔曲线的划线百分比
@property CGFloat strokeStart;
@property CGFloat strokeEnd;
// 虚线开始的位置
@property CGFloat lineDashPhase;
// 虚线设置,数组中奇数位实线长度,偶数位带遍空白长度
@property(nullable, copy) NSArray *lineDashPattern;
// 线的宽度
@property CGFloat lineWidth;
// 最大斜接长度 只有lineJoin属性为kCALineJoinMiter时miterLimit才有效
@property CGFloat miterLimit;
// 线端点样式(样式与 贝塞尔曲线的CGLineCap 属性一致)
@property(copy) CAShapeLayerLineCap lineCap;
// 拐角样式(样式与 贝塞尔曲线的CGLineJoin 属性一致)
@property(copy) CAShapeLayerLineJoin lineJoin;

3.2.2 舉個栗子🌰

使用上面的一些屬性,再結合貝塞爾曲線,我們實現瞭如下一些效果:

貝塞爾曲線在iOS端的繪圖實踐 9

其中圖五的效果,代碼實現如下:

UIBezierPath *maskPath = [UIBezierPath bezierPath];
for (NSInteger i = 1; i < 9; i++) {
    UIBezierPath *tempPath = [UIBezierPath bezierPathWithRect:CGRectMake(190 - 20 * i, 550 - 10 * i, 40 * i, 20 * i)];
    [maskPath appendPath:tempPath];
}
[maskPath stroke];

// CAShapeLayer
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
maskLayer.frame = self.view.bounds;
maskLayer.path = maskPath.CGPath;
maskLayer.lineWidth = 5;
maskLayer.strokeColor = [UIColor purpleColor].CGColor;
maskLayer.fillRule = kCAFillRuleEvenOdd;
maskLayer.fillColor = [UIColor cyanColor].CGColor;
maskLayer.strokeStart = 0.2;
maskLayer.strokeEnd = 0.5;
maskLayer.lineDashPattern = @[@(10), @(10), @(30), @(30)];
maskLayer.lineDashPhase = 0;
[self.view.layer addSublayer:maskLayer];

3.3 CAGradientLayer

蘋果官網註釋:“A layer that draws a color gradient over its background color, filling the shape of the layer (including rounded corners)” 專門用於在背景色上繪製顏色漸變的圖層,填充圖層的形狀。

3.3.1 看一下它獨特的屬性:

// colors属性是CAGradientLayer的特殊属性,完美实现几种颜色的过渡。
@property(nullable, copy) NSArray *colors;
// 定义每个梯度停止的位置。取值范围为0~1递增
@property(nullable, copy) NSArray *locations;
// 决定了变色范围的起始点
@property CGPoint startPoint;
// 决定了变色范围的结束点
@property CGPoint endPoint;
// startPoint 和 endPoint两者的连线决定变色的趋势

3.3.2 舉個栗子🌰

使用上面的一些屬性我們實現瞭如下一些效果:

貝塞爾曲線在iOS端的繪圖實踐 10

其中圖五的效果,代碼實現如下:

CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = CGRectMake(20, 450, 150, 150);
gradientLayer.locations = @[@(0.2), @(0.5), @(0.6), @(0.8)];
gradientLayer.startPoint = CGPointMake(0, 0);
gradientLayer.endPoint = CGPointMake(1, 1);
gradientLayer.colors = @[(id)[UIColor purpleColor].CGColor, (id)[UIColor greenColor].CGColor, (id)[UIColor orangeColor].CGColor, (id)[UIColor blackColor].CGColor];
[self.view.layer addSublayer:gradientLayer];

3.4 再舉個栗子🌰🌰

當CAGradientLayer + CAShapeLayer + 貝塞爾曲線會有什麼效果? 上代碼~

- (void)setupUI {
    // 贝塞尔曲线
    UIBezierPath *maskPath = [UIBezierPath bezierPath];
    [maskPath moveToPoint:CGPointMake(100, 220)];
    [maskPath addLineToPoint:CGPointMake(200, 150)];
    [maskPath addLineToPoint:CGPointMake(300, 220)];
    [maskPath stroke];

    UIBezierPath *maskBottomPath = [UIBezierPath bezierPath];
    [maskBottomPath moveToPoint:CGPointMake(280, 250)];
    [maskBottomPath addCurveToPoint:CGPointMake(120, 250) controlPoint1:CGPointMake(250, 320) controlPoint2:CGPointMake(150, 320)];
    [maskBottomPath stroke];

    [maskPath appendPath:maskBottomPath];

    // CAShapeLayer
    CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
    maskLayer.frame = self.view.bounds;
    maskLayer.path = maskPath.CGPath;
    maskLayer.lineWidth = 20;
    maskLayer.strokeColor = UIColorFromRGB(0xF0F5FF).CGColor;
    maskLayer.lineCap = kCALineCapRound;
    maskLayer.lineJoin = kCALineJoinRound;
    maskLayer.fillColor = [UIColor clearColor].CGColor;
    maskLayer.strokeStart = 0;
    maskLayer.strokeEnd = 0;
    [self.view.layer addSublayer:maskLayer];

    // CAGradientLayer
    NSMutableArray *colorArray = [NSMutableArray new];
    for (NSInteger i = 0; i < 6; i++) {
        [colorArray addObject:[self arc4randomColor]];
    }

    CAGradientLayer *gradientLayer = [CAGradientLayer layer];
    gradientLayer.frame = self.view.bounds;
    gradientLayer.colors = colorArray;
    gradientLayer.startPoint = CGPointMake(0, 0.5);
    gradientLayer.endPoint = CGPointMake(1, 0.5);
    gradientLayer.mask = maskLayer;
    [self.view.layer addSublayer:gradientLayer];

    // 创建全局并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    // 创建定时器
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    // 设置定时器,每N秒触发
    int64_t intervalInNanoseconds = (int64_t)(0.3 * NSEC_PER_SEC);
    dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), (uint64_t)intervalInNanoseconds, 0);
    // 设置定时器处理事件
    dispatch_source_set_event_handler(timer, ^{
        dispatch_async(dispatch_get_main_queue(), ^{

            if (maskLayer.strokeEnd < 0.6) {
                maskLayer.strokeEnd += 0.4;
            } else if (maskLayer.strokeEnd < 0.8){
                maskLayer.strokeEnd += 0.2;
            } else if (maskLayer.strokeEnd < 1){
                maskLayer.strokeEnd += 0.1;
            } else {
                maskLayer.strokeEnd = 1;
                if (maskLayer.strokeStart < 0.6) {
                    maskLayer.strokeStart += 0.4;
                } else if (maskLayer.strokeStart < 0.8){
                    maskLayer.strokeStart += 0.2;
                } else if (maskLayer.strokeStart < 1){
                    maskLayer.strokeStart += 0.1;
                } else {
                    [colorArray removeObjectAtIndex:0];
                    [colorArray addObject:[self arc4randomColor]];
                    gradientLayer.colors = colorArray;

                    maskLayer.strokeStart = 0;
                    maskLayer.strokeEnd = 0;
                }
            }
        });
    });

    _timer = timer;

    // 开启定时器
    dispatch_resume(_timer);
}

- (id)arc4randomColor {
    return (id)[UIColor colorWithRed:arc4random()%255/255.f
                               green:arc4random()%255/255.f
                                blue:arc4random()%255/255.f
                               alpha:1].CGColor;
}

運行結果👇

<https://v.qq.com/x/page/l3146iykm06.html

其他layer怎麼使用? 貝塞爾曲線+ Layer 還可以組合出更多神奇的反應! 感興趣的可以去試試哦~本文僅為拋磚引玉~~

本文轉載自公眾號貝殼產品技術(ID:beikeTC)。

原文鏈接

貝塞爾曲線在iOS端的繪圖實踐