teal-voronoi-echoes-2ehz/index.html

252 lines
No EOL
8.8 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<title>Neurameba · motd.social</title>
<style>
body { margin: 0; overflow: hidden; background: #000; color: #fff; font-family: monospace; }
#canvas { display: block; }
#attribution { position: absolute; bottom: 20px; right: 20px; font-size: 10px; opacity: 0.5; }
</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 resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
window.addEventListener('resize', resize);
resize();
const params = {
motion: 0.5,
density: 0.5,
complexity: 0.5,
connectedness: 0.5,
lifespan: 0.5,
pulse: 1.07,
tone: {
anger: 0.00,
sadness: 0.00,
curiosity: 0.70,
dryness: 0.80,
playfulness: 0.30,
tension: 0.00
}
};
let time = 0;
const points = [];
const voronoiCells = [];
// Initialize points
function initPoints() {
const count = Math.floor(params.density * 1000) + 100;
for (let i = 0; i < count; i++) {
points.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
vx: (Math.random() - 0.5) * params.motion * 2,
vy: (Math.random() - 0.5) * params.motion * 2,
size: Math.random() * 3 + 1,
color: getColor()
});
}
}
function getColor() {
const hue = 180 + Math.sin(time * 0.01) * 30;
const saturation = 30 + params.tone.playfulness * 70;
const lightness = 60 + params.tone.dryness * 20;
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
}
function updatePoints() {
points.forEach(p => {
p.x += p.vx;
p.y += p.vy;
if (p.x < 0 || p.x > canvas.width) p.vx *= -1;
if (p.y < 0 || p.y > canvas.height) p.vy *= -1;
});
// Generate voronoi diagram
voronoiCells.length = 0;
const cells = Delaunay.triangulate(points.map(p => [p.x, p.y]));
for (let i = 0; i < cells.length; i += 3) {
const a = points[cells[i]];
const b = points[cells[i+1]];
const c = points[cells[i+2]];
const centroid = {
x: (a.x + b.x + c.x) / 3,
y: (a.y + b.y + c.y) / 3
};
const edges = [];
edges.push({x1: a.x, y1: a.y, x2: b.x, y2: b.y});
edges.push({x1: b.x, y1: b.y, x2: c.x, y2: c.y});
edges.push({x1: c.x, y1: c.y, x2: a.x, y2: a.y});
voronoiCells.push({
centroid,
edges,
color: getColor()
});
}
}
function draw() {
// Fade effect for low lifespan
ctx.fillStyle = `rgba(0, 0, 0, ${1 - params.lifespan})`;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw voronoi cells
voronoiCells.forEach(cell => {
ctx.strokeStyle = cell.color;
ctx.lineWidth = 1;
cell.edges.forEach(edge => {
ctx.beginPath();
ctx.moveTo(edge.x1, edge.y1);
ctx.lineTo(edge.x2, edge.y2);
ctx.stroke();
});
// Draw centroid if high complexity
if (params.complexity > 0.5) {
ctx.fillStyle = cell.color;
ctx.beginPath();
ctx.arc(cell.centroid.x, cell.centroid.y, 2, 0, Math.PI * 2);
ctx.fill();
}
});
time += params.pulse;
}
function animate() {
updatePoints();
draw();
requestAnimationFrame(animate);
}
// Delaunay triangulation library
const Delaunay = {
triangulate: function(points) {
const edges = [];
const triangles = [];
// Bowyer-Watson algorithm
const superTriangle = this.createSuperTriangle(points);
triangles.push(superTriangle);
for (let i = 0; i < points.length; i++) {
const point = points[i];
const badTriangles = [];
for (let j = 0; j < triangles.length; j++) {
const triangle = triangles[j];
if (this.circumCircleContains(triangle, point)) {
badTriangles.push(triangle);
}
}
const polygon = [];
for (let j = 0; j < badTriangles.length; j++) {
const triangle = badTriangles[j];
for (let k = 0; k < 3; k++) {
const edge = {
p1: triangle[k],
p2: triangle[(k+1)%3]
};
let shared = false;
for (let m = 0; m < badTriangles.length; m++) {
if (m !== j) {
for (let n = 0; n < 3; n++) {
if (badTriangles[m][n] === edge.p1 && badTriangles[m][(n+1)%3] === edge.p2) {
shared = true;
break;
}
}
}
if (shared) break;
}
if (!shared) {
polygon.push(edge);
}
}
}
triangles.filter(t => !badTriangles.includes(t));
for (let j = 0; j < polygon.length; j++) {
const edge = polygon[j];
triangles.push([edge.p1, edge.p2, points.length]);
}
}
// Remove super triangle points
return triangles
.filter(t => t.indexOf(points.length) === -1 &&
t.indexOf(points.length+1) === -1 &&
t.indexOf(points.length+2) === -1)
.flat();
},
createSuperTriangle: function(points) {
let minX = points[0][0], maxX = minX;
let minY = points[0][1], maxY = minY;
for (let i = 0; i < points.length; i++) {
const p = points[i];
if (p[0] < minX) minX = p[0];
if (p[0] > maxX) maxX = p[0];
if (p[1] < minY) minY = p[1];
if (p[1] > maxY) maxY = p[1];
}
const dx = maxX - minX;
const dy = maxY - minY;
const deltaMax = Math.max(dx, dy) * 2;
const midX = (minX + maxX) / 2;
const midY = (minY + maxY) / 2;
return [
[midX - deltaMax, midY - deltaMax],
[midX, midY + deltaMax],
[midX + deltaMax, midY - deltaMax]
];
},
circumCircleContains: function(triangle, point) {
const A = triangle[0];
const B = triangle[1];
const C = triangle[2];
const ax = A[0], ay = A[1];
const bx = B[0], by = B[1];
const cx = C[0], cy = C[1];
const px = point[0], py = point[1];
const d = (ax*(by-cy) + bx*(cy-ay) + cx*(ay-by))*2;
if (d === 0) return false;
const x = ((ax*ax+ay*ay)*(by-cy) + (bx*bx+by*by)*(cy-ay) + (cx*cx+cy*cy)*(ay-by)) / d;
const y = ((ax*ax+ay*ay)*(cx-bx) + (bx*bx+by*by)*(ax-cx) + (cx*cx+cy*cy)*(bx-ax)) / d;
const radius = Math.sqrt((px-x)*(px-x) + (py-y)*(py-y));
const dist = Math.sqrt((ax-x)*(ax-x) + (ay-y)*(ay-y));
return radius >= dist;
}
};
initPoints();
animate();
</script>
</body>
</html>