Skip to content

Instantly share code, notes, and snippets.

@christophergorexyz
Last active May 13, 2016 22:37
Show Gist options
  • Select an option

  • Save christophergorexyz/b7dbda66594bf2bace73c623345393b5 to your computer and use it in GitHub Desktop.

Select an option

Save christophergorexyz/b7dbda66594bf2bace73c623345393b5 to your computer and use it in GitHub Desktop.
A Mandelbrot Viewer
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>Mandelbrot</title>
<style>
html,
body {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
font-family: monospace;
}
#canvas {
background: transparent;
width: 100%;
height: 100%;
cursor: zoom-in;
}
#controls {
position: absolute;
background: White;
padding: 1rem;
left: 50px;
top: 50px;
}
#set-location {
display: none;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<div id="controls">
<div>
<a id="get-png" href="#" target="_blank">GET PNG</a>
</div>
<div>
<button onclick="zoomIn();">+</button>
<button onclick="zoomOut();">-</button>
<button onclick="reset();">Reset</button>
</div>
<div id="status"></div>
<div>
<button onclick="save();">Save</button>
<button onclick="showSetLocation();">Set Location</button>
</div>
<div id="set-location">
<div>
<label for="scale">Scale</label>
<input id="scale" type="text" />
</div>
<div>
<label for="x-location">X</label>
<input id="x-location" type="text" />
</div>
<div>
<label for="y-location">Y</label>
<input id="y-location" type="text" />
</div>
<button onclick="setLocation();">go</button>
</div>
<div id="saved-list"></div>
</div>
<script type="text/javascript">
//black is used if the pixel is ~approximately~ in the set.
var _black = {
r: 0,
g: 0,
b: 0
};
/*
* the following loop was
* modified from rainbowify
* (https://github.com/maxogden/rainbowify)
* which lifted from mocha
* (https://github.com/visionmedia/mocha/blob/master/lib/reporters/nyan.js)
* to generate the color palette
*/
var _palette = [];
for (var i = 0; i < (6 * 7); i++) {
var pi3 = Math.floor(Math.PI / 3);
var n = (i * (1.0 / 6));
var r = Math.floor(3 * Math.sin(n) + 3) * 255 / 5;
var g = Math.floor(3 * Math.sin(n + 2 * pi3) + 3) * 255 / 5;
var b = Math.floor(3 * Math.sin(n + 4 * pi3) + 3) * 255 / 5;
_palette.push({
r: r,
g: g,
b: b
});
}
var _status = document.getElementById('status');
var _canvas = document.getElementById('canvas');
var _savedList = document.getElementById('saved-list');
var _context = _canvas.getContext('2d');
var savedData = window.localStorage.getItem('locations');
var savedLocations = [];
if (savedData) {
savedLocations = JSON.parse(savedData);
updateSavedList();
}
var _imageData;
var _data;
var xStep;
var yStep;
var pMax;
var pMin;
var qMax;
var qMin;
//The bounds of the mandelbrot set
var initLeftEdge = leftEdge = -2.5;
var initRightEdge = rightEdge = 1;
var initTopEdge = topEdge = -1;
var initBottomEdge = bottomEdge = 1;
var _horizontalOffset = initRightEdge - ((initRightEdge - initLeftEdge) / 2);
var _viewData = {
scale: 1,
x: _horizontalOffset,
y: 0
}; //the current center of the image
var _mandelRatio = (initRightEdge - initLeftEdge) / (initBottomEdge - initTopEdge);
var _imageRatio = _canvas.width / _canvas.height;
var _maxIterations = 1000;
//zoom and displacement
function renderScene(zoom, dx, dy) {
var zoom = zoom || _viewData.scale;
var dx = dx || _viewData.x - _horizontalOffset / zoom;
var dy = dy || _viewData.y;
if (_imageRatio > _mandelRatio) {
var percentage = (_imageRatio / _mandelRatio);
var difference = (initRightEdge - initLeftEdge) * percentage;
//TODO: this shouldn't work
leftEdge = -difference / (2 * (2.5 / 3.5));
rightEdge = difference / (2 * (3.5 / 2.5));
topEdge = initTopEdge;
bottomEdge = initBottomEdge;
} else if (_imageRatio < _mandelRatio) {
var percentage = (_mandelRatio / _imageRatio);
var difference = (initBottomEdge - initTopEdge) * percentage;
topEdge = -difference / 2;
bottomEdge = difference / 2;
leftEdge = initLeftEdge;
rightEdge = initRightEdge;
}
pMax = rightEdge / zoom + dx;
pMin = leftEdge / zoom + dx;
qMax = bottomEdge / zoom + dy;
qMin = topEdge / zoom + dy;
xStep = (pMax - pMin) / _imageData.width;
yStep = (qMax - qMin) / _imageData.height;
//An implementation of the Escape Time Algorithm
//https://en.wikipedia.org/wiki/Mandelbrot_set#Escape_time_algorithm
for (var py = 0; py < _imageData.height; py++) {
for (var px = 0; px < _imageData.width; px++) {
//the canvas pixel data is a bit awkward to get at...
//see: https://www.w3.org/TR/2dcontext/#pixel-manipulation
var dataIndex = (py * _imageData.width + px) * 4;
//scale the pixel values to frame the bounds of the set
var x0 = pMin + xStep * px;
var y0 = qMin + yStep * py;
var x = 0.0;
var y = 0.0;
var iteration = 0;
while (x * x + y * y < 2 * 2 && iteration < _maxIterations) {
var tempX = x * x - y * y + x0;
y = 2 * x * y + y0;
x = tempX;
iteration++;
}
//if we've maxed out our iterations, it's a close
//enough approximation, so color it black
var color = iteration === _maxIterations ? _black : _palette[iteration % _palette.length];
_data[dataIndex] = color.r;
_data[dataIndex + 1] = color.g;
_data[dataIndex + 2] = color.b;
_data[dataIndex + 3] = 255;
}
}
//draw it!
_context.putImageData(_imageData, 0, 0);
_writeStatus();
document.getElementById('get-png').href = _canvas.toDataURL('image/png');
}
_canvas.addEventListener('click', function(e) {
//need to translate clicked (x, y) to viewframe's (x, y)
var scale = _viewData.scale;
var px = e.layerX;
var py = e.layerY;
//scale the pixel values to frame the bounds of the set
var x0 = pMin + xStep * px;
var y0 = qMin + yStep * py;
_viewData.scale *= 2;
_viewData.x = x0;
_viewData.y = y0;
renderScene();
});
function _handleWindowResize() {
_canvas.width = window.innerWidth;
_canvas.height = window.innerHeight;
_imageData = _context.createImageData(_canvas.width, _canvas.height);
_imageRatio = _imageData.width / _imageData.height;
_data = _imageData.data;
renderScene();
}
function zoomIn() {
_viewData.scale *= 2;
renderScene();
}
function zoomOut() {
_viewData.scale /= 2;
renderScene();
}
function reset() {
_viewData.scale = 1;
_viewData.x = -0.75;
_viewData.y = 0;
renderScene();
}
function save() {
_viewData.name = 'location ' + (savedLocations.length + 1);
savedLocations.push(_viewData);
window.localStorage.setItem('locations', JSON.stringify(savedLocations));
}
function load(l) {
_viewData.x = l.x;
_viewData.y = l.y;
_viewData.scale = l.scale;
renderScene();
}
function showSetLocation() {
document.getElementById('set-location').style.display = 'block';
}
function setLocation() {
var scaleElement = document.getElementById('scale');
var xElement = document.getElementById('x-location');
var yElement = document.getElementById('y-location');
var location = {};
location.scale = parseFloat(scaleElement.value);
location.x = parseFloat(xElement.value);
location.y = parseFloat(yElement.value);
scaleElement.value = '';
xElement.value = '';
yElement.value = '';
document.getElementById('set-location').style.display = 'none';
load(location);
}
function updateSavedList() {
for (var l in savedLocations) {
var linkwrapper = document.createElement('div');
var anchor = document.createElement('a');
anchor.onclick = function(e) {
e.preventDefault();
load(savedLocations[l]);
};
anchor.href = '#';
anchor.innerHTML = savedLocations[l].name;
linkwrapper.appendChild(anchor);
_savedList.appendChild(linkwrapper);
}
}
function _writeStatus() {
var zoomText = document.createElement('div');
zoomText.textContent = 'Scale: ' + _viewData.scale;
var xText = document.createElement('div');
xText.textContent = 'x0: ' + _viewData.x;
var yText = document.createElement('div');
yText.textContent = 'y0: ' + _viewData.y;
_status.innerHTML = '';
_status.appendChild(zoomText);
_status.appendChild(xText);
_status.appendChild(yText);
}
window.onresize = _handleWindowResize;
_handleWindowResize();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment