From 1fc1672acdf1907f80253a532826176f13f5e518 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Tue, 2 Jun 2026 21:33:47 +0300 Subject: [PATCH] =?UTF-8?q?feat(math6):=20=D0=B6=D0=B8=D0=B2=D0=BE=D0=B9?= =?UTF-8?q?=20=D0=B3=D1=80=D0=B0=D1=84=D0=B8=D0=BA=20y=3Dkx=20/=20y=3Dk/x?= =?UTF-8?q?=20(=D0=93=D0=BB.5=20=C2=A73)=20=E2=80=94=20=D0=BF=D0=BB=D0=B0?= =?UTF-8?q?=D0=B2=D0=BD=D0=BE=D0=B5=20=D0=BF=D0=B5=D1=80=D0=B5=D1=82=D0=B5?= =?UTF-8?q?=D0=BA=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=BF=D1=80=D0=B8=20k?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Math6Anim.plotLive: canvas-плоскость с сеткой/осями; кривая плавно «перетекает» (easing к целевому k). Переключатель прямая (y=kx, через начало) / обратная (y=k/x, две ветви). Слайдер k (−4..4, шаг 0,5) двигает кривую вживую. Вшито в Гл.5 §3 рядом со статичным графиком. Headless-safe. Тесты 19/19. Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/tests/math6-page.test.js | 3 +++ frontend/js/math6_anim.js | 42 ++++++++++++++++++++++++++++++ frontend/textbooks/math_6_ch5.html | 15 +++++++++++ 3 files changed, 60 insertions(+) diff --git a/backend/tests/math6-page.test.js b/backend/tests/math6-page.test.js index e4ace80..ada1534 100644 --- a/backend/tests/math6-page.test.js +++ b/backend/tests/math6-page.test.js @@ -189,6 +189,9 @@ test('анимации: canvas-демо монтируются (headless-safe)', 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'); + r5.doc.defaultView.goTo('p3'); await wait(100); + assert.ok(r5.doc.querySelector('#p3-livefig canvas'), 'canvas «живой график y=kx/k÷x» §5.3'); + assert.ok(r5.doc.querySelectorAll('#p3-live [data-m]').length === 2, 'переключатель прямая/обратная §5.3'); assert.deepEqual(r5.errors, [], 'ch5 без ошибок: ' + r5.errors.join(' | ')); }); diff --git a/frontend/js/math6_anim.js b/frontend/js/math6_anim.js index 653c7b4..9daef37 100644 --- a/frontend/js/math6_anim.js +++ b/frontend/js/math6_anim.js @@ -236,4 +236,46 @@ M.carGraph = function (host, opts) { 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; } + }; +}; + })(window); diff --git a/frontend/textbooks/math_6_ch5.html b/frontend/textbooks/math_6_ch5.html index 5589040..d91ac55 100644 --- a/frontend/textbooks/math_6_ch5.html +++ b/frontend/textbooks/math_6_ch5.html @@ -307,6 +307,11 @@ function buildP3(){ +'
Двигай коэффициент $k$ — смотри, как меняется наклон прямой.
' +'
' +'
'; + h+='
Анимация
Живой график: двигай k
' + +'
Переключай вид зависимости и двигай $k$ — график плавно перетекает. Прямая $y=kx$ всегда проходит через начало координат; гипербола $y=k/x$ — две ветви, через начало не проходит.
' + +'
' + +'
' + +'
'; h+='
Интерактив 2
Прямая или обратная?
' +'
Определи по графику вид зависимости.
' +'
Вопрос 1 / 6Очки: 0 / 6
' @@ -322,6 +327,16 @@ function buildP3(){ h+=secNav('p2','app')+readBtn('p3'); box.innerHTML=h; renderMath(box); + (function(){ + if(!window.Math6Anim) return; + var ctrl=Math6Anim.plotLive(document.getElementById('p3-livefig'),{k:2,mode:'kx'}); + var sl=document.getElementById('p3-lk'), kv=document.getElementById('p3-lkv'); + sl.oninput=function(){ var v=+sl.value; kv.textContent=String(v).replace('.',','); ctrl.setK(v); }; + document.querySelectorAll('#p3-live [data-m]').forEach(function(b){ b.addEventListener('click',function(){ + document.querySelectorAll('#p3-live [data-m]').forEach(function(x){x.classList.remove('primary');}); b.classList.add('primary'); + ctrl.setMode(b.getAttribute('data-m')); }); }); + })(); + (function(){ var sl=document.getElementById('p3-k'), fig=document.getElementById('p3-fig'); function render(){ var k=+sl.value; document.getElementById('p3-kv').textContent=k;