Skip to content

Commit

Permalink
first attempt to use esmodules
Browse files Browse the repository at this point in the history
  • Loading branch information
markrian committed Oct 2, 2018
1 parent 2f72695 commit 843a4fe
Show file tree
Hide file tree
Showing 7 changed files with 288 additions and 283 deletions.
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

<body>
<div id="fractal"></div>
<script src="index.js"></script>
<script src="index.js" type="module"></script>
</body>

</html>
285 changes: 3 additions & 282 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,290 +1,11 @@
/**
* Mandelbrot Layer/Renderer
*/
function Job(message) {
let _resolve, _reject;
const promise = new Promise((resolve, reject) => {
_resolve = resolve;
_reject = reject;
});
import L from './leaflet-shim.js';
import MandelbrotLayer from './mandelbrot-layer.js';

const id = Job.id++;
return {
id,
promise,
resolve: _resolve,
reject: _reject,
message: Object.assign({}, { id }, message),
posted: false,
};
}
Job.id = 0;

class MandelbrotRenderer {
constructor(numWorkers) {
this._jobs = new Map();
this._workers = [];
const boundOnMessage = this.onMessage.bind(this);
while (numWorkers--) {
const worker = new Worker('worker.js');
worker.onmessage = boundOnMessage;
worker.idle = true;
this._workers.push(worker);
}
}

getImageData(coords, tileSize, iterations, zoom) {
const job = Job({ coords, tileSize, iterations, zoom });
this._jobs.set(job.id, job);

// try to send message to idle worker, if any
for (const worker of this._workers) {
if (worker.idle) {
this.postJob(job, worker);
break;
}
}

return job.promise;
}

postJob(job, worker) {
if (!worker.idle) {
throw new Error('tried to post message to non-idle worker');
}
worker.idle = false;
job.posted = true;
worker.postMessage(job.message);
}

onMessage(event) {
const worker = event.target;
worker.idle = true;

// resolve deferred
const job = this._jobs.get(event.data.id);
if (job !== undefined) {
job.resolve(event.data.imageData);
this._jobs.delete(event.data.id);
}

// post new message if any deferreds still present
const nextJob = this.getNextJob();
if (nextJob !== undefined) {
this.postJob(nextJob, worker);
}
}

getNextJob() {
for (const [id, job] of this._jobs) {
if (!job.posted) {
return job;
}
}
}

clearJobs(predicate) {
if (predicate === undefined) {
this._jobs.clear();
return;
}
const toRemove = [];
for (const [id, job] of this._jobs) {
if (!job.posted && predicate(job)) {
toRemove.push(id);
}
}
toRemove.forEach(id => this._jobs.delete(id));
}
}

L.GridLayer.MandelbrotLayer = L.GridLayer.extend({
options: {
iterations: 64,
workers: 4,
updateWhenIdle: true,
updateWhenZooming: false,
},

initialize(options) {
L.Util.setOptions(this, options);
this._renderer = new MandelbrotRenderer(this.options.workers);
},

_multIterations(factor) {
this._renderer.clearJobs();
this.options.iterations = Math.round(this.options.iterations * factor);
this.redraw();
this._map.fire('iterationschange', { value: this.options.iterations });
},

increaseIterations() {
this._multIterations(2);
},

decreaseIterations() {
this._multIterations(.5);
},

onAdd(map) {
L.GridLayer.prototype.onAdd.apply(this, arguments);
map._mandelbrotLayer = this;
map.on('zoom', () => {
const zoom = map.getZoom();
this._renderer.clearJobs(job => job.message.zoom !== zoom);
});
map.fire('iterationschange', { value: this.options.iterations });
},

createTile(coords, done) {
const tileSize = this.getTileSize();
const tile = document.createElement('canvas');
tile.width = tileSize.x;
tile.height = tileSize.y;
const ctx = tile.getContext('2d');

const zoomFactor = Math.pow(2, -coords.z);
const complexBounds = {
realMin: coords.x * zoomFactor,
imagMin: coords.y * zoomFactor,
realMax: (coords.x + 1) * zoomFactor,
imagMax: (coords.y + 1) * zoomFactor,
};

this._renderer.getImageData(complexBounds, tileSize, this.options.iterations, coords.z)
.then(imageData => {
ctx.putImageData(imageData, 0, 0);
done(null, tile);
})
.catch(error => {
console.error(error);
done(error);
});

return tile;
},
});

L.Map.include({
getIterations() {
return this._mandelbrotLayer.options.iterations;
},

increaseIterations() {
this._mandelbrotLayer.increaseIterations();
},

decreaseIterations() {
this._mandelbrotLayer.decreaseIterations();
},
});


/**
* Iterations Control
*/
L.Control.Iterations = L.Control.extend({
options: {
position: 'bottomright',
iterationsTitle: 'The current number of iterations',
decreaseIterationsTitle: 'Decrease the number of iterations',
increaseIterationsTitle: 'Increase the number of iterations',
iterationsHeaderTitle: 'The current number of iterations',
increaseIterationsText: '+',
decreaseIterationsText: '−',
},

onAdd() {
const name = 'mandelbrot-iterations';
const container = L.DomUtil.create('div', name + ' leaflet-bar');

this._iterationsHeader = this._createHeader(
'',
this.options.iterationsTitle,
name + '-header',
container,
);

const buttonsContainer = L.DomUtil.create('div', name + '-controls', container)
this._increaseIterationsButton = this._createButton(
this.options.increaseIterationsText,
this.options.increaseIterationsTitle,
name + '-increase',
buttonsContainer,
this._increaseIterations
);
this._decreaseIterationsButton = this._createButton(
this.options.decreaseIterationsText,
this.options.decreaseIterationsTitle,
name + '-decrease',
buttonsContainer,
this._decreaseIterations
);

this._map.on('iterationschange', this._updateIterationsHeader, this);

return container;
},

_createHeader: function (html, title, className, container, fn) {
var header = L.DomUtil.create('span', className, container);
header.innerHTML = html;
header.title = title;
header.setAttribute('aria-label', title);
L.DomEvent.disableClickPropagation(header);
return header;
},

_createButton: function (html, title, className, container, fn) {
var link = L.DomUtil.create('a', className, container);
link.innerHTML = html;
link.href = '#';
link.title = title;

link.setAttribute('role', 'button');
link.setAttribute('aria-label', title);

L.DomEvent.disableClickPropagation(link);
L.DomEvent.on(link, 'click', L.DomEvent.stop);
L.DomEvent.on(link, 'click', fn, this);
L.DomEvent.on(link, 'click', this._refocusOnMap, this);

return link;
},

_increaseIterations() {
this._map.increaseIterations();
},

_decreaseIterations() {
this._map.decreaseIterations();
},

_updateIterationsHeader(e) {
this._iterationsHeader.textContent = String(e.value);
}
});

L.Map.mergeOptions({
iterationsControl: false,
});

L.Map.addInitHook(function () {
if (this.options.iterationsControl) {
this._iterationsControl = new L.Control.Iterations();
this.addControl(this._iterationsControl);
}
});


/**
* Entry point
*/
const map = L.map('fractal', {
center: [0, 0],
zoom: 0,
crs: L.CRS.Simple,
iterationsControl: true,
});

map.addLayer(new L.GridLayer.MandelbrotLayer());
map.addLayer(new MandelbrotLayer());
18 changes: 18 additions & 0 deletions job.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export default function Job(message) {
let _resolve, _reject;
const promise = new Promise((resolve, reject) => {
_resolve = resolve;
_reject = reject;
});

const id = Job.id++;
return {
id,
promise,
resolve: _resolve,
reject: _reject,
message: Object.assign({}, { id }, message),
posted: false,
};
}
Job.id = 0;
9 changes: 9 additions & 0 deletions leaflet-shim.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export default const L = getLeaflet();

function getLeaflet() {
if ('L' in window) {
return window.L;
}

throw new Error("Leaflet hasn't been loaded!");
}
Loading

0 comments on commit 843a4fe

Please sign in to comment.