228 lines
No EOL
7.9 KiB
HTML
228 lines
No EOL
7.9 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Voronoi Bloom</title>
|
|
<style>
|
|
body { margin: 0; overflow: hidden; background: #0a0a1a; }
|
|
canvas { display: block; }
|
|
.credit { position: fixed; bottom: 10px; left: 50%; transform: translateX(-50%); color: #4a4a5a; font-family: monospace; font-size: 10px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<canvas id="canvas"></canvas>
|
|
<div class="credit">neurameba · motd.social</div>
|
|
<script>
|
|
const canvas = document.getElementById('canvas');
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
function resize() {
|
|
canvas.width = window.innerWidth;
|
|
canvas.height = window.innerHeight;
|
|
}
|
|
window.addEventListener('resize', resize);
|
|
resize();
|
|
|
|
// Dynamic voronoi parameters
|
|
const points = [];
|
|
const velocities = [];
|
|
const colors = [];
|
|
const sizes = [];
|
|
const maxPoints = 200;
|
|
const minDistance = 20;
|
|
const repulsionRadius = 30;
|
|
const energyDecay = 0.98;
|
|
const pulseSpeed = 0.02;
|
|
const colorBase = [200, 200, 200];
|
|
|
|
// Initialize
|
|
for (let i = 0; i < maxPoints; i++) {
|
|
points.push({
|
|
x: Math.random() * canvas.width,
|
|
y: Math.random() * canvas.height
|
|
});
|
|
velocities.push({
|
|
x: (Math.random() - 0.5) * 2,
|
|
y: (Math.random() - 0.5) * 2
|
|
});
|
|
sizes.push(Math.random() * 2 + 1);
|
|
updateColor(i);
|
|
}
|
|
|
|
function updateColor(i) {
|
|
const hue = (Math.random() * 30 + 200) % 360;
|
|
const sat = 20 + Math.random() * 20;
|
|
const lum = 80 + Math.random() * 10;
|
|
colors[i] = `hsl(${hue}, ${sat}%, ${lum}%)`;
|
|
}
|
|
|
|
function updatePoints() {
|
|
const pulse = 0.3 + 0.7 * Math.sin(Date.now() * pulseSpeed);
|
|
|
|
for (let i = 0; i < points.length; i++) {
|
|
// Move points
|
|
points[i].x += velocities[i].x * pulse * 0.494;
|
|
points[i].y += velocities[i].y * pulse * 0.494;
|
|
|
|
// Boundary check
|
|
if (points[i].x < 0 || points[i].x > canvas.width) velocities[i].x *= -1;
|
|
if (points[i].y < 0 || points[i].y > canvas.height) velocities[i].y *= -1;
|
|
|
|
// Energy decay
|
|
sizes[i] *= energyDecay;
|
|
|
|
// Repulsion
|
|
for (let j = i + 1; j < points.length; j++) {
|
|
const dx = points[j].x - points[i].x;
|
|
const dy = points[j].y - points[i].y;
|
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
if (dist < repulsionRadius && dist > 0) {
|
|
const force = (repulsionRadius - dist) / repulsionRadius;
|
|
velocities[i].x -= dx * force * 0.1;
|
|
velocities[i].y -= dy * force * 0.1;
|
|
velocities[j].x += dx * force * 0.1;
|
|
velocities[j].y += dy * force * 0.1;
|
|
}
|
|
}
|
|
|
|
// Too small? recreate
|
|
if (sizes[i] < 0.2) {
|
|
points[i] = {
|
|
x: Math.random() * canvas.width,
|
|
y: Math.random() * canvas.height
|
|
};
|
|
velocities[i] = {
|
|
x: (Math.random() - 0.5) * 2,
|
|
y: (Math.random() - 0.5) * 2
|
|
};
|
|
sizes[i] = Math.random() * 2 + 1;
|
|
updateColor(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
function drawVoronoi() {
|
|
const delaunay = Delaunator.from(points.map(p => [p.x, p.y]));
|
|
const voronoi = new Voronoi(delaunay, points.map(p => [p.x, p.y]));
|
|
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
voronoi.cells.forEach((cell, i) => {
|
|
if (cell.length === 0) return;
|
|
|
|
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();
|
|
|
|
const intensity = sizes[i] / 2;
|
|
ctx.fillStyle = colors[i];
|
|
ctx.globalAlpha = intensity * 0.8;
|
|
ctx.fill();
|
|
|
|
ctx.globalAlpha = intensity * 0.3;
|
|
ctx.strokeStyle = colors[i];
|
|
ctx.lineWidth = 1;
|
|
ctx.stroke();
|
|
});
|
|
|
|
ctx.globalAlpha = 1;
|
|
}
|
|
|
|
function animate() {
|
|
updatePoints();
|
|
drawVoronoi();
|
|
requestAnimationFrame(animate);
|
|
}
|
|
|
|
animate();
|
|
|
|
// Voronoi implementation (minimal)
|
|
class Voronoi {
|
|
constructor(delaunay, points) {
|
|
this.delaunay = delaunay;
|
|
this.points = points;
|
|
this.cells = [];
|
|
this.compute();
|
|
}
|
|
|
|
compute() {
|
|
const triangles = this.delaunay.triangles;
|
|
const halfedges = this.delaunay.halfedges;
|
|
const edges = this.delaunay.edges;
|
|
|
|
// For each point, find its adjacent triangles
|
|
const adjacency = {};
|
|
for (let i = 0; i < this.points.length; i++) {
|
|
adjacency[i] = [];
|
|
}
|
|
|
|
for (let i = 0; i < triangles.length; i += 3) {
|
|
const a = triangles[i];
|
|
const b = triangles[i + 1];
|
|
const c = triangles[i + 2];
|
|
|
|
adjacency[a].push(i);
|
|
adjacency[b].push(i + 1);
|
|
adjacency[c].push(i + 2);
|
|
}
|
|
|
|
// Build voronoi cells
|
|
for (let i = 0; i < this.points.length; i++) {
|
|
const cell = [];
|
|
const visited = new Set();
|
|
|
|
// Get all circumcenters of adjacent triangles
|
|
for (const triIndex of adjacency[i]) {
|
|
const a = triangles[triIndex];
|
|
const b = triangles[triIndex + 1];
|
|
const c = triangles[triIndex + 2];
|
|
|
|
if (a === i || b === i || c === i) {
|
|
const circumcenter = this.circumcenter(
|
|
this.points[a],
|
|
this.points[b],
|
|
this.points[c]
|
|
);
|
|
|
|
if (!visited.has(circumcenter.join(','))) {
|
|
cell.push(circumcenter);
|
|
visited.add(circumcenter.join(','));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sort cell vertices in clockwise order
|
|
if (cell.length > 2) {
|
|
const center = this.points[i];
|
|
cell.sort((a, b) => {
|
|
const angleA = Math.atan2(a[1] - center[1], a[0] - center[0]);
|
|
const angleB = Math.atan2(b[1] - center[1], b[0] - center[0]);
|
|
return angleA - angleB;
|
|
});
|
|
}
|
|
|
|
this.cells.push(cell);
|
|
}
|
|
}
|
|
|
|
circumcenter(a, b, c) {
|
|
const ax = a[0], ay = a[1];
|
|
const bx = b[0], by = b[1];
|
|
const cx = c[0], cy = c[1];
|
|
|
|
const d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
|
|
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;
|
|
|
|
return [ux, uy];
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |