{
const defaultColor = [0, 0, 0];
const rgbFromStr = (str ) => {
return str
?.match(/rgb\((\d+),\s?(\d+),\s?(\d+)\)/)
?.slice(1, 4)
?.map(x => +x / 255) ?? defaultColor;
};
const getHeight = feature => feature.properties.max_ft - feature.properties.min_ft
const [minHeight, maxHeight] = d3.extent(tile.features, f => getHeight(f))
const zScale = d3.scaleLinear().domain([0, maxHeight]).range([0, 1]);
const breakSize = 20
async function initLineRenderer({ context, device }) {
const margin = 0;
const projection = d3.geoMercator().fitExtent(
[
[margin, margin],
[TILE_WIDTH - margin, TILE_HEIGHT - margin],
],
tile,
);
const path = feature => {
const height = getHeight(feature)
switch (feature.geometry.type) {
case 'LineString': {
return feature.geometry.coordinates.reduce(
(vertices , [lng, lat] ) => {
const [y, x] = projection([lng, lat]) ;
vertices.push([x, y, zScale(height), height]);
return vertices;
},
[],
);
}
case 'Polygon': {
const maxRing = feature.geometry.coordinates.reduce(
(max, ring) => !max || ring.length > max.length ? ring : max,
null
)
const vertices = maxRing.map(coords => [
...projection(coords).reverse(),
zScale(height),
height
]);
for (let i = 0; i < Math.floor(height / breakSize); i++) {
vertices.push(
maxRing.map(coords => [
...projection(coords).reverse(),
zScale(i * breakSize),
i * breakSize
])
)
}
return { vertices, multi: true }
}
default:
return null;
}
};
// WARNING color is interpolated inaccurately (based on the extent of the tile heights,
// not absolute height) and should be fixed in a non-demo
const normElevation = d3.scaleLinear()
.domain(
[0, maxHeight]
// d3.extent(tile.features, f => getHeight(f)) /* as [number, number] */,
)
.range([0, 1]);
const lines = tile.features
// .filter(feature => +feature.properties.ELEVATION % 10 === 0)
.flatMap(feature => {
const verts = path(feature);
if (!verts) {
return null;
}
// unified building color
const buildingColor = colorScheme === 'random' &&
[util.rand(), util.rand(), util.rand(), 1];
return (verts.multi ? verts.vertices : [verts]).map(v => {
const segmentColor = colorScheme === 'randomSegment' &&
[util.rand(), util.rand(), util.rand(), 1];
return {
vertices: v,
color: v.map(vertex => {
switch (colorScheme) {
case 'random': return buildingColor;
case 'randomSegment': return segmentColor;
default: {
const color = d3.color(
d3[colorScheme](normElevation(vertex[3])),
)
return [...rgbFromStr(color?.formatRgb()), 1]
}
}
}),
}
});
})
.filter(Boolean);
return new LineRenderer(
device,
context,
lines,
);
}
async function main() {
let frame;
const { context, device } = await gpu.init(TILE_WIDTH, TILE_HEIGHT);
const renderer = await initLineRenderer({ context, device });
// camera
const projection = mat4.perspective((90 * Math.PI) / 180, TILE_WIDTH / TILE_HEIGHT, 0.01, 256);
let position = [0, 0, 0.6];
const target = [0, 0, 0];
const up = [0, 1, 0];
const rotation = -Math.PI / 3;
const view = mat4.lookAt(
[...position],
target,
up,
);
mat4.rotateX(view, rotation, view)
const viewProjection = mat4.multiply(projection, view);
const minZoom = 0.5;
const maxZoom = 5.0;
const zoomSpeed = 0.001;
function zoom(e) {
e.preventDefault();
const vd = [
target[0] - position[0],
target[1] - position[1],
target[2] - position[2],
];
const len = Math.sqrt(vd[0] * vd[0] + vd[1] * vd[1] + vd[2] * vd[2]);
vd[0] /= len;
vd[1] /= len;
vd[2] /= len;
const amt = e.deltaY * zoomSpeed;
const np = [
position[0] - vd[0] * amt,
position[1] - vd[1] * amt,
position[2] - vd[2] * amt
];
const dist = Math.sqrt(np[0] * np[0] + np[1] * np[1] + np[2] * np[2]);
if (dist >= minZoom && dist <= maxZoom) {
position = np;
mat4.lookAt(position, target, up, view);
mat4.rotateX(view, rotation, view);
mat4.multiply(projection, view, viewProjection);
}
}
function tick() {
let t = performance.now() * 0.0002;
const matrix = mat4.copy(viewProjection);
mat4.rotateZ(viewProjection, t, matrix);
renderer.render(matrix);
frame = requestAnimationFrame(tick);
}
frame = requestAnimationFrame(tick);
context.canvas.addEventListener('wheel', zoom);
// Specific to ObservableHQ.com:
invalidation.then(() => {
cancelAnimationFrame(frame);
renderer.cleanup();
device.destroy();
context.canvas.removeEventListener('wheel', zoom);
})
return { context }
}
return (await main()).context.canvas
}