Published
Edited
Jul 15, 2021
1 star
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
mutable voronoiPoints = []
Insert cell
{
const context = DOM.context2d(imageWidth, imageHeight, 1);
context.drawImage(image, 0, 0, imageWidth, imageHeight);

const img = await imageJS.Image.fromCanvas(context.canvas);

// lowThreshold: Low threshold for the hysteresis procedure (default: 10).
// highThreshold: High threshold for the hysteresis procedure (default: 30).
// gaussianBlur: Sigma parameter for the gaussian filter step (default: 1.1).

const grey = img.grey();

const defaultOptions = {
lowThreshold: 10,
highThreshold: 30,
gaussianBlur: 1.1
};

const edges = grey.cannyEdge({
...defaultOptions,
lowThreshold: 30,
highThreshold: 30,
gaussianBlur: 2.0
});

const contextOut = DOM.context2d(imageWidth, imageHeight, 1);

edges.data.forEach((d, i) => {
const x = i % img.width;
const y = Math.floor(i / img.width);
contextOut.beginPath();
contextOut.fillStyle = d === 0 ? `white` : `red`;
contextOut.fillRect(x, y, 1, 1);
});

const voronoiPointsAll = getCells(100).map((cell) => {
const { x, y, width, height } = cell;
const pixels = getPixels2({
context: contextOut,
x,
y,
width,
height
});
const average = d3.mean(pixels, (d) => d.lightness);
return [x, y, average];
});

mutable voronoiPoints = voronoiPointsAll;

const voronoiPointsFiltered = voronoiPointsAll.filter((d) => d[2] < 255);

const voronoiCells = getVoronoiCells(voronoiPointsFiltered, {
width: imageWidth,
height: imageHeight
});

const voronoiPaths = voronoiCells.map(
(d) => htl.svg`<path fill="none" stroke="black" d=${d.pathDefinition} />`
);

return htl.html`<svg viewBox=${`0 0 ${imageWidth} ${imageHeight}`} style="border: 1px dashed black; width: 400px">${voronoiPaths}</svg>`;

// contextOut.clearRect(0, 0, imageWidth, imageHeight);

// voronoiCells.forEach((cell) => {
// const path = new Path2D(cell.pathDefinition);
// contextOut.strokeStyle = "black";
// contextOut.stroke(path);
// });

// return contextOut.canvas;
}
Insert cell
{
// const voronoiCells = getVoronoiCells(voronoiPoints, {
// width: imageWidth,
// height: imageHeight
// });

// const voronoiPaths = voronoiCells.map(
// (d) => htl.svg`<path fill="none" stroke="black" d=${d.pathDefinition} />`
// );

const colorScale = d3.scaleLinear([0, 255], ["white", "black"]);
const bumpMax = 100;
const bumpScale = d3.scaleLinear([0, 255], [bumpMax, 0]);

const dots = voronoiPoints.map(
([x, y, average]) =>
htl.svg`<circle r="2" cx=${x} cy=${y} fill=${colorScale(average)} />`
);

const lines = d3
.groups(voronoiPoints, (d) => d[1])
.map(([rowY, values]) => {
const chunks = values
.map(([x, y, lightness]) => {
const bump = bumpScale(lightness);
return `L ${x} ${y + bump}`;
})
.join(" ");
return `M 0 ${rowY} ${chunks}`;
})
.map((pathDef) => {
return htl.svg`<path fill="none" stroke="black" d=${pathDef} />`;
});

// return lines;
const children = lines;

return htl.html`<svg viewBox=${`0 0 ${imageWidth} ${imageHeight}`} style="border: 1px dashed black; width: 400px">${children}</svg>`;
}
Insert cell
Insert cell
imageJS = import("https://cdn.skypack.dev/image-js")
Insert cell
imagetracer = require("imagetracerjs")
Insert cell
function getVoronoiCells(points, { width = 100, height = 100 } = {}) {
const voronoi = d3.Delaunay.from(points).voronoi([0, 0, width, height]);
const cells = points.map((point, i) => {
const pathDefinition = voronoi.renderCell(i);
return { pathDefinition, point };
});
return cells;
}
Insert cell
function getPixels2({ context, x, y, width, height }) {
const imageData = context.getImageData(x, y, width, height);
const pixels = imageData.data;
const out = [];
const channels = ["r", "g", "b", "a"];
let current = {};
for (const [index, pixel] of pixels.entries()) {
const mod = index % 4;
const channel = channels[mod];
current[channel] = pixel;
if (mod === 3) {
const column = index % width;
const row = Math.floor(index / width);
const { r, g, b } = current;
const lightness = Math.floor((r + g + b) / 3);
out.push({
...current,
x: x + column,
y: y + row,
lightness
});
current = {};
}
}
return out;
}
Insert cell
function getPixels({ context, x, y, width, height, sample = 0.5 }) {
const imageData = context.getImageData(x, y, width, height);
const pixels = imageData.data;
const out = [];
const keys = ["r", "g", "b", "a"];
let current = {};
pixels.forEach((d, i) => {
const mod = i % 4;
const key = keys[mod];
current[key] = d;
if (mod === 3) {
if (Math.random() > sample) {
current = {};
return;
}
const pixelIndex = Math.floor(i / 4);
const column = pixelIndex % width;
const row = Math.floor(pixelIndex / width);
Object.assign(current, {
index: pixelIndex,
column,
row,
x: x + column,
y: y + row
});
const { r, g, b } = current;
const lightness = Math.floor((r + g + b) / 3);
const grayscale = `rgb(${lightness}, ${lightness}, ${lightness})`;
out.push({ ...current, lightness, grayscale });
current = {};
}
});
return out;
}
Insert cell
function getCells(columns = 50) {
const aspectRatio = imageWidth / imageHeight;
const rows = Math.ceil(columns / aspectRatio);
const cellWidth = Math.ceil(imageWidth / columns);
const cellHeight = Math.ceil(imageHeight / rows);
const numCells = columns * rows;
const cells = Array.from({ length: numCells }).map((d, i) => {
const column = i % columns;
const row = Math.floor(i / columns);
return {
x: column * cellWidth,
y: row * cellHeight,
column,
row,
width: cellWidth,
height: cellHeight
};
});
return cells;
}
Insert cell
function renderWithTransform({ func, columns, sample }) {
const context = DOM.context2d(imageWidth, imageHeight, 1);
context.canvas.style.width = "400px";
context.canvas.style.border = "1px solid black";
context.drawImage(image, 0, 0, imageWidth, imageHeight);

const outContext = DOM.context2d(imageWidth, imageHeight, 1);
outContext.canvas.style.width = context.canvas.style.width;

const withPixels = getCells(columns).map((cell, index) => {
const pixels = getPixels({
...cell,
context,
sample
});
return {
...cell,
pixels: pixels
};
});

const transformed = func({ cells: withPixels });

transformed.forEach((cell, index) => {
const { x, y, width, height, fillStyle } = cell;
outContext.beginPath();
outContext.fillStyle = fillStyle;
outContext.fillRect(x, y, width, height);
});

outContext.canvas.cells = transformed;

return outContext.canvas;
}
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