この記事は公開されてから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周りの理解が深まったように思います。 表現力を高めていこう??