birth: Voronoi Pulse Fields
This commit is contained in:
parent
418f855c14
commit
bc668be4e1
1 changed files with 265 additions and 0 deletions
265
index.html
Normal file
265
index.html
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
```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;
|
||||
font-family: 'Courier New', monospace;
|
||||
color: #ccc;
|
||||
}
|
||||
#info {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
font-size: 11px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
<div id="info">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();
|
||||
|
||||
// Parameters derived from input
|
||||
const params = {
|
||||
motion: 0.434,
|
||||
density: 0.494,
|
||||
complexity: 0.495,
|
||||
connectedness: 0.491,
|
||||
lifespan: 0.479,
|
||||
pulse: { avg: 0.58, min: 0.30, max: 2.00 },
|
||||
tone: { curiosity: 0.70, dryness: 0.90, playfulness: 0.10 }
|
||||
};
|
||||
|
||||
// Voronoi setup
|
||||
const sites = [];
|
||||
const cells = [];
|
||||
const voronoi = new Voronoi();
|
||||
const bbox = { xl: 0, xr: canvas.width, yt: 0, yb: canvas.height };
|
||||
|
||||
// Create initial sites
|
||||
const siteCount = Math.floor(100 + params.density * 200);
|
||||
for (let i = 0; i < siteCount; i++) {
|
||||
sites.push({
|
||||
x: Math.random() * canvas.width,
|
||||
y: Math.random() * canvas.height,
|
||||
origX: 0,
|
||||
origY: 0
|
||||
});
|
||||
}
|
||||
|
||||
// Main animation
|
||||
let time = 0;
|
||||
let energy = params.pulse.avg * 500;
|
||||
const maxEnergy = 1000;
|
||||
const minEnergy = 0;
|
||||
|
||||
function animate() {
|
||||
// Clear with semi-transparent for glow effect
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.05)';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Update sites based on motion
|
||||
const pulse = params.pulse.avg * (params.pulse.min + (params.pulse.max - params.pulse.min) * Math.sin(time * 0.002));
|
||||
const motionFactor = params.motion * 2;
|
||||
|
||||
sites.forEach(site => {
|
||||
// Add small random motion
|
||||
site.x += (Math.random() - 0.5) * motionFactor * pulse;
|
||||
site.y += (Math.random() - 0.5) * motionFactor * pulse;
|
||||
|
||||
// Keep within bounds
|
||||
site.x = Math.max(0, Math.min(canvas.width, site.x));
|
||||
site.y = Math.max(0, Math.min(canvas.height, site.y));
|
||||
|
||||
// Store original position for drawing
|
||||
site.origX = site.x;
|
||||
site.origY = site.y;
|
||||
});
|
||||
|
||||
// Relax voronoi diagram
|
||||
if (Math.random() < 0.2) {
|
||||
const diagram = voronoi.compute(sites, bbox);
|
||||
cells.length = 0;
|
||||
diagram.cells.forEach(cell => {
|
||||
if (cell.halfedges.length >= 3) {
|
||||
cells.push({
|
||||
site: cell.site,
|
||||
halfedges: cell.halfedges.map(he =>
|
||||
({ getStartpoint: () => ({
|
||||
x: he.getStartpoint().x + (Math.random() - 0.5) * 2,
|
||||
y: he.getStartpoint().y + (Math.random() - 0.5) * 2
|
||||
}),
|
||||
getEndpoint: () => ({
|
||||
x: he.getEndpoint().x + (Math.random() - 0.5) * 2,
|
||||
y: he.getEndpoint().y + (Math.random() - 0.5) * 2
|
||||
}) })
|
||||
)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Draw voronoi cells
|
||||
ctx.strokeStyle = `rgba(255, 255, 255, ${0.8 * params.tone.dryness})`;
|
||||
ctx.lineWidth = 1 * (1 + params.complexity * 0.5);
|
||||
|
||||
cells.forEach(cell => {
|
||||
ctx.beginPath();
|
||||
cell.halfedges.forEach((he, i) => {
|
||||
const start = he.getStartpoint();
|
||||
const end = he.getEndpoint();
|
||||
|
||||
if (i === 0) {
|
||||
ctx.moveTo(start.x, start.y);
|
||||
} else {
|
||||
ctx.lineTo(start.x, start.y);
|
||||
}
|
||||
ctx.lineTo(end.x, end.y);
|
||||
});
|
||||
ctx.closePath();
|
||||
ctx.stroke();
|
||||
});
|
||||
|
||||
// Draw site points with energy-based size
|
||||
const energyFactor = energy / maxEnergy;
|
||||
ctx.fillStyle = `rgba(0, 255, 255, ${0.5 * params.tone.curiosity})`;
|
||||
sites.forEach(site => {
|
||||
const size = 2 + energyFactor * 4;
|
||||
ctx.beginPath();
|
||||
ctx.arc(site.origX, site.origY, size, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
});
|
||||
|
||||
// Update energy based on pulse
|
||||
energy += (params.pulse.avg - 0.5) * 20;
|
||||
energy = Math.max(minEnergy, Math.min(maxEnergy, energy));
|
||||
|
||||
time++;
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
// Simple Voronoi implementation
|
||||
function Voronoi() {
|
||||
this.compute = function(sites, bbox) {
|
||||
const cells = [];
|
||||
const edges = [];
|
||||
|
||||
// Create cells
|
||||
for (let i = 0; i < sites.length; i++) {
|
||||
cells.push({
|
||||
site: sites[i],
|
||||
halfedges: []
|
||||
});
|
||||
}
|
||||
|
||||
// Simple edge creation (not a full Voronoi implementation)
|
||||
for (let i = 0; i < cells.length; i++) {
|
||||
for (let j = i + 1; j < cells.length; j++) {
|
||||
edges.push({
|
||||
left: cells[i],
|
||||
right: cells[j],
|
||||
start: null,
|
||||
end: null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Simplistic edge clipping
|
||||
edges.forEach(edge => {
|
||||
const dx = edge.right.site.x - edge.left.site.x;
|
||||
const dy = edge.right.site.y - edge.left.site.y;
|
||||
const d = Math.sqrt(dx * dx + dy * dy);
|
||||
const nx = -dy / d;
|
||||
const ny = dx / d;
|
||||
|
||||
edge.start = {
|
||||
x: edge.left.site.x + nx * 1000,
|
||||
y: edge.left.site.y + ny * 1000
|
||||
};
|
||||
edge.end = {
|
||||
x: edge.right.site.x - nx * 1000,
|
||||
y: edge.right.site.y - ny * 1000
|
||||
};
|
||||
|
||||
// Clip to bbox
|
||||
const clip = (p1, p2) => {
|
||||
const points = [];
|
||||
const regions = [
|
||||
[bbox.xl, bbox.yt], [bbox.xr, bbox.yt],
|
||||
[bbox.xr, bbox.yb], [bbox.xl, bbox.yb]
|
||||
];
|
||||
|
||||
for (let i = 0; i < regions.length; i++) {
|
||||
const p = regions[i];
|
||||
const nextP = regions[(i + 1) % regions.length];
|
||||
const cx = nextP[0] - p[0];
|
||||
const cy = nextP[1] - p[1];
|
||||
|
||||
const d1 = (p1.x - p[0]) * cy - (p1.y - p[1]) * cx;
|
||||
const d2 = (p2.x - p[0]) * cy - (p2.y - p[1]) * cx;
|
||||
|
||||
if ((d1 > 0 && d2 > 0) || (d1 < 0 && d2 < 0)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const t = d1 / (d1 - d2);
|
||||
points.push({
|
||||
x: p1.x + (p2.x - p1.x) * t,
|
||||
y: p1.y + (p2.y - p1.y) * t
|
||||
});
|
||||
}
|
||||
|
||||
if (points.length > 0) {
|
||||
return points;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const clippedStart = clip(edge.start, edge.end);
|
||||
if (clippedStart.length > 0) {
|
||||
edge.start = clippedStart[0];
|
||||
edge.end = clippedStart[clippedStart.length - 1];
|
||||
}
|
||||
});
|
||||
|
||||
// Assign halfedges to cells
|
||||
edges.forEach(edge => {
|
||||
edge.left.halfedges.push({
|
||||
edge: edge,
|
||||
getStartpoint: () => edge.start,
|
||||
getEndpoint: () => edge.end
|
||||
});
|
||||
|
||||
edge.right.halfedges.push({
|
||||
edge: edge,
|
||||
getStartpoint: () => edge.end,
|
||||
getEndpoint: () => edge.start
|
||||
});
|
||||
});
|
||||
|
||||
return { cells: cells };
|
||||
};
|
||||
}
|
||||
|
||||
animate();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
Loading…
Add table
Reference in a new issue