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
+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);