Files
Learn_System/frontend/js/labs/crystal.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

339 lines
12 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';
/* ═══════════════════════════════════════════════
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;
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) ── */