birth: Voronoi Fields in Monochrome
This commit is contained in:
parent
1132018048
commit
37c809435f
1 changed files with 403 additions and 0 deletions
403
index.html
Normal file
403
index.html
Normal file
|
|
@ -0,0 +1,403 @@
|
|||
<!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>
|
||||
Loading…
Add table
Reference in a new issue