3D_slit_simulation / controls.js
AK51's picture
Upload 8 files
0905839 verified
/**
* 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;