Space travel animated GIF generated by Golang

I created "Space travel" animated GIF with Golang:

Space travel (463KB)

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:

  1. Create image.RBGA instance.
  2. Draw using draw2d to the image
  3. Convert image.RBGA to image.Paletted
  4. Call gif.EncodeAll with []*image.Paletted in 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,
    })
}