265 lines
No EOL
9.2 KiB
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>
|
|
``` |