├── .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 |
21 |
22 | |
23 |
24 |
25 | |
26 |
27 |
28 |
29 |
30 | |
31 |
32 |
33 | |
34 |
35 |
36 |
37 | #### Grayscale
38 |
39 |
40 |
41 |
42 | |
43 |
44 |
45 | |
46 |
47 |
48 |
49 |
50 | |
51 |
52 |
53 | |
54 |
55 |
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 |
--------------------------------------------------------------------------------