feat(math6): stepPlayer — все «Разборы по шагам» стали интерактивными

Math6Anim.stepPlayer (DOM): пошаговый плеер с кнопками Назад/Дальше/Авто
и точками прогресса, рендерит KaTeX по шагам. Math6Anim.stepifyExamples
сканирует секцию и превращает карточки «Разбор по шагам» (<ol> в теле) в
такой плеер. Движок зовёт stepifyExamples в goTo (guarded) → автоматически
во ВСЕХ главах и параграфах, включая простые работы с дробями/столбиком.
Подключён math6_anim в Гл.2,3 (теперь во всех 6). Тесты math6: 20/20.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-02 21:44:34 +03:00
parent 3f5333588c
commit 8edab2196f
5 changed files with 79 additions and 0 deletions
+15
View File
@@ -195,6 +195,21 @@ test('анимации: canvas-демо монтируются (headless-safe)',
assert.deepEqual(r5.errors, [], 'ch5 без ошибок: ' + r5.errors.join(' | ')); 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 () => { test('hub: 6 карточек глав + курсовой финал', async () => {
const { doc, errors } = await loadDom('math_6_hub.html'); const { doc, errors } = await loadDom('math_6_hub.html');
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | ')); assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
+61
View File
@@ -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 =
'<div class="m6-step-view" style="min-height:58px;padding:14px 16px;background:var(--sec-acc-soft,var(--pri-soft));border-radius:10px;font-size:1.02rem;line-height:1.65"></div>'
+ '<div style="display:flex;gap:8px;align-items:center;justify-content:center;margin-top:10px;flex-wrap:wrap">'
+ '<button class="btn" data-act="prev" type="button">Назад</button>'
+ '<span class="m6-step-dots" style="display:inline-flex;gap:5px"></span>'
+ '<button class="btn primary" data-act="next" type="button">Дальше</button>'
+ '<button class="btn" data-act="auto" type="button">Авто</button></div>';
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 с <ol> в теле) в интерактивный 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('<div style="font-weight:600">' + intro + '</div>');
for (var li = 0; li < lis.length; li++) steps.push('<div><b style="color:var(--pri,#4f46e5)">Шаг ' + (li + 1) + '.</b> ' + lis[li].innerHTML + '</div>');
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); })(window);
+1
View File
@@ -184,6 +184,7 @@ function goTo(id) {
buildSidebar(id); buildSidebar(id);
window.scrollTo({ top: 0, behavior: 'smooth' }); window.scrollTo({ top: 0, behavior: 'smooth' });
if ((STATE.progress[id] || 0) < 10) bumpProgress(id, 10); 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); if (el && window.renderMathInElement) setTimeout(function () { renderMath(el); }, 0);
setTimeout(function () { try { wrapGlossary(el); } catch (e) {} }, 60); setTimeout(function () { try { wrapGlossary(el); } catch (e) {} }, 60);
markLastPara(id); markLastPara(id);
+1
View File
@@ -17,6 +17,7 @@
<script src="/js/api.js" defer></script> <script src="/js/api.js" defer></script>
<script src="/js/xp.js" defer></script> <script src="/js/xp.js" defer></script>
<script src="/js/math6_svg.js" defer></script> <script src="/js/math6_svg.js" defer></script>
<script src="/js/math6_anim.js" defer></script>
<script src="/js/math6_engine.js" defer></script> <script src="/js/math6_engine.js" defer></script>
<style>:root{--pri:#0891b2;--pri2:#0e7490;--pri-soft:#cffafe;--acc:#06b6d4;--acc2:#0891b2;--acc-soft:#ecfeff}</style> <style>:root{--pri:#0891b2;--pri2:#0e7490;--pri-soft:#cffafe;--acc:#06b6d4;--acc2:#0891b2;--acc-soft:#ecfeff}</style>
</head> </head>
+1
View File
@@ -17,6 +17,7 @@
<script src="/js/api.js" defer></script> <script src="/js/api.js" defer></script>
<script src="/js/xp.js" defer></script> <script src="/js/xp.js" defer></script>
<script src="/js/math6_svg.js" defer></script> <script src="/js/math6_svg.js" defer></script>
<script src="/js/math6_anim.js" defer></script>
<script src="/js/math6_engine.js" defer></script> <script src="/js/math6_engine.js" defer></script>
<style>:root{--pri:#7c3aed;--pri2:#6d28d9;--pri-soft:#ede9fe;--acc:#8b5cf6;--acc2:#7c3aed;--acc-soft:#f5f3ff}</style> <style>:root{--pri:#7c3aed;--pri2:#6d28d9;--pri-soft:#ede9fe;--acc:#8b5cf6;--acc2:#7c3aed;--acc-soft:#f5f3ff}</style>
</head> </head>