Published
Edited
May 30, 2022
1 fork
21 stars
Insert cell
Insert cell
portfolio = {
for (;;) {
for (let promise of imagePromises) {
await visibility();
yield await promise;
await Promises.delay(10000);
}
}
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
mutable resetFileInput = null
Insert cell
{
const img = html`<img />`;
img.src = await Files.url(customImg);
mutable image = img;
mutable resetFileInput = mutable resetFileInput + 1;
return `Image ${mutable resetFileInput} Loaded`;
}
Insert cell
Insert cell
//
// Obtain an ImageData object from image
//
imgdata = imageData(image)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
//
// Average color and color error for a given imagedata within a given rectangle
//
function getStat(imgdata, x0, y0, w, h) {
const {data, width, height} = imgdata;
const getPixel = (x, y) => {
const offset = (y * width + x) * 4;
return data.slice(offset, offset + 3);
};
let sum = [0, 0, 0];
for (let x = x0; x < x0 + w; ++x) {
for (let y = y0; y < y0 + h; ++y) {
const pix = getPixel(x, y);
sum[0] += pix[0];
sum[1] += pix[1];
sum[2] += pix[2];
}
}
const npix = w * h;
const avg = sum.map((comp) => comp / npix);
let maxErr = 0;
for (let x = x0; x < x0 + w; ++x) {
for (let y = y0; y < y0 + h; ++y) {
const pix = getPixel(x, y);
maxErr = Math.max(
maxErr,
(pix[0] - avg[0]) ** 2 + (pix[1] - avg[1]) ** 2 + (pix[2] - avg[2]) ** 2
);
}
}
return { avg, maxErr };
}
Insert cell
//
// Returns an array of quadrants from building a quadtree on imagedata 'data', such
// that no quadrant has size smaller than minSz and all quadrants have less than the maximum color
// error 'epsilon'.
//
function quadrants(imgdata, epsilon, minSz, maxSz=1024) {
const { data, width, height } = imgdata;
const quads = [];
const visit = (x0, y0, w, h) => {
let { avg, maxErr } = getStat(imgdata, x0, y0, w, h);
if ((Math.max(w, h) <= minSz || maxErr < epsilon) && Math.max(w, h) <= maxSz) {
quads.push({
x0,
y0,
w,
h,
center: [x0 + w / 2, y0 + h / 2],
avg,
maxErr
});
} else {
if (w >= h) {
let sz = 1 << ~~Math.log2(w);
if (sz == w) sz /= 2;
let [l, r, wl, wr] = [x0, x0 + sz, sz, w - sz];
if (h > sz) {
visit(l, y0, wl, sz);
visit(r, y0, wr, sz);
visit(l, y0 + sz, wl, h - sz);
visit(r, y0 + sz, wr, h - sz);
} else {
visit(l, y0, wl, h);
visit(r, y0, wr, h);
}
} else {
let sz = 1 << ~~Math.log2(h);
if (sz == h) sz /= 2;
let [t, b, ht, hb] = [y0, y0 + sz, sz, h - sz];
if (w > sz) {
visit(x0, t, sz, ht);
visit(x0, b, sz, hb);
visit(x0 + sz, t, w - sz, ht);
visit(x0 + sz, b, w - sz, hb);
} else {
visit(x0, t, w, ht);
visit(x0, b, w, hb);
}
}
}
};
visit(0, 0, width, height);
return quads;
}
Insert cell
quads = quadrants(imgdata, sqrtEpsilon ** 2, 1 << logMinSz, 1 << logMaxSz)
Insert cell
Insert cell
{
const del = d3.Delaunay.from(quads.map((q) => q.center));
const { width, height } = imgdata;
const vor = del.voronoi([0, 0, width, height]);
const ctx = DOM.context2d(width, height, 1);
ctx.strokeStyle = "black";
vor.render(ctx);
vor.renderBounds(ctx);
ctx.stroke();
return ctx.canvas;
}
Insert cell
Insert cell
{
const { width, height } = imgdata;
const ctx = DOM.context2d(width, height, 1);
const n = width * height;
const imgdata2 = new ImageData(width, height);
const data = imgdata2.data;
for (let i = 0; i < n; i++) {
const offset = i * 4;
const val = Math.round(luminance[i] * 255);
data[offset] = data[offset + 1] = data[offset + 2] = val;
data[offset + 3] = 255;
}
ctx.putImageData(imgdata2, 0, 0);
return ctx.canvas;
}
Insert cell
luminance = {
const { width, height, data } = imgdata;
const n = width * height;
const lumArray = new Float64Array(n);
for (let i = 0; i < n; i++) {
const offset = i * 4;
lumArray[i] =
(data[offset] * 0.2126 +
data[offset + 1] * 0.7152 +
data[offset + 2] * 0.0722) /
255;
}
return lumArray;
}
Insert cell
Insert cell
{
const { width, height } = imgdata;
const ctx = DOM.context2d(width, height, 1);
const n = width * height;
const imgdata2 = new ImageData(width, height);
const data = imgdata2.data;
for (let i = 0; i < n; i++) {
const offset = i * 4;
const val = Math.round((1 - lumGradient[i]) * 255);
data[offset] = data[offset + 1] = data[offset + 2] = val;
data[offset + 3] = 255;
}
ctx.putImageData(imgdata2, 0, 0);
return ctx.canvas;
}
Insert cell
lumGradient = {
const { width, height } = imgdata;
const n = width * height;
const lumGrad = new Float64Array(n);
const getLum = (x, y) => luminance[y * width + x];
for (let x = 0; x < width; x++) {
let left = Math.max(0, x - 1);
let right = Math.min(width - 1, x + 1);
for (let y = 0; y < height; y++) {
let up = Math.max(0, y - 1);
let down = Math.min(height - 1, y + 1);
lumGrad[y * width + x] = Math.sqrt(
(getLum(right, y) - getLum(left, y)) ** 2 +
(getLum(x, up) - getLum(x, down)) ** 2
);
}
}
return lumGrad;
}
Insert cell
Insert cell
Insert cell
viewof centroidal = {
const { width, height, data } = imgdata;
const n = quads.length;
const points = new Float64Array(n * 2);
quads.forEach(({ center }, i) => {
points[i * 2] = center[0];
points[i * 2 + 1] = center[1];
});
const del = new d3.Delaunay(points);
const vor = del.voronoi([0, 0, width, height]);
const c = new Float64Array(n * 2);
const s = new Float64Array(n);
const getDensity = (x, y) => lumGradient[y * width + x];
const getCentroids = () => {
// Compute the weighted centroid for each Voronoi cell.
c.fill(0);
s.fill(0);
for (let y = 0, i = 0; y < height; ++y) {
for (let x = 0; x < width; ++x) {
const w = getDensity(x, y);
i = del.find(x + 0.5, y + 0.5, i);
s[i] += w;
c[i * 2] += w * (x + 0.5);
c[i * 2 + 1] += w * (y + 0.5);
}
}
for (let i = 0; i < n; ++i) {
const x0 = points[i * 2],
y0 = points[i * 2 + 1];
const x1 = s[i] ? c[i * 2] / s[i] : x0,
y1 = s[i] ? c[i * 2 + 1] / s[i] : y0;
points[i * 2] = x1;
points[i * 2 + 1] = y1;
}
};
const jitter = () => {
for (let i = 0; i < n; ++i) {
// This is necessary to get around a bug in voronoi when centers are aligned in a grid
const x = points[i * 2],
y = points[i * 2 + 1];
points[i * 2] = x + Math.random() * 0.5 - 0.25;
points[i * 2 + 1] = y + Math.random() * 0.5 - 0.25;
}
};
let ctx = DOM.context2d(width, height, 1);
ctx.canvas.value = null;
ctx.strokeStyle = "black";
for (let i = 0; i < lloydIterations; i++) {
getCentroids();
if (i == lloydIterations - 1) jitter();
vor.update();
ctx.clearRect(0, 0, width, height);
ctx.beginPath();
vor.render(ctx);
vor.renderBounds(ctx);
ctx.stroke();
for (let i = 0; i < n; i++) {
ctx.beginPath();
ctx.arc(points[i * 2], points[i * 2 + 1], 1, 0, Math.PI * 2);
ctx.fill();
}
yield ctx.canvas;
}
ctx.canvas.value = vor;
ctx.canvas.dispatchEvent(new CustomEvent("input"));
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
imagePromises = [
FileAttachment("flower2.png").image(),
FileAttachment("butterfly1.png").image(),
FileAttachment("cat1.png").image(),
FileAttachment("flower1.png").image(),
FileAttachment("fruit1.png").image(),
FileAttachment("hanger.png").image(),
FileAttachment("neon.png").image(),
FileAttachment("origami1.png").image(),
FileAttachment("painting1.png").image(),
FileAttachment("portrait1.png").image()
]
Insert cell
Insert cell
import {
image as randomImg,
imageData,
viewof imageConfig
} from "@esperanc/voronoi-mosaics"
Insert cell
import { file } from "@jashkenas/inputs"
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