|
<!DOCTYPE html> |
|
<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> |