6

Sine Map: Bifurcation Cascade

hover lower half to inspect orbit at r

The sine map exhibits the same period-doubling cascade to chaos as the logistic map, with bifurcation spacings governed by the universal Feigenbaum constant . Hover the lower half of the canvas to inspect an orbit at a chosen ; the qualitative geometry of the cascade is identical to the logistic case โ€” a signature of universality in unimodal one-dimensional maps.

idle
147 lines ยท vanilla
view source
const R0 = 0.5, R1 = 1.0;
const STEPS = [8, 4, 2, 1];
let stepIdx = 0;
let col = 0;
let painted = null; // Set of (px) painted at current or finer pass
let W = 0, H = 0;
let bg, bctx;
let lastWidth = 0, lastHeight = 0;

const annots = [
  [0.7200, "period 2"],
  [0.8333, "period 4"],
  [0.8586, "period 8"],
  [0.8650, "rโˆž โ‰ˆ 0.8650"],
  [0.9375, "period 3 window"],
];

const PAD_BOTTOM = 22;
const PAD_TOP = 22;

function plotHeight(h) { return h - PAD_TOP - PAD_BOTTOM; }

function ensureBuffer(width, height) {
  if (width === lastWidth && height === lastHeight && bg) return;
  lastWidth = width; lastHeight = height;
  bg = new OffscreenCanvas(width, height);
  bctx = bg.getContext("2d");
  bctx.fillStyle = "#0a0a12";
  bctx.fillRect(0, 0, width, height);
  drawAxes(bctx, width, height);
  stepIdx = 0;
  col = 0;
  painted = new Set();
}

function drawAxes(ctx, w, h) {
  ctx.strokeStyle = "#2a2a3a";
  ctx.lineWidth = 1;
  ctx.font = "11px monospace";
  ctx.fillStyle = "#7a7a92";
  for (let i = 0; i <= 5; i++) {
    const r = R0 + (R1 - R0) * i / 5;
    const x = i / 5 * w;
    ctx.beginPath();
    ctx.moveTo(x, h - PAD_BOTTOM);
    ctx.lineTo(x, h - PAD_BOTTOM + 5);
    ctx.stroke();
    ctx.fillText(r.toFixed(2), x + 3, h - 6);
  }
  const ph = plotHeight(h);
  for (let i = 0; i <= 4; i++) {
    const v = i / 4;
    const y = h - PAD_BOTTOM - ph * v;
    ctx.beginPath();
    ctx.moveTo(0, y);
    ctx.lineTo(8, y);
    ctx.stroke();
    ctx.fillText(v.toFixed(2), 10, y - 2);
  }
}

function plotCol(ctx, px, w, h) {
  const r = R0 + (R1 - R0) * px / w;
  let x = 0.3 + 0.4 * ((px * 9301 + 49297) % 233280) / 233280;
  // settle
  for (let i = 0; i < 400; i++) x = r * Math.sin(Math.PI * x);
  const ph = plotHeight(h);
  ctx.fillStyle = "rgba(180,220,255,0.18)";
  for (let i = 0; i < 220; i++) {
    x = r * Math.sin(Math.PI * x);
    // x can be slightly outside [0,1] near r=1; clamp display
    const xv = x < 0 ? 0 : (x > 1 ? 1 : x);
    const py = h - PAD_BOTTOM - ph * xv;
    ctx.fillRect(px, py, 1, 1);
  }
}

function init({ width, height }) { ensureBuffer(width, height); }

function tick({ ctx, width, height, input }) {
  ensureBuffer(width, height);
  W = width; H = height;

  // progressive refinement passes
  if (stepIdx < STEPS.length) {
    const step = STEPS[stepIdx];
    const perFrame = Math.max(2, Math.ceil(W / (step * 80)));
    let plotted = 0;
    while (plotted < perFrame && col < W) {
      if (!painted.has(col)) {
        plotCol(bctx, col, W, H);
        painted.add(col);
        plotted++;
      }
      col += step;
    }
    if (col >= W) {
      stepIdx++;
      col = 0;
    }
  }

  ctx.drawImage(bg, 0, 0);

  // top bar
  ctx.fillStyle = "rgba(10,10,18,0.7)";
  ctx.fillRect(0, 0, W, 20);
  ctx.fillStyle = "rgba(200,200,220,0.9)";
  ctx.font = "12px monospace";
  ctx.fillText("Sine map  x โ†’ r sin(ฯ€ x)", 12, 14);

  // annotations (only when their column has been painted)
  ctx.font = "10px monospace";
  for (const [r, lbl] of annots) {
    const x = (r - R0) / (R1 - R0) * W;
    // only draw if at least the coarsest pass has covered this region
    if (stepIdx === 0 && x > col) continue;
    ctx.strokeStyle = "rgba(255,180,90,0.4)";
    ctx.beginPath();
    ctx.moveTo(x, 22);
    ctx.lineTo(x, H - PAD_BOTTOM);
    ctx.stroke();
    ctx.fillStyle = "rgba(255,210,140,0.9)";
    ctx.fillText(lbl, x + 3, 34);
  }

  // hover inspector โ€” only when in lower half of canvas
  const mx = input.mouseX, my = input.mouseY;
  if (my > H * 0.55 && mx >= 0 && mx <= W && my <= H) {
    const r = R0 + (R1 - R0) * mx / W;

    // vertical tick at r
    ctx.strokeStyle = "rgba(255,255,255,0.6)";
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.moveTo(mx, 22);
    ctx.lineTo(mx, H - PAD_BOTTOM);
    ctx.stroke();

    // inset bottom-right: orbit values at this r
    const iw = 220, ih = 70;
    const ix = W - iw - 10;
    const iy = H - ih - PAD_BOTTOM - 6;
    ctx.fillStyle = "rgba(15,15,25,0.93)";
    ctx.strokeStyle = "#4a5a8a";
    ctx.fillRect(ix, iy, iw, ih);
    ctx.strokeRect(ix, iy, iw, ih);
    ctx.fillStyle = "#cfd6ff";
    ctx.font = "11px monospace";
    ctx.fillText(`r = ${r.toFixed(4)}`, ix + 6, iy + 13);
    ctx.fillStyle = "#7a8aaa";
    ctx.fillText("last 120 iterations", ix + 6, iy + 25);

    let xv = 0.3;
    for (let i = 0; i < 400; i++) xv = r * Math.sin(Math.PI * xv);
    const px0 = ix + 6, py0 = iy + 30, pw = iw - 12, ph2 = ih - 36;
    ctx.strokeStyle = "#2a2a3a";
    ctx.strokeRect(px0, py0, pw, ph2);
    ctx.strokeStyle = "#9ad0ff";
    ctx.lineWidth = 1;
    ctx.beginPath();
    const N = 120;
    for (let i = 0; i < N; i++) {
      xv = r * Math.sin(Math.PI * xv);
      const xc = xv < 0 ? 0 : (xv > 1 ? 1 : xv);
      const xx = px0 + i / (N - 1) * pw;
      const yy = py0 + ph2 - xc * ph2;
      if (i === 0) ctx.moveTo(xx, yy); else ctx.lineTo(xx, yy);
    }
    ctx.stroke();
  }
}

Comments (0)

Log in to comment.