Public
Edited
Jun 20, 2021
Importers
19 stars
Insert cell
Insert cell
Insert cell
Insert cell
html`${viewof worldParameters}`
Insert cell
//svg`<svg width="${width}px" height="${height}" style="background: white">${anaglyphRenderer(processedScene)}</svg>`
Insert cell
viewof terrainExaggeration = Inputs.range([0.25, 10], {value: 1.75, step: 0.25, label: "Terrain Exaggeration"})
Insert cell
viewof lineSimplification = Inputs.range([0.1, 3], {value: 0.2, step: 0.1, label: "Line Simplification"})
Insert cell
viewof numOfLevels = Inputs.range([7, 35], {value: 18, step: 1, label: "Number of Levels"})
Insert cell
computeSlipperyCoords(10, 37.29948148,-113.09397012).join('/')
Insert cell
options = Object.assign({
"10/188/402": "Grand Canyon, USA",
"10/906/404": "Mount Fuji, Japan",
"10/561.97/594.3": "Fish River Canyon, Nambia",
"11/916.26/548.78": "Maelifell, Iceland",
"9/309.13/260.36": "Mount Kilimanjaro, Tanzania",
"9/82.83/180.39": "Mount Rainier, WA, USA",
"10/164.44/363.49": "Mt. Saint Helens, USA",
"11/609.28/1111.53": "Cotahuasi Canyon, Peru",
"10/303.92/678.95": "Perito Moreno Glacier, Argentina",
"10/767.43/424.78": "Yarlang Tsangpo, China",
"10/164.66/376.49": "Crater Lake, USA",
"10/190.31/397.5": "Tabernacle Dome, Zion National Park, USA",
"10/532/292": "Naeroyfjord, Norway",
"10/171/395": "Yosemite, USA",
"11/2014/1267": "Mount Taranaki, New Zealand",
"12/784.81/1622.03": "Barringer Crater, AZ, USA",
"11/376.79/705.69": "Glacier National Park, MO, USA",
"10/163/395": "Bay Area, USA",
"10/302.46/289.96": "Pingualuit Crater, QC, Canada",
})
Insert cell
locationLabel = {
let label = "Hello World"
label = options [location]
return label
}
Insert cell
locationLabel.length
Insert cell
label = {
const theta = parameters.rZ - Math.PI / 2
const mag = 10 * Math.sqrt(2)
const shift = theta - Math.PI / 2
const shiftMag = locationLabel.length / 2 * 0.7
const x = Math.cos(theta) * mag// + Math.cos(shift) * shiftMag
const y = Math.sin(theta) * mag// + Math.sin(shift) * shiftMag
const label = [[x,y,1 + terrainExaggeration,1]]
label.text = locationLabel
label.textAnchor = "middle"
return label
}
Insert cell
html`${viewof perspectiveParameters}`
Insert cell
Insert cell
Insert cell
tileImg = fetchImage(`https://a.tiles.mapbox.com/v4/mapbox.terrain-rgb/${location}.png?access_token=${mapboxToken}`)
Insert cell
Insert cell
Insert cell
Insert cell
terrainMatrix = {
const size = Math.sqrt(terrain.length)
const m = d3.range(size).map((i) => {
const start = i * size
const end = start + size
const slice = Array.from(terrain.slice(start, end))
return slice
})
return m
}
Insert cell
md`From here, we diverge. I compute a bunch of height bands, and then use Marching Squares to compute isobands.`
Insert cell
size = Math.sqrt(terrain.length)
Insert cell
heightExtent = d3.extent(terrain)
Insert cell
heightThresholds = {
const range = heightExtent[1] - heightExtent[0]
const targetBand = Math.floor(range / numOfLevels)
const numOfBands = Math.ceil(range / targetBand)
const bandDelta = Math.round(range / numOfBands / targetBand) * targetBand
const minHeight = Math.max(heightExtent[0], 1)
return d3.range(numOfBands).map((i) => {
return Math.ceil(minHeight) + i * bandDelta
})
}
Insert cell
isolines = {
return heightThresholds.map((lower) => {
return {
lines: MarchingSquares.isoBands(terrainMatrix, lower, heightExtent[1] + 1, { noFrame: false, linearRing: true }),
threshold: lower
}
})
.filter(d => {
return d.lines.length > 0
})
}
Insert cell
md`Here's a 2D preview of the isobands I used to make sure this code worked.`
Insert cell
TwoDimensionsChart = {
const levels = isolines.map((lvl) => {
const rings = lvl.lines.map((isoline) => {
const path = d3.line()(isoline)
return svg`<path d="${path}" fill="none" stroke="black" stroke-width="0.1" />`
})
return svg`<g>${rings}</g>`
})
return svg`<svg width="600" height="600" viewBox="0 0 ${size} ${size}">
<g>${levels}</g>
</svg>`
}
Insert cell
xScale = d3.scaleLinear().domain([0, size]).range([-10,10])
Insert cell
yScale = d3.scaleLinear().domain([0, size]).range([-10,10])
Insert cell
zScale = d3.scaleLinear().domain(heightExtent).range([0,terrainExaggeration])
Insert cell
outlineEdges = false
Insert cell
edges = Object.assign({
top: terrainMatrix[0],
bottom: terrainMatrix[terrainMatrix.length - 1],
left: terrainMatrix.map((r) => r[0]),
right: terrainMatrix.map((r) => r[terrainMatrix.length - 1]),
})
Insert cell
md`Using some d3 scales, I compute the {x, y, z} coordinates for each isoband line, and put them into the "scene"`
Insert cell
projectedIsobands = {
const shapes = []
isolines.forEach((lvl, i) => {
//lvl.lines.forEach((isoline) => {
//const lvl = isolines[levelIndex]
lvl.lines.forEach((isoline) => {
let entity = simplify(isoline.map((d) => {
return { x: d[0], y: d[1] }
}),
lineSimplification
).map((c) => {
let copy = [
xScale(c.x),
yScale(c.y),
zScale(lvl.threshold),
1
]
return copy
})
entity.closed = true
entity.fillColor = "#fff"
entity.strokeWidth = 0.5
entity.level = i
entity.elevation = lvl.threshold
shapes.push(entity)
})
})
return processScene(shapes, parameters)
}
Insert cell
scene = {
let scene = []
let scale = 0.075
//const allRings = []
isolines.forEach((lvl, i) => {
//lvl.lines.forEach((isoline) => {
//const lvl = isolines[levelIndex]
lvl.lines.forEach((isoline) => {
let entity = simplify(isoline.map((d) => {
return { x: d[0], y: d[1] }
}),
lineSimplification
).map((c) => {
let copy = [
xScale(c.x),
yScale(c.y),
zScale(lvl.threshold),
1
]
return copy
})
entity.closed = true
entity.fillColor = "#fff"
entity.strokeWidth = 0.5
scene.push(entity)
})
})
scene.push(label)
const east = [[14, 0, terrainExaggeration / 2, 1]]
east.text = "E"
east.textAnchor = "middle"
const west = [[-14, 0, terrainExaggeration / 2, 1]]
west.text = "W"
west.textAnchor = "middle"
const south = [[0, 14, terrainExaggeration / 2, 1]]
south.text = "S"
south.textAnchor = "middle"
const north = [[0, -14, terrainExaggeration / 2, 1]]
north.text = "N"
north.textAnchor = "middle"
scene.push(north)
scene.push(south)
scene.push(east)
scene.push(west)
if (outlineEdges) {
const topEdge = edges.top.map((height, i) => {
return [
xScale(i),
yScale(0),
zScale(height),
1
]
})

scene.push(topEdge)
const bottomEdge = edges.bottom.map((height, i) => {
return [
xScale(i),
yScale(size),
zScale(height),
1
]
})

scene.push(bottomEdge)

const leftEdge = edges.left.map((height, i) => {
return [
xScale(0),
yScale(i),
zScale(height),
1
]
})

scene.push(leftEdge)

const rightEdge = edges.right.map((height, i) => {
return [
xScale(size),
yScale(i),
zScale(height),
1
]
})

scene.push(rightEdge)
}
return scene
}
Insert cell
parameters = Object.assign({ ...worldParameters, ...perspectiveParameters })
Insert cell
processedScene = {
var t0 = performance.now()
const result = processScene(scene, parameters)
var t1 = performance.now()
console.log(`processor: ${t1 - t0}`)
return result
}
Insert cell
anaglyphRenderer = (processedScene) => {
var t0 = performance.now()
const leftLayer = []
const rightLayer = []
const behindProjectionPlane = (item) => {
return item.map(points => {
points.map(d => d[0][2]).find((d => d <= 0)) === undefined &&
points.map(d => d[1][2]).find((d => d <= 0)) === undefined
})

return true
}
let allLeftPath = ''
let allRightPath = ''
const svgStuff = processedScene
.filter(behindProjectionPlane)
.forEach((item) => {
if (item.length > 1) {
const leftPath = "M " + item.map(p => `${p[0][0]} ${p[0][1]}`).join(' L ') + ((item.closed) ? " Z" : '')
const rightPath = "M " + item.map(p => `${p[1][0]} ${p[1][1]}`).join(' L ') + ((item.closed) ? " Z" : '')
// allLeftPath += ' ' + leftPath
// allRightPath += ' ' + rightPath
leftLayer.push(svg`<path d="${leftPath}" fill="${(item.fillColor) ? item.fillColor : "none"}" stroke="${colors.left}" stroke-width="${(item.strokeWidth) ? item.strokeWidth : 1}" />`)
rightLayer.push(svg`<path d="${rightPath}" fill="${(item.fillColor) ? item.fillColor : "none"}" stroke="${colors.right}" stroke-width="${(item.strokeWidth) ? item.strokeWidth : 1}" />`)
} else {
if (item.text) {
let shift = 0
if (item.textAnchor === "middle") { shift = -item.text.length * 9 / 2 }
leftLayer.push(svg`<g transform="translate(${item[0][0][0] + shift},${item[0][0][1]})">${letterPlotter(item.text, { color: colors.left })}</g>`)
rightLayer.push(svg`<g transform="translate(${item[0][1][0] + shift},${item[0][1][1]})">${letterPlotter(item.text, { color: colors.right })}</g>`)
} else {
leftLayer.push(svg`<circle cx="${item[0][0][0]}" cy="${item[0][0][1]}" r="1" fill="${colors.left}" />`)
rightLayer.push(svg`<circle cx="${item[0][1][0]}" cy="${item[0][1][1]}" r="1" fill="${colors.right}" />`)
}
}
})
// if (allLeftPath !== '') {
// leftLayer.push(svg`<path d="${allLeftPath}" fill="white" stroke="${colors.left}" stroke-width="1" style="mix-blend-mode: multiply;" />`)
// }
// if (allRightPath !== '') {
// rightLayer.push(svg`<path d="${allRightPath}" fill="white" stroke="${colors.right}" stroke-width="1" style="mix-blend-mode: multiply;" />`)
// }
var t1 = performance.now()
console.log(`renderer: ${t1 - t0}`)
return svg`<g>
<g transform="translate(${width / 2},${height / 2})" style="mix-blend-mode: multiply;">
${rightLayer}
</g>
<g transform="translate(${width / 2},${height / 2})" style="mix-blend-mode: multiply;">
${leftLayer}
</g>
</g>`
}
Insert cell
gridSize = 257
Insert cell
Insert cell
computeSlipperyCoords = (zoom, lat, lon) => {
const n = Math.pow(2, zoom)
const lon_deg = lon
const lat_rad = lat * Math.PI / 180
const xtile = n * ((lon_deg + 180) / 360)
const ytile = n * (1 - (Math.log(Math.tan(lat_rad) + (1 / Math.cos(lat_rad))) / Math.PI)) / 2
return [zoom, Math.round(xtile * 100) / 100, Math.round(ytile * 100) / 100]
}
Insert cell
computeSlipperyCoords(10, 35.3606, 138.7274)
Insert cell
Insert cell
Insert cell
{
const el = svg`<svg width="${width}px" height="${height}" style="background: white">${anaglyphRenderer(processedScene)}</svg>`
let isMouseDown = false
return Object.assign(el, {
onmousedown: event => {
isMouseDown = true
},
onmousemove: event => {
if (isMouseDown === false) return;
event.preventDefault();
const newRZ = -Math.PI + event.layerX / width * 2 * Math.PI
const newRX = -Math.PI / 2 + event.layerY / height * Math.PI / 2
const newWorldParams = {
...worldParameters,
rZ: newRZ,
rX: newRX
}
const params = {
...perspectiveParameters,
...newWorldParams
}
el.querySelectorAll('*').forEach(n => n.remove());
el.append(anaglyphRenderer(processScene(scene, params)))
},
onmouseup: event => {
isMouseDown = false
}
})
}
Insert cell
Insert cell
Insert cell
viewof perspectiveParameters = {
// near, zRange, eyeGap, scale
const controller = view`<table>
<tr><td>Camera Offset: ${["cameraOffset", Inputs.range([10, 1000], { value: 40, step: 10 })]}</td></tr>
<tr><td>Near (i.e. distance in front of the carema where we start projecting, aka. the projection plane): ${["near", Inputs.range([10, 40], { value: 10, step: 1 })]}</td></tr>
<tr><td>zRange (Far - Near) - i.e. how far past the projection plane to include: ${["zRange", Inputs.range([1, 40], { value: 15, step: 1 })]}</td></tr>
<tr><td>Eye Gap: ${["eyeGap", Inputs.range([0, 4], { value: 0.8, step: 0.01 })]}</td></tr>
<tr><td>Scale: ${["scale", Inputs.range([1, 20001], { value: 3800, step: 10 })]}</td></tr>
<tr><td>Horizontal Shift Window: ${["horizontalShift", Inputs.range([-10, 10], { value: 0, step: 0.1 })]}</td></tr>
<tr><td>Vertical Shift Window: ${["verticalShift", Inputs.range([-10, 10], { value: 0, step: 0.1 })]}</td></tr>
<tr><td>Shift in X': ${["xprime", Inputs.range([-500, 500], { value: 0, step: 1 })]}</td></tr>
<tr><td>Shift in Y': ${["yprime", Inputs.range([-500, 500], { value: 0, step: 1 })]}</td></tr>
</table>`
return controller
}
Insert cell
viewof worldParameters = {
// rX, rY, rZ
const controller = view`<table>
<tr><td>Rotation in X: ${["rX", Inputs.range([-Math.PI / 2, Math.PI / 2], { value: -Math.PI * 3 / 16, step: Math.PI / 32 })]}</td></tr>
<tr><td>Rotation in Y: ${["rY", Inputs.range([-Math.PI / 2, Math.PI / 2], { value: 0, step: Math.PI / 32 })]}</td></tr>
<tr><td>Rotation in Z: ${["rZ", Inputs.range([-Math.PI, Math.PI], { value: -Math.PI / 4, step: Math.PI / 128 })]}</td></tr>
<tr><td>Shift in X: ${["mx", Inputs.range([-100, 100], { value: 0, step: 1 })]}</td></tr>
<tr><td>Shift in Y: ${["my", Inputs.range([-100, 100], { value: 0, step: 1 })]}</td></tr>
<tr><td>Shift in Z: ${["mz", Inputs.range([-100, 100], { value: 0, step: 1 })]}</td></tr>
<tr><td>Move Camera in X: ${["cx", Inputs.range([-100, 100], { value: 0, step: 1 })]}</td></tr>
<tr><td>Move Camera in Y: ${["cy", Inputs.range([-100, 100], { value: 2, step: 1 })]}</td></tr>
<tr><td>Move Camera in Z: ${["cz", Inputs.range([-100, 100], { value: 0, step: 1 })]}</td></tr>
</table>`
return controller
}
Insert cell
MarchingSquares = import(
"https://unpkg.com/marchingsquares@1.3.3/dist/marchingsquares-esm.js?module"
)
Insert cell
viewof left = {
// rX, rY, rZ
const controller = view`<table>
<tr><td>R: ${["r", Inputs.range([0, 255], { value: 255, step: 1 })]}</td></tr>
<tr><td>G: ${["g", Inputs.range([0, 255], { value: 0, step: 1 })]}</td></tr>
<tr><td>B: ${["b", Inputs.range([0, 255], { value: 0, step: 1 })]}</td></tr>
</table>`

return controller
}
Insert cell
Insert cell
colors = Object.assign({
left: `rgb(${left.r},${left.g},${left.b})`,
right: `rgb(${right.r},${right.g},${right.b})`
})
Insert cell
import { renderer, processScene } with { height } from '@tonyhschu/anaglyph-pipeline-in-gpujs'
Insert cell
import {letterPlotter} from '@tonyhschu/experiment-with-plottable-letters'
Insert cell
import {view} from '@tomlarkworthy/view'
Insert cell
simplify = require('simplify-js')
Insert cell
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more