Published
Edited
May 24, 2018
2 forks
1 star
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
const width = resolution, height = resolution
let canvas = DOM.canvas(width,height),
context = canvas.getContext("2d")

canvas.style.width = "400px";
canvas.style.imageRendering = "pixelated"

let projection = d3.geoMercator().fitExtent([[0,0], [width,height]], district)
let time = Date.now()
let monochrome = renderGeoToBitmap(district, projection, width, height)
let unantialiased = threshold(monochrome)
let eroded = erode(unantialiased, width, height)

mutable renderTime = Date.now() - time
// draw our target value in black
eroded = Uint8Array.from(eroded, d => (d == thresholdValue) ? 255: d)
// amplify for visual output
let amplified = Uint8ClampedArray.from(eroded, val => val * 255 * 4 / resolution)
let imgData = new ImageData(monochromeToMultichannel(amplified), width, height)
context.putImageData(imgData,0,0)
return canvas

}
Insert cell
function renderGeoToCanvas(context, geo, projection, fillStyle='#000000') {
let pathGenerator = d3.geoPath().projection(projection).context(context)
context.save();
context.fillStyle = fillStyle;
context.beginPath();
pathGenerator(district)
context.fill();
context.restore();
}
Insert cell
function renderGeoToBitmap(geo, projection, width, height, fillStyle='#000000') {
let canvas = DOM.canvas(width,height),
context = canvas.getContext("2d")
renderGeoToCanvas(context, geo, projection)
return imageChannel(context.getImageData(0, 0, width, height).data)
}
Insert cell
function maxNeighbours(imgData,x,y,width,height) {
/*
neighbours = [
1,1,1
1, 1,
1,1,1
]
0 ... outside polygon
255 ... inside polygon
*/
let max=-1;
// cylce through 8 neighbours by adding offsets -1..1 to x,y location
for (let dy=-1; dy<=1; dy++) {
for (let dx=-1; dx<=1; dx++) {
// ignore actual pixel
if (dx == 0 && dy == 0) continue;
let px = x+dx,
py = y+dy;
// check image boundaries
if (px<0 || py<0 || px>=width || py>=height) continue;
let val = imgData[py*width + px];
// 255 is value for "inside, but not yet processed", so set to -1 to be ingored/wrapped
if (val != 255 && val > max) max = val;
}
}
// return the maximum of all neighbours, increased by 1
// interior values will be -1, so will return 0 for an interior point without neighbours
return max+1;
}

Insert cell
function erode(imgData, width, height) {

// we can't write to the image we read from in the same pass to not disturb the data
// so create two images
let imgRead = Uint8ClampedArray.from(imgData)
let imgWrite = Uint8ClampedArray.from(imgData)
let goOn = true
while (goOn) {
goOn = false
for (let y=0; y<height; y++) {
for (let x=0; x<width; x++) {
let idx = y*width+x;
if (imgRead[idx] == 255) {
let val = maxNeighbours(imgRead, x, y, width, height)
if (val) {
// clamp at alpha=254, to not overrule original polygon
imgWrite[idx] = Math.min(val,254)
}
else {
goOn = true
}
}
}
}
// copy image for next iteration
if (goOn) imgRead.set(imgWrite)
}
return imgWrite
}
Insert cell
function threshold(imgData, threshold=254) {
return Uint8Array.from(imgData, d => (d > threshold) ? 255: 0)
}
Insert cell
function imageChannel(imgData, channel=3, numChannels=4) {
console.assert(imgData.length % numChannels == 0, "Image does not have " + numChannels + " channels!")
let newData = new Uint8Array(imgData.length / numChannels)
for (let i=0; i<newData.length; i++) {
newData[i] = imgData[i*numChannels+channel];
}
return newData;
}
Insert cell
function monochromeToMultichannel(imgData, channel=3, numChannels=4, destImage=null) {
if (!destImage) destImage = new Uint8ClampedArray(imgData.length*numChannels)
for (let i=0; i<imgData.length; i++) {
destImage[i*numChannels+channel] = imgData[i];
}
return destImage;
}
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

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