Рисование гладких кривых - необходимые методы

Как сгладить набор точек в приложении для рисования iOS во время движения? Я пробовал 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];
}

экран телефона

10 ответов

Я только что реализовал нечто подобное в проекте, над которым я работаю. Мое решение состояло в том, чтобы использовать сплайн Catmull-Rom вместо сплайнов Безье. Они обеспечивают очень плавную кривую ЧЕРЕЗ набор точек, а не безлинейный сплайн вокруг точек.

// 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 основана на некотором коде Эрики Садун в одной из ее книг, я немного изменил его, чтобы получить полную сглаженную кривую. Это реализовано как категория на UIBezierPath и отлично сработало для меня.

Исходный путь выделен красным, сглаженный - зеленым

Здесь есть несколько хороших ответов, хотя я думаю, что они либо далеки (ответ user1244109 поддерживает только горизонтальные касательные, бесполезные для общих кривых), либо слишком сложны (извините, поклонники Catmull-Rom).

Я реализовал это гораздо более простым способом, используя кривые Безье. Им нужны начальная точка, конечная точка и контрольная точка. Естественно, можно использовать точки касания в качестве начальной и конечной точек. Не делай этого! Нет подходящих контрольных точек для использования. Вместо этого попробуйте эту идею: используйте точки касания в качестве контрольных точек, а средние точки в качестве начальной / конечной точек. Таким образом, вы гарантированно получите правильные касательные, а код просто глуп. Вот алгоритм:

  1. Точка "приземления" - это начало пути, и магазин location в prevPoint,
  2. Для каждого перетаскиваемого местоположения рассчитайте midPoint, точка между currentPoint а также prevPoint,
    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)
}

Вот мои заметки:

пример алгоритма

@Rakesh абсолютно прав - вам не нужно использовать алгоритм Catmull-Rom, если вы просто хотите изогнутую линию. И ссылка, которую он предложил, делает именно это. Итак, вот дополнение к его ответу.

Приведенный ниже код НЕ использует алгоритм и гранулярность Catmull-Rom, но рисует изогнутую квадратом линию (контрольные точки рассчитываются для вас). По сути, это то, что сделано в учебнике рисования от руки ios, предложенном Ракешем, но в автономном методе, который вы можете отбросить куда угодно (или в категорию UIBezierPath) и получить изогнутый квадратом сплайн из коробки.

Вам нужно иметь массив CGPointзавернут в NSValue"s

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

Вот результат:

Ключом к плавному объединению двух кривых Безье является то, что соответствующие контрольные точки и начальные / конечные точки на кривых должны быть коллинеарными. Представьте, что контрольная точка и конечная точка образуют линию, которая касается кривой в конечной точке. Если одна кривая начинается в той же точке, где заканчивается другая, и если они обе имеют одинаковую касательную в этой точке, кривая будет гладкой. Вот немного кода для иллюстрации:

- (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 заканчивается в point2, path2 начинается в point2и контрольные точки 2 и 3 имеют одинаковое значение Y, commonY, с point2, Вы можете изменить любое значение в коде так, как вам нравится; до тех пор, пока все эти три точки находятся на одной линии, два пути будут плавно соединяться. (В приведенном выше коде строка y = commonY, Линия не должна быть параллельной оси X; просто легче понять, что точки коллинеарны таким образом.)

Вот изображение, которое рисует код выше:

два пути соединились гладко

Посмотрев на ваш код, причина, по которой ваша кривая имеет неровный угол, заключается в том, что вы рассматриваете контрольные точки как точки на кривой. На кривой Безье контрольные точки обычно не находятся на кривой. Поскольку вы берете контрольные точки из кривой, контрольные точки и точки пересечения не коллинеарны, и поэтому пути не соединяются плавно.

Нам нужно кое-что заметить, прежде чем применять какой-либо алгоритм к зафиксированным точкам.

  1. Обычно UIKit не дает очки на равном расстоянии.
  2. Нам нужно вычислить промежуточные точки между двумя CGPoints[которые были получены методом Touch Move]

Теперь, чтобы получить плавную линию, есть так много способов.

В некоторых случаях мы можем добиться применения алгоритмов полиномов второй степени или полиномов третьей степени или 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;
}

Для достижения этого нам необходимо использовать этот метод. BezierSpline код находится в C#, чтобы генерировать массивы контрольных точек для более сплайна Безье. Я преобразовал этот код в Objective C, и он прекрасно работает для меня.

Чтобы преобразовать код из C# в Objective C. понимать код C# построчно, даже если вы не знаете C#, вы должны знать C++/Java?

При конвертации:

  1. Замените структуру Point, используемую здесь, на CGPoint.

  2. Замените массив Point на NSMutableArray и сохраните значения NSvalues, заключающие в себе CGPoints.

  3. Замените все двойные массивы на NSMutableArrays и сохраните обертку NSNumber в ней.

  4. используйте objectAtIndex: метод в случае индекса для доступа к элементам массива.

  5. используйте replaceObjectAtIndex:withObject: для хранения объектов по определенному индексу.

    Помните, что NSMutableArray является связным списком, а C# использует динамические массивы, поэтому у них уже есть существующие индексы. В вашем случае, в NSMutableArray, если он пуст, вы не можете хранить объекты со случайными индексами, как это делает код C#. они иногда в этом коде C# заполняют индекс 1 до индекса 0, и они могут сделать это, так как индекс 1 существует. в NSMutabelArrays здесь, индекс 1 должен быть там, если вы хотите вызвать replaceObject для него. поэтому перед сохранением чего-либо создайте метод, который добавит n объектов NSNull в NSMutableArray.

ТАКЖЕ:

у этой логики есть статический метод, который будет принимать массив точек и давать вам два массива:

  1. массив первых контрольных точек.

  2. массив вторых контрольных точек.

Эти массивы будут содержать первую и вторую контрольную точку для каждой кривой между двумя точками, которые вы проходите в первом массиве.

В моем случае у меня уже были все точки, и я мог нарисовать кривую через них.

В случае, когда вы рисуете, вам понадобится несколько наборов точек, через которые вы хотите, чтобы плавная кривая проходила.

и обновить, вызвав setNeedsDisplay и нарисовать сплайн, который является ничем иным, как UIBezierPath между двумя смежными точками в первом массиве. и взятие контрольных точек из обоих массивов контрольных точек по индексу.

Проблема в вашем случае заключается в том, что трудно понять, перемещая все критические точки, чтобы взять.

Что вы можете сделать: просто перемещая палец, продолжайте рисовать прямые линии между предыдущей и текущей точкой. Линии будут настолько маленькими, что их не будет видно невооруженным глазом, так что они будут маленькими маленькими прямыми линиями, если вы не увеличите масштаб.

ОБНОВИТЬ

Любой, кто заинтересован в реализации Objective C по ссылке выше, может обратиться к

это GitHub репо.

Я написал это некоторое время назад, и он не поддерживает ARC, но вы можете легко отредактировать его, удалить несколько вызовов релиза и автоматического выпуска и заставить его работать с ARC.

Этот просто генерирует два массива контрольных точек для набора точек, к которым каждый хочет присоединиться, используя более сплайновое число.

Не нужно писать так много кода.

Просто обратитесь к руководству по рисованию от ios; это действительно сглаживает рисунок, также есть механизм кэширования, так что производительность не снижается, даже если вы продолжаете рисовать непрерывно.

Вот код в 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
}

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

Я попробовал все вышеперечисленное, но не могу заставить его работать. Один из ответов даже для меня дает неверный результат. После поиска больше я нашел это: https://github.com/sam-keene/uiBezierPath-hermite-curve. Я не писал этот код, но я реализовал его, и он работает очень хорошо. Просто скопируйте UIBezierPath+Interpolation.m/h и CGPointExtension.m/h. Тогда вы используете это так:

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

Это действительно надежное и аккуратное решение в целом.

Я нашел довольно хороший учебник, в котором описывается небольшая модификация рисования кривой Безье, которая действительно очень хорошо сглаживает края. Это, по сути, то, на что ссылается Калеб выше, о размещении конечных точек соединения на одной линии с контрольными точками. Это один из лучших уроков (о чем угодно), которые я читал за последнее время. И это идет с полностью работающим проектом XCode.

Меня вдохновил ответ 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://stackru.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;
}
Другие вопросы по тегам