feat(math6): полоса процента (Гл.2 §1) + фильтр множества (Гл.3 §1)

Math6Anim.barModel — полоса 0..100%, заполняется (easing) к проценту,
синхронно %↔десятичная↔дробь; вшита в §2.1 на тот же ползунок, что и сетка 100.
Math6Anim.setFilter — числа 1..12 по очереди проходят сквозь «фильтр свойства»
(чётные/кратные 3/больше 6), подходящие падают в множество; кнопки смены свойства;
вшита в §3.1. Теперь во ВСЕХ 6 главах есть canvas-анимации + stepPlayer везде.
Headless-safe. Тесты 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 22:00:57 +03:00
parent 97966ba2df
commit 302b062649
4 changed files with 78 additions and 3 deletions
+55
View File
@@ -415,6 +415,61 @@ M.reflectFold = function (host, opts) {
return { stop: L.stop };
};
/* ============================ ДЕМО 11: ПОЛОСА ПРОЦЕНТА (% ↔ дробь ↔ десятичная) ============================ */
M.barModel = function (host, opts) {
opts = opts || {}; var p = opts.percent != null ? opts.percent : 35;
var W0 = 460, H0 = 130; var sc = sceneCanvas(host, W0, H0); var cap = caption(host, '');
var st = { p: p, target: p }; var pad = 30, barY = 30, barH = 42, x0 = pad, x1 = W0 - pad;
function draw() {
var ctx = sc.ctx; if (!ctx) return; st.p += (st.target - st.p) * 0.18;
var pri = cssVar('--pri', '#0891b2'), acc = cssVar('--pri2', '#0e7490'), mut = cssVar('--muted', '#64748b'), bd = cssVar('--border', '#e2e8f0');
ctx.clearRect(0, 0, W0, H0);
ctx.fillStyle = bd; ctx.fillRect(x0, barY, x1 - x0, barH);
var w = (x1 - x0) * Math.max(0, Math.min(100, st.p)) / 100;
ctx.fillStyle = pri; ctx.fillRect(x0, barY, w, barH);
ctx.strokeStyle = acc; ctx.lineWidth = 2; ctx.strokeRect(x0, barY, x1 - x0, barH);
ctx.font = '11px JetBrains Mono, monospace'; ctx.textAlign = 'center';
[0, 25, 50, 75, 100].forEach(function (tk) { var x = x0 + (x1 - x0) * tk / 100; ctx.strokeStyle = mut; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(x, barY + barH); ctx.lineTo(x, barY + barH + 6); ctx.stroke(); ctx.fillStyle = mut; ctx.fillText(tk + '%', x, barY + barH + 20); });
var pr = Math.round(st.p);
if (w > 36) { ctx.fillStyle = '#fff'; ctx.font = 'bold 15px Inter, sans-serif'; ctx.fillText(pr + '%', x0 + w / 2, barY + barH / 2 + 5); }
}
var L = loop(host, draw);
function gcd(a, b) { return b ? gcd(b, a % b) : a; }
function setCap(v) { var k = gcd(v, 100) || 1; cap.innerHTML = '$' + v + '\\% = ' + (v / 100).toString().replace('.', '{,}') + ' = \\dfrac{' + (v / k) + '}{' + (100 / k) + '}$'; if (W.renderMathInElement) try { W.renderMathInElement(cap, { delimiters: [{ left: '$', right: '$', display: false }], throwOnError: false }); } catch (e) {} }
setCap(p);
return { stop: L.stop, set: function (v) { st.target = v; setCap(v); } };
};
/* ============================ ДЕМО 12: ФИЛЬТР МНОЖЕСТВА (числа сквозь свойство) ============================ */
M.setFilter = function (host, opts) {
opts = opts || {}; var W0 = 420, H0 = 300; var sc = sceneCanvas(host, W0, H0); var cap = caption(host, '');
var st = { label: opts.label || 'чётные', test: opts.test || function (n) { return n % 2 === 0; }, nums: opts.nums || [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] };
var filterY = 132, boxY = 200, idx = 0, cur = null, startT = 0, period = 1.0, collected = [];
function reset() { idx = 0; cur = null; collected = []; startT = 0; }
function draw(t) {
var ctx = sc.ctx; if (!ctx) return;
var pri = cssVar('--pri', '#7c3aed'), acc = cssVar('--pri2', '#6d28d9'), mut = cssVar('--muted', '#64748b');
ctx.clearRect(0, 0, W0, H0);
ctx.fillStyle = 'rgba(124,58,237,0.12)'; ctx.fillRect(40, filterY, W0 - 80, 16);
ctx.strokeStyle = pri; ctx.lineWidth = 2; ctx.strokeRect(40, filterY, W0 - 80, 16);
ctx.fillStyle = acc; ctx.font = 'bold 13px Inter, sans-serif'; ctx.textAlign = 'center'; ctx.fillText('Фильтр: ' + st.label, W0 / 2, filterY - 8);
ctx.strokeStyle = mut; ctx.setLineDash([5, 4]); ctx.strokeRect(40, boxY, W0 - 80, 64); ctx.setLineDash([]);
ctx.fillStyle = mut; ctx.font = '14px JetBrains Mono, monospace'; ctx.textAlign = 'left'; ctx.fillText('{ ' + collected.join('; ') + ' }', 52, boxY + 38);
ctx.textAlign = 'center';
for (var q = idx; q < st.nums.length; q++) { var x = 70 + (q - idx) * 28; if (x > W0 - 40) break; ctx.fillStyle = mut; ctx.font = '14px JetBrains Mono, monospace'; ctx.fillText(st.nums[q], x, 36); }
if (cur != null) {
var p = Math.min(1, (t - startT) / period), matched = st.test(cur);
var toY = matched ? boxY + 38 : filterY - 4, y = 36 + (toY - 36) * p, col = matched ? '#059669' : '#e11d48';
ctx.fillStyle = col; ctx.font = 'bold 19px Inter, sans-serif'; ctx.fillText(cur, W0 / 2, y);
if (p >= 1) { if (matched && collected.indexOf(cur) < 0) collected.push(cur); cur = null; startT = t + 0.25; }
} else if (idx < st.nums.length) { if (t - startT > 0.25) { cur = st.nums[idx]; idx++; startT = t; } }
else if (t - startT > 1.4) { reset(); startT = t; }
}
var L = loop(host, draw);
cap.innerHTML = 'Числа по очереди проходят через фильтр. Подходящие падают в множество, остальные — отсеиваются.';
return { stop: L.stop, set: function (label, test) { st.label = label; st.test = test; reset(); } };
};
/* ============================ КОМПОНЕНТ: ПОШАГОВЫЙ ПЛЕЕР (DOM, не canvas) ============================ */
M.stepPlayer = function (host, opts) {
opts = opts || {}; var steps = opts.steps || []; if (!steps.length) return { stop: function () {} };