<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Kuramoto Synchronization</title> <style> @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;600&family=EB+Garamond:ital,wght@0,400;0,600;1,400&display=swap'); :root { –bg: #07080a; –bg2: #0d0f12; –panel: #111418; –border: #1e2530; –accent: #4fc3f7; –accent2: #e0a44a; –accent3: #7dd3a8; –dim: #3a4555; –text: #c8d4e0; –text-dim: #5a6a7a; –synced: #4fc3f7; –chaos: #e05a7a; –transition: #e0a44a; } - { margin: 0; padding: 0; box-sizing: border-box; } body { background: var(–bg); color: var(–text); font-family: ‘JetBrains Mono’, monospace; min-height: 100vh; display: grid; grid-template-rows: auto 1fr auto; overflow-x: hidden; } header { padding: 24px 32px 16px; border-bottom: 1px solid var(–border); display: flex; align-items: baseline; gap: 24px; } .title { font-family: ‘EB Garamond’, serif; font-size: 1.5rem; color: var(–text); letter-spacing: 0.02em; } .subtitle { font-size: 0.65rem; color: var(–text-dim); letter-spacing: 0.12em; text-transform: uppercase; } .equation { margin-left: auto; font-size: 0.75rem; color: var(–dim); font-style: italic; font-family: ‘EB Garamond’, serif; } .main { display: grid; grid-template-columns: 1fr 320px; gap: 0; overflow: hidden; } .canvas-area { position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 24px; gap: 20px; border-right: 1px solid var(–border); } canvas { border-radius: 4px; } #phase-canvas { background: var(–bg2); border: 1px solid var(–border); } #order-canvas { background: var(–bg2); border: 1px solid var(–border); } .canvas-label { font-size: 0.6rem; letter-spacing: 0.15em; text-transform: uppercase; color: var(–text-dim); align-self: flex-start; margin-bottom: -14px; } .sidebar { display: flex; flex-direction: column; gap: 0; overflow-y: auto; } .panel { padding: 20px 20px; border-bottom: 1px solid var(–border); } .panel-title { font-size: 0.58rem; letter-spacing: 0.2em; text-transform: uppercase; color: var(–text-dim); margin-bottom: 16px; } .control-row { margin-bottom: 14px; } .control-label { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 6px; } .control-name { font-size: 0.7rem; color: var(–text); } .control-value { font-size: 0.8rem; color: var(–accent); font-weight: 600; min-width: 36px; text-align: right; } input[type=range] { -webkit-appearance: none; width: 100%; height: 2px; background: var(–border); border-radius: 2px; outline: none; cursor: pointer; } input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; width: 12px; height: 12px; background: var(–accent); border-radius: 50%; cursor: pointer; box-shadow: 0 0 8px var(–accent); } .preset-buttons { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-bottom: 4px; } .preset-btn { background: var(–panel); border: 1px solid var(–border); color: var(–text-dim); font-family: ‘JetBrains Mono’, monospace; font-size: 0.6rem; letter-spacing: 0.08em; padding: 7px 6px; cursor: pointer; text-align: center; text-transform: uppercase; transition: all 0.15s; border-radius: 2px; } .preset-btn:hover { border-color: var(–accent); color: var(–accent); background: rgba(79,195,247,0.05); } .preset-btn.active { border-color: var(–accent2); color: var(–accent2); background: rgba(224,164,74,0.08); } .metrics { display: grid; gap: 10px; } .metric { display: flex; justify-content: space-between; align-items: center; } .metric-name { font-size: 0.62rem; color: var(–text-dim); text-transform: uppercase; letter-spacing: 0.1em; } .metric-value { font-size: 0.9rem; font-weight: 600; } .metric-bar { width: 100%; height: 3px; background: var(–border); border-radius: 2px; margin-top: 4px; overflow: hidden; } .metric-fill { height: 100%; border-radius: 2px; transition: width 0.1s, background 0.3s; } .state-indicator { display: flex; align-items: center; gap: 10px; padding: 10px 12px; background: var(–bg2); border: 1px solid var(–border); border-radius: 3px; margin-top: 6px; } .state-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; box-shadow: 0 0 6px currentColor; transition: background 0.3s, box-shadow 0.3s; } .state-text { font-size: 0.65rem; letter-spacing: 0.08em; text-transform: uppercase; } .info-block { font-size: 0.63rem; color: var(–text-dim); line-height: 1.7; font-family: ‘EB Garamond’, serif; font-size: 0.78rem; } .info-block em { color: var(–accent2); font-style: normal; } .control-row .hint { font-size: 0.58rem; color: var(–dim); margin-top: 3px; font-family: ‘EB Garamond’, serif; } footer { padding: 10px 32px; border-top: 1px solid var(–border); display: flex; gap: 32px; align-items: center; } .footer-stat { font-size: 0.58rem; color: var(–text-dim); letter-spacing: 0.1em; text-transform: uppercase; } .footer-stat span { color: var(–accent); margin-left: 8px; } .reset-btn { margin-left: auto; background: none; border: 1px solid var(–border); color: var(–text-dim); font-family: ‘JetBrains Mono’, monospace; font-size: 0.58rem; letter-spacing: 0.12em; text-transform: uppercase; padding: 6px 14px; cursor: pointer; border-radius: 2px; transition: all 0.15s; } .reset-btn:hover { border-color: var(–accent); color: var(–accent); } </style> </head> <body> <header> <div> <div class="title">Kuramoto Synchronization</div> <div class="subtitle">Collective order from coupled oscillators</div> </div> <div class="equation">dθᵢ/dt = ωᵢ + (K/N) Σⱼ sin(θⱼ − θᵢ)</div> </header> <div class="main"> <div class="canvas-area"> <div class="canvas-label">Phase space — oscillators on the unit circle</div> <canvas id="phase-canvas" width="460" height="460"></canvas> <div class="canvas-label">Order parameter r(t) — synchronization over time</div> <canvas id="order-canvas" width="460" height="120"></canvas> </div> <div class="sidebar"> ``` <div class="panel"> <div class="panel-title">Presets</div> <div class="preset-buttons"> <button class="preset-btn" onclick="applyPreset('chaos')">Chaos</button> <button class="preset-btn" onclick="applyPreset('edge')">Edge</button> <button class="preset-btn" onclick="applyPreset('sync')">Sync</button> <button class="preset-btn" onclick="applyPreset('heart')">Heart</button> <button class="preset-btn" onclick="applyPreset('epilepsy')">Seizure</button> <button class="preset-btn" onclick="applyPreset('flock')">Flock</button> </div> </div> <div class="panel"> <div class="panel-title">Parameters</div> <div class="control-row"> <div class="control-label"> <span class="control-name">Coupling K</span> <span class="control-value" id="k-val">1.0</span> </div> <input type="range" id="k-slider" min="0" max="6" step="0.05" value="1.0"> <div class="hint">Critical transition near K ≈ 2ω̄</div> </div> <div class="control-row"> <div class="control-label"> <span class="control-name">Freq. spread σ</span> <span class="control-value" id="sigma-val">1.0</span> </div> <input type="range" id="sigma-slider" min="0.1" max="4" step="0.1" value="1.0"> <div class="hint">Natural frequency variance</div> </div> <div class="control-row"> <div class="control-label"> <span class="control-name">Oscillators N</span> <span class="control-value" id="n-val">40</span> </div> <input type="range" id="n-slider" min="5" max="100" step="5" value="40"> </div> <div class="control-row"> <div class="control-label"> <span class="control-name">Noise η</span> <span class="control-value" id="noise-val">0.00</span> </div> <input type="range" id="noise-slider" min="0" max="2" step="0.05" value="0"> <div class="hint">Stochastic perturbation</div> </div> <div class="control-row"> <div class="control-label"> <span class="control-name">Speed</span> <span class="control-value" id="speed-val">1.0×</span> </div> <input type="range" id="speed-slider" min="0.2" max="4" step="0.1" value="1.0"> </div> </div> <div class="panel"> <div class="panel-title">Live Metrics</div> <div class="metrics"> <div> <div class="metric"> <div class="metric-name">Order r</div> <div class="metric-value" id="r-val" style="color: var(--accent)">0.00</div> </div> <div class="metric-bar"><div class="metric-fill" id="r-bar" style="width:0%;background:var(--accent)"></div></div> </div> <div> <div class="metric"> <div class="metric-name">Mean freq ψ̇</div> <div class="metric-value" id="psi-val" style="color: var(--accent3)">0.00</div> </div> </div> <div> <div class="metric"> <div class="metric-name">Phase variance</div> <div class="metric-value" id="var-val" style="color: var(--accent2)">0.00</div> </div> <div class="metric-bar"><div class="metric-fill" id="var-bar" style="width:50%;background:var(--accent2)"></div></div> </div> </div> <div class="state-indicator" id="state-ind"> <div class="state-dot" id="state-dot"></div> <div class="state-text" id="state-text">Initializing…</div> </div> </div> <div class="panel"> <div class="panel-title">Context</div> <div class="info-block" id="context-text"> At low coupling, oscillators spin at their own rates — <em>disorder</em>.<br><br> As K crosses the critical threshold, phase locking begins. The order parameter r jumps from near 0 toward 1 — a <em>phase transition</em> in the statistical physics sense.<br><br> The same mathematics describes cardiac pacemakers, epileptic seizures, power grid cascades, and neural gamma rhythms. </div> </div> ``` </div> </div> <footer> <div class="footer-stat">Frame<span id="frame-count">0</span></div> <div class="footer-stat">t<span id="time-count">0.00</span>s</div> <div class="footer-stat">K/σ<span id="ratio-val">1.00</span></div> <button class="reset-btn" onclick="resetSim()">↺ Reset</button> </footer> <script> const phaseCanvas = document.getElementById('phase-canvas'); const orderCanvas = document.getElementById('order-canvas'); const pc = phaseCanvas.getContext('2d'); const oc = orderCanvas.getContext('2d'); let N = 40, K = 1.0, sigma = 1.0, noise = 0, speed = 1.0; let phases = [], omegas = [], orderHistory = []; let frame = 0, t = 0; let animId; const presets = { chaos: { K: 0.3, sigma: 2.0, noise: 0.1, N: 40, ctx: "Weak coupling, high frequency spread. Each oscillator runs at its own rate. <em>Pure disorder</em> — no collective behavior emerges." }, edge: { K: 2.0, sigma: 1.0, noise: 0.05, N: 40, ctx: "Near the <em>critical threshold</em>. The system hovers between order and chaos — clusters form and dissolve. This edge state is biologically common." }, sync: { K: 5.0, sigma: 1.0, noise: 0.02, N: 40, ctx: "Strong coupling overwhelms individual differences. All oscillators <em>phase-lock</em> into a single coherent rhythm. r → 1." }, heart: { K: 3.5, sigma: 0.5, noise: 0.08, N: 60, ctx: "Cardiac pacemaker model. Tight frequency distribution, moderate coupling. SA node behavior: robust synchrony with slight natural variation." }, epilepsy: { K: 6.0, sigma: 0.2, noise: 0.0, N: 80, ctx: "<em>Pathological hypersynchrony.</em> In epilepsy, neural populations lock too tightly — r → 1 becomes a seizure. Healthy brains maintain r ~ 0.3–0.6." }, flock: { K: 2.5, sigma: 1.5, noise: 0.3, N: 60, ctx: "Flocking analog: higher noise (wind, perturbation), moderate coupling (neighbor influence). Partial synchrony with <em>emergent coherent motion</em>." }, }; function applyPreset(name) { document.querySelectorAll('.preset-btn').forEach(b => b.classList.remove('active')); event.target.classList.add('active'); const p = presets[name]; K = p.K; sigma = p.sigma; noise = p.noise; N = p.N; document.getElementById('k-slider').value = K; document.getElementById('sigma-slider').value = sigma; document.getElementById('noise-slider').value = noise; document.getElementById('n-slider').value = N; document.getElementById('k-val').textContent = K.toFixed(2); document.getElementById('sigma-val').textContent = sigma.toFixed(1); document.getElementById('noise-val').textContent = noise.toFixed(2); document.getElementById('n-val').textContent = N; document.getElementById('context-text').innerHTML = p.ctx; resetSim(); } function initSim() { phases = []; omegas = []; orderHistory = []; for (let i = 0; i < N; i++) { phases.push(Math.random() * 2 * Math.PI); omegas.push(gaussRand() * sigma); } } function gaussRand() { let u = 0, v = 0; while (!u) u = Math.random(); while (!v) v = Math.random(); return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v); } function resetSim() { frame = 0; t = 0; initSim(); } function step(dt) { const newPhases = [...phases]; for (let i = 0; i < N; i++) { let sum = 0; for (let j = 0; j < N; j++) { sum += Math.sin(phases[j] - phases[i]); } newPhases[i] = phases[i] + dt * (omegas[i] + (K / N) * sum + noise * gaussRand()); } phases = newPhases; } function orderParam() { let sx = 0, sy = 0; for (let i = 0; i < N; i++) { sx += Math.cos(phases[i]); sy += Math.sin(phases[i]); } const r = Math.sqrt(sx * sx + sy * sy) / N; const psi = Math.atan2(sy, sx); return { r, psi }; } function drawPhase() { const W = phaseCanvas.width, H = phaseCanvas.height; const cx = W / 2, cy = H / 2; const R = Math.min(W, H) * 0.38; pc.fillStyle = '#0d0f12'; pc.fillRect(0, 0, W, H); // Grid rings for (let ri = 0.25; ri <= 1.01; ri += 0.25) { pc.beginPath(); pc.arc(cx, cy, R * ri, 0, Math.PI * 2); pc.strokeStyle = ri === 1 ? '#1e2530' : '#141820'; pc.lineWidth = ri === 1 ? 1 : 0.5; pc.stroke(); } // Axis lines pc.strokeStyle = '#141820'; pc.lineWidth = 0.5; pc.beginPath(); pc.moveTo(cx - R - 10, cy); pc.lineTo(cx + R + 10, cy); pc.stroke(); pc.beginPath(); pc.moveTo(cx, cy - R - 10); pc.lineTo(cx, cy + R + 10); pc.stroke(); // Order vector const { r, psi } = orderParam(); pc.beginPath(); pc.moveTo(cx, cy); pc.lineTo(cx + Math.cos(psi) * R * r, cy + Math.sin(psi) * R * r); const rColor = r > 0.75 ? '#4fc3f7' : r > 0.4 ? '#e0a44a' : '#e05a7a'; pc.strokeStyle = rColor; pc.lineWidth = 2; pc.stroke(); // Order circle if (r > 0.05) { pc.beginPath(); pc.arc(cx + Math.cos(psi) * R * r, cy + Math.sin(psi) * R * r, 5, 0, Math.PI * 2); pc.fillStyle = rColor; pc.shadowColor = rColor; pc.shadowBlur = 12; pc.fill(); pc.shadowBlur = 0; } // Oscillator dots for (let i = 0; i < N; i++) { const px = cx + Math.cos(phases[i]) * R; const py = cy + Math.sin(phases[i]) * R; // Tail pc.beginPath(); pc.arc(px, py, 3.5, 0, Math.PI * 2); const hue = (phases[i] / (Math.PI * 2) * 360 + 180) % 360; pc.fillStyle = `hsla(${hue}, 70%, 65%, 0.85)`; pc.shadowColor = `hsl(${hue}, 70%, 65%)`; pc.shadowBlur = 6; pc.fill(); pc.shadowBlur = 0; } // r label pc.font = '11px JetBrains Mono'; pc.fillStyle = '#3a4555'; pc.fillText('r = ' + r.toFixed(3), 12, H - 12); } function drawOrder() { const W = orderCanvas.width, H = orderCanvas.height; oc.fillStyle = '#0d0f12'; oc.fillRect(0, 0, W, H); // Grid lines [0.25, 0.5, 0.75, 1.0].forEach(level => { oc.beginPath(); oc.moveTo(0, H - level * H * 0.85 - 8); oc.lineTo(W, H - level * H * 0.85 - 8); oc.strokeStyle = '#141820'; oc.lineWidth = level === 1.0 ? 0.5 : 0.3; oc.stroke(); oc.fillStyle = '#2a3540'; oc.font = '9px JetBrains Mono'; oc.fillText(level.toFixed(2), 4, H - level * H * 0.85 - 10); }); if (orderHistory.length < 2) return; const maxPts = W; const pts = orderHistory.slice(-maxPts); const step = W / maxPts; oc.beginPath(); for (let i = 0; i < pts.length; i++) { const x = i * step; const y = H - pts[i] * H * 0.85 - 8; if (i === 0) oc.moveTo(x, y); else oc.lineTo(x, y); } const lastR = pts[pts.length - 1]; const lineColor = lastR > 0.75 ? '#4fc3f7' : lastR > 0.4 ? '#e0a44a' : '#e05a7a'; oc.strokeStyle = lineColor; oc.lineWidth = 1.5; oc.stroke(); } function updateControls() { K = +document.getElementById('k-slider').value; sigma = +document.getElementById('sigma-slider').value; noise = +document.getElementById('noise-slider').value; speed = +document.getElementById('speed-slider').value; const newN = +document.getElementById('n-slider').value; if (newN !== N) { N = newN; initSim(); } document.getElementById('k-val').textContent = K.toFixed(2); document.getElementById('sigma-val').textContent = sigma.toFixed(1); document.getElementById('noise-val').textContent = noise.toFixed(2); document.getElementById('n-val').textContent = N; document.getElementById('speed-val').textContent = speed.toFixed(1) + '×'; } function updateMetrics(r, psi) { document.getElementById('r-val').textContent = r.toFixed(3); document.getElementById('r-bar').style.width = (r * 100).toFixed(1) + '%'; const rColor = r > 0.75 ? 'var(--synced)' : r > 0.4 ? 'var(--transition)' : 'var(--chaos)'; document.getElementById('r-val').style.color = rColor; document.getElementById('r-bar').style.background = rColor; // Phase variance let sumCos = 0, sumSin = 0; for (let i = 0; i < N; i++) { sumCos += Math.cos(phases[i]); sumSin += Math.sin(phases[i]); } const variance = 1 - (Math.sqrt(sumCos*sumCos + sumSin*sumSin) / N); document.getElementById('var-val').textContent = variance.toFixed(3); document.getElementById('var-bar').style.width = (variance * 100).toFixed(1) + '%'; const avgOmega = omegas.reduce((a,b)=>a+b,0)/N; document.getElementById('psi-val').textContent = avgOmega.toFixed(3); // State const dot = document.getElementById('state-dot'); const stateText = document.getElementById('state-text'); if (r > 0.8) { dot.style.background = 'var(--synced)'; dot.style.color = 'var(--synced)'; stateText.textContent = 'Phase-locked — synchronized'; stateText.style.color = 'var(--synced)'; } else if (r > 0.5) { dot.style.background = 'var(--transition)'; dot.style.color = 'var(--transition)'; stateText.textContent = 'Partial sync — clusters forming'; stateText.style.color = 'var(--transition)'; } else if (r > 0.25) { dot.style.background = 'var(--transition)'; stateText.textContent = 'Near critical threshold'; stateText.style.color = 'var(--transition)'; } else { dot.style.background = 'var(--chaos)'; stateText.textContent = 'Incoherent — disordered'; stateText.style.color = 'var(--chaos)'; } document.getElementById('frame-count').textContent = frame; document.getElementById('time-count').textContent = t.toFixed(2); document.getElementById('ratio-val').textContent = (K / sigma).toFixed(2); } function loop() { updateControls(); const dt = 0.016 * speed; step(dt); t += dt; frame++; const { r, psi } = orderParam(); orderHistory.push(r); if (orderHistory.length > 1000) orderHistory.shift(); drawPhase(); drawOrder(); updateMetrics(r, psi); animId = requestAnimationFrame(loop); } ['k-slider','sigma-slider','noise-slider','n-slider','speed-slider'].forEach(id => { document.getElementById(id).addEventListener('input', () => { if (id === 'sigma-slider') { sigma = +document.getElementById('sigma-slider').value; for (let i = 0; i < N; i++) omegas[i] = gaussRand() * sigma; } }); }); resetSim(); loop(); </script> </body> </html>
I'll convert this beautiful Kuramoto synchronization visualization into a Next.js React application. Let me first check the existing project structure, then build the interactive simulation.
Now I'll build the Kuramoto synchronization visualization. I'll update the layout with proper fonts and metadata, customize the globals.css for the dark scientific aesthetic, and create the interactive simulation components.
Now let me create the main Kuramoto simulation component and page:
The Kuramoto synchronization visualization is now built as a Next.js React application. It features:
The visualization demonstrates the Kuramoto phase transition: as coupling K crosses the critical threshold (~2σ), the system transitions from incoherent disorder (r ≈ 0) to synchronized phase-locking (r → 1). Try dragging the K slider from 0 to 6 to watch clusters nucleate and synchronization emerge in real time.
You can send messages while the preview loads