LoginSignup
2
0

More than 5 years have passed since last update.

[Swift] Win32API Arc関数互換の描画方法

Last updated at Posted at 2018-06-04

はじめに

Twitterで困っている人がいたのでちょっと作ってみました。

私もWin32のプログラムを書く身としてはこの辺の悩みはよくわかります。

手っ取り早くコードだけ欲しい方向け

ちょっといい加減に書いたので全パターンでうまく行くかわかりません(矩形が負になるものとか面倒なので試していません)。

/// Win32API Arc API互換っぽい関数 by Takabo Soft
func arcWin32(_ x1: CGFloat, _ y1: CGFloat, _ x2: CGFloat, _ y2: CGFloat, _ x3: CGFloat, _ y3: CGFloat, _ x4: CGFloat, _ y4: CGFloat) -> CGPath {
    let path = CGMutablePath()

    let width = x2 - x1
    let height = y2 - y1
    let xCenter = (x1 + x2) / 2
    let yCenter = (y1 + y2) / 2
    let center = CGPoint(x: xCenter, y: yCenter)
    let radius = max(width, height) / 2
    let yScale = height / width

    let transform = CGAffineTransform(translationX: 0, y: -yCenter).concatenating(CGAffineTransform(scaleX: 1, y: yScale)).concatenating(CGAffineTransform(translationX: 0, y: +yCenter))

    let invertedTransform = transform.inverted()
    let inv3 = CGPoint(x: x3, y: y3).applying(invertedTransform)
    let inv4 = CGPoint(x: x4, y: y4).applying(invertedTransform)

    path.addArc(center: center, radius: radius, startAngle: atan2(inv3.y - yCenter, inv3.x - xCenter), endAngle: atan2(inv4.y - yCenter, inv4.x - xCenter), clockwise: true, transform: transform)

    return path
}

開発のポイント1 真円のみの場合

CoreGraphicsのaddArcは1つの半径と二つの角度を渡して円弧を描画をする関数です。
したがって真円に限定すれば、角度さえ求めることができれば、わりと簡単に解決します。

// 真円にのみ対応したコード
func arcWin32(_ x1: CGFloat, _ y1: CGFloat, _ x2: CGFloat, _ y2: CGFloat, _ x3: CGFloat, _ y3: CGFloat, _ x4: CGFloat, _ y4: CGFloat) -> CGPath {
    let path = CGMutablePath()

    let xCenter = (x1 + x2) / 2
    let yCenter = (y1 + y2) / 2
    let center = CGPoint(x: xCenter, y: yCenter)
    let radius = (x2 - x1) / 2

    path.addArc(center: center, radius: radius, startAngle: atan2(y3 - yCenter, x3 - xCenter), endAngle: atan2(y4 - yCenter, x4 - xCenter), clockwise: true)

    return path
}

中心点や半径は特に説明の必要は無いと思います。
問題はstartAngleendAngleに渡している値です。

ここでは直角三角形で言うところの「高さ」と「底辺」の長さを渡すと「成す角度(ラジアン単位)」を返してくれる便利なatan2関数を使用しています。

sankaku0.gif

上の図で言うとθはatan2(b, a)で求めることができます。

atan2は大体の処理系で標準関数として実装されています。
Appleのリファレンスはページは存在するものの残念な内容になっていますが・・・
https://developer.apple.com/documentation/coregraphics/1455332-atan2

もう一つのポイントとして、Y軸の向きが気になる方がいらっしゃるのではないでしょうか。
iOS上での座標系は数学のグラフで出てくるY軸とは向きが真逆です。

そのため、atan2に渡す場合もそれを考慮しないといけないのではないか...と思うかもしれませんが、そもそも最初のツイートにある通りaddArc関数は開始角度が例えば45度ならY=0よりも下側に出る、つまりもともとY軸が逆になっているため、逆のまま渡すと最終描画結果は期待したものになります(逆の逆で戻る)。

※この辺のY軸逆転問題はCoreGraphicsを使っていると頻出しますが、たぶんCoreGraphicsの座標系と、後から出てきたiOSの座標系が違うのにそれをラップして使えるようにした結果、いろんな所で綻びが出ているのかなと勝手に思っています。

描画テスト結果:
Simulator Screen Shot - iPad Air 2 - 2018-06-04 at 10.50.14.png

この時使ったソース:

// 真円にのみ対応したコード
func arcWin32(_ x1: CGFloat, _ y1: CGFloat, _ x2: CGFloat, _ y2: CGFloat, _ x3: CGFloat, _ y3: CGFloat, _ x4: CGFloat, _ y4: CGFloat) -> CGPath {
    let path = CGMutablePath()

    let xCenter = (x1 + x2) / 2
    let yCenter = (y1 + y2) / 2
    let center = CGPoint(x: xCenter, y: yCenter)
    let radius = (x2 - x1) / 2

    path.addArc(center: center, radius: radius, startAngle: atan2(y3 - yCenter, x3 - xCenter), endAngle: atan2(y4 - yCenter, x4 - xCenter), clockwise: true)

    return path
}

/// テスト用に枠や点をつけたもの
func arcWin32Test(_ context: CGContext, _ x1: CGFloat, _ y1: CGFloat, _ x2: CGFloat, _ y2: CGFloat, _ x3: CGFloat, _ y3: CGFloat, _ x4: CGFloat, _ y4: CGFloat) {

    let xCenter = (x1 + x2) / 2
    let yCenter = (y1 + y2) / 2

    UIColor.blue.setStroke()
    context.stroke(CGRect(x: x1, y: y1, width: x2 - x1, height: y2 - y1))

    UIColor.green.setStroke()
    for (x, y) in [(x3, y3), (x4, y4)] {
        context.move(to: CGPoint(x: xCenter, y: yCenter))
        context.addLine(to: CGPoint(x: x, y: y))
        context.strokePath()
    }

    UIColor.red.setFill()
    for (x, y) in [(x3, y3), (x4, y4), (xCenter, yCenter)] {
        context.fillEllipse(in: CGRect(x: x - 2, y: y - 2, width: 4, height: 4))
    }

    UIColor.black.setStroke()
    context.addPath(arcWin32(x1, y1, x2, y2, x3, y3, x4, y4))
    context.strokePath()

}

class MyView: UIView {
    override func draw(_ rect: CGRect) {
        guard let c = UIGraphicsGetCurrentContext() else { return }
        arcWin32Test(c, 30, 30, 30 + 100, 30 + 100, 110, 30, 150, 150)
    }
}

class ViewController: UIViewController {
    override func loadView() {
        super.loadView()
        view.backgroundColor = .black

        let v = MyView(frame: CGRect(x: 20, y: 20, width: 200, height: 200))
        v.backgroundColor = .white
        view.addSubview(v)
    }
}

開発のポイント2 楕円に対応させる場合

さて、Win32API側のArc関数は楕円弧にも対応しています。
しかしCoreGraphics側はradiusしか引数が無いため、あれひょっとして楕円弧は描けないんじゃ?となりますが、なんとかなります。

addArc関数はよく見ると最後にtransformという引数があります。
https://developer.apple.com/documentation/coregraphics/cgmutablepath/2427140-addarc
(まあ別にここに無くても良いんですが・・・)

transformは図形をアフィン変換するためのもので、移動・回転・拡大縮小などが行えます。
また、それらは連結(合成)することができます。

例えばCGAffineTransform(scaleX: 1, y: 0.5)を渡せば縦が半分に潰れた図形が描画できます。

今回は中心点のY座標を中心に縦方向の拡大縮小を行いたいので、上にずらす→スケールする→下にずらす、というアフィン変換を連結して使用します。

ここまでのコードと実行結果です。

func arcWin32(_ x1: CGFloat, _ y1: CGFloat, _ x2: CGFloat, _ y2: CGFloat, _ x3: CGFloat, _ y3: CGFloat, _ x4: CGFloat, _ y4: CGFloat) -> CGPath {
    let path = CGMutablePath()

    let width = x2 - x1
    let height = y2 - y1
    let xCenter = (x1 + x2) / 2
    let yCenter = (y1 + y2) / 2
    let center = CGPoint(x: xCenter, y: yCenter)
    let radius = max(width, height) / 2
    let yScale = height / width

    let transform = CGAffineTransform(translationX: 0, y: -yCenter).concatenating(CGAffineTransform(scaleX: 1, y: yScale)).concatenating(CGAffineTransform(translationX: 0, y: +yCenter))

    path.addArc(center: center, radius: radius, startAngle: atan2(y3 - yCenter, x3 - xCenter), endAngle: atan2(y4 - yCenter, x4 - xCenter), clockwise: true, transform: transform)

    return path
}

中略

class MyView: UIView {
    override func draw(_ rect: CGRect) {
        guard let c = UIGraphicsGetCurrentContext() else { return }
        arcWin32Test(c, 30, 30, 30 + 100, 30 + 50, 110, 30, 150, 150)
    }
}

Simulator Screen Shot - iPad Air 2 - 2018-06-04 at 11.25.18.png

外接する矩形とは一致しましたが、今度は角度がちょっとずれています。
縦に潰したのでそりゃそうです。

今度はこれを補正してやらないといけないわけですが、潰れた結果が座標(x3,y3)になれば良いわけですよね?

aa.png

そこで、transformの逆行列を作成して、(x3, y3)に適用し、その座標をもとに角度を求めてやればよさそうです。

    let invertedTransform = transform.inverted()
    let inv3 = CGPoint(x: x3, y: y3).applying(invertedTransform)

    ... startAngle: atan2(inv3.y - yCenter, inv3.x - xCenter)

最終的にできたものが以下です。

/// Win32API Arc API互換っぽい関数 by Takabo Soft
func arcWin32(_ x1: CGFloat, _ y1: CGFloat, _ x2: CGFloat, _ y2: CGFloat, _ x3: CGFloat, _ y3: CGFloat, _ x4: CGFloat, _ y4: CGFloat) -> CGPath {
    let path = CGMutablePath()

    let width = x2 - x1
    let height = y2 - y1
    let xCenter = (x1 + x2) / 2
    let yCenter = (y1 + y2) / 2
    let center = CGPoint(x: xCenter, y: yCenter)
    let radius = max(width, height) / 2
    let yScale = height / width

    let transform = CGAffineTransform(translationX: 0, y: -yCenter).concatenating(CGAffineTransform(scaleX: 1, y: yScale)).concatenating(CGAffineTransform(translationX: 0, y: +yCenter))

    let invertedTransform = transform.inverted()
    let inv3 = CGPoint(x: x3, y: y3).applying(invertedTransform)
    let inv4 = CGPoint(x: x4, y: y4).applying(invertedTransform)

    path.addArc(center: center, radius: radius, startAngle: atan2(inv3.y - yCenter, inv3.x - xCenter), endAngle: atan2(inv4.y - yCenter, inv4.x - xCenter), clockwise: true, transform: transform)

    return path
}

/// テスト用に枠や点をつけたもの
func arcWin32Test(_ context: CGContext, _ x1: CGFloat, _ y1: CGFloat, _ x2: CGFloat, _ y2: CGFloat, _ x3: CGFloat, _ y3: CGFloat, _ x4: CGFloat, _ y4: CGFloat) {

    let xCenter = (x1 + x2) / 2
    let yCenter = (y1 + y2) / 2

    UIColor.blue.setStroke()
    context.stroke(CGRect(x: x1, y: y1, width: x2 - x1, height: y2 - y1))

    UIColor.green.setStroke()
    for (x, y) in [(x3, y3), (x4, y4)] {
        context.move(to: CGPoint(x: xCenter, y: yCenter))
        context.addLine(to: CGPoint(x: x, y: y))
        context.strokePath()
    }

    UIColor.red.setFill()
    for (x, y) in [(x3, y3), (x4, y4), (xCenter, yCenter)] {
        context.fillEllipse(in: CGRect(x: x - 2, y: y - 2, width: 4, height: 4))
    }

    UIColor.black.setStroke()
    context.addPath(arcWin32(x1, y1, x2, y2, x3, y3, x4, y4))
    context.strokePath()

}

class MyView: UIView {
    override func draw(_ rect: CGRect) {
        guard let c = UIGraphicsGetCurrentContext() else { return }
        arcWin32Test(c, 30, 30, 30 + 100, 30 + 50, 110, 30, 150, 150)
    }
}

class ViewController: UIViewController {
    override func loadView() {
        super.loadView()
        view.backgroundColor = .black

        let v = MyView(frame: CGRect(x: 20, y: 20, width: 200, height: 200))
        v.backgroundColor = .white
        view.addSubview(v)
    }
}

実行結果:
Simulator Screen Shot - iPad Air 2 - 2018-06-04 at 11.39.18.png

上手くでましたね。

終わりに

すでに他の方法があったらごめんなさい。
バグがあったらごめんなさい(チェックはしてください)。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0