// Voronoi 2D
//
// Classic 2D Voronoi diagram: scatter N seed points, color each pixel
// by its nearest seed using the chosen distance metric.
//
// Distance metrics:
// Euclidean — circular cells (standard Voronoi)
// Manhattan — diamond-shaped cells
// Chebyshev — square cells
//
// Performance: sample every `step` pixels and fill a step×step block.
export const PARAMS = {
count: { value: 30, min: 5, max: 200, step: 1, label: "Seed Points", folder: "Structure", rebuildOnChange: true },
metric: { value: "Euclidean", options: ["Euclidean", "Manhattan", "Chebyshev"], label: "Distance Metric", folder: "Structure" },
step: { value: 4, min: 1, max: 16, step: 1, label: "Sample Step (px)", folder: "Performance" },
hue: { value: 200, min: 0, max: 360, step: 1, label: "Base Hue", folder: "Appearance" },
hueSpread: { value: 240, min: 0, max: 360, step: 1, label: "Hue Spread", folder: "Appearance" },
showSeeds: { value: true, label: "Show Seed Points", folder: "Appearance" },
showEdges: { value: false, label: "Show Cell Edges", folder: "Appearance" },
};
// HSL to RGB conversion (all values 0–255)
function hslToRgb(h, s, l) {
// h: 0-360, s: 0-1, l: 0-1
const c = (1 - Math.abs(2 * l - 1)) * s;
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
const m = l - c / 2;
let r = 0, g = 0, b = 0;
if (h < 60) { r = c; g = x; b = 0; }
else if (h < 120) { r = x; g = c; b = 0; }
else if (h < 180) { r = 0; g = c; b = x; }
else if (h < 240) { r = 0; g = x; b = c; }
else if (h < 300) { r = x; g = 0; b = c; }
else { r = c; g = 0; b = x; }
return [
Math.round((r + m) * 255),
Math.round((g + m) * 255),
Math.round((b + m) * 255),
];
}
// Distance functions
function distEuclidean(x1, y1, x2, y2) {
const dx = x1 - x2, dy = y1 - y2;
return dx * dx + dy * dy; // squared is fine for comparison
}
function distManhattan(x1, y1, x2, y2) {
return Math.abs(x1 - x2) + Math.abs(y1 - y2);
}
function distChebyshev(x1, y1, x2, y2) {
return Math.max(Math.abs(x1 - x2), Math.abs(y1 - y2));
}
// Module-level state — p5 runtime does NOT pass state to sketchDraw
let state;
export function sketchSetup(p, w, h) {
p.pixelDensity(1);
// Generate max possible seeds up front; sketchDraw will slice to params.count
const MAX_SEEDS = 200;
const seeds = [];
for (let i = 0; i < MAX_SEEDS; i++) {
seeds.push({
x: p.random(w),
y: p.random(h),
hueOffset: p.random(360),
});
}
state = { seeds, w, h };
return state;
}
export function sketchDraw(p, w, h, params) {
if (!state) return;
const { seeds } = state;
const count = Math.min(params.count, seeds.length);
const activeSeed = seeds.slice(0, count);
const step = params.step;
let distFn;
if (params.metric === "Manhattan") distFn = distManhattan;
else if (params.metric === "Chebyshev") distFn = distChebyshev;
else distFn = distEuclidean;
// pixelDensity(1) MUST be called before loadPixels so pixel array is sized correctly
p.pixelDensity(1);
p.loadPixels();
for (let y = 0; y < h; y += step) {
for (let x = 0; x < w; x += step) {
// Find nearest seed
let minDist = Infinity;
let nearestIdx = 0;
for (let i = 0; i < count; i++) {
const d = distFn(x, y, activeSeed[i].x, activeSeed[i].y);
if (d < minDist) {
minDist = d;
nearestIdx = i;
}
}
// Color from nearest seed's hue — hueOffset is already 0-360, spread maps it to hueSpread range
const hue = (params.hue + (activeSeed[nearestIdx].hueOffset / 360) * params.hueSpread) % 360;
const [r, g, b] = hslToRgb(hue, 0.7, 0.45);
// Edge detection (optional): check neighbor pixel
let isEdge = false;
if (params.showEdges && (x + step < w || y + step < h)) {
const nx = x + step, ny = y;
let minD2 = Infinity, nearIdx2 = 0;
for (let i = 0; i < count; i++) {
const d = distFn(nx, ny, activeSeed[i].x, activeSeed[i].y);
if (d < minD2) { minD2 = d; nearIdx2 = i; }
}
if (nearIdx2 !== nearestIdx) isEdge = true;
if (!isEdge) {
const nx2 = x, ny2 = y + step;
let minD3 = Infinity, nearIdx3 = 0;
for (let i = 0; i < count; i++) {
const d = distFn(nx2, ny2, activeSeed[i].x, activeSeed[i].y);
if (d < minD3) { minD3 = d; nearIdx3 = i; }
}
if (nearIdx3 !== nearestIdx) isEdge = true;
}
}
const fr = isEdge ? 255 : r;
const fg = isEdge ? 255 : g;
const fb = isEdge ? 255 : b;
// Fill block
for (let dy = 0; dy < step && y + dy < h; dy++) {
for (let dx = 0; dx < step && x + dx < w; dx++) {
const idx = ((y + dy) * w + (x + dx)) * 4;
p.pixels[idx] = fr;
p.pixels[idx + 1] = fg;
p.pixels[idx + 2] = fb;
p.pixels[idx + 3] = 255;
}
}
}
}
p.updatePixels();
// Draw seed points
if (params.showSeeds) {
p.noStroke();
p.fill(255);
for (let i = 0; i < count; i++) {
p.circle(activeSeed[i].x, activeSeed[i].y, 8);
}
p.fill(0);
for (let i = 0; i < count; i++) {
p.circle(activeSeed[i].x, activeSeed[i].y, 4);
}
}
}