/** * main.js — Application entry point, animation loop, module coordination. * Loaded as a plain script after all other modules. */ (function () { 'use strict'; // Default simulation parameters var NX = 800; var NY = 600; var DX = 1.0; var DT = 0.4; var C = 1.0; var DAMPING_WIDTH = 50; var DEFAULT_WAVELENGTH = 20; var DEFAULT_SLIT_WIDTH = 40; // in grid cells var DEFAULT_SLIT_SEPARATION = 80; // in grid cells var BARRIER_COL = Math.floor(600 * 0.35); // 210 var DETECTOR_COL = Math.floor(600 * 0.55); // 330 — initial position var STEPS_PER_FRAME = 3; var HEIGHT_SCALE = 5.0; // Module instances var solver = null; var solver2 = null; // Second solver for orthogonal polarization (slit 2 only) var sceneManager = null; var waveSurface = null; var intensityPanel = null; var controlPanel = null; var useOrthogonalPol = false; var usePolarizer45 = false; var polarizerMesh = null; var POLARIZER_COL = 0; // Will be set between barrier and detector var slitFilterGroup = null; // Yellow/green filter planes at slits var currentAmplitude = 2.0; // Track current amplitude for element heights var colorGradient = null; var playing = false; /** * Build a BarrierConfig from the current control panel params. * @param {SimParams} params * @returns {BarrierConfig} */ function getElementHeight() { return currentAmplitude * HEIGHT_SCALE * 1.5 + 5; } function buildBarrierConfig(params) { var config = { mode: params.mode, barrierCol: BARRIER_COL, slitWidth: Math.round(params.slitWidth), slitSeparation: Math.round(params.slitSeparation), ny: NY }; // If in double-slit mode and one slit is closed, switch to single-slit // positioned at the open slit's location if (params.mode === 'double') { var topOpen = params.topSlitOpen !== false; var bottomOpen = params.bottomSlitOpen !== false; if (!topOpen && !bottomOpen) { // Both closed: full barrier (use single slit with width 0) config.mode = 'single'; config.slitWidth = 0; } else if (!topOpen) { // Only bottom slit open config.mode = 'single'; var centerY = Math.floor(NY / 2); var halfSep = Math.floor(config.slitSeparation / 2); config._slitCenter = centerY + halfSep; } else if (!bottomOpen) { // Only top slit open config.mode = 'single'; var centerY = Math.floor(NY / 2); var halfSep = Math.floor(config.slitSeparation / 2); config._slitCenter = centerY - halfSep; } } return config; } /** * Build a barrier config with only slit 1 open (for orthogonal polarization). */ function buildSlit1OnlyConfig(params) { var sw = Math.round(params.slitWidth); var sep = Math.round(params.slitSeparation); var centerY = Math.floor(NY / 2); var halfSep = Math.floor(sep / 2); // Slit 1 is at centerY - halfSep. Use single-slit mode centered there. return { mode: 'single', barrierCol: BARRIER_COL, slitWidth: sw, slitSeparation: 0, ny: NY, _slitCenter: centerY - halfSep }; } /** * Build a barrier config with only slit 2 open (for orthogonal polarization). */ function buildSlit2OnlyConfig(params) { var sw = Math.round(params.slitWidth); var sep = Math.round(params.slitSeparation); var centerY = Math.floor(NY / 2); var halfSep = Math.floor(sep / 2); return { mode: 'single', barrierCol: BARRIER_COL, slitWidth: sw, slitSeparation: 0, ny: NY, _slitCenter: centerY + halfSep }; } // Barrier meshes group — holds individual wall segments var barrierGroup = null; /** * Create or update the 3D barrier in the scene. * Renders individual red wall segments with gaps for slit openings, * so the user can clearly see where the slits are. * @param {BarrierConfig} config */ function updateBarrierMesh(config) { // Remove old barrier group if (barrierGroup && sceneManager && sceneManager.scene) { sceneManager.scene.remove(barrierGroup); barrierGroup.traverse(function (child) { if (child.geometry) child.geometry.dispose(); if (child.material) child.material.dispose(); }); } barrierGroup = new THREE.Group(); var barrierThickness = 2; var barrierHeight = getElementHeight(); var surfaceWidth = (NX - 1) * DX; var surfaceDepth = (NY - 1) * DX; var barrierX = -surfaceWidth / 2 + config.barrierCol * DX; var material = new THREE.MeshPhongMaterial({ color: 0xcc2222, specular: 0x441111, shininess: 40 }); // Compute wall segments (barrier regions) from the slit config var centerY = Math.floor(config.ny / 2); var halfSlit = Math.floor(config.slitWidth / 2); var segments = []; // each: { startZ, endZ } in grid coords if (config.mode === 'single') { var slitStart = centerY - halfSlit; var slitEnd = slitStart + config.slitWidth; // Wall below slit if (slitStart > 0) segments.push({ start: 0, end: slitStart }); // Wall above slit if (slitEnd < config.ny) segments.push({ start: slitEnd, end: config.ny }); } else { var halfSep = Math.floor(config.slitSeparation / 2); var slit1Start = (centerY - halfSep) - halfSlit; var slit1End = slit1Start + config.slitWidth; var slit2Start = (centerY + halfSep) - halfSlit; var slit2End = slit2Start + config.slitWidth; var topOpen = config.topSlitOpen !== false; var bottomOpen = config.bottomSlitOpen !== false; if (!topOpen && !bottomOpen) { // Both closed: full wall segments.push({ start: 0, end: config.ny }); } else if (!topOpen) { // Top slit closed: wall from 0 to slit2Start, then slit2End to ny if (slit2Start > 0) segments.push({ start: 0, end: slit2Start }); if (slit2End < config.ny) segments.push({ start: slit2End, end: config.ny }); } else if (!bottomOpen) { // Bottom slit closed: wall from 0 to slit1Start, then slit1End to ny if (slit1Start > 0) segments.push({ start: 0, end: slit1Start }); if (slit1End < config.ny) segments.push({ start: slit1End, end: config.ny }); } else { // Both open: normal double slit if (slit1Start > 0) segments.push({ start: 0, end: slit1Start }); if (slit1End < slit2Start) segments.push({ start: slit1End, end: slit2Start }); if (slit2End < config.ny) segments.push({ start: slit2End, end: config.ny }); } } // Create a box for each wall segment for (var i = 0; i < segments.length; i++) { var seg = segments[i]; var segLength = (seg.end - seg.start) * DX; var segCenter = ((seg.start + seg.end) / 2) * DX - surfaceDepth / 2; var geo = new THREE.BoxGeometry(barrierThickness, barrierHeight, segLength); var mesh = new THREE.Mesh(geo, material); mesh.position.set(barrierX, barrierHeight / 2, segCenter); barrierGroup.add(mesh); } if (sceneManager && sceneManager.scene) { sceneManager.scene.add(barrierGroup); } } /** * Create or update the 45° polarizer plane in the 3D scene. * Positioned halfway between barrier and detector. * @param {boolean} visible */ function updatePolarizerMesh(visible) { if (polarizerMesh && sceneManager && sceneManager.scene) { sceneManager.scene.remove(polarizerMesh); if (polarizerMesh.geometry) polarizerMesh.geometry.dispose(); if (polarizerMesh.material) polarizerMesh.material.dispose(); polarizerMesh = null; } if (!visible) return; POLARIZER_COL = Math.floor((BARRIER_COL + DETECTOR_COL) / 2); var surfaceWidth = (NX - 1) * DX; var surfaceDepth = (NY - 1) * DX; var worldX = -surfaceWidth / 2 + POLARIZER_COL * DX; var screenHeight = getElementHeight(); var geometry = new THREE.PlaneGeometry(surfaceDepth, screenHeight); var material = new THREE.MeshBasicMaterial({ color: 0x4488ff, transparent: true, opacity: 0.2, side: THREE.DoubleSide, depthWrite: false }); polarizerMesh = new THREE.Mesh(geometry, material); polarizerMesh.rotation.y = Math.PI / 2; polarizerMesh.position.set(worldX, screenHeight / 2 - 5, 0); if (sceneManager && sceneManager.scene) { sceneManager.scene.add(polarizerMesh); } } /** * Create or remove small colored filter planes at each slit opening. * Yellow for slit 1, green for slit 2. Only shown in orthogonal polarization mode. * @param {boolean} visible * @param {object} params - current sim params */ function updateSlitFilters(visible, params) { // Remove old if (slitFilterGroup && sceneManager && sceneManager.scene) { sceneManager.scene.remove(slitFilterGroup); slitFilterGroup.traverse(function (child) { if (child.geometry) child.geometry.dispose(); if (child.material) child.material.dispose(); }); slitFilterGroup = null; } if (!visible || !params) return; slitFilterGroup = new THREE.Group(); var sw = Math.round(params.slitWidth); var sep = Math.round(params.slitSeparation); var centerY = Math.floor(NY / 2); var halfSep = Math.floor(sep / 2); var halfSlit = Math.floor(sw / 2); var surfaceWidth = (NX - 1) * DX; var surfaceDepth = (NY - 1) * DX; var barrierX = -surfaceWidth / 2 + BARRIER_COL * DX; var filterOffset = 3; // slightly in front of barrier (toward source) var filterHeight = getElementHeight(); // Slit 1 center var slit1CenterY = centerY - halfSep; var slit1WorldZ = (slit1CenterY * DX) - surfaceDepth / 2; var filterDepth = sw * DX; // Yellow filter for slit 1 var geo1 = new THREE.PlaneGeometry(filterDepth, filterHeight); var mat1 = new THREE.MeshBasicMaterial({ color: 0xffcc00, transparent: true, opacity: 0.35, side: THREE.DoubleSide, depthWrite: false }); var filter1 = new THREE.Mesh(geo1, mat1); filter1.rotation.y = Math.PI / 2; filter1.position.set(barrierX - filterOffset, filterHeight / 2 - 3, slit1WorldZ); slitFilterGroup.add(filter1); // Slit 2 center var slit2CenterY = centerY + halfSep; var slit2WorldZ = (slit2CenterY * DX) - surfaceDepth / 2; // Green filter for slit 2 var geo2 = new THREE.PlaneGeometry(filterDepth, filterHeight); var mat2 = new THREE.MeshBasicMaterial({ color: 0x00cc44, transparent: true, opacity: 0.35, side: THREE.DoubleSide, depthWrite: false }); var filter2 = new THREE.Mesh(geo2, mat2); filter2.rotation.y = Math.PI / 2; filter2.position.set(barrierX - filterOffset, filterHeight / 2 - 3, slit2WorldZ); slitFilterGroup.add(filter2); if (sceneManager && sceneManager.scene) { sceneManager.scene.add(slitFilterGroup); } } /** * Initialise all modules, set default parameters, and start the animation loop. */ function init() { // 1. Get DOM containers var sceneContainer = document.getElementById('scene-container'); var intensityContainer = document.getElementById('intensity-container'); var controlContainer = document.getElementById('control-container'); // 2. Create FDTD solver solver = new FDTDSolver({ nx: NX, ny: NY, dx: DX, dt: DT, c: C, dampingWidth: DAMPING_WIDTH }); solver.wavelength = DEFAULT_WAVELENGTH; solver.sourceAmplitude = 2.0; solver.sourceWidth = 600; // 2b. Create second solver for orthogonal polarization mode solver2 = new FDTDSolver({ nx: NX, ny: NY, dx: DX, dt: DT, c: C, dampingWidth: DAMPING_WIDTH }); solver2.wavelength = DEFAULT_WAVELENGTH; solver2.sourceAmplitude = 2.0; solver2.sourceWidth = 600; // 3. Create and init SceneManager sceneManager = new SceneManager(); sceneManager.init(sceneContainer, { nx: NX, ny: NY, detectorCol: DETECTOR_COL }); // 4. Create and init WaveSurface, add mesh to scene waveSurface = new WaveSurface(); var surfaceMesh = waveSurface.init(NX, NY, DX); if (sceneManager.scene && surfaceMesh) { sceneManager.scene.add(surfaceMesh); } // 5. Create color gradient colorGradient = new ColorGradient(); // 6. Create and init IntensityPanel intensityPanel = new IntensityPanel(); intensityPanel.init(intensityContainer, NY); // 7. Create and init ControlPanel controlPanel = new ControlPanel(); var controlInner = document.getElementById('control-inner') || controlContainer; controlPanel.init(controlInner); // 8. Build initial barrier — default is double slit, light, orthogonal var defaultParams = { mode: 'double', slitWidth: DEFAULT_SLIT_WIDTH, slitSeparation: DEFAULT_SLIT_SEPARATION, wavelength: DEFAULT_WAVELENGTH, playing: false }; // Orthogonal polarization: set up two solvers with one slit each useOrthogonalPol = true; solver.setBarrier(buildSlit1OnlyConfig(defaultParams)); solver2.setBarrier(buildSlit2OnlyConfig(defaultParams)); // 9. Create 3D barrier mesh var initVisualConfig = { mode: 'double', barrierCol: BARRIER_COL, slitWidth: DEFAULT_SLIT_WIDTH, slitSeparation: DEFAULT_SLIT_SEPARATION, ny: NY, topSlitOpen: true, bottomSlitOpen: true }; updateBarrierMesh(initVisualConfig); // 9b. Show slit filter planes for orthogonal mode updateSlitFilters(true, defaultParams); // Track previous params to detect actual parameter changes vs just play/pause var prevSimParams = { mode: 'double', slitWidth: DEFAULT_SLIT_WIDTH, slitSeparation: DEFAULT_SLIT_SEPARATION, wavelength: DEFAULT_WAVELENGTH, amplitude: 2.0, sourceWidth: 600, detectorPos: DETECTOR_COL, waveNature: 'light', polarization: 'orthogonal' }; // 10. Wire controlPanel.onChange controlPanel.onChange = function (params) { // Update solver wavelength and amplitude solver.wavelength = params.wavelength; solver.sourceAmplitude = params.amplitude; solver.sourceWidth = params.sourceWidth; solver2.wavelength = params.wavelength; solver2.sourceAmplitude = params.amplitude; solver2.sourceWidth = params.sourceWidth; // Track current amplitude for element heights currentAmplitude = params.amplitude; // Determine if orthogonal polarization mode useOrthogonalPol = (params.waveNature === 'light' && params.mode === 'double' && params.polarization === 'orthogonal'); usePolarizer45 = useOrthogonalPol && params.polarizer45; // Update 45° polarizer plane visibility updatePolarizerMesh(usePolarizer45); // Update slit filter planes (yellow/green) for orthogonal mode updateSlitFilters(useOrthogonalPol, params); // Update detector screen height if (sceneManager) { sceneManager.updateDetectorHeight(getElementHeight()); } // Update detector position if (params.detectorPos != null) { DETECTOR_COL = Math.max(1, Math.min(NX - 1, params.detectorPos)); if (sceneManager && sceneManager.detectorMarker) { var surfaceWidth = (NX - 1) * DX; var worldX = -surfaceWidth / 2 + DETECTOR_COL * DX; sceneManager.detectorMarker.position.x = worldX; } } // Check if polarization or wave nature changed (needs full solver reset) var polChanged = ( params.waveNature !== prevSimParams.waveNature || params.polarization !== prevSimParams.polarization || params.mode !== prevSimParams.mode ); // Check if simulation parameters changed (not just play/pause) var simChanged = ( polChanged || params.slitWidth !== prevSimParams.slitWidth || params.slitSeparation !== prevSimParams.slitSeparation || params.wavelength !== prevSimParams.wavelength || params.sourceWidth !== prevSimParams.sourceWidth ); // Set up barriers based on polarization mode if (useOrthogonalPol) { // Two independent solvers, each with one slit var topOpen = params.topSlitOpen !== false; var bottomOpen = params.bottomSlitOpen !== false; if (topOpen) { solver.setBarrier(buildSlit1OnlyConfig(params)); } else { // Close slit 1: full barrier solver.setBarrier({ mode: 'single', barrierCol: BARRIER_COL, slitWidth: 0, slitSeparation: 0, ny: NY }); } if (bottomOpen) { solver2.setBarrier(buildSlit2OnlyConfig(params)); } else { solver2.setBarrier({ mode: 'single', barrierCol: BARRIER_COL, slitWidth: 0, slitSeparation: 0, ny: NY }); } } else { // Single solver with both slits var newBarrierConfig = buildBarrierConfig(params); solver.setBarrier(newBarrierConfig); } if (simChanged) { // Reset solvers when polarization/mode changes so wave field restarts clean if (polChanged) { solver.reset(); solver2.reset(); } intensityPanel.reset(NY); prevSimParams = Object.assign({}, params); } // Update barrier mesh — always show double-slit visual with open/closed state var visualBarrierConfig = { mode: params.mode, barrierCol: BARRIER_COL, slitWidth: Math.round(params.slitWidth), slitSeparation: Math.round(params.slitSeparation), ny: NY, topSlitOpen: params.topSlitOpen !== false, bottomSlitOpen: params.bottomSlitOpen !== false }; updateBarrierMesh(visualBarrierConfig); // Update playing state playing = params.playing; solver.paused = !playing; solver2.paused = !playing; }; // 11. Wire controlPanel.onReset controlPanel.onReset = function () { solver.reset(); solver2.reset(); intensityPanel.reset(NY); if (sceneManager) { sceneManager.resetCamera(); } playing = false; solver.paused = true; controlPanel.params.playing = false; }; // 12. Wire window resize handler (RAF-throttled) var resizePending = false; window.addEventListener('resize', function () { if (resizePending) return; resizePending = true; requestAnimationFrame(function () { resizePending = false; var w = Math.max(sceneContainer.clientWidth, 1); var h = Math.max(sceneContainer.clientHeight, 1); if (sceneManager) { sceneManager.resize(w, h); } if (intensityPanel) { intensityPanel.resize(); } }); }); // 13. Wire view buttons var btnTopView = document.getElementById('btn-top-view'); var btnAngleView = document.getElementById('btn-angle-view'); var btn135View = document.getElementById('btn-135-view'); if (btnTopView && btnAngleView && btn135View) { var allBtns = [btnTopView, btnAngleView, btn135View]; function setActiveBtn(active) { allBtns.forEach(function(b) { b.style.background = 'rgba(255,255,255,0.05)'; b.style.color = '#8899bb'; }); active.style.background = 'rgba(100,140,255,0.2)'; active.style.color = '#c0ccff'; } btnTopView.addEventListener('click', function () { sceneManager.setBottomView('top'); setActiveBtn(btnTopView); }); btnAngleView.addEventListener('click', function () { sceneManager.setBottomView('angle'); setActiveBtn(btnAngleView); }); btn135View.addEventListener('click', function () { sceneManager.setBottomView('135'); setActiveBtn(btn135View); }); } // 14. Start animation loop animate(); } /** * requestAnimationFrame loop — steps the solver, updates visuals, renders. */ function animate() { requestAnimationFrame(animate); if (!solver || !waveSurface || !sceneManager) return; // Step the solver(s) if playing if (playing) { solver.step(STEPS_PER_FRAME); if (useOrthogonalPol) { solver2.step(STEPS_PER_FRAME); } } // Get current amplitude var amplitude; if (useOrthogonalPol) { // Orthogonal polarization: sum intensities (not amplitudes) // For display, show sqrt(a1² + a2²) with sign of the larger component var a1 = solver.getAmplitude(); var a2 = solver2.getAmplitude(); var combined = new Float32Array(a1.length); for (var i = 0; i < a1.length; i++) { combined[i] = a1[i] + a2[i]; // For visual display, sum amplitudes } amplitude = combined; } else { amplitude = solver.getAmplitude(); } // Update wave surface visuals waveSurface.updateHeights(amplitude, HEIGHT_SCALE); if (useOrthogonalPol) { // Custom coloring: yellow for slit 1, green for slit 2 waveSurface.updateColorsOrthogonal(solver.getAmplitude(), solver2.getAmplitude()); } else { waveSurface.updateColors(amplitude, colorGradient); } // Sample intensity if playing if (playing) { if (useOrthogonalPol) { var a1 = solver.getAmplitude(); var a2 = solver2.getAmplitude(); var alpha = 0.003; for (var y = 0; y < NY; y++) { var v1 = a1[y * NX + DETECTOR_COL]; var v2 = a2[y * NX + DETECTOR_COL]; var intensity; if (usePolarizer45) { var projected = (v1 + v2) / Math.SQRT2; intensity = projected * projected; intensityPanel.lastAmplitude[y] = projected; } else { intensity = v1 * v1 + v2 * v2; intensityPanel.lastAmplitude[y] = v1 + v2; } intensityPanel.accumulatedIntensity[y] = intensityPanel.accumulatedIntensity[y] * (1 - alpha) + intensity * alpha; } intensityPanel.sampleCount++; } else { intensityPanel.sample(amplitude, DETECTOR_COL, NX); } } // Draw intensity panel intensityPanel.draw(); // Render 3D scene sceneManager.render(); } // When loaded dynamically after the ES module bootstrap, DOMContentLoaded // has already fired. Check readyState and call init immediately if ready. if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();