Published
Edited
Oct 25, 2018
2 forks
13 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function geoWarp(targetProjection, context) {
/*
* geoWarp is designed to have a similar interface to geoPath -- it takes a
* projection and a context, and returns a function. The function can be used
* to paint an image onto a canvas context, much like how a path function can
* be used to paint GeoJSON geometries.
*
* The code has been instrumented to log some timing information so I could
* better understand what the performance bottlenecks are. You can ignore the
* various `performance.now()` calls and updates to the `stats` object; they are
* not part of the core algorithm.
*/
return function(sourceProjection, image) {
const start = performance.now();
const stats = {
pixels: { total: 0, sourced: 0, cheated: 0 },
setup: 0,
cheat: 0,
invert: 0,
project: 0,
copy: 0,
paint: 0,
unaccounted: 0,
};
let checkpoint = performance.now();
const bounds = d3.geoPath(targetProjection).bounds(sphere);
const sourceContext = DOM.canvas(image.width, image.height).getContext("2d");
sourceContext.drawImage(image, 0, 0, image.width, image.height);

const source = sourceContext.getImageData(0, 0, image.width, image.height)
const sourceData = source.data;
const dpr = window.devicePixelRatio || 1;
// TODO the dimensions of the target are hardcoded; can we get them from the context instead?
const target = context.getImageData(0, 0, width * dpr, height * dpr);
const targetData = target.data;
// make a "cheat sheet" by projecting the sphere and filling it; we can use the resulting image data
// to decide whether to invert or not.
const cheatSheetContext = DOM.context2d(width, height);
cheatSheetContext.beginPath();
d3.geoPath(targetProjection, cheatSheetContext)(sphere);
cheatSheetContext.fill();
const cheatSheetImageData = cheatSheetContext.getImageData(0, 0, width * dpr, height * dpr);
const cheatSheetData = cheatSheetImageData.data;
stats.pixels.total = targetData.length / 4;
stats.setup = performance.now() - checkpoint;
for (var y = 0; y < target.height; y += 1) {
for (var x = 0; x < target.width; x += 1) {
checkpoint = performance.now();
let i = (y * target.width + x) * 4; // compute `i` early so we can use it for the cheat sheet
const cheatPixelAlpha = cheatSheetData[i + 3];

stats.cheat += performance.now() - checkpoint;
// if any cheat pixel isn't black, it hasn't been filled when we projected the sphere, so skip it.
if (cheatPixelAlpha === 0) {
stats.pixels.cheated += 1;
continue;
}
checkpoint = performance.now();

const p = [x / dpr, y / dpr];
const s = targetProjection.invert(p);
stats.invert += performance.now() - checkpoint;
if (!s) continue;
const [λ, φ] = s;

if (λ > 180 || λ < -180 || φ > 90 || φ < -90) continue;
stats.pixels.sourced += 1;
checkpoint = performance.now();
const q = sourceProjection([λ, φ]);
const u = Math.round(q[0]),
v = Math.round(q[1]);
let now = performance.now();
stats.project += now - checkpoint;
checkpoint = now;
// let i = (y * target.width + x) * 4;
let j = (v * image.width + u) * 4;
targetData[i] = sourceData[j];
targetData[++i] = sourceData[++j];
targetData[++i] = sourceData[++j];
targetData[++i] = 255;
stats.copy += performance.now() - checkpoint;
}
}
checkpoint = performance.now();
context.putImageData(target, 0, 0);
const end = performance.now();
stats.paint += end - checkpoint;
stats.unaccounted = (end - start) - (stats.setup + stats.invert + stats.project + stats.copy + stats.paint);
mutable stats = stats;
}
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more