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

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

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

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 Я НЕ рисую этот BezierPath, поэтому все функции CGContext... в drawRect не могут мне помочь: (

Спасибо!

Ответ 1

Изменить

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


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

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

Это длинный ответ. Короткий вариант: D


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

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

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


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

enter image description here

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

Простой способ сделать это - создать 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

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

Зная склоны всех трех сторон в треугольнике, радиус угла и центр кругов (точки пересечения), угол начала и останова для каждого закругленного угла - это угол наклона этой стороны - 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 раза для вычисления трех углов:

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

Ответ 2

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

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 выполняет геометрию для вас.

Ответ 3

Скругленный треугольник в 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 {
    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)

Ответ 4

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

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

Если кому-то интересно, вот мой код для равностороннего треугольника, указывающего вправо. 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().

Ответ 5

Основываясь на @David answer, я написал небольшое расширение 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()

Ответ 6

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

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