Space travel animated GIF generated by Golang
I created "Space travel" animated GIF with Golang:

Source code is at the bottom of this article. Let me explain the library I used.
Using draw2d to draw image
It's difficult to draw complicated figure without using external library. With draw2d library, we can draw lines, arcs, bezier curves and primitive shapes. Of course, we can set line color and fill color.
Following code renders #808080 rectangle using draw2dimg and draw2dkit.
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 function requires image.RGBA object, although animated gif encoder (gif.EncodeAll) expects image.Palettted to be passed.
So, when we generate an animated GIF with draw2d, we have to:
- Create
image.RBGAinstance. - Draw using draw2d to the image
- Convert
image.RBGAtoimage.Paletted - Call
gif.EncodeAllwith[]*image.Palettedin order to create animated gif
Convert image.RBGA to image.Paletted
gif.Encode automatically convert image.RBGA to image.Paletted using draw.FloydSteinberg, but gif.EncodeAll doesn't.
So we have to convert for our own. Let's convert using draw.FloydSteinberg like gif.Encode method:
package main
import (
"image"
"image/color"
"image/draw"
)
func main() {
img := image.NewRGBA(image.Rect(0, 0, 200, 200))
// Initialize palette (#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})
// Dithering
pm := image.NewPaletted(img.Bounds(), palette)
draw.FloydSteinberg.Draw(pm, img.Bounds(), img, image.ZP)
}
Full source code
Here is the full source code (100 lines):
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
}
// Draw stars in order to generate perfect loop GIF
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)
// Draw background
gc.SetFillColor(color.Gray{0x11})
draw2dkit.Rectangle(gc, 0, 0, w, h)
gc.Fill()
gc.Close()
// Draw stars
for _, circle := range circles {
circle.Draw(gc, ratio)
}
// Dithering
pm := image.NewPaletted(img.Bounds(), palette)
draw.FloydSteinberg.Draw(pm, img.Bounds(), img, image.ZP)
return pm
}
func main() {
// Create 4000 stars
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})
}
// Intiialize palette (#000000, #111111, ..., #ffffff)
palette = color.Palette{}
for i := 0; i < 16; i++ {
palette = append(palette, color.Gray{uint8(i) * 0x11})
}
// Generate 30 frames
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)
}
// Output 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,
})
}