From 97966ba2dfd2fbbed6bc24ca6b87c65cf98f6b9b Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Tue, 2 Jun 2026 21:56:57 +0300 Subject: [PATCH] =?UTF-8?q?feat(math6):=20=D1=81=D0=B8=D0=BC=D0=BC=D0=B5?= =?UTF-8?q?=D1=82=D1=80=D0=B8=D1=8F=20(=D0=93=D0=BB.6=20=C2=A74=20=D1=86?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=D1=80=D0=B0=D0=BB=D1=8C=D0=BD=D0=B0=D1=8F,?= =?UTF-8?q?=20=C2=A75=20=D0=BE=D1=81=D0=B5=D0=B2=D0=B0=D1=8F)=20=E2=80=94?= =?UTF-8?q?=20reflectFold?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Math6Anim.reflectFold: на координатной плоскости треугольник плавно переходит на свой образ — центральная (поворот 180° вокруг O, режим 'central') или осевая (отражение через Oy, режим 'axial'); образ показан красным пунктиром, ось/центр выделены. Один компонент закрыл §4 и §5. 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 | 37 ++++++++++++++++++++++++++++++ frontend/textbooks/math_6_ch6.html | 10 ++++++++ 3 files changed, 51 insertions(+) diff --git a/backend/tests/math6-page.test.js b/backend/tests/math6-page.test.js index 8bcef78..b959a65 100644 --- a/backend/tests/math6-page.test.js +++ b/backend/tests/math6-page.test.js @@ -174,6 +174,10 @@ test('анимации: canvas-демо монтируются (headless-safe)', r6.doc.defaultView.goTo('p2'); await wait(100); assert.ok(r6.doc.querySelector('#p2-roll canvas'), 'canvas «колесо» §6.2'); assert.ok(r6.doc.querySelector('#p2-sweep canvas'), 'canvas «заметание площади» §6.2'); + r6.doc.defaultView.goTo('p4'); await wait(100); + assert.ok(r6.doc.querySelector('#p4-symfig canvas'), 'canvas «центральная симметрия» §6.4'); + r6.doc.defaultView.goTo('p5'); await wait(100); + assert.ok(r6.doc.querySelector('#p5-symfig canvas'), 'canvas «осевая симметрия» §6.5'); assert.deepEqual(r6.errors, [], 'ch6 без ошибок: ' + r6.errors.join(' | ')); // Глава 1 §6: площадная модель умножения const r1 = await loadDom('math_6_ch1.html'); diff --git a/frontend/js/math6_anim.js b/frontend/js/math6_anim.js index 9093a11..a2e7a57 100644 --- a/frontend/js/math6_anim.js +++ b/frontend/js/math6_anim.js @@ -378,6 +378,43 @@ M.coordGame = function (host, opts) { 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 }; +}; + /* ============================ КОМПОНЕНТ: ПОШАГОВЫЙ ПЛЕЕР (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_ch6.html b/frontend/textbooks/math_6_ch6.html index 3938096..aaa46c5 100644 --- a/frontend/textbooks/math_6_ch6.html +++ b/frontend/textbooks/math_6_ch6.html @@ -405,9 +405,14 @@ function buildP4(){ +'
' +'
$x\'=$ $y\'=$
' +''; + h+='
Анимация
Центральная симметрия вживую
' + +'
Жёлтый треугольник плавно поворачивается на $180°$ вокруг центра $O$ и ложится на свой образ (красный пунктир).
' + +'
'; h+=secNav('p3','p5')+readBtn('p4'); box.innerHTML=h; renderMath(box); + (function(){ if(window.Math6Anim) Math6Anim.reflectFold(document.getElementById('p4-symfig'),{mode:'central'}); })(); + (function(){ var i2=0,score2=0,cur2=null; function gen2(){ var ax=_pick([-5,-4,-3,-2,-1,1,2,3,4,5]),ay=_pick([-5,-4,-3,-2,-1,1,2,3,4,5]); cur2={ax:ax,ay:ay,rx:-ax,ry:-ay}; } @@ -469,9 +474,14 @@ function buildP5(){ +'
' +'
' +''; + h+='
Анимация
Осевая симметрия вживую
' + +'
Жёлтый треугольник «складывается» через ось $Oy$ (красный пунктир) и ложится на свой образ.
' + +'
'; h+=secNav('p4','final')+readBtn('p5'); box.innerHTML=h; renderMath(box); + (function(){ if(window.Math6Anim) Math6Anim.reflectFold(document.getElementById('p5-symfig'),{mode:'axial'}); })(); + (function(){ var i2=0,score2=0,cur2=null; function gen2(){ var ax=_pick([-4,-3,-2,-1,1,2,3,4]),ay=_pick([-4,-3,-2,-1,1,2,3,4]),axis=_pick(['Oy','Ox']); cur2={ax:ax,ay:ay,axis:axis,rx:axis==='Oy'?-ax:ax,ry:axis==='Oy'?ay:-ay}; }