|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<meta charset="utf-8"> |
|
<style> |
|
body { |
|
margin: 0; |
|
} |
|
svg { |
|
cursor: move; |
|
} |
|
canvas { |
|
position: absolute; |
|
z-index: -1; |
|
} |
|
.sphere { |
|
fill: none; |
|
stroke: #000; |
|
stroke-width: 2px; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<script src="https://d3js.org/d3.v5.min.js"></script> |
|
<script src="https://unpkg.com/versor@0.0.4/build/versor.min.js"></script> |
|
<script src="https://unpkg.com/d3-inertia@0.0.7/build/d3-inertia.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 highp 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> |
|
// Starting rotation. |
|
var rotation = [150, 0, 0]; |
|
|
|
// Select the canvas from the document. |
|
var canvas = d3.select("body").append("canvas").node(); |
|
|
|
// 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 = "risk.jpg"; |
|
image.onload = readySoon; |
|
|
|
// SVG map declarations |
|
var svg = d3.select("body").append("svg"); |
|
var circle = svg.append("path").attr("class", "sphere").datum({type: "Sphere"}); |
|
var projection = d3.geoOrthographic().rotate(rotation); |
|
var path = d3.geoPath(projection); |
|
|
|
// Hack to ensure correct inference of window dimensions. |
|
function readySoon() { |
|
setTimeout(() => { |
|
resize(); |
|
ready(); |
|
window.onresize = () => resize(); |
|
}, 10); |
|
} |
|
|
|
function resize(){ |
|
var w = window.innerWidth, h = window.innerHeight; |
|
var width = Math.min(w, h), |
|
height = width; |
|
|
|
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); |
|
|
|
svg.attr("width", w).attr("height", h); |
|
|
|
projection |
|
.scale(width / 2) |
|
.clipAngle(90) |
|
.translate([w / 2, height / 2]) |
|
.precision(.5); |
|
|
|
circle.attr("d", path); |
|
} |
|
|
|
function ready() { |
|
// Create a texture 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 |
|
|
|
var inertia = d3.geoInertiaDrag(svg, () => redraw(true), projection); |
|
var speed = .2; |
|
var t = d3.timer(e => { |
|
var r = projection.rotate().map((d, i) => i === 0 ? d -= speed : d); |
|
projection.rotate(r); |
|
redraw(); |
|
}); |
|
|
|
function redraw(clicked) { |
|
if (clicked) t.stop(); // After the first inertia drag, stop the timer. |
|
|
|
context.uniform3fv(rotateUniform, projection.rotate()); // Three-axis rotation |
|
context.bindTexture(context.TEXTURE_2D, texture); // XXX Safari |
|
context.drawArrays(context.TRIANGLE_FAN, 0, 4); |
|
} |
|
} |
|
</script> |
|
</body> |
|
</html> |