Skip to content

Instantly share code, notes, and snippets.

@PM2Ring
Created June 9, 2024 11:32
Show Gist options
  • Save PM2Ring/bd6887d5a8dbca3d69c9703d1d270f56 to your computer and use it in GitHub Desktop.
Save PM2Ring/bd6887d5a8dbca3d69c9703d1d270f56 to your computer and use it in GitHub Desktop.
Simple Mandelbrot set, in JavaScript
<!DOCTYPE html>
<html>
<head>
<title>Mandelbrot</title>
<meta http-equiv="Content-Script-Type" content="text/javascript">
<style type="text/css">
h { text-align: center; }
#mycanvas { border: 1px solid black; cursor: crosshair;
margin-left: 1%; margin-right: 1%; }
</style>
<script>
var canvas, ctx, canvasData, pixels, palette, palsize = 1 + 6*255, grid,
imax, width, height, ou, ov, ox, oy, cmul, imul, go = false, timeout, kcount;
var zoom =
{
//str, value, delta, depth, scale,
set: function(s)
{
switch(s)
{
case 'out':
this.str = s; this.value = 2.0; this.delta = -1; break;
case 'pan':
this.str = s; this.value = 1.0; this.delta = 0; break;
case 'in':
this.str = s; this.value = 0.5; this.delta = 1; break;
case 'reset':
this.str = 'in'; this.value = 0.5; this.delta = 1;
this.depth = -1; this.scale = 6.5 / width;
}
},
update: function()
{
if (this.delta)
set_imax(this.delta);
this.depth += this.delta;
this.scale *= this.value;
}
};
function byId(id) { return document.getElementById(id); }
function show(s) { byId("output").innerHTML = s; }
//Convert hue t to RGB. 0 <= t < 1
function hue2RGB(t)
{
var m = 255, i, j, h;
i = Math.floor(m * t);
j = Math.floor(((i % 43) / 43) * m);
switch(Math.floor(i / 43))
{
case 0: h = [ m, j, 0]; break;
case 1: h = [m-j, m, 0]; break;
case 2: h = [ 0, m, j]; break;
case 3: h = [ 0, m-j, m]; break;
case 4: h = [ j, 0, m]; break;
case 5: h = [ m, 0, m-j]; break;
}
return h;
}
function create_palette(n)
{
var i, p = new Array(n);
p[0] = [0, 0, 0];
for (i=1; i<n; i++)
p[i] = hue2RGB(i / (n-1));
return p;
}
/* Convert pixel number to real */
function u2x(u){ return ox + zoom.scale * (u - ou); }
function v2y(v){ return oy + zoom.scale * (ov - v); }
function ev_mouse(evt)
{
var u, v;
//Ignore right mouse button.
if (evt.button > 1) return;
// Get the mouse position.
if (evt.layerX != undefined)
{
// Firefox
u = evt.layerX;
v = evt.layerY;
}
else if (ev.offsetX != undefined)
{
// Opera
u = evt.offsetX;
v = evt.offsetY;
}
else return;
//Convert the position so it's relative to the canvas element.
u -= canvas.offsetLeft;
v -= canvas.offsetTop;
//Convert from pixel co-ord to complex
ox = u2x(u); oy = v2y(v);
window.status = u + ', ' + v + '->' + ox + ', ' + oy;
do_mandel();
}
function ev_key(evt)
{
var ch;
if (!evt.charCode) return;
switch(ch = String.fromCharCode(evt.charCode))
{
case 'd': set_imax(-1); break;
case 'D': set_imax(1); break;
case 'm': imul -= 1; break;
case 'M': imul += 1; break;
case 'c': cmul -= 1; break;
case 'C': cmul += 1; break;
case 'i': zoom.set('in'); break;
case 'I': zoom.set('in');
ox = u2x(ou); oy = v2y(ov); do_mandel(); break;
case 'o': zoom.set('out'); break;
case 'O': zoom.set('out');
ox = u2x(ou); oy = v2y(ov); do_mandel(); break;
case 'p': zoom.set('pan'); break;
case 'P': zoom.set('pan'); do_mandel(); break;
case 'r': update_canvas(); break;
case 'y': toggle_timer(); break;
case 'Y': cmul = -cmul; break;
case 'Z':
case 'z': init_mandel(); do_mandel(); break;
default: alert("Unknown key command [" + ch + "]");
}
show_status();
}
//Set maximum number of iterations
function set_imax(m)
{
var iscale = 1.1;
switch(m)
{
case -1: imax = Math.floor(0.5 + imax / iscale);
case 0: imax = 145;
case 1: imax = Math.floor(0.5 + imax * iscale);
}
}
function show_status()
{
show("zoom = " + zoom.str + "<br>"
+ "zoom depth = " + zoom.depth + "<br>"
+ "iteration <u>d</u>epth = " + imax + "<br>"
+ "palette <u>m</u>ultiplier = " + imul + "<br>"
+ "colour c<u>y</u>cling = " + (go ? "on" : "off") + "<br>"
+ "<u>c</u>ycle multiplier = " + cmul + "<br>"
);
}
function toggle_timer()
{
if(go = !go)
timer();
else
window.clearTimeout(timeout);
}
function init_mandel()
{
ox = -0.5; oy = 0; kcount = 0; cmul = 13; imul = 31;
set_imax(0); zoom.set('reset');
}
/* Find iteration count value for a single mandelbrot pixel */
function mandelpoint(cx, cy)
{
var i, x = cx, y = cy, x2, y2;
for(i=0; i<imax; i++)
{
x2 = x * x;
y2 = y * y;
if(x2 + y2 > 4.)
break;
/* Do actual Mandelbrot calculation */
y = 2. * x * y + cy;
x = x2 - y2 + cx;
}
/* Point is in Mandelbrot set if i==imax */
return i < imax ? i : -1;
}
function do_mandel()
{
var i = 0, d, x, y, u, v, x0, y0;
zoom.update();
show_status();
d = zoom.scale;
x0 = u2x(0);
y0 = v2y(0);
for (y=y0, v=0; v<height; v++, y-=d)
for (x=x0, u=0; u<width; u++, x+=d)
grid[i++] = mandelpoint(x, y);
update_canvas();
}
function timer()
{
if (go)
{
update_canvas();
if (++kcount == palsize) kcount = 0;
timeout = window.setTimeout("timer()", 40);
}
}
function update_canvas()
{
var i, j, p, g,
ps = palsize - 1,
kc = (kcount * cmul) % ps;
//Copy grid to pixel array
for (j=i=0; i<grid.length; i++, j+=4)
{
g = grid[i];
p = palette[g < 0 ? 0 : 1 + (ps + (kc + g * imul) % ps) % ps];
pixels[j] = p[0];
pixels[j+1] = p[1];
pixels[j+2] = p[2];
}
ctx.putImageData(canvasData, 0, 0);
}
function show_image()
{
var w = window.open('about:blank'), data = canvas.toDataURL('image/png');
w.document.write('<img src=' + data + '><br>'
+ '<textarea cols="80" rows="12">' + data + '</textarea>');
w.document.close();
}
function setup()
{
canvas = byId('mycanvas');
if (canvas.getContext)
{
//Get window height & calculate canvas dimensions for a 4:3 aspect
height = 6 * Math.floor(0.92 * window.innerHeight / 6);
//height = 120;
canvas.height = height;
canvas.width = width = 4 * height / 3;
ou = width / 2;
ov = height / 2;
//Initialize temporary pixel grid
grid = new Array(width*height);
palette = create_palette(palsize);
ctx = canvas.getContext('2d');
canvasData = ctx.createImageData(width, height)
pixels = canvasData.data;
//Make all canvas pixels opaque
for (var i=0; i<grid.length; i++)
pixels[4*i + 3] = 255;
init_mandel();
zoom.update();
canvas.addEventListener('mousedown', ev_mouse, true);
window.addEventListener('keypress', ev_key, false);
show("Ready<br>Press z to start.");
}
else alert("Sorry, I can't set up the canvas!");
}
</script>
</head>
<body onload="setup();">
<table><tr><td>
<canvas id="mycanvas" oncontextmenu="return true;">
If you can read this, your browser does not support the HTML5 Canvas.
</canvas>
<td><table><tr><td>
<h4>Status</h4>
<div id="output"></div>
<hr>
<h4>Key commands</h4>
i, I: zoom <u>i</u>n<br>
o, O: zoom <u>o</u>ut<br>
p, P: <u>p</u>an<br>
z, Z: reset to ground <u>z</u>ero<br>
<br>
d: iteration <u>d</u>epth decrease <br>
D: iteration <u>D</u>epth increase <br>
<br>
m: palette <u>m</u>ultiplier decrease <br>
M: palette <u>M</u>ultiplier increase <br>
y: toggle colour c<u>y</u>cling<br>
Y: reverse colour c<u>Y</u>cling<br>
c: <u>c</u>ycle speed decrease<br>
C: <u>C</u>ycle speed increase<br>
r: <u>r</u>epaint <br>
<input type="button" value="Show image" onclick="show_image();">
</table></table>
<hr>
<h3>Mandelbrot generator</h3>
This JavaScript program allows you to explore the Mandelbrot set. It has a
fixed colour palette, but you can achieve some variation by using the
palette multiplier commands. It also has variable speed palette cycling.
<br>
Colour calculations are performed after the Mandelbrot set data is calculated, so
the current image can be viewed with different palette multipliers without being
recalculated. The display will be automatically updated to the new palette multiplier
if colour cycling is turned on. If colour cycling is off, just press r to repaint the
image with the new parameters.
<h5>Zooming</h5>
The lower case i, o &amp; p keys select zooming via the mouse. Wherever you
click the left mouse button on the canvas will become the centre of the new image.
<br>
The upper case I, O &amp; P keys cause the image to zoom at the canvas centre point.
<br>
When zooming in or out, the maximum iteration depth will automatically be scaled, but
it may be desirable to adjust this manually (with d &amp; D), depending on the
location you're zooming to.
<br>
The z key resets the program to the initial default values and displays the whole
Mandelbrot set.
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment