diff --git a/backend/tests/chemistry7-page.test.js b/backend/tests/chemistry7-page.test.js
index e16db67..bfe152c 100644
--- a/backend/tests/chemistry7-page.test.js
+++ b/backend/tests/chemistry7-page.test.js
@@ -131,6 +131,7 @@ test('ch1 Волна 3: интерактивы §7–§9 монтируются
assert.match(doc.querySelector('#p8-out').textContent, /100/, 'M_r(CaCO3)=100');
doc.defaultView.goTo('p9'); await wait(100);
assert.ok(doc.querySelector('#p9-bld #p9-a'), 'конструктор валентности §9');
+ assert.ok(doc.querySelector('#p9-vis svg circle'), 'схема валентных связей §9');
assert.match(doc.querySelector('#p9-bout').textContent, /Al/, 'формула по валентности построена');
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
});
@@ -145,6 +146,7 @@ test('ch1 Волна 4: §10–§12 + ЛО1 + финал главы монтир
assert.ok(doc.querySelector('#p11-bal svg'), 'весы сохранения массы §11');
doc.defaultView.goTo('p12'); await wait(120);
assert.ok(doc.querySelector('#p12-mount').childElementCount > 0, 'балансировщик §12');
+ assert.ok(doc.querySelector('#p12-tally .c7-atom'), 'подсчёт атомов §12 (летящие атомы)');
doc.defaultView.goTo('final1'); await wait(120);
assert.ok(doc.querySelectorAll('#navDotsfinal1 .nav-dot').length >= 6, 'боссы финала главы');
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
diff --git a/frontend/js/chem7_anim.js b/frontend/js/chem7_anim.js
index cc47dfc..dcbba7f 100644
--- a/frontend/js/chem7_anim.js
+++ b/frontend/js/chem7_anim.js
@@ -264,9 +264,40 @@
return { stop: function () { try { host.innerHTML = ''; host.style.height = ''; } catch (e) {} }, el: b };
}
+ /* ---- валентные «крючки»: атомы A и B с чёрточками-связями, соединяющимися (§9) ---- */
+ function valenceLink(host, spec) {
+ var ns = 'http://www.w3.org/2000/svg';
+ var na = spec.a.n, nb = spec.b.n, va = spec.a.val, vb = spec.b.val, lcm = va * na;
+ var W0 = 300, r = 17, lx = 50, rx = W0 - 50;
+ var H0 = Math.max(na, nb, 1) * 50 + 20;
+ function ycol(n, k) { var gap = 50, top = (H0 - (n - 1) * gap) / 2; return top + k * gap; }
+ function spread(idx, val) { return (idx - (val - 1) / 2) * 9; }
+ var colA = spec.a.color || '#6366f1', colB = spec.b.color || '#ef4444';
+ var bonds = '';
+ for (var t = 0; t < lcm; t++) {
+ var la = Math.floor(t / va), rb = Math.floor(t / vb);
+ var sy = ycol(na, la) + spread(t % va, va), ey = ycol(nb, rb) + spread(t % vb, vb);
+ var sx = lx + r, ex = rx - r, len = Math.hypot(ex - sx, ey - sy);
+ bonds += '';
+ }
+ function atom(x, y, el, col) {
+ return ''
+ + '' + el + '';
+ }
+ var atoms = '';
+ for (var i = 0; i < na; i++) atoms += atom(lx, ycol(na, i), spec.a.el, colA);
+ for (var j = 0; j < nb; j++) atoms += atom(rx, ycol(nb, j), spec.b.el, colB);
+ host.innerHTML = '';
+ if (!HEADLESS && !reduced()) {
+ var lines = host.querySelectorAll('line');
+ W.requestAnimationFrame(function () { W.requestAnimationFrame(function () { lines.forEach(function (l) { l.setAttribute('stroke-dashoffset', '0'); }); }); });
+ }
+ return { stop: function () { try { host.innerHTML = ''; } catch (e) {} } };
+ }
+
W.Chem7Anim = {
HEADLESS: HEADLESS, reduced: reduced, ease: ease, loop: loop, sceneCanvas: sceneCanvas,
molecule3d: molecule3d, separation: separation, colorMorph: colorMorph, confettiSmall: confettiSmall, observeVisible: observeVisible,
- bubbleField: bubbleField, precipField: precipField, flameBox: flameBox, colorBlock: colorBlock
+ bubbleField: bubbleField, precipField: precipField, flameBox: flameBox, colorBlock: colorBlock, valenceLink: valenceLink
};
})(window);
diff --git a/frontend/js/chem7_ch1_widgets.js b/frontend/js/chem7_ch1_widgets.js
index 107610a..78ec135 100644
--- a/frontend/js/chem7_ch1_widgets.js
+++ b/frontend/js/chem7_ch1_widgets.js
@@ -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 ''; }).join(''); }
function optB(){ return VB.map(function(e,i){ return ''; }).join(''); }
m.innerHTML = '
'
- +'
';
+ +''
+ +''
+ +'';
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 = 'Валентности: ' + a[0] + ' = ' + 'I'.repeat(a[1]).replace('IIII','IV') + ', ' + b[0] + ' = ' + 'I'.repeat(b[1]) + '
'
- + 'Наименьшее общее кратное валентностей = ' + lcm + '
'
- + 'Индексы: ' + a[0] + ' → ' + ia + ', ' + b[0] + ' → ' + ib + '
'
- + 'Формула: ' + (C().formula ? C().formula(raw) : raw) + '
'
- + 'Проверка: ' + ia + '·' + a[1] + ' = ' + ib + '·' + b[1] + ' = ' + lcm + ' единиц валентности — совпало.';
+ + 'Каждая чёрточка-связь соединена — все валентности заняты.
'
+ + 'НОК валентностей = ' + lcm + '; индексы: ' + a[0] + ' → ' + ia + ', ' + b[0] + ' → ' + ib + '
'
+ + 'Формула: ' + (C().formula ? C().formula(raw) : raw) + '';
}
$('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', '');
+ 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 += ''; return s; }
+ function col(title, tot) { return '' + title + '
' + Object.keys(tot).map(function (e) { return '
' + e + '' + dots(e, tot[e]) + '× ' + tot[e] + '
'; }).join('') + '
'; }
+ 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 = '' + col('Реагенты — атомы', left) + col('Продукты — атомы', right) + '
'
+ + '' + (ok ? '✓ Число атомов каждого элемента слева и справа совпадает — уравнение сбалансировано.' : 'Атомы не уравнены.') + '
';
+ 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();
}