function geoWarp(targetProjection, context) {
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;
}
}