240 lines
No EOL
8.3 KiB
HTML
240 lines
No EOL
8.3 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 Organism</title>
|
|
<style>
|
|
body { margin: 0; overflow: hidden; background: #0a0a0a; }
|
|
canvas { display: block; }
|
|
#attribution { position: absolute; bottom: 10px; right: 10px; color: #444; font-family: monospace; font-size: 10px; }
|
|
</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
|
|
const params = {
|
|
motion: 0.580,
|
|
density: 0.358,
|
|
complexity: 0.726,
|
|
connectedness: 0.530,
|
|
lifespan: 0.340,
|
|
pulse: { avg: 0.35, min: 0.30, max: 1.20 },
|
|
tone: { anger: 0.00, sadness: 0.00, curiosity: 0.20, dryness: 0.90, playfulness: 0.00, tension: 0.00 }
|
|
};
|
|
|
|
// Generate Voronoi diagram with dynamic points
|
|
let points = [];
|
|
const pointCount = Math.max(50, Math.floor(params.density * 200 + 50));
|
|
const maxSpeed = params.motion * 2 + 0.5;
|
|
|
|
// Initialize points
|
|
for (let i = 0; i < pointCount; i++) {
|
|
points.push({
|
|
x: Math.random() * canvas.width,
|
|
y: Math.random() * canvas.height,
|
|
vx: (Math.random() - 0.5) * maxSpeed,
|
|
vy: (Math.random() - 0.5) * maxSpeed,
|
|
color: `hsl(0, 0%, ${Math.random() * 20 + 20}%)`,
|
|
size: Math.random() * 2 + 1,
|
|
energy: 38.8
|
|
});
|
|
}
|
|
|
|
// Voronoi diagram data
|
|
let edges = [];
|
|
let cells = [];
|
|
|
|
// Distance function
|
|
function distance(p1, p2) {
|
|
return Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2);
|
|
}
|
|
|
|
// Bounding box intersection
|
|
function lineBoxIntersection(x1, y1, x2, y2, xmin, ymin, xmax, ymax) {
|
|
const d1 = (x1 < xmin && x2 < xmin) || (x1 > xmax && x2 > xmax) ||
|
|
(y1 < ymin && y2 < ymin) || (y1 > ymax && y2 > ymax);
|
|
if (d1) return false;
|
|
|
|
const coef1 = (y2 - y1) / (x2 - x1);
|
|
const coef2 = y1 - coef1 * x1;
|
|
|
|
let x = xmin;
|
|
let y = coef1 * x + coef2;
|
|
if (y >= ymin && y <= ymax) return true;
|
|
|
|
x = xmax;
|
|
y = coef1 * x + coef2;
|
|
if (y >= ymin && y <= ymax) return true;
|
|
|
|
y = ymin;
|
|
x = (y - coef2) / coef1;
|
|
if (x >= xmin && x <= xmax) return true;
|
|
|
|
y = ymax;
|
|
x = (y - coef2) / coef1;
|
|
if (x >= xmin && x <= xmax) return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
// Calculate Voronoi diagram (simplified)
|
|
function calculateVoronoi() {
|
|
edges = [];
|
|
cells = points.map((_, i) => ({ site: i, edges: [], neighbors: [] }));
|
|
|
|
// For each point, find its nearest neighbors and potential edges
|
|
for (let i = 0; i < points.length; i++) {
|
|
const p1 = points[i];
|
|
let closest = null;
|
|
let minDist = Infinity;
|
|
|
|
for (let j = i + 1; j < points.length; j++) {
|
|
const p2 = points[j];
|
|
const dist = distance(p1, p2);
|
|
|
|
if (dist < minDist) {
|
|
minDist = dist;
|
|
closest = j;
|
|
}
|
|
}
|
|
|
|
if (closest !== null) {
|
|
// Create bidirectional edge
|
|
const edge = {
|
|
p1: i,
|
|
p2: closest,
|
|
length: minDist,
|
|
color: p1.color,
|
|
width: 1 + (p1.size + points[closest].size) / 4
|
|
};
|
|
|
|
cells[i].edges.push(edge);
|
|
cells[closest].edges.push(edge);
|
|
cells[i].neighbors.push(closest);
|
|
cells[closest].neighbors.push(i);
|
|
edges.push(edge);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update points
|
|
function updatePoints() {
|
|
const pulseFactor = params.pulse.avg + (Math.random() - 0.5) * params.pulse.max * 0.1;
|
|
|
|
points.forEach(p => {
|
|
// Move with slight bias toward center
|
|
const centerX = canvas.width / 2;
|
|
const centerY = canvas.height / 2;
|
|
const dx = centerX - p.x;
|
|
const dy = centerY - p.y;
|
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
p.vx += dx / (dist * 100 + 100) * params.motion * 0.5;
|
|
p.vy += dy / (dist * 100 + 100) * params.motion * 0.5;
|
|
|
|
// Apply speed limit
|
|
const speed = Math.sqrt(p.vx * p.vx + p.vy * p.vy);
|
|
if (speed > maxSpeed) {
|
|
p.vx = (p.vx / speed) * maxSpeed;
|
|
p.vy = (p.vy / speed) * maxSpeed;
|
|
}
|
|
|
|
// Update position
|
|
p.x += p.vx * pulseFactor;
|
|
p.y += p.vy * pulseFactor;
|
|
|
|
// Boundary check
|
|
if (p.x < 0 || p.x > canvas.width) p.vx *= -0.5;
|
|
if (p.y < 0 || p.y > canvas.height) p.vy *= -0.5;
|
|
|
|
// Random small variations
|
|
p.vx += (Math.random() - 0.5) * 0.1 * params.motion;
|
|
p.vy += (Math.random() - 0.5) * 0.1 * params.motion;
|
|
|
|
// Fade color based on lifespan
|
|
if (params.lifespan < 0.5) {
|
|
const alpha = params.lifespan * 2;
|
|
const hsl = p.color.match(/\d+/g);
|
|
hsl[2] = Math.max(5, parseInt(hsl[2]) * alpha);
|
|
p.color = `hsl(${hsl[0]}, ${hsl[1]}%, ${hsl[2]}%)`;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Draw Voronoi edges
|
|
function drawVoronoi() {
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
// Draw edges with glow
|
|
edges.forEach(edge => {
|
|
ctx.strokeStyle = edge.color;
|
|
ctx.lineWidth = edge.width * (0.5 + params.complexity * 0.5);
|
|
ctx.beginPath();
|
|
|
|
const p1 = points[edge.p1];
|
|
const p2 = points[edge.p2];
|
|
|
|
ctx.moveTo(p1.x, p1.y);
|
|
ctx.lineTo(p2.x, p2.y);
|
|
ctx.stroke();
|
|
|
|
// Add glow effect
|
|
ctx.shadowBlur = 10 * params.complexity;
|
|
ctx.shadowColor = edge.color;
|
|
ctx.stroke();
|
|
ctx.shadowBlur = 0;
|
|
});
|
|
|
|
// Draw points
|
|
points.forEach(p => {
|
|
ctx.fillStyle = p.color;
|
|
ctx.beginPath();
|
|
ctx.arc(p.x, p.y, p.size * (0.5 + params.density * 0.5), 0, Math.PI * 2);
|
|
ctx.fill();
|
|
});
|
|
}
|
|
|
|
// Animation loop
|
|
function animate() {
|
|
updatePoints();
|
|
calculateVoronoi();
|
|
drawVoronoi();
|
|
|
|
// Randomly add/remove points based on density
|
|
if (Math.random() < 0.02) {
|
|
if (points.length < pointCount * 1.5 && Math.random() < params.density) {
|
|
points.push({
|
|
x: Math.random() * canvas.width,
|
|
y: Math.random() * canvas.height,
|
|
vx: (Math.random() - 0.5) * maxSpeed,
|
|
vy: (Math.random() - 0.5) * maxSpeed,
|
|
color: `hsl(0, 0%, ${Math.random() * 20 + 20}%)`,
|
|
size: Math.random() * 2 + 1,
|
|
energy: 38.8
|
|
});
|
|
} else if (points.length > pointCount * 0.5 && Math.random() > params.density) {
|
|
points.splice(Math.floor(Math.random() * points.length), 1);
|
|
}
|
|
}
|
|
|
|
requestAnimationFrame(animate);
|
|
}
|
|
|
|
animate();
|
|
</script>
|
|
</body>
|
|
</html> |