feat(chemistry7): визуал V3 (Глава 3) — пузырьки, морфинг цвета, индикаторы

Подключён chem7_anim.js в Главу 3.
- §21 ряд активности (звёздный): клик металла левее H₂ → анимация пузырьков
  H₂ (bubbleField); правее (Cu, Ag) — «реакция не идёт»;
- §19 восстановление CuO: colorBlock плавно чёрный→красный (медь); горение —
  пламя водорода;
- §20/ЛО3 индикаторы: блок плавно меняет цвет на цвет индикатора в кислоте.

Тесты 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:51:27 +03:00
parent e8cb95be55
commit 33f968bff9
3 changed files with 31 additions and 10 deletions
+5
View File
@@ -194,8 +194,10 @@ test('ch3 Волна 1: §18 + §19 + §20 + ЛО3 монтируются', asyn
assert.ok(doc.querySelector('#p18-card svg'), 'паспорт водорода §18'); assert.ok(doc.querySelector('#p18-card svg'), 'паспорт водорода §18');
doc.defaultView.goTo('p19'); await wait(100); doc.defaultView.goTo('p19'); await wait(100);
assert.ok(doc.querySelector('#p19-rx #p19-pick'), 'реакции водорода §19'); assert.ok(doc.querySelector('#p19-rx #p19-pick'), 'реакции водорода §19');
assert.ok(doc.querySelector('#p19-stage div'), 'анимация реакции §19');
doc.defaultView.goTo('p20'); await wait(100); doc.defaultView.goTo('p20'); await wait(100);
assert.ok(doc.querySelector('#p20-ind #p20-ind-ind'), 'индикаторы §20'); assert.ok(doc.querySelector('#p20-ind #p20-ind-ind'), 'индикаторы §20');
assert.ok(doc.querySelector('#p20-ind-drop div'), 'анимация индикатора §20');
assert.ok(doc.querySelector('#p20-acids table'), 'таблица кислот §20'); assert.ok(doc.querySelector('#p20-acids table'), 'таблица кислот §20');
doc.defaultView.goTo('lo3'); await wait(100); doc.defaultView.goTo('lo3'); await wait(100);
assert.ok(doc.querySelector('#lo3-ind #lo3-ind-ind'), 'индикаторы ЛО3'); assert.ok(doc.querySelector('#lo3-ind #lo3-ind-ind'), 'индикаторы ЛО3');
@@ -206,6 +208,9 @@ test('ch3 Волна 2: §21 + ЛО4 + §22 + ПР3 + финал главы мо
const { doc, errors } = await loadDom('chemistry_7_ch3.html'); const { doc, errors } = await loadDom('chemistry_7_ch3.html');
doc.defaultView.goTo('p21'); await wait(100); doc.defaultView.goTo('p21'); await wait(100);
assert.ok(doc.querySelector('#p21-act .act-cell'), 'ряд активности §21'); assert.ok(doc.querySelector('#p21-act .act-cell'), 'ряд активности §21');
// клик по Zn (левее H₂) → пузырьки H₂
doc.querySelector('#p21-act .act-cell[data-i="5"]').dispatchEvent(new doc.defaultView.Event('click', { bubbles: true })); await wait(40);
assert.ok(doc.querySelector('#p21-tube div'), 'пузырьки H₂ при реакции металла с кислотой §21');
doc.defaultView.goTo('lo4'); await wait(100); doc.defaultView.goTo('lo4'); await wait(100);
assert.ok(doc.querySelector('#lo4-rx #lo4-go'), 'опыт металл+кислота ЛО4'); assert.ok(doc.querySelector('#lo4-rx #lo4-go'), 'опыт металл+кислота ЛО4');
doc.defaultView.goTo('p22'); await wait(100); doc.defaultView.goTo('p22'); await wait(100);
+25 -10
View File
@@ -35,14 +35,21 @@
]; ];
function mount_p19() { function mount_p19() {
var m = $('p19-rx'); if (!m || m._built) return; m._built = 1; var m = $('p19-rx'); if (!m || m._built) return; m._built = 1;
var idx = 0; var idx = 0, anim = null;
function stopAnim(){ if(anim){anim.stop();anim=null;} }
function render(){ function render(){
stopAnim();
var r = RX[idx]; var r = RX[idx];
var swatch = idx===1 ? '<div style="margin-top:6px"><span style="display:inline-block;width:46px;height:20px;background:#1f2937;border-radius:4px;vertical-align:middle"></span> CuO (чёрный) &rarr; <span style="display:inline-block;width:46px;height:20px;background:#b45309;border-radius:4px;vertical-align:middle"></span> Cu (красный)</div>' : '';
m.innerHTML = '<div class="fld"><label>Реакция</label><select id="p19-pick">' m.innerHTML = '<div class="fld"><label>Реакция</label><select id="p19-pick">'
+ RX.map(function(x,i){ return '<option value="'+i+'"'+(i===idx?' selected':'')+'>'+esc(x.name)+'</option>'; }).join('') + '</select></div>' + RX.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) + '</div><div style="font-size:.86rem;color:var(--muted);margin-top:6px">' + esc(r.note) + '</div>' + swatch + '</div>'; + '<div id="p19-stage" style="margin:8px 0"></div>'
$('p19-pick').addEventListener('change', function(e){ idx=+e.target.value; m._built=0; render(); }); + '<div class="out ok" style="margin-top:8px"><div style="font-size:1.05rem">' + ceq(r.eq) + '</div><div style="font-size:.86rem;color:var(--muted);margin-top:6px">' + esc(r.note) + '</div></div>';
var stage = $('p19-stage');
if (stage && W.Chem7Anim) {
if (idx === 1) anim = W.Chem7Anim.colorBlock(stage, '#1f2937', '#b45309', 'CuO (чёрный) → Cu (красная медь)', 1800);
else anim = W.Chem7Anim.flameBox(stage, { color: '#93c5fd' });
}
$('p19-pick').addEventListener('change', function(e){ idx=+e.target.value; render(); });
} }
render(); render();
} }
@@ -60,17 +67,20 @@
}; };
function indicatorWidget(mountId, withAcidPick) { function indicatorWidget(mountId, withAcidPick) {
var m = $(mountId); if (!m || m._built) return; m._built = 1; var m = $(mountId); if (!m || m._built) return; m._built = 1;
var ind = 'Лакмус', acid = 0; var ind = 'Лакмус', acid = 0, anim = null;
function strip(color){ return '<div style="width:120px;height:34px;border-radius:8px;border:1.5px solid var(--border);background:'+color+';display:inline-block;vertical-align:middle"></div>'; } function strip(color){ return '<div style="width:120px;height:34px;border-radius:8px;border:1.5px solid var(--border);background:'+color+';display:inline-block;vertical-align:middle"></div>'; }
function render(){ function render(){
if (anim) { anim.stop(); anim = null; }
var a = ACIDS[acid], col = INDIC[ind]; var a = ACIDS[acid], col = INDIC[ind];
m.innerHTML = '<div class="fld"><label>Индикатор</label><select id="'+mountId+'-ind">' m.innerHTML = '<div class="fld"><label>Индикатор</label><select id="'+mountId+'-ind">'
+ Object.keys(INDIC).map(function(k){ return '<option'+(k===ind?' selected':'')+'>'+k+'</option>'; }).join('') + '</select>' + Object.keys(INDIC).map(function(k){ return '<option'+(k===ind?' selected':'')+'>'+k+'</option>'; }).join('') + '</select>'
+ (withAcidPick ? '<label>Кислота</label><select id="'+mountId+'-acid">' + ACIDS.map(function(x,i){ return '<option value="'+i+'"'+(i===acid?' selected':'')+'>'+fml(x.f)+' ('+x.name+')</option>'; }).join('') + '</select>' : '') + '</div>' + (withAcidPick ? '<label>Кислота</label><select id="'+mountId+'-acid">' + ACIDS.map(function(x,i){ return '<option value="'+i+'"'+(i===acid?' selected':'')+'>'+fml(x.f)+' ('+x.name+')</option>'; }).join('') + '</select>' : '') + '</div>'
+ '<div id="'+mountId+'-drop" style="margin-top:8px"></div>'
+ '<div class="out ok" style="margin-top:8px">В нейтральной среде: ' + strip(col.neutral[0]) + ' <b>'+col.neutral[1]+'</b><br>' + '<div class="out ok" style="margin-top:8px">В нейтральной среде: ' + strip(col.neutral[0]) + ' <b>'+col.neutral[1]+'</b><br>'
+ 'В кислоте' + (withAcidPick?(' ('+fml(a.f)+')'):'') + ': ' + strip(col.acid[0]) + ' <b>'+col.acid[1]+'</b></div>'; + 'В кислоте' + (withAcidPick?(' ('+fml(a.f)+')'):'') + ': ' + strip(col.acid[0]) + ' <b>'+col.acid[1]+'</b></div>';
$(mountId+'-ind').addEventListener('change', function(e){ ind=e.target.value; m._built=0; render(); }); if (W.Chem7Anim) anim = W.Chem7Anim.colorBlock($(mountId+'-drop'), col.neutral[0], col.acid[0], ind + ' в кислоте → ' + col.acid[1], 900);
if (withAcidPick) $(mountId+'-acid').addEventListener('change', function(e){ acid=+e.target.value; m._built=0; render(); }); $(mountId+'-ind').addEventListener('change', function(e){ ind=e.target.value; render(); });
if (withAcidPick) $(mountId+'-acid').addEventListener('change', function(e){ acid=+e.target.value; render(); });
} }
render(); render();
} }
@@ -87,21 +97,26 @@
var ROW = ['K','Ca','Na','Mg','Al','Zn','Fe','Ni','Sn','Pb','H','Cu','Hg','Ag','Pt','Au']; var ROW = ['K','Ca','Na','Mg','Al','Zn','Fe','Ni','Sn','Pb','H','Cu','Hg','Ag','Pt','Au'];
function mount_p21() { function mount_p21() {
var m = $('p21-act'); if (!m || m._built) return; m._built = 1; var m = $('p21-act'); if (!m || m._built) return; m._built = 1;
var hIdx = ROW.indexOf('H'); var hIdx = ROW.indexOf('H'), anim = null;
function stopAnim(){ if(anim){anim.stop();anim=null;} }
m.innerHTML = '<div style="display:flex;flex-wrap:wrap;gap:4px">' m.innerHTML = '<div style="display:flex;flex-wrap:wrap;gap:4px">'
+ ROW.map(function(el,i){ var isH=el==='H'; return '<button class="act-cell" data-i="'+i+'" style="padding:6px 9px;border-radius:7px;border:1.5px solid '+(isH?'#dc2626':'var(--border)')+';background:'+(isH?'#fee2e2':'var(--card)')+';color:var(--text);font-weight:700;cursor:'+(isH?'default':'pointer')+'">'+(isH?'H₂':el)+'</button>'; }).join('') + '</div>' + ROW.map(function(el,i){ var isH=el==='H'; return '<button class="act-cell" data-i="'+i+'" style="padding:6px 9px;border-radius:7px;border:1.5px solid '+(isH?'#dc2626':'var(--border)')+';background:'+(isH?'#fee2e2':'var(--card)')+';color:var(--text);font-weight:700;cursor:'+(isH?'default':'pointer')+'">'+(isH?'H₂':el)+'</button>'; }).join('') + '</div>'
+ '<div style="font-size:.8rem;color:var(--muted);margin-top:4px">Слева активность убывает вправо. Граница — водород H₂.</div>' + '<div style="font-size:.8rem;color:var(--muted);margin-top:4px">Слева активность убывает вправо. Граница — водород H₂. Кликни металл — «опусти» его в кислоту.</div>'
+ '<div id="p21-tube" style="margin-top:8px"></div>'
+ '<div class="out" id="p21-act-out" style="margin-top:8px">Кликни по металлу — узнаешь, вытесняет ли он водород из кислоты.</div>'; + '<div class="out" id="p21-act-out" style="margin-top:8px">Кликни по металлу — узнаешь, вытесняет ли он водород из кислоты.</div>';
var out = $('p21-act-out'); var out = $('p21-act-out');
m.querySelectorAll('.act-cell').forEach(function(b){ m.querySelectorAll('.act-cell').forEach(function(b){
b.addEventListener('click', function(){ b.addEventListener('click', function(){
var i=+b.dataset.i, el=ROW[i]; if(el==='H'){ out.className='out'; out.innerHTML='<b>Водород H₂</b> — граница ряда активности.'; return; } var i=+b.dataset.i, el=ROW[i], tube=$('p21-tube'); stopAnim();
if(el==='H'){ out.className='out'; out.innerHTML='<b>Водород H₂</b> — граница ряда активности.'; if(tube)tube.innerHTML=''; return; }
out.className='out ok'; out.className='out ok';
if(i<hIdx){ if(i<hIdx){
var extra = (i<=2) ? ' <span style="color:#dc2626">Внимание: очень активный металл — с кислотами реагирует бурно (для получения водорода используют Zn, Fe).</span>' : ''; var extra = (i<=2) ? ' <span style="color:#dc2626">Внимание: очень активный металл — с кислотами реагирует бурно (для получения водорода используют Zn, Fe).</span>' : '';
out.innerHTML = '<b>'+el+'</b> стоит левее H₂ → <b>вытесняет водород</b> из соляной и серной кислот: образуются соль и $H_2\\uparrow$.'+extra; out.innerHTML = '<b>'+el+'</b> стоит левее H₂ → <b>вытесняет водород</b> из соляной и серной кислот: образуются соль и $H_2\\uparrow$.'+extra;
if (tube && W.Chem7Anim) anim = W.Chem7Anim.bubbleField(tube, { color:'rgba(255,255,255,.85)', h:96 });
} else { } else {
out.innerHTML = '<b>'+el+'</b> стоит правее H₂ → водород из кислот <b>не вытесняет</b> (например, медь и серебро с этими кислотами не реагируют).'; out.innerHTML = '<b>'+el+'</b> стоит правее H₂ → водород из кислот <b>не вытесняет</b> (например, медь и серебро с этими кислотами не реагируют).';
if (tube) tube.innerHTML = '<div class="out" style="text-align:center;color:var(--muted)">реакция не идёт — пузырьков нет</div>';
} }
if (W.chem8RenderMath) try { W.chem8RenderMath(out); } catch(e){} if (W.chem8RenderMath) try { W.chem8RenderMath(out); } catch(e){}
}); });
+1
View File
@@ -23,6 +23,7 @@ html.dark{--bg:#140a24;--border:#3b2a63;--pri-soft:rgba(124,58,237,.18);--sec-ac
<script src="/js/biochem-core.js" defer></script> <script src="/js/biochem-core.js" defer></script>
<script src="/js/chem8_svg.js" defer></script> <script src="/js/chem8_svg.js" defer></script>
<script src="/js/chem7_svg.js" defer></script> <script src="/js/chem7_svg.js" defer></script>
<script src="/js/chem7_anim.js" defer></script>
<script src="/js/chem7_ch3_widgets.js" defer></script> <script src="/js/chem7_ch3_widgets.js" defer></script>
<script src="/js/chem8_engine.js" defer></script> <script src="/js/chem8_engine.js" defer></script>
</head> </head>