284 lines
No EOL
11 KiB
HTML
284 lines
No EOL
11 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>neural nebula</title>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
overflow: hidden;
|
|
background: #0a0a1a;
|
|
color: #e0e0e0;
|
|
font-family: 'Courier New', monospace;
|
|
}
|
|
canvas {
|
|
display: block;
|
|
width: 100vw;
|
|
height: 100vh;
|
|
}
|
|
#attr {
|
|
position: absolute;
|
|
bottom: 15px;
|
|
right: 15px;
|
|
font-size: 10px;
|
|
opacity: 0.6;
|
|
pointer-events: none;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<canvas id="c"></canvas>
|
|
<div id="attr">neurameba · motd.social</div>
|
|
<script>
|
|
const canvas = document.getElementById('c');
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
function resize() {
|
|
canvas.width = window.innerWidth;
|
|
canvas.height = window.innerHeight;
|
|
}
|
|
window.addEventListener('resize', resize);
|
|
resize();
|
|
|
|
const params = {
|
|
motion: 0.454,
|
|
density: 0.487,
|
|
complexity: 0.492,
|
|
connectedness: 0.514,
|
|
lifespan: 0.501,
|
|
pulse: { avg: 0.94, min: 0.30, max: 1.70 },
|
|
tone: { anger: 0.00, sadness: 0.00, curiosity: 0.70, dryness: 0.80, playfulness: 0.20, tension: 0.00 },
|
|
nodes: 63,
|
|
branches: 39,
|
|
loops: 56,
|
|
maxDepth: 17,
|
|
thicknessRatio: 1.25,
|
|
fractalDim: 1.730,
|
|
energy: 208.7
|
|
};
|
|
|
|
class Node {
|
|
constructor(x, y) {
|
|
this.x = x;
|
|
this.y = y;
|
|
this.vx = 0;
|
|
this.vy = 0;
|
|
this.neighbors = [];
|
|
this.depth = 0;
|
|
this.energy = 200 + Math.random() * 50;
|
|
this.lifespan = params.lifespan * 1000 + Math.random() * 2000;
|
|
this.createdAt = Date.now();
|
|
this.pulse = params.pulse.min + Math.random() * (params.pulse.max - params.pulse.min);
|
|
}
|
|
|
|
connect(other) {
|
|
this.neighbors.push(other);
|
|
other.neighbors.push(this);
|
|
}
|
|
|
|
update(dt) {
|
|
this.energy -= dt * 0.01;
|
|
if (Date.now() - this.createdAt > this.lifespan) {
|
|
this.energy = 0;
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
draw() {
|
|
const age = Date.now() - this.createdAt;
|
|
const t = Math.min(1, age / this.lifespan);
|
|
const pulseFactor = this.pulse * (0.9 + Math.sin(Date.now() * 0.001 * 0.5) * 0.1);
|
|
|
|
const size = 1.5 + (params.thicknessRatio * 1.2) * pulseFactor * t;
|
|
const alpha = Math.min(1, this.energy / 100) * (0.7 + t * 0.3);
|
|
|
|
ctx.beginPath();
|
|
ctx.arc(this.x, this.y, size, 0, Math.PI * 2);
|
|
ctx.fillStyle = `hsla(180, 40%, 80%, ${alpha})`;
|
|
ctx.fill();
|
|
|
|
this.neighbors.forEach(n => {
|
|
const dist = Math.hypot(this.x - n.x, this.y - n.y);
|
|
const strength = (n.energy + this.energy) / 200 * pulseFactor * 0.8;
|
|
const lineAlpha = Math.min(0.6, strength * 0.5);
|
|
if (lineAlpha > 0.05) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(this.x, this.y);
|
|
ctx.lineTo(n.x, n.y);
|
|
ctx.strokeStyle = `hsla(180, 30%, 90%, ${lineAlpha})`;
|
|
ctx.lineWidth = 0.5 * pulseFactor * strength;
|
|
ctx.stroke();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
class NeuralNebula {
|
|
constructor() {
|
|
this.nodes = [];
|
|
this.nodesToRemove = [];
|
|
this.initNodes();
|
|
this.initConnections();
|
|
}
|
|
|
|
initNodes() {
|
|
const centerX = canvas.width / 2;
|
|
const centerY = canvas.height / 2;
|
|
const maxRadius = Math.min(canvas.width, canvas.height) * 0.4;
|
|
const nodeCount = params.nodes;
|
|
|
|
for (let i = 0; i < nodeCount; i++) {
|
|
const angle = (i / nodeCount) * Math.PI * 2;
|
|
const radius = maxRadius * Math.pow(Math.random(), 0.7);
|
|
const x = centerX + Math.cos(angle) * radius + (Math.random() - 0.5) * 200;
|
|
const y = centerY + Math.sin(angle) * radius + (Math.random() - 0.5) * 200;
|
|
this.nodes.push(new Node(x, y));
|
|
}
|
|
}
|
|
|
|
initConnections() {
|
|
const connectionMatrix = Array(this.nodes.length).fill().map(() => Array(this.nodes.length).fill(0));
|
|
|
|
// Create a small-world network with some random long-range connections
|
|
this.nodes.forEach((node, i) => {
|
|
const neighborCount = Math.floor(params.branches / this.nodes.length) + (Math.random() > 0.5 ? 1 : 0);
|
|
|
|
for (let j = 0; j < neighborCount; j++) {
|
|
let target;
|
|
if (Math.random() < params.connectedness && this.nodes.length > 1) {
|
|
// Prefer nearby nodes but allow some long-range connections
|
|
const sorted = [...this.nodes].sort((a, b) => {
|
|
const da = Math.hypot(a.x - node.x, a.y - node.y);
|
|
const db = Math.hypot(b.x - node.x, b.y - node.y);
|
|
return da - db;
|
|
});
|
|
const nearby = sorted.slice(0, Math.max(5, Math.floor(this.nodes.length * 0.3)));
|
|
target = nearby[Math.floor(Math.random() * nearby.length)];
|
|
} else {
|
|
target = this.nodes[Math.floor(Math.random() * this.nodes.length)];
|
|
}
|
|
|
|
if (target !== node && !node.neighbors.includes(target) && connectionMatrix[i][this.nodes.indexOf(target)] < 2) {
|
|
connectionMatrix[i][this.nodes.indexOf(target)]++;
|
|
node.connect(target);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Create some loops by connecting nodes with similar depths
|
|
if (params.loops > 0 && this.nodes.length > 2) {
|
|
const depthGroups = this.nodes.reduce((acc, node) => {
|
|
const depth = Math.min(5, Math.floor(node.depth));
|
|
acc[depth] = acc[depth] || [];
|
|
acc[depth].push(node);
|
|
return acc;
|
|
}, {});
|
|
|
|
Object.values(depthGroups).forEach(group => {
|
|
if (group.length > 1) {
|
|
group.forEach(node => {
|
|
const other = group[Math.floor(Math.random() * group.length)];
|
|
if (node !== other && !node.neighbors.includes(other)) {
|
|
node.connect(other);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
update(dt) {
|
|
// Update nodes and collect dead ones
|
|
this.nodes.forEach(node => {
|
|
const alive = node.update(dt);
|
|
if (!alive) {
|
|
this.nodesToRemove.push(node);
|
|
}
|
|
});
|
|
|
|
// Remove dead nodes and their connections
|
|
if (this.nodesToRemove.length > 0) {
|
|
this.nodesToRemove.forEach(node => {
|
|
this.nodes = this.nodes.filter(n => n !== node);
|
|
this.nodes.forEach(n => n.neighbors = n.neighbors.filter(neighbor => neighbor !== node));
|
|
});
|
|
this.nodesToRemove = [];
|
|
|
|
// Reinitialize with new nodes if too many have died
|
|
if (this.nodes.length < params.nodes * 0.3) {
|
|
this.nodes.forEach(n => n.neighbors = []);
|
|
this.initNodes();
|
|
this.initConnections();
|
|
}
|
|
}
|
|
|
|
// Update node positions with some attraction/repulsion
|
|
this.nodes.forEach(node => {
|
|
const centerX = canvas.width / 2;
|
|
const centerY = canvas.height / 2;
|
|
|
|
// Attract to center
|
|
const dx = centerX - node.x;
|
|
const dy = centerY - node.y;
|
|
node.vx += dx * 0.0001 * params.motion;
|
|
node.vy += dy * 0.0001 * params.motion;
|
|
|
|
// Repel from other nodes
|
|
this.nodes.forEach(other => {
|
|
if (node !== other) {
|
|
const dist = Math.hypot(node.x - other.x, node.y - other.y);
|
|
if (dist < 100) {
|
|
const repel = (100 - dist) / 100 * params.motion;
|
|
node.vx -= (other.x - node.x) * repel * 0.0002;
|
|
node.vy -= (other.y - node.y) * repel * 0.0002;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Apply velocity
|
|
node.x += node.vx;
|
|
node.y += node.vy;
|
|
node.vx *= 0.95;
|
|
node.vy *= 0.95;
|
|
|
|
// Bound to canvas
|
|
node.x = Math.max(0, Math.min(canvas.width, node.x));
|
|
node.y = Math.max(0, Math.min(canvas.height, node.y));
|
|
});
|
|
}
|
|
|
|
draw() {
|
|
// Background glow
|
|
ctx.fillStyle = 'rgba(0, 30, 50, 0.05)';
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
// Nodes and connections
|
|
this.nodes.forEach(node => {
|
|
node.draw();
|
|
});
|
|
}
|
|
}
|
|
|
|
const nebula = new NeuralNebula();
|
|
let lastTime = 0;
|
|
|
|
function animate(time) {
|
|
const dt = time - lastTime;
|
|
lastTime = time;
|
|
|
|
nebula.update(dt * 0.001);
|
|
|
|
ctx.save();
|
|
ctx.globalCompositeOperation = 'screen';
|
|
nebula.draw();
|
|
ctx.restore();
|
|
|
|
requestAnimationFrame(animate);
|
|
}
|
|
|
|
requestAnimationFrame(animate);
|
|
</script>
|
|
</body>
|
|
</html> |