绘制平滑曲线 - 需要的方法

时间:2012-01-02 15:46:05

标签: iphone objective-c ios ipad bezier

如何在移动时平滑iOS绘图应用程序中的一组点?我尝试过UIBezier路径,但是当我只移动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];
}

phone screen

12 个答案:

答案 0 :(得分:62)

我刚刚在我正在开发的项目中实现了类似的东西。我的解决方案是使用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上的类别实现的,并且对我来说非常好。

The original path is in red, the smoothed path is in green.

答案 1 :(得分:29)

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

下面的代码 NOT 使用Catmull-Rom算法&amp;粒度,但绘制四条曲线(控制点是为您计算的)。这基本上是在Rakesh建议的ios freehand drawing tutorial中完成的,但是在一个独立的方法中,您可以放在任何地方(或在UIBezierPath类别中)并获得开箱即用的四边形样条线。 / p>

您需要在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;
}

结果如下: enter image description here

答案 2 :(得分:21)

使两条贝塞尔曲线平滑连接的关键是相关控制点和曲线上的起点/终点必须共线。将控制点和端点视为形成与端点处的曲线相切的线。如果一条曲线在另一条曲线的另一端结束的同一点开始,并且如果它们在该点处具有相同的切线,则曲线将是平滑的。这里有一些代码可以说明:

- (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共享相同的Y值commonY,与point2。您可以根据需要更改代码中的任何值;只要这三个点全部落在同一条线上,两条路径就会顺利连接。 (在上面的代码中,行是y = commonY。该行不必与X轴平行;只是更容易看到这些点是共线的。)

以上是上面代码所绘制的图像:

two paths joined smoothly

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

答案 3 :(得分:18)

这里有一些好的答案,虽然我认为它们要么是关闭的(user1244109的答案只支持水平切线,对通用曲线没用),或者过于复杂(对不起Catmull-Rom粉丝)。

我使用Quad bezier曲线以更简单的方式实现了这一点。这些需要一个起点,一个终点和一个控制点。做的自然事情可能是使用触点作为开始&amp;终点。 不要这样做!没有合适的控制点可供使用。相反,尝试这个想法:使用触摸点作为控制点,使用 midpoints 作为起点/终点。你可以保证以这种方式使用正确的切线,而且代码很简单。这是算法:

  1. “触地”点是路径的起点,并将location存储在prevPoint中。
  2. 对于每个拖动的位置,计算midPointcurrentPointprevPoint之间的点。
    1. 如果这是第一个被拖动的位置,请将currentPoint添加为线段。
    2. 对于将来的所有要点,在midPoint处添加终止的四边形曲线,并使用prevPoint作为控制点 。这将创建一个从上一个点到当前点轻轻弯曲的线段。
  3. currentPoint存储在prevPoint中,并重复#2直到拖动结束。
  4. 将最终点添加为另一个直线段,以完成路径。
  5. 这样可以产生非常好看的曲线,因为使用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)
    }
    

    以下是我的笔记:

    example of algorithm

答案 4 :(得分:7)

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

  1. 一般情况下,UIKit不会给出相等距离的分数。
  2. 我们需要计算两个CGPoints之间的中间点[已用Touch移动方法捕获]
  3. 现在要获得流畅的线条,有很多方法。

    有时我们可以通过应用二次多项式或三次多项式或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;
    }
    

答案 5 :(得分:4)

为实现这一目标,我们需要使用此方法。 BezierSpline 代码在C#中生成bezier样条曲线的控制点数组。我将此代码转换为Objective C,它对我来说非常出色。

将代码从C#转换为Objective C. 逐行理解C#代码,即使你不了解C#,你一定要了解C ++ / Java吗?

转换时:

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

  2. 用NSMutableArray替换Point数组并存储包含CGPoints的NSvalues。

  3. 用NSMutableArrays替换所有双数组,并将NSNumber包装为double。

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

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

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

  6. 另外:

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

    1. 第一个控制点数组。

    2. 第二个控制点数组。

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

      在我的情况下,我已经拥有了所有的分数,我可以通过它们绘制曲线。

      在你绘制的情况下,你需要一些如何提供一组你希望平滑曲线通过的点。

      并通过调用setNeedsDisplay进行刷新并绘制样条曲线,该样条曲线在第一个数组中的两个相邻点之间只是UIBezierPath。 并从控制点阵列索引控制点。

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

      你能做的是: 只需在移动手指的同时,在前一点和当前点之间绘制直线。 线条会很小,肉眼看不到它们是小的小直线,除非你放大。

      <强>更新

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

      this GitHub回购。

      我在某个时候写过它并且它不支持ARC,但你可以轻松地编辑它并删除一些发布和自动释放调用,并让它与ARC一起工作。

      这个只为一组点生成两个控制点阵列,这些点想要使用贝塞尔样条连接。

答案 6 :(得分:2)

不需要编写这么多代码。

请参阅ios freehand drawing tutorial;它确实使绘图平滑,也有缓存机制,即使你不断绘制,性能也不会降低。

答案 7 :(得分:1)

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

答案 8 :(得分:1)

我尝试了以上所有方法,但无法使其正常运行。其中一个答案会给我带来破碎的结果。经过更多搜索,我发现了这个:https://github.com/sam-keene/uiBezierPath-hermite-curve。我没有编写这段代码,但是我实现了它并且它的工作非常好。只需复制UIBezierPath + Interpolation.m / h和CGPointExtension.m / h即可。然后你就像这样使用它:

Using client secrets: D:\Programs\Python35\share/youtube_upload/client_secrets.json
Using credentials file: C:\Users\Name\.youtube-upload-credentials.json
Using category: Education (id=27)
Start upload: render\video\DCCA.mp4
Video URL: https://www.youtube.com/watch?v=*censored*
[RequestError] Server response: {
 "error": {
  "errors": [
   {
    "domain": "youtube.thumbnail",
    "reason": "forbidden",
    "message": "The authenticated user doesnt have permissions to upload and set custom video thumbnails.",
    "locationType": "header",
    "location": "Authorization"
   }
  ],
  "code": 403,
  "message": "The authenticated user doesnt have permissions to upload and set custom video thumbnails."
 }
}

这是一个非常强大而整洁的解决方案。

答案 9 :(得分:1)

夫特:

        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;
        }

答案 10 :(得分:0)

这是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
}

答案 11 :(得分:0)

我从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;
}