Rendering the Julia set in Golang
Table of Contents
Github | https://gist.github.com/ambuc/a36d007d177c7dd90153dc9ab174ee12 |
---|
Introduction⌗
In anticipation of Advent of Code 2019 I decided to brush up on http://golang.org/. I had written a crypto (codebreaking, not currency) library in Go in 2016 but I didn’t remember much of it.
The Julia set⌗
The Julia set is a fractal. It can be rendered by, for each point $(x,y)$ in a grid (ideally centered around $(0,0)$), taking the complex number $z = x + iy$ and applying $f_c(z) = z^2 + c$ to it repeatedly until it escapes some threshold from the origin. The precise values used are variable and somewhat open to artistic interpretation; in the code below, we use $c = -0.8 + 0.156i$ and a threshold of $1.9$.
Here is a gif of the lower-right-hand-quadrant of the Julia set where each frame has one more iteration of $f_c(z)$ than the previous one.
And here is the code which produces it:
package main
// Renders a series of julia_n.pngs which can be zipped together into a gif.
//
// Example usage:
// go run julia.go
// convert /tmp/julia*.png -loop 0 animation.gif
import (
"fmt"
"github.com/fogleman/gg"
"image/color"
"math"
"math/cmplx"
)
const (
// Variables for the Julia set.
kConst complex128 = -0.8 + 0.156i
kThreshold float64 = 1.9
// The scaling factor between pixels and floating point coordinates. A higher
// number means a more zoomed-in image.
kScaling float64 = 600.0
// The dimensions of the image.
kDim int = 500
// The number of frames to render at the most.
kSteps int = 50
)
// Computes a fractional |mu|-value given the integer number of steps |i| and
// the complex value |z|.
func Mu(i int, z complex128) float64 {
return float64(i) + 1.0 - math.Log(math.Log(cmplx.Abs(z)))/math.Log(2.0)
}
// Computes the shade for each pixel in a 2D array and returns the maximum
// mu-value (shade analogue) encountered during the aforementioned process.
func Compute(pixels *[kDim][kDim]float64, steps int) float64 {
var max_mu float64 = 0.0
for x := 0; x < kDim; x++ {
a := float64(x) / kScaling
for y := 0; y < kDim; y++ {
b := float64(y) / kScaling
z := complex(a, b)
for i := 0; i < steps; i++ {
z = cmplx.Pow(z, 2) + kConst
if cmplx.Abs(z) > kThreshold {
var mu float64 = Mu(i, z)
if mu > max_mu {
max_mu = mu
}
pixels[x][y] = mu
break
}
}
}
}
return max_mu
}
// Maps a floating-point number between 0 and 1 to a grayscale color between 0
// and 255.
func Shade(n float64) color.Color {
return color.Gray{uint8(n * (math.MaxInt8 - 1))}
}
// Renders a 2D array of pixels to a gg.Context object.
func Render(max_mu float64, pixels [kDim][kDim]float64) *gg.Context {
dc := gg.NewContext(kDim, kDim)
for x := 0; x < kDim; x++ {
for y := 0; y < kDim; y++ {
dc.SetColor(Shade(pixels[x][y] / max_mu))
dc.SetPixel(x, y)
}
}
dc.SetRGB(0, 0, 0)
dc.Fill()
return dc
}
func main() {
for f := 0; f < kSteps; f++ {
fmt.Printf("Step %v\n", f)
var pixels [kDim][kDim]float64
max_mu := Compute(&pixels, f /*steps*/)
Render(max_mu, pixels).SavePNG(fmt.Sprintf("/tmp/julia_%v.png", f))
}
}