web-dev-qa-db-fra.com

UIBezierPath Triangle avec des bords arrondis

J'ai conçu ce code pour générer un chemin de Bézier utilisé comme chemin par CAShapeLayer pour masquer une vue UIV (la hauteur et la largeur de la vue sont variables)

Ce code génère un triangle avec des arêtes vives, mais je voudrais le rendre arrondi! J'ai passé 2 bonnes heures à essayer de travailler avec addArcWithCenter..., lineCapStyle et lineJoinStyle etc., mais rien ne semble fonctionner pour moi.

UIBezierPath *bezierPath = [UIBezierPath bezierPath];

CGPoint center = CGPointMake(rect.size.width / 2, 0);
CGPoint bottomLeft = CGPointMake(10, rect.size.height - 0);
CGPoint bottomRight = CGPointMake(rect.size.width - 0, rect.size.height - 0);

[bezierPath moveToPoint:center];
[bezierPath addLineToPoint:bottomLeft];
[bezierPath addLineToPoint:bottomRight];
[bezierPath closePath];

Ma question est donc la suivante: comment pourrais-je arrondir TOUS les bords d’un triangle dans un UIBezierPath (ai-je besoin de sous-couches, de chemins multiples, etc.)?

N.B Je ne dessine PAS ce BezierPath, donc toutes les fonctions CGContext... dans drawRect ne peuvent pas m'aider :(

Merci!

19
H Bellamy

Modifier

FWIW: Cette réponse sert son objectif éducatif en expliquant ce que CGPathAddArcToPoint(...) fait pour vous. Je vous recommande vivement de le lire car cela vous aidera à comprendre et à apprécier l'API CGPath. Ensuite, vous devriez continuer et utiliser cela, comme le montre la réponse de an0 , au lieu de ce code lorsque vous arrondissez les bords de votre application. Ce code ne doit être utilisé comme référence que si vous souhaitez jouer avec et apprendre à propos de calculs de géométrie comme celui-ci.


Réponse originale

Parce que je trouve ces questions si amusantes, je devais y répondre :)

C'est une longue réponse. Il n'y a pas de version courte: D


Remarque: pour ma propre simplicité, ma solution repose sur des hypothèses concernant les points utilisés pour former le triangle, telles que:

  • La zone du triangle est suffisamment grande pour s'adapter au coin arrondi (par exemple, la hauteur du triangle est supérieure au diamètre des cercles dans les coins. Je ne vérifie pas ou n'essaie pas d'éviter tout type de résultats étranges. cela peut arriver autrement.
  • Les coins sont listés dans le sens inverse des aiguilles d'une montre. Vous pourriez le faire fonctionner pour n'importe quel ordre, mais cela semblait être une contrainte assez juste à ajouter pour plus de simplicité. 

Si vous le souhaitez, vous pouvez utiliser la même technique pour arrondir n’importe quel polygone, à condition qu’il soit strictement convexe (c’est-à-dire qu’il ne s’agisse pas d’une étoile pointue). Je n’expliquerai pas comment faire mais le principe est le même.


Tout commence par un triangle, que vous voulez arrondir avec un rayon, r:

enter image description here

Le triangle arrondi doit être contenu dans le triangle en pointe. La première étape consiste donc à rechercher les emplacements, aussi proches que possible des angles, où vous pouvez ajuster un cercle avec le rayon, r.

Une façon simple de procéder consiste à créer 3 nouvelles lignes parallèles aux 3 côtés du triangle et à décaler chacune des distances r vers l'intérieur, orthogonales au côté du côté d'origine. 

Pour ce faire, vous calculez la pente/l'angle de chaque ligne et le décalage à appliquer aux deux nouveaux points:

CGFloat angle = atan2f(end.y - start.y,
                       end.x - start.x);

CGVector offset = CGVectorMake(-sinf(angle)*radius,
                                cosf(angle)*radius);

Remarque: pour plus de clarté, j'utilise le type CGVector (disponible dans iOS 7), mais vous pouvez également utiliser un point ou une taille pour fonctionner avec les versions précédentes du système d'exploitation.

vous ajoutez ensuite le décalage aux points de début et de fin pour chaque ligne:

CGPoint offsetStart = CGPointMake(start.x + offset.dx,
                                  start.y + offset.dy);

CGPoint offsetEnd   = CGPointMake(end.x + offset.dx,
                                  end.y + offset.dy);

Quand vous le ferez, vous verrez que les trois lignes se coupent à trois endroits:

enter image description here

Chaque point d'intersection correspond exactement à la distance r de deux des côtés (en supposant que le triangle est suffisamment grand, comme indiqué ci-dessus)}.

Vous pouvez calculer l'intersection de deux lignes comme suit:

//       (x1⋅y2-y1⋅x2)(x3-x4) - (x1-x2)(x3⋅y4-y3⋅x4)
// px =  –––––––––––––––––––––––––––––––––––––––––––
//            (x1-x2)(y3-y4) - (y1-y2)(x3-x4)

//       (x1⋅y2-y1⋅x2)(y3-y4) - (y1-y2)(x3⋅y4-y3⋅x4)
// py =  –––––––––––––––––––––––––––––––––––––––––––
//            (x1-x2)(y3-y4) - (y1-y2)(x3-x4)

CGFloat intersectionX = ((x1*y2-y1*x2)*(x3-x4) - (x1-x2)*(x3*y4-y3*x4)) / ((x1-x2)*(y3-y4) - (y1-y2)*(x3-x4));
CGFloat intersectionY = ((x1*y2-y1*x2)*(y3-y4) - (y1-y2)*(x3*y4-y3*x4)) / ((x1-x2)*(y3-y4) - (y1-y2)*(x3-x4));

CGPoint intersection = CGPointMake(intersectionX, intersectionY);

où (x1, y1) à (x2, y2) est la première ligne et (x3, y3) à (x4, y4) est la deuxième ligne. 

Si vous placez ensuite un cercle de rayon r sur chaque point d'intersection, vous constaterez qu'il en sera ainsi pour le triangle arrondi (en ignorant les différentes largeurs de ligne du triangle et des cercles):

enter image description here

Maintenant, pour créer le triangle arrondi, vous voulez créer un tracé qui passe d’une ligne à l’arc en une ligne (etc.) sur les points où le triangle initial est orthogonal aux points d’intersection. C'est aussi le point où les cercles tangent le triangle d'origine.

Connaissant les pentes des 3 côtés du triangle, le rayon et le centre des cercles (les points d’intersection), les angles de départ et d’arrêt de chaque coin arrondi correspondent à la pente de ce côté - 90 degrés. Pour regrouper ces éléments, j'ai créé une structure dans mon code, mais vous n'avez pas à le faire si vous ne voulez pas:

typedef struct {
    CGPoint centerPoint;
    CGFloat startAngle;
    CGFloat endAngle;
} CornerPoint;

Pour réduire la duplication de code, j'ai créé une méthode pour moi-même qui calcule l'intersection et les angles d'un point donné une ligne d'un point, par un autre, à un point final (ce n'est pas fermé donc ce n'est pas un triangle):

enter image description here

Le code est le suivant (c’est vraiment le code que j’ai montré ci-dessus, mis ensemble):

- (CornerPoint)roundedCornerWithLinesFrom:(CGPoint)from
                                      via:(CGPoint)via
                                       to:(CGPoint)to
                               withRadius:(CGFloat)radius
{
    CGFloat fromAngle = atan2f(via.y - from.y,
                               via.x - from.x);
    CGFloat toAngle   = atan2f(to.y  - via.y,
                               to.x  - via.x);

    CGVector fromOffset = CGVectorMake(-sinf(fromAngle)*radius,
                                        cosf(fromAngle)*radius);
    CGVector toOffset   = CGVectorMake(-sinf(toAngle)*radius,
                                        cosf(toAngle)*radius);


    CGFloat x1 = from.x +fromOffset.dx;
    CGFloat y1 = from.y +fromOffset.dy;

    CGFloat x2 = via.x  +fromOffset.dx;
    CGFloat y2 = via.y  +fromOffset.dy;

    CGFloat x3 = via.x  +toOffset.dx;
    CGFloat y3 = via.y  +toOffset.dy;

    CGFloat x4 = to.x   +toOffset.dx;
    CGFloat y4 = to.y   +toOffset.dy;

    CGFloat intersectionX = ((x1*y2-y1*x2)*(x3-x4) - (x1-x2)*(x3*y4-y3*x4)) / ((x1-x2)*(y3-y4) - (y1-y2)*(x3-x4));
    CGFloat intersectionY = ((x1*y2-y1*x2)*(y3-y4) - (y1-y2)*(x3*y4-y3*x4)) / ((x1-x2)*(y3-y4) - (y1-y2)*(x3-x4));

    CGPoint intersection = CGPointMake(intersectionX, intersectionY);

    CornerPoint corner;
    corner.centerPoint = intersection;
    corner.startAngle  = fromAngle - M_PI_2;
    corner.endAngle    = toAngle   - M_PI_2;

    return corner;
}

J'ai ensuite utilisé ce code 3 fois pour calculer les 3 coins:

CornerPoint leftCorner  = [self roundedCornerWithLinesFrom:right
                                                       via:left
                                                        to:top
                                                withRadius:radius];

CornerPoint topCorner   = [self roundedCornerWithLinesFrom:left
                                                       via:top
                                                        to:right
                                                withRadius:radius];

CornerPoint rightCorner = [self roundedCornerWithLinesFrom:top
                                                       via:right
                                                        to:left
                                                withRadius:radius];

Maintenant, ayant toutes les données nécessaires, commence la partie où nous créons le chemin réel. Je vais me fier au fait que CGPathAddArc ajoutera une ligne droite du point actuel au point de départ pour ne pas avoir à tracer ces lignes moi-même (il s'agit d'un comportement documenté). 

Le seul point que je dois calculer manuellement est le point de départ du chemin. Je choisis le début du coin inférieur droit (pas de raison spécifique). A partir de là, vous ajoutez simplement un arc avec le centre aux points d'intersection à partir des angles de départ et d'arrivée:

CGMutablePathRef roundedTrianglePath = CGPathCreateMutable();
// manually calculated start point
CGPathMoveToPoint(roundedTrianglePath, NULL,
                  leftCorner.centerPoint.x + radius*cosf(leftCorner.startAngle),
                  leftCorner.centerPoint.y + radius*sinf(leftCorner.startAngle));
// add 3 arcs in the 3 corners 
CGPathAddArc(roundedTrianglePath, NULL,
             leftCorner.centerPoint.x, leftCorner.centerPoint.y,
             radius,
             leftCorner.startAngle, leftCorner.endAngle,
             NO);
CGPathAddArc(roundedTrianglePath, NULL,
             topCorner.centerPoint.x, topCorner.centerPoint.y,
             radius,
             topCorner.startAngle, topCorner.endAngle,
             NO);
CGPathAddArc(roundedTrianglePath, NULL,
             rightCorner.centerPoint.x, rightCorner.centerPoint.y,
             radius,
             rightCorner.startAngle, rightCorner.endAngle,
             NO);
// close the path
CGPathCloseSubpath(roundedTrianglePath); 

Vous cherchez quelque chose comme ça:

enter image description here

Le résultat final sans toutes les lignes de support, ressemble à ceci:

enter image description here

102
David Rönnqvist

@ La géométrie de David est cool et pédagogique. Mais vous n'avez vraiment pas besoin de parcourir toute la géométrie de cette façon. Je vais offrir un code beaucoup plus simple:

CGFloat radius = 20;
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, (center.x + bottomLeft.x) / 2, (center.y + bottomLeft.y) / 2);
CGPathAddArcToPoint(path, NULL, bottomLeft.x, bottomLeft.y, bottomRight.x, bottomRight.y, radius);
CGPathAddArcToPoint(path, NULL, bottomRight.x, bottomRight.y, center.x, center.y, radius);
CGPathAddArcToPoint(path, NULL, center.x, center.y, bottomLeft.x, bottomLeft.y, radius);
CGPathCloseSubpath(path);

UIBezierPath *bezierPath = [UIBezierPath bezierPathWithCGPath:path];
CGPathRelease(path);

bezierPath est ce dont vous avez besoin. Le point clé est que CGPathAddArcToPoint effectue la géométrie à votre place.

33
an0

Triangle arrondi dans Swift 4

 enter image description here

Basé sur @ an0's answer above:

override func viewDidLoad() {
    let triangle = CAShapeLayer()
    triangle.fillColor = UIColor.lightGray.cgColor
    triangle.path = createRoundedTriangle(width: 200, height: 200, radius: 8)
    triangle.position = CGPoint(x: 200, y: 300)

    view.layer.addSublayer(triangle)
}

func createRoundedTriangle(width: CGFloat, height: CGFloat, radius: CGFloat) -> CGPath {
    // Points of the triangle
    let point1 = CGPoint(x: -width / 2, y: height / 2)
    let point2 = CGPoint(x: 0, y: -height / 2)
    let point3 = CGPoint(x: width / 2, y: height / 2)

    let path = CGMutablePath()
    path.move(to: CGPoint(x: 0, y: height / 2))
    path.addArc(tangent1End: point1, tangent2End: point2, radius: radius)
    path.addArc(tangent1End: point2, tangent2End: point3, radius: radius)
    path.addArc(tangent1End: point3, tangent2End: point1, radius: radius)
    path.closeSubpath()

    return path
}

Créer le triangle en tant que UIBezierPath

let cgPath = createRoundedTriangle(width: 200, height: 200, radius: 8)
let path = UIBezierPath(cgPath: cgPath)
13
bonus

@ an0 - Merci! Je suis tout nouveau, je n'ai donc pas la réputation de marquer comme utile ou commentaire, mais vous m'avez fait gagner beaucoup de temps aujourd'hui.

Je sais que cela dépasse la portée de la question, mais la même logique fonctionne avec CGContextBeginPath(), CGContextMoveToPoint(), CGContextAddArcToPoint() et CGContextClosePath() au cas où quelqu'un aurait besoin de l'utiliser à la place.

Si quelqu'un est intéressé, voici mon code pour un triangle équilatéral pointant à droite. La triangleFrame est une CGRect prédéfinie et la radius est une CGFloat prédéfinie.

CGContextRef context = UIGraphicsGetCurrentContext();
CGContextBeginPath(context);

CGContextMoveToPoint(context, CGRectGetMinX(triangleFrame), CGRectGetMidY(triangleFrame)); CGRectGetMidY(triangleFrame));
CGContextAddArcToPoint(context, CGRectGetMinX(triangleFrame), CGRectGetMidY(triangleFrame), CGRectGetMinX(triangleFrame), CGRectGetMinY(triangleFrame), radius);
CGContextAddArcToPoint(context, CGRectGetMinX(triangleFrame), CGRectGetMinY(triangleFrame), CGRectGetMaxX(triangleFrame), CGRectGetMidY(triangleFrame), radius);
CGContextAddArcToPoint(context, CGRectGetMaxX(triangleFrame), CGRectGetMidY(triangleFrame), CGRectGetMinX(triangleFrame), CGRectGetMaxY(triangleFrame), radius);
CGContextAddArcToPoint(context, CGRectGetMinX(triangleFrame), CGRectGetMaxY(triangleFrame), CGRectGetMinX(triangleFrame), CGRectGetMidY(triangleFrame), radius);

Bien entendu, chaque point peut être défini plus spécifiquement pour les triangles irréguliers. Vous pouvez alors CGContextFillPath() ou CGContextStrokePath() si vous le souhaitez.

1
Crazy8s

Basé sur @ David's answer j'ai écrit une petite extension UIBezierPath qui ajoute la même fonctionnalité que CGPath:

extension UIBezierPath {
    // Based on https://stackoverflow.com/a/20646446/634185
    public func addArc(from startPoint: CGPoint,
                       to endPoint: CGPoint,
                       centerPoint: CGPoint,
                       radius: CGFloat,
                       clockwise: Bool) {
        let fromAngle = atan2(centerPoint.y - startPoint.y,
                              centerPoint.x - startPoint.x)
        let toAngle = atan2(endPoint.y - centerPoint.y,
                            endPoint.x - centerPoint.x)

        let fromOffset = CGVector(dx: -sin(fromAngle) * radius,
                                  dy: cos(fromAngle) * radius)
        let toOffset = CGVector(dx: -sin(toAngle) * radius,
                                dy: cos(toAngle) * radius)

        let x1 = startPoint.x + fromOffset.dx
        let y1 = startPoint.y + fromOffset.dy

        let x2 = centerPoint.x + fromOffset.dx
        let y2 = centerPoint.y + fromOffset.dy

        let x3 = centerPoint.x + toOffset.dx
        let y3 = centerPoint.y + toOffset.dy

        let x4 = endPoint.x + toOffset.dx
        let y4 = endPoint.y + toOffset.dy

        let intersectionX =
            ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4))
                / ((x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4))
        let intersectionY =
            ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4))
                / ((x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4))

        let intersection = CGPoint(x: intersectionX, y: intersectionY)
        let startAngle = fromAngle - .pi / 2.0
        let endAngle = toAngle - .pi / 2.0

        addArc(withCenter: intersection,
               radius: radius,
               startAngle: startAngle,
               endAngle: endAngle,
               clockwise: clockwise)
    }
}

Donc, avec cela, le code devrait être quelque chose comme:

let path = UIBezierPath()

let center = CGPoint(x: rect.size.width / 2, y: 0)
let bottomLeft = CGPoint(x: 10, y: rect.size.height - 0)
let bottomRight = CGPoint(x: rect.size.width - 0, y: rect.size.height - 0)

path.move(to: center);
path.addArc(from: center,
            to: bottomRight,
            centerPoint: bottomLeft,
            radius: radius,
            clockwise: false)
path.addLine(to: bottomRight)
path.close()
0
Diogo T