feat(labs): 4 школьные хим. симы + визуальная прокачка лаборатории

4 НОВЫЕ СИМЫ (школьная программа 8-11 классов):

Органика (organic.js, 1545 строк):
- Конструктор молекул: drag атомов C/H/O/N/Cl/S, валентности, click-pair bonds
- Авто-определение класса: алкан/алкен/алкин/спирт/альдегид/кислота/эфир/амин/аромат
- IUPAC-имена для C1-C10
- Гомологические ряды: 7 рядов с slider количества углеродов, M, T_кип, T_пл
- 6 качественных реакций: Br₂ вода, KMnO₄, Ag₂O/NH₃ (серебряное зеркало), Cu(OH)₂, FeCl₃, I₂

Периодическая таблица (periodic.js, 118 элементов):
- Стандартный вид 18×9 + лантаноиды/актиноиды
- Карточка элемента: Z, M, конфигурация, степени окисления, ЭО, ρ, T_пл/T_кип
- Боровская модель электронных оболочек (анимированная)
- Подсветка: 11 типов / s/p/d/f-блоки / без подсветки
- Графики свойств по периоду/группе (ЭО, M, плотность, T_пл/T_кип)
- Поиск по символу/имени/Z/массе

Качественный анализ (qualanalysis.js, 24 иона):
- 15 катионов: Na/K/NH₄/Mg/Ca/Ba/Al/Fe²⁺/Fe³⁺/Cu/Ag/Pb/Zn/H/OH
- 10 анионов: Cl/Br/I/SO₄/SO₃/CO₃/NO₃/PO₄/S²/CH₃COO
- 9 реактивов + пламя
- 2 режима: «определи ион» и «неизвестное вещество» с логом наблюдений
- Анимация капли, осадка с цветом, газовых пузырей, пламени

Растворы (solutions.js, 4 режима):
- Калькулятор: m_в, m_р-ра, ρ, T → ω, ν, C_М, C_Н с понятной логикой пересчёта
- Разбавление с before/after визуализацией
- Смешивание двух растворов с правилом рычага
- Кривые растворимости 8 веществ + задача перекристаллизации
- 15 пресетов веществ (NaCl, NaOH, H₂SO₄, CuSO₄·5H₂O, глюкоза, сахароза, ...)

ВИЗУАЛЬНАЯ ПРОКАЧКА (_chem_visuals.js, helper file):

12 функций школьной лабораторной графики:
- drawErlenmeyer / drawBeaker / drawBurette / drawTube — proper SVG-paths со шкалой
- drawSpiritLamp — стеклянный резервуар + фитиль + анимированное пламя
- animateGasBubbles / animatePrecipitateFall — анимация продуктов
- drawProductLabel — fade-in/out стрелка ↑/↓ с подписью
- drawEduTooltip — bubble с пояснением реакции
- drawDeskBackground / drawVesselShadow — лабораторный фон
- drawPHStrip — pH-индикаторная полоса с маркером

Прокачено 6 chem-сим: chemsandbox, flask, titration, electrolysis, ionexchange, redox
Каждая получила: фон парты, тени под колбами, анимированные стрелки продуктов,
educational tooltips из поля 'why' реакции. Спиртовка с пламенем в flask.
pH-полоса в titration.

Каталог теперь: 39 симуляций (было 35 + 4 новых).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-26 13:08:35 +03:00
parent add17b1bb4
commit ea2526dc73
15 changed files with 5738 additions and 21 deletions
+43 -5
View File
@@ -94,6 +94,12 @@ class FlaskSim {
this._last = 0;
this._paused = false;
/* product label animation */
this._prodLabelAge = -1;
this._prodLabelText = '';
this._prodLabelType = 'gas';
this._time = 0;
this.onUpdate = null;
this.W = 0; this.H = 0;
this._g = {};
@@ -239,6 +245,7 @@ class FlaskSim {
_tick(now) {
const dt = Math.min((now - this._last) / 1000, 0.05);
this._last = now;
this._time += dt;
if (window.LabFX) LabFX.particles.update(dt);
if (!this._paused) {
this._wave += dt * 1.7;
@@ -253,6 +260,11 @@ class FlaskSim {
this._stepSplashes(dt);
this._stepCaustics(dt);
}
/* product label age */
if (this._prodLabelAge >= 0) {
this._prodLabelAge += dt / 3.0;
if (this._prodLabelAge >= 1.0) this._prodLabelAge = -1;
}
this.draw();
if (this.onUpdate) this.onUpdate(this.info());
}
@@ -328,6 +340,13 @@ class FlaskSim {
this._bubTmr--;
}
/* trigger H2 product label when reaction first picks up */
if (md.h2 > 0 && this._rxRate > 0.05 && this._prodLabelAge < 0 && window.ChemVisuals) {
this._prodLabelText = 'H₂ ';
this._prodLabelType = 'gas';
this._prodLabelAge = 0;
}
if (md.rust && Math.random() < rate * dt * 14) {
this._spawnDust(m.x + (Math.random() - 0.5) * m.r, m.y + m.r * 0.3, '#8B3A0A', 0.65);
}
@@ -544,11 +563,23 @@ class FlaskSim {
/* Стол */
const tableY = g.cy + g.r + 8;
const tg = ctx.createLinearGradient(0, tableY, 0, tableY + 48);
tg.addColorStop(0, '#19223a'); tg.addColorStop(1, '#0c101e');
ctx.fillStyle = tg; ctx.fillRect(0, tableY, W, H - tableY);
ctx.strokeStyle = 'rgba(90,120,200,0.20)'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(0, tableY); ctx.lineTo(W, tableY); ctx.stroke();
if (window.ChemVisuals) {
ChemVisuals.drawDeskBackground(ctx, W, H, tableY);
ChemVisuals.drawVesselShadow(ctx, g.cx, tableY + 2, g.r);
} else {
const tg = ctx.createLinearGradient(0, tableY, 0, tableY + 48);
tg.addColorStop(0, '#19223a'); tg.addColorStop(1, '#0c101e');
ctx.fillStyle = tg; ctx.fillRect(0, tableY, W, H - tableY);
ctx.strokeStyle = 'rgba(90,120,200,0.20)'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(0, tableY); ctx.lineTo(W, tableY); ctx.stroke();
}
/* Spirit lamp under flask when flame is on */
if (window.ChemVisuals) {
const lampX = g.cx;
const lampY = tableY + 18;
ChemVisuals.drawSpiritLamp(ctx, lampX, lampY, this._flameOn, this._time);
}
this._drawFlaskShadow(ctx, tableY);
this._drawLiquid(ctx);
@@ -566,6 +597,13 @@ class FlaskSim {
this._drawInfoPanel(ctx);
if (!this._metal || this._metal.mass <= 0.01) this._drawHint(ctx);
if (window.LabFX) LabFX.particles.draw(ctx);
/* animated product labels */
if (window.ChemVisuals && this._prodLabelAge >= 0) {
ChemVisuals.drawProductLabel(ctx, g.cx, g.nt - 10, this._prodLabelText, this._prodLabelType, this._prodLabelAge);
if (this._prodLabelType === 'gas') {
ChemVisuals.animateGasBubbles(ctx, g.cx, g.nt - 8, 'rgba(200,235,255,0.8)', this._time);
}
}
}
/* ── Тень/отражение колбы на столе ── */