ios objective-c core-graphics uibezierpath cashapelayer

ios - UIBezierPath Triangle con bordes redondeados



objective-c core-graphics (6)

He diseñado este código para generar una ruta Bezier que se utiliza como ruta para que CAShapeLayer pueda enmascarar una UIView (la altura y el ancho de la vista son variables)

Este código genera un triángulo con bordes afilados, pero me gustaría hacerlo con esquinas redondeadas. He pasado unas buenas 2 horas tratando de trabajar con addArcWithCenter... , lineCapStyle y lineJoinStyle pero nada parece funcionar para mí.

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

Entonces, mi pregunta es, ¿cómo podría redondear TODOS los bordes de un triángulo en un UIBezierPath (necesito subcapas, múltiples rutas, etc.)?

NB: NO estoy dibujando este BezierPath, por lo que todas las funciones CGContext... en drawRect no pueden ayudarme :(

¡Gracias!


Triángulo redondeado en Swift 4

Basado en la answer anterior de @ an0:

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 }

Creando el triángulo como un UIBezierPath

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


Editar

FWIW: Esta respuesta cumple su propósito educativo al explicar lo que CGPathAddArcToPoint(...) hace por ti. Le recomendaría encarecidamente que lo lea, ya que le ayudará a comprender y apreciar la API de CGPath. Luego, debe seguir adelante y usar eso, como se ve en la respuesta de an0 , en lugar de este código cuando redondea los bordes en su aplicación. Este código solo debe usarse como referencia si quiere jugar y aprender sobre cálculos de geometría como este.

Respuesta original

Porque encuentro preguntas como esta muy divertidas, tuve que responderlas :)

Esta es una respuesta larga. No hay versión corta: D

Nota: para mi propia simplicidad, mi solución es hacer algunas suposiciones sobre los puntos que se están utilizando para formar el triángulo, tales como:

  • El área del triángulo es lo suficientemente grande como para caber en la esquina redondeada (por ejemplo, la altura del triángulo es mayor que el diámetro de los círculos en las esquinas. No estoy comprobando ni tratando de evitar ningún tipo de resultados extraños que puedan ocurrir) de otra manera.
  • Las esquinas se enumeran en orden antihorario. Podría hacerlo funcionar para cualquier orden, pero se sintió como una restricción justa para agregar para simplificar.

Si lo desea, puede usar la misma técnica para redondear cualquier polígono siempre que sea estrictamente convexo (es decir, no una estrella puntiaguda). No explicaré cómo hacerlo, pero sigue el mismo principio.

Todo comienza con un triángulo, del que quieres redondear las esquinas con un radio, r :

El triángulo redondeado debe estar contenido en el triángulo puntiagudo, de modo que el primer paso sea encontrar las ubicaciones lo más cerca posible de las esquinas, donde pueda encajar un círculo con el radio, r.

Una forma sencilla de hacer esto es crear 3 nuevas líneas paralelas a los 3 lados en el triángulo y desplazar cada una de las distancias r hacia adentro, ortogonales hacia el lado del lado original.

Para hacer esto, calcula la pendiente / ángulo de cada línea y el desplazamiento para aplicar a los dos nuevos puntos:

CGFloat angle = atan2f(end.y - start.y, end.x - start.x); CGVector offset = CGVectorMake(-sinf(angle)*radius, cosf(angle)*radius);

Nota: para mayor claridad, estoy usando el tipo CGVector (disponible en iOS 7), pero también puedes usar un punto o un tamaño para trabajar con versiones anteriores del sistema operativo .

luego agrega el desplazamiento a los puntos inicial y final para cada línea:

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

Cuando lo hagas verás que las tres líneas se intersecan en tres lugares:

Cada punto de intersección es exactamente la distancia r de dos de los lados (asumiendo que el triángulo es lo suficientemente grande, como se indicó anteriormente) .

Puedes calcular la intersección de dos líneas como:

// (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);

donde (x1, y1) a (x2, y2) es la primera línea y (x3, y3) a (x4, y4) es la segunda línea.

Si luego coloca un círculo, con el radio r , en cada punto de intersección, puede ver que en efecto lo hará para el triángulo redondeado (ignorando los diferentes anchos de línea del triángulo y los círculos):

Ahora, para crear el triángulo redondeado, desea crear una ruta que cambie de una línea a un arco a una línea (etc.) en los puntos donde el triángulo original es ortogonal a los puntos de intersección. Este es también el punto donde los círculos tangentes el triángulo original.

Conociendo las pendientes de los 3 lados en el triángulo, el radio de la esquina y el centro de los círculos (los puntos de intersección), el ángulo de inicio y parada para cada esquina redondeada es la pendiente de ese lado: 90 grados. Para agrupar estas cosas, creé una estructura en mi código, pero no tienes que hacerlo si no quieres:

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

Para reducir la duplicación de código, creé un método para mí mismo que calcula la intersección y los ángulos para un punto dada una línea desde un punto, a través de otro, hasta un punto final (no está cerrado, por lo que no es un triángulo):

El código es el siguiente (en realidad es solo el código que he mostrado arriba, junto):

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

Luego utilicé ese código 3 veces para calcular las 3 esquinas:

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

Ahora, teniendo todos los datos necesarios, comienza la parte donde creamos la ruta real. Voy a confiar en el hecho de que CGPathAddArc agregará una línea recta desde el punto actual hasta el punto de inicio para no tener que dibujar esas líneas (esto es un comportamiento documentado).

El único punto que tengo que calcular manualmente es el punto de inicio de la ruta. Elijo el inicio de la esquina inferior derecha (sin motivo específico). Desde allí, simplemente agrega un arco con el centro en los puntos de intersección desde los ángulos inicial y final:

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

Buscando algo como esto:

El resultado final sin todas las líneas de soporte, se ve así:


@ an0 - ¡Gracias! Soy nuevo, así que no tengo la reputación de marcar como útil o comentar, pero hoy me has ahorrado un montón de tiempo.

Sé que va más allá del alcance de la pregunta, pero la misma lógica funciona con CGContextBeginPath() , CGContextMoveToPoint() , CGContextAddArcToPoint() y CGContextClosePath() en caso de que alguien necesite usar eso en su lugar.

Si alguien está interesado, aquí está mi código para un triángulo equilátero que apunta hacia la derecha. El triangleFrame es un CGRect predefinido y el radius es un CGFloat predefinido.

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

Por supuesto, cada punto se puede definir más específicamente para los triángulos irregulares. Luego puede CGContextFillPath() o CGContextStrokePath() como lo desee.


Basado en la answer @ David, escribí una pequeña extensión UIBezierPath que agrega la misma funcionalidad que CGPath tiene:

extension UIBezierPath { // Based on https://.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) } }

Así que con esto, el código debería ser algo como:

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


Es mucho más complicado sería simplemente agregar dos líneas de código:

UIBezierPath *bezierPath = [UIBezierPath bezierPath]; bezierPath.lineCapStyle = kCGLineCapRound; bezierPath.lineJoinStyle = kCGLineJoinRound; CGPoint center = CGPointMake(rect.size.width / 2, 0); ...

EDITAR:

Para ver que las esquinas están redondeadas necesitas hacer un triángulo un poco menos que los límites rectos:

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


La geometría de @ David es genial y educativa. Pero realmente no necesitas recorrer toda la geometría de esta manera. Te ofrezco un código mucho más 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 es lo que necesitas. El punto clave es que CGPathAddArcToPoint hace la geometría por usted.