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:
@@ -305,6 +305,16 @@ class ChemSandboxSim {
|
||||
this._quizResult = null; // 'correct' | 'wrong' | null
|
||||
this._quizResultT = 0;
|
||||
|
||||
// edu-tooltip state
|
||||
this._eduTooltipAge = -1; // -1 = inactive; 0..1 = active
|
||||
this._eduTooltipLines = [];
|
||||
this._showHints = true; // can be toggled
|
||||
|
||||
// product label animation state
|
||||
this._prodLabelAge = -1;
|
||||
this._prodLabelText = '';
|
||||
this._prodLabelType = 'gas';
|
||||
|
||||
this.onUpdate = null;
|
||||
this.onQuizUpdate = null; // callback(quizInfo)
|
||||
this.fit();
|
||||
@@ -560,6 +570,36 @@ class ChemSandboxSim {
|
||||
if (fx.violent) this._spawnSparks(35);
|
||||
}
|
||||
|
||||
/* product label animation */
|
||||
if (fx.gas && window.ChemVisuals) {
|
||||
this._prodLabelText = fx.gas + ' ';
|
||||
this._prodLabelType = 'gas';
|
||||
this._prodLabelAge = 0;
|
||||
} else if (fx.precip && window.ChemVisuals) {
|
||||
this._prodLabelText = (fx.precip.n || '') + ' ';
|
||||
this._prodLabelType = 'precip';
|
||||
this._prodLabelAge = 0;
|
||||
}
|
||||
|
||||
/* educational tooltip */
|
||||
if (rx.why && this._showHints && window.ChemVisuals) {
|
||||
const typeLabel = rx.type ? 'Тип: ' + rx.type : '';
|
||||
const why = rx.why.replace(/<[^>]+>/g, ''); /* strip SVG tags */
|
||||
this._eduTooltipLines = [
|
||||
typeLabel,
|
||||
...why.split(' ').reduce((acc, w) => {
|
||||
const last = acc[acc.length - 1];
|
||||
if (last && (last + ' ' + w).length < 32) {
|
||||
acc[acc.length - 1] = last + ' ' + w;
|
||||
} else {
|
||||
acc.push(w);
|
||||
}
|
||||
return acc;
|
||||
}, []),
|
||||
].filter(Boolean).slice(0, 4);
|
||||
this._eduTooltipAge = 0;
|
||||
}
|
||||
|
||||
if (window.LabFX) {
|
||||
const { cx, cy } = this._g;
|
||||
if (fx.violent) {
|
||||
@@ -711,6 +751,18 @@ class ChemSandboxSim {
|
||||
}
|
||||
}
|
||||
|
||||
/* advance edu-tooltip age (total lifespan = 4s) */
|
||||
if (this._eduTooltipAge >= 0) {
|
||||
this._eduTooltipAge += dt / 4.0;
|
||||
if (this._eduTooltipAge >= 1.0) this._eduTooltipAge = -1;
|
||||
}
|
||||
|
||||
/* advance product label age (total lifespan = 3s) */
|
||||
if (this._prodLabelAge >= 0) {
|
||||
this._prodLabelAge += dt / 3.0;
|
||||
if (this._prodLabelAge >= 1.0) this._prodLabelAge = -1;
|
||||
}
|
||||
|
||||
this._updatePour(dt);
|
||||
this._updateBubbles(dt);
|
||||
this._updatePrecip(dt);
|
||||
@@ -821,10 +873,16 @@ class ChemSandboxSim {
|
||||
if (this.mixContents.length === 0 && !this.lastReaction && !this._quizMode) this._drawHint();
|
||||
if (this._quizMode) this._drawQuizOverlay();
|
||||
if (window.LabFX) LabFX.particles.draw(ctx);
|
||||
/* edu-tooltip overlay */
|
||||
if (window.ChemVisuals && this._eduTooltipAge >= 0 && this._eduTooltipLines.length > 0) {
|
||||
const { cx, nt } = this._g;
|
||||
ChemVisuals.drawEduTooltip(ctx, cx, nt - 20, 200, this._eduTooltipLines, this._eduTooltipAge);
|
||||
}
|
||||
}
|
||||
|
||||
_drawBackground() {
|
||||
const { ctx, W, H } = this;
|
||||
const { cy, r } = this._g;
|
||||
const bg = ctx.createRadialGradient(W / 2, H * 0.38, 0, W / 2, H * 0.38, W * 0.75);
|
||||
bg.addColorStop(0, '#0c0c1a');
|
||||
bg.addColorStop(1, '#050508');
|
||||
@@ -835,17 +893,26 @@ class ChemSandboxSim {
|
||||
ctx.lineWidth = 0.5;
|
||||
for (let x = 0; x < W; x += 30) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke(); }
|
||||
for (let y = 0; y < H; y += 30) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke(); }
|
||||
/* desk surface behind flask */
|
||||
if (window.ChemVisuals) {
|
||||
const tableY = cy + r + 6;
|
||||
ChemVisuals.drawDeskBackground(ctx, W, H, tableY);
|
||||
}
|
||||
}
|
||||
|
||||
_drawFlaskShadow() {
|
||||
const { ctx } = this;
|
||||
const { cx, cy, r } = this._g;
|
||||
// shadow beneath flask
|
||||
const sg = ctx.createRadialGradient(cx, cy + r + 8, 0, cx, cy + r + 8, r * 1.1);
|
||||
sg.addColorStop(0, 'rgba(0,0,0,0.25)');
|
||||
sg.addColorStop(1, 'rgba(0,0,0,0)');
|
||||
ctx.fillStyle = sg;
|
||||
ctx.fillRect(cx - r * 1.2, cy + r - 2, r * 2.4, 25);
|
||||
if (window.ChemVisuals) {
|
||||
ChemVisuals.drawVesselShadow(ctx, cx, cy + r + 4, r);
|
||||
} else {
|
||||
// fallback
|
||||
const sg = ctx.createRadialGradient(cx, cy + r + 8, 0, cx, cy + r + 8, r * 1.1);
|
||||
sg.addColorStop(0, 'rgba(0,0,0,0.25)');
|
||||
sg.addColorStop(1, 'rgba(0,0,0,0)');
|
||||
ctx.fillStyle = sg;
|
||||
ctx.fillRect(cx - r * 1.2, cy + r - 2, r * 2.4, 25);
|
||||
}
|
||||
}
|
||||
|
||||
_drawLiquid() {
|
||||
@@ -933,6 +1000,11 @@ class ChemSandboxSim {
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
|
||||
/* precipitate product label with animated arrow */
|
||||
if (window.ChemVisuals && this._prodLabelType === 'precip' && this._prodLabelAge >= 0) {
|
||||
ChemVisuals.drawProductLabel(ctx, cx, cy + r + 2, this._prodLabelText, 'precip', this._prodLabelAge);
|
||||
}
|
||||
}
|
||||
|
||||
_drawBubbles() {
|
||||
@@ -952,12 +1024,19 @@ class ChemSandboxSim {
|
||||
// gas label above neck
|
||||
if (this._gasLabel && this._bubbles.length > 0) {
|
||||
const { cx, nt } = this._g;
|
||||
ctx.save();
|
||||
ctx.font = '11px "JetBrains Mono", monospace';
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.45)';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(this._gasLabel + ' ↑', cx, nt - 14);
|
||||
ctx.restore();
|
||||
if (window.ChemVisuals && this._prodLabelAge >= 0) {
|
||||
/* animated product label with arrow */
|
||||
ChemVisuals.drawProductLabel(ctx, cx, nt - 10, this._prodLabelText, 'gas', this._prodLabelAge);
|
||||
/* continuous bubble particles near neck */
|
||||
ChemVisuals.animateGasBubbles(ctx, cx, nt - 6, 'rgba(200,235,255,0.8)', this._time);
|
||||
} else {
|
||||
ctx.save();
|
||||
ctx.font = '11px "JetBrains Mono", monospace';
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.45)';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(this._gasLabel + ' ', cx, nt - 14);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1435,9 +1514,10 @@ class ChemSandboxSim {
|
||||
ctx.font = '14px sans-serif';
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.18)';
|
||||
ctx.fillText('Выберите реагенты на полке или панели', W / 2, cy);
|
||||
ctx.font = '36px serif';
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.06)';
|
||||
ctx.fillText('\u{1F9EA}', W / 2, cy - 30);
|
||||
/* small test-tube icon (canvas-drawn, no emoji) */
|
||||
if (window.ChemVisuals) {
|
||||
ChemVisuals.drawTube(ctx, W / 2, cy - 52, 36, null);
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user