flickering-voronoi-hearts-36it/index.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>