From 555f701b5771cfe6d43dd2da391b0fa84286667e Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Tue, 2 Jun 2026 21:53:47 +0300 Subject: [PATCH] =?UTF-8?q?feat(math6):=20=D1=83=D0=BC=D0=BD=D0=BE=D0=B6?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5-=D0=BF=D1=80=D1=8B=D0=B6=D0=BA=D0=B8?= =?UTF-8?q?=20(=D0=93=D0=BB.4=20=C2=A77)=20+=20=D0=BA=D0=BE=D0=BE=D1=80?= =?UTF-8?q?=D0=B4=D0=B8=D0=BD=D0=B0=D1=82=D0=BD=D1=8B=D0=B9=20=D1=82=D0=B8?= =?UTF-8?q?=D1=80=20(=D0=93=D0=BB.5=20=C2=A71)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- backend/tests/math6-page.test.js | 4 ++ frontend/js/math6_anim.js | 73 +++++++++++++++++++++++++ frontend/textbooks/math_6_ch4.html | 9 +++ frontend/textbooks/math_6_ch5.html | 5 ++ plans/textbooks-6/PLAN_MATH_6_VISUAL.md | 7 ++- 5 files changed, 95 insertions(+), 3 deletions(-) diff --git a/backend/tests/math6-page.test.js b/backend/tests/math6-page.test.js index cbfcc9d..8bcef78 100644 --- a/backend/tests/math6-page.test.js +++ b/backend/tests/math6-page.test.js @@ -186,6 +186,8 @@ test('анимации: canvas-демо монтируются (headless-safe)', assert.ok(r4.doc.querySelector('#p4-walk canvas'), 'canvas «прыжки по прямой» §4.4'); r4.doc.defaultView.goTo('p1'); await wait(100); assert.ok(r4.doc.querySelector('#p1-therm-fig canvas'), 'canvas «термометр» §4.1'); + r4.doc.defaultView.goTo('p7'); await wait(100); + assert.ok(r4.doc.querySelector('#p7-jumpfig canvas'), 'canvas «умножение-прыжки» §4.7'); assert.deepEqual(r4.errors, [], 'ch4 без ошибок: ' + r4.errors.join(' | ')); // Глава 5 §2: машинка + график const r5 = await loadDom('math_6_ch5.html'); @@ -194,6 +196,8 @@ test('анимации: canvas-демо монтируются (headless-safe)', 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'); + r5.doc.defaultView.goTo('p1'); await wait(100); + assert.ok(r5.doc.querySelector('#p1-game canvas'), 'canvas «координатный тир» §5.1'); assert.deepEqual(r5.errors, [], 'ch5 без ошибок: ' + r5.errors.join(' | ')); }); diff --git a/frontend/js/math6_anim.js b/frontend/js/math6_anim.js index 7e44f60..9093a11 100644 --- a/frontend/js/math6_anim.js +++ b/frontend/js/math6_anim.js @@ -305,6 +305,79 @@ M.thermometer = function (host, opts) { 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 = '
Очки: 0 · кликни по узлу сетки
'; + 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 () {} }; diff --git a/frontend/textbooks/math_6_ch4.html b/frontend/textbooks/math_6_ch4.html index b794a6d..a261cc4 100644 --- a/frontend/textbooks/math_6_ch4.html +++ b/frontend/textbooks/math_6_ch4.html @@ -500,9 +500,18 @@ function buildP7(){ +'
' +'
' +''; + h+='
Анимация
Умножение — это прыжки
' + +'
$a \\cdot b$ — сделать $a$ прыжков по $b$ от нуля. Зелёные прыжки — вправо ($b>0$), красные — влево ($b<0$).
' + +'
' + +'
' + +'
'; h+=secNav('p6','p8')+readBtn('p7'); box.innerHTML=h; renderMath(box); + (function(){ if(!window.Math6Anim) return; var ja=document.getElementById('p7-ja'), jb=document.getElementById('p7-jb'), jump=null; + function r(){ var a=+ja.value,b=+jb.value; document.getElementById('p7-jav').textContent=a; document.getElementById('p7-jbv').textContent=b; if(jump)jump.stop(); jump=Math6Anim.numberLineJumps(document.getElementById('p7-jumpfig'),{a:a,b:b}); } + ja.oninput=r; jb.oninput=r; r(); })(); + (function(){ var out=document.getElementById('p7-out'); function render(s){ var map={pp:['$(+)\\cdot(+)=+$','плюс'],pn:['$(+)\\cdot(-)=-$','минус'],np:['$(-)\\cdot(+)=-$','минус'],nn:['$(-)\\cdot(-)=+$','плюс']}; diff --git a/frontend/textbooks/math_6_ch5.html b/frontend/textbooks/math_6_ch5.html index d91ac55..166f682 100644 --- a/frontend/textbooks/math_6_ch5.html +++ b/frontend/textbooks/math_6_ch5.html @@ -137,9 +137,14 @@ function buildP1(){ +'
' +'
' +''; + h+='
Игра
Координатный тир: поставь точку
' + +'
Тебе называют координаты — кликни по нужному узлу сетки. Попал — очко и новая цель; промахнулся — покажу, где была точка.
' + +'
'; h+=secNav(null,'p2')+readBtn('p1'); box.innerHTML=h; renderMath(box); + (function(){ if(window.Math6Anim) Math6Anim.coordGame(document.getElementById('p1-game'),{}); })(); + (function(){ var i=0,score=0,cur=null; function gen(){ cur={x:_ri(-5,5), y:_ri(-5,5)}; } diff --git a/plans/textbooks-6/PLAN_MATH_6_VISUAL.md b/plans/textbooks-6/PLAN_MATH_6_VISUAL.md index 16d24af..2b7efb2 100644 --- a/plans/textbooks-6/PLAN_MATH_6_VISUAL.md +++ b/plans/textbooks-6/PLAN_MATH_6_VISUAL.md @@ -27,7 +27,7 @@ | 11 | **`thermometer`** (canvas) | Столбик термометра ↑↓, ±числа, **`|x|` как измеренное расстояние до 0**, противоположные — зеркально. | 4.1, 4.2 | | 12 | **`numberLineJumps`** (canvas) | Умножение как **повторные прыжки** ($3\cdot(-2)$ = три прыжка по −2); вычитание = прыжок противоположного. | 4.5, 4.7, 4.8 | | 13 | **`coordGame`** (canvas) | «Морской бой/клад»: поставь точку по координатам; перекрестье от осей; четверти подсвечиваются. | 5.1 | -| 14 | **`solid3d` + `unfoldNet`** (canvas) | Вращение тел (куб/призма/пирамида/цилиндр/конус) + **разворачивание развёртки** и сборка обратно. | 6.1 | +| 14 | ~~`solid3d` + `unfoldNet`~~ | **ИСКЛЮЧЕНО** (по решению). Гл.6 §1 остаётся со статичной SVG-галереей тел + развёртки + квизы. | — | | 15 | **`triangleDrag`** (SVG) | Тащишь вершину — тип треугольника **пересчитывается вживую**, штрихи равных сторон и дуги углов обновляются. | 6.3 | | 16 | **`reflectFold`** (canvas) | **Складывание** фигуры через ось (осевая симметрия) и **поворот на 180°** вокруг точки (центральная). | 6.4, 6.5 | @@ -83,7 +83,7 @@ - **§3** y=kx / y=k/x → `plotLive` ✓. ### Глава 6 — Наглядная геометрия -- **§1** Тела/развёртки → **`solid3d` + `unfoldNet`** (вращение + раскрытие/сборка развёртки). +- **§1** Тела/развёртки → **без 3D-анимации (исключено)**: статичная SVG-галерея тел + развёртки + квизы «грани/рёбра/вершины» и «какое тело из развёртки» (уже есть). - **§2** Окружность/круг → `rollingCircle` + `sweepArea` ✓. - **§3** Виды треугольников → **`triangleDrag`** (тащишь вершину — тип пересчитывается). - **§4** Центральная симметрия → **`reflectFold`** (поворот на 180° вокруг $O$). @@ -103,7 +103,8 @@ --- ## D. Подход к раскатке -1. **Opus строит реюзабельные компоненты** (раздел A) — это canvas/3D, риск, нет авто-визуальной проверки, нужен headless-guard + тест «монтируется». Приоритет: `stepPlayer` → `columnOp` → `longDivision` → `barModel` → `thermometer`/`numberLineJumps` → `vennDrag` → `solid3d/unfoldNet` → `triangleDrag` → `reflectFold` → остальное. +1. **Opus строит реюзабельные компоненты** (раздел A) — это canvas/3D, риск, нет авто-визуальной проверки, нужен headless-guard + тест «монтируется». Сделано: `stepPlayer`, `thermometer`, `plotLive`, `carGraph`, `rollingCircle`, `sweepArea`, `areaModel`, `numberLineWalk`. +Остаток приоритетно: `numberLineJumps` → `coordGame` → `triangleDrag` → `reflectFold` → `barModel`/`pieGrow` → `vennDrag`/`setFilter` → `balanceScale`/`constAreaRect`. (3D-тела исключены.) 2. **Sonnet-воркфлоу вшивает** компоненты в §§ по карте (раздел B), по главе на агента, через `if(window.Math6Anim){…}`, с само-проверкой тестом (как делали при обогащении). 3. Каждый компонент → запись в тест «canvas-демо монтируются (headless-safe)».