Public
Edited
Aug 3, 2023
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
window.colorMap = sampleColorReplacementMap
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
sampleTileGpuOutput = {
const options = {
colorTable: sampleColorTable,
wrapX: sampleWrapX,
maxTileSize: sampleTileSize
}
const tsOptions = _.pick(options, ["wrapX", "maxTileSize"]);
const tileSlicer = new TileSlicerPadded(sampleImage, tsOptions);
const mapRect = {
x: TILE_PADDING,
y: TILE_PADDING,
width: tileSlicer.imageWidth,
height: tileSlicer.imageHeight
};
debugger;
const reglProgram = getReglProgram(tileSlicer.baseTileSize, options.colorTable);
const bitmap = await tileSlicer.getTileBitmap(sampleTileRect);
const flattenedIO = await gpuProcessTile(bitmap, sampleTileRect, mapRect, reglProgram);
bitmap.close();
reglProgram.destroy();
return flattenedIO;
}
Insert cell
Insert cell
Insert cell
Insert cell
sampleTraceResult = {
const options = {
colorTable: sampleColorTable,
wrapX: sampleWrapX,
maxTileSize: sampleTileSize
}
const ts = performance.now();
const result = await gpuTraceImage(sampleImage, options);
result.runtime = performance.now() - ts;
return result;
}
Insert cell
Insert cell
proj = getMapGeoProjection(d3.geoMiller, [sampleImage.naturalWidth, sampleImage.naturalHeight], mapGeoExtent)
Insert cell
Insert cell
ColorMask.toHex(4278190335)
Insert cell
sampleTopology = {
const traceResult = sampleTraceResult;
return await buildTopology(traceResult, { projection: proj })
}
Insert cell
Insert cell
{
const result = [];
for (let tileCount of d3.range(1, 33)) {
const batchSizeRaw = tileCount / Math.ceil(tileCount / 6);
const batchSize = Math.ceil(tileCount / Math.ceil(tileCount / 6));
const batchCount = Math.ceil(tileCount / batchSize);
result.push({tileCount, batchSizeRaw, batchSize, batchCount});
}
return result;
}
Insert cell
{
const ts = performance.now();
return getReglProgram(4096);
return performance.now() - ts;
}
Insert cell
chunk([1, 2, 3, 4, 5, 6, 7], 8)
Insert cell
function chunk(array, chunkSize) {
array = [...array];
var chunks = [];
for (var i = 0; i < array.length; i += chunkSize) {
chunks.push(array.slice(i, i + chunkSize));
}
return chunks;
}
Insert cell
async function gpuTraceImage(image, options) {
let allCoordTypes = new Map(Object.keys(coordTypeEncoding.values).map(k => [k, []]));
const allPixelVectors = new Map();
allPixelVectors.getOrCreate = (key) => {
let vectors = allPixelVectors.get(key);
if (!vectors) allPixelVectors.set(key, vectors = []);
return vectors;
}

function ingestParsedResult(parsedTileResult) {
const { coordTypes, pixelVectors } = parsedTileResult;
for (let [coordType, coordNums] of parsedTileResult.coordTypes) {
allCoordTypes.get(coordType).push(...coordNums);
}
for (let [colorNum, tilePixelVectors] of parsedTileResult.pixelVectors) {
allPixelVectors.getOrCreate(colorNum).push(...tilePixelVectors);
}
}

const { colorTable, fnOnTileProcessed } = (options ?? {});
const tsOptions = _.pick(options, ["wrapX", "maxTileSize"]);
const tileSlicer = new TileSlicerPadded(image, tsOptions);
const mapRect = {
x: TILE_PADDING,
y: TILE_PADDING,
width: tileSlicer.imageWidth,
height: tileSlicer.imageHeight
};
const reglProgram = getReglProgram(tileSlicer.baseTileSize, colorTable);
for (let tileRect of tileSlicer.getTileRects()) {
// commence to tracing
const bitmap = await tileSlicer.getTileBitmap(tileRect);
const pixelIO = await gpuProcessTile(bitmap, tileRect, mapRect, reglProgram);
const parsed = await parseGpuTileResult(tileRect, pixelIO);
ingestParsedResult(parsed);
await pauseIfNeeded();
}
reglProgram.destroy();

const coordTypesReversed = new Map();
for (let [coordType, coordNums] of [...allCoordTypes]) {
coordNums.forEach(coordNum => coordTypesReversed.set(coordNum, coordType));
}
allCoordTypes = MaskMap.around(coordTypesReversed, CoordMask);
for (let [colorNum, xydNums] of [...allPixelVectors]) {
/*
const xydSet = new Set();
for (let xydNum of xydNums) {
const reverseXydNum = XydMask.getReverseNum(xydNum);
if (xydSet.has(reverseXydNum)) {
debugger;
xydSet.delete(reverseXydNum);
}
else {
xydSet.add(xydNum);
}
}
allPixelVectors.set(colorNum, MaskSet.around(xydSet, XydMask));
*/
allPixelVectors.set(colorNum, MaskSet.around(new Set(xydNums), XydMask));
}
const polygonRings = await pixelVectorsToRings(allPixelVectors, allCoordTypes, [tileSlicer.imageWidth, tileSlicer.imageHeight]);
const bbox = [0, 0, image.naturalWidth ?? image.width, image.naturalHeight ?? image.height];
return { bbox, polygonRings, coordTypes: allCoordTypes, pixelVectors: allPixelVectors };
}
Insert cell
async function gpuProcessTile(bitmap, tileRect, mapRect, reglProgram) {
const { regl, commands, buffers } = reglProgram;
const tileTexture = regl.texture({ data: bitmap });
const tileDimensions = [ bitmap.width, bitmap.height ];

const mapEdgesAbs = {
x0: mapRect.x,
y0: mapRect.y,
x1: mapRect.x + mapRect.width,
y1: mapRect.y + mapRect.height
};
const mapBboxVec = [
mapEdgesAbs.x0 - tileRect.x,
mapEdgesAbs.y0 - tileRect.y,
mapEdgesAbs.x1 - tileRect.x - 1,
mapEdgesAbs.y1 - tileRect.y - 1
]
const tileBboxVec = [
TILE_PADDING,
TILE_PADDING,
TILE_PADDING + tileRect.width - 1,
TILE_PADDING + tileRect.height - 1
]

Object.values(buffers).forEach(b => b.resize(...tileDimensions));

commands.replaceColors({ uSampler: tileTexture, framebuffer: buffers.color });
await glClientWaitAsync(regl, 'replaceColors');
tileTexture.destroy();


commands.traceImage({ uSampler: buffers.color, mapBboxVec, tileBboxVec, framebuffer: buffers.traced });
await glClientWaitAsync(regl, 'traced');
const results = [];
const rect = Bbox.dims(tileBboxVec);
const data = await readFramebufferAsync(regl, buffers.traced, rect);
return new PixelIO(rect.width, rect.height, data);
}
Insert cell
Bbox.dims([0,0,100,100]);
Insert cell
async function parseGpuTileResult(rect, flattenedIO) {

// Initialize result objects
const coordTypes = new Map(Object.keys(coordTypeEncoding.values).map(k => [k, []]));
const pixelVectors = new Map();
pixelVectors.getOrCreate = (key) => {
let xydVectors = pixelVectors.get(key);
if (!xydVectors) pixelVectors.set(key, xydVectors = []);
return xydVectors;
}

// Read flattenedIO
const { width, height } = rect;
let loopCount = 0;
for (let { colorNum: flattenedColorNum, x: tileX, y: tileY } of flattenedIO.getNonZeroPixels()) {
const x = tileX + rect.x;
const y = tileY + rect.y;
const pixelData = GpuColorMask.toObject(flattenedColorNum);

// coordType
if (pixelData.coordType) {
coordTypes.get(pixelData.coordType).push(CoordMask.fromArray([x,y]));
}

// pixelVectors
if (pixelData.colorOpacity) {
// Fork of http://defghi1977.html.xdomain.jp/tech/dotrace/dotrace.htm
const { n, e, s, w } = pixelData.neighbors;
if (!n && !e && !s && !w) continue;

const vectors = pixelVectors.getOrCreate(pixelData.colorNum);
// pixelData.n means it has a neighbor to the north, so we want to draw the eastward-pointing vector, etc.
if (n) vectors.push(XydMask.fromArray([x, y, XydMask.direcs.e]));
if (e) vectors.push(XydMask.fromArray([x+1, y, XydMask.direcs.s]));
if (s) vectors.push(XydMask.fromArray([x+1, y+1, XydMask.direcs.w]));
if (w) vectors.push(XydMask.fromArray([x, y+1, XydMask.direcs.n]));
}

// if (loopCount++ % 10000) await pauseIfNeeded();
}

return { pixelVectors, coordTypes };
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function getPassThruCommand(regl) {
const command = {
frag: passThruFragShader,
uniforms: { uSampler: regl.prop("uSampler") },
framebuffer: regl.prop("framebuffer")
};
return regl(Object.assign({}, reglCommandDefaults, command));
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function getTraceImageCommand(regl) {
let command = {
frag: traceImageFragShader,
uniforms: {
uSampler: regl.prop("uSampler"),
mapBboxVec: regl.prop("mapBboxVec"),
tileBboxVec: regl.prop("tileBboxVec")
},
framebuffer: regl.prop("framebuffer")
};
return regl(Object.assign({}, reglCommandDefaults, command));
}
Insert cell
function getDrawEdgesCommand(regl) {
let command = {
frag: drawEdgesFragShader,
uniforms: {
uSampler: regl.prop("uSampler"),
mapBboxVec: regl.prop("mapBboxVec"),
tileBboxVec: regl.prop("tileBboxVec")
},
framebuffer: regl.prop("framebuffer")
};
return regl(Object.assign({}, reglCommandDefaults, command));
}
Insert cell
async function glClientWaitAsync(glOrRegl, label) {
label ??= "glClientWaitAsync";
const gl = glOrRegl._gl ?? glOrRegl;
const sync = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0);
const ts = performance.now();
gl.flush();

await new Promise((resolve, reject) => {
function test() {
const res = gl.clientWaitSync(sync, gl.SYNC_FLUSH_COMMANDS_BIT, 0);
if (res === gl.WAIT_FAILED) {
reject();
return;
}
if (res === gl.TIMEOUT_EXPIRED) {
setTimeout(test, 10);
return;
}
resolve();
}
test();
});

gl.deleteSync(sync);
console.log(label, performance.now() - ts);
}
Insert cell
async function readFramebufferAsync(regl, framebuffer, rect) {
const gl = regl._gl;
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);

async function readAsync() {
// Define and start filling PBO
const pbo = gl.createBuffer();
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbo);
gl.bufferData(gl.PIXEL_PACK_BUFFER, data.byteLength, gl.STREAM_READ);
gl.readPixels(x, y, width, height, gl.RGBA, gl.UNSIGNED_BYTE, 0);
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
// Wait for PBO ready
await glClientWaitAsync(gl, 'readAsync');
// read PBO
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbo);
gl.getBufferSubData(gl.PIXEL_PACK_BUFFER, 0, data);
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);

// clean up and return
gl.deleteBuffer(pbo);
return data;
}

let promise;
{
let fnResolve, fnReject;
promise = new Promise((resolve, reject) => { fnResolve = resolve; fnReject = reject });
promise.resolve = fnResolve;
promise.reject = fnReject;
}

framebuffer.use(function() {
readAsync().then(r => {
regl._refresh();
promise.resolve(data);
}).catch(e => {
try {
regl._refresh();
}
catch (re) {
console.log(re);
}
promise.reject(e);
});
})

await promise;
return data;
}
Insert cell
function readFast(regl, framebuffer, rect, data) {
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;
data ??= new Uint8Array(width * height * 4);

// gl.readPixels(x, y, width, height, gl.RGBA, gl.UNSIGNED_BYTE, data);
// 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(x, y, 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
parseGpuTileResult(sampleTileGpuResult)
Insert cell
async function _gpuProcessImage(image, options) {
const allCoordTypes = new Map();
const allPixelVectors = new Map();
allPixelVectors.getOrCreate = (key) => {
let maskSet = allPixelVectors.get(key);
if (!maskSet) allPixelVectors.set(key, maskSet = new Set());
return maskSet;
}

function ingestParsedResult(parsedTileResult) {
const { coordTypes, pixelVectors } = parsedTileResult;
for (let [coordNum, coordType] of parsedTileResult.coordTypes) {
allCoordTypes.set(coordNum, coordType)
}
for (let [colorNum, tilePixelVectors] of parsedTileResult.pixelVectors) {
const allVectors = allPixelVectors.getOrCreate(colorNum);
tilePixelVectors.forEach(v => allVectors.add(v));
}
}

const { colorTable, wrapX, maxTileSize, fnOnTileProcessed } = (options ?? {});

const tileSlicer = new TileSlicerPadded(image, { wrapX, maxTileSize });
const reglProgram = getReglProgram(tileSlicer.baseTileSize, colorTable);
for (let rect of tileSlicer.getTileRects()) {
const bitmap = await tileSlicer.getTileBitmap(rect);
const gpuTileResult = await gpuProcessTile(bitmap, rect, reglProgram, { fnOnTileProcessed, wrapX });
bitmap.close();
const parsedTileResult = parseGpuTileResult(gpuTileResult);
ingestParsedResult(parsedTileResult);
}

return { allCoordTypes, allPixelVectors };
/*

reglProgram.destroy();
const polygonRings = await pixelVectorsToRings(pixelVectors, coordClasses, [tileSlicer.imageWidth, tileSlicer.imageHeight]);
const bbox = [0, 0, image.naturalWidth ?? image.width, image.naturalHeight ?? image.height];
return { bbox, polygonRings, coordClasses, wrapX: !!wrapX };
*/
}
Insert cell
Insert cell
getMapEdgeCoords({n: true, e: true, s: true, w: true}, { x: 5, y: 5, width: 10, height: 10 })
Insert cell
function getMapEdgeCoords(mapEdges, rect) {
const mapEdgePixels = {};
const x0 = rect.x;
const x1 = rect.x + rect.width;
const y0 = rect.y;
const y1 = rect.y + rect.height;

const xValues = d3.range(x0, x1);
const yValues = d3.range(y0, y1);
if (mapEdges.n) mapEdgePixels.n = d3.cross(xValues, [x0]);
if (mapEdges.e) mapEdgePixels.e = d3.cross([x1-1], yValues);
if (mapEdges.s) mapEdgePixels.s = d3.cross(xValues, [y1-1]);
if (mapEdges.w) mapEdgePixels.w = d3.cross([y0], yValues);

return mapEdgePixels;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{

const coordTypes = new MaskMap(CoordMask);
coordTypes.set([1,1], "junction");
function testCollinear([ax, ay], [bx, by], [cx, cy]) {
return (bx-ax) * (cy-ay) == (cx-ax) * (by-ay);
}
function simplifyArc(arc, coordTypes) {
const simplifiedArc = [arc[0]];
for (let i = 1; i < arc.length - 2; i++) {
const coord = arc[i];
const prevCoord = arc[i-1];
const nextCoord = arc[i+1];
if (coordTypes.get(coord) == "junction" || !testCollinear(coord, prevCoord, nextCoord)) {
simplifiedArc.push(coord);
}
}
simplifiedArc.push(arc.at(-1));
return simplifiedArc;
}

const coords = [[0,0], [1,1], [2,2], [3,3], [2,3], [2,4], [3,4]];
return simplifyArc(coords, coordTypes);
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
li = new LoadIndicator(sampleImage);
Insert cell
li.canvas
Insert cell
Insert cell
Insert cell
Insert cell
{
const sourceColorNum = ColorMask.fromArray([10, 20, 30, 83]);
const obj = GpuColorMask.toObject(sourceColorNum);
const obj2 = _.cloneDeep(obj);

obj2.colorNum = ColorMask.fromArray([...ColorMask.toArray(obj2.colorNum).slice(0,3), 6]);
obj2.neighbors.e = true;
obj2.coordType = "junction";
const reverse = GpuColorMask.fromObject(obj2);
const rereverse = GpuColorMask.toObject(reverse);
return { sourceColorNum, obj, reverse, rereverse, finalColor: ColorMask.toObject(rereverse.colorNum) }
}
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
Bbox.fromDims({x: 5, y: 0, width: 105, height: 110})
Insert cell
class Bbox {
static isEqual(bbox1, bbox2) {
if (bbox1 == bbox2) return true;
return (bbox1?.length == 4 &&
bbox1?.length == 4 &&
bbox1[0] == bbox2[0] &&
bbox1[1] == bbox2[1] &&
bbox1[2] == bbox2[2] &&
bbox1[3] == bbox2[3]);
}
static contains(bbox1, bbox2) {
if (bbox1 == bbox2) return false;
return (bbox1?.length === bbox2?.length &&
bbox1[0] < bbox2[0] &&
bbox1[1] < bbox2[1] &&
bbox1[2] > bbox2[2] &&
bbox1[3] > bbox2[3]);
}
static covers(bbox1, bbox2) {
if (bbox1 == bbox2) return true;
return (bbox1?.length === bbox2?.length &&
bbox1[0] <= bbox2[0] &&
bbox1[1] <= bbox2[1] &&
bbox1[2] >= bbox2[2] &&
bbox1[3] >= bbox2[3]);
}
static containsPoint(bbox, point) {
return Bbox.contains(bbox, [...point, ...point]);
}
static coversPoint(bbox, point) {
return Bbox.covers(bbox, [...point, ...point]);
}
static isPointOnEdge(bbox, point) {
return bbox[0] == point[0] ||
bbox[1] == point[1] ||
bbox[2] == point[0] ||
bbox[3] == point[1];
}
static width(bbox) {
return bbox[2] - bbox[0] + 1;
}
static height(bbox) {
return bbox[3] - bbox[1] + 1;
}
static dims(bbox) {
return { x: bbox[0], y: bbox[1], width: Bbox.width(bbox), height: Bbox.height(bbox) };
}
static fromDims(dims) {
return [
dims.x,
dims.y,
dims.width + dims.x - 1,
dims.height + dims.y - 1
];
}
static fromPoints(points) {
let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
for (let [x, y] of points) {
if (x < x0) x0 = x;
if (x > x1) x1 = x;
if (y < y0) y0 = y;
if (y > y1) y1 = y;
}
return [x0, y0, x1, y1];
}
static area(bbox) {
return Bbox.width(bbox) * Bbox.height(bbox);
}
static buffer(bbox, amount) {
return [bbox[0] - amount, bbox[1] - amount, bbox[2] + amount, bbox[3] + amount];
}
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
viewof traceImageFragShader = {
let neighborsDefines = Object.entries(neighborsEncoding.values)
.map(([key, value]) => `float f${_.startCase(key)} = ${(value / 255).toFixed(32)};`)
.join("\n");
const coordDefines = Object.entries(coordTypeEncoding.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;
// traceImageFragShader

uniform sampler2D uSampler;
uniform ivec4 mapBboxVec;
uniform ivec4 tileBboxVec;
out vec4 fragColor;



/* Common ------------------------------------------------------------------------------ */
struct bbox {
ivec2 xy0;
ivec2 xy1;
};
struct edges {
bool N;
bool E;
bool S;
bool W;
};
bbox getBbox(ivec4 mapBboxVec) {
return bbox(mapBboxVec.xy, mapBboxVec.zw);
}
bool isInsideBbox(ivec2 xy, bbox box) {
return !(any(lessThan(xy, box.xy0)) || any(greaterThan(xy, box.xy1)));
}
edges getEdges(ivec2 xy, bbox mapBbox) {
edges mapEdges = edges(false, false, false, false);

mapEdges.N = (xy.y == mapBbox.xy0.y);
mapEdges.E = (xy.x == mapBbox.xy1.x);
mapEdges.S = (xy.y == mapBbox.xy1.y);
mapEdges.W = (xy.x == mapBbox.xy0.x);
return mapEdges;
}



/* Neighbor finder --------------------------------------------------------------------- */
${neighborsDefines.trim()}

float getEncodedNeighbors(ivec2 xyC, edges mapEdges) {
float encodedNeighbors = 0.0;
vec4 cC = texelFetch(uSampler, xyC, 0);
if (cC.a == 0.0) return encodedNeighbors;

vec4 cN = texelFetchOffset(uSampler, xyC, 0, ivec2( 0, -1));
vec4 cE = texelFetchOffset(uSampler, xyC, 0, ivec2( 1, 0));
vec4 cS = texelFetchOffset(uSampler, xyC, 0, ivec2( 0, 1));
vec4 cW = texelFetchOffset(uSampler, xyC, 0, ivec2(-1, 0));

if (mapEdges.N || cC != cN) encodedNeighbors += fN;
if (mapEdges.E || cC != cE) encodedNeighbors += fE;
if (mapEdges.S || cC != cS) encodedNeighbors += fS;
if (mapEdges.W || cC != cW) encodedNeighbors += fW;

return encodedNeighbors;
}



/* coordType detector ------------------------------------------------------------------ */
${coordDefines.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 isEdgeJunction(ivec2 xyC, edges mapEdges, directionColors dirColors) {
edges neighbors = edges(
dirColors.C.a + dirColors.W.a > 0.0,
dirColors.W.a + dirColors.SW.a > 0.0,
dirColors.N.a + dirColors.NW.a > 0.0,
dirColors.C.a + dirColors.N.a > 0.0
);
return ${dirKeys4.map(dir => `(mapEdges.${dir} && neighbors.${dir})`).join(" || ")};
}

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 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 isMaybeCorner(directionColors dirColors) {
return (dirColors.C != dirColors.W && dirColors.C != dirColors.NW && dirColors.C != dirColors.N);
}
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);
}

float getEncodedCoordType(ivec2 xyC, edges mapEdges) {
directions[4] allDirs = directions[4](${dirKeys4.map(k => `directions${k}`).join(", ")});
directionColors dirColorsN = readColors(xyC, directionsN);

// Edge junction test
if (isEdgeJunction(xyC, mapEdges, dirColorsN)) return fJunction;

// Junction test
if (isJunction(dirColorsN)) return fJunction;

// If we're on the south or west edge of the map, only junctions matter
if (mapEdges.S || mapEdges.W) return 0.0;


// Begin more expensive tests
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)) return fIntrusion;
}

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

// Default value
return 0.0;
}



/* main -------------------------------------------------------------------------------- */
const float fOpaque = ${(colorOpacityEncoding.values[255] / 255).toFixed(32)};

void main () {
fragColor = vec4(0.0);
ivec2 xyC = ivec2(round(gl_FragCoord.xy - 0.5));
vec4 cC = texelFetch(uSampler, xyC, 0);

bbox mapBbox = getBbox(mapBboxVec);
bbox tileBbox = getBbox(tileBboxVec);
// neighbors
float encodedNeighbors = 0.0;
edges mapEdges = getEdges(xyC, mapBbox);
if (cC.a != 0.0 && isInsideBbox(xyC, tileBbox)) encodedNeighbors = getEncodedNeighbors(xyC, mapEdges);

// coordType
float encodedCoordType = 0.0;
mapBbox.xy1 += ivec2(1);
mapEdges = getEdges(xyC, mapBbox);
if (mapEdges.S || mapEdges.W) tileBbox.xy1 += ivec2(1);
if (isInsideBbox(xyC, tileBbox)) encodedCoordType = getEncodedCoordType(xyC, mapEdges);

// If there's no trace data, exit
if (encodedNeighbors + encodedCoordType == 0.0) return;

// combine
float encodedColorOpacity = 0.0;
if (encodedNeighbors > 0.0) {
fragColor.rgb = cC.rgb;
encodedColorOpacity = fOpaque;
}

fragColor.a = encodedNeighbors + encodedCoordType + encodedColorOpacity;
}`);
}
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

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