web-dev-qa-db-fra.com

Dessin de courbes lisses - Méthodes nécessaires

Comment lisser un ensemble de points dans une application de dessin iOS PENDANT LE DÉPLACEMENT? J'ai essayé UIBezierpaths mais tout ce que je reçois sont des extrémités dentelées où elles se croisent, quand je viens de déplacer les points 1,2,3,4 - 2,3,4,5. J'ai entendu parler de courbes splines et de tous les autres types. Je suis assez nouveau dans la programmation iPhone et je ne comprends pas comment le programmer dans mon application de dessin à quartz. Un exemple solide serait grandement apprécié, j'ai passé des semaines à tourner en rond et je n'arrive jamais à trouver de code iOS pour cette tâche. La plupart des articles contiennent un lien vers une Java ou des pages sur wikipedia sur l'ajustement de courbe qui ne fait rien pour moi. Je ne veux pas non plus passer à openGL ES. J'espère que quelqu'un pourra enfin fournir du code pour répondre à cette question qui circule.


C'était mon code pour UIBezierPath qui laissait les bords à l'intersection ///

MISE À JOUR À UNE RÉPONSE CI-DESSOUS

#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

55
BDGapps

Je viens de mettre en œuvre quelque chose de similaire dans un projet sur lequel je travaille. Ma solution a été d'utiliser une spline Catmull-Rom au lieu d'utiliser des splines de Bézier. Ceux-ci fournissent une courbe très lisse à travers un ensemble de points plutôt qu'une spline de Bézier "autour" des points.

// 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

L'implémentation originale de Catmull-Rom est basée sur du code d'Erica Sadun dans l'un de ses livres, je l'ai légèrement modifié pour permettre une courbe entièrement lissée. Ceci est implémenté en tant que catégorie sur UIBezierPath et a très bien fonctionné pour moi.

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

62
Joshua Weinberg

@Rakesh a absolument raison - vous n'avez pas besoin d'utiliser l'algorithme Catmull-Rom si vous voulez juste une ligne courbe. Et le lien qu'il a suggéré fait exactement cela. Voici donc un ajout à sa réponse .

Le code ci-dessous utilise [~ # ~] pas [~ # ~] utilise l'algorithme Catmull-Rom et la granularité, mais dessine une ligne quadruple courbe (contrôle les points sont calculés pour vous). C'est essentiellement ce qui est fait dans le tutoriel de dessin à main levée ios suggéré par Rakesh, mais dans une méthode autonome que vous pouvez déposer n'importe où (ou dans une catégorie UIBezierPath) et obtenir une spline quadruple courbe hors de la boîte .

Vous devez avoir un tableau de CGPoint enveloppé dans 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;
}

Voici le résultat: enter image description here

29
user1244109

La clé pour que deux courbes de Bézier se rejoignent en douceur est que les points de contrôle pertinents et les points de début/fin sur les courbes doivent être colinéaires. Considérez le point de contrôle et le point final comme formant une ligne tangente à la courbe au point final. Si une courbe commence au même point où une autre se termine et si elles ont toutes deux la même ligne tangente à ce point, la courbe sera lisse. Voici un peu de code pour illustrer:

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

Remarquerez que path1 fini à point2, path2 commence à point2 et les points de contrôle 2 et 3 partagent la même valeur Y, commonY, avec point2. Vous pouvez modifier n'importe quelle valeur du code à votre guise; tant que ces trois points tombent tous sur la même ligne, les deux chemins se rejoignent en douceur. (Dans le code ci-dessus, la ligne est y = commonY. Il n'est pas nécessaire que la ligne soit parallèle à l'axe X; il est juste plus facile de voir que les points sont colinéaires de cette façon.)

Voici l'image que le code ci-dessus dessine:

two paths joined smoothly

Après avoir regardé votre code, la raison pour laquelle votre courbe est irrégulière est que vous pensez aux points de contrôle comme des points sur la courbe. Dans une courbe de Bézier, les points de contrôle ne sont généralement pas sur la courbe. Puisque vous prenez les points de contrôle de la courbe, les points de contrôle et le point d'intersection sont pas colinéaires, et les chemins ne se rejoignent donc pas en douceur.

21
Caleb

Quelques bonnes réponses ici, bien que je pense qu'elles sont soit éloignées (la réponse de user1244109 ne prend en charge que les tangentes horizontales, pas utiles pour les courbes génériques), soit trop compliquées (désolé les fans de Catmull-Rom).

J'ai implémenté cela d'une manière beaucoup plus simple, en utilisant des courbes de quadrillage de Bézier. Ceux-ci ont besoin d'un point de départ, d'un point d'arrivée et d'un point de contrôle. La chose naturelle à faire pourrait être d'utiliser les points de contact comme points de début et de fin. Ne faites pas ça! Il n'y a pas de points de contrôle appropriés à utiliser. Au lieu de cela, essayez cette idée: utilisez les points de contact comme points de contrôle et les points médians comme points de début/fin. Vous êtes assuré d'avoir des tangentes appropriées de cette façon, et le code est simple et stupide. Voici l'algorithme:

  1. Le point "touch down" est le début du chemin et stocke location dans prevPoint.
  2. Pour chaque emplacement déplacé, calculez midPoint, le point entre currentPoint et prevPoint.
    1. S'il s'agit du premier emplacement déplacé, ajoutez currentPoint comme segment de ligne.
    2. Pour tous les points futurs, ajoutez une courbe quadruple qui se termine au midPoint, et utilisez le prevPoint comme le point de contrôle . Cela créera un segment qui se courbe doucement du point précédent au point actuel.
  3. Stockez currentPoint dans prevPoint et répétez # 2 jusqu'à la fin du glissement.
  4. Ajoutez le dernier point comme un autre segment droit pour terminer le chemin.

Il en résulte de très belles courbes, car l'utilisation des midPoints garantit que la courbe est une tangente lisse aux extrémités (voir photo ci-jointe).

Le code Swift ressemble à ceci:

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

Ou, si vous avez un tableau de points et que vous voulez construire le UIBezierPath en une seule fois:

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

Voici mes notes:

example of algorithm

20
colinta

Nous devons observer quelque chose avant d'appliquer un algorithme sur les points capturés.

  1. Généralement, UIKit ne donne pas les points à égale distance.
  2. Nous devons calculer les points intermédiaires entre deux CGPoints [qui ont capturé avec la méthode tactile Touch]

Maintenant, pour obtenir une ligne fluide, il existe de nombreuses façons.

Parfois, nous pouvons atteindre cet objectif en appliquant des algorithmes polynomiaux du deuxième degré ou polynomiaux du troisième degré ou 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;
}
7
user1129629

Pour y parvenir, nous devons utiliser cette méthode. BezierSpline le code est en C # pour générer des tableaux de points de contrôle pour une spline de Bézier. J'ai converti ce code en objectif C et cela fonctionne à merveille pour moi.

Pour convertir le code de C # en objectif C. comprendre le code C # ligne par ligne, même si vous ne connaissez pas C #, vous devez connaître C++/Java?

Lors de la conversion:

  1. Remplacez la structure Point utilisée ici par CGPoint.

  2. Remplacez le tableau de points par NSMutableArray et stockez-y NSvalues ​​enveloppant les CGPoints.

  3. Remplacez tous les tableaux doubles par des tableaux NSMutableArray et stockez le doublage NSNumber en double.

  4. utilisez objectAtIndex: méthode en cas d'indice pour accéder aux éléments du tableau.

  5. utilisez replaceObjectAtIndex: withObject: pour stocker des objets à un index spécifique.

    N'oubliez pas que NSMutableArray est une LinkedList et que C # utilise des tableaux dynamiques afin qu'ils aient déjà des index existants. Dans votre cas, dans un NSMutableArray s'il est vide, vous ne pouvez pas stocker d'objets à des indices aléatoires comme le fait le code C #. parfois, dans ce code C #, remplissez l'index 1 avant l'index 0 et ils peuvent le faire tant que l'index 1 existe. dans NSMutabelArrays ici, l'index 1 devrait être là si vous voulez appeler replaceObject dessus. donc avant de stocker quoi que ce soit, créez une méthode qui ajoutera n objets NSNull dans NSMutableArray.

AUSSI :

bien cette logique a une méthode statique qui acceptera un tableau de points et vous donnera deux tableaux: -

  1. tableau des premiers points de contrôle.

  2. tableau de deuxièmes points de contrôle.

Ces tableaux contiendront le premier et le deuxième point de contrôle pour chaque courbe entre deux points que vous passez dans le premier tableau.

Dans mon cas, j'avais déjà tous les points et je pouvais tracer une courbe à travers eux.

Dans votre cas, pendant le dessin, vous aurez besoin de savoir comment fournir un ensemble de points à travers lesquels vous souhaitez qu'une courbe lisse passe.

et actualisez en appelant setNeedsDisplay et dessinez la spline qui n'est rien d'autre que UIBezierPath entre deux points adjacents dans le premier tableau. et prendre des points de contrôle à partir des deux tableaux de points de contrôle par index.

Le problème dans votre cas est que c'est difficile à comprendre lorsque vous déplacez tous les points critiques à prendre.

Ce que vous pouvez faire est: tout en déplaçant le doigt, continuez à tracer des lignes droites entre le point précédent et le point actuel. Les lignes seront si petites qu'elles ne seront pas visibles à l'œil nu qu'elles sont de petites petites lignes droites à moins que vous ne zoomiez.

[~ # ~] mise à jour [~ # ~]

Toute personne intéressée par une implémentation Objectif C du lien ci-dessus peut se référer à

this Repo GitHub.

Je l'ai écrit il y a quelque temps et il ne prend pas en charge ARC, mais vous pouvez facilement le modifier et supprimer quelques appels de version et de libération automatique et le faire fonctionner avec ARC.

Celui-ci génère simplement deux tableaux de points de contrôle pour un ensemble de points que l'on veut joindre en utilisant une spline de Bézier.

4
Amogh Talpallikar

Vous n'avez pas besoin d'écrire autant de code.

Référez-vous simplement au tutoriel de dessin à main levée ios ; cela facilite vraiment le dessin, le mécanisme de cache est également là pour que les performances ne diminuent pas même lorsque vous continuez à dessiner en continu.

2
Rakesh

J'ai trouvé n joli tutoriel sympa qui décrit une légère modification du dessin de la courbe de Bézier qui a tendance à lisser assez bien les bords. C'est essentiellement ce à quoi Caleb fait référence ci-dessus à propos de mettre les points d'extrémité de jonction sur la même ligne que les points de contrôle. C'est l'un des meilleurs tutoriels (sur n'importe quoi) que j'ai lu depuis un moment. Et il est livré avec un projet Xcode entièrement fonctionnel.

1
d512

Rapide:

        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
Jairo Bambang Oetomo

J'ai essayé tout ce qui précède, mais je ne peux pas le faire fonctionner. Une des réponses a même donné un résultat cassé pour moi. En recherchant plus, j'ai trouvé ceci: https://github.com/sam-keene/uiBezierPath-hermite-curve . Je n'ai pas écrit ce code, mais je l'ai implémenté et cela fonctionne vraiment très bien. Copiez simplement UIBezierPath + Interpolation.m/h et CGPointExtension.m/h. Ensuite, vous l'utilisez comme ceci:

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

C'est vraiment une solution robuste et soignée dans l'ensemble.

1
GeneCode

Voici le code en 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
}
1
Mike Zriel