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, ])
ここでふと気づいたのだけど、ASCIIの文字も縦になるのはいつからなのだろう。 確か昔のiOSだと横に倒れたままだったと記憶しているのだけど…昔過ぎて忘れた。
ASCIIの文字は倒しておきたいので
アトリビュートを .verticalGlyphForm: false
にしておきたい。
ということは文字単位で判定してアトリビュートを切り替える?
縦書きにしたいものはとりあえず全部全角にしておく?
役物の対応を見ていて、全角コロン:
(U+FF1A)、三点リーダ…
(U+2026)、全角ダッシュ —
(U+2014)が縦にならないのは、
これは仕様なのか未対応なのか何かパラメータがあるのか、ちょっとわからなかった。
世の中ちょっとわからないことが多い。
Core Text で縦書き
Core Text でテキストを描画するには
CTFrameDraw(_:_:)
を呼べばいいのだけど、そのパラメータをどうつくるかという話。
FrameDraw 第2引数の CGContext
は UIGraphicsGetCurrentContext()
で得られる。
FrameDraw 第1引数の CTFrame
は
CTFramesetterCreateFrame(_:_:_:_:)
で作れる。
CreateFrame 第3引数
の CGPath
は UIBezierPath
から作れるけど、
CGRect
からなら CGPath(rect:transform:)
のほうが早い。
CreateFrame 第1引数の CTFramesetter
は
CTFramesetterCreateWithAttributedString(_:)
で作れる。
引数の CFAttributedString
は API ドキュメントによると
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 を描画したもの。
文字が裏返っている理由は、
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 を点線で描いてある。
裏返るのは直ったけど、縦書きの向きにしたいので90度回転させる。
context.rotate(by: CGFloat.pi/2) // 90度回転 context.scaleBy(x: 1, y: -1) // 上下反転(LLO→ULO)
順序はこの通りでないといけない。
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()))!
さて調子に乗って NSString.draw(at:withAttributes:)
と混ぜると大変なことになる
これは 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 拡張で文字列を描くやり方
iOS の UIView
派生クラスで
func draw(_:)
を override
して
UIBezierPath
やら UIImage
やらを描画する横並びで、
文字列も描画するやつの話。
import UIKit
すると
NSString.draw(at:withAttributes:)
や
NSString.draw(in:withAttributes:)
が使える。
NSString
は String
から 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()
ところでなぜだか 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()
これは、暗黙の型変換がされているのか、何か特別な対応がされているのか、 小一時間調べたくらいじゃわからなかった。 (教えて偉い人)
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で実装していた。
案の定なかなか動かないのだけど、いちど動いてしまえばああそうか、みたいな簡単なものである。 なかなかうまくいかないのは日本語の解釈の問題なので、 それはつまり読み取り側の能力が足りないからだけど、 アルゴリズムは自然言語じゃなく架空でいいのでプログラミング言語っぽい言語で書いて欲しいものである。
ノード配列から最小値を持つノードを見つける、なんてのはSwiftっぽい書き方ができてちょっとうれしい。
while let n = openList.min(by: { (a, b) -> Bool in a.value < b.value }) {〜}
ただ「取り出す」などと書いてあるとそれはリストからremoveすることも意味していたりするので 削除するとなるとindexが必要で…などとなってちょっとAh...という気持ちになる
別件で UIGraphicsImageRenderer
も調べていたので問題と解を画像化してみたりしてみた。
迷路
迷路生成アルゴリズムを実装した。 (壁伸ばし法)
1が壁で0が通路
1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 1 0 1 1 1 1 0 1 1 1 0 1 1 0 0 0 1 0 0 0 1 1 1 1 0 1 0 1 0 1 1 0 0 0 0 0 1 0 1 1 1 1 1 1 1 1 1 1
Flood-fillアルゴリズムで通路(0)を2で塗りつぶす。
1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 1 2 1 1 1 1 2 1 1 1 2 1 1 2 2 2 1 2 2 2 1 1 1 1 2 1 2 1 2 1 1 2 2 2 2 2 1 2 1 1 1 1 1 1 1 1 1 1
通路(0)がなくなっていれば、通路は連結している(壁に塞がれた通路は無い)と診断できる。
同様に壁(1)を3で塗りつぶす。
3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 3 2 3 3 3 3 2 3 3 3 2 3 3 2 2 2 3 2 2 2 3 3 3 3 2 3 2 3 2 3 3 2 2 2 2 2 3 2 3 3 3 3 3 3 3 3 3 3
壁(1)がなくなっていれば、壁は連結している(通路に回廊は無い)と診断できる