feat(chemistry7): визуал V4 (Глава 4) — электролиз 2:1, индикаторы, титрование

Подключён chem7_anim.js в Главу 4.
- §23 (звёздный): электролиз воды — два потока пузырьков H₂ (18) и O₂ (9),
  наглядно 2:1;
- §24/ЛО5 индикаторы щёлочи: блок плавно меняет цвет (фенолфталеин → малиновый);
- §25/ПР4 нейтрализация (звёздный): раствор плавно обесцвечивается
  малиновый → бесцветный (colorBlock).

Все 4 главы анимированы. Тесты chem7: 16/16; полный прогон 162/165 (3 — baseline Auth).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-30 19:54:50 +03:00
parent 33f968bff9
commit 639f985e6f
3 changed files with 29 additions and 11 deletions
+3
View File
@@ -225,13 +225,16 @@ test('ch3 Волна 2: §21 + ЛО4 + §22 + ПР3 + финал главы мо
test('ch4: вся глава 4 (§23–§26 + ЛО5 + ПР4 + финал) монтируется', async () => {
const { doc, errors } = await loadDom('chemistry_7_ch4.html');
assert.ok(doc.querySelector('#p23-water #p23-pick'), 'разложение/реакции воды §23');
assert.ok(doc.querySelector('#p23-bub-h div'), 'пузырьки электролиза 2:1 §23');
doc.defaultView.goTo('p24'); await wait(100);
assert.ok(doc.querySelector('#p24-bld #p24-m'), 'конструктор оснований §24');
assert.ok(doc.querySelector('#p24-ind #p24-ind-sel'), 'индикаторы щёлочи §24');
assert.ok(doc.querySelector('#p24-ind-drop div'), 'анимация индикатора §24');
doc.defaultView.goTo('lo5'); await wait(100);
assert.ok(doc.querySelector('#lo5-ind #lo5-ind-sel'), 'индикаторы ЛО5');
doc.defaultView.goTo('p25'); await wait(100);
assert.ok(doc.querySelector('#p25-neu #p25-neu-go'), 'нейтрализация §25');
assert.ok(doc.querySelector('#p25-neu-cup div'), 'анимация раствора §25');
doc.defaultView.goTo('pr4'); await wait(100);
assert.ok(doc.querySelector('#pr4-neu #pr4-neu-go'), 'нейтрализация ПР4');
doc.defaultView.goTo('p26'); await wait(100);
+25 -11
View File
@@ -36,15 +36,23 @@
}
function mount_p23() {
var m = $('p23-water'); if (!m || m._built) return; m._built = 1;
var idx = 0;
var idx = 0, anims = [];
function stopAnim(){ anims.forEach(function(a){ try { a.stop(); } catch(e){} }); anims = []; }
function render(){
stopAnim();
var r = WRX[idx];
m.innerHTML = (idx===0 ? decompSvg() : '')
m.innerHTML = (idx===0 ? decompSvg()
+ '<div style="display:flex;gap:10px;margin-top:6px"><div style="flex:1"><div style="text-align:center;font-size:.76rem;font-weight:700;color:#1d4ed8">H₂ — 2 объёма</div><div id="p23-bub-h"></div></div>'
+ '<div style="flex:1"><div style="text-align:center;font-size:.76rem;font-weight:700;color:#b91c1c">O₂ — 1 объём</div><div id="p23-bub-o"></div></div></div>' : '')
+ '<div class="fld"><label>Реакция воды</label><select id="p23-pick">'
+ WRX.map(function(x,i){ return '<option value="'+i+'"'+(i===idx?' selected':'')+'>'+esc(x.name)+'</option>'; }).join('') + '</select></div>'
+ '<div class="out ok" style="margin-top:8px"><div style="font-size:1.05rem">'+ceq(r.eq,{cond:r.cond})+'</div>'
+ '<div style="font-size:.86rem;color:var(--muted);margin-top:6px">'+esc(r.note)+'</div></div>';
$('p23-pick').addEventListener('change', function(e){ idx=+e.target.value; m._built=0; render(); });
if (idx===0 && W.Chem7Anim) {
anims.push(W.Chem7Anim.bubbleField($('p23-bub-h'), { color:'rgba(96,165,250,.9)', count:18, h:84, bg:'linear-gradient(180deg,#dbeafe,transparent)' }));
anims.push(W.Chem7Anim.bubbleField($('p23-bub-o'), { color:'rgba(248,113,113,.9)', count:9, h:84, bg:'linear-gradient(180deg,#fee2e2,transparent)' }));
}
$('p23-pick').addEventListener('change', function(e){ idx=+e.target.value; render(); });
}
render();
}
@@ -57,14 +65,17 @@
};
function alkIndicator(mountId) {
var m = $(mountId); if (!m || m._built) return; m._built = 1;
var ind = 'Фенолфталеин';
var ind = 'Фенолфталеин', anim = null;
function render(){
if (anim) { anim.stop(); anim = null; }
var c = ALK_IND[ind];
m.innerHTML = '<div class="fld"><label>Индикатор</label><select id="'+mountId+'-sel">'
+ Object.keys(ALK_IND).map(function(k){ return '<option'+(k===ind?' selected':'')+'>'+k+'</option>'; }).join('') + '</select></div>'
+ '<div id="'+mountId+'-drop" style="margin-top:8px"></div>'
+ '<div class="out ok" style="margin-top:8px">В нейтральной среде: ' + strip(c.neutral[0]) + ' <b>'+c.neutral[1]+'</b><br>'
+ 'В щёлочи: ' + strip(c.alk[0]) + ' <b>'+c.alk[1]+'</b></div>';
$(mountId+'-sel').addEventListener('change', function(e){ ind=e.target.value; m._built=0; render(); });
if (W.Chem7Anim) anim = W.Chem7Anim.colorBlock($(mountId+'-drop'), c.neutral[0], c.alk[0], ind + ' в щёлочи → ' + c.alk[1], 900);
$(mountId+'-sel').addEventListener('change', function(e){ ind=e.target.value; render(); });
}
render();
}
@@ -94,16 +105,19 @@
/* §25 / ПР4 — нейтрализация (фенолфталеин малиновый → бесцветный) */
function mount_neutral(mountId) {
var m = $(mountId); if (!m || m._built) return; m._built = 1;
var done = false;
function beaker(color){ return '<svg viewBox="0 0 80 90" width="70" style="vertical-align:middle"><path d="M20 10 h40 v30 l14 38 a6 6 0 0 1-6 8 H12 a6 6 0 0 1-6-8 l14-38 Z" fill="none" stroke="var(--muted)" stroke-width="2"/><path d="M16 52 h48 l8 22 a4 4 0 0 1-4 5 H12 a4 4 0 0 1-4-5 Z" fill="'+color+'"/></svg>'; }
var done = false, anim = null;
function render(){
m.innerHTML = '<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap">' + beaker(done?'#f8fafc':'#db2777')
+ '<div>'+(done
if (anim) { anim.stop(); anim = null; }
m.innerHTML = '<div id="'+mountId+'-cup" style="margin-bottom:8px"></div>'
+ '<div style="font-size:.92rem">'+(done
? 'Раствор стал <b>бесцветным</b> — кислота нейтрализовала щёлочь. Реакция завершена.'
: 'В щёлочи с фенолфталеином раствор <b>малиновый</b>. Добавляй кислоту по каплям.')+'</div></div>'
: 'В щёлочи с фенолфталеином раствор <b>малиновый</b>. Добавляй кислоту по каплям.')+'</div>'
+ '<div class="fld" style="margin-top:8px"><button class="btn primary" id="'+mountId+'-go">'+(done?'Сбросить':'Добавить кислоту')+'</button></div>'
+ (done ? '<div class="out ok" style="margin-top:8px"><div style="font-size:1.05rem">'+ceq('HCl + NaOH = NaCl + H2O')+'</div><div style="font-size:.84rem;color:var(--muted);margin-top:4px">Кислота + основание → соль + вода. Это реакция <b>нейтрализации</b>.</div></div>' : '');
$(mountId+'-go').addEventListener('click', function(){ done=!done; m._built=0; render(); });
if (W.Chem7Anim) anim = done
? W.Chem7Anim.colorBlock($(mountId+'-cup'), '#db2777', '#f8fafc', 'малиновый → бесцветный', 1600)
: W.Chem7Anim.colorBlock($(mountId+'-cup'), '#db2777', '#db2777', 'щёлочь + фенолфталеин', 1);
$(mountId+'-go').addEventListener('click', function(){ done=!done; render(); });
}
render();
}
+1
View File
@@ -23,6 +23,7 @@ html.dark{--bg:#0a1222;--border:#1e3a5f;--pri-soft:rgba(37,99,235,.18);--sec-acc
<script src="/js/biochem-core.js" defer></script>
<script src="/js/chem8_svg.js" defer></script>
<script src="/js/chem7_svg.js" defer></script>
<script src="/js/chem7_anim.js" defer></script>
<script src="/js/chem7_ch4_widgets.js" defer></script>
<script src="/js/chem8_engine.js" defer></script>
</head>