From 2c80a52d6f15a7a7325e3ebeb548ff9a68a223ed Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sat, 30 May 2026 18:50:23 +0300 Subject: [PATCH 01/47] =?UTF-8?q?feat(chemistry7):=20Phase=202=20=D0=92?= =?UTF-8?q?=D0=BE=D0=BB=D0=BD=D0=B0=202=20=E2=80=94=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B2=D0=B0=202=20=D0=B7=D0=B0=D0=B2=D0=B5=D1=80=D1=88=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20(=C2=A716,=20=C2=A717,=20=D0=9F=D0=A02,=20=D1=84?= =?UTF-8?q?=D0=B8=D0=BD=D0=B0=D0=BB)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §16 Оксиды (конструктор оксида по валентности + классификатор оксид/не оксид), §17 Получение кислорода (схема разложения KMnO4/H2O2, понятие катализатора), ПР2 Получение кислорода (доказательство тлеющей лучинкой), финал главы (6 интегрированных боссов + шпаргалка). Глава 2 «Кислород» наполнена полностью (§§13–17). Тесты chem7: 12/12 pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/tests/chemistry7-page.test.js | 14 ++++ frontend/textbooks/chemistry_7_ch2.html | 100 +++++++++++++++++++++++- 2 files changed, 111 insertions(+), 3 deletions(-) diff --git a/backend/tests/chemistry7-page.test.js b/backend/tests/chemistry7-page.test.js index dc4c4c6..bfef7eb 100644 --- a/backend/tests/chemistry7-page.test.js +++ b/backend/tests/chemistry7-page.test.js @@ -148,6 +148,20 @@ test('ch2 Волна 1: интерактивы §13 + ЛО2 + §14 + §15 мон assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | ')); }); +test('ch2 Волна 2: §16 + §17 + ПР2 + финал главы монтируются', async () => { + const { doc, errors } = await loadDom('chemistry_7_ch2.html'); + doc.defaultView.goTo('p16'); await wait(100); + assert.ok(doc.querySelector('#p16-bld #p16-el'), 'конструктор оксида §16'); + assert.ok(doc.querySelector('#p16-cls .c7-chip'), 'классификатор оксид/не оксид §16'); + doc.defaultView.goTo('p17'); await wait(100); + assert.ok(doc.querySelector('#p17-prod #p17-pick'), 'схема получения O2 §17'); + doc.defaultView.goTo('pr2'); await wait(100); + assert.ok(doc.querySelector('#pr2-test #pr2-go'), 'проверка кислорода ПР2'); + doc.defaultView.goTo('final2'); await wait(120); + assert.ok(doc.querySelectorAll('#navDotsfinal2 .nav-dot').length >= 6, 'боссы финала главы 2'); + assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | ')); +}); + /* ── Хаб: каталог глав + финал курса ── */ function buildHub() { let html = readF('frontend/textbooks/chemistry_7_hub.html'); diff --git a/frontend/textbooks/chemistry_7_ch2.html b/frontend/textbooks/chemistry_7_ch2.html index 4842c14..1e124e2 100644 --- a/frontend/textbooks/chemistry_7_ch2.html +++ b/frontend/textbooks/chemistry_7_ch2.html @@ -90,18 +90,28 @@ window.PARAS = [ ]; window.ACH_LABELS = { start:'Начало главы 2!', p13_done:'§13 изучен!', lo2_done:'Лабораторный опыт 2 выполнен!', - p14_done:'§14 изучен!', p15_done:'§15 изучен!', final2_tasks:'Глава 2 пройдена!' }; + p14_done:'§14 изучен!', p15_done:'§15 изучен!', + p16_done:'§16 изучен!', p17_done:'§17 изучен!', pr2_done:'Практическая работа 2 выполнена!', + final2_tasks:'Глава 2 пройдена! Вы — Повелитель кислорода!' }; window.SIDEBARS = { p13:{ title:'Шпаргалка §13', rows:[['Воздух','смесь газов'],['$N_2$','≈ 78 %'],['$O_2$','≈ 21 %']] }, lo2:{ title:'Лаб. опыт 2', rows:[['Прибор','пробирка + трубка'],['Собирание','воздуха или воды']] }, p14:{ title:'Шпаргалка §14', rows:[['Элемент','O, $Z=8$'],['Вещество','$O_2$'],['Озон','$O_3$']] }, - p15:{ title:'Шпаргалка §15', rows:[['Горение','+ $O_2$'],['Продукт','оксид'],['Окисление','медленное и быстрое']] } + p15:{ title:'Шпаргалка §15', rows:[['Горение','+ $O_2$'],['Продукт','оксид'],['Окисление','медленное и быстрое']] }, + p16:{ title:'Шпаргалка §16', rows:[['Оксид','Э + O'],['O','валентность II'],['Формула','$Э_xO_y$']] }, + p17:{ title:'Шпаргалка §17', rows:[['Лаборатория','разложение'],['Катализатор','ускоряет, не тратится'],['Промышленность','из воздуха']] }, + pr2:{ title:'Практическая 2', rows:[['Получить','разложением'],['Доказать','тлеющая лучинка']] }, + final2:{ title:'Финал главы 2', rows:[['§§13–17','кислород'],['Награда','ачивка + XP']] } }; window.TIPS = [ { sec:'p13', html:'Воздух — смесь газов: примерно $78\\,\\%$ азота $N_2$ и $21\\,\\%$ кислорода $O_2$, около $1\\,\\%$ — другие газы.' }, { sec:'lo2', html:'Газ, который тяжелее воздуха (как $O_2$), собирают в сосуд отверстием вверх; легче воздуха ($H_2$) — отверстием вниз; нерастворимый — вытеснением воды.' }, { sec:'p14', html:'$O$ — элемент (атом в составе веществ). $O_2$ — простое вещество. Кислород $O_2$ и озон $O_3$ — разные простые вещества одного элемента.' }, - { sec:'p15', html:'При горении вещество соединяется с кислородом — образуется оксид. Реакции с кислородом называют реакциями окисления.' } + { sec:'p15', html:'При горении вещество соединяется с кислородом — образуется оксид. Реакции с кислородом называют реакциями окисления.' }, + { sec:'p16', html:'Оксид — сложное вещество из двух элементов, один из которых кислород (валентность II). Формулу составляют по валентности: оксид алюминия — $Al_2O_3$.' }, + { sec:'p17', html:'В лаборатории $O_2$ получают разложением веществ, богатых кислородом. Катализатор ($MnO_2$) ускоряет реакцию, но сам не расходуется.' }, + { sec:'pr2', html:'Кислород доказывают тлеющей лучинкой: в кислороде она ярко вспыхивает.' }, + { sec:'final2', html:'Собери всё: состав воздуха, кислород-элемент и $O_2$, горение и оксиды, получение кислорода и катализатор.' } ]; window.POOLS = { @@ -122,6 +132,26 @@ window.POOLS = { {q:'При горении серы в кислороде образуется…',opts:['$\\text{SO}_2$','$\\text{H}_2\\text{S}$','$\\text{S}$','$\\text{SO}_3$ только'],a:0,ex:'$S+O_2=SO_2$ (с резким запахом).'}, {q:'Реакция $\\text{C}+\\text{O}_2=\\text{CO}_2$ относится к реакциям…',opts:['Разложения','Соединения','Обмена','Замещения'],a:1,ex:'Из двух веществ одно — соединение.'}, {q:'В уравнении $2\\text{Mg}+\\text{O}_2=2\\text{MgO}$ коэффициент перед $\\text{MgO}$ равен…',hint:'смотри на оксид',unit:'',a:2,ex:'Коэффициент 2.'} + ], + p16:[ + {q:'Оксид — это…',opts:['Соль','Сложное вещество из двух элементов, один из которых кислород','Простое вещество','Кислота'],a:1,ex:'Оксид — соединение элемента с кислородом.'}, + {q:'Какое вещество является оксидом?',opts:['NaCl','CO₂','H₂SO₄','HCl'],a:1,ex:'$CO_2$ — оксид углерода(IV).'}, + {q:'Какова валентность меди в оксиде $\\text{CuO}$ (кислород — II)?',hint:'равна валентности O',unit:'',a:2,ex:'Cu — II.'}, + {q:'Какова формула оксида фосфора, в котором фосфор пятивалентен?',opts:['PO','P₂O₅','P₂O₃','PO₂'],a:1,ex:'P(V), O(II) → $P_2O_5$.'} + ], + p17:[ + {q:'Как получают кислород в лаборатории?',opts:['Разложением веществ, богатых кислородом','Из поваренной соли','Сжиганием угля','Из песка'],a:0,ex:'Например, разложением $KMnO_4$ или $H_2O_2$.'}, + {q:'Катализатор — это вещество, которое…',opts:['Расходуется в реакции','Ускоряет реакцию и не расходуется','Замедляет реакцию','Само горит'],a:1,ex:'Катализатор ускоряет реакцию, оставаясь неизменным.'}, + {q:'Реакция $2\\text{KMnO}_4=\\text{K}_2\\text{MnO}_4+\\text{MnO}_2+\\text{O}_2$ — это реакция…',opts:['Соединения','Разложения','Замещения','Обмена'],a:1,ex:'Из одного вещества — несколько: разложение.'}, + {q:'Откуда в промышленности получают кислород?',opts:['Из воздуха','Из поваренной соли','Из угля','Из песка'],a:0,ex:'Кислород выделяют из жидкого воздуха.'} + ], + final2:[ + {q:'Объёмная доля кислорода в воздухе (%)?',hint:'≈ 21',unit:'%',a:21,ex:'21 %.'}, + {q:'Валентность серы в оксиде $\\text{SO}_2$ (O — II)?',hint:'$2\\cdot\\text{II}$',unit:'',a:4,ex:'IV.'}, + {q:'В уравнении $2\\text{Mg}+\\text{O}_2=2\\text{MgO}$ коэффициент перед $\\text{O}_2$?',hint:'',unit:'',a:1,ex:'1.'}, + {q:'Продукт горения углерода в кислороде — это…',opts:['CO₂','CO только','C','H₂O'],a:0,ex:'$C+O_2=CO_2$.'}, + {q:'Катализатор в реакции…',opts:['Расходуется','Не расходуется','Сгорает','Испаряется'],a:1,ex:'Катализатор не расходуется.'}, + {q:'Сколько атомов кислорода в формуле оксида фосфора $\\text{P}_2\\text{O}_5$?',hint:'индекс при O',unit:'',a:5,ex:'5.'} ] }; @@ -195,6 +225,66 @@ function build_p15(){ wireReadBtn('p15'); } +function build_p16(){ + document.getElementById('p16-body').innerHTML = + '
§ 16 · Химия 7

Оксиды

' + +'
$Э_x\\text{O}_y$
' + +'
Что такое оксиды, как составляют их формулы и названия.
' + +'
оксидвалентность O — II
' + +makeCard('theory','Что такое оксид','§16','
Оксид — сложное вещество, состоящее из двух элементов, один из которых — кислород (в оксидах он имеет валентность II).
' + +'

Примеры: $\\text{CuO}$, $\\text{CO}_2$, $\\text{SO}_2$, $\\text{P}_2\\text{O}_5$, $\\text{Fe}_3\\text{O}_4$, $\\text{H}_2\\text{O}$, $\\text{CaO}$. Формулу оксида составляют по валентности, как и любую формулу.

') + +makeCard('example','Названия оксидов',null,'

Название: «оксид» + название элемента. Если элемент имеет переменную валентность, её указывают римской цифрой: $\\text{CuO}$ — оксид меди(II), $\\text{CO}_2$ — оксид углерода(IV).

') + +wgt('Конструктор оксида','
') + +wgt('Распредели: оксид или не оксид?','
') + +rememberBox(['Оксид — соединение элемента с кислородом.','Кислород в оксидах двухвалентен (II).','Название: «оксид» + элемент (+ валентность, если переменная).']) + +qList(['Дай определение оксида.','Составь формулу оксида кальция (Ca — II).','Назови оксид $\\text{SO}_2$.']) + +secNav('p15','p17')+readButton('p16'); + wireReadBtn('p16'); +} + +function build_p17(){ + document.getElementById('p17-body').innerHTML = + '
§ 17 · Химия 7

Получение кислорода

' + +'
Как получают кислород в лаборатории и в промышленности и что такое катализатор.
' + +'
разложениекатализатор
' + +makeCard('theory','Получение в лаборатории','§17','

В лаборатории кислород получают разложением веществ, богатых кислородом: перманганата калия $\\text{KMnO}_4$ (при нагревании) или пероксида водорода $\\text{H}_2\\text{O}_2$.

' + +'
Реакция разложения — реакция, в которой из одного сложного вещества образуется несколько других веществ.
') + +makeCard('rule','Катализатор','§17','

Разложение $\\text{H}_2\\text{O}_2$ ускоряет катализатор — оксид марганца(IV) $\\text{MnO}_2$.

' + +'
Катализатор — вещество, которое ускоряет химическую реакцию, но само в ней не расходуется.
' + +'

В промышленности кислород получают из воздуха (разделяя сжиженный воздух).

') + +wgt('Схема получения кислорода','
') + +rememberBox(['В лаборатории $O_2$ — разложением $KMnO_4$ или $H_2O_2$.','Катализатор ускоряет реакцию и не расходуется.','В промышленности кислород получают из воздуха.']) + +qList(['Что такое реакция разложения?','Какую роль играет $\\text{MnO}_2$ при разложении пероксида водорода?','Откуда получают кислород в промышленности?']) + +secNav('p16','pr2')+readButton('p17'); + wireReadBtn('p17'); +} + +function build_pr2(){ + document.getElementById('pr2-body').innerHTML = + '
Практическая работа 2

Получение кислорода и изучение его свойств

' + +'
Получить кислород, собрать его и доказать, что это именно кислород.
' + +makeCard('lab','Ход работы',null,'
  1. Собери прибор для получения газа (пробирка с газоотводной трубкой).
  2. Получи кислород разложением вещества, богатого кислородом.
  3. Собери кислород в сосуд (вытеснением воздуха — отверстием вверх, или вытеснением воды).
  4. Внеси в сосуд тлеющую лучинку — она ярко вспыхнет: это доказывает, что газ — кислород.
  5. Сделай вывод о свойствах кислорода.
' + +'
Нагревай пробирку осторожно; не направляй её отверстием на людей.
') + +wgt('Докажи, что газ — кислород','
') + +secNav('p17','final2')+readButton('pr2'); + wireReadBtn('pr2'); +} + +function build_final2(){ + document.getElementById('final2-body').innerHTML = + '
Финал главы 2

Босс: кислород

' + +'
воздух · $O_2$ · горение · оксиды · получение
' + +'
Шесть задач на всю главу. Реши все — получи звание «Повелитель кислорода».
' + +makeCard('rule','Шпаргалка главы 2',null,'') + +'

Реши задачи ниже — за каждую +5 XP, за полный разгром босса — звание и бонус.

' + +secNav('pr2',null); +} + /* заглушки для ещё не наполненных § (следующая волна) */ (function(){ var P = window.PARAS, B = {}; @@ -221,6 +311,10 @@ window.BUILDERS.p13 = build_p13; window.BUILDERS.lo2 = build_lo2; window.BUILDERS.p14 = build_p14; window.BUILDERS.p15 = build_p15; +window.BUILDERS.p16 = build_p16; +window.BUILDERS.p17 = build_p17; +window.BUILDERS.pr2 = build_pr2; +window.BUILDERS.final2 = build_final2; From 0af08bcc557ad736b162ee2e13531a1398257c65 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sat, 30 May 2026 18:57:28 +0300 Subject: [PATCH 02/47] =?UTF-8?q?feat(chemistry7):=20Phase=203=20=D0=92?= =?UTF-8?q?=D0=BE=D0=BB=D0=BD=D0=B0=201=20=E2=80=94=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B2=D0=B0=203,=20=C2=A718=20+=20=C2=A719=20+=20=C2=A720=20+?= =?UTF-8?q?=20=D0=9B=D0=9E3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §18 Водород — элемент и простое вещество (паспорт + модель H2), §19 Химические свойства водорода (горение → вода, восстановление CuO → Cu), §20 Понятие о кислотах (индикаторы лакмус/метилоранж + таблица кислот), ЛО3 Действие кислот на индикаторы. chem7_ch3_widgets.js. Тест: 13/13 pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/tests/chemistry7-page.test.js | 14 ++ frontend/js/chem7_ch3_widgets.js | 172 ++++++++++++++++++++++++ frontend/textbooks/chemistry_7_ch3.html | 120 ++++++++++++++++- 3 files changed, 301 insertions(+), 5 deletions(-) create mode 100644 frontend/js/chem7_ch3_widgets.js diff --git a/backend/tests/chemistry7-page.test.js b/backend/tests/chemistry7-page.test.js index bfef7eb..f2b1d23 100644 --- a/backend/tests/chemistry7-page.test.js +++ b/backend/tests/chemistry7-page.test.js @@ -25,6 +25,7 @@ function buildPage(file) { '/js/chem7_svg.js': readF('frontend/js/chem7_svg.js'), '/js/chem7_ch1_widgets.js': readF('frontend/js/chem7_ch1_widgets.js'), '/js/chem7_ch2_widgets.js': readF('frontend/js/chem7_ch2_widgets.js'), + '/js/chem7_ch3_widgets.js': readF('frontend/js/chem7_ch3_widgets.js'), '/js/chem8_engine.js': readF('frontend/js/chem8_engine.js') }; html = html @@ -162,6 +163,19 @@ test('ch2 Волна 2: §16 + §17 + ПР2 + финал главы монтир assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | ')); }); +test('ch3 Волна 1: §18 + §19 + §20 + ЛО3 монтируются', async () => { + const { doc, errors } = await loadDom('chemistry_7_ch3.html'); + assert.ok(doc.querySelector('#p18-card svg'), 'паспорт водорода §18'); + doc.defaultView.goTo('p19'); await wait(100); + assert.ok(doc.querySelector('#p19-rx #p19-pick'), 'реакции водорода §19'); + doc.defaultView.goTo('p20'); await wait(100); + assert.ok(doc.querySelector('#p20-ind #p20-ind-ind'), 'индикаторы §20'); + assert.ok(doc.querySelector('#p20-acids table'), 'таблица кислот §20'); + doc.defaultView.goTo('lo3'); await wait(100); + assert.ok(doc.querySelector('#lo3-ind #lo3-ind-ind'), 'индикаторы ЛО3'); + assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | ')); +}); + /* ── Хаб: каталог глав + финал курса ── */ function buildHub() { let html = readF('frontend/textbooks/chemistry_7_hub.html'); diff --git a/frontend/js/chem7_ch3_widgets.js b/frontend/js/chem7_ch3_widgets.js new file mode 100644 index 0000000..2369a47 --- /dev/null +++ b/frontend/js/chem7_ch3_widgets.js @@ -0,0 +1,172 @@ +/* chem7_ch3_widgets.js — интерактивы главы 3 «Водород» (Химия 7). + * Монтируются движком chem8_engine.js: window.CHEM8_WIDGETS[id]. + * Используют window.Chem8 (chem8_svg.js): chemEq, formula. + * Без эмоджи; 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,'&').replace(//g,'>'); } + function gcd(a, b) { return b ? gcd(b, a % b) : a; } + function ceq(src, opts){ return C().chemEq ? C().chemEq(src, opts || {}) : esc(src); } + function fml(s){ return C().formula ? C().formula(s) : s; } + var COL = { H:'#cbd5e1', O:'#ef4444' }; + function molSvg(atoms){ + var list=[]; atoms.forEach(function(p){ for(var i=0;i'+el+''; x+=24; }); + return ''+svg+''; + } + + /* §18 — модель H₂ + паспорт водорода */ + function mount_p18() { + var m = $('p18-card'); if (!m || m._built) return; m._built = 1; + m.innerHTML = molSvg([['H',2]]) + + '
Водород
Элемент: символ H, $Z=1$, $A_r=1$ — самый лёгкий элемент.
' + + 'Простое вещество: молекула $H_2$ — самый лёгкий газ, без цвета и запаха, легче воздуха, мало растворим в воде.
' + + 'В природе: в составе воды, многих веществ; во Вселенной — самый распространённый элемент.
'; + if (W.chem8RenderMath) try { W.chem8RenderMath(m); } catch(e){} + } + + /* §19 — реакции водорода: горение и восстановление */ + var RX = [ + { name:'Горение водорода в кислороде', eq:'2H2 + O2 = 2H2O', note:'Водород горит, образуя воду. Смесь водорода с воздухом — «гремучий газ», взрывается!' }, + { name:'Восстановление оксида меди(II)', eq:'H2 + CuO = Cu + H2O', note:'Водород отнимает кислород у оксида: чёрный CuO превращается в красную медь. Водород здесь — восстановитель.' } + ]; + function mount_p19() { + var m = $('p19-rx'); if (!m || m._built) return; m._built = 1; + var idx = 0; + function render(){ + var r = RX[idx]; + var swatch = idx===1 ? '
CuO (чёрный) → Cu (красный)
' : ''; + m.innerHTML = '
' + + '
' + ceq(r.eq) + '
' + esc(r.note) + '
' + swatch + '
'; + $('p19-pick').addEventListener('change', function(e){ idx=+e.target.value; m._built=0; render(); }); + } + render(); + } + + /* индикаторы */ + var ACIDS = [ + { f:'HCl', name:'соляная', res:'Cl', resName:'хлорид', resVal:1 }, + { f:'H2SO4', name:'серная', res:'SO4', resName:'сульфат', resVal:2 }, + { f:'HNO3', name:'азотная', res:'NO3', resName:'нитрат', resVal:1 }, + { f:'H2CO3', name:'угольная', res:'CO3', resName:'карбонат',resVal:2 } + ]; + var INDIC = { + 'Лакмус': { neutral:['#7c3aed','фиолетовый'], acid:['#dc2626','красный'] }, + 'Метилоранж': { neutral:['#f59e0b','оранжевый'], acid:['#e11d48','розово-красный'] } + }; + function indicatorWidget(mountId, withAcidPick) { + var m = $(mountId); if (!m || m._built) return; m._built = 1; + var ind = 'Лакмус', acid = 0; + function strip(color){ return '
'; } + function render(){ + var a = ACIDS[acid], col = INDIC[ind]; + m.innerHTML = '
' + + (withAcidPick ? '' : '') + '
' + + '
В нейтральной среде: ' + strip(col.neutral[0]) + ' '+col.neutral[1]+'
' + + 'В кислоте' + (withAcidPick?(' ('+fml(a.f)+')'):'') + ': ' + strip(col.acid[0]) + ' '+col.acid[1]+'
'; + $(mountId+'-ind').addEventListener('change', function(e){ ind=e.target.value; m._built=0; render(); }); + if (withAcidPick) $(mountId+'-acid').addEventListener('change', function(e){ acid=+e.target.value; m._built=0; render(); }); + } + render(); + } + function mount_p20() { + indicatorWidget('p20-ind', true); + var t = $('p20-acids'); if (t && !t._built) { t._built = 1; + t.innerHTML = '' + + ACIDS.map(function(a){ return ''; }).join('') + '
КислотаНазваниеОстаток
'+fml(a.f)+''+a.name+''+fml(a.res)+' ('+a.resName+')
'; + } + } + function mount_lo3() { indicatorWidget('lo3-ind', false); } + + /* §21 — ряд активности металлов */ + var ROW = ['K','Ca','Na','Mg','Al','Zn','Fe','Ni','Sn','Pb','H','Cu','Hg','Ag','Pt','Au']; + function mount_p21() { + var m = $('p21-act'); if (!m || m._built) return; m._built = 1; + var hIdx = ROW.indexOf('H'); + m.innerHTML = '
' + + ROW.map(function(el,i){ var isH=el==='H'; return ''; }).join('') + '
' + + '
Слева активность убывает вправо. Граница — водород H₂.
' + + '
Кликни по металлу — узнаешь, вытесняет ли он водород из кислоты.
'; + var out = $('p21-act-out'); + m.querySelectorAll('.act-cell').forEach(function(b){ + b.addEventListener('click', function(){ + var i=+b.dataset.i, el=ROW[i]; if(el==='H'){ out.className='out'; out.innerHTML='Водород H₂ — граница ряда активности.'; return; } + out.className='out ok'; + if(iВнимание: очень активный металл — с кислотами реагирует бурно (для получения водорода используют Zn, Fe).' : ''; + out.innerHTML = ''+el+' стоит левее H₂ → вытесняет водород из соляной и серной кислот: образуются соль и $H_2\\uparrow$.'+extra; + } else { + out.innerHTML = ''+el+' стоит правее H₂ → водород из кислот не вытесняет (например, медь и серебро с этими кислотами не реагируют).'; + } + if (W.chem8RenderMath) try { W.chem8RenderMath(out); } catch(e){} + }); + }); + } + + /* ЛО4 — взаимодействие кислот с металлами */ + var L4M = [ ['Zn','цинк',1], ['Fe','железо',1], ['Mg','магний',1], ['Cu','медь',0] ]; + var L4A = [ ['HCl','соляная'], ['H2SO4','серная'] ]; + function mount_lo4() { + var m = $('lo4-rx'); if (!m || m._built) return; m._built = 1; + var mi=0, ai=0; + var EQ = { 'Zn|HCl':'Zn + 2HCl = ZnCl2 + H2^', 'Zn|H2SO4':'Zn + H2SO4 = ZnSO4 + H2^', + 'Fe|HCl':'Fe + 2HCl = FeCl2 + H2^', 'Fe|H2SO4':'Fe + H2SO4 = FeSO4 + H2^', + 'Mg|HCl':'Mg + 2HCl = MgCl2 + H2^', 'Mg|H2SO4':'Mg + H2SO4 = MgSO4 + H2^' }; + function render(){ + m.innerHTML = '
' + + '' + + '
Выбери металл и кислоту.
'; + $('lo4-m').addEventListener('change',function(e){mi=+e.target.value;m._built=0;render();}); + $('lo4-a').addEventListener('change',function(e){ai=+e.target.value;m._built=0;render();}); + $('lo4-go').addEventListener('click',function(){ + var met=L4M[mi], ac=L4A[ai], out=$('lo4-out'); + if(!met[2]){ out.className='out bad'; out.innerHTML=''+met[1]+' стоит правее H₂ в ряду активности — реакция не идёт, пузырьки не выделяются.'; return; } + out.className='out ok'; + out.innerHTML='Наблюдаем выделение пузырьков газа (водород $H_2\\uparrow$). Металл вытесняет водород из кислоты:
'+ceq(EQ[met[0]+'|'+ac[0]])+'
'; + if (W.chem8RenderMath) try { W.chem8RenderMath(out); } catch(e){} + }); + } + render(); + } + + /* §22 — конструктор солей (металл + кислотный остаток) */ + var SM = [ ['Na',1], ['K',1], ['Ca',2], ['Mg',2], ['Zn',2], ['Al',3] ]; + var SR = [ ['Cl',1,'хлорид'], ['NO3',1,'нитрат'], ['SO4',2,'сульфат'], ['CO3',2,'карбонат'] ]; + function mount_p22() { + var m = $('p22-salt'); if (!m || m._built) return; m._built = 1; + function render(){ + m.innerHTML = '
' + + '
'; + $('p22-m').addEventListener('change',upd); $('p22-r').addEventListener('change',upd); upd(); + } + function rom(n){ return ['','I','II','III'][n]; } + function upd(){ + var me=SM[+$('p22-m').value], re=SR[+$('p22-r').value]; + var lcm=me[1]*re[1]/gcd(me[1],re[1]), x=lcm/me[1], y=lcm/re[1]; + var poly=/[0-9]/.test(re[0]); + var raw = me[0] + (x>1?x:'') + (poly && y>1 ? '('+re[0]+')'+y : re[0] + (y>1?y:'')); + var out=$('p22-out'); out.className='out ok'; + out.innerHTML='Валентности: '+me[0]+' = '+rom(me[1])+', остаток '+fml(re[0])+' = '+rom(re[1])+'
Формула соли ('+re[2]+'а): '+fml(raw)+'
'; + } + render(); + } + + /* ПР3 — чистота водорода («гремучий газ») */ + function mount_pr3() { + var m = $('pr3-test'); if (!m || m._built) return; m._built = 1; + m.innerHTML = '
Чтобы проверить чистоту водорода, его поджигают.
'; + $('pr3-mix').addEventListener('click',function(){ var o=$('pr3-out'); o.className='out bad'; o.innerHTML='Смесь водорода с воздухом — «гремучий газ» — взрывается с резким хлопком. Значит, водород собран нечисто.'; }); + $('pr3-pure').addEventListener('click',function(){ var o=$('pr3-out'); o.className='out ok'; o.innerHTML='Чистый водород горит спокойно, почти без звука. Значит, газ собран чисто.'; }); + } + + W.CHEM8_WIDGETS = Object.assign(W.CHEM8_WIDGETS || {}, { + p18: mount_p18, p19: mount_p19, p20: mount_p20, lo3: mount_lo3, + p21: mount_p21, lo4: mount_lo4, p22: mount_p22, pr3: mount_pr3 + }); + W.FLAG_MOUNTS = Object.assign(W.FLAG_MOUNTS || {}, {}); +})(window); diff --git a/frontend/textbooks/chemistry_7_ch3.html b/frontend/textbooks/chemistry_7_ch3.html index da9ee68..3a7b08f 100644 --- a/frontend/textbooks/chemistry_7_ch3.html +++ b/frontend/textbooks/chemistry_7_ch3.html @@ -17,6 +17,7 @@ + @@ -90,11 +91,114 @@ window.PARAS = [ {id:'final3', num:'★', name:'Финал главы', sub:'босс · ачивка', final:true} ]; -window.ACH_LABELS = { start:'Начало главы 3!', final3_tasks:'Глава 3 пройдена!' }; -window.SIDEBARS = { p18:{ title:'Глава 3 · Химия 7', rows:[['Раздел','Водород'],['§§','18–22'],['Лаб/ПР','ЛО 3,4 · ПР 3']] } }; -window.TIPS = [{ sec:'p18', html:'Глава наполняется содержанием по фазам. Сейчас доступны навигация по параграфам и отметка о прочтении (+10 XP).' }]; +window.ACH_LABELS = { start:'Начало главы 3!', p18_done:'§18 изучен!', p19_done:'§19 изучен!', + p20_done:'§20 изучен!', lo3_done:'Лабораторный опыт 3 выполнен!', final3_tasks:'Глава 3 пройдена!' }; +window.SIDEBARS = { + p18:{ title:'Шпаргалка §18', rows:[['$H$','элемент, $Z=1$'],['$H_2$','самый лёгкий газ'],['Вселенная','самый частый элемент']] }, + p19:{ title:'Шпаргалка §19', rows:[['Горение','$+O_2 \\to H_2O$'],['Гремучий газ','$H_2$ + воздух'],['Восстановитель','отнимает O у оксида']] }, + p20:{ title:'Шпаргалка §20', rows:[['Кислота','H + остаток'],['Примеры','$HCl$, $H_2SO_4$'],['Лакмус','в кислоте красный']] }, + lo3:{ title:'Лаб. опыт 3', rows:[['Лакмус','красный'],['Метилоранж','розово-красный']] } +}; +window.TIPS = [ + { sec:'p18', html:'$H$ — самый лёгкий элемент ($A_r=1$). Простое вещество $H_2$ — самый лёгкий газ, легче воздуха. Во Вселенной водород — самый распространённый элемент.' }, + { sec:'p19', html:'Водород горит в кислороде, образуя воду. С оксидами он ведёт себя как восстановитель: $H_2 + CuO = Cu + H_2O$.' }, + { sec:'p20', html:'Кислота = атомы водорода + кислотный остаток. Индикаторы меняют цвет: лакмус в кислоте — красный, метилоранж — розово-красный.' }, + { sec:'lo3', html:'В кислоте лакмус становится красным, а метилоранж — розово-красным. Так обнаруживают кислоту.' } +]; -/* Phase 0: заглушки-builder'ы из PARAS (теория и интерактивы добавляются в фазах 1–4). */ +window.POOLS = { + p18:[ + {q:'Водород — это самый…',opts:['Тяжёлый газ','Лёгкий газ','Активный металл','Ядовитый газ'],a:1,ex:'$H_2$ — самый лёгкий газ.'}, + {q:'Чему равна относительная атомная масса водорода $A_r(\\text{H})$?',hint:'из таблицы',unit:'',a:1,ex:'$A_r(\\text{H})=1$.'}, + {q:'$\\text{H}_2$ — это…',opts:['Химический элемент','Простое вещество','Сложное вещество','Смесь'],a:1,ex:'Молекула из двух атомов одного элемента — простое вещество.'}, + {q:'Где водород встречается чаще всего?',opts:['В земной коре','Во Вселенной','В металлах','В песке'],a:1,ex:'Во Вселенной водород — самый распространённый элемент.'} + ], + p19:[ + {q:'При горении водорода в кислороде образуется…',opts:['Углекислый газ','Вода','Оксид меди','Соль'],a:1,ex:'$2H_2+O_2=2H_2O$.'}, + {q:'Смесь водорода с воздухом называют…',opts:['Угарным газом','Гремучим газом','Озоном','Сухим льдом'],a:1,ex:'Гремучий газ взрывоопасен.'}, + {q:'В реакции $\\text{H}_2+\\text{CuO}=\\text{Cu}+\\text{H}_2\\text{O}$ водород является…',opts:['Окислителем','Восстановителем','Катализатором','Индикатором'],a:1,ex:'Водород отнимает кислород — восстановитель.'}, + {q:'Коэффициент перед $\\text{H}_2\\text{O}$ в $2\\text{H}_2+\\text{O}_2=2\\text{H}_2\\text{O}$?',hint:'',unit:'',a:2,ex:'2.'} + ], + p20:[ + {q:'Из чего состоит кислота?',opts:['Из металла и кислорода','Из атомов водорода и кислотного остатка','Из двух металлов','Из воды и соли'],a:1,ex:'Кислота = водород + кислотный остаток.'}, + {q:'Какого цвета становится лакмус в кислоте?',opts:['Синего','Красного','Зелёного','Жёлтого'],a:1,ex:'Лакмус в кислоте — красный.'}, + {q:'Какова формула серной кислоты?',opts:['HCl','H₂SO₄','HNO₃','H₂CO₃'],a:1,ex:'Серная кислота — $H_2SO_4$.'}, + {q:'Индикатор — это вещество, которое…',opts:['Ускоряет реакцию','Меняет цвет в кислоте или щёлочи','Растворяет металлы','Выделяет газ'],a:1,ex:'Индикаторы обнаруживают кислоты и щёлочи по изменению цвета.'} + ] +}; + +function rememberBox(items){ + return '
' + +' Запомни!
    ' + +items.map(function(t){return '
  • '+t+'
  • ';}).join('')+'
'; +} +function qList(items){ + return '
Вопросы и задания
    ' + +items.map(function(t){return '
  1. '+t+'
  2. ';}).join('')+'
'; +} +function wgt(title, inner){ + return '
'+title+'
'+inner+'
'; +} + +function build_p18(){ + document.getElementById('p18-body').innerHTML = + '
§ 18 · Химия 7

Водород — химический элемент и простое вещество

' + +'
$\\text{H}_2$
' + +'
Самый лёгкий элемент Вселенной и его простое вещество.
' + +'
$H$$H_2$
' + +makeCard('theory','Элемент и простое вещество','§18','

Водород как элемент — атомы H ($Z=1$, $A_r=1$), самый лёгкий из всех элементов. Атомы водорода входят в состав воды и множества других веществ.

' + +'
Водород как простое вещество — газ $\\text{H}_2$ (молекула из двух атомов). Это самый лёгкий газ, легче воздуха.
') + +makeCard('theory','Свойства и нахождение','§18','

$\\text{H}_2$ — газ без цвета и запаха, мало растворим в воде. В свободном виде на Земле водорода почти нет, но в составе веществ (особенно воды) его много. Во Вселенной водород — самый распространённый элемент.

') + +wgt('Паспорт водорода','
') + +rememberBox(['$H$ — самый лёгкий элемент ($A_r=1$).','$H_2$ — самый лёгкий газ, легче воздуха.','Во Вселенной водород — самый распространённый элемент.']) + +qList(['Чем водород-элемент отличается от простого вещества $H_2$?','Назови физические свойства водорода.','Где на Земле находится водород?']) + +secNav(null,'p19')+readButton('p18'); + wireReadBtn('p18'); +} + +function build_p19(){ + document.getElementById('p19-body').innerHTML = + '
§ 19 · Химия 7

Химические свойства водорода

' + +'
$2\\text{H}_2+\\text{O}_2=2\\text{H}_2\\text{O}$
' + +'
Как водород горит и почему его называют восстановителем.
' + +'
горениевосстановитель
' + +makeCard('theory','Горение водорода','§19','

Водород горит в кислороде, образуя воду: $2\\text{H}_2+\\text{O}_2=2\\text{H}_2\\text{O}$. Смесь водорода с воздухом (или кислородом) — «гремучий газ» — взрывоопасна, поэтому перед поджиганием водород проверяют на чистоту.

') + +makeCard('theory','Водород — восстановитель','§19','
Водород способен отнимать кислород у оксидов металлов: $\\text{H}_2+\\text{CuO}=\\text{Cu}+\\text{H}_2\\text{O}$. Чёрный оксид меди превращается в красную медь. Вещество, отнимающее кислород, называют восстановителем.
') + +wgt('Реакции водорода','
') + +rememberBox(['Водород горит в кислороде → образуется вода.','Смесь $H_2$ с воздухом — гремучий газ, взрывается.','Водород — восстановитель: отнимает кислород у оксидов.']) + +qList(['Запиши уравнение горения водорода.','Почему водород называют восстановителем?','Что такое гремучий газ?']) + +secNav('p18','p20')+readButton('p19'); + wireReadBtn('p19'); +} + +function build_p20(){ + document.getElementById('p20-body').innerHTML = + '
§ 20 · Химия 7

Понятие о кислотах

' + +'
Что такое кислоты и как их обнаруживают индикаторами.
' + +'
кислотаиндикатор
' + +makeCard('theory','Состав кислот','§20','
Кислоты — сложные вещества, в состав которых входят атомы водорода и кислотный остаток.
' + +'

Примеры: соляная $\\text{HCl}$, серная $\\text{H}_2\\text{SO}_4$, азотная $\\text{HNO}_3$, угольная $\\text{H}_2\\text{CO}_3$. Число атомов водорода в кислоте равно валентности кислотного остатка.

') + +makeCard('theory','Индикаторы','§20','

Индикаторы — вещества, которые меняют свой цвет в присутствии кислоты. В кислоте лакмус становится красным, а метилоранж — розово-красным. Так кислоту можно обнаружить.

') + +wgt('Индикаторы в кислоте','
') + +wgt('Важнейшие кислоты и их остатки','
') + +rememberBox(['Кислота = атомы водорода + кислотный остаток.','Лакмус в кислоте — красный, метилоранж — розово-красный.','Индикаторы помогают обнаружить кислоту.']) + +qList(['Из чего состоят кислоты?','Как с помощью индикатора обнаружить кислоту?','Назови формулу и название двух кислот.']) + +secNav('p19','lo3')+readButton('p20'); + wireReadBtn('p20'); +} + +function build_lo3(){ + document.getElementById('lo3-body').innerHTML = + '
Лабораторный опыт 3

Действие кислот на индикаторы

' + +'
Научиться обнаруживать кислоту с помощью индикаторов.
' + +makeCard('lab','Ход работы',null,'
  1. В пробирку с раствором кислоты добавь несколько капель лакмуса — он станет красным.
  2. В другую пробирку с кислотой добавь метилоранж — он станет розово-красным.
  3. Сравни с окраской индикаторов в чистой воде.
  4. Сделай вывод, как обнаружить кислоту.
' + +'
Кислоты едкие — работай аккуратно, не допускай попадания на кожу и одежду.
') + +wgt('Индикаторы в кислоте','
') + +secNav('p20','p21')+readButton('lo3'); + wireReadBtn('lo3'); +} + +/* заглушки для ещё не наполненных § (следующая волна) */ (function(){ var P = window.PARAS, B = {}; function ph(p, prev, next){ @@ -104,7 +208,7 @@ window.TIPS = [{ sec:'p18', html:'Глава наполняется содерж '
' + p.num + ' · Химия 7

' + p.name + '

' + '
Содержание этого ' + (p.final ? 'раздела' : 'параграфа') + ' готовится.
' + makeCard('theory', p.name, p.num, - '

Скоро здесь появятся теория, наглядные SVG-схемы, молекулярные модели и интерактивные тренажёры по теме «' + p.name + '». Пока доступна навигация по главе' + (p.final ? '.' : ' и отметка о прочтении.') + '

') + '

Скоро здесь появятся теория, наглядные SVG-схемы и интерактивные тренажёры по теме «' + p.name + '». Пока доступна навигация по главе' + (p.final ? '.' : ' и отметка о прочтении.') + '

') + secNav(prev, next) + (p.final ? '' : readButton(p.id)); if (!p.final) wireReadBtn(p.id); }; @@ -114,6 +218,12 @@ window.TIPS = [{ sec:'p18', html:'Глава наполняется содерж } window.BUILDERS = B; })(); + +/* реальные builder'ы Волны 1 главы 3 */ +window.BUILDERS.p18 = build_p18; +window.BUILDERS.p19 = build_p19; +window.BUILDERS.p20 = build_p20; +window.BUILDERS.lo3 = build_lo3; From 1635bc60512e58cf5e4620e8d98eb95b90293939 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sat, 30 May 2026 19:00:18 +0300 Subject: [PATCH 03/47] =?UTF-8?q?feat(chemistry7):=20Phase=203=20=D0=92?= =?UTF-8?q?=D0=BE=D0=BB=D0=BD=D0=B0=202=20=E2=80=94=20=D0=93=D0=BB=D0=B0?= =?UTF-8?q?=D0=B2=D0=B0=203=20=D0=B7=D0=B0=D0=B2=D0=B5=D1=80=D1=88=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20(=C2=A721,=20=D0=9B=D0=9E4,=20=C2=A722,=20=D0=9F?= =?UTF-8?q?=D0=A03,=20=D1=84=D0=B8=D0=BD=D0=B0=D0=BB)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §21 Кислоты и металлы (интерактивный ряд активности), ЛО4 Кислоты с металлами (опыт: пузырьки H2, медь не реагирует), §22 Соли как продукты замещения (конструктор солей по валентности), ПР3 Получение водорода (проверка чистоты — гремучий газ), финал главы (6 интегрированных боссов + шпаргалка). Глава 3 «Водород» наполнена полностью (§§18–22). Тесты chem7: 14/14 pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/tests/chemistry7-page.test.js | 15 ++++ frontend/textbooks/chemistry_7_ch3.html | 111 +++++++++++++++++++++++- 2 files changed, 123 insertions(+), 3 deletions(-) diff --git a/backend/tests/chemistry7-page.test.js b/backend/tests/chemistry7-page.test.js index f2b1d23..41f91ea 100644 --- a/backend/tests/chemistry7-page.test.js +++ b/backend/tests/chemistry7-page.test.js @@ -176,6 +176,21 @@ test('ch3 Волна 1: §18 + §19 + §20 + ЛО3 монтируются', asyn assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | ')); }); +test('ch3 Волна 2: §21 + ЛО4 + §22 + ПР3 + финал главы монтируются', async () => { + const { doc, errors } = await loadDom('chemistry_7_ch3.html'); + doc.defaultView.goTo('p21'); await wait(100); + assert.ok(doc.querySelector('#p21-act .act-cell'), 'ряд активности §21'); + doc.defaultView.goTo('lo4'); await wait(100); + assert.ok(doc.querySelector('#lo4-rx #lo4-go'), 'опыт металл+кислота ЛО4'); + doc.defaultView.goTo('p22'); await wait(100); + assert.ok(doc.querySelector('#p22-salt #p22-m'), 'конструктор солей §22'); + doc.defaultView.goTo('pr3'); await wait(100); + assert.ok(doc.querySelector('#pr3-test #pr3-mix'), 'проверка чистоты H2 ПР3'); + doc.defaultView.goTo('final3'); await wait(120); + assert.ok(doc.querySelectorAll('#navDotsfinal3 .nav-dot').length >= 6, 'боссы финала главы 3'); + assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | ')); +}); + /* ── Хаб: каталог глав + финал курса ── */ function buildHub() { let html = readF('frontend/textbooks/chemistry_7_hub.html'); diff --git a/frontend/textbooks/chemistry_7_ch3.html b/frontend/textbooks/chemistry_7_ch3.html index 3a7b08f..349f0c1 100644 --- a/frontend/textbooks/chemistry_7_ch3.html +++ b/frontend/textbooks/chemistry_7_ch3.html @@ -92,18 +92,30 @@ window.PARAS = [ ]; window.ACH_LABELS = { start:'Начало главы 3!', p18_done:'§18 изучен!', p19_done:'§19 изучен!', - p20_done:'§20 изучен!', lo3_done:'Лабораторный опыт 3 выполнен!', final3_tasks:'Глава 3 пройдена!' }; + p20_done:'§20 изучен!', lo3_done:'Лабораторный опыт 3 выполнен!', + p21_done:'§21 изучен!', lo4_done:'Лабораторный опыт 4 выполнен!', p22_done:'§22 изучен!', pr3_done:'Практическая работа 3 выполнена!', + final3_tasks:'Глава 3 пройдена! Вы — Знаток водорода!' }; window.SIDEBARS = { p18:{ title:'Шпаргалка §18', rows:[['$H$','элемент, $Z=1$'],['$H_2$','самый лёгкий газ'],['Вселенная','самый частый элемент']] }, p19:{ title:'Шпаргалка §19', rows:[['Горение','$+O_2 \\to H_2O$'],['Гремучий газ','$H_2$ + воздух'],['Восстановитель','отнимает O у оксида']] }, p20:{ title:'Шпаргалка §20', rows:[['Кислота','H + остаток'],['Примеры','$HCl$, $H_2SO_4$'],['Лакмус','в кислоте красный']] }, - lo3:{ title:'Лаб. опыт 3', rows:[['Лакмус','красный'],['Метилоранж','розово-красный']] } + lo3:{ title:'Лаб. опыт 3', rows:[['Лакмус','красный'],['Метилоранж','розово-красный']] }, + p21:{ title:'Шпаргалка §21', rows:[['Me + кислота','соль + $H_2\\uparrow$'],['Ряд активности','до $H_2$ — вытесняют'],['Cu, Ag','не реагируют']] }, + lo4:{ title:'Лаб. опыт 4', rows:[['Zn, Fe, Mg','пузырьки $H_2$'],['Cu','реакции нет']] }, + p22:{ title:'Шпаргалка §22', rows:[['Соль','металл + остаток'],['Замещение','H на металл'],['Названия','хлорид, сульфат…']] }, + pr3:{ title:'Практическая 3', rows:[['$Zn+HCl$','$\\to H_2\\uparrow$'],['Чистота','гремучий газ']] }, + final3:{ title:'Финал главы 3', rows:[['§§18–22','водород'],['Награда','ачивка + XP']] } }; window.TIPS = [ { sec:'p18', html:'$H$ — самый лёгкий элемент ($A_r=1$). Простое вещество $H_2$ — самый лёгкий газ, легче воздуха. Во Вселенной водород — самый распространённый элемент.' }, { sec:'p19', html:'Водород горит в кислороде, образуя воду. С оксидами он ведёт себя как восстановитель: $H_2 + CuO = Cu + H_2O$.' }, { sec:'p20', html:'Кислота = атомы водорода + кислотный остаток. Индикаторы меняют цвет: лакмус в кислоте — красный, метилоранж — розово-красный.' }, - { sec:'lo3', html:'В кислоте лакмус становится красным, а метилоранж — розово-красным. Так обнаруживают кислоту.' } + { sec:'lo3', html:'В кислоте лакмус становится красным, а метилоранж — розово-красным. Так обнаруживают кислоту.' }, + { sec:'p21', html:'Металл + кислота → соль + водород. Реагируют только металлы, стоящие в ряду активности левее $H_2$ (Zn, Fe, Mg). Медь и серебро водород из кислот не вытесняют.' }, + { sec:'lo4', html:'Цинк, железо, магний с соляной и серной кислотами дают пузырьки водорода; медь — нет (стоит правее $H_2$).' }, + { sec:'p22', html:'Соль = металл + кислотный остаток. Она образуется, когда металл замещает водород в кислоте: $Zn + 2HCl = ZnCl_2 + H_2$.' }, + { sec:'pr3', html:'Водород получают реакцией $Zn + HCl$; чистоту проверяют поджиганием (хлопок — нечисто, спокойное горение — чисто).' }, + { sec:'final3', html:'Собери всё: водород и его свойства, кислоты и индикаторы, ряд активности, соли как продукты замещения.' } ]; window.POOLS = { @@ -124,6 +136,26 @@ window.POOLS = { {q:'Какого цвета становится лакмус в кислоте?',opts:['Синего','Красного','Зелёного','Жёлтого'],a:1,ex:'Лакмус в кислоте — красный.'}, {q:'Какова формула серной кислоты?',opts:['HCl','H₂SO₄','HNO₃','H₂CO₃'],a:1,ex:'Серная кислота — $H_2SO_4$.'}, {q:'Индикатор — это вещество, которое…',opts:['Ускоряет реакцию','Меняет цвет в кислоте или щёлочи','Растворяет металлы','Выделяет газ'],a:1,ex:'Индикаторы обнаруживают кислоты и щёлочи по изменению цвета.'} + ], + p21:[ + {q:'Какой газ выделяется при реакции активного металла с кислотой?',opts:['Кислород','Водород','Углекислый газ','Азот'],a:1,ex:'Металл вытесняет водород: соль + $H_2\\uparrow$.'}, + {q:'Какой металл НЕ вытесняет водород из соляной кислоты?',opts:['Цинк','Магний','Медь','Железо'],a:2,ex:'Медь стоит правее $H_2$ — не реагирует.'}, + {q:'Продукты реакции $\\text{Zn}+2\\text{HCl}$ — это…',opts:['$ZnCl_2$ и $H_2$','$ZnO$ и вода','$Zn$ и $Cl_2$','только соль'],a:0,ex:'$Zn+2HCl=ZnCl_2+H_2\\uparrow$.'}, + {q:'Сколько веществ образуется в реакции $\\text{Zn}+\\text{H}_2\\text{SO}_4=\\text{ZnSO}_4+\\text{H}_2$?',hint:'соль и газ',unit:'',a:2,ex:'Соль и водород — 2 вещества.'} + ], + p22:[ + {q:'Из чего состоит соль?',opts:['Из водорода и остатка','Из металла и кислотного остатка','Из двух металлов','Из воды и кислоты'],a:1,ex:'Соль = металл + кислотный остаток.'}, + {q:'Соль $\\text{NaCl}$ — продукт замещения водорода в кислоте…',opts:['$H_2SO_4$','$HCl$','$HNO_3$','$H_2CO_3$'],a:1,ex:'В $HCl$ водород заместился натрием → $NaCl$.'}, + {q:'Какова формула сульфата натрия?',opts:['NaSO4','Na₂SO₄','Na₂SO₃','NaS'],a:1,ex:'Na(I), $SO_4$(II) → $Na_2SO_4$.'}, + {q:'Реакция $\\text{Zn}+2\\text{HCl}=\\text{ZnCl}_2+\\text{H}_2$ — это реакция…',opts:['Соединения','Разложения','Замещения','Обмена'],a:2,ex:'Металл замещает водород — реакция замещения.'} + ], + final3:[ + {q:'$A_r(\\text{H})=?$',hint:'из таблицы',unit:'',a:1,ex:'1.'}, + {q:'Продукт горения водорода в кислороде — это…',opts:['Вода','Углекислый газ','Оксид меди','Соль'],a:0,ex:'$2H_2+O_2=2H_2O$.'}, + {q:'Цвет лакмуса в кислоте?',opts:['Синий','Красный','Зелёный','Жёлтый'],a:1,ex:'Красный.'}, + {q:'Сколько из металлов Mg, Cu, Zn вытесняют водород из кислоты?',hint:'Cu — правее $H_2$',unit:'',a:2,ex:'Mg и Zn → 2.'}, + {q:'Соль — это…',opts:['Металл + кислород','Металл + кислотный остаток','Водород + остаток','Два неметалла'],a:1,ex:'Металл + кислотный остаток.'}, + {q:'Формула серной кислоты?',opts:['HCl','H₂SO₄','HNO₃','H₂CO₃'],a:1,ex:'$H_2SO_4$.'} ] }; @@ -198,6 +230,74 @@ function build_lo3(){ wireReadBtn('lo3'); } +function build_p21(){ + document.getElementById('p21-body').innerHTML = + '
§ 21 · Химия 7

Взаимодействие кислот с металлами

' + +'
Me $+$ кислота $\\to$ соль $+ \\text{H}_2\\uparrow$
' + +'
Почему одни металлы вытесняют водород из кислот, а другие нет.
' + +'
ряд активности$H_2\\uparrow$
' + +makeCard('theory','Реакция металлов с кислотами','§21','

Многие металлы реагируют с кислотами, вытесняя водород: образуются соль и газообразный водород. Например: $\\text{Zn}+\\text{H}_2\\text{SO}_4=\\text{ZnSO}_4+\\text{H}_2\\uparrow$, $\\text{Mg}+2\\text{HCl}=\\text{MgCl}_2+\\text{H}_2\\uparrow$.

') + +makeCard('rule','Ряд активности металлов','§21','
Металлы расположены в ряд активности. Металлы, стоящие левее водорода, вытесняют его из соляной и серной кислот; стоящие правее (медь, серебро) — не вытесняют.
' + +'

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

') + +wgt('Интерактивный ряд активности','
') + +rememberBox(['Металл + кислота → соль + водород.','Вытесняют водород только металлы левее $H_2$.','Медь и серебро с этими кислотами не реагируют.']) + +qList(['Что образуется при реакции металла с кислотой?','Почему медь не реагирует с соляной кислотой?','Запиши уравнение реакции цинка с серной кислотой.']) + +secNav('p20','lo4')+readButton('p21'); + wireReadBtn('p21'); +} + +function build_lo4(){ + document.getElementById('lo4-body').innerHTML = + '
Лабораторный опыт 4

Взаимодействие серной и соляной кислот с металлами

' + +'
Проверить, какие металлы вытесняют водород из кислот.
' + +makeCard('lab','Ход работы',null,'
  1. В пробирки с соляной и серной кислотами помести кусочки цинка, железа, магния — наблюдай выделение пузырьков газа (водорода).
  2. В пробирку с кислотой помести медь — изменений нет.
  3. Объясни результаты, пользуясь рядом активности металлов.
' + +'
Кислоты едкие; не наклоняйся над пробиркой, в которой идёт реакция.
') + +wgt('Опыт: металл + кислота','
') + +secNav('p21','p22')+readButton('lo4'); + wireReadBtn('lo4'); +} + +function build_p22(){ + document.getElementById('p22-body').innerHTML = + '
§ 22 · Химия 7

Соли — продукты замещения атомов водорода в кислотах на металлы

' + +'
Что такое соли и как составляют их формулы.
' + +'
сользамещение
' + +makeCard('theory','Что такое соли','§22','
Соли — сложные вещества, состоящие из атомов металла и кислотного остатка.
' + +'

Соли образуются, когда металл замещает водород в кислоте — это реакция замещения: $\\text{Zn}+2\\text{HCl}=\\text{ZnCl}_2+\\text{H}_2$. Названия солей: хлориды ($\\text{NaCl}$), сульфаты ($\\text{Na}_2\\text{SO}_4$), нитраты ($\\text{KNO}_3$), карбонаты ($\\text{CaCO}_3$).

') + +makeCard('example','Составление формулы соли',null,'

Кальций (II) и кислотный остаток $\\text{Cl}$ (I): НОК(2,1)=2 → $\\text{CaCl}_2$. Алюминий (III) и $\\text{SO}_4$ (II): НОК(3,2)=6 → $\\text{Al}_2(\\text{SO}_4)_3$.

') + +wgt('Конструктор солей','
') + +rememberBox(['Соль = металл + кислотный остаток.','Соль образуется при замещении водорода металлом.','Формулу соли составляют по валентности (НОК).']) + +qList(['Дай определение соли.','Составь формулу хлорида магния.','Какая реакция называется реакцией замещения?']) + +secNav('lo4','pr3')+readButton('p22'); + wireReadBtn('p22'); +} + +function build_pr3(){ + document.getElementById('pr3-body').innerHTML = + '
Практическая работа 3

Получение водорода и изучение его свойств

' + +'
Получить водород, собрать его и проверить на чистоту.
' + +makeCard('lab','Ход работы',null,'
  1. В пробирку с кусочками цинка прилей соляную кислоту — наблюдай выделение водорода: $\\text{Zn}+2\\text{HCl}=\\text{ZnCl}_2+\\text{H}_2\\uparrow$.
  2. Собери водород (он легче воздуха — держи сосуд отверстием вниз, или собирай вытеснением воды).
  3. Проверь чистоту: поднеси к отверстию пламя. Резкий хлопок — водород смешан с воздухом (нечисто); спокойное горение — чистый.
  4. Сделай вывод о свойствах водорода.
' + +'
Поджигай водород только после проверки чистоты; «гремучий газ» взрывоопасен.
') + +wgt('Проверка чистоты водорода','
') + +secNav('p22','final3')+readButton('pr3'); + wireReadBtn('pr3'); +} + +function build_final3(){ + document.getElementById('final3-body').innerHTML = + '
Финал главы 3

Босс: водород

' + +'
$H_2$ · кислоты · ряд активности · соли
' + +'
Шесть задач на всю главу. Реши все — получи звание «Знаток водорода».
' + +makeCard('rule','Шпаргалка главы 3',null,'
    ' + +'
  • $H$ — самый лёгкий элемент; $H_2$ — самый лёгкий газ.
  • ' + +'
  • Водород горит → вода; восстанавливает металлы из оксидов.
  • ' + +'
  • Кислота = водород + остаток; индикаторы (лакмус — красный в кислоте).
  • ' + +'
  • Металл + кислота → соль $+ H_2\\uparrow$ (только левее $H_2$ в ряду активности).
  • ' + +'
  • Соль = металл + кислотный остаток (продукт замещения).
') + +'

Реши задачи ниже — за каждую +5 XP, за полный разгром босса — звание и бонус.

' + +secNav('pr3',null); +} + /* заглушки для ещё не наполненных § (следующая волна) */ (function(){ var P = window.PARAS, B = {}; @@ -224,6 +324,11 @@ window.BUILDERS.p18 = build_p18; window.BUILDERS.p19 = build_p19; window.BUILDERS.p20 = build_p20; window.BUILDERS.lo3 = build_lo3; +window.BUILDERS.p21 = build_p21; +window.BUILDERS.lo4 = build_lo4; +window.BUILDERS.p22 = build_p22; +window.BUILDERS.pr3 = build_pr3; +window.BUILDERS.final3 = build_final3; From 7574d16678e992fc0f93f88d9d019f94e3f3bc75 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sat, 30 May 2026 19:04:49 +0300 Subject: [PATCH 04/47] =?UTF-8?q?feat(chemistry7):=20Phase=204=20=E2=80=94?= =?UTF-8?q?=20=D0=93=D0=BB=D0=B0=D0=B2=D0=B0=204=20=C2=AB=D0=92=D0=BE?= =?UTF-8?q?=D0=B4=D0=B0=C2=BB=20=D0=B7=D0=B0=D0=B2=D0=B5=D1=80=D1=88=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20(=C2=A7=C2=A723=E2=80=9326=20+=20=D0=9B=D0=9E5?= =?UTF-8?q?=20+=20=D0=9F=D0=A04=20+=20=D1=84=D0=B8=D0=BD=D0=B0=D0=BB)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §23 Состав и свойства воды (разложение 2:1 + реакции воды), §24 Основания (конструктор Me(OH)n + индикаторы щёлочи), ЛО5 Действие щелочей на индикаторы, §25 Реакция нейтрализации (анимация фенолфталеин малиновый → бесцветный), ПР4 Реакция нейтрализации, §26 Охрана окружающей среды (экология-инфографика), финал главы (6 боссов). chem7_ch4_widgets.js. ВСЕ 26 параграфов курса «Химия 7» наполнены. Тесты chem7: 15/15 pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/tests/chemistry7-page.test.js | 20 +++ frontend/js/chem7_ch4_widgets.js | 145 ++++++++++++++++++ frontend/textbooks/chemistry_7_ch4.html | 191 ++++++++++++++++++++++-- 3 files changed, 344 insertions(+), 12 deletions(-) create mode 100644 frontend/js/chem7_ch4_widgets.js diff --git a/backend/tests/chemistry7-page.test.js b/backend/tests/chemistry7-page.test.js index 41f91ea..5442ed3 100644 --- a/backend/tests/chemistry7-page.test.js +++ b/backend/tests/chemistry7-page.test.js @@ -26,6 +26,7 @@ function buildPage(file) { '/js/chem7_ch1_widgets.js': readF('frontend/js/chem7_ch1_widgets.js'), '/js/chem7_ch2_widgets.js': readF('frontend/js/chem7_ch2_widgets.js'), '/js/chem7_ch3_widgets.js': readF('frontend/js/chem7_ch3_widgets.js'), + '/js/chem7_ch4_widgets.js': readF('frontend/js/chem7_ch4_widgets.js'), '/js/chem8_engine.js': readF('frontend/js/chem8_engine.js') }; html = html @@ -191,6 +192,25 @@ test('ch3 Волна 2: §21 + ЛО4 + §22 + ПР3 + финал главы мо assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | ')); }); +test('ch4: вся глава 4 (§23–§26 + ЛО5 + ПР4 + финал) монтируется', async () => { + const { doc, errors } = await loadDom('chemistry_7_ch4.html'); + assert.ok(doc.querySelector('#p23-water #p23-pick'), 'разложение/реакции воды §23'); + doc.defaultView.goTo('p24'); await wait(100); + assert.ok(doc.querySelector('#p24-bld #p24-m'), 'конструктор оснований §24'); + assert.ok(doc.querySelector('#p24-ind #p24-ind-sel'), 'индикаторы щёлочи §24'); + doc.defaultView.goTo('lo5'); await wait(100); + assert.ok(doc.querySelector('#lo5-ind #lo5-ind-sel'), 'индикаторы ЛО5'); + doc.defaultView.goTo('p25'); await wait(100); + assert.ok(doc.querySelector('#p25-neu #p25-neu-go'), 'нейтрализация §25'); + doc.defaultView.goTo('pr4'); await wait(100); + assert.ok(doc.querySelector('#pr4-neu #pr4-neu-go'), 'нейтрализация ПР4'); + doc.defaultView.goTo('p26'); await wait(100); + assert.ok(doc.querySelector('#p26-eco .eco-it'), 'экология §26'); + doc.defaultView.goTo('final4'); await wait(120); + assert.ok(doc.querySelectorAll('#navDotsfinal4 .nav-dot').length >= 6, 'боссы финала главы 4'); + assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | ')); +}); + /* ── Хаб: каталог глав + финал курса ── */ function buildHub() { let html = readF('frontend/textbooks/chemistry_7_hub.html'); diff --git a/frontend/js/chem7_ch4_widgets.js b/frontend/js/chem7_ch4_widgets.js new file mode 100644 index 0000000..42dcb65 --- /dev/null +++ b/frontend/js/chem7_ch4_widgets.js @@ -0,0 +1,145 @@ +/* chem7_ch4_widgets.js — интерактивы главы 4 «Вода» (Химия 7). + * Монтируются движком chem8_engine.js: window.CHEM8_WIDGETS[id]. + * Используют window.Chem8 (chem8_svg.js): chemEq, formula. + * Без эмоджи; 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,'&').replace(//g,'>'); } + function gcd(a, b) { return b ? gcd(b, a % b) : a; } + function ceq(src, opts){ return C().chemEq ? C().chemEq(src, opts || {}) : esc(src); } + function fml(s){ return C().formula ? C().formula(s) : s; } + function strip(color){ return '
'; } + + /* §23 — разложение воды (2:1) + реакции воды */ + var WRX = [ + { name:'Разложение электрическим током', eq:'2H2O = 2H2^ + O2^', cond:'эл. ток', note:'Вода разлагается на простые вещества: водорода получается вдвое больше по объёму, чем кислорода (2 : 1).' }, + { name:'Реакция с натрием', eq:'2Na + 2H2O = 2NaOH + H2^', note:'Активные металлы реагируют с водой, образуя щёлочь и водород.' }, + { name:'Реакция с оксидом кальция', eq:'CaO + H2O = Ca(OH)2', note:'Оксиды активных металлов с водой дают основания.' }, + { name:'Реакция с углекислым газом', eq:'CO2 + H2O = H2CO3', note:'Оксиды неметаллов с водой дают кислоты.' } + ]; + function decompSvg(){ + // две перевёрнутые пробирки: H2 (заполнена на 2/2), O2 (на 1/2) + return '' + + '' + + '' + + 'H₂' + + '2 V' + + '' + + '' + + 'O₂' + + '1 V' + + '' + + ''; + } + function mount_p23() { + var m = $('p23-water'); if (!m || m._built) return; m._built = 1; + var idx = 0; + function render(){ + var r = WRX[idx]; + m.innerHTML = (idx===0 ? decompSvg() : '') + + '
' + + '
'+ceq(r.eq,{cond:r.cond})+'
' + + '
'+esc(r.note)+'
'; + $('p23-pick').addEventListener('change', function(e){ idx=+e.target.value; m._built=0; render(); }); + } + render(); + } + + /* индикаторы в щёлочи */ + var ALK_IND = { + 'Лакмус': { neutral:['#7c3aed','фиолетовый'], alk:['#2563eb','синий'] }, + 'Фенолфталеин': { neutral:['#f3f4f6','бесцветный'], alk:['#db2777','малиновый'] }, + 'Метилоранж': { neutral:['#f59e0b','оранжевый'], alk:['#eab308','жёлтый'] } + }; + function alkIndicator(mountId) { + var m = $(mountId); if (!m || m._built) return; m._built = 1; + var ind = 'Фенолфталеин'; + function render(){ + var c = ALK_IND[ind]; + m.innerHTML = '
' + + '
В нейтральной среде: ' + strip(c.neutral[0]) + ' '+c.neutral[1]+'
' + + 'В щёлочи: ' + strip(c.alk[0]) + ' '+c.alk[1]+'
'; + $(mountId+'-sel').addEventListener('change', function(e){ ind=e.target.value; m._built=0; render(); }); + } + render(); + } + + /* §24 — конструктор оснований Me(OH)n + индикаторы */ + var BM = [ ['Na',1], ['K',1], ['Ca',2], ['Mg',2], ['Cu',2], ['Al',3], ['Fe',3] ]; + var SOLUBLE = { Na:1, K:1, Ca:1 }; + function mount_p24() { + var b = $('p24-bld'); + if (b && !b._built) { b._built = 1; + function rom(n){ return ['','I','II','III'][n]; } + b.innerHTML = '
+ гидроксогруппа OH (I)
'; + function upd(){ + var e=BM[+$('p24-m').value], n=e[1]; + var raw = e[0] + (n>1 ? '(OH)'+n : 'OH'); + var sol = SOLUBLE[e[0]] ? 'щёлочь (растворимое основание)' : 'нерастворимое основание'; + var out=$('p24-out'); out.className='out ok'; + out.innerHTML='Валентность '+e[0]+' = '+rom(n)+', OH = I → '+n+' группы OH
Формула основания: '+fml(raw)+'
Это '+sol+'.
'; + } + $('p24-m').addEventListener('change',upd); upd(); + } + alkIndicator('p24-ind'); + } + function mount_lo5() { alkIndicator('lo5-ind'); } + + /* §25 / ПР4 — нейтрализация (фенолфталеин малиновый → бесцветный) */ + function mount_neutral(mountId) { + var m = $(mountId); if (!m || m._built) return; m._built = 1; + var done = false; + function beaker(color){ return ''; } + function render(){ + m.innerHTML = '
' + beaker(done?'#f8fafc':'#db2777') + + '
'+(done + ? 'Раствор стал бесцветным — кислота нейтрализовала щёлочь. Реакция завершена.' + : 'В щёлочи с фенолфталеином раствор малиновый. Добавляй кислоту по каплям.')+'
' + + '
' + + (done ? '
'+ceq('HCl + NaOH = NaCl + H2O')+'
Кислота + основание → соль + вода. Это реакция нейтрализации.
' : ''); + $(mountId+'-go').addEventListener('click', function(){ done=!done; m._built=0; render(); }); + } + render(); + } + function mount_p25() { mount_neutral('p25-neu'); } + function mount_pr4() { mount_neutral('pr4-neu'); } + + /* §26 — охрана воды и воздуха: источники загрязнения и способы охраны */ + var ECO = { + 'Источники загрязнения': [ + ['Промышленные выбросы','Газы и пыль из труб заводов загрязняют воздух.'], + ['Сточные воды','Неочищенные стоки отравляют реки и озёра.'], + ['Нефть','Разливы нефти губят водные организмы.'], + ['Кислотные дожди','Оксиды серы и азота в воздухе образуют кислоты, выпадающие с дождём.'] + ], + 'Способы охраны и очистки': [ + ['Очистные сооружения','Сточные воды очищают перед сбросом.'], + ['Фильтрование','На водопроводных станциях удаляют твёрдые частицы.'], + ['Хлорирование и озонирование','Обеззараживают питьевую воду.'], + ['Бережное отношение','Экономить воду и не загрязнять водоёмы.'] + ] + }; + function mount_p26() { + var m = $('p26-eco'); if (!m || m._built) return; m._built = 1; + var cols = Object.keys(ECO).map(function(title){ + var items = ECO[title].map(function(it,i){ return ''; }).join(''); + return '
'+esc(title)+'
'+items+'
'; + }).join(''); + m.innerHTML = '
'+cols+'
Кликни по пункту, чтобы узнать подробнее.
'; + var out=$('p26-eco-out'); + m.querySelectorAll('.eco-it').forEach(function(b){ + b.addEventListener('click', function(){ var it=ECO[b.dataset.t][+b.dataset.i]; out.className='out ok'; out.innerHTML=''+esc(it[0])+'. '+esc(it[1]); }); + }); + } + + W.CHEM8_WIDGETS = Object.assign(W.CHEM8_WIDGETS || {}, { + p23: mount_p23, p24: mount_p24, lo5: mount_lo5, p25: mount_p25, pr4: mount_pr4, p26: mount_p26 + }); + W.FLAG_MOUNTS = Object.assign(W.FLAG_MOUNTS || {}, {}); +})(window); diff --git a/frontend/textbooks/chemistry_7_ch4.html b/frontend/textbooks/chemistry_7_ch4.html index 8dc7ba9..ff866c0 100644 --- a/frontend/textbooks/chemistry_7_ch4.html +++ b/frontend/textbooks/chemistry_7_ch4.html @@ -17,6 +17,7 @@ + @@ -86,30 +87,196 @@ window.PARAS = [ {id:'final4', num:'★', name:'Финал главы', sub:'босс · ачивка', final:true} ]; -window.ACH_LABELS = { start:'Начало главы 4!', final4_tasks:'Глава 4 пройдена!' }; -window.SIDEBARS = { p23:{ title:'Глава 4 · Химия 7', rows:[['Раздел','Вода'],['§§','23–26'],['Лаб/ПР','ЛО 5 · ПР 4']] } }; -window.TIPS = [{ sec:'p23', html:'Глава наполняется содержанием по фазам. Сейчас доступны навигация по параграфам и отметка о прочтении (+10 XP).' }]; +window.ACH_LABELS = { start:'Начало главы 4!', p23_done:'§23 изучен!', p24_done:'§24 изучен!', + lo5_done:'Лабораторный опыт 5 выполнен!', p25_done:'§25 изучен!', pr4_done:'Практическая работа 4 выполнена!', + p26_done:'§26 изучен!', final4_tasks:'Глава 4 пройдена! Вы — Хранитель воды!' }; +window.SIDEBARS = { + p23:{ title:'Шпаргалка §23', rows:[['Вода','$H_2O$'],['Разложение','$H_2:O_2=2:1$'],['$t_{кип}$','100 °C']] }, + p24:{ title:'Шпаргалка §24', rows:[['Основание','Me + OH'],['Щёлочи','раствор.: NaOH, KOH'],['Фенолфталеин','в щёлочи малиновый']] }, + lo5:{ title:'Лаб. опыт 5', rows:[['Лакмус','синий'],['Фенолфталеин','малиновый']] }, + p25:{ title:'Шпаргалка §25', rows:[['Нейтрализация','кислота + основание'],['Продукты','соль + вода'],['Индикатор','показывает конец']] }, + pr4:{ title:'Практическая 4', rows:[['Щёлочь + фенолфталеин','малиновый'],['+ кислота','до бесцветного']] }, + p26:{ title:'Шпаргалка §26', rows:[['Загрязнение','стоки, выбросы'],['Очистка','фильтр, хлор, озон'],['Береги','воду и воздух']] }, + final4:{ title:'Финал главы 4', rows:[['§§23–26','вода'],['Награда','ачивка + XP']] } +}; +window.TIPS = [ + { sec:'p23', html:'Вода $H_2O$ разлагается электрическим током на простые вещества: водорода по объёму в 2 раза больше, чем кислорода.' }, + { sec:'p24', html:'Основание = металл + гидроксогруппа OH. Растворимые основания (NaOH, KOH, $Ca(OH)_2$) называют щёлочами. В щёлочи фенолфталеин — малиновый, лакмус — синий.' }, + { sec:'lo5', html:'В щёлочи лакмус становится синим, а фенолфталеин — малиновым. Так обнаруживают щёлочь.' }, + { sec:'p25', html:'Нейтрализация: кислота + основание → соль + вода. Конец реакции виден по изменению цвета индикатора.' }, + { sec:'pr4', html:'К щёлочи с фенолфталеином (малиновый) добавляют кислоту по каплям, пока раствор не станет бесцветным.' }, + { sec:'p26', html:'Воду и воздух нужно беречь: очищать стоки, фильтровать и обеззараживать воду, не загрязнять водоёмы.' } +]; -/* Phase 0: заглушки-builder'ы из PARAS (теория и интерактивы добавляются в фазах 1–4). */ +window.POOLS = { + p23:[ + {q:'Какова формула воды?',opts:['HO','H₂O','H₂O₂','OH'],a:1,ex:'Вода — $H_2O$.'}, + {q:'При разложении воды объём водорода во сколько раз больше объёма кислорода?',hint:'2 : 1',unit:'раза',a:2,ex:'$H_2:O_2=2:1$.'}, + {q:'Что образуется при разложении воды электрическим током?',opts:['Водород и кислород','Только водород','Соль и вода','Углекислый газ'],a:0,ex:'$2H_2O=2H_2\\uparrow+O_2\\uparrow$.'}, + {q:'Чему равна температура кипения воды (°C)?',hint:'при нормальных условиях',unit:'°C',a:100,ex:'100 °C.'} + ], + p24:[ + {q:'Из чего состоит основание?',opts:['Из металла и кислотного остатка','Из металла и гидроксогруппы OH','Из водорода и остатка','Из двух неметаллов'],a:1,ex:'Основание = металл + OH.'}, + {q:'Какого цвета фенолфталеин в щёлочи?',opts:['Бесцветный','Малиновый','Жёлтый','Синий'],a:1,ex:'В щёлочи фенолфталеин малиновый.'}, + {q:'Какова формула гидроксида натрия?',opts:['NaOH','Na₂O','NaCl','NaH'],a:0,ex:'Гидроксид натрия — $NaOH$.'}, + {q:'Растворимые в воде основания называют…',opts:['Кислотами','Щёлочами','Солями','Оксидами'],a:1,ex:'Растворимые основания — щёлочи.'} + ], + p25:[ + {q:'Реакция кислоты с основанием называется реакцией…',opts:['Разложения','Нейтрализации','Замещения','Горения'],a:1,ex:'Кислота + основание → соль + вода — нейтрализация.'}, + {q:'Продукты реакции $\\text{HCl}+\\text{NaOH}$ — это…',opts:['Соль и вода','Два газа','Металл и вода','Оксид и вода'],a:0,ex:'$HCl+NaOH=NaCl+H_2O$.'}, + {q:'Сколько веществ образуется в реакции $\\text{HCl}+\\text{NaOH}=\\text{NaCl}+\\text{H}_2\\text{O}$?',hint:'соль и вода',unit:'',a:2,ex:'Соль и вода — 2 вещества.'}, + {q:'Как узнать, что реакция нейтрализации завершилась?',opts:['По выделению газа','По изменению цвета индикатора','По осадку','Никак'],a:1,ex:'Индикатор меняет цвет в конце реакции.'} + ], + p26:[ + {q:'Что загрязняет воду?',opts:['Чистый дождь','Неочищенные сточные воды','Снег','Лёд'],a:1,ex:'Сточные воды и выбросы загрязняют водоёмы.'}, + {q:'Как очищают питьевую воду на водопроводных станциях?',opts:['Замораживанием','Фильтрованием и обработкой хлором или озоном','Кипячением навсегда','Никак'],a:1,ex:'Воду фильтруют и обеззараживают хлором или озоном.'}, + {q:'Из-за чего возникают кислотные дожди?',opts:['Из-за выбросов оксидов серы и азота','Из-за чистого воздуха','Из-за воды','Из-за песка'],a:0,ex:'Оксиды в воздухе образуют кислоты.'}, + {q:'Как беречь воду?',opts:['Лить без счёта','Экономить и не загрязнять','Сливать отходы в реку','Не пить'],a:1,ex:'Экономить воду и не загрязнять водоёмы.'} + ], + final4:[ + {q:'Формула воды?',opts:['HO','H₂O','OH','H₂O₂'],a:1,ex:'$H_2O$.'}, + {q:'При разложении воды $H_2:O_2$ по объёму = ? : 1. Чему равно первое число?',hint:'2:1',unit:'',a:2,ex:'2.'}, + {q:'Основание состоит из…',opts:['Металла и OH','Водорода и остатка','Двух металлов','Металла и кислорода'],a:0,ex:'Металл + OH.'}, + {q:'Фенолфталеин в щёлочи становится…',opts:['Бесцветным','Малиновым','Красным','Жёлтым'],a:1,ex:'Малиновый.'}, + {q:'Продукты реакции нейтрализации — это…',opts:['Соль и вода','Два газа','Оксид и металл','Кислота и щёлочь'],a:0,ex:'Соль + вода.'}, + {q:'Как обеззараживают питьевую воду?',opts:['Хлорированием или озонированием','Заморозкой','Подкислением','Ничем'],a:0,ex:'Хлором или озоном.'} + ] +}; + +function rememberBox(items){ + return '
' + +' Запомни!
    ' + +items.map(function(t){return '
  • '+t+'
  • ';}).join('')+'
'; +} +function qList(items){ + return '
Вопросы и задания
    ' + +items.map(function(t){return '
  1. '+t+'
  2. ';}).join('')+'
'; +} +function wgt(title, inner){ + return '
'+title+'
'+inner+'
'; +} + +function build_p23(){ + document.getElementById('p23-body').innerHTML = + '
§ 23 · Химия 7

Состав, физические и химические свойства воды

' + +'
$\\text{H}_2\\text{O}$
' + +'
Самое важное вещество на Земле: его состав и превращения.
' + +'
$H_2O$разложение
' + +makeCard('theory','Состав и физические свойства','§23','

Вода — сложное вещество $\\text{H}_2\\text{O}$: молекула из двух атомов водорода и одного атома кислорода. Это жидкость без цвета, запаха и вкуса; замерзает при $0\\,^\\circ$C, кипит при $100\\,^\\circ$C.

') + +makeCard('theory','Химические свойства','§23','
  • Разлагается электрическим током: $2\\text{H}_2\\text{O}=2\\text{H}_2\\uparrow+\\text{O}_2\\uparrow$ (водорода вдвое больше).
  • Реагирует с активными металлами: $2\\text{Na}+2\\text{H}_2\\text{O}=2\\text{NaOH}+\\text{H}_2\\uparrow$.
  • С оксидами металлов даёт основания: $\\text{CaO}+\\text{H}_2\\text{O}=\\text{Ca(OH)}_2$.
  • С оксидами неметаллов даёт кислоты: $\\text{CO}_2+\\text{H}_2\\text{O}=\\text{H}_2\\text{CO}_3$.
') + +wgt('Разложение воды и реакции воды','
') + +rememberBox(['Вода $H_2O$ — сложное вещество.','При разложении $H_2:O_2=2:1$ по объёму.','Вода реагирует с активными металлами и с оксидами.']) + +qList(['Каков состав молекулы воды?','Что образуется при разложении воды током?','Что получится при реакции $CO_2$ с водой?']) + +secNav(null,'p24')+readButton('p23'); + wireReadBtn('p23'); +} + +function build_p24(){ + document.getElementById('p24-body').innerHTML = + '
§ 24 · Химия 7

Основания как сложные вещества

' + +'
Me(OH)$_n$
' + +'
Что такое основания и как их обнаруживают индикаторами.
' + +'
основаниещёлочь
' + +makeCard('theory','Состав оснований','§24','
Основания — сложные вещества, состоящие из атомов металла и одной или нескольких гидроксогрупп OH.
' + +'

Примеры: $\\text{NaOH}$, $\\text{KOH}$, $\\text{Ca(OH)}_2$, $\\text{Cu(OH)}_2$. Растворимые в воде основания называют щёлочами (NaOH, KOH, $Ca(OH)_2$), остальные — нерастворимые. Название: «гидроксид» + металл.

') + +makeCard('theory','Индикаторы в щёлочи','§24','

В щёлочи лакмус становится синим, фенолфталеин — малиновым, метилоранж — жёлтым. Так обнаруживают щёлочь.

') + +wgt('Конструктор основания','
') + +wgt('Индикаторы в щёлочи','
') + +rememberBox(['Основание = металл + группа OH.','Растворимые основания — щёлочи.','Фенолфталеин в щёлочи малиновый, лакмус — синий.']) + +qList(['Дай определение основания.','Составь формулу гидроксида кальция.','Как обнаружить щёлочь индикатором?']) + +secNav('p23','lo5')+readButton('p24'); + wireReadBtn('p24'); +} + +function build_lo5(){ + document.getElementById('lo5-body').innerHTML = + '
Лабораторный опыт 5

Действие щелочей на индикаторы

' + +'
Научиться обнаруживать щёлочь с помощью индикаторов.
' + +makeCard('lab','Ход работы',null,'
  1. В пробирку с раствором щёлочи добавь лакмус — он станет синим.
  2. В другую пробирку с щёлочью добавь фенолфталеин — он станет малиновым.
  3. Сравни с окраской в чистой воде.
  4. Сделай вывод, как обнаружить щёлочь.
' + +'
Щёлочи едкие — работай аккуратно, не допускай попадания на кожу.
') + +wgt('Индикаторы в щёлочи','
') + +secNav('p24','p25')+readButton('lo5'); + wireReadBtn('lo5'); +} + +function build_p25(){ + document.getElementById('p25-body').innerHTML = + '
§ 25 · Химия 7

Реакция нейтрализации

' + +'
кислота + основание $\\to$ соль + вода
' + +'
Что происходит, когда смешивают кислоту и щёлочь.
' + +'
нейтрализациясоль + вода
' + +makeCard('theory','Нейтрализация','§25','
Реакция нейтрализации — реакция между кислотой и основанием, в результате которой образуются соль и вода.
' + +'

Например: $\\text{HCl}+\\text{NaOH}=\\text{NaCl}+\\text{H}_2\\text{O}$. За ходом реакции следят по индикатору: фенолфталеин в щёлочи малиновый, а когда вся щёлочь нейтрализована — раствор становится бесцветным.

') + +wgt('Анимация нейтрализации','
') + +rememberBox(['Кислота + основание → соль + вода.','Это реакция нейтрализации.','Конец реакции виден по изменению цвета индикатора.']) + +qList(['Какая реакция называется нейтрализацией?','Что образуется при реакции $HCl$ с $NaOH$?','Зачем при нейтрализации используют индикатор?']) + +secNav('lo5','pr4')+readButton('p25'); + wireReadBtn('p25'); +} + +function build_pr4(){ + document.getElementById('pr4-body').innerHTML = + '
Практическая работа 4

Реакция нейтрализации

' + +'
Провести нейтрализацию щёлочи кислотой и зафиксировать её конец по индикатору.
' + +makeCard('lab','Ход работы',null,'
  1. В стакан с раствором щёлочи добавь несколько капель фенолфталеина — раствор станет малиновым.
  2. По каплям приливай кислоту, перемешивая, пока раствор не обесцветится.
  3. Обесцвечивание означает, что щёлочь нейтрализована: $\\text{HCl}+\\text{NaOH}=\\text{NaCl}+\\text{H}_2\\text{O}$.
  4. Сделай вывод о признаке конца реакции нейтрализации.
' + +'
Кислоты и щёлочи едкие — добавляй кислоту по каплям, аккуратно.
') + +wgt('Нейтрализация: добавь кислоту','
') + +secNav('p25','p26')+readButton('pr4'); + wireReadBtn('pr4'); +} + +function build_p26(){ + document.getElementById('p26-body').innerHTML = + '
§ 26 · Химия 7

Охрана окружающей среды

' + +'
Почему важно беречь воду и воздух и как их защищают.
' + +'
экологияочистка воды
' + +makeCard('theory','Загрязнение и охрана','§26','

Вода и воздух — главные природные богатства. Их загрязняют промышленные выбросы, неочищенные сточные воды, разливы нефти. Оксиды серы и азота в воздухе вызывают кислотные дожди.

' + +'
Чтобы беречь природу, сточные воды очищают на специальных сооружениях, питьевую воду фильтруют и обеззараживают хлором или озоном, а каждый человек должен экономить воду и не загрязнять водоёмы.
') + +wgt('Источники загрязнения и способы охраны','
') + +rememberBox(['Воду и воздух загрязняют выбросы и стоки.','Воду очищают фильтрованием и обеззараживанием.','Беречь природу — задача каждого человека.']) + +qList(['Что загрязняет воду и воздух?','Как очищают питьевую воду?','Что может сделать каждый, чтобы беречь воду?']) + +secNav('pr4','final4')+readButton('p26'); + wireReadBtn('p26'); +} + +function build_final4(){ + document.getElementById('final4-body').innerHTML = + '
Финал главы 4

Босс: вода

' + +'
$H_2O$ · основания · нейтрализация · экология
' + +'
Шесть задач на всю главу. Реши все — получи звание «Хранитель воды».
' + +makeCard('rule','Шпаргалка главы 4',null,'
    ' + +'
  • Вода $H_2O$ разлагается током: $H_2:O_2=2:1$; реагирует с металлами и оксидами.
  • ' + +'
  • Основание = металл + OH; щёлочи — растворимые основания.
  • ' + +'
  • Индикаторы в щёлочи: лакмус — синий, фенолфталеин — малиновый.
  • ' + +'
  • Нейтрализация: кислота + основание → соль + вода.
  • ' + +'
  • Воду и воздух надо беречь и очищать.
') + +'

Реши задачи ниже — за каждую +5 XP, за полный разгром босса — звание и бонус. А затем проверь себя в финале всего курса на главной странице!

' + +secNav('p26',null); +} + +/* placeholder-страховка (на случай нерасставленных override) */ (function(){ var P = window.PARAS, B = {}; function ph(p, prev, next){ return function(){ var el = document.getElementById(p.id + '-body'); if (!el) return; - el.innerHTML = - '
' + p.num + ' · Химия 7

' + p.name + '

' - + '
Содержание этого ' + (p.final ? 'раздела' : 'параграфа') + ' готовится.
' - + makeCard('theory', p.name, p.num, - '

Скоро здесь появятся теория, наглядные SVG-схемы, молекулярные модели и интерактивные тренажёры по теме «' + p.name + '». Пока доступна навигация по главе' + (p.final ? '.' : ' и отметка о прочтении.') + '

') + el.innerHTML = '
' + p.num + ' · Химия 7

' + p.name + '

' + + makeCard('theory', p.name, p.num, '

Содержание готовится.

') + secNav(prev, next) + (p.final ? '' : readButton(p.id)); if (!p.final) wireReadBtn(p.id); }; } - for (var i = 0; i < P.length; i++) { - B[P[i].id] = ph(P[i], i > 0 ? P[i-1].id : null, i < P.length-1 ? P[i+1].id : null); - } + for (var i = 0; i < P.length; i++) B[P[i].id] = ph(P[i], i > 0 ? P[i-1].id : null, i < P.length-1 ? P[i+1].id : null); window.BUILDERS = B; })(); + +window.BUILDERS.p23 = build_p23; +window.BUILDERS.p24 = build_p24; +window.BUILDERS.lo5 = build_lo5; +window.BUILDERS.p25 = build_p25; +window.BUILDERS.pr4 = build_pr4; +window.BUILDERS.p26 = build_p26; +window.BUILDERS.final4 = build_final4; From 26eaee5c572f58ead205edf445e6362234c2adab Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sat, 30 May 2026 19:09:17 +0300 Subject: [PATCH 05/47] =?UTF-8?q?fix(chemistry7):=20=D1=82=D0=B5=D0=BC?= =?UTF-8?q?=D0=B0=20=D0=B3=D0=BB=D0=B0=D0=B2=D1=8B=20+=20=D1=84=D0=BE?= =?UTF-8?q?=D0=BD=20para-hero=20(=D0=BE=D0=B1=D0=BB=D0=B0=D1=81=D1=82?= =?UTF-8?q?=D1=8C=20=C2=A7-=D0=B7=D0=B0=D0=B3=D0=BE=D0=BB=D0=BE=D0=B2?= =?UTF-8?q?=D0=BA=D0=B0=20=D1=81=D0=BB=D0=B8=D0=B2=D0=B0=D0=BB=D0=B0=D1=81?= =?UTF-8?q?=D1=8C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Страницы глав наследовали amber-палитру chem8-textbook.css и базовый .para-hero без фона (нужен модификатор .ph-N) → блок заголовка § сливался с фоном. Добавлен per-chapter diff --git a/frontend/textbooks/chemistry_7_ch2.html b/frontend/textbooks/chemistry_7_ch2.html index 1e124e2..078d227 100644 --- a/frontend/textbooks/chemistry_7_ch2.html +++ b/frontend/textbooks/chemistry_7_ch2.html @@ -10,6 +10,12 @@ + diff --git a/frontend/textbooks/chemistry_7_ch3.html b/frontend/textbooks/chemistry_7_ch3.html index 349f0c1..21f31e1 100644 --- a/frontend/textbooks/chemistry_7_ch3.html +++ b/frontend/textbooks/chemistry_7_ch3.html @@ -10,6 +10,12 @@ + diff --git a/frontend/textbooks/chemistry_7_ch4.html b/frontend/textbooks/chemistry_7_ch4.html index ff866c0..7b79c64 100644 --- a/frontend/textbooks/chemistry_7_ch4.html +++ b/frontend/textbooks/chemistry_7_ch4.html @@ -10,6 +10,12 @@ + From a33f622a35cecfa860c7273dad5ee38f15f8e3af Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sat, 30 May 2026 19:12:48 +0300 Subject: [PATCH 06/47] =?UTF-8?q?style(textbooks):=20=D0=BA=D0=BE=D0=BC?= =?UTF-8?q?=D0=BF=D0=B0=D0=BA=D1=82=D0=BD=D0=B0=D1=8F=20=D0=BA=D0=BD=D0=BE?= =?UTF-8?q?=D0=BF=D0=BA=D0=B0=20=C2=AB=D0=92=20=D0=BB=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=80=D0=B0=D1=82=D0=BE=D1=80=D0=B8=D1=8E=C2=BB=20(=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BD=D0=BA=D0=B0=20+=20=D1=81=D1=87=D1=91=D1=82?= =?UTF-8?q?=D1=87=D0=B8=D0=BA)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Кнопка на карточке учебника наследовала .tb-btn{flex:1} и растягивалась наравне с «Продолжить» — длинный текст переносился на 3 строки, колба вставала посреди слова. Теперь .tb-lab-btn — компактный квадрат (как кнопка ДЗ): только колба, при нескольких связях добавляется число; полное название в title. flex:0 0 auto + white-space:nowrap убирают перенос, колба тонирована в --violet как научный акцент. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/textbooks.html | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/frontend/textbooks.html b/frontend/textbooks.html index b219108..2f2923e 100644 --- a/frontend/textbooks.html +++ b/frontend/textbooks.html @@ -164,6 +164,13 @@ width:auto; min-width:42px; padding:9px 12px; flex:0 0 auto; } + .tb-lab-btn { + width:auto; min-width:42px; padding:9px 12px; + flex:0 0 auto; white-space:nowrap; + font-variant-numeric: tabular-nums; font-weight:800; + } + .tb-lab-btn svg { stroke:var(--violet); flex-shrink:0; } + .tb-lab-btn:hover { border-color:var(--violet); background:rgba(155,93,229,.08); } .tb-empty { grid-column: 1 / -1; @@ -521,9 +528,8 @@ ${isTeacher ? `` : ''} - ${(labLinks[t.slug] && labLinks[t.slug].length) ? `` : ''} From c1ef1ecee9d5d83568f33b313824e66e0955d116 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sat, 30 May 2026 19:25:06 +0300 Subject: [PATCH 07/47] =?UTF-8?q?docs(chemistry7):=20=D0=BF=D0=BB=D0=B0?= =?UTF-8?q?=D0=BD=20=D0=B2=D0=B8=D0=B7=D1=83=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE?= =?UTF-8?q?=D0=B3=D0=BE=20=D0=B8=20=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D0=B0?= =?UTF-8?q?=D0=BA=D1=82=D0=B8=D0=B2=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=B0=D0=BF?= =?UTF-8?q?=D0=B3=D1=80=D0=B5=D0=B9=D0=B4=D0=B0=20(=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=BC=D0=B0=D1=86=D0=B8=D0=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ~15 флагманских анимированных интерактивов поверх готового учебника: общий движок chem7_anim.js (частицы, пузырьки, пламя, морфинг цвета, RAF-реестр с паузой вне экрана), апгрейд виджетов по главам (разделение смесей, 3D-молекулы, горение, ряд активности с пузырьками, электролиз 2:1, титрование). Фазы V0-V5, правила (reduced-motion, тёмная тема, перф, достоверная химия). Монтаж в существующие контейнеры. Co-Authored-By: Claude Opus 4.8 (1M context) --- plans/textbooks-7/PLAN_CHEMISTRY_7_VISUAL.md | 227 +++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 plans/textbooks-7/PLAN_CHEMISTRY_7_VISUAL.md diff --git a/plans/textbooks-7/PLAN_CHEMISTRY_7_VISUAL.md b/plans/textbooks-7/PLAN_CHEMISTRY_7_VISUAL.md new file mode 100644 index 0000000..dbca6ac --- /dev/null +++ b/plans/textbooks-7/PLAN_CHEMISTRY_7_VISUAL.md @@ -0,0 +1,227 @@ +# План: визуальный и интерактивный апгрейд учебника «Химия 7» + +**Дата:** 2026-05-30 +**Контекст:** дополнение к [PLAN_CHEMISTRY_7.md](PLAN_CHEMISTRY_7.md). Базовый учебник реализован +полностью (все 26 §, 4 главы, виджеты в `chem7_ch1..4_widgets.js`), но интерактивы сейчас +в основном **статичные**: SVG-картинки, клик-раскрытие, `' + MIX.map(function (x, i) { return ''; }).join('') + '' @@ -85,8 +87,9 @@ + '
' + METHODS.map(function (mt) { return ''; }).join('') + '
' - + '
Выбери способ разделения.
'; - $(mountId + '-pick').addEventListener('change', function (e) { idx = +e.target.value; m._built = 0; render(); }); + + '
Выбери способ разделения — при верном ответе увидишь анимацию.
' + + '
'; + $(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 () { @@ -95,6 +98,10 @@ out.innerHTML = ok ? 'Верно! ' + esc(cur.method) + '. ' + esc(cur.why) : 'Не подходит. Подумай, чем различаются вещества в смеси (растворимость, магнитные свойства, температура кипения, плотность).'; + stopAnim(); + var host = $(mountId + '-anim'); + if (ok && W.Chem7Anim && host) anim = W.Chem7Anim.separation(host, cur.kind); + else if (host) host.innerHTML = ''; }); }); } @@ -201,19 +208,39 @@ +'
'+esc(note)+'
'; } - /* §5 — галерея простых веществ */ - function mount_p5() { - var m = $('p5-gal'); if (!m || m._built) return; m._built = 1; - m.innerHTML = '
' - + molCard('Водород','H2',[['H',2]],'2 атома H — двухатомная молекула') - + molCard('Кислород','O2',[['O',2]],'2 атома O') - + molCard('Озон','O3',[['O',3]],'3 атома O — тоже простое вещество') - + molCard('Азот','N2',[['N',2]],'2 атома N') - + '
Во всех молекулах — атомы одного элемента → это простые вещества. Кислород $\\text{O}_2$ и озон $\\text{O}_3$ образованы одним элементом, но это разные простые вещества.
'; - if (W.chem8RenderMath) try { W.chem8RenderMath(m); } catch(e){} + /* 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 = '
3D-модели недоступны.
'; return; } + var cur = keys[0], handle = null; + function render() { + if (handle) handle.stop(); + host.innerHTML = '
' + + keys.map(function (k) { return ''; }).join('') + '
' + + '
' + + '
' + caption + ' Перетаскивай модель мышью, чтобы повернуть.
'; + 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(); } - /* §6 — классификатор простое/сложное + галерея сложных веществ */ + /* §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, { @@ -223,16 +250,7 @@ { t:'N₂', b:0 }, { t:'NH₃', b:1 }, { t:'S', b:0 }, { t:'CH₄', b:1 } ] }); - var g = $('p6-gal'); - if (g && !g._built) { g._built = 1; - g.innerHTML = '
' - + molCard('Вода','H2O',[['O',1],['H',2]],'2 элемента: H и O') - + molCard('Углекислый газ','CO2',[['C',1],['O',2]],'2 элемента: C и O') - + molCard('Метан','CH4',[['C',1],['H',4]],'2 элемента: C и H') - + molCard('Аммиак','NH3',[['N',1],['H',3]],'2 элемента: N и H') - + '
В каждой молекуле — атомы разных элементов → это сложные вещества.
'; - if (W.chem8RenderMath) try { W.chem8RenderMath(g); } catch(e){} - } + molViewer($('p6-gal'), ['H2O', 'CO2', 'CH4', 'NH3'], 'Сложное вещество — атомы разных элементов.'); } /* ── Волна 3 ── */ diff --git a/frontend/textbooks/chemistry_7_ch1.html b/frontend/textbooks/chemistry_7_ch1.html index c6aaa87..65d3604 100644 --- a/frontend/textbooks/chemistry_7_ch1.html +++ b/frontend/textbooks/chemistry_7_ch1.html @@ -23,6 +23,7 @@ html.dark{--bg:#0a1a12;--border:#1f4030;--pri-soft:rgba(5,150,105,.18);--sec-acc + From 41985a93eb58d3ce63aa03bf1ddf0c2126ed2c59 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sat, 30 May 2026 19:42:33 +0300 Subject: [PATCH 09/47] =?UTF-8?q?feat(chemistry7):=20=D0=B2=D0=B8=D0=B7?= =?UTF-8?q?=D1=83=D0=B0=D0=BB=20V1=20=E2=80=94=20=D0=B0=D0=BD=D0=B8=D0=BC?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F=20=C2=A710=20(=D0=BF=D1=80=D0=B8=D0=B7?= =?UTF-8?q?=D0=BD=D0=B0=D0=BA=D0=B8=20=D1=80=D0=B5=D0=B0=D0=BA=D1=86=D0=B8?= =?UTF-8?q?=D0=B8)=20=D0=B8=20=C2=A711=20(=D0=BE=D1=81=D0=B0=D0=B4=D0=BE?= =?UTF-8?q?=D0=BA)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit chem7_anim.js: CSS-хелперы (jsdom-safe, без canvas) — bubbleField (пузырьки газа), precipField (падающий осадок + слой), flameBox (мерцающее пламя+искры), colorBlock (плавная смена цвета вещества). §10/ЛО1: «Провести опыт» проигрывает анимацию по типу опыта (малахит зеленеет→чернеет, голубой осадок CuSO4+NaOH, синее пламя серы, пузырьки CO2). §11: при «Смешать» формируется осадок Cu(OH)2, весы остаются ровными. Тесты chem7: 16/16 pass; полный прогон 162/165 (3 — baseline Auth). Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/tests/chemistry7-page.test.js | 8 ++++ frontend/js/chem7_anim.js | 64 ++++++++++++++++++++++++++- frontend/js/chem7_ch1_widgets.js | 27 ++++++++--- 3 files changed, 93 insertions(+), 6 deletions(-) diff --git a/backend/tests/chemistry7-page.test.js b/backend/tests/chemistry7-page.test.js index 37be400..8d907b0 100644 --- a/backend/tests/chemistry7-page.test.js +++ b/backend/tests/chemistry7-page.test.js @@ -111,6 +111,14 @@ test('ch1 V-пилот: 3D-молекулы §5/§6 + анимация разд assert.ok(btn, 'кнопка верного метода §2 найдена'); btn.dispatchEvent(new doc.defaultView.Event('click', { bubbles: true })); await wait(50); assert.ok(doc.querySelector('#p2-sep-anim canvas'), 'сцена разделения §2 (canvas)'); + // §10: анимация признаков реакции после «Провести опыт» + doc.defaultView.goTo('p10'); await wait(120); + doc.getElementById('p10-signs-go').dispatchEvent(new doc.defaultView.Event('click', { bubbles: true })); await wait(40); + assert.ok(doc.querySelector('#p10-signs-stage div'), 'анимация признаков реакции §10'); + // §11: осадок появляется при «Смешать» + doc.defaultView.goTo('p11'); await wait(120); + doc.getElementById('p11-mix').dispatchEvent(new doc.defaultView.Event('click', { bubbles: true })); await wait(40); + assert.ok(doc.querySelector('#p11-stage div'), 'анимация осадка §11'); assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | ')); }); diff --git a/frontend/js/chem7_anim.js b/frontend/js/chem7_anim.js index 7d2e540..cc47dfc 100644 --- a/frontend/js/chem7_anim.js +++ b/frontend/js/chem7_anim.js @@ -203,8 +203,70 @@ setTimeout(function () { try { host.removeChild(box); } catch (e) {} }, 1300); } + /* ---- CSS-анимации (jsdom-safe, без canvas): пузырьки, осадок, пламя, смена цвета ---- */ + function injectKeyframes() { + if (D.getElementById('chem7-kf')) return; + var st = D.createElement('style'); st.id = 'chem7-kf'; + st.textContent = + '@keyframes c7-rise{0%{transform:translateY(0) scale(.6);opacity:0}15%{opacity:.9}100%{transform:translateY(-92px) scale(1);opacity:0}}' + + '@keyframes c7-fall{0%{transform:translateY(-26px);opacity:0}18%{opacity:1}100%{transform:translateY(58px);opacity:.85}}' + + '@keyframes c7-flick{0%,100%{transform:scaleY(1);opacity:.92}50%{transform:scaleY(1.18) translateY(-3px);opacity:1}}'; + (D.head || D.documentElement).appendChild(st); + } + function fieldHost(host, h) { + host.innerHTML = ''; host.style.position = 'relative'; host.style.height = h + 'px'; + host.style.overflow = 'hidden'; host.style.borderRadius = '12px'; + return host; + } + // поток пузырьков газа снизу вверх + function bubbleField(host, opts) { + opts = opts || {}; injectKeyframes(); fieldHost(host, opts.h || 120); + host.style.background = opts.bg || 'linear-gradient(180deg,var(--pri-soft),transparent)'; + var n = opts.count || 14, col = opts.color || 'rgba(255,255,255,.85)'; + for (var i = 0; i < n; i++) { + var d = D.createElement('div'), sz = rand(5, 11); + d.style.cssText = 'position:absolute;bottom:6px;left:' + rand(8, 92) + '%;width:' + sz + 'px;height:' + sz + 'px;border-radius:50%;background:' + col + ';border:1px solid rgba(0,0,0,.12);animation:c7-rise ' + rand(1.3, 2.4).toFixed(2) + 's linear ' + rand(0, 1.6).toFixed(2) + 's infinite'; + host.appendChild(d); + } + return { stop: function () { try { host.innerHTML = ''; host.style.height = ''; } catch (e) {} } }; + } + // осадок: частицы падают и оседают слоем + function precipField(host, opts) { + opts = opts || {}; injectKeyframes(); fieldHost(host, opts.h || 120); + host.style.background = opts.bg || 'linear-gradient(180deg,transparent,var(--pri-soft))'; + var n = opts.count || 16, col = opts.color || '#38bdf8'; + var sed = D.createElement('div'); sed.style.cssText = 'position:absolute;left:0;right:0;bottom:0;height:14px;background:' + col + ';opacity:.55;border-radius:0 0 12px 12px'; host.appendChild(sed); + for (var i = 0; i < n; i++) { + var d = D.createElement('div'), sz = rand(5, 9); + d.style.cssText = 'position:absolute;top:8px;left:' + rand(8, 92) + '%;width:' + sz + 'px;height:' + sz + 'px;border-radius:50%;background:' + col + ';animation:c7-fall ' + rand(1.1, 2.0).toFixed(2) + 's ease-in ' + rand(0, 1.4).toFixed(2) + 's infinite'; + host.appendChild(d); + } + return { stop: function () { try { host.innerHTML = ''; host.style.height = ''; } catch (e) {} } }; + } + // пламя (мерцающая капля-градиент) + function flameBox(host, opts) { + opts = opts || {}; injectKeyframes(); fieldHost(host, opts.h || 120); + var col = opts.color || '#f97316'; + var f = D.createElement('div'); + f.style.cssText = 'position:absolute;left:50%;bottom:10px;transform:translateX(-50%);transform-origin:bottom center;width:46px;height:78px;border-radius:50% 50% 50% 50%/60% 60% 40% 40%;background:radial-gradient(circle at 50% 75%,#fde047,' + col + ' 60%,transparent 78%);animation:c7-flick .5s ease-in-out infinite'; + host.appendChild(f); + if (opts.sparks) for (var i = 0; i < 8; i++) { var s = D.createElement('div'); s.style.cssText = 'position:absolute;bottom:14px;left:' + rand(38, 62) + '%;width:3px;height:3px;border-radius:50%;background:#fb923c;animation:c7-rise ' + rand(.8, 1.4).toFixed(2) + 's linear ' + rand(0, 1).toFixed(2) + 's infinite'; host.appendChild(s); } + return { stop: function () { try { host.innerHTML = ''; host.style.height = ''; } catch (e) {} } }; + } + // блок вещества с плавной сменой цвета (зелёный→чёрный и т.п.) + function colorBlock(host, fromC, toC, label, ms) { + fieldHost(host, 90); ms = ms || 1800; + var b = D.createElement('div'); + b.style.cssText = 'position:absolute;inset:14px;border-radius:10px;background:' + fromC + ';transition:background ' + ms + 'ms ease;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:700;text-shadow:0 1px 2px rgba(0,0,0,.4)'; + b.textContent = label || ''; + host.appendChild(b); + W.requestAnimationFrame(function () { W.requestAnimationFrame(function () { b.style.background = toC; }); }); + return { stop: function () { try { host.innerHTML = ''; host.style.height = ''; } catch (e) {} }, el: b }; + } + W.Chem7Anim = { HEADLESS: HEADLESS, reduced: reduced, ease: ease, loop: loop, sceneCanvas: sceneCanvas, - molecule3d: molecule3d, separation: separation, colorMorph: colorMorph, confettiSmall: confettiSmall, observeVisible: observeVisible + molecule3d: molecule3d, separation: separation, colorMorph: colorMorph, confettiSmall: confettiSmall, observeVisible: observeVisible, + bubbleField: bubbleField, precipField: precipField, flameBox: flameBox, colorBlock: colorBlock }; })(window); diff --git a/frontend/js/chem7_ch1_widgets.js b/frontend/js/chem7_ch1_widgets.js index b5e435d..107610a 100644 --- a/frontend/js/chem7_ch1_widgets.js +++ b/frontend/js/chem7_ch1_widgets.js @@ -325,17 +325,30 @@ { 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; + var idx = 0, anim = null; + function stopAnim() { if (anim) { anim.stop(); anim = null; } } function render() { + stopAnim(); m.innerHTML = '
' + '
' + + '
' + '
Выбери опыт и нажми «Провести опыт».
'; - $(mountId + '-pick').addEventListener('change', function (e) { idx = +e.target.value; m._built = 0; render(); }); + $(mountId + '-pick').addEventListener('change', function (e) { idx = +e.target.value; render(); }); $(mountId + '-go').addEventListener('click', function () { - var d = DEMOS[idx], out = $(mountId + '-out'); out.className = 'out ok'; + var d = DEMOS[idx], out = $(mountId + '-out'); + stopAnim(); anim = demoAnim(idx, $(mountId + '-stage')); + out.className = 'out ok'; out.innerHTML = 'Наблюдаемые признаки реакции:
' + d.signs.map(function (s) { return '
✓ ' + esc(s) + '
'; }).join('') + '
Эти признаки указывают, что произошла химическая реакция — образовались новые вещества.
'; @@ -366,13 +379,17 @@ + '' + (mixed ? 'продукты' : 'реагенты') + '' + ''; } + var anim = null; function render() { + if (anim) { anim.stop(); anim = null; } m.innerHTML = scale() + '
' + (mixed - ? 'После реакции: осадок Cu(OH)₂ + раствор Na₂SO₄. Стрелка весов не сдвинулась — масса сохранилась (100 г = 100 г).' + ? 'После реакции: осадок Cu(OH)₂ + раствор Na₂SO₄. Стрелка весов не сдвинулась — масса сохранилась (100 г = 100 г).' : 'До реакции: раствор CuSO₄ + раствор NaOH, общая масса 100 г.') + '
' + + '
' + ''; - $('p11-mix').addEventListener('click', function () { mixed = !mixed; m._built = 0; render(); }); + if (mixed && W.Chem7Anim) anim = W.Chem7Anim.precipField($('p11-stage'), { color: '#38bdf8', h: 96 }); + $('p11-mix').addEventListener('click', function () { mixed = !mixed; render(); }); } render(); } From e8cb95be55fe306da929eea381195bc57e184974 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sat, 30 May 2026 19:45:53 +0300 Subject: [PATCH 10/47] =?UTF-8?q?feat(chemistry7):=20=D0=B2=D0=B8=D0=B7?= =?UTF-8?q?=D1=83=D0=B0=D0=BB=20V2=20=E2=80=94=20=D0=B7=D0=B2=D1=91=D0=B7?= =?UTF-8?q?=D0=B4=D0=BD=D1=8B=D0=B9=20=D1=84=D0=BB=D0=B0=D0=B3=D0=BC=D0=B0?= =?UTF-8?q?=D0=BD=20=C2=A715=20=C2=AB=D0=93=D0=BE=D1=80=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=C2=BB=20(=D0=B0=D0=BD=D0=B8=D0=BC=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=BF=D0=BB=D0=B0=D0=BC=D0=B5=D0=BD=D0=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Подключён chem7_anim.js в Главу 2. §15: статичное SVG-пламя заменено на анимированный flameBox с достоверным цветом по веществу — углерод оранжевое, сера синее, фосфор ярко-белое, железо/магний с искрами; продукт-оксид и уравнение всплывают. Тесты chem7: 16/16 pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/tests/chemistry7-page.test.js | 1 + frontend/js/chem7_ch2_widgets.js | 25 +++++++++++++++---------- frontend/textbooks/chemistry_7_ch2.html | 1 + 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/backend/tests/chemistry7-page.test.js b/backend/tests/chemistry7-page.test.js index 8d907b0..475df7e 100644 --- a/backend/tests/chemistry7-page.test.js +++ b/backend/tests/chemistry7-page.test.js @@ -171,6 +171,7 @@ test('ch2 Волна 1: интерактивы §13 + ЛО2 + §14 + §15 мон assert.ok(doc.querySelector('#p15-burn #p15-go'), 'симулятор горения §15'); doc.defaultView.goTo('p15'); doc.getElementById('p15-go').dispatchEvent(new doc.defaultView.Event('click', { bubbles: true })); assert.match(doc.querySelector('#p15-out').textContent, /оксид/, 'горение даёт оксид'); + assert.ok(doc.querySelector('#p15-stage div'), 'анимация пламени §15'); assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | ')); }); diff --git a/frontend/js/chem7_ch2_widgets.js b/frontend/js/chem7_ch2_widgets.js index 4109066..f8a7291 100644 --- a/frontend/js/chem7_ch2_widgets.js +++ b/frontend/js/chem7_ch2_widgets.js @@ -86,28 +86,33 @@ /* §15 — симулятор горения: вещество + O₂ → оксид */ var FUELS = [ - { el:'C', name:'углерод', eq:'C + O2 = CO2', note:'горит с образованием углекислого газа' }, - { el:'S', name:'сера', eq:'S + O2 = SO2', note:'горит синим пламенем, резкий запах' }, - { el:'P', name:'фосфор', eq:'4P + 5O2 = 2P2O5', note:'горит ярко, белый дым' }, - { el:'Fe', name:'железо', eq:'3Fe + 2O2 = Fe3O4', note:'горит, разбрасывая искры' }, - { el:'Mg', name:'магний', eq:'2Mg + O2 = 2MgO', note:'ослепительно яркое пламя' } + { el:'C', name:'углерод', eq:'C + O2 = CO2', flame:'#f97316', sparks:false, note:'горит с образованием углекислого газа' }, + { el:'S', name:'сера', eq:'S + O2 = SO2', flame:'#3b82f6', sparks:false, note:'горит синим пламенем, резкий запах' }, + { el:'P', name:'фосфор', eq:'4P + 5O2 = 2P2O5', flame:'#fde68a', sparks:false, note:'горит ярко, с белым дымом' }, + { el:'Fe', name:'железо', eq:'3Fe + 2O2 = Fe3O4', flame:'#f59e0b', sparks:true, note:'горит, разбрасывая искры' }, + { el:'Mg', name:'магний', eq:'2Mg + O2 = 2MgO', flame:'#e0f2fe', sparks:true, note:'горит ослепительно ярким пламенем' } ]; function flame(){ return ''; } function mount_p15() { var m = $('p15-burn'); if (!m || m._built) return; m._built = 1; - var idx = 0; + var idx = 0, anim = null; + function stopAnim() { if (anim) { anim.stop(); anim = null; } } function render(){ - var f = FUELS[idx]; + stopAnim(); m.innerHTML = '
' + '
' + + '
' + '
Выбери вещество и подожги его в кислороде.
'; - $('p15-pick').addEventListener('change', function(e){ idx=+e.target.value; m._built=0; render(); }); + $('p15-pick').addEventListener('change', function(e){ idx=+e.target.value; render(); }); $('p15-go').addEventListener('click', function(){ - var out = $('p15-out'); out.className='out ok'; - out.innerHTML = flame() + ' ' + esc(f.name[0].toUpperCase()+f.name.slice(1)) + ' горит в кислороде: ' + esc(f.note) + '.
' + var f = FUELS[idx], out = $('p15-out'); + stopAnim(); + if (W.Chem7Anim) anim = W.Chem7Anim.flameBox($('p15-stage'), { color: f.flame, sparks: f.sparks }); + out.className='out ok'; + out.innerHTML = '' + esc(f.name[0].toUpperCase()+f.name.slice(1)) + ' горит в кислороде: ' + esc(f.note) + '.
' + '
' + ceq(f.eq) + '
' + '
Продукт — оксид (соединение элемента с кислородом).
'; }); diff --git a/frontend/textbooks/chemistry_7_ch2.html b/frontend/textbooks/chemistry_7_ch2.html index 078d227..ffea7ec 100644 --- a/frontend/textbooks/chemistry_7_ch2.html +++ b/frontend/textbooks/chemistry_7_ch2.html @@ -23,6 +23,7 @@ html.dark{--bg:#08191c;--border:#164e5b;--pri-soft:rgba(8,145,178,.18);--sec-acc + From 33f968bff9945f6b422b41b3f87e2c7430ef9516 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sat, 30 May 2026 19:51:27 +0300 Subject: [PATCH 11/47] =?UTF-8?q?feat(chemistry7):=20=D0=B2=D0=B8=D0=B7?= =?UTF-8?q?=D1=83=D0=B0=D0=BB=20V3=20(=D0=93=D0=BB=D0=B0=D0=B2=D0=B0=203)?= =?UTF-8?q?=20=E2=80=94=20=D0=BF=D1=83=D0=B7=D1=8B=D1=80=D1=8C=D0=BA=D0=B8?= =?UTF-8?q?,=20=D0=BC=D0=BE=D1=80=D1=84=D0=B8=D0=BD=D0=B3=20=D1=86=D0=B2?= =?UTF-8?q?=D0=B5=D1=82=D0=B0,=20=D0=B8=D0=BD=D0=B4=D0=B8=D0=BA=D0=B0?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Подключён chem7_anim.js в Главу 3. - §21 ряд активности (звёздный): клик металла левее H₂ → анимация пузырьков H₂ (bubbleField); правее (Cu, Ag) — «реакция не идёт»; - §19 восстановление CuO: colorBlock плавно чёрный→красный (медь); горение — пламя водорода; - §20/ЛО3 индикаторы: блок плавно меняет цвет на цвет индикатора в кислоте. Тесты chem7: 16/16; полный прогон 162/165 (3 — baseline Auth). Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/tests/chemistry7-page.test.js | 5 ++++ frontend/js/chem7_ch3_widgets.js | 35 ++++++++++++++++++------- frontend/textbooks/chemistry_7_ch3.html | 1 + 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/backend/tests/chemistry7-page.test.js b/backend/tests/chemistry7-page.test.js index 475df7e..2789b82 100644 --- a/backend/tests/chemistry7-page.test.js +++ b/backend/tests/chemistry7-page.test.js @@ -194,8 +194,10 @@ test('ch3 Волна 1: §18 + §19 + §20 + ЛО3 монтируются', asyn assert.ok(doc.querySelector('#p18-card svg'), 'паспорт водорода §18'); doc.defaultView.goTo('p19'); await wait(100); assert.ok(doc.querySelector('#p19-rx #p19-pick'), 'реакции водорода §19'); + assert.ok(doc.querySelector('#p19-stage div'), 'анимация реакции §19'); doc.defaultView.goTo('p20'); await wait(100); assert.ok(doc.querySelector('#p20-ind #p20-ind-ind'), 'индикаторы §20'); + assert.ok(doc.querySelector('#p20-ind-drop div'), 'анимация индикатора §20'); assert.ok(doc.querySelector('#p20-acids table'), 'таблица кислот §20'); doc.defaultView.goTo('lo3'); await wait(100); assert.ok(doc.querySelector('#lo3-ind #lo3-ind-ind'), 'индикаторы ЛО3'); @@ -206,6 +208,9 @@ test('ch3 Волна 2: §21 + ЛО4 + §22 + ПР3 + финал главы мо const { doc, errors } = await loadDom('chemistry_7_ch3.html'); doc.defaultView.goTo('p21'); await wait(100); assert.ok(doc.querySelector('#p21-act .act-cell'), 'ряд активности §21'); + // клик по Zn (левее H₂) → пузырьки H₂ + doc.querySelector('#p21-act .act-cell[data-i="5"]').dispatchEvent(new doc.defaultView.Event('click', { bubbles: true })); await wait(40); + assert.ok(doc.querySelector('#p21-tube div'), 'пузырьки H₂ при реакции металла с кислотой §21'); doc.defaultView.goTo('lo4'); await wait(100); assert.ok(doc.querySelector('#lo4-rx #lo4-go'), 'опыт металл+кислота ЛО4'); doc.defaultView.goTo('p22'); await wait(100); diff --git a/frontend/js/chem7_ch3_widgets.js b/frontend/js/chem7_ch3_widgets.js index 2369a47..a13bdfd 100644 --- a/frontend/js/chem7_ch3_widgets.js +++ b/frontend/js/chem7_ch3_widgets.js @@ -35,14 +35,21 @@ ]; function mount_p19() { var m = $('p19-rx'); if (!m || m._built) return; m._built = 1; - var idx = 0; + var idx = 0, anim = null; + function stopAnim(){ if(anim){anim.stop();anim=null;} } function render(){ + stopAnim(); var r = RX[idx]; - var swatch = idx===1 ? '
CuO (чёрный) → Cu (красный)
' : ''; m.innerHTML = '
' - + '
' + ceq(r.eq) + '
' + esc(r.note) + '
' + swatch + '
'; - $('p19-pick').addEventListener('change', function(e){ idx=+e.target.value; m._built=0; render(); }); + + '
' + + '
' + ceq(r.eq) + '
' + esc(r.note) + '
'; + var stage = $('p19-stage'); + if (stage && W.Chem7Anim) { + if (idx === 1) anim = W.Chem7Anim.colorBlock(stage, '#1f2937', '#b45309', 'CuO (чёрный) → Cu (красная медь)', 1800); + else anim = W.Chem7Anim.flameBox(stage, { color: '#93c5fd' }); + } + $('p19-pick').addEventListener('change', function(e){ idx=+e.target.value; render(); }); } render(); } @@ -60,17 +67,20 @@ }; function indicatorWidget(mountId, withAcidPick) { var m = $(mountId); if (!m || m._built) return; m._built = 1; - var ind = 'Лакмус', acid = 0; + var ind = 'Лакмус', acid = 0, anim = null; function strip(color){ return '
'; } function render(){ + if (anim) { anim.stop(); anim = null; } var a = ACIDS[acid], col = INDIC[ind]; m.innerHTML = '
' + (withAcidPick ? '' : '') + '
' + + '
' + '
В нейтральной среде: ' + strip(col.neutral[0]) + ' '+col.neutral[1]+'
' + 'В кислоте' + (withAcidPick?(' ('+fml(a.f)+')'):'') + ': ' + strip(col.acid[0]) + ' '+col.acid[1]+'
'; - $(mountId+'-ind').addEventListener('change', function(e){ ind=e.target.value; m._built=0; render(); }); - if (withAcidPick) $(mountId+'-acid').addEventListener('change', function(e){ acid=+e.target.value; m._built=0; render(); }); + if (W.Chem7Anim) anim = W.Chem7Anim.colorBlock($(mountId+'-drop'), col.neutral[0], col.acid[0], ind + ' в кислоте → ' + col.acid[1], 900); + $(mountId+'-ind').addEventListener('change', function(e){ ind=e.target.value; render(); }); + if (withAcidPick) $(mountId+'-acid').addEventListener('change', function(e){ acid=+e.target.value; render(); }); } render(); } @@ -87,21 +97,26 @@ var ROW = ['K','Ca','Na','Mg','Al','Zn','Fe','Ni','Sn','Pb','H','Cu','Hg','Ag','Pt','Au']; function mount_p21() { var m = $('p21-act'); if (!m || m._built) return; m._built = 1; - var hIdx = ROW.indexOf('H'); + var hIdx = ROW.indexOf('H'), anim = null; + function stopAnim(){ if(anim){anim.stop();anim=null;} } m.innerHTML = '
' + ROW.map(function(el,i){ var isH=el==='H'; return ''; }).join('') + '
' - + '
Слева активность убывает вправо. Граница — водород H₂.
' + + '
Слева активность убывает вправо. Граница — водород H₂. Кликни металл — «опусти» его в кислоту.
' + + '
' + '
Кликни по металлу — узнаешь, вытесняет ли он водород из кислоты.
'; var out = $('p21-act-out'); m.querySelectorAll('.act-cell').forEach(function(b){ b.addEventListener('click', function(){ - var i=+b.dataset.i, el=ROW[i]; if(el==='H'){ out.className='out'; out.innerHTML='Водород H₂ — граница ряда активности.'; return; } + var i=+b.dataset.i, el=ROW[i], tube=$('p21-tube'); stopAnim(); + if(el==='H'){ out.className='out'; out.innerHTML='Водород H₂ — граница ряда активности.'; if(tube)tube.innerHTML=''; return; } out.className='out ok'; if(iВнимание: очень активный металл — с кислотами реагирует бурно (для получения водорода используют Zn, Fe).' : ''; out.innerHTML = ''+el+' стоит левее H₂ → вытесняет водород из соляной и серной кислот: образуются соль и $H_2\\uparrow$.'+extra; + if (tube && W.Chem7Anim) anim = W.Chem7Anim.bubbleField(tube, { color:'rgba(255,255,255,.85)', h:96 }); } else { out.innerHTML = ''+el+' стоит правее H₂ → водород из кислот не вытесняет (например, медь и серебро с этими кислотами не реагируют).'; + if (tube) tube.innerHTML = '
реакция не идёт — пузырьков нет
'; } if (W.chem8RenderMath) try { W.chem8RenderMath(out); } catch(e){} }); diff --git a/frontend/textbooks/chemistry_7_ch3.html b/frontend/textbooks/chemistry_7_ch3.html index 21f31e1..4adf69e 100644 --- a/frontend/textbooks/chemistry_7_ch3.html +++ b/frontend/textbooks/chemistry_7_ch3.html @@ -23,6 +23,7 @@ html.dark{--bg:#140a24;--border:#3b2a63;--pri-soft:rgba(124,58,237,.18);--sec-ac + From 639f985e6f6a375bece7f62743109c6d20a9620d Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sat, 30 May 2026 19:54:50 +0300 Subject: [PATCH 12/47] =?UTF-8?q?feat(chemistry7):=20=D0=B2=D0=B8=D0=B7?= =?UTF-8?q?=D1=83=D0=B0=D0=BB=20V4=20(=D0=93=D0=BB=D0=B0=D0=B2=D0=B0=204)?= =?UTF-8?q?=20=E2=80=94=20=D1=8D=D0=BB=D0=B5=D0=BA=D1=82=D1=80=D0=BE=D0=BB?= =?UTF-8?q?=D0=B8=D0=B7=202:1,=20=D0=B8=D0=BD=D0=B4=D0=B8=D0=BA=D0=B0?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D1=8B,=20=D1=82=D0=B8=D1=82=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Подключён chem7_anim.js в Главу 4. - §23 (звёздный): электролиз воды — два потока пузырьков H₂ (18) и O₂ (9), наглядно 2:1; - §24/ЛО5 индикаторы щёлочи: блок плавно меняет цвет (фенолфталеин → малиновый); - §25/ПР4 нейтрализация (звёздный): раствор плавно обесцвечивается малиновый → бесцветный (colorBlock). Все 4 главы анимированы. Тесты chem7: 16/16; полный прогон 162/165 (3 — baseline Auth). Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/tests/chemistry7-page.test.js | 3 +++ frontend/js/chem7_ch4_widgets.js | 36 +++++++++++++++++-------- frontend/textbooks/chemistry_7_ch4.html | 1 + 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/backend/tests/chemistry7-page.test.js b/backend/tests/chemistry7-page.test.js index 2789b82..e16db67 100644 --- a/backend/tests/chemistry7-page.test.js +++ b/backend/tests/chemistry7-page.test.js @@ -225,13 +225,16 @@ test('ch3 Волна 2: §21 + ЛО4 + §22 + ПР3 + финал главы мо test('ch4: вся глава 4 (§23–§26 + ЛО5 + ПР4 + финал) монтируется', async () => { const { doc, errors } = await loadDom('chemistry_7_ch4.html'); assert.ok(doc.querySelector('#p23-water #p23-pick'), 'разложение/реакции воды §23'); + assert.ok(doc.querySelector('#p23-bub-h div'), 'пузырьки электролиза 2:1 §23'); doc.defaultView.goTo('p24'); await wait(100); assert.ok(doc.querySelector('#p24-bld #p24-m'), 'конструктор оснований §24'); assert.ok(doc.querySelector('#p24-ind #p24-ind-sel'), 'индикаторы щёлочи §24'); + assert.ok(doc.querySelector('#p24-ind-drop div'), 'анимация индикатора §24'); doc.defaultView.goTo('lo5'); await wait(100); assert.ok(doc.querySelector('#lo5-ind #lo5-ind-sel'), 'индикаторы ЛО5'); doc.defaultView.goTo('p25'); await wait(100); assert.ok(doc.querySelector('#p25-neu #p25-neu-go'), 'нейтрализация §25'); + assert.ok(doc.querySelector('#p25-neu-cup div'), 'анимация раствора §25'); doc.defaultView.goTo('pr4'); await wait(100); assert.ok(doc.querySelector('#pr4-neu #pr4-neu-go'), 'нейтрализация ПР4'); doc.defaultView.goTo('p26'); await wait(100); diff --git a/frontend/js/chem7_ch4_widgets.js b/frontend/js/chem7_ch4_widgets.js index 42dcb65..b85b14c 100644 --- a/frontend/js/chem7_ch4_widgets.js +++ b/frontend/js/chem7_ch4_widgets.js @@ -36,15 +36,23 @@ } function mount_p23() { var m = $('p23-water'); if (!m || m._built) return; m._built = 1; - var idx = 0; + var idx = 0, anims = []; + function stopAnim(){ anims.forEach(function(a){ try { a.stop(); } catch(e){} }); anims = []; } function render(){ + stopAnim(); var r = WRX[idx]; - m.innerHTML = (idx===0 ? decompSvg() : '') + m.innerHTML = (idx===0 ? decompSvg() + + '
H₂ — 2 объёма
' + + '
O₂ — 1 объём
' : '') + '
' + '
'+ceq(r.eq,{cond:r.cond})+'
' + '
'+esc(r.note)+'
'; - $('p23-pick').addEventListener('change', function(e){ idx=+e.target.value; m._built=0; render(); }); + if (idx===0 && W.Chem7Anim) { + anims.push(W.Chem7Anim.bubbleField($('p23-bub-h'), { color:'rgba(96,165,250,.9)', count:18, h:84, bg:'linear-gradient(180deg,#dbeafe,transparent)' })); + anims.push(W.Chem7Anim.bubbleField($('p23-bub-o'), { color:'rgba(248,113,113,.9)', count:9, h:84, bg:'linear-gradient(180deg,#fee2e2,transparent)' })); + } + $('p23-pick').addEventListener('change', function(e){ idx=+e.target.value; render(); }); } render(); } @@ -57,14 +65,17 @@ }; function alkIndicator(mountId) { var m = $(mountId); if (!m || m._built) return; m._built = 1; - var ind = 'Фенолфталеин'; + var ind = 'Фенолфталеин', anim = null; function render(){ + if (anim) { anim.stop(); anim = null; } var c = ALK_IND[ind]; m.innerHTML = '
' + + '
' + '
В нейтральной среде: ' + strip(c.neutral[0]) + ' '+c.neutral[1]+'
' + 'В щёлочи: ' + strip(c.alk[0]) + ' '+c.alk[1]+'
'; - $(mountId+'-sel').addEventListener('change', function(e){ ind=e.target.value; m._built=0; render(); }); + if (W.Chem7Anim) anim = W.Chem7Anim.colorBlock($(mountId+'-drop'), c.neutral[0], c.alk[0], ind + ' в щёлочи → ' + c.alk[1], 900); + $(mountId+'-sel').addEventListener('change', function(e){ ind=e.target.value; render(); }); } render(); } @@ -94,16 +105,19 @@ /* §25 / ПР4 — нейтрализация (фенолфталеин малиновый → бесцветный) */ function mount_neutral(mountId) { var m = $(mountId); if (!m || m._built) return; m._built = 1; - var done = false; - function beaker(color){ return ''; } + var done = false, anim = null; function render(){ - m.innerHTML = '
' + beaker(done?'#f8fafc':'#db2777') - + '
'+(done + if (anim) { anim.stop(); anim = null; } + m.innerHTML = '
' + + '
'+(done ? 'Раствор стал бесцветным — кислота нейтрализовала щёлочь. Реакция завершена.' - : 'В щёлочи с фенолфталеином раствор малиновый. Добавляй кислоту по каплям.')+'
' + : 'В щёлочи с фенолфталеином раствор малиновый. Добавляй кислоту по каплям.')+'
' + '
' + (done ? '
'+ceq('HCl + NaOH = NaCl + H2O')+'
Кислота + основание → соль + вода. Это реакция нейтрализации.
' : ''); - $(mountId+'-go').addEventListener('click', function(){ done=!done; m._built=0; render(); }); + if (W.Chem7Anim) anim = done + ? W.Chem7Anim.colorBlock($(mountId+'-cup'), '#db2777', '#f8fafc', 'малиновый → бесцветный', 1600) + : W.Chem7Anim.colorBlock($(mountId+'-cup'), '#db2777', '#db2777', 'щёлочь + фенолфталеин', 1); + $(mountId+'-go').addEventListener('click', function(){ done=!done; render(); }); } render(); } diff --git a/frontend/textbooks/chemistry_7_ch4.html b/frontend/textbooks/chemistry_7_ch4.html index 7b79c64..b75c506 100644 --- a/frontend/textbooks/chemistry_7_ch4.html +++ b/frontend/textbooks/chemistry_7_ch4.html @@ -23,6 +23,7 @@ html.dark{--bg:#0a1222;--border:#1e3a5f;--pri-soft:rgba(37,99,235,.18);--sec-acc + From ac6552b44fba3c02269e57cc8cb509c5c40242fc Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sat, 30 May 2026 20:07:06 +0300 Subject: [PATCH 13/47] =?UTF-8?q?feat(chemistry7):=20=D0=B2=D0=B8=D0=B7?= =?UTF-8?q?=D1=83=D0=B0=D0=BB=20V1-=D1=85=D0=B2=D0=BE=D1=81=D1=82=20?= =?UTF-8?q?=E2=80=94=20=C2=A79=20=D0=B2=D0=B0=D0=BB=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=BD=D1=8B=D0=B5=20=D1=81=D0=B2=D1=8F=D0=B7=D0=B8=20+=20?= =?UTF-8?q?=C2=A712=20=D0=BF=D0=BE=D0=B4=D1=81=D1=87=D1=91=D1=82=20=D0=B0?= =?UTF-8?q?=D1=82=D0=BE=D0=BC=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §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) --- backend/tests/chemistry7-page.test.js | 2 ++ frontend/js/chem7_anim.js | 33 +++++++++++++++++++- frontend/js/chem7_ch1_widgets.js | 45 ++++++++++++++++++++++----- 3 files changed, 72 insertions(+), 8 deletions(-) diff --git a/backend/tests/chemistry7-page.test.js b/backend/tests/chemistry7-page.test.js index e16db67..bfe152c 100644 --- a/backend/tests/chemistry7-page.test.js +++ b/backend/tests/chemistry7-page.test.js @@ -131,6 +131,7 @@ test('ch1 Волна 3: интерактивы §7–§9 монтируются assert.match(doc.querySelector('#p8-out').textContent, /100/, 'M_r(CaCO3)=100'); doc.defaultView.goTo('p9'); await wait(100); assert.ok(doc.querySelector('#p9-bld #p9-a'), 'конструктор валентности §9'); + assert.ok(doc.querySelector('#p9-vis svg circle'), 'схема валентных связей §9'); assert.match(doc.querySelector('#p9-bout').textContent, /Al/, 'формула по валентности построена'); assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | ')); }); @@ -145,6 +146,7 @@ test('ch1 Волна 4: §10–§12 + ЛО1 + финал главы монтир assert.ok(doc.querySelector('#p11-bal svg'), 'весы сохранения массы §11'); doc.defaultView.goTo('p12'); await wait(120); assert.ok(doc.querySelector('#p12-mount').childElementCount > 0, 'балансировщик §12'); + assert.ok(doc.querySelector('#p12-tally .c7-atom'), 'подсчёт атомов §12 (летящие атомы)'); doc.defaultView.goTo('final1'); await wait(120); assert.ok(doc.querySelectorAll('#navDotsfinal1 .nav-dot').length >= 6, 'боссы финала главы'); assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | ')); diff --git a/frontend/js/chem7_anim.js b/frontend/js/chem7_anim.js index cc47dfc..dcbba7f 100644 --- a/frontend/js/chem7_anim.js +++ b/frontend/js/chem7_anim.js @@ -264,9 +264,40 @@ return { stop: function () { try { host.innerHTML = ''; host.style.height = ''; } catch (e) {} }, el: b }; } + /* ---- валентные «крючки»: атомы A и B с чёрточками-связями, соединяющимися (§9) ---- */ + function valenceLink(host, spec) { + var ns = 'http://www.w3.org/2000/svg'; + var na = spec.a.n, nb = spec.b.n, va = spec.a.val, vb = spec.b.val, lcm = va * na; + var W0 = 300, r = 17, lx = 50, rx = W0 - 50; + var H0 = Math.max(na, nb, 1) * 50 + 20; + function ycol(n, k) { var gap = 50, top = (H0 - (n - 1) * gap) / 2; return top + k * gap; } + function spread(idx, val) { return (idx - (val - 1) / 2) * 9; } + var colA = spec.a.color || '#6366f1', colB = spec.b.color || '#ef4444'; + var bonds = ''; + for (var t = 0; t < lcm; t++) { + var la = Math.floor(t / va), rb = Math.floor(t / vb); + var sy = ycol(na, la) + spread(t % va, va), ey = ycol(nb, rb) + spread(t % vb, vb); + var sx = lx + r, ex = rx - r, len = Math.hypot(ex - sx, ey - sy); + bonds += ''; + } + function atom(x, y, el, col) { + return '' + + '' + el + ''; + } + var atoms = ''; + for (var i = 0; i < na; i++) atoms += atom(lx, ycol(na, i), spec.a.el, colA); + for (var j = 0; j < nb; j++) atoms += atom(rx, ycol(nb, j), spec.b.el, colB); + host.innerHTML = '' + bonds + atoms + ''; + if (!HEADLESS && !reduced()) { + var lines = host.querySelectorAll('line'); + W.requestAnimationFrame(function () { W.requestAnimationFrame(function () { lines.forEach(function (l) { l.setAttribute('stroke-dashoffset', '0'); }); }); }); + } + return { stop: function () { try { host.innerHTML = ''; } catch (e) {} } }; + } + W.Chem7Anim = { HEADLESS: HEADLESS, reduced: reduced, ease: ease, loop: loop, sceneCanvas: sceneCanvas, molecule3d: molecule3d, separation: separation, colorMorph: colorMorph, confettiSmall: confettiSmall, observeVisible: observeVisible, - bubbleField: bubbleField, precipField: precipField, flameBox: flameBox, colorBlock: colorBlock + bubbleField: bubbleField, precipField: precipField, flameBox: flameBox, colorBlock: colorBlock, valenceLink: valenceLink }; })(window); diff --git a/frontend/js/chem7_ch1_widgets.js b/frontend/js/chem7_ch1_widgets.js index 107610a..78ec135 100644 --- a/frontend/js/chem7_ch1_widgets.js +++ b/frontend/js/chem7_ch1_widgets.js @@ -295,23 +295,30 @@ 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 ''; }).join(''); } function optB(){ return VB.map(function(e,i){ return ''; }).join(''); } m.innerHTML = '
' - +'
'; + +'' + +'
' + +'
'; 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 = 'Валентности: ' + a[0] + ' = ' + 'I'.repeat(a[1]).replace('IIII','IV') + ', ' + b[0] + ' = ' + 'I'.repeat(b[1]) + '
' - + 'Наименьшее общее кратное валентностей = ' + lcm + '
' - + 'Индексы: ' + a[0] + ' → ' + ia + ', ' + b[0] + ' → ' + ib + '
' - + 'Формула: ' + (C().formula ? C().formula(raw) : raw) + '
' - + 'Проверка: ' + ia + '·' + a[1] + ' = ' + ib + '·' + b[1] + ' = ' + lcm + ' единиц валентности — совпало.
'; + + 'Каждая чёрточка-связь соединена — все валентности заняты.
' + + 'НОК валентностей = ' + lcm + '; индексы: ' + a[0] + ' → ' + ia + ', ' + b[0] + ' → ' + ib + '
' + + 'Формула: ' + (C().formula ? C().formula(raw) : raw) + ''; } $('p9-a').addEventListener('change', upd); $('p9-b').addEventListener('change', upd); upd(); } @@ -394,10 +401,34 @@ render(); } - /* §12 — балансировщик уравнений (переиспользуем Chem8.equationBalancer) */ + /* §12 — балансировщик + анимированный подсчёт атомов (слева/справа) */ + var ELC = { H:'#cbd5e1', O:'#ef4444', C:'#334155', N:'#3b82f6', S:'#eab308', Fe:'#b45309', P:'#f97316', Cl:'#22c55e', Mg:'#22c55e', Ca:'#a78bfa', Na:'#a78bfa', Cu:'#ea580c', Zn:'#64748b', Al:'#6366f1', K:'#a78bfa' }; function mount_p12() { var pick = $('p12-pick'), mount = $('p12-mount'); if (!pick || pick._built || !C().equationBalancer) return; pick._built = 1; - function build() { var parts = pick.value.split('|'); C().equationBalancer(mount, { skeleton: parts[0], solution: parts[1].split(',').map(Number) }); } + if (!$('p12-tally')) mount.insertAdjacentHTML('afterend', '
'); + 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 += ''; return s; } + function col(title, tot) { return '
' + title + '
' + Object.keys(tot).map(function (e) { return '
' + e + '' + dots(e, tot[e]) + '× ' + tot[e] + '
'; }).join('') + '
'; } + 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 = '
' + col('Реагенты — атомы', left) + col('Продукты — атомы', right) + '
' + + '
' + (ok ? '✓ Число атомов каждого элемента слева и справа совпадает — уравнение сбалансировано.' : 'Атомы не уравнены.') + '
'; + 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(); } From 5f481f5d11aee8c32cea67fa5ae3527bc366078f Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sat, 30 May 2026 21:25:17 +0300 Subject: [PATCH 14/47] =?UTF-8?q?fix(admin):=20=D1=80=D0=B5=D0=BD=D0=B4?= =?UTF-8?q?=D0=B5=D1=80=20KaTeX=20=D0=B2=20=D1=81=D0=B5=D0=BA=D1=86=D0=B8?= =?UTF-8?q?=D0=B8=20=C2=AB=D0=92=D0=BE=D0=BF=D1=80=D0=BE=D1=81=D1=8B=C2=BB?= =?UTF-8?q?=20=E2=80=94=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D1=8B=20=D1=80=D0=B0=D0=B7=D0=B4=D0=B5=D0=BB=D0=B8=D1=82=D0=B5?= =?UTF-8?q?=D0=BB=D0=B8=20$=E2=80=A6$=20=D0=B8=20$$=E2=80=A6$$?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit renderMath в _shared.js распознавал только \(…\) и \[…\], из-за чего 873 вопроса с долларовыми разделителями не рендерили формулы в админке. $$ ставится раньше $, чтобы auto-render не принял его за два пустых $. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/js/admin/_shared.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/js/admin/_shared.js b/frontend/js/admin/_shared.js index be6ed2d..14f2bda 100644 --- a/frontend/js/admin/_shared.js +++ b/frontend/js/admin/_shared.js @@ -30,9 +30,13 @@ /* ─── KaTeX rendering ─── */ const KATEX_OPTS = { + // Порядок важен: многосимвольные/display-разделители ($$, \[) идут раньше + // одиночного $, иначе auto-render распознает $$ как два пустых $. delimiters: [ - { left: '\\(', right: '\\)', display: false }, + { left: '$$', right: '$$', display: true }, { left: '\\[', right: '\\]', display: true }, + { left: '\\(', right: '\\)', display: false }, + { left: '$', right: '$', display: false }, ], throwOnError: false, }; From 7c32501e1865001169601eccfe6136043a45a4c1 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sat, 30 May 2026 21:29:00 +0300 Subject: [PATCH 15/47] =?UTF-8?q?fix(admin):=20=D0=BE=D1=82=D0=BE=D0=B1?= =?UTF-8?q?=D1=80=D0=B0=D0=B6=D0=B0=D1=82=D1=8C=20HTML-=D1=80=D0=B0=D0=B7?= =?UTF-8?q?=D0=BC=D0=B5=D1=82=D0=BA=D1=83=20=D0=B2=D0=BE=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D1=81=D0=BE=D0=B2=20=D0=B2=20=D1=81=D0=B5=D0=BA=D1=86=D0=B8?= =?UTF-8?q?=D0=B8=20=C2=AB=D0=92=D0=BE=D0=BF=D1=80=D0=BE=D1=81=D1=8B=C2=BB?= =?UTF-8?q?=20=D0=BF=D1=80=D0=B8=20allow=5Fhtml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Секция игнорировала флаг allow_html и всегда экранировала текст/опции/ пояснение, из-за чего
, и пр. показывались как сырой текст. Теперь — как в test-run.html: allow_html ? raw : esc. Также добавлен q.allow_html в SELECT списка вопросов (его не было в ответе API). Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/src/controllers/questionController.js | 2 +- frontend/js/admin/sections/questions.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/src/controllers/questionController.js b/backend/src/controllers/questionController.js index 02575cb..54b8521 100644 --- a/backend/src/controllers/questionController.js +++ b/backend/src/controllers/questionController.js @@ -44,7 +44,7 @@ function list(req, res) { const sql = ` SELECT q.id, q.text, q.type, q.correct_text, q.difficulty, q.explanation, q.image, - q.year, q.source_type, + q.year, q.source_type, q.allow_html, t.name AS topic, t.id AS topic_id, s.name AS subject_name, s.slug AS subject_slug, (SELECT json_group_array(json_object( diff --git a/frontend/js/admin/sections/questions.js b/frontend/js/admin/sections/questions.js index 57bbe26..4e2e44e 100644 --- a/frontend/js/admin/sections/questions.js +++ b/frontend/js/admin/sections/questions.js @@ -71,15 +71,15 @@ const diffCls = `diff-${q.difficulty}`; const optsHtml = (q.options || []).map(o => `
- ${o.is_correct ? '' : ''}${esc(o.text)} + ${o.is_correct ? '' : ''}${q.allow_html ? o.text : esc(o.text)}
`).join(''); const explHtml = q.explanation - ? `
Пояснение: ${esc(q.explanation)}
` : ''; + ? `
Пояснение: ${q.allow_html ? q.explanation : esc(q.explanation)}
` : ''; return `
#${q.id}
-
${esc(q.text)}
+
${q.allow_html ? q.text : esc(q.text)}
${q.subject_name ? `${esc(q.subject_name)}` : ''} ${q.topic ? `${esc(q.topic)}` : ''} From 8b5d9238b596808f962f12295bec9925729eeeaa Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sat, 30 May 2026 21:34:34 +0300 Subject: [PATCH 16/47] =?UTF-8?q?chore(backend):=20nodemon.json=20?= =?UTF-8?q?=E2=80=94=20=D0=B0=D0=B2=D1=82=D0=BE-=D0=BF=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B0=20=D1=81=D0=B5?= =?UTF-8?q?=D1=80=D0=B2=D0=B5=D1=80=D0=B0=20=D0=BF=D1=80=D0=B8=20=D0=B8?= =?UTF-8?q?=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D0=B8=20src/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Следим только за src/ (js,json,yaml), игнорируем тесты; data/, логи и uploads/ вне src/, поэтому циклов перезапуска нет. Запуск: npm run dev. Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/nodemon.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 backend/nodemon.json diff --git a/backend/nodemon.json b/backend/nodemon.json new file mode 100644 index 0000000..eb6c63a --- /dev/null +++ b/backend/nodemon.json @@ -0,0 +1,6 @@ +{ + "watch": ["src"], + "ext": "js,json,yaml,yml", + "ignore": ["src/**/*.test.js"], + "delay": "250" +} From b67fac6407d205e36fbb5002239e0bf69005050c Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sat, 30 May 2026 22:37:59 +0300 Subject: [PATCH 17/47] =?UTF-8?q?feat(biochem):=20=D0=A4=D0=B0=D0=B7=D0=B0?= =?UTF-8?q?=202.1/2.2/2.4=20=E2=80=94=20=D1=81=D0=B5=D1=80=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D0=BD=D1=8B=D0=B9=20chem.js=20+=20/analyze=20+=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=B4=D1=81=D0=BA=D0=B0=D0=B7=D0=BA=D0=B8=20=D0=B2=D0=B0?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=82=D0=BD=D0=BE=D1=81=D1=82=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - biochem-core.js dual-export (browser window.BIO + Node module.exports), без дублей - BIO.valency: подробные подсказки валентности (2.4), общие для редактора и сервера - services/chem.js: серверный анализ поверх того же ядра (analyze/validate) - POST /api/biochem/analyze (2.2); /validate переведён на ядро (+фикс формата связей) - api.js: LS.biochemAnalyze Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/src/controllers/biochemController.js | 23 +++++++--- backend/src/routes/biochem.js | 1 + backend/src/services/chem.js | 46 ++++++++++++++++++++ frontend/biochem.html | 11 ++--- frontend/js/biochem-core.js | 46 ++++++++++++++++++-- js/api.js | 3 +- plans/BIOCHEM_UPGRADE.md | 21 +++++---- 7 files changed, 125 insertions(+), 26 deletions(-) create mode 100644 backend/src/services/chem.js diff --git a/backend/src/controllers/biochemController.js b/backend/src/controllers/biochemController.js index 9ac3eed..4de8572 100644 --- a/backend/src/controllers/biochemController.js +++ b/backend/src/controllers/biochemController.js @@ -1,6 +1,7 @@ 'use strict'; const db = require('../db/db'); const { awardXP, checkAchievements } = require('./gamificationController'); +const chem = require('../services/chem'); /* ── Helpers ─────────────────────────────────────────────────────────── */ const MAX_V = { H:1, C:4, N:3, O:2, P:5, S:6, Cl:1, Na:1, Ca:2, K:1, Mg:2, Fe:3, Br:1, I:1, F:1 }; @@ -128,14 +129,26 @@ function validate(req, res) { return res.status(400).json({ error: 'atoms[] and bonds[] required' }); if (atoms.length === 0) return res.json({ valid: false, formula: '', issues: [] }); - const formula = hillFormula(atoms); - const issues = valencyIssues(atoms, bonds); - const valid = issues.length === 0; - + // Единое химическое ядро (Фаза 2.1): формула + валентность с подсказками (2.4) + const { valid, formula, issues } = chem.validate(atoms, bonds); const known = valid ? stmts.getMolByFormula.get(formula) : null; res.json({ valid, formula, issues, known: known || null }); } +/* ── POST /api/biochem/analyze — полный химический анализ структуры (2.2) ─ */ +function analyze(req, res) { + const { atoms, bonds } = req.body || {}; + if (!Array.isArray(atoms)) + return res.status(400).json({ error: 'atoms[] обязателен' }); + if (atoms.length === 0) + return res.json({ formula: '', mass: 0, dbe: null, valency: [] }); + try { + res.json(chem.analyze(atoms, Array.isArray(bonds) ? bonds : [])); + } catch (e) { + res.status(500).json({ error: e.message }); + } +} + /* ── GET /api/biochem/reactions ─────────────────────────────────────── */ function getReactions(_req, res) { const rows = stmts.getReactions.all().map(r => ({ @@ -374,7 +387,7 @@ function tryParse(v, fallback) { } module.exports = { - getElements, getMolecules, getMolecule, validate, + getElements, getMolecules, getMolecule, validate, analyze, getReactions, getChallenges, solveChallenge, getSaved, saveMolecule, deleteSaved, getPathways, getPathwayProgress, savePathwayProgress, diff --git a/backend/src/routes/biochem.js b/backend/src/routes/biochem.js index 38ace09..bc37da4 100644 --- a/backend/src/routes/biochem.js +++ b/backend/src/routes/biochem.js @@ -8,6 +8,7 @@ router.get('/elements', c.getElements); router.get('/molecules', c.getMolecules); router.get('/molecules/:id', c.getMolecule); router.post('/validate', c.validate); +router.post('/analyze', c.analyze); router.get('/reactions', c.getReactions); router.get('/challenges', c.getChallenges); router.post('/challenges/:id/solve', c.solveChallenge); diff --git a/backend/src/services/chem.js b/backend/src/services/chem.js new file mode 100644 index 0000000..397af26 --- /dev/null +++ b/backend/src/services/chem.js @@ -0,0 +1,46 @@ +'use strict'; +/* + * chem.js — серверный химический слой (Фаза 2.1/2.2). + * + * Переиспользует то же ядро, что и фронт (frontend/js/biochem-core.js, + * `window.BIO`), вместо дублирования химии: формулы/масса/DBE, частичные + * заряды, дипольный момент (по 3D-геометрии VSEPR), полярность, функциональные + * группы, гибридизация, проверка валентности. + * + * Ядро самодостаточно (без DOM/canvas в чистых функциях) и при require в Node + * экспортирует объект BIO через module.exports. + */ +const path = require('path'); +const BIO = require(path.join(__dirname, '..', '..', '..', 'frontend', 'js', 'biochem-core.js')); + +/* Полный анализ структуры → {formula, mass, dbe, geometry, polarity, dipole, + * charges, groups, massFractions, valency}. Бросает на некорректном вводе. */ +function analyze(atoms, bonds) { + const an = BIO.analyze(atoms, bonds || []); + if (!an) return null; + return { + formula: an.formula, + mass: an.mass, + dbe: an.dbe, + atomCount: an.atomCount, + geometry: an.geometry, // {shape, hybridization, angle, centerSym} + polarity: an.polarity ? an.polarity.label : null, // «Полярная» / «Неполярная» / … + dipole: an.dipole, + charges: an.charges, + groups: an.groups, + massFractions: an.massFractions, + valency: BIO.valency(atoms, bonds || []), + }; +} + +/* Проверка корректности: формула + проблемы валентности (с подсказками). */ +function validate(atoms, bonds) { + const issues = BIO.valency(atoms, bonds || []); + return { + valid: issues.length === 0, + formula: BIO.hillFormula(atoms || []), + issues, + }; +} + +module.exports = { analyze, validate, BIO }; diff --git a/frontend/biochem.html b/frontend/biochem.html index 096e8c2..2ddd791 100644 --- a/frontend/biochem.html +++ b/frontend/biochem.html @@ -683,13 +683,8 @@ function getBondSum(id) { return bonds.reduce((s,b) => s + (b.from===id||b.to===id ? (b.order||b.o||1) : 0), 0); } function getIssues() { - return atoms.filter(a => { - const used = bonds.reduce((s,b)=>(b.from===a.id||b.to===a.id)?s+(b.order||1):s,0); - return used > (ELEMENTS[a.s]?.maxV ?? 4); - }).map(a => { - const used = bonds.reduce((s,b)=>(b.from===a.id||b.to===a.id)?s+(b.order||1):s,0); - return { id:a.id, s:a.s, used, max: ELEMENTS[a.s]?.maxV??4 }; - }); + // Единая проверка валентности из ядра (с подсказками, Фаза 2.4) + return BIO.valency(atoms, bonds); } // ── Live molecular stats ── @@ -1192,7 +1187,7 @@ function updateInfo() { const issues = getIssues(); const issDiv = document.getElementById('bp-issues'); if (issues.length) { - issDiv.innerHTML = issues.map(i => `
${i.s}: ${i.used}/${i.max} связей
`).join(''); + issDiv.innerHTML = issues.map(i => `
${i.msg}
`).join(''); } else if (formula) { issDiv.innerHTML = '
Валентность в норме
'; } else { diff --git a/frontend/js/biochem-core.js b/frontend/js/biochem-core.js index a622e0b..8747a60 100644 --- a/frontend/js/biochem-core.js +++ b/frontend/js/biochem-core.js @@ -911,15 +911,53 @@ }, }; - /* ── Экспорт ──────────────────────────────────────────────────────────── */ - global.BIO = { + /* ── Валентность: подробная проверка с подсказками (Фаза 2.4) ────────── + * Возвращает массив проблем-«перевалентностей» с готовым человекочитаемым + * текстом: [{ id, symbol, name, used, max, over, kind:'error', msg }]. + * Работает с обоими форматами связей (from/to/order и f/t/o) через bF/bT/bO. + */ + function _bondWord(n) { + const m10 = n % 10, m100 = n % 100; + if (m10 === 1 && m100 !== 11) return 'связь'; + if (m10 >= 2 && m10 <= 4 && (m100 < 12 || m100 > 14)) return 'связи'; + return 'связей'; + } + function valency(atoms, bonds) { + if (!atoms || !atoms.length) return []; + const sum = {}; + for (const b of (bonds || [])) { + const f = bF(b), t = bT(b), o = bO(b); + sum[f] = (sum[f] || 0) + o; + sum[t] = (sum[t] || 0) + o; + } + const out = []; + for (const a of atoms) { + const e = el(a.s); + const used = sum[a.id] || 0; + const max = e.maxV != null ? e.maxV : 4; + if (used > max) { + const over = used - max; + out.push({ + id: a.id, symbol: a.s, name: e.name, used, max, over, kind: 'error', + msg: e.name + ' (' + a.s + '): занято ' + used + ' ' + _bondWord(used) + + ', максимум ' + max + ' — убери ' + over, + }); + } + } + return out; + } + + /* ── Экспорт (браузер: window.BIO; Node: module.exports) ──────────────── */ + var _api = { ELEMENTS, el, bF, bT, bO, counts, hillFormula, molarMass, parseFormula, dbe, - partialCharges, dipole, polarity, massFractions, functionalGroups, analyze, + partialCharges, dipole, polarity, massFractions, functionalGroups, analyze, valency, balance, parseSmiles, toJSON, download, render2D, vsepr, render3D, chargeColor, safe, RING_TEMPLATES, _hexRgb, _lighten, _darken, }; -})(window); + global.BIO = _api; + if (typeof module !== 'undefined' && module.exports) module.exports = _api; +})(typeof window !== 'undefined' ? window : globalThis); diff --git a/js/api.js b/js/api.js index b39d6e9..1ea3303 100644 --- a/js/api.js +++ b/js/api.js @@ -937,6 +937,7 @@ async function biochemGetElements() { return req('GET', '/biochem/elements' async function biochemGetMolecules(p={}) { return req('GET', `/biochem/molecules?${new URLSearchParams(p)}`); } async function biochemGetMolecule(id) { return req('GET', `/biochem/molecules/${id}`); } async function biochemValidate(atoms,bonds){ return req('POST','/biochem/validate',{atoms,bonds}); } +async function biochemAnalyze(atoms,bonds){ return req('POST','/biochem/analyze',{atoms,bonds}); } async function biochemGetReactions() { return req('GET', '/biochem/reactions'); } async function biochemGetChallenges() { return req('GET', '/biochem/challenges'); } async function biochemSolveChallenge(id,payload) { return req('POST',`/biochem/challenges/${id}/solve`,payload); } @@ -1065,7 +1066,7 @@ window.LS = { clearFeaturesCache, hideDisabledFeatures, showBoardIfAllowed, - biochemGetElements, biochemGetMolecules, biochemGetMolecule, biochemValidate, + biochemGetElements, biochemGetMolecules, biochemGetMolecule, biochemValidate, biochemAnalyze, biochemGetReactions, biochemGetChallenges, biochemSolveChallenge, biochemGetSaved, biochemSave, biochemDeleteSaved, biochemGetPathways, biochemGetPathwayProgress, biochemSavePathwayProgress, diff --git a/plans/BIOCHEM_UPGRADE.md b/plans/BIOCHEM_UPGRADE.md index 0e4c342..4518a71 100644 --- a/plans/BIOCHEM_UPGRADE.md +++ b/plans/BIOCHEM_UPGRADE.md @@ -74,14 +74,19 @@ Считать химию, а не хранить класс строкой. -- [ ] 2.1 `backend/src/services/chem.js`: - - Полярность связей по разнице электроотрицательностей; **дипольный момент** молекулы (вектор-сумма с учётом 3D-геометрии из Фазы 1) → polar/nonpolar обоснованно. - - Частичные заряды (упрощённый Gasteiger / EN-метод) для раскраски атомов. - - DBE (степень ненасыщенности), молярная масса, массовые доли элементов. - - Гибридизация центра, классификация функциональных групп через SMARTS-подобные паттерны (вынести из хардкода фронта). -- [ ] 2.2 API `POST /api/biochem/analyze` (atoms,bonds → {formula, mass, dbe, dipole, polarity, charges, groups, hybridization}). Заменить фронтовую эвристику. -- [ ] 2.3 В редакторе: тепловая карта частичных зарядов (toggle), стрелка диполя в 3D, панель «геометрия и полярность». -- [ ] 2.4 Расширенная валидация: вместо «лимит превышен» — подсказки («у C занято 5 связей, максимум 4», «кислород обычно 2 связи»). +> Серверный срез (тег `biochem-phase2-server`): `backend/src/services/chem.js` +> **переиспользует то же ядро** `biochem-core.js` (сделан dual-export: браузер +> `window.BIO` + Node `module.exports`) — без дублирования химии. Эндпоинт +> `POST /api/biochem/analyze` отдаёт {formula, mass, dbe, geometry, polarity, +> dipole, charges, groups, massFractions, valency}; `/validate` переведён на +> ядро (плюс чинит баг формата связей b.o/order). 2.4: `BIO.valency` с +> подсказками («Углерод (C): занято 5 связей, максимум 4 — убери 1»), +> используется и в редакторе, и на сервере. + +- [x] 2.1 `backend/src/services/chem.js`: переиспользует ядро `BIO` (полярность/диполь по 3D-VSEPR, частичные заряды, DBE/масса/массовые доли, гибридизация, функциональные группы) — без дубля логики на сервере. +- [x] 2.2 API `POST /api/biochem/analyze` (atoms,bonds → {formula, mass, dbe, dipole, polarity, charges, groups, hybridization, valency}). Живой анализ в редакторе оставлен client-side (мгновенно); сервер — авторитетный расчёт + валидация на сохранении. +- [x] 2.3 В редакторе: тепловая карта частичных зарядов (toggle), стрелка диполя в 3D, панель «геометрия и полярность». +- [x] 2.4 Расширенная валидация: `BIO.valency` даёт подсказки («Углерод (C): занято 5 связей, максимум 4 — убери 1») вместо «лимит превышен»; единая логика в редакторе и на сервере. --- From cff9973dcf3deb12778137345ebabf87cad9c15e Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sun, 31 May 2026 08:03:02 +0300 Subject: [PATCH 18/47] =?UTF-8?q?fix(biochem):=20=D0=B0=D0=B2=D0=B0=D1=82?= =?UTF-8?q?=D0=B0=D1=80=D0=BA=D0=B0=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20LS.?= =?UTF-8?q?renderNavAvatar=20=D0=BD=D0=B0=20=D0=B2=D1=81=D0=B5=D1=85=20?= =?UTF-8?q?=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D0=B0=D1=85=20=D0=BC?= =?UTF-8?q?=D0=BE=D0=B4=D1=83=D0=BB=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Заменил ручное ava.textContent=initials на LS.renderNavAvatar(ava, user) в biochem.html / -library / -reactions / -properties. biochem-pathways.html уже был корректен. Co-Authored-By: Claude Sonnet 4.6 --- frontend/biochem-library.html | 2 +- frontend/biochem-properties.html | 2 +- frontend/biochem-reactions.html | 2 +- frontend/biochem.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/biochem-library.html b/frontend/biochem-library.html index 5e239f4..de09ac6 100644 --- a/frontend/biochem-library.html +++ b/frontend/biochem-library.html @@ -368,7 +368,7 @@ if (!user) location.href = '/login'; const nav = document.getElementById('nav-user'); const ava = document.getElementById('nav-avatar'); if (nav) nav.textContent = user?.name?.split(' ')[0] || 'Пользователь'; -if (ava) ava.textContent = (user?.name||'LS').split(' ').slice(0,2).map(w=>w[0]?.toUpperCase()||'').join('')||'LS'; +LS.renderNavAvatar(ava, user); if (isAdmin) document.getElementById('btn-admin').style.display = ''; if (isTeacher) document.getElementById('btn-classes').style.display = ''; LS.showBoardIfAllowed(); diff --git a/frontend/biochem-properties.html b/frontend/biochem-properties.html index f5e6bdf..6dc28e1 100644 --- a/frontend/biochem-properties.html +++ b/frontend/biochem-properties.html @@ -249,7 +249,7 @@ if (!user) location.href = '/login'; const nav = document.getElementById('nav-user'); const ava = document.getElementById('nav-avatar'); if (nav) nav.textContent = user?.name?.split(' ')[0] || 'Пользователь'; -if (ava) ava.textContent = (user?.name||'LS').split(' ').slice(0,2).map(w=>w[0]?.toUpperCase()||'').join('')||'LS'; +LS.renderNavAvatar(ava, user); if (isAdmin) document.getElementById('btn-admin').style.display = ''; if (isTeacher) document.getElementById('btn-classes').style.display = ''; LS.showBoardIfAllowed(); diff --git a/frontend/biochem-reactions.html b/frontend/biochem-reactions.html index 3bd6de8..a57123d 100644 --- a/frontend/biochem-reactions.html +++ b/frontend/biochem-reactions.html @@ -369,7 +369,7 @@ if (!user) location.href = '/login'; const nav = document.getElementById('nav-user'); const ava = document.getElementById('nav-avatar'); if (nav) nav.textContent = user?.name?.split(' ')[0] || 'Пользователь'; -if (ava) ava.textContent = (user?.name||'LS').split(' ').slice(0,2).map(w=>w[0]?.toUpperCase()||'').join('')||'LS'; +LS.renderNavAvatar(ava, user); if (isAdmin) document.getElementById('btn-admin').style.display = ''; if (isTeacher) document.getElementById('btn-classes').style.display = ''; LS.showBoardIfAllowed(); diff --git a/frontend/biochem.html b/frontend/biochem.html index 2ddd791..19ee569 100644 --- a/frontend/biochem.html +++ b/frontend/biochem.html @@ -532,7 +532,7 @@ if (!user) location.href = '/login'; const nav = document.getElementById('nav-user'); const ava = document.getElementById('nav-avatar'); if (nav) nav.textContent = user?.name?.split(' ')[0] || 'Пользователь'; -if (ava) ava.textContent = (user?.name||'LS').split(' ').slice(0,2).map(w=>w[0]?.toUpperCase()||'').join('')||'LS'; +LS.renderNavAvatar(ava, user); if (isAdmin) { document.getElementById('btn-admin').style.display=''; } if (isTeacher) { document.getElementById('btn-classes').style.display=''; } LS.showBoardIfAllowed(); From ec8403e26ccb983978aa59f5d389bce6869995fa Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sun, 31 May 2026 08:11:19 +0300 Subject: [PATCH 19/47] =?UTF-8?q?feat(admin/gam):=20=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=D0=BD=D0=B0=20=D1=84?= =?UTF-8?q?=D0=BE=D1=80=D0=BC=D0=B0=20=D0=BD=D0=B0=D1=87=D0=B8=D1=81=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=20XP/=D0=BC=D0=BE=D0=BD=D0=B5=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - select с полным списком пользователей + фильтр по имени (вместо typeahead) - пресеты XP (0/10/25/50/100/250) и монет (0/10/25/50) с подсветкой активного - пресеты причин (кнопки) + поле для своей причины - fix: xp/coins теперь Number(value) без || 0 — значение 0 не начисляется - форма сброса прогресса — тоже select из того же кэша пользователей Co-Authored-By: Claude Sonnet 4.6 --- frontend/admin.html | 103 ++++++++++--- frontend/js/admin/sections/gam.js | 238 +++++++++++++++++++----------- 2 files changed, 228 insertions(+), 113 deletions(-) diff --git a/frontend/admin.html b/frontend/admin.html index f5f2d30..6ac4457 100644 --- a/frontend/admin.html +++ b/frontend/admin.html @@ -653,6 +653,8 @@ .adm-toggle .thumb { position: absolute; top: 3px; left: 3px; width: 18px; height: 18px; border-radius: 50%; background: #fff; box-shadow: 0 1px 4px rgba(0,0,0,.15); transition: transform .2s; } .adm-toggle input:checked ~ .track { background: var(--green, #06d6a0); } .adm-toggle input:checked ~ .thumb { transform: translateX(18px); } + .gam-xp-preset.active, .gam-coins-preset.active { background: var(--violet); color: #fff; border-color: var(--violet); } + .gam-xp-preset:hover:not(.active), .gam-coins-preset:hover:not(.active) { border-color: var(--violet); color: var(--violet); } .adm-user-search { position: relative; } .adm-user-search .us-results { position: absolute; top: 100%; left: 0; right: 0; z-index: 50; background: #fff; border: 1.5px solid var(--border-h); border-radius: 12px; max-height: 240px; overflow-y: auto; box-shadow: 0 8px 24px rgba(15,23,42,0.12); display: none; } .adm-user-search .us-results.open { display: block; } @@ -1376,39 +1378,94 @@
Начислить XP / Монеты
-
-