gomobile でタッチ位置をいい具合に計算する

この記事は [ACCESS Advent Calendar](http://qiita.com/advent-calendar/2015/access) 23日目の記事です。
ACCESS の @pankona です。

# この記事は

gomobile でタッチ位置をいい感じに計算する TIPS 的な記事です。

# 簡単に言うと

* タッチ位置は Pixel 単位で通知される
* 画面の大きさは Point 単位で扱われている
* なので、タッチ位置を size.Event.PixelsPerPt で割ると、ちょうど見た目とタッチ位置が合うようになるよ！

ということが言いたい記事です。
以下はその説明です。

# サンプルに見る gomobile でのタッチ座標の扱われ方

まずサンプルを見てみる。
gomobile のリポジトリ内には、タッチを使ったサンプルソースがある。
https://github.com/golang/mobile/blob/master/example/basic/main.go

上記サンプルソースにおいて、メインループの中で以下のように書いているところが
タッチイベントを拾っているところである。

```go
case touch.Event:
	touchX = e.X
	touchY = e.Y
```

タッチ情報は、画面がタッチされるたびに `touch.Event` という構造体で渡される。
`touch.Event` の GoDoc は以下。
[GoDoc - touch](https://godoc.org/golang.org/x/mobile/event/touch)

構造体は具体的には以下のような感じ。

```go
type Event struct {
    // X and Y are the touch location, in pixels.
    X, Y float32

    // Sequence is the sequence number. The same number is shared by all events
    // in a sequence. A sequence begins with a single TypeBegin, is followed by
    // zero or more TypeMoves, and ends with a single TypeEnd. A Sequence
    // distinguishes concurrent sequences but its value is subsequently reused.
    Sequence Sequence

    // Type is the touch type.
    Type Type
}
```

構造体には `X` と `Y` という変数が格納されていて、つまりこれがタッチされた座標を示しているのであるが、
注目すべきは、 **これの単位が pixel である** ということである。

# 画面の大きさは Pt

一方、ここでは画面サイズの話である。

前回、私が書いた記事であるこれ ↓
[gomobile で背景画像をアスペクト比を保ちつつ画面サイズにフィットさせる](http://qiita.com/pankona/items/cc901143503312e9388d) でもちょろっと触れているが、
画面の大きさいっぱいに画像を拡大しようと思ったときには、size.Event 構造体として通知される情報のうち、
`WidthPt`、`HeightPt` を用いる形になる。

size.Event 構造体の定義と具体的な値は以下のようなである。

[size - GoDoc](https://godoc.org/golang.org/x/mobile/event/size)

```go
type Event struct {
    // WidthPx and HeightPx are the window's dimensions in pixels.
    WidthPx, HeightPx int

    // WidthPt and HeightPt are the window's dimensions in points (1/72 of an
    // inch).
    WidthPt, HeightPt geom.Pt

    // PixelsPerPt is the window's physical resolution. It is the number of
    // pixels in a single geom.Pt, from the golang.org/x/mobile/geom package.
    //
    // There are a wide variety of pixel densities in existing phones and
    // tablets, so apps should be written to expect various non-integer
    // PixelsPerPt values. In general, work in geom.Pt.
    PixelsPerPt float32

    // Orientation is the orientation of the device screen.
    Orientation Orientation
}
```

ちなみに、 size.Event として通知される値、Nexus 9 の場合は ↓

```go
{
    1536, // WidthPx
    1952, // HeightPx
    345.60pt, // WidthPt
    439.20pt, // HeightPt
    4.4444447, // PixelsPerPt
    1 // Orientation
}
```

このように、Nexus 9 の場合、画面サイズは 345.60x439.20 (pt) として扱われている。

# 以上を踏まえて、タッチ位置情報を画面の大きさを踏まえて変換する

上述の通り、タッチ位置は Pixel 単位で通知される。

なので、たとえば「タッチした位置にスプライトを動かそう」と思って、
以下のようなコードを書くのは間違いである。

```go
    // タッチ位置として通知された値をそのまま保存しておいて...
    case touch.Event:
        touchX = e.X
        touchY = e.Y

    (中略)

    // initialize affine variable
    affine = &f32.Affine{
                 {1, 0, 0}, 
                 {0, 1, 0}, 
             }
    
    // affine.Translate でタッチ位置の情報を使用 → タッチした位置にスプライトが移動することを期待
    affine.Translate(affine, touchX, touchY)

    // scale to 140x90
    affine.Scale(affine, 140, 90)

    // apply affine transformation
    eng.SetTransform(n, *affine)
```

Nexus 9 の場合、 `touchX`、は 0 〜 1536 (px) を取りうる。 `touchY` は、 0 〜 1952 (px) を取りうる。
一方、 `size.Event` で通知される画面サイズは 345.60 x 439.20 (pt) である。
上記のコードを用いた場合、たとえば画面の右下端をタッチして `(x, y) = (1536, 1952)` みたいな値をとるときは、
大きく画面の外をタッチされたとして扱われてしまうことになり、結果 スプライトが画面から消え失せるみたいな現象が発生してしまう。

# PixelsPerPt でタッチ位置の数値を割る

`size.Event` 構造体には、`PixelsPerPt` という変数が格納されている。
読んで字の如く、Pixel と Pt の比を表した値である。
たとえば Nexus 9 の場合、`4.4444447` である。

この値を使えば、タッチ情報の Pixel を画面サイズの Pt に変換できる。

```go

    // size.Event 構造体の中身を覚えておく
    case size.Event:
        var sz size.Event
        sz = e    

    // タッチ位置として通知された値をそのまま保存しておいて...
    case touch.Event:
        touchX = e.X
        touchY = e.Y

    (中略)

    // initialize affine variable
    affine = &f32.Affine{
                 {1, 0, 0}, 
                 {0, 1, 0}, 
             }
    
    // affine.Translate でタッチ位置の情報を使用。Pixel → Pt に変換しつつ。
    affine.Translate(affine, touchX / sz.PixelsPerPt, touchY / sz.PixelsPerPt)

    // scale to 140x90
    affine.Scale(affine, 140, 90)

    // apply affine transformation
    eng.SetTransform(n, *affine)
```

こうすると、Pixel 単位で渡ってきたタッチ位置の情報が 345.60 x 439.20 (pt) の間のおさまる値に変換されていい感じになるのです。

以上、タッチ位置 (Pixel) → 画面上の位置 (Pt) に変換する方法でした！

# おわりに

というわけで gomobile の細かい話でした。
PixelsPerPt あたりを使った具体的なコード例はなかなか見つからないと思うので、
この記事が誰かの何かの参考になれば実に幸いです。ありがとうございました。

[ACCESS Advent Calendar](http://qiita.com/advent-calendar/2015/access)、明日は @DaisukeKondo さんの出番です。
ご期待ください！