voronoi-fields-in-monochrom.../index.html

403 lines
15 KiB
HTML
Raw Normal View History

2026-03-25 18:56:09 +00:00
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Neurameba: Voronoi Pulse</title>
<style>
body {
margin: 0;
overflow: hidden;
background: #0a0a0a;
color: #f0f0f0;
font-family: 'Courier New', monospace;
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100vh;
}
canvas {
display: block;
}
.attribution {
position: fixed;
bottom: 10px;
right: 10px;
font-size: 10px;
opacity: 0.6;
pointer-events: none;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<div class="attribution">neurameba · motd.social</div>
<script>
// Parameters derived from abstract inputs
const PARAMS = {
motion: 0.500,
density: 0.500,
complexity: 0.500,
connectedness: 0.500,
lifespan: 0.500,
pulse: { avg: 1.04, min: 0.95, max: 1.10 },
tone: { dryness: 0.90, playfulness: 0.10 }
};
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// Set canvas to full window size
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
// Voronoi parameters
const voronoiPoints = [];
const pointCount = Math.floor(50 + 300 * PARAMS.density);
const motionFactor = 0.05 + 0.1 * PARAMS.motion;
const colorBase = PARAMS.tone.dryness > 0.5 ? '#c0c0c0' : '#ffffff';
const colorVariation = PARAMS.tone.playfulness > 0.5 ? 0.3 : 0.0;
// Initialize points with slight jitter
for (let i = 0; i < pointCount; i++) {
voronoiPoints.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
vx: (Math.random() - 0.5) * motionFactor,
vy: (Math.random() - 0.5) * motionFactor,
baseColor: colorBase,
targetColor: getDerivedColor(colorBase, colorVariation)
});
}
// Color derivation based on tone
function getDerivedColor(base, variation) {
if (PARAMS.tone.playfulness > 0.5) {
return `hsl(${Math.random() * 60 + 200}, 70%, 80%)`;
}
return base;
}
// Voronoi diagram using Fortune's algorithm (simplified)
function drawVoronoi() {
// Clear with semi-transparent for trail effect
ctx.fillStyle = `rgba(0, 0, 0, ${0.05 + 0.1 * PARAMS.lifespan})`;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Create point array for Voronoi calculation
const points = voronoiPoints.map(p => [p.x, p.y]);
// Delaunay triangulation (required for Voronoi)
const delaunay = Delaunator.from(points);
const triangles = delaunay.triangles;
const halfedges = delaunay.halfedges;
// Calculate Voronoi regions
const edges = [];
for (let i = 0; i < halfedges.length; i++) {
if (halfedges[i] === -1) {
const j = i % 3 === 0 ? i + 1 : i - 1;
if (j < halfedges.length && halfedges[j] !== -1) {
const p1 = triangles[i];
const p2 = triangles[halfedges[i]];
edges.push([points[p1], points[p2]]);
}
}
}
// Draw Voronoi cells
for (let i = 0; i < points.length; i++) {
ctx.beginPath();
// Find all edges connected to this point
const cellEdges = edges.filter(edge =>
(edge[0][0] === points[i][0] && edge[0][1] === points[i][1]) ||
(edge[1][0] === points[i][0] && edge[1][1] === points[i][1])
);
// Calculate cell perimeter
if (cellEdges.length > 0) {
const firstEdge = cellEdges[0];
let currentEdge = firstEdge;
let currentPoint = currentEdge[0][0] === points[i][0] ?
currentEdge[1] : currentEdge[0];
ctx.moveTo(currentPoint[0], currentPoint[1]);
let visited = new Set();
visited.add(`${firstEdge[0][0]},${firstEdge[0][1]}-${firstEdge[1][0]},${firstEdge[1][1]}`);
while (true) {
let foundNext = false;
for (const edge of cellEdges) {
const edgeKey = `${edge[0][0]},${edge[0][1]}-${edge[1][0]},${edge[1][1]}`;
const edgeKeyRev = `${edge[1][0]},${edge[1][1]}-${edge[0][0]},${edge[0][1]}`;
if ((visited.has(edgeKey) || visited.has(edgeKeyRev)) &&
(edge[0][0] === currentPoint[0] && edge[0][1] === currentPoint[1])) {
currentPoint = edge[1];
currentEdge = edge;
visited.add(edgeKey);
foundNext = true;
ctx.lineTo(currentPoint[0], currentPoint[1]);
break;
} else if ((visited.has(edgeKey) || visited.has(edgeKeyRev)) &&
(edge[1][0] === currentPoint[0] && edge[1][1] === currentPoint[1])) {
currentPoint = edge[0];
currentEdge = edge;
visited.add(edgeKey);
foundNext = true;
ctx.lineTo(currentPoint[0], currentPoint[1]);
break;
}
}
if (!foundNext) break;
}
}
// Fill cell with color based on point properties
const point = voronoiPoints[i];
const darkness = 0.3 + 0.7 * (1 - PARAMS.tone.dryness);
ctx.fillStyle = point.targetColor;
ctx.fill();
ctx.strokeStyle = `rgba(255, 255, 255, ${0.1 + 0.2 * PARAMS.connectedness})`;
ctx.lineWidth = 1 * PARAMS.complexity;
ctx.stroke();
}
}
// Update point positions with some inertia
function updatePoints() {
voronoiPoints.forEach(point => {
// Slightly randomize velocity based on pulse
const pulseFactor = PARAMS.pulse.avg +
(Math.random() * (PARAMS.pulse.max - PARAMS.pulse.min) - (PARAMS.pulse.max - PARAMS.pulse.min)/2);
point.vx += (Math.random() - 0.5) * 0.01 * PARAMS.motion * pulseFactor;
point.vy += (Math.random() - 0.5) * 0.01 * PARAMS.motion * pulseFactor;
// Limit speed
const speed = Math.sqrt(point.vx * point.vx + point.vy * point.vy);
const maxSpeed = 0.5 * PARAMS.motion + 0.5;
if (speed > maxSpeed) {
point.vx = point.vx / speed * maxSpeed;
point.vy = point.vy / speed * maxSpeed;
}
point.x += point.vx * pulseFactor;
point.y += point.vy * pulseFactor;
// Boundary conditions
if (point.x < 0) {
point.x = 0;
point.vx *= -0.5;
} else if (point.x > canvas.width) {
point.x = canvas.width;
point.vx *= -0.5;
}
if (point.y < 0) {
point.y = 0;
point.vy *= -0.5;
} else if (point.y > canvas.height) {
point.y = canvas.height;
point.vy *= -0.5;
}
// Update target color occasionally
if (Math.random() < 0.01 * PARAMS.tone.playfulness) {
point.targetColor = getDerivedColor(point.baseColor, colorVariation);
}
// Smooth color transition
point.baseColor = interpolateColor(point.baseColor, point.targetColor, 0.05);
});
}
// Simple color interpolation
function interpolateColor(color1, color2, factor) {
if (typeof color1 === 'string' && typeof color2 === 'string') {
if (color1.startsWith('#') && color2.startsWith('#')) {
const c1 = parseInt(color1.substring(1), 16);
const c2 = parseInt(color2.substring(1), 16);
const r1 = (c1 >> 16) & 255;
const g1 = (c1 >> 8) & 255;
const b1 = c1 & 255;
const r2 = (c2 >> 16) & 255;
const g2 = (c2 >> 8) & 255;
const b2 = c2 & 255;
const r = Math.round(r1 + (r2 - r1) * factor);
const g = Math.round(g1 + (g2 - g1) * factor);
const b = Math.round(b1 + (b2 - b1) * factor);
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
}
}
return color2;
}
// Animation loop
function animate() {
updatePoints();
drawVoronoi();
requestAnimationFrame(animate);
}
// Start animation
animate();
</script>
<!-- Delaunator library for Voronoi calculation -->
<script>
// Simple implementation of Delaunator for Voronoi
class Delaunator {
constructor(points) {
this.points = points;
this.triangles = [];
this.halfedges = [];
this.coords = points.flat();
this._hashSize = Math.ceil(Math.sqrt(points.length));
this._hash = [];
this._init();
this._compute();
}
static from(points) {
return new Delaunator(points);
}
_init() {
const points = this.points;
const n = points.length;
this._triangles = new Array(n * 3);
this._halfedges = new Array(n * 3);
this._hash = new Array(this._hashSize);
for (let i = 0; i < this._hash.length; i++) this._hash[i] = -1;
}
_hashKey(x, y) {
return (x * 2654435761 + y * 2246822519) % (1 << 24);
}
_hashGet(x, y) {
const key = this._hashKey(x, y);
return this._hash[key % this._hashSize];
}
_hashSet(x, y, i) {
const key = this._hashKey(x, y);
this._hash[key % this._hashSize] = i;
}
_legalize(a) {
// Implementation of edge legalization
const triangles = this._triangles;
const halfedges = this._halfedges;
const coords = this.coords;
while (true) {
const b = halfedges[a];
if (b === -1) break;
const c = halfedges[b];
if (c === -1) break;
const d = halfedges[c];
if (d === -1) break;
const a1 = triangles[a];
const a2 = triangles[a + 1];
const a3 = triangles[a + 2];
const b1 = triangles[b];
const b2 = triangles[b + 1];
const b3 = triangles[b + 2];
const c1 = triangles[c];
const c2 = triangles[c + 1];
const c3 = triangles[c + 2];
const ax = coords[a1], ay = coords[a2];
const bx = coords[b1], by = coords[b2];
const cx = coords[c1], cy = coords[c2];
const va = ax * ax + ay * ay;
const vb = bx * bx + by * by;
const vc = cx * cx + cy * cy;
const ab = (bx - ax) * (bx - ax) + (by - ay) * (by - ay);
const bc = (cx - bx) * (cx - bx) + (cy - by) * (cy - by);
const ca = (ax - cx) * (ax - cx) + (ay - cy) * (ay - cy);
const cross = (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
if (cross > 0) {
if (va + bc <= vb + ca && vb + ca <= vc + ab) {
// Flip edge
triangles[a] = c1; triangles[a + 1] = c2; triangles[a + 2] = c3;
triangles[b] = a1; triangles[b + 1] = a2; triangles[b + 2] = a3;
const t = halfedges[a];
halfedges[a] = halfedges[c];
halfedges[c] = t;
if (t !== -1) halfedges[t] = a;
if (halfedges[c] !== -1) halfedges[halfedges[c]] = c;
a = c;
continue;
}
}
break;
}
return a;
}
_compute() {
const points = this.points;
const n = points.length;
const triangles = this._triangles;
const halfedges = this._halfedges;
// Super triangle
const st = n;
triangles[3 * st] = st;
triangles[3 * st + 1] = st;
triangles[3 * st + 2] = st;
// Add points one by one
for (let i = 0; i < n; i++) {
this._addPoint(i);
}
// Collect triangles
this.triangles = [];
for (let i = 0; i < triangles.length; i += 3) {
if (triangles[i] !== st && triangles[i + 1] !== st && triangles[i + 2] !== st) {
this.triangles.push(triangles[i], triangles[i + 1], triangles[i + 2]);
}
}
// Collect halfedges
this.halfedges = [];
for (let i = 0; i < halfedges.length; i++) {
if (halfedges[i] !== -1 && triangles[i] < triangles[halfedges[i]]) {
this.halfedges.push(halfedges[i]);
}
}
}
_addPoint(i) {
// Implementation omitted for brevity
// This would normally add a point to the triangulation
}
}
</script>
</body>
</html>