voronoi-echoes-in-grey-welf/index.html

249 lines
No EOL
8.5 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 Dreamscape</title>
<style>
body {
margin: 0;
overflow: hidden;
background: #0a0a0a;
font-family: 'Courier New', monospace;
}
canvas {
display: block;
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
}
#attribution {
position: fixed;
bottom: 10px;
right: 10px;
color: #555;
font-size: 10px;
z-index: 10;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<div id="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();
// Parameters derived from the generative input
const params = {
motion: 0.5,
density: 0.5,
complexity: 0.5,
connectedness: 0.5,
lifespan: 0.5,
pulse: { avg: 1.07, min: 1.0, max: 1.1 },
tone: {
anger: 0.0,
sadness: 0.0,
curiosity: 0.0,
dryness: 0.9,
playfulness: 0.0,
tension: 0.0
}
};
// Voronoi-specific parameters
const siteCount = Math.floor(50 + params.density * 300);
const relaxIterations = Math.floor(3 + params.complexity * 5);
const voronoiPoints = [];
const cells = [];
const cellTrails = [];
// Initialize Voronoi sites
function initSites() {
voronoiPoints.length = 0;
for (let i = 0; i < siteCount; i++) {
voronoiPoints.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,
life: params.lifespan * 1000,
maxLife: params.lifespan * 1000
});
}
}
// Relax Voronoi sites (reduce jagged edges)
function relaxSites() {
for (let iter = 0; iter < relaxIterations; iter++) {
const newPoints = [];
// Calculate centroids
const centroids = new Array(siteCount).fill().map(() => ({x:0, y:0, count:0}));
// For each site, find its Voronoi cell and calculate centroid
for (let i = 0; i < siteCount; i++) {
const p1 = voronoiPoints[i];
let cx = 0, cy = 0, count = 0;
for (let j = 0; j < siteCount; j++) {
const p2 = voronoiPoints[j];
if (i === j) continue;
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
const d = Math.sqrt(dx*dx + dy*dy);
if (d < 20 + params.complexity * 30) {
cx += p2.x;
cy += p2.y;
count++;
}
}
if (count > 0) {
centroids[i] = {
x: cx / count,
y: cy / count,
count: count
};
}
}
// Move points toward centroids
for (let i = 0; i < siteCount; i++) {
const centroid = centroids[i];
if (centroid.count > 0) {
voronoiPoints[i].x = voronoiPoints[i].x * 0.5 + centroid.x * 0.5;
voronoiPoints[i].y = voronoiPoints[i].y * 0.5 + centroid.y * 0.5;
}
}
}
}
// Perform Delaunay triangulation and Voronoi diagram
function computeVoronoi() {
// Clear previous cells
cells.length = 0;
// Create a copy of points that we can sort
const sortedPoints = [...voronoiPoints].sort((a, b) => {
return a.x - b.x;
});
// Simple Voronoi implementation (approximate)
// For each pixel, find closest point
for (let x = 0; x < canvas.width; x += 2) {
for (let y = 0; y < canvas.height; y += 2) {
let closest = 0;
let minDist = Infinity;
let secondMinDist = Infinity;
for (let i = 0; i < siteCount; i++) {
const p = voronoiPoints[i];
const dx = x - p.x;
const dy = y - p.y;
const dist = dx*dx + dy*dy;
if (dist < minDist) {
secondMinDist = minDist;
minDist = dist;
closest = i;
} else if (dist < secondMinDist) {
secondMinDist = dist;
}
}
// Store cell with anchor point
cells.push({
x: x,
y: y,
color: `hsl(0, 0%, ${20 + 50 * (minDist / (secondMinDist + 1))}%)`,
anchor: closest,
dist: minDist
});
}
}
}
// Update points movement
function updatePoints() {
voronoiPoints.forEach(point => {
// Aging
point.life -= params.pulse.avg * 2;
if (point.life <= 0) {
point.x = Math.random() * canvas.width;
point.y = Math.random() * canvas.height;
point.life = point.maxLife;
}
// Movement
point.x += point.vx * (0.5 + Math.sin(Date.now() * 0.001) * 0.5);
point.y += point.vy * (0.5 + Math.sin(Date.now() * 0.001) * 0.5);
// Boundary check
if (point.x < 0 || point.x > canvas.width) point.vx *= -1;
if (point.y < 0 || point.y > canvas.height) point.vy *= -1;
});
}
// Draw with dryness-appropriate style
function drawVoronoi() {
if (params.tone.dryness > 0.7) {
// Monochrome style for dryness
ctx.fillStyle = 'rgba(50, 50, 50, 0.05)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw cells with subtle variations
cells.forEach(cell => {
const intensity = Math.min(50, Math.max(10, 10 + cell.dist * 0.01));
ctx.fillStyle = `rgba(180, 180, 180, ${0.03 + params.tone.dryness * 0.02})`;
ctx.fillRect(cell.x, cell.y, 1.5, 1.5);
});
} else {
// Alternative style if dryness is lower
ctx.fillStyle = 'rgba(10, 5, 15, 0.05)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
cells.forEach(cell => {
const gray = Math.floor(30 + cell.dist * 0.05);
ctx.fillStyle = `rgba(${gray}, ${gray}, ${gray}, 0.03)`;
ctx.fillRect(cell.x, cell.y, 2, 2);
});
}
// Draw point circles to show movement
voronoiPoints.forEach(point => {
const size = 2 + Math.sin(Date.now() * 0.005 + point.x) * 0.5;
ctx.fillStyle = `hsl(0, 0%, ${70 + Math.sin(point.x * 0.01) * 10}%)`;
ctx.beginPath();
ctx.arc(point.x, point.y, size, 0, Math.PI * 2);
ctx.fill();
});
}
function animate() {
updatePoints();
relaxSites();
computeVoronoi();
drawVoronoi();
requestAnimationFrame(animate);
}
initSites();
relaxSites();
computeVoronoi();
animate();
</script>
</body>
</html>