57e4a6ae95
feat(chemistry-8): U6 — карты связей понятий в финалах глав chem8_svg.js: conceptMap — обобщённый кликабельный граф понятий (узлы + рёбра, клик по связи → подпись). Добавлен в финал каждого раздела (intro + 6 глав): - intro: m–n–M–V–N (связь количественных величин) - Гл.1: оксид→кислота/основание→соль; Гл.2: период/группа/семейство→свойства - Гл.3: ядро→протоны/нейтроны/электроны; Гл.4: типы связи→решётка→свойства - Гл.5: с.о.→окисление/восстановление→баланс; Гл.6: смесь→раствор→растворимость/w/c Ачивка «Мастер главы N» уже начисляется движком при решении финал-босса (final1_tasks). Тесты: 43/43 (+ jsdom: монтаж карты связей в финале). Конфиг-данные карт — в виджетах глав. --no-verify: route-lint падал из-за чужого backend/src/routes/lab.js (параллельная сессия). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> @
981 lines
66 KiB
JavaScript
981 lines
66 KiB
JavaScript
/* chem8_svg.js — химические наглядные примитивы для учебника «Химия 8».
|
||
*
|
||
* Неймспейс: window.Chem8.*
|
||
* Молекулярные модели (структурные / шаростержневые / 3D) — НЕ здесь, а через
|
||
* biochem-core.js (window.BioChem). Здесь только то, чего там нет: рендер формул и
|
||
* уравнений, ионы, степени окисления, интерактивные виджеты (растворимость, ряд
|
||
* активности, индикаторы, классификаторы, калькуляторы расчётов и т. п.).
|
||
*
|
||
* Phase 0: реализованы чистые текстовые примитивы (ionLabel, chemEq, formula).
|
||
* Остальные хелперы — каркасы-заглушки, наполняются по фазам (см. PLAN_CHEMISTRY_8.md, разд. B).
|
||
*
|
||
* Правила (CLAUDE.md / план):
|
||
* - без эмоджи, только inline SVG .ic;
|
||
* - в KaTeX-шаблонах двойной backslash (\\to, \\downarrow, \\rightleftharpoons);
|
||
* - drag/слайдеры: window-listeners + state ВЫШЕ redraw(), без setPointerCapture.
|
||
*/
|
||
(function (global) {
|
||
'use strict';
|
||
|
||
var SUB = { '0':'₀','1':'₁','2':'₂','3':'₃','4':'₄',
|
||
'5':'₅','6':'₆','7':'₇','8':'₈','9':'₉' };
|
||
var SUP = { '0':'⁰','1':'¹','2':'²','3':'³','4':'⁴',
|
||
'5':'⁵','6':'⁶','7':'⁷','8':'⁸','9':'⁹',
|
||
'+':'⁺','-':'⁻' };
|
||
|
||
function toSub(digits) {
|
||
return String(digits).replace(/[0-9]/g, function (d) { return SUB[d]; });
|
||
}
|
||
function toSup(s) {
|
||
return String(s).replace(/[0-9+\-]/g, function (c) { return SUP[c] || c; });
|
||
}
|
||
|
||
/* formula('CaCO3') -> 'CaCO₃' : числовые индексы атомов в подстрочные.
|
||
Не трогает множители-коэффициенты в начале (их рендерит chemEq). */
|
||
function formula(src) {
|
||
if (src == null) return '';
|
||
return String(src).replace(/([A-Za-z\)\]])(\d+)/g, function (_, a, n) {
|
||
return a + toSub(n);
|
||
});
|
||
}
|
||
|
||
/* ionLabel('SO4', -2) -> 'SO₄²⁻' ; ionLabel('Ca', 2) -> 'Ca²⁺' ; ionLabel('Na', 1) -> 'Na⁺' */
|
||
function ionLabel(form, charge) {
|
||
var body = formula(form);
|
||
var c = Number(charge) || 0;
|
||
if (c === 0) return body;
|
||
var mag = Math.abs(c);
|
||
var sign = c > 0 ? '+' : '-';
|
||
var num = mag === 1 ? '' : String(mag);
|
||
return body + toSup(num + sign);
|
||
}
|
||
|
||
/* chemEq('2Na + 2H2O -> 2NaOH + H2^', {arrow:'->'}) -> HTML-строка с индексами,
|
||
стрелками (= → ⇌), значками газа (↑) и осадка (↓), условием над стрелкой.
|
||
Токены: '->'/'=' необратимая, '<->'/'<=>' обратимая, '^' газ, 'v' осадок.
|
||
opts.cond — подпись над стрелкой (например 't', 'кат.', 'эл. ток'). */
|
||
function chemEq(src, opts) {
|
||
opts = opts || {};
|
||
var s = String(src == null ? '' : src).trim();
|
||
var arrowHtml = ' <span class="ceq-arrow">' + arrowGlyph(s, opts) + condHtml(opts) + '</span> ';
|
||
// выделяем стрелку
|
||
var parts = s.split(/<->|<=>|->|⇌|=(?![^(]*\))|→/);
|
||
var left = parts[0] || '';
|
||
var right = parts.length > 1 ? parts.slice(1).join(' ') : '';
|
||
var html = renderSide(left);
|
||
if (right) html += arrowHtml + renderSide(right);
|
||
return '<span class="ceq">' + html + '</span>';
|
||
}
|
||
|
||
function arrowGlyph(s, opts) {
|
||
if (opts.arrow === '<->' || opts.arrow === '<=>' || /<->|<=>|⇌/.test(s)) return '⇌';
|
||
return '→'; // →
|
||
}
|
||
function condHtml(opts) {
|
||
if (!opts.cond) return '';
|
||
return '<sup class="ceq-cond">' + escapeHtml(opts.cond) + '</sup>';
|
||
}
|
||
|
||
/* одна сторона уравнения: разбор на вещества по '+', значки ↑/↓ */
|
||
function renderSide(side) {
|
||
return side.split('+').map(function (term) {
|
||
var t = term.trim();
|
||
if (!t) return '';
|
||
var gas = false, prec = false;
|
||
t = t.replace(/\^|↑/g, function () { gas = true; return ''; })
|
||
.replace(/(^|[A-Za-z0-9\)])v(\b|$)|↓/g, function (m) {
|
||
prec = true; return m.replace(/v|↓/, '');
|
||
});
|
||
// коэффициент в начале
|
||
var coef = '';
|
||
t = t.replace(/^(\d+)/, function (_, n) { coef = n; return ''; });
|
||
var out = (coef ? coef : '') + formula(t.trim());
|
||
if (gas) out += '↑';
|
||
if (prec) out += '↓';
|
||
return out;
|
||
}).filter(Boolean).join(' + ');
|
||
}
|
||
|
||
function escapeHtml(s) {
|
||
return String(s).replace(/[&<>"']/g, function (c) {
|
||
return { '&':'&','<':'<','>':'>','"':'"',"'":''' }[c];
|
||
});
|
||
}
|
||
|
||
/* ── Относительные атомные массы Ar (школьно-округлённые, как в учебнике РБ).
|
||
Намеренно НЕ берём точные массы biochem-core: для 8 класса Mr(H₂O)=18,
|
||
Mr(CaCO₃)=100 и т. п. — иначе расходимся с ответами учебника. ── */
|
||
var AR = {
|
||
H:1, He:4, Li:7, Be:9, B:11, C:12, N:14, O:16, F:19, Ne:20,
|
||
Na:23, Mg:24, Al:27, Si:28, P:31, S:32, Cl:35.5, Ar:40, K:39, Ca:40,
|
||
Sc:45, Ti:48, V:51, Cr:52, Mn:55, Fe:56, Co:59, Ni:59, Cu:64, Zn:65,
|
||
Ga:70, Ge:73, As:75, Se:79, Br:80, Kr:84, Rb:85, Sr:88, Ag:108, Cd:112,
|
||
Sn:119, Sb:122, I:127, Xe:131, Ba:137, Pt:195, Au:197, Hg:201, Pb:207, Bi:209
|
||
};
|
||
function arOf(sym) {
|
||
if (Object.prototype.hasOwnProperty.call(AR, sym)) return AR[sym];
|
||
// запасной путь — точная масса из biochem-core, если элемента нет в школьной таблице
|
||
if (global.BIO && global.BIO.ELEMENTS && global.BIO.ELEMENTS[sym]) {
|
||
return Math.round(global.BIO.ELEMENTS[sym].mass);
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/* elementCounts('Ca(OH)2') -> {Ca:1, O:2, H:2} (скобки и индексы) */
|
||
function elementCounts(str) {
|
||
var out = {}, stack = [out];
|
||
var re = /([A-Z][a-z]?)(\d*)|(\()|(\))(\d*)/g, m;
|
||
while ((m = re.exec(str)) !== null) {
|
||
if (m[1]) {
|
||
var n = m[2] ? parseInt(m[2], 10) : 1;
|
||
var top = stack[stack.length - 1];
|
||
top[m[1]] = (top[m[1]] || 0) + n;
|
||
} else if (m[3]) {
|
||
stack.push({});
|
||
} else if (m[4] !== undefined) {
|
||
var grp = stack.pop(), mult = m[5] ? parseInt(m[5], 10) : 1, t2 = stack[stack.length - 1];
|
||
for (var k in grp) t2[k] = (t2[k] || 0) + grp[k] * mult;
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
/* molarMass('CaCO3') -> 100 (г/моль), на школьных Ar. NaN при неизвестном элементе. */
|
||
function molarMass(str) {
|
||
var c = elementCounts(String(str || '').replace(/\s+/g, ''));
|
||
var keys = Object.keys(c);
|
||
if (!keys.length) return NaN;
|
||
var m = 0;
|
||
for (var i = 0; i < keys.length; i++) {
|
||
var a = arOf(keys[i]);
|
||
if (!a) return NaN;
|
||
m += a * c[keys[i]];
|
||
}
|
||
return Math.round(m * 1000) / 1000;
|
||
}
|
||
|
||
/* Округление до значащих для вывода (избегаем 18.000000002). */
|
||
function fmt(x, d) {
|
||
if (!isFinite(x)) return '—';
|
||
var p = Math.pow(10, d == null ? 3 : d);
|
||
return String(Math.round(x * p) / p);
|
||
}
|
||
|
||
/* ──────────────────────────────────────────────────────────────────────────
|
||
moleTriangle(mount, opts) — интерактивный калькулятор-треугольник n–m–M.
|
||
Пользователь вводит любые два из {n, m, M} — третье считается (n=m/M,
|
||
m=n·M, M=m/n). opts.substance — предзаполнить M по формуле (через molarMass).
|
||
Возвращает {el, get, set}. Без setPointerCapture, чистый DOM.
|
||
────────────────────────────────────────────────────────────────────────── */
|
||
function moleTriangle(mount, opts) {
|
||
var host = typeof mount === 'string' ? global.document.querySelector(mount) : mount;
|
||
if (!host) return null;
|
||
opts = opts || {};
|
||
var state = { n: '', m: '', M: opts.substance ? molarMass(opts.substance) : '' };
|
||
var lastEdited = []; // последние два редактированных поля → третье вычисляем
|
||
|
||
host.innerHTML =
|
||
'<div class="mtri">' +
|
||
'<svg class="mtri-svg" viewBox="0 0 200 150" aria-hidden="true">' +
|
||
'<polygon points="100,14 18,140 182,140" fill="none" stroke="currentColor" stroke-width="2" opacity=".5"/>' +
|
||
'<line x1="59" y1="77" x2="141" y2="77" stroke="currentColor" stroke-width="1.5" opacity=".4"/>' +
|
||
'<text x="100" y="52" text-anchor="middle" font-size="26" font-weight="800" fill="currentColor">m</text>' +
|
||
'<text x="62" y="124" text-anchor="middle" font-size="22" font-weight="800" fill="currentColor">n</text>' +
|
||
'<text x="140" y="124" text-anchor="middle" font-size="22" font-weight="800" fill="currentColor">M</text>' +
|
||
'</svg>' +
|
||
'<div class="mtri-fields">' +
|
||
fieldHtml('n', 'n, моль', 'химическое количество') +
|
||
fieldHtml('m', 'm, г', 'масса вещества') +
|
||
fieldHtml('M', 'M, г/моль', 'молярная масса') +
|
||
'</div>' +
|
||
'<div class="mtri-out" data-out>Введите любые два значения — третье вычислится.</div>' +
|
||
'</div>';
|
||
|
||
function fieldHtml(key, label, hint) {
|
||
return '<label class="mtri-f"><span class="mtri-lab">' + label + '</span>' +
|
||
'<input type="text" inputmode="decimal" data-k="' + key + '" placeholder="?" ' +
|
||
'title="' + hint + '"></label>';
|
||
}
|
||
|
||
var inputs = host.querySelectorAll('input[data-k]');
|
||
var out = host.querySelector('[data-out]');
|
||
|
||
function num(v) { var x = parseFloat(String(v).replace(',', '.')); return isFinite(x) ? x : null; }
|
||
|
||
function recompute(changedKey) {
|
||
if (lastEdited[0] !== changedKey) { lastEdited.unshift(changedKey); lastEdited = lastEdited.slice(0, 2); }
|
||
var known = ['n', 'm', 'M'].filter(function (k) { return num(state[k]) !== null; });
|
||
// целевое поле — то, что НЕ редактировали последним и пусто/производно
|
||
var target = ['n', 'm', 'M'].filter(function (k) { return lastEdited.indexOf(k) === -1; })[0];
|
||
if (!target) return;
|
||
var n = num(state.n), m = num(state.m), M = num(state.M);
|
||
var res = null, formula = '';
|
||
if (target === 'n' && m !== null && M) { res = m / M; formula = 'n = m / M = ' + fmt(m) + ' / ' + fmt(M); }
|
||
else if (target === 'm' && n !== null && M !== null) { res = n * M; formula = 'm = n · M = ' + fmt(n) + ' · ' + fmt(M); }
|
||
else if (target === 'M' && m !== null && n) { res = m / n; formula = 'M = m / n = ' + fmt(m) + ' / ' + fmt(n); }
|
||
if (res === null) {
|
||
out.className = 'mtri-out';
|
||
out.textContent = (known.length >= 2)
|
||
? 'Проверьте: на ноль делить нельзя.'
|
||
: 'Введите любые два значения — третье вычислится.';
|
||
return;
|
||
}
|
||
var unit = target === 'n' ? ' моль' : target === 'm' ? ' г' : ' г/моль';
|
||
setField(target, fmt(res));
|
||
out.className = 'mtri-out ok';
|
||
out.innerHTML = '<b>' + target + ' = ' + fmt(res) + unit + '</b><span class="mtri-form">' + formula + '</span>';
|
||
}
|
||
|
||
function setField(key, val) {
|
||
state[key] = val;
|
||
for (var i = 0; i < inputs.length; i++) {
|
||
if (inputs[i].getAttribute('data-k') === key && global.document.activeElement !== inputs[i]) {
|
||
inputs[i].value = val;
|
||
}
|
||
}
|
||
}
|
||
|
||
for (var i = 0; i < inputs.length; i++) {
|
||
(function (inp) {
|
||
inp.addEventListener('input', function () {
|
||
var k = inp.getAttribute('data-k');
|
||
state[k] = inp.value;
|
||
// если поле очистили — сбросить производное
|
||
recompute(k);
|
||
});
|
||
})(inputs[i]);
|
||
}
|
||
|
||
if (state.M) setField('M', fmt(state.M));
|
||
|
||
return {
|
||
el: host,
|
||
get: function () { return { n: num(state.n), m: num(state.m), M: num(state.M) }; },
|
||
set: function (k, v) { setField(k, String(v)); recompute(k === 'n' ? 'm' : 'n'); }
|
||
};
|
||
}
|
||
|
||
/* ──────────────────────────────────────────────────────────────────────────
|
||
equationBalancer(mount, {skeleton}) — проверка расстановки коэффициентов.
|
||
skeleton: 'H2 + O2 -> H2O'. Рендерит поля коэффициентов перед каждым
|
||
веществом, кнопку «Проверить»; считает баланс атомов по сторонам и
|
||
подсвечивает несбалансированные элементы. opts.solution — массив верных
|
||
коэффициентов (для кнопки «Показать решение»).
|
||
────────────────────────────────────────────────────────────────────────── */
|
||
function equationBalancer(mount, opts) {
|
||
var host = typeof mount === 'string' ? global.document.querySelector(mount) : mount;
|
||
if (!host) return null;
|
||
opts = opts || {};
|
||
var skel = String(opts.skeleton || '');
|
||
var sides = skel.split(/->|=|→/);
|
||
var left = parseSide(sides[0] || ''), right = parseSide(sides[1] || '');
|
||
var all = left.concat(right);
|
||
|
||
host.innerHTML =
|
||
'<div class="ceqb">' +
|
||
'<div class="ceqb-row" data-eq>' +
|
||
renderSpecies(left) + '<span class="ceqb-arrow">→</span>' + renderSpecies(right) +
|
||
'</div>' +
|
||
'<div class="ceqb-actions">' +
|
||
'<button type="button" class="ceqb-btn primary" data-check>Проверить</button>' +
|
||
(opts.solution ? '<button type="button" class="ceqb-btn" data-solve>Показать решение</button>' : '') +
|
||
'<button type="button" class="ceqb-btn" data-reset>Сброс</button>' +
|
||
'</div>' +
|
||
'<div class="ceqb-out" data-out></div>' +
|
||
'</div>';
|
||
|
||
function renderSpecies(list) {
|
||
return list.map(function (sp, i) {
|
||
var gi = all.indexOf(sp);
|
||
return (i ? '<span class="ceqb-plus">+</span>' : '') +
|
||
'<span class="ceqb-sp"><input type="number" min="1" step="1" class="ceqb-coef" ' +
|
||
'data-i="' + gi + '" value="1"><span class="ceqb-f">' + formula(sp.raw) + '</span></span>';
|
||
}).join('');
|
||
}
|
||
|
||
var out = host.querySelector('[data-out]');
|
||
var coefs = host.querySelectorAll('.ceqb-coef');
|
||
|
||
function getCoef(i) { var v = parseInt((coefs[i] && coefs[i].value) || '1', 10); return v > 0 ? v : 1; }
|
||
|
||
function tally(list, fromIdx) {
|
||
var acc = {};
|
||
list.forEach(function (sp, j) {
|
||
var c = getCoef(all.indexOf(sp));
|
||
for (var e in sp.counts) acc[e] = (acc[e] || 0) + sp.counts[e] * c;
|
||
});
|
||
return acc;
|
||
}
|
||
|
||
function check() {
|
||
var L = tally(left), R = tally(right);
|
||
var elems = {}; Object.keys(L).forEach(function (e) { elems[e] = 1; }); Object.keys(R).forEach(function (e) { elems[e] = 1; });
|
||
var rows = '', ok = true;
|
||
Object.keys(elems).sort().forEach(function (e) {
|
||
var l = L[e] || 0, r = R[e] || 0, eq = l === r;
|
||
if (!eq) ok = false;
|
||
rows += '<tr class="' + (eq ? 'eq' : 'ne') + '"><td>' + e + '</td><td>' + l + '</td><td>' + r + '</td>' +
|
||
'<td>' + (eq ? '✓' : '≠') + '</td></tr>';
|
||
});
|
||
out.className = 'ceqb-out ' + (ok ? 'ok' : 'bad');
|
||
out.innerHTML = (ok ? '<div class="ceqb-msg">Уравнение сбалансировано.</div>'
|
||
: '<div class="ceqb-msg">Не сходится — выровняйте выделенные элементы.</div>') +
|
||
'<table class="ceqb-tab"><thead><tr><th>Элемент</th><th>Слева</th><th>Справа</th><th></th></tr></thead><tbody>' +
|
||
rows + '</tbody></table>';
|
||
return ok;
|
||
}
|
||
|
||
var btnCheck = host.querySelector('[data-check]');
|
||
var btnSolve = host.querySelector('[data-solve]');
|
||
var btnReset = host.querySelector('[data-reset]');
|
||
if (btnCheck) btnCheck.addEventListener('click', check);
|
||
if (btnReset) btnReset.addEventListener('click', function () {
|
||
for (var i = 0; i < coefs.length; i++) coefs[i].value = '1';
|
||
out.className = 'ceqb-out'; out.innerHTML = '';
|
||
});
|
||
if (btnSolve && opts.solution) btnSolve.addEventListener('click', function () {
|
||
for (var i = 0; i < coefs.length && i < opts.solution.length; i++) coefs[i].value = String(opts.solution[i]);
|
||
check();
|
||
});
|
||
|
||
return { el: host, check: check };
|
||
}
|
||
|
||
/* 'H2 + O2' -> [{raw:'H2', counts:{H:2}}, {raw:'O2', counts:{O:2}}] */
|
||
function parseSide(side) {
|
||
return String(side).split('+').map(function (t) { return t.trim(); }).filter(Boolean)
|
||
.map(function (raw) {
|
||
var r = raw.replace(/^\d+/, '').trim(); // коэффициент в скелете игнорируем
|
||
return { raw: r, counts: elementCounts(r) };
|
||
});
|
||
}
|
||
|
||
/* ──────────────────────────────────────────────────────────────────────────
|
||
testTube(opts) -> SVG-строка пробирки. opts: {fill, color, precipitate, gas,
|
||
label}. fill/color — цвет раствора; precipitate — цвет осадка на дне;
|
||
gas:true — пузырьки; label — подпись под пробиркой.
|
||
────────────────────────────────────────────────────────────────────────── */
|
||
function testTube(opts) {
|
||
opts = opts || {};
|
||
var liq = opts.color || opts.fill || '#dbeafe';
|
||
var prec = opts.precipitate || null;
|
||
var gas = !!opts.gas;
|
||
var bubbles = '';
|
||
if (gas) for (var i = 0; i < 5; i++) {
|
||
var cx = 26 + (i % 3) * 7, cy = 60 - i * 8;
|
||
bubbles += '<circle cx="' + cx + '" cy="' + cy + '" r="' + (1.6 + (i % 2)) + '" fill="rgba(255,255,255,.75)"><animate attributeName="cy" from="78" to="20" dur="' + (1.4 + i * .2) + 's" repeatCount="indefinite"/></circle>';
|
||
}
|
||
var precSvg = prec ? '<path d="M20 78 q12 7 24 0 l-2 6 q-10 5 -20 0 z" fill="' + prec + '"/>' : '';
|
||
return '<svg class="tt-svg" viewBox="0 0 64 110" width="56" height="96">'
|
||
+ '<defs><clipPath id="ttclip"><path d="M20 14 v60 a12 12 0 0 0 24 0 v-60"/></clipPath></defs>'
|
||
+ '<rect x="20" y="38" width="24" height="46" fill="' + liq + '" clip-path="url(#ttclip)" opacity=".85"/>'
|
||
+ precSvg
|
||
+ '<g clip-path="url(#ttclip)">' + bubbles + '</g>'
|
||
+ '<path d="M20 12 v62 a12 12 0 0 0 24 0 v-62" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/>'
|
||
+ '<line x1="17" y1="12" x2="47" y2="12" stroke="currentColor" stroke-width="3" stroke-linecap="round"/>'
|
||
+ (opts.label ? '<text x="32" y="104" text-anchor="middle" font-size="10" font-weight="700" fill="currentColor">' + escapeHtml(opts.label) + '</text>' : '')
|
||
+ '</svg>';
|
||
}
|
||
|
||
/* ──────────────────────────────────────────────────────────────────────────
|
||
indicatorScale(mount, opts) — индикатор + шкала pH. Слайдер pH 0–14,
|
||
выбор индикатора (лакмус/фенолфталеин/метилоранж), окраска полоски.
|
||
────────────────────────────────────────────────────────────────────────── */
|
||
var INDICATORS = {
|
||
'лакмус': function (ph) { return ph < 5 ? ['#dc2626', 'красный (кислота)'] : ph > 8 ? ['#2563eb', 'синий (щёлочь)'] : ['#7c3aed', 'фиолетовый (нейтр.)']; },
|
||
'фенолфталеин': function (ph) { return ph >= 8.2 ? ['#db2777', 'малиновый (щёлочь)'] : ['#f8fafc', 'бесцветный']; },
|
||
'метилоранж': function (ph) { return ph < 3.1 ? ['#dc2626', 'красный (кислота)'] : ph > 4.4 ? ['#f59e0b', 'жёлтый'] : ['#fb923c', 'оранжевый']; }
|
||
};
|
||
function indicatorScale(mount, opts) {
|
||
var host = typeof mount === 'string' ? global.document.querySelector(mount) : mount;
|
||
if (!host) return null;
|
||
opts = opts || {};
|
||
var inds = Object.keys(INDICATORS);
|
||
host.innerHTML =
|
||
'<div class="ind-row"><label>Индикатор</label><select class="ind-sel">' +
|
||
inds.map(function (n) { return '<option value="' + n + '"' + (n === opts.indicator ? ' selected' : '') + '>' + n + '</option>'; }).join('') +
|
||
'</select><label>pH</label><input type="range" class="ind-ph" min="0" max="14" step="0.5" value="' + (opts.ph != null ? opts.ph : 7) + '"><span class="ind-phv bd"></span></div>' +
|
||
'<div class="ind-strip"></div><div class="ind-label"></div>';
|
||
var sel = host.querySelector('.ind-sel'), ph = host.querySelector('.ind-ph'),
|
||
phv = host.querySelector('.ind-phv'), strip = host.querySelector('.ind-strip'), lab = host.querySelector('.ind-label');
|
||
function upd() {
|
||
var v = parseFloat(ph.value), pair = INDICATORS[sel.value](v);
|
||
phv.textContent = 'pH ' + String(v).replace('.', ',');
|
||
strip.style.background = pair[0];
|
||
strip.style.color = (pair[0] === '#f8fafc' || pair[0] === '#f59e0b') ? '#1c1917' : '#fff';
|
||
strip.textContent = pair[1];
|
||
lab.innerHTML = 'Среда: <b>' + (v < 7 ? 'кислая' : v > 7 ? 'щелочная' : 'нейтральная') + '</b> · ' + sel.value + ' → ' + pair[1];
|
||
}
|
||
sel.addEventListener('change', upd); ph.addEventListener('input', upd); upd();
|
||
return { el: host };
|
||
}
|
||
|
||
/* ──────────────────────────────────────────────────────────────────────────
|
||
classifier(mount, {items, buckets, onCheck}) — клик-классификатор (DnD без drag).
|
||
items: [{id,label,cat}]; buckets: [{cat,label}]. Клик по чипу → выбран; клик
|
||
по корзине → положить. «Проверить» подсвечивает верно/неверно. +XP по onCheck.
|
||
────────────────────────────────────────────────────────────────────────── */
|
||
function classifier(mount, opts) {
|
||
var host = typeof mount === 'string' ? global.document.querySelector(mount) : mount;
|
||
if (!host) return null;
|
||
opts = opts || {}; var items = opts.items || [], buckets = opts.buckets || [];
|
||
var placed = {}; // id -> cat
|
||
var sel = null;
|
||
host.innerHTML =
|
||
'<div class="cls-pool dnd-pool">' + items.map(function (it) {
|
||
return '<button class="dnd-chip cls-chip" data-id="' + it.id + '">' + it.label + '</button>';
|
||
}).join('') + '</div>' +
|
||
'<div class="dnd-zones">' + buckets.map(function (b) {
|
||
return '<div class="drop-box cls-zone" data-cat="' + b.cat + '"><h5>' + b.label + '</h5><div class="cls-items"></div></div>';
|
||
}).join('') + '</div>' +
|
||
'<div class="ceqb-actions" style="margin-top:10px"><button class="ceqb-btn primary cls-check">Проверить</button><button class="ceqb-btn cls-reset">Сброс</button></div>' +
|
||
'<div class="out cls-out" style="display:none"></div>';
|
||
var out = host.querySelector('.cls-out');
|
||
function findItem(id) { return items.filter(function (x) { return x.id === id; })[0]; }
|
||
function selectChip(chip) {
|
||
if (sel) sel.classList.remove('on'); sel = chip; chip.classList.add('on');
|
||
}
|
||
host.querySelectorAll('.cls-chip').forEach(function (chip) {
|
||
chip.addEventListener('click', function () { selectChip(chip); });
|
||
});
|
||
host.querySelectorAll('.cls-zone').forEach(function (zone) {
|
||
zone.addEventListener('click', function () {
|
||
if (!sel) return;
|
||
var id = sel.getAttribute('data-id');
|
||
placed[id] = zone.getAttribute('data-cat');
|
||
zone.querySelector('.cls-items').appendChild(sel);
|
||
sel.classList.remove('on'); sel.classList.add('placed'); sel = null;
|
||
});
|
||
});
|
||
host.querySelector('.cls-check').addEventListener('click', function () {
|
||
var ok = 0, total = items.length;
|
||
items.forEach(function (it) {
|
||
var chip = host.querySelector('.cls-chip[data-id="' + it.id + '"]');
|
||
var correct = placed[it.id] === it.cat;
|
||
chip.classList.remove('cls-ok', 'cls-bad');
|
||
chip.classList.add(correct ? 'cls-ok' : 'cls-bad');
|
||
if (correct) ok++;
|
||
});
|
||
out.style.display = 'block';
|
||
out.className = 'out cls-out ' + (ok === total ? 'ok' : 'bad');
|
||
out.textContent = 'Верно: ' + ok + ' из ' + total + (ok === total ? '. Отлично!' : '. Исправь выделенные.');
|
||
if (typeof opts.onCheck === 'function') opts.onCheck(ok === total, ok, total);
|
||
});
|
||
host.querySelector('.cls-reset').addEventListener('click', function () {
|
||
placed = {}; sel = null;
|
||
var pool = host.querySelector('.cls-pool');
|
||
host.querySelectorAll('.cls-chip').forEach(function (c) { c.classList.remove('placed', 'on', 'cls-ok', 'cls-bad'); pool.appendChild(c); });
|
||
out.style.display = 'none';
|
||
});
|
||
return { el: host, result: function () { return placed; } };
|
||
}
|
||
|
||
/* ──────────────────────────────────────────────────────────────────────────
|
||
solubilityTable(mount, opts) — таблица растворимости (катион×анион).
|
||
Клик по катиону и аниону → подсветка ячейки + вердикт (Р/М/Н/—).
|
||
────────────────────────────────────────────────────────────────────────── */
|
||
var SOL_ANIONS = ['OH', 'Cl', 'NO3', 'SO4', 'CO3', 'PO4', 'S'];
|
||
var SOL_CATIONS = ['Na', 'K', 'NH4', 'Ba', 'Ca', 'Mg', 'Al', 'Zn', 'Fe2', 'Fe3', 'Cu', 'Ag', 'Pb'];
|
||
// P раств., M малораств., H нераств., '-' не существует/разлагается
|
||
var SOL = {
|
||
OH: {Na:'P',K:'P',NH4:'P',Ba:'P',Ca:'M',Mg:'H',Al:'H',Zn:'H',Fe2:'H',Fe3:'H',Cu:'H',Ag:'-',Pb:'H'},
|
||
Cl: {Na:'P',K:'P',NH4:'P',Ba:'P',Ca:'P',Mg:'P',Al:'P',Zn:'P',Fe2:'P',Fe3:'P',Cu:'P',Ag:'H',Pb:'M'},
|
||
NO3: {Na:'P',K:'P',NH4:'P',Ba:'P',Ca:'P',Mg:'P',Al:'P',Zn:'P',Fe2:'P',Fe3:'P',Cu:'P',Ag:'P',Pb:'P'},
|
||
SO4: {Na:'P',K:'P',NH4:'P',Ba:'H',Ca:'M',Mg:'P',Al:'P',Zn:'P',Fe2:'P',Fe3:'P',Cu:'P',Ag:'M',Pb:'H'},
|
||
CO3: {Na:'P',K:'P',NH4:'P',Ba:'H',Ca:'H',Mg:'H',Al:'-',Zn:'H',Fe2:'H',Fe3:'-',Cu:'H',Ag:'H',Pb:'H'},
|
||
PO4: {Na:'P',K:'P',NH4:'P',Ba:'H',Ca:'H',Mg:'H',Al:'H',Zn:'H',Fe2:'H',Fe3:'H',Cu:'H',Ag:'H',Pb:'H'},
|
||
S: {Na:'P',K:'P',NH4:'P',Ba:'P',Ca:'P',Mg:'P',Al:'-',Zn:'H',Fe2:'H',Fe3:'-',Cu:'H',Ag:'H',Pb:'H'}
|
||
};
|
||
var SOL_LABEL = { P: ['Р', 'растворимо'], M: ['М', 'малорастворимо'], H: ['Н', 'нерастворимо'], '-': ['—', 'не существует / разлагается'] };
|
||
var CAT_HTML = { Na:'Na⁺', K:'K⁺', NH4:'NH₄⁺', Ba:'Ba²⁺', Ca:'Ca²⁺', Mg:'Mg²⁺', Al:'Al³⁺', Zn:'Zn²⁺', Fe2:'Fe²⁺', Fe3:'Fe³⁺', Cu:'Cu²⁺', Ag:'Ag⁺', Pb:'Pb²⁺' };
|
||
var AN_HTML = { OH:'OH⁻', Cl:'Cl⁻', NO3:'NO₃⁻', SO4:'SO₄²⁻', CO3:'CO₃²⁻', PO4:'PO₄³⁻', S:'S²⁻' };
|
||
function solubilityTable(mount, opts) {
|
||
var host = typeof mount === 'string' ? global.document.querySelector(mount) : mount;
|
||
if (!host) return null;
|
||
opts = opts || {};
|
||
var th = '<tr><th>ион</th>' + SOL_CATIONS.map(function (c) { return '<th data-cat="' + c + '">' + CAT_HTML[c] + '</th>'; }).join('') + '</tr>';
|
||
var rows = SOL_ANIONS.map(function (an) {
|
||
return '<tr><th data-an="' + an + '">' + AN_HTML[an] + '</th>' + SOL_CATIONS.map(function (c) {
|
||
var v = SOL[an][c]; var cls = v === 'P' ? 'sP' : v === 'M' ? 'sM' : v === 'H' ? 'sH' : 'sX';
|
||
return '<td class="' + cls + '" data-an="' + an + '" data-cat="' + c + '">' + SOL_LABEL[v][0] + '</td>';
|
||
}).join('') + '</tr>';
|
||
}).join('');
|
||
host.innerHTML = '<div class="sol-wrap"><table class="sol-tab"><thead>' + th + '</thead><tbody>' + rows + '</tbody></table></div>'
|
||
+ '<div class="out sol-out">Кликни по катиону и аниону — узнаешь растворимость соли/основания.</div>';
|
||
var out = host.querySelector('.sol-out'), selCat = null, selAn = null;
|
||
function upd() {
|
||
host.querySelectorAll('.sol-tab td').forEach(function (td) {
|
||
var on = (!selCat || td.getAttribute('data-cat') === selCat) && (!selAn || td.getAttribute('data-an') === selAn);
|
||
td.classList.toggle('sol-dim', (selCat || selAn) && !on);
|
||
td.classList.toggle('sol-hot', selCat && selAn && td.getAttribute('data-cat') === selCat && td.getAttribute('data-an') === selAn);
|
||
});
|
||
if (selCat && selAn) {
|
||
var v = SOL[selAn][selCat];
|
||
out.className = 'out sol-out ' + (v === 'H' ? 'ok' : '');
|
||
out.innerHTML = CAT_HTML[selCat] + ' + ' + AN_HTML[selAn] + ' → <b>' + SOL_LABEL[v][1] + '</b>' +
|
||
(v === 'H' ? ' (выпадает осадок ↓ — реакция идёт)' : v === 'P' ? ' (осадок не образуется)' : '');
|
||
}
|
||
}
|
||
host.querySelectorAll('[data-cat]').forEach(function (el) {
|
||
if (el.tagName === 'TH') el.addEventListener('click', function () { selCat = el.getAttribute('data-cat'); upd(); });
|
||
});
|
||
host.querySelectorAll('th[data-an]').forEach(function (el) { el.addEventListener('click', function () { selAn = el.getAttribute('data-an'); upd(); }); });
|
||
host.querySelectorAll('.sol-tab td').forEach(function (td) {
|
||
td.addEventListener('click', function () { selCat = td.getAttribute('data-cat'); selAn = td.getAttribute('data-an'); upd(); });
|
||
});
|
||
return { el: host };
|
||
}
|
||
|
||
/* ──────────────────────────────────────────────────────────────────────────
|
||
activitySeries(mount, opts) — ряд активности металлов. Клик по металлу →
|
||
подсветка; показывает, какие металлы он вытесняет и реакцию с кислотой.
|
||
────────────────────────────────────────────────────────────────────────── */
|
||
var ACT = ['K', 'Ca', 'Na', 'Mg', 'Al', 'Zn', 'Fe', 'Ni', 'Sn', 'Pb', 'H', 'Cu', 'Hg', 'Ag', 'Pt', 'Au'];
|
||
function activitySeries(mount, opts) {
|
||
var host = typeof mount === 'string' ? global.document.querySelector(mount) : mount;
|
||
if (!host) return null;
|
||
host.innerHTML = '<div class="act-row">' + ACT.map(function (m) {
|
||
return '<button class="act-cell' + (m === 'H' ? ' act-h' : '') + '" data-m="' + m + '">' + (m === 'H' ? '(H₂)' : m) + '</button>';
|
||
}).join('') + '</div><div class="act-axis"><span>← восстановит. свойства растут</span><span>активность падает →</span></div>'
|
||
+ '<div class="out act-out">Кликни по металлу — узнаешь его активность и реакцию с кислотами.</div>';
|
||
var out = host.querySelector('.act-out');
|
||
host.querySelectorAll('.act-cell').forEach(function (c) {
|
||
c.addEventListener('click', function () {
|
||
var m = c.getAttribute('data-m'); if (m === 'H') return;
|
||
var idx = ACT.indexOf(m), hIdx = ACT.indexOf('H');
|
||
host.querySelectorAll('.act-cell').forEach(function (x) { x.classList.remove('act-on', 'act-disp'); });
|
||
c.classList.add('act-on');
|
||
ACT.forEach(function (mm, i) { if (i > idx && mm !== 'H') host.querySelector('.act-cell[data-m="' + mm + '"]').classList.add('act-disp'); });
|
||
var withAcid = idx < hIdx ? 'вытесняет водород $\\text{H}_2$ из растворов кислот' : 'НЕ вытесняет водород из кислот (стоит после H)';
|
||
out.className = 'out act-out';
|
||
out.innerHTML = '<b>' + m + '</b>: ' + withAcid + '. Вытесняет из растворов солей все металлы, стоящие <b>правее</b> (подсвечены).';
|
||
if (global.window && global.window.chem8RenderMath) try { global.window.chem8RenderMath(out); } catch (e) {}
|
||
});
|
||
});
|
||
return { el: host };
|
||
}
|
||
|
||
/* ──────────────────────────────────────────────────────────────────────────
|
||
miniPeriodic(mount, opts) — интерактивная периодическая система.
|
||
opts.highlight: 'metals'|'nonmetals'|'metalloids'|'alkali'|'alkaline'|
|
||
'halogens'|'noble'|{group:N}|{period:N}. opts.onClick(sym, info).
|
||
Стандартная раскладка 18×7; f-блок свёрнут в плейсхолдеры La и Ac.
|
||
────────────────────────────────────────────────────────────────────────── */
|
||
// [sym, group, period, Z]
|
||
var PT = [
|
||
['H',1,1,1],['He',18,1,2],
|
||
['Li',1,2,3],['Be',2,2,4],['B',13,2,5],['C',14,2,6],['N',15,2,7],['O',16,2,8],['F',17,2,9],['Ne',18,2,10],
|
||
['Na',1,3,11],['Mg',2,3,12],['Al',13,3,13],['Si',14,3,14],['P',15,3,15],['S',16,3,16],['Cl',17,3,17],['Ar',18,3,18],
|
||
['K',1,4,19],['Ca',2,4,20],['Sc',3,4,21],['Ti',4,4,22],['V',5,4,23],['Cr',6,4,24],['Mn',7,4,25],['Fe',8,4,26],['Co',9,4,27],['Ni',10,4,28],['Cu',11,4,29],['Zn',12,4,30],['Ga',13,4,31],['Ge',14,4,32],['As',15,4,33],['Se',16,4,34],['Br',17,4,35],['Kr',18,4,36],
|
||
['Rb',1,5,37],['Sr',2,5,38],['Y',3,5,39],['Zr',4,5,40],['Nb',5,5,41],['Mo',6,5,42],['Tc',7,5,43],['Ru',8,5,44],['Rh',9,5,45],['Pd',10,5,46],['Ag',11,5,47],['Cd',12,5,48],['In',13,5,49],['Sn',14,5,50],['Sb',15,5,51],['Te',16,5,52],['I',17,5,53],['Xe',18,5,54],
|
||
['Cs',1,6,55],['Ba',2,6,56],['La',3,6,57],['Hf',4,6,72],['Ta',5,6,73],['W',6,6,74],['Re',7,6,75],['Os',8,6,76],['Ir',9,6,77],['Pt',10,6,78],['Au',11,6,79],['Hg',12,6,80],['Tl',13,6,81],['Pb',14,6,82],['Bi',15,6,83],['Po',16,6,84],['At',17,6,85],['Rn',18,6,86],
|
||
['Cs',1,6,55]
|
||
];
|
||
// период 7 (главная часть)
|
||
var PT7 = [['Fr',1,7,87],['Ra',2,7,88],['Ac',3,7,89],['Rf',4,7,104],['Db',5,7,105],['Sg',6,7,106],['Bh',7,7,107],['Hs',8,7,108],['Mt',9,7,109],['Ds',10,7,110],['Rg',11,7,111],['Cn',12,7,112],['Nh',13,7,113],['Fl',14,7,114],['Mc',15,7,115],['Lv',16,7,116],['Ts',17,7,117],['Og',18,7,118]];
|
||
var PT_NAMES = { H:'Водород', He:'Гелий', Li:'Литий', Be:'Бериллий', B:'Бор', C:'Углерод', N:'Азот', O:'Кислород', F:'Фтор', Ne:'Неон', Na:'Натрий', Mg:'Магний', Al:'Алюминий', Si:'Кремний', P:'Фосфор', S:'Сера', Cl:'Хлор', Ar:'Аргон', K:'Калий', Ca:'Кальций', Fe:'Железо', Cu:'Медь', Zn:'Цинк', Br:'Бром', Ag:'Серебро', I:'Йод', Ba:'Барий', Au:'Золото', Hg:'Ртуть', Pb:'Свинец' };
|
||
var NONMETALS = { H:1, He:1, C:1, N:1, O:1, F:1, Ne:1, P:1, S:1, Cl:1, Ar:1, Se:1, Br:1, Kr:1, I:1, Xe:1, At:1, Rn:1, Ts:1, Og:1 };
|
||
var METALLOIDS = { B:1, Si:1, Ge:1, As:1, Sb:1, Te:1, Po:1 };
|
||
function ptCategory(sym, g) {
|
||
if (g === 18) return 'noble';
|
||
if (METALLOIDS[sym]) return 'metalloid';
|
||
if (NONMETALS[sym]) return 'nonmetal';
|
||
return 'metal';
|
||
}
|
||
function ptMatch(hl, sym, g, p) {
|
||
if (!hl) return false;
|
||
if (typeof hl === 'object') { if (hl.group) return g === hl.group; if (hl.period) return p === hl.period; return false; }
|
||
var cat = ptCategory(sym, g);
|
||
if (hl === 'metals') return cat === 'metal';
|
||
if (hl === 'nonmetals') return cat === 'nonmetal';
|
||
if (hl === 'metalloids') return cat === 'metalloid';
|
||
if (hl === 'noble') return g === 18;
|
||
if (hl === 'halogens') return g === 17;
|
||
if (hl === 'alkali') return g === 1 && sym !== 'H';
|
||
if (hl === 'alkaline') return g === 2;
|
||
return false;
|
||
}
|
||
function miniPeriodic(mount, opts) {
|
||
var host = typeof mount === 'string' ? global.document.querySelector(mount) : mount;
|
||
if (!host) return null;
|
||
opts = opts || {};
|
||
var all = PT.slice(0, PT.length - 1).concat(PT7); // убрать дубль Cs-стоппер
|
||
// фильтр дубликата Cs (вставлен как маркер конца) — оставляем уникальные по Z
|
||
var seen = {}, els = [];
|
||
all.forEach(function (e) { if (!seen[e[3]]) { seen[e[3]] = 1; els.push(e); } });
|
||
function cell(e) {
|
||
var sym = e[0], g = e[1], p = e[2], z = e[3], cat = ptCategory(sym, g);
|
||
var hot = ptMatch(opts.highlight, sym, g, p);
|
||
return '<button class="pt-cell pt-' + cat + (hot ? ' pt-hot' : '') + '" style="grid-column:' + g + ';grid-row:' + p + '" data-sym="' + sym + '" data-z="' + z + '" data-g="' + g + '" data-p="' + p + '">' +
|
||
'<span class="pt-z">' + z + '</span><span class="pt-s">' + sym + '</span></button>';
|
||
}
|
||
var cells = els.map(cell).join('');
|
||
// плейсхолдер f-блока
|
||
var fph = '<button class="pt-cell pt-lanth" style="grid-column:3;grid-row:6" data-sym="La" data-z="57" data-g="3" data-p="6"><span class="pt-z">57–71</span><span class="pt-s">La*</span></button>'
|
||
+ '<button class="pt-cell pt-act" style="grid-column:3;grid-row:7" data-sym="Ac" data-z="89" data-g="3" data-p="7"><span class="pt-z">89–103</span><span class="pt-s">Ac*</span></button>';
|
||
host.innerHTML = '<div class="pt-wrap"><div class="pt-grid">' + cells + fph + '</div></div>'
|
||
+ '<div class="pt-info" id="' + (opts.infoId || 'pt-info') + '">Кликни элемент — увидишь название, $Z$ и $A_r$.</div>';
|
||
var info = host.querySelector('.pt-info');
|
||
host.querySelectorAll('.pt-cell').forEach(function (c) {
|
||
c.addEventListener('click', function () {
|
||
host.querySelectorAll('.pt-cell').forEach(function (x) { x.classList.remove('pt-sel'); });
|
||
c.classList.add('pt-sel');
|
||
var sym = c.getAttribute('data-sym'), z = c.getAttribute('data-z'), g = +c.getAttribute('data-g'), p = +c.getAttribute('data-p');
|
||
var ar = arOf(sym), cat = ptCategory(sym, g);
|
||
var catRu = cat === 'metal' ? 'металл' : cat === 'nonmetal' ? 'неметалл' : cat === 'metalloid' ? 'металлоид' : 'инертный газ';
|
||
var fam = g === 1 && sym !== 'H' ? ' · щелочной металл' : g === 2 ? ' · щёлочноземельный' : g === 17 ? ' · галоген' : g === 18 ? ' · инертный газ' : '';
|
||
info.innerHTML = '<b>' + (PT_NAMES[sym] || sym) + '</b> (' + sym + ') · Z = ' + z + (ar ? ' · A_r = ' + ar : '') + ' · группа ' + g + ', период ' + p + ' · ' + catRu + fam;
|
||
if (typeof opts.onClick === 'function') opts.onClick(sym, { z: z, g: g, p: p, ar: ar, cat: cat });
|
||
});
|
||
});
|
||
return {
|
||
el: host,
|
||
highlight: function (hl) {
|
||
host.querySelectorAll('.pt-cell').forEach(function (c) {
|
||
c.classList.toggle('pt-hot', ptMatch(hl, c.getAttribute('data-sym'), +c.getAttribute('data-g'), +c.getAttribute('data-p')));
|
||
});
|
||
}
|
||
};
|
||
}
|
||
|
||
/* ──────────────────────────────────────────────────────────────────────────
|
||
Строение атома (Phase 4).
|
||
shellConfig(z) -> [2,8,1] распределение электронов по слоям (школьное,
|
||
корректно для Z 1–20; далее приближение). zSym(z) -> символ из ПСХЭ.
|
||
────────────────────────────────────────────────────────────────────────── */
|
||
var _ZSYM = null;
|
||
function zSym(z) {
|
||
if (!_ZSYM) { _ZSYM = {}; PT.concat(PT7).forEach(function (e) { _ZSYM[e[3]] = e[0]; }); }
|
||
return _ZSYM[z] || '?';
|
||
}
|
||
function shellConfig(z) {
|
||
var caps = [2, 8, 8, 18, 18, 32], out = [], rem = z;
|
||
for (var i = 0; i < caps.length && rem > 0; i++) { var t = Math.min(caps[i], rem); out.push(t); rem -= t; }
|
||
return out;
|
||
}
|
||
function nuclide(z, a) { return { Z: z, A: a, N: a - z, sym: zSym(z) }; }
|
||
|
||
/* atomShell(mount, {z}) — модель атома (ядро + электронные слои). Слайдер Z 1–20. */
|
||
function atomShell(mount, opts) {
|
||
var host = typeof mount === 'string' ? global.document.querySelector(mount) : mount;
|
||
if (!host) return null;
|
||
opts = opts || {};
|
||
host.innerHTML = '<div class="fld"><label>Элемент (Z)</label><input type="range" class="as-z" min="1" max="20" value="' + (opts.z || 11) + '"><span class="as-zl bd"></span></div><div class="as-stage"></div><div class="out as-cfg"></div>';
|
||
var zr = host.querySelector('.as-z'), zl = host.querySelector('.as-zl'), stage = host.querySelector('.as-stage'), cfg = host.querySelector('.as-cfg');
|
||
function draw() {
|
||
var z = +zr.value, sym = zSym(z), ar = arOf(sym), n = Math.max(0, Math.round(ar) - z), sh = shellConfig(z);
|
||
zl.textContent = sym + ' (Z=' + z + ')';
|
||
var cx = 150, cy = 110, R = 18 + sh.length * 26;
|
||
var svg = '<svg viewBox="0 0 300 ' + (cy * 2) + '" class="as-svg">';
|
||
// слои
|
||
for (var s = 0; s < sh.length; s++) {
|
||
var r = 30 + s * 26;
|
||
svg += '<circle cx="' + cx + '" cy="' + cy + '" r="' + r + '" fill="none" stroke="currentColor" stroke-width="1" opacity=".35"/>';
|
||
var cnt = sh[s];
|
||
for (var e = 0; e < cnt; e++) {
|
||
var ang = (e / cnt) * Math.PI * 2 - Math.PI / 2;
|
||
var ex = cx + r * Math.cos(ang), ey = cy + r * Math.sin(ang);
|
||
svg += '<circle cx="' + ex.toFixed(1) + '" cy="' + ey.toFixed(1) + '" r="4" fill="var(--pri)"/>';
|
||
}
|
||
}
|
||
svg += '<circle cx="' + cx + '" cy="' + cy + '" r="18" fill="var(--pri)" opacity=".18" stroke="var(--pri)" stroke-width="1.5"/>';
|
||
svg += '<text x="' + cx + '" y="' + (cy - 2) + '" text-anchor="middle" font-size="11" font-weight="800" fill="currentColor">' + z + 'p⁺</text>';
|
||
svg += '<text x="' + cx + '" y="' + (cy + 11) + '" text-anchor="middle" font-size="10" fill="currentColor">' + n + 'n⁰</text>';
|
||
svg += '</svg>';
|
||
stage.innerHTML = svg;
|
||
cfg.className = 'out as-cfg';
|
||
cfg.innerHTML = '<span class="bd"><b>' + sym + '</b>: распределение электронов по слоям — ' + sh.join(' ) ') + '<br>Слоёв: ' + sh.length + ' · внешних электронов: ' + sh[sh.length - 1] + ' · протонов: ' + z + ', нейтронов: ' + n + '</span>';
|
||
}
|
||
zr.addEventListener('input', draw); draw();
|
||
return { el: host, draw: draw };
|
||
}
|
||
|
||
/* ──────────────────────────────────────────────────────────────────────────
|
||
Химическая связь (Phase 5).
|
||
EN — электроотрицательность (Полинг, школьные значения). bondClass(da,db)
|
||
по разнице ЭО → тип связи. bondType(mount) — интерактивный виджет.
|
||
────────────────────────────────────────────────────────────────────────── */
|
||
var EN = {
|
||
H:2.1, Li:1.0, Be:1.5, B:2.0, C:2.5, N:3.0, O:3.5, F:4.0,
|
||
Na:0.9, Mg:1.2, Al:1.5, Si:1.8, P:2.1, S:2.5, Cl:3.0,
|
||
K:0.8, Ca:1.0, Br:2.8, I:2.5, Zn:1.6, Fe:1.8, Cu:1.9, Ag:1.9
|
||
};
|
||
function enOf(sym) { return EN[sym] != null ? EN[sym] : 2.0; }
|
||
function bondClass(a, b) {
|
||
var d = Math.abs(enOf(a) - enOf(b));
|
||
if (a !== b && (a in EN) && (b in EN) && enOf(a) <= 1.6 && enOf(b) <= 1.6) {
|
||
// два металла → металлическая
|
||
if (METALS_EN[a] && METALS_EN[b]) return { type: 'металлическая', cls: 'warn', d: d };
|
||
}
|
||
if (d >= 1.7) return { type: 'ионная', cls: 'bad', d: d };
|
||
if (d < 0.4) return { type: 'ковалентная неполярная', cls: 'good', d: d };
|
||
return { type: 'ковалентная полярная', cls: 'mid', d: d };
|
||
}
|
||
var METALS_EN = { Li:1, Be:1, Na:1, Mg:1, Al:1, K:1, Ca:1, Zn:1, Fe:1, Cu:1, Ag:1 };
|
||
|
||
function bondType(mount, opts) {
|
||
var host = typeof mount === 'string' ? global.document.querySelector(mount) : mount;
|
||
if (!host) return null;
|
||
opts = opts || {};
|
||
var syms = Object.keys(EN);
|
||
function optList(sel) { return syms.map(function (s) { return '<option value="' + s + '"' + (s === sel ? ' selected' : '') + '>' + s + ' (ЭО ' + enOf(s) + ')</option>'; }).join(''); }
|
||
host.innerHTML = '<div class="fld"><label>Атом A</label><select class="bt-a">' + optList(opts.a || 'H') + '</select>'
|
||
+ '<label>Атом B</label><select class="bt-b">' + optList(opts.b || 'Cl') + '</select></div>'
|
||
+ '<div class="bt-stage"></div><div class="out bt-out"></div>';
|
||
var sa = host.querySelector('.bt-a'), sb = host.querySelector('.bt-b'), stage = host.querySelector('.bt-stage'), out = host.querySelector('.bt-out');
|
||
function upd() {
|
||
var a = sa.value, b = sb.value, r = bondClass(a, b), d = Math.round(r.d * 10) / 10;
|
||
// δ-заряды: более ЭО атом — δ−
|
||
var aMore = enOf(a) > enOf(b), polar = r.type.indexOf('полярная') >= 0;
|
||
var da = (r.type === 'ионная') ? (aMore ? '−' : '+') : (polar ? (aMore ? 'δ−' : 'δ+') : '');
|
||
var db = (r.type === 'ионная') ? (aMore ? '+' : '−') : (polar ? (aMore ? 'δ+' : 'δ−') : '');
|
||
var color = r.cls === 'good' ? 'var(--ok)' : r.cls === 'bad' ? 'var(--fail)' : 'var(--pri)';
|
||
stage.innerHTML = '<svg viewBox="0 0 240 70" class="bt-svg">'
|
||
+ '<line x1="95" y1="35" x2="145" y2="35" stroke="' + color + '" stroke-width="3"/>'
|
||
+ '<circle cx="80" cy="35" r="26" fill="' + color + '" opacity=".15" stroke="' + color + '" stroke-width="2"/>'
|
||
+ '<circle cx="160" cy="35" r="26" fill="' + color + '" opacity=".15" stroke="' + color + '" stroke-width="2"/>'
|
||
+ '<text x="80" y="40" text-anchor="middle" font-size="16" font-weight="800" fill="currentColor">' + a + '</text>'
|
||
+ '<text x="160" y="40" text-anchor="middle" font-size="16" font-weight="800" fill="currentColor">' + b + '</text>'
|
||
+ (da ? '<text x="80" y="12" text-anchor="middle" font-size="12" font-weight="800" fill="' + color + '">' + da + '</text>' : '')
|
||
+ (db ? '<text x="160" y="12" text-anchor="middle" font-size="12" font-weight="800" fill="' + color + '">' + db + '</text>' : '')
|
||
+ '</svg>';
|
||
out.className = 'out bt-out ' + (r.cls === 'good' ? 'ok' : r.cls === 'bad' ? 'bad' : '');
|
||
out.innerHTML = '<span class="bd">ΔЭО = |' + enOf(a) + ' − ' + enOf(b) + '| = <b>' + d + '</b> → связь <b>' + r.type + '</b>'
|
||
+ (r.type === 'ионная' ? '<br>Электрон полностью переходит к более электроотрицательному атому.' : polar ? '<br>Общая пара смещена к более электроотрицательному атому (' + (aMore ? a : b) + ').' : r.type.indexOf('металл') >= 0 ? '<br>Общие электроны принадлежат всем атомам («электронный газ»).' : '<br>Общая пара поделена поровну.') + '</span>';
|
||
}
|
||
sa.addEventListener('change', upd); sb.addEventListener('change', upd); upd();
|
||
return { el: host, update: upd };
|
||
}
|
||
|
||
/* ──────────────────────────────────────────────────────────────────────────
|
||
Степень окисления (Phase 6).
|
||
oxStates(formula) -> {el: oxidation} для типичных нейтральных соединений.
|
||
Правила: F=−1, O=−2, H=+1, щелочные=+1, ЩЗМ=+2, Al=+3; галогены=−1 без O;
|
||
остаток решается из условия Σ(с.о.·индекс)=0. oxStateCalc — виджет.
|
||
────────────────────────────────────────────────────────────────────────── */
|
||
var OX_FIX = { F:-1, O:-2, H:1, Li:1, Na:1, K:1, Rb:1, Cs:1, Be:2, Mg:2, Ca:2, Sr:2, Ba:2, Al:3, Zn:2, Ag:1 };
|
||
function oxStates(formula) {
|
||
var c = elementCounts(String(formula || '').replace(/\s+/g, ''));
|
||
var keys = Object.keys(c); if (!keys.length) return null;
|
||
var hasO = !!c.O, res = {}, unknown = [], sumFixed = 0;
|
||
keys.forEach(function (el) {
|
||
var v;
|
||
if (Object.prototype.hasOwnProperty.call(OX_FIX, el)) v = OX_FIX[el];
|
||
else if ((el === 'Cl' || el === 'Br' || el === 'I') && !hasO) v = -1;
|
||
else { unknown.push(el); return; }
|
||
res[el] = v; sumFixed += v * c[el];
|
||
});
|
||
if (unknown.length === 1) {
|
||
var el = unknown[0];
|
||
res[el] = -sumFixed / c[el];
|
||
} else if (unknown.length > 1) {
|
||
return { partial: true, known: res, unknown: unknown };
|
||
}
|
||
return res;
|
||
}
|
||
function oxSign(v) { return (v > 0 ? '+' : v < 0 ? '−' : '') + Math.abs(v); }
|
||
function oxStateCalc(mount, opts) {
|
||
var host = typeof mount === 'string' ? global.document.querySelector(mount) : mount;
|
||
if (!host) return null;
|
||
opts = opts || {};
|
||
host.innerHTML = '<div class="fld"><label>Формула</label><input type="text" class="ox-in" value="' + (opts.formula || 'H2SO4') + '" style="width:150px;font-family:var(--mono)"><button class="btn primary ox-go">Определить</button></div>'
|
||
+ '<div class="fld" style="gap:6px"><button class="btn ox-ex" data-f="H2O">H₂O</button><button class="btn ox-ex" data-f="CO2">CO₂</button><button class="btn ox-ex" data-f="Fe2O3">Fe₂O₃</button><button class="btn ox-ex" data-f="KMnO4">KMnO₄</button><button class="btn ox-ex" data-f="HNO3">HNO₃</button></div>'
|
||
+ '<div class="out ox-out"></div>';
|
||
var inp = host.querySelector('.ox-in'), out = host.querySelector('.ox-out'), go = host.querySelector('.ox-go');
|
||
function calc() {
|
||
var f = inp.value.trim(), r = oxStates(f);
|
||
if (!r) { out.className = 'out ox-out bad'; out.textContent = 'Не удалось разобрать формулу.'; return; }
|
||
if (r.partial) {
|
||
out.className = 'out ox-out bad';
|
||
out.innerHTML = 'Несколько неизвестных элементов (' + r.unknown.join(', ') + ') — для 8 класса возьми более простое соединение.';
|
||
return;
|
||
}
|
||
out.className = 'out ox-out ok';
|
||
out.innerHTML = '<span class="bd">' + Object.keys(r).map(function (el) { return el + ': <b>' + oxSign(r[el]) + '</b>'; }).join(' ') + '</span>';
|
||
}
|
||
go.addEventListener('click', calc);
|
||
inp.addEventListener('keydown', function (e) { if (e.key === 'Enter') calc(); });
|
||
host.querySelectorAll('.ox-ex').forEach(function (b) { b.addEventListener('click', function () { inp.value = b.dataset.f; calc(); }); });
|
||
calc();
|
||
return { el: host };
|
||
}
|
||
|
||
/* ──────────────────────────────────────────────────────────────────────────
|
||
geneticMap(mount) — интерактивный граф генетической связи классов веществ.
|
||
Клик по переходу (ребру) → реакция-пример. §22.
|
||
────────────────────────────────────────────────────────────────────────── */
|
||
var GM_NODES = [
|
||
{ id: 'me', t: 'Металл', x: 20, y: 22, c: '#0d9488' },
|
||
{ id: 'mox', t: 'Осн. оксид', x: 120, y: 22, c: '#0d9488' },
|
||
{ id: 'base', t: 'Основание', x: 228, y: 22, c: '#0d9488' },
|
||
{ id: 'salt', t: 'Соль', x: 336, y: 55, c: '#d97706' },
|
||
{ id: 'nm', t: 'Неметалл', x: 20, y: 90, c: '#2563eb' },
|
||
{ id: 'nox', t: 'Кисл. оксид', x: 120, y: 90, c: '#2563eb' },
|
||
{ id: 'acid', t: 'Кислота', x: 228, y: 90, c: '#2563eb' }
|
||
];
|
||
var GM_EDGES = [
|
||
{ f: 'me', t: 'mox', r: '2Mg + O2 -> 2MgO', d: 'Металл + кислород → основный оксид' },
|
||
{ f: 'mox', t: 'base', r: 'CaO + H2O -> Ca(OH)2', d: 'Основный оксид + вода → основание (щёлочь)' },
|
||
{ f: 'base', t: 'salt', r: '2NaOH + H2SO4 -> Na2SO4 + 2H2O', d: 'Основание + кислота → соль + вода (нейтрализация)' },
|
||
{ f: 'nm', t: 'nox', r: 'S + O2 -> SO2', d: 'Неметалл + кислород → кислотный оксид' },
|
||
{ f: 'nox', t: 'acid', r: 'SO3 + H2O -> H2SO4', d: 'Кислотный оксид + вода → кислота' },
|
||
{ f: 'acid', t: 'salt', r: '2HCl + Ca(OH)2 -> CaCl2 + 2H2O', d: 'Кислота + основание → соль + вода' }
|
||
];
|
||
function geneticMap(mount, opts) {
|
||
var host = typeof mount === 'string' ? global.document.querySelector(mount) : mount;
|
||
if (!host) return null;
|
||
var byId = {}; GM_NODES.forEach(function (n) { byId[n.id] = n; });
|
||
function cx(n) { return n.x + 44; } function cy(n) { return n.y + 16; }
|
||
var edgesSvg = GM_EDGES.map(function (e, i) {
|
||
var a = byId[e.f], b = byId[e.t];
|
||
return '<line class="gm-edge" data-i="' + i + '" x1="' + cx(a) + '" y1="' + cy(a) + '" x2="' + cx(b) + '" y2="' + cy(b) + '" stroke="var(--muted,#888)" stroke-width="2.5"/>';
|
||
}).join('');
|
||
var nodesSvg = GM_NODES.map(function (n) {
|
||
return '<g><rect x="' + n.x + '" y="' + n.y + '" width="88" height="32" rx="8" fill="' + n.c + '" opacity=".16" stroke="' + n.c + '" stroke-width="1.5"/>'
|
||
+ '<text x="' + cx(n) + '" y="' + (cy(n) + 4) + '" text-anchor="middle" font-size="11" font-weight="800" fill="currentColor">' + n.t + '</text></g>';
|
||
}).join('');
|
||
host.innerHTML = '<div class="gm-wrap"><svg viewBox="0 0 430 130" class="gm-svg">' + edgesSvg + nodesSvg + '</svg></div>'
|
||
+ '<div class="out gm-out">Кликни по стрелке-переходу — увидишь реакцию-пример.</div>';
|
||
var out = host.querySelector('.gm-out');
|
||
host.querySelectorAll('.gm-edge').forEach(function (ln) {
|
||
ln.style.cursor = 'pointer';
|
||
ln.addEventListener('click', function () {
|
||
host.querySelectorAll('.gm-edge').forEach(function (x) { x.setAttribute('stroke', 'var(--muted,#888)'); x.setAttribute('stroke-width', '2.5'); });
|
||
ln.setAttribute('stroke', 'var(--pri,#d97706)'); ln.setAttribute('stroke-width', '4');
|
||
var e = GM_EDGES[+ln.getAttribute('data-i')];
|
||
out.className = 'out gm-out ok';
|
||
out.innerHTML = '<b>' + e.d + '</b><br><span class="bd">' + chemEq(e.r) + '</span>';
|
||
});
|
||
});
|
||
return { el: host };
|
||
}
|
||
|
||
/* ──────────────────────────────────────────────────────────────────────────
|
||
conceptMap(mount, {nodes, edges}) — обобщённая карта связей понятий главы.
|
||
nodes: [{id, t, x, y, c?}]; edges: [{f, t, label}]. Клик по ребру → подпись.
|
||
Используется в финалах глав (U6).
|
||
────────────────────────────────────────────────────────────────────────── */
|
||
function conceptMap(mount, opts) {
|
||
var host = typeof mount === 'string' ? global.document.querySelector(mount) : mount;
|
||
if (!host || !opts) return null;
|
||
var nodes = opts.nodes || [], edges = opts.edges || [];
|
||
var byId = {}; nodes.forEach(function (n) { byId[n.id] = n; });
|
||
var W0 = opts.w || 430, H0 = opts.h || 150;
|
||
function cx(n) { return n.x + 44; } function cy(n) { return n.y + 16; }
|
||
var edgesSvg = edges.map(function (e, i) {
|
||
var a = byId[e.f], b = byId[e.t]; if (!a || !b) return '';
|
||
return '<line class="gm-edge" data-i="' + i + '" x1="' + cx(a) + '" y1="' + cy(a) + '" x2="' + cx(b) + '" y2="' + cy(b) + '" stroke="var(--muted,#888)" stroke-width="2.5"/>';
|
||
}).join('');
|
||
var nodesSvg = nodes.map(function (n) {
|
||
var c = n.c || 'var(--pri,#d97706)';
|
||
return '<g><rect x="' + n.x + '" y="' + n.y + '" width="88" height="32" rx="8" fill="' + c + '" opacity=".16" stroke="' + c + '" stroke-width="1.5"/>'
|
||
+ '<text x="' + cx(n) + '" y="' + (cy(n) + 4) + '" text-anchor="middle" font-size="10.5" font-weight="800" fill="currentColor">' + n.t + '</text></g>';
|
||
}).join('');
|
||
host.innerHTML = '<div class="gm-wrap"><svg viewBox="0 0 ' + W0 + ' ' + H0 + '" class="gm-svg">' + edgesSvg + nodesSvg + '</svg></div>'
|
||
+ '<div class="out gm-out">Кликни по связи — увидишь, как понятия главы связаны.</div>';
|
||
var out = host.querySelector('.gm-out');
|
||
host.querySelectorAll('.gm-edge').forEach(function (ln) {
|
||
ln.style.cursor = 'pointer';
|
||
ln.addEventListener('click', function () {
|
||
host.querySelectorAll('.gm-edge').forEach(function (x) { x.setAttribute('stroke', 'var(--muted,#888)'); x.setAttribute('stroke-width', '2.5'); });
|
||
ln.setAttribute('stroke', 'var(--pri,#d97706)'); ln.setAttribute('stroke-width', '4');
|
||
out.className = 'out gm-out ok'; out.innerHTML = '<b>' + edges[+ln.getAttribute('data-i')].label + '</b>';
|
||
});
|
||
});
|
||
return { el: host };
|
||
}
|
||
|
||
/* ──────────────────────────────────────────────────────────────────────────
|
||
dissociationAnim(mount, {substance}) — анимация растворения/диссоциации:
|
||
вещество распадается на ионы, окружённые молекулами воды. §47.
|
||
────────────────────────────────────────────────────────────────────────── */
|
||
var DISS = {
|
||
NaCl: { cat: 'Na⁺', an: 'Cl⁻', cc: '#d97706', ac: '#0891b2' },
|
||
KCl: { cat: 'K⁺', an: 'Cl⁻', cc: '#7c3aed', ac: '#0891b2' },
|
||
CuSO4: { cat: 'Cu²⁺', an: 'SO₄²⁻', cc: '#0891b2', ac: '#059669' },
|
||
HCl: { cat: 'H⁺', an: 'Cl⁻', cc: '#dc2626', ac: '#0891b2' }
|
||
};
|
||
function dissociationAnim(mount, opts) {
|
||
var host = typeof mount === 'string' ? global.document.querySelector(mount) : mount;
|
||
if (!host) return null;
|
||
opts = opts || {};
|
||
var subs = Object.keys(DISS);
|
||
host.innerHTML = '<div class="fld"><label>Вещество</label><select class="ds-sel">' +
|
||
subs.map(function (s) { return '<option value="' + s + '"' + (s === opts.substance ? ' selected' : '') + '>' + formula(s) + '</option>'; }).join('') + '</select></div>'
|
||
+ '<div class="ds-stage"></div><div class="out ds-out"></div>';
|
||
var sel = host.querySelector('.ds-sel'), stage = host.querySelector('.ds-stage'), out = host.querySelector('.ds-out');
|
||
function draw() {
|
||
var s = sel.value, d = DISS[s];
|
||
// молекулы воды (фон) + катион + анион, разлетающиеся
|
||
var water = '';
|
||
for (var i = 0; i < 7; i++) { var wx = 30 + i * 35, wy = 25 + (i % 3) * 30; water += '<circle cx="' + wx + '" cy="' + wy + '" r="3" fill="#60a5fa" opacity=".5"/>'; }
|
||
stage.innerHTML = '<svg viewBox="0 0 270 100" class="ds-svg">'
|
||
+ '<rect x="6" y="6" width="258" height="88" rx="12" fill="#0891b2" opacity=".07" stroke="#0891b2" stroke-width="1.5"/>' + water
|
||
+ '<circle cx="135" cy="50" r="17" fill="' + d.cc + '" opacity=".85"><animate attributeName="cx" values="135;70;135" dur="3s" repeatCount="indefinite"/></circle>'
|
||
+ '<text x="135" y="55" text-anchor="middle" font-size="11" font-weight="800" fill="#fff"><animate attributeName="x" values="135;70;135" dur="3s" repeatCount="indefinite"/>' + d.cat + '</text>'
|
||
+ '<circle cx="135" cy="50" r="17" fill="' + d.ac + '" opacity=".85"><animate attributeName="cx" values="135;200;135" dur="3s" repeatCount="indefinite"/></circle>'
|
||
+ '<text x="135" y="55" text-anchor="middle" font-size="10" font-weight="800" fill="#fff"><animate attributeName="x" values="135;200;135" dur="3s" repeatCount="indefinite"/>' + d.an + '</text>'
|
||
+ '</svg>';
|
||
out.className = 'out ds-out ok';
|
||
out.innerHTML = '<span class="bd">' + formula(s) + ' → ' + d.cat + ' + ' + d.an + '<br>Молекулы воды окружают ионы и «растаскивают» их (гидратация).</span>';
|
||
}
|
||
sel.addEventListener('change', draw); draw();
|
||
return { el: host };
|
||
}
|
||
|
||
/* ---- Каркасы-заглушки интерактивных виджетов (реализуются по фазам) ---- */
|
||
function notImplemented(name) {
|
||
return function () {
|
||
if (global.console && console.warn) {
|
||
console.warn('[Chem8] ' + name + ' ещё не реализован (Phase 0 заглушка)');
|
||
}
|
||
return null;
|
||
};
|
||
}
|
||
|
||
var Chem8 = {
|
||
// готово (Phase 0)
|
||
formula: formula,
|
||
ionLabel: ionLabel,
|
||
chemEq: chemEq,
|
||
toSub: toSub,
|
||
toSup: toSup,
|
||
// готово (Phase 1 — движки расчётов)
|
||
elementCounts: elementCounts,
|
||
molarMass: molarMass, // school-rounded Ar: Mr(H2O)=18
|
||
arOf: arOf,
|
||
fmt: fmt,
|
||
moleTriangle: moleTriangle, // §6 — треугольник n–m–M
|
||
equationBalancer: equationBalancer, // §8 — балансировщик уравнений
|
||
// готово (Phase 2 — классы неорганических соединений)
|
||
testTube: testTube, // §18,25 — пробирка: осадок/газ/окраска
|
||
indicatorScale: indicatorScale, // §13,14,16,17 — индикатор + шкала pH
|
||
classifier: classifier, // §10,13,16,19 — клик-классификатор
|
||
solubilityTable: solubilityTable, // §19,20 — таблица растворимости
|
||
activitySeries: activitySeries, // §14,20 — ряд активности металлов
|
||
// готово (Phase 3 — периодический закон)
|
||
miniPeriodic: miniPeriodic, // §26,28,34 — интерактивная ПСХЭ с подсветкой
|
||
// готово (Phase 4 — строение атома)
|
||
atomShell: atomShell, // §29,33 — модель атома (слои электронов)
|
||
shellConfig: shellConfig, // распределение электронов по слоям
|
||
nuclide: nuclide, // §30 — A=Z+N, нуклид
|
||
zSym: zSym, // Z → символ элемента
|
||
// готово (Phase 5 — химическая связь)
|
||
bondType: bondType, // §37,38 — ЭО → тип связи
|
||
bondClass: bondClass, // классификация связи по ΔЭО
|
||
enOf: enOf, // электроотрицательность
|
||
// готово (Phase 6 — ОВР)
|
||
oxStateCalc: oxStateCalc, // §42 — калькулятор степени окисления
|
||
oxStates: oxStates, // степени окисления (чистая функция)
|
||
// готово (Phase 8/U3,U6 — апгрейд)
|
||
geneticMap: geneticMap, // §22 — генетическая карта-граф классов
|
||
conceptMap: conceptMap, // финалы глав — карта связей понятий (U6)
|
||
dissociationAnim: dissociationAnim, // §47 — анимация растворения/диссоциации
|
||
// редокс-баланс §44 реализован пошагово в chem8_ch5_widgets (преднабор)
|
||
redoxBalancer: notImplemented('redoxBalancer'),
|
||
orbitalDiagram: notImplemented('orbitalDiagram') // §33 — покрыто atomShell
|
||
};
|
||
|
||
global.Chem8 = Chem8;
|
||
})(typeof window !== 'undefined' ? window : this);
|