package main import ( "flag" "fmt" "image" "image/color" "image/png" "math" "os" "sync" "time" "deadbeef.codes/steven/mandelbrot/palette" ) var ( colorPalette string colorStep float64 xpos, ypos float64 ) var waitGroup sync.WaitGroup func init() { flag.Float64Var(&colorStep, "step", 12000, "Color smooth step. Value should be greater than iteration count, otherwise the value will be adjusted to the iteration count.") flag.Float64Var(&xpos, "xpos", -0.00275, "Point position on the real axis (defined on `x` axis)") flag.Float64Var(&ypos, "ypos", 0.78912, "Point position on the imaginary axis (defined on `y` axis)") flag.StringVar(&colorPalette, "palette", "Plan9", "Hippi | Plan9 | AfternoonBlue | SummerBeach | Biochimist | Fiesta") flag.Parse() } func main() { //var escapeRadius float64 = 0.125689 var escapeRadius float64 = -0.01 maxIteration := 800 imageSmoothness := 1 width := 1920 height := 1080 if colorStep < float64(maxIteration) { colorStep = float64(maxIteration) } colors := interpolateColors(&colorPalette, colorStep) if len(colors) > 0 { fmt.Println("Rendering images...") for i := 1; i <= 15000; i++ { render(maxIteration, colors, escapeRadius, imageSmoothness, width, height, fmt.Sprintf("images/%08d.png", i)) escapeRadius += 0.001 maxIteration += 1 } } time.Sleep(time.Second) } func interpolateColors(paletteCode *string, numberOfColors float64) []color.RGBA { var factor float64 steps := []float64{} cols := []uint32{} interpolated := []uint32{} interpolatedColors := []color.RGBA{} for _, v := range palette.ColorPalettes { factor = 1.0 / numberOfColors switch v.Keyword { case *paletteCode: if paletteCode != nil { for index, col := range v.Colors { if col.Step == 0.0 && index != 0 { stepRatio := float64(index+1) / float64(len(v.Colors)) step := float64(int(stepRatio*100)) / 100 // truncate to 2 decimal precision steps = append(steps, step) } else { steps = append(steps, col.Step) } r, g, b, a := col.Color.RGBA() r /= 0xff g /= 0xff b /= 0xff a /= 0xff uintColor := uint32(r)<<24 | uint32(g)<<16 | uint32(b)<<8 | uint32(a) cols = append(cols, uintColor) } var min, max, minColor, maxColor float64 if len(v.Colors) == len(steps) && len(v.Colors) == len(cols) { for i := 0.0; i <= 1; i += factor { for j := 0; j < len(v.Colors)-1; j++ { if i >= steps[j] && i < steps[j+1] { min = steps[j] max = steps[j+1] minColor = float64(cols[j]) maxColor = float64(cols[j+1]) uintColor := cosineInterpolation(maxColor, minColor, (i-min)/(max-min)) interpolated = append(interpolated, uint32(uintColor)) } } } } for _, pixelValue := range interpolated { r := pixelValue >> 24 & 0xff g := pixelValue >> 16 & 0xff b := pixelValue >> 8 & 0xff a := 0xff interpolatedColors = append(interpolatedColors, color.RGBA{uint8(r), uint8(g), uint8(b), uint8(a)}) } } } } return interpolatedColors } func render(maxIteration int, colors []color.RGBA, escapeRadius float64, imageSmoothness, width, height int, outputFile string) { fmt.Printf("rendering image %s\n", outputFile) width = width * imageSmoothness height = height * imageSmoothness ratio := float64(height) / float64(width) xmin, xmax := xpos-escapeRadius/2.0, math.Abs(xpos+escapeRadius/2.0) ymin, ymax := ypos-escapeRadius*ratio/2.0, math.Abs(ypos+escapeRadius*ratio/2.0) image := image.NewRGBA(image.Rectangle{image.Point{0, 0}, image.Point{width, height}}) for iy := 0; iy < height; iy++ { waitGroup.Add(1) go func(iy int) { defer waitGroup.Done() for ix := 0; ix < width; ix++ { var x = xmin + (xmax-xmin)*float64(ix)/float64(width-1) var y = ymin + (ymax-ymin)*float64(iy)/float64(height-1) norm, it := mandelIteration(x, y, maxIteration) iteration := float64(maxIteration-it) + math.Log(norm) if int(math.Abs(iteration)) < len(colors)-1 { color1 := colors[int(math.Abs(iteration))] color2 := colors[int(math.Abs(iteration))+1] color := linearInterpolation(rgbaToUint(color1), rgbaToUint(color2), uint32(iteration)) image.Set(ix, iy, uint32ToRgba(color)) } } }(iy) } waitGroup.Wait() output, _ := os.Create(outputFile) png.Encode(output, image) } func cosineInterpolation(c1, c2, mu float64) float64 { mu2 := (1 - math.Cos(mu*math.Pi)) / 2.0 return c1*(1-mu2) + c2*mu2 } func linearInterpolation(c1, c2, mu uint32) uint32 { return c1*(1-mu) + c2*mu } func mandelIteration(cx, cy float64, maxIter int) (float64, int) { var x, y, xx, yy float64 = 0.0, 0.0, 0.0, 0.0 for i := 0; i < maxIter; i++ { xy := x * y xx = x * x yy = y * y if xx+yy > 4 { return xx + yy, i } x = xx - yy + cx y = 2*xy + cy } logZn := (x*x + y*y) / 2 return logZn, maxIter } func rgbaToUint(color color.RGBA) uint32 { r, g, b, a := color.RGBA() r /= 0xff g /= 0xff b /= 0xff a /= 0xff return uint32(r)<<24 | uint32(g)<<16 | uint32(b)<<8 | uint32(a) } func uint32ToRgba(col uint32) color.RGBA { r := col >> 24 & 0xff g := col >> 16 & 0xff b := col >> 8 & 0xff a := 0xff return color.RGBA{uint8(r), uint8(g), uint8(b), uint8(a)} }