Skip to content

Commit

Permalink
Mandelbrot updates (fullscreen)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexristinmaa committed Nov 12, 2024
1 parent a3bb528 commit ab6f194
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 26 deletions.
126 changes: 125 additions & 1 deletion alexanderristinmaa/app/(experiments)/mandelbrot/page.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,128 @@ tags:
- WebAssembly
- Fractals
---
# Mandelbrot with Webassembly
# Mandelbrot with Webassembly

This is my first experience using webassembly! It turned out to be very interesting.

![mandelbrot](/mandelbrot/mandelbrot.png "Mandelbrot")

## Go "backend"
The idea was to create a go-mandelbrot kind of
backend. It looks something like this:

```go
func mandelbrot(xStart, xEnd, yStart, yEnd float64, iterations int) {
deltaX := complex((xEnd-xStart)/float64(WIDTH), 0)
deltaY := complex(0, (yEnd-yStart)/float64(HEIGHT))

coord := complex(xStart, yStart)

scaleIterations := 255.0 / float64(iterations)

for y := 0; y < HEIGHT; y++ {
coord = complex(xStart, imag(coord))

for x := 0; x < WIDTH*4; x += 4 {
clr := testPoint(coord, iterations, scaleIterations)
index := y*WIDTH*4 + x

pixels[index] = clr
pixels[index+1] = clr
pixels[index+2] = clr
pixels[index+3] = 255

coord += deltaX
}

coord += deltaY
}
}

func testPoint(c complex128, iterations int, scale float64) byte {
z := c
var zold complex128
i := 0
period := 0

for ; i < iterations; i++ {
if real(z)*real(z)+imag(z)*imag(z) > 4 {
break
}

z = z*z + c

if z == zold {
return byte(float64(iterations) * scale)
}

period++
if period == 20 {
zold = z
period = 0
}
}

return byte(float64(i) * scale)
}
```

The keen eye will have noticed the use of the `pixels` varaible which is not seen anywhere in the code. This is because the `pixels` variable is actually a buffer which is accessible from the javascript iterface. It is initalized by calling this function:

```go
func allocateBuffer(w, h int) *byte {
WIDTH = w
HEIGHT = h
pixels = make([]byte, w*h*4)

return &pixels[0]
}
```

The `mandelbrot` and `allocateBuffer` functions are exposed to the javascript client.

## Compiling and connecting
The code can then be compiled like this: (more info at [Go Wiki Webassembly](https://go.dev/wiki/WebAssembly))

```bash
GOOS=js GOARCH=wasm go build -o main.wasm
```

Using the `wasm_exec.js` file provided by golang (see wiki link) the exposed go functions are loaded and called in the javascript like this:

```typescript
await import("./mandelbrot/wasm_exec") // Node.js import

const go = new global.Go(); // Defined in wasm_exec.js
const WASM_URL = '/mandelbrot/wasm.wasm';

wasm = (await WebAssembly.instantiateStreaming(fetch(WASM_URL), go.importObject)).instance;
go.run(wasm);

({allocateBuffer, mandelbrot} = wasm.exports);
({width: WIDTH, height: HEIGHT} = document.getElementById("mandelbrotCanvas")!.getBoundingClientRect());
bufferPointer = allocateBuffer(WIDTH, HEIGHT);

renderMandelbrot();
```

where this is the `renderMandelbrot` function which calls the `mandelbrot` function to populate a buffer with new pixel-values and then renders them on the canvas.

```typescript
function renderMandelbrot() {
mandelbrot(xStart,xEnd,yStart,yEnd, iterations); // go function

// Get the buffer (pixels in the go function) as a Uint8Array
const bufferArray =
new Uint8ClampedArray(
(wasm.exports.memory as WebAssembly.Memory)
.buffer
.slice(bufferPointer, bufferPointer+WIDTH*HEIGHT*4)
);

const ctx = (document.getElementById("mandelbrotCanvas")! as HTMLCanvasElement).getContext("2d");
const imageData = new ImageData(bufferArray, WIDTH, HEIGHT, {colorSpace: "srgb"});

ctx?.putImageData(imageData,0,0);
}
```
71 changes: 49 additions & 22 deletions alexanderristinmaa/app/(experiments)/mandelbrot/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,41 +10,58 @@ let mandelbrot: any, allocateBuffer: any;
let bufferPointer: number;
let WIDTH : number, HEIGHT : number;

let xStart = -2;
let xEnd = 0.47;
let yStart = -1.12;
let yEnd = 1.12;
const initialBBox = [-2, -1.12, 0.47, 1.12];

let [xStart, yStart, xEnd, yEnd] = initialBBox;

const ZOOMFACTOR = 8;

function scaleMandelbrot(canvasWidth: number, canvasHeight: number, initialBBox: number[]): number[] {
let ratio = canvasWidth / canvasHeight;

if(ratio > 1) {
let diff2 = (initialBBox[2] - initialBBox[0]) * (ratio - 1) / 2;
return [initialBBox[0] - diff2, initialBBox[1], initialBBox[2] + diff2, initialBBox[3]];
}
ratio = 1 / ratio;
let diff2 = (initialBBox[3] - initialBBox[1]) * (ratio - 1) / 2;

return [initialBBox[0], initialBBox[1] - diff2, initialBBox[2], initialBBox[3] + diff2];

}

export default function Home() {
const [iterations, setIterations] = useState(255);

// useEffect is to make sure it is only run client-side
useEffect(() => {
import("./mandelbrot/wasm_exec").then(() => {
const ctx = (document.getElementById("mandelbrotCanvas") as HTMLCanvasElement).getContext("2d");

ctx!.canvas.width = Math.floor(ctx!.canvas.clientWidth);
ctx!.canvas.height = Math.floor(ctx!.canvas.clientHeight);

(async () => {
await import("./mandelbrot/wasm_exec")

const go = new global.Go(); // Defined in wasm_exec.js
const WASM_URL = '/mandelbrot/wasm.wasm';

WebAssembly.instantiateStreaming(fetch(WASM_URL), go.importObject).then(function (obj) {
wasm = obj.instance;
go.run(wasm);

({allocateBuffer, mandelbrot} = wasm.exports);

({width: WIDTH, height: HEIGHT} = document.getElementById("mandelbrotCanvas")!.getBoundingClientRect());

bufferPointer = allocateBuffer(WIDTH, HEIGHT);
wasm = (await WebAssembly.instantiateStreaming(fetch(WASM_URL), go.importObject)).instance;
go.run(wasm);

renderMandelbrot();
})
});
}, [])
({allocateBuffer, mandelbrot} = wasm.exports);


[WIDTH, HEIGHT] = [ctx!.canvas.width, ctx!.canvas.height];
[xStart, yStart, xEnd, yEnd] = scaleMandelbrot(WIDTH, HEIGHT, initialBBox);

bufferPointer = allocateBuffer(WIDTH, HEIGHT);

renderMandelbrot();
})()
}, [])

function renderMandelbrot() {
console.log(xStart, xEnd, yStart, yEnd)
console.log(xStart, yStart, xEnd, yEnd);
mandelbrot(xStart,xEnd,yStart,yEnd, iterations);

// Get the buffer as a Uint8Array
Expand All @@ -71,12 +88,22 @@ export default function Home() {
renderMandelbrot();
}

function resetMandelbrot() {
[xStart, yStart, xEnd, yEnd] = scaleMandelbrot(WIDTH, HEIGHT, initialBBox);

renderMandelbrot();
}


return (
<div>
<canvas id="mandelbrotCanvas" width="700px" height="700px" onClick={zoomMandelbrot}></canvas>
<button onClick={renderMandelbrot}>DISPLAY</button>
<canvas id="mandelbrotCanvas" style={{width: "100vw", height: "90vh"}} onClick={zoomMandelbrot}></canvas>
<p>Click to zoom</p>
<span>Amount of iterations (better resolution, less performant): </span>
<input type="number" value={iterations} onChange={(e) => setIterations(parseInt(e.target.value) || 0)} />
<br />
<button onClick={renderMandelbrot}>DISPLAY</button>
<button onClick={resetMandelbrot}>RESET</button>
</div>
)
}
2 changes: 1 addition & 1 deletion alexanderristinmaa/app/(homepage)/alex/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export default function Home() {
return (
<div id={styles.imgText}>
<div id={styles.imgDiv}>
<img id={styles.img} src='/alex/me.jpg'></img>
<img id={styles.img} src='/alex/me.jpg' alt="vacker bild på alex med suddig bakgrund"></img>
<p><i>Jag i Fontainebleau, Frankrike. 2024</i></p>
</div>
<div id={styles.textDiv}>
Expand Down
2 changes: 0 additions & 2 deletions alexanderristinmaa/public/mandelbrot/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,6 @@ func mandelbrot(xStart, xEnd, yStart, yEnd float64, iterations int) {

scaleIterations := 255.0 / float64(iterations)

fmt.Println(HEIGHT, WIDTH)

for y := 0; y < HEIGHT; y++ {
coord = complex(xStart, imag(coord))

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit ab6f194

Please sign in to comment.