From 61de12e2de759f41b3fa0d89a2fa40e72eabd781 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Tue, 2 Jun 2026 21:29:03 +0300 Subject: [PATCH] =?UTF-8?q?feat(math6):=20=D0=B5=D1=89=D1=91=202=20canvas-?= =?UTF-8?q?=D0=B4=D0=B5=D0=BC=D0=BE=20=E2=80=94=20=D0=BF=D1=80=D1=8B=D0=B6?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=BF=D0=BE=20=D0=BF=D1=80=D1=8F=D0=BC=D0=BE?= =?UTF-8?q?=D0=B9=20(=C2=B1)=20=D0=B8=20=D0=BC=D0=B0=D1=88=D0=B8=D0=BD?= =?UTF-8?q?=D0=BA=D0=B0+=D0=B3=D1=80=D0=B0=D1=84=D0=B8=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Math6Anim расширен: numberLineWalk (анимированные стрелки-шаги a→b на числовой прямой для сложения рациональных) и carGraph (машина едет по дороге, а график «путь–время» вычерчивается синхронно; горизонталь = стоянка). Вшито: Гл.4 §4 (прыжки, ползунки a,b) и Гл.5 §2 (машинка+график). Headless-safe. Тесты math6: 19/19 (анимации в Гл.1/4/5/6 монтируются). Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/tests/math6-page.test.js | 10 +++++ frontend/js/math6_anim.js | 62 ++++++++++++++++++++++++++++++ frontend/textbooks/math_6_ch4.html | 8 ++-- frontend/textbooks/math_6_ch5.html | 6 +++ 4 files changed, 83 insertions(+), 3 deletions(-) diff --git a/backend/tests/math6-page.test.js b/backend/tests/math6-page.test.js index e851232..e4ace80 100644 --- a/backend/tests/math6-page.test.js +++ b/backend/tests/math6-page.test.js @@ -180,6 +180,16 @@ test('анимации: canvas-демо монтируются (headless-safe)', r1.doc.defaultView.goTo('p6'); await wait(100); assert.ok(r1.doc.querySelector('#p6-area canvas'), 'canvas «площадная модель» §1.6'); assert.deepEqual(r1.errors, [], 'ch1 без ошибок: ' + r1.errors.join(' | ')); + // Глава 4 §4: прыжки по числовой прямой + const r4 = await loadDom('math_6_ch4.html'); + r4.doc.defaultView.goTo('p4'); await wait(100); + assert.ok(r4.doc.querySelector('#p4-walk canvas'), 'canvas «прыжки по прямой» §4.4'); + assert.deepEqual(r4.errors, [], 'ch4 без ошибок: ' + r4.errors.join(' | ')); + // Глава 5 §2: машинка + график + const r5 = await loadDom('math_6_ch5.html'); + r5.doc.defaultView.goTo('p2'); await wait(100); + assert.ok(r5.doc.querySelector('#p2-car canvas'), 'canvas «машинка + график» §5.2'); + assert.deepEqual(r5.errors, [], 'ch5 без ошибок: ' + r5.errors.join(' | ')); }); test('hub: 6 карточек глав + курсовой финал', async () => { diff --git a/frontend/js/math6_anim.js b/frontend/js/math6_anim.js index af2672c..653c7b4 100644 --- a/frontend/js/math6_anim.js +++ b/frontend/js/math6_anim.js @@ -174,4 +174,66 @@ M.areaModel = function (host, opts) { 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 = 'Машина едет — график «путь–время» вычерчивается сам. Где линия горизонтальна (с 2 до 3 ч) — машина стоит: время идёт, а путь не растёт.'; + return { stop: L.stop }; +}; + })(window); diff --git a/frontend/textbooks/math_6_ch4.html b/frontend/textbooks/math_6_ch4.html index 98ac31e..337275e 100644 --- a/frontend/textbooks/math_6_ch4.html +++ b/frontend/textbooks/math_6_ch4.html @@ -17,6 +17,7 @@ + @@ -307,7 +308,7 @@ function buildP4(){ h+='
Интерактив 1
Сложение на прямой
' +'
Двигай слагаемые — результат отмечается на координатной прямой.
' +'
' - +'
'; + +'
'; h+='
Интерактив 2
Тренажёр сложения
' +'
Сложи рациональные числа.
' +'
Пример 1 / 6Очки: 0 / 6
' @@ -318,10 +319,11 @@ function buildP4(){ box.innerHTML=h; renderMath(box); (function(){ - var asl=document.getElementById('p4-asl'), bsl=document.getElementById('p4-bsl'), fig=document.getElementById('p4-fig'), out=document.getElementById('p4-out'); + var asl=document.getElementById('p4-asl'), bsl=document.getElementById('p4-bsl'), fig=document.getElementById('p4-fig'), out=document.getElementById('p4-out'), walk=null; function render(){ var a=+asl.value,b=+bsl.value,s=a+b; document.getElementById('p4-av').textContent=a; document.getElementById('p4-bv').textContent=b; fig.innerHTML=Math6.numberLine({min:-12,max:12,minor:1,major:2,width:580,marks:[{v:a,label:'a',color:'#4f46e5'},{v:s,label:'a+b',color:'#059669',above:false}]}); - out.innerHTML='
$'+a+' + '+_par(b)+' = '+s+'$
'; renderMath(out); } + out.innerHTML='
$'+a+' + '+_par(b)+' = '+s+'$
'; renderMath(out); + if(window.Math6Anim){ if(walk)walk.stop(); walk=Math6Anim.numberLineWalk(document.getElementById('p4-walk'),{a:a,b:b}); } } asl.oninput=render; bsl.oninput=render; render(); })(); diff --git a/frontend/textbooks/math_6_ch5.html b/frontend/textbooks/math_6_ch5.html index c4742be..5589040 100644 --- a/frontend/textbooks/math_6_ch5.html +++ b/frontend/textbooks/math_6_ch5.html @@ -17,6 +17,7 @@ + @@ -218,9 +219,14 @@ function buildP2(){ +'
' +'
' +'
'; + h+='
Анимация
Машинка едет — график рисуется сам
' + +'
Смотри, как движение машины по дороге превращается в линию на графике «путь–время». Горизонтальный участок — машина стоит.
' + +'
'; h+=secNav('p1','p3')+readBtn('p2'); box.innerHTML=h; renderMath(box); + (function(){ if(window.Math6Anim) Math6Anim.carGraph(document.getElementById('p2-car'),{}); })(); + var G=[{x:0,y:2},{x:1,y:4},{x:2,y:4},{x:3,y:6},{x:4,y:5},{x:5,y:5},{x:6,y:3}]; function gy(x){ for(var k=0;k