Треугольник UIBezierPath со скругленными краями

Я разработал этот код для создания пути Безье, который используется в качестве пути для CAShapeLayer для маскировки UIView (высота и ширина представления являются переменными)

этот код генерирует треугольник с острыми краями, но я хотел бы сделать его круглым! Я потратил 2 часа на работу с addArcWithCenter..., lineCapStyle и lineJoinStyle etc но ничего не работает для меня.

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

Итак, мой вопрос в том, как я буду округлять все края треугольник в UIBezierPath (мне нужны подслои, несколько путей и т. д.)?

N. B я не рисую этот Безиерпат, так что все CGContext... функции drawRect не могут мне помочь :(

спасибо!

6 ответов


редактировать

FWIW: этот ответ служит своей образовательной цели, объясняя, что CGPathAddArcToPoint(...) делает для вас. Я настоятельно рекомендую вам прочитать его, поскольку это поможет вам понять и оценить API CGPath. Тогда вы должны идти вперед и использовать это, как видно в an0 это, вместо этого кода, когда вы округляете края в своем приложении. Этот код следует использовать только в качестве ссылки, если вы хотите поиграть и узнать о геометрии такие расчеты.


оригинальный ответ

потому что я нахожу такие вопросы, как это так весело, я должен был ответить на него:)

это длинный ответ. Нет короткой версии: D


Примечание: Для моей собственной простоты мое решение делает некоторые предположения о точках, которые используются для формирования треугольника, таких как:

  • площадь треугольника достаточно велика, чтобы соответствовать закругленный угол (например, высота треугольника больше диаметра окружностей в углах. Я не проверяю и не пытаюсь предотвратить какие-либо странные результаты, которые могут произойти в противном случае.
  • углы перечислены в встречном часовом порядке. Вы можете заставить его работать для любого заказа, но он чувствовал себя достаточно справедливым ограничением, чтобы добавить для простоты.

если вы хотели, то вы смогли использовать такой же метод к вокруг любого многоугольника, если он строго выпуклый (т. е. не заостренная звезда). Я не буду объяснять, как это сделать, но он следует тому же принципу.


все начинается с треугольника, который вы хотите округлить по углам с некоторым радиусом,r:

enter image description here

закругленный треугольник должен содержаться в остроконечном треугольнике, поэтому первый шаг-найти места, как можно ближе к углам, где вы может поместиться круг с радиусом, r.

простой способ сделать это-создать 3 новые линии, параллельные 3 сторонам в треугольнике, и сдвинуть каждую из расстояний r внутрь, ортогонально к стороне первоначальной стороны.

для этого вы вычисляете наклон / угол каждой линии и смещение для применения к двум новым точкам:

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

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

Примечание: Для ясности я использую тип CGVector( доступный в iOS 7), но вы можете так же хорошо использовать точку или размер для работы с предыдущими версиями ОС.

затем вы добавляете смещение в начальную и конечную точки для каждой строки:

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

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

когда вы это сделаете, вы увидите, что три линии пересекаются друг с другом в трех местах:

enter image description here

каждая точка пересечения-это именно расстояние r С двух сторон (предполагая, что треугольник достаточно большой, как говорится выше).

вы можете рассчитать пересечение двух линий как:

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

где (x1, y1) - (x2, y2) - первая строка, а (x3, y3) - (x4, y4) - вторая строка.

если затем поставить круг, с радиусом r, на каждой точке пересечения вы можете видеть, что это действительно будет для округленного треугольника (игнорируя различные ширины линий треугольника и кругов):

enter image description here

теперь создайте округлый треугольник, который вы хотите создать путь, который изменяется от линии к дуге к линии (и т. д.) в местах, где исходный треугольник является ортогональной к точкам пересечения. Это также точка, где круги касательные к исходному треугольнику.

зная наклоны всех 3 сторон в треугольнике, радиус угла и центр окружностей (точки пересечения), угол начала и остановки для каждого закругленного угла - это наклон этой стороны-90 градусов. К сгруппируйте эти вещи вместе, я создал структуру в своем коде, но вам не нужно, если вы не хотите:

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

чтобы уменьшить дублирование кода, я создал для себя метод, который вычисляет пересечение и углы для одной точки, заданной линией из одной точки через другую, до конечной точки (она не закрыта, поэтому это не треугольник):

enter image description here

код выглядит следующим образом (это просто код, который я показал выше, поставить вместе):

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

затем я использовал этот код 3 раза для вычисления 3 углов:

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

теперь, имея все необходимые данные, начинает часть, где мы создаем фактический путь. Я собираюсь полагаться на то, что CGPathAddArc добавит прямую линию от текущей точки к начальной точке, чтобы не рисовать эти линии самостоятельно (это документированное поведение).

единственная точка, которую я должен вычислить вручную, - это начальная точка пути. Я выберите начало нижнего правого угла (без конкретной причины). Оттуда вы просто добавляете дугу с центром в точках пересечения от начального и конечного углов:

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

выглядит примерно так:

enter image description here

конечный результат без всех линий поддержки, выглядят так:

enter image description here


@Давид это круто и познавательно. Но вам действительно не нужно проходить всю геометрию таким образом. Я предложу гораздо более простой код:

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 это то, что вам нужно. Ключевым моментом является то, что CGPathAddArcToPoint делает геометрию для вас.


округлый треугольник в Swift 4

enter image description here

на основе @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
}

создание треугольника как UIBezierPath

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

@an0 - спасибо! Я новенький, так что у меня нет репутации, которую можно было бы отметить как полезную или комментировать, но вы сэкономили мне кучу времени сегодня.

Я знаю, что это выходит за рамки вопроса, но та же логика работает с CGContextBeginPath(), CGContextMoveToPoint(), CGContextAddArcToPoint() и CGContextClosePath() в случае, если кто-то захочет использовать это вместо этого.

если кто-то заинтересован, вот мой код для равностороннего треугольника, указывающего вправо. The triangleFrame - это предопределенный CGRect и radius - это предопределено CGFloat.

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

конечно, каждая точка может быть определена более конкретно для нерегулярных треугольников. Вы можете тогда CGContextFillPath() или CGContextStrokePath() по желанию.


на основе @Дэвид ответ я написал немного UIBezierPath расширение, которое добавляет ту же функциональность, что и 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)
    }
}

таким образом, код должен быть чем-то вроде:

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

гораздо менее сложным было бы просто добавить две строки кода:

UIBezierPath *bezierPath = [UIBezierPath bezierPath];
bezierPath.lineCapStyle = kCGLineCapRound;
bezierPath.lineJoinStyle = kCGLineJoinRound;

CGPoint center = CGPointMake(rect.size.width / 2, 0);
... 

EDIT:

чтобы увидеть, что углы закруглены, вам нужно сделать треугольник немного меньше, чем прямые границы:

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