feat(chemistry7): визуал V1-хвост — §9 валентные связи + §12 подсчёт атомов

§9: добавлена схема «связей-крючков» (Chem7Anim.valenceLink, SVG) — атомы A и B
с чёрточками валентности, связи прорисовываются (draw-in); число связей = НОК.
§12: под балансировщиком — анимированный подсчёт атомов (реагенты vs продукты),
атомы-точки появляются масштабированием; подтверждается баланс слева=справа.

Все интерактивы Химии 7 анимированы. Тесты chem7: 16/16; полный прогон 162/165.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-30 20:07:06 +03:00
parent 639f985e6f
commit ac6552b44f
3 changed files with 72 additions and 8 deletions
+38 -7
View File
@@ -295,23 +295,30 @@
function gcd(a, b) { return b ? gcd(b, a % b) : a; }
var VA = [ ['Na', 1], ['K', 1], ['H', 1], ['Mg', 2], ['Ca', 2], ['Zn', 2], ['Cu', 2], ['Al', 3], ['C', 4] ];
var VB = [ ['O', 2], ['Cl', 1], ['S', 2] ];
var BCOL = { O:'#ef4444', Cl:'#22c55e', S:'#eab308' };
function mount_p9() {
var m = $('p9-bld'); if (!m || m._built) return; m._built = 1;
var vanim = null;
function optA(){ return VA.map(function(e,i){ return '<option value="'+i+'"'+(e[0]==='Al'?' selected':'')+'>'+e[0]+' (валентность '+'I'.repeat(e[1]).replace('IIII','IV')+')</option>'; }).join(''); }
function optB(){ return VB.map(function(e,i){ return '<option value="'+i+'">'+e[0]+' (валентность '+'I'.repeat(e[1])+')</option>'; }).join(''); }
m.innerHTML = '<div class="fld"><label>Элемент A</label><select id="p9-a">'+optA()+'</select>'
+'<label>Элемент B</label><select id="p9-b">'+optB()+'</select></div><div class="out" id="p9-bout"></div>';
+'<label>Элемент B</label><select id="p9-b">'+optB()+'</select></div>'
+'<div id="p9-vis" style="margin:8px 0;display:flex;justify-content:center"></div>'
+'<div class="out" id="p9-bout"></div>';
function upd() {
var a = VA[+$('p9-a').value], b = VB[+$('p9-b').value];
var lcm = a[1] * b[1] / gcd(a[1], b[1]);
var ia = lcm / a[1], ib = lcm / b[1];
var raw = a[0] + (ia > 1 ? ia : '') + b[0] + (ib > 1 ? ib : '');
if (vanim) { vanim.stop(); vanim = null; }
if (W.Chem7Anim) vanim = W.Chem7Anim.valenceLink($('p9-vis'), {
a: { el:a[0], val:a[1], n:ia, color:'#6366f1' },
b: { el:b[0], val:b[1], n:ib, color:BCOL[b[0]] || '#ef4444' } });
var out = $('p9-bout'); out.className = 'out ok';
out.innerHTML = '<span class="bd">Валентности: ' + a[0] + ' = ' + 'I'.repeat(a[1]).replace('IIII','IV') + ', ' + b[0] + ' = ' + 'I'.repeat(b[1]) + '<br>'
+ 'Наименьшее общее кратное валентностей = <b>' + lcm + '</b><br>'
+ 'Индексы: ' + a[0] + ' → ' + ia + ', ' + b[0] + ' → ' + ib + '<br>'
+ 'Формула: <b style="font-size:1.15rem">' + (C().formula ? C().formula(raw) : raw) + '</b><br>'
+ 'Проверка: ' + ia + '·' + a[1] + ' = ' + ib + '·' + b[1] + ' = ' + lcm + ' единиц валентности — совпало.</span>';
+ 'Каждая чёрточка-связь соединена — все валентности заняты.<br>'
+ 'НОК валентностей = <b>' + lcm + '</b>; индексы: ' + a[0] + ' → ' + ia + ', ' + b[0] + ' → ' + ib + '<br>'
+ 'Формула: <b style="font-size:1.15rem">' + (C().formula ? C().formula(raw) : raw) + '</b></span>';
}
$('p9-a').addEventListener('change', upd); $('p9-b').addEventListener('change', upd); upd();
}
@@ -394,10 +401,34 @@
render();
}
/* §12 — балансировщик уравнений (переиспользуем Chem8.equationBalancer) */
/* §12 — балансировщик + анимированный подсчёт атомов (слева/справа) */
var ELC = { H:'#cbd5e1', O:'#ef4444', C:'#334155', N:'#3b82f6', S:'#eab308', Fe:'#b45309', P:'#f97316', Cl:'#22c55e', Mg:'#22c55e', Ca:'#a78bfa', Na:'#a78bfa', Cu:'#ea580c', Zn:'#64748b', Al:'#6366f1', K:'#a78bfa' };
function mount_p12() {
var pick = $('p12-pick'), mount = $('p12-mount'); if (!pick || pick._built || !C().equationBalancer) return; pick._built = 1;
function build() { var parts = pick.value.split('|'); C().equationBalancer(mount, { skeleton: parts[0], solution: parts[1].split(',').map(Number) }); }
if (!$('p12-tally')) mount.insertAdjacentHTML('afterend', '<div id="p12-tally" style="margin-top:10px"></div>');
function sumSide(list, coeffs, off) {
var tot = {};
list.forEach(function (sp, i) { var cnt = C().elementCounts ? C().elementCounts(sp) : {}; var co = coeffs[off + i] || 1; for (var e in cnt) tot[e] = (tot[e] || 0) + cnt[e] * co; });
return tot;
}
function dots(el, n) { var s = ''; for (var i = 0; i < n; i++) s += '<span class="c7-atom" style="display:inline-block;width:13px;height:13px;border-radius:50%;margin:1px;background:' + (ELC[el] || '#94a3b8') + ';transform:scale(.2);opacity:0;transition:transform .3s ease,opacity .3s ease"></span>'; return s; }
function col(title, tot) { return '<div style="flex:1;min-width:140px"><div style="font-weight:700;font-size:.82rem;margin-bottom:4px">' + title + '</div>' + Object.keys(tot).map(function (e) { return '<div style="display:flex;align-items:center;gap:6px;margin:2px 0"><b style="width:26px">' + e + '</b>' + dots(e, tot[e]) + '<span style="color:var(--muted);font-size:.8rem">× ' + tot[e] + '</span></div>'; }).join('') + '</div>'; }
function tally(skeleton, coeffs) {
var t = $('p12-tally'); if (!t) return;
var sides = skeleton.split(/->|=/);
var L = sides[0].split('+').map(function (s) { return s.trim(); });
var Rr = (sides[1] || '').split('+').map(function (s) { return s.trim(); });
var left = sumSide(L, coeffs, 0), right = sumSide(Rr, coeffs, L.length);
var ok = Object.keys(left).every(function (e) { return left[e] === right[e]; }) && Object.keys(right).every(function (e) { return left[e] === right[e]; });
t.innerHTML = '<div style="display:flex;gap:14px;flex-wrap:wrap">' + col('Реагенты — атомы', left) + col('Продукты — атомы', right) + '</div>'
+ '<div class="out ' + (ok ? 'ok' : 'bad') + '" style="margin-top:6px">' + (ok ? '&#10003; Число атомов каждого элемента слева и справа <b>совпадает</b> — уравнение сбалансировано.' : 'Атомы не уравнены.') + '</div>';
if (W.Chem7Anim && !W.Chem7Anim.HEADLESS) {
var a = t.querySelectorAll('.c7-atom');
a.forEach(function (d, i) { d.style.transitionDelay = (i * 28) + 'ms'; });
W.requestAnimationFrame(function () { W.requestAnimationFrame(function () { a.forEach(function (d) { d.style.transform = 'scale(1)'; d.style.opacity = '1'; }); }); });
}
}
function build() { var parts = pick.value.split('|'); var coeffs = parts[1].split(',').map(Number); C().equationBalancer(mount, { skeleton: parts[0], solution: coeffs }); tally(parts[0], coeffs); }
pick.addEventListener('change', build); build();
}