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.RBGA
instance. - Draw using draw2d to the image
- Convert
image.RBGA
toimage.Paletted
- 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,
})
}