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!