Published
Edited
Jul 7, 2021
3 forks
9 stars
Insert cell
Insert cell
Insert cell
projection = d3.geoOrthographic().rotate([-120, -30, 0])
Insert cell
Insert cell
Insert cell
Insert cell
import { zoom } from "@d3/versor-zooming"
Insert cell
Insert cell
maxTextureSize = {
const gl = document.createElement("canvas").getContext("webgl");
return gl.getParameter(gl.MAX_TEXTURE_SIZE);
}
Insert cell
image = d3.image(
"https://upload.wikimedia.org/wikipedia/commons/f/f7/Normal_Mercator_map_85deg.jpg",
{
crossOrigin: "anonymous"
}
)
Insert cell
// the image must be "power of 2" to use a mipmap
imageP2 = {
const w = Math.min(
maxTextureSize,
2 ** Math.ceil(Math.log(image.naturalWidth) / Math.log(2))
), // better upscale if possible
h = w / 1;
const context = DOM.context2d(w, h, 1);
context.drawImage(image, 0, 0, w, h);

return Object.assign(context.canvas, { style: "max-height: 100px;" });
}
Insert cell
Insert cell
function mapShader(projection, glsl, postprojection, footer) {
const pixelRatio = Math.min(1.5, devicePixelRatio);

const canvas = document.createElement("canvas");
canvas.width = Math.floor(width * pixelRatio);
canvas.height = Math.floor(height * pixelRatio);
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;

const regl = createREGL({
canvas,
attributes: { antialias: false, preserveDrawingBuffer: true },
optionalExtensions: ['OES_standard_derivatives', 'EXT_shader_texture_lod']
});

const texture = regl.texture({
data: imageP2,
mipmap: "nice",
min: "linear mipmap linear",
mag: "linear",
wrapS: "repeat",
flipY: true
});

const draw = createDrawCommand(
regl,
projection,
glsl,
postprojection,
footer,
width,
height,
texture
);

return Object.assign(canvas, { regl, draw });
}
Insert cell
createREGL = require("regl")
Insert cell
createDrawCommand = (
regl,
projection,
proj,
postprojection,
footer,
width,
height,
texture
) =>
regl({
frag: fragmentShader(proj, postprojection, footer),

vert: vertexShader(),

attributes: {
position: [-4, 0, 0, -4, 4, 4]
},
count: 3,

uniforms: {
texture,
u_scale: () => projection.scale(),
u_angle: () => projection.angle(),
u_translate: () => {
// accounts for projection.center()
const r = projection.rotate(),
t = projection.rotate([0, 0])([0, 0]);
projection.rotate(r);
return t;
},
u_rotate: () => projection.rotate(),
u_size: () => [width, height]
},

depth: { enable: false }
})
Insert cell
Insert cell
fragmentShader = (projection, postprojection, footer) => `

#ifdef GL_EXT_shader_texture_lod
#extension GL_EXT_shader_texture_lod : enable
#endif

#ifdef GL_OES_standard_derivatives
#extension GL_OES_standard_derivatives : enable
#endif

precision highp float;
uniform sampler2D texture;
uniform vec3 u_rotate;

const float pi = 3.141592653589793;
const float halfPi = pi * 0.5;
const float tau = pi * 2.0;
const float radians = pi / 180.0;
const float degrees = 1.0 / radians;

varying vec2 p;

${applyRotation()}

void main(void) {
float x = p.x;
float y = p.y;
float lambda, phi;

bool transparent = false;

// inverse projection
${projection}

// rotate
applyRotation(u_rotate, lambda, phi);

// texture coordinates
vec2 t = vec2(fract(lambda / tau - 0.5), 0.5 + phi / pi);

#ifdef GL_OES_standard_derivatives
// avoid a mipmap seam by controlling the derivative of t.x
if (fwidth(t.x) > 0.25 && t.x < 0.5) t.x += 1.0;
#endif

${postprojection}

// read the textures
#ifdef GL_EXT_shader_texture_lod
float scale = 0.8 * cos(phi * 0.97);
gl_FragColor = texture2DGradEXT(texture, vec2(t.x, t.y * 0.996 + 0.002), dFdx(t) * scale, dFdy(t) * scale);
#else
gl_FragColor = texture2D(texture, t);
#endif

if (transparent) gl_FragColor.a = 0.0;

${footer || "// no footer"}
}
`
Insert cell
Insert cell
vertexShader = () => `
precision highp float;
uniform float u_scale;
uniform float u_angle;
uniform vec2 u_translate;
uniform vec2 u_size;
attribute vec2 position;
varying vec2 p;

vec2 rotate2d(vec2 p, float a) {
float s = sin(a), c = cos(a);
return mat2(c, -s, s, c) * p;
}

void main () {
p = u_translate - 0.5 * u_size * (position * vec2(1, -1) + 1.0);
p = rotate2d(p, -u_angle * 0.017453292519943295) / u_scale * vec2(-1, 1);
gl_Position = vec4(position, 0, 1);
}`
Insert cell
Insert cell
applyRotation = () => `

// rotations, ported from d3-geo

void applyRotation(in vec3 rotate, 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);

deltaLambda = rotate.x * radians;
deltaPhi = rotate.y * radians;
deltaGamma = rotate.z * radians;

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;

k = clamp(k, -1.0, 1.0); // avoid a hole at the poles
phi = asin(k);
}
`
Insert cell
Insert cell
height = {
// fullscreen?
if (width > 1000) return window.screen ? window.screen.height : width * .75;

const [[x0, y0], [x1, y1]] = d3
.geoPath(projection.fitWidth(width, { type: "Sphere" }))
.bounds({ type: "Sphere" });
const dy = Math.ceil(y1 - y0),
l = Math.min(Math.ceil(x1 - x0), dy);
projection.scale((projection.scale() * (l - 1)) / l).precision(0.2);
return dy;
}
Insert cell
projection.fitExtent([[10, 10], [width - 10, height - 10]], { type: "Sphere" })
Insert cell
d3 = require("d3@7", "d3-geo-projection@3")
Insert cell
import { checkbox, select } from "@jashkenas/inputs"
Insert cell
import {fullscreen} from "@fil/fullscreen"
Insert cell
fullscreen(map)
Insert cell
import { radians } from "@fil/math"
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more