【问题标题】:Drawing Smooth Curves - Methods Needed绘制平滑曲线 - 所需方法
【发布时间】:2012-01-31 22:15:57
【问题描述】:

如何在 iOS 绘图应用程序 WHILE MOVING 中平滑一组点?我已经尝试过 UIBezierpaths,但是当我将点 1、2、3、4 - 2、3、4、5 移动时,我得到的只是它们相交的锯齿状末端。我听说过样条曲线和所有其他类型。我对 iPhone 编程很陌生,不明白如何在我的石英绘图应用程序中对其进行编程。一个可靠的例子将不胜感激,我已经花了几周的时间在圈子里跑,我似乎永远找不到任何 iOS 代码来完成这项任务。大多数帖子只是链接到一个 java 模拟或维基百科上关于曲线拟合的页面,这对我没有任何作用。我也不想切换到openGL ES。我希望最终有人可以提供代码来回答这个循环问题。


这是我的 UIBezierPath 代码,它在交叉路口留下边缘 ///

更新到下面的答案

#define VALUE(_INDEX_) [NSValue valueWithCGPoint:points[_INDEX_]]
#define POINT(_INDEX_) [(NSValue *)[points objectAtIndex:_INDEX_] CGPointValue]

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

    if (points.count < 4) return [self bezierPath];

    // Add control points to make the math make sense
    [points insertObject:[points objectAtIndex:0] atIndex:0];
    [points addObject:[points lastObject]];

    UIBezierPath *smoothedPath = [self bezierPath];
    [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);

        // now add n points starting at p1 + dx/dy up until p2 using Catmull-Rom splines
        for (int i = 1; i < granularity; i++)
        {
            float t = (float) i * (1.0f / (float) granularity);
            float tt = t * t;
            float ttt = tt * t;

            CGPoint pi; // intermediate point
            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);
            [smoothedPath addLineToPoint:pi];
        }

        // Now add p2
        [smoothedPath addLineToPoint:p2];
    }

    // finish by adding the last point
    [smoothedPath addLineToPoint:POINT(points.count - 1)];

    return smoothedPath;
}
- (PVPoint *)pointAppendingCGPoint:(CGPoint)CGPoint
{
    PVPoint *newPoint = [[PVPoint alloc] initInsertingIntoManagedObjectContext:[self managedObjectContext]];
    [newPoint setCGPoint:CGPoint];
    [newPoint setOrder:[NSNumber numberWithUnsignedInteger:[[self points] count]]];
    [[self mutableSetValueForKey:@"points"] addObject:newPoint];
    [(NSMutableArray *)[self pointsOrdered] addObject:newPoint];
    [[self bezierPath] addLineToPoint:CGPoint];
    return [newPoint autorelease];

    if ([self bezierPath] && [pointsOrdered count] > 3)
    {
        PVPoint *control1 = [pointsOrdered objectAtIndex:[pointsOrdered count] - 2];
        PVPoint *control2 = [pointsOrdered objectAtIndex:[pointsOrdered count] - 1];
        [bezierPath moveToPoint:[[pointsOrdered objectAtIndex:[pointsOrdered count] - 3] CGPoint]];
        [[self bezierPath] addCurveToPoint:CGPoint controlPoint1:[control1 CGPoint] controlPoint2:[control2 CGPoint]];

    }

}

- (BOOL)isComplete { return [[self points] count] > 1; }

- (UIBezierPath *)bezierPath
{
    if (!bezierPath)
    {
        bezierPath = [UIBezierPath bezierPath];
        for (NSUInteger p = 0; p < [[self points] count]; p++)
        {
            if (!p) [bezierPath moveToPoint:[(PVPoint *)[[self pointsOrdered] objectAtIndex:p] CGPoint]];
            else [bezierPath addLineToPoint:[(PVPoint *)[[self pointsOrdered] objectAtIndex:p] CGPoint]];
        }
        [bezierPath retain];
    }

    return bezierPath;
}

- (CGPathRef)CGPath
{
    return [[self bezierPath] CGPath];
}

【问题讨论】:

  • 你能告诉我们锯齿状的末端吗?
  • 请查看上面的 UIBezierPath 代码。
  • 苹果有一个名为 QuartzDemo 的示例代码
  • 我知道,但是在快速绘制时它不会提供平滑曲线...我需要一个平滑公式或其他东西
  • 你能发一张它现在的截图吗?

标签: iphone objective-c ios ipad bezier


【解决方案1】:

我刚刚在我正在从事的项目中实现了类似的东西。我的解决方案是使用 Catmull-Rom 样条而不是使用 Bezier 样条。这些通过一组点而不是贝塞尔样条曲线“围绕”点提供了非常平滑的曲线。

// Based on code from Erica Sadun

#import "UIBezierPath+Smoothing.h"

void getPointsFromBezier(void *info, const CGPathElement *element);
NSArray *pointsFromBezierPath(UIBezierPath *bpath);


#define VALUE(_INDEX_) [NSValue valueWithCGPoint:points[_INDEX_]]
#define POINT(_INDEX_) [(NSValue *)[points objectAtIndex:_INDEX_] CGPointValue]

@implementation UIBezierPath (Smoothing)

// Get points from Bezier Curve
void getPointsFromBezier(void *info, const CGPathElement *element) 
{
    NSMutableArray *bezierPoints = (__bridge NSMutableArray *)info;    

    // Retrieve the path element type and its points
    CGPathElementType type = element->type;
    CGPoint *points = element->points;

    // Add the points if they're available (per type)
    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;
{
    NSMutableArray *points = [pointsFromBezierPath(self) mutableCopy];

    if (points.count < 4) return [self copy];

    // Add control points to make the math make sense
    [points insertObject:[points objectAtIndex:0] atIndex:0];
    [points addObject:[points lastObject]];

    UIBezierPath *smoothedPath = [self 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);

        // now add n points starting at p1 + dx/dy up until p2 using Catmull-Rom splines
        for (int i = 1; i < granularity; i++)
        {
            float t = (float) i * (1.0f / (float) granularity);
            float tt = t * t;
            float ttt = tt * t;

            CGPoint pi; // intermediate point
            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);
            [smoothedPath addLineToPoint:pi];
        }

        // Now add p2
        [smoothedPath addLineToPoint:p2];
    }

    // finish by adding the last point
    [smoothedPath addLineToPoint:POINT(points.count - 1)];

    return smoothedPath;
}


@end

最初的 Catmull-Rom 实现是基于 Erica Sadun 在她的一本书中的一些代码,我对其稍作修改以实现完全平滑的曲线。这是作为 UIBezierPath 上的一个类别实现的,对我来说效果很好。

【讨论】:

  • 我将如何实现这个?我已经发布了我的代码的更完整图片。
  • 您错误地实现了UIBezierCurve+Smoothing.h。要定义一个类别,它应该是@interface UIBezierCurve (Smoothing),您还需要声明方法。如果您需要更具体的帮助,我们应该将其移至聊天。
  • 感激不尽。它的崇高。我真的很想知道为什么不接受答案。这是我平滑线条的最佳答案。
  • 你好@JoshuaWeinberg,你能显示你的触摸开始、触摸移动和触摸结束的代码吗?
  • 这比 quadCurvedPathWithPoints 解决方案要好得多。
【解决方案2】:

让两条贝塞尔曲线平滑连接的关键是相关控制点和曲线上的起点/终点必须共线。将控制点和端点想象成一条与端点处的曲线相切的线。如果一条曲线的起点与另一条曲线的终点相同,并且它们在该点具有相同的切线,则曲线将是平滑的。这里有一段代码来说明:

- (void)drawRect:(CGRect)rect
{   
#define commonY 117

    CGPoint point1 = CGPointMake(20, 20);
    CGPoint point2 = CGPointMake(100, commonY);
    CGPoint point3 = CGPointMake(200, 50);
    CGPoint controlPoint1 = CGPointMake(50, 60);
    CGPoint controlPoint2 = CGPointMake(20, commonY);
    CGPoint controlPoint3 = CGPointMake(200, commonY);
    CGPoint controlPoint4 = CGPointMake(250, 75);

    UIBezierPath *path1 = [UIBezierPath bezierPath];
    UIBezierPath *path2 = [UIBezierPath bezierPath];

    [path1 setLineWidth:3.0];
    [path1 moveToPoint:point1];
    [path1 addCurveToPoint:point2 controlPoint1:controlPoint1 controlPoint2:controlPoint2];
    [[UIColor blueColor] set];
    [path1 stroke];

    [path2 setLineWidth:3.0];
    [path2 moveToPoint:point2];
    [path2 addCurveToPoint:point3 controlPoint1:controlPoint3 controlPoint2:controlPoint4];
    [[UIColor orangeColor] set];
    [path2 stroke];
}

请注意,path1point2 结束,path2point2 开始,控制点 2 和 3 与 point2 共享相同的 Y 值 commonY。您可以根据需要更改代码中的任何值;只要这三个点都落在同一条线上,两条路径就会顺利连接。 (在上面的代码中,直线是y = commonY。直线不必平行于 X 轴;这样更容易看出这些点是共线的。)

这是上面代码绘制的图像:

查看您的代码后,您的曲线呈锯齿状的原因是您将控制点视为曲线上的点。在贝塞尔曲线中,控制点通常不在曲线上。由于您要从曲线中获取控制点,因此控制点和交点共线,因此路径不会平滑连接。

【讨论】:

  • 如何在上面的代码中使用一组不断移动的点来实现这一点?
  • @BDGapps 数学! (抱歉回答轻率,但 cmets 部分不适合讨论您的重​​要问题。)
  • @BDGapps 听起来您的问题归结为控制点的放置位置。如上所述,它们需要落在与曲线相切的直线上,但是直线在哪里?这在某种程度上取决于你。您可以找出相交处的切线by taking the derivative 的位置——在这方面有很多信息可以帮助您。归根结底,yAak 是对的——您需要做一些数学运算,并就您希望曲线的外观做出一些决定。
  • 此处描述的 Caleb 想法已在此处实现徒手绘图:mobile.tutsplus.com/tutorials/iphone/ios-sdk_freehand-drawing(查看文章底部的示例结果)
  • 共线性观察...哇,这简化了很多。谢谢
【解决方案3】:

在对捕获的点应用任何算法之前,我们需要观察一些事情。

  1. 一般 UIKit 不会给出等距离的点。
  2. 我们需要计算两个CGPoints之间的中间点[使用Touch移动方法捕获]

现在要获得流畅的线条,有很多方法。

有时我们可以通过应用二次多项式或三次多项式或 catmullRomSpline 算法来实现

- (float)findDistance:(CGPoint)point lineA:(CGPoint)lineA lineB:(CGPoint)lineB
{
    CGPoint v1 = CGPointMake(lineB.x - lineA.x, lineB.y - lineA.y);
    CGPoint v2 = CGPointMake(point.x - lineA.x, point.y - lineA.y);
    float lenV1 = sqrt(v1.x * v1.x + v1.y * v1.y);
    float lenV2 = sqrt(v2.x * v2.x + v2.y * v2.y);
    float angle = acos((v1.x * v2.x + v1.y * v2.y) / (lenV1 * lenV2));
    return sin(angle) * lenV2;
}

- (NSArray *)douglasPeucker:(NSArray *)points epsilon:(float)epsilon
{
    int count = [points count];
    if(count < 3) {
        return points;
    }

    //Find the point with the maximum distance
    float dmax = 0;
    int index = 0;
    for(int i = 1; i < count - 1; i++) {
        CGPoint point = [[points objectAtIndex:i] CGPointValue];
        CGPoint lineA = [[points objectAtIndex:0] CGPointValue];
        CGPoint lineB = [[points objectAtIndex:count - 1] CGPointValue];
        float d = [self findDistance:point lineA:lineA lineB:lineB];
        if(d > dmax) {
            index = i;
            dmax = d;
        }
    }

    //If max distance is greater than epsilon, recursively simplify
    NSArray *resultList;
    if(dmax > epsilon) {
        NSArray *recResults1 = [self douglasPeucker:[points subarrayWithRange:NSMakeRange(0, index + 1)] epsilon:epsilon];

        NSArray *recResults2 = [self douglasPeucker:[points subarrayWithRange:NSMakeRange(index, count - index)] epsilon:epsilon];

        NSMutableArray *tmpList = [NSMutableArray arrayWithArray:recResults1];
        [tmpList removeLastObject];
        [tmpList addObjectsFromArray:recResults2];
        resultList = tmpList;
    } else {
        resultList = [NSArray arrayWithObjects:[points objectAtIndex:0], [points objectAtIndex:count - 1],nil];
    }

    return resultList;
}

- (NSArray *)catmullRomSplineAlgorithmOnPoints:(NSArray *)points segments:(int)segments
{
    int count = [points count];
    if(count < 4) {
        return points;
    }

    float b[segments][4];
    {
        // precompute interpolation parameters
        float t = 0.0f;
        float dt = 1.0f/(float)segments;
        for (int i = 0; i < segments; i++, t+=dt) {
            float tt = t*t;
            float ttt = tt * t;
            b[i][0] = 0.5f * (-ttt + 2.0f*tt - t);
            b[i][1] = 0.5f * (3.0f*ttt -5.0f*tt +2.0f);
            b[i][2] = 0.5f * (-3.0f*ttt + 4.0f*tt + t);
            b[i][3] = 0.5f * (ttt - tt);
        }
    }

    NSMutableArray *resultArray = [NSMutableArray array];

    {
        int i = 0; // first control point
        [resultArray addObject:[points objectAtIndex:0]];
        for (int j = 1; j < segments; j++) {
            CGPoint pointI = [[points objectAtIndex:i] CGPointValue];
            CGPoint pointIp1 = [[points objectAtIndex:(i + 1)] CGPointValue];
            CGPoint pointIp2 = [[points objectAtIndex:(i + 2)] CGPointValue];
            float px = (b[j][0]+b[j][1])*pointI.x + b[j][2]*pointIp1.x + b[j][3]*pointIp2.x;
            float py = (b[j][0]+b[j][1])*pointI.y + b[j][2]*pointIp1.y + b[j][3]*pointIp2.y;
            [resultArray addObject:[NSValue valueWithCGPoint:CGPointMake(px, py)]];
        }
    }

    for (int i = 1; i < count-2; i++) {
        // the first interpolated point is always the original control point
        [resultArray addObject:[points objectAtIndex:i]];
        for (int j = 1; j < segments; j++) {
            CGPoint pointIm1 = [[points objectAtIndex:(i - 1)] CGPointValue];
            CGPoint pointI = [[points objectAtIndex:i] CGPointValue];
            CGPoint pointIp1 = [[points objectAtIndex:(i + 1)] CGPointValue];
            CGPoint pointIp2 = [[points objectAtIndex:(i + 2)] CGPointValue];
            float px = b[j][0]*pointIm1.x + b[j][1]*pointI.x + b[j][2]*pointIp1.x + b[j][3]*pointIp2.x;
            float py = b[j][0]*pointIm1.y + b[j][1]*pointI.y + b[j][2]*pointIp1.y + b[j][3]*pointIp2.y;
            [resultArray addObject:[NSValue valueWithCGPoint:CGPointMake(px, py)]];
        }
    }

    {
        int i = count-2; // second to last control point
        [resultArray addObject:[points objectAtIndex:i]];
        for (int j = 1; j < segments; j++) {
            CGPoint pointIm1 = [[points objectAtIndex:(i - 1)] CGPointValue];
            CGPoint pointI = [[points objectAtIndex:i] CGPointValue];
            CGPoint pointIp1 = [[points objectAtIndex:(i + 1)] CGPointValue];
            float px = b[j][0]*pointIm1.x + b[j][1]*pointI.x + (b[j][2]+b[j][3])*pointIp1.x;
            float py = b[j][0]*pointIm1.y + b[j][1]*pointI.y + (b[j][2]+b[j][3])*pointIp1.y;
            [resultArray addObject:[NSValue valueWithCGPoint:CGPointMake(px, py)]];
        }
    }
    // the very last interpolated point is the last control point
    [resultArray addObject:[points objectAtIndex:(count - 1)]]; 

    return resultArray;
}

【讨论】:

  • 非常好!你能分享一些关于这 2 种方法及其参数的 cmets 吗?
【解决方案4】:

为了实现这一点,我们需要使用这个方法。 BezierSpline 代码在 C# 中,用于为贝塞尔样条生成控制点数组。我将此代码转换为 Objective C,它对我来说非常有用。

将代码从 C# 转换为 Objective C。 逐行看懂C#代码,就算不懂C#,也一定懂C++/Java吧?

转换时:

  1. 将此处使用的 Point 结构替换为 CGPoint。

  2. 用 NSMutableArray 替换 Point 数组,并在其中存储 NSvalues 包装 CGPoints。

  3. 用 NSMutableArrays 替换所有双精度数组,并在其中存储 NSNumber 包装双精度。

  4. 在下标的情况下使用 objectAtIndex: 方法来访问数组元素。

  5. 使用 replaceObjectAtIndex:withObject: 将对象存储在特定索引处。

    请记住,NSMutableArray 是一个linkedList,而 C# 使用的是动态数组,因此它们已经具有现有索引。 在您的情况下,如果 NSMutableArray 为空,则不能像 C# 代码那样将对象存储在随机索引处。 他们有时在这个 C# 代码中,在索引 0 之前填充索引 1,并且他们可以在索引 1 存在时这样做。 在这里的 NSMutabelArrays 中,如果你想在其上调用 replaceObject,索引 1 应该在那里。 所以在存储任何东西之前,先创建一个方法,在 NSMutableArray 中添加 n 个 NSNull 对象。

还有:

这个逻辑有一个静态方法,它将接受一个点数组并给你两个数组:-

  1. 第一个控制点数组。

  2. 第二个控制点数组。

这些数组将保存您在第一个数组中传递的两个点之间的每条曲线的第一个和第二个控制点。

在我的例子中,我已经有了所有的点,我可以通过它们绘制曲线。

在您绘制的情况下,您将需要一些方法来提供一组您希望平滑曲线通过的点。

并通过调用 setNeedsDisplay 刷新并在第一个数组中的两个相邻点之间绘制只是 UIBezierPath 的样条线。 并从两个控制点数组索引中获取控制点。

您的问题是,在移动所有关键点时很难理解。

你可以做的是: 只需在移动手指的同时在前一点和当前点之间画直线。 线条会非常小,以至于肉眼无法看到,除非您放大,否则它们是很小的直线。

更新

任何对上述链接的Objective C实现感兴趣的人都可以参考

thisGitHub 仓库。

我以前写过它,它不支持 ARC,但您可以轻松地对其进行编辑并删除一些发布和自动释放调用,并使其与 ARC 一起使用。

这只是为一组想要使用贝塞尔样条连接的点生成两个控制点数组。

【讨论】:

  • 在我搬家时可以使用吗?如果是这样我如何将这段代码变成objective-c代码???
  • @BDGapps 不,此代码仅适用于完整的点集。它不适用于插入交互式草图数据。
【解决方案5】:

不需要写这么多代码。

只需参考ios freehand drawing tutorial;画起来真的很流畅,还有缓存机制,即使你一直画,性能也不会下降。

【讨论】:

  • 这是一个很棒的教程,如果我有无限的时间,我会修改这个想法,因为我确信它可以成为一个非常优秀的解决方案的基础。为了快速修复,@joshua 给出的 Catmull-Rom 答案对我有用。如果我有时间,我会处理的问题是,这些选项在快速绘制时提供了很好的平滑效果,但在缓慢绘制时,路径中的点太多以至于平滑可以忽略不计。算法需要在平滑之前简化路径。
【解决方案6】:

我发现 a pretty nice tutorial 描述了对贝塞尔曲线图的轻微修改,它确实可以很好地平滑边缘。这基本上就是 Caleb 上面提到的关于将连接端点与控制点放在同一条线上的内容。这是我一段时间以来读过的最好的教程之一。它附带了一个完整的 Xcode 项目。

【讨论】:

    【解决方案7】:

    @Rakesh 是绝对正确的——如果你只想要一条曲线,你不需要使用 Catmull-Rom 算法。他建议的链接正是如此。所以这是对his answer 的补充。

    下面的代码使用 Catmull-Rom 算法和粒度,但绘制了一条四曲线(为您计算控制点)。这基本上是在 Rakesh 建议的 ios freehand drawing tutorial 中所做的,但是在一个独立的方法中,您可以放置​​在任何地方(或在 UIBezierPath 类别中)并获得开箱即用的四曲线样条。

    您确实需要将CGPoint 的数组包裹在NSValue

    + (UIBezierPath *)quadCurvedPathWithPoints:(NSArray *)points
    {
        UIBezierPath *path = [UIBezierPath bezierPath];
    
        NSValue *value = points[0];
        CGPoint p1 = [value CGPointValue];
        [path moveToPoint:p1];
    
        if (points.count == 2) {
            value = points[1];
            CGPoint p2 = [value CGPointValue];
            [path addLineToPoint:p2];
            return path;
        }
    
        for (NSUInteger i = 1; i < points.count; i++) {
            value = points[i];
            CGPoint p2 = [value CGPointValue];
    
            CGPoint midPoint = midPointForPoints(p1, p2);
            [path addQuadCurveToPoint:midPoint controlPoint:controlPointForPoints(midPoint, p1)];
            [path addQuadCurveToPoint:p2 controlPoint:controlPointForPoints(midPoint, p2)];
    
            p1 = p2;
        }
        return path;
    }
    
    static CGPoint midPointForPoints(CGPoint p1, CGPoint p2) {
        return CGPointMake((p1.x + p2.x) / 2, (p1.y + p2.y) / 2);
    }
    
    static CGPoint controlPointForPoints(CGPoint p1, CGPoint p2) {
        CGPoint controlPoint = midPointForPoints(p1, p2);
        CGFloat diffY = abs(p2.y - controlPoint.y);
    
        if (p1.y < p2.y)
            controlPoint.y += diffY;
        else if (p1.y > p2.y)
            controlPoint.y -= diffY;
    
        return controlPoint;
    }
    

    结果如下:

    【讨论】:

    • 这是最好的答案。只需复制粘贴给定的代码,它就像魅力一样工作
    • 任何关于如何改进结果的想法,如果 f.e.会有几个点上升?现在该代码会产生波浪状的曲线?!?
    • 这是生成平滑图表的最简单方法。
    • 曲线上的点稳定上升或下降时切线不正确。
    • 这是我使用此代码得到的结果...它对我不起作用i.imgur.com/cbpwEX0.jpg
    【解决方案8】:

    这里有一些不错的答案,尽管我认为它们要么是偏离的(user1244109 的答案只支持水平切线,对通用曲线没有用处),要么过于复杂(对不起 Catmull-Rom 粉丝)。

    我使用四倍贝塞尔曲线以更简单的方式实现了这一点。这些需要一个起点、一个终点和一个控制点。很自然的做法可能是使用接触点作为起点和终点。 不要这样做! 没有合适的控制点可供使用。相反,试试这个想法:使用触摸点作为控制点,中点作为起点/终点。这样可以保证你有正确的切线,而且代码很简单。算法如下:

    1. “着陆”点是路径的起点,并将location 存储在prevPoint 中。
    2. 对于每个拖动的位置,计算midPoint,即currentPointprevPoint 之间的点。
      1. 如果这是第一个拖动的位置,请将currentPoint 添加为线段。
      2. 对于未来的所有点,添加一条midPoint 处终止的四边形曲线,并使用prevPoint 作为控制点。这将创建一个从前一点到当前点轻轻弯曲的线段。
    3. currentPoint 存储在prevPoint 中,然后重复#2 直到拖动结束。
    4. 将终点添加为另一个直线段,以完成路径。

    这会产生非常好看的曲线,因为使用 midPoints 可以保证曲线在端点处是平滑的切线(参见附图)。

    Swift 代码如下所示:

    var bezierPath = UIBezierPath()
    var prevPoint: CGPoint?
    var isFirst = true
    
    override func touchesBegan(touchesSet: Set<UITouch>, withEvent event: UIEvent?) {
        let location = touchesSet.first!.locationInView(self)
        bezierPath.removeAllPoints()
        bezierPath.moveToPoint(location)
        prevPoint = location
    }
    
    override func touchesMoved(touchesSet: Set<UITouch>, withEvent event: UIEvent?) {
        let location = touchesSet.first!.locationInView(self)
    
        if let prevPoint = prevPoint {
            let midPoint = CGPoint(
                x: (location.x + prevPoint.x) / 2,
                y: (location.y + prevPoint.y) / 2,
            )
            if isFirst {
                bezierPath.addLineToPoint(midPoint)
            else {
                bezierPath.addQuadCurveToPoint(midPoint, controlPoint: prevPoint)
            }
            isFirst = false
        }
        prevPoint = location
    }
    
    override func touchesEnded(touchesSet: Set<UITouch>, withEvent event: UIEvent?) {
        let location = touchesSet.first!.locationInView(self)
        bezierPath.addLineToPoint(location)
    }
    

    或者,如果您有一个点数组并想一次性构建UIBezierPath

    var points: [CGPoint] = [...]
    var bezierPath = UIBezierPath()
    var prevPoint: CGPoint?
    var isFirst = true
    
    // obv, there are lots of ways of doing this. let's
    // please refrain from yak shaving in the comments
    for point in points {
        if let prevPoint = prevPoint {
            let midPoint = CGPoint(
                x: (point.x + prevPoint.x) / 2,
                y: (point.y + prevPoint.y) / 2,
            )
            if isFirst {
                bezierPath.addLineToPoint(midPoint)
            }
            else {
                bezierPath.addQuadCurveToPoint(midPoint, controlPoint: prevPoint)
            }
            isFirst = false
        }
        else { 
            bezierPath.moveToPoint(point)
        }
        prevPoint = point
    }
    if let prevPoint = prevPoint {
        bezierPath.addLineToPoint(prevPoint)
    }
    

    这是我的笔记:

    【讨论】:

    • 很好的答案,希望我能多次投票。这与大多数其他算法的最大区别在于它只在开始时调用一次 bezierPath.moveToPoint。如果您想在绘制路径后保留路径并对其做一些有用的事情,您将拥有一个不错的、封闭的 UIBezierPath 来使用。其他人在绘制曲线的每一段之前调用 .moveToPoint。仅用于绘图很好,但如果您想在最后构建一个 CAShapeLayer,则不是很好。
    • @nwales 我一直在错误地使用“牦牛剃须”这个短语!我应该说“自行车脱落”。
    • 谢谢你,科林塔。我发现这非常有用。
    【解决方案9】:

    我尝试了以上所有方法,但无法正常工作。一个答案甚至对我来说产生了一个破碎的结果。在搜索更多内容后,我发现了这个:https://github.com/sam-keene/uiBezierPath-hermite-curve。我没有编写此代码,但我实现了它并且它工作得非常好。只需复制 UIBezierPath+Interpolation.m/h 和 CGPointExtension.m/h。然后你像这样使用它:

    UIBezierPath *path = [UIBezierPath interpolateCGPointsWithHermite:arrayPoints closed:YES];
    

    这确实是一个强大而简洁的整体解决方案。

    【讨论】:

    • 真正简单但功能强大。
    • 顺便说一下,interpolateCGPointsWithHermite:closed: 方法不需要CGPointExtension.m/h 文件。只需复制UIBezierPath+Interpolation.m/h 并删除interpolateCGPointsWithCatmullRom:closed:alpha 方法。
    【解决方案10】:

    斯威夫特:

            let point1 = CGPoint(x: 50, y: 100)
    
            let point2 = CGPoint(x: 50 + 1 * CGFloat(60) * UIScreen.main.bounds.width / 375, y: 200)
    
            let point3 = CGPoint(x: 50 + 2 * CGFloat(60) * UIScreen.main.bounds.width / 375, y: 250)
    
            let point4 = CGPoint(x: 50 + 3 * CGFloat(60) * UIScreen.main.bounds.width / 375, y: 50)
    
            let point5 = CGPoint(x: 50 + 4 * CGFloat(60) * UIScreen.main.bounds.width / 375, y: 100)
    
    
            let points = [point1, point2, point3, point4, point5]
    
            let bezier = UIBezierPath()
    
    
            let count = points.count
    
            var prevDx = CGFloat(0)
            var prevDy = CGFloat(0)
    
            var prevX = CGFloat(0)
            var prevY = CGFloat(0)
    
            let div = CGFloat(7)
    
    
            for i in 0..<count {
                let x = points[i].x
                let y = points[i].y
    
                var dx = CGFloat(0)
                var dy = CGFloat(0)
    
                if (i == 0) {
                    bezier.move(to: points[0])
                    let nextX = points[i + 1].x
                    let nextY = points[i + 1].y
    
                    prevDx = (nextX - x) / div
                    prevDy = (nextY - y) / div
                    prevX = x
                    prevY = y
                } else if (i == count - 1) {
                    dx = (x - prevX) / div
                    dy = (y - prevY) / div
                } else {
    
                    let nextX = points[i + 1].x
                    let nextY = points[i + 1].y
                    dx = (nextX - prevX) / div;
                    dy = (nextY - prevY) / div;
                }
    
                bezier.addCurve(to: CGPoint(x: x, y: y), controlPoint1: CGPoint(x: prevX + prevDx, y: prevY + prevDy), controlPoint2: CGPoint(x: x - dx, y: y - dy))
    
                prevDx = dx;
                prevDy = dy;
                prevX = x;
                prevY = y;
            }
    

    【讨论】:

      【解决方案11】:

      这是 Swift 4/5 中的代码

      func quadCurvedPathWithPoint(points: [CGPoint] ) -> UIBezierPath {
          let path = UIBezierPath()
          if points.count > 1 {
              var prevPoint:CGPoint?
              for (index, point) in points.enumerated() {
                  if index == 0 {
                      path.move(to: point)
                  } else {
                      if index == 1 {
                          path.addLine(to: point)
                      }
                      if prevPoint != nil {
                          let midPoint = self.midPointForPoints(from: prevPoint!, to: point)
                          path.addQuadCurve(to: midPoint, controlPoint: controlPointForPoints(from: midPoint, to: prevPoint!))
                          path.addQuadCurve(to: point, controlPoint: controlPointForPoints(from: midPoint, to: point))
                      }
                  }
                  prevPoint = point
              }
          }
          return path
      }
      
      func midPointForPoints(from p1:CGPoint, to p2: CGPoint) -> CGPoint {
          return CGPoint(x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2)
      }
      
      func controlPointForPoints(from p1:CGPoint,to p2:CGPoint) -> CGPoint {
          var controlPoint = midPointForPoints(from:p1, to: p2)
          let  diffY = abs(p2.y - controlPoint.y)
          if p1.y < p2.y {
              controlPoint.y = controlPoint.y + diffY
          } else if ( p1.y > p2.y ) {
              controlPoint.y = controlPoint.y - diffY
          }
          return controlPoint
      }
      

      【讨论】:

        【解决方案12】:

        我的灵感来自于 u/User1244109 的回答……但它仅在点每次都不断上下波动时才有效,因此每个点都应由 S 曲线连接。

        我根据他的回答构建了包含自定义逻辑以检查该点是否将成为局部最小值,如果是,则使用 S 曲线,否则根据它之前和之后的点,或者它是否应该切线弯曲,如果是这样,我使用切线的交点作为控制点。

        #define AVG(__a, __b) (((__a)+(__b))/2.0)
        
        -(UIBezierPath *)quadCurvedPathWithPoints:(NSArray *)points {
            
            if (points.count < 2) {
                return [UIBezierPath new];
            }
            
            UIBezierPath *path = [UIBezierPath bezierPath];
            
            CGPoint p0 = [points[0] CGPointValue];
            CGPoint p1 = [points[1] CGPointValue];
            
            [path moveToPoint:p0];
            
            if (points.count == 2) {
                [path addLineToPoint:p1];
                return path;
            }
            
            for (int i = 1; i <= points.count-1; i++) {
                
                CGPoint p1 = [points[i-1] CGPointValue];
                CGPoint p2 = [points[i] CGPointValue];//current point
                CGPoint p0 = p1;
                CGPoint p3 = p2;
                if (i-2 >= 0) {
                    p0 = [points[i-2] CGPointValue];
                }
                if (i+1 <= points.count-1) {
                    p3 = [points[i+1] CGPointValue];
                }
            
                if (p2.y == p1.y) {
                    [path addLineToPoint:p2];
                    continue;
                }
                
                float previousSlope = p1.y-p0.y;
                float currentSlope = p2.y-p1.y;
                float nextSlope = p3.y-p2.y;
                
                BOOL shouldCurveUp = NO;
                BOOL shouldCurveDown = NO;
                BOOL shouldCurveS = NO;
                BOOL shouldCurveTangental = NO;
                
                if (previousSlope < 0) {//up hill
                    
                    if (currentSlope < 0) {//up hill
                        
                        if (nextSlope < 0) {//up hill
                            
                            shouldCurveTangental = YES;
                            
                        } else {//down hill
                            
                            shouldCurveUp = YES;
                            
                        }
                        
                    } else {//down hill
                        
                        if (nextSlope > 0) {//down hill
                            
                            shouldCurveUp = YES;
                            
                        } else {//up hill
                            
                            shouldCurveS = YES;
                            
                        }
                        
                    }
                    
                } else {//down hill
                    
                    if (currentSlope > 0) {//down hill
                        
                        if (nextSlope > 0) {//down hill
                            
                            shouldCurveTangental = YES;
                            
                        } else {//up hill
                            
                            shouldCurveDown = YES;
                            
                        }
                        
                    } else {//up hill
                        
                        if (nextSlope < 0) {//up hill
                            
                            shouldCurveDown = YES;
                            
                        } else {//down hill
                            
                            shouldCurveS = YES;
                            
                        }
                        
                    }
                    
                }
                
                if (shouldCurveUp) {
                    [path addQuadCurveToPoint:p2 controlPoint:CGPointMake(AVG(p1.x, p2.x), MIN(p1.y, p2.y))];
                }
                if (shouldCurveDown) {
                    [path addQuadCurveToPoint:p2 controlPoint:CGPointMake(AVG(p1.x, p2.x), MAX(p1.y, p2.y))];
                }
                if (shouldCurveS) {
                    CGPoint midPoint = midPointForPoints(p1, p2);
                    [path addQuadCurveToPoint:midPoint controlPoint:controlPointForPoints(midPoint, p1)];
                    [path addQuadCurveToPoint:p2 controlPoint:controlPointForPoints(midPoint, p2)];
                }
                if (shouldCurveTangental) {
                    
                    float nextTangent_dy = p3.y-p2.y;
                    float nextTangent_dx = p3.x-p2.x;
                    float previousTangent_dy = p1.y-p0.y;
                    float previousTangent_dx = p1.x-p0.x;
                    
                    float nextTangent_m = 0;
                    if (nextTangent_dx != 0) {
                        nextTangent_m = nextTangent_dy/nextTangent_dx;
                    }
                    float previousTangent_m = 0;
                    if (nextTangent_dx != 0) {
                        previousTangent_m = previousTangent_dy/previousTangent_dx;
                    }
                    
                    if (isnan(previousTangent_m) ||
                        isnan(nextTangent_m) ||
                        nextTangent_dx == 0 ||
                        previousTangent_dx == 0) {//division by zero would have occured, etc
                        [path addLineToPoint:p2];
                    } else {
                        
                        CGPoint nextTangent_start = CGPointMake(p1.x, (nextTangent_m*p1.x) - (nextTangent_m*p2.x) + p2.y);
                        CGPoint nextTangent_end = CGPointMake(p2.x, (nextTangent_m*p2.x) - (nextTangent_m*p2.x) + p2.y);
                        
                        CGPoint previousTangent_start = CGPointMake(p1.x, (previousTangent_m*p1.x) - (previousTangent_m*p1.x) + p1.y);
                        CGPoint previousTangent_end = CGPointMake(p2.x, (previousTangent_m*p2.x) - (previousTangent_m*p1.x) + p1.y);
                        
                        NSValue *tangentIntersection_pointValue = [self intersectionOfLineFrom:nextTangent_start to:nextTangent_end withLineFrom:previousTangent_start to:previousTangent_end];
                        
                        if (tangentIntersection_pointValue) {
                            [path addQuadCurveToPoint:p2 controlPoint:[tangentIntersection_pointValue CGPointValue]];
                        } else {
                            [path addLineToPoint:p2];
                        }
                        
                    }
                    
                }
                
            }
            
            return path;
            
        }
        
        -(NSValue *)intersectionOfLineFrom:(CGPoint)p1 to:(CGPoint)p2 withLineFrom:(CGPoint)p3 to:(CGPoint)p4 {//from https://stackoverflow.com/a/15692290/2057171
            CGFloat d = (p2.x - p1.x)*(p4.y - p3.y) - (p2.y - p1.y)*(p4.x - p3.x);
            if (d == 0)
                return nil; // parallel lines
            CGFloat u = ((p3.x - p1.x)*(p4.y - p3.y) - (p3.y - p1.y)*(p4.x - p3.x))/d;
            CGFloat v = ((p3.x - p1.x)*(p2.y - p1.y) - (p3.y - p1.y)*(p2.x - p1.x))/d;
            if (u < 0.0 || u > 1.0)
                return nil; // intersection point not between p1 and p2
            if (v < 0.0 || v > 1.0)
                return nil; // intersection point not between p3 and p4
            CGPoint intersection;
            intersection.x = p1.x + u * (p2.x - p1.x);
            intersection.y = p1.y + u * (p2.y - p1.y);
            
            return [NSValue valueWithCGPoint:intersection];
        }
        
        static CGPoint midPointForPoints(CGPoint p1, CGPoint p2) {
            return CGPointMake((p1.x + p2.x) / 2, (p1.y + p2.y) / 2);
        }
        
        static CGPoint controlPointForPoints(CGPoint p1, CGPoint p2) {
            CGPoint controlPoint = midPointForPoints(p1, p2);
            CGFloat diffY = fabs(p2.y - controlPoint.y);
            
            if (p1.y < p2.y)
                controlPoint.y += diffY;
            else if (p1.y > p2.y)
                controlPoint.y -= diffY;
            
            return controlPoint;
        }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2012-06-09
          • 1970-01-01
          • 2017-06-10
          • 2017-01-14
          • 2015-11-18
          • 2023-03-09
          • 2013-01-20
          相关资源
          最近更新 更多