Files
Learn_System/frontend/js/math6_anim.js
T
Maxim Dolgolyov 555f701b57 feat(math6): умножение-прыжки (Гл.4 §7) + координатный тир (Гл.5 §1)
Math6Anim.numberLineJumps — a·b как a прыжков-дуг по b на числовой прямой
(зелёные вправо, красные влево, приземление на произведение); ползунки a,b.
Math6Anim.coordGame — «поставь точку (x;y)»: клик по узлу сетки, проверка,
счёт, при промахе показывает верную точку. План: 3D-тела исключены.
Headless-safe. Тесты math6: 20/20.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 21:53:47 +03:00

443 lines
32 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* math6_anim.js — движок анимированных canvas-демонстраций «Математики 6».
* Неймспейс window.Math6Anim. Используется билдерами глав (math_6_chN.html).
*
* Принципы (как в chem7_anim.js — проверено):
* - один RAF-цикл на демо, пауза вне вьюпорта (IntersectionObserver), stop() при уходе;
* - prefers-reduced-motion → один статичный кадр вместо анимации;
* - HEADLESS (jsdom-тесты / HeadlessChrome): getContext НЕ вызывается (ctx=null),
* строится только DOM-каркас → тесты не падают;
* - тёмная тема и акценты — через CSS-переменные; ⛔ без эмодзи.
*
* Каждое демо: fn(host, opts) → { stop(), set(opts) }. Перерисовка при смене параметра — set().
*/
(function (W) {
'use strict';
if (W.Math6Anim && W.Math6Anim.__installed) return;
var D = W.document;
var M = W.Math6Anim = W.Math6Anim || {};
M.__installed = true;
var HEADLESS = (typeof navigator !== 'undefined' && /jsdom|HeadlessChrome/i.test(navigator.userAgent || ''));
function reduced() { try { return !!(W.matchMedia && W.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) { return false; } }
function now() { try { return W.performance && W.performance.now ? W.performance.now() : Date.now(); } catch (e) { return Date.now(); } }
function cssVar(name, fb) { try { var v = getComputedStyle(D.documentElement).getPropertyValue(name).trim(); return v || fb; } catch (e) { return fb; } }
function kf(x) { var s = (Math.round(x * 100) / 100).toString(); return s.replace('.', ','); }
function observeVisible(host, cb) {
if (typeof IntersectionObserver === 'undefined') { cb(true); return { disconnect: function () {} }; }
var io = new IntersectionObserver(function (es) { cb(es[0] && es[0].isIntersecting); }, { threshold: 0.01 });
io.observe(host); return io;
}
/* canvas в host; ctx=null в HEADLESS (getContext не зовём) */
function sceneCanvas(host, w, h) {
host.innerHTML = '';
var cv = D.createElement('canvas');
var dpr = HEADLESS ? 1 : (W.devicePixelRatio || 1);
cv.width = w * dpr; cv.height = h * dpr;
cv.style.width = '100%'; cv.style.maxWidth = w + 'px'; cv.style.height = 'auto';
cv.style.borderRadius = '12px'; cv.style.display = 'block'; cv.style.margin = '0 auto';
cv.style.background = 'var(--card,#fff)'; cv.style.border = '1px solid var(--border,#e2e8f0)';
host.appendChild(cv);
var ctx = null;
if (!HEADLESS) { try { ctx = cv.getContext('2d'); if (ctx) ctx.scale(dpr, dpr); } catch (e) { ctx = null; } }
return { cv: cv, ctx: ctx, w: w, h: h };
}
/* RAF-цикл; step(tSec). В HEADLESS/reduced — один кадр step(0). Возвращает {stop}. */
function loop(host, step) {
if (HEADLESS || reduced()) { try { step(0); } catch (e) {} return { stop: function () {} }; }
var raf = 0, running = true, visible = true, t0 = now();
var io = observeVisible(host, function (v) { visible = v; });
function frame() {
if (!running) return;
if (visible) { try { step((now() - t0) / 1000); } catch (e) { running = false; return; } }
raf = W.requestAnimationFrame(frame);
}
raf = W.requestAnimationFrame(frame);
return { stop: function () { running = false; try { W.cancelAnimationFrame(raf); } catch (e) {} try { io.disconnect(); } catch (e) {} } };
}
function caption(host, html) {
var c = D.createElement('div');
c.style.cssText = 'text-align:center;font-weight:700;color:var(--pri2,#3730a3);margin-top:8px;font-size:1.02rem';
c.innerHTML = html; host.appendChild(c);
if (W.renderMathInElement) try { W.renderMathInElement(c, { delimiters: [{ left: '$', right: '$', display: false }], throwOnError: false }); } catch (e) {}
return c;
}
/* ============================ ДЕМО 1: КОЛЕСО КАТИТСЯ (C = πd = 2πr) ============================ */
M.rollingCircle = function (host, opts) {
opts = opts || {}; var r = opts.r || 3;
var W0 = 520, H0 = 200;
var sc = sceneCanvas(host, W0, H0);
var cap = caption(host, '');
var PI = 3.14, period = 4.2;
function draw(tSec) {
var ctx = sc.ctx; var pri = cssVar('--pri', '#4f46e5'), acc = cssVar('--pri2', '#3730a3'), mut = cssVar('--muted', '#64748b');
if (!ctx) return;
var Rpx = 26 + r * 5; if (Rpx > 60) Rpx = 60;
var baseY = H0 - 46, startX = 34;
var p = (tSec % period) / period; /* 0..1 — один оборот */
var travel = p * 2 * Math.PI * Rpx;
var cx = startX + Rpx + travel, cy = baseY - Rpx, ang = p * 2 * Math.PI;
ctx.clearRect(0, 0, W0, H0);
/* линия земли */
ctx.strokeStyle = mut; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(20, baseY); ctx.lineTo(W0 - 20, baseY); ctx.stroke();
/* развёрнутый путь (длина окружности) — толстая лента */
ctx.strokeStyle = acc; ctx.lineWidth = 5; ctx.lineCap = 'round';
ctx.beginPath(); ctx.moveTo(startX + Rpx, baseY + 10); ctx.lineTo(startX + Rpx + travel, baseY + 10); ctx.stroke();
/* колесо */
ctx.strokeStyle = pri; ctx.lineWidth = 3; ctx.fillStyle = 'rgba(79,70,229,0.08)';
ctx.beginPath(); ctx.arc(cx, cy, Rpx, 0, 2 * Math.PI); ctx.fill(); ctx.stroke();
/* спица + метка на ободе (видно вращение) */
var mx = cx + Rpx * Math.sin(ang), my = cy + Rpx * Math.cos(ang);
ctx.strokeStyle = pri; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(mx, my); ctx.stroke();
ctx.fillStyle = '#e11d48'; ctx.beginPath(); ctx.arc(mx, my, 5, 0, 2 * Math.PI); ctx.fill();
/* центр */
ctx.fillStyle = acc; ctx.beginPath(); ctx.arc(cx, cy, 3, 0, 2 * Math.PI); ctx.fill();
/* подпись пройденного пути */
ctx.fillStyle = mut; ctx.font = '12px JetBrains Mono, monospace'; ctx.textAlign = 'center';
ctx.fillText('путь за ' + (p < 0.999 ? Math.round(p * 100) + '% оборота' : 'полный оборот'), startX + Rpx + travel / 2, baseY + 30);
}
var L = loop(host, draw);
cap.innerHTML = 'За один полный оборот колесо проходит путь, равный длине окружности: $C = 2\\pi r = \\pi d = ' + kf(2 * PI * r) + '$ (при $r=' + r + '$).';
if (W.renderMathInElement) try { W.renderMathInElement(cap, { delimiters: [{ left: '$', right: '$', display: false }], throwOnError: false }); } catch (e) {}
return { stop: L.stop, host: host };
};
/* ============================ ДЕМО 2: ЗАМЕТАНИЕ ПЛОЩАДИ КРУГА (S = πr²) ============================ */
M.sweepArea = function (host, opts) {
opts = opts || {}; var r = opts.r || 3;
var W0 = 300, H0 = 300; var sc = sceneCanvas(host, W0, H0);
var cap = caption(host, '');
var PI = 3.14, period = 3.6;
function draw(tSec) {
var ctx = sc.ctx; if (!ctx) return;
var pri = cssVar('--pri', '#4f46e5'), acc = cssVar('--pri2', '#3730a3'), mut = cssVar('--muted', '#64748b');
var cx = W0 / 2, cy = H0 / 2, Rpx = 40 + r * 12; if (Rpx > 130) Rpx = 130;
var p = (tSec % period) / period; var sweep = p * 2 * Math.PI;
ctx.clearRect(0, 0, W0, H0);
/* контур круга */
ctx.strokeStyle = pri; ctx.lineWidth = 2.5; ctx.beginPath(); ctx.arc(cx, cy, Rpx, 0, 2 * Math.PI); ctx.stroke();
/* заметённый сектор */
ctx.fillStyle = 'rgba(79,70,229,0.22)'; ctx.beginPath(); ctx.moveTo(cx, cy);
ctx.arc(cx, cy, Rpx, -Math.PI / 2, -Math.PI / 2 + sweep); ctx.closePath(); ctx.fill();
/* движущийся радиус */
var ex = cx + Rpx * Math.cos(-Math.PI / 2 + sweep), ey = cy + Rpx * Math.sin(-Math.PI / 2 + sweep);
ctx.strokeStyle = '#e11d48'; ctx.lineWidth = 2.5; ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(ex, ey); ctx.stroke();
/* подпись r */
ctx.fillStyle = acc; ctx.font = '13px JetBrains Mono, monospace'; ctx.textAlign = 'left';
ctx.fillText('r = ' + r, cx + 6, cy - 6);
ctx.fillStyle = acc; ctx.beginPath(); ctx.arc(cx, cy, 3, 0, 2 * Math.PI); ctx.fill();
}
var L = loop(host, draw);
cap.innerHTML = 'Радиус заметает круг — его площадь $S = \\pi r^2 = ' + kf(PI * r * r) + '$ (при $r=' + r + '$).';
if (W.renderMathInElement) try { W.renderMathInElement(cap, { delimiters: [{ left: '$', right: '$', display: false }], throwOnError: false }); } catch (e) {}
return { stop: L.stop, host: host };
};
/* ============================ ДЕМО 3: ПЛОЩАДНАЯ МОДЕЛЬ УМНОЖЕНИЯ (a · b) ============================ */
M.areaModel = function (host, opts) {
opts = opts || {}; var a = opts.a || 1.2, b = opts.b || 0.3;
var W0 = 360, H0 = 300; var sc = sceneCanvas(host, W0, H0);
var cap = caption(host, '');
var period = 2.8;
function draw(tSec) {
var ctx = sc.ctx; if (!ctx) return;
var pri = cssVar('--pri', '#4f46e5'), acc = cssVar('--pri2', '#3730a3'), mut = cssVar('--muted', '#64748b'), bd = cssVar('--border', '#e2e8f0');
var pad = 40, plotW = W0 - pad - 16, plotH = H0 - pad - 16;
var maxU = Math.max(3, Math.ceil(a), Math.ceil(b)); /* единиц по каждой стороне на сетке (адаптивно) */
var unit = Math.min(plotW, plotH) / maxU;
var x0 = pad, y0 = H0 - pad;
var aw = a * unit, bh = b * unit;
var p = Math.min(1, (tSec % period) / (period * 0.7)); /* заполнение 0..1, потом пауза */
ctx.clearRect(0, 0, W0, H0);
/* сетка 0,1 */
ctx.strokeStyle = bd; ctx.lineWidth = 0.7;
for (var gx = 0; gx <= maxU * 10; gx++) { var X = x0 + gx * unit / 10; if (X > x0 + maxU * unit + 0.5) break; ctx.globalAlpha = (gx % 10 === 0) ? 1 : 0.4; ctx.beginPath(); ctx.moveTo(X, y0); ctx.lineTo(X, y0 - maxU * unit); ctx.stroke(); }
for (var gy = 0; gy <= maxU * 10; gy++) { var Y = y0 - gy * unit / 10; ctx.globalAlpha = (gy % 10 === 0) ? 1 : 0.4; ctx.beginPath(); ctx.moveTo(x0, Y); ctx.lineTo(x0 + maxU * unit, Y); ctx.stroke(); }
ctx.globalAlpha = 1;
/* прямоугольник a×b, заполняется слева направо */
ctx.fillStyle = 'rgba(79,70,229,0.28)';
ctx.fillRect(x0, y0 - bh, aw * p, bh);
ctx.strokeStyle = pri; ctx.lineWidth = 2.5; ctx.strokeRect(x0, y0 - bh, aw, bh);
/* оси-подписи */
ctx.fillStyle = acc; ctx.font = '13px JetBrains Mono, monospace';
ctx.textAlign = 'center'; ctx.fillText('a = ' + kf(a), x0 + aw / 2, y0 + 22);
ctx.save(); ctx.translate(x0 - 14, y0 - bh / 2); ctx.rotate(-Math.PI / 2); ctx.fillText('b = ' + kf(b), 0, 0); ctx.restore();
}
var L = loop(host, draw);
var prod = Math.round(a * b * 1e6) / 1e6;
cap.innerHTML = 'Площадь прямоугольника = произведению сторон: $' + kf(a) + ' \\cdot ' + kf(b) + ' = ' + kf(prod) + '$.';
if (W.renderMathInElement) try { W.renderMathInElement(cap, { delimiters: [{ left: '$', right: '$', display: false }], throwOnError: false }); } catch (e) {}
return { stop: L.stop, host: host };
};
/* ============================ ДЕМО 4: ПРЫЖКИ ПО ЧИСЛОВОЙ ПРЯМОЙ (a + b) ============================ */
M.numberLineWalk = function (host, opts) {
opts = opts || {}; var a = opts.a != null ? opts.a : 3, b = opts.b != null ? opts.b : -5;
var sum = a + b, W0 = 540, H0 = 150;
var sc = sceneCanvas(host, W0, H0); var cap = caption(host, '');
var min = Math.min(-3, 0, a, sum) - 1, max = Math.max(3, 0, a, sum) + 1, period = 4.2;
function X(v) { var pad = 30; return pad + (v - min) / (max - min) * (W0 - 2 * pad); }
function arrow(ctx, x1, x2, y, col) {
ctx.strokeStyle = col; ctx.fillStyle = col; ctx.lineWidth = 3; ctx.lineCap = 'round';
ctx.beginPath(); ctx.moveTo(x1, y); ctx.lineTo(x2, y); ctx.stroke();
var d = x2 >= x1 ? 1 : -1; ctx.beginPath(); ctx.moveTo(x2, y); ctx.lineTo(x2 - d * 9, y - 5); ctx.lineTo(x2 - d * 9, y + 5); ctx.closePath(); ctx.fill();
}
function draw(t) {
var ctx = sc.ctx; if (!ctx) return; var mut = cssVar('--muted', '#64748b'), acc = cssVar('--pri2', '#3730a3');
var axisY = H0 - 50; ctx.clearRect(0, 0, W0, H0);
ctx.strokeStyle = mut; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(15, axisY); ctx.lineTo(W0 - 15, axisY); ctx.stroke();
ctx.font = '11px JetBrains Mono, monospace'; ctx.textAlign = 'center';
for (var v = Math.ceil(min); v <= Math.floor(max); v++) { var x = X(v); ctx.strokeStyle = mut; ctx.beginPath(); ctx.moveTo(x, axisY - 5); ctx.lineTo(x, axisY + 5); ctx.stroke(); ctx.fillStyle = (v === 0 ? acc : mut); ctx.fillText(v, x, axisY + 20); }
var p = (t % period) / period, p1 = Math.min(1, p / 0.4), p2 = Math.max(0, Math.min(1, (p - 0.45) / 0.4));
arrow(ctx, X(0), X(0) + (X(a) - X(0)) * p1, axisY - 14, a >= 0 ? '#059669' : '#e11d48');
if (p2 > 0) arrow(ctx, X(a), X(a) + (X(sum) - X(a)) * p2, axisY - 30, b >= 0 ? '#059669' : '#e11d48');
if (p > 0.88) { ctx.fillStyle = '#e11d48'; ctx.beginPath(); ctx.arc(X(sum), axisY, 6, 0, 2 * Math.PI); ctx.fill(); ctx.fillStyle = acc; ctx.font = '14px Unbounded, sans-serif'; ctx.fillText('' + sum, X(sum), axisY - 42); }
}
var L = loop(host, draw);
cap.innerHTML = '$' + a + ' + (' + b + ') = ' + sum + '$ — от нуля шагаем ' + (a >= 0 ? 'вправо' : 'влево') + ' на ' + Math.abs(a) + ', затем ' + (b >= 0 ? 'вправо' : 'влево') + ' на ' + Math.abs(b) + '.';
if (W.renderMathInElement) try { W.renderMathInElement(cap, { delimiters: [{ left: '$', right: '$', display: false }], throwOnError: false }); } catch (e) {}
return { stop: L.stop };
};
/* ============================ ДЕМО 5: МАШИНКА + ГРАФИК «ПУТЬ–ВРЕМЯ» ============================ */
M.carGraph = function (host, opts) {
var W0 = 460, H0 = 330; var sc = sceneCanvas(host, W0, H0); var cap = caption(host, '');
var J = [{ t: 0, s: 0 }, { t: 1, s: 40 }, { t: 2, s: 80 }, { t: 3, s: 80 }, { t: 4, s: 120 }, { t: 5, s: 160 }];
var Tmax = 5, Smax = 160, period = 7;
function sAt(tt) { for (var i = 0; i < J.length - 1; i++) { if (tt >= J[i].t && tt <= J[i + 1].t) { var f = (tt - J[i].t) / ((J[i + 1].t - J[i].t) || 1); return J[i].s + f * (J[i + 1].s - J[i].s); } } return J[J.length - 1].s; }
function draw(t) {
var ctx = sc.ctx; if (!ctx) return;
var pri = cssVar('--pri', '#059669'), mut = cssVar('--muted', '#64748b'), bd = cssVar('--border', '#e2e8f0');
var cur = (t % period) / period * Tmax;
ctx.clearRect(0, 0, W0, H0);
var roadY = 52, rx0 = 24, rx1 = W0 - 24;
ctx.strokeStyle = bd; ctx.lineWidth = 8; ctx.lineCap = 'round'; ctx.beginPath(); ctx.moveTo(rx0, roadY); ctx.lineTo(rx1, roadY); ctx.stroke();
var carX = rx0 + (sAt(cur) / Smax) * (rx1 - rx0);
ctx.fillStyle = pri; ctx.fillRect(carX - 13, roadY - 11, 26, 14);
ctx.fillStyle = '#1e293b'; ctx.beginPath(); ctx.arc(carX - 7, roadY + 4, 4, 0, 6.3); ctx.arc(carX + 7, roadY + 4, 4, 0, 6.3); ctx.fill();
ctx.fillStyle = mut; ctx.font = '13px Inter, sans-serif'; ctx.textAlign = 'center';
ctx.fillText('время: ' + (Math.round(cur * 10) / 10) + ' ч · путь: ' + Math.round(sAt(cur)) + ' км', W0 / 2, 24);
var gx0 = 46, gy0 = H0 - 28, gw = W0 - 70, gh = H0 - 120;
function GX(tt) { return gx0 + (tt / Tmax) * gw; } function GY(ss) { return gy0 - (ss / Smax) * gh; }
ctx.strokeStyle = mut; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(gx0, gy0 - gh); ctx.lineTo(gx0, gy0); ctx.lineTo(gx0 + gw, gy0); ctx.stroke();
ctx.fillStyle = mut; ctx.font = '11px Inter, sans-serif'; ctx.textAlign = 'center'; ctx.fillText('t, ч', gx0 + gw, gy0 + 16); ctx.textAlign = 'left'; ctx.fillText('s, км', gx0 - 38, gy0 - gh + 4);
ctx.strokeStyle = bd; ctx.lineWidth = 1.5; ctx.beginPath(); J.forEach(function (pt, i) { var x = GX(pt.t), y = GY(pt.s); if (i) ctx.lineTo(x, y); else ctx.moveTo(x, y); }); ctx.stroke();
ctx.strokeStyle = pri; ctx.lineWidth = 3; ctx.lineCap = 'round'; ctx.beginPath(); ctx.moveTo(GX(0), GY(0));
for (var tt = 0; tt <= cur; tt += 0.04) ctx.lineTo(GX(tt), GY(sAt(tt)));
ctx.stroke();
ctx.fillStyle = '#e11d48'; ctx.beginPath(); ctx.arc(GX(cur), GY(sAt(cur)), 5, 0, 2 * Math.PI); ctx.fill();
}
var L = loop(host, draw);
cap.innerHTML = 'Машина едет — график «путь–время» вычерчивается сам. Где линия <b>горизонтальна</b> (с 2 до 3 ч) — машина <b>стоит</b>: время идёт, а путь не растёт.';
return { stop: L.stop };
};
/* ============================ ДЕМО 6: ЖИВОЙ ГРАФИК y=kx / y=k/x ============================ */
M.plotLive = function (host, opts) {
opts = opts || {};
var W0 = 360, H0 = 360; var sc = sceneCanvas(host, W0, H0);
var st = { k: opts.k != null ? opts.k : 2, target: opts.k != null ? opts.k : 2, mode: opts.mode || 'kx' };
var XMIN = -6, XMAX = 6, YMIN = -6, YMAX = 6, pad = 24;
function X(x) { return pad + (x - XMIN) / (XMAX - XMIN) * (W0 - 2 * pad); }
function Y(y) { return H0 - pad - (y - YMIN) / (YMAX - YMIN) * (H0 - 2 * pad); }
function draw() {
var ctx = sc.ctx; if (!ctx) return;
var pri = cssVar('--pri', '#059669'), acc = cssVar('--pri2', '#047857'), bd = cssVar('--border', '#e2e8f0'), axc = cssVar('--text', '#0f172a');
st.k += (st.target - st.k) * 0.12;
ctx.clearRect(0, 0, W0, H0);
ctx.strokeStyle = bd; ctx.lineWidth = 0.8;
for (var gx = XMIN; gx <= XMAX; gx++) { if (gx === 0) continue; ctx.beginPath(); ctx.moveTo(X(gx), Y(YMIN)); ctx.lineTo(X(gx), Y(YMAX)); ctx.stroke(); }
for (var gy = YMIN; gy <= YMAX; gy++) { if (gy === 0) continue; ctx.beginPath(); ctx.moveTo(X(XMIN), Y(gy)); ctx.lineTo(X(XMAX), Y(gy)); ctx.stroke(); }
ctx.strokeStyle = axc; ctx.lineWidth = 1.6; ctx.beginPath(); ctx.moveTo(X(XMIN), Y(0)); ctx.lineTo(X(XMAX), Y(0)); ctx.moveTo(X(0), Y(YMIN)); ctx.lineTo(X(0), Y(YMAX)); ctx.stroke();
ctx.fillStyle = axc; ctx.font = 'italic 13px serif'; ctx.textAlign = 'left'; ctx.fillText('x', X(XMAX) - 4, Y(0) + 16); ctx.fillText('y', X(0) + 8, Y(YMAX) + 12);
ctx.strokeStyle = pri; ctx.lineWidth = 3; ctx.lineJoin = 'round'; ctx.lineCap = 'round';
if (st.mode === 'kx') {
ctx.beginPath(); var started = false;
for (var x = XMIN; x <= XMAX; x += 0.06) { var y = st.k * x; if (y < YMIN - 1 || y > YMAX + 1) { started = false; continue; } if (started) ctx.lineTo(X(x), Y(y)); else { ctx.moveTo(X(x), Y(y)); started = true; } }
ctx.stroke();
} else {
[[XMIN, -0.12], [0.12, XMAX]].forEach(function (seg) {
ctx.beginPath(); var s2 = false;
for (var x2 = seg[0]; x2 <= seg[1]; x2 += 0.04) { if (Math.abs(x2) < 0.08) continue; var y2 = st.k / x2; if (y2 < YMIN - 1 || y2 > YMAX + 1) { s2 = false; continue; } if (s2) ctx.lineTo(X(x2), Y(y2)); else { ctx.moveTo(X(x2), Y(y2)); s2 = true; } }
ctx.stroke();
});
}
ctx.fillStyle = acc; ctx.font = 'bold 16px Inter, sans-serif'; ctx.textAlign = 'left';
var kk = Math.round(st.k * 10) / 10;
ctx.fillText(st.mode === 'kx' ? ('y = ' + kf(kk) + ' · x') : ('y = ' + kf(kk) + ' / x'), pad + 4, pad + 10);
}
var L = loop(host, draw);
return {
stop: L.stop,
setK: function (v) { st.target = v; if (st.mode === 'kdx' && Math.abs(v) < 0.5) st.target = (v < 0 ? -0.5 : 0.5); },
setMode: function (m) { st.mode = m; if (m === 'kdx' && Math.abs(st.target) < 0.5) st.target = 0.5; }
};
};
/* ============================ ДЕМО 7: ТЕРМОМЕТР (±числа, модуль) ============================ */
M.thermometer = function (host, opts) {
opts = opts || {}; var W0 = 220, H0 = 320; var sc = sceneCanvas(host, W0, H0); var cap = caption(host, '');
var st = { v: opts.value != null ? opts.value : 5, target: opts.value != null ? opts.value : 5 };
var MIN = -10, MAX = 10, cx = 64, top = 26, bot = H0 - 56, bulbR = 18;
function Y(v) { return bot - (v - MIN) / (MAX - MIN) * (bot - top); }
function draw() {
var ctx = sc.ctx; if (!ctx) return; st.v += (st.target - st.v) * 0.15;
var mut = cssVar('--muted', '#64748b'), txt = cssVar('--text', '#0f172a');
ctx.clearRect(0, 0, W0, H0);
ctx.strokeStyle = mut; ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(cx - 9, top); ctx.arc(cx, top, 9, Math.PI, 0); ctx.lineTo(cx + 9, bot); ctx.moveTo(cx - 9, top); ctx.lineTo(cx - 9, bot); ctx.stroke();
ctx.beginPath(); ctx.arc(cx, bot + bulbR - 4, bulbR, 0, 2 * Math.PI); ctx.fillStyle = '#fee2e2'; ctx.fill(); ctx.strokeStyle = mut; ctx.stroke();
ctx.font = '11px JetBrains Mono, monospace'; ctx.textAlign = 'left';
for (var v = MIN; v <= MAX; v += 2) { var y = Y(v); ctx.strokeStyle = mut; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(cx + 12, y); ctx.lineTo(cx + 18, y); ctx.stroke(); ctx.fillStyle = (v === 0 ? txt : mut); ctx.fillText(v + '°', cx + 22, y + 4); }
var col = st.v >= 0 ? '#dc2626' : '#2563eb', y0 = Y(0), yv = Y(st.v);
ctx.fillStyle = col; ctx.fillRect(cx - 5, Math.min(y0, yv), 10, Math.abs(yv - y0));
ctx.beginPath(); ctx.arc(cx, bot + bulbR - 4, bulbR - 3, 0, 2 * Math.PI); ctx.fill();
ctx.beginPath(); ctx.arc(cx, yv, 4, 0, 2 * Math.PI); ctx.fill();
ctx.fillStyle = txt; ctx.font = 'bold 17px Inter, sans-serif'; ctx.textAlign = 'center'; ctx.fillText(Math.round(st.v) + '°', cx, H0 - 14);
}
var L = loop(host, draw);
function setCap(v) { if (!cap) return; cap.innerHTML = '$' + v + '°$ — это ' + (v > 0 ? 'тепло, выше нуля' : (v < 0 ? 'мороз, ниже нуля' : 'ровно ноль')) + '. Модуль $|' + v + '| = ' + Math.abs(v) + '$ — это расстояние до нуля.'; if (W.renderMathInElement) try { W.renderMathInElement(cap, { delimiters: [{ left: '$', right: '$', display: false }], throwOnError: false }); } catch (e) {} }
setCap(st.target);
return { stop: L.stop, set: function (v) { st.target = v; setCap(v); } };
};
function _riA(a, b) { return a + Math.floor(Math.random() * (b - a + 1)); }
/* ============================ ДЕМО 8: УМНОЖЕНИЕ КАК ПРЫЖКИ (a · b) ============================ */
M.numberLineJumps = function (host, opts) {
opts = opts || {}; var a = opts.a != null ? opts.a : 3, b = opts.b != null ? opts.b : -2; // a прыжков по b
var prod = a * b, W0 = 540, H0 = 150; var sc = sceneCanvas(host, W0, H0); var cap = caption(host, '');
var min = Math.min(-2, 0, prod) - 1, max = Math.max(2, 0, prod) + 1, period = Math.max(3.5, a * 0.8 + 1.6);
function X(v) { var pad = 30; return pad + (v - min) / (max - min) * (W0 - 2 * pad); }
function hop(ctx, x1, x2, baseY, col) {
var mx = (x1 + x2) / 2; ctx.strokeStyle = col; ctx.lineWidth = 2.5; ctx.beginPath(); ctx.moveTo(x1, baseY); ctx.quadraticCurveTo(mx, baseY - 24, x2, baseY); ctx.stroke();
var d = x2 >= x1 ? 1 : -1; ctx.fillStyle = col; ctx.beginPath(); ctx.moveTo(x2, baseY); ctx.lineTo(x2 - d * 7, baseY - 6); ctx.lineTo(x2 - d * 7, baseY + 2); ctx.closePath(); ctx.fill();
}
function draw(t) {
var ctx = sc.ctx; if (!ctx) return; var mut = cssVar('--muted', '#64748b'), acc = cssVar('--pri2', '#3730a3');
var axisY = H0 - 46; ctx.clearRect(0, 0, W0, H0);
ctx.strokeStyle = mut; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(15, axisY); ctx.lineTo(W0 - 15, axisY); ctx.stroke();
ctx.font = '11px JetBrains Mono, monospace'; ctx.textAlign = 'center';
for (var v = Math.ceil(min); v <= Math.floor(max); v++) { var x = X(v); ctx.strokeStyle = mut; ctx.beginPath(); ctx.moveTo(x, axisY - 5); ctx.lineTo(x, axisY + 5); ctx.stroke(); ctx.fillStyle = (v === 0 ? acc : mut); ctx.fillText(v, x, axisY + 20); }
var p = (t % period) / period, prog = p * (a + 0.5), doneJumps = Math.min(a, Math.floor(prog)), partial = Math.min(1, prog - doneJumps);
var col = b >= 0 ? '#059669' : '#e11d48', prev = 0;
for (var j = 0; j < doneJumps; j++) { hop(ctx, X(prev), X(prev + b), axisY, col); prev += b; }
if (doneJumps < a) { hop(ctx, X(prev), X(prev + b * partial), axisY, col); }
var pos = doneJumps >= a ? prod : prev;
ctx.fillStyle = '#e11d48'; ctx.beginPath(); ctx.arc(X(pos), axisY, 5, 0, 2 * Math.PI); ctx.fill();
if (doneJumps >= a) { ctx.fillStyle = acc; ctx.font = '14px Unbounded, sans-serif'; ctx.fillText('' + prod, X(prod), axisY - 30); }
}
var L = loop(host, draw);
var bp = b < 0 ? '(' + b + ')' : '' + b;
cap.innerHTML = '$' + a + ' \\cdot ' + bp + ' = ' + prod + '$ — это ' + a + ' ' + (a === 1 ? 'прыжок' : (a < 5 ? 'прыжка' : 'прыжков')) + ' по ' + b + ' от нуля.';
if (W.renderMathInElement) try { W.renderMathInElement(cap, { delimiters: [{ left: '$', right: '$', display: false }], throwOnError: false }); } catch (e) {}
return { stop: L.stop };
};
/* ============================ ДЕМО 9: КООРДИНАТНЫЙ ТИР («поставь точку») ============================ */
M.coordGame = function (host, opts) {
var W0 = 320, H0 = 320; var sc = sceneCanvas(host, W0, H0);
var ui = D.createElement('div'); ui.style.cssText = 'text-align:center;margin-top:8px';
ui.innerHTML = '<div class="qbox" id="cg-q" style="margin-bottom:6px"></div><div style="color:var(--muted);font-size:.9rem">Очки: <b id="cg-s">0</b> · кликни по узлу сетки</div>';
host.appendChild(ui);
var XMIN = -5, XMAX = 5, YMIN = -5, YMAX = 5, pad = 24;
function X(x) { return pad + (x - XMIN) / (XMAX - XMIN) * (W0 - 2 * pad); }
function Y(y) { return H0 - pad - (y - YMIN) / (YMAX - YMIN) * (H0 - 2 * pad); }
var st = { tx: 2, ty: 3, score: 0, placed: null, ok: false, reveal: 0 };
function setQ() { var q = ui.querySelector('#cg-q'); if (q) { q.innerHTML = 'Поставь точку $(' + st.tx + ';\\,' + st.ty + ')$'; if (W.renderMathInElement) try { W.renderMathInElement(q, { delimiters: [{ left: '$', right: '$', display: false }], throwOnError: false }); } catch (e) {} } }
function newTarget() { st.tx = _riA(-5, 5); st.ty = _riA(-5, 5); st.placed = null; st.reveal = 0; setQ(); }
function draw() {
var ctx = sc.ctx; if (!ctx) return; var bd = cssVar('--border', '#e2e8f0'), axc = cssVar('--text', '#0f172a'), mut = cssVar('--muted', '#64748b');
ctx.clearRect(0, 0, W0, H0);
ctx.strokeStyle = bd; ctx.lineWidth = 0.8;
for (var gx = XMIN; gx <= XMAX; gx++) { ctx.beginPath(); ctx.moveTo(X(gx), Y(YMIN)); ctx.lineTo(X(gx), Y(YMAX)); ctx.stroke(); }
for (var gy = YMIN; gy <= YMAX; gy++) { ctx.beginPath(); ctx.moveTo(X(XMIN), Y(gy)); ctx.lineTo(X(XMAX), Y(gy)); ctx.stroke(); }
ctx.strokeStyle = axc; ctx.lineWidth = 1.6; ctx.beginPath(); ctx.moveTo(X(XMIN), Y(0)); ctx.lineTo(X(XMAX), Y(0)); ctx.moveTo(X(0), Y(YMIN)); ctx.lineTo(X(0), Y(YMAX)); ctx.stroke();
ctx.fillStyle = axc; ctx.font = 'italic 12px serif'; ctx.fillText('x', X(XMAX) - 2, Y(0) + 14); ctx.fillText('y', X(0) + 7, Y(YMAX) + 11);
if (st.reveal > 0) { ctx.fillStyle = '#059669'; ctx.beginPath(); ctx.arc(X(st.tx), Y(st.ty), 7, 0, 2 * Math.PI); ctx.fill(); }
if (st.placed) { ctx.fillStyle = st.ok ? '#059669' : '#e11d48'; ctx.beginPath(); ctx.arc(X(st.placed.x), Y(st.placed.y), 6, 0, 2 * Math.PI); ctx.fill(); }
}
var L = loop(host, draw);
if (!HEADLESS && sc.ctx) {
sc.cv.style.cursor = 'crosshair';
sc.cv.addEventListener('pointerdown', function (e) {
var r = sc.cv.getBoundingClientRect();
var dx = Math.round(XMIN + (e.clientX - r.left) / r.width * (XMAX - XMIN));
var dy = Math.round(YMIN + (r.height - (e.clientY - r.top)) / r.height * (YMAX - YMIN));
dx = Math.max(XMIN, Math.min(XMAX, dx)); dy = Math.max(YMIN, Math.min(YMAX, dy));
st.placed = { x: dx, y: dy }; st.ok = (dx === st.tx && dy === st.ty);
if (st.ok) { st.score++; var s = ui.querySelector('#cg-s'); if (s) s.textContent = st.score; setTimeout(newTarget, 700); }
else { st.reveal = 1; setTimeout(function () { st.reveal = 0; st.placed = null; }, 1100); }
});
}
setQ();
return { stop: L.stop };
};
/* ============================ КОМПОНЕНТ: ПОШАГОВЫЙ ПЛЕЕР (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);