60 votes

Dessiner des courbes lisses - Méthodes nécessaires

Comment lisser un ensemble de points dans une application de dessin iOS TOUT EN SE DÉPLACANT ? J'ai essayé UIBezierpaths mais tout ce que j'obtiens, ce sont des extrémités en dents de scie aux points d'intersection, lorsque je déplace simplement les points 1,2,3,4 - 2,3,4,5. J'ai entendu parler des courbes splines et de tous les autres types. Je suis assez novice en matière de programmation iPhone et je ne comprends pas comment programmer cela dans mon application de dessin quartz. Un exemple solide serait grandement apprécié, j'ai passé des semaines à tourner en rond et je ne semble jamais trouver de code iOS pour cette tâche. La plupart des articles renvoient à une simulation en Java ou à des pages de Wikipedia sur l'ajustement des courbes, ce qui ne m'apporte rien. De plus, je ne veux pas passer à l'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 le UIBezierPath qui laissait les bords à l'intersection///.

MISE À JOUR D'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

0 votes

Pouvez-vous nous montrer les extrémités déchiquetées ?

0 votes

Veuillez regarder ci-dessus pour le code UIBezierPath.

0 votes

Il existe un exemple de code d'Apple appelé QuartzDemo.

65voto

Joshua Weinberg Points 22701

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. Celles-ci fournissent une courbe très douce à travers un ensemble de points plutôt qu'une courbe 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 le code d'Erica Sadun dans un de ses livres, je l'ai légèrement modifié pour permettre une courbe lissée complète. Ceci est implémenté comme une catégorie sur UIBezierPath et a très bien fonctionné pour moi.

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

0 votes

Comment puis-je mettre cela en œuvre ? J'ai posté une image plus complète de mon code.

0 votes

Le code que j'ai fourni est une catégorie sur UIBezierCurve. Vous pouvez alors appeler UIBezierCurve *smoothCurve = [myJaggedPath smoothedCurveWithGranularity:40] pour obtenir une courbe douce. Le paramètre de granularité peut être ajusté en fonction des exigences de vitesse et de précision.

0 votes

Dans quelle méthode dois-je mettre cela ? Désolé pour toutes ces questions !

32voto

user1244109 Points 339

@Rakesh a tout à fait raison - vous n'avez pas besoin d'utiliser l'algorithme de Catmull-Rom si vous voulez simplement 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 fait PAS utilise l'algorithme et la granularité de Catmull-Rom, mais dessine une ligne à quadruple courbure (les points de contrôle sont calculés pour vous). C'est essentiellement ce qui est fait dans le tutoriel ios de dessin à main levée 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 quad-courbée hors de la boîte.

Vous devez disposer d'un tableau de CGPoint est enveloppé dans 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;
}

Voici le résultat : enter image description here

21voto

Caleb Points 72897

Pour que deux courbes de Bézier se rejoignent sans problème, il faut que les points de contrôle pertinents et les points de départ et d'arrivée des courbes soient colinéaires. Imaginez que le point de contrôle et le point d'arrivée forment une ligne tangente à la courbe au point d'arrivée. 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 ce point :

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

Remarquez que path1 se termine à point2 , path2 commence à point2 et les points de contrôle 2 et 3 partagent la même valeur Y, commonY con point2 . Vous pouvez changer n'importe quelle valeur dans le code comme vous le souhaitez ; tant que ces trois points se trouvent tous sur la même ligne, les deux chemins se rejoindront en douceur. (Dans le code ci-dessus, la ligne est y = commonY . La ligne n'a pas besoin d'être parallèle à l'axe X ; il est simplement 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 examiné votre code, la raison pour laquelle votre courbe est en dents de scie est que vous considérez les 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 à partir de la courbe, les points de contrôle et le point d'intersection sont les suivants no colinéaires, et les chemins ne se rejoignent donc pas en douceur.

7voto

user1129629 Points 61

Nous devons observer certaines choses avant d'appliquer un algorithme sur des points capturés.

  1. En général, UIKit ne donne pas les points à distance égale.
  2. Nous devons calculer les points intermédiaires entre deux CGPoints [qui ont été capturés avec la méthode Touch moved].

Maintenant pour obtenir une ligne lisse, il y a tellement de façons.

Parfois, nous pouvons obtenir ce résultat en appliquant des algorithmes polynomiaux du second degré ou 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;
}

4voto

Amogh Talpallikar Points 4234

Pour y parvenir, nous devons utiliser cette méthode. BézierSpline 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 Objective C et il fonctionne brillamment pour moi.

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

Pendant la conversion :

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

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

  3. Remplacer tous les tableaux de doubles par NSMutableArrays et y stocker des NSNumber enveloppant des doubles.

  4. utiliser la méthode objectAtIndex : 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, qui ont donc déjà des indices existants. Dans votre cas, si un NSMutableArray est vide, vous ne pouvez pas stocker des objets à des indices aléatoires comme le fait le code C#. Dans ce code C#, ils remplissent parfois l'index 1 avant l'index 0 et ils peuvent le faire car l'index 1 existe. Dans NSMutabelArrays ici, l'index 1 devrait être présent 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 le NSMutableArray.

ALSO :

cette logique a une méthode statique qui accepte un tableau de points et vous donne deux tableaux :-.

  1. réseau de premiers points de contrôle.

  2. réseau de seconds points de contrôle.

Ces tableaux contiendront le premier et le second 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 dessiner une courbe à travers eux.

Dans votre cas, lorsque vous dessinez, vous devrez fournir un ensemble de points par lesquels vous voulez qu'une courbe lisse passe.

et rafraîchir en appelant setNeedsDisplay et dessiner la spline qui n'est rien d'autre que UIBezierPath entre deux points adjacents dans le premier tableau. et prendre les points de contrôle des deux tableaux de points de contrôle en fonction de leur indice.

Le problème dans votre cas est qu'il est difficile de comprendre pendant le déplacement quels sont les points critiques à prendre.

Ce que vous pouvez faire : Tout simplement 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'il ne sera pas possible de voir à l'œil nu qu'il s'agit de petites lignes droites, à moins de zoomer.

UPDATE

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

este 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 à release et autorelease pour 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 à l'aide d'une spline de bézier.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X