Solución:
Editar
FWIW: Esta respuesta cumple su propósito educativo al explicar qué CGPathAddArcToPoint(...)
hace por ti. Le recomiendo encarecidamente que lo lea, ya que lo ayudará a comprender y apreciar la API de CGPath. Luego, debe continuar y usar eso, como se ve en la respuesta de an0, en lugar de este código cuando redondee los bordes en su aplicación. Este código solo debe usarse como referencia si desea jugar y aprender sobre cálculos geométricos como este.
Respuesta original
Debido a que encuentro preguntas como esta tan divertidas, tuve que responderlas 🙂
Esta es una respuesta larga. No hay una versión corta: D
Nota: Para mi propia simplicidad, mi solución es hacer algunas suposiciones sobre los puntos que se están usando 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 revisando ni tratando de evitar ningún tipo de resultados extraños que puedan ocurrir de lo contrario.
- Las esquinas se enumeran en el sentido contrario a las agujas del reloj. Podría hacerlo funcionar para cualquier pedido, pero se sintió como una restricción lo suficientemente justa para agregar por simplicidad.
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). Sin embargo, no explicaré cómo hacerlo, pero sigue el mismo principio.
Todo comienza con un triángulo, cuyas esquinas quieres redondear con un radio, r:
El triángulo redondeado debe estar contenido en el triángulo puntiagudo, por lo que el primer paso es encontrar las ubicaciones, lo más cerca posible de las esquinas, donde se puede ajustar un círculo con el radio, r.
Una forma sencilla de hacer esto es crear 3 nuevas líneas paralelas a los 3 lados del triángulo y desplazar cada una de las distancias. r hacia adentro, ortogonal al 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 puede 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 de 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 cruzan entre sí en tres lugares:
Cada punto de intersección es exactamente la distancia r desde dos de los lados (asumiendo que el triángulo es lo suficientemente grande, como se indicó anteriormente).
Puede 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 pones un círculo, con el radio r, en cada punto de intersección, puede ver que sí lo será 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 son tangentes al triángulo original.
Conociendo las pendientes de los 3 lados del triángulo, el radio de la esquina y el centro de los círculos (los puntos de intersección), el ángulo de inicio y finalización 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, reunido):
- (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 usé 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 yo mismo (este 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). A partir de ahí, 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);
Luciendo algo como esto:
El resultado final sin todas las líneas de soporte, tiene este aspecto:
La geometría de @ David es genial y educativa. Pero realmente no es necesario pasar por toda la geometría de esta manera. Ofreceré 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 ti.
Triángulo redondeado en Swift 4
Un patio de recreo basado en la respuesta de @ an0 anterior.
import UIKit
import PlaygroundSupport
import CoreGraphics
var view = UIView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
view.backgroundColor = .black
PlaygroundPage.current.liveView = view
let triangle = CAShapeLayer()
triangle.fillColor = UIColor.systemGreen.cgColor
triangle.path = createRoundedTriangle(width: 220, height: 200, radius: 15)
triangle.position = CGPoint(x: 140, y: 130)
view.layer.addSublayer(triangle)
func createRoundedTriangle(width: CGFloat, height: CGFloat, radius: CGFloat) -> CGPath {
// Draw the triangle path with its origin at the center.
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
}