Skip to content

Commit 570aedc

Browse files
committed
Polish demo typography and add Floquet phase portrait variant
1 parent 0ec8216 commit 570aedc

File tree

8 files changed

+1902
-158
lines changed

8 files changed

+1902
-158
lines changed

content/blog/nonlinear-system-study/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,6 @@ If you want to play with them directly:
1919
- [Van der Pol oscillator (3D)](/nonlinear/vdp.html)
2020
- [Torus oscillator and Poincare section](/nonlinear/torus_oscillator.html)
2121
- [Floquet stability and parametric resonance](/nonlinear/floquet.html)
22+
- [Floquet phase portrait variant](/nonlinear/floquet2.html)
2223

2324
Nothing fancy, just practical visual notes I can revisit later. I’ll keep adding more as I go.

static/nonlinear/double_well.html

Lines changed: 125 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,35 +4,65 @@
44
<meta charset="UTF-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
66
<title>Double-Well Potential Simulation</title>
7+
<link rel="preconnect" href="https://fonts.googleapis.com">
8+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9+
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+3:wght@400;500;600;700&family=STIX+Two+Text:wght@500;600;700&display=swap" rel="stylesheet">
710
<style>
811
:root {
12+
--font-ui: 'Source Sans 3', 'Helvetica Neue', Arial, sans-serif;
13+
--font-display: 'STIX Two Text', Georgia, serif;
914
--primary: #2563eb;
1015
--secondary: #475569;
1116
--bg: #f8fafc;
1217
--panel: #ffffff;
1318
--danger: #ef4444;
19+
--text: #1e293b;
20+
--heading: #0f172a;
21+
--muted: #64748b;
22+
--canvas-bg: #ffffff;
23+
--note-bg: #f1f5f9;
24+
--theme-button-bg: rgba(255, 255, 255, 0.88);
25+
--theme-button-border: rgba(148, 163, 184, 0.35);
26+
}
27+
28+
:root[data-theme="dark"] {
29+
--primary: #7dd3fc;
30+
--secondary: #94a3b8;
31+
--bg: #020617;
32+
--panel: #0f172a;
33+
--danger: #fb7185;
34+
--text: #e2e8f0;
35+
--heading: #f8fafc;
36+
--muted: #94a3b8;
37+
--canvas-bg: #081121;
38+
--note-bg: #111c2e;
39+
--theme-button-bg: rgba(15, 23, 42, 0.88);
40+
--theme-button-border: rgba(148, 163, 184, 0.2);
1441
}
1542

1643
* {
1744
box-sizing: border-box;
1845
}
1946

2047
body {
21-
font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
48+
font-family: var(--font-ui);
2249
background-color: var(--bg);
23-
color: #1e293b;
50+
color: var(--text);
2451
display: flex;
2552
flex-direction: column;
2653
align-items: center;
2754
margin: 0;
2855
padding: 20px;
2956
min-height: 100vh;
57+
position: relative;
3058
}
3159

3260
h1 {
3361
margin-bottom: 0.5rem;
3462
font-size: 1.5rem;
35-
color: #0f172a;
63+
color: var(--heading);
64+
font-family: var(--font-display);
65+
letter-spacing: -0.02em;
3666
}
3767

3868
.subtitle {
@@ -41,6 +71,7 @@
4171
margin-bottom: 1.5rem;
4272
max-width: 600px;
4373
text-align: center;
74+
line-height: 1.45;
4475
}
4576

4677
.main-container {
@@ -67,7 +98,7 @@
6798
canvas {
6899
border: 1px solid #e2e8f0;
69100
border-radius: 4px;
70-
background-color: #fff;
101+
background-color: var(--canvas-bg);
71102
cursor: crosshair;
72103
display: block;
73104
width: min(100%, 500px);
@@ -95,6 +126,7 @@
95126
font-size: 0.9rem;
96127
display: flex;
97128
justify-content: space-between;
129+
letter-spacing: -0.01em;
98130
}
99131

100132
input[type="range"] {
@@ -103,7 +135,7 @@
103135
}
104136

105137
.value-display {
106-
font-family: 'Courier New', monospace;
138+
font-family: ui-monospace, 'SFMono-Regular', Menlo, Consolas, monospace;
107139
color: var(--primary);
108140
}
109141

@@ -133,12 +165,19 @@
133165
.math-note {
134166
margin-top: 10px;
135167
padding: 10px;
136-
background: #f1f5f9;
168+
background: var(--note-bg);
137169
border-radius: 6px;
138170
font-size: 0.85rem;
139171
line-height: 1.5;
140172
border-left: 4px solid var(--primary);
141173
}
174+
.subtitle mjx-container,
175+
.math-note mjx-container {
176+
margin: 0 0.03em;
177+
}
178+
mjx-container {
179+
font-size: 1.03em !important;
180+
}
142181

143182
.legend {
144183
display: flex;
@@ -153,6 +192,25 @@
153192
}
154193
.dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
155194

195+
#theme-toggle {
196+
position: fixed;
197+
top: max(14px, env(safe-area-inset-top));
198+
right: max(14px, env(safe-area-inset-right));
199+
z-index: 20;
200+
width: 44px;
201+
height: 44px;
202+
border-radius: 999px;
203+
border: 1px solid var(--theme-button-border);
204+
background: var(--theme-button-bg);
205+
color: var(--heading);
206+
font-size: 1.2rem;
207+
line-height: 1;
208+
display: inline-flex;
209+
align-items: center;
210+
justify-content: center;
211+
box-shadow: 0 12px 24px rgba(15, 23, 42, 0.12);
212+
}
213+
156214
.button-row {
157215
flex-direction: row;
158216
gap: 10px;
@@ -197,6 +255,8 @@
197255
</head>
198256
<body>
199257

258+
<button id="theme-toggle" type="button" aria-label="Toggle color scheme"></button>
259+
200260
<h1>Particle in a Double-Well Potential</h1>
201261
<div class="subtitle">
202262
Visualizing the conservative system from Example 6.5.2: $\ddot{x} = x - x^3$.<br>
@@ -254,6 +314,55 @@ <h1>Particle in a Double-Well Potential</h1>
254314
<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
255315

256316
<script>
317+
const THEME_KEY = 'nonlinear-demo-theme';
318+
const themeToggle = document.getElementById('theme-toggle');
319+
const themePalettes = {
320+
light: {
321+
axis: '#cbd5e1',
322+
axisText: '#64748b',
323+
contour: '#e2e8f0',
324+
separatrix: '#94a3b8',
325+
active: '#2563eb',
326+
particle: '#ef4444',
327+
potential: '#334155',
328+
energyLine: '#2563eb',
329+
kinetic: 'rgba(239, 68, 68, 0.5)'
330+
},
331+
dark: {
332+
axis: '#334155',
333+
axisText: '#94a3b8',
334+
contour: 'rgba(148, 163, 184, 0.22)',
335+
separatrix: '#64748b',
336+
active: '#7dd3fc',
337+
particle: '#fb7185',
338+
potential: '#93c5fd',
339+
energyLine: '#7dd3fc',
340+
kinetic: 'rgba(251, 113, 133, 0.55)'
341+
}
342+
};
343+
let themeColors = themePalettes.light;
344+
345+
function getPreferredTheme() {
346+
const stored = localStorage.getItem(THEME_KEY);
347+
if (stored === 'light' || stored === 'dark') return stored;
348+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
349+
}
350+
351+
function applyTheme(theme) {
352+
document.documentElement.dataset.theme = theme;
353+
themeColors = themePalettes[theme];
354+
themeToggle.textContent = theme === 'dark' ? '☀' : '☾';
355+
themeToggle.setAttribute('aria-label', theme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme');
356+
}
357+
358+
themeToggle.addEventListener('click', () => {
359+
const nextTheme = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
360+
localStorage.setItem(THEME_KEY, nextTheme);
361+
applyTheme(nextTheme);
362+
});
363+
364+
applyTheme(getPreferredTheme());
365+
257366
// --- Configuration ---
258367
const phaseCanvas = document.getElementById('phaseCanvas');
259368
const phaseCtx = phaseCanvas.getContext('2d');
@@ -362,7 +471,7 @@ <h1>Particle in a Double-Well Potential</h1>
362471
// --- Drawing ---
363472

364473
function drawAxes(ctx, width, height, yPosOverride = null) {
365-
ctx.strokeStyle = '#cbd5e1';
474+
ctx.strokeStyle = themeColors.axis;
366475
ctx.lineWidth = 1;
367476
ctx.beginPath();
368477

@@ -377,7 +486,7 @@ <h1>Particle in a Double-Well Potential</h1>
377486
ctx.stroke();
378487

379488
// Labels
380-
ctx.fillStyle = '#64748b';
489+
ctx.fillStyle = themeColors.axisText;
381490
ctx.font = '12px Arial';
382491
ctx.fillText('x', width - 15, yAxisPos + 15);
383492
if(yPosOverride === null) ctx.fillText('y', width/2 + 5, 15);
@@ -390,7 +499,7 @@ <h1>Particle in a Double-Well Potential</h1>
390499
phaseCtx.lineWidth = 1;
391500

392501
energies.forEach(e => {
393-
phaseCtx.strokeStyle = e === 0 ? '#94a3b8' : '#e2e8f0'; // Darker for separatrix
502+
phaseCtx.strokeStyle = e === 0 ? themeColors.separatrix : themeColors.contour; // Darker for separatrix
394503
if (e === 0) phaseCtx.setLineDash([5, 3]);
395504
else phaseCtx.setLineDash([]);
396505

@@ -437,15 +546,15 @@ <h1>Particle in a Double-Well Potential</h1>
437546
}
438547

439548
if (isActive) {
440-
phaseCtx.strokeStyle = '#2563eb';
549+
phaseCtx.strokeStyle = themeColors.active;
441550
phaseCtx.lineWidth = 2;
442551
}
443552
phaseCtx.stroke();
444553
}
445554

446555
function drawPotentialCurve() {
447556
energyCtx.beginPath();
448-
energyCtx.strokeStyle = '#334155';
557+
energyCtx.strokeStyle = themeColors.potential;
449558
energyCtx.lineWidth = 2;
450559

451560
for (let px = 0; px < energyCanvas.width; px++) {
@@ -461,7 +570,7 @@ <h1>Particle in a Double-Well Potential</h1>
461570

462571
function drawEnergyLine() {
463572
energyCtx.beginPath();
464-
energyCtx.strokeStyle = '#2563eb';
573+
energyCtx.strokeStyle = themeColors.energyLine;
465574
energyCtx.lineWidth = 1;
466575
energyCtx.setLineDash([5, 5]);
467576
let py = energyOffsetY - currentEnergy * energyScaleY;
@@ -470,7 +579,7 @@ <h1>Particle in a Double-Well Potential</h1>
470579
energyCtx.stroke();
471580
energyCtx.setLineDash([]);
472581

473-
energyCtx.fillStyle = '#2563eb';
582+
energyCtx.fillStyle = themeColors.energyLine;
474583
energyCtx.fillText(`E = ${currentEnergy.toFixed(2)}`, 10, py - 5);
475584
}
476585

@@ -496,7 +605,7 @@ <h1>Particle in a Double-Well Potential</h1>
496605

497606
// Draw Particle on Phase Plane
498607
let pScreen = toScreen(state.x, state.y);
499-
phaseCtx.fillStyle = '#ef4444';
608+
phaseCtx.fillStyle = themeColors.particle;
500609
phaseCtx.beginPath();
501610
phaseCtx.arc(pScreen.x, pScreen.y, 6, 0, Math.PI * 2);
502611
phaseCtx.fill();
@@ -516,15 +625,15 @@ <h1>Particle in a Double-Well Potential</h1>
516625
let eScreen = toEnergyScreen(state.x, potVal);
517626

518627
// Draw potential energy bead
519-
energyCtx.fillStyle = '#ef4444';
628+
energyCtx.fillStyle = themeColors.particle;
520629
energyCtx.beginPath();
521630
energyCtx.arc(eScreen.x, eScreen.y, 6, 0, Math.PI * 2);
522631
energyCtx.fill();
523632

524633
// Draw Kinetic Energy bar (Difference between E line and V curve)
525634
let totalEScreenY = energyOffsetY - currentEnergy * energyScaleY;
526635
energyCtx.beginPath();
527-
energyCtx.strokeStyle = 'rgba(239, 68, 68, 0.5)';
636+
energyCtx.strokeStyle = themeColors.kinetic;
528637
energyCtx.lineWidth = 2;
529638
energyCtx.moveTo(eScreen.x, eScreen.y); // From V(x)
530639
energyCtx.lineTo(eScreen.x, totalEScreenY); // To E

0 commit comments

Comments
 (0)