web-dev-qa-db-ja.com

丸みを帯びたエッジを持つUIBezierPath三角形

CAVieweLayerがUIViewをマスクするためのパスとして使用されるベジェパスを生成するようにこのコードを設計しました(ビューの高さと幅は可変です)

このコードは鋭いエッジの三角形を生成しますが、角を丸くしたいと思います! addArcWithCenter...lineCapStylelineJoinStyleなどを使用して2時間ほど過ごしましたが、何も機能しないようです。

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を描画していないので、drawRectのすべてのCGContext...関数は私を助けることができません:(

ありがとう!

21
H Bellamy

編集

FWIW:この回答は、CGPathAddArcToPoint(...)があなたのために何をするかを説明することにより、教育目的に役立ちます。 CGPath APIを理解して理解するのに役立つので、これを一読することを強くお勧めします。次に、アプリのエッジを丸めるときに、このコードの代わりに an0's answer に示されているように、それを使用する必要があります。このコードは、このようなジオメトリ計算をいじって学びたい場合にのみ参照として使用する必要があります。


元の答え

私はこのような質問がとても楽しいので、それに答える必要がありました:)

これは長い答えです。短いバージョンはありません:D


注:私自身の簡単さのために、私の解決策は、三角形を形成するために使用されている点について、次のようにいくつかの仮定をすることです:

  • 三角形の領域は、丸みを帯びた角に合うのに十分な大きさです(たとえば、三角形の高さは、角の円の直径よりも大きいです。私は、あらゆる種類の奇妙な点を確認したり、防止しようとしていませんそうでなければ起こるかもしれない結果。
  • コーナーは反時計回りにリストされています。どのような順序でも機能させることができますが、簡単にするために追加するのに十分な制約のように感じました。

必要に応じて、同じ手法を使用して、厳密に凸状である(つまり、先のとがった星でない)限り、任意のポリゴンを丸めることができます。方法は説明しませんが、同じ原則に従います。


すべてが三角形で始まり、角をいくつかの半径で丸めたいr

enter image description here

丸みを帯びた三角形は先のとがった三角形に含まれている必要があるので、最初のステップは、半径rの円にフィットできる、コーナーにできるだけ近い場所を見つけることです。

これを行う簡単な方法は、三角形の3つの辺に平行な3つの新しい線を作成し、元の辺の辺に直交する距離rをそれぞれ内側にシフトすることです。

これを行うには、各線の傾き/角度と、2つの新しいポイントに適用するオフセットを計算します。

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

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

注:わかりやすくするために、CGVectorタイプ(iOS 7で利用可能)を使用していますが、ポイントまたはサイズを使用して、以前のOSバージョンで作業することもできます

次に、各線の始点と終点の両方にオフセットを追加します。

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

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

これを行うと、3つの線が3つの場所で互いに交差していることがわかります。

enter image description here

各交点は、正確には2つの辺からの距離r(上記のように三角形が十分に大きいと仮定)です。

2本の線の交点は次のように計算できます。

//       (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)は2番目の行です。

次に、半径rの円を各交点に配置すると、実際に丸みを帯びた三角形(三角形と円の異なる線幅を無視)に対応することがわかります。

enter image description here

次に、丸みを帯びた三角形を作成するために、元の三角形が交点に直交する点で、線から円弧、線(など)に変化するパスを作成します。これは、円が元の三角形に接する点でもあります。

三角形の3つの辺すべての傾き、角の半径、円の中心(交点)がわかっているので、丸みを帯びた各角の開始角度と停止角度は、その辺の傾き(90度)です。これらをグループ化するために、コードに構造体を作成しましたが、必要がない場合は、次のようにする必要はありません。

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

コードの重複を減らすために、ある点から別の点を経由して最終点までの線を与えられた1つの点の交点と角度を計算するメソッドを自分で作成しました(閉じていないため、三角形ではありません)。

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

112

@Davidのジオメトリはクールで教育的です。ただし、この方法でジオメトリ全体を確認する必要はありません。より簡単なコードを提供します。

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がジオメトリを自動的に実行することです。

37
an0

Swift 4の角丸三角形

enter image description here

@ an0の answer 上記に基づく:

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)
17
5bars

@ 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()を実行できます。

1
Crazy8s

@Davidの answer に基づいて、CGPathと同じ機能を追加する小さなUIBezierPath拡張機能を作成しました。

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()
0
Diogo T