feat(math6): симметрия (Гл.6 §4 центральная, §5 осевая) — reflectFold

Math6Anim.reflectFold: на координатной плоскости треугольник плавно
переходит на свой образ — центральная (поворот 180° вокруг O, режим
'central') или осевая (отражение через Oy, режим 'axial'); образ показан
красным пунктиром, ось/центр выделены. Один компонент закрыл §4 и §5.
Headless-safe. Тесты math6: 20/20.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-02 21:56:57 +03:00
parent 555f701b57
commit 97966ba2df
3 changed files with 51 additions and 0 deletions
+4
View File
@@ -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');
+37
View File
@@ -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 () {} };
+10
View File
@@ -405,9 +405,14 @@ function buildP4(){
+'<div id="p4-q2" class="qbox"></div>'
+'<div style="display:flex;gap:10px;justify-content:center;align-items:center;flex-wrap:wrap">$x\'=$ <input type="number" id="p4-x2" class="tinp" style="width:70px;text-align:center"> $y\'=$ <input type="number" id="p4-y2" class="tinp" style="width:70px;text-align:center"><button class="btn primary" id="p4-go2">Проверить</button></div>'
+'<div class="feedback" id="p4-fb2"></div></div>';
h+='<div class="wg" id="p4-symfig-wg"><div class="wg-header"><span class="wg-badge">Анимация</span><div class="wg-title">Центральная симметрия вживую</div></div>'
+'<div class="wg-help">Жёлтый треугольник плавно поворачивается на $180°$ вокруг центра $O$ и ложится на свой образ (красный пунктир).</div>'
+'<div id="p4-symfig"></div></div>';
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(){
+'<div id="p5-q2" class="qbox"></div>'
+'<div style="display:flex;gap:10px;justify-content:center;flex-wrap:wrap"><button class="btn primary" data-ax2="Oy">Ось $Oy$</button><button class="btn primary" data-ax2="Ox">Ось $Ox$</button></div>'
+'<div class="feedback" id="p5-fb2"></div></div>';
h+='<div class="wg" id="p5-symfig-wg"><div class="wg-header"><span class="wg-badge">Анимация</span><div class="wg-title">Осевая симметрия вживую</div></div>'
+'<div class="wg-help">Жёлтый треугольник «складывается» через ось $Oy$ (красный пунктир) и ложится на свой образ.</div>'
+'<div id="p5-symfig"></div></div>';
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}; }