【iOS】CAShapeLayerの二つのfillRuleの違い

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

おばんです、味噌汁をこぼしてやけどしました。味噌汁を許しません。田中です。

今日はCALayerのmaskと塗りつぶし描画周りの話です。

!!!この記事の内容に誤りを発見しました。急ぎ修正します。!!!

修正版をアップしました! 今記事ではなく、下記の記事が正しい内容となっておりますので下記リンクの記事を参照ください。

【iOS】CAShapeLayerの二つのfillRuleの違い(修正版) | Developers.IO

CAShapeLayerを使うパターン

UIViewをくり抜く

まさしく以下のリンク先がやりたかったことです。
UIViewをくり抜く - Qiita

色付きのLayerとそのLayerをくりぬくためのLayerを用意して前者に後者をmaskとして使う。
これをその通り実装して動きはしましたが、

maskLayer.fillRule = kCAFillRuleEvenOdd

この箇所の意味だけ理解できず、調べてみたのでまとめてみます。

CAShapeLayerのfillRuleとは?

CAShapeLayerに指定したpathの色の塗りつぶしのルールのこと。
fillRuleには以下の二つがあります。

  • kCAFillRuleNonZero
  • kCAFillRuleEvenOdd

それぞれの違いを見ていきますが、その前にpathによる塗りつぶしルールの前説明から。

pathによる塗りつぶしルールについて

maskするような時などには、pathで指定する領域同士(と、あるいはLayer)が重なる状態になるはずです。
例えば以下のような図のパターンです。

14060362_1086440731440996_1150026493_o

pathには時計回りで結ばれるものと反時計回りで結ばれるものがあります。

14012641_1086438881441181_1771357337_o

この時、時計回りのpathの場合は+1、反時計回りのpathの場合は-1するものと考えます。

kCAFillRuleNonZerokCAFillRuleEvenOddの違いは、この重なった図同士の合計値がいくつになり、その数字をどう見るかによって塗りつぶしをどう行うかを決めるところにあります。

kCAFillRuleNonZeroとkCAFillRuleEvenOddの違い

kCAFillRuleNonZero

kCAFillRuleNonZeroは塗りつぶしルールを、パスが交差した領域の合計値が0だったらpathの領域外であるとみなします。

14012670_1086438908107845_10813477_o

それは逆を言うとパスが交差した領域の合計値が0以外だったらpathの領域内にあるとみなすということです。
kCAFillRuleNonZeroは合計値を0とそれ以外の二つで判別します。

kCAFillRuleEvenOdd

kCAFillRuleEvenOddは塗りつぶしルールを、パスが交差した領域の合計値が0または偶数だったらpathの領域外であるとみなします。
(塗り忘れで後から画像加工しましたが、重なった領域がどちらも塗られていない状態です)

14037839_1086438911441178_231113020_o

それは逆を言うとパスが交差した領域の合計値が奇数だったらpathの領域内にあるとみなすということです。
kCAFillRuleEvenOddは合計値を0と偶数、それと奇数かどうかの二つで判別します。

pathの合計値と塗りつぶしのサンプル

14002401_1086438891441180_758426651_o

14001945_1086438878107848_452367747_o

UIViewのくりぬきにおけるfillRule設定

冒頭で参考にした実装の箇所で述べたfillRuleの設定はkCAFillRuleEvenOddでした。
つまりpath(またはlayer)の重なりの合計値を0と偶数、それと奇数かどうか判別していたということです。
そのために中心が円状にくりぬかれたUIViewを生成できたという仕組みだったのですね。

そのハズなんだけど...

四角形の中に時計回りと反時計回りのpathで指定した三角形を以下のようなコードで実装してみると、どちらで試しても同じ結果となりました。

class TestView:UIView {
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        let hollowTargetLayer = CALayer()
        hollowTargetLayer.bounds = self.bounds
        hollowTargetLayer.position = CGPoint(
            x: CGRectGetWidth(self.bounds) / 2.0,
            y: CGRectGetHeight(self.bounds) / 2.0
        )
        hollowTargetLayer.backgroundColor = UIColor.blackColor().CGColor
        hollowTargetLayer.opacity = 0.5
        
        let maskLayer = CAShapeLayer()
        maskLayer.bounds = hollowTargetLayer.bounds
        
        // 右回り
//        let triangle = UIBezierPath()
//        triangle.moveToPoint(CGPointMake(50, 50))
//        triangle.addLineToPoint(CGPointMake(100, 250))
//        triangle.addLineToPoint(CGPointMake(25, 100))
//        triangle.closePath()
        
        // 左回り
        let triangle = UIBezierPath()
        triangle.moveToPoint(CGPointMake(50, 100))
        triangle.addLineToPoint(CGPointMake(100, 300))
        triangle.addLineToPoint(CGPointMake(25, 250))
        triangle.closePath()
        
        triangle.appendPath(UIBezierPath(rect: maskLayer.bounds))
        
        maskLayer.fillColor = UIColor.blackColor().CGColor
        maskLayer.path = triangle.CGPath
        maskLayer.position = CGPoint(
            x: CGRectGetWidth(hollowTargetLayer.bounds) / 2.0,
            y: CGRectGetHeight(hollowTargetLayer.bounds) / 2.0
        )
        
//        maskLayer.fillRule = kCAFillRuleEvenOdd
        maskLayer.fillRule = kCAFillRuleNonZero
        hollowTargetLayer.mask = maskLayer
        layer.addSublayer(hollowTargetLayer)
    }
}

雑記ではありますが、思考経路を記しておきます。

14060150_1086438888107847_1935020842_o

まとめ

日本語のiOSにおけるfillRuleの説明でこのように書かれている記事がなく、公式の英語の説明を読むもパッとイメージできず苦戦しました。
まだ謎が残されてはいますが、なんとなくまた一歩CALayer周りの理解が深まったように思います。
表現力を高めていこう??

参考