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(){ +'
' +'