├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ └── lint.yml ├── .gitignore ├── README.md ├── benchmark_test.go ├── customTypes.go ├── examples ├── ex1-zoom-1.png ├── ex2-zoom-53.png ├── ex3-zoom-1.5e6.png ├── ex4-zoom-1e11.png ├── ex5-zoom-4e12.png ├── ex6-gray-7.png ├── ex7-gray-8.png ├── ex8-gray-9.png └── ex9-gray-48.png ├── go.mod ├── go.sum ├── hsl.go ├── linear.go ├── locations.json ├── main.go ├── mandelbrot.go ├── rand.go └── scraper.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" 16 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | go-version: ['1.19.x', '1.20.x', '1.21.x'] 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Setup Go ${{ matrix.go-version }} 20 | uses: actions/setup-go@v5 21 | with: 22 | go-version: ${{ matrix.go-version }} 23 | 24 | - name: Install dependencies 25 | run: go get ./... 26 | 27 | - name: Build 28 | run: go build -v -race ./... 29 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | golangci: 14 | name: lint 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | go-version: ['1.19.x', '1.20.x', '1.21.x'] 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Setup Go ${{ matrix.go-version }} 22 | uses: actions/setup-go@v5 23 | with: 24 | go-version: ${{ matrix.go-version }} 25 | - name: golangci-lint 26 | uses: golangci/golangci-lint-action@v6 27 | with: 28 | version: v1.54 29 | args: --verbose 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | 23 | # Images 24 | *.png 25 | 26 | # VScode 27 | .vscode/ 28 | launch.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fractals 2 | 3 | **fractals** is a customizable renderer for the Mandelbrot set written in Go. It uses Go's **goroutines** to achieve high performance. 4 | 5 | ### 🚀 Featured in [Golang Weekly #464](https://golangweekly.com/issues/464) 🚀 6 | 7 | ## Usage 8 | ```sh 9 | git clone https://github.com/joweich/fractals.git 10 | cd fractals 11 | go build 12 | ./fractals -h # to see list of available customizations 13 | ./fractals -height 1000 -width 1000 # fractals.exe for Windows systems 14 | ``` 15 | 16 | ## Examples 17 | #### Colored 18 | 19 | 20 | 23 | 26 | 27 | 28 | 31 | 34 | 35 |
21 | 22 | 24 | 25 |
29 | 30 | 32 | 33 |
36 | 37 | #### Grayscale 38 | 39 | 40 | 43 | 46 | 47 | 48 | 51 | 54 | 55 |
41 | 42 | 44 | 45 |
49 | 50 | 52 | 53 |
56 | 57 | ## About the Algorithm 58 | ### The Math in a Nutshell 59 | The Mandelbrot set is defined as the set of complex numbers $c$ for which the series 60 | 61 | $$z_{n+1} = z²_n + c$$ 62 | 63 | is bounded for all $n ≥ 0$. In other words, $c$ is part of the Mandelbrot set if $z_n$ does not approach infinity. This is equivalent to the magnitude $|z_n| ≤ 2$ for all $n ≥ 0$. 64 | 65 | ### But how is this visualized in a colorful image? 66 | The image is interpreted as complex plane, i.e. the horizontal axis being the real part and the vertical axis representing the complex part of $c$. 67 | 68 | The colors are determined by the so-called **naïve escape time algorithm**. It's as simple as that: A pixel is painted in a predefined color (often black) if it's in the set and will have another color if it's not. The color is determined by the number of iterations $n$ needed for $z_n$ to exceed $|z_n| = 2$. This $n$ is the escape time, and $|z_n| ≥ 2$ is the escape condition. In our implementation, this is done via the _hue_ parameter in the [HSL color model](https://en.wikipedia.org/wiki/HSL_and_HSV) for non-grayscale images, and the _lightness_ parameter for grayscale images. 69 | 70 | ### And how does it leverage Goroutines? 71 | Each row of the image is added as a job to a [channel](https://go.dev/doc/effective_go#channels). These jobs are distributed using [goroutines](https://go.dev/doc/effective_go#goroutines) (lightweight threads managed by the Go runtime) that are spun off by consuming from the channel until it's empty. 72 | 73 | ## Advanced Rendering Features 74 | * Linear color mixing ([source](https://github.com/ncruces/go-image/blob/v0.1.0/imageutil/srgb.go)) 75 | * Anti-aliasing by random sampling ([source](https://www.fractalus.com/info/antialias.htm)) 76 | * _Normative iteration count_ to smooth stair-step function ([math behind](http://linas.org/art-gallery/escape/escape.html)) 77 | -------------------------------------------------------------------------------- /benchmark_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | type BenchmarkConfig struct { 11 | MaxIterValues []int 12 | ImgSizeValues []int 13 | loc Location 14 | } 15 | 16 | func getBenchmarkConfig() BenchmarkConfig { 17 | imgConf = ImageConfig{ 18 | Width: -1, 19 | Height: -1, 20 | Samples: 5, 21 | MaxIter: -1, 22 | Offset: 0.0, 23 | Mixing: true, 24 | InsideBlack: true, 25 | Grayscale: false, 26 | RndGlobal: uint64(time.Now().UnixNano()), 27 | } 28 | 29 | return BenchmarkConfig{ 30 | MaxIterValues: []int{10, 100, 1000, 10000}, 31 | ImgSizeValues: []int{100, 1000}, 32 | loc: Location{ 33 | XCenter: -0.5, 34 | YCenter: 0, 35 | Zoom: 1, 36 | }, 37 | } 38 | 39 | } 40 | 41 | func BenchmarkRenderImage(b *testing.B) { 42 | benchmarkConfig := getBenchmarkConfig() 43 | 44 | for _, size := range benchmarkConfig.ImgSizeValues { 45 | imgConf.Height = size 46 | imgConf.Width = size 47 | for _, maxIter := range benchmarkConfig.MaxIterValues { 48 | imgConf.MaxIter = maxIter 49 | testId := fmt.Sprintf("Size_%d_MaxIter_%d", size, maxIter) 50 | b.Run(testId, func(subB *testing.B) { 51 | img := image.NewRGBA(image.Rect(0, 0, imgConf.Width, imgConf.Height)) 52 | 53 | subB.ResetTimer() 54 | for i := 0; i < subB.N; i++ { 55 | renderImage(img, benchmarkConfig.loc) 56 | } 57 | }) 58 | } 59 | } 60 | } 61 | 62 | func BenchmarkRenderRow(b *testing.B) { 63 | benchmarkConfig := getBenchmarkConfig() 64 | 65 | for _, size := range benchmarkConfig.ImgSizeValues { 66 | imgConf.Height = size 67 | imgConf.Width = size 68 | for _, maxIter := range benchmarkConfig.MaxIterValues { 69 | imgConf.MaxIter = maxIter 70 | testId := fmt.Sprintf("Size_%d_MaxIter_%d", size, maxIter) 71 | b.Run(testId, func(subB *testing.B) { 72 | img := image.NewRGBA(image.Rect(0, 0, imgConf.Width, imgConf.Height)) 73 | subB.ResetTimer() 74 | 75 | rndLocal := RandUint64(&imgConf.RndGlobal) 76 | for i := 0; i < subB.N; i++ { 77 | renderRow(benchmarkConfig.loc, imgConf.Height/2, img, &rndLocal) 78 | } 79 | }) 80 | } 81 | } 82 | } 83 | 84 | func BenchmarkGetColorForPixel(b *testing.B) { 85 | benchmarkConfig := getBenchmarkConfig() 86 | imgConf.Height = 1000 87 | imgConf.Width = 1000 88 | 89 | for _, maxIter := range benchmarkConfig.MaxIterValues { 90 | imgConf.MaxIter = maxIter 91 | testId := fmt.Sprintf("MaxIter_%d", maxIter) 92 | b.Run(testId, func(subB *testing.B) { 93 | subB.ResetTimer() 94 | 95 | for i := 0; i < subB.N; i++ { 96 | getColorForPixel(benchmarkConfig.loc, imgConf.Height/2, imgConf.Width/2, &imgConf.RndGlobal) 97 | } 98 | }) 99 | } 100 | } 101 | 102 | func BenchmarkGetColorFromMandelbrotUnlimited(b *testing.B) { 103 | magnitude := 2.0 104 | iterations := 100 105 | b.ResetTimer() 106 | 107 | for i := 0; i < b.N; i++ { 108 | getColorFromMandelbrot(true, magnitude, iterations) 109 | } 110 | } 111 | 112 | func BenchmarkRunMandelbrot(b *testing.B) { 113 | benchmarkConfig := getBenchmarkConfig() 114 | 115 | c := complex(-0.5, 0) // (-0.5, 0) is part of the mandelbrot set, i.e. zn is bounded for all zn 116 | for _, maxIter := range benchmarkConfig.MaxIterValues { 117 | testId := fmt.Sprintf("MaxIter_%d", maxIter) 118 | b.Run(testId, func(subB *testing.B) { 119 | imgConf.MaxIter = maxIter 120 | subB.ResetTimer() 121 | 122 | for i := 0; i < subB.N; i++ { 123 | runMandelbrot(c) 124 | } 125 | }) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /customTypes.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type Location struct { 4 | XCenter float64 5 | YCenter float64 6 | Zoom float64 7 | } 8 | 9 | type LocationsFile struct { 10 | Locations []Location 11 | } 12 | 13 | type ImageConfig struct { 14 | Width int 15 | Height int 16 | Samples int 17 | MaxIter int 18 | Offset float64 19 | Mixing bool 20 | InsideBlack bool 21 | Grayscale bool 22 | RndGlobal uint64 23 | } 24 | -------------------------------------------------------------------------------- /examples/ex1-zoom-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joweich/fractals/7e1dd0eb7dd9d376cf3b274e7bcb62354d4db7c8/examples/ex1-zoom-1.png -------------------------------------------------------------------------------- /examples/ex2-zoom-53.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joweich/fractals/7e1dd0eb7dd9d376cf3b274e7bcb62354d4db7c8/examples/ex2-zoom-53.png -------------------------------------------------------------------------------- /examples/ex3-zoom-1.5e6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joweich/fractals/7e1dd0eb7dd9d376cf3b274e7bcb62354d4db7c8/examples/ex3-zoom-1.5e6.png -------------------------------------------------------------------------------- /examples/ex4-zoom-1e11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joweich/fractals/7e1dd0eb7dd9d376cf3b274e7bcb62354d4db7c8/examples/ex4-zoom-1e11.png -------------------------------------------------------------------------------- /examples/ex5-zoom-4e12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joweich/fractals/7e1dd0eb7dd9d376cf3b274e7bcb62354d4db7c8/examples/ex5-zoom-4e12.png -------------------------------------------------------------------------------- /examples/ex6-gray-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joweich/fractals/7e1dd0eb7dd9d376cf3b274e7bcb62354d4db7c8/examples/ex6-gray-7.png -------------------------------------------------------------------------------- /examples/ex7-gray-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joweich/fractals/7e1dd0eb7dd9d376cf3b274e7bcb62354d4db7c8/examples/ex7-gray-8.png -------------------------------------------------------------------------------- /examples/ex8-gray-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joweich/fractals/7e1dd0eb7dd9d376cf3b274e7bcb62354d4db7c8/examples/ex8-gray-9.png -------------------------------------------------------------------------------- /examples/ex9-gray-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joweich/fractals/7e1dd0eb7dd9d376cf3b274e7bcb62354d4db7c8/examples/ex9-gray-48.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module fractals 2 | 3 | go 1.20 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joweich/fractals/7e1dd0eb7dd9d376cf3b274e7bcb62354d4db7c8/go.sum -------------------------------------------------------------------------------- /hsl.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "image/color" 5 | ) 6 | 7 | // https://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c 8 | 9 | func hueToRGB(p, q, t float64) float64 { 10 | if t < 0 { 11 | t += 1 12 | } 13 | if t > 1 { 14 | t -= 1 15 | } 16 | switch { 17 | case t < 1.0/6.0: 18 | return p + (q-p)*6*t 19 | case t < 1.0/2.0: 20 | return q 21 | case t < 2.0/3.0: 22 | return p + (q-p)*(2.0/3.0-t)*6 23 | default: 24 | return p 25 | } 26 | } 27 | 28 | func hslToRGB(h, s, l float64) color.RGBA { 29 | var r, g, b float64 30 | if s == 0 { 31 | r, g, b = l, l, l 32 | } else { 33 | var q, p float64 34 | if l < 0.5 { 35 | q = l * (1 + s) 36 | } else { 37 | q = l + s - l*s 38 | } 39 | p = 2*l - q 40 | r = hueToRGB(p, q, h+1.0/3.0) 41 | g = hueToRGB(p, q, h) 42 | b = hueToRGB(p, q, h-1.0/3.0) 43 | } 44 | return color.RGBA{R: uint8(r * 255), G: uint8(g * 255), B: uint8(b * 255), A: 255} 45 | } 46 | -------------------------------------------------------------------------------- /linear.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // https://github.com/ncruces/go-image/blob/v0.1.0/imageutil/srgb.go 4 | 5 | var rgb2lin = [...]uint16{ 6 | 0x0000, 0x0014, 0x0028, 0x003c, 0x0050, 0x0063, 0x0077, 0x008b, 7 | 0x009f, 0x00b3, 0x00c7, 0x00db, 0x00f1, 0x0108, 0x0120, 0x0139, 8 | 0x0154, 0x016f, 0x018c, 0x01ab, 0x01ca, 0x01eb, 0x020e, 0x0232, 9 | 0x0257, 0x027d, 0x02a5, 0x02ce, 0x02f9, 0x0325, 0x0353, 0x0382, 10 | 0x03b3, 0x03e5, 0x0418, 0x044d, 0x0484, 0x04bc, 0x04f6, 0x0532, 11 | 0x056f, 0x05ad, 0x05ed, 0x062f, 0x0673, 0x06b8, 0x06fe, 0x0747, 12 | 0x0791, 0x07dd, 0x082a, 0x087a, 0x08ca, 0x091d, 0x0972, 0x09c8, 13 | 0x0a20, 0x0a79, 0x0ad5, 0x0b32, 0x0b91, 0x0bf2, 0x0c55, 0x0cba, 14 | 0x0d20, 0x0d88, 0x0df2, 0x0e5e, 0x0ecc, 0x0f3c, 0x0fae, 0x1021, 15 | 0x1097, 0x110e, 0x1188, 0x1203, 0x1280, 0x1300, 0x1381, 0x1404, 16 | 0x1489, 0x1510, 0x159a, 0x1625, 0x16b2, 0x1741, 0x17d3, 0x1866, 17 | 0x18fb, 0x1993, 0x1a2c, 0x1ac8, 0x1b66, 0x1c06, 0x1ca7, 0x1d4c, 18 | 0x1df2, 0x1e9a, 0x1f44, 0x1ff1, 0x20a0, 0x2150, 0x2204, 0x22b9, 19 | 0x2370, 0x242a, 0x24e5, 0x25a3, 0x2664, 0x2726, 0x27eb, 0x28b1, 20 | 0x297b, 0x2a46, 0x2b14, 0x2be3, 0x2cb6, 0x2d8a, 0x2e61, 0x2f3a, 21 | 0x3015, 0x30f2, 0x31d2, 0x32b4, 0x3399, 0x3480, 0x3569, 0x3655, 22 | 0x3742, 0x3833, 0x3925, 0x3a1a, 0x3b12, 0x3c0b, 0x3d07, 0x3e06, 23 | 0x3f07, 0x400a, 0x4110, 0x4218, 0x4323, 0x4430, 0x453f, 0x4651, 24 | 0x4765, 0x487c, 0x4995, 0x4ab1, 0x4bcf, 0x4cf0, 0x4e13, 0x4f39, 25 | 0x5061, 0x518c, 0x52b9, 0x53e9, 0x551b, 0x5650, 0x5787, 0x58c1, 26 | 0x59fe, 0x5b3d, 0x5c7e, 0x5dc2, 0x5f09, 0x6052, 0x619e, 0x62ed, 27 | 0x643e, 0x6591, 0x66e8, 0x6840, 0x699c, 0x6afa, 0x6c5b, 0x6dbe, 28 | 0x6f24, 0x708d, 0x71f8, 0x7366, 0x74d7, 0x764a, 0x77c0, 0x7939, 29 | 0x7ab4, 0x7c32, 0x7db3, 0x7f37, 0x80bd, 0x8246, 0x83d1, 0x855f, 30 | 0x86f0, 0x8884, 0x8a1b, 0x8bb4, 0x8d50, 0x8eef, 0x9090, 0x9235, 31 | 0x93dc, 0x9586, 0x9732, 0x98e2, 0x9a94, 0x9c49, 0x9e01, 0x9fbb, 32 | 0xa179, 0xa339, 0xa4fc, 0xa6c2, 0xa88b, 0xaa56, 0xac25, 0xadf6, 33 | 0xafca, 0xb1a1, 0xb37b, 0xb557, 0xb737, 0xb919, 0xbaff, 0xbce7, 34 | 0xbed2, 0xc0c0, 0xc2b1, 0xc4a5, 0xc69c, 0xc895, 0xca92, 0xcc91, 35 | 0xce94, 0xd099, 0xd2a1, 0xd4ad, 0xd6bb, 0xd8cc, 0xdae0, 0xdcf7, 36 | 0xdf11, 0xe12e, 0xe34e, 0xe571, 0xe797, 0xe9c0, 0xebec, 0xee1b, 37 | 0xf04d, 0xf282, 0xf4ba, 0xf6f5, 0xf933, 0xfb74, 0xfdb8, 0xffff, 38 | } 39 | var lin2rgb = [...]uint16{ 40 | 0x0000, 0x0cfc, 0x15f9, 0x1c6b, 0x21ce, 0x2671, 0x2a93, 0x2e53, 41 | 0x31c6, 0x34fb, 0x37fd, 0x3ad3, 0x3d83, 0x4013, 0x4286, 0x44e0, 42 | 0x4722, 0x4950, 0x4b6b, 0x4d75, 0x4f6f, 0x515b, 0x5339, 0x550a, 43 | 0x56d0, 0x588b, 0x5a3c, 0x5be3, 0x5d82, 0x5f17, 0x60a5, 0x622b, 44 | 0x63a9, 0x6521, 0x6692, 0x67fd, 0x6962, 0x6ac1, 0x6c1a, 0x6d6f, 45 | 0x6ebe, 0x7008, 0x714e, 0x7290, 0x73cc, 0x7505, 0x763a, 0x776b, 46 | 0x7898, 0x79c1, 0x7ae7, 0x7c0a, 0x7d29, 0x7e45, 0x7f5e, 0x8074, 47 | 0x8187, 0x8297, 0x83a4, 0x84af, 0x85b7, 0x86bd, 0x87c0, 0x88c0, 48 | 0x89be, 0x8aba, 0x8bb4, 0x8cab, 0x8da1, 0x8e94, 0x8f85, 0x9074, 49 | 0x9161, 0x924d, 0x9336, 0x941e, 0x9503, 0x95e7, 0x96ca, 0x97aa, 50 | 0x9889, 0x9967, 0x9a42, 0x9b1d, 0x9bf5, 0x9ccc, 0x9da2, 0x9e76, 51 | 0x9f49, 0xa01b, 0xa0eb, 0xa1b9, 0xa287, 0xa353, 0xa41e, 0xa4e7, 52 | 0xa5b0, 0xa677, 0xa73d, 0xa802, 0xa8c5, 0xa988, 0xaa49, 0xab09, 53 | 0xabc8, 0xac87, 0xad44, 0xae00, 0xaebb, 0xaf75, 0xb02d, 0xb0e5, 54 | 0xb19d, 0xb253, 0xb308, 0xb3bc, 0xb46f, 0xb522, 0xb5d3, 0xb684, 55 | 0xb734, 0xb7e3, 0xb891, 0xb93e, 0xb9ea, 0xba96, 0xbb41, 0xbbeb, 56 | 0xbc94, 0xbd3d, 0xbde4, 0xbe8b, 0xbf32, 0xbfd7, 0xc07c, 0xc120, 57 | 0xc1c3, 0xc266, 0xc308, 0xc3a9, 0xc44a, 0xc4ea, 0xc589, 0xc628, 58 | 0xc6c6, 0xc763, 0xc800, 0xc89c, 0xc937, 0xc9d2, 0xca6d, 0xcb06, 59 | 0xcb9f, 0xcc38, 0xccd0, 0xcd67, 0xcdfe, 0xce94, 0xcf2a, 0xcfbf, 60 | 0xd053, 0xd0e7, 0xd17b, 0xd20e, 0xd2a0, 0xd332, 0xd3c3, 0xd454, 61 | 0xd4e5, 0xd574, 0xd604, 0xd693, 0xd721, 0xd7af, 0xd83c, 0xd8c9, 62 | 0xd956, 0xd9e2, 0xda6d, 0xdaf8, 0xdb83, 0xdc0d, 0xdc97, 0xdd20, 63 | 0xdda9, 0xde32, 0xdeba, 0xdf41, 0xdfc8, 0xe04f, 0xe0d6, 0xe15b, 64 | 0xe1e1, 0xe266, 0xe2eb, 0xe36f, 0xe3f3, 0xe477, 0xe4fa, 0xe57c, 65 | 0xe5ff, 0xe681, 0xe702, 0xe784, 0xe804, 0xe885, 0xe905, 0xe985, 66 | 0xea04, 0xea83, 0xeb02, 0xeb80, 0xebfe, 0xec7c, 0xecf9, 0xed76, 67 | 0xedf3, 0xee6f, 0xeeeb, 0xef67, 0xefe2, 0xf05d, 0xf0d8, 0xf152, 68 | 0xf1cc, 0xf246, 0xf2bf, 0xf338, 0xf3b1, 0xf429, 0xf4a1, 0xf519, 69 | 0xf591, 0xf608, 0xf67f, 0xf6f6, 0xf76c, 0xf7e2, 0xf858, 0xf8cd, 70 | 0xf942, 0xf9b7, 0xfa2c, 0xfaa0, 0xfb14, 0xfb88, 0xfbfc, 0xfc6f, 71 | 0xfce2, 0xfd54, 0xfdc7, 0xfe39, 0xfeab, 0xff1d, 0xff8e, 0xffff, 72 | } 73 | 74 | func RGBToLinear(rgb uint8) uint16 { 75 | return rgb2lin[rgb] 76 | } 77 | func LinearToRGB(lin uint16) uint8 { 78 | mul := uint64(lin) * 0xff0100 79 | div := uint32(mul >> 32) 80 | l := uint32(lin2rgb[uint8(div)]) 81 | 82 | return uint8((uint64(257*l+uint32(uint64(uint32(mul))*257>>32)* 83 | (uint32(lin2rgb[uint8(div+1)])-l)+0x8100) * 0x1fc05f9) >> 41) 84 | } 85 | -------------------------------------------------------------------------------- /locations.json: -------------------------------------------------------------------------------- 1 | { 2 | "Locations": [ 3 | { 4 | "XCenter": -0.7463, 5 | "YCenter": 0.1102, 6 | "Zoom": 200 7 | }, 8 | { 9 | "XCenter": -0.7453, 10 | "YCenter": 0.1127, 11 | "Zoom": 1538.4615384615386 12 | }, 13 | { 14 | "XCenter": -0.74529, 15 | "YCenter": 0.113075, 16 | "Zoom": 6666.666666666667 17 | }, 18 | { 19 | "XCenter": -0.745428, 20 | "YCenter": 0.113009, 21 | "Zoom": 33333.333333333336 22 | }, 23 | { 24 | "XCenter": -0.16, 25 | "YCenter": 1.0405, 26 | "Zoom": 38.46153846153846 27 | }, 28 | { 29 | "XCenter": -0.925, 30 | "YCenter": 0.266, 31 | "Zoom": 31.25 32 | }, 33 | { 34 | "XCenter": -1.25066, 35 | "YCenter": 0.02012, 36 | "Zoom": 5882.35294117647 37 | }, 38 | { 39 | "XCenter": -0.748, 40 | "YCenter": 0.1, 41 | "Zoom": 714.2857142857143 42 | }, 43 | { 44 | "XCenter": -0.235125, 45 | "YCenter": 0.827215, 46 | "Zoom": 24999.999999999996 47 | }, 48 | { 49 | "XCenter": -0.722, 50 | "YCenter": 0.246, 51 | "Zoom": 52.631578947368425 52 | }, 53 | { 54 | "XCenter": -1.315180982097868, 55 | "YCenter": 0.073481649996795, 56 | "Zoom": 10000000000000 57 | }, 58 | { 59 | "XCenter": -0.156653458, 60 | "YCenter": 1.039128122, 61 | "Zoom": 499999999.99999994 62 | }, 63 | { 64 | "XCenter": -0.1568046, 65 | "YCenter": 1.0390207, 66 | "Zoom": 1000000000000 67 | }, 68 | { 69 | "XCenter": -0.16070135, 70 | "YCenter": 1.0375665, 71 | "Zoom": 10000000 72 | }, 73 | { 74 | "XCenter": 0.2549870375144766, 75 | "YCenter": -0.0005679790528465, 76 | "Zoom": 10000000000000 77 | }, 78 | { 79 | "XCenter": 0.267235642726, 80 | "YCenter": -0.003347589624, 81 | "Zoom": 8695652173.913044 82 | }, 83 | { 84 | "XCenter": -0.0452407411, 85 | "YCenter": 0.986816213, 86 | "Zoom": 5714285.714285715 87 | }, 88 | { 89 | "XCenter": -0.0452407411, 90 | "YCenter": 0.9868162204352258, 91 | "Zoom": 227272727.27272728 92 | }, 93 | { 94 | "XCenter": -0.0452407411, 95 | "YCenter": 0.9868162204352258, 96 | "Zoom": 1470588235.2941177 97 | }, 98 | { 99 | "XCenter": -0.0452407411, 100 | "YCenter": 0.9868162204352258, 101 | "Zoom": 3703703703.703704 102 | }, 103 | { 104 | "XCenter": -0.04524074130409, 105 | "YCenter": 0.9868162207157838, 106 | "Zoom": 434782608695.6522 107 | }, 108 | { 109 | "XCenter": -0.04524074130409, 110 | "YCenter": 0.9868162207157852, 111 | "Zoom": 14705882352941.176 112 | }, 113 | { 114 | "XCenter": 0.281717921930775, 115 | "YCenter": 0.5771052841488505, 116 | "Zoom": 194174757281.5534 117 | }, 118 | { 119 | "XCenter": 0.281717921930775, 120 | "YCenter": 0.5771052841488505, 121 | "Zoom": 800000000000 122 | }, 123 | { 124 | "XCenter": 0.281717921930775, 125 | "YCenter": 0.5771052841488505, 126 | "Zoom": 2105263157894.7368 127 | }, 128 | { 129 | "XCenter": 0.281717921930775, 130 | "YCenter": 0.5771052841488505, 131 | "Zoom": 5617977528089.888 132 | }, 133 | { 134 | "XCenter": 0.281717921930775, 135 | "YCenter": 0.5771052841488505, 136 | "Zoom": 11764705882352.941 137 | }, 138 | { 139 | "XCenter": 0.281717921930775, 140 | "YCenter": 0.5771052841488505, 141 | "Zoom": 25000000000000 142 | }, 143 | { 144 | "XCenter": 0.281717921930775, 145 | "YCenter": 0.5771052841488505, 146 | "Zoom": 52083333333333.336 147 | }, 148 | { 149 | "XCenter": -0.840719, 150 | "YCenter": 0.22442, 151 | "Zoom": 12658.227848101267 152 | }, 153 | { 154 | "XCenter": -0.81153120295763, 155 | "YCenter": 0.20142958206181, 156 | "Zoom": 3333.3333333333335 157 | }, 158 | { 159 | "XCenter": -0.81153120295763, 160 | "YCenter": 0.20142958206181, 161 | "Zoom": 169491.5254237288 162 | }, 163 | { 164 | "XCenter": -0.81153120295763, 165 | "YCenter": 0.20142958206181, 166 | "Zoom": 21739130.43478261 167 | }, 168 | { 169 | "XCenter": -0.81153120295763, 170 | "YCenter": 0.20142958206181, 171 | "Zoom": 662251655.6291391 172 | }, 173 | { 174 | "XCenter": -0.81153120295763, 175 | "YCenter": 0.20142958206181, 176 | "Zoom": 8928571428571.428 177 | }, 178 | { 179 | "XCenter": -0.8115312340458353, 180 | "YCenter": 0.2014296112433656, 181 | "Zoom": 29411764705882.35 182 | }, 183 | { 184 | "XCenter": 0.452721018749286, 185 | "YCenter": 0.39649427698014, 186 | "Zoom": 9090909090909.092 187 | }, 188 | { 189 | "XCenter": 0.45272105023, 190 | "YCenter": 0.396494224267, 191 | "Zoom": 370370370.3703703 192 | }, 193 | { 194 | "XCenter": 0.45272105023, 195 | "YCenter": 0.396494224267, 196 | "Zoom": 2564102564.1025643 197 | }, 198 | { 199 | "XCenter": 0.45272105023, 200 | "YCenter": 0.396494224267, 201 | "Zoom": 7142857142.857142 202 | }, 203 | { 204 | "XCenter": -1.1533577030005, 205 | "YCenter": 0.307486987838885, 206 | "Zoom": 1886792452.8301885 207 | }, 208 | { 209 | "XCenter": -1.1533577030005, 210 | "YCenter": 0.307486987838885, 211 | "Zoom": 10526315789473.684 212 | }, 213 | { 214 | "XCenter": -1.15412664822215, 215 | "YCenter": 0.30877492767139, 216 | "Zoom": 322580645.1612903 217 | }, 218 | { 219 | "XCenter": -1.15412664822215, 220 | "YCenter": 0.30877492767139, 221 | "Zoom": 16129032258.064514 222 | }, 223 | { 224 | "XCenter": -1.15412664822215, 225 | "YCenter": 0.30877492767139, 226 | "Zoom": 105263157894.73685 227 | }, 228 | { 229 | "XCenter": -1.15412664822215, 230 | "YCenter": 0.30877492767139, 231 | "Zoom": 270270270270.27026 232 | }, 233 | { 234 | "XCenter": -1.7590170270659, 235 | "YCenter": 0.01916067191295, 236 | "Zoom": 909090909090.9092 237 | }, 238 | { 239 | "XCenter": -1.99999911758738, 240 | "YCenter": 0, 241 | "Zoom": 675675675675.6757 242 | }, 243 | { 244 | "XCenter": -1.99999911758738, 245 | "YCenter": 0, 246 | "Zoom": 1694915254237.288 247 | }, 248 | { 249 | "XCenter": -1.99999911758738, 250 | "YCenter": 0, 251 | "Zoom": 4000000000000 252 | }, 253 | { 254 | "XCenter": 0.432539867562512, 255 | "YCenter": 0.226118675951765, 256 | "Zoom": 312500 257 | }, 258 | { 259 | "XCenter": 0.432539867562512, 260 | "YCenter": 0.226118675951765, 261 | "Zoom": 3125000000000 262 | }, 263 | { 264 | "XCenter": 0.432539867562512, 265 | "YCenter": 0.226118675951765, 266 | "Zoom": 13698630136986.3 267 | }, 268 | { 269 | "XCenter": 0.432539867562512, 270 | "YCenter": 0.226118675951818, 271 | "Zoom": 54945054945054.945 272 | }, 273 | { 274 | "XCenter": 0.3369844464869, 275 | "YCenter": 0.048778219666, 276 | "Zoom": 55555555555.55556 277 | }, 278 | { 279 | "XCenter": 0.3369844464873, 280 | "YCenter": 0.0487782196791, 281 | "Zoom": 238095238095.2381 282 | }, 283 | { 284 | "XCenter": 0.33698444648918, 285 | "YCenter": 0.048778219681, 286 | "Zoom": 4761904761904.762 287 | }, 288 | { 289 | "XCenter": 0.2929859127507, 290 | "YCenter": 0.6117848324958, 291 | "Zoom": 1492537.3134328357 292 | }, 293 | { 294 | "XCenter": 0.2929859127507, 295 | "YCenter": 0.6117848324958, 296 | "Zoom": 1162790697.6744184 297 | }, 298 | { 299 | "XCenter": 0.2929859127507, 300 | "YCenter": 0.6117848324958, 301 | "Zoom": 22727272727.272724 302 | }, 303 | { 304 | "XCenter": 0.2929859127507, 305 | "YCenter": 0.6117848324958, 306 | "Zoom": 100000000000 307 | }, 308 | { 309 | "XCenter": -0.936532336, 310 | "YCenter": 0.2633616, 311 | "Zoom": 5714285.714285715 312 | }, 313 | { 314 | "XCenter": -0.7336438924199521, 315 | "YCenter": 0.2455211406714035, 316 | "Zoom": 2325581395.348837 317 | }, 318 | { 319 | "XCenter": -0.7336438924199521, 320 | "YCenter": 0.2455211406714035, 321 | "Zoom": 22222222222222.223 322 | } 323 | ] 324 | } -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "image" 7 | "image/color" 8 | "image/png" 9 | "log" 10 | "os" 11 | "runtime" 12 | "strconv" 13 | "time" 14 | ) 15 | 16 | var imgConf ImageConfig 17 | 18 | func main() { 19 | parseImageConfigArgs() 20 | generateImagesFromLocations(getLocations()) 21 | } 22 | 23 | func parseImageConfigArgs() { 24 | imgWidthPtr := flag.Int("width", 1920, "The width of the image in pixels.") 25 | imgHeightPtr := flag.Int("height", 1024, "The height of the image in pixels.") 26 | samplesPtr := flag.Int("samples", 5, "The number of samples") 27 | maxIterPtr := flag.Int("iter", 500, "The max. number of iterations.") 28 | OffsetPtr := flag.Float64("offset", 0.0, "The HSL offset in the range [0, 1)") 29 | mixingPtr := flag.Bool("mixing", true, "Use linear color mixing.") 30 | insideBlackPtr := flag.Bool("black", true, "Paint area inside in black.") 31 | grayscalePtr := flag.Bool("grayscale", false, "Paint image in grayscale.") 32 | 33 | flag.Parse() 34 | 35 | imgConf = ImageConfig{ 36 | Width: *imgWidthPtr, 37 | Height: *imgHeightPtr, 38 | Samples: *samplesPtr, 39 | MaxIter: *maxIterPtr, 40 | Offset: *OffsetPtr, 41 | Mixing: *mixingPtr, 42 | InsideBlack: *insideBlackPtr, 43 | Grayscale: *grayscalePtr, 44 | RndGlobal: uint64(time.Now().UnixNano()), 45 | } 46 | } 47 | 48 | func generateImagesFromLocations(locs LocationsFile) { 49 | dirPath := fmt.Sprintf("results/%d", imgConf.MaxIter) 50 | if _, err := os.Stat(dirPath); os.IsNotExist(err) { 51 | if err := os.MkdirAll(dirPath, 0755); err != nil { 52 | panic(err) 53 | } 54 | } 55 | 56 | for index, loc := range locs.Locations { 57 | log.Println("Allocating and rendering image", index+1) 58 | img := image.NewRGBA(image.Rect(0, 0, imgConf.Width, imgConf.Height)) 59 | renderImage(img, loc) 60 | 61 | log.Println("Encoding image", index+1) 62 | filename := "results/" + strconv.Itoa(imgConf.MaxIter) + "/" + strconv.Itoa(index+1) 63 | f, err := os.Create(filename + ".png") 64 | if err != nil { 65 | panic(err) 66 | } 67 | err = png.Encode(f, img) 68 | if err != nil { 69 | panic(err) 70 | } 71 | } 72 | } 73 | 74 | func renderImage(img *image.RGBA, loc Location) { 75 | jobs := make(chan int) 76 | 77 | for i := 0; i < runtime.NumCPU(); i++ { 78 | rndLocal := RandUint64(&imgConf.RndGlobal) 79 | go func() { 80 | for y := range jobs { 81 | renderRow(loc, y, img, &rndLocal) 82 | } 83 | }() 84 | } 85 | 86 | for y := 0; y < imgConf.Height; y++ { 87 | jobs <- y 88 | fmt.Printf("\r%d/%d (%d%%)", y, imgConf.Height, int(100*(float64(y)/float64(imgConf.Height)))) 89 | } 90 | fmt.Printf("\r%d/%[1]d (100%%)\n", imgConf.Height) 91 | } 92 | 93 | func renderRow(loc Location, y int, img *image.RGBA, rndLocal *uint64) { 94 | for x := 0; x < imgConf.Width; x++ { 95 | cr, cg, cb := getColorForPixel(loc, x, y, rndLocal) 96 | img.SetRGBA(x, y, color.RGBA{R: cr, G: cg, B: cb, A: 255}) 97 | } 98 | } 99 | 100 | func getColorForPixel(loc Location, x int, y int, rndLocal *uint64) (uint8, uint8, uint8) { 101 | var r, g, b int 102 | for i := 0; i < imgConf.Samples; i++ { 103 | c := getColorForComplexNr(convertPixelToComplexNr(loc, x, y, rndLocal)) 104 | 105 | if imgConf.Mixing { 106 | r += int(RGBToLinear(c.R)) 107 | g += int(RGBToLinear(c.G)) 108 | b += int(RGBToLinear(c.B)) 109 | } else { 110 | r += int(c.R) 111 | g += int(c.G) 112 | b += int(c.B) 113 | } 114 | } 115 | 116 | var cr, cg, cb uint8 117 | if imgConf.Mixing { 118 | cr = LinearToRGB(uint16(float64(r) / float64(imgConf.Samples))) 119 | cg = LinearToRGB(uint16(float64(g) / float64(imgConf.Samples))) 120 | cb = LinearToRGB(uint16(float64(b) / float64(imgConf.Samples))) 121 | } else { 122 | cr = uint8(float64(r) / float64(imgConf.Samples)) 123 | cg = uint8(float64(g) / float64(imgConf.Samples)) 124 | cb = uint8(float64(b) / float64(imgConf.Samples)) 125 | } 126 | return cr, cg, cb 127 | } 128 | 129 | func convertPixelToComplexNr(loc Location, x int, y int, rndLocal *uint64) complex128 { 130 | ratio := float64(imgConf.Width) / float64(imgConf.Height) 131 | 132 | // RandFloat64() is added for anti-aliasing 133 | nx := (1/loc.Zoom)*ratio*((float64(x)+RandFloat64(rndLocal))/float64(imgConf.Width)-0.5) + loc.XCenter 134 | ny := (1/loc.Zoom)*((float64(y)+RandFloat64(rndLocal))/float64(imgConf.Height)-0.5) - loc.YCenter 135 | return complex(nx, ny) 136 | } 137 | -------------------------------------------------------------------------------- /mandelbrot.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "image/color" 5 | "math" 6 | ) 7 | 8 | func getColorForComplexNr(c complex128) color.RGBA { 9 | return getColorFromMandelbrot(runMandelbrot(c)) 10 | } 11 | 12 | func getColorFromMandelbrot(isUnlimited bool, magnitude float64, iterations int) color.RGBA { 13 | if isUnlimited { 14 | // adapted http://linas.org/art-gallery/escape/escape.html 15 | smooth := (float64(iterations) + 1 - math.Log(math.Log(magnitude))/math.Log(2)) / float64(imgConf.MaxIter) 16 | offset := smooth + imgConf.Offset 17 | mod := math.Mod(offset, 1) 18 | if imgConf.Grayscale { 19 | return hslToRGB(0, 0, mod) 20 | } 21 | return hslToRGB(mod, 1, 0.5) 22 | } else if imgConf.InsideBlack { 23 | return color.RGBA{R: 0, G: 0, B: 0, A: 255} 24 | } else { 25 | return color.RGBA{R: 255, G: 255, B: 255, A: 255} 26 | } 27 | } 28 | 29 | func runMandelbrot(c complex128) (bool, float64, int) { 30 | var z complex128 31 | 32 | for i := 1; i < imgConf.MaxIter; i++ { 33 | z = z*z + c 34 | magnitudeSquared := real(z)*real(z) + imag(z)*imag(z) 35 | if magnitudeSquared > 4 { 36 | return true, math.Sqrt(magnitudeSquared), i 37 | } 38 | } 39 | return false, 0, 0 40 | } 41 | -------------------------------------------------------------------------------- /rand.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func RandUint64(rng *uint64) uint64 { 4 | *rng = *rng*0x3243f6a8885a308d + 1 5 | r := *rng 6 | r ^= r >> 32 7 | r *= 1111111111111111111 8 | r ^= r >> 32 9 | return r 10 | } 11 | 12 | func RandFloat64(rng *uint64) float64 { 13 | return float64(RandUint64(rng)/2) / (1 << 63) 14 | } 15 | -------------------------------------------------------------------------------- /scraper.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "os" 7 | ) 8 | 9 | func getLocations() LocationsFile { 10 | log.Println("Reading location data...") 11 | file, err := os.ReadFile("locations.json") 12 | if err != nil { 13 | panic(err) 14 | } 15 | 16 | locs := LocationsFile{} 17 | _ = json.Unmarshal( 18 | file, 19 | &locs, 20 | ) 21 | 22 | zoom1Fractal := Location{ 23 | XCenter: -0.75, 24 | YCenter: 0, 25 | Zoom: 1, 26 | } 27 | locs.Locations = append(locs.Locations, zoom1Fractal) 28 | 29 | log.Printf("Found %v locations.", len(locs.Locations)) 30 | return locs 31 | } 32 | --------------------------------------------------------------------------------