Core Graphics で半透明図形を描く

図形を塗りつぶす。

        UIColor.red.setFill()
        UIBezierPath(rect: CGRect(x: 80, y: 80, width: 100, height: 100)).fill()

重ねて描く。

        UIColor.blue.setFill()
        UIBezierPath(rect: CGRect(x: 100, y: 100, width: 100, height: 100)).fill()

ここから半透明で図形を描く。

まず現在の描画文脈 CGContext インスタンスを得る。

        let context = UIGraphicsGetCurrentContext()!

alpha値を変更するので後でもとに戻す。

        context.saveGState()
        defer { context.restoreGState() }

alpha値を指定する。

        context.setAlpha(0.5)

不透明のときと同じように重ねて描く。

        UIColor.yellow.setFill()
        UIBezierPath(rect: CGRect(x: 120, y: 40, width: 100, height: 100)).fill()

        UIColor.cyan.setFill()
        UIBezierPath(rect: CGRect(x: 140, y: 60, width: 100, height: 100)).fill()

半透明の図形が重なる。

f:id:hrt1ro:20181009112424p:plain

さて「半透明図形を重ねる」のではなく、 「不透明図形を重ねたやつを半透明で重ねたい」という要件がある、 ということもあるかもしれない。

CGContext.beginTransparencyLayer(auxiliaryInfo:)APIドキュメントによるとこう書いてある。

The global alpha is set to 1.

setAlpha() の後、TransparencyLayerを beginend で囲めば、 不透明で重ねた図形を 半透明で描くことができる。

        context.beginTransparencyLayer(in: CGRect(x: 120.0, y: 40.0, width: 120.0, height: 120.0), auxiliaryInfo: nil)
        defer { context.endTransparencyLayer() }

こんなかんじ

f:id:hrt1ro:20181009112442p:plain

beginendはネストできるので、beginendの間で setAlpha() すればネストした内と外の alpha値を乗算した半透明で描かれることになる。

Core Text で縦書き(2)

NSAttributedString.draw(in:) なら Core Text 無しで縦書きできんじゃね? と思って試してみたけどそんな単純じゃなかった。 役物だけ回転しているので近づこうとしている気配はあるけど、 なにか指定方法があるのかどうなのか、ちょっとわからなかった。

さて Core Text をつかうほうだけど、 前回ASCII文字を混ぜたらどうも行間が広くなってしまうと書いたけど、 パラグラフスタイルの行の高さをフォントの高さくらいで指定してやると良いようだ。 ついでにフォントも指定してやる。

    let font = UIFont.systemFont(ofSize: 14)
    let paraStyle = NSMutableParagraphStyle()
    paraStyle.maximumLineHeight = font.lineHeight
    let astr = NSAttributedString(string: string, attributes: [
      .font: font,
      .verticalGlyphForm: true,
      .paragraphStyle: paraStyle,
    ])

f:id:hrt1ro:20180930142123p:plain

ここでふと気づいたのだけど、ASCIIの文字も縦になるのはいつからなのだろう。 確か昔のiOSだと横に倒れたままだったと記憶しているのだけど…昔過ぎて忘れた。

ASCIIの文字は倒しておきたいので アトリビュート.verticalGlyphForm: false にしておきたい。 ということは文字単位で判定してアトリビュートを切り替える? 縦書きにしたいものはとりあえず全部全角にしておく?

役物の対応を見ていて、全角コロン(U+FF1A)、三点リーダ(U+2026)、全角ダッシュ (U+2014)が縦にならないのは、 これは仕様なのか未対応なのか何かパラメータがあるのか、ちょっとわからなかった。

世の中ちょっとわからないことが多い。

Core Text で縦書き

Core Text でテキストを描画するには CTFrameDraw(_:_:) を呼べばいいのだけど、そのパラメータをどうつくるかという話。

FrameDraw 第2引数の CGContextUIGraphicsGetCurrentContext() で得られる。

FrameDraw 第1引数の CTFrameCTFramesetterCreateFrame(_:_:_:_:) で作れる。

CreateFrame 第3引数 の CGPathUIBezierPath から作れるけど、 CGRect からなら CGPath(rect:transform:) のほうが早い。

CreateFrame 第1引数の CTFramesetterCTFramesetterCreateWithAttributedString(_:) で作れる。

引数の CFAttributedStringAPI ドキュメントによると

CFAttributedString is “toll-free bridged” with its Foundation counterpart, NSAttributedString.

とのことなので NSAttributedString を使えば良いようである。

let astr = NSAttributedString(string: string) // アトリビュートなし

アトリビュートに縦書きを指定するには、

let astr = NSAttributedString(string: string, attributes: [.verticalGlyphForm: true])

CTFrame を作るところだけまとめると、

func createFrame(string: String, rect: CGRect) -> CTFrame? {
    let astr = NSAttributedString(string: string, attributes: [.verticalGlyphForm: true])
    let setter = CTFramesetterCreateWithAttributedString(astr)
    let path = CGPath(rect: rect, transform: nil)
    return CTFramesetterCreateFrame(setter, CFRange(), path, nil)
}

描画はこうなる。

let frame = createFrame(string: string, rect: rect)!
let context = UIGraphicsGetCurrentContext()!
CTFrameDraw(frame, context)

赤い枠線は rect を描画したもの。

f:id:hrt1ro:20180927132425p:plain

文字が裏返っている理由は、 Core Textは LLO(Lower Left Origin)の座標系で描くのだけど、 UIKit 由来の CGContext は ULO (Upper Left Origin) の座標系だから。 LLOで描いたものがULOに正しく描画されるように、座標変換させる必要がある。

座標変換するときは、行儀よく保存・復帰させよう。

context.saveGState()
defer { context.restoreGState() }

LLO→ULO座標変換はy軸を反転させるのがふつうなので

context.scaleBy(x: 1, y: -1) // 上下反転(LLO→ULO)

変換後の座標系での rect を点線で描いてある。

f:id:hrt1ro:20180927132459p:plain

裏返るのは直ったけど、縦書きの向きにしたいので90度回転させる。

context.rotate(by: CGFloat.pi/2) // 90度回転
context.scaleBy(x: 1, y: -1) // 上下反転(LLO→ULO)

順序はこの通りでないといけない。

f:id:hrt1ro:20180927132521p:plain

90度回転させると枠の縦横が入れ替わってしまうので、rect を LLO の座標系に逆変換してやらねばなるまい。

まず、一度 Affine 変換をつくってCTMに結合するやりかたに変更する。

let transform = CGAffineTransform(rotationAngle: CGFloat.pi/2).scaledBy(x: 1, y: -1)
context.concatenate(transform)

逆変換したrectで描くと、正位置に出るはず。

let frame = createFrame(string: string, rect: rect.applying(transform.inverted()))!

f:id:hrt1ro:20180927132542p:plain

さて調子に乗って NSString.draw(at:withAttributes:) と混ぜると大変なことになる

f:id:hrt1ro:20180927132556p:plain

これは NSString.draw(at:withAttributes:)CGContext.textMatrix を変更するのだけど、元に戻してくれないため。 そして CTFrameDraw(_:_:)CGContext.textMatrix に従って描画するため、だから。

しかたがないので自分で元に戻してやる。というかそもそもそういうもののようである。

context.textMatrix = CGAffineTransform.identity

まとめるとこんなかんじ。

func drawVText(context: CGContext, string: String, rect: CGRect) {
    context.saveGState()
    defer { context.restoreGState() }
    let transform = CGAffineTransform(rotationAngle: CGFloat.pi/2).scaledBy(x: 1, y: -1)
    context.concatenate(transform)
    let frame = createFrame(string: string, rect: rect.applying(transform.inverted()))!
    context.textMatrix = CGAffineTransform.identity
    CTFrameDraw(frame, context)
}
func createFrame(string: String, rect: CGRect) -> CTFrame? {
    let astr = NSAttributedString(string: string, attributes: [.verticalGlyphForm: true])
    let setter = CTFramesetterCreateWithAttributedString(astr)
    let path = CGPath(rect: rect, transform: nil)
    return CTFramesetterCreateFrame(setter, CFRange(), path, nil)
}

さてASCIIな文字を混ぜたりするとちょっと期待と異なるので、調整してやらねばならないのだけれども、 たぶん次回に続く

NSString の UIKit 拡張で文字列を描くやり方

iOSUIView 派生クラスで func draw(_:)override して UIBezierPath やら UIImage やらを描画する横並びで、 文字列も描画するやつの話。

import UIKit すると NSString.draw(at:withAttributes:)NSString.draw(in:withAttributes:) が使える。

NSStringString から as で型変換できる

let string = "Hello"
let nsstring = string as NSString

(10, 10)の位置に文字列を描く(アトリビュートなし)。

let p = CGPoint(x: 10, y: 10)
nsstring.draw(at: p)

ついでに枠線も描いてみる。 NSString.size(withAttributes:) で文字列を描くために必要な領域の高さと幅が得られる。

let size = nsstring.size()
UIColor.red.setStroke()
UIBezierPath(rect: CGRect(origin: p, size: size)).stroke()

f:id:hrt1ro:20180925095849p:plain

ところでなぜだか NSString ではなく Stringインスタンスでも NSString.draw(at:withAttributes:) が呼び出せる様子。

let string = "Hello"
let p = CGPoint(x: 10, y: 10)
string.draw(at: p)  // ←これ
let size = string.size() // ←これも
UIColor.red.setStroke()
UIBezierPath(rect: CGRect(origin: p, size: size)).stroke()

これは、暗黙の型変換がされているのか、何か特別な対応がされているのか、 小一時間調べたくらいじゃわからなかった。 (教えて偉い人)

diff(A*)

Rogue Likeか Tower Defense か何かで使おうとしてA*をつくっていたけど これってdiffだよね〜と思ってたらやっぱりdiffだったので diffにも使えるように改造していた。

ランダムに問題を生成してedit graph をA*でたどったもの f:id:hrt1ro:20180919130111p:plain

A*のパラメータとしては

  • S: スタートは左上隅
  • G: ゴールは右下隅
  • ノード展開は、3方向(→↘↓) か 2方向(→↓)
  • h(n) は d←n-G で |dx| と |dy| の大きい方
  • f値が同じときのタイブレークは Gまでのユークリッド距離で小さい方を選ぶ
  • COST(n,m)は1

で、だいたいこんなかんじ

Xcode update

Xcode のバージョンが 10.0 に上がった!

Swift のバージョンが 4.2 に上がった!

$ xcodebuild -version

Xcode 10.0
Build version 10A255

$ swift --version
Apple Swift version 4.2 (swiftlang-1000.11.37.1 clang-1000.11.45.1)
Target: x86_64-apple-darwin17.7.0

A*

ここのところA*をSwiftで実装していた。

アルゴリズムwikipediaのとおり

https://ja.wikipedia.org/wiki/A*

案の定なかなか動かないのだけど、いちど動いてしまえばああそうか、みたいな簡単なものである。 なかなかうまくいかないのは日本語の解釈の問題なので、 それはつまり読み取り側の能力が足りないからだけど、 アルゴリズム自然言語じゃなく架空でいいのでプログラミング言語っぽい言語で書いて欲しいものである。

ノード配列から最小値を持つノードを見つける、なんてのはSwiftっぽい書き方ができてちょっとうれしい。

    while let n = openList.min(by: { (a, b) -> Bool in a.value < b.value }) {〜}

ただ「取り出す」などと書いてあるとそれはリストからremoveすることも意味していたりするので 削除するとなるとindexが必要で…などとなってちょっとAh...という気持ちになる

別件で UIGraphicsImageRenderer も調べていたので問題と解を画像化してみたりしてみた。 f:id:hrt1ro:20180914144317p:plain