<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link rel="icon" src="/images/favicon.gif" />
<title>WebGL Fractal Thing</title>
<style type="text/css">
* {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
-o-box-sizing: border-box;
box-sizing: border-box;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
}
html, body, canvas { margin: 0; border: 0; padding: 0; width: 100%; height: 100%; }
canvas { display: block; image-rendering: pixelated; background: #f00; }
#buttons { position: absolute; top: 0; left: 0; width: 100%; height: 100%; text-align: center; }
label { padding: 2px 6px; }
label, button {
font-family: sans-serif;
font-weight: bold;
color: #fff;
background: #555;
border: 1px outset #555;
font-size: 1.5em;
}
#data {
position: absolute;
width: 100%;
bottom: 0;
padding: 8px;
color: #ff0;
font-size: 2em;
font-family: monospace;
font-weight: bold;
text-align: center;
-webkit-text-stroke: .8px #000;
-moz-text-stroke: .8px #000;
-ms-text-stroke: .8px #000;
-o-text-stroke: .8px #000;
text-stroke: .8px #000;
}
</style>
<script id="line-fragment" type="x-shader/fragment">
void main () { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); }
</script>
<script id="line-vertex" type="x-shader/fragment">
attribute vec2 coord;
void main () { gl_Position = vec4(coord, 0.0, 1.0); }
</script>
<script id="complex-fragment" type="x-shader/fragment">
#define LIMIT x
precision mediump float;
precision mediump int;
vec2 z, c;
varying vec2 Z, C;
uniform int limit;
void main () {
z = Z;
c = C;
int age = 1;
vec2 old = vec2(0.0, 0.0);
bool escaped = false;
vec2 square;
for (int i = 0; i < LIMIT; i++) {
square = z * z;
if (escaped) {
float r = 10.0 * (float(i) - log2(log(square.x + square.y) * 0.5)) / float(LIMIT);
gl_FragColor = vec4(r * 0.125, r * 0.25, r, 1.0);
return;
}
if (square.x + square.y > 16.0)
escaped = true;
z.y = 2.0 * z.x * z.y + c.y;
z.x = square.x - square.y + c.x;
if (old == z) break;
if (i > age) { age += age; old = z; }
}
gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
}
</script>
<script id="complex-vertex" type="x-shader/vertex">
precision mediump float;
precision mediump int;
attribute vec2 coord;
varying vec2 Z, C;
uniform vec2 seed, center;
uniform float scale, ratio;
uniform bool julia;
uniform int limit;
void main () {
if (julia) {
Z = scale * vec2(ratio, 1.0) * coord + center;
C = seed;
} else {
C = scale * vec2(ratio, 1.0) * coord + center;
Z = vec2(0.0, 0.0);
}
gl_Position = vec4(coord, 0.0, 1.0);
}
</script>
<script type="text/javascript">
function compile_shader (type, id) {
var shader = gl.createShader(type);
gl.shaderSource(shader, document.getElementById(id).innerHTML);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS))
console.log(gl.getShaderInfoLog(shader));
else console.log("successfully compiled "+id+" shader");
return shader;
}
function trackOrbit (coord) {
var orbit = []
var z = { x: coord[0], y: coord[1] };
var c = { x: coord[0], y: coord[1] };
var scl = scale.data;
var ctr = center.data;
if (julia.active) {
scl = julia.scale;
ctr = julia.center;
c = { x: seed.data[0], y: seed.data[1] };
}
//console.log(z.x+"-"+ctr[0]+"/"+scl);
for (var i = 0; i < iterations.limit; i++) {
orbit[i+i] = (z.x - ctr[0]) / scl / ratio.data;
orbit[i+i+1] = (z.y - ctr[1]) / scl;
z = {
y: 2 * z.x * z.y + c.y,
x: z.x * z.x - z.y * z.y + c.x
};
}
gl.bindBuffer(gl.ARRAY_BUFFER, buffer.line);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(orbit), gl.STATIC_DRAW);
}
var drawRequested = false;
function requestDraw (e) {
if (drawRequested) return;
drawRequested = true;
if (e && (!julia.active || julia.active && orbit))
trackOrbit(planeCoords(e.clientX, e.clientY));
requestAnimationFrame(draw);
}
var fps = [];
var last = 0;
function draw (time) {
function fixed (x) { return (x < 0 ? "" : "&nbsp;") + x.toFixed(4); }
function signed (x) { return (x < 0 ? "" : "+") + x.toFixed(4); }
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
if (!julia.active || orbit) {
gl.useProgram(shaders.line);
gl.bindBuffer(gl.ARRAY_BUFFER, buffer.line);
gl.vertexAttribPointer(attrib.line, 2, gl.FLOAT, gl.FALSE, 8, 0);
gl.drawArrays(gl.LINE_STRIP, 0, iterations.limit);
gl.useProgram(shaders.complex);
gl.bindBuffer(gl.ARRAY_BUFFER, buffer.canvas);
gl.vertexAttribPointer(attrib.canvas, 2, gl.FLOAT, gl.FALSE, 8, 0);
}
fps.push(1000 / (time - last));
last = time;
if (fps.length > 1000) fps.shift();
data.innerHTML = (
"seed: " + fixed(seed.data[0]) + signed(seed.data[1]) +
"i, center: " + fixed(center.data[0]) + signed(center.data[1]) +
"i, radius: " + scale.data.toFixed(4) +
", fps: " + (fps.reduce(function (a, b) {
return (a + b);
}, 0) / fps.length).toFixed(2)
);
resolution.innerHTML = "Resolution: 1/" + resolution.data;
iterations.innerHTML = "Iterations: " + iterations.limit;
drawRequested = false;
}
function cached_uniform (pgm, dest, type) {
var u = gl.getUniformLocation(pgm, dest);
var types = [ gl.INT, gl.FLOAT,
gl.INT_VEC2, gl.INT_VEC3, gl.INT_VEC4,
gl.FLOAT_VEC2, gl.FLOAT_VEC3, gl.FLOAT_VEC4,
gl.FLOAT_MAT2, gl.FLOAT_MAT3, gl.FLOAT_MAT4
];
type = types.indexOf(type);
var functions = [
gl.uniform1i.bind(gl), gl.uniform1f.bind(gl),
gl.uniform2iv.bind(gl), gl.uniform3iv.bind(gl),
gl.uniform4iv.bind(gl), gl.uniform2fv.bind(gl),
gl.uniform3fv.bind(gl), gl.uniform4fv.bind(gl),
gl.uniformMatrix2fv.bind(gl),
gl.uniformMatrix3fv.bind(gl),
gl.uniformMatrix4fv.bind(gl)
];
var f = functions[type];
if (type < 2) // scalars
return function (v) { return f(u, v); }
if (type < 5) // integer arrays
return function (v) { return f(u, new Int32Array(v)); }
// float arrays
return function (v) { return f(u, new Float32Array(v)) }
}
function slice (x) { return Array.prototype.slice.call(x); }
function fragment_limit (lim) {
var src = document.getElementById("complex-fragment");
src.innerHTML = src.innerHTML.replace(
/#define LIMIT .*/,
"#define LIMIT " + lim
);
var fragment = compile_shader(gl.FRAGMENT_SHADER, "complex-fragment");
gl.attachShader(shaders.complex, fragment);
gl.linkProgram(shaders.complex);
if (!gl.getProgramParameter(shaders.complex, gl.LINK_STATUS))
console.log(gl.getProgramInfoLog(shaders.complex));
gl.detachShader(shaders.complex, fragment);
gl.deleteShader(fragment);
if ("ratio" in window) {
( seed.push = cached_uniform(shaders.complex, "seed", gl.FLOAT_VEC2))( seed.data );
( julia.push = cached_uniform(shaders.complex, "julia", gl.INT ))( julia.active);
scale.push = cached_uniform(shaders.complex, "scale", gl.FLOAT );
ratio.push = cached_uniform(shaders.complex, "ratio", gl.FLOAT );
center.push = cached_uniform(shaders.complex, "center", gl.FLOAT_VEC2);
if (julia.active) {
scale.push(julia.scale);
center.push(julia.center);
} else {
scale.push(scale.data);
center.push(center.data);
}
onresize();
} else {
ratio = { push: cached_uniform(shaders.complex, "ratio", gl.FLOAT ) };
seed = { push: cached_uniform(shaders.complex, "seed", gl.FLOAT_VEC2) };
julia = { push: cached_uniform(shaders.complex, "julia", gl.INT ) };
scale = { push: cached_uniform(shaders.complex, "scale", gl.FLOAT ) };
center = { push: cached_uniform(shaders.complex, "center", gl.FLOAT_VEC2) };
}
}
function screenCoords (x, y) {
// not the best function name
// converts screenspace coords to complex plane coords
return [
2 * ratio.data * scale.data * (x / (innerWidth - 1) - .5),
2 * scale.data * (.5 - y / (innerHeight - 1))
];
}
function planeCoords (x, y) {
var tmp = screenCoords(x, y);
return [ tmp[0] + center.data[0], tmp[1] + center.data[1] ];
}
function cancelBubble (e) { return !((e || event).cancelBubble = true); }
addEventListener("load", function load (e) {
/* disable selection */
document.body.onselectstart = function () { return false; }
removeEventListener("load", load, false);
canvas = document.getElementsByTagName("canvas")[0];
gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
if (!gl) {
alert("Sorry, you need WebGL for this.");
//history.back();
}
iterations = document.getElementById("iterations");
iterations.up = document.getElementById("iterations-up");
iterations.dn = document.getElementById("iterations-dn");
iterations.limit = 100;
shaders = {};
shaders.line = gl.createProgram();
var vert = compile_shader(gl.VERTEX_SHADER, "line-vertex");
gl.attachShader(shaders.line, vert);
var frag = compile_shader(gl.FRAGMENT_SHADER, "line-fragment");
gl.attachShader(shaders.line, frag);
gl.linkProgram(shaders.line);
if (!gl.getProgramParameter(shaders.line, gl.LINK_STATUS))
console.log(gl.getProgramInfoLog(shaders.line));
gl.deleteShader(vert);
gl.detachShader(shaders.line, vert);
gl.deleteShader(frag);
gl.detachShader(shaders.line, frag);
shaders.complex = gl.createProgram();
var vert = compile_shader(gl.VERTEX_SHADER, "complex-vertex");
gl.attachShader(shaders.complex, vert);
fragment_limit(iterations.limit);
gl.deleteShader(vert);
//gl.detachShader(shaders.complex, vert);
gl.useProgram(shaders.complex);
attrib = {
line: gl.getAttribLocation(shaders.line, "coord"),
canvas: gl.getAttribLocation(shaders.complex, "coord")
};
gl.enableVertexAttribArray(attrib.line);
gl.enableVertexAttribArray(attrib.complex);
buffer = {
line: gl.createBuffer(),
canvas: gl.createBuffer()
};
gl.bindBuffer(gl.ARRAY_BUFFER, buffer.canvas);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
-1,-1,-1, 1, 1,-1, 1, 1
]), gl.STATIC_DRAW);
gl.vertexAttribPointer(attrib.complex, 2, gl.FLOAT, gl.FALSE, 8, 0);
orbit = false;
mouse = { x: 0, y: 0 };
dragging = false;
moved = false;
document.onmousemove = function (e) {
moved = dragging;
if (!julia.active || !orbit)
seed.push(seed.data = planeCoords(e.clientX, e.clientY));
if (!julia.active) {
if (dragging) {
center.data[0] += 2 * scale.data * (mouse.x - e.clientX)
/ (innerHeight - 1);
center.data[1] -= 2 * scale.data * (mouse.y - e.clientY)
/ (innerHeight - 1);
center.push(center.data);
mouse.x = e.clientX;
mouse.y = e.clientY;
}
}
requestDraw(e);
}
document.oncontextmenu = function (e) {
if (julia.active) orbit = !orbit;
requestDraw(e);
return false;
}
document.onmousedown = function (e) {
if (0 != e.button) return;
mouse.x = e.clientX;
mouse.y = e.clientY;
dragging = true;
}
document.onmouseup = function (e) {
if (0 != e.button) return;
dragging = false;
}
document.onclick = function (e) {
if (moved)
return moved = false;
seed.push(seed.data);
julia.active = !julia.active;
julia.push(julia.active);
if (julia.active) {
scale.push(julia.scale);
center.push(julia.center);
} else {
center.push(center.data);
scale.push(scale.data);
}
requestDraw(e);
}
document.onwheel = function (e) {
if (julia.active) return;
function sign (x) {
return typeof x === 'number' ? x ? x < 0 ? -1 : 1 : x === x ? 0 : NaN : NaN;
}
var coords = screenCoords(e.clientX, e.clientY);
center.data[0] += sign(e.deltaY) * -coords[0] / 9.9;
center.data[1] += sign(e.deltaY) * -coords[1] / 9.9;
center.push(center.data);
scale.push(scale.data += sign(e.deltaY) * scale.data / 10);
requestDraw(e);
}
onresize = function (e) {
canvas.height = innerHeight / resolution.data;
canvas.width = innerWidth / resolution.data;
gl.viewport(0, 0, canvas.width, canvas.height);
ratio.push(ratio.data = innerWidth / innerHeight);
requestDraw(e);
}
// make buttons eat input
var controls = slice(document.getElementsByTagName("button"));
controls = controls.concat(slice(document.getElementsByTagName("label")));
for (var i = 0; i < controls.length; i++) {
controls[i].onclick =
controls[i].onmouseup =
controls[i].onmousedown =
controls[i].oncontextmenu = cancelBubble;
}
iterations.dn.onclick = function (e) {
if ((iterations.limit -= 50) < 50) iterations.limit = 50;
fragment_limit(iterations.limit);
requestDraw(e);
return cancelBubble(e);
}
iterations.up.onclick = function (e) {
if (!iterations.warned) {
iterations.warned = true;
alert(
"Setting the iteration limit too high will cause frame renders" +
" to time out, which will crash WebGL. You've been warned."
);
}
fragment_limit(iterations.limit += 50);
requestDraw(e);
return cancelBubble(e);
}
resolution = document.getElementById("resolution");
resolution.up = document.getElementById("resolution-up");
resolution.dn = document.getElementById("resolution-dn");
resolution.dn.onclick = function (e) {
++resolution.data;
onresize();
requestDraw(e);
return cancelBubble(e);
}
resolution.up.onclick = function (e) {
if (--resolution.data < 1) resolution.data = 1;
onresize();
requestDraw(e);
return cancelBubble(e);
}
var reset = document.getElementById("reset");
reset.onclick = function (e) {
orbit = false;
seed.push(seed.data = [ 0, 0 ]);
scale.push(scale.data = 1.3);
center.push(center.data = [ 0, 0 ]);
julia.push(julia.active = true);
julia.center = [ 0, 0 ];
julia.scale = 1.3;
resolution.data = 2;
iterations.limit = 100;
onresize();
return cancelBubble(e);
}
reset.click();
}, false);
</script>
</head>
<body>
<canvas>
</canvas>
<div id="buttons">
<button id="reset">Reset</button>
<label id="iterations"></label>
<button id="iterations-up">&#9650;</button>
<button id="iterations-dn">&#9660;</button>
<label id="resolution"></label>
<button id="resolution-up">&#9650;</button>
<button id="resolution-dn">&#9660;</button>
</div>
<div id="data"></div>
</body>
</html>