voronoi-pulse-fields-vho9/index.html

265 lines
No EOL
9.2 KiB
HTML

```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>
```