Saltar al contenido

Triángulo UIBezierPath con bordes redondeados

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:

ingrese la descripción de la imagen aquí

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:

ingrese la descripción de la imagen aquí

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

ingrese la descripción de la imagen aquí

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

ingrese la descripción de la imagen aquí

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:

ingrese la descripción de la imagen aquí

El resultado final sin todas las líneas de soporte, tiene este aspecto:

ingrese la descripción de la imagen aquí

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.

triángulo redondeado en veloz

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
}
¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *