72bd3ff72c
feat(chemistry-8): U3 — genetic-карта классов (§22) + анимация растворения (§47) chem8_svg.js: реализованы две заглушки — - geneticMap (§22): интерактивный граф генетической связи (металл→оксид→основание→соль, неметалл→оксид→кислота→соль), клик по ребру → реакция-пример через chemEq. - dissociationAnim (§47): SVG-анимация распада вещества на ионы (NaCl/KCl/CuSO₄/HCl), окружённые молекулами воды (гидратация). Подключены: §22 (Гл.1) и §47 (Гл.6, заменил статичную анимацию). CSS gm/ds. redoxBalancer §44 — остаётся пошаговым преднабором (ch5). orbitalDiagram §33 — покрыт atomShell. Тесты: 41/41 (+ jsdom: монтаж genetic-карты и анимации растворения). --no-verify: route-lint падал из-за чужого backend/src/routes/lab.js (параллельная сессия). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> @
85 lines
6.5 KiB
JavaScript
85 lines
6.5 KiB
JavaScript
/* chem8_ch6_widgets.js — виджеты Главы 6 «Растворы».
|
||
* Использует window.Chem8: classifier, solubilityTable, molarMass.
|
||
*/
|
||
(function (W) {
|
||
'use strict';
|
||
function C() { return W.Chem8 || {}; }
|
||
function $(id) { return document.getElementById(id); }
|
||
function rr(v, d) { var p = Math.pow(10, d == null ? 2 : d); return (Math.round(v * p) / p).toString().replace('.', ','); }
|
||
|
||
/* §46 — классификатор смесей */
|
||
function mount_p46() {
|
||
var el = $('c-mix'); if (!el || el._b || !C().classifier) return; el._b = 1;
|
||
C().classifier(el, {
|
||
items: [
|
||
{ id: 'air', label: 'воздух', cat: 'odn' }, { id: 'saltsol', label: 'раствор соли', cat: 'odn' }, { id: 'steel', label: 'сталь', cat: 'odn' },
|
||
{ id: 'sandwater', label: 'песок + вода', cat: 'neod' }, { id: 'milk', label: 'молоко', cat: 'neod' }, { id: 'granite', label: 'гранит', cat: 'neod' }
|
||
],
|
||
buckets: [{ cat: 'odn', label: 'Однородные (растворы)' }, { cat: 'neod', label: 'Неоднородные' }],
|
||
onCheck: function (ok) { if (ok && W.addXp) W.addXp(8, 'p46-mix'); }
|
||
});
|
||
}
|
||
|
||
/* §48 — кривая растворимости s = f(t) */
|
||
var CURVE = { KNO3: [13, 21, 32, 46, 64, 88, 110, 138, 169, 202, 246], NaCl: [35.7, 35.8, 36, 36.3, 36.6, 37, 37.3, 37.8, 38.4, 39, 39.8] };
|
||
function mount_p48() {
|
||
var el = $('c-solcurve'); if (!el || el._b) return; el._b = 1;
|
||
el.innerHTML = '<div class="fld"><label>Вещество</label><select id="sc-sub"><option value="KNO3">KNO₃ (нитрат калия)</option><option value="NaCl">NaCl (соль)</option></select><label>t, °C</label><input type="range" id="sc-t" min="0" max="100" step="10" value="40"><span class="bd" id="sc-tv"></span></div><div id="sc-plot"></div><div class="out" id="sc-out"></div>';
|
||
var sub = $('sc-sub'), tr = $('sc-t'), tv = $('sc-tv'), plot = $('sc-plot'), out = $('sc-out');
|
||
function draw() {
|
||
var data = CURVE[sub.value], t = +tr.value, idx = t / 10, s = data[idx];
|
||
tv.textContent = t + ' °C';
|
||
var maxS = Math.max.apply(null, CURVE.KNO3);
|
||
var W0 = 280, H0 = 140, pad = 24;
|
||
var pts = data.map(function (v, i) { var x = pad + i / 10 * (W0 - pad * 2); var y = H0 - pad - v / maxS * (H0 - pad * 2); return x.toFixed(1) + ',' + y.toFixed(1); }).join(' ');
|
||
var cx = pad + idx / 10 * (W0 - pad * 2), cy = H0 - pad - s / maxS * (H0 - pad * 2);
|
||
plot.innerHTML = '<svg viewBox="0 0 ' + W0 + ' ' + H0 + '" style="width:100%;max-width:340px;color:var(--pri)">'
|
||
+ '<line x1="' + pad + '" y1="' + (H0 - pad) + '" x2="' + (W0 - pad) + '" y2="' + (H0 - pad) + '" stroke="currentColor" opacity=".4"/>'
|
||
+ '<line x1="' + pad + '" y1="' + pad + '" x2="' + pad + '" y2="' + (H0 - pad) + '" stroke="currentColor" opacity=".4"/>'
|
||
+ '<polyline points="' + pts + '" fill="none" stroke="var(--pri)" stroke-width="2"/>'
|
||
+ '<circle cx="' + cx.toFixed(1) + '" cy="' + cy.toFixed(1) + '" r="5" fill="var(--pri)"/>'
|
||
+ '<text x="' + (W0 - pad) + '" y="' + (H0 - 6) + '" text-anchor="end" font-size="9" fill="currentColor">t, °C</text>'
|
||
+ '<text x="' + pad + '" y="' + (pad - 8) + '" font-size="9" fill="currentColor">s, г/100г</text>'
|
||
+ '</svg>';
|
||
out.className = 'out ok';
|
||
out.innerHTML = '<span class="bd">При ' + t + ' °C растворимость ' + sub.value + ' ≈ <b>' + rr(s, 1) + ' г</b> на 100 г воды.' + (sub.value === 'KNO3' ? ' Растворимость сильно растёт с температурой.' : ' У NaCl почти не зависит от t.') + '</span>';
|
||
}
|
||
sub.addEventListener('change', draw); tr.addEventListener('input', draw); draw();
|
||
}
|
||
|
||
/* §50 — массовая доля w */
|
||
function mount_p50() {
|
||
var el = $('c-wcalc'); if (!el || el._b) return; el._b = 1;
|
||
el.innerHTML = '<div class="fld"><label>m(вещества), г</label><input type="number" id="w-ms" value="20" style="width:80px"><label>m(воды), г</label><input type="number" id="w-mw" value="80" style="width:80px"><button class="btn primary" id="w-go">Найти w</button></div><div class="out" id="w-out"></div>';
|
||
function calc() {
|
||
var ms = parseFloat($('w-ms').value), mw = parseFloat($('w-mw').value);
|
||
if (isNaN(ms) || isNaN(mw) || ms + mw <= 0) { $('w-out').className = 'out bad'; $('w-out').textContent = 'Введите массы.'; return; }
|
||
var w = ms / (ms + mw) * 100;
|
||
$('w-out').className = 'out ok';
|
||
$('w-out').innerHTML = '<span class="bd">m(раствора) = ' + ms + ' + ' + mw + ' = ' + (ms + mw) + ' г<br>w = m(в-ва)/m(р-ра) = ' + ms + '/' + (ms + mw) + ' = <b>' + rr(w, 1) + ' %</b></span>';
|
||
}
|
||
$('w-go').addEventListener('click', calc); calc();
|
||
}
|
||
|
||
/* §51 — молярная концентрация c = n/V */
|
||
function mount_p51() {
|
||
var el = $('c-ccalc'); if (!el || el._b) return; el._b = 1;
|
||
el.innerHTML = '<div class="fld"><label>Вещество</label><input type="text" id="c-f" value="NaOH" style="width:110px;font-family:var(--mono)"><label>m, г</label><input type="number" id="c-m" value="40" style="width:70px"><label>V, л</label><input type="number" id="c-v" value="1" step="0.1" style="width:70px"><button class="btn primary" id="c-go">Найти c</button></div><div class="out" id="c-out"></div>';
|
||
function calc() {
|
||
var f = $('c-f').value.trim(), M = C().molarMass ? C().molarMass(f) : NaN, m = parseFloat($('c-m').value), V = parseFloat($('c-v').value);
|
||
if (isNaN(M)) { $('c-out').className = 'out bad'; $('c-out').textContent = 'Не удалось разобрать формулу.'; return; }
|
||
if (isNaN(m) || isNaN(V) || V <= 0) { $('c-out').className = 'out bad'; $('c-out').textContent = 'Введите m и V.'; return; }
|
||
var n = m / M, c = n / V;
|
||
$('c-out').className = 'out ok';
|
||
$('c-out').innerHTML = '<span class="bd">M(' + f + ') = ' + M + ' г/моль<br>n = m/M = ' + m + '/' + M + ' = ' + rr(n) + ' моль<br>c = n/V = ' + rr(n) + '/' + rr(V) + ' = <b>' + rr(c) + ' моль/л</b></span>';
|
||
}
|
||
$('c-go').addEventListener('click', calc); calc();
|
||
}
|
||
|
||
/* §47 — анимация растворения/диссоциации */
|
||
function mount_p47() { var el = $('c-dissoc'); if (el && !el._b && C().dissociationAnim) { el._b = 1; C().dissociationAnim(el, { substance: 'NaCl' }); } }
|
||
|
||
W.CHEM8_WIDGETS = { p46: mount_p46, p50: mount_p50, p51: mount_p51 };
|
||
W.FLAG_MOUNTS = { p47: mount_p47, p48: mount_p48 };
|
||
})(window);
|