Skip to content

Instantly share code, notes, and snippets.

@Fil
Last active December 22, 2016 15:55
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 Fil/0d0f6a01385eb4133fe40aff6e7ad159 to your computer and use it in GitHub Desktop.
Save Fil/0d0f6a01385eb4133fe40aff6e7ad159 to your computer and use it in GitHub Desktop.
d3 Voronoi Worker
license: mit

This block implements two independent processes:

  • one that computes the new values of data (by moving them randomly around)

  • one that asks a javascript worker to compute the voronoi tesselation of the data, and draws the polygons on callback

A SVG overlay tracks the mouse: it stays responsive even if the voronoi calculation is sluggish.

We watch the performance of both processes and display their fps rates. Tweak the data range and time intervals to see how they relate.

<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<style>
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
div#fps,svg { position: fixed; top:0; left:0; color: white; }
</style>
</head>
<body>
<canvas width=960 height=500 />
<script>
const canvas = d3.select("canvas"),
width = canvas.attr('width'),
height = canvas.attr('height'),
context = canvas.node().getContext("2d"),
color = d3.scaleLinear().range(["brown", "steelblue"]);
const tracker = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.attr("pointer-events", "none")
.append('circle')
.attr("r", 10)
.attr("stroke", "white")
.attr("fill", "none");
const fpsdiv = d3.select('body').append('div').attr('id','fps');
let data = d3.range(1000)
.map(d => [Math.random()*width, Math.random()*height, Math.random()]);
let voronoi, workervoronoi;
let fps = [0,0,0],
last = performance.now();
setInterval(() => {
fps[2]++;
let now = performance.now();
fps = fps.map(d => d * Math.pow(0.9, (now-last)/1000));
//fpsdiv.html('fps: ' + fps.map(d => Math.round(d * 10 * 5 / fps[2])/10));
fpsdiv.html('FPS<br>data: ' + Math.round(fps[0] * 10 * 5 / fps[2])/10 + '<br>' + 'image: ' + Math.round(fps[1] * 10 * 5 / fps[2])/10);
last = now;
}, 200);
d3.interval(() => {
data.forEach((d,i) => {
if (i===0) return;
data[i][0] += Math.random() - Math.random();
data[i][1] += Math.random() - Math.random();
});
fps[0]++;
}, 100);
canvas.on('mousemove', function() {
data[0] = d3.mouse(this);
tracker
.attr('cx', data[0][0])
.attr('cy', data[0][1]);
});
d3.interval(draw, 100);
let radius = 10;
function draw() {
if (typeof workervoronoi == 'function' && !workervoronoi.busy) {
workervoronoi.busy = true;
workervoronoi(data, function(polygons){
polygons.forEach( (d) => {
var c = d.data[2] ? color(d.data[2]) : 'white';
context.beginPath();
context.fillStyle = c;
drawCell(d);
context.fill();
});
workervoronoi.busy = false;
fps[1]++;
});
}
}
createWorker('worker.js', (worker) => {
workervoronoi = function(data, callback){
worker.postMessage({
data: data,
extent: [[-1,-1], [width+1, height+1]],
});
worker.onmessage = function (e) {
if (e.data.polygons) {
callback(e.data.polygons);
}
}
}
});
function drawCell(cell) {
if (!cell) return false;
context.moveTo(cell[0][0], cell[0][1]);
for (var j = 1, m = cell.length; j < m; ++j) {
context.lineTo(cell[j][0], cell[j][1]);
}
context.closePath();
return true;
}
// this is for blockbuilder
function createWorker(s, cb) {
d3.queue()
.defer(d3.text, s)
.awaitAll(function (err, scripts) {
const worker = new Worker(window.URL.createObjectURL(new Blob(scripts, {
type: "text/javascript"
})));
if (typeof cb == 'function') {
cb(worker);
}
});
}
</script>
</body>
importScripts('https://d3js.org/d3.v4.min.js');
self.onmessage = function(e){
let msg = e.data;
let voronoi = d3.voronoi().extent(msg.extent);
self.postMessage({
polygons: voronoi.polygons(msg.data)
});
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment