AK51's picture
Upload 8 files
0905839 verified
/**
* 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();
}
})();