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

context.canvas.style.width = "400px";
context.canvas.style.border = "1px solid black";

const grid = getCells(100).map((cell) => {
const { x, y, width, height } = cell;
const pixels = getPixels2({
context,
x,
y,
width,
height
});
const meanLightness = d3.mean(pixels, (d) => d.lightness);
return {
...cell,
meanLightness
};
});

const columns = d3.max(grid, (d) => d.column) + 1;
const rows = d3.max(grid, (d) => d.row) + 1;

const xScale = d3.scaleLinear([0, columns], [0, imageWidth]);
const yScale = d3.scaleLinear([0, rows], [0, imageHeight]);

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

// grid.forEach((d) => {
// const { x, y, width, height, meanLightness } = d;
// context.beginPath();
// context.fillStyle = d3
// .color("red")
// .copy({ opacity: d3.scaleLinear([0, 255], [0, 1])(meanLightness) });
// context.fillRect(x, y, width, height);
// });

const contours = d3
.contours()
// .thresholds([0, 10, 50, 100, 150, 200, 240, 255])
.thresholds(d3.range(0, 300, 20))
.size([columns, rows])(grid.map((d) => d.meanLightness));

// Re-scale in place
contours.forEach(({ coordinates }) => {
coordinates.forEach((lineStrings) => {
lineStrings.forEach((points) => {
points.forEach((point) => {
point[0] = xScale(point[0]);
point[1] = yScale(point[1]);
});
});
});
});

mutable contours_ = contours;

const contourPaths = contours
.map((geoJSON) => d3.geoPath()(geoJSON))
.filter((d) => d)
.map(
(pathDef) => htl.svg`<path fill="none" stroke="black" d=${pathDef} />`
);

// return contourPaths;

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

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

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more