/** * Application Controller * Coordinates all subsystems and manages application state */ class ApplicationController { constructor() { this.quantumEngine = null; this.particleSampler = null; this.renderer = null; this.guiController = null; this.configuration = null; this.isMobile = this.detectMobile(); // Adjust default particle count for mobile devices const defaultParticleCount = this.isMobile ? 30000 : CONSTANTS.DEFAULT_PARTICLE_COUNT; // Application state this.state = { atomicNumber: 1, elementSymbol: 'H', displaySettings: { particleCount: defaultParticleCount, particleSize: CONSTANTS.PARTICLE_SIZE, opacity: CONSTANTS.PARTICLE_OPACITY, scale: 1.0, // Scale factor for atom size (0.1 to 3.0) animationEnabled: true, // Animation on by default animationSpeed: CONSTANTS.PARTICLE_ANIMATION_SPEED, // Animation speed multiplier clippingEnabled: false, // Clipping off by default xClipPosition: 0, // X-axis clipping position yClipPosition: 0, // Y-axis clipping position zClipPosition: 0, // Z-axis clipping position hueGradientEnabled: false, // Color hue gradient off by default hueGradientIntensity: 120 // Hue shift amount in degrees (0-360) }, cameraSettings: { autoRotate: false, rotationSpeed: 0.2, rotationAxis: 'y' // 'x', 'y', or 'z' } }; } /** * Detect if device is mobile * @returns {boolean} */ detectMobile() { return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || window.innerWidth <= 768; } /** * Initialize all subsystems * @param {HTMLCanvasElement} canvas * @returns {Promise} */ async initialize(canvas) { try { console.log('AppController: Checking WebGL support...'); // Check WebGL support if (!this.checkWebGLSupport()) { throw new Error('WebGL is not supported in your browser. Please use a modern browser like Chrome, Firefox, or Edge.'); } console.log('AppController: WebGL supported'); console.log('AppController: Initializing quantum mechanics engine...'); // Initialize quantum mechanics engine this.quantumEngine = new QuantumMechanicsEngine(); this.quantumEngine.setAtomicNumber(this.state.atomicNumber); console.log('AppController: Initializing particle sampler...'); // Initialize particle sampler this.particleSampler = new ParticleSampler(this.quantumEngine); console.log('AppController: Initializing renderer...'); // Initialize renderer this.renderer = new VisualizationRenderer(canvas); this.renderer.initialize(); console.log('AppController: Initializing GUI...'); // Initialize GUI this.guiController = new GUIController(this); this.guiController.initialize(); console.log('AppController: Generating initial visualization...'); // Generate initial visualization await this.setAtomicNumber(this.state.atomicNumber); // Enable animation by default this.renderer.setAnimationEnabled(this.state.displaySettings.animationEnabled); this.renderer.setAnimationSpeed(this.state.displaySettings.animationSpeed); console.log('AppController: Starting animation loop...'); // Start animation loop this.renderer.animate(); console.log('AppController: Initialization complete!'); return true; } catch (error) { console.error('Initialization error:', error); throw error; } } /** * Check WebGL support * @returns {boolean} */ checkWebGLSupport() { try { const canvas = document.createElement('canvas'); const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); return !!gl; } catch (e) { return false; } } /** * Set atomic number and update visualization * @param {number} atomicNumber */ async setAtomicNumber(atomicNumber) { console.log('setAtomicNumber called with:', atomicNumber); // Clamp atomic number to valid range const clampedZ = Math.max(1, Math.min(CONSTANTS.MAX_ATOMIC_NUMBER, Math.floor(atomicNumber))); if (clampedZ !== atomicNumber) { console.warn(`Atomic number ${atomicNumber} out of range, clamped to ${clampedZ}`); } console.log('Setting atomic number to:', clampedZ); this.state.atomicNumber = clampedZ; this.quantumEngine.setAtomicNumber(clampedZ); // Build electron configuration console.log('Building electron configuration...'); this.configuration = new ElectronConfiguration(clampedZ); this.configuration.build(); // Find the outermost orbital (highest n, then highest l, then highest m) let outermostOrbital = null; let maxN = 0; let maxL = -1; let maxM = -999; this.configuration.orbitals.forEach(orbital => { if (orbital.electrons > 0) { // Only consider orbitals with electrons if (orbital.n > maxN || (orbital.n === maxN && orbital.l > maxL) || (orbital.n === maxN && orbital.l === maxL && orbital.m > maxM)) { maxN = orbital.n; maxL = orbital.l; maxM = orbital.m; outermostOrbital = orbital; } } }); // Deselect all orbitals, then select only the outermost one this.configuration.orbitals.forEach(orbital => { orbital.visible = false; }); if (outermostOrbital) { outermostOrbital.visible = true; console.log(`Outermost orbital selected: ${outermostOrbital.getDesignation()} (m=${outermostOrbital.m})`); } console.log('Configuration built:', this.configuration.elementSymbol, 'with', this.configuration.orbitals.length, 'orbitals'); this.state.elementSymbol = this.configuration.elementSymbol; // Update GUI if (this.guiController) { console.log('Updating GUI...'); this.guiController.updateElementInfo(clampedZ); this.guiController.createOrbitalControls(this.configuration.orbitals); } // Update visualization console.log('Updating visualization...'); await this.updateVisualization(); console.log('setAtomicNumber complete'); } /** * Update visualization with current configuration */ async updateVisualization() { if (!this.configuration) return; console.log('Updating visualization...'); // Show loading spinner this.showLoadingSpinner(true, 'Generating electron cloud...'); // Clear existing orbitals this.renderer.clearAllOrbitals(); // Filter orbitals: show ALL visible orbitals to get proper 3D shapes // This includes empty orbitals (0 electrons) to show complete dumbbell/cloverleaf shapes const visibleOrbitals = this.configuration.orbitals.filter( orbital => orbital.visible ); console.log(`Visualizing ${visibleOrbitals.length} orbitals (including empty ones for proper 3D shape)`); const totalParticleCount = this.state.displaySettings.particleCount; // Progressive loading: start with fewer particles, then add more const progressiveLevels = [0.3, 0.6, 1.0]; // 30%, 60%, 100% for (let levelIndex = 0; levelIndex < progressiveLevels.length; levelIndex++) { const level = progressiveLevels[levelIndex]; const currentTotalParticles = Math.floor(totalParticleCount * level); // Distribute particles evenly across visible orbitals const particleCountPerOrbital = visibleOrbitals.length > 0 ? Math.floor(currentTotalParticles / visibleOrbitals.length) : currentTotalParticles; console.log(`Progressive level ${levelIndex + 1}/${progressiveLevels.length}: ${Math.round(level * 100)}% - ${particleCountPerOrbital} particles per orbital`); // Clear previous level's particles before adding new level if (levelIndex > 0) { this.renderer.clearAllOrbitals(); } // Generate particles for EACH INDIVIDUAL ORBITAL separately for (let i = 0; i < visibleOrbitals.length; i++) { const orbital = visibleOrbitals[i]; try { // Update loading message this.updateLoadingMessage(`Generating ${orbital.getDesignation()} orbital... ${Math.round(level * 100)}% (${i + 1}/${visibleOrbitals.length})`); // Use distributed particle count // Empty orbitals (0 electrons) get 30% of the particle count for fainter appearance const orbitalParticleCount = orbital.electrons > 0 ? particleCountPerOrbital : Math.floor(particleCountPerOrbital * 0.3); const particles = this.particleSampler.generateOrbitalParticles( orbital.n, orbital.l, orbital.m, orbitalParticleCount ); // Create particle system for this orbital with current scale and custom color this.renderer.createOrbitalParticles( orbital.getId(), particles, orbital.getType(), this.state.displaySettings.scale, orbital.color, // Pass custom color (null if not set) this.state.displaySettings.hueGradientEnabled, // Pass hue gradient flag this.state.displaySettings.hueGradientIntensity // Pass hue gradient intensity ); } catch (error) { console.error(`Error generating orbital ${orbital.getId()}:`, error); } } // Hide loading spinner after first level to show progressive rendering if (levelIndex === 0) { this.showLoadingSpinner(false); } // Delay between levels to show progressive rendering (longer delay for visibility) if (levelIndex < progressiveLevels.length - 1) { await new Promise(resolve => setTimeout(resolve, 200)); } } // Ensure loading spinner is hidden this.showLoadingSpinner(false); // Adjust camera to fit visible orbitals if (visibleOrbitals.length > 0) { // Find the largest n value among visible orbitals const maxN = Math.max(...visibleOrbitals.map(o => o.n)); this.adjustCameraForOrbital(maxN); // Adjust particle size based on orbital size // For orbitals larger than 1s, use larger particle size for better visibility const hasLargeOrbital = visibleOrbitals.some(o => o.n > 1 || o.l > 0); if (hasLargeOrbital) { this.state.displaySettings.particleSize = 0.3; this.updateParticleSize(0.3); } else { // Reset to default for 1s this.state.displaySettings.particleSize = CONSTANTS.PARTICLE_SIZE; this.updateParticleSize(CONSTANTS.PARTICLE_SIZE); } } console.log('Visualization update complete'); } /** * Show/hide loading spinner * @param {boolean} show * @param {string} message - Loading message */ showLoadingSpinner(show, message = 'Loading...') { const loadingOverlay = document.getElementById('loading-overlay'); const loadingText = document.getElementById('loading-text'); if (show) { loadingOverlay.style.display = 'flex'; if (loadingText) loadingText.textContent = message; } else { loadingOverlay.style.display = 'none'; } } /** * Update loading message * @param {string} message - Loading message */ updateLoadingMessage(message) { const loadingText = document.getElementById('loading-text'); if (loadingText) { loadingText.textContent = message; } } /** * Visualize a single orbital with specific quantum numbers * @param {number} n - Principal quantum number * @param {number} l - Azimuthal quantum number * @param {number} m - Magnetic quantum number */ async visualizeSingleOrbital(n, l, m) { console.log(`Visualizing single orbital: n=${n}, l=${l}, m=${m}`); // Validate quantum numbers if (l >= n) { throw new Error(`Invalid quantum numbers: l (${l}) must be less than n (${n})`); } if (Math.abs(m) > l) { throw new Error(`Invalid quantum numbers: |m| (${Math.abs(m)}) must be ≤ l (${l})`); } // Clear existing orbitals this.renderer.clearAllOrbitals(); // Create a single orbital object const orbital = new Orbital(n, l, m); orbital.electrons = 1; // Treat as if it has one electron for visualization orbital.visible = true; console.log(`Created orbital: ${orbital.getDesignation()} (${orbital.getId()})`); // Generate particles for this orbital const particleCount = this.state.displaySettings.particleCount; const particles = this.particleSampler.generateOrbitalParticles(n, l, m, particleCount); console.log(`Generated ${particles.length} particles`); // Create particle system with current scale this.renderer.createOrbitalParticles( orbital.getId(), particles, orbital.getType(), this.state.displaySettings.scale ); // Update GUI to show only this orbital if (this.guiController) { this.guiController.createOrbitalControls([orbital]); } console.log('Single orbital visualization complete'); } /** * Visualize a single orbital with specific quantum numbers * @param {number} n - Principal quantum number * @param {number} l - Azimuthal quantum number * @param {number} m - Magnetic quantum number */ async visualizeSingleOrbital(n, l, m) { console.log(`Visualizing single orbital: n=${n}, l=${l}, m=${m}`); // Validate quantum numbers if (l >= n) { throw new Error(`Invalid quantum numbers: l (${l}) must be less than n (${n})`); } if (Math.abs(m) > l) { throw new Error(`Invalid quantum numbers: |m| (${Math.abs(m)}) must be ≤ l (${l})`); } // Clear existing orbitals this.renderer.clearAllOrbitals(); // Create a single orbital object const orbital = new Orbital(n, l, m); orbital.electrons = 1; // Treat as if it has one electron for visualization orbital.visible = true; console.log(`Created orbital: ${orbital.getDesignation()} (${orbital.getId()})`); // Generate particles for this orbital const particleCount = this.state.displaySettings.particleCount; const particles = this.particleSampler.generateOrbitalParticles(n, l, m, particleCount); console.log(`Generated ${particles.length} particles`); // Create particle system this.renderer.createOrbitalParticles( orbital.getId(), particles, orbital.getType() ); // Update GUI to show only this orbital if (this.guiController) { this.guiController.createOrbitalControls([orbital]); } console.log('Single orbital visualization complete'); } /** * Set orbital visibility * @param {number} n * @param {number} l * @param {number} m * @param {boolean} visible */ setOrbitalVisibility(n, l, m, visible) { if (!this.configuration) return; const orbital = this.configuration.orbitals.find( o => o.n === n && o.l === l && o.m === m ); if (orbital) { orbital.visible = visible; this.updateVisualization(); } } /** * Reset camera to default position */ resetCamera() { if (this.renderer && this.renderer.camera) { this.renderer.camera.position.set(8, 8, 8); this.renderer.camera.lookAt(0, 0, 0); if (this.renderer.controls) { this.renderer.controls.reset(); } } } /** * Adjust camera distance based on orbital size * @param {number} n - Principal quantum number of the outermost orbital */ adjustCameraForOrbital(n) { if (!this.renderer || !this.renderer.camera) return; // Calculate camera distance based on principal quantum number // Larger n values need more distance to fit in view // Use n² scaling with increased multiplier for very large orbitals const baseDistance = 10; const scaleFactor = Math.max(1, (n * n) / 2); // Increased from /4 to /2 for more zoom out const distance = baseDistance * scaleFactor; console.log(`Adjusting camera for n=${n}: distance=${distance.toFixed(2)}`); // Set camera position maintaining the 45-degree angle view const angle = Math.PI / 4; // 45 degrees const x = distance * Math.cos(angle); const y = distance * Math.cos(angle); const z = distance * Math.cos(angle); this.renderer.camera.position.set(x, y, z); this.renderer.camera.lookAt(0, 0, 0); // Update controls target if (this.renderer.controls) { this.renderer.controls.target.set(0, 0, 0); this.renderer.controls.update(); } } /** * Set auto-rotate * @param {boolean} enabled */ setAutoRotate(enabled) { this.state.cameraSettings.autoRotate = enabled; if (this.renderer) { this.renderer.setAutoRotate(enabled, this.state.cameraSettings.rotationAxis); } } /** * Set rotation speed * @param {number} speed */ setRotationSpeed(speed) { this.state.cameraSettings.rotationSpeed = speed; if (this.renderer) { this.renderer.setRotationSpeed(speed); } } /** * Set rotation axis * @param {string} axis - 'x', 'y', or 'z' */ setRotationAxis(axis) { this.state.cameraSettings.rotationAxis = axis; if (this.renderer && this.state.cameraSettings.autoRotate) { this.renderer.setAutoRotate(true, axis); } } /** * Set atom scale * @param {number} scale - Scale factor (0.1 to 3.0) */ setScale(scale) { this.state.displaySettings.scale = scale; // Apply scale to all orbital particle systems if (this.renderer && this.renderer.orbitals) { this.renderer.orbitals.forEach((particleSystem) => { particleSystem.scale.set(scale, scale, scale); }); } } /** * Update particle size for all orbitals * @param {number} size - Particle size */ updateParticleSize(size) { if (this.renderer && this.renderer.orbitals) { this.renderer.orbitals.forEach((particleSystem) => { particleSystem.material.size = size; }); } } /** * Set axis visibility * @param {string} axis - 'x', 'y', or 'z' * @param {boolean} visible - visibility state */ setAxisVisibility(axis, visible) { if (this.renderer) { this.renderer.setAxisVisibility(axis, visible); } } /** * Set particle animation speed * @param {number} speed - Animation speed multiplier */ setAnimationSpeed(speed) { this.state.displaySettings.animationSpeed = speed; if (this.renderer) { this.renderer.setAnimationSpeed(speed); } } /** * Enable/disable particle animation * @param {boolean} enabled - Animation enabled state */ setAnimationEnabled(enabled) { this.state.displaySettings.animationEnabled = enabled; if (this.renderer) { this.renderer.setAnimationEnabled(enabled); } } /** * Enable/disable clipping plane * @param {boolean} enabled - Clipping enabled state */ setClippingEnabled(enabled) { this.state.displaySettings.clippingEnabled = enabled; // Reset all clip positions to 0 when disabling if (!enabled) { this.state.displaySettings.xClipPosition = 0; this.state.displaySettings.yClipPosition = 0; this.state.displaySettings.zClipPosition = 0; if (this.renderer) { this.renderer.setClipPosition('x', 0); this.renderer.setClipPosition('y', 0); this.renderer.setClipPosition('z', 0); } // Update GUI sliders to show 0 if (this.guiController) { this.guiController.updateClipPositions(0, 0, 0); } } if (this.renderer) { this.renderer.setClippingEnabled(enabled); } } /** * Set X-axis clipping position * @param {number} position - Position along X-axis */ setXClipPosition(position) { this.state.displaySettings.xClipPosition = position; if (this.renderer) { this.renderer.setClipPosition('x', position); } } /** * Set Y-axis clipping position * @param {number} position - Position along Y-axis */ setYClipPosition(position) { this.state.displaySettings.yClipPosition = position; if (this.renderer) { this.renderer.setClipPosition('y', position); } } /** * Set Z-axis clipping position * @param {number} position - Position along Z-axis */ setZClipPosition(position) { this.state.displaySettings.zClipPosition = position; if (this.renderer) { this.renderer.setClipPosition('z', position); } } /** * Enable/disable color hue gradient * @param {boolean} enabled - Hue gradient enabled state */ setHueGradientEnabled(enabled) { this.state.displaySettings.hueGradientEnabled = enabled; // Regenerate visualization with new color mode this.updateVisualization(); } /** * Set hue gradient intensity * @param {number} intensity - Hue shift amount in degrees (0-360) */ setHueGradientIntensity(intensity) { this.state.displaySettings.hueGradientIntensity = intensity; // Regenerate visualization with new gradient intensity this.updateVisualization(); } /** * Get current state * @returns {Object} */ getState() { return { ...this.state, configuration: this.configuration, fps: this.renderer ? this.renderer.getFPS() : 0 }; } /** * Reset to default state */ async reset() { this.state.atomicNumber = 1; this.state.displaySettings = { particleCount: CONSTANTS.DEFAULT_PARTICLE_COUNT, particleSize: CONSTANTS.PARTICLE_SIZE, opacity: CONSTANTS.PARTICLE_OPACITY }; this.state.cameraSettings = { autoRotate: false, rotationSpeed: 0.2, rotationAxis: 'y' }; await this.setAtomicNumber(1); this.resetCamera(); } /** * Dispose of all resources */ dispose() { if (this.renderer) { this.renderer.dispose(); } if (this.guiController) { this.guiController.destroy(); } } } // Make class available globally window.ApplicationController = ApplicationController;