diff --git a/backend/tests/chemistry8-page.test.js b/backend/tests/chemistry8-page.test.js index 34a4e0c..0f96f32 100644 --- a/backend/tests/chemistry8-page.test.js +++ b/backend/tests/chemistry8-page.test.js @@ -82,6 +82,12 @@ test('ch1: тренажёр задач отрисован для §10', async () assert.ok(doc.querySelectorAll('#navDotsp10 .nav-dot').length >= 4, 'навигация по задачам §10'); }); +test('ch1: генетическая карта §22 монтируется (U3)', async () => { + const { doc } = await loadDom('chemistry_8_ch1.html', '/js/chem8_ch1_widgets.js'); + doc.defaultView.goTo('p22'); await wait(120); + assert.ok(doc.querySelectorAll('#c-genetic .gm-edge').length >= 6, 'граф классов §22'); +}); + /* ── Глава 2 ── */ test('ch2: SPA без ошибок, 6 карточек, §24 активен, ПСХЭ', async () => { const { doc, errors } = await loadDom('chemistry_8_ch2.html', '/js/chem8_ch2_widgets.js'); @@ -203,3 +209,9 @@ test('ch6: SPA без ошибок, 8 карточек, §46 активен, w/c doc.defaultView.goTo('p51'); await wait(120); assert.ok(doc.querySelector('#c-ccalc #c-go'), 'калькулятор c §51'); }); + +test('ch6: анимация растворения §47 монтируется (U3)', async () => { + const { doc } = await loadDom('chemistry_8_ch6.html', '/js/chem8_ch6_widgets.js'); + doc.defaultView.goTo('p47'); await wait(120); + assert.ok(doc.querySelector('#c-dissoc .ds-svg'), 'анимация диссоциации §47'); +}); diff --git a/backend/tests/chemistry8.test.js b/backend/tests/chemistry8.test.js index 4e8d7b5..59fcd8e 100644 --- a/backend/tests/chemistry8.test.js +++ b/backend/tests/chemistry8.test.js @@ -65,7 +65,7 @@ test('Chem8.elementCounts — скобки и индексы', () => { }); test('Chem8 — оставшиеся заглушки возвращают null и не падают', () => { - for (const fn of ['redoxBalancer', 'orbitalDiagram', 'dissociationAnim', 'geneticMap']) { + for (const fn of ['redoxBalancer', 'orbitalDiagram']) { assert.equal(typeof C[fn], 'function', fn + ' определён'); assert.equal(C[fn]({}), null, fn + ' заглушка возвращает null'); } diff --git a/frontend/css/chem8-textbook.css b/frontend/css/chem8-textbook.css index 382dd67..5d971f9 100644 --- a/frontend/css/chem8-textbook.css +++ b/frontend/css/chem8-textbook.css @@ -401,6 +401,16 @@ html.dark .lat-card h4{color:var(--pri-l)} .amph-stage{display:flex;justify-content:center;margin:8px 0} .amph-out{margin-top:6px} +/* геном-карта классов (§22) */ +.gm-svg{width:100%;max-width:440px;height:auto;color:var(--text);display:block;margin:4px auto} +.gm-out{margin-top:8px} +.gm-edge{transition:stroke .15s,stroke-width .15s} + +/* диссоциация/растворение (§47) */ +.ds-svg{width:100%;max-width:300px;height:auto;display:block;margin:6px auto} +.ds-stage{display:flex;justify-content:center} +.ds-out{margin-top:6px} + /* exa-step (разбор примеров) */ .exa-step{font-family:var(--mono);font-size:.9rem;background:var(--card-soft);border-left:3px solid var(--pri);border-radius:0 8px 8px 0;padding:8px 12px;margin:6px 0} diff --git a/frontend/js/chem8_ch1_widgets.js b/frontend/js/chem8_ch1_widgets.js index e51ed8d..f2e377c 100644 --- a/frontend/js/chem8_ch1_widgets.js +++ b/frontend/js/chem8_ch1_widgets.js @@ -98,6 +98,9 @@ render(); } + /* §22 — генетическая карта классов */ + function mount_p22() { var el = $('c-genetic'); if (el && !el._b && C().geneticMap) { el._b = 1; C().geneticMap(el, {}); } } + W.CHEM8_WIDGETS = { p13: mount_p13, p16: mount_p16, p17: mount_p17, p18: mount_p18 }; - W.FLAG_MOUNTS = { p10: mount_p10, p14: mount_p14, p19: mount_p19, p20: mount_p20, p23: mount_p23 }; + W.FLAG_MOUNTS = { p10: mount_p10, p14: mount_p14, p19: mount_p19, p20: mount_p20, p22: mount_p22, p23: mount_p23 }; })(window); diff --git a/frontend/js/chem8_ch6_widgets.js b/frontend/js/chem8_ch6_widgets.js index bbffc45..e1d8106 100644 --- a/frontend/js/chem8_ch6_widgets.js +++ b/frontend/js/chem8_ch6_widgets.js @@ -76,6 +76,9 @@ $('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 = { p48: mount_p48 }; + W.FLAG_MOUNTS = { p47: mount_p47, p48: mount_p48 }; })(window); diff --git a/frontend/js/chem8_svg.js b/frontend/js/chem8_svg.js index ed833cb..a293bc8 100644 --- a/frontend/js/chem8_svg.js +++ b/frontend/js/chem8_svg.js @@ -800,6 +800,94 @@ 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 ''; + }).join(''); + var nodesSvg = GM_NODES.map(function (n) { + return '' + + '' + n.t + ''; + }).join(''); + host.innerHTML = '
' + edgesSvg + nodesSvg + '
' + + '
Кликни по стрелке-переходу — увидишь реакцию-пример.
'; + 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 = '' + e.d + '
' + chemEq(e.r) + ''; + }); + }); + 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 = '
' + + '
'; + 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 += ''; } + stage.innerHTML = '' + + '' + water + + '' + + '' + d.cat + '' + + '' + + '' + d.an + '' + + ''; + out.className = 'out ds-out ok'; + out.innerHTML = '' + formula(s) + ' → ' + d.cat + ' + ' + d.an + '
Молекулы воды окружают ионы и «растаскивают» их (гидратация).
'; + } + sel.addEventListener('change', draw); draw(); + return { el: host }; + } + /* ---- Каркасы-заглушки интерактивных виджетов (реализуются по фазам) ---- */ function notImplemented(name) { return function () { @@ -844,11 +932,12 @@ // готово (Phase 6 — ОВР) oxStateCalc: oxStateCalc, // §42 — калькулятор степени окисления oxStates: oxStates, // степени окисления (чистая функция) - // заглушки (см. план, разд. B) — наполняются позже - redoxBalancer: notImplemented('redoxBalancer'), // §44 — e-баланс ОВР (пошагово в ch5) - orbitalDiagram: notImplemented('orbitalDiagram'), // §33 — орбитальная диаграмма - dissociationAnim: notImplemented('dissociationAnim'),// §47 — анимация растворения - geneticMap: notImplemented('geneticMap') // §22 — генетическая карта-граф классов + // готово (Phase 8/U3 — апгрейд) + geneticMap: geneticMap, // §22 — генетическая карта-граф классов + dissociationAnim: dissociationAnim, // §47 — анимация растворения/диссоциации + // редокс-баланс §44 реализован пошагово в chem8_ch5_widgets (преднабор) + redoxBalancer: notImplemented('redoxBalancer'), + orbitalDiagram: notImplemented('orbitalDiagram') // §33 — покрыто atomShell }; global.Chem8 = Chem8; diff --git a/frontend/textbooks/chemistry_8_ch1.html b/frontend/textbooks/chemistry_8_ch1.html index 7ee5ce8..77f9b44 100644 --- a/frontend/textbooks/chemistry_8_ch1.html +++ b/frontend/textbooks/chemistry_8_ch1.html @@ -328,6 +328,7 @@ function bp21(){ document.getElementById('p21-body').innerHTML = function bp22(){ document.getElementById('p22-body').innerHTML = hero(4,'§ 22 · Глава 1','Взаимосвязь классов · ПР 3','генетическая связь','Все классы связаны цепочками превращений — от простого вещества до соли.',['генетика','ПР.3']) +makeCard('theory','Генетическая связь','§22','

Ряд металла: металл → основный оксид → основание → соль
Na → Na₂O → NaOH → NaCl

Ряд неметалла: неметалл → кислотный оксид → кислота → соль
S → SO₃ → H₂SO₄ → Na₂SO₄

Эти ряды «встречаются» в солях — продукте реакции кислоты и основания.

') + +flag('Генетическая карта классов','Кликни по стрелке-переходу — увидишь реакцию-пример. Два ряда (металл и неметалл) сходятся в соли.','
') +'
Цепочка превращений

Ca → CaO → Ca(OH)₂ → CaCl₂. Попробуй записать уравнение каждого перехода!

' +makeCard('lab','Практическая работа 3 · Экспериментальные задачи',null,'

По выданным реактивам осуществи цепочку превращений и докажи получение каждого вещества (например, получи из меди — оксид меди, затем соль).

') +rememberBox(['Металл и неметалл — начала двух генетических рядов.','Соль — точка встречи кислотного и основного «миров».']) diff --git a/frontend/textbooks/chemistry_8_ch6.html b/frontend/textbooks/chemistry_8_ch6.html index 31bcbc0..3e7b1f6 100644 --- a/frontend/textbooks/chemistry_8_ch6.html +++ b/frontend/textbooks/chemistry_8_ch6.html @@ -174,9 +174,7 @@ function bp46(){ document.getElementById('p46-body').innerHTML = function bp47(){ document.getElementById('p47-body').innerHTML = hero(4,'§ 47 · Глава 6','Растворение веществ в воде','растворитель + в-во','Что происходит, когда сахар «исчезает» в чае: вода разбирает вещество на частицы.',['раствор','гидратация']) +makeCard('theory','Как идёт растворение','§47','

Раствор = растворитель (чаще вода) + растворённое вещество. При растворении молекулы воды окружают частицы вещества и «растаскивают» их — это гидратация. При этом тепло может выделяться (растворение щёлочи) или поглощаться (растворение соли).

') - +'
Растворение частиц вещества
' - +[ '40,30','70,55','110,35','150,60','185,30' ].map(function(p,i){var xy=p.split(',');return '';}).join('') - +'
Частицы вещества (точки) равномерно распределяются среди молекул воды.
' + +flag('Анимация растворения и гидратации','Выбери вещество — увидишь, как оно распадается на ионы, окружённые молекулами воды.','
') +rememberBox(['Раствор = растворитель + растворённое вещество.','Гидратация — окружение частиц молекулами воды.']) +qList(['Из чего состоит любой раствор?','Что такое гидратация?']) +secNav('p46','p48')+readButton('p47'); wireReadBtn('p47'); }