Go 言語の標準画像処理パッケージで万華鏡画像を生成するデモアプリを作った

# はじめに
* Go 言語の素振りに github や slack の自動生成されるアイコン (Identicon) みたいなの作ってみたかった
* github みたらすでに Go 言語の実装があった https://github.com/cupcake/sigil ので +α ということで万華鏡的な画像を生成するアプリを作ってみた

# デモ
* https://go-kaleidoscope.herokuapp.com/
* クエリパラメータの `w` で画像サイズを `n` で生成する要素数を指定

# サンプル
![kaleidoscope1.png](https://qiita-image-store.s3.amazonaws.com/0/17778/2105a991-7fda-75c0-19e5-140f0dee50f7.png)
![kaleidoscope2.png](https://qiita-image-store.s3.amazonaws.com/0/17778/2b5adcc4-af62-5e0e-b12a-3d60f48ecb4b.png)

# レポジトリ
* https://github.com/nirasan/go-kaleidoscope

# ソースコード

## main.go
* サーバー起動用
* ルーティングくらいしか書いてない

```go
package main

import (
	"github.com/nirasan/go-kaleidoscope/handler"
	"log"
	"net/http"
	"os"
)

func main() {
	// 万華鏡画像表示 | クエリパラメータの w で画像サイズ(ピクセル) n で生成要素数(多いほどごちゃっとする)の指定
	http.HandleFunc("/", handler.Kaleidoscope)
	// 砂嵐画像表示
	http.HandleFunc("/mono", handler.Mono)
	// 縞画像表示
	http.HandleFunc("/stripe", handler.Stripe)
	// 縞画像の回転
	http.HandleFunc("/rotate", handler.Rotate)
	// 縞画像の扇形に切り抜いて回転してコピー
	http.HandleFunc("/sector", handler.Sector)
	// sector でピクセルの欠けが出たので修正版
	http.HandleFunc("/sector2", handler.Sector2)
	// 環境変数から公開ポートの指定
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}
	// サーバー起動
	log.Fatal(http.ListenAndServe(":"+port, nil))
}
```

## handler/kaleidoscope.go
* サーバーから呼び出されるハンドラー関数
* クエリパラメータをパースして画像生成関数に渡して描画している

```go
package handler

import (
	"github.com/nirasan/go-kaleidoscope/kaleidoscope"
	"image/png"
	"net/http"
	"strconv"
)

func Kaleidoscope(w http.ResponseWriter, r *http.Request) {

	q := r.URL.Query()

	// 画像サイズ
	width := 0
	if ws := q.Get("w"); ws != "" {
		var err error
		width, err = strconv.Atoi(ws)
		if err != nil {
			http.Error(w, "Invalid w parameter, must be an integer", http.StatusBadRequest)
			return
		}
	}

	// 生成要素数
	num := 0
	if ns := q.Get("n"); ns != "" {
		var err error
		num, err = strconv.Atoi(ns)
		if err != nil {
			http.Error(w, "Invalid n parameter, must be an integer", http.StatusBadRequest)
			return
		}
	}

	img := kaleidoscope.CreateImage(width, num)

	w.Header().Set("Content-Type", "image/png")
	png.Encode(w, img)
}
```

## kaleidoscope/kaleidoscope.go
* 画像生成パッケージ
* 指定されたサイズの画像の中に指定された個数の丸や四角を生成
* 丸や四角は大きさと色と位置はランダム
* 要素を配置し終えたら画像をもう一枚用意し先の画像から扇形に切り抜いたものを回転させながら円形に貼り付ける

```go
package kaleidoscope

import (
	"image"
	"image/color"
	"math"
	"math/rand"
	"time"
)

var (
	defaultWidth           = 300
	defaultHeight          = 300
	defaultCenterX         = 150
	defaultCenterY         = 150
	defaultSectorR float64 = 145
	defaultNum             = 30
)

// 万華鏡画像生成
func CreateImage(w, n int) *image.RGBA {

	rand.Seed(time.Now().UnixNano())

	width := defaultWidth
	height := defaultHeight
	centerX := defaultCenterX
	centerY := defaultCenterY
	sectorR := defaultSectorR

	// 画像サイズ
	if w != 0 {
		width = w
		height = width
		centerX = width / 2
		centerY = height / 2
		sectorR = float64(centerX) * 0.9
	}

	// 生成要素数
	num := defaultNum
	if n != 0 {
		num = n
	}

	rectList := []Rect{}
	circleList := []Circle{}

	// 丸と四角の生成
	for i := 0; i < num; i++ {
		rect := Rect{
			Position: &Position{X: rand.Intn(width), Y: rand.Intn(height)},
			Color:    randomColor(),
			W:        rand.Intn(50),
			H:        rand.Intn(50),
		}
		rectList = append(rectList, rect)
		circle := Circle{
			Position: &Position{X: rand.Intn(width), Y: rand.Intn(height)},
			Color:    randomColor(),
			R:        rand.Intn(50),
		}
		circleList = append(circleList, circle)
	}

	// 丸と四角の描画
	img := image.NewRGBA(image.Rect(0, 0, width, height))
	for x := 0; x < width; x++ {
		for y := 0; y < height; y++ {
			c := color.RGBA{255, 255, 255, 255}
			skip := false
			for _, circle := range circleList {
				if !skip && circle.Contain(x, y) {
					c = circle.Color
					skip = true
				}
			}
			for _, rect := range rectList {
				if !skip && rect.Contain(x, y) {
					c = rect.Color
					skip = true
				}
			}
			img.SetRGBA(x, y, c)
		}
	}

	// 扇形の範囲をコピーして回転
	// 扇形の回転軌道上にいる場合は元の座標から画素をとってくる
	img2 := image.NewRGBA(image.Rect(0, 0, width, height))
	for x := 0; x < width; x++ {
		for y := 0; y < height; y++ {
			degree, ok := getDegree(sectorR, centerX, centerY, centerX, height, x, y)
			if ok {
				for _, deg := range []float64{0, 60, 120, 180} {
					if degree < deg-30 || deg+30 <= degree {
						continue
					}
					// 180 度以上は逆回転させる
					if x < centerX {
						deg *= -1
					}
					rad := deg * math.Pi / 180
					x1 := float64(x-centerX)*math.Cos(rad) - float64(y-centerY)*math.Sin(rad) + float64(centerX)
					y1 := float64(x-centerX)*math.Sin(rad) + float64(y-centerY)*math.Cos(rad) + float64(centerY)
					x2, y2 := int(math.Floor(x1+.5)), int(math.Floor(y1+.5))
					if len(img.Pix) > y2*img.Stride+x2 {
						img2.SetRGBA(x, y, img.RGBAAt(x2, y2))
					}
				}
			}
		}
	}

	return img2
}

func randomColor() color.RGBA {
	return color.RGBA{
		R: uint8(rand.Intn(255)),
		G: uint8(rand.Intn(255)),
		B: uint8(rand.Intn(255)),
		A: 255,
	}
}

// x1, y1 と x2, y2 を中心線とする扇形の回転上にあったら中心線との角度を返す
func getDegree(r float64, x1, y1, x2, y2, x3, y3 int) (float64, bool) {
	xa, ya := x2-x1, y2-y1
	xb, yb := x3-x1, y3-y1
	ac := float64(xa*xb + ya*yb)
	an := math.Pow(float64(xa), 2) + math.Pow(float64(ya), 2)
	bn := math.Pow(float64(xb), 2) + math.Pow(float64(yb), 2)
	rad := math.Acos(ac / math.Sqrt(an*bn))
	deg := rad * 180 / math.Pi
	dist := math.Sqrt(math.Pow(float64(xb), 2) + math.Pow(float64(yb), 2))
	if dist < r {
		return deg, true
	}
	return 0, false
}

type Position struct {
	X, Y int
}

type Circle struct {
	*Position
	R     int
	Color color.RGBA
}

func (c *Circle) Contain(x, y int) bool {
	r := math.Sqrt(math.Pow(float64(x-c.Position.X), 2) + math.Pow(float64(y-c.Position.Y), 2))
	return r <= float64(c.R)
}

type Rect struct {
	*Position
	W, H  int
	Color color.RGBA
}

func (r *Rect) Contain(x, y int) bool {
	x1, y1 := r.Position.X-r.W/2, r.Position.Y-r.H/2
	x2, y2 := x1+r.W, y1+r.H
	return x1 <= x && x <= x2 && y1 <= y && y <= y2
}
```

# 簡単な解説

* 以下の部分が画像データの生成

```go
img := image.NewRGBA(image.Rect(0, 0, width, height))
```

* 以下の部分で縦 x 横 y のピクセルに c の色をのせる

```go
for x := 0; x < width; x++ {
  for y := 0; y < height; y++ {
    c := color.RGBA{255, 255, 255, 255}
    ... 中略 ...
    img.SetRGBA(x, y, c)
  }
}
```

* 丸 (Circle) と四角 (Rect) は生成されると論理的な位置を持ち、各ピクセルが丸や四角に内包されていたら色をもらって描画する

```go
c := color.RGBA{255, 255, 255, 255}
skip := false
for _, circle := range circleList {
  if !skip && circle.Contain(x, y) {
    c = circle.Color
    skip = true
  }
}
for _, rect := range rectList {
  if !skip && rect.Contain(x, y) {
    c = rect.Color
    skip = true
  }
}
img.SetRGBA(x, y, c)
```

* 以下で描画用の画像データをもう一つ作成

```go
img2 := image.NewRGBA(image.Rect(0, 0, width, height))
```

* 画像中央の点から垂直に下に伸ばした線と中央から各点へ伸ばした線との角度を取得

```go
degree, ok := getDegree(sectorR, centerX, centerY, centerX, height, x, y)
```

* 各点がコピー元の扇形を何度回転させた位置にあるかを判定してコピー元の色を描画用の画像データにコピーする
* 扇形を単純に回転させてペーストすると http://homepage2.nifty.com/tsugu/sotuken/rotation/#basic にあるようにピクセルの抜けが出てくるので記載のとおりに対処した

```go
if ok {
  for _, deg := range []float64{0, 60, 120, 180} {
    if degree < deg-30 || deg+30 <= degree {
      continue
    }
    // 180 度以上は逆回転させる
    if x < centerX {
      deg *= -1
    }
    rad := deg * math.Pi / 180
    x1 := float64(x-centerX)*math.Cos(rad) - float64(y-centerY)*math.Sin(rad) + float64(centerX)
    y1 := float64(x-centerX)*math.Sin(rad) + float64(y-centerY)*math.Cos(rad) + float64(centerY)
    x2, y2 := int(math.Floor(x1+.5)), int(math.Floor(y1+.5))
    if len(img.Pix) > y2*img.Stride+x2 {
      img2.SetRGBA(x, y, img.RGBAAt(x2, y2))
    }
  }
}
```

* 最後にヘッダと画像データを出力

```go
w.Header().Set("Content-Type", "image/png")
png.Encode(w, img2)
```