package main import ( "image" "image/color" "image/png" "log" "math" "math/cmplx" "net/http" "runtime" "strconv" "strings" "sync" "deadbeef.codes/steven/mandelmapper/palette" ) const ( Size = 128 Iterations = (1<<16 - 1) / 128 ) var ( colors []color.RGBA queue = make(chan pixel) ) type pixel struct { out *image.RGBA x, y int tileX, tileY int64 tileZoom uint16 wg *sync.WaitGroup } func init() { http.HandleFunc("/mandelbrot/", renderTile) } func main() { runtime.GOMAXPROCS(runtime.NumCPU()) for i := 0; i < runtime.GOMAXPROCS(0); i++ { go computeThread() } colorStep := float64(Iterations) // colors = interpolateColors("Plan9", colorStep) colors = interpolateColors("Vivid", colorStep) log.Fatal(http.ListenAndServe(":6161", nil)) } func computeThread() { for p := range queue { val := mandelbrot( complex( (float64(p.x)/Size+float64(p.tileX))/float64(uint(1<<(p.tileZoom-1))), (float64(p.y)/Size+float64(p.tileY))/float64(uint(1<<(p.tileZoom-1))), ), ) p.out.SetRGBA(p.x, p.y, colors[val]) p.wg.Done() } } //interpolateColors accepts a color palette and number of desired colors and builds a slice of colors by interpolating the gaps 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 != "" { 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 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 renderTile(w http.ResponseWriter, r *http.Request) { components := strings.Split(r.URL.Path, "/")[1:] if len(components) != 4 || components[0] != "mandelbrot" || components[3][len(components[3])-4:] != ".png" { w.WriteHeader(http.StatusNotFound) return } components[3] = components[3][:len(components[3])-4] tileX, err := strconv.ParseInt(components[2], 10, 64) if err != nil { w.WriteHeader(http.StatusNotFound) return } tileY, err := strconv.ParseInt(components[3], 10, 64) if err != nil { w.WriteHeader(http.StatusNotFound) return } tileZoom, err := strconv.ParseUint(components[1], 10, 8) if err != nil { w.WriteHeader(http.StatusNotFound) return } var wg sync.WaitGroup wg.Add(Size * Size) img := image.NewRGBA(image.Rect(0, 0, Size, Size)) for x := 0; x < Size; x++ { for y := 0; y < Size; y++ { queue <- pixel{img, x, y, tileX - int64(1<