'use strict'; /* ═══════════════════════════════════════════════ CrystalSim — 3D crystal lattice (Three.js) NaCl, Diamond, BCC metal, FCC metal ═══════════════════════════════════════════════ */ class CrystalSim { 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.5)); const dir = new THREE.DirectionalLight(0xffffff, 0.8); dir.position.set(5, 8, 6); this.scene.add(dir); const pt = new THREE.PointLight(0x9B5DE5, 0.4, 50); pt.position.set(-4, 3, 5); this.scene.add(pt); this.camera.position.set(8, 6, 8); this.camera.lookAt(0, 0, 0); /* orbit-like manual controls */ this._drag = false; this._prevX = 0; this._prevY = 0; this._rotY = 0.6; this._rotX = 0.4; this._dist = 12; 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(5, Math.min(30, 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._lattice = 'nacl'; this._group = new THREE.Group(); this.scene.add(this._group); this._buildLattice('nacl'); this.fit(); this.play(); } /* ── public ── */ setLattice(type) { this._lattice = type; if (window.LabFX) LabFX.sound.play('chime', { pitch: 1.2, volume: 0.3 }); this._buildLattice(type); } 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; } /* ── lattice builders ── */ _buildLattice(type) { // clear while (this._group.children.length) { const c = this._group.children[0]; c.geometry?.dispose(); c.material?.dispose(); this._group.remove(c); } const builders = { nacl: () => this._buildNaCl(), diamond: () => this._buildDiamond(), bcc: () => this._buildBCC(), fcc: () => this._buildFCC(), }; (builders[type] || builders.nacl)(); } _sphere(r, color) { const geo = new THREE.SphereGeometry(r, 24, 24); const mat = new THREE.MeshPhysicalMaterial({ color, metalness: 0.1, roughness: 0.3, clearcoat: 0.6, clearcoatRoughness: 0.2, }); return new THREE.Mesh(geo, mat); } _bond(from, to, color = 0x555555) { const dir = new THREE.Vector3().subVectors(to, from); const len = dir.length(); const geo = new THREE.CylinderGeometry(0.04, 0.04, len, 8); const mat = new THREE.MeshStandardMaterial({ color, opacity: 0.5, transparent: true }); const mesh = new THREE.Mesh(geo, mat); mesh.position.copy(from).add(dir.multiplyScalar(0.5)); mesh.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir.normalize()); return mesh; } _buildNaCl() { const n = 3; // 3×3×3 unit cells const a = 2.0; // lattice constant const offset = -(n - 1) * a / 2; const positions = []; for (let i = 0; i < n; i++) for (let j = 0; j < n; j++) for (let k = 0; k < n; k++) { const x = offset + i * a, y = offset + j * a, z = offset + k * a; const isNa = (i + j + k) % 2 === 0; const s = this._sphere(isNa ? 0.28 : 0.38, isNa ? 0x9B5DE5 : 0x06D6E0); s.position.set(x, y, z); this._group.add(s); positions.push({ x, y, z }); } // bonds to nearest neighbors for (let i = 0; i < positions.length; i++) for (let j = i + 1; j < positions.length; j++) { const dx = positions[i].x - positions[j].x; const dy = positions[i].y - positions[j].y; const dz = positions[i].z - positions[j].z; const d2 = dx * dx + dy * dy + dz * dz; if (Math.abs(d2 - a * a) < 0.01) { const b = this._bond( new THREE.Vector3(positions[i].x, positions[i].y, positions[i].z), new THREE.Vector3(positions[j].x, positions[j].y, positions[j].z), 0x444466 ); this._group.add(b); } } } _buildDiamond() { const a = 2.5; const basis = [ [0, 0, 0], [0.5, 0.5, 0], [0.5, 0, 0.5], [0, 0.5, 0.5], [0.25, 0.25, 0.25], [0.75, 0.75, 0.25], [0.75, 0.25, 0.75], [0.25, 0.75, 0.75], ]; const n = 2; const offset = -(n * a) / 2; const allPos = []; for (let ci = 0; ci < n; ci++) for (let cj = 0; cj < n; cj++) for (let ck = 0; ck < n; ck++) for (const [bx, by, bz] of basis) { const x = offset + (ci + bx) * a; const y = offset + (cj + by) * a; const z = offset + (ck + bz) * a; const s = this._sphere(0.22, 0x34d399); s.position.set(x, y, z); this._group.add(s); allPos.push(new THREE.Vector3(x, y, z)); } // bonds const bondLen = a * Math.sqrt(3) / 4; const tol = bondLen * 0.15; for (let i = 0; i < allPos.length; i++) for (let j = i + 1; j < allPos.length; j++) { const d = allPos[i].distanceTo(allPos[j]); if (Math.abs(d - bondLen) < tol) { this._group.add(this._bond(allPos[i], allPos[j], 0x228866)); } } } _buildBCC() { const a = 2.2, n = 3; const offset = -(n - 1) * a / 2; const allPos = []; for (let i = 0; i < n; i++) for (let j = 0; j < n; j++) for (let k = 0; k < n; k++) { // corner atoms const x1 = offset + i * a, y1 = offset + j * a, z1 = offset + k * a; const s1 = this._sphere(0.3, 0xF15BB5); s1.position.set(x1, y1, z1); this._group.add(s1); allPos.push(new THREE.Vector3(x1, y1, z1)); // body center (except last cell in each dimension) if (i < n - 1 && j < n - 1 && k < n - 1) { const cx = x1 + a / 2, cy = y1 + a / 2, cz = z1 + a / 2; const s2 = this._sphere(0.3, 0xF59E0B); s2.position.set(cx, cy, cz); this._group.add(s2); allPos.push(new THREE.Vector3(cx, cy, cz)); } } // bonds const bondLen = a * Math.sqrt(3) / 2; const tol = bondLen * 0.1; for (let i = 0; i < allPos.length; i++) for (let j = i + 1; j < allPos.length; j++) { const d = allPos[i].distanceTo(allPos[j]); if (Math.abs(d - bondLen) < tol) { this._group.add(this._bond(allPos[i], allPos[j], 0x664444)); } } } _buildFCC() { const a = 2.4, n = 3; const offset = -(n - 1) * a / 2; const allPos = []; for (let i = 0; i < n; i++) for (let j = 0; j < n; j++) for (let k = 0; k < n; k++) { const x = offset + i * a, y = offset + j * a, z = offset + k * a; // corner const s = this._sphere(0.25, 0x60a5fa); s.position.set(x, y, z); this._group.add(s); allPos.push(new THREE.Vector3(x, y, z)); // face centers (only for cell interiors) if (i < n - 1 && j < n - 1) { const f1 = this._sphere(0.25, 0x60a5fa); f1.position.set(x + a / 2, y + a / 2, z); this._group.add(f1); allPos.push(new THREE.Vector3(x + a / 2, y + a / 2, z)); } if (i < n - 1 && k < n - 1) { const f2 = this._sphere(0.25, 0x60a5fa); f2.position.set(x + a / 2, y, z + a / 2); this._group.add(f2); allPos.push(new THREE.Vector3(x + a / 2, y, z + a / 2)); } if (j < n - 1 && k < n - 1) { const f3 = this._sphere(0.25, 0x60a5fa); f3.position.set(x, y + a / 2, z + a / 2); this._group.add(f3); allPos.push(new THREE.Vector3(x, y + a / 2, z + a / 2)); } } // bonds to nearest neighbors (a/√2) const bondLen = a / Math.SQRT2; const tol = bondLen * 0.1; for (let i = 0; i < allPos.length; i++) for (let j = i + 1; j < allPos.length; j++) { const d = allPos[i].distanceTo(allPos[j]); if (Math.abs(d - bondLen) < tol) { this._group.add(this._bond(allPos[i], allPos[j], 0x334466)); } } } /* ── animation ── */ _loop() { if (!this._running) return; requestAnimationFrame(() => this._loop()); if (this._autoSpin) this._rotY += 0.003; 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 crystalSim = null; function _openCrystal() { document.getElementById('sim-topbar-title').textContent = 'Кристаллическая решётка'; _simShow('sim-crystal'); requestAnimationFrame(() => requestAnimationFrame(() => { if (!crystalSim) { crystalSim = new CrystalSim(document.getElementById('crystal-container')); } else { crystalSim.fit(); crystalSim.play(); } })); } function setCrystal(type, btn) { document.querySelectorAll('.crystal-type-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 (crystalSim) crystalSim.setLattice(type); } /* ── molecular orbitals (3D) ── */