5
Rössler Attractor
move cursor to scrub c
idle
149 lines · vanilla
view source
const A = 0.2;
const B = 0.2;
const C_MIN = 4.0;
const C_MAX = 10.0;
const TRAIL_MAX = 3500;
const SUBSTEPS = 5;
const DT_SIM = 0.012;
const IDLE_FRAMES = 90;
let state;
let trail;
let head;
let count;
let hueOffset;
let cParam;
let cTarget;
let idleCounter;
let autoPhase;
let lastMouseX;
let lastMouseY;
let W;
let H;
let cx;
let cy;
let scale;
function derivs(x, y, z, c) {
return [
-y - z,
x + A * y,
B + z * (x - c),
];
}
function rk4Step(s, h, c) {
const [x, y, z] = s;
const k1 = derivs(x, y, z, c);
const k2 = derivs(x + 0.5 * h * k1[0], y + 0.5 * h * k1[1], z + 0.5 * h * k1[2], c);
const k3 = derivs(x + 0.5 * h * k2[0], y + 0.5 * h * k2[1], z + 0.5 * h * k2[2], c);
const k4 = derivs(x + h * k3[0], y + h * k3[1], z + h * k3[2], c);
return [
x + (h / 6) * (k1[0] + 2 * k2[0] + 2 * k3[0] + k4[0]),
y + (h / 6) * (k1[1] + 2 * k2[1] + 2 * k3[1] + k4[1]),
z + (h / 6) * (k1[2] + 2 * k2[2] + 2 * k3[2] + k4[2]),
];
}
function pushPoint(x, y) {
trail[head * 2] = x;
trail[head * 2 + 1] = y;
head = (head + 1) % TRAIL_MAX;
if (count < TRAIL_MAX) count++;
}
function init({ canvas, ctx, width, height }) {
W = width;
H = height;
cx = W * 0.5;
cy = H * 0.5;
scale = Math.min(W, H) / 32;
state = [0.1, 0.0, 0.0];
trail = new Float32Array(TRAIL_MAX * 2);
head = 0;
count = 0;
hueOffset = 0;
cParam = 5.7;
cTarget = 5.7;
idleCounter = IDLE_FRAMES + 1;
autoPhase = 0;
lastMouseX = -1;
lastMouseY = -1;
for (let i = 0; i < 2000; i++) {
state = rk4Step(state, DT_SIM, cParam);
}
ctx.fillStyle = '#05060a';
ctx.fillRect(0, 0, W, H);
}
function tick({ ctx, dt, frame, width, height, input }) {
if (width !== W || height !== H) {
W = width;
H = height;
cx = W * 0.5;
cy = H * 0.5;
scale = Math.min(W, H) / 32;
}
const mx = input.mouseX;
const my = input.mouseY;
const mouseMoved = (mx !== lastMouseX || my !== lastMouseY) && mx >= 0 && my >= 0;
if (mouseMoved) {
idleCounter = 0;
cTarget = C_MIN + Math.max(0, Math.min(1, mx / W)) * (C_MAX - C_MIN);
} else {
idleCounter++;
}
lastMouseX = mx;
lastMouseY = my;
if (idleCounter > IDLE_FRAMES) {
autoPhase += dt * 0.18;
const s = 0.5 - 0.5 * Math.cos(autoPhase);
cTarget = C_MIN + s * (C_MAX - C_MIN);
}
cParam += (cTarget - cParam) * Math.min(1, dt * 2.5);
ctx.globalCompositeOperation = 'source-over';
ctx.fillStyle = 'rgba(5, 6, 10, 0.16)';
ctx.fillRect(0, 0, W, H);
const steps = Math.max(1, Math.min(10, Math.round(SUBSTEPS * (dt / (1 / 60)))));
for (let i = 0; i < steps; i++) {
state = rk4Step(state, DT_SIM, cParam);
pushPoint(state[0], state[1]);
}
hueOffset = (hueOffset + dt * 14) % 360;
ctx.globalCompositeOperation = 'lighter';
ctx.lineWidth = 1.2;
ctx.lineCap = 'round';
const n = count;
if (n > 1) {
const startIdx = (head - n + TRAIL_MAX) % TRAIL_MAX;
let prevX = trail[startIdx * 2];
let prevY = trail[startIdx * 2 + 1];
let prevPX = cx + prevX * scale;
let prevPY = cy - prevY * scale;
for (let i = 1; i < n; i++) {
const idx = (startIdx + i) % TRAIL_MAX;
const x = trail[idx * 2];
const y = trail[idx * 2 + 1];
const px = cx + x * scale;
const py = cy - y * scale;
const t = i / n;
const hue = (hueOffset + t * 300) % 360;
const alpha = 0.04 + t * 0.55;
ctx.strokeStyle = `hsla(${hue.toFixed(1)}, 95%, 62%, ${alpha.toFixed(3)})`;
ctx.beginPath();
ctx.moveTo(prevPX, prevPY);
ctx.lineTo(px, py);
ctx.stroke();
prevPX = px;
prevPY = py;
}
ctx.fillStyle = `hsla(${hueOffset.toFixed(1)}, 100%, 82%, 0.95)`;
ctx.beginPath();
ctx.arc(prevPX, prevPY, 2.2, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalCompositeOperation = 'source-over';
ctx.fillStyle = 'rgba(5, 6, 10, 0.55)';
ctx.fillRect(8, 8, 168, 38);
ctx.fillStyle = '#e6e8ef';
ctx.font = '13px ui-monospace, Menlo, monospace';
ctx.textBaseline = 'top';
ctx.fillText(`c = ${cParam.toFixed(3)}`, 16, 14);
ctx.fillStyle = '#7d8aa3';
ctx.font = '11px ui-monospace, Menlo, monospace';
ctx.fillText(idleCounter > IDLE_FRAMES ? 'auto-cycling' : 'mouse latched', 16, 30);
}
Comments (0)
Log in to comment.