Files
Learn_System/frontend/js/labs/orbitals.js
T
Maxim Dolgolyov ae31e4c4e8 refactor: distribute lab-init.js into 34 engine files
lab-init.js: 4098 -> 543 lines (infrastructure + THEORY only)

Each sim's _open*() + UI helpers moved to its engine file:
graph.js, projectile.js, collision.js, magnetic.js, triangle.js,
geometry.js, trigcircle.js, gas.js (molphys), coulomb.js, circuit.js,
reactions.js (chemistry), newton.js (dynamics), chemsandbox.js,
celldivision.js, photosynthesis.js, angrybirds.js, quadratic.js,
normaldist.js, graphtransform.js, pendulum.js, equilibrium.js,
thinlens.js, mirror.js, isoprocess.js, titration.js, refraction.js,
probability.js, bohratom.js, electrolysis.js, waves.js,
crystal.js, orbitals.js, stereo.js, hydrostatics.js

All 34 engine files syntax-checked OK.
2026-05-08 14:54:54 +03:00

366 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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;
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 ── */