237 lines
No EOL
8.9 KiB
HTML
237 lines
No EOL
8.9 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 Nebula</title>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
overflow: hidden;
|
|
background: #0a0a1a;
|
|
font-family: 'Courier New', monospace;
|
|
}
|
|
canvas {
|
|
display: block;
|
|
background: transparent;
|
|
}
|
|
#info {
|
|
position: absolute;
|
|
bottom: 10px;
|
|
left: 0;
|
|
right: 0;
|
|
color: #fff;
|
|
text-align: center;
|
|
font-size: 10px;
|
|
pointer-events: none;
|
|
}
|
|
</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 from prompt
|
|
const params = {
|
|
motion: 0.606,
|
|
density: 0.563,
|
|
complexity: 0.528,
|
|
connectedness: 0.376,
|
|
lifespan: 0.463,
|
|
loops: 118,
|
|
maxDepth: 10,
|
|
thicknessRatio: 1.25,
|
|
fractalDim: 0.5,
|
|
finalEnergy: 64.8,
|
|
pulse: { avg: 0.36, min: 0.3, max: 1.35 },
|
|
tone: { curiosity: 0.8, dryness: 0.9 }
|
|
};
|
|
|
|
// Voronoi setup
|
|
const points = [];
|
|
const cells = [];
|
|
const edges = [];
|
|
const colors = [];
|
|
|
|
// Generate points
|
|
const pointCount = Math.max(10, Math.floor(100 * params.density));
|
|
for (let i = 0; i < pointCount; i++) {
|
|
points.push({
|
|
x: Math.random() * canvas.width,
|
|
y: Math.random() * canvas.height,
|
|
vx: (Math.random() - 0.5) * 2 * params.motion,
|
|
vy: (Math.random() - 0.5) * 2 * params.motion,
|
|
lifetime: params.lifespan > 0.5 ? 1000 : 200 + Math.random() * 100
|
|
});
|
|
}
|
|
|
|
// Colors based on tone
|
|
const baseColor = params.tone.dryness > 0.7 ? [0.9, 0.9, 0.9] : [0.2, 0.8, 0.7];
|
|
const saturation = params.tone.dryness > 0.7 ? 0.1 : 0.8;
|
|
const lightness = 0.7 + Math.sin(Date.now() * 0.001) * 0.1;
|
|
|
|
// Animation state
|
|
let time = 0;
|
|
|
|
function animate() {
|
|
time += 1;
|
|
|
|
// Clear with subtle trail
|
|
ctx.fillStyle = `rgba(10, 10, 26, ${0.05 + 0.05 * params.motion})`;
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
// Update points
|
|
points.forEach(p => {
|
|
p.x += p.vx;
|
|
p.y += p.vy;
|
|
|
|
if (p.lifetime-- < 0) {
|
|
// Regenerate point if faded
|
|
p.x = Math.random() * canvas.width;
|
|
p.y = Math.random() * canvas.height;
|
|
p.vx = (Math.random() - 0.5) * 2 * params.motion;
|
|
p.vy = (Math.random() - 0.5) * 2 * params.motion;
|
|
p.lifetime = params.lifespan > 0.5 ? 1000 : 200 + Math.random() * 100;
|
|
}
|
|
|
|
// Edge wrapping
|
|
if (p.x < 0) p.x = canvas.width;
|
|
if (p.x > canvas.width) p.x = 0;
|
|
if (p.y < 0) p.y = canvas.height;
|
|
if (p.y > canvas.height) p.y = 0;
|
|
});
|
|
|
|
// Calculate Voronoi diagram
|
|
const voronoi = new Voronoi();
|
|
const bbox = { xl: 0, xr: canvas.width, yt: 0, yb: canvas.height };
|
|
const diagram = voronoi.compute(points, bbox);
|
|
|
|
// Draw Voronoi cells
|
|
ctx.strokeStyle = `hsla(${200 + Math.sin(time*0.001)*30}, ${saturation*100}%, ${lightness*100}%, 0.3)`;
|
|
ctx.lineWidth = 0.5 + params.thicknessRatio * 0.5;
|
|
|
|
diagram.cells.forEach(cell => {
|
|
if (!cell.halfedges.length) return;
|
|
|
|
const first = cell.halfedges[0].getStartpoint();
|
|
ctx.beginPath();
|
|
ctx.moveTo(first.x, first.y);
|
|
|
|
for (let i = 0; i < cell.halfedges.length; i++) {
|
|
const edge = cell.halfedges[i];
|
|
const end = edge.getEndpoint();
|
|
ctx.lineTo(end.x, end.y);
|
|
}
|
|
ctx.closePath();
|
|
ctx.stroke();
|
|
|
|
// Fill with semi-transparent color
|
|
const hue = 200 + Math.floor(cell.site.y % 30);
|
|
const sat = saturation * 80;
|
|
const light = lightness * 100 + Math.sin(cell.site.x * 0.01 + time*0.001) * 10;
|
|
ctx.fillStyle = `hsla(${hue}, ${sat}%, ${light}%, 0.1)`;
|
|
ctx.fill();
|
|
});
|
|
|
|
requestAnimationFrame(animate);
|
|
}
|
|
|
|
// Simple Voronoi implementation (diagram only)
|
|
function Voronoi() {
|
|
this.compute = function(sites, bbox) {
|
|
const diagram = {
|
|
cells: [],
|
|
edges: [],
|
|
vertices: []
|
|
};
|
|
|
|
// Simple centroidal Voronoi with Lloyd relaxation
|
|
for (let i = 0; i < 3; i++) {
|
|
diagram.cells = [];
|
|
const diagramEdges = [];
|
|
|
|
// Assign each point to cells (simplified)
|
|
const cells = sites.map(site => ({
|
|
site,
|
|
halfedges: [],
|
|
close: (p1, p2) => {
|
|
const edge = { leftSite: site, rightSite: p2 };
|
|
site.halfedges.push({
|
|
edge,
|
|
getStartpoint: () => site,
|
|
getEndpoint: () => p2
|
|
});
|
|
diagramEdges.push(edge);
|
|
return edge;
|
|
}
|
|
}));
|
|
|
|
// Connect neighbors (simplified adjacency)
|
|
for (let i = 0; i < sites.length; i++) {
|
|
const neighbors = [];
|
|
for (let j = 0; j < sites.length; j++) {
|
|
if (i === j) continue;
|
|
const dx = sites[j].x - sites[i].x;
|
|
const dy = sites[j].y - sites[i].y;
|
|
const dist = dx*dx + dy*dy;
|
|
if (dist < 20000 * params.density) {
|
|
neighbors.push(sites[j]);
|
|
}
|
|
}
|
|
|
|
if (neighbors.length > 0) {
|
|
// Connect to nearest neighbors
|
|
neighbors.sort((a,b) => {
|
|
const dx1 = a.x - sites[i].x;
|
|
const dy1 = a.y - sites[i].y;
|
|
const dx2 = b.x - sites[i].x;
|
|
const dy2 = b.y - sites[i].y;
|
|
return (dx1*dx1 + dy1*dy1) - (dx2*dx2 + dy2*dy2);
|
|
});
|
|
|
|
for (let k = 0; k < Math.min(3, neighbors.length); k++) {
|
|
cells[i].close(neighbors[k]);
|
|
}
|
|
}
|
|
}
|
|
|
|
diagram.cells = cells;
|
|
this.cells = cells;
|
|
|
|
// Update sites positions slightly (Lloyd relaxation)
|
|
sites.forEach((site, i) => {
|
|
let cx = 0, cy = 0, count = 0;
|
|
cells[i].halfedges.forEach(h => {
|
|
const p = h.getEndpoint();
|
|
cx += p.x;
|
|
cy += p.y;
|
|
count++;
|
|
});
|
|
|
|
if (count > 0) {
|
|
site.x = cx / count;
|
|
site.y = cy / count;
|
|
}
|
|
});
|
|
}
|
|
|
|
return diagram;
|
|
};
|
|
}
|
|
|
|
animate();
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html> |