
【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)が重なる状態になるはずです。
例えば以下のような図のパターンです。

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

この時、時計回りのpathの場合は+1、反時計回りのpathの場合は-1するものと考えます。
kCAFillRuleNonZeroとkCAFillRuleEvenOddの違いは、この重なった図同士の合計値がいくつになり、その数字をどう見るかによって塗りつぶしをどう行うかを決めるところにあります。
kCAFillRuleNonZeroとkCAFillRuleEvenOddの違い
kCAFillRuleNonZero
kCAFillRuleNonZeroは塗りつぶしルールを、パスが交差した領域の合計値が0だったらpathの領域外であるとみなします。

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

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


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)
    }
}
雑記ではありますが、思考経路を記しておきます。

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














