Public
Edited
Oct 18, 2022
Insert cell
viewof location = html`<select>
<option value="10/906/404">Mount Fuji, Japan</option>
<option value="10/188/402">Grand Canyon, USA</option>
<option value="10/171/395">Yosemite, USA</option>
<option value="11/2014/1267">Mount Taranaki, New Zealand</option>
<option value="10/163/395">Bay Area, USA</option>
</select>`;
Insert cell
renderer.domElement
Insert cell
Insert cell
{
const numTriangles = updatedGeometry.drawRange.count / 3;
const maxTriangles = tileSize * tileSize * 2;
return md`Generated <b>${numTriangles}</b> (${(100 * numTriangles / maxTriangles).toFixed(2)}%) triangles at <b>${maxError.toFixed(1)}</b>m error from a <b>${tileSize}×${tileSize}</b>px tile.`
}
Insert cell
terrainViz = drawGrid(terrain)
Insert cell
{
const size = Math.min(width, 640);
const ratio = size / tileSize;
const ctx = DOM.context2d(size, size, 2);
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, size, size);

ctx.globalAlpha = 0.4;
ctx.drawImage(terrainViz, 0, 0, size, size);
ctx.globalAlpha = 1;
ctx.lineWidth = 0.5;
ctx.beginPath();

function split(ax, ay, bx, by, cx, cy) {
const mx = (ax + bx) >> 1;
const my = (ay + by) >> 1;

if (Math.abs(ax - cx) + Math.abs(ay - cy) > 1 && errors[my * (tileSize + 1) + mx] > maxError) {
ctx.moveTo(mx * ratio, my * ratio);
ctx.lineTo(cx * ratio, cy * ratio);
split(cx, cy, ax, ay, mx, my);
split(bx, by, cx, cy, mx, my);
}
}
ctx.moveTo(0, 0);
ctx.lineTo(ratio * tileSize, ratio * tileSize);
split(0, 0, tileSize, tileSize, tileSize, 0, 0);
split(tileSize, tileSize, 0, 0, 0, tileSize, 0);
ctx.stroke();
return ctx.canvas;
}
Insert cell
tileImg = fetchImage(`https://a.tiles.mapbox.com/v4/mapbox.terrain-rgb/${location}.png?access_token=${mapboxToken}`)
Insert cell
tileCoord = location.split('/').map(Number)
Insert cell
tileSize = tileImg.width
Insert cell
function fetchImage(src) {
return new Promise((resolve, reject) => {
const image = new Image;
image.crossOrigin = "anonymous";
image.src = src;
image.onload = () => resolve(image);
image.onerror = reject;
});
}
Insert cell
gridSize = tileSize + 1
Insert cell
terrain = {
const ctx = DOM.context2d(tileSize, tileSize, 1);
ctx.drawImage(tileImg, 0, 0);
const data = ctx.getImageData(0, 0, tileSize, tileSize).data;

const terrain = new Float32Array(gridSize * gridSize);
// decode terrain values
for (let y = 0; y < tileSize; y++) {
for (let x = 0; x < tileSize; x++) {
const k = (y * tileSize + x) * 4;
const r = data[k + 0];
const g = data[k + 1];
const b = data[k + 2];
terrain[y * gridSize + x] = (r * 256 * 256 + g * 256.0 + b) / 10.0 - 10000.0;
}
}
// backfill right and bottom borders
for (let x = 0; x < gridSize - 1; x++) {
terrain[gridSize * (gridSize - 1) + x] = terrain[gridSize * (gridSize - 2) + x];
}
for (let y = 0; y < gridSize; y++) {
terrain[gridSize * y + gridSize - 1] = terrain[gridSize * y + gridSize - 2];
}
return terrain;
}
Insert cell
{
const canvas = drawGrid(errors, 0.1, 0.85);
canvas.style.width = Math.min((canvas.width * 2), width, 514) + 'px';
return canvas;
}
Insert cell
viewof replay = html`<button>Replay animation</button>`
Insert cell
{
replay;
await visibility();
const size = Math.min(width, 512);
const ratio = size / tileSize;
const ctx = DOM.context2d(size, size, 2);
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, size, size);

ctx.globalAlpha = 0.5;
ctx.drawImage(terrainViz, 0, 0, size, size);
ctx.globalAlpha = 1;
function drawLine(ax, ay, bx, by, color) {
ctx.beginPath();
ctx.moveTo(ax * ratio, ay * ratio);
ctx.lineTo(bx * ratio, by * ratio);
ctx.strokeStyle = color;
ctx.stroke();
}
function* split(ax, ay, bx, by, cx, cy) {
const mx = (ax + bx) >> 1;
const my = (ay + by) >> 1;

if (Math.abs(ax - cx) + Math.abs(ay - cy) > 1 && errors[my * (tileSize + 1) + mx] > 100) {
drawLine(mx, my, cx, cy, 'red');
yield ctx.canvas;
yield* split(cx, cy, ax, ay, mx, my);
yield* split(bx, by, cx, cy, mx, my);
drawLine(mx, my, cx, cy, 'black');
}
}
drawLine(0, 0, tileSize, tileSize, 'red');
yield* split(0, 0, tileSize, tileSize, tileSize, 0, 0);
yield* split(tileSize, tileSize, 0, 0, 0, tileSize, 0);
drawLine(0, 0, tileSize, tileSize, 'black');
}
Insert cell
errors = {
const errors = new Float32Array(gridSize * gridSize);

const numSmallestTriangles = tileSize * tileSize;
const numTriangles = numSmallestTriangles * 2 - 2; // 2 + 4 + 8 + ... 2^k = 2 * 2^k - 2
const lastLevelIndex = numTriangles - numSmallestTriangles;
// iterate over all possible triangles, starting from the smallest level
for (let i = numTriangles - 1; i >= 0; i--) {
// get triangle coordinates from its index in an implicit binary tree
let id = i + 2;
let ax = 0, ay = 0, bx = 0, by = 0, cx = 0, cy = 0;
if (id & 1) {
bx = by = cx = tileSize; // bottom-left triangle
} else {
ax = ay = cy = tileSize; // top-right triangle
}
while ((id >>= 1) > 1) {
const mx = (ax + bx) >> 1;
const my = (ay + by) >> 1;

if (id & 1) { // left half
bx = ax; by = ay;
ax = cx; ay = cy;
} else { // right half
ax = bx; ay = by;
bx = cx; by = cy;
}
cx = mx; cy = my;
}

// calculate error in the middle of the long edge of the triangle
const interpolatedHeight = (terrain[ay * gridSize + ax] + terrain[by * gridSize + bx]) / 2;
const middleIndex = ((ay + by) >> 1) * gridSize + ((ax + bx) >> 1);
const middleError = Math.abs(interpolatedHeight - terrain[middleIndex]);
if (i >= lastLevelIndex) { // smallest triangles
errors[middleIndex] = middleError;

} else { // bigger triangles; accumulate error with children
const leftChildError = errors[((ay + cy) >> 1) * gridSize + ((ax + cx) >> 1)];
const rightChildError = errors[((by + cy) >> 1) * gridSize + ((bx + cx) >> 1)];
errors[middleIndex] = Math.max(errors[middleIndex], middleError, leftChildError, rightChildError);
}
}
return errors;
}
Insert cell
updatedGeometry = {
let i = 0;
const indices = geometry.index.array;
function processTriangle(ax, ay, bx, by, cx, cy) {
// middle of the long edge
const mx = (ax + bx) >> 1;
const my = (ay + by) >> 1;

if (Math.abs(ax - cx) + Math.abs(ay - cy) > 1 && errors[my * gridSize + mx] > maxError) {
// triangle doesn't approximate the surface well enough; split it into two
processTriangle(cx, cy, ax, ay, mx, my);
processTriangle(bx, by, cx, cy, mx, my);

} else {
// add a triangle to the final mesh
indices[i++] = ay * gridSize + ax;
indices[i++] = by * gridSize + bx;
indices[i++] = cy * gridSize + cx;
}
}

processTriangle(0, 0, tileSize, tileSize, tileSize, 0);
processTriangle(tileSize, tileSize, 0, 0, 0, tileSize);
geometry.index.needsUpdate = true;
geometry.setDrawRange(0, i);
renderer.render(scene, camera);
return geometry;
}
Insert cell
THREE = {
const THREE = window.THREE = await require("three@0.107.0/build/three.min.js");
await require("three@0.107.0/examples/js/controls/OrbitControls.js").catch(() => {});
return THREE;
}
Insert cell
mapboxToken = 'pk.eyJ1IjoibW91cm5lciIsImEiOiJWWnRiWG1VIn0.j6eccFHpE3Q04XPLI7JxbA'
Insert cell
terrainExaggeration = 1.5
Insert cell
drawGrid = (data, cutoff = 1.0, max = 1.0) => {
const size = Math.sqrt(data.length);
const ctx = DOM.context2d(size, size, 1);
ctx.canvas.style.imageRendering = 'pixelated';
if (!ctx.canvas.style.imageRendering) ctx.canvas.style.imageRendering = 'crisp-edges';
const imgData = ctx.getImageData(0, 0, size, size);
const minZ = data.reduce((a, b) => Math.min(a, b));
const maxZ = data.reduce((a, b) => Math.max(a, b));
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
const k = y * size + x;
const [r, g, b] = turbo(Math.min(max, max * Math.min((data[k] - minZ) / (maxZ - minZ), cutoff) / cutoff));
imgData.data[4 * k + 0] = r;
imgData.data[4 * k + 1] = g;
imgData.data[4 * k + 2] = b;
imgData.data[4 * k + 3] = 255;
}
}
ctx.putImageData(imgData, 0, 0);
return ctx.canvas;
}
Insert cell
metersPerPixel = {
const [z, x, y] = tileCoord;
const numTiles = Math.pow(2, z);
const lat = 2 * Math.atan(Math.exp((1 - 2 * (y + 0.5) / numTiles) * Math.PI)) - Math.PI / 2;
return 40075016.7 * Math.cos(lat) / numTiles / tileSize;
}
Insert cell
camera = {
const camera = new THREE.PerspectiveCamera(45, Math.min(width, 640) / height, 0.1, 1000);
camera.position.set(0.2, -0.7, 0.9);
camera.up.set(0, 0, 1);
camera.lookAt(new THREE.Vector3(0, 0, 0));
return camera;
}
Insert cell
height = 420
Insert cell
renderer = {
const renderer = new THREE.WebGLRenderer({antialias: true});
const controls = new THREE.OrbitControls(camera, renderer.domElement);
invalidation.then(() => (controls.dispose(), renderer.dispose()));
renderer.setSize(Math.min(width, 640), height);
renderer.setPixelRatio(devicePixelRatio);
controls.addEventListener("change", () => renderer.render(scene, camera));
renderer.render(scene, camera);
return renderer;
}
Insert cell
geometry = {
const geometry = new THREE.BufferGeometry();

const vertices = new Float32Array(gridSize * gridSize * 3);
const indices = new Uint32Array(tileSize * tileSize * 6);
let index = 0;

for (let y = 0; y <= tileSize; y++) {
for (let x = 0; x <= tileSize; x++) {
const i = y * gridSize + x;
vertices[3 * i + 0] = x / tileSize - 0.5;
vertices[3 * i + 1] = 0.5 - y / tileSize;
vertices[3 * i + 2] = terrain[i] / metersPerPixel / tileSize * terrainExaggeration;

indices[index++] = i + 1;
indices[index++] = i;
indices[index++] = i + gridSize;
indices[index++] = i + 1;
indices[index++] = i + gridSize;
indices[index++] = i + gridSize + 1;
}
}
geometry.addAttribute('position', new THREE.BufferAttribute(vertices, 3));
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
geometry.computeVertexNormals();
return geometry;
}
Insert cell
turbo = (x) => [
34.61 + x * (1172.33 - x * (10793.56 - x * (33300.12 - x * (38394.49 - x * 14825.05)))),
23.31 + x * (557.33 + x * (1225.33 - x * (3574.96 - x * (1073.77 + x * 707.56)))),
27.2 + x * (3211.1 - x * (15327.97 - x * (27814 - x * (22569.18 - x * 6838.66))))
]
Insert cell
scene = {
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x333333);
const material = new THREE.MeshNormalMaterial({flatShading: true, side: THREE.DoubleSide});
const mesh = new THREE.Mesh(geometry, material);
mesh.name = 'terrain';
scene.add(mesh);
return scene;
}
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