/** * 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;