Skip to content

Instantly share code, notes, and snippets.

@HarryStevens
Last active November 26, 2019 01:24
Show Gist options
  • Save HarryStevens/676a9e3d5681365045197238cdc1ba8b to your computer and use it in GitHub Desktop.
Save HarryStevens/676a9e3d5681365045197238cdc1ba8b to your computer and use it in GitHub Desktop.
Raster Reprojection I
license: gpl-3.0

You can reproject equirectangular raster tiles onto a Canvas with an orthographic projection. You can also combine the Canvas with SVG raster tiles.

Toggle the checkbox to see what it looks like with and without a clipping mask.

You can achieve significant performance improvements with WebGL. See Raster Reprojection II for a demonstration.

The image is from Wikimedia Commons and described thus:

Heightmap of Earth's surface (including water and ice) in equirectangular projection, normalized as 8-bit grayscale, where lighter values indicate higher elevation. Sea level is shown as #0c0c0c.

Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
<!DOCTYPE html>
<html>
<head>
<style>
body {
margin: 0;
}
.boundary, .polygon {
fill: none;
stroke: white;
}
.boundary {
stroke-opacity: .6;
}
.polygon {
stroke-opacity: .2;
stroke-dasharray: 5, 5;
}
svg, canvas, div {
position: absolute;
}
div {
z-index: 1;
background: rgba(255, 255, 255, .8);
padding: 6px 12px 6px 4px;
font-family: "Helvetica Neue", sans-serif;
font-size: .9em;
}
.mask {
display: none;
}
.mask.show {
display: block;
}
</style>
</head>
<body>
<div><input type="checkbox" checked />Mask</div>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://unpkg.com/topojson@3"></script>
<script>
var width = window.innerWidth, height = window.innerHeight;
var canvas = d3.select("body").append("canvas").attr("width", width).attr("height", height);
var svg = d3.select("body").append("svg").attr("width", width).attr("height", height);
var context = canvas.node().getContext("2d");
var projection = d3.geoOrthographic();
var path = d3.geoPath().projection(projection);
var mask = svg.append("defs")
.append("mask")
.attr("id", "hole");
var mask_rect = mask.append("rect").style("fill", "white").attr("width", width).attr("height", height);
var mask_circle = mask.append("path").datum({type: "Sphere"}).style("fill", "black");
var rect = svg.append("rect").attr("class", "mask").attr("mask", "url(#hole)").style("fill", "white").attr("width", width).attr("height", height);
d3.json("countries.json", (error, world) => {
if (error) throw error;
var mesh = topojson.mesh(world, world.objects.land, (a, b) => a === b);
var feature = topojson.feature(world, world.objects.countries);
projection.fitSize([width, height], mesh);
mask_circle.attr("d", path);
var image = new Image;
image.src = "raster.jpg";
image.onload = () => draw();
window.onresize = () => {
width = window.innerWidth, height = window.innerHeight;
projection.fitSize([width, height], mesh);
canvas.attr("width", width).attr("height", height);
svg.attr("width", width).attr("height", height);
mask_rect.attr("width", width).attr("height", height);
mask_circle.attr("d", path);
rect.attr("width", width).attr("height", height);
draw();
};
d3.timer((t) => {
projection.rotate([t / 90, t / 90, t / 90]);
draw();
});
rect.classed("show", true);
d3.select("input").on("click", () => {
rect.classed("show", d3.select("input").property("checked"));
});
function draw(){
// See: https://bl.ocks.org/mbostock/4329423
context.drawImage(image, 0, 0, width, height);
var sourceData = context.getImageData(0, 0, width, height).data,
target = context.createImageData(width, height),
targetData = target.data;
for (var y = 0, i = -1; y < height; ++y) {
for (var x = 0; x < width; ++x) {
var p = projection.invert([x, y]), lambda = p[0], phi = p[1];
if (lambda > 180 || lambda < -180 || phi > 90 || phi < -90) { i += 4; continue; }
var q = ((90 - phi) / 180 * height | 0) * width + ((180 + lambda) / 360 * width | 0) << 2;
targetData[++i] = sourceData[q];
targetData[++i] = sourceData[++q];
targetData[++i] = sourceData[++q];
targetData[++i] = 255;
}
}
context.putImageData(target, 0, 0);
var polygons = svg.selectAll(".polygon")
.data(feature.features);
polygons.enter().append("path")
.attr("class", "polygon")
.merge(polygons)
.attr("d", path);
var boundary = svg.selectAll(".boundary")
.data([mesh]);
boundary.enter().append("path")
.attr("class", "boundary")
.merge(boundary)
.attr("clip-path", "url(#clip)")
.attr("d", path);
}
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment