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!
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.
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:
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:
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:
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):
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):
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:
Le résultat final sans toutes les lignes de support, ressemble à ceci:
@ 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.
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
}
let cgPath = createRoundedTriangle(width: 200, height: 200, radius: 8)
let path = UIBezierPath(cgPath: cgPath)
@ 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.
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()