web-sys: Julia Set

While not showing off a lot of web_sys API surface area, this example shows a neat fractal that you can make! _ [wasm-bindgen Guide]

Original [web-sys: Julia Set]

setup the project

cargo new julia_set --lib
cd julia_set
mkdir -p www/js www/html www/css
cargo add wasm-bindgen
cargo add web-sys -F "ImageData, CanvasRenderingContext2d"

Edit Cargo.toml

[lib]
crate-type = ["cdylib"]

and change

web-sys = { version = "0.3.69", features = ["ImageData", "CanvasRenderingContext2d"] }

to

[dependencies.web-sys]
version = "0.3.69"
features = [
  "ImageData",
  "CanvasRenderingContext2d",
]

the index.html

<!doctype html>
<html>
  <head>
    <meta content="text/html;charset=utf-8" http-equiv="Content-Type"/>
    <meta name="viewport" content="initial-scale=1.0, user-scalable=no"/>
    <title>Julia Set: nobundle</title>
    <link rel="stylesheet" href="../css/normalize.css">
    <link rel="stylesheet" href="../css/skeleton.css">
    <link rel="stylesheet" href="../css/style.css">
  </head>
  <body>
    <main>
      <div class="controls">
        <h1>C =</h1>
        <div>
          <label for="real">real</label>
          <input type="number" name="real" id="real" value="-0.15" step="0.001">
        </div>
        <div>
          <label for="imaginary">imaginary</label>
          <input type="number" name="imaginary" id="imaginary" value="0.65" step="0.001">
        </div>
        <button class="button-primary" id="render">render</button>
      </div>
      <canvas id="drawing" width="600" height="600"></canvas>
    </main>

    <script type="module" src="../js/index.js"></script>
  </body>
</html>

put them in www/css

Next in www/js/index.js

import init, { draw } from "../pkg/julia_set.js"

async function run() {
    const wasm = await init().catch(console.error);
    console.log(wasm);
    const canvas = document.getElementById('drawing');
    const ctx = canvas.getContext('2d');
    const realInput = document.getElementById('real');
    const imaginaryInput = document.getElementById('imaginary');
    const renderBtn = document.getElementById('render');

    renderBtn.addEventListener('click', () => {
        const real = parseFloat(realInput.value) || 0;
        const imaginary = parseFloat(imaginaryInput.value) || 0;
        draw(ctx, 600, 600, real, imaginary);
    });

    draw(ctx, 600, 600, -0.15, 0.65);
}

run();

Finaly in Rust code in lib.rs

#![allow(unused)]
fn main() {
// src/lib.rs

use std::ops::Add;
use wasm_bindgen::prelude::*;
use wasm_bindgen::Clamped;
use web_sys::{CanvasRenderingContext2d, ImageData};

#[wasm_bindgen]
pub fn draw(
    ctx: &CanvasRenderingContext2d,
    width: u32,
    height: u32,
    real: f64,
    imaginary: f64,
) -> Result<(), JsValue> {
    // The real workhorse of this algorithm, generating pixel data
    let c = Complex { real, imaginary };
    let data = get_julia_set(width, height, c);
    let data = ImageData::new_with_u8_clamped_array_and_sh(Clamped(&data), width, height)?;
    ctx.put_image_data(&data, 0.0, 0.0)
}

fn get_julia_set(width: u32, height: u32, c: Complex) -> Vec<u8> {
    let mut data = Vec::new();

    let param_i = 1.5;
    let param_r = 1.5;
    let scale = 0.005;

    for x in 0..width {
        for y in 0..height {
            let z = Complex {
                real: y as f64 * scale - param_r,
                imaginary: x as f64 * scale - param_i,
            };
            let iter_index = get_iter_index(z, c);
            data.push((iter_index / 4) as u8);
            data.push((iter_index / 2) as u8);
            data.push(iter_index as u8);
            data.push(255);
        }
    }

    data
}

fn get_iter_index(z: Complex, c: Complex) -> u32 {
    let mut iter_index: u32 = 0;
    let mut z = z;
    while iter_index < 900 {
        if z.norm() > 2.0 {
            break;
        }
        z = z.square() + c;
        iter_index += 1;
    }
    iter_index
}

#[derive(Clone, Copy, Debug)]
struct Complex {
    real: f64,
    imaginary: f64,
}

impl Complex {
    fn square(self) -> Complex {
        let real = (self.real * self.real) - (self.imaginary * self.imaginary);
        let imaginary = 2.0 * self.real * self.imaginary;
        Complex { real, imaginary }
    }

    fn norm(&self) -> f64 {
        (self.real * self.real) + (self.imaginary * self.imaginary)
    }
}

impl Add<Complex> for Complex {
    type Output = Complex;

    fn add(self, rhs: Complex) -> Complex {
        Complex {
            real: self.real + rhs.real,
            imaginary: self.imaginary + rhs.imaginary,
        }
    }
}

}

build and serve

wasm-pack build --target web --no-typescript --out-dir www/pkg
http www

open index.html

firefox http://localhost:8000/html/

julia set


What's next?

<-- web-sys: canvas hello world WebAudio -->