6
Sine Map: Bifurcation Cascade
hover lower half to inspect orbit at r
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.