51db000545
Math6Anim.pieGrow (растущие сектора, §7 — заменил статичный Math6.pie, цвета синхронны легенде), balanceScale (весы a·d ? b·c, §3, кнопка «другой пример»), constAreaRect (обратная проп. = постоянная площадь, §4, ползунок x). Headless-safe. Тесты math6: 20/20 (поправлен ассерт §7 svg→canvas). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
605 lines
46 KiB
JavaScript
605 lines
46 KiB
JavaScript
/* 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 };
|
||
};
|
||
|
||
/* ============================ ДЕМО 10: СИММЕТРИЯ (осевая / центральная) ============================ */
|
||
M.reflectFold = function (host, opts) {
|
||
opts = opts || {}; var mode = opts.mode || 'axial';
|
||
var W0 = 340, H0 = 340; var sc = sceneCanvas(host, W0, H0); var cap = caption(host, '');
|
||
var XMIN = -6, XMAX = 6, YMIN = -6, YMAX = 6, pad = 22, period = 4;
|
||
var fig = [{ x: 1, y: 1 }, { x: 4, y: 1 }, { x: 2, y: 4 }];
|
||
function img(p) { return mode === 'central' ? { x: -p.x, y: -p.y } : { x: -p.x, y: p.y }; }
|
||
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 poly(ctx, pts, fill, stroke, dash) {
|
||
ctx.beginPath(); pts.forEach(function (p, i) { var x = X(p.x), y = Y(p.y); if (i) ctx.lineTo(x, y); else ctx.moveTo(x, y); }); ctx.closePath();
|
||
ctx.setLineDash(dash || []); if (fill) { ctx.fillStyle = fill; ctx.fill(); } ctx.strokeStyle = stroke; ctx.lineWidth = 2; ctx.stroke(); ctx.setLineDash([]);
|
||
}
|
||
function draw(t) {
|
||
var ctx = sc.ctx; if (!ctx) return; var bd = cssVar('--border', '#e2e8f0'), axc = cssVar('--text', '#0f172a');
|
||
ctx.clearRect(0, 0, W0, H0);
|
||
ctx.strokeStyle = bd; ctx.lineWidth = 0.7;
|
||
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.4; 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();
|
||
if (mode === 'axial') { ctx.strokeStyle = '#e11d48'; ctx.lineWidth = 2.5; ctx.setLineDash([6, 4]); ctx.beginPath(); ctx.moveTo(X(0), Y(YMIN)); ctx.lineTo(X(0), Y(YMAX)); ctx.stroke(); ctx.setLineDash([]); }
|
||
else { ctx.fillStyle = '#e11d48'; ctx.beginPath(); ctx.arc(X(0), Y(0), 5, 0, 2 * Math.PI); ctx.fill(); ctx.font = '12px JetBrains Mono, monospace'; ctx.fillText('O', X(0) + 8, Y(0) - 8); }
|
||
var imgPts = fig.map(img);
|
||
poly(ctx, imgPts, 'rgba(225,29,72,0.06)', 'rgba(225,29,72,0.5)', [5, 4]);
|
||
poly(ctx, fig, 'rgba(37,99,235,0.12)', '#2563eb', null);
|
||
var p = (t % period) / period, e = p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2;
|
||
var ghost = fig.map(function (pt, i) { var ip = imgPts[i]; return { x: pt.x + (ip.x - pt.x) * e, y: pt.y + (ip.y - pt.y) * e }; });
|
||
poly(ctx, ghost, 'rgba(217,119,6,0.32)', '#d97706', null);
|
||
}
|
||
var L = loop(host, draw);
|
||
cap.innerHTML = mode === 'central'
|
||
? 'Центральная симметрия: точка $(x;\\,y)$ переходит в $(-x;\\,-y)$ — поворот на $180°$ вокруг центра $O$.'
|
||
: 'Осевая симметрия: точка $(x;\\,y)$ переходит в $(-x;\\,y)$ — отражение через ось $Oy$, как складывание листа по оси.';
|
||
if (W.renderMathInElement) try { W.renderMathInElement(cap, { delimiters: [{ left: '$', right: '$', display: false }], throwOnError: false }); } catch (e) {}
|
||
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(); } };
|
||
};
|
||
|
||
/* ============================ ДЕМО 13: РАСТУЩАЯ КРУГОВАЯ ДИАГРАММА ============================ */
|
||
M.pieGrow = function (host, opts) {
|
||
opts = opts || {}; var W0 = 240, H0 = 240; var sc = sceneCanvas(host, W0, H0); var cap = caption(host, '');
|
||
var pal = ['#0891b2', '#f59e0b', '#e11d48', '#059669', '#7c3aed'];
|
||
var st = { segs: opts.segs || [{ label: 'A', value: 1 }], t0: null }; var period = 3.4;
|
||
function draw(t) {
|
||
var ctx = sc.ctx; if (!ctx) return; if (st.t0 === null) st.t0 = t; var p = Math.min(1, ((t - st.t0) % period) / (period * 0.7));
|
||
var cx = W0 / 2, cy = H0 / 2, r = W0 / 2 - 12, total = st.segs.reduce(function (a, s) { return a + s.value; }, 0) || 1, sweep = -Math.PI / 2 + p * 2 * Math.PI;
|
||
ctx.clearRect(0, 0, W0, H0); var ang = -Math.PI / 2;
|
||
st.segs.forEach(function (s, i) {
|
||
var frac = s.value / total, a0 = ang, a1 = ang + frac * 2 * Math.PI, drawTo = Math.min(a1, sweep);
|
||
if (drawTo > a0) {
|
||
ctx.fillStyle = s.color || pal[i % pal.length]; ctx.beginPath(); ctx.moveTo(cx, cy); ctx.arc(cx, cy, r, a0, drawTo); ctx.closePath(); ctx.fill();
|
||
ctx.strokeStyle = cssVar('--card', '#fff'); ctx.lineWidth = 1.5; ctx.stroke();
|
||
if (sweep >= a1 && frac > 0.05) { var mid = a0 + frac * Math.PI, lx = cx + r * 0.6 * Math.cos(mid), ly = cy + r * 0.6 * Math.sin(mid); ctx.fillStyle = '#fff'; ctx.font = 'bold 12px Inter, sans-serif'; ctx.textAlign = 'center'; ctx.fillText(Math.round(frac * 100) + '%', lx, ly + 4); }
|
||
}
|
||
ang = a1;
|
||
});
|
||
}
|
||
var L = loop(host, draw);
|
||
return { stop: L.stop, set: function (segs) { st.segs = segs; st.t0 = null; } };
|
||
};
|
||
|
||
/* ============================ ДЕМО 14: ВЕСЫ ПРОПОРЦИИ (a·d ? b·c) ============================ */
|
||
M.balanceScale = function (host, opts) {
|
||
opts = opts || {}; var W0 = 380, H0 = 230; var sc = sceneCanvas(host, W0, H0); var cap = caption(host, '');
|
||
var st = { a: 3, b: 4, c: 6, d: 8, ang: 0, target: 0 }; if (opts.a != null) { st.a = opts.a; st.b = opts.b; st.c = opts.c; st.d = opts.d; }
|
||
function info() { var L = st.a * st.d, R = st.b * st.c; return { L: L, R: R, eq: L === R }; }
|
||
function setTarget() { var f = info(); st.target = Math.max(-0.26, Math.min(0.26, (f.R - f.L) * 0.02)); }
|
||
setTarget();
|
||
function draw() {
|
||
var ctx = sc.ctx; if (!ctx) return; st.ang += (st.target - st.ang) * 0.15;
|
||
var mut = cssVar('--muted', '#64748b'), pri = cssVar('--pri', '#0891b2'); var f = info();
|
||
ctx.clearRect(0, 0, W0, H0); var cx = W0 / 2, pivotY = 64, beamLen = 128;
|
||
ctx.strokeStyle = mut; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(cx, pivotY); ctx.lineTo(cx, H0 - 26); ctx.stroke();
|
||
ctx.beginPath(); ctx.moveTo(cx - 40, H0 - 26); ctx.lineTo(cx + 40, H0 - 26); ctx.stroke();
|
||
var dx = Math.cos(st.ang) * beamLen, dy = Math.sin(st.ang) * beamLen;
|
||
ctx.strokeStyle = pri; ctx.lineWidth = 4; ctx.lineCap = 'round'; ctx.beginPath(); ctx.moveTo(cx - dx, pivotY - dy); ctx.lineTo(cx + dx, pivotY + dy); ctx.stroke();
|
||
function pan(x, y, label, col) { ctx.strokeStyle = mut; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x, y + 22); ctx.stroke(); ctx.fillStyle = col; ctx.beginPath(); ctx.ellipse(x, y + 30, 30, 9, 0, 0, 2 * Math.PI); ctx.fill(); ctx.fillStyle = '#fff'; ctx.font = 'bold 14px Inter, sans-serif'; ctx.textAlign = 'center'; ctx.fillText(label, x, y + 34); }
|
||
pan(cx - dx, pivotY - dy, '' + f.L, f.eq ? '#059669' : '#0891b2');
|
||
pan(cx + dx, pivotY + dy, '' + f.R, f.eq ? '#059669' : '#e11d48');
|
||
ctx.fillStyle = f.eq ? '#059669' : mut; ctx.font = 'bold 13px Inter, sans-serif'; ctx.textAlign = 'center';
|
||
ctx.fillText('a·d = ' + f.L + (f.eq ? ' = ' : ' ≠ ') + f.R + ' = b·c' + (f.eq ? ' ✓ пропорция верна' : ''), cx, 22);
|
||
}
|
||
var L = loop(host, draw);
|
||
cap.innerHTML = 'Пропорция $a:b = c:d$ верна, когда произведение крайних равно произведению средних: $a\\cdot d = b\\cdot c$ (весы в равновесии).';
|
||
if (W.renderMathInElement) try { W.renderMathInElement(cap, { delimiters: [{ left: '$', right: '$', display: false }], throwOnError: false }); } catch (e) {}
|
||
return { stop: L.stop, set: function (a, b, c, d) { st.a = a; st.b = b; st.c = c; st.d = d; setTarget(); } };
|
||
};
|
||
|
||
/* ============================ ДЕМО 15: ОБРАТНАЯ ПРОПОРЦИЯ = ПОСТОЯННАЯ ПЛОЩАДЬ ============================ */
|
||
M.constAreaRect = function (host, opts) {
|
||
opts = opts || {}; var K = opts.area || 12; var W0 = 360, H0 = 250; var sc = sceneCanvas(host, W0, H0); var cap = caption(host, '');
|
||
var st = { w: 3, tw: 3 };
|
||
function draw() {
|
||
var ctx = sc.ctx; if (!ctx) return; st.w += (st.tw - st.w) * 0.18; var w = st.w, h = K / w;
|
||
var pri = cssVar('--pri', '#0891b2'), acc = cssVar('--pri2', '#0e7490'); var pad = 40, unit = Math.min((W0 - 2 * pad) / K, (H0 - 2 * pad) / K), x0 = pad, y0 = H0 - pad;
|
||
ctx.clearRect(0, 0, W0, H0);
|
||
ctx.fillStyle = 'rgba(8,145,178,0.25)'; ctx.fillRect(x0, y0 - h * unit, w * unit, h * unit);
|
||
ctx.strokeStyle = pri; ctx.lineWidth = 2.5; ctx.strokeRect(x0, y0 - h * unit, w * unit, h * unit);
|
||
ctx.fillStyle = acc; ctx.font = '13px JetBrains Mono, monospace'; ctx.textAlign = 'center'; ctx.fillText('x = ' + (Math.round(w * 10) / 10), x0 + w * unit / 2, y0 + 20);
|
||
ctx.save(); ctx.translate(x0 - 16, y0 - h * unit / 2); ctx.rotate(-Math.PI / 2); ctx.fillText('y = ' + (Math.round(h * 10) / 10), 0, 0); ctx.restore();
|
||
ctx.fillStyle = acc; ctx.font = 'bold 15px Inter, sans-serif'; ctx.fillText('x · y = ' + K + ' — площадь постоянна', W0 / 2, 22);
|
||
}
|
||
var L = loop(host, draw);
|
||
cap.innerHTML = 'Обратная пропорциональность: чем больше $x$, тем меньше $y$, но произведение (площадь) не меняется: $x\\cdot y = ' + K + '$.';
|
||
if (W.renderMathInElement) try { W.renderMathInElement(cap, { delimiters: [{ left: '$', right: '$', display: false }], throwOnError: false }); } catch (e) {}
|
||
return { stop: L.stop, set: function (w) { st.tw = w; } };
|
||
};
|
||
|
||
/* ============================ КОМПОНЕНТ: ПОШАГОВЫЙ ПЛЕЕР (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);
|