voronoi
Drifting Voronoi cells with shifting colors.
p5.js
import p5 from 'p5';
import { hslToRgb } from '../../../utils/color';
new p5((p: p5) => {
let pts: { x: number; y: number; vx: number; vy: number; hue: number; hueSpeed: number }[] = [];
let buf: p5.Graphics;
let prevCells = 0;
const c = controls({
cells: { label: 'cells', min: 5, max: 150, value: 50 },
speed: { label: 'speed', min: 1, max: 30, rawValue: 10, value: 1.0, parse: (r) => parseInt(r) / 10, format: (v) => v.toFixed(1) },
hueshift: { label: 'hue shift', min: 0, max: 100, rawValue: 50, value: 0.50, parse: (r) => parseInt(r) / 100, format: (v) => v.toFixed(2) },
edge: { label: 'edge width', min: 1, max: 20, rawValue: 5, value: 0.05, parse: (r) => parseInt(r) / 100, format: (v) => v.toFixed(2) },
res: { label: 'resolution', min: 1, max: 8, value: 4 },
});
function initPoints(count: number) {
pts = [];
for (let i = 0; i < count; i++) {
pts.push({
x: p.random(p.width),
y: p.random(p.height),
vx: p.random(-1, 1),
vy: p.random(-1, 1),
hue: (i / count) * 360,
hueSpeed: p.random(0.2, 0.8),
});
}
prevCells = count;
}
function rebuildBuffer() {
buf = p.createGraphics(Math.ceil(p.width / c.res), Math.ceil(p.height / c.res));
buf.pixelDensity(1);
}
p.setup = () => {
const el = document.getElementById('canvas')!.parentElement!;
const cv = p.createCanvas(el.clientWidth, el.clientHeight);
cv.parent(el);
document.getElementById('canvas')?.remove();
p.noSmooth();
rebuildBuffer();
initPoints(c.cells);
};
p.draw = () => {
// Sync point count
if (c.cells !== prevCells) {
if (c.cells > prevCells) {
for (let i = prevCells; i < c.cells; i++) {
pts.push({
x: p.random(p.width),
y: p.random(p.height),
vx: p.random(-1, 1),
vy: p.random(-1, 1),
hue: (i / c.cells) * 360,
hueSpeed: p.random(0.2, 0.8),
});
}
} else {
pts.length = c.cells;
}
prevCells = c.cells;
}
// Rebuild buffer if resolution changed
const targetW = Math.ceil(p.width / c.res);
if (buf.width !== targetW) rebuildBuffer();
const scale = c.res;
const bw = buf.width;
const bh = buf.height;
const t = p.frameCount * 0.01;
const edgeThreshold = c.edge;
const numPts = pts.length;
buf.loadPixels();
const px = buf.pixels;
for (let by = 0; by < bh; by++) {
for (let bx = 0; bx < bw; bx++) {
const wx = bx * scale;
const wy = by * scale;
let minDist = Infinity;
let secondDist = Infinity;
let closest = 0;
for (let i = 0; i < numPts; i++) {
const dx = wx - pts[i].x;
const dy = wy - pts[i].y;
const dist = dx * dx + dy * dy;
if (dist < minDist) {
secondDist = minDist;
minDist = dist;
closest = i;
} else if (dist < secondDist) {
secondDist = dist;
}
}
const pt = pts[closest];
const d1 = Math.sqrt(minDist);
const d2 = Math.sqrt(secondDist);
const edgeDist = (d2 - d1) / (d2 + d1 + 1);
const h = ((pt.hue + d1 * 0.3 + Math.sin(t + wx * 0.01) * 20) % 360 + 360) % 360;
const s = (70 + edgeDist * 25) / 100;
const l = edgeDist < edgeThreshold
? (5 + edgeDist * (100 / (edgeThreshold / 0.05))) / 100
: (15 + edgeDist * 35) / 100;
const [r, g, b] = hslToRgb(h / 360, s, l);
const idx = (by * bw + bx) * 4;
px[idx] = r;
px[idx + 1] = g;
px[idx + 2] = b;
px[idx + 3] = 255;
}
}
buf.updatePixels();
p.image(buf, 0, 0, p.width, p.height);
const spd = c.speed;
const hueSpd = c.hueshift;
for (const pt of pts) {
pt.x += (pt.vx + Math.sin(t * 2 + pt.hue) * 0.3) * spd;
pt.y += (pt.vy + Math.cos(t * 2 + pt.hue) * 0.3) * spd;
if (pt.x < 0 || pt.x > p.width) pt.vx *= -1;
if (pt.y < 0 || pt.y > p.height) pt.vy *= -1;
pt.x = p.constrain(pt.x, 0, p.width);
pt.y = p.constrain(pt.y, 0, p.height);
pt.hue = (pt.hue + pt.hueSpeed * hueSpd) % 360;
}
};
});