Spaces:
Running
Running
| /** | |
| * ControlPanel — DOM-based control panel (sliders, buttons, toggles) and event wiring. | |
| * Loaded as a plain script; class is defined on the global scope. | |
| */ | |
| /** | |
| * @typedef {Object} SimParams | |
| * @property {'single'|'double'} mode | |
| * @property {number} slitWidth - in wavelength units | |
| * @property {number} slitSeparation - in wavelength units | |
| * @property {number} wavelength - in grid-cell units | |
| * @property {boolean} playing | |
| */ | |
| /** | |
| * Clamp simulation parameters to their valid ranges. | |
| * @param {SimParams} params | |
| * @returns {SimParams} A new object with clamped values. | |
| */ | |
| function clampParams(params) { | |
| return Object.assign({}, params, { | |
| slitWidth: Math.min(600, Math.max(1, params.slitWidth)), | |
| slitSeparation: Math.min(150, Math.max(10, params.slitSeparation)), | |
| wavelength: Math.min(40, Math.max(10, params.wavelength)), | |
| amplitude: Math.min(5.0, Math.max(0.01, params.amplitude)), | |
| }); | |
| } | |
| class ControlPanel { | |
| constructor() { | |
| /** @type {SimParams} */ | |
| this.params = { | |
| mode: 'double', | |
| slitWidth: 40, | |
| slitSeparation: 80, | |
| wavelength: 20, | |
| amplitude: 2.0, | |
| sourceWidth: 600, | |
| detectorPos: 330, | |
| waveNature: 'light', | |
| polarization: 'orthogonal', | |
| polarizer45: false, | |
| topSlitOpen: true, | |
| bottomSlitOpen: true, | |
| playing: false, | |
| }; | |
| /** @type {function(SimParams): void} */ | |
| this.onChange = null; | |
| /** @type {function(): void} */ | |
| this.onReset = null; | |
| } | |
| /** | |
| * Build DOM elements inside the container and wire events. | |
| * @param {HTMLElement} container | |
| */ | |
| init(container) { | |
| var self = this; | |
| // Root wrapper | |
| var wrapper = document.createElement('div'); | |
| wrapper.style.cssText = 'display:flex;flex-wrap:wrap;align-items:center;gap:8px 12px;font-size:12px;color:#d0d4e8;'; | |
| // --- Helper: create a labeled slider group --- | |
| function createSliderGroup(label, min, max, step, value) { | |
| var group = document.createElement('div'); | |
| group.style.cssText = 'display:flex;align-items:center;gap:4px;background:rgba(255,255,255,0.04);padding:3px 8px;border-radius:6px;min-width:0;'; | |
| var lbl = document.createElement('label'); | |
| lbl.textContent = label; | |
| lbl.style.cssText = 'white-space:nowrap;color:#8899cc;font-size:11px;font-weight:500;'; | |
| var input = document.createElement('input'); | |
| input.type = 'range'; | |
| input.min = String(min); | |
| input.max = String(max); | |
| input.step = String(step); | |
| input.value = String(value); | |
| input.style.cssText = 'width:80px;accent-color:#7b9fff;cursor:pointer;height:6px;touch-action:none;'; | |
| var display = document.createElement('span'); | |
| display.textContent = String(value); | |
| display.style.cssText = 'min-width:28px;text-align:right;font-variant-numeric:tabular-nums;color:#a0b0ff;font-size:11px;font-weight:600;'; | |
| group.appendChild(lbl); | |
| group.appendChild(input); | |
| group.appendChild(display); | |
| return { group: group, input: input, display: display }; | |
| } | |
| // --- Helper: create a styled button --- | |
| function createButton(text, bg) { | |
| var btn = document.createElement('button'); | |
| btn.textContent = text; | |
| btn.style.cssText = | |
| 'padding:8px 14px;border:none;border-radius:6px;cursor:pointer;font-size:11px;font-weight:700;color:#fff;background:' + bg + ';letter-spacing:0.3px;transition:all 0.15s;box-shadow:0 2px 8px rgba(0,0,0,0.3);touch-action:manipulation;min-height:36px;'; | |
| btn.addEventListener('mouseenter', function () { btn.style.transform = 'scale(1.05)'; btn.style.boxShadow = '0 3px 12px rgba(0,0,0,0.4)'; }); | |
| btn.addEventListener('mouseleave', function () { btn.style.transform = 'scale(1)'; btn.style.boxShadow = '0 2px 8px rgba(0,0,0,0.3)'; }); | |
| return btn; | |
| } | |
| // ---- Mode toggle (single / double) ---- | |
| var modeGroup = document.createElement('div'); | |
| modeGroup.style.cssText = 'display:flex;align-items:center;gap:6px;'; | |
| var modeLbl = document.createElement('label'); | |
| modeLbl.textContent = 'Mode:'; | |
| modeLbl.style.cssText = 'white-space:nowrap;color:#8899cc;font-size:12px;font-weight:500;'; | |
| var modeSelect = document.createElement('select'); | |
| modeSelect.style.cssText = | |
| 'padding:6px 10px;border:1px solid rgba(100,140,255,0.25);border-radius:6px;background:rgba(255,255,255,0.06);color:#c0ccff;font-size:12px;cursor:pointer;font-weight:500;min-height:32px;'; | |
| var optSingle = document.createElement('option'); | |
| optSingle.value = 'single'; | |
| optSingle.textContent = 'Single Slit'; | |
| var optDouble = document.createElement('option'); | |
| optDouble.value = 'double'; | |
| optDouble.textContent = 'Double Slit'; | |
| modeSelect.appendChild(optSingle); | |
| modeSelect.appendChild(optDouble); | |
| modeSelect.value = this.params.mode; | |
| modeGroup.appendChild(modeLbl); | |
| modeGroup.appendChild(modeSelect); | |
| // ---- Wave nature selector ---- | |
| var waveGroup = document.createElement('div'); | |
| waveGroup.style.cssText = 'display:flex;align-items:center;gap:6px;'; | |
| var waveLbl = document.createElement('label'); | |
| waveLbl.textContent = 'Wave:'; | |
| waveLbl.style.cssText = 'white-space:nowrap;color:#8899cc;font-size:12px;font-weight:500;'; | |
| var waveSelect = document.createElement('select'); | |
| waveSelect.style.cssText = 'padding:6px 10px;border:1px solid rgba(100,140,255,0.25);border-radius:6px;background:rgba(255,255,255,0.06);color:#c0ccff;font-size:12px;cursor:pointer;font-weight:500;min-height:32px;'; | |
| var optSound = document.createElement('option'); | |
| optSound.value = 'sound'; optSound.textContent = 'Sound'; | |
| var optLight = document.createElement('option'); | |
| optLight.value = 'light'; optLight.textContent = 'Light'; | |
| waveSelect.appendChild(optSound); | |
| waveSelect.appendChild(optLight); | |
| waveSelect.value = this.params.waveNature; | |
| waveGroup.appendChild(waveLbl); | |
| waveGroup.appendChild(waveSelect); | |
| // ---- Polarization selector (only visible for light + double slit) ---- | |
| var polGroup = document.createElement('div'); | |
| polGroup.style.cssText = 'display:flex;align-items:center;gap:6px;'; | |
| var polLbl = document.createElement('label'); | |
| polLbl.textContent = 'Polarization:'; | |
| polLbl.style.cssText = 'white-space:nowrap;color:#8899cc;font-size:12px;font-weight:500;'; | |
| var polSelect = document.createElement('select'); | |
| polSelect.style.cssText = 'padding:6px 10px;border:1px solid rgba(100,140,255,0.25);border-radius:6px;background:rgba(255,255,255,0.06);color:#c0ccff;font-size:12px;cursor:pointer;font-weight:500;min-height:32px;'; | |
| var optSamePol = document.createElement('option'); | |
| optSamePol.value = 'same'; optSamePol.textContent = 'Same (∥)'; | |
| var optOrthPol = document.createElement('option'); | |
| optOrthPol.value = 'orthogonal'; optOrthPol.textContent = 'Orthogonal (⊥)'; | |
| polSelect.appendChild(optSamePol); | |
| polSelect.appendChild(optOrthPol); | |
| polSelect.value = this.params.polarization; | |
| polGroup.appendChild(polLbl); | |
| polGroup.appendChild(polSelect); | |
| // Show only when light + double slit | |
| polGroup.style.display = (this.params.waveNature === 'light' && this.params.mode === 'double') ? 'flex' : 'none'; | |
| // ---- 45° Polarizer toggle (only visible for orthogonal polarization) ---- | |
| var pol45Group = document.createElement('div'); | |
| pol45Group.style.cssText = 'display:flex;align-items:center;gap:6px;'; | |
| var pol45Check = document.createElement('input'); | |
| pol45Check.type = 'checkbox'; | |
| pol45Check.checked = this.params.polarizer45; | |
| pol45Check.style.cssText = 'accent-color:#ffaa00;cursor:pointer;'; | |
| var pol45Lbl = document.createElement('label'); | |
| pol45Lbl.textContent = '45° Polarizer'; | |
| pol45Lbl.style.cssText = 'white-space:nowrap;color:#ffcc44;cursor:pointer;font-size:12px;font-weight:500;'; | |
| pol45Lbl.addEventListener('click', function() { pol45Check.click(); }); | |
| pol45Group.appendChild(pol45Check); | |
| pol45Group.appendChild(pol45Lbl); | |
| pol45Group.style.display = (this.params.waveNature === 'light' && this.params.mode === 'double' && this.params.polarization === 'orthogonal') ? 'flex' : 'none'; | |
| // ---- Slit open/close checkboxes (only in double-slit mode) ---- | |
| var slitToggleGroup = document.createElement('div'); | |
| slitToggleGroup.style.cssText = 'display:flex;align-items:center;gap:10px;'; | |
| var topSlitCheck = document.createElement('input'); | |
| topSlitCheck.type = 'checkbox'; | |
| topSlitCheck.checked = this.params.topSlitOpen; | |
| topSlitCheck.style.cssText = 'accent-color:#ffcc00;cursor:pointer;'; | |
| var topSlitLbl = document.createElement('label'); | |
| topSlitLbl.textContent = 'Top Slit'; | |
| topSlitLbl.style.cssText = 'white-space:nowrap;color:#8899cc;cursor:pointer;font-size:11px;font-weight:500;'; | |
| topSlitLbl.addEventListener('click', function() { topSlitCheck.click(); }); | |
| var bottomSlitCheck = document.createElement('input'); | |
| bottomSlitCheck.type = 'checkbox'; | |
| bottomSlitCheck.checked = this.params.bottomSlitOpen; | |
| bottomSlitCheck.style.cssText = 'accent-color:#00cc44;cursor:pointer;'; | |
| var bottomSlitLbl = document.createElement('label'); | |
| bottomSlitLbl.textContent = 'Bottom Slit'; | |
| bottomSlitLbl.style.cssText = 'white-space:nowrap;color:#8899cc;cursor:pointer;font-size:11px;font-weight:500;'; | |
| bottomSlitLbl.addEventListener('click', function() { bottomSlitCheck.click(); }); | |
| slitToggleGroup.appendChild(topSlitCheck); | |
| slitToggleGroup.appendChild(topSlitLbl); | |
| slitToggleGroup.appendChild(bottomSlitCheck); | |
| slitToggleGroup.appendChild(bottomSlitLbl); | |
| slitToggleGroup.style.display = this.params.mode === 'double' ? 'flex' : 'none'; | |
| // ---- Slit width slider ---- | |
| var slitWidthCtrl = createSliderGroup('Slit Width:', 1, 600, 1, this.params.slitWidth); | |
| // ---- Slit separation slider (hidden in single mode) ---- | |
| var slitSepCtrl = createSliderGroup('Separation:', 10, 150, 1, this.params.slitSeparation); | |
| this._sepGroup = slitSepCtrl.group; | |
| slitSepCtrl.group.style.display = this.params.mode === 'double' ? 'flex' : 'none'; | |
| // ---- Wavelength slider ---- | |
| var wavelengthCtrl = createSliderGroup('Wavelength (λ):', 10, 40, 1, this.params.wavelength); | |
| // ---- Amplitude slider ---- | |
| var amplitudeCtrl = createSliderGroup('Amplitude:', 0.01, 5.0, 0.01, this.params.amplitude); | |
| // ---- Source width slider ---- | |
| var sourceWidthCtrl = createSliderGroup('Source Width:', 1, 600, 1, this.params.sourceWidth); | |
| // ---- Detector position slider ---- | |
| var detectorPosCtrl = createSliderGroup('Detector:', 50, 700, 1, this.params.detectorPos); | |
| // ---- Buttons ---- | |
| var btnGroup = document.createElement('div'); | |
| btnGroup.style.cssText = 'display:flex;align-items:center;gap:6px;margin-left:auto;'; | |
| var startBtn = createButton('▶ Start', 'linear-gradient(135deg, #2e8b57, #3cb371)'); | |
| var pauseBtn = createButton('⏸ Pause', 'linear-gradient(135deg, #cc7700, #e8a020)'); | |
| var resetBtn = createButton('↺ Reset', 'linear-gradient(135deg, #c62828, #e53935)'); | |
| var aboutBtn = createButton('ℹ About', 'linear-gradient(135deg, #4a5568, #6b7280)'); | |
| btnGroup.appendChild(startBtn); | |
| btnGroup.appendChild(pauseBtn); | |
| btnGroup.appendChild(resetBtn); | |
| btnGroup.appendChild(aboutBtn); | |
| // ---- Assemble sliders into wrapper ---- | |
| wrapper.appendChild(modeGroup); | |
| wrapper.appendChild(waveGroup); | |
| wrapper.appendChild(polGroup); | |
| wrapper.appendChild(pol45Group); | |
| wrapper.appendChild(slitToggleGroup); | |
| wrapper.appendChild(slitWidthCtrl.group); | |
| wrapper.appendChild(slitSepCtrl.group); | |
| wrapper.appendChild(wavelengthCtrl.group); | |
| wrapper.appendChild(amplitudeCtrl.group); | |
| wrapper.appendChild(sourceWidthCtrl.group); | |
| wrapper.appendChild(detectorPosCtrl.group); | |
| container.appendChild(wrapper); | |
| // ---- Put buttons in the always-visible buttons container ---- | |
| var btnContainer = document.getElementById('control-buttons'); | |
| if (btnContainer) { | |
| btnContainer.appendChild(startBtn); | |
| btnContainer.appendChild(pauseBtn); | |
| btnContainer.appendChild(resetBtn); | |
| btnContainer.appendChild(aboutBtn); | |
| } else { | |
| // Fallback: put buttons in the wrapper | |
| wrapper.appendChild(btnGroup); | |
| } | |
| // ---- Internal notify helper ---- | |
| function notify() { | |
| self.params = clampParams(self.params); | |
| if (self.onChange) self.onChange(self.getParams()); | |
| } | |
| // ---- Wire events ---- | |
| // Helper to update polarization visibility | |
| function updatePolVisibility() { | |
| var isLightDouble = (self.params.waveNature === 'light' && self.params.mode === 'double'); | |
| polGroup.style.display = isLightDouble ? 'flex' : 'none'; | |
| pol45Group.style.display = (isLightDouble && self.params.polarization === 'orthogonal') ? 'flex' : 'none'; | |
| slitToggleGroup.style.display = self.params.mode === 'double' ? 'flex' : 'none'; | |
| } | |
| // Mode toggle | |
| modeSelect.addEventListener('change', function () { | |
| self.params.mode = modeSelect.value; | |
| slitSepCtrl.group.style.display = modeSelect.value === 'double' ? 'flex' : 'none'; | |
| updatePolVisibility(); | |
| notify(); | |
| }); | |
| // Wave nature | |
| waveSelect.addEventListener('change', function () { | |
| self.params.waveNature = waveSelect.value; | |
| updatePolVisibility(); | |
| notify(); | |
| }); | |
| // Polarization | |
| polSelect.addEventListener('change', function () { | |
| self.params.polarization = polSelect.value; | |
| updatePolVisibility(); | |
| notify(); | |
| }); | |
| // 45° Polarizer | |
| pol45Check.addEventListener('change', function () { | |
| self.params.polarizer45 = pol45Check.checked; | |
| notify(); | |
| }); | |
| // Top slit toggle | |
| topSlitCheck.addEventListener('change', function () { | |
| self.params.topSlitOpen = topSlitCheck.checked; | |
| notify(); | |
| }); | |
| // Bottom slit toggle | |
| bottomSlitCheck.addEventListener('change', function () { | |
| self.params.bottomSlitOpen = bottomSlitCheck.checked; | |
| notify(); | |
| }); | |
| // Slit width | |
| slitWidthCtrl.input.addEventListener('input', function () { | |
| var v = parseInt(slitWidthCtrl.input.value, 10); | |
| self.params.slitWidth = Math.min(600, Math.max(1, v)); | |
| slitWidthCtrl.input.value = String(self.params.slitWidth); | |
| slitWidthCtrl.display.textContent = String(self.params.slitWidth); | |
| notify(); | |
| }); | |
| // Slit separation | |
| slitSepCtrl.input.addEventListener('input', function () { | |
| var v = parseInt(slitSepCtrl.input.value, 10); | |
| self.params.slitSeparation = Math.min(150, Math.max(10, v)); | |
| slitSepCtrl.input.value = String(self.params.slitSeparation); | |
| slitSepCtrl.display.textContent = String(self.params.slitSeparation); | |
| notify(); | |
| }); | |
| // Wavelength | |
| wavelengthCtrl.input.addEventListener('input', function () { | |
| var v = parseInt(wavelengthCtrl.input.value, 10); | |
| self.params.wavelength = Math.min(40, Math.max(10, v)); | |
| wavelengthCtrl.input.value = String(self.params.wavelength); | |
| wavelengthCtrl.display.textContent = String(self.params.wavelength); | |
| notify(); | |
| }); | |
| // Amplitude | |
| amplitudeCtrl.input.addEventListener('input', function () { | |
| var v = parseFloat(amplitudeCtrl.input.value); | |
| self.params.amplitude = Math.min(5.0, Math.max(0.01, v)); | |
| amplitudeCtrl.input.value = String(self.params.amplitude); | |
| amplitudeCtrl.display.textContent = self.params.amplitude.toFixed(2); | |
| notify(); | |
| }); | |
| // Source width | |
| sourceWidthCtrl.input.addEventListener('input', function () { | |
| var v = parseInt(sourceWidthCtrl.input.value, 10); | |
| self.params.sourceWidth = Math.min(600, Math.max(1, v)); | |
| sourceWidthCtrl.input.value = String(self.params.sourceWidth); | |
| sourceWidthCtrl.display.textContent = String(self.params.sourceWidth); | |
| notify(); | |
| }); | |
| // Detector position | |
| detectorPosCtrl.input.addEventListener('input', function () { | |
| var v = parseInt(detectorPosCtrl.input.value, 10); | |
| self.params.detectorPos = Math.min(700, Math.max(50, v)); | |
| detectorPosCtrl.input.value = String(self.params.detectorPos); | |
| detectorPosCtrl.display.textContent = String(self.params.detectorPos); | |
| notify(); | |
| }); | |
| // Start | |
| startBtn.addEventListener('click', function () { | |
| self.params.playing = true; | |
| notify(); | |
| }); | |
| // Pause | |
| pauseBtn.addEventListener('click', function () { | |
| self.params.playing = false; | |
| notify(); | |
| }); | |
| // Reset | |
| resetBtn.addEventListener('click', function () { | |
| if (self.onReset) self.onReset(); | |
| }); | |
| // About | |
| aboutBtn.addEventListener('click', function () { | |
| var overlay = document.getElementById('about-overlay'); | |
| if (overlay) overlay.style.display = 'flex'; | |
| }); | |
| } | |
| /** | |
| * Return a copy of the current parameters. | |
| * @returns {SimParams} | |
| */ | |
| getParams() { | |
| return Object.assign({}, this.params); | |
| } | |
| } | |
| window.ControlPanel = ControlPanel; | |
| window.clampParams = clampParams; | |