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:
@@ -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(' | '));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<script src="/js/api.js" defer></script>
|
||||
<script src="/js/xp.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>
|
||||
<style>:root{--pri:#0891b2;--pri2:#0e7490;--pri-soft:#cffafe;--acc:#06b6d4;--acc2:#0891b2;--acc-soft:#ecfeff}</style>
|
||||
</head>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<script src="/js/api.js" defer></script>
|
||||
<script src="/js/xp.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>
|
||||
<style>:root{--pri:#7c3aed;--pri2:#6d28d9;--pri-soft:#ede9fe;--acc:#8b5cf6;--acc2:#7c3aed;--acc-soft:#f5f3ff}</style>
|
||||
</head>
|
||||
|
||||
Reference in New Issue
Block a user