├── .gitignore ├── FractalLayer.js ├── FractalWorker.js ├── PaletteControl.css ├── PaletteControl.js ├── Palettes.js ├── README.md ├── gh-fork-ribbon.css ├── index.html └── leaflet-hash.js /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .c9revisions -------------------------------------------------------------------------------- /FractalLayer.js: -------------------------------------------------------------------------------- 1 | L.TileLayer.FractalLayer = L.TileLayer.Canvas.extend({ 2 | options: { 3 | async: true, 4 | maxZoom:23, 5 | continuousWorld:true 6 | }, 7 | initialize: function (colorController, numWorkers, fractalType, maxIter, cr, ci) { 8 | this.fractalType = fractalType || "mandlebrot"; 9 | this.numWorkers = numWorkers; 10 | this._workers = []; 11 | this._colorController = colorController; 12 | 13 | this.messages={}; 14 | this.queue={total: numWorkers}; 15 | this.cr = cr || -0.74543; 16 | this.ci = ci || 0.11301; 17 | this.maxIter = maxIter || 500; 18 | this._paletteName = null; 19 | this._paletteSended = false; 20 | }, 21 | onAdd: function(map) { 22 | var _this = this; 23 | var i = 0; 24 | var next; 25 | this.queue.free = []; 26 | this.queue.len =0; 27 | this.queue.tiles = []; 28 | this._workers = new Array(this.numWorkers); 29 | 30 | while(i 4) { 11 | break; 12 | } 13 | x = xn; 14 | y = yn; 15 | } 16 | 17 | return iter; 18 | }, 19 | 'burningShip': function(cx, cy, maxIter) { 20 | var iter, xn, yn, x = 0, y = 0; 21 | for (iter = 0; iter < maxIter; iter++) { 22 | xn = x*x - y*y - cx; 23 | yn = 2*Math.abs(x*y) + cy; 24 | if (xn*xn + yn*yn > 4) { 25 | break; 26 | } 27 | x = xn; 28 | y = yn; 29 | } 30 | 31 | return iter; 32 | }, 33 | 'multibrot3': function(cx, cy, maxIter) { 34 | var iter, xn, yn, x = 0, y = 0; 35 | for (iter = 0; iter < maxIter; iter++) { 36 | xn=Math.pow(x,3)-3*x*Math.pow(y,2) + cx; 37 | yn=3*Math.pow(x,2)*y-Math.pow(y,3) + cy; 38 | if (xn*xn + yn*yn > 4) { 39 | break; 40 | } 41 | x = xn; 42 | y = yn; 43 | } 44 | 45 | return iter; 46 | }, 47 | 'multibrot5': function(cx, cy, maxIter) { 48 | var iter, xn, yn, x = 0, y = 0; 49 | for (iter = 0; iter < maxIter; iter++) { 50 | xn=Math.pow(x,5)-(10*Math.pow(x,3)*Math.pow(y,2))+(5*x*Math.pow(y,4)) + cx; 51 | yn=(5*Math.pow(x,4)*y)-(10*x*x*Math.pow(y,3))+Math.pow(y,5) + cy; 52 | if (xn*xn + yn*yn > 4) { 53 | break; 54 | } 55 | x = xn; 56 | y = yn; 57 | } 58 | 59 | return iter; 60 | }, 61 | 'tricorn': function(cx, cy, maxIter) { 62 | var iter, xn, yn, x = 0, y = 0; 63 | for (iter = 0; iter < maxIter; iter++) { 64 | xn = x*x - y*y - cx; 65 | yn =(x+x)*(-y) + cy; 66 | if (xn*xn + yn*yn > 4) { 67 | break; 68 | } 69 | x = xn; 70 | y = yn; 71 | } 72 | 73 | return iter; 74 | }, 75 | 'julia': function(cx, cy, maxIter, cr, ci) { 76 | var iter, xn, yn, x = cx, y = cy; 77 | for (iter = 0; iter < maxIter; iter++) { 78 | xn = x*x - y*y + cr; 79 | yn = (x*y)*2 + ci; 80 | if (xn*xn + yn*yn > 4) { 81 | break; 82 | } 83 | x = xn; 84 | y = yn; 85 | } 86 | 87 | return iter; 88 | } 89 | } 90 | 91 | var commands = { 92 | palette: function(data, cb) { 93 | palette = new Uint32Array(data.palette); 94 | }, 95 | render: function(data,cb) { 96 | if (!palette) { 97 | cb(); 98 | return; 99 | }; 100 | 101 | var scale = Math.pow(2, data.z - 1); 102 | var x0 = data.x / scale - 1; 103 | var y0 = data.y / scale - 1; 104 | var d = 1/(scale<<8); 105 | var pixels = new Array(65536); 106 | var MAX_ITER=data.maxIter; 107 | var c,cx,cy,iter,i=0,px,py; 108 | 109 | var debugIter = []; 110 | 111 | while (i < 65536) { 112 | px = i%256; 113 | py = (i-px)>>8; 114 | cx = x0 + px*d; 115 | cy = y0 + py*d; 116 | iter = fractalFunctions[data.type](cx, cy, MAX_ITER, data.cr, data.ci); 117 | pixels[i++] = palette[iter]; 118 | } 119 | var array = new Uint32Array(pixels); 120 | data.pixels = array.buffer; 121 | cb(data,[data.pixels]); 122 | } 123 | } 124 | 125 | function callBack(a,b){ 126 | self.postMessage(a,b); 127 | } 128 | 129 | self.onmessage=function(e){ 130 | var commandName = e.data.command; 131 | 132 | if (commandName in commands) { 133 | commands[commandName](e.data, callBack); 134 | } 135 | }; -------------------------------------------------------------------------------- /PaletteControl.css: -------------------------------------------------------------------------------- 1 | .fractal-palette-buttons { 2 | display: inline-block; 3 | } 4 | 5 | .fractal-palette-button { 6 | border: 2px; 7 | border-radius: 5px; 8 | } 9 | 10 | .fractal-palette-button-active { 11 | border-style: solid; 12 | border-color: rgb(140, 140, 140); 13 | background: rgb(165, 165, 165); 14 | } -------------------------------------------------------------------------------- /PaletteControl.js: -------------------------------------------------------------------------------- 1 | var PaletteControl = L.Control.extend({ 2 | options: { 3 | initPalette: 'hsv' 4 | }, 5 | initialize: function(fractalLayers, options) { 6 | L.setOptions(this, options); 7 | this._fractalLayers = fractalLayers; 8 | }, 9 | 10 | onAdd: function (map) { 11 | // create the control container with a particular class name 12 | var container = L.DomUtil.create('div', 'leaflet-control-layers leaflet-control-layers-expanded'); 13 | var _this = this; 14 | 15 | var title = document.createElement('span'); 16 | title.innerHTML = 'Palette '; 17 | 18 | var buttonsContainer = L.DomUtil.create('div', 'fractal-palette-buttons'); 19 | 20 | this._buttons = []; 21 | 22 | paletteController.forEach(function(paletteName) { 23 | var paletteButton = L.DomUtil.create('button', 'fractal-palette-button', buttonsContainer); 24 | paletteButton.innerHTML = paletteName; 25 | paletteButton.paletteName = paletteName; 26 | 27 | _this._buttons.push(paletteButton); 28 | 29 | L.DomEvent.on(paletteButton, 'click', function() { 30 | _this._update(this.paletteName); 31 | }); 32 | }) 33 | 34 | this._update(this.options.initPalette); 35 | 36 | container.appendChild(title); 37 | container.appendChild(buttonsContainer); 38 | 39 | return container; 40 | }, 41 | 42 | _update: function(activePaletteName) { 43 | this._buttons.forEach(function(button) { 44 | if (button.paletteName !== activePaletteName) { 45 | L.DomUtil.removeClass(button, 'fractal-palette-button-active'); 46 | } else { 47 | L.DomUtil.addClass(button, 'fractal-palette-button-active'); 48 | } 49 | }) 50 | 51 | for (var l in this._fractalLayers) { 52 | this._fractalLayers[l].setPalette(activePaletteName); 53 | } 54 | } 55 | }); -------------------------------------------------------------------------------- /Palettes.js: -------------------------------------------------------------------------------- 1 | var paletteController = { 2 | //each generator receives index from 0 to 1 and return RGBA values (as array of 4 numbers 0<= x <= 255) 3 | _paletteGenerators: { 4 | 'hsv': function(colorIndex) { 5 | var h = colorIndex * 360; 6 | var s = (1 - colorIndex < 1e-2) ? 0 : 0.75; 7 | //from http://schinckel.net/2012/01/10/hsv-to-rgb-in-javascript/ 8 | var v = 0.75*255; 9 | var vi = Math.floor(v); 10 | var rgb, i, data = []; 11 | if (s === 0) { 12 | rgb = [Math.floor(0.75*255), Math.floor(0.1875*255), Math.floor(0.75*255)]; 13 | } else { 14 | h = h / 60; 15 | i = Math.floor(h); 16 | data = [Math.floor(v*(1-s)), Math.floor(v*(1-s*(h-i))), Math.floor(v*(1-s*(1-(h-i))))]; 17 | switch(i) { 18 | case 0: 19 | rgb = [vi, data[2], data[0]]; 20 | break; 21 | case 1: 22 | rgb = [data[1], vi, data[0]]; 23 | break; 24 | case 2: 25 | rgb = [data[0], vi, data[2]]; 26 | break; 27 | case 3: 28 | rgb = [data[0], data[1], vi]; 29 | break; 30 | case 4: 31 | rgb = [data[2], data[0], vi]; 32 | break; 33 | default: 34 | rgb = [vi, data[0], data[1]]; 35 | break; 36 | } 37 | } 38 | rgb[3] = 255; 39 | return rgb; 40 | }, 41 | 'green': function(colorIndex) { 42 | return [0, Math.floor(colorIndex * 255), 0, 255] 43 | }, 44 | 'candy': function(colorIndex) { 45 | var i = Math.floor(colorIndex * 360) % 5 46 | switch(i) { 47 | case 0: return[30,144,255,255]; 48 | case 1: return[255,64,64,255]; 49 | case 2: return[173,255,47,255]; 50 | case 3: return[191,239,255,255]; 51 | case 4: return[154,50,205,255]; 52 | } 53 | } 54 | }, 55 | addPalette: function(paletteName, paletteGenerator) { 56 | this._paletteGenerators[paletteName] = paletteGenerator; 57 | }, 58 | getPaletteAsBuffer: function(paletteName, numIndexes) { 59 | if (!(paletteName in this._paletteGenerators)) { 60 | return; 61 | } 62 | 63 | var generator = this._paletteGenerators[paletteName]; 64 | 65 | var res = new Array(numIndexes+1); 66 | 67 | 68 | for (var c = 0; c < numIndexes+1; c++) { 69 | var color = generator(c / numIndexes); 70 | res[c] = color[0] + (color[1]<<8) + (color[2]<<16) + (color[3]<<24); 71 | } 72 | 73 | return (new Uint32Array(res)).buffer; 74 | }, 75 | forEach: function(callback) { 76 | for (var p in this._paletteGenerators) { 77 | callback(p); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This example demonstrates usage of [Leaflet](http://leafletjs.com) mapping library with HTML5 Web Workers. 2 | 3 | `L.TileLayer.Canvas` is used to visualize different types of fractals. All rendering is performed in Web Workers resulting in smooth user interaction. 4 | 5 | Many thanks to @calvinmetcalf for significant example improvements! 6 | 7 | [Check the demo here](http://aparshin.github.io/leaflet-fractal/)! -------------------------------------------------------------------------------- /gh-fork-ribbon.css: -------------------------------------------------------------------------------- 1 | /* Left will inherit from right (so we don't need to duplicate code */ 2 | .github-fork-ribbon { 3 | /* The right and left lasses determine the side we attach our banner to */ 4 | position: absolute; 5 | 6 | /* Add a bit of padding to give some substance outside the "stitching" */ 7 | padding: 2px 0; 8 | 9 | /* Set the base colour */ 10 | background-color: #a00; 11 | 12 | /* Set a gradient: transparent black at the top to almost-transparent black at the bottom */ 13 | background-image: -webkit-gradient(linear, left top, left bottom, from(rgba(0, 0, 0, 0.00)), to(rgba(0, 0, 0, 0.15))); 14 | background-image: -webkit-linear-gradient(top, rgba(0, 0, 0, 0.00), rgba(0, 0, 0, 0.15)); 15 | background-image: -moz-linear-gradient(top, rgba(0, 0, 0, 0.00), rgba(0, 0, 0, 0.15)); 16 | background-image: -o-linear-gradient(top, rgba(0, 0, 0, 0.00), rgba(0, 0, 0, 0.15)); 17 | background-image: -ms-linear-gradient(top, rgba(0, 0, 0, 0.00), rgba(0, 0, 0, 0.15)); 18 | background-image: linear-gradient(top, rgba(0, 0, 0, 0.00), rgba(0, 0, 0, 0.15)); 19 | filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#000000', EndColorStr='#000000'); 20 | 21 | /* Add a drop shadow */ 22 | -webkit-box-shadow: 0px 2px 3px 0px rgba(0, 0, 0, 0.5); 23 | box-shadow: 0px 2px 3px 0px rgba(0, 0, 0, 0.5); 24 | 25 | z-index: 9999; 26 | } 27 | 28 | .github-fork-ribbon a { 29 | /* Set the font */ 30 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 31 | font-size: 13px; 32 | font-weight: 700; 33 | color: white; 34 | 35 | /* Set the text properties */ 36 | text-decoration: none; 37 | text-shadow: 0 -1px rgba(0,0,0,0.5); 38 | text-align: center; 39 | 40 | /* Set the geometry. If you fiddle with these you'll also need to tweak the top and right values in #github-fork-ribbon. */ 41 | width: 200px; 42 | line-height: 20px; 43 | 44 | /* Set the layout properties */ 45 | display: inline-block; 46 | padding: 2px 0; 47 | 48 | /* Add "stitching" effect */ 49 | border-width: 1px 0; 50 | border-style: dotted; 51 | border-color: rgba(255,255,255,0.7); 52 | } 53 | 54 | .github-fork-ribbon-wrapper { 55 | width: 150px; 56 | height: 150px; 57 | position: absolute; 58 | overflow: hidden; 59 | top: 0; 60 | } 61 | 62 | .github-fork-ribbon-wrapper.left { 63 | left: 0; 64 | } 65 | 66 | .github-fork-ribbon-wrapper.right { 67 | right: 0; 68 | } 69 | 70 | .github-fork-ribbon-wrapper.left-bottom { 71 | position: fixed; 72 | top: inherit; 73 | bottom: 0; 74 | left: 0; 75 | } 76 | 77 | .github-fork-ribbon-wrapper.right-bottom { 78 | position: fixed; 79 | top: inherit; 80 | bottom: 0; 81 | right: 0; 82 | } 83 | 84 | .github-fork-ribbon-wrapper.right .github-fork-ribbon { 85 | top: 42px; 86 | right: -43px; 87 | 88 | /* Rotate the banner 45 degrees */ 89 | -webkit-transform: rotate(45deg); 90 | -moz-transform: rotate(45deg); 91 | -o-transform: rotate(45deg); 92 | transform: rotate(45deg); 93 | } 94 | 95 | .github-fork-ribbon-wrapper.left .github-fork-ribbon { 96 | top: 42px; 97 | left: -43px; 98 | 99 | /* Rotate the banner -45 degrees */ 100 | -webkit-transform: rotate(-45deg); 101 | -moz-transform: rotate(-45deg); 102 | -o-transform: rotate(-45deg); 103 | transform: rotate(-45deg); 104 | } 105 | 106 | 107 | .github-fork-ribbon-wrapper.left-bottom .github-fork-ribbon { 108 | top: 80px; 109 | left: -43px; 110 | 111 | /* Rotate the banner -45 degrees */ 112 | -webkit-transform: rotate(45deg); 113 | -moz-transform: rotate(45deg); 114 | -o-transform: rotate(45deg); 115 | transform: rotate(45deg); 116 | } 117 | 118 | .github-fork-ribbon-wrapper.right-bottom .github-fork-ribbon { 119 | top: 80px; 120 | right: -43px; 121 | 122 | /* Rotate the banner -45 degrees */ 123 | -webkit-transform: rotate(-45deg); 124 | -moz-transform: rotate(-45deg); 125 | -o-transform: rotate(-45deg); 126 | transform: rotate(-45deg); 127 | } 128 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leaflet TileLayer.Canvas with Web Workers example 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 24 | 25 | 26 | 27 | 28 | 29 |
30 |
31 | Fork me on GitHub 32 |
33 |
34 | 35 |
36 | 37 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /leaflet-hash.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | L.Hash = L.Class.extend({ 4 | initialize: function(map, options) { 5 | this.map = map; 6 | this.options = options || {}; 7 | if (!this.options.path) { 8 | if (this.options.lc) { 9 | this.options.path = '{base}/{z}/{lat}/{lng}'; 10 | } else { 11 | this.options.path = '{z}/{lat}/{lng}'; 12 | } 13 | } 14 | if (this.options.lc && !this.options.formatBase) { 15 | this.options.formatBase = [ 16 | /[\s\:\+\-\*\:A-Z]/g, function(match) { 17 | if (match.match(/\s/)) { 18 | return "_"; 19 | } else if (match.match(/[\:\+\-\*]/)) { 20 | return ""; 21 | } 22 | if (match.match(/[A-Z]/)) { 23 | return match.toLowerCase(); 24 | } 25 | } 26 | ]; 27 | } 28 | if (this.map._loaded) { 29 | return this.startListning(); 30 | } else { 31 | return this.map.on("load", this.startListning); 32 | } 33 | }, 34 | startListning: function() { 35 | var onHashChange, 36 | _this = this; 37 | if (location.hash) { 38 | this.updateFromState(this.parseHash(location.hash)); 39 | } 40 | if (history.pushState) { 41 | if (!location.hash) { 42 | history.replaceState.apply(history, this.formatState()); 43 | } 44 | window.onpopstate = function(event) { 45 | if (event.state) { 46 | return _this.updateFromState(event.state); 47 | } 48 | }; 49 | this.map.on("moveend", function() { 50 | var pstate; 51 | pstate = _this.formatState(); 52 | if (location.hash !== pstate[2] && !_this.moving) { 53 | return history.pushState.apply(history, pstate); 54 | } 55 | }); 56 | } else { 57 | if (!location.hash) { 58 | location.hash = this.formatState()[2]; 59 | } 60 | onHashChange = function() { 61 | var pstate; 62 | pstate = _this.formatState(); 63 | if (location.hash !== pstate[2] && !_this.moving) { 64 | return location.hash = pstate[2]; 65 | } 66 | }; 67 | this.map.on("moveend", onHashChange); 68 | if (('onhashchange' in window) && (window.documentMode === void 0 || window.documentMode > 7)) { 69 | window.onhashchange = function() { 70 | if (location.hash) { 71 | return _this.updateFromState(_this.parseHash(location.hash)); 72 | } 73 | }; 74 | } else { 75 | this.hashChangeInterval = setInterval(onHashChange, 50); 76 | } 77 | } 78 | return this.map.on("baselayerchange", function(e) { 79 | var pstate, _ref; 80 | _this.base = (_ref = _this.options.lc._layers[e.layer._leaflet_id].name).replace.apply(_ref, _this.options.formatBase); 81 | pstate = _this.formatState(); 82 | if (history.pushState) { 83 | if (location.hash !== pstate[2] && !_this.moving) { 84 | return history.pushState.apply(history, pstate); 85 | } 86 | } else { 87 | if (location.hash !== pstate[2] && !_this.moving) { 88 | return location.hash = pstate[2]; 89 | } 90 | } 91 | }); 92 | }, 93 | parseHash: function(hash) { 94 | var args, lat, latIndex, lngIndex, lon, out, path, zIndex, zoom; 95 | path = this.options.path.split("/"); 96 | zIndex = path.indexOf("{z}"); 97 | latIndex = path.indexOf("{lat}"); 98 | lngIndex = path.indexOf("{lng}"); 99 | if (hash.indexOf("#") === 0) { 100 | hash = hash.substr(1); 101 | } 102 | args = hash.split("/"); 103 | if (args.length > 2) { 104 | zoom = parseInt(args[zIndex], 10); 105 | lat = parseFloat(args[latIndex]); 106 | lon = parseFloat(args[lngIndex]); 107 | if (isNaN(zoom) || isNaN(lat) || isNaN(lon)) { 108 | return false; 109 | } else { 110 | out = { 111 | center: new L.LatLng(lat, lon), 112 | zoom: zoom 113 | }; 114 | if (args.length > 3) { 115 | out.base = args[path.indexOf("{base}")]; 116 | return out; 117 | } else { 118 | return out; 119 | } 120 | } 121 | } else { 122 | return false; 123 | } 124 | }, 125 | updateFromState: function(state) { 126 | if (this.moving) { 127 | return; 128 | } 129 | this.moving = true; 130 | this.map.setView(state.center, state.zoom); 131 | if (state.base) { 132 | this.setBase(state.base); 133 | } 134 | this.moving = false; 135 | return true; 136 | }, 137 | formatState: function() { 138 | var center, precision, state, template, zoom; 139 | center = this.map.getCenter(); 140 | zoom = this.map.getZoom(); 141 | precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2)); 142 | state = { 143 | center: center, 144 | zoom: zoom 145 | }; 146 | template = { 147 | lat: center.lat.toFixed(precision), 148 | lng: center.lng.toFixed(precision), 149 | z: zoom 150 | }; 151 | if (this.options.path.indexOf("{base}") > -1) { 152 | state.base = this.getBase(); 153 | template.base = state.base; 154 | } 155 | return [state, "a", '#' + L.Util.template(this.options.path, template)]; 156 | }, 157 | setBase: function(base) { 158 | var i, inputs, len, _ref; 159 | this.base = base; 160 | inputs = this.options.lc._form.getElementsByTagName('input'); 161 | len = inputs.length; 162 | i = 0; 163 | while (i < len) { 164 | if (inputs[i].name === 'leaflet-base-layers' && (_ref = this.options.lc._layers[inputs[i].layerId].name).replace.apply(_ref, this.options.formatBase) === base) { 165 | inputs[i].checked = true; 166 | this.options.lc._onInputClick(); 167 | return true; 168 | } 169 | i++; 170 | } 171 | }, 172 | getBase: function() { 173 | var i, inputs, len, _ref; 174 | if (this.base) { 175 | return this.base; 176 | } 177 | inputs = this.options.lc._form.getElementsByTagName('input'); 178 | len = inputs.length; 179 | i = 0; 180 | while (i < len) { 181 | if (inputs[i].name === 'leaflet-base-layers' && inputs[i].checked) { 182 | this.base = (_ref = this.options.lc._layers[inputs[i].layerId].name).replace.apply(_ref, this.options.formatBase); 183 | return this.base; 184 | } 185 | } 186 | return false; 187 | }, 188 | remove: function() { 189 | this.map.off("moveend"); 190 | if (window.onpopstate) { 191 | window.onpopstate = null; 192 | } 193 | location.hash = ""; 194 | return clearInterval(this.hashChangeInterval); 195 | } 196 | }); 197 | 198 | L.hash = function(map, options) { 199 | return new L.Hash(map, options); 200 | }; 201 | 202 | L.Map.include({ 203 | addHash: function(options) { 204 | if (this._loaded) { 205 | this._hash = L.hash(this, options); 206 | } else { 207 | this.on("load", function() { 208 | return this._hash = L.hash(this, options); 209 | }); 210 | } 211 | return this; 212 | }, 213 | removeHash: function() { 214 | this._hash.remove(); 215 | return this; 216 | } 217 | }); 218 | 219 | }).call(this); 220 | --------------------------------------------------------------------------------