<div class="controls">
<label>r = <span id="rval">3.50</span>
<input type="range" id="rslider" min="1" max="4" step="0.005" value="3.5">
</label>
<button id="animBtn" type="button">Animate sweep</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<canvas id="bif" width="640" height="360"></canvas>
<p class="caption" id="caption">Drag r to explore. Watch the bifurcation cascade.</p>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: #0f1117; color: #e2e8f0; }
.controls {
display: flex; align-items: center; gap: .7rem; flex-wrap: wrap;
padding: .5rem .6rem; background: #1a1f2e; border-bottom: 1px solid #2d3550;
}
label { font-size: .88rem; color: #94a3b8; display: flex; align-items: center; gap: .5rem; flex: 1; }
#rslider { flex: 1; min-width: 120px; accent-color: #60a5fa; cursor: pointer; }
#rval { font-weight: 700; color: #60a5fa; min-width: 2.8ch; display: inline-block; }
button {
font: 600 .82rem system-ui; padding: .35rem .75rem;
border: 1px solid #3b82f6; background: #3b82f6; color: #fff;
border-radius: 6px; cursor: pointer; white-space: nowrap;
}
button.ghost { background: transparent; color: #60a5fa; border-color: #334155; }
canvas { display: block; width: 100%; height: auto; }
.caption {
font-size: .8rem; color: #64748b; padding: .35rem .6rem;
background: #1a1f2e; border-top: 1px solid #2d3550; min-height: 1.8em;
}
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Logistic map bifurcation diagram
// For each r in [rMin, rMax], iterate x = r*x*(1-x) N times to burn in,
// then collect KEEP more values and plot them as dots.
// A vertical red line tracks the slider's current r value.
const canvas = document.getElementById('bif');
const ctx = canvas.getContext('2d');
const slider = document.getElementById('rslider');
const rvalEl = document.getElementById('rval');
const caption = document.getElementById('caption');
const animBtn = document.getElementById('animBtn');
const resetBtn = document.getElementById('resetBtn');
const W = canvas.width, H = canvas.height;
const R_MIN = 1.0, R_MAX = 4.0;
const BURN = 300, KEEP = 150;
const DOT_ALPHA = 0.35; // per-dot opacity for the full diagram
// Pre-build the bifurcation image once
let bifImageData = null;
function buildBifurcation() {
const id = ctx.createImageData(W, H);
const px = id.data;
const cols = W;
for (let xi = 0; xi < cols; xi++) {
const r = R_MIN + (R_MAX - R_MIN) * (xi / (cols - 1));
let x = 0.5;
for (let i = 0; i < BURN; i++) x = r * x * (1 - x);
for (let i = 0; i < KEEP; i++) {
x = r * x * (1 - x);
const yi = Math.round((1 - x) * (H - 1));
if (yi < 0 || yi >= H) continue;
const idx = (yi * W + xi) * 4;
// Accumulate: use cyan dots
px[idx] = Math.min(255, px[idx] + 0);
px[idx + 1] = Math.min(255, px[idx + 1] + 180);
px[idx + 2] = Math.min(255, px[idx + 2] + 255);
px[idx + 3] = Math.min(255, px[idx + 3] + 90);
}
}
bifImageData = id;
}
function describeR(r) {
if (r < 1.0) return 'r < 1: population dies out every time.';
if (r < 3.0) return fmt('r = %r%: stable fixed point — population converges.', { r: r.toFixed(3) });
if (r < 3.449) return fmt('r = %r%: period-2 oscillation (first bifurcation).', { r: r.toFixed(3) });
if (r < 3.544) return fmt('r = %r%: period-4 oscillation.', { r: r.toFixed(3) });
if (r < 3.5644) return fmt('r = %r%: period-8 oscillation.', { r: r.toFixed(3) });
if (r < 3.5688) return fmt('r = %r%: period-16 — approaching chaos.', { r: r.toFixed(3) });
if (r < 3.5699) return fmt('r = %r%: very high-period orbit — edge of chaos.', { r: r.toFixed(3) });
if (r < 3.6269) return fmt('r = %r%: chaotic regime begins (r ≈ 3.57).', { r: r.toFixed(3) });
if (r < 3.6786) return fmt('r = %r%: chaotic — with embedded periodic windows.', { r: r.toFixed(3) });
if (r < 3.6828) return fmt('r = %r%: period-3 window (Li-Yorke: period 3 → chaos).', { r: r.toFixed(3) });
if (r < 3.74) return fmt('r = %r%: post period-3 window, back to chaos.', { r: r.toFixed(3) });
if (r < 3.8284) return fmt('r = %r%: chaotic with occasional order windows.', { r: r.toFixed(3) });
if (r < 3.8570) return fmt('r = %r%: period-5 window inside chaos.', { r: r.toFixed(3) });
if (r < 4.0) return fmt('r = %r%: deep chaos — orbit fills [0,1] densely.', { r: r.toFixed(3) });
return 'r = 4.000: fully chaotic — orbit ergodically covers [0,1].';
}
function draw(r) {
ctx.clearRect(0, 0, W, H);
if (bifImageData) ctx.putImageData(bifImageData, 0, 0);
// Red vertical line for current r
const xi = Math.round((r - R_MIN) / (R_MAX - R_MIN) * (W - 1));
ctx.save();
ctx.strokeStyle = 'rgba(239,68,68,0.9)';
ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.moveTo(xi, 0); ctx.lineTo(xi, H); ctx.stroke();
ctx.restore();
// Show the orbit at this r (bright dots on top)
let x = 0.5;
for (let i = 0; i < BURN; i++) x = r * x * (1 - x);
ctx.fillStyle = 'rgba(255, 200, 50, 0.9)';
for (let i = 0; i < 80; i++) {
x = r * x * (1 - x);
const yi = Math.round((1 - x) * (H - 1));
ctx.fillRect(xi - 1, yi - 1, 3, 3);
}
rvalEl.textContent = r.toFixed(3);
caption.textContent = describeR(r);
}
// Animation state
let animating = false, animId = null, animR = 1.0;
function startAnim() {
animating = true; animR = 1.0; animBtn.textContent = 'Stop';
function step() {
if (!animating || animR > 4.0) { stopAnim(); return; }
slider.value = animR;
draw(animR);
animR += 0.004;
animId = requestAnimationFrame(step);
}
animId = requestAnimationFrame(step);
}
function stopAnim() {
animating = false; animBtn.textContent = 'Animate sweep';
if (animId) cancelAnimationFrame(animId);
}
slider.addEventListener('input', () => { stopAnim(); draw(parseFloat(slider.value)); });
animBtn.addEventListener('click', () => { animating ? stopAnim() : startAnim(); });
resetBtn.addEventListener('click', () => { stopAnim(); slider.value = 3.5; draw(3.5); });
// Build diagram then draw initial state
buildBifurcation();
draw(3.5);
// Verify core math (console assertions — visible in the demo console tab)
// Fixed point: for r=2.5, x* = 1 - 1/r = 0.6
(function verifyLogisticMap() {
// 1. Fixed point convergence
let x = 0.5, r = 2.5;
for (let i = 0; i < 500; i++) x = r * x * (1 - x);
const expected = 1 - 1/r; // = 0.6
console.assert(Math.abs(x - expected) < 1e-9,
fmt('Fixed point test failed: got %x% expected %expected%', { x: x, expected: expected }));
// 2. Period-2 orbit at r = 3.2
r = 3.2; x = 0.5;
for (let i = 0; i < 500; i++) x = r * x * (1 - x);
const a = x;
x = r * x * (1 - x); const b = x;
x = r * x * (1 - x); const c = x;
console.assert(Math.abs(a - c) < 1e-9,
'Period-2 test failed: orbit not length 2');
console.assert(Math.abs(a - b) > 0.01,
'Period-2 test: a and b should be different');
// 3. Chaos at r=4: Lyapunov > 0 (log(2) theoretically)
r = 4.0; x = 0.5; let lyap = 0;
for (let i = 0; i < 10000; i++) {
x = r * x * (1 - x);
lyap += Math.log(Math.abs(r * (1 - 2*x)));
}
lyap /= 10000;
console.assert(lyap > 0.5, fmt('Lyapunov at r=4 should be positive (ln2≈0.693), got %lyap%', { lyap: lyap }));
console.log('Logistic map verification passed:',
fmt('fixed-point=%expected%', { expected: expected.toFixed(4) }),
fmt('| period-2 a=%a% b=%b%', { a: a.toFixed(4), b: b.toFixed(4) }),
fmt('| Lyapunov(r=4)=%lyap%', { lyap: lyap.toFixed(4) }));
})();
<div class="controls">
<label>r = <span id="rval">3.50</span>
<input type="range" id="rslider" min="1" max="4" step="0.005" value="3.5">
</label>
<button id="animBtn" type="button">Animate sweep</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<canvas id="bif" width="640" height="360"></canvas>
<p class="caption" id="caption">Drag r to explore. Watch the bifurcation cascade.</p>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: #0f1117; color: #e2e8f0; }
.controls {
display: flex; align-items: center; gap: .7rem; flex-wrap: wrap;
padding: .5rem .6rem; background: #1a1f2e; border-bottom: 1px solid #2d3550;
}
label { font-size: .88rem; color: #94a3b8; display: flex; align-items: center; gap: .5rem; flex: 1; }
#rslider { flex: 1; min-width: 120px; accent-color: #60a5fa; cursor: pointer; }
#rval { font-weight: 700; color: #60a5fa; min-width: 2.8ch; display: inline-block; }
button {
font: 600 .82rem system-ui; padding: .35rem .75rem;
border: 1px solid #3b82f6; background: #3b82f6; color: #fff;
border-radius: 6px; cursor: pointer; white-space: nowrap;
}
button.ghost { background: transparent; color: #60a5fa; border-color: #334155; }
canvas { display: block; width: 100%; height: auto; }
.caption {
font-size: .8rem; color: #64748b; padding: .35rem .6rem;
background: #1a1f2e; border-top: 1px solid #2d3550; min-height: 1.8em;
}
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Logistic map bifurcation diagram
// For each r in [rMin, rMax], iterate x = r*x*(1-x) N times to burn in,
// then collect KEEP more values and plot them as dots.
// A vertical red line tracks the slider's current r value.
const canvas = document.getElementById('bif');
const ctx = canvas.getContext('2d');
const slider = document.getElementById('rslider');
const rvalEl = document.getElementById('rval');
const caption = document.getElementById('caption');
const animBtn = document.getElementById('animBtn');
const resetBtn = document.getElementById('resetBtn');
const W = canvas.width, H = canvas.height;
const R_MIN = 1.0, R_MAX = 4.0;
const BURN = 300, KEEP = 150;
const DOT_ALPHA = 0.35; // per-dot opacity for the full diagram
// Pre-build the bifurcation image once
let bifImageData = null;
function buildBifurcation() {
const id = ctx.createImageData(W, H);
const px = id.data;
const cols = W;
for (let xi = 0; xi < cols; xi++) {
const r = R_MIN + (R_MAX - R_MIN) * (xi / (cols - 1));
let x = 0.5;
for (let i = 0; i < BURN; i++) x = r * x * (1 - x);
for (let i = 0; i < KEEP; i++) {
x = r * x * (1 - x);
const yi = Math.round((1 - x) * (H - 1));
if (yi < 0 || yi >= H) continue;
const idx = (yi * W + xi) * 4;
// Accumulate: use cyan dots
px[idx] = Math.min(255, px[idx] + 0);
px[idx + 1] = Math.min(255, px[idx + 1] + 180);
px[idx + 2] = Math.min(255, px[idx + 2] + 255);
px[idx + 3] = Math.min(255, px[idx + 3] + 90);
}
}
bifImageData = id;
}
function describeR(r) {
if (r < 1.0) return 'r < 1: population dies out every time.';
if (r < 3.0) return fmt('r = %r%: stable fixed point — population converges.', { r: r.toFixed(3) });
if (r < 3.449) return fmt('r = %r%: period-2 oscillation (first bifurcation).', { r: r.toFixed(3) });
if (r < 3.544) return fmt('r = %r%: period-4 oscillation.', { r: r.toFixed(3) });
if (r < 3.5644) return fmt('r = %r%: period-8 oscillation.', { r: r.toFixed(3) });
if (r < 3.5688) return fmt('r = %r%: period-16 — approaching chaos.', { r: r.toFixed(3) });
if (r < 3.5699) return fmt('r = %r%: very high-period orbit — edge of chaos.', { r: r.toFixed(3) });
if (r < 3.6269) return fmt('r = %r%: chaotic regime begins (r ≈ 3.57).', { r: r.toFixed(3) });
if (r < 3.6786) return fmt('r = %r%: chaotic — with embedded periodic windows.', { r: r.toFixed(3) });
if (r < 3.6828) return fmt('r = %r%: period-3 window (Li-Yorke: period 3 → chaos).', { r: r.toFixed(3) });
if (r < 3.74) return fmt('r = %r%: post period-3 window, back to chaos.', { r: r.toFixed(3) });
if (r < 3.8284) return fmt('r = %r%: chaotic with occasional order windows.', { r: r.toFixed(3) });
if (r < 3.8570) return fmt('r = %r%: period-5 window inside chaos.', { r: r.toFixed(3) });
if (r < 4.0) return fmt('r = %r%: deep chaos — orbit fills [0,1] densely.', { r: r.toFixed(3) });
return 'r = 4.000: fully chaotic — orbit ergodically covers [0,1].';
}
function draw(r) {
ctx.clearRect(0, 0, W, H);
if (bifImageData) ctx.putImageData(bifImageData, 0, 0);
// Red vertical line for current r
const xi = Math.round((r - R_MIN) / (R_MAX - R_MIN) * (W - 1));
ctx.save();
ctx.strokeStyle = 'rgba(239,68,68,0.9)';
ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.moveTo(xi, 0); ctx.lineTo(xi, H); ctx.stroke();
ctx.restore();
// Show the orbit at this r (bright dots on top)
let x = 0.5;
for (let i = 0; i < BURN; i++) x = r * x * (1 - x);
ctx.fillStyle = 'rgba(255, 200, 50, 0.9)';
for (let i = 0; i < 80; i++) {
x = r * x * (1 - x);
const yi = Math.round((1 - x) * (H - 1));
ctx.fillRect(xi - 1, yi - 1, 3, 3);
}
rvalEl.textContent = r.toFixed(3);
caption.textContent = describeR(r);
}
// Animation state
let animating = false, animId = null, animR = 1.0;
function startAnim() {
animating = true; animR = 1.0; animBtn.textContent = 'Stop';
function step() {
if (!animating || animR > 4.0) { stopAnim(); return; }
slider.value = animR;
draw(animR);
animR += 0.004;
animId = requestAnimationFrame(step);
}
animId = requestAnimationFrame(step);
}
function stopAnim() {
animating = false; animBtn.textContent = 'Animate sweep';
if (animId) cancelAnimationFrame(animId);
}
slider.addEventListener('input', () => { stopAnim(); draw(parseFloat(slider.value)); });
animBtn.addEventListener('click', () => { animating ? stopAnim() : startAnim(); });
resetBtn.addEventListener('click', () => { stopAnim(); slider.value = 3.5; draw(3.5); });
// Build diagram then draw initial state
buildBifurcation();
draw(3.5);
// Verify core math (console assertions — visible in the demo console tab)
// Fixed point: for r=2.5, x* = 1 - 1/r = 0.6
(function verifyLogisticMap() {
// 1. Fixed point convergence
let x = 0.5, r = 2.5;
for (let i = 0; i < 500; i++) x = r * x * (1 - x);
const expected = 1 - 1/r; // = 0.6
console.assert(Math.abs(x - expected) < 1e-9,
fmt('Fixed point test failed: got %x% expected %expected%', { x: x, expected: expected }));
// 2. Period-2 orbit at r = 3.2
r = 3.2; x = 0.5;
for (let i = 0; i < 500; i++) x = r * x * (1 - x);
const a = x;
x = r * x * (1 - x); const b = x;
x = r * x * (1 - x); const c = x;
console.assert(Math.abs(a - c) < 1e-9,
'Period-2 test failed: orbit not length 2');
console.assert(Math.abs(a - b) > 0.01,
'Period-2 test: a and b should be different');
// 3. Chaos at r=4: Lyapunov > 0 (log(2) theoretically)
r = 4.0; x = 0.5; let lyap = 0;
for (let i = 0; i < 10000; i++) {
x = r * x * (1 - x);
lyap += Math.log(Math.abs(r * (1 - 2*x)));
}
lyap /= 10000;
console.assert(lyap > 0.5, fmt('Lyapunov at r=4 should be positive (ln2≈0.693), got %lyap%', { lyap: lyap }));
console.log('Logistic map verification passed:',
fmt('fixed-point=%expected%', { expected: expected.toFixed(4) }),
fmt('| period-2 a=%a% b=%b%', { a: a.toFixed(4), b: b.toFixed(4) }),
fmt('| Lyapunov(r=4)=%lyap%', { lyap: lyap.toFixed(4) }));
})();
Comments
Loading comments...