Using WebGL, you can reproject equirectangular raster tiles onto a Canvas with an orthographic projection. You can also combine the Canvas with SVG raster tiles.
WebGL is far more performant than HTML5 Canvas. For a comparison, see Raster Reprojection I.
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.
Most of the code for this block comes from Mike Bostock, Jason Davies and Philippe Rivière.
xxxxxxxxxx
<html>
<head>
<meta charset="utf-8">
<style>
body {
margin: 0;
}
canvas {
position: absolute;
z-index: -1;
}
.country, .boundary {
fill: none;
stroke: #fff;
}
.country {
stroke-opacity: .2;
stroke-dasharray: 5, 5;
}
.boundary {
stroke-opacity: .6;
}
</style>
</head>
<body>
<canvas></canvas>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://unpkg.com/topojson@3.0.2/dist/topojson.min.js"></script>
<script id="vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;
varying vec2 pos;
void main(void) {
gl_Position = vec4(a_position, 0.0, 1.0);
pos = a_position;
}
</script>
<script id="fragment-shader" type="x-shader/x-fragment">
precision mediump float;
uniform sampler2D u_image;
uniform vec2 u_translate; /* width/2, height/2 */
uniform float u_scale; /* in pixels ! */
uniform vec3 u_rotate; /* rotation in degrees ! */
const float c_pi = 3.14159265358979323846264;
const float c_halfPi = c_pi * 0.5;
const float c_twoPi = c_pi * 2.0;
void applyRotation(in float rotatex, in float rotatey, in float rotatez,
inout float lambda, inout float phi) {
float x, y, rho, c, cosphi, z, deltaLambda, deltaPhi, deltaGamma, cosDeltaPhi,
sinDeltaPhi, cosDeltaGamma, sinDeltaGamma, k, circle, proj, a, b;
cosphi = cos(phi);
x = cos(lambda) * cosphi;
y = sin(lambda) * cosphi;
z = sin(phi);
// inverse rotation
deltaLambda = rotatex / 90.0 * c_halfPi; // rotate[0]
deltaPhi = -rotatey / 90.0 * c_halfPi; // rotate[1]
deltaGamma = -rotatez / 90.0 * c_halfPi; // rotate[2]
cosDeltaPhi = cos(deltaPhi);
sinDeltaPhi = sin(deltaPhi);
cosDeltaGamma = cos(deltaGamma);
sinDeltaGamma = sin(deltaGamma);
k = z * cosDeltaGamma - y * sinDeltaGamma;
lambda = atan(y * cosDeltaGamma + z * sinDeltaGamma,
x * cosDeltaPhi + k * sinDeltaPhi) -
deltaLambda;
k = k * cosDeltaPhi - x * sinDeltaPhi;
if (k > 0.99999)
k = 0.99999; // south pole (for some reason it goes > 1 near the pole??)
if (k < -0.99999)
k = -0.99999; // north pole
phi = asin(k);
}
void main(void) {
float x = (gl_FragCoord.x - u_translate.x) / u_scale;
float y = (u_translate.y - gl_FragCoord.y) / u_scale;
// Inverse orthographic projection
float rho = sqrt(x * x + y * y);
// Color the point (px, py) only if it exists in the texture
if (rho < 1.0) {
float c = asin(rho);
float sinc = sin(c);
float cosc = cos(c);
float lambda = atan(x * sinc, rho * cosc);
float phi = asin(y * sinc / rho);
// Apply the three-axis rotation
applyRotation(u_rotate.x, u_rotate.y, u_rotate.z, lambda, phi);
// pixels
float px = fract((lambda + c_pi) / c_twoPi);
float py = fract((phi + c_halfPi) / c_pi);
gl_FragColor = texture2D(u_image, vec2(px, py));
float intensity = 1.1; // boost the pixel by some factor
gl_FragColor[0] = intensity * gl_FragColor[0] * (1.3 - 0.3 * sqrt(gl_FragColor[0]));
gl_FragColor[1] = intensity * gl_FragColor[1];
gl_FragColor[2] = intensity * gl_FragColor[2];
}
}
</script>
<script>
// Select the canvas from the document.
var canvas = document.querySelector("canvas");
// Create the WebGL context, with fallback for experimental support.
var context = canvas.getContext("webgl")
|| canvas.getContext("experimental-webgl");
// Compile the vertex shader.
var vertexShader = context.createShader(context.VERTEX_SHADER);
context.shaderSource(vertexShader, document.querySelector("#vertex-shader").textContent);
context.compileShader(vertexShader);
if (!context.getShaderParameter(vertexShader, context.COMPILE_STATUS)) throw new Error(context.getShaderInfoLog(vertexShader));
// Compile the fragment shader.
var fragmentShader = context.createShader(context.FRAGMENT_SHADER);
context.shaderSource(fragmentShader, document.querySelector("#fragment-shader").textContent);
context.compileShader(fragmentShader);
if (!context.getShaderParameter(fragmentShader, context.COMPILE_STATUS)) throw new Error(context.getShaderInfoLog(fragmentShader));
// Link and use the program.
var program = context.createProgram();
context.attachShader(program, vertexShader);
context.attachShader(program, fragmentShader);
context.linkProgram(program);
if (!context.getProgramParameter(program, context.LINK_STATUS)) throw new Error(context.getProgramInfoLog(program));
context.useProgram(program);
// Define the positions (as vec2) of the square that covers the canvas.
var positionBuffer = context.createBuffer();
context.bindBuffer(context.ARRAY_BUFFER, positionBuffer);
context.bufferData(context.ARRAY_BUFFER, new Float32Array([
-1.0, -1.0,
+1.0, -1.0,
+1.0, +1.0,
-1.0, +1.0
]), context.STATIC_DRAW);
// Bind the position buffer to the position attribute.
var positionAttribute = context.getAttribLocation(program, "a_position");
context.enableVertexAttribArray(positionAttribute);
context.vertexAttribPointer(positionAttribute, 2, context.FLOAT, false, 0, 0);
// Extract the projection parameters.
var translateUniform = context.getUniformLocation(program, "u_translate"),
scaleUniform = context.getUniformLocation(program, "u_scale"),
rotateUniform = context.getUniformLocation(program, "u_rotate");
// Load the reference image.
var image = new Image;
image.src = "raster.jpg";
image.onload = readySoon;
// SVG map declarations
var svg = d3.select("body").append("svg");
var projection = d3.geoOrthographic();
var path = d3.geoPath(projection);
// Hack to ensure correct inference of window dimensions.
function readySoon() {
d3.json("world.json", (error, map) => {
if (error) throw error;
var feature = topojson.feature(map, map.objects.countries),
mesh = topojson.feature(map, map.objects.land);
setTimeout(() => {
resize(feature);
ready(feature, mesh);
}, 10);
self.onresize = () => resize(feature);
});
}
function resize(feature) {
var w = self.innerWidth, h = self.innerHeight;
var width = Math.min(w, h),
height = width;
// The canvas is absolutely positioned in order to overlay it with the SVG.
// To get it to center, do some math.
d3.select(canvas).style("left", ((w - width) / 2) + "px");
canvas.setAttribute("width", width);
canvas.setAttribute("height", height);
context.uniform2f(translateUniform, width / 2, height / 2);
context.uniform1f(scaleUniform, height / 2);
context.viewport(0, 0, width, height);
// Basic D3 + TopoJSON map
svg.attr("width", w).attr("height", h);
projection.fitSize([w, height], feature);
}
function ready(feature, mesh) {
// Create a texture and a mipmap for accurate minification.
var texture = context.createTexture();
context.bindTexture(context.TEXTURE_2D, texture);
context.texParameteri(context.TEXTURE_2D, context.TEXTURE_MAG_FILTER, context.LINEAR);
context.texParameteri(context.TEXTURE_2D, context.TEXTURE_MIN_FILTER, context.LINEAR_MIPMAP_LINEAR);
context.texImage2D(context.TEXTURE_2D, 0, context.RGBA, context.RGBA, context.UNSIGNED_BYTE, image);
context.texParameteri(context.TEXTURE_2D, context.TEXTURE_WRAP_S, context.CLAMP_TO_EDGE);
context.texParameteri(context.TEXTURE_2D, context.TEXTURE_WRAP_T, context.CLAMP_TO_EDGE);
context.texParameteri(context.TEXTURE_2D, context.TEXTURE_MIN_FILTER, context.LINEAR); // or NEAREST
// The current rotation and speed.
var rotate = [0, 0, 0],
speed = .5;
redraw();
// Rotate and redraw!
function redraw() {
rotate = rotate.map(d => d += speed);
context.uniform3fv(rotateUniform, rotate); // Three-axis rotation
context.bindTexture(context.TEXTURE_2D, texture); // XXX Safari
context.drawArrays(context.TRIANGLE_FAN, 0, 4);
requestAnimationFrame(redraw);
projection.rotate(rotate);
var countries = svg.selectAll(".country")
.data(feature.features);
countries.enter().append("path")
.attr("class", "country")
.merge(countries)
.attr("d", path);
var boundaries = svg.selectAll(".boundary")
.data([mesh]);
boundaries.enter().append("path")
.attr("class", "boundary")
.merge(boundaries)
.attr("d", path);
}
}
// A polyfill for requestAnimationFrame.
if (!self.requestAnimationFrame) requestAnimationFrame =
self.webkitRequestAnimationFrame
|| self.mozRequestAnimationFrame
|| self.msRequestAnimationFrame
|| self.oRequestAnimationFrame
|| function(f) { setTimeout(f, 17); };
</script>
</body>
</html>
https://d3js.org/d3.v4.min.js
https://unpkg.com/topojson@3.0.2/dist/topojson.min.js