From 8edab2196f0449d8bea821284b3cf6e311b8e927 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Tue, 2 Jun 2026 21:44:34 +0300 Subject: [PATCH] =?UTF-8?q?feat(math6):=20stepPlayer=20=E2=80=94=20=D0=B2?= =?UTF-8?q?=D1=81=D0=B5=20=C2=AB=D0=A0=D0=B0=D0=B7=D0=B1=D0=BE=D1=80=D1=8B?= =?UTF-8?q?=20=D0=BF=D0=BE=20=D1=88=D0=B0=D0=B3=D0=B0=D0=BC=C2=BB=20=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D0=BB=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=D1=8B=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Math6Anim.stepPlayer (DOM): пошаговый плеер с кнопками Назад/Дальше/Авто и точками прогресса, рендерит KaTeX по шагам. Math6Anim.stepifyExamples сканирует секцию и превращает карточки «Разбор по шагам» (
    в теле) в такой плеер. Движок зовёт stepifyExamples в goTo (guarded) → автоматически во ВСЕХ главах и параграфах, включая простые работы с дробями/столбиком. Подключён math6_anim в Гл.2,3 (теперь во всех 6). Тесты math6: 20/20. Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/tests/math6-page.test.js | 15 ++++++++ frontend/js/math6_anim.js | 61 ++++++++++++++++++++++++++++++ frontend/js/math6_engine.js | 1 + frontend/textbooks/math_6_ch2.html | 1 + frontend/textbooks/math_6_ch3.html | 1 + 5 files changed, 79 insertions(+) diff --git a/backend/tests/math6-page.test.js b/backend/tests/math6-page.test.js index ada1534..8832898 100644 --- a/backend/tests/math6-page.test.js +++ b/backend/tests/math6-page.test.js @@ -195,6 +195,21 @@ test('анимации: canvas-демо монтируются (headless-safe)', assert.deepEqual(r5.errors, [], 'ch5 без ошибок: ' + r5.errors.join(' | ')); }); +test('stepPlayer: «Разбор по шагам» становится интерактивным плеером', async () => { + // Глава 5 §1 — есть карточка «Разбор по шагам» + const r5 = await loadDom('math_6_ch5.html'); + r5.doc.defaultView.goTo('p1'); await wait(120); + assert.ok(r5.doc.querySelector('#p1-body .m6-step-view'), 'плеер шагов §5.1'); + assert.ok(r5.doc.querySelectorAll('#p1-body [data-act="next"]').length >= 1, 'кнопка «Дальше» §5.1'); + assert.ok(r5.doc.querySelectorAll('#p1-body .m6-step-dots span').length >= 3, 'точки шагов §5.1'); + assert.deepEqual(r5.errors, [], 'ch5 без ошибок: ' + r5.errors.join(' | ')); + // Глава 2 §1 + const r2 = await loadDom('math_6_ch2.html'); + r2.doc.defaultView.goTo('p1'); await wait(120); + assert.ok(r2.doc.querySelector('#p1-body .m6-step-view'), 'плеер шагов §2.1'); + assert.deepEqual(r2.errors, [], 'ch2 без ошибок: ' + r2.errors.join(' | ')); +}); + test('hub: 6 карточек глав + курсовой финал', async () => { const { doc, errors } = await loadDom('math_6_hub.html'); assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | ')); diff --git a/frontend/js/math6_anim.js b/frontend/js/math6_anim.js index 9daef37..2508745 100644 --- a/frontend/js/math6_anim.js +++ b/frontend/js/math6_anim.js @@ -278,4 +278,65 @@ M.plotLive = function (host, opts) { }; }; +/* ============================ КОМПОНЕНТ: ПОШАГОВЫЙ ПЛЕЕР (DOM, не canvas) ============================ */ +M.stepPlayer = function (host, opts) { + opts = opts || {}; var steps = opts.steps || []; if (!steps.length) return { stop: function () {} }; + var i = 0, auto = null; + host.innerHTML = ''; + var wrap = D.createElement('div'); + wrap.innerHTML = + '
    ' + + '
    ' + + '' + + '' + + '' + + '
    '; + host.appendChild(wrap); + var view = wrap.querySelector('.m6-step-view'), dots = wrap.querySelector('.m6-step-dots'); + var prevB = wrap.querySelector('[data-act="prev"]'), nextB = wrap.querySelector('[data-act="next"]'), autoB = wrap.querySelector('[data-act="auto"]'); + for (var k = 0; k < steps.length; k++) { var dt = D.createElement('span'); dt.style.cssText = 'width:9px;height:9px;border-radius:50%;background:var(--border,#cbd5e1);transition:background .2s'; dots.appendChild(dt); } + function render() { + view.innerHTML = steps[i] || ''; + if (W.renderMathInElement) try { W.renderMathInElement(view, { delimiters: [{ left: '$$', right: '$$', display: true }, { left: '$', right: '$', display: false }, { left: '\\[', right: '\\]', display: true }, { left: '\\(', right: '\\)', display: false }], throwOnError: false }); } catch (e) {} + var ds = dots.children; for (var k2 = 0; k2 < ds.length; k2++) ds[k2].style.background = (k2 === i ? 'var(--pri,#4f46e5)' : (k2 < i ? 'var(--ok,#10b981)' : 'var(--border,#cbd5e1)')); + prevB.disabled = i <= 0; nextB.disabled = i >= steps.length - 1; + prevB.style.opacity = prevB.disabled ? 0.5 : 1; nextB.style.opacity = nextB.disabled ? 0.5 : 1; + } + function go(d) { i = Math.max(0, Math.min(steps.length - 1, i + d)); render(); } + function stopAuto() { if (auto) { try { clearInterval(auto); } catch (e) {} auto = null; autoB.textContent = 'Авто'; } } + prevB.addEventListener('click', function () { stopAuto(); go(-1); }); + nextB.addEventListener('click', function () { stopAuto(); go(1); }); + autoB.addEventListener('click', function () { + if (auto) { stopAuto(); return; } + if (i >= steps.length - 1) { i = 0; render(); } + autoB.textContent = 'Пауза'; + auto = setInterval(function () { if (i >= steps.length - 1) { stopAuto(); return; } go(1); }, 1500); + }); + render(); + return { stop: stopAuto }; +}; + +/* Превратить карточки «Разбор по шагам» (.card с
      в теле) в интерактивный stepPlayer. */ +M.stepifyExamples = function (root) { + if (!root) return; + var cards = root.querySelectorAll('.card'); + for (var ci = 0; ci < cards.length; ci++) { + var card = cards[ci]; + var titleEl = card.querySelector('.card-title'); if (!titleEl) continue; + if (!/шаг/i.test(titleEl.textContent || '')) continue; + var body = card.querySelector('.card-body'); if (!body || body.__stepified) continue; + var ol = body.querySelector('ol'); if (!ol) continue; + var lis = ol.querySelectorAll('li'); if (lis.length < 2) continue; + var steps = [], intro = '', outro = '', node = body.firstChild; + while (node && node !== ol) { intro += (node.nodeType === 1 ? node.outerHTML : (node.nodeType === 3 ? node.textContent : '')); node = node.nextSibling; } + node = ol.nextSibling; while (node) { outro += (node.nodeType === 1 ? node.outerHTML : (node.nodeType === 3 ? node.textContent : '')); node = node.nextSibling; } + if (intro.replace(/<[^>]*>/g, '').trim()) steps.push('
      ' + intro + '
      '); + for (var li = 0; li < lis.length; li++) steps.push('
      Шаг ' + (li + 1) + '. ' + lis[li].innerHTML + '
      '); + if (outro.replace(/<[^>]*>/g, '').trim()) steps.push(outro); + body.__stepified = true; body.innerHTML = ''; + var hostEl = D.createElement('div'); body.appendChild(hostEl); + M.stepPlayer(hostEl, { steps: steps }); + } +}; + })(window); diff --git a/frontend/js/math6_engine.js b/frontend/js/math6_engine.js index 2982aa9..9b83535 100644 --- a/frontend/js/math6_engine.js +++ b/frontend/js/math6_engine.js @@ -184,6 +184,7 @@ function goTo(id) { buildSidebar(id); window.scrollTo({ top: 0, behavior: 'smooth' }); if ((STATE.progress[id] || 0) < 10) bumpProgress(id, 10); + if (el && window.Math6Anim && window.Math6Anim.stepifyExamples) { try { window.Math6Anim.stepifyExamples(el); } catch (e) {} } if (el && window.renderMathInElement) setTimeout(function () { renderMath(el); }, 0); setTimeout(function () { try { wrapGlossary(el); } catch (e) {} }, 60); markLastPara(id); diff --git a/frontend/textbooks/math_6_ch2.html b/frontend/textbooks/math_6_ch2.html index 607a834..0644b34 100644 --- a/frontend/textbooks/math_6_ch2.html +++ b/frontend/textbooks/math_6_ch2.html @@ -17,6 +17,7 @@ + diff --git a/frontend/textbooks/math_6_ch3.html b/frontend/textbooks/math_6_ch3.html index 60d5715..003ebaf 100644 --- a/frontend/textbooks/math_6_ch3.html +++ b/frontend/textbooks/math_6_ch3.html @@ -17,6 +17,7 @@ +