Public
Edited
Jul 19, 2023
Importers
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
sampleImage2 = {
const img = await FileAttachment("provinces.png").image();
// const img = await FileAttachment("worldMap2048@1.png").image();
img.style.width = (img.naturalWidth / devicePixelRatio) + "px";
img.style.border = "2px dotted #ff00ff";
return img;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
proj = getMapGeoProjection(d3.geoMiller, [sampleImage.naturalWidth, sampleImage.naturalHeight], mapGeoExtent)
Insert cell
Insert cell
sampleTopology = await buildTopology(sampleTraceResult); //, { projection: proj })
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function readFast(regl, framebuffer, rect) {
const gl = regl._gl;
const format = gl.RGBA;
const type = gl.UNSIGNED_BYTE;
rect ??= {};

const x = rect.x ?? 0;
const y = rect.y ?? 0;
const width = rect.width ?? framebuffer.width;
const height = rect.height ?? framebuffer.height;
const data = new Uint8Array(width * height * 4);

// Modified from https://github.com/KhronosGroup/WebGL/blob/main/sdk/tests/conformance2/reading/read-pixels-from-rgb8-into-pbo-bug.html
framebuffer.use(function() {
var pbo = gl.createBuffer();
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbo);
gl.bufferData(gl.PIXEL_PACK_BUFFER, width * height * 4, gl.STREAM_READ);
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, 0);
gl.getBufferSubData(gl.PIXEL_PACK_BUFFER, 0, data);
});
regl._refresh();
return data;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
inputSlicer = new TileSlicer(createBitmap(sampleImage), { maxTileSize: GLHelper.maxTextureSize });
Insert cell
GLHelper.maxTextureSize
Insert cell
async function getFramebufferFromBitmap(bitmap, reglProgram) {
const inputSlicer = new TileSlicer(bitmap, { maxTileSize: GLHelper.maxTextureSize });
const { regl, commands } = reglProgram;

const { mat4 } = glMatrix;
const framebuffer = regl.framebuffer({ depthStencil: false, shape: [bitmap.width, bitmap.height] });
const allProps = [];
for (let rect of inputSlicer.getTileRectsRelative()) {
const tileBitmap = await inputSlicer.getTileBitmap(rect);
const uSampler = regl.texture({
width: rect.width,
height: rect.height,
data: tileBitmap
});

const uTransform = [];
mat4.ortho(uTransform, 0, bitmap.width, 0, bitmap.height, -1, 1); // this matrix will convert from pixels to clip space
mat4.translate(uTransform, uTransform, [rect.x, rect.y, 1]); // this matrix will translate our quad to rect.xy
mat4.scale(uTransform, uTransform, [rect.width, rect.height, 1]); // this matrix will scale our 1-unit quad to bitmap.wh

const pxOffset = [rect.x, rect.y];
commands.drawImage({ uSampler, uTransform, pxOffset, framebuffer });
}

return framebuffer;
}
Insert cell
{
const options = { colorTable: sampleColorTable };
const colorTable = options?.colorTable;
const paddingOptions = _.pick(options, ["padding", "wrapX"]);
const { bitmap: inputBitmap, sourceRect } = await getPaddedImage(sampleImage, paddingOptions);
const inputBitmapDims = [inputBitmap.width, inputBitmap.height];
const program = getReglProgram(inputBitmapDims);
const { regl, commands, destroy } = program;

const inputBuffer = await getFramebufferFromBitmap(inputBitmap, program);
// const data = regl.read({ framebuffer: inputBuffer });
const buffers = {
color: regl.framebuffer({ depthStencil: false, shape: inputBitmapDims }),
neighbors: regl.framebuffer({ depthStencil: false, shape: inputBitmapDims }),
coords: regl.framebuffer({ depthStencil: false, shape: inputBitmapDims })
}
commands.replaceColors({ uSampler: inputBuffer, framebuffer: buffers.color });
commands.findNeighbors({ uSampler: buffers.color, framebuffer: buffers.neighbors });
commands.classifyCoords({ uSampler: buffers.color, mapEdgePx: { N: -1, E: 10000, S: 10000, W: -1 }, framebuffer: buffers.coords });
const tmp = Object.values(buffers).map(b => {
debugger;
const data1000 = readFast(regl, b, { x: 0, y: 0, width: 1000, height: 1000 });
return { buffer: b, width: b.width, height: b.height, data: data1000.filter(d => d)}
});
tmp.stats = regl.stats;
return tmp;

/*
const data = regl.read({ framebuffer: buffers.color });
const io = new PixelIO(...inputBitmapDims, data);
return createCanvasElement(io);
*/
}
Insert cell
/*
async function* getGpuTileResults(image, options) {
const colorTable = options?.colorTable;
const tileSize = options?.tileSize ?? 1024;
const paddingOptions = _.pick(options, ["padding", "wrapX"]);
const { bitmap: inputBitmap, sourceRect } = await getPaddedImage(image, paddingOptions);
const inputBitmapDims = [inputBitmap.width, inputBitmap.height];
const program = getReglProgram(inputBitmapDims, colorTable);
const { regl, commands } = program;

const inputBuffer = await getFramebufferFromBitmap(inputBitmap, program);
const buffers = {
color: regl.framebuffer({ depthStencil: false, shape: inputBitmapDims }),
neighbors: regl.framebuffer({ depthStencil: false, shape: inputBitmapDims }),
coords: regl.framebuffer({ depthStencil: false, shape: inputBitmapDims })
}
commands.replaceColors({ uSampler: inputBuffer, framebuffer: buffers.color });
commands.findNeighbors({ uSampler: buffers.color, framebuffer: buffers.neighbors });
commands.classifyCoords({ uSampler: buffers.color, framebuffer: buffers.coords });
inputBuffer.destroy();

const slicer = new TileSlicerBase(sourceRect, tileSize);
for (let { absolute, relative } of slicer.getTileRects()) {
const result = {
offset: [relative.x, relative.y]
};
for (let key of Object.keys(buffers)) {
if (key == "coords") {
absolute = {...absolute}, absolute.width += 1, absolute.height += 1;
relative = {...relative}, relative.width += 1, relative.height += 1;
}
const data = regl.read({ framebuffer: buffers[key], ...absolute });
result[key + "IO"] = new PixelIO(relative.width, relative.height, data);
}
closeMapEdges(result, relative.mapEdges);
yield result;
}
[...Object.values(buffers), program].forEach(o => o.destroy());
}
*/
Insert cell
{
const ts = performance.now();
// combined coordClasses and pixelVectors
const numCoordClasses = new Map();
const numPixelVectors = new Map();
numPixelVectors.getOrCreate = (key) => {
let maskSet = numPixelVectors.get(key);
if (!maskSet) numPixelVectors.set(key, maskSet = new Set());
return maskSet;
}

function ingestGpuTileResult(tileResult) {
for (let [colorNum, newVectorNums] of tileResult.pixelVectors) {
const vectorNums = numPixelVectors.getOrCreate(colorNum);
newVectorNums.forEach(v => vectorNums.add(v));
}
for (let [coordNum, coordClass] of tileResult.coordClasses) {
numCoordClasses.set(coordNum, coordClass);
}
}
const tileGenerator = getGpuTileResults(sampleImage, { colorTable: sampleColorTable, tileSize: 1024 });
for await (let tile of tileGenerator) {
const tr = {
pixelVectors: extractPixelVectors(tile),
coordClasses: extractCoordClasses(tile)
}
ingestGpuTileResult(tr);
}

return {
numCoordClasses,
numPixelVectors,
time: performance.now() - ts
}
}
Insert cell
{
const canvases = [];
const tileGenerator = gpuProcessImageYieldTiles(sampleImage, { colorTable: sampleColorTable });
for await (let tile of tileGenerator) {
canvases.push(createCanvasElement(tile.colorIO));
}
canvases.forEach(c => c.style.maxWidth = "25%");
return htl.html`${canvases}`;
}
Insert cell
class GpuImageTracer {
static {
GpuImageTracer.maxWorkerCount = Math.max(1, navigator.hardwareConcurrency - 1)
}
constructor(options) {
options = options ?? {};
this.colorTable = options.colorTable;
this.onTileProcessed = options.fnOnTileProcessed ?? (() => true);
this.tileSize = options.tileSize;

// combined coordClasses and pixelVectors
this.coordClasses = new MaskMap(CoordMask);
const pixelVectors = this.pixelVectors = new Map();
pixelVectors.getOrCreate = (key) => {
let maskSet = pixelVectors.get(key);
if (!maskSet) pixelVectors.set(key, maskSet = new MaskSet(XydMask));
return maskSet;
}
}

async traceImage(image) {
// image
this.image = image;
this.imageSize = [image.naturalWidth ?? image.width, image.naturalHeight ?? image.height];
// tileSlicer
const tsOptions = {};
if (this.tileSize) tsOptions.tileSize = this.tileSize;
const tileSlicer = new TileSlicer(this.image, tsOptions);
const tileRects = tileSlicer.getTileRects();

// worker pool
const workers = [];
{
const start = performance.now();
const workerPromises = [];
const workerCount = Math.min(tileRects.length, GpuImageTracer.maxWorkerCount);
for (let i = 0; i < workerCount; i++) {
const p = getWorker(tileSlicer.baseTileSize, this.colorTable).then(w => workers.push(w));
workerPromises.push(p);
}
await Promise.all(workerPromises);
console.log(workerCount + ' workers initialized', performance.now() - start);
}
const promises = new Set();
const busyWorkers = new Set();

// process tiles
for (let rect of tileSlicer.getTileRects()) {
while (busyWorkers.size >= workers.length) {
await Promise.race([...promises]);
}
const freeWorker = workers.find(w => !busyWorkers.has(w));
busyWorkers.add(freeWorker);
const bitmap = await tileSlicer.getTileBitmap(rect);
let promise = freeWorker.gpuReadTile(bitmap, rect).then(r => {
promises.delete(promise);
busyWorkers.delete(freeWorker);
this._ingestGpuTileResult(r);
new Promise(() => this.onTileProcessed(rect.unpadded));
bitmap.close();
});
promises.add(promise);
await pauseIfNeeded();
}
await Promise.all([...promises]);
tileSlicer.destroy();
await pauseIfNeeded();
workers.forEach(w => w.terminate());
const result = await this._buildRings();
await pauseIfNeeded();
return result;
}

async traceImageSerial(image) {
// image
this.image = image;
this.imageSize = [image.naturalWidth ?? image.width, image.naturalHeight ?? image.height];
// tileSlicer
const tsOptions = {};
if (this.tileSize) tsOptions.tileSize = this.tileSize;
const tileSlicer = new TileSlicer(this.image, tsOptions);

// reglProgram
const reglProgram = getReglProgram(tileSlicer.baseTileSize, this.colorTable);
// process tiles
for (let rect of tileSlicer.getTileRects()) {
const bitmap = await tileSlicer.getTileBitmap(rect);
const gpuOutput = await gpuProcessTile(bitmap, rect, reglProgram);
bitmap.close();

const tileResult = {};
tileResult.pixelVectors = await extractPixelVectors(gpuOutput);
await pauseIfNeeded();
tileResult.coordClasses = await extractCoordClasses(gpuOutput);
await pauseIfNeeded();

this._ingestGpuTileResult(tileResult);
await pauseIfNeeded();

new Promise(() => this.onTileProcessed(rect.unpadded));
}
tileSlicer.destroy();
const result = await this._buildRings();
await pauseIfNeeded();
return result;
}

_ingestGpuTileResult(tileResult) {
for (let [colorNum, newVectors] of tileResult.pixelVectors) {
this.pixelVectors.getOrCreate(colorNum).addValues(newVectors);
}
for (let [coord, coordClass] of tileResult.coordClasses) {
this.coordClasses.set(coord, coordClass);
}
}

async _buildRings() {
const polygonRings = await pixelVectorsToRings(this.pixelVectors, this.coordClasses);
const bbox = [0, 0, ...this.imageSize];
return { bbox, polygonRings, coordClasses: this.coordClasses };
}
}
Insert cell
getReglProgram(1024)
Insert cell
Insert cell
function closeMapEdges(gpuTileOutput, mapEdges) {
const { colorIO, neighborsIO, coordsIO } = gpuTileOutput;

const { width, height } = colorIO;
const xValues = d3.range(0, width);
const yValues = d3.range(0, height);
const mapEdgePixels = {};
if (mapEdges.n) mapEdgePixels.n = d3.cross(xValues, [0]);
if (mapEdges.e) mapEdgePixels.e = d3.cross([width-1], yValues);
if (mapEdges.s) mapEdgePixels.s = d3.cross(xValues, [height-1]);
if (mapEdges.w) mapEdgePixels.w = d3.cross([0], yValues);
for (let dir of Object.keys(mapEdgePixels)) {
mapEdgePixels[dir] = mapEdgePixels[dir].filter(coord => colorIO.getColorNum(...coord));
}

// neighborsIO
if (neighborsIO) {
for (let [dir, pixels] of Object.entries(mapEdgePixels)) {
for (let pixel of pixels) {
const neighborColor = ColorMask.toObject(neighborsIO.getColorNum(...pixel));
const neighbors = {...neighborEncodings.decode(neighborColor.r)};
neighbors[dir] = true;
neighborColor.r = neighborEncodings.encode(neighbors);
neighborColor.a = 255;
neighborsIO.setColorNum(...pixel, ColorMask.fromObject(neighborColor));
}
}
}

// coordsIO
if (coordsIO) {
const junctionColorNum = ColorMask.fromArray([coordEncodings.values.junction, 0, 0, 255]);
const coordsToUpdate = new MaskSet(CoordMask);
const pixelToCoords = {
n: ([x,y]) => [[x, y ], [x+1, y ]], // Along the north edge of the map, we want the NW and NE corners of the pixel
e: ([x,y]) => [[x+1, y ], [x+1, y+1]],
s: ([x,y]) => [[x, y+1], [x+1, y+1]],
w: ([x,y]) => [[x, y ], [x, y+1]]
};
for (let [dir, pixels] of Object.entries(mapEdgePixels)) {
pixels.flatMap(p => pixelToCoords[dir](p)).forEach(c => coordsToUpdate.add(c));
}
for (let coord of coordsToUpdate) {
if (coord[0] < coordsIO.width && coord[1] < coordsIO.height) {
coordsIO.setColorNum(...coord, junctionColorNum);
}
}
}
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
li = undefined; // new LoadIndicator(sampleImage);
Insert cell
li.canvas
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
ts.getTileRectsRelative()
Insert cell
ts = new TileSlicer(getPaddedImage(sampleImage), { tileSize: 2048, padding: 5 });
Insert cell
ts.rect
Insert cell
class TileSlicer extends TileSlicerBase {
constructor(imageSource, options) {
const bitmap = createBitmap(imageSource);
options = Object.assign({ rect: { x: 0, y: 0, width: bitmap.width, height: bitmap.height }, tileSize: Infinity }, options);
super(options.rect, options.tileSize);
this.bitmap = bitmap;
this._tileBitmapRefs = [];
}
destroy() {
const bitmaps = [this.bitmap, ...this._tileBitmapRefs.map(r => r.deref())].filter(b => b);
bitmaps.forEach(b => b.close());
}
async getTileBitmap(rect) {
if (isNaN(rect.x)) return;
rect = {...rect};
rect.x += this.x0;
rect.y += this.y0;
rect.width += this.x0;
rect.height += this.y0
const tileBitmap = await createImageBitmap(this.bitmap, rect.x, rect.y, rect.width, rect.height);
this._tileBitmapRefs.push(new WeakRef(tileBitmap));
return tileBitmap;
}
}
Insert cell
class TileSlicerBase {
constructor(rect, baseTileSize = Infinity) {
this.x0 = rect.x;
this.y0 = rect.y;
this.width = rect.width;
this.height = rect.height;
this.baseTileSize = Math.min(baseTileSize, Math.max(rect.width, rect.height));
}
get imageSize() {
return [this.width, this.height];
}
get tileCount() {
const counts = this.tileCounts;
return counts[0] * counts[1];
}
get tileCounts() {
const { width, height, baseTileSize } = this;
let countX = Math.ceil(width / baseTileSize);
let countY = Math.ceil(height / baseTileSize);
return [countX, countY];
}
get tileIndices() {
const [xCount, yCount] = this.tileCounts;
return _.sortBy(d3.cross(d3.range(0, xCount), d3.range(0, yCount)), [1, 0]) ;
}
getTileRectRelative(tileX, tileY) {
const { width, height, baseTileSize } = this;
const tileX0 = tileX * baseTileSize;
const tileY0 = tileY * baseTileSize;
const tileX1 = Math.min(tileX0 + baseTileSize, width);
const tileY1 = Math.min(tileY0 + baseTileSize, height);

const mapEdges = {
n: tileY0 == 0,
e: tileX1 == width,
s: tileY1 == height,
w: tileX0 == 0
};
let sourceRect = { x: tileX0, y: tileY0, width: tileX1 - tileX0, height: tileY1 - tileY0, mapEdges };
if (sourceRect.width <= 0 || sourceRect.height <= 0) sourceRect = { x: NaN, y: NaN, width: NaN, height: NaN, mapEdges: {} };
return sourceRect;
}
getTileRectsRelative() {
return this.tileIndices.map(t => this.getTileRectRelative(...t));
}
getTileRectAbsolute(relativeRect) {
const rect = {...relativeRect};
rect.x += this.x0;
rect.y += this.y0;
return rect;
}
getTileRectsAbsolute() {
return this.getTileRectsRelative().map(t => this.getTileRectAbsolute(t));
}

getTileRect(tileX, tileY) {
const relative = this.getTileRectRelative(tileX, tileY);
const absolute = this.getTileRectAbsolute(relative);
return { relative, absolute };
}
getTileRects() {
return this.tileIndices.map(t => this.getTileRect(...t));
}
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
viewof classifyCoordsFragShader = {
const coordDefines = Object.entries(coordEncodings.values)
.map(([key, value]) => `const float f${_.startCase(key)} = ${(value / 255).toFixed(32)};`)
.join("\n");

// Starts with north and goes clockwise
const dirKeys4 = ["N", "E", "S", "W"];
const dirKeys = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"];
const cAndDirKeys = ["C", ...dirKeys];
const compassPointsN = [[0,-1],[1,-1],[1,0],[1,1],[0,1],[-1,1],[-1,0],[-1,-1]];
const compassPoints = {
N: compassPointsN,
E: [...compassPointsN.slice(2), ...compassPointsN.slice(0, 2)],
S: [...compassPointsN.slice(4), ...compassPointsN.slice(0, 4)],
W: [...compassPointsN.slice(6), ...compassPointsN.slice(0, 6)]
}
const compassCenters = {
N: [ 0, 0],
E: [-1, 0],
S: [-1, -1],
W: [ 0, -1]
}
return highlightGlsl.fragment(`#version 300 es
precision mediump float;
// classifyCoordsFragShader

${coordDefines.trim()}

uniform sampler2D uSampler;
${dirKeys4.map(d => `uniform int mapEdge${d}px;`).join("\n").trim()}

struct directions {
${cAndDirKeys.map(k => ` ivec2 ${k};`).join("\n").trim() /* ivec2 C, ivec2 N...NW */}
};
struct directionColors {
${cAndDirKeys.map(k => ` vec4 ${k};`).join("\n").trim() /* vec4 C, vec4 N...NW */}
};
${Object.entries(compassPoints) /* const directions directions[N..W] = ... */
.map(([key, values]) => {
const params = [compassCenters[key], ...values].map(([x,y]) => `ivec2(${x}, ${y})`).join(", ");
return `const directions directions${key} = directions(${params});`
})
.join("\n")}

directionColors readColors(ivec2 xyC, directions dirs) {
ivec2 newXyC = xyC + dirs.C;
vec4 cC = texelFetch(uSampler, newXyC, 0);
return directionColors(cC, ${dirKeys.map(dir => `texelFetch(uSampler, newXyC + dirs.${dir}, 0)`).join(", ") /* texelFetch(uSampler, newXyC + dirs.[N..NW], 0) */});
}


bool isJunction(directionColors dirColors) {
// Count the number of unique colors in the 4px range [C, W, NW, N]
int colorCount = 1;
vec4 uniqueColor1 = dirColors.C;
vec4 uniqueColor2 = vec4(-1.0);
vec4 uniqueColor3 = vec4(-1.0);
if (dirColors.W != uniqueColor1) {
colorCount++;
uniqueColor2 = dirColors.W;
}
if (dirColors.NW != uniqueColor1 && dirColors.NW != uniqueColor2) {
colorCount++;
uniqueColor3 = dirColors.NW;
}
if (dirColors.N != uniqueColor1 && dirColors.N != uniqueColor2 && dirColors.N != uniqueColor3) {
colorCount++;
}

if (colorCount >= 3) {
// Standard junction: 3 distinct colors touch at this coord
return true;
}
if (colorCount == 2 && dirColors.C == dirColors.NW && dirColors.N == dirColors.W) {
// Special case junction: 2 distinct colors in an hourglass shape 4-pixel diagonal hourglass
return true;
}
return false;
}

bool isMaybeCorner(directionColors dirColors) {
return (dirColors.C != dirColors.W && dirColors.C != dirColors.NW && dirColors.C != dirColors.N);
}

bool isIntrusion(directionColors dirColors, ivec2 xyC, directions dirs) {
// Assumption: isMaybeCorner test already passed

// Horizontal check
for (int i = 1; i < 5; i++) {
ivec2 xyE = xyC + i * dirs.E;
vec4 cE = texelFetch(uSampler, xyE, 0);
vec4 cNE = texelFetch(uSampler, xyE + dirs.N, 0);
if (cNE == dirColors.C) {
// It advanced north again. This isn't an intrusion.
break;
}
if (cE != dirColors.C) {
// It retreated south without advancing north
return true;
}
}

// Vertical check
for (int i = 1; i < 5; i++) {
ivec2 xyS = xyC + i * dirs.S;
vec4 cS = texelFetch(uSampler, xyS, 0);
vec4 cSW = texelFetch(uSampler, xyS + dirs.W, 0);
if (cSW == dirColors.C) {
// It advanced west again. Not an intrusion.
break;
}
if (cS != dirColors.C) {
// It retreated east without advancing west.
return true;
}
}

return false;
}

bool isCorner(directionColors dirColors) {
// Assumption: isMaybeCorner test already passed
if (dirColors.C == dirColors.NE || dirColors.C == dirColors.SW) return false;
return (dirColors.C == dirColors.E && dirColors.C == dirColors.SE && dirColors.C == dirColors.S);
}

bool isEdge(ivec2 xyC, directionColors dirColors) {
bool result = false;
if (xyC.y == mapEdgeNpx) result = result || (dirColors.C.a + dirColors.W.a) > 0.0;
if (xyC.x > 0 && xyC.y == mapEdgeEpx+1) result = result || (dirColors.W.a + dirColors.NW.a) > 0.0;
if (xyC.y > 0 && xyC.y == mapEdgeSpx+1) result = result || (dirColors.N.a + dirColors.NW.a) > 0.0;
if (xyC.x == mapEdgeWpx) result = result || (dirColors.C.a + dirColors.N.a) > 0.0;
return result;
}

out vec4 fragColor;
void main () {
fragColor = vec4(0.0);

ivec2 xyC = ivec2(round(gl_FragCoord.xy - 0.5));
directions[4] allDirs = directions[4](${dirKeys4.map(k => `directions${k}`).join(", ")});
directionColors dirColorsN = readColors(xyC, directionsN);

// Edge test
if (isEdge(xyC, dirColorsN)) {
fragColor = vec4(fJunction, 0.0, 0.0, 1.0);
return;
}

// Junction test
if (isJunction(dirColorsN)) {
fragColor = vec4(fJunction, 0.0, 0.0, 1.0);
return;
}

directionColors[4] allDirColors = directionColors[4](dirColorsN, ${d3.range(1,4).map(i => `readColors(xyC, allDirs[${i}])`).join(", ")});
bool[4] allDirMaybes = bool[4](${d3.range(0,4).map(i => `isMaybeCorner(allDirColors[${i}])`).join(", ")});

// Intrusion test
for (int i = 0; i < 4; i++) {
if (!allDirMaybes[i]) continue;
directionColors dirColors = allDirColors[i];
directions dirs = allDirs[i];
ivec2 xyCOffset = xyC + dirs.C;
if (isIntrusion(dirColors, xyCOffset, dirs)) {
fragColor = vec4(fIntrusion, 0.0, 0.0, 1.0);
return;
}
}

// Corner test
for (int i = 0; i < 4; i++) {
if (!allDirMaybes[i]) continue;
directionColors dirColors = allDirColors[i];
if (isCorner(dirColors)) {
fragColor = vec4(fCorner, 0.0, 0.0, 1.0);
return;
}
}

}`);
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
glMatrix = require('https://bundle.run/gl-matrix@3.4.3')
Insert cell
glMatrix.mat4.scale
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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