Files
Learn_System/frontend/js/chem7_ch1_widgets.js
Maxim Dolgolyov ac6552b44f 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>
2026-05-30 20:07:06 +03:00

443 lines
31 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* chem7_ch1_widgets.js — интерактивы главы 1 «Первоначальные химические понятия» (Химия 7).
* Монтируются движком chem8_engine.js: window.CHEM8_WIDGETS[id] / window.FLAG_MOUNTS[id].
* Используют window.Chem8 (chem8_svg.js): molarMass, elementCounts, arOf, fmt, equationBalancer.
* Без эмоджи; KaTeX-рендер — через window.chem8RenderMath.
*/
(function (W) {
'use strict';
function C() { return W.Chem8 || {}; }
function $(id) { return document.getElementById(id); }
function esc(s){ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
/* ── общий мини-классификатор: чипы → 2 корзины ──────────────────── */
function classifier(mount, opts) {
if (!mount || mount._built) return; mount._built = 1;
var items = opts.items.slice(); // {t, b} t=подпись, b=индекс корзины
var buckets = opts.buckets; // [name0, name1]
var placed = {}; // idx -> bucket
var pool = items.map(function (it, i) { return i; });
function colorOf(ok){ return ok ? '#059669' : '#dc2626'; }
function render() {
var chips = pool.map(function (i) {
return '<button class="c7-chip" data-i="' + i + '" style="padding:7px 12px;border:1.5px solid var(--border);border-radius:9px;background:var(--card);color:var(--text);font-weight:600;font-size:.9rem;cursor:pointer;margin:3px">' + esc(items[i].t) + '</button>';
}).join('') || '<span style="color:var(--muted)">Все карточки распределены.</span>';
var cols = buckets.map(function (name, bi) {
var inb = Object.keys(placed).filter(function (k) { return placed[k] === bi; });
var cells = inb.map(function (k) {
var ok = items[k].b === bi;
return '<div style="padding:5px 10px;margin:3px 0;border-radius:8px;font-size:.86rem;font-weight:600;color:#fff;background:' + colorOf(ok) + '">' + esc(items[k].t) + (ok ? ' &#10003;' : ' &#10007;') + '</div>';
}).join('') || '<div style="color:var(--muted);font-size:.82rem">перетащите сюда…</div>';
return '<div class="c7-bucket" data-b="' + bi + '" style="flex:1;min-width:140px;border:1.5px dashed var(--border);border-radius:11px;padding:10px;background:var(--pri-soft)"><div style="font-weight:700;margin-bottom:6px">' + esc(name) + '</div>' + cells + '</div>';
}).join('');
mount.innerHTML = '<div style="margin-bottom:10px">' + chips + '</div><div style="display:flex;gap:10px;flex-wrap:wrap">' + cols + '</div>'
+ '<div style="margin-top:8px;font-size:.84rem;color:var(--muted)">Кликни карточку, затем — корзину. Зелёный — верно, красный — ошибка.</div>';
bind();
}
var sel = null;
function bind() {
mount.querySelectorAll('.c7-chip').forEach(function (b) {
b.addEventListener('click', function () {
mount.querySelectorAll('.c7-chip').forEach(function (x) { x.style.outline = ''; });
sel = +b.dataset.i; b.style.outline = '2px solid var(--pri)';
});
});
mount.querySelectorAll('.c7-bucket').forEach(function (col) {
col.addEventListener('click', function () {
if (sel == null) return;
placed[sel] = +col.dataset.b;
pool = pool.filter(function (x) { return x !== sel; });
sel = null; render();
});
});
}
render();
}
/* §1 — классификатор «тело / вещество» */
function mount_p1() {
var m = $('p1-cls'); if (!m) return;
classifier(m, {
buckets: ['Физическое тело', 'Вещество'],
items: [
{ t: 'стакан', b: 0 }, { t: 'вода', b: 1 }, { t: 'гвоздь', b: 0 }, { t: 'железо', b: 1 },
{ t: 'ложка', b: 0 }, { t: 'сахар', b: 1 }, { t: 'медь', b: 1 }, { t: 'линейка', b: 0 }
]
});
}
/* §2 / ПР1 — разделитель смесей: выбери метод для смеси */
var MIX = [
{ mix: 'Песок и вода', method: 'Фильтрование', kind: 'filter', why: 'Песок не растворяется — задерживается фильтром, вода проходит.' },
{ mix: 'Соль и вода', method: 'Выпаривание', kind: 'evaporate', why: 'Вода испаряется, соль остаётся на дне.' },
{ mix: 'Железные опилки и сера', method: 'Магнит', kind: 'magnet', why: 'Железо притягивается магнитом, сера — нет.' },
{ mix: 'Вода и растительное масло', method: 'Отстаивание (делительная воронка)', kind: 'settle', why: 'Масло легче воды и не смешивается — слои разделяют.' },
{ mix: 'Спирт и вода', method: 'Перегонка (дистилляция)', kind: 'distill', why: 'У спирта и воды разные температуры кипения.' }
];
var METHODS = ['Фильтрование', 'Выпаривание', 'Магнит', 'Отстаивание (делительная воронка)', 'Перегонка (дистилляция)'];
function mount_sep(mountId) {
var m = $(mountId); if (!m || m._built) return; m._built = 1;
var idx = 0, anim = null;
function stopAnim() { if (anim) { anim.stop(); anim = null; } }
function render() {
stopAnim();
var cur = MIX[idx];
m.innerHTML = '<div class="fld"><label>Смесь</label><select id="' + mountId + '-pick">'
+ MIX.map(function (x, i) { return '<option value="' + i + '"' + (i === idx ? ' selected' : '') + '>' + esc(x.mix) + '</option>'; }).join('') + '</select></div>'
+ '<div style="margin:8px 0;font-weight:600">Каким способом разделить смесь «' + esc(cur.mix) + '»?</div>'
+ '<div style="display:flex;flex-wrap:wrap;gap:6px">' + METHODS.map(function (mt) {
return '<button class="c7-m btn" data-m="' + esc(mt) + '">' + esc(mt) + '</button>';
}).join('') + '</div>'
+ '<div class="out" id="' + mountId + '-out" style="margin-top:8px">Выбери способ разделения — при верном ответе увидишь анимацию.</div>'
+ '<div id="' + mountId + '-anim" style="margin-top:8px;display:flex;justify-content:center"></div>';
$(mountId + '-pick').addEventListener('change', function (e) { idx = +e.target.value; render(); });
var out = $(mountId + '-out');
m.querySelectorAll('.c7-m').forEach(function (b) {
b.addEventListener('click', function () {
var ok = b.dataset.m === cur.method;
out.className = 'out ' + (ok ? 'ok' : 'bad');
out.innerHTML = ok
? '<b>Верно!</b> ' + esc(cur.method) + '. ' + esc(cur.why)
: '<b>Не подходит.</b> Подумай, чем различаются вещества в смеси (растворимость, магнитные свойства, температура кипения, плотность).';
stopAnim();
var host = $(mountId + '-anim');
if (ok && W.Chem7Anim && host) anim = W.Chem7Anim.separation(host, cur.kind);
else if (host) host.innerHTML = '';
});
});
}
render();
}
function mount_p2() { mount_sep('p2-sep'); }
function mount_pr1() { mount_sep('pr1-sep'); }
/* §3 — каталог элементов + тренажёр «символ ↔ название» */
var EL = {
H: [1, 'Водород'], He: [2, 'Гелий'], Li: [3, 'Литий'], C: [6, 'Углерод'], N: [7, 'Азот'],
O: [8, 'Кислород'], F: [9, 'Фтор'], Na: [11, 'Натрий'], Mg: [12, 'Магний'], Al: [13, 'Алюминий'],
Si: [14, 'Кремний'], P: [15, 'Фосфор'], S: [16, 'Сера'], Cl: [17, 'Хлор'], K: [19, 'Калий'],
Ca: [20, 'Кальций'], Fe: [26, 'Железо'], Cu: [29, 'Медь'], Zn: [30, 'Цинк'], Ag: [47, 'Серебро']
};
function mount_p3() {
var grid = $('p3-el'), info = $('p3-elinfo');
if (grid && !grid._built) {
grid._built = 1;
Object.keys(EL).forEach(function (s) {
var ar = C().arOf ? C().arOf(s) : '';
var c = document.createElement('div'); c.className = 'el-cell';
c.innerHTML = '<span class="z">' + EL[s][0] + '</span><span class="s">' + s + '</span><span class="a">' + ar + '</span>';
c.addEventListener('click', function () {
grid.querySelectorAll('.el-cell').forEach(function (x) { x.classList.remove('on'); }); c.classList.add('on');
if (info) info.innerHTML = '<b>' + EL[s][1] + '</b> (' + s + ') &middot; порядковый номер Z = ' + EL[s][0] + ' &middot; A_r = ' + ar;
});
grid.appendChild(c);
});
}
/* drill: дан символ → выбери название */
var d = $('p3-drill'); if (d && !d._built) {
d._built = 1;
var keys = Object.keys(EL), order = keys.slice(), qi = 0, score = 0, total = 0;
function nextQ() {
var sym = order[qi % order.length];
var correct = EL[sym][1];
var opts = [correct];
while (opts.length < 4) { var r = EL[keys[(qi * 7 + opts.length * 13 + 3) % keys.length]][1]; if (opts.indexOf(r) < 0) opts.push(r); }
// детерминированная перестановка
opts = opts.sort(function (a, b) { return ((a.length + qi) % 3) - ((b.length + qi) % 3); });
d.innerHTML = '<div style="font-weight:700;margin-bottom:6px">Какому элементу соответствует символ <span style="font-family:var(--mono);color:var(--pri-d);font-size:1.1rem">' + sym + '</span>?</div>'
+ '<div style="display:flex;flex-wrap:wrap;gap:6px">' + opts.map(function (o) { return '<button class="c7-d btn" data-o="' + esc(o) + '">' + esc(o) + '</button>'; }).join('') + '</div>'
+ '<div class="out" id="p3-drill-out" style="margin-top:8px">Счёт: ' + score + ' из ' + total + '</div>';
var out = $('p3-drill-out');
d.querySelectorAll('.c7-d').forEach(function (b) {
b.addEventListener('click', function () {
total++; var ok = b.dataset.o === correct; if (ok) score++;
out.className = 'out ' + (ok ? 'ok' : 'bad');
out.innerHTML = (ok ? '<b>Верно!</b> ' : '<b>Нет.</b> ') + sym + ' — это ' + correct + '. Счёт: ' + score + ' из ' + total;
qi++;
setTimeout(nextQ, 850);
});
});
}
nextQ();
}
}
/* ── Волна 2 ── */
/* §4 — «весы атомов»: во сколько раз один атом тяжелее другого */
var ABEL = ['H','C','N','O','Na','Mg','Al','S','Cl','Ca','Fe','Cu','Zn','Ag'];
function mount_p4() {
var m = $('p4-bal'); if (!m || m._built) return; m._built = 1;
function opts(sel){ return ABEL.map(function(e){ var ar=C().arOf?C().arOf(e):''; return '<option value="'+e+'"'+(e===sel?' selected':'')+'>'+e+' (A_r='+ar+')</option>'; }).join(''); }
m.innerHTML = '<div class="fld"><label>Атом A</label><select id="p4-a">'+opts('S')+'</select>'
+'<label>Атом B</label><select id="p4-b">'+opts('O')+'</select></div><div class="out" id="p4-out"></div>';
function upd(){
var a=$('p4-a').value, b=$('p4-b').value, ara=+C().arOf(a), arb=+C().arOf(b);
var out=$('p4-out');
if(!ara||!arb){ out.textContent='—'; return; }
var big=Math.max(ara,arb), sm=Math.min(ara,arb), k=big/sm;
var heavier=ara>=arb?a:b, lighter=ara>=arb?b:a;
out.className='out ok';
out.innerHTML='<span class="bd">A_r('+a+')='+ara+', A_r('+b+')='+arb+'<br>'
+(ara===arb?('Атомы '+a+' и '+b+' имеют одинаковую массу.')
:('Атом <b>'+heavier+'</b> тяжелее атома '+lighter+' в <b>'+(Math.round(k*100)/100).toString().replace('.',',')+'</b> раз.'))+'</span>';
}
$('p4-a').addEventListener('change',upd); $('p4-b').addEventListener('change',upd); upd();
}
/* рисуем молекулу как набор шариков-атомов */
var COL = { H:'#cbd5e1', O:'#ef4444', N:'#3b82f6', C:'#334155', S:'#eab308', Cl:'#22c55e', Fe:'#b45309', Na:'#a78bfa' };
var RAD = { H:11, O:16, N:15, C:16, S:18, Cl:17, Fe:18, Na:17 };
function molBalls(atoms) {
// atoms: [[el,n],...]; рисуем в ряд
var balls = [], cx = 26;
atoms.forEach(function(pair){ for(var i=0;i<pair[1];i++){ balls.push(pair[0]); } });
var W0 = Math.max(120, cx*2 + balls.reduce(function(s,e){return s+(RAD[e]||14)*2+8;},0));
var x = cx, svg = '';
balls.forEach(function(el){
var r = RAD[el]||14; x += r;
svg += '<circle cx="'+x+'" cy="34" r="'+r+'" fill="'+(COL[el]||'#94a3b8')+'" stroke="rgba(0,0,0,.25)"/>'
+ '<text x="'+x+'" y="39" text-anchor="middle" font-size="13" font-weight="700" fill="#fff">'+el+'</text>';
x += r + 8;
});
return '<svg viewBox="0 0 '+W0+' 68" width="100%" style="max-width:'+W0+'px;height:auto">'+svg+'</svg>';
}
function molCard(name, formula, atoms, note) {
return '<div style="border:1.5px solid var(--border);border-radius:11px;padding:10px 12px;background:var(--card)">'
+'<div style="font-weight:700;margin-bottom:2px">'+esc(name)+' &middot; '+(C().formula?C().formula(formula):formula)+'</div>'
+ molBalls(atoms)
+'<div style="font-size:.82rem;color:var(--muted);margin-top:4px">'+esc(note)+'</div></div>';
}
/* 3D-модели молекул для §5/§6 (через Chem7Anim.molecule3d) */
var MOL = {
H2: { atoms:[{el:'H',x:-0.7,y:0,z:0},{el:'H',x:0.7,y:0,z:0}], bonds:[[0,1]] },
O2: { atoms:[{el:'O',x:-0.75,y:0,z:0},{el:'O',x:0.75,y:0,z:0}], bonds:[[0,1]] },
O3: { atoms:[{el:'O',x:0,y:0.45,z:0},{el:'O',x:-1.05,y:-0.4,z:0},{el:'O',x:1.05,y:-0.4,z:0}], bonds:[[0,1],[0,2]] },
N2: { atoms:[{el:'N',x:-0.7,y:0,z:0},{el:'N',x:0.7,y:0,z:0}], bonds:[[0,1]] },
H2O: { atoms:[{el:'O',x:0,y:0,z:0},{el:'H',x:-0.78,y:0.6,z:0},{el:'H',x:0.78,y:0.6,z:0}], bonds:[[0,1],[0,2]] },
CO2: { atoms:[{el:'C',x:0,y:0,z:0},{el:'O',x:-1.15,y:0,z:0},{el:'O',x:1.15,y:0,z:0}], bonds:[[0,1],[0,2]] },
CH4: { atoms:[{el:'C',x:0,y:0,z:0},{el:'H',x:0.63,y:0.63,z:0.63},{el:'H',x:-0.63,y:-0.63,z:0.63},{el:'H',x:-0.63,y:0.63,z:-0.63},{el:'H',x:0.63,y:-0.63,z:-0.63}], bonds:[[0,1],[0,2],[0,3],[0,4]] },
NH3: { atoms:[{el:'N',x:0,y:0.32,z:0},{el:'H',x:0.94,y:-0.3,z:0},{el:'H',x:-0.47,y:-0.3,z:0.82},{el:'H',x:-0.47,y:-0.3,z:-0.82}], bonds:[[0,1],[0,2],[0,3]] }
};
function fmlName(k) { return C().formula ? C().formula(k) : k; }
function molViewer(host, keys, caption) {
if (!host || host._built) return; host._built = 1;
var A = W.Chem7Anim;
if (!A || !A.molecule3d) { host.innerHTML = '<div class="out">3D-модели недоступны.</div>'; return; }
var cur = keys[0], handle = null;
function render() {
if (handle) handle.stop();
host.innerHTML = '<div class="fld" style="flex-wrap:wrap;gap:6px">'
+ keys.map(function (k) { return '<button class="btn mv-b' + (k === cur ? ' primary' : '') + '" data-k="' + k + '">' + fmlName(k) + '</button>'; }).join('') + '</div>'
+ '<div id="' + host.id + '-stage" style="display:flex;justify-content:center;padding:8px 0"></div>'
+ '<div class="out" style="margin-top:4px">' + caption + ' Перетаскивай модель мышью, чтобы повернуть.</div>';
handle = A.molecule3d($(host.id + '-stage'), MOL[cur]);
host.querySelectorAll('.mv-b').forEach(function (b) { b.addEventListener('click', function () { cur = b.dataset.k; render(); }); });
}
render();
}
/* §5 — 3D-модели простых веществ */
function mount_p5() { molViewer($('p5-gal'), ['H2', 'O2', 'O3', 'N2'], 'Простое вещество — атомы одного элемента.'); }
/* §6 — классификатор простое/сложное + 3D-модели сложных веществ */
function mount_p6() {
var c = $('p6-cls');
if (c) classifier(c, {
buckets: ['Простое вещество', 'Сложное вещество'],
items: [
{ t:'O₂', b:0 }, { t:'H₂O', b:1 }, { t:'Fe', b:0 }, { t:'CO₂', b:1 },
{ t:'N₂', b:0 }, { t:'NH₃', b:1 }, { t:'S', b:0 }, { t:'CH₄', b:1 }
]
});
molViewer($('p6-gal'), ['H2O', 'CO2', 'CH4', 'NH3'], 'Сложное вещество — атомы разных элементов.');
}
/* ── Волна 3 ── */
/* §7 — разбор химической формулы на состав */
function mount_p7() {
var inp = $('p7-in'), out = $('p7-out'), go = $('p7-go'); if (!inp || inp._built) return; inp._built = 1;
function calc() {
var f = inp.value.trim(), cnt = C().elementCounts ? C().elementCounts(f) : null;
if (!cnt || !Object.keys(cnt).length) { out.className = 'out bad'; out.textContent = 'Не удалось разобрать формулу. Проверь символы элементов (например, H2SO4).'; return; }
var els = Object.keys(cnt), tot = els.reduce(function (s, e) { return s + cnt[e]; }, 0);
out.className = 'out ok';
out.innerHTML = '<span class="bd"><b>' + (C().formula ? C().formula(f) : f) + '</b><br>'
+ 'Элементов: <b>' + els.length + '</b> (' + (els.length === 1 ? 'простое' : 'сложное') + ' вещество)<br>'
+ els.map(function (e) { return e + ': ' + cnt[e] + ' ' + (cnt[e] === 1 ? 'атом' : 'атома(ов)'); }).join('<br>')
+ '<br>Всего атомов в формуле: <b>' + tot + '</b></span>';
}
go.addEventListener('click', calc);
inp.addEventListener('keydown', function (e) { if (e.key === 'Enter') calc(); });
document.querySelectorAll('.p7-ex').forEach(function (b) { b.addEventListener('click', function () { inp.value = b.dataset.f; calc(); }); });
calc();
}
/* §8 — калькулятор относительной молекулярной массы M_r */
function mount_p8() {
var inp = $('p8-in'), out = $('p8-out'), go = $('p8-go'); if (!inp || inp._built) return; inp._built = 1;
function calc() {
var f = inp.value.trim(), cnt = C().elementCounts ? C().elementCounts(f) : null, mr = C().molarMass ? C().molarMass(f) : NaN;
if (!cnt || isNaN(mr)) { out.className = 'out bad'; out.textContent = 'Не удалось разобрать формулу.'; return; }
out.className = 'out ok';
out.innerHTML = '<span class="bd"><b>M_r(' + f + ') = ' + C().fmt(mr) + '</b><br>'
+ Object.keys(cnt).map(function (e) { return e + ': A_r=' + (C().arOf ? C().arOf(e) : '?') + ' × ' + cnt[e]; }).join(' &nbsp;|&nbsp; ')
+ '<br>Σ = ' + Object.keys(cnt).map(function (e) { return (C().arOf ? C().arOf(e) : '?') + '·' + cnt[e]; }).join(' + ') + ' = ' + C().fmt(mr) + '</span>';
}
go.addEventListener('click', calc);
inp.addEventListener('keydown', function (e) { if (e.key === 'Enter') calc(); });
document.querySelectorAll('.p8-ex').forEach(function (b) { b.addEventListener('click', function () { inp.value = b.dataset.f; calc(); }); });
calc();
}
/* §9 — конструктор формулы по валентности (НОК индексов) */
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 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>'
+ 'Каждая чёрточка-связь соединена — все валентности заняты.<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();
}
/* ── Волна 4 ── */
/* §10 / ЛО1 — детектор признаков химической реакции */
var DEMOS = [
{ name: 'Нагревание малахита', signs: ['изменение цвета: зелёный → чёрный', 'выделение газа (водяной пар и углекислый газ)'] },
{ name: 'Сливание растворов CuSO₄ и NaOH', signs: ['образование осадка (голубой)', 'изменение цвета раствора'] },
{ name: 'Горение серы', signs: ['выделение света и тепла (пламя)', 'появление резкого запаха'] },
{ name: 'Добавление соды в уксус', signs: ['выделение газа (пузырьки)'] }
];
// анимация на каждый опыт (через Chem7Anim, CSS-хелперы)
function demoAnim(idx, host) {
var A = W.Chem7Anim; if (!A || !host) return null;
if (idx === 0) return A.colorBlock(host, '#16a34a', '#1f2937', 'малахит → CuO + газы', 2000); // зелёный → чёрный
if (idx === 1) return A.precipField(host, { color: '#38bdf8' }); // голубой осадок
if (idx === 2) return A.flameBox(host, { color: '#3b82f6', sparks: true }); // синее пламя серы
return A.bubbleField(host, { color: 'rgba(255,255,255,.85)' }); // пузырьки газа
}
function mount_signs(mountId) {
var m = $(mountId); if (!m || m._built) return; m._built = 1;
var idx = 0, anim = null;
function stopAnim() { if (anim) { anim.stop(); anim = null; } }
function render() {
stopAnim();
m.innerHTML = '<div class="fld"><label>Опыт</label><select id="' + mountId + '-pick">'
+ DEMOS.map(function (d, i) { return '<option value="' + i + '"' + (i === idx ? ' selected' : '') + '>' + esc(d.name) + '</option>'; }).join('') + '</select>'
+ '<button class="btn primary" id="' + mountId + '-go">Провести опыт</button></div>'
+ '<div id="' + mountId + '-stage" style="margin:8px 0"></div>'
+ '<div class="out" id="' + mountId + '-out">Выбери опыт и нажми «Провести опыт».</div>';
$(mountId + '-pick').addEventListener('change', function (e) { idx = +e.target.value; render(); });
$(mountId + '-go').addEventListener('click', function () {
var d = DEMOS[idx], out = $(mountId + '-out');
stopAnim(); anim = demoAnim(idx, $(mountId + '-stage'));
out.className = 'out ok';
out.innerHTML = '<b>Наблюдаемые признаки реакции:</b><div style="margin-top:6px">'
+ d.signs.map(function (s) { return '<div style="padding:5px 10px;margin:3px 0;border-radius:8px;background:var(--pri-soft);font-weight:600">&#10003; ' + esc(s) + '</div>'; }).join('')
+ '</div><div style="font-size:.84rem;color:var(--muted);margin-top:6px">Эти признаки указывают, что произошла <b>химическая реакция</b> — образовались новые вещества.</div>';
});
}
render();
}
function mount_p10() { mount_signs('p10-signs'); }
function mount_lo1() { mount_signs('lo1-signs'); }
/* §11 — весы сохранения массы */
function mount_p11() {
var m = $('p11-bal'); if (!m || m._built) return; m._built = 1;
var mixed = false;
function scale(level) {
// level: 0 = равновесие
return '<svg viewBox="0 0 320 130" width="100%" style="max-width:340px">'
+ '<line x1="160" y1="14" x2="160" y2="40" stroke="var(--muted)" stroke-width="3"/>'
+ '<line x1="60" y1="40" x2="260" y2="40" stroke="var(--muted)" stroke-width="3"/>'
+ '<circle cx="160" cy="14" r="6" fill="var(--pri)"/>'
+ '<rect x="30" y="55" width="80" height="34" rx="6" fill="var(--pri-soft)" stroke="var(--border)"/>'
+ '<rect x="210" y="55" width="80" height="34" rx="6" fill="var(--pri-soft)" stroke="var(--border)"/>'
+ '<line x1="60" y1="40" x2="70" y2="55" stroke="var(--muted)" stroke-width="2"/><line x1="110" y1="40" x2="100" y2="55" stroke="var(--muted)" stroke-width="2"/>'
+ '<line x1="210" y1="40" x2="220" y2="55" stroke="var(--muted)" stroke-width="2"/><line x1="260" y1="40" x2="250" y2="55" stroke="var(--muted)" stroke-width="2"/>'
+ '<text x="70" y="77" font-size="13" font-weight="700" fill="var(--text)">100 г</text>'
+ '<text x="250" y="77" font-size="13" font-weight="700" fill="var(--text)">100 г</text>'
+ '<text x="70" y="108" font-size="11" fill="var(--muted)">' + (mixed ? 'продукты' : 'реагенты') + '</text>'
+ '<text x="225" y="108" font-size="11" fill="var(--muted)">' + (mixed ? 'продукты' : 'реагенты') + '</text>'
+ '</svg>';
}
var anim = null;
function render() {
if (anim) { anim.stop(); anim = null; }
m.innerHTML = scale()
+ '<div style="margin:6px 0;font-size:.92rem">' + (mixed
? 'После реакции: <b>осадок Cu(OH)₂ + раствор Na₂SO₄</b>. Стрелка весов <b>не сдвинулась</b> — масса сохранилась (100 г = 100 г).'
: 'До реакции: <b>раствор CuSO₄ + раствор NaOH</b>, общая масса 100 г.') + '</div>'
+ '<div id="p11-stage" style="margin:6px 0"></div>'
+ '<button class="btn primary" id="p11-mix">' + (mixed ? 'Сбросить' : 'Смешать растворы') + '</button>';
if (mixed && W.Chem7Anim) anim = W.Chem7Anim.precipField($('p11-stage'), { color: '#38bdf8', h: 96 });
$('p11-mix').addEventListener('click', function () { mixed = !mixed; render(); });
}
render();
}
/* §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;
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();
}
W.CHEM8_WIDGETS = Object.assign(W.CHEM8_WIDGETS || {}, {
p1: mount_p1, p2: mount_p2, pr1: mount_pr1, p3: mount_p3,
p4: mount_p4, p5: mount_p5, p6: mount_p6,
p7: mount_p7, p8: mount_p8, p9: mount_p9,
p10: mount_p10, lo1: mount_lo1, p11: mount_p11
});
W.FLAG_MOUNTS = Object.assign(W.FLAG_MOUNTS || {}, { p12: mount_p12 });
})(window);