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+='