Published
Edited
Apr 5, 2020
67 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
videoElement = html`<video style="display: block; height: 150px;" crossorigin=anonymous controls autoplay muted loop>
<source src="https://observable-demo-misc.s3-us-west-1.amazonaws.com/videos/Australia_fires_smoke-1080p-opt-frames.mp4"/>
</video>`
Insert cell
html`<style>
input[type=button] {
font-size: 16px;
}
.legend {
text-align: center;
}
.legend svg {
margin: 0 auto -8px;
}
.legend svg:last-child {
margin-bottom: 0;
padding-bottom: 5px;
}
.legend text {
font-size: 12px;
font-weight: 500;
}
.legend .tick text {
font-size: 11px;
text-anchor: end;
}
.legend .tick:first-child text {
text-anchor: start;
}
.legend .tick line {
stroke: none;
}
</style>`
Insert cell
function aerosolLegend(title, c0, c1, c2, ticks) {
return legend({
color: d3.scaleSequential(
d3
.scaleLinear()
.domain([0, 0.5, 1])
.range([c0, c1, c2])
.interpolate(d3.interpolateHcl)
),
ticks: ticks ? 1 : 0,
tickSize: 0,
title,
tickFormat: d => (d ? "More" : "Less")
});
}
Insert cell
timeScale = d3.scaleLinear()
.domain([0, video.duration])
.range([new Date("2019-11-01T12:00:00Z"), new Date("2020-01-14T16:00:00Z")])
Insert cell
jumpToAustralia = {
aus;
if (this) {
mutable rotation = [4.3, -0.27];
viewof spin.input.checked = false;
viewof spin.dispatchEvent(new CustomEvent("input", { bubbles: true }));
}
return true;
}
Insert cell
time = {
while (true) {
yield timeScale(video.currentTime);
await Promises.tick(200);
}
}
Insert cell
formatTime = d3.timeFormat("%B %d, %Y")
Insert cell
spinner = {
spin;
if (viewof spin.input.checked) {
while (true) {
const [x, y] = mutable rotation;
yield (mutable rotation = [x - 0.003, y]);
}
}
}
Insert cell
restarter = {
restart;
video.currentTime = 0;
}
Insert cell
totalFrames = Math.ceil(video.duration * 30)
Insert cell
video = {
let resolve;
const promise = new Promise(_ => (resolve = _)).then(() => {
videoElement.play();
return videoElement;
});
videoElement.addEventListener("canplay", resolve);
return promise;
}
Insert cell
mutable rotation = [-10.1, 0.13]
Insert cell
w = Math.min(width, 720)
Insert cell
init = {
gl.useProgram(program);
gl.enableVertexAttribArray(a_vertex);
gl.vertexAttribPointer(a_vertex, 2, gl.FLOAT, false, 0, 0);
gl.uniform2f(u_translate, viewof gl.width / 2, viewof gl.height / 2);
gl.uniform1f(u_scale, viewof gl.height / 2 - 1);
gl.viewport(0, 0, viewof gl.width, viewof gl.height);
}
Insert cell
videoWidth = 1920
Insert cell
videoHeight = 960
Insert cell
videoCanvas = {
const ctx = DOM.context2d(2048, 1024);
ctx.canvas.style.width = "1px";
while (true) {
ctx.drawImage(video, 0, 0, 2048, 1024);
yield ctx.canvas;
await Promises.delay(33.3);
}
}
Insert cell
draw = {
init;
gl.uniform2fv(u_rotate, rotation);
const tex = texture(videoCanvas);
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
invalidation.then(() => gl.deleteTexture(tex));
}
Insert cell
projection = d3.geoOrthographic().precision(0.1)
Insert cell
projection
Insert cell
function drag(projection) {
let x0, y0;
function dragstarted() {
x0 = d3.event.x;
y0 = d3.event.y;
}
function dragged() {
const dx = (d3.event.x - x0) / 500;
const dy = (d3.event.y - y0) / 500;
const [x, y] = mutable rotation;
mutable rotation = [x - dx, y + dy];
x0 = d3.event.x;
y0 = d3.event.y;
}
return d3.drag()
.on("start", dragstarted)
.on("drag", dragged);
}
Insert cell
fragmentShader = {
const shader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(shader, `
precision highp float;

uniform sampler2D u_image;
uniform vec2 u_translate;
uniform float u_scale;
uniform vec2 u_rotate;

const float c_pi = 3.14159265358979323846264;
const float c_halfPi = c_pi * 0.5;
const float c_twoPi = c_pi * 2.0;

float cosphi0 = cos(u_rotate.y);
float sinphi0 = sin(u_rotate.y);

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);
if (rho > 1.0) return;
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);

// inverse rotation
float cosphi = cos(phi);
float x1 = cos(lambda) * cosphi;
float y1 = sin(lambda) * cosphi;
float z1 = sin(phi);
lambda = atan(y1, x1 * cosphi0 + z1 * sinphi0) + u_rotate.x;
phi = asin(z1 * cosphi0 - x1 * sinphi0);

gl_FragColor = texture2D(u_image, vec2((lambda + c_pi) / c_twoPi, (phi + c_halfPi) / c_pi));
}
`);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(shader));
return shader;
}
Insert cell
vertexShader = {
const shader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(shader, `
attribute vec2 a_vertex;

void main(void) {
gl_Position = vec4(a_vertex, 0.0, 1.0);
}
`);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(shader));
return shader;
}
Insert cell
vertexBuffer = {
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, Float32Array.of(-1, -1, +1, -1, +1, +1, -1, +1), gl.STATIC_DRAW);
return buffer;
}
Insert cell
function texture(canvas) {
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);
return texture;
}
Insert cell
program = {
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(program));
return program;
}
Insert cell
a_vertex = gl.getAttribLocation(program, "a_vertex")
Insert cell
u_translate = gl.getUniformLocation(program, "u_translate")
Insert cell
u_scale = gl.getUniformLocation(program, "u_scale")
Insert cell
u_rotate = gl.getUniformLocation(program, "u_rotate")
Insert cell
height = width
Insert cell
import { slider, button, checkbox } from "@jashkenas/inputs"
Insert cell
d3 = require("d3@5")
Insert cell
import {legend} from "@d3/color-legend"
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