'use strict'; /* ═══════════════════════════════════════════════ OrbitalsSim — 3D molecular orbitals (Three.js) s, p, d orbitals + H₂ / H₂O molecular bonding ═══════════════════════════════════════════════ */ class OrbitalsSim { constructor(container) { this.container = container; this._running = false; /* Three.js */ this.scene = new THREE.Scene(); this.camera = new THREE.PerspectiveCamera(50, 1, 0.1, 200); this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); this.renderer.setClearColor(0x0D0D1A, 1); container.appendChild(this.renderer.domElement); /* lighting */ this.scene.add(new THREE.AmbientLight(0xffffff, 0.45)); const dir = new THREE.DirectionalLight(0xffffff, 0.7); dir.position.set(5, 8, 6); this.scene.add(dir); const pt = new THREE.PointLight(0x9B5DE5, 0.3, 50); pt.position.set(-4, 3, 5); this.scene.add(pt); this.camera.position.set(6, 4, 6); this.camera.lookAt(0, 0, 0); /* orbit controls (manual) */ this._drag = false; this._prevX = 0; this._prevY = 0; this._rotY = 0.6; this._rotX = 0.3; this._dist = 8; this._autoSpin = true; const el = this.renderer.domElement; el.style.cursor = 'grab'; el.addEventListener('pointerdown', e => { this._drag = true; this._prevX = e.clientX; this._prevY = e.clientY; this._autoSpin = false; el.style.cursor = 'grabbing'; }); window.addEventListener('pointerup', () => { this._drag = false; el.style.cursor = 'grab'; }); window.addEventListener('pointermove', e => { if (!this._drag) return; this._rotY += (e.clientX - this._prevX) * 0.008; this._rotX += (e.clientY - this._prevY) * 0.008; this._rotX = Math.max(-1.4, Math.min(1.4, this._rotX)); this._prevX = e.clientX; this._prevY = e.clientY; }); el.addEventListener('wheel', e => { e.preventDefault(); this._dist = Math.max(3, Math.min(20, this._dist + e.deltaY * 0.02)); }, { passive: false }); /* touch */ el.addEventListener('touchstart', e => { if (e.touches.length === 1) { this._drag = true; this._prevX = e.touches[0].clientX; this._prevY = e.touches[0].clientY; this._autoSpin = false; } }, { passive: true }); el.addEventListener('touchmove', e => { if (!this._drag || e.touches.length !== 1) return; const t = e.touches[0]; this._rotY += (t.clientX - this._prevX) * 0.008; this._rotX += (t.clientY - this._prevY) * 0.008; this._rotX = Math.max(-1.4, Math.min(1.4, this._rotX)); this._prevX = t.clientX; this._prevY = t.clientY; }, { passive: true }); el.addEventListener('touchend', () => { this._drag = false; }); /* resize */ this._ro = new ResizeObserver(() => this.fit()); this._ro.observe(container); /* state */ this._mode = 's'; this._group = new THREE.Group(); this.scene.add(this._group); this._buildOrbital('s'); this.fit(); this.play(); } /* ── public ── */ setMode(mode) { this._mode = mode; if (window.LabFX) LabFX.sound.play('whoosh', { pitch: 1.1, volume: 0.3 }); this._buildOrbital(mode); } fit() { const w = this.container.clientWidth || 600; const h = this.container.clientHeight || 400; this.camera.aspect = w / h; this.camera.updateProjectionMatrix(); this.renderer.setSize(w, h); } play() { if (!this._running) { this._running = true; this._loop(); } } stop() { this._running = false; } pause() { this._running = false; } /* ── clear scene group ── */ _clear() { while (this._group.children.length) { const c = this._group.children[0]; if (c.geometry) c.geometry.dispose(); if (c.material) { if (Array.isArray(c.material)) c.material.forEach(m => m.dispose()); else c.material.dispose(); } this._group.remove(c); } } /* ── orbital builders ── */ _buildOrbital(mode) { this._clear(); const b = { s: () => this._buildS(), p: () => this._buildP(), d: () => this._buildD(), h2: () => this._buildH2(), h2o: () => this._buildH2O(), }; (b[mode] || b.s)(); } /* nucleus dot */ _nucleus(pos, color = 0xffffff) { const geo = new THREE.SphereGeometry(0.12, 16, 16); const mat = new THREE.MeshStandardMaterial({ color, emissive: color, emissiveIntensity: 0.5 }); const m = new THREE.Mesh(geo, mat); m.position.copy(pos); this._group.add(m); return m; } /* cloud lobe (ellipsoid) */ _lobe(pos, scale, color, opacity = 0.3) { const geo = new THREE.SphereGeometry(1, 32, 32); const mat = new THREE.MeshPhysicalMaterial({ color, transparent: true, opacity, metalness: 0, roughness: 0.6, clearcoat: 0.3, side: THREE.DoubleSide, depthWrite: false, }); const mesh = new THREE.Mesh(geo, mat); mesh.position.copy(pos); mesh.scale.set(scale.x, scale.y, scale.z); this._group.add(mesh); return mesh; } /* particle cloud (points) */ _cloud(center, radius, count, color) { const positions = new Float32Array(count * 3); for (let i = 0; i < count; i++) { // gaussian distribution const r = radius * Math.pow(Math.random(), 0.33); const theta = Math.random() * Math.PI * 2; const phi = Math.acos(2 * Math.random() - 1); positions[i * 3] = center.x + r * Math.sin(phi) * Math.cos(theta); positions[i * 3 + 1] = center.y + r * Math.sin(phi) * Math.sin(theta); positions[i * 3 + 2] = center.z + r * Math.cos(phi); } const geo = new THREE.BufferGeometry(); geo.setAttribute('position', new THREE.BufferAttribute(positions, 3)); const mat = new THREE.PointsMaterial({ color, size: 0.04, transparent: true, opacity: 0.6, depthWrite: false }); const pts = new THREE.Points(geo, mat); this._group.add(pts); return pts; } /* ── s orbital: spherical ── */ _buildS() { this._nucleus(new THREE.Vector3(0, 0, 0)); this._lobe(new THREE.Vector3(0, 0, 0), new THREE.Vector3(1.5, 1.5, 1.5), 0x9B5DE5, 0.18); this._cloud(new THREE.Vector3(0, 0, 0), 1.5, 2000, 0x9B5DE5); } /* ── p orbitals: 3 dumbbell shapes ── */ _buildP() { this._nucleus(new THREE.Vector3(0, 0, 0)); // px (red) this._lobe(new THREE.Vector3(1.2, 0, 0), new THREE.Vector3(0.7, 0.5, 0.5), 0xF15BB5, 0.25); this._lobe(new THREE.Vector3(-1.2, 0, 0), new THREE.Vector3(0.7, 0.5, 0.5), 0xF15BB5, 0.25); // py (green) this._lobe(new THREE.Vector3(0, 1.2, 0), new THREE.Vector3(0.5, 0.7, 0.5), 0x34d399, 0.25); this._lobe(new THREE.Vector3(0, -1.2, 0), new THREE.Vector3(0.5, 0.7, 0.5), 0x34d399, 0.25); // pz (blue) this._lobe(new THREE.Vector3(0, 0, 1.2), new THREE.Vector3(0.5, 0.5, 0.7), 0x60a5fa, 0.25); this._lobe(new THREE.Vector3(0, 0, -1.2), new THREE.Vector3(0.5, 0.5, 0.7), 0x60a5fa, 0.25); // axis labels this._addLabel('px', 2, 0, 0, 0xF15BB5); this._addLabel('py', 0, 2, 0, 0x34d399); this._addLabel('pz', 0, 0, 2, 0x60a5fa); } /* ── d orbital: dxy cloverleaf ── */ _buildD() { this._nucleus(new THREE.Vector3(0, 0, 0)); // four lobes in xy plane const r = 1.1, s = 0.45; const angles = [Math.PI / 4, 3 * Math.PI / 4, 5 * Math.PI / 4, 7 * Math.PI / 4]; const colors = [0xF59E0B, 0x9B5DE5, 0xF59E0B, 0x9B5DE5]; // alternating sign for (let i = 0; i < 4; i++) { const x = r * Math.cos(angles[i]); const y = r * Math.sin(angles[i]); this._lobe(new THREE.Vector3(x, y, 0), new THREE.Vector3(s, s, s * 0.6), colors[i], 0.28); } // dz² torus + lobes this._lobe(new THREE.Vector3(0, 0, 1.3), new THREE.Vector3(0.35, 0.35, 0.6), 0x06D6E0, 0.2); this._lobe(new THREE.Vector3(0, 0, -1.3), new THREE.Vector3(0.35, 0.35, 0.6), 0x06D6E0, 0.2); // torus ring const tGeo = new THREE.TorusGeometry(0.7, 0.18, 16, 32); const tMat = new THREE.MeshPhysicalMaterial({ color: 0x06D6E0, transparent: true, opacity: 0.15, side: THREE.DoubleSide, depthWrite: false }); const torus = new THREE.Mesh(tGeo, tMat); this._group.add(torus); this._addLabel('d_{xy}', 1.8, 1.8, 0, 0xF59E0B); this._addLabel('d_{z²}', 0, 0, 2.2, 0x06D6E0); } /* ── H₂ sigma bond ── */ _buildH2() { const sep = 1.5; this._nucleus(new THREE.Vector3(-sep / 2, 0, 0), 0xffffff); this._nucleus(new THREE.Vector3(sep / 2, 0, 0), 0xffffff); // individual 1s orbitals (faded) this._lobe(new THREE.Vector3(-sep / 2, 0, 0), new THREE.Vector3(0.8, 0.8, 0.8), 0x9B5DE5, 0.1); this._lobe(new THREE.Vector3(sep / 2, 0, 0), new THREE.Vector3(0.8, 0.8, 0.8), 0x9B5DE5, 0.1); // bonding σ (overlap region — elongated ellipsoid) this._lobe(new THREE.Vector3(0, 0, 0), new THREE.Vector3(1.4, 0.6, 0.6), 0x06D6E0, 0.22); this._cloud(new THREE.Vector3(0, 0, 0), 1.0, 3000, 0x06D6E0); // bond line const bGeo = new THREE.CylinderGeometry(0.03, 0.03, sep, 8); const bMat = new THREE.MeshStandardMaterial({ color: 0xffffff, emissive: 0xffffff, emissiveIntensity: 0.3 }); const bond = new THREE.Mesh(bGeo, bMat); bond.rotation.z = Math.PI / 2; this._group.add(bond); this._addLabel('H', -sep / 2 - 0.5, 0.6, 0, 0xffffff); this._addLabel('H', sep / 2 + 0.3, 0.6, 0, 0xffffff); this._addLabel('σ', 0, -0.9, 0, 0x06D6E0); } /* ── H₂O bent molecule ── */ _buildH2O() { const angle = 104.5 * Math.PI / 180; const bondLen = 1.6; const oPos = new THREE.Vector3(0, 0, 0); const h1 = new THREE.Vector3(-bondLen * Math.sin(angle / 2), -bondLen * Math.cos(angle / 2), 0); const h2 = new THREE.Vector3(bondLen * Math.sin(angle / 2), -bondLen * Math.cos(angle / 2), 0); // nuclei const oNuc = this._nucleus(oPos, 0xF15BB5); oNuc.scale.set(1.5, 1.5, 1.5); this._nucleus(h1, 0xffffff); this._nucleus(h2, 0xffffff); // O lone pairs (above) this._lobe(new THREE.Vector3(-0.5, 0.8, 0), new THREE.Vector3(0.4, 0.5, 0.35), 0xF59E0B, 0.2); this._lobe(new THREE.Vector3(0.5, 0.8, 0), new THREE.Vector3(0.4, 0.5, 0.35), 0xF59E0B, 0.2); // O-H σ bonds (electron density) const mid1 = new THREE.Vector3().addVectors(oPos, h1).multiplyScalar(0.5); const mid2 = new THREE.Vector3().addVectors(oPos, h2).multiplyScalar(0.5); this._lobe(mid1, new THREE.Vector3(0.4, 0.7, 0.35), 0x06D6E0, 0.2); this._lobe(mid2, new THREE.Vector3(0.4, 0.7, 0.35), 0x06D6E0, 0.2); // bond lines for (const hPos of [h1, h2]) { const d = new THREE.Vector3().subVectors(hPos, oPos); const len = d.length(); const bGeo = new THREE.CylinderGeometry(0.04, 0.04, len, 8); const bMat = new THREE.MeshStandardMaterial({ color: 0xaaaaaa }); const bond = new THREE.Mesh(bGeo, bMat); bond.position.copy(oPos).add(d.clone().multiplyScalar(0.5)); bond.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), d.normalize()); this._group.add(bond); } // electron cloud this._cloud(oPos, 1.2, 1500, 0xF15BB5); this._addLabel('O', 0.3, 0.3, 0, 0xF15BB5); this._addLabel('H', h1.x - 0.4, h1.y - 0.3, 0, 0xffffff); this._addLabel('H', h2.x + 0.2, h2.y - 0.3, 0, 0xffffff); this._addLabel('104.5°', 0, -0.5, 0, 0x888888); } /* ── text label (using sprite) ── */ _addLabel(text, x, y, z, color) { const canvas = document.createElement('canvas'); canvas.width = 128; canvas.height = 48; const ctx = canvas.getContext('2d'); ctx.font = 'bold 28px Manrope, sans-serif'; ctx.fillStyle = '#' + color.toString(16).padStart(6, '0'); ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(text, 64, 24); const tex = new THREE.CanvasTexture(canvas); const mat = new THREE.SpriteMaterial({ map: tex, transparent: true, depthWrite: false }); const sprite = new THREE.Sprite(mat); sprite.position.set(x, y, z); sprite.scale.set(1, 0.4, 1); this._group.add(sprite); } /* ── animation ── */ _loop() { if (!this._running) return; requestAnimationFrame(() => this._loop()); if (this._autoSpin) this._rotY += 0.004; this.camera.position.set( this._dist * Math.sin(this._rotY) * Math.cos(this._rotX), this._dist * Math.sin(this._rotX), this._dist * Math.cos(this._rotY) * Math.cos(this._rotX) ); this.camera.lookAt(0, 0, 0); this.renderer.render(this.scene, this.camera); } } /* ─── lab UI init ─────────────────────────────────── */ var orbitalsSim = null; function _openOrbitals() { document.getElementById('sim-topbar-title').textContent = 'Молекулярные орбитали'; _simShow('sim-orbitals'); requestAnimationFrame(() => requestAnimationFrame(() => { if (!orbitalsSim) { orbitalsSim = new OrbitalsSim(document.getElementById('orbitals-container')); } else { orbitalsSim.fit(); orbitalsSim.play(); } })); } function setOrbital(mode, btn) { document.querySelectorAll('.orbital-mode-btn').forEach(b => { b.classList.remove('active'); b.style.borderColor = ''; b.style.color = ''; }); btn.classList.add('active'); btn.style.borderColor = '#9B5DE5'; btn.style.color = '#9B5DE5'; if (orbitalsSim) orbitalsSim.setMode(mode); } /* ── stereometry 3D ── */