5

Rössler Attractor

move cursor to scrub c

Rössler's three-variable flow , , integrated with RK4 and projected onto the plane. With fixed, sweeping from 4 to 10 walks the system through a period-doubling cascade into a single-band chaotic ribbon.

idle
158 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;

// RK4 step, in-place into `s = [x, y, z]`. No per-step allocations.
function rk4Step(s, h, c) {
  const x = s[0], y = s[1], z = s[2];
  const k1x = -y - z;
  const k1y = x + A * y;
  const k1z = B + z * (x - c);

  const x2 = x + 0.5 * h * k1x;
  const y2 = y + 0.5 * h * k1y;
  const z2 = z + 0.5 * h * k1z;
  const k2x = -y2 - z2;
  const k2y = x2 + A * y2;
  const k2z = B + z2 * (x2 - c);

  const x3 = x + 0.5 * h * k2x;
  const y3 = y + 0.5 * h * k2y;
  const z3 = z + 0.5 * h * k2z;
  const k3x = -y3 - z3;
  const k3y = x3 + A * y3;
  const k3z = B + z3 * (x3 - c);

  const x4 = x + h * k3x;
  const y4 = y + h * k3y;
  const z4 = z + h * k3z;
  const k4x = -y4 - z4;
  const k4y = x4 + A * y4;
  const k4z = B + z4 * (x4 - c);

  const sixth = h / 6;
  s[0] = x + sixth * (k1x + 2 * k2x + 2 * k3x + k4x);
  s[1] = y + sixth * (k1y + 2 * k2y + 2 * k3y + k4y);
  s[2] = z + sixth * (k1z + 2 * k2z + 2 * k3z + k4z);
}

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++) {
    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++) {
    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.