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(); }