
Prompt
Create an interactive voronoi mesh of different random colours and where the user tap-dragging or click-dragging moves the nearest point around, triggering a smooth regeneration of the mesh
A system prompt was added to support web rendering
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Interactive Voronoi Mesh</title>
<style>
html,body{
margin:0;
height:100%;
overflow:hidden;
background:#111;
}
canvas{
display:block;
width:100%;
height:100%;
touch-action:none; /* disable default touch behaviours */
cursor:grab;
}
canvas:active{
cursor:grabbing;
}
</style>
<!-- D3 v7 -->
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
</head>
<body>
<canvas id="voronoi"></canvas>
<script>
(function () {
// --------------------------------------------------
// basic sizing
const canvas = document.getElementById('voronoi');
const ctx = canvas.getContext('2d');
let width = canvas.width = window.innerWidth;
let height = canvas.height = window.innerHeight;
// --------------------------------------------------
// parameters
const POINT_COUNT = 80;
let points = [];
let colors = [];
// random helpers
const rand = n => Math.random() * n;
const randColor = () =>
`hsl(${Math.random()*360},${60 + Math.random()*30}%,${40 + Math.random()*20}%)`;
// populate initial points & colours
function initPoints() {
points = Array.from({length: POINT_COUNT}, () => [rand(width), rand(height)]);
colors = Array.from({length: POINT_COUNT}, randColor);
}
initPoints();
// --------------------------------------------------
// Voronoi calculation & drawing
let voronoi, delaunay;
function computeVoronoi() {
delaunay = d3.Delaunay.from(points);
voronoi = delaunay.voronoi([0,0,width,height]);
}
function draw() {
ctx.clearRect(0,0,width,height);
ctx.lineWidth = 0.8;
ctx.strokeStyle = '#222';
// draw each cell
for (let i=0; i<points.length; ++i){
const cell = voronoi.cellPolygon(i);
if (!cell) continue;
ctx.beginPath();
ctx.moveTo(cell[0][0], cell[0][1]);
for (let j=1; j<cell.length; ++j){
ctx.lineTo(cell[j][0], cell[j][1]);
}
ctx.closePath();
ctx.fillStyle = colors[i];
ctx.fill();
ctx.stroke();
}
}
// --------------------------------------------------
// interaction (mouse + touch)
let draggingIndex = null;
function pointerPos(event){
if (event.touches) { // touch
return [event.touches[0].clientX, event.touches[0].clientY];
}
return [event.clientX, event.clientY];
}
canvas.addEventListener('pointerdown', e=>{
const [x,y] = pointerPos(e);
draggingIndex = delaunay ? delaunay.find(x,y) : null;
if (draggingIndex != null){
e.preventDefault();
canvas.setPointerCapture(e.pointerId);
}
});
canvas.addEventListener('pointermove', e=>{
if (draggingIndex!=null){
const [x,y] = pointerPos(e);
points[draggingIndex][0] = x;
points[draggingIndex][1] = y;
computeVoronoi();
requestAnimationFrame(draw);
}
});
canvas.addEventListener('pointerup', e=>{
draggingIndex = null;
canvas.releasePointerCapture(e.pointerId);
});
canvas.addEventListener('pointercancel', ()=> draggingIndex=null);
// --------------------------------------------------
// initial compute and draw
computeVoronoi();
draw();
// --------------------------------------------------
// handle resize
window.addEventListener('resize',()=>{
width = canvas.width = window.innerWidth;
height = canvas.height = window.innerHeight;
computeVoronoi();
draw();
});
})();
</script>
</body>
</html>
```Of course, here is an interactive Voronoi mesh. Click and drag on the canvas to move the points and watch the mesh regenerate in real-time.
```html
<!DOCTYPE html>
<html>
<head>
<title>Interactive Voronoi Mesh</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
background-color: #111;
/* Prevents pull-to-refresh and other touch actions on the page */
touch-action: none;
}
canvas {
display: block;
cursor: grab;
}
canvas:active {
cursor: grabbing;
}
</style>
</head>
<body>
<canvas id="voronoiCanvas"></canvas>
<!-- This example uses the D3.js library for Voronoi calculations and color scales -->
<script src="https://d3js.org/d3.v7.min.js"></script>
<script>
// --- 1. SETUP ---
const canvas = document.getElementById('voronoiCanvas');
const context = canvas.getContext('2d');
let width = window.innerWidth;
let height = window.innerHeight;
canvas.width = width;
canvas.height = height;
const numPoints = 100;
let sites = [];
let draggedPointIndex = null;
let delaunay, voronoi;
// --- 2. DATA GENERATION ---
function generateSites() {
// Create an array of site objects with random positions and colors
sites = d3.range(numPoints).map(() => ({
x: Math.random() * width,
y: Math.random() * height,
color: `hsl(${Math.random() * 360}, 80%, 60%)`
}));
updateDiagrams();
}
// --- 3. VORONOI CALCULATION ---
function updateDiagrams() {
// d3.Delaunay requires an array of points in the format [[x1, y1], [x2, y2], ...]
const pointsArray = sites.map(s => [s.x, s.y]);
delaunay = d3.Delaunay.from(pointsArray);
// Create the Voronoi diagram, bounded by the canvas size
voronoi = delaunay.voronoi([0, 0, width, height]);
}
// --- 4. RENDERING ---
function render() {
// Clear the canvas with a dark background
context.fillStyle = '#1a1a1a';
context.fillRect(0, 0, width, height);
// Draw Voronoi cells
context.lineWidth = 1.5;
context.strokeStyle = 'white';
for (let i = 0; i < sites.length; i++) {
context.beginPath();
voronoi.renderCell(i, context); // D3 utility to draw a cell path
context.fillStyle = sites[i].color;
context.fill();
context.stroke();
}
// Draw the sites (the points themselves)
context.fillStyle = 'white';
context.strokeStyle = 'black';
context.lineWidth = 1.5;
for (const site of sites) {
context.beginPath();
context.arc(site.x, site.y, 4, 0, 2 * Math.PI);
context.fill();
context.stroke();
}
}
// --- 5. INTERACTIVITY ---
// Use pointer events to support both mouse and touch input seamlessly
canvas.addEventListener('pointerdown', (event) => {
const x = event.clientX;
const y = event.clientY;
// Find the index of the site closest to the click/tap
draggedPointIndex = delaunay.find(x, y);
// Move the point to the cursor position for immediate feedback
if (draggedPointIndex !== undefined) {
sites[draggedPointIndex].x = x;
sites[draggedPointIndex].y = y;
updateDiagrams();
render();
}
});
canvas.addEventListener('pointermove', (event) => {
// If we are not dragging a point, do nothing
if (draggedPointIndex === null) return;
// Update the position of the dragged point
sites[draggedPointIndex].x = event.clientX;
sites[draggedPointIndex].y = event.clientY;
// Regenerate the diagrams and redraw the canvas
updateDiagrams();
render();
});
const stopDragging = () => {
// De-select the point
draggedPointIndex = null;
}
canvas.addEventListener('pointerup', stopDragging);
canvas.addEventListener('pointerleave', stopDragging);
// --- 6. RESIZE HANDLING ---
window.addEventListener('resize', () => {
width = window.innerWidth;
height = window.innerHeight;
canvas.width = width;
canvas.height = height;
// Regenerate points to fit the new screen size
generateSites();
render();
});
// --- 7. INITIALIZATION ---
generateSites();
render();
</script>
</body>
</html>
``````html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
<title>Interactive Voronoi Mesh</title>
<style>
html, body {
height: 100%;
margin: 0;
overflow: hidden;
}
body {
background: #222;
margin: 0;
padding: 0;
}
canvas {
display: block;
width: 100vw;
height: 100vh;
cursor: grab;
touch-action: none;
background: #222;
}
</style>
</head>
<body>
<canvas id="voronoi"></canvas>
<script src="https://cdn.jsdelivr.net/npm/d3-delaunay@6.0.2/dist/d3-delaunay.min.js"></script>
<script>
// Configuration
const POINT_COUNT = 20;
const MIN_DISTANCE = 25;
const LERP_SPEED = 0.20; // Higher is faster mesh morphing
// Canvas Setup
const canvas = document.getElementById('voronoi');
const ctx = canvas.getContext('2d');
let width = window.innerWidth;
let height = window.innerHeight;
function resizeCanvas() {
width = window.innerWidth;
height = window.innerHeight;
canvas.width = width;
canvas.height = height;
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
// Utility: Generate random pastel colors
function randomColor() {
const h = Math.floor(Math.random() * 360);
return `hsl(${h},70%,70%)`;
}
// Points data structure
function randomPoint() {
return [Math.random() * width, Math.random() * height];
}
let points = [];
let targetPoints = [];
let colors = [];
function initPoints() {
points = [];
targetPoints = [];
colors = [];
for (let i = 0; i < POINT_COUNT; ++i) {
points.push(randomPoint());
targetPoints.push(points[i].slice());
colors.push(randomColor());
}
}
initPoints();
// Interpolation for smoothing
function lerp(a, b, t) {
return a + (b - a) * t;
}
function lerpPoints(points, targets, t) {
for(let i=0; i < points.length; ++i) {
points[i][0] = lerp(points[i][0], targets[i][0], t);
points[i][1] = lerp(points[i][1], targets[i][1], t);
}
}
// Drawing Voronoi cells
function drawVoronoi() {
ctx.clearRect(0, 0, width, height);
// Compute Voronoi for display points
const delaunay = d3.Delaunay.from(points);
const voronoi = delaunay.voronoi([0, 0, width, height]);
for (let i = 0; i < points.length; ++i) {
ctx.save();
ctx.beginPath();
voronoi.renderCell(i, ctx);
ctx.fillStyle = colors[i];
ctx.globalAlpha = 0.88;
ctx.fill();
ctx.globalAlpha = 1;
ctx.strokeStyle = "#fff5";
ctx.lineWidth = 1;
ctx.stroke();
// Draw seed circle
ctx.beginPath();
ctx.arc(points[i][0], points[i][1], 4, 0, 2 * Math.PI);
ctx.fillStyle = "#fafafa";
ctx.shadowColor = "#aaa";
ctx.shadowBlur = 3;
ctx.fill();
ctx.restore();
}
}
// Animation loop
function animate() {
lerpPoints(points, targetPoints, LERP_SPEED);
drawVoronoi();
requestAnimationFrame(animate);
}
animate();
// --- Interactivity ---
let draggingIndex = null;
let dragOffset = [0, 0];
function getPointer(e) {
if (e.touches && e.touches.length) {
return [e.touches[0].clientX, e.touches[0].clientY];
}
return [e.clientX, e.clientY];
}
function nearestPoint(pt) {
let minD = Infinity, minIdx = -1;
for (let i = 0; i < points.length; ++i) {
const dx = pt[0] - points[i][0];
const dy = pt[1] - points[i][1];
const dist = dx * dx + dy * dy;
if (dist < minD) {
minD = dist; minIdx = i;
}
}
return [minIdx, Math.sqrt(minD)];
}
function pointerDown(e) {
const pt = getPointer(e);
const [idx, dist] = nearestPoint(pt);
if (dist < MIN_DISTANCE * MIN_DISTANCE) {
draggingIndex = idx;
dragOffset = [points[idx][0] - pt[0], points[idx][1] - pt[1]];
canvas.style.cursor = 'grabbing';
e.preventDefault();
}
}
function pointerMove(e) {
if (draggingIndex !== null) {
const pt = getPointer(e);
// Don't let seed leave boundaries
let nx = Math.max(0, Math.min(width, pt[0] + dragOffset[0]));
let ny = Math.max(0, Math.min(height, pt[1] + dragOffset[1]));
targetPoints[draggingIndex][0] = nx;
targetPoints[draggingIndex][1] = ny;
e.preventDefault();
}
}
function pointerUp(e) {
if (draggingIndex !== null) {
draggingIndex = null;
canvas.style.cursor = 'grab';
e.preventDefault();
}
}
// Mouse events
canvas.addEventListener('mousedown', pointerDown);
window.addEventListener('mousemove', pointerMove);
window.addEventListener('mouseup', pointerUp);
// Touch events
canvas.addEventListener('touchstart', pointerDown, {passive: false});
window.addEventListener('touchmove', pointerMove, {passive: false});
window.addEventListener('touchend', pointerUp, {passive: false});
window.addEventListener('touchcancel', pointerUp, {passive: false});
// Keyboard: 'r' resets points
window.addEventListener('keydown', e => {
if (e.key === 'r' || e.key === 'R') {
initPoints();
}
});
// Initial hint for user
function drawHint() {
ctx.save();
ctx.font = "22px sans-serif";
ctx.fillStyle = "#fff";
ctx.globalAlpha = 0.9;
ctx.textAlign = "center";
ctx.fillText("Drag seeds to move Voronoi regions", width/2, height-40);
ctx.font = "16px sans-serif";
ctx.globalAlpha = 0.8;
ctx.fillText("Press 'R' to randomize", width/2, height-18);
ctx.restore();
}
setTimeout(() => {
drawVoronoi();
drawHint();
}, 200);
</script>
</body>
</html>
```
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Interactive Voronoi Mesh</title>
<style>
html, body { margin: 0; padding: 0; overflow: hidden; }
canvas { display: block; }
</style>
</head>
<body>
<canvas id="voronoi"></canvas>
<script type="module">
import { Delaunay } from 'https://cdn.jsdelivr.net/npm/d3-delaunay@6/+esm';
const canvas = document.getElementById('voronoi');
let width = window.innerWidth;
let height = window.innerHeight;
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
window.addEventListener('resize', () => {
width = window.innerWidth;
height = window.innerHeight;
canvas.width = width;
canvas.height = height;
draw();
});
let points = (() => {
const p = [];
const cols = 10, rows = 10;
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
p.push(
(x + 0.5) * width / cols,
(y + 0.5) * height / rows
);
}
}
return p;
})();
const numPoints = points.length / 2;
const colors = Array(numPoints).fill().map(() =>
`#${Math.floor(Math.random() * 0x1000000).toString(16).padStart(6, '0')}`
);
let draggingIndex = null;
canvas.addEventListener('pointerdown', (e) => {
const rect = canvas.getBoundingClientRect();
const x = (e.clientX - rect.left) * width / rect.width;
const y = (e.clientY - rect.top) * height / rect.height;
let minDist = Infinity;
draggingIndex = -1;
for (let i = 0; i < points.length; i += 2) {
const dx = points[i] - x;
const dy = points[i+1] - y;
const dist = dx * dx + dy * dy;
if (dist < minDist) {
minDist = dist;
draggingIndex = i / 2;
}
}
});
window.addEventListener('pointerup', () => draggingIndex = null);
window.addEventListener('pointermove', (e) => {
if (draggingIndex === null) return;
const rect = canvas.getBoundingClientRect();
const x = (e.clientX - rect.left) * width / rect.width;
const y = (e.clientY - rect.top) * height / rect.height;
points[draggingIndex * 2] = x;
points[draggingIndex * 2 + 1] = y;
draw();
});
function draw() {
ctx.clearRect(0, 0, width, height);
try {
const delaunay = new Delaunay(points);
const voronoi = delaunay.voronoi([0, 0, width, height]);
for (let i = 0; i < numPoints; i++) {
const polygon = voronoi.cellPolygon(i);
if (!polygon || polygon.length < 3) continue;
ctx.fillStyle = colors[i];
ctx.beginPath();
for (let j = 0; j < polygon.length; j++) {
const [x, y] = polygon[j];
if (j === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.closePath();
ctx.fill();
}
} catch (e) {
console.error("Voronoi rendering error", e);
}
}
draw();
</script>
</body>
</html>
```Here's a complete HTML file that creates an interactive Voronoi mesh using the D3.js library. The mesh points are randomly colored, and you can drag the nearest point around to update the mesh dynamically with smooth transitions.
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interactive Voronoi Mesh</title>
<style>
body {
margin: 0;
overflow: hidden;
background: #f0f0f0;
}
svg {
display: block;
}
</style>
</head>
<body>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script>
// Set up dimensions
const width = window.innerWidth;
const height = window.innerHeight;
// Create SVG container
const svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
// Generate random points
const numPoints = 20;
const points = d3.range(numPoints).map(() => ({
x: Math.random() * width,
y: Math.random() * height,
color: d3.interpolateRainbow(Math.random()) // Random color using D3 interpolate
}));
// Create Voronoi diagram
const voronoi = d3.voronoi()
.x(d => d.x)
.y(d => d.y)
.extent([[0, 0], [width, height]]);
// Initial render of Voronoi cells
let cells = svg.selectAll("path")
.data(voronoi.polygons(points))
.join("path")
.attr("d", d => d ? "M" + d.join("L") + "Z" : null)
.attr("fill", (d, i) => points[i].color)
.attr("stroke", "#fff")
.attr("stroke-width", 1)
.style("transition", "d 0.2s ease"); // Smooth transition for updates
// Dragging state
let draggedPoint = null;
// Handle mouse/touch events
svg.on("mousedown touchstart", function(event) {
event.preventDefault();
const [mouseX, mouseY] = d3.pointer(event);
// Find the nearest point to the mouse/touch position
const distances = points.map(p => Math.sqrt((p.x - mouseX) ** 2 + (p.y - mouseY) ** 2));
const nearestIndex = distances.indexOf(Math.min(...distances));
draggedPoint = points[nearestIndex];
});
svg.on("mousemove touchmove", function(event) {
if (draggedPoint) {
event.preventDefault();
const [mouseX, mouseY] = d3.pointer(event);
// Update the position of the dragged point
draggedPoint.x = Math.max(0, Math.min(width, mouseX));
draggedPoint.y = Math.max(0, Math.min(height, mouseY));
// Update Voronoi cells with transition
cells.data(voronoi.polygons(points))
.attr("d", d => d ? "M" + d.join("L") + "Z" : null);
}
});
svg.on("mouseup touchend touchcancel", function() {
draggedPoint = null;
});
// Handle window resize
window.addEventListener("resize", () => {
svg.attr("width", window.innerWidth).attr("height", window.innerHeight);
voronoi.extent([[0, 0], [window.innerWidth, window.innerHeight]]);
cells.data(voronoi.polygons(points))
.attr("d", d => d ? "M" + d.join("L") + "Z" : null);
});
</script>
</body>
</html>
```
This code creates a dynamic Voronoi mesh where each cell is filled with a random color. You can click and drag (or tap and drag on touch devices) to move the nearest point, and the mesh will regenerate smoothly with a transition effect. The implementation uses D3.js via a CDN for both the Voronoi diagram generation and the color interpolation. Resize the window to see the mesh adapt to the new dimensions.```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Interactive Voronoi Mesh</title>
<style>
html, body {
margin: 0;
padding: 0;
overflow: hidden;
width: 100%;
height: 100%;
}
svg {
display: block;
}
</style>
</head>
<body>
<!-- Load D3.js from CDN -->
<script src="https://d3js.org/d3.v6.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const width = window.innerWidth;
const height = window.innerHeight;
const numSites = 50;
// Initialize random sites with random rainbow colors
const sites = d3.range(numSites).map(() => ({
x: Math.random() * width,
y: Math.random() * height,
color: d3.interpolateRainbow(Math.random())
}));
// Create the SVG
const svg = d3.select('body')
.append('svg')
.attr('width', width)
.attr('height', height);
let activeIndex = null;
// Drag behavior
const drag = d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended);
svg.call(drag);
function dragstarted(event) {
// Find nearest site to pointer on drag start
const [mx, my] = d3.pointer(event);
activeIndex = d3.range(numSites).reduce((best, i) => {
const dx = sites[i].x - mx;
const dy = sites[i].y - my;
const d2 = dx*dx + dy*dy;
return d2 < best.d2 ? { i, d2 } : best;
}, { i: null, d2: Infinity }).i;
}
function dragged(event) {
if (activeIndex !== null) {
const [mx, my] = d3.pointer(event);
// Clamp within bounds
sites[activeIndex].x = Math.max(0, Math.min(width, mx));
sites[activeIndex].y = Math.max(0, Math.min(height, my));
render();
}
}
function dragended(event) {
activeIndex = null;
}
// Render the Voronoi diagram
function render() {
const delaunay = d3.Delaunay.from(sites, d => d.x, d => d.y);
const voronoi = delaunay.voronoi([0, 0, width, height]);
const polygons = sites.map((d, i) => voronoi.cellPolygon(i));
const paths = svg.selectAll('path').data(polygons);
paths.join(
enter => enter.append('path')
.attr('fill', (d, i) => sites[i].color)
.attr('stroke', '#000')
.attr('stroke-width', 1)
.attr('d', d => d ? 'M' + d.join('L') + 'Z' : ''),
update => update
.transition().duration(100)
.attr('d', d => d ? 'M' + d.join('L') + 'Z' : ''),
exit => exit.remove()
);
}
// Initial render
render();
// Resize handling
window.addEventListener('resize', () => {
const w = window.innerWidth;
const h = window.innerHeight;
svg.attr('width', w).attr('height', h);
render();
});
});
</script>
</body>
</html>
```# Interactive Voronoi Mesh Visualization
I'll create an interactive Voronoi diagram where users can drag points to dynamically update the mesh with smooth transitions.
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interactive Voronoi Mesh</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script src="https://unpkg.com/d3-delaunay@6"></script>
<style>
body {
margin: 0;
overflow: hidden;
background: #1a1a2e;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
#container {
position: relative;
width: 100vw;
height: 100vh;
}
canvas {
display: block;
}
.title {
position: absolute;
top: 20px;
left: 0;
right: 0;
text-align: center;
color: rgba(255, 255, 255, 0.8);
font-size: 2.5rem;
font-weight: 700;
text-shadow: 0 2px 10px rgba(0,0,0,0.5);
pointer-events: none;
}
.instructions {
position: absolute;
bottom: 20px;
left: 0;
right: 0;
text-align: center;
color: rgba(255, 255, 255, 0.6);
font-size: 1.1rem;
padding: 10px 20px;
background: rgba(0, 0, 0, 0.3);
border-radius: 10px;
max-width: 600px;
margin: 0 auto;
}
.point-count {
position: absolute;
top: 20px;
right: 20px;
color: rgba(255, 255, 255, 0.7);
background: rgba(0, 0, 0, 0.3);
padding: 8px 15px;
border-radius: 20px;
font-size: 0.9rem;
}
</style>
</head>
<body>
<div id="container">
<div class="title">Interactive Voronoi Mesh</div>
<div class="point-count">Points: <span id="count">50</span></div>
<div class="instructions">Click and drag any point to move it. The Voronoi diagram will update smoothly.</div>
<canvas id="voronoiCanvas"></canvas>
</div>
<script>
// Configuration
const CONFIG = {
pointCount: 50,
pointRadius: 5,
animationDuration: 500,
colors: ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FFBE0B', '#FB5607', '#8338EC', '#3A86FF']
};
// State
const state = {
points: [],
width: window.innerWidth,
height: window.innerHeight,
activePoint: null,
canvas: null,
ctx: null,
delaunay: null,
voronoi: null
};
// Initialize
function init() {
state.canvas = document.getElementById('voronoiCanvas');
state.ctx = state.canvas.getContext('2d');
resizeCanvas();
generatePoints();
computeVoronoi();
draw();
setupEventListeners();
}
// Resize canvas to match window
function resizeCanvas() {
state.width = window.innerWidth;
state.height = window.innerHeight;
state.canvas.width = state.width;
state.canvas.height = state.height;
}
// Generate random points
function generatePoints() {
state.points = [];
for (let i = 0; i < CONFIG.pointCount; i++) {
state.points.push({
x: Math.random() * state.width,
y: Math.random() * state.height,
color: CONFIG.colors[Math.floor(Math.random() * CONFIG.colors.length)],
originalColor: null
});
}
document.getElementById('count').textContent = CONFIG.pointCount;
}
// Compute Voronoi diagram
function computeVoronoi() {
const points = state.points.map(p => [p.x, p.y]);
state.delaunay = d3.Delaunay.from(points);
state.voronoi = state.delaunay.voronoi([0, 0, state.width, state.height]);
}
// Draw Voronoi diagram
function draw() {
const ctx = state.ctx;
ctx.clearRect(0, 0, state.width, state.height);
// Draw Voronoi cells
for (let i = 0; i < state.points.length; i++) {
const cell = state.voronoi.cellPolygon(i);
if (!cell) continue;
ctx.fillStyle = state.points[i].color;
ctx.beginPath();
ctx.moveTo(cell[0][0], cell[0][1]);
for (let j = 1; j < cell.length; j++) {
ctx.lineTo(cell[j][0], cell[j][1]);
}
ctx.closePath();
ctx.fill();
// Draw cell borders
ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
ctx.lineWidth = 1;
ctx.stroke();
}
// Draw points
state.points.forEach(point => {
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(point.x, point.y, CONFIG.pointRadius, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = 'rgba(0, 0, 0, 0.5)';
ctx.lineWidth = 1.5;
ctx.stroke();
});
}
// Find nearest point to mouse position
function findNearestPoint(x, y) {
if (!state.delaunay) return null;
return state.delaunay.find(x, y);
}
// Handle mouse/touch events
function handleStart(x, y) {
const index = findNearestPoint(x, y);
if (index !== null) {
state.activePoint = index;
state.points[index].originalColor = state.points[index].color;
state.points[index].color = 'white';
draw();
}
}
function handleMove(x, y) {
if (state.activePoint !== null) {
// Constrain point to canvas
state.points[state.activePoint].x = Math.max(0, Math.min(state.width, x));
state.points[state.activePoint].y = Math.max(0, Math.min(state.height, y));
computeVoronoi();
draw();
}
}
function handleEnd() {
if (state.activePoint !== null) {
state.points[state.activePoint].color = state.points[state.activePoint].originalColor;
state.points[state.activePoint].originalColor = null;
state.activePoint = null;
draw();
}
}
// Set up event listeners
function setupEventListeners() {
// Mouse events
state.canvas.addEventListener('mousedown', (e) => {
handleStart(e.clientX, e.clientY);
});
state.canvas.addEventListener('mousemove', (e) => {
handleMove(e.clientX, e.clientY);
});
window.addEventListener('mouseup', () => {
handleEnd();
});
// Touch events
state.canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
const touch = e.touches[0];
handleStart(touch.clientX, touch.clientY);
});
state.canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
const touch = e.touches[0];
handleMove(touch.clientX, touch.clientY);
});
window.addEventListener('touchend', () => {
handleEnd();
});
// Window resize
window.addEventListener('resize', () => {
resizeCanvas();
computeVoronoi();
draw();
});
// Add keyboard shortcut to reset points
document.addEventListener('keydown', (e) => {
if (e.key === 'r' || e.key === 'R') {
generatePoints();
computeVoronoi();
draw();
}
});
}
// Initialize when page loads
window.addEventListener('load', init);
</script>
</body>
</html>
```
## Features
1. **Interactive Voronoi Diagram**: Displays a colorful Voronoi mesh with randomly colored cells
2. **Point Dragging**: Click and drag any point to move it around
3. **Dynamic Updates**: The Voronoi diagram smoothly updates as you drag points
4. **Responsive Design**: Automatically adjusts to window size changes
5. **Touch Support**: Works on both desktop and mobile devices
6. **Visual Feedback**: Points highlight when being dragged
7. **Reset Function**: Press 'R' to generate a new set of random points
The visualization uses D3.js and d3-delaunay for efficient Voronoi computation. The canvas rendering ensures smooth performance even with many points. The design features a dark theme with vibrant cell colors for an aesthetically pleasing experience.```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interactive Voronoi Mesh</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
background: #000;
font-family: Arial, sans-serif;
}
canvas {
display: block;
cursor: crosshair;
}
.info {
position: absolute;
top: 10px;
left: 10px;
color: white;
font-size: 14px;
background: rgba(0,0,0,0.5);
padding: 10px;
border-radius: 5px;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<div class="info">
Click and drag to move Voronoi points
</div>
<script>
class VoronoiMesh {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.width = window.innerWidth;
this.height = window.innerHeight;
this.points = [];
this.colors = [];
this.numPoints = 25;
this.isDragging = false;
this.dragPointIndex = -1;
this.animationFrame = null;
this.init();
this.setupEventListeners();
this.animate();
}
init() {
this.canvas.width = this.width;
this.canvas.height = this.height;
// Generate random points
for (let i = 0; i < this.numPoints; i++) {
this.points.push({
x: Math.random() * this.width,
y: Math.random() * this.height,
targetX: 0,
targetY: 0
});
// Generate random vibrant colors
this.colors.push(this.generateRandomColor());
}
// Set initial targets
this.points.forEach(point => {
point.targetX = point.x;
point.targetY = point.y;
});
}
generateRandomColor() {
const hue = Math.random() * 360;
const saturation = 60 + Math.random() * 40; // 60-100%
const lightness = 45 + Math.random() * 20; // 45-65%
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
}
setupEventListeners() {
// Mouse events
this.canvas.addEventListener('mousedown', (e) => this.onPointerDown(e.clientX, e.clientY));
this.canvas.addEventListener('mousemove', (e) => this.onPointerMove(e.clientX, e.clientY));
this.canvas.addEventListener('mouseup', () => this.onPointerUp());
// Touch events
this.canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
const touch = e.touches[0];
this.onPointerDown(touch.clientX, touch.clientY);
});
this.canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
const touch = e.touches[0];
this.onPointerMove(touch.clientX, touch.clientY);
});
this.canvas.addEventListener('touchend', (e) => {
e.preventDefault();
this.onPointerUp();
});
// Window resize
window.addEventListener('resize', () => this.resize());
}
onPointerDown(x, y) {
this.isDragging = true;
this.dragPointIndex = this.findNearestPoint(x, y);
}
onPointerMove(x, y) {
if (this.isDragging && this.dragPointIndex !== -1) {
const point = this.points[this.dragPointIndex];
point.targetX = x;
point.targetY = y;
}
}
onPointerUp() {
this.isDragging = false;
this.dragPointIndex = -1;
}
findNearestPoint(x, y) {
let minDistance = Infinity;
let nearestIndex = -1;
for (let i = 0; i < this.points.length; i++) {
const point = this.points[i];
const distance = Math.sqrt((x - point.x) ** 2 + (y - point.y) ** 2);
if (distance < minDistance) {
minDistance = distance;
nearestIndex = i;
}
}
return nearestIndex;
}
updatePoints() {
const easing = 0.1;
this.points.forEach(point => {
const dx = point.targetX - point.x;
const dy = point.targetY - point.y;
point.x += dx * easing;
point.y += dy * easing;
});
}
drawVoronoi() {
const imageData = this.ctx.createImageData(this.width, this.height);
const data = imageData.data;
for (let x = 0; x < this.width; x += 2) { // Skip pixels for performance
for (let y = 0; y < this.height; y += 2) {
let minDistance = Infinity;
let nearestPointIndex = 0;
// Find nearest point
for (let i = 0; i < this.points.length; i++) {
const point = this.points[i];
const distance = Math.sqrt((x - point.x) ** 2 + (y - point.y) ** 2);
if (distance < minDistance) {
minDistance = distance;
nearestPointIndex = i;
}
}
// Get color for this region
const color = this.hexToRgb(this.hslToHex(this.colors[nearestPointIndex]));
// Set pixel color (and adjacent pixels for performance)
for (let dx = 0; dx < 2 && x + dx < this.width; dx++) {
for (let dy = 0; dy < 2 && y + dy < this.height; dy++) {
const pixelIndex = ((y + dy) * this.width + (x + dx)) * 4;
data[pixelIndex] = color.r; // Red
data[pixelIndex + 1] = color.g; // Green
data[pixelIndex + 2] = color.b; // Blue
data[pixelIndex + 3] = 255; // Alpha
}
}
}
}
this.ctx.putImageData(imageData, 0, 0);
}
drawPoints() {
this.points.forEach((point, index) => {
this.ctx.fillStyle = 'white';
this.ctx.strokeStyle = 'black';
this.ctx.lineWidth = 2;
this.ctx.beginPath();
this.ctx.arc(point.x, point.y, 4, 0, Math.PI * 2);
this.ctx.fill();
this.ctx.stroke();
// Highlight dragged point
if (index === this.dragPointIndex) {
this.ctx.strokeStyle = 'yellow';
this.ctx.lineWidth = 3;
this.ctx.beginPath();
this.ctx.arc(point.x, point.y, 8, 0, Math.PI * 2);
this.ctx.stroke();
}
});
}
hslToHex(hslString) {
const match = hslString.match(/hsl\((\d+),\s*(\d+)%,\s*(\d+)%\)/);
if (!match) return '#000000';
const h = parseInt(match[1]) / 360;
const s = parseInt(match[2]) / 100;
const l = parseInt(match[3]) / 100;
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1/6) return p + (q - p) * 6 * t;
if (t < 1/2) return q;
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
const r = Math.round(hue2rgb(p, q, h + 1/3) * 255);
const g = Math.round(hue2rgb(p, q, h) * 255);
const b = Math.round(hue2rgb(p, q, h - 1/3) * 255);
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
}
hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : { r: 0, g: 0, b: 0 };
}
resize() {
this.width = window.innerWidth;
this.height = window.innerHeight;
this.canvas.width = this.width;
this.canvas.height = this.height;
// Keep points within new bounds
this.points.forEach(point => {
point.x = Math.min(point.x, this.width);
point.y = Math.min(point.y, this.height);
point.targetX = Math.min(point.targetX, this.width);
point.targetY = Math.min(point.targetY, this.height);
});
}
animate() {
this.ctx.clearRect(0, 0, this.width, this.height);
this.updatePoints();
this.drawVoronoi();
this.drawPoints();
this.animationFrame = requestAnimationFrame(() => this.animate());
}
destroy() {
if (this.animationFrame) {
cancelAnimationFrame(this.animationFrame);
}
}
}
// Initialize the Voronoi mesh
const canvas = document.getElementById('canvas');
const voronoiMesh = new VoronoiMesh(canvas);
</script>
</body>
</html>
```
This interactive Voronoi mesh features:
1. **Random colored regions** - Each Voronoi cell has a randomly generated vibrant color
2. **Interactive dragging** - Click or touch and drag to move the nearest point
3. **Smooth animation** - Points smoothly interpolate to their target positions using easing
4. **Real-time regeneration** - The mesh updates continuously as you drag points
5. **Visual feedback** - The point being dragged is highlighted with a yellow outline
6. **Touch support** - Works on both desktop (mouse) and mobile (touch) devices
7. **Responsive** - Adapts to window resizing
The algorithm works by:
- Finding the nearest point to your cursor/touch
- Moving that point smoothly toward your drag position
- Continuously calculating which point is closest to each pixel
- Coloring each pixel based on its nearest point
- Creating the characteristic Voronoi cell pattern
You can drag multiple points around to see how the colored regions dynamically reshape and flow into each other!