绘制平滑曲线 - 所需方法

60
在iOS绘图应用程序中移动时如何平滑一组点?我尝试了UIBezierPaths,但所有交叉点都有锯齿状的边缘,当我将点从1,2,3,4移动到2,3,4,5时。我听说过样条曲线和其他类型的曲线,但我对iPhone编程还比较新,不知道如何在我的quartz绘图应用程序中编写它。一个完整的示例将是非常感激的,我已经花了几个星期困在这个问题上,似乎找不到任何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


你能给我们展示一下这个不规则结尾吗? - Tommy
有一个来自苹果的示例代码叫做QuartzDemo。 - IPaPa
如果你想深入了解这个主题而不仅仅是看示例代码(从长远来看这将是更好的选择),那么我强烈建议获取一份《计算机图形学数学元素》(Rogers和Adams)的副本。虽然它最初是在点阵打印机时代编写的,但参数曲线、样条、贝塞尔等的处理非常出色,并附有可靠的伪代码。 - Cruachan
我和一个同事最近在贝塞尔曲线上写了一篇博客文章:http://www.scottlogic.co.uk/2011/08/bezier-demo/。希望这篇文章能让你更好地理解贝塞尔曲线的工作原理,帮到你。 - Simon Withington
Tony Ngo的平滑线条绘制博客详细介绍了他的实现方法。我按照他的技术在企业iPad应用程序中实现了它,效果非常好。 - Ken W
显示剩余6条评论
12个回答

66

我在一个项目中实现了类似的功能。我的解决方案是使用Catmull-Rom样条曲线而不是Bezier样条曲线。这些曲线提供了一条通过一组点的非常平滑的曲线,而不是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的类别实现的,并且对我非常有效。

原始路径为红色,平滑路径为绿色。


2
你实现了UIBezierCurve+Smoothing.h的代码有误。如果要定义一个类别,应该是@interface UIBezierCurve (Smoothing),并且你还需要声明方法。如果你需要更具体的帮助,我们应该转到聊天中讨论。 - Joshua Weinberg
1
无法感谢。太妙了。我真的很想知道为什么这个答案没有被接受。这是我得到的最好的平滑线条的答案。 - Vignesh
你好,能否请您查看一下这个链接:https://dev59.com/a3rZa4cB1Zd3GeqP7dUj 并指导我如何继续进行。另外,您上面的代码仅在触摸结束后尝试平滑曲线。如果我想在触摸移动时进行平滑处理怎么办?请帮忙。 - Ranjit
1
你好@JoshuaWeinberg,能否展示一下你的touches began、touches moved和touches ended代码。 - Ranjit
2
这比quadCurvedPathWithPoints解决方案好多了。 - Gamma-Point
显示剩余13条评论

39

这里有一些好的回答,但我认为它们要么偏离了主题(user1244109的答案只支持水平切线,对于通用曲线没有用),要么过于复杂(抱歉,Catmull-Rom的粉丝们)。

我以一种更简单的方式实现了这个功能,使用二次贝塞尔曲线。它们需要一个起点、一个终点和一个控制点。自然的想法可能是使用触摸点作为起点和终点。不要这样做!没有适当的控制点可供使用。相反,尝试这个想法:将触摸点用作控制点,将中点用作起点/终点。这样,您就保证了正确的切线,并且代码非常简单。以下是算法:

  1. "touch down"点是路径的起点,将location存储在prevPoint中。
  2. 对于每个拖动的位置,计算midPoint,即currentPointprevPoint之间的点。
    1. 如果这是第一个拖动的位置,则将currentPoint添加为直线段。
    2. 对于将来的所有点,添加一个二次曲线,以midPoint为终点,并使用prevPoint作为控制点。这将创建一个从前一个点到当前点缓慢弯曲的线段。
  3. currentPoint存储在prevPoint中,并重复#2,直到拖动结束。
  4. 添加最后一个点作为另一个直线段,完成路径。

这样可以得到非常漂亮的曲线,因为使用中点保证了曲线在端点处是平滑的切线(请参见附图)。

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

以下是我的笔记:

算法示例


牦牛剃毛 -> https://medium.com/@firehoseproject/a-guide-to-yak-shaving-your-code-d30f98dc759 - nwales
很棒的答案,希望我能多次点赞。这个算法与大多数其他算法的最大区别在于它只在开始时调用bezierPath.moveToPoint一次。如果你想在绘制后保留并做一些有用的事情,你将拥有一个漂亮的、封闭的UIBezierPath来处理。其他算法在绘制曲线的每个段之前都会调用.moveToPoint。对于仅仅绘制来说还好,但如果你想在最后构建一个CAShapeLayer就不太好了。 - jday
@nwales 我一直错误地使用了“剃牦牛”的说法!我应该说“涂自行车棚”。 - colinta
谢谢您,colinta。我觉得这非常有用。 - David Spry
我创建了一个快速应用程序来说明。连接控制点的线与连接前后点的线平行。控制点向量的长度似乎应该是所控制段的长度的1/3左右。https://www.youtube.com/watch?v=P9vgRbiYqag - Victor Engel
显示剩余3条评论

33

@Rakesh完全正确-如果你只想要一条曲线,就不需要使用Catmull-Rom算法。他建议的链接正是如此。因此,这里是他的答案的补充。

以下代码不使用Catmull-Rom算法和粒度,但绘制了一个二次曲线(控制点已为您计算)。这本质上就是Rakesh建议的ios手绘教程中所做的事情,但是它是一个独立的方法,您可以将其放在任何地方(或在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;
}

这是结果: 在此输入图片描述

1
有没有任何想法可以改进结果,例如如果有几个点上升?现在那段代码会产生波浪形曲线?!? - Georg
这段代码唯一的问题是,如果数据输入是100、200、100、50,那么线段从100到200会正确地画出弧形,但是在回来时会出现问题。我觉得你需要跟踪前面的x个点,其中x是局部最大值的索引,以及当前正在绘制的点之前的x个索引值。 - jsetting32
4
曲线上的点稳定上升或下降时,切线是不正确的。 - Gamma-Point
这是我答案中提供的相同代码(请参考链接)。但很高兴看到有所改进。 :) - YogiAR
1
这是我使用这段代码得到的结果...对我来说它不起作用。http://i.imgur.com/cbpwEX0.jpg - Reza.Ab
显示剩余8条评论

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];
}
请注意,path1 结束于 point2path2 开始于 point2,且控制点 2 和 3 与 point2 共用相同的 Y 值 commonY。你可以随意更改代码中的任何值;只要这三个点都在同一条直线上,两条路径就会平滑连接。(在上面的代码中,该线是 y = commonY。该线不必与 X 轴平行;以这种方式看到这些点共线只是更容易。)

下面是代码绘制的图像:

two paths joined smoothly

看了你的代码后,发现你认为控制点是曲线上的点。然而在贝塞尔曲线中,控制点通常不在曲线上。由于你从曲线中获取了控制点,因此控制点和交点不是共线的,所以路径不能平滑连接。


如何在上面的代码中实现一个不断移动的点集? - BDGapps
2
@BDGapps 数学!(抱歉回答有些轻浮,但是评论区不适合讨论您的非平凡问题。) - MechEthan
@BDGapps 你的问题似乎归结于控制点的放置位置。如上所述,它们需要落在交点处曲线切线上,但是在这条线上的哪个位置?这在某种程度上取决于你自己。通过对交点处的导数进行计算,你可以确定切线的位置 -- 这将提供很多有关如何解决这个问题的信息。最终,yAak 是正确的 -- 你需要做一些数学计算,并且需要根据你希望曲线的外观作出一些决策。 - Caleb
这里描述的Caleb的想法已经在这里实现了:http://mobile.tutsplus.com/tutorials/iphone/ios-sdk_freehand-drawing/(在文章底部查看示例结果) - Aky
1
共线性观察...哇,这简化了很多。谢谢。 - codrut

7

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

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

现在有很多方法可以获得平滑的线。

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

1
非常好!你能分享一下这两种方法及其参数的一些评论吗? - Zenuka

4
为了实现这一点,我们需要使用这种方法。BezierSpline该代码是用C#编写的,用于生成贝塞尔样条的控制点数组。我将此代码转换为Objective C,对我非常有效。
将代码从C#转换为Objective C。 逐行理解C#代码,即使您不知道C#,您也必须知道C++/Java吧?
在转换时:
  1. 将此处使用的 Point struct 替换为 CGPoint。

  2. 将 Point 数组替换为 NSMutableArray,并在其中存储 NSValue 包装的 CGPoint。

  3. 将所有 double 数组替换为 NSMutableArrays,并在其中存储 NSNumber 包装的 double。

  4. 在访问数组元素时,使用 objectAtIndex: 方法代替下标。

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

    请记住,NSMutableArray 是一个链表,而 C# 使用的是动态数组,因此它们已经有现有的索引。 在您的情况下,在 NSMutableArray 为空时,不能像 C# 代码那样在随机的索引处存储对象。 他们有时会在这个 C# 代码中,在索引 0 之前填充索引 1,这样做是可以的,因为索引 1 存在。 在这里的 NSMutabelArrays 中,如果想要调用 replaceObject,则应该存在索引 1。 因此,在存储任何内容之前,请编写一个方法,在 NSMutableArray 中添加 n 个 NSNull 对象。

另外:

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

  1. 第一个控制点数组。

  2. 第二个控制点数组。

这些数组将保存你在第一个数组中传递的两个点之间每条曲线的第一和第二控制点。
在我的情况下,我已经有了所有的点,可以通过它们来绘制曲线。
在你的情况下,在绘制时,你需要以某种方式提供一组点,通过这些点你想要让平滑曲线通过。
通过调用setNeedsDisplay并在第一个数组中相邻点之间绘制UIBezierPath来刷新,并从两个控制点数组按索引取控制点。
问题在于,在移动时很难理解需要采取哪些关键点。
你可以这样做: 简单地在移动手指时,在前一个点和当前点之间画直线。 这些线段非常小,除非你放大,否则肉眼是看不到它们是由许多小的直线组成的。
更新
对于上面链接的Objective C实现感兴趣的人可以参考GitHub存储库。
我写了一些时间,它不支持ARC,但你可以轻松地编辑它并删除一些释放和autorelease调用,并使用ARC使其正常工作。
这个程序会为一组想要使用贝塞尔样条连接的点生成两个控制点数组。

这段代码在我移动时能用吗?如果可以,我该如何将该代码转换为Objective-C代码? - BDGapps
@BDGapps 不,这段代码仅适用于完整的点集。它不适用于插值交互式绘制的数据。 - Adam Lockhart

2
不需要写这么多代码。只需参考iOS自由绘图教程即可,它可以使绘图更加平滑,同时还有缓存机制,使得即使你一直画画,性能也不会下降。

2
这是一个非常好的教程,如果我有无限的时间,我会尝试这个想法,因为我相信它可以成为一个非常优秀的解决方案的基础。对于快速修复,@joshua 给出的 Catmull-Rom 答案对我很有用。如果我有时间,我会处理的问题是,这些选项在快速绘制时提供了漂亮的平滑结果,但在缓慢绘制时,路径中有太多点,使得平滑效果微不足道。算法需要在平滑之前简化路径。 - Brett Donald

2

以下是 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
}

2

Swift:

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

1

我发现了一个非常好的教程,它描述了对Bezier曲线绘制的轻微修改,这确实可以很好地平滑边缘。本质上,它就是Caleb在上面提到的将连接端点放在控制点所在直线上的方法。这是我最近阅读过的最好的教程之一(关于任何事情)。而且它还附带了一个完全可用的Xcode项目。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接