feat(chemistry7): визуальный апгрейд V0 (движок) + пилот V1

chem7_anim.js — анимационный движок (window.Chem7Anim): RAF-цикл с паузой
вне экрана (IntersectionObserver), prefers-reduced-motion, headless-guard
(jsdom-safe: молекулы на SVG, canvas без getContext в тестах),
molecule3d (вращающаяся 3D-модель, drag), separation (частицы:
фильтр/выпаривание/магнит/отстаивание/перегонка), colorMorph, confettiSmall.

Пилот в Главе 1:
- §5/§6: статичные галереи → вращающиеся 3D-модели (H2/O2/O3/N2, H2O/CO2/CH4/NH3) с переключателем;
- §2/ПР1: при верном методе разделения проигрывается анимация частиц.

Тесты chem7: 16/16 pass; полный прогон 162/165 (3 — baseline Auth).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-30 19:35:44 +03:00
parent c1ef1ecee9
commit f620562124
4 changed files with 274 additions and 29 deletions
+47 -29
View File
@@ -67,17 +67,19 @@
/* §2 / ПР1 — разделитель смесей: выбери метод для смеси */
var MIX = [
{ mix: 'Песок и вода', method: 'Фильтрование', why: 'Песок не растворяется — задерживается фильтром, вода проходит.' },
{ mix: 'Соль и вода', method: 'Выпаривание', why: 'Вода испаряется, соль остаётся на дне.' },
{ mix: 'Железные опилки и сера', method: 'Магнит', why: 'Железо притягивается магнитом, сера — нет.' },
{ mix: 'Вода и растительное масло', method: 'Отстаивание (делительная воронка)', why: 'Масло легче воды и не смешивается — слои разделяют.' },
{ mix: 'Спирт и вода', method: 'Перегонка (дистилляция)', why: 'У спирта и воды разные температуры кипения.' }
{ mix: 'Песок и вода', method: 'Фильтрование', kind: 'filter', why: 'Песок не растворяется — задерживается фильтром, вода проходит.' },
{ mix: 'Соль и вода', method: 'Выпаривание', kind: 'evaporate', why: 'Вода испаряется, соль остаётся на дне.' },
{ mix: 'Железные опилки и сера', method: 'Магнит', kind: 'magnet', why: 'Железо притягивается магнитом, сера — нет.' },
{ mix: 'Вода и растительное масло', method: 'Отстаивание (делительная воронка)', kind: 'settle', why: 'Масло легче воды и не смешивается — слои разделяют.' },
{ mix: 'Спирт и вода', method: 'Перегонка (дистилляция)', kind: 'distill', why: 'У спирта и воды разные температуры кипения.' }
];
var METHODS = ['Фильтрование', 'Выпаривание', 'Магнит', 'Отстаивание (делительная воронка)', 'Перегонка (дистилляция)'];
function mount_sep(mountId) {
var m = $(mountId); if (!m || m._built) return; m._built = 1;
var idx = 0;
var idx = 0, anim = null;
function stopAnim() { if (anim) { anim.stop(); anim = null; } }
function render() {
stopAnim();
var cur = MIX[idx];
m.innerHTML = '<div class="fld"><label>Смесь</label><select id="' + mountId + '-pick">'
+ MIX.map(function (x, i) { return '<option value="' + i + '"' + (i === idx ? ' selected' : '') + '>' + esc(x.mix) + '</option>'; }).join('') + '</select></div>'
@@ -85,8 +87,9 @@
+ '<div style="display:flex;flex-wrap:wrap;gap:6px">' + METHODS.map(function (mt) {
return '<button class="c7-m btn" data-m="' + esc(mt) + '">' + esc(mt) + '</button>';
}).join('') + '</div>'
+ '<div class="out" id="' + mountId + '-out" style="margin-top:8px">Выбери способ разделения.</div>';
$(mountId + '-pick').addEventListener('change', function (e) { idx = +e.target.value; m._built = 0; render(); });
+ '<div class="out" id="' + mountId + '-out" style="margin-top:8px">Выбери способ разделения — при верном ответе увидишь анимацию.</div>'
+ '<div id="' + mountId + '-anim" style="margin-top:8px;display:flex;justify-content:center"></div>';
$(mountId + '-pick').addEventListener('change', function (e) { idx = +e.target.value; render(); });
var out = $(mountId + '-out');
m.querySelectorAll('.c7-m').forEach(function (b) {
b.addEventListener('click', function () {
@@ -95,6 +98,10 @@
out.innerHTML = ok
? '<b>Верно!</b> ' + esc(cur.method) + '. ' + esc(cur.why)
: '<b>Не подходит.</b> Подумай, чем различаются вещества в смеси (растворимость, магнитные свойства, температура кипения, плотность).';
stopAnim();
var host = $(mountId + '-anim');
if (ok && W.Chem7Anim && host) anim = W.Chem7Anim.separation(host, cur.kind);
else if (host) host.innerHTML = '';
});
});
}
@@ -201,19 +208,39 @@
+'<div style="font-size:.82rem;color:var(--muted);margin-top:4px">'+esc(note)+'</div></div>';
}
/* §5 — галерея простых веществ */
function mount_p5() {
var m = $('p5-gal'); if (!m || m._built) return; m._built = 1;
m.innerHTML = '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px">'
+ molCard('Водород','H2',[['H',2]],'2 атома H — двухатомная молекула')
+ molCard('Кислород','O2',[['O',2]],'2 атома O')
+ molCard('Озон','O3',[['O',3]],'3 атома O — тоже простое вещество')
+ molCard('Азот','N2',[['N',2]],'2 атома N')
+ '</div><div style="font-size:.84rem;color:var(--muted);margin-top:8px">Во всех молекулах — атомы <b>одного</b> элемента → это <b>простые вещества</b>. Кислород $\\text{O}_2$ и озон $\\text{O}_3$ образованы одним элементом, но это разные простые вещества.</div>';
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 = '<div class="out">3D-модели недоступны.</div>'; return; }
var cur = keys[0], handle = null;
function render() {
if (handle) handle.stop();
host.innerHTML = '<div class="fld" style="flex-wrap:wrap;gap:6px">'
+ keys.map(function (k) { return '<button class="btn mv-b' + (k === cur ? ' primary' : '') + '" data-k="' + k + '">' + fmlName(k) + '</button>'; }).join('') + '</div>'
+ '<div id="' + host.id + '-stage" style="display:flex;justify-content:center;padding:8px 0"></div>'
+ '<div class="out" style="margin-top:4px">' + caption + ' Перетаскивай модель мышью, чтобы повернуть.</div>';
handle = A.molecule3d($(host.id + '-stage'), MOL[cur]);
host.querySelectorAll('.mv-b').forEach(function (b) { b.addEventListener('click', function () { cur = b.dataset.k; render(); }); });
}
render();
}
/* §6классификатор простое/сложное + галерея сложных веществ */
/* §53D-модели простых веществ */
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 = '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:10px">'
+ 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')
+ '</div><div style="font-size:.84rem;color:var(--muted);margin-top:8px">В каждой молекуле — атомы <b>разных</b> элементов → это <b>сложные вещества</b>.</div>';
if (W.chem8RenderMath) try { W.chem8RenderMath(g); } catch(e){}
}
molViewer($('p6-gal'), ['H2O', 'CO2', 'CH4', 'NH3'], 'Сложное вещество — атомы разных элементов.');
}
/* ── Волна 3 ── */