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:
@@ -131,6 +131,7 @@ test('ch1 Волна 3: интерактивы §7–§9 монтируются
|
|||||||
assert.match(doc.querySelector('#p8-out').textContent, /100/, 'M_r(CaCO3)=100');
|
assert.match(doc.querySelector('#p8-out').textContent, /100/, 'M_r(CaCO3)=100');
|
||||||
doc.defaultView.goTo('p9'); await wait(100);
|
doc.defaultView.goTo('p9'); await wait(100);
|
||||||
assert.ok(doc.querySelector('#p9-bld #p9-a'), 'конструктор валентности §9');
|
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.match(doc.querySelector('#p9-bout').textContent, /Al/, 'формула по валентности построена');
|
||||||
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
|
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
|
||||||
});
|
});
|
||||||
@@ -145,6 +146,7 @@ test('ch1 Волна 4: §10–§12 + ЛО1 + финал главы монтир
|
|||||||
assert.ok(doc.querySelector('#p11-bal svg'), 'весы сохранения массы §11');
|
assert.ok(doc.querySelector('#p11-bal svg'), 'весы сохранения массы §11');
|
||||||
doc.defaultView.goTo('p12'); await wait(120);
|
doc.defaultView.goTo('p12'); await wait(120);
|
||||||
assert.ok(doc.querySelector('#p12-mount').childElementCount > 0, 'балансировщик §12');
|
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);
|
doc.defaultView.goTo('final1'); await wait(120);
|
||||||
assert.ok(doc.querySelectorAll('#navDotsfinal1 .nav-dot').length >= 6, 'боссы финала главы');
|
assert.ok(doc.querySelectorAll('#navDotsfinal1 .nav-dot').length >= 6, 'боссы финала главы');
|
||||||
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
|
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
|
||||||
|
|||||||
@@ -264,9 +264,40 @@
|
|||||||
return { stop: function () { try { host.innerHTML = ''; host.style.height = ''; } catch (e) {} }, el: b };
|
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 += '<line x1="' + sx + '" y1="' + sy.toFixed(1) + '" x2="' + ex + '" y2="' + ey.toFixed(1) + '" stroke="#94a3b8" stroke-width="3" stroke-linecap="round" stroke-dasharray="' + len.toFixed(1) + '" stroke-dashoffset="' + (HEADLESS ? 0 : len.toFixed(1)) + '" style="transition:stroke-dashoffset .5s ease ' + (t * 0.08).toFixed(2) + 's"/>';
|
||||||
|
}
|
||||||
|
function atom(x, y, el, col) {
|
||||||
|
return '<circle cx="' + x + '" cy="' + y.toFixed(1) + '" r="' + r + '" fill="' + col + '" stroke="rgba(0,0,0,.2)"/>'
|
||||||
|
+ '<text x="' + x + '" y="' + (y + 4).toFixed(1) + '" text-anchor="middle" font-size="13" font-weight="700" fill="#fff">' + el + '</text>';
|
||||||
|
}
|
||||||
|
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 = '<svg viewBox="0 0 ' + W0 + ' ' + H0 + '" width="100%" style="max-width:' + W0 + 'px;height:auto">' + bonds + atoms + '</svg>';
|
||||||
|
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 = {
|
W.Chem7Anim = {
|
||||||
HEADLESS: HEADLESS, reduced: reduced, ease: ease, loop: loop, sceneCanvas: sceneCanvas,
|
HEADLESS: HEADLESS, reduced: reduced, ease: ease, loop: loop, sceneCanvas: sceneCanvas,
|
||||||
molecule3d: molecule3d, separation: separation, colorMorph: colorMorph, confettiSmall: confettiSmall, observeVisible: observeVisible,
|
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);
|
})(window);
|
||||||
|
|||||||
@@ -295,23 +295,30 @@
|
|||||||
function gcd(a, b) { return b ? gcd(b, a % b) : a; }
|
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 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 VB = [ ['O', 2], ['Cl', 1], ['S', 2] ];
|
||||||
|
var BCOL = { O:'#ef4444', Cl:'#22c55e', S:'#eab308' };
|
||||||
function mount_p9() {
|
function mount_p9() {
|
||||||
var m = $('p9-bld'); if (!m || m._built) return; m._built = 1;
|
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 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(''); }
|
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>'
|
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() {
|
function upd() {
|
||||||
var a = VA[+$('p9-a').value], b = VB[+$('p9-b').value];
|
var a = VA[+$('p9-a').value], b = VB[+$('p9-b').value];
|
||||||
var lcm = a[1] * b[1] / gcd(a[1], b[1]);
|
var lcm = a[1] * b[1] / gcd(a[1], b[1]);
|
||||||
var ia = lcm / a[1], ib = lcm / b[1];
|
var ia = lcm / a[1], ib = lcm / b[1];
|
||||||
var raw = a[0] + (ia > 1 ? ia : '') + b[0] + (ib > 1 ? ib : '');
|
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';
|
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>'
|
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>'
|
+ 'Каждая чёрточка-связь соединена — все валентности заняты.<br>'
|
||||||
+ 'Индексы: ' + a[0] + ' → ' + ia + ', ' + b[0] + ' → ' + ib + '<br>'
|
+ 'НОК валентностей = <b>' + lcm + '</b>; индексы: ' + a[0] + ' → ' + ia + ', ' + b[0] + ' → ' + ib + '<br>'
|
||||||
+ 'Формула: <b style="font-size:1.15rem">' + (C().formula ? C().formula(raw) : raw) + '</b><br>'
|
+ 'Формула: <b style="font-size:1.15rem">' + (C().formula ? C().formula(raw) : raw) + '</b></span>';
|
||||||
+ 'Проверка: ' + ia + '·' + a[1] + ' = ' + ib + '·' + b[1] + ' = ' + lcm + ' единиц валентности — совпало.</span>';
|
|
||||||
}
|
}
|
||||||
$('p9-a').addEventListener('change', upd); $('p9-b').addEventListener('change', upd); upd();
|
$('p9-a').addEventListener('change', upd); $('p9-b').addEventListener('change', upd); upd();
|
||||||
}
|
}
|
||||||
@@ -394,10 +401,34 @@
|
|||||||
render();
|
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() {
|
function mount_p12() {
|
||||||
var pick = $('p12-pick'), mount = $('p12-mount'); if (!pick || pick._built || !C().equationBalancer) return; pick._built = 1;
|
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 ? '✓ Число атомов каждого элемента слева и справа <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();
|
pick.addEventListener('change', build); build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user