Click anywhere on the plate to add impact points (seeds). Each click shatters the surface along the Voronoi boundaries — the cracks run exactly where two seeds are equidistant. You can add up to 30 seeds; use Reset to start fresh with a blank plate.
<!-- Voronoi shattering demo — click to seed, plate fractures along Voronoi edges -->
<div class="toolbar">
<span class="hint-text">Click on the plate to drop impact points — the surface shatters along Voronoi boundaries.</span>
<button id="btn-reset" type="button">Reset</button>
</div>
<canvas id="canvas" width="480" height="360" title="Click to add seed points and shatter the plate"></canvas>
<p class="seed-count" id="seed-count">Seeds: 0</p>
/* Layout and visual style for the Voronoi shattering demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: .5rem;
gap: .5rem;
flex-wrap: wrap;
}
.hint-text { font-size: .85rem; color: #555; flex: 1; }
button {
font: 600 13px system-ui, sans-serif;
padding: .35rem .8rem;
border: 1px solid #1d3557;
background: #1d3557;
color: #fff;
border-radius: 7px;
cursor: pointer;
white-space: nowrap;
}
button:hover { background: #16304e; }
/* Canvas fills container, crosshair cursor for precision clicking */
#canvas {
display: block;
width: 100%;
max-width: 480px;
border: 2px solid #c8d4de;
border-radius: 10px;
cursor: crosshair;
touch-action: none;
}
.seed-count { font-size: .8rem; color: #777; margin: .4rem 0 0; }
// Voronoi shattering: nearest-seed partition of the canvas into colored cells
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width;
const H = canvas.height;
const MAX_SEEDS = 30;
// Pastel color palette — one color per Voronoi cell (seed)
const PALETTE = [
'#c9e4f5','#f5e6c9','#d4f5c9','#f5c9d4','#e8c9f5',
'#c9f5ef','#f5f0c9','#c9cbf5','#f5d9c9','#d1f5c9',
'#f5c9ec','#c9dff5','#f0f5c9','#f5c9c9','#c9f5db',
'#f5e8c9','#d9c9f5','#c9f5f1','#f5cac9','#c9f5e5',
'#e5c9f5','#c9f3f5','#f5d4c9','#c9f5cd','#f5c9e0',
'#cef5c9','#f5c9f0','#c9ecf5','#f5f5c9','#f5c9de'
];
// Application state: array of seed points {x, y}
let seeds = [];
// Build the Voronoi diagram by painting each pixel the color of its nearest seed.
// This brute-force raster approach is O(W*H*n) but clear and correct for small n.
function buildVoronoiImage() {
// Write directly to pixel buffer for speed
const imgData = ctx.createImageData(W, H);
const data = imgData.data;
for (let y = 0; y < H; y++) {
for (let x = 0; x < W; x++) {
// Find the nearest seed by squared Euclidean distance
let minDist = Infinity;
let nearest = 0;
for (let i = 0; i < seeds.length; i++) {
const dx = x - seeds[i].x;
const dy = y - seeds[i].y;
const d = dx * dx + dy * dy;
if (d < minDist) { minDist = d; nearest = i; }
}
// Decode the hex color for this cell and write RGBA into the pixel buffer
const hex = PALETTE[nearest % PALETTE.length];
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
const idx = (y * W + x) * 4;
data[idx] = r; data[idx + 1] = g; data[idx + 2] = b; data[idx + 3] = 255;
}
}
ctx.putImageData(imgData, 0, 0);
}
// Overlay crack lines (Voronoi edges) and seed dots on top of the colored cells
function drawEdgesAndSeeds() {
// Read the painted image and detect color boundaries (Voronoi edges)
const imgData = ctx.getImageData(0, 0, W, H);
const d = imgData.data;
// Draw dark lines wherever adjacent pixels belong to different seeds
ctx.save();
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = 1;
// Scan every pixel; compare with right neighbor and bottom neighbor
for (let y = 0; y < H - 1; y++) {
for (let x = 0; x < W - 1; x++) {
const idxA = (y * W + x) * 4;
const idxB = (y * W + (x + 1)) * 4;
const idxC = ((y + 1) * W + x) * 4;
const diffH = Math.abs(d[idxA] - d[idxB]) + Math.abs(d[idxA + 1] - d[idxB + 1]) + Math.abs(d[idxA + 2] - d[idxB + 2]);
const diffV = Math.abs(d[idxA] - d[idxC]) + Math.abs(d[idxA + 1] - d[idxC + 1]) + Math.abs(d[idxA + 2] - d[idxC + 2]);
// Large color difference means we crossed a Voronoi boundary — draw a crack
if (diffH > 20) {
ctx.globalAlpha = 0.55;
ctx.fillStyle = '#1d3557';
ctx.fillRect(x, y, 2, 1);
}
if (diffV > 20) {
ctx.globalAlpha = 0.55;
ctx.fillStyle = '#1d3557';
ctx.fillRect(x, y, 1, 2);
}
}
}
ctx.globalAlpha = 1;
ctx.restore();
// Draw a small dot at each seed position (impact point)
seeds.forEach((s, i) => {
ctx.beginPath();
ctx.arc(s.x, s.y, 5, 0, Math.PI * 2);
ctx.fillStyle = '#1d3557';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1.5;
ctx.stroke();
});
}
function redraw() {
ctx.clearRect(0, 0, W, H);
if (seeds.length === 0) {
// No seeds yet — show a blank plate with instructions
ctx.fillStyle = '#eef3f7';
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = '#8aa0b0';
ctx.font = '15px system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('Click to add impact points', W / 2, H / 2);
ctx.textAlign = 'start';
return;
}
buildVoronoiImage();
drawEdgesAndSeeds();
}
function updateCount() {
document.getElementById('seed-count').textContent =
'Seeds: ' + seeds.length + ' / ' + MAX_SEEDS;
}
function addSeed(x, y) {
if (seeds.length >= MAX_SEEDS) return;
seeds.push({ x, y });
redraw();
updateCount();
}
// Mouse click handler — maps CSS pixels to canvas pixels via scale factors
canvas.addEventListener('click', function(e) {
const rect = canvas.getBoundingClientRect();
const scaleX = W / rect.width;
const scaleY = H / rect.height;
const cx = (e.clientX - rect.left) * scaleX;
const cy = (e.clientY - rect.top) * scaleY;
addSeed(Math.round(cx), Math.round(cy));
});
document.getElementById('btn-reset').addEventListener('click', function() {
seeds = [];
redraw();
updateCount();
});
// Initial render
redraw();
updateCount();
<!-- Voronoi shattering demo — click to seed, plate fractures along Voronoi edges -->
<div class="toolbar">
<span class="hint-text">Click on the plate to drop impact points — the surface shatters along Voronoi boundaries.</span>
<button id="btn-reset" type="button">Reset</button>
</div>
<canvas id="canvas" width="480" height="360" title="Click to add seed points and shatter the plate"></canvas>
<p class="seed-count" id="seed-count">Seeds: 0</p>
/* Layout and visual style for the Voronoi shattering demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: .5rem;
gap: .5rem;
flex-wrap: wrap;
}
.hint-text { font-size: .85rem; color: #555; flex: 1; }
button {
font: 600 13px system-ui, sans-serif;
padding: .35rem .8rem;
border: 1px solid #1d3557;
background: #1d3557;
color: #fff;
border-radius: 7px;
cursor: pointer;
white-space: nowrap;
}
button:hover { background: #16304e; }
/* Canvas fills container, crosshair cursor for precision clicking */
#canvas {
display: block;
width: 100%;
max-width: 480px;
border: 2px solid #c8d4de;
border-radius: 10px;
cursor: crosshair;
touch-action: none;
}
.seed-count { font-size: .8rem; color: #777; margin: .4rem 0 0; }
// Voronoi shattering: nearest-seed partition of the canvas into colored cells
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width;
const H = canvas.height;
const MAX_SEEDS = 30;
// Pastel color palette — one color per Voronoi cell (seed)
const PALETTE = [
'#c9e4f5','#f5e6c9','#d4f5c9','#f5c9d4','#e8c9f5',
'#c9f5ef','#f5f0c9','#c9cbf5','#f5d9c9','#d1f5c9',
'#f5c9ec','#c9dff5','#f0f5c9','#f5c9c9','#c9f5db',
'#f5e8c9','#d9c9f5','#c9f5f1','#f5cac9','#c9f5e5',
'#e5c9f5','#c9f3f5','#f5d4c9','#c9f5cd','#f5c9e0',
'#cef5c9','#f5c9f0','#c9ecf5','#f5f5c9','#f5c9de'
];
// Application state: array of seed points {x, y}
let seeds = [];
// Build the Voronoi diagram by painting each pixel the color of its nearest seed.
// This brute-force raster approach is O(W*H*n) but clear and correct for small n.
function buildVoronoiImage() {
// Write directly to pixel buffer for speed
const imgData = ctx.createImageData(W, H);
const data = imgData.data;
for (let y = 0; y < H; y++) {
for (let x = 0; x < W; x++) {
// Find the nearest seed by squared Euclidean distance
let minDist = Infinity;
let nearest = 0;
for (let i = 0; i < seeds.length; i++) {
const dx = x - seeds[i].x;
const dy = y - seeds[i].y;
const d = dx * dx + dy * dy;
if (d < minDist) { minDist = d; nearest = i; }
}
// Decode the hex color for this cell and write RGBA into the pixel buffer
const hex = PALETTE[nearest % PALETTE.length];
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
const idx = (y * W + x) * 4;
data[idx] = r; data[idx + 1] = g; data[idx + 2] = b; data[idx + 3] = 255;
}
}
ctx.putImageData(imgData, 0, 0);
}
// Overlay crack lines (Voronoi edges) and seed dots on top of the colored cells
function drawEdgesAndSeeds() {
// Read the painted image and detect color boundaries (Voronoi edges)
const imgData = ctx.getImageData(0, 0, W, H);
const d = imgData.data;
// Draw dark lines wherever adjacent pixels belong to different seeds
ctx.save();
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = 1;
// Scan every pixel; compare with right neighbor and bottom neighbor
for (let y = 0; y < H - 1; y++) {
for (let x = 0; x < W - 1; x++) {
const idxA = (y * W + x) * 4;
const idxB = (y * W + (x + 1)) * 4;
const idxC = ((y + 1) * W + x) * 4;
const diffH = Math.abs(d[idxA] - d[idxB]) + Math.abs(d[idxA + 1] - d[idxB + 1]) + Math.abs(d[idxA + 2] - d[idxB + 2]);
const diffV = Math.abs(d[idxA] - d[idxC]) + Math.abs(d[idxA + 1] - d[idxC + 1]) + Math.abs(d[idxA + 2] - d[idxC + 2]);
// Large color difference means we crossed a Voronoi boundary — draw a crack
if (diffH > 20) {
ctx.globalAlpha = 0.55;
ctx.fillStyle = '#1d3557';
ctx.fillRect(x, y, 2, 1);
}
if (diffV > 20) {
ctx.globalAlpha = 0.55;
ctx.fillStyle = '#1d3557';
ctx.fillRect(x, y, 1, 2);
}
}
}
ctx.globalAlpha = 1;
ctx.restore();
// Draw a small dot at each seed position (impact point)
seeds.forEach((s, i) => {
ctx.beginPath();
ctx.arc(s.x, s.y, 5, 0, Math.PI * 2);
ctx.fillStyle = '#1d3557';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1.5;
ctx.stroke();
});
}
function redraw() {
ctx.clearRect(0, 0, W, H);
if (seeds.length === 0) {
// No seeds yet — show a blank plate with instructions
ctx.fillStyle = '#eef3f7';
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = '#8aa0b0';
ctx.font = '15px system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('Click to add impact points', W / 2, H / 2);
ctx.textAlign = 'start';
return;
}
buildVoronoiImage();
drawEdgesAndSeeds();
}
function updateCount() {
document.getElementById('seed-count').textContent =
'Seeds: ' + seeds.length + ' / ' + MAX_SEEDS;
}
function addSeed(x, y) {
if (seeds.length >= MAX_SEEDS) return;
seeds.push({ x, y });
redraw();
updateCount();
}
// Mouse click handler — maps CSS pixels to canvas pixels via scale factors
canvas.addEventListener('click', function(e) {
const rect = canvas.getBoundingClientRect();
const scaleX = W / rect.width;
const scaleY = H / rect.height;
const cx = (e.clientX - rect.left) * scaleX;
const cy = (e.clientY - rect.top) * scaleY;
addSeed(Math.round(cx), Math.round(cy));
});
document.getElementById('btn-reset').addEventListener('click', function() {
seeds = [];
redraw();
updateCount();
});
// Initial render
redraw();
updateCount();
Notice how every crack is a straight line segment — a piece of the perpendicular bisector between two seeds. Three cracks always meet at a single point (a Voronoi vertex), which is equidistant from exactly three seeds. The more seeds you add, the finer the fracture — just like real glass shatters into smaller fragments near an impact point.
Comments
Loading comments...