Generating colored Julia sets in the browser using wasm.
To draw a trippy "fractal" on an HTML <canvas>
using this module requires
the following steps:
- Load the wasm module into your Javascript script. If you want it to
signal to the browser when it panics, expose your panic singnalling script
to the
pnk()
function, like so:
let wasm_mod = {};
WebAssembly.instantiateStreaming(
fetch("http:// ... /jset_wasm.wasm"),
{
"env": {
"pnk": your_panic_alert_function,
},
}
).then(obj => wasm_mod = obj);
- Call
set_gradient()
for each color gradient in your trippy fractal. For example, to have it fade from black to blue to cyan to white to black again:
// |-start color--| |-end color----|
// args: gradient #, red, green, blue, red, green, blue, # of steps
wasm_mod.instance.exports.set_gradient(0, 0, 0, 0, 0, 0, 255, 256);
wasm_mod.instance.exports.set_gradient(1, 0, 0, 255, 0, 255, 255, 256);
wasm_mod.instance.exports.set_gradient(2, 0, 255, 255, 255, 255, 255, 256);
wasm_mod.instance.exports.set_gradient(3, 255, 255, 255, 0, 0, 0, 256);
- Call
set_n_gradients()
to inform the module of the number of gradients.
wasm_mod.instance.exports.set_n_gradients(4); // We set gradients 0-3 above.
- Call
update_color_map()
to make a color map out of the gradient information you have set.
wasm_mod.instance.exports.update_color_map();
- If you want to use the polynomial iterator, specify the polynomial
function to use with
set_coeff()
to set each coefficient and to tell the module how many coefficients you want to useset_n_coeffs()
.
// Specify iterator as f(z) = z^2 + 0.7e^(i0.63π)
// Calculate the real and imaginary parts of the constant term from
// its polar coordinates:
const re_part = 0.7 * Math.cos(0.63 * Math.PI);
const im_part = 0.7 * Math.sin(0.63 * Math.PI);
wasm_mod.instance.exports.set_coeff(0, re_part, im_part);
wasm_mod.instance.exports.set_coeff(1, 0, 0);
wasm_mod.instance.exports.set_coeff(2, 1.0, 0);
wasm_mod.instance.exports.set_n_coeffs(3);
Be careful with the polynomial iterator. While the Mandlebrot iterator is guaranteed to produce an interesting image, most sets of polynomial coefficients will not. The one specified in the example above will, so if you want quick results, start with that, and make small incremental changes until you have something you like. Keep your coefficients small, especially the higher-degree ones.
- Call
redraw()
with the appropriate image parameters to churn through all the calculations and write image data to the exposedIMAGE
buffer.
wasm_mod.instance.exports.redraw(
xpix, // width of canvas in pixels
ypix, // height of canvas in pixels
x, // real coordinate of upper-left-hand corner of image
y, // imaginary coordinate of upper-left-hand corner of image
width, // width of image on the Complex plane
poly_p, // boolean indicating whenther to use the polynomial iterator
smooth // desired intensity of smoothing (from 0.0 to 1.0)
);
- Wrap the
IMAGE
buffer in aUint8ClampedArray
, wrap it in anImageData
object, and put it in the<canvas>
's"2d"
context.
const data = new ImageData(
new UInt8ClampedArray(
wasm_mod.instance.exports.memory.buffer,
wasm_mod.instance.exports.BUFFER.value,
4 * xpix * ypix // 4 bytes of data per pixel
),
xpix // image width in pixels
);
const canvas = document.getElementById("your-canvas");
canvas.getContext("2d").putImageData(data, 0, 0);
The most time-consuming step is the call to redraw()
--it's the one that
iterates a complex value associated with each pixel until it either diverges
or runs out of color map. If you don't need to redo the iteration, but just
want to recolor the image, you can use the exposed function recolor()
:
wasm_mod.instance.exports.set_gradient(0, 0, 0, 0, 0, 255, 0, 128);
wasm_mod.instance.exports.set_gradient(0, 0, 255, 0, 0, 0, 255, 256);
wasm_mod.instance.exports.set_gradient(0, 0, 255, 255, 0, 0, 0, 256);
wasm_mod.instance.exports.set_n_gradients(3); // We just specified 3 gradients.
wasm_mod.instance.exports.recolor(); // <-- This call right here.
// Then we need to shove the data into the <canvas> again.
const data = new ImageData(
new UInt8ClampedArray(
wasm_mod.instance.exports.memory.buffer,
wasm_mod.instance.exports.BUFFER.value,
4 * xpix * ypix
),
xpix
);
const canvas = document.getElementById("your-canvas");
canvas.getContext("2d").putImageData(data, 0, 0);
There is one caveat here: If you specify a new color map that is longer than
the old color map (that is, the sum of the number of shades in the collection
of gradients is larger), it won't display properly until you call This has been changed so that if the color map is extended,
the iterator will make another pass through the image, but only operate on
the points that would have iterated off the end of the old color map. I think
this is a good compromise; it keeps recoloring relatively fast unless you have
a lot of slow-diverging (or non-diverging) points in the image.redraw()
again. I don't think it's possible to fix this while keeping the recolor()
call fast.
All the core functionality is now implemented.
user-specifiable polynomial iterationdone!user-specifiable color mapdone!- user-specifiable "default" color (the one used when the iterator runs out of colormap before a point diverges)
perhaps some type of smoothing, blurring, or downsamplingI added a parameter for a very simple smoothing pass. It's not as bad as I though it might have been; it's very subtle, even at full intensity.- improved UI of the web interface
- drag-resizable canvas
- better ergonomics/visuals for entering polynomial coefficients
- fixed-size controls
<div>
- sucking less on mobile