122 lines
No EOL
4.2 KiB
HTML
122 lines
No EOL
4.2 KiB
HTML
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>Turing’s Subtle Dance</title>
|
||
<style>
|
||
body { margin: 0; overflow: hidden; background: #000; color: #fff; font-family: 'Courier New', monospace; }
|
||
canvas { display: block; }
|
||
#info { position: absolute; bottom: 10px; left: 10px; font-size: 12px; text-shadow: 1px 1px 2px #000; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<canvas id="c"></canvas>
|
||
<div id="info">neurameba · motd.social</div>
|
||
<script>
|
||
const canvas = document.getElementById('c');
|
||
const ctx = canvas.getContext('2d');
|
||
|
||
function resize() {
|
||
canvas.width = innerWidth;
|
||
canvas.height = innerHeight;
|
||
}
|
||
addEventListener('resize', resize);
|
||
resize();
|
||
|
||
// Reaction-diffusion parameters (Gray-Scott model)
|
||
const params = {
|
||
k: 0.055,
|
||
f: 0.03,
|
||
da: 1.0,
|
||
db: 0.5,
|
||
scale: 2.5,
|
||
decay: 0.01
|
||
};
|
||
|
||
// Grid setup
|
||
const size = Math.floor(Math.min(innerWidth, innerHeight) / params.scale);
|
||
const grid = {
|
||
a: new Float64Array(size * size),
|
||
b: new Float64Array(size * size),
|
||
nextA: new Float64Array(size * size),
|
||
nextB: new Float64Array(size * size)
|
||
};
|
||
|
||
// Initialize with sparse random seed
|
||
for (let i = 0; i < grid.a.length; i++) {
|
||
grid.a[i] = 1.0;
|
||
grid.b[i] = Math.random() < 0.01 ? 1.0 : 0.0;
|
||
}
|
||
|
||
// Time and rendering
|
||
let time = 0;
|
||
const pulse = 1.1 + 0.2 * Math.sin(Date.now() * 0.001 * 2 * Math.PI / params.pulse);
|
||
|
||
function update() {
|
||
// Random disturbances based on motion parameter
|
||
for (let i = 0; i < 10 * pulse; i++) {
|
||
const idx = Math.floor(Math.random() * grid.a.length);
|
||
grid.b[idx] = Math.min(1.0, grid.b[idx] + 0.1 * (0.5 + Math.random() * 0.5));
|
||
}
|
||
|
||
// Reaction-diffusion step
|
||
const lapA = new Float64Array(grid.a.length);
|
||
const lapB = new Float64Array(grid.b.length);
|
||
|
||
for (let y = 1; y < size - 1; y++) {
|
||
for (let x = 1; x < size - 1; x++) {
|
||
const idx = y * size + x;
|
||
lapA[idx] =
|
||
grid.a[idx - 1] + grid.a[idx + 1] + grid.a[idx - size] + grid.a[idx + size] -
|
||
4 * grid.a[idx];
|
||
lapB[idx] =
|
||
grid.b[idx - 1] + grid.b[idx + 1] + grid.b[idx - size] + grid.b[idx + size] -
|
||
4 * grid.b[idx];
|
||
}
|
||
}
|
||
|
||
for (let i = 0; i < grid.a.length; i++) {
|
||
const feed = params.f * (1 - grid.a[i]);
|
||
const kill = params.k + params.f * params.k / (1 + params.k) * grid.b[i];
|
||
const reaction = grid.a[i] * grid.b[i] * grid.b[i];
|
||
|
||
grid.nextA[i] = grid.a[i] + params.da * lapA[i] - reaction + feed;
|
||
grid.nextB[i] = grid.b[i] + params.db * lapB[i] + reaction - kill - params.decay * grid.b[i];
|
||
|
||
grid.nextA[i] = Math.max(0, Math.min(1, grid.nextA[i]));
|
||
grid.nextB[i] = Math.max(0, Math.min(1, grid.nextB[i]));
|
||
}
|
||
|
||
// Swap buffers
|
||
[grid.a, grid.nextA] = [grid.nextA, grid.a];
|
||
[grid.b, grid.nextB] = [grid.nextB, grid.b];
|
||
|
||
// Render
|
||
ctx.fillStyle = 'rgba(0, 0, 0, 0.05)';
|
||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||
|
||
const imageData = ctx.createImageData(canvas.width, canvas.height);
|
||
const data = imageData.data;
|
||
|
||
for (let y = 0; y < size; y++) {
|
||
for (let x = 0; x < size; x++) {
|
||
const idx = y * size + x;
|
||
const val = grid.b[idx];
|
||
const bright = Math.floor(255 * val);
|
||
const i = (y * canvas.width + x) * 4;
|
||
data[i] = data[i+1] = data[i+2] = bright;
|
||
data[i+3] = 255;
|
||
}
|
||
}
|
||
|
||
ctx.putImageData(imageData, 0, 0);
|
||
|
||
time++;
|
||
requestAnimationFrame(update);
|
||
}
|
||
|
||
// Start simulation
|
||
update();
|
||
</script>
|
||
</body>
|
||
</html> |