宇宙旅行風のアニメーション GIF を Golang で生成してみました。完成品はこちら。
ソースコードはこの記事の末尾に掲載しています。以下では使ったライブラリーやテクニックを簡単に説明します。
draw2d を使って描画する
Golang の標準ライブラリーだけでは複雑な図形を描画するのは難しいので、draw2d を使ってみることにした。こいつを使えば、線とか弧とかベジェ曲線を描けるし、線の色や塗る色も設定できる。
次のコードでは、draw2dimg
と draw2dkit
を使って、#808080 の四角を描画する例。
package main
import (
"github.com/llgcode/draw2d/draw2dimg"
"github.com/llgcode/draw2d/draw2dkit"
"image"
"image/color"
)
func main() {
img := image.NewRGBA(image.Rect(0, 0, 200, 200))
gc := draw2dimg.NewGraphicContext(img)
// Draw rectangle (#808080)
gc.SetFillColor(color.Gray{0x80})
draw2dkit.Rectangle(gc, 50, 50, 100, 100)
gc.Fill()
gc.Close()
}
draw2dimg.NewGraphicContext
は引数に image.RGBA
(透明度つきの RGB 画像) を渡す必要があるんだけど、アニメーション GIF を gif.EncodeAll
で作るときには image.Palettted
(パレットの色だけを使った画像) を渡さなきゃいけない。
つまり、draw2d でアニメーション GIF を作るには、次のような処理が必要になる。
iamge.RBGA
を作る- draw2d を使って描画する
image.RBGA
をimage.Paletted
に変換するgif.EncodeAll
に[]*image.Paletted
を渡して、アニメーション GIF を作る
image.RBGA
を image.Paletted
に変換する方法
1 枚の GIF を生成する gif.Encode
は自動的に image.RBGA
を image.Paletted
に変換するんだけど、アニメーション GIF を生成する gif.EncodeAll
は変換してくれない。
なので、自分で変換処理を実装する必要がある。といっても、gif.Encode
と同じように、標準ライブラリで用意された draw.FloydSteinberg
を使って フロイド-スタインバーグ・ディザリング を使うと簡単。こんな風に。
package main
import (
"image"
"image/color"
"image/draw"
)
func main() {
img := image.NewRGBA(image.Rect(0, 0, 200, 200))
// パレットを準備 (#ffffff, #000000, #ff0000)
var palette color.Palette = color.Palette{}
palette = append(palette, color.White)
palette = append(palette, color.Black)
palette = append(palette, color.RGBA{0xff, 0x00, 0x00, 0xff})
// ディザリングする
pm := image.NewPaletted(img.Bounds(), palette)
draw.FloydSteinberg.Draw(pm, img.Bounds(), img, image.ZP)
}
ソースコード全体
全部で 100 行になってます。
package main
import (
"github.com/llgcode/draw2d/draw2dimg"
"github.com/llgcode/draw2d/draw2dkit"
"image"
"image/color"
"image/draw"
"image/gif"
"math"
"math/rand"
"os"
)
var w, h float64 = 500, 250
var palette color.Palette = color.Palette{}
var zCycle float64 = 8
var zMin, zMax float64 = 1, 15
type Point struct {
X, Y float64
}
type Circle struct {
X, Y, Z, R float64
}
// ループするように星を描画する
func (c *Circle) Draw(gc *draw2dimg.GraphicContext, ratio float64) {
z := c.Z - ratio*zCycle
for z < zMax {
if z >= zMin {
x, y, r := c.X/z, c.Y/z, c.R/z
gc.SetFillColor(color.White)
gc.Fill()
draw2dkit.Circle(gc, w/2+x, h/2+y, r)
gc.Close()
}
z += zCycle
}
}
func drawFrame(circles []Circle, ratio float64) *image.Paletted {
img := image.NewRGBA(image.Rect(0, 0, int(w), int(h)))
gc := draw2dimg.NewGraphicContext(img)
// 背景を描画
gc.SetFillColor(color.Gray{0x11})
draw2dkit.Rectangle(gc, 0, 0, w, h)
gc.Fill()
gc.Close()
// 星を描画
for _, circle := range circles {
circle.Draw(gc, ratio)
}
// ディザリングする
pm := image.NewPaletted(img.Bounds(), palette)
draw.FloydSteinberg.Draw(pm, img.Bounds(), img, image.ZP)
return pm
}
func main() {
// 4000 個の星を準備
circles := []Circle{}
for len(circles) < 4000 {
x, y := rand.Float64()*8-4, rand.Float64()*8-4
if math.Abs(x) < 0.5 && math.Abs(y) < 0.5 {
continue
}
z := rand.Float64() * zCycle
circles = append(circles, Circle{x * w, y * h, z, 5})
}
// パレットを準備 (#000000, #111111, ..., #ffffff)
palette = color.Palette{}
for i := 0; i < 16; i++ {
palette = append(palette, color.Gray{uint8(i) * 0x11})
}
// 30 個の画像を作成
var images []*image.Paletted
var delays []int
count := 30
for i := 0; i < count; i++ {
pm := drawFrame(circles, float64(i)/float64(count))
images = append(images, pm)
delays = append(delays, 4)
}
// gif を出力
f, _ := os.OpenFile("space.gif", os.O_WRONLY|os.O_CREATE, 0600)
defer f.Close()
gif.EncodeAll(f, &gif.GIF{
Image: images,
Delay: delays,
})
}