birth: Voronoi Pulse in Monochrome
This commit is contained in:
parent
be7c8469f4
commit
331787525d
1 changed files with 184 additions and 0 deletions
184
index.html
Normal file
184
index.html
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Neurameba Cellular Voronoi</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: #0a0a0a;
|
||||
color: #fff;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
canvas {
|
||||
display: block;
|
||||
}
|
||||
.attribution {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
font-size: 10px;
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
<div class="attribution">neurameba · motd.social</div>
|
||||
|
||||
<script>
|
||||
const canvas = document.getElementById('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
function resizeCanvas() {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
}
|
||||
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
resizeCanvas();
|
||||
|
||||
// Configuration parameters
|
||||
const params = {
|
||||
density: 0.5,
|
||||
complexity: 0.5,
|
||||
motion: 0.5,
|
||||
connectedness: 0.5,
|
||||
lifespan: 0.5,
|
||||
pulseAvg: 1.25,
|
||||
pulseMin: 1.0,
|
||||
pulseMax: 1.55,
|
||||
dryness: 0.9,
|
||||
curiosity: 0.1
|
||||
};
|
||||
|
||||
// Voronoi cell structure
|
||||
class VoronoiCell {
|
||||
constructor(x, y, size, velocity, persistence) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.originX = x;
|
||||
this.originY = y;
|
||||
this.size = size;
|
||||
this.velocity = velocity;
|
||||
this.persistence = persistence;
|
||||
this.age = 0;
|
||||
this.maxAge = 100 + Math.random() * 200;
|
||||
this.nodes = [];
|
||||
this.generateNodes();
|
||||
}
|
||||
|
||||
generateNodes() {
|
||||
const nodeCount = 3 + Math.floor(params.complexity * 8);
|
||||
for (let i = 0; i < nodeCount; i++) {
|
||||
const angle = (i / nodeCount) * Math.PI * 2;
|
||||
const distance = this.size * 0.3 * (0.7 + Math.random() * 0.6);
|
||||
this.nodes.push({
|
||||
x: this.x + Math.cos(angle) * distance,
|
||||
y: this.y + Math.sin(angle) * distance,
|
||||
baseX: this.x + Math.cos(angle) * distance,
|
||||
baseY: this.y + Math.sin(angle) * distance,
|
||||
size: this.size * (0.3 + Math.random() * 0.4),
|
||||
persistence: this.persistence * (0.7 + Math.random() * 0.6)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
update(pulseFactor) {
|
||||
this.age++;
|
||||
const pulseLerp = (Math.sin(Date.now() * params.pulseAvg * 0.001) * 0.5 + 0.5) *
|
||||
(params.pulseMax - params.pulseMin) + params.pulseMin;
|
||||
|
||||
this.x = this.originX + (Math.sin(Date.now() * 0.0005 * pulseFactor) * 50);
|
||||
this.y = this.originY + (Math.cos(Date.now() * 0.0003 * pulseFactor) * 30);
|
||||
|
||||
this.nodes.forEach(node => {
|
||||
node.x = node.baseX + Math.sin(Date.now() * 0.0004 * node.persistence) * 3;
|
||||
node.y = node.baseY + Math.cos(Date.now() * 0.0002 * node.persistence) * 2;
|
||||
});
|
||||
}
|
||||
|
||||
draw() {
|
||||
ctx.beginPath();
|
||||
const gradient = ctx.createRadialGradient(
|
||||
this.x, this.y, 0,
|
||||
this.x, this.y, this.size
|
||||
);
|
||||
const alpha = this.age / this.maxAge;
|
||||
const adjustedAlpha = alpha * (0.7 + params.dryness * 0.3);
|
||||
|
||||
gradient.addColorStop(0, `rgba(255, 255, 255, ${adjustedAlpha})`);
|
||||
gradient.addColorStop(1, `rgba(255, 255, 255, 0)`);
|
||||
ctx.fillStyle = gradient;
|
||||
|
||||
ctx.moveTo(this.nodes[0].x, this.nodes[0].y);
|
||||
for (let i = 1; i <= this.nodes.length; i++) {
|
||||
const node = this.nodes[i % this.nodes.length];
|
||||
ctx.lineTo(node.x, node.y);
|
||||
}
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
// Create voronoi cells with density constraints
|
||||
const cells = [];
|
||||
function initCells() {
|
||||
const count = Math.floor(params.density * 150) + 50;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const size = 10 + Math.random() * 30;
|
||||
const x = Math.random() * canvas.width;
|
||||
const y = Math.random() * canvas.height;
|
||||
|
||||
// Avoid overlapping initial positions
|
||||
let tooClose = false;
|
||||
for (const cell of cells) {
|
||||
const dist = Math.sqrt((x - cell.x) ** 2 + (y - cell.y) ** 2);
|
||||
if (dist < (size + cell.size) * 0.5) {
|
||||
tooClose = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!tooClose) {
|
||||
const velocity = 0.0005 + Math.random() * 0.001;
|
||||
const persistence = 0.5 + Math.random() * 0.5;
|
||||
cells.push(new VoronoiCell(x, y, size, velocity, persistence));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let lastPulse = 0;
|
||||
function animate() {
|
||||
// Clear with slight fade
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.05)';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Calculate pulse factor for motion variation
|
||||
const pulseNow = Date.now();
|
||||
const pulseFactor = (Math.sin(pulseNow * 0.0002) * 0.5 + 0.5) *
|
||||
(params.pulseMax - params.pulseMin) + params.pulseMin;
|
||||
|
||||
// Update and draw cells
|
||||
cells.forEach(cell => {
|
||||
cell.update(pulseFactor);
|
||||
cell.draw();
|
||||
});
|
||||
|
||||
// Occasionally add new cells to maintain density
|
||||
if (Math.random() < params.density * 0.01 && cells.length < 200) {
|
||||
initCells();
|
||||
}
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
// Start animation
|
||||
initCells();
|
||||
animate();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Reference in a new issue