Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Risk
license: gpl-3.0
height: 700
border: no

The board was designed by Derek Hakkim and used with his permission. I cropped off the borders and resized it to 2048x1024 pixels so it could be reprojected with WebGL.

This block is based on raster reprojection work by Philippe Rivière, Mike Bostock and Jason Davies.

<!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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment