Githubhttps://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))
  }
}