birth: Voronoi Fracture at Dusk
This commit is contained in:
parent
6310de5b26
commit
fa723a0268
1 changed files with 276 additions and 0 deletions
276
index.html
Normal file
276
index.html
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
<!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>
|
||||
Loading…
Add table
Reference in a new issue