169 lines
No EOL
5.4 KiB
HTML
169 lines
No EOL
5.4 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Bioluminescent Tendrils</title>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
overflow: hidden;
|
|
background: #0a0a1a;
|
|
font-family: 'Courier New', monospace;
|
|
}
|
|
canvas {
|
|
display: block;
|
|
}
|
|
#attribution {
|
|
position: fixed;
|
|
bottom: 10px;
|
|
right: 10px;
|
|
color: #4a4a4a;
|
|
font-size: 10px;
|
|
font-family: monospace;
|
|
}
|
|
</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();
|
|
|
|
// Parameters
|
|
const params = {
|
|
motion: 0.5,
|
|
density: 0.5,
|
|
complexity: 0.5,
|
|
connectedness: 0.5,
|
|
lifespan: 0.5,
|
|
pulse: 1.08,
|
|
tones: { anger: 0, sadness: 0, curiosity: 0.1, dryness: 0.9, playfulness: 0, tension: 0 }
|
|
};
|
|
|
|
// L-system rules
|
|
const rules = [
|
|
{ axiom: 'X', productions: [
|
|
{ pattern: 'X', replacement: 'F-[[X]+X]+F[+FX]-X' },
|
|
{ pattern: 'F', replacement: 'FF' }
|
|
], angle: 25, step: 5 }
|
|
];
|
|
|
|
// System state
|
|
let generations = [];
|
|
let currentGeneration = 0;
|
|
const maxGenerations = 10;
|
|
const colors = [
|
|
'hsl(180, 30%, 80%)', // teal (curiosity)
|
|
'hsl(0, 0%, 80%)', // white (dryness)
|
|
'hsl(120, 10%, 60%)', // muted green
|
|
'hsl(210, 15%, 70%)' // muted blue
|
|
];
|
|
|
|
// Initialize
|
|
function init() {
|
|
generations = [];
|
|
currentGeneration = 0;
|
|
const startSymbol = rules[0].axiom;
|
|
let currentString = startSymbol;
|
|
|
|
// Generate L-system strings
|
|
for (let i = 0; i < maxGenerations; i++) {
|
|
let nextString = '';
|
|
for (const c of currentString) {
|
|
let replaced = false;
|
|
for (const rule of rules[0].productions) {
|
|
if (c === rule.pattern) {
|
|
nextString += rule.replacement;
|
|
replaced = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!replaced) nextString += c;
|
|
}
|
|
generations.push({ string: nextString, angle: rules[0].angle });
|
|
currentString = nextString;
|
|
}
|
|
}
|
|
|
|
// Draw
|
|
function draw() {
|
|
// Clear with fade
|
|
ctx.fillStyle = 'rgba(10, 10, 26, 0.05)';
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
// Draw current generation
|
|
if (currentGeneration < generations.length) {
|
|
const gen = generations[currentGeneration];
|
|
const angle = gen.angle;
|
|
const step = rules[0].step * (0.8 + 0.2 * Math.sin(Date.now() * 0.001 * params.pulse));
|
|
|
|
ctx.save();
|
|
ctx.translate(canvas.width/2, canvas.height);
|
|
ctx.strokeStyle = colors[currentGeneration % colors.length];
|
|
ctx.lineWidth = 1 + 0.5 * Math.sin(Date.now() * 0.0005 * params.pulse);
|
|
ctx.lineCap = 'round';
|
|
|
|
let stack = [];
|
|
let pos = { x: 0, y: 0 };
|
|
let dir = -Math.PI/2;
|
|
|
|
for (const c of gen.string) {
|
|
switch (c) {
|
|
case 'F':
|
|
const x1 = pos.x;
|
|
const y1 = pos.y;
|
|
pos.x += step * Math.cos(dir);
|
|
pos.y += step * Math.sin(dir);
|
|
ctx.beginPath();
|
|
ctx.moveTo(x1, y1);
|
|
ctx.lineTo(pos.x, pos.y);
|
|
ctx.stroke();
|
|
break;
|
|
case '+':
|
|
dir += angle * Math.PI/180;
|
|
break;
|
|
case '-':
|
|
dir -= angle * Math.PI/180;
|
|
break;
|
|
case '[':
|
|
stack.push({ pos: { ...pos }, dir });
|
|
break;
|
|
case ']':
|
|
const state = stack.pop();
|
|
pos = state.pos;
|
|
dir = state.dir;
|
|
break;
|
|
case 'X':
|
|
// Branch point - do nothing
|
|
break;
|
|
}
|
|
}
|
|
ctx.restore();
|
|
|
|
currentGeneration++;
|
|
} else {
|
|
// Loop back
|
|
init();
|
|
}
|
|
}
|
|
|
|
// Animation loop
|
|
function animate() {
|
|
draw();
|
|
requestAnimationFrame(animate);
|
|
}
|
|
|
|
init();
|
|
animate();
|
|
</script>
|
|
</body>
|
|
</html> |