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 {
4171 margin-bottom : 1.5rem ;
4272 max-width : 600px ;
4373 text-align : center;
74+ line-height : 1.45 ;
4475 }
4576
4677 .main-container {
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 );
95126 font-size : 0.9rem ;
96127 display : flex;
97128 justify-content : space-between;
129+ letter-spacing : -0.01em ;
98130 }
99131
100132 input [type = "range" ] {
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
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;
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 ;
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