261 lines
No EOL
10 KiB
HTML
261 lines
No EOL
10 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Neurameba Organism</title>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
overflow: hidden;
|
|
background: #000;
|
|
font-family: 'Courier New', monospace;
|
|
color: #fff;
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100vh;
|
|
}
|
|
canvas {
|
|
display: block;
|
|
flex: 1;
|
|
}
|
|
#attribution {
|
|
text-align: right;
|
|
padding: 10px;
|
|
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 resizeCanvas() {
|
|
canvas.width = window.innerWidth;
|
|
canvas.height = window.innerHeight;
|
|
}
|
|
|
|
window.addEventListener('resize', resizeCanvas);
|
|
resizeCanvas();
|
|
|
|
// Parameters derived from the abstract specifications
|
|
const params = {
|
|
motion: 0.463,
|
|
density: 0.496,
|
|
complexity: 0.313,
|
|
connectedness: 0.434,
|
|
lifespan: 0.678,
|
|
pulse: { avg: 0.72, min: 0.30, max: 1.40 },
|
|
tone: { anger: 0.00, sadness: 0.00, curiosity: 0.30, dryness: 0.80, playfulness: 0.00, tension: 0.00 },
|
|
topology: {
|
|
survivingNodes: 7,
|
|
branchCount: 7,
|
|
loops: 25,
|
|
maxDepth: 8,
|
|
thicknessRatio: 1.25,
|
|
fractalDimension: 0.500,
|
|
finalEnergy: 28.8
|
|
}
|
|
};
|
|
|
|
// Reaction-diffusion system with adaptive parameters
|
|
const W = canvas.width;
|
|
const H = canvas.height;
|
|
|
|
// Initialize grids
|
|
let A = new Array(W * H).fill(0);
|
|
let B = new Array(W * H).fill(0);
|
|
let A_prev = new Array(W * H).fill(0);
|
|
let B_prev = new Array(W * H).fill(0);
|
|
|
|
// Reaction-diffusion parameters
|
|
const Da = 1.0 + params.motion * 0.5;
|
|
const Db = 0.5 + params.connectedness * 0.3;
|
|
const feed = 0.0545 + params.complexity * 0.02;
|
|
const kill = 0.062 + params.connectedness * 0.01;
|
|
const timeStep = 0.05;
|
|
const scale = 10 * (1 - params.dryness);
|
|
|
|
// Initialize with sparse pattern based on density
|
|
function initPattern() {
|
|
const sparsity = 1 - params.density;
|
|
for (let i = 0; i < W * H; i++) {
|
|
const x = i % W;
|
|
const y = Math.floor(i / W);
|
|
|
|
// Create sparse initial pattern with some clustering
|
|
const distToCenter = Math.sqrt(Math.pow(x - W/2, 2) + Math.pow(y - H/2, 2));
|
|
const centerFactor = 1 - Math.min(1, distToCenter / Math.max(W, H) * 2);
|
|
|
|
A_prev[i] = 1;
|
|
B_prev[i] = 0;
|
|
|
|
// Add some variation based on complexity
|
|
if (Math.random() > sparsity * (0.7 + params.complexity * 0.3)) {
|
|
A_prev[i] *= 1 - Math.random() * 0.3;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Reaction-diffusion step
|
|
function step() {
|
|
// Swap buffers
|
|
[A_prev, A] = [A, A_prev];
|
|
[B_prev, B] = [B, B_prev];
|
|
|
|
const width = W;
|
|
const height = H;
|
|
|
|
for (let y = 1; y < height - 1; y++) {
|
|
for (let x = 1; x < width - 1; x++) {
|
|
const i = x + y * width;
|
|
|
|
// Diffusion
|
|
A[i] = A_prev[i] +
|
|
(Da * laplacian(A_prev, x, y, width, height) -
|
|
A_prev[i] * B_prev[i] * B_prev[i] +
|
|
feed * (1 - A_prev[i])) * timeStep;
|
|
|
|
B[i] = B_prev[i] +
|
|
(Db * laplacian(B_prev, x, y, width, height) +
|
|
A_prev[i] * B_prev[i] * B_prev[i] -
|
|
(kill + feed) * B_prev[i]) * timeStep;
|
|
|
|
// Apply pulse variation
|
|
const pulseFactor = params.pulse.avg + (Math.random() * (params.pulse.max - params.pulse.min) - (params.pulse.max - params.pulse.min)/2);
|
|
A[i] *= pulseFactor;
|
|
B[i] *= pulseFactor;
|
|
|
|
// Apply energy decay
|
|
A[i] *= 1 - (params.topology.finalEnergy / 100) * timeStep;
|
|
B[i] *= 1 - (params.topology.finalEnergy / 100) * timeStep;
|
|
|
|
// Clamp values
|
|
A[i] = Math.max(0, Math.min(1, A[i]));
|
|
B[i] = Math.max(0, Math.min(1, B[i]));
|
|
}
|
|
}
|
|
}
|
|
|
|
function laplacian(grid, x, y, width, height) {
|
|
let sum = 0;
|
|
for (let dy = -1; dy <= 1; dy++) {
|
|
for (let dx = -1; dx <= 1; dx++) {
|
|
if (dx === 0 && dy === 0) continue;
|
|
const nx = x + dx;
|
|
const ny = y + dy;
|
|
if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
|
|
sum += grid[nx + ny * width];
|
|
} else {
|
|
sum += grid[x + y * width]; // Boundary condition
|
|
}
|
|
}
|
|
}
|
|
return sum / 8 - grid[x + y * width];
|
|
}
|
|
|
|
// Drawing with adaptive complexity and connectedness
|
|
function draw() {
|
|
ctx.fillStyle = 'rgba(0, 0, 0, 0.05)';
|
|
ctx.fillRect(0, 0, W, H);
|
|
|
|
const cellSize = Math.max(1, Math.floor(2 * (1 - params.density) + 1));
|
|
const gap = 2;
|
|
|
|
// Draw reaction-diffusion pattern with varying thickness
|
|
for (let y = 0; y < H; y += cellSize + gap) {
|
|
for (let x = 0; x < W; x += cellSize + gap) {
|
|
const i = x + y * W;
|
|
if (i >= A.length) continue;
|
|
|
|
const a = A[i];
|
|
const b = B[i];
|
|
const combined = Math.min(1, (a + b) * params.topology.thicknessRatio);
|
|
|
|
if (combined > 0.1) {
|
|
const hue = (params.tone.curiosity * 180) + (params.tone.dryness * 360 * (1 - a - b));
|
|
const saturation = 50 + params.tone.dryness * 30;
|
|
const value = 80 + Math.min(20, combined * 50);
|
|
const alpha = combined * (0.5 + params.pulse.avg * 0.5);
|
|
|
|
ctx.fillStyle = `hsla(${hue}, ${saturation}%, ${value}%, ${alpha})`;
|
|
|
|
// Vary shape complexity based on parameter
|
|
const complexityFactor = params.complexity;
|
|
const shapeType = Math.floor(complexityFactor * 3);
|
|
|
|
switch(shapeType) {
|
|
case 0:
|
|
// Simple circles
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, cellSize * combined * 2, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
break;
|
|
case 1:
|
|
// Squares with rotation
|
|
ctx.save();
|
|
ctx.translate(x, y);
|
|
ctx.rotate(combined * Math.PI * 2);
|
|
ctx.fillRect(-cellSize * combined, -cellSize * combined,
|
|
cellSize * 2 * combined, cellSize * 2 * combined);
|
|
ctx.restore();
|
|
break;
|
|
case 2:
|
|
// Complex polygons
|
|
ctx.beginPath();
|
|
const sides = 3 + Math.floor(combined * 7);
|
|
for (let s = 0; s < sides; s++) {
|
|
const angle = (s / sides) * Math.PI * 2 + combined * Math.PI;
|
|
const px = x + Math.cos(angle) * cellSize * combined * 1.5;
|
|
const py = y + Math.sin(angle) * cellSize * combined * 1.5;
|
|
if (s === 0) ctx.moveTo(px, py);
|
|
else ctx.lineTo(px, py);
|
|
}
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add some loops/connectedness patterns
|
|
if (params.topology.loops > 10) {
|
|
ctx.strokeStyle = `rgba(255, 255, 255, ${params.motion * 0.3})`;
|
|
ctx.lineWidth = 1;
|
|
|
|
for (let i = 0; i < params.topology.loops * 0.5; i++) {
|
|
ctx.beginPath();
|
|
const centerX = W/2 + Math.sin(Date.now() * 0.001 * params.motion) * W/3 * Math.sin(i * 0.2);
|
|
const centerY = H/2 + Math.cos(Date.now() * 0.0007 * params.motion) * H/3 * Math.cos(i * 0.15);
|
|
const radius = 50 + i * 5;
|
|
const segments = 5 + Math.floor(params.topology.maxDepth * 0.5);
|
|
|
|
for (let s = 0; s <= segments; s++) {
|
|
const angle = (s / segments) * Math.PI * 2 + Date.now() * 0.002 * params.motion * (i % 2 === 0 ? 1 : -1);
|
|
const x = centerX + Math.cos(angle) * radius;
|
|
const y = centerY + Math.sin(angle) * radius;
|
|
if (s === 0) ctx.moveTo(x, y);
|
|
else ctx.lineTo(x, y);
|
|
}
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Animation loop
|
|
function animate() {
|
|
step();
|
|
draw();
|
|
requestAnimationFrame(animate);
|
|
}
|
|
|
|
initPattern();
|
|
animate();
|
|
</script>
|
|
</body>
|
|
</html> |