Fraktálové obrázky v Go

Saturday, Oct 31, 2020 by Nelo
Generovane fraktaly - praca s komplexnymi cislami v Go a generovanie obrazkov v realnom case. Jednoduchy HTTP server s interaktivnym javascriptom.

Fraktály sú dobre známe sebe-podobné množiny, ktoré vyzerajú podobne v rôznych úrovniach zväčšenia. Najznámejší fraktál nesie meno po svojom objaviteľovi, polsko-francúzsko-americkom matematikovi - Benoitovi Mandelbrotovi. V roku 1980, keď ako jeden z prvých použil počítačovú grafiku na zobrazovanie fraktálnej geometrie pomocou IBM strojov.

V Go vďaka natívnej podpore komplexných čísel a knižníc na prácu s obrázkami vieme jednoducho vytvoriť web server, ktorý interaktívne generuje fraktálové obrazy do png. Vytvoríme si Go server, HTML a jednoduchý JavaScript, pomocou ktorého budeme hľadať zaujímavé štruktúry:

  • klikom myši vycentrovať
  • + zväčšiť
  • - zmenšiť
  • 1-4 zmeniť vzorec (mandelbrot, julia, newton, mix)
  • 5-9 zmeniť farebnú paletu
  • q resetovať nastavenia

Galéria

Všetky obrázky v galérii boli generované zdrojovým kódom nižšie a neboli použité žiadne ďalšie efekty:

Komplexné funkcie

Fraktály sú najčastejšie dynamické systémy generovné iteratívne komplexnými funkciami. Pri mandelbrotovej alebo julia množine zisťujeme, či a po akom počte iterácií unikne absolútna hodnota za nejakú hranicu. Vytvoríme si štyri funkcie s komplexným číslom ako argumentom (vstupný pixel), vracajúcu výsledné komplexné číslo po iteráciach a počet iterácií. Súbor si uložme ako “fractals.go”:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main

import (
	"math/cmplx"
)

func mandelbrot(z complex128) (complex128, uint8) {
	const iterations = 255
	var v complex128
	for i := uint8(0); i < iterations; i++ {
		v = v*v + z
		if cmplx.Abs(v) > 2 {
			return z, i
		}
	}
	return 0, 0
}

func julia(z complex128) (complex128, uint8) {
	const iterations = 255
	const c = 0.285 + 0.01i
	const R = 10
	for i := uint8(0); i < iterations; i++ {
		z = z*z + c
		if cmplx.Abs(z) > R {
			return z, i
		}
	}
	return 0, 0
}

func mix(z complex128) (complex128, uint8) {
	return cmplx.Tan(z*z) + cmplx.Sqrt(cmplx.Acos(z)) + (0.5-0.001i)*cmplx.Acos(z), 0
}

func newton(z complex128) (complex128, uint8) {
	const iterations = 255
	for i := uint8(0); i < iterations; i++ {
		z -= (z - 1/(z*z*z)) / 4
		if cmplx.Abs(z*z*z*z-1) < 1e-9 {
			return z, i
		}
	}
	return 0, 0
}

Web server s image handlerom

Tento web server má dva handlere:

  • /image.png vygeneruje nový obrázok, imgHandler najskôr načíta HTTP request parametre a potom vyrenderuje každý riadok obrázka v osobitnej gorutine
  • / fileserver pre všetko ostatné

Všimnime si, že komplexné funkcie aj funkcie na generovanie farieb sú vďaka Go podpore funkcií ako hodnôt (first-class-citizens) uložené do slicu. Kvôli volaniu v goroutine je renderLine funkcia je vytvorená ako lambda funkcia s closure - uzavretým scope, ktorý obsahuje “c” a “f” funkcie, “img” a “wg” WaitGroup.

Funkcia getFloat nie je veľmi zaujímavá, len extrahuje požadovaný HTTP request parameter a snaží sa skonvertovať ho na float64 hodnotu, ak sa to nepodarí vracia default hodnotu.

Súbor si uložme ako “main.go”:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
package main

import (
	"image"
	"image/color"
	"image/png"
	"log"
	"net/http"
	"strconv"
	"sync"
)

var (
	fractals = []func(complex128) (complex128, uint8){mandelbrot, julia, newton, mix} // fractal functions
	colors   = []func(z complex128, n uint8) color.Color{                             // color functions
		func(z complex128, n uint8) color.Color {
			return color.YCbCr{uint8(255 - 15*n), uint8(real(z) * 255), uint8(imag(z) * 255)}
		},
		func(z complex128, n uint8) color.Color {
			return color.YCbCr{uint8(255 - 15*n), uint8(real(z) * real(z) * 255), uint8(imag(z) * imag(z) * 255)}
		},
		func(z complex128, n uint8) color.Color {
			return color.YCbCr{192, uint8(real(z)*128) + 127, uint8(imag(z)*128) + 127}
		},
		func(z complex128, n uint8) color.Color {
			return color.YCbCr{uint8(real(z)*128) + 127, uint8(imag(z)*128) + 127, 255 - 7*n}
		},
		func(z complex128, n uint8) color.Color {
			return color.Gray{uint8(real(z)/imag(z)) - 15*n}
		},
	}
)

func main() {
	const (
		port = ":4200"
		dir  = "./"
	)
	log.Printf("listening on %q...", port)
	http.HandleFunc("/image.png", imgHandler)        // generate image.png
	http.Handle("/", http.FileServer(http.Dir(dir))) // serve everything else
	log.Fatal(http.ListenAndServe(port, nil))
}

// parse GET parameter and convert into float64 or return default
func getFloat(r *http.Request, name string, def float64) float64 {
	keys, ok := r.URL.Query()[name]
	if !ok || len(keys[0]) < 1 {
		return def
	}
	out, err := strconv.ParseFloat(keys[0], 64)
	if err != nil {
		return def
	}
	return out
}

// return image.png
func imgHandler(w http.ResponseWriter, r *http.Request) {
	cx, cy, zm := getFloat(r, "x", 0), getFloat(r, "y", 0), getFloat(r, "z", 2)
	xmin, ymin, xmax, ymax := cx-zm, cy-zm, cx+zm, cy+zm
	width, height := 1024, 1024

	f := fractals[int(getFloat(r, "t", 0))]               // complex function
	c := colors[int(getFloat(r, "c", 0))]                 // color function
	img := image.NewRGBA(image.Rect(0, 0, width, height)) // image

	// render one row in gorutine
	var wg sync.WaitGroup
	renderLine := func(py int) {
		y := float64(py)/float64(height)*(ymax-ymin) + ymin
		for px := 0; px < width; px++ {
			x := float64(px)/float64(width)*(xmax-xmin) + xmin
			z := complex(x, y)
			var pc color.Color = color.RGBA{0, 0, 0, 255}
			if v, i := f(z); v != 0 || i != 0 {
				pc = c(v, i)
			}
			img.Set(px, py, pc) // set pixel color
		}
		wg.Done()
	}

	// render every row
    wg.Add(height)
    for py := 0; py < height; py++ {
		go renderLine(py)
	}
	wg.Wait()

	// write HTTP output
	w.Header().Set("Content-Type", "image/png")
	png.Encode(w, img)
}

Html a JavaScript časť

Samotný index.html obsahuje pomocné CSS, jquery z CDN-ka, <div> do ktorého vypíšeme status a <img> do ktorého budeme zapisovať vygenerovaný obrázok.

Funkcia update() prepíše src atribút obrázku, čím vyvolá jeho načítanie a aktualizuje status. Zavolaný image.png endpoint s požadovanými parametrami vygeneruje nový obrázok.

Keď sa celý dokument načíta $(document).ready(), tak sa aktualizuje obrázok a nastavia sa funkcie na klik myškou na obrázku a klávesové skratky.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Fractal</title>
<style>
    html, body{
        font-family: monospace;
        height: 100%;
        width: 100%;
        margin: 0;
        text-align: center;
        color: white;
        background-color: #333;
    }
    img{
        margin: 0px auto;
        display: block;
        cursor: pointer;
        min-height: 400px;
    }
</style>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script type="text/javascript">
cx = 0 // center x
cy = 0 // center y
zm = 2 // zoom
w = 1024 // image width
h = 1024 // image height
tp = 0 // fractal type
cr = 0 // color palette
function update() {
    $("#canvas").attr("src", "image.png?x=" + cx + "&y=" + cy + "&z=" + zm + "&t=" + tp + "&c=" + cr)
    $("#output").html("x: " + cx + "<br> y: " + cy + "<br> z: " + zm + "<br> t: " + tp + "<br> c: " + cr)
}
$(document).ready(function() {
    update()

    $("img").on("click", function(event) {
        var x = event.pageX - this.offsetLeft
        var y = event.pageY - this.offsetTop
        cx += 2.0 * zm * (x / w - 0.5)
        cy += 2.0 * zm * (y / w - 0.5)
        update()
    })

    $(document).keypress(function(event) {
        dirty = false
        switch(event.which) {
            case 43: // +
                zm /= 1.5
                dirty = true
                break
            case 45: // -
                zm *= 1.5
                dirty = true
                break
            case 113: // q
                cx = 0
                cy = 0
                zm = 2
                dirty = true
                break
            case 49: // 1
            case 50: // 2
            case 51: // 3
            case 52: // 4
                tp = event.which - 49
                dirty = true
                break;
            case 53: // 5
            case 54: // 6
            case 55: // 7
            case 56: // 8
            case 57: // 9
                cr = event.which - 53
                dirty = true
                break
            default:
                console.log( event.which )
                break
        }
        if(dirty) {
            update()
        }
    })
})
</script>
</head>
<body>
    <div id="output" style="float: left; text-align: left;width:200px"></div>
    <img id="canvas" title="click = center, +/- zoom, 1-4 = type, 5-8 = color, q = reset"/>
</body>
</html>

Záver

Finálny program skompilujeme a spustíme go run . a v prehliadači si otvoríme lokálnu stránku http://localhost:4200/.

Screenshot z fraktálu

V tomto dieli sme si ukázali, že Go nie je výborné len na sieťové a systémové CLI aplikácie, ale taktiež na výpočty s komplexnými číslami a interaktívne generovanie obrázkov do webového prehliadača. Na kráse fraktálnej geometrie ma neprestáva fascinovať ako tie najjednoduchšie vzorce dávajú vznik nekonečne komplexným štruktúram.

Referencie

  1. Mandelbrot set
  2. Julia set
  3. Newton fractal
  4. Package image
  5. Go Closures
  6. Complex Numbers in GoLang

Vaše otázky, návrhy a komentáre

Verím, že vás tento návod inšpiroval a budem vďačný ak dáte spätnú väzbu a pomôže mi zamerať sa na to čo by vás zaujímalo.

TAK ČO HOVORÍŠ ?

Kontaktuj nás ak potrebuješ pomoc, veľmi radi pomôžeme.

Kontaktuj nás
xxx