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
+61
View File
@@ -97,6 +97,12 @@ class RedoxSim {
this._oParts = [];
this._precip = [];
this._gas = [];
/* edu-tooltip + product labels */
this._eduTooltipAge = -1;
this._eduTooltipLines = [];
this._prodLabelAge = -1;
this._prodLabelText = '';
this._prodLabelType = 'precip';
this.W = 0; this.H = 0;
this.onUpdate = null;
this.fit();
@@ -233,6 +239,39 @@ class RedoxSim {
p.phase += dt;
this._clamp(p);
});
/* trigger product label + edu-tooltip once */
if (window.ChemVisuals && this._prodLabelAge < 0) {
const rxn = RedoxSim.RXN[this.rxnId];
if (rxn.precip) {
this._prodLabelText = (rxn.pname || '') + ' ';
this._prodLabelType = 'precip';
this._prodLabelAge = 0;
} else if (rxn.gas) {
this._prodLabelText = (rxn.gname || '') + ' ';
this._prodLabelType = 'gas';
this._prodLabelAge = 0;
}
if (this._eduTooltipAge < 0 && rxn.eq_mol) {
const stripSVG = s => (s || '').replace(/<[^>]+>/g, '->');
const eqClean = stripSVG(rxn.eq_ion || rxn.eq_mol).slice(0, 38);
this._eduTooltipLines = [
(rxn.name || '').slice(0, 34),
'e⁻: от восстановителя к окислителю',
eqClean,
].filter(Boolean).slice(0, 4);
this._eduTooltipAge = 0;
}
}
}
/* advance ages */
if (this._eduTooltipAge >= 0) {
this._eduTooltipAge += dt / 4.0;
if (this._eduTooltipAge >= 1.0) this._eduTooltipAge = -1;
}
if (this._prodLabelAge >= 0) {
this._prodLabelAge += dt / 3.0;
if (this._prodLabelAge >= 1.0) this._prodLabelAge = -1;
}
/* Electrons — quadratic bezier arc */
@@ -343,6 +382,12 @@ class RedoxSim {
}
}
/* desk */
if (window.ChemVisuals) {
ChemVisuals.drawDeskBackground(ctx, W, H, H * 0.82);
ChemVisuals.drawVesselShadow(ctx, W / 2, H * 0.82, W * 0.38);
}
this._drawBeaker(ctx, W, H);
this._drawParticles(ctx, rxn);
this._drawElectrons(ctx);
@@ -350,6 +395,22 @@ class RedoxSim {
if (rxn.gas) this._drawGas(ctx, rxn);
this._drawPanel(ctx, W, H, rxn);
if (window.LabFX) LabFX.particles.draw(this.ctx);
/* animated product label */
if (window.ChemVisuals && this._prodLabelAge >= 0) {
const labelY = this._prodLabelType === 'gas' ? H * 0.12 : H * 0.76;
ChemVisuals.drawProductLabel(ctx, W / 2, labelY, this._prodLabelText, this._prodLabelType, this._prodLabelAge);
if (this._prodLabelType === 'gas') {
ChemVisuals.animateGasBubbles(ctx, W / 2, H * 0.16, rxn.gcolor || 'rgba(200,235,255,0.8)', this._t);
} else {
ChemVisuals.animatePrecipitateFall(ctx, W / 2, H * 0.72, rxn.pcolor || '#CCC', this._t);
}
}
/* edu tooltip */
if (window.ChemVisuals && this._eduTooltipAge >= 0 && this._eduTooltipLines.length > 0) {
ChemVisuals.drawEduTooltip(ctx, W / 2, H * 0.10, 210, this._eduTooltipLines, this._eduTooltipAge);
}
}
_drawBeaker(ctx, W, H) {