Skip to content

Instantly share code, notes, and snippets.

@fedelebron
Created July 21, 2009 07:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fedelebron/151184 to your computer and use it in GitHub Desktop.
Save fedelebron/151184 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html>
<head>
<title>Mandelbrot in JS + Canvas</title>
<meta charset="UTF-8">
<style type="text/css">
#centered {
margin-left: 35%;
margin-top: 10%;
width: 430px;
}
#canvas {
cursor: crosshair;
}
#cover {
position: absolute;
width: 100%;
height: 100%;
top: 0px;
left: 0px;
opacity: 0.5;
background-color: #000000;
z-index: 2;
cursor: wait;
}
</style>
</head>
<body>
<div id="centered">
<canvas id="canvas" width="400" height="300"></canvas>
</div>
<div id="cover">&nbsp;</div>
<script>
var MandelbrotMaker = function(canvas, displacement, zoom) {
this.canvas = canvas === undefined ? document.getElementById('canvas') : canvas;
if(this.canvas === null) {
throw "Canvas not found.";
}
this.displacement = displacement === undefined ? [0, 0] : displacement; // 0 <= x, y <= 1 (try [-0.713, 0.278])
this.stretch = zoom === undefined ? [1,1] : [zoom, zoom]; // 0 < x, y (try [1500, 1500])
this.iterations = 100; // 0 < X
this.convergence_radius = 4;
this.escape_threshold = 20;
this.pallete_size = 2<<8;
this.starting_color = [157,21,21]; // leave null for random bicolor gradient
this.ending_color = [8,192,68];
this.init();
};
MandelbrotMaker.prototype.init = function() {
this.convergence_radius_squared = this.convergence_radius*this.convergence_radius;
this.ctx = this.canvas.getContext('2d');
this.width = this.canvas.width;
this.height = this.canvas.height;
// this serves as a buffer, we don't want to be repainting
// on each pixel calculation
this.canvasData = this.ctx.createImageData(this.width, this.height);
};
MandelbrotMaker.prototype.escapeTime = function(c, d) {
var a = 0, b = 0, ta /* temp a*/, aa, bb, modulus;
for(var i = 0; i < this.iterations; i++) {
aa = a*a;
bb = b*b;
ta = aa - bb + c;
b = 2*a*b + d;
a = ta;
modulus = aa+bb;
if(modulus > this.convergence_radius_squared) {
return [i, modulus]; // point exploded after i iterations
}
}
return [Infinity, modulus]; // point never explodes; it's a member of the set
};
MandelbrotMaker.prototype.generatePainter = function() {
var mrand = function(a, b) {
return (Math.random() * b + a) % b;
};
var table = [];
if(this.starting_color === null && this.ending_color === null) {
var ll = [mrand(0, 255), mrand(0, 255), mrand(0,255)]; // pick two random colors
var ul = [mrand(0, 255), mrand(0, 255), mrand(0,255)]; // to start and end the degrade
} else {
var ll = this.starting_color;
var ul = this.ending_color;
}
// how much should each component increase with each iteration?
var increases = [(ul[0] - ll[0])/this.pallete_size, (ul[1] - ll[1])/this.pallete_size, (ul[2] - ll[2])/this.pallete_size];
for(var i = 0; i < this.pallete_size; i++) {
table[i] = [Math.floor(ll[0] + i*increases[0]), Math.floor(ll[1] + i*increases[1]), Math.floor(ll[2]+i*increases[2])];
}
var lnP = Math.log(2);
var tlnB = 2*Math.log(this.escape_threshold);
var getColorId = function(iterations, value) {
return iterations + Math.log(tlnB - Math.log(Math.log(value)))/lnP;
};
var that = this; // hack: we lose "this" when we create the generator function;
// "that" now refers to the MandelbrotMaker object
return function(iterations, value) {
var index = Math.floor((getColorId(iterations, value))*(that.pallete_size/100))-2;
if(index < 0) { // gorgeous, -1%4 = -1, to JS. son of a...
index = 0;
}
if(table[index] === undefined) {
return [0,0,0];
}
return table[index];
};
};
MandelbrotMaker.prototype.setColor = function(pixel, color) {
this.canvasData.data[pixel+0] = color[0];
this.canvasData.data[pixel+1] = color[1];
this.canvasData.data[pixel+2] = color[2];
this.canvasData.data[pixel+3] = 255; // RGBA, we want full opacity
};
MandelbrotMaker.prototype.coverPage = function(callback) {
document.getElementById('cover').style.display = '';
setTimeout(callback, 1);
};
MandelbrotMaker.prototype.uncoverPage = function() {
document.getElementById('cover').style.display = 'none';
document.getElementById('canvas').style.cursor = 'crosshair';
};
MandelbrotMaker.prototype.getDisplacement = function() {
return this.displacement;
};
MandelbrotMaker.prototype.getZoom = function() {
return this.stretch[0];
};
MandelbrotMaker.prototype.paint = function() {
var that = this;
this.coverPage(function() {
var painter = that.generatePainter();
var time, value, temp, color, pixel;
var black = [0,0,0];
for(var x_0 = -(that.width/2); x_0 < that.width/2; x_0++) {
for(var y_0 = -(that.height/2); y_0 < that.height/2; y_0++) {
// time it takes for the function to escape the origin
temp = that.escapeTime(x_0/(100*that.stretch[0]) + that.displacement[0], y_0/(100*that.stretch[1]) + that.displacement[1]);
time = temp[0];
value = temp[1];
if(time === Infinity) { // point never escaped, it's a member of the set
color = black;
} else {
color = painter(time, value); // point exploded after time iterations, and its modulus was value
}
// translate the pixel's location to a point within the canvas
pixel = (that.width/2+x_0 + (that.height/2+y_0) * that.width) * 4;
that.setColor(pixel, color);
}
}
// put buffer back on canvas
that.ctx.putImageData(that.canvasData, 0, 0);
that.uncoverPage();
});
};
document.addEventListener('DOMContentLoaded', function() {
var canvas = document.getElementById('canvas');
var maker = new MandelbrotMaker(canvas, [0, 0], 1);
maker.paint();
canvas.addEventListener('click', (function(maker){
var m = maker;
return function(e) {
/*e.pageX, e.pageY give us the click coords relative to the page */
var rect = document.getElementById('canvas').getBoundingClientRect();
var coords = [e.pageX - rect.left, e.pageY - rect.top];
var displacement = [(coords[0]-canvas.width/2)/rect.width, (coords[1]-canvas.height/2)/rect.height];
var currentZoom = m.getZoom();
var currentDisplacement = m.getDisplacement();
m = new MandelbrotMaker(canvas, [currentDisplacement[0] + displacement[0]/(currentZoom/2), currentDisplacement[1] + displacement[1]/(currentZoom/2)], currentZoom*1.75);
m.paint();
}
})(maker), false);
}, true);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment