Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Slit Experiment Simulator</title> | |
| <style> | |
| *, *::before, *::after { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| html, body { | |
| width: 100%; | |
| height: 100%; | |
| font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, sans-serif; | |
| background: linear-gradient(135deg, #0f0c29 0%, #1a1a3e 40%, #24243e 100%); | |
| color: #e8e8f0; | |
| overflow: hidden; | |
| -webkit-text-size-adjust: 100%; | |
| touch-action: manipulation; | |
| } | |
| #app { | |
| display: flex; | |
| flex-direction: column; | |
| width: 100%; | |
| height: 100%; | |
| overflow: hidden; | |
| } | |
| #viewport-row { | |
| display: flex; | |
| flex-direction: row; | |
| flex: 1; | |
| min-height: 0; | |
| overflow: hidden; | |
| } | |
| #scene-container { | |
| flex: 1; | |
| min-height: 150px; | |
| background: linear-gradient(180deg, #1a1a30 0%, #252540 100%); | |
| position: relative; | |
| overflow: hidden; | |
| touch-action: none; | |
| } | |
| #intensity-container { | |
| width: 200px; | |
| background: linear-gradient(180deg, #12122a 0%, #1a1a30 100%); | |
| border-left: 1px solid rgba(100, 140, 255, 0.2); | |
| position: relative; | |
| overflow: hidden; | |
| flex-shrink: 0; | |
| } | |
| #control-container { | |
| width: 100%; | |
| background: linear-gradient(180deg, #161630 0%, #0e0e24 100%); | |
| border-top: 2px solid rgba(100, 140, 255, 0.2); | |
| padding: 8px 10px; | |
| overflow-x: auto; | |
| overflow-y: auto; | |
| max-height: 40vh; | |
| flex-shrink: 0; | |
| box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.3); | |
| -webkit-overflow-scrolling: touch; | |
| } | |
| #error-overlay { | |
| display: none; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: #0f0c29; | |
| color: #e0e0e0; | |
| z-index: 9999; | |
| justify-content: center; | |
| align-items: center; | |
| text-align: center; | |
| padding: 2rem; | |
| font-size: 1.2rem; | |
| } | |
| #error-overlay p { | |
| max-width: 500px; | |
| line-height: 1.6; | |
| } | |
| @media (min-width: 1024px) { | |
| #intensity-container { | |
| width: 240px; | |
| } | |
| } | |
| /* Mobile: narrower intensity panel, compact controls */ | |
| @media (max-width: 768px) { | |
| #intensity-container { | |
| width: 120px; | |
| } | |
| #scene-container { | |
| min-height: 120px; | |
| } | |
| #control-container { | |
| padding: 6px 8px; | |
| max-height: 50vh; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="app"> | |
| <div id="viewport-row"> | |
| <div id="scene-container"> | |
| <div style="position:absolute;top:6px;left:10px;font-size:clamp(13px,2.5vw,20px);font-weight:700;color:#fff;pointer-events:none;z-index:10;text-shadow:0 2px 8px rgba(80,120,255,0.4),0 1px 3px rgba(0,0,0,0.6);letter-spacing:0.5px;">🌊 3D Slit Simulation</div> | |
| <div id="view-buttons" style="position:absolute;top:30px;left:10px;display:flex;gap:4px;z-index:10;flex-wrap:wrap;"> | |
| <button id="btn-top-view" style="padding:3px 8px;border:1px solid rgba(100,140,255,0.3);border-radius:5px;background:rgba(100,140,255,0.2);color:#c0ccff;font-size:clamp(9px,1.5vw,11px);cursor:pointer;font-weight:600;white-space:nowrap;">Top View</button> | |
| <button id="btn-angle-view" style="padding:3px 8px;border:1px solid rgba(100,140,255,0.3);border-radius:5px;background:rgba(255,255,255,0.05);color:#8899bb;font-size:clamp(9px,1.5vw,11px);cursor:pointer;font-weight:600;white-space:nowrap;">45° View</button> | |
| <button id="btn-135-view" style="padding:3px 8px;border:1px solid rgba(100,140,255,0.3);border-radius:5px;background:rgba(255,255,255,0.05);color:#8899bb;font-size:clamp(9px,1.5vw,11px);cursor:pointer;font-weight:600;white-space:nowrap;">135° View</button> | |
| </div> | |
| </div> | |
| <div id="intensity-container"></div> | |
| </div> | |
| <div id="control-container"> | |
| <div id="control-buttons" style="display:flex;align-items:center;justify-content:center;gap:6px;padding:6px 0;"></div> | |
| <div id="control-toggle" style="display:none;text-align:center;padding:4px 0;cursor:pointer;color:#8899cc;font-size:12px;font-weight:600;user-select:none;touch-action:manipulation;">▼ Sliders ▼</div> | |
| <div id="control-inner"></div> | |
| </div> | |
| </div> | |
| <div id="error-overlay"> | |
| <p>Required libraries could not be loaded. Please check your internet connection and try again.</p> | |
| </div> | |
| <!-- About modal --> | |
| <div id="about-overlay" style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);z-index:9998;justify-content:center;align-items:center;"> | |
| <div style="background:linear-gradient(135deg, #1a1a3e 0%, #252550 100%);border:1px solid rgba(100,140,255,0.3);border-radius:14px;max-width:560px;max-height:85vh;overflow-y:auto;padding:20px 24px;color:#d0d4e8;font-size:13px;line-height:1.7;position:relative;box-shadow:0 8px 40px rgba(0,0,0,0.5);margin:10px;width:calc(100% - 20px);"> | |
| <button id="about-close" style="position:absolute;top:10px;right:14px;background:none;border:none;color:#8899cc;font-size:20px;cursor:pointer;">✕</button> | |
| <h2 style="margin:0 0 12px;font-size:20px;color:#fff;text-shadow:0 1px 6px rgba(80,120,255,0.3);">🌊 3D Slit Simulation</h2> | |
| <p>This simulator models wave propagation through single and double slits using a 2D FDTD (Finite-Difference Time-Domain) solver. The wave field is rendered as a 3D height-mapped surface with amplitude-based coloring — warm tones for peaks, cool tones for troughs.</p> | |
| <p>A planar wave source on the left emits coherent waves toward a barrier with configurable slit openings. As waves pass through the slits, they diffract and interfere, producing characteristic patterns on the 3D surface and the 2D intensity panel.</p> | |
| <p style="color:#aabbdd;"><span style="color:#ffcc44;">⬤</span> <span style="color:#00cc44;">⬤</span> <b style="color:#c0ccff;">Polarization (Light mode):</b> In double-slit mode with Light selected, you can choose between Same (∥) and Orthogonal (⊥) polarization. With same polarization, waves from both slits interfere normally, producing bright fringes. With orthogonal polarization, each slit emits a different polarization component — these cannot interfere, so the fringe pattern disappears and you see only two overlapping diffraction envelopes.</p> | |
| <p style="color:#aabbdd;"><span style="color:#4488ff;">⬤</span> <b style="color:#c0ccff;">45° Polarizer:</b> When orthogonal polarization is active, enabling the 45° polarizer places a diagonal filter between the slits and the detector. This filter projects both orthogonal components onto the same axis: E<sub>out</sub> = (E₁ + E₂)/√2. Since both components now share the same polarization direction, they can interfere again — and the fringe pattern reappears, just like same-polarization mode. This demonstrates a key principle of quantum optics: erasing "which-path" information restores interference.</p> | |
| <p>The green detector screen samples the wave amplitude along a vertical line. The 2D panel shows the live waveform (green) and the sliding-window averaged intensity (red).</p> | |
| <p style="margin-top:16px;color:#8899aa;font-style:italic;text-align:right;">Created by Andy Kong</p> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| var overlay = document.getElementById('about-overlay'); | |
| var closeBtn = document.getElementById('about-close'); | |
| if (closeBtn) closeBtn.addEventListener('click', function() { overlay.style.display = 'none'; }); | |
| overlay.addEventListener('click', function(e) { if (e.target === overlay) overlay.style.display = 'none'; }); | |
| // Collapsible sliders on mobile (buttons stay visible) | |
| var toggle = document.getElementById('control-toggle'); | |
| var inner = document.getElementById('control-inner'); | |
| function checkMobile() { | |
| if (window.innerWidth < 768) { | |
| toggle.style.display = 'block'; | |
| inner.style.display = 'none'; | |
| toggle.textContent = '▼ Sliders ▼'; | |
| } else { | |
| toggle.style.display = 'none'; | |
| inner.style.display = ''; | |
| } | |
| } | |
| if (toggle && inner) { | |
| checkMobile(); | |
| window.addEventListener('resize', checkMobile); | |
| toggle.addEventListener('click', function() { | |
| if (inner.style.display === 'none') { | |
| inner.style.display = ''; | |
| toggle.textContent = '▲ Sliders ▲'; | |
| } else { | |
| inner.style.display = 'none'; | |
| toggle.textContent = '▼ Sliders ▼'; | |
| } | |
| }); | |
| } | |
| }); | |
| </script> | |
| <!-- Three.js via ES Module importmap --> | |
| <script type="importmap"> | |
| { | |
| "imports": { | |
| "three": "https://unpkg.com/three@0.160.0/build/three.module.js", | |
| "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/" | |
| } | |
| } | |
| </script> | |
| <!-- Bootstrap: import THREE + OrbitControls as ES modules, expose as globals, then load app --> | |
| <script type="module"> | |
| import * as THREE_MODULE from 'three'; | |
| import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; | |
| // Expose as a mutable global so plain-script app modules can use them | |
| const THREE = Object.assign({}, THREE_MODULE); | |
| THREE.OrbitControls = OrbitControls; | |
| window.THREE = THREE; | |
| // CDN load error detection | |
| if (typeof THREE === 'undefined') { | |
| document.getElementById('error-overlay').style.display = 'flex'; | |
| } | |
| // Dynamically load application scripts in order after THREE is available | |
| const scripts = [ | |
| 'fdtd-solver.js', | |
| 'scene-manager.js', | |
| 'wave-surface.js', | |
| 'intensity-panel.js', | |
| 'controls.js', | |
| 'main.js' | |
| ]; | |
| for (const src of scripts) { | |
| await new Promise((resolve, reject) => { | |
| const s = document.createElement('script'); | |
| s.src = src; | |
| s.onload = resolve; | |
| s.onerror = reject; | |
| document.body.appendChild(s); | |
| }); | |
| } | |
| </script> | |
| </body> | |
| </html> | |