ebitenで点滅するウィンドウをタッチ操作で選択してみたの

# タッチ操作で複数のウィンドウから1つ選ぶ

![image.png](https://qiita-image-store.s3.amazonaws.com/0/166225/a6611ba8-03c1-61f2-2dac-a6b3b45ce386.png)

今回は2つの取組みを紹介します٩( 'ω' )و

- 枠が点滅するウィンドウの実装
- 複数あるウィンドウをタッチ操作で選択して、選択したウィンドウだけ枠を点滅させる

解説に使うコードは以下に置いています。

> [kemokemo/ebiten-sketchbook - blink-framewindow](https://github.com/kemokemo/ebiten-sketchbook/tree/master/blink-framewindow)
> [kemokemo/ebiten-sketchbook - select-one](https://github.com/kemokemo/ebiten-sketchbook/tree/master/select-one)

# 使用環境

- go version go1.10.2 windows/amd64
- ebiten rev. [c39c211d1fe0744b3a2315a122f89c7074a54a49](https://github.com/hajimehoshi/ebiten/tree/c39c211d1fe0744b3a2315a122f89c7074a54a49)
- gomobile version +bf2d30a Sun Apr 29 17:04:17 2018
- Android NDK r16b

# 実装内容の説明

## 点滅機能付きのウィンドウ`FrameWindow`の説明

### 枠付きのウィンドウ

実はとても単純な仕組みです。下図をご覧ください。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/166225/2214168d-d865-9ac9-4a8d-963c02c75fa8.png)

* 枠の内側となる画像`innerImg`
* 上記よりも上下左右に枠の幅`frameWidth`の分だけ大きい画像`frameImg`

これらの画像を作って、`innerImg`を上に重ねて描画するだけです。

[frame_window.go](https://github.com/kemokemo/ebiten-sketchbook/blob/master/blink-framewindow/internal/ui/frame_window.go)

```frame_window.go
// NewFrameWindow returns a FrameWindow.
//
// The width and height are used for the inner region excluding the frame.
// If 0 is set to the frameWidth, the frame will not be drawn.
func NewFrameWindow(x, y, width, height, frameWidth int) (*FrameWindow, error) {
	fw := FrameWindow{
		rect: image.Rectangle{
			Min: image.Point{X: x, Y: y},
			Max: image.Point{X: x + width, Y: y + height},
		},
	}
	var err error
	fw.innerImg, err = ebiten.NewImage(width, height, ebiten.FilterDefault)
	if err != nil {
		return nil, err
	}
	err = fw.innerImg.Fill(color.White)
	if err != nil {
		return nil, err
	}
	fw.innerOp = &ebiten.DrawImageOptions{}
	fw.innerOp.GeoM.Translate(float64(x), float64(y))

	if frameWidth > 0 {
		fw.frameImg, err = ebiten.NewImage(width+frameWidth*2, height+frameWidth*2, ebiten.FilterDefault)
		if err != nil {
			return nil, err
		}
		err = fw.frameImg.Fill(color.White)
		if err != nil {
			return nil, err
		}
		fw.frameDarkOp = &ebiten.DrawImageOptions{}
		fw.frameDarkOp.GeoM.Translate(float64(x-frameWidth), float64(y-frameWidth))
		fw.frameLightOp = &ebiten.DrawImageOptions{}
		fw.frameLightOp.GeoM.Translate(float64(x-frameWidth), float64(y-frameWidth))
	}
	return &fw, nil
}
```

### 枠を点滅させる

![image.png](https://qiita-image-store.s3.amazonaws.com/0/166225/a3576c55-8b0c-4dc9-c420-7fcc0f67706a.png)

`ebiten`で画像を描画する時には`DrawImageOptions`を指定しますが、枠の描画に使う`DrawImageOptions`に明るい`frameLightOp`と暗い`frameDarkOp`を用意しておいて、`frameImg`の描画に使う`DrawImageOptions`を時間経過で変更すれば点滅動作となります。やったね！٩( 'ω' )و

## `FrameWindow`の使い方

### 動作イメージ

実際に使ってみるとこんな感じになります。

![blink_frame-window.gif](https://qiita-image-store.s3.amazonaws.com/0/166225/24be77d0-e921-80f9-4053-6eb381167ee0.gif)

### インスタンスの生成と色や点滅の設定

```go
var err error
frameWindow, err = ui.NewFrameWindow(20, 10, 100, 120, 5)
if err != nil {
  return err
}

frameWindow.SetColors(
  color.RGBA{64, 64, 64, 255},
  color.RGBA{192, 192, 192, 255},
  color.RGBA{0, 148, 255, 255})
frameWindow.SetBlink(true)
```

`SetColors`では引数で、`ウィンドウ内側`、`枠の暗い時の色`、`枠の明るい時の色`を順に指定します。

```frame_window.go
// SetColors sets the colors of the window's inner region and the frame's
// normal color.
// If you need to blink the frame, please use the SetBlinkFrame method.
func (w *FrameWindow) SetColors(inner, frameDark, frameLight color.RGBA) {
	w.innerOp.ColorM.Scale(colorScale(inner))

以下省略
```

`SetBlink`に`true`を渡せば点滅`On`、点滅させないときは`false`を渡します。

### 描画する

あとは、`ebiten.Run`に第一引数で渡すコールバック関数、例えば以下のような`update`メソッドなど定期的に描画の更新を行う処理で`FrameWindow`の`DrawWindow`メソッドを実行するだけです。

```go
func update(screen *ebiten.Image) error {
	if ebiten.IsRunningSlowly() {
		return nil
	}

	frameWindow.DrawWindow(screen)
	return nil
}
```

## タッチ位置とウィンドウの領域の重なりを検知する

ここまでに説明してきた`FrameWindow`は、複数表示して`キャラクター選択画面`や`アイテム選択画面`などで使うことをイメージしています。これをタッチ操作で選択したいと思います。

[前回の記事](https://qiita.com/KemoKemo/items/31a29de33b2340bd96d6)の試作でタッチ位置は取得可能になっていますので、後は「タッチした位置とウィンドウ描画領域の重なりの有無」が分かればタッチされたウィンドウが判別できそうです。

実は上記で説明した`NewFrameWindow`メソッドの冒頭で、枠を含まないウィンドウ領域を算出しており`GetWindowRect`メソッドで取得できる仕組みにしてあります。

```frame_window.go
// GetWindowRect returns the rectangle of this window.
func (w *FrameWindow) GetWindowRect() image.Rectangle {
	return w.rect
}
```

そこで、`ユーザーが最初にタッチした位置`が`引数で渡した領域に含まれるか否か`を`bool`値で返してくれるメソッドを作りました。`FrameWindow`の`GetWindowRect`から取得した`image.Rectangle`を渡せば、タッチされたウィンドウが判別できます。

[input.go](https://github.com/kemokemo/ebiten-sketchbook/blob/master/select-one/input.go)

```input.go
// IsTouched returns the state that touched or not.
func IsTouched(r image.Rectangle) bool {
	IDs := ebiten.TouchIDs()
	if len(IDs) == 0 {
		return false
	}

	// use only the first touched point
	sort.Slice(IDs, func(i, j int) bool {
		return IDs[i] < IDs[j]
	})
	x, y := ebiten.TouchPosition(IDs[0])

	min := r.Min
	max := r.Max
	if min.X < x && x < max.X && min.Y < y && y < max.Y {
		return true
	}
	return false
}
```

## タッチされたウィンドウだけ点滅させる

後は「タッチされたよ！」とわかったウィンドウに対して`SetBlink(true)`を実行して描画すればおｋです。
次回はスワイプ操作のハンドリングとかやってみたい！