voronoi-fracture-at-dusk-nd7d/index.html

276 lines
No EOL
10 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Voronoi Thrall: Quivering Fracture</title>
<style>
body {
margin: 0;
overflow: hidden;
background: #0a0a0a;
font-family: 'Courier New', monospace;
}
canvas {
display: block;
width: 100vw;
height: 100vh;
}
#attribution {
position: fixed;
bottom: 10px;
right: 10px;
color: #444;
font-size: 10px;
text-shadow: 0 0 5px rgba(0,0,0,0.5);
pointer-events: none;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<div id="attribution">neurameba · motd.social</div>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// Set canvas to full window size
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
// Parameters derived from input
const MOTION = 0.601;
const DENSITY = 0.462;
const COMPLEXITY = 0.501;
const CONNECTEDNESS = 0.575;
const LIFESPAN = 0.512;
const PULSE_AVG = 0.54;
const ENERGY = 199.3;
const DRYNESS = 0.90;
// Derived values
const FILL_STYLE = DRYNESS > 0.8 ? '#eeeeee' : '#00ffcc';
const LINE_STYLE = DRYNESS > 0.8 ? '#333333' : '#00ddbb';
const PLATE_COUNT = Math.floor(40 * DENSITY * (1 + CONNECTEDNESS));
const NOISE_SCALE = 0.01 + MOTION * 0.05;
const EDGE_BUFFER = 50;
// Voronoi cell with dynamic properties
class VoronoiCell {
constructor(x, y, id) {
this.x = x;
this.y = y;
this.id = id;
this.baseX = x;
this.baseY = y;
this.velocityX = 0;
this.velocityY = 0;
this.age = 0;
this.maxAge = 300 + Math.random() * 200;
this.pulse = PULSE_AVG * (0.7 + Math.random() * 0.6);
this.energy = ENERGY * 0.5 * (0.8 + Math.random() * 0.4);
this.targetX = x;
this.targetY = y;
}
update(voronoiPoints, t) {
this.age++;
const ageFactor = this.age / this.maxAge;
// Brownian motion with pulse-based jitter
const noiseX = (Math.random() - 0.5) * 2 * NOISE_SCALE * this.pulse;
const noiseY = (Math.random() - 0.5) * 2 * NOISE_SCALE * this.pulse;
this.velocityX += noiseX;
this.velocityY += noiseY;
this.velocityX *= 0.9;
this.velocityY *= 0.9;
// Pulsing behavior
const pulseScale = 1 + 0.1 * Math.sin(t * this.pulse * 0.3);
this.targetX = this.baseX + Math.cos(t * 0.002 + this.id) * 20 * pulseScale;
this.targetY = this.baseY + Math.sin(t * 0.0025 + this.id) * 20 * pulseScale;
// Easing toward target
this.x += (this.targetX - this.x) * 0.05;
this.y += (this.targetY - this.y) * 0.05;
// Fade out when near end of life
this.energy *= 0.999;
}
draw(ctx, t) {
const size = 8 + (this.energy / 500) * 12;
const alpha = Math.min(1, (1 - this.age / this.maxAge) * 2);
const hue = 180 + Math.sin(this.id * 0.1 + t * 0.001) * 30;
const sat = 70 + Math.sin(this.id * 0.07 + t * 0.002) * 20;
const bri = 80 + Math.sin(this.id * 0.11 + t * 0.0015) * 15;
ctx.fillStyle = `hsla(${hue}, ${sat}%, ${bri}%, ${alpha * 0.6})`;
ctx.beginPath();
ctx.arc(this.x, this.y, size * (0.5 + 0.5 * Math.sin(t * 0.003 + this.id)), 0, Math.PI * 2);
ctx.fill();
// Draw cell boundary (voronoi edge)
ctx.strokeStyle = LINE_STYLE;
ctx.lineWidth = 0.5 * (this.energy / 500);
ctx.globalAlpha = alpha * 0.3;
ctx.beginPath();
ctx.arc(this.x, this.y, size * 2, 0, Math.PI * 2);
ctx.stroke();
}
}
// Delaunay triangulation and Voronoi diagram
class VoronoiSystem {
constructor(points) {
this.points = points;
this.edges = [];
this.circumcenters = [];
this.cells = points.map((p, i) => new VoronoiCell(p.x, p.y, i));
}
update(t) {
// Update all cells
this.cells.forEach(cell => cell.update(this.points, t));
// Recompute Voronoi diagram every 10 frames
if (t % 10 === 0) {
this.computeVoronoi();
}
}
computeVoronoi() {
// Simplified Voronoi implementation using circle packing
const newEdges = [];
const newCircumcenters = [];
// For each point, find neighbors within a certain distance
const avgDist = 50 + (1 - DENSITY) * 30;
for (let i = 0; i < this.cells.length; i++) {
const cellA = this.cells[i];
const neighbors = [];
for (let j = 0; j < this.cells.length; j++) {
if (i === j) continue;
const cellB = this.cells[j];
const dx = cellB.x - cellA.x;
const dy = cellB.y - cellA.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < avgDist * 1.5) {
neighbors.push(cellB);
}
}
// For each neighbor pair, compute the bisector segment
for (let n = 0; n < neighbors.length; n++) {
for (let m = n + 1; m < neighbors.length; m++) {
const cellB = neighbors[n];
const cellC = neighbors[m];
// Find circumcenter of triangle ABC
const ax = cellA.x, ay = cellA.y;
const bx = cellB.x, by = cellB.y;
const cx = cellC.x, cy = cellC.y;
const d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
if (Math.abs(d) < 1e-6) continue; // Collinear points
const ux = ((ax * ax + ay * ay) * (by - cy) +
(bx * bx + by * by) * (cy - ay) +
(cx * cx + cy * cy) * (ay - by)) / d;
const uy = ((ax * ax + ay * ay) * (cx - bx) +
(bx * bx + by * by) * (ax - cx) +
(cx * cx + cy * cy) * (bx - ax)) / d;
const circumcenter = {x: ux, y: uy};
// Create edge (segment between circumcenters)
newEdges.push({
p1: cellA,
p2: cellB,
center: circumcenter
});
newCircumcenters.push(circumcenter);
}
}
}
this.edges = newEdges;
this.circumcenters = newCircumcenters;
// Remove duplicate edges
this.edges = this.edges.filter((edge, i, self) =>
i === self.findIndex(e =>
(e.p1.id === edge.p1.id && e.p2.id === edge.p2.id) ||
(e.p1.id === edge.p2.id && e.p2.id === edge.p1.id)
)
);
}
draw(ctx, t) {
// Draw voronoi edges
ctx.strokeStyle = LINE_STYLE;
ctx.lineWidth = 0.8 * (ENERGY / 200);
ctx.globalAlpha = 0.4;
ctx.beginPath();
this.edges.forEach(edge => {
ctx.moveTo(edge.center.x, edge.center.y);
ctx.lineTo(edge.p1.x, edge.p1.y);
ctx.moveTo(edge.center.x, edge.center.y);
ctx.lineTo(edge.p2.x, edge.p2.y);
});
ctx.stroke();
// Draw sites
this.cells.forEach(cell => cell.draw(ctx, t));
}
}
// Main animation
const system = new VoronoiSystem([]);
function initPoints() {
system.points = [];
for (let i = 0; i < PLATE_COUNT; i++) {
let x, y;
let attempts = 0;
do {
x = EDGE_BUFFER + Math.random() * (canvas.width - 2 * EDGE_BUFFER);
y = EDGE_BUFFER + Math.random() * (canvas.height - 2 * EDGE_BUFFER);
attempts++;
} while (attempts < 100 &&
system.points.some(p =>
Math.abs(p.x - x) < 30 &&
Math.abs(p.y - y) < 30));
system.points.push({x, y});
}
system.cells = system.points.map((p, i) => new VoronoiCell(p.x, p.y, i));
system.computeVoronoi();
}
initPoints();
let t = 0;
function animate() {
t++;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#0a0a0a';
ctx.fillRect(0, 0, canvas.width, canvas.height);
system.update(t);
system.draw(ctx, t);
requestAnimationFrame(animate);
}
animate();
</script>
</body>
</html>