Last active
May 13, 2016 22:37
-
-
Save christophergorexyz/b7dbda66594bf2bace73c623345393b5 to your computer and use it in GitHub Desktop.
A Mandelbrot Viewer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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