feat(math6): полировка Гл.2 — pieGrow, balanceScale, constAreaRect

Math6Anim.pieGrow (растущие сектора, §7 — заменил статичный Math6.pie,
цвета синхронны легенде), balanceScale (весы a·d ? b·c, §3, кнопка «другой
пример»), constAreaRect (обратная проп. = постоянная площадь, §4, ползунок x).
Headless-safe. Тесты math6: 20/20 (поправлен ассерт §7 svg→canvas).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-02 22:07:56 +03:00
parent 302b062649
commit 51db000545
3 changed files with 96 additions and 5 deletions
+70
View File
@@ -470,6 +470,76 @@ M.setFilter = function (host, opts) {
return { stop: L.stop, set: function (label, test) { st.label = label; st.test = test; reset(); } };
};
/* ============================ ДЕМО 13: РАСТУЩАЯ КРУГОВАЯ ДИАГРАММА ============================ */
M.pieGrow = function (host, opts) {
opts = opts || {}; var W0 = 240, H0 = 240; var sc = sceneCanvas(host, W0, H0); var cap = caption(host, '');
var pal = ['#0891b2', '#f59e0b', '#e11d48', '#059669', '#7c3aed'];
var st = { segs: opts.segs || [{ label: 'A', value: 1 }], t0: null }; var period = 3.4;
function draw(t) {
var ctx = sc.ctx; if (!ctx) return; if (st.t0 === null) st.t0 = t; var p = Math.min(1, ((t - st.t0) % period) / (period * 0.7));
var cx = W0 / 2, cy = H0 / 2, r = W0 / 2 - 12, total = st.segs.reduce(function (a, s) { return a + s.value; }, 0) || 1, sweep = -Math.PI / 2 + p * 2 * Math.PI;
ctx.clearRect(0, 0, W0, H0); var ang = -Math.PI / 2;
st.segs.forEach(function (s, i) {
var frac = s.value / total, a0 = ang, a1 = ang + frac * 2 * Math.PI, drawTo = Math.min(a1, sweep);
if (drawTo > a0) {
ctx.fillStyle = s.color || pal[i % pal.length]; ctx.beginPath(); ctx.moveTo(cx, cy); ctx.arc(cx, cy, r, a0, drawTo); ctx.closePath(); ctx.fill();
ctx.strokeStyle = cssVar('--card', '#fff'); ctx.lineWidth = 1.5; ctx.stroke();
if (sweep >= a1 && frac > 0.05) { var mid = a0 + frac * Math.PI, lx = cx + r * 0.6 * Math.cos(mid), ly = cy + r * 0.6 * Math.sin(mid); ctx.fillStyle = '#fff'; ctx.font = 'bold 12px Inter, sans-serif'; ctx.textAlign = 'center'; ctx.fillText(Math.round(frac * 100) + '%', lx, ly + 4); }
}
ang = a1;
});
}
var L = loop(host, draw);
return { stop: L.stop, set: function (segs) { st.segs = segs; st.t0 = null; } };
};
/* ============================ ДЕМО 14: ВЕСЫ ПРОПОРЦИИ (a·d ? b·c) ============================ */
M.balanceScale = function (host, opts) {
opts = opts || {}; var W0 = 380, H0 = 230; var sc = sceneCanvas(host, W0, H0); var cap = caption(host, '');
var st = { a: 3, b: 4, c: 6, d: 8, ang: 0, target: 0 }; if (opts.a != null) { st.a = opts.a; st.b = opts.b; st.c = opts.c; st.d = opts.d; }
function info() { var L = st.a * st.d, R = st.b * st.c; return { L: L, R: R, eq: L === R }; }
function setTarget() { var f = info(); st.target = Math.max(-0.26, Math.min(0.26, (f.R - f.L) * 0.02)); }
setTarget();
function draw() {
var ctx = sc.ctx; if (!ctx) return; st.ang += (st.target - st.ang) * 0.15;
var mut = cssVar('--muted', '#64748b'), pri = cssVar('--pri', '#0891b2'); var f = info();
ctx.clearRect(0, 0, W0, H0); var cx = W0 / 2, pivotY = 64, beamLen = 128;
ctx.strokeStyle = mut; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(cx, pivotY); ctx.lineTo(cx, H0 - 26); ctx.stroke();
ctx.beginPath(); ctx.moveTo(cx - 40, H0 - 26); ctx.lineTo(cx + 40, H0 - 26); ctx.stroke();
var dx = Math.cos(st.ang) * beamLen, dy = Math.sin(st.ang) * beamLen;
ctx.strokeStyle = pri; ctx.lineWidth = 4; ctx.lineCap = 'round'; ctx.beginPath(); ctx.moveTo(cx - dx, pivotY - dy); ctx.lineTo(cx + dx, pivotY + dy); ctx.stroke();
function pan(x, y, label, col) { ctx.strokeStyle = mut; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x, y + 22); ctx.stroke(); ctx.fillStyle = col; ctx.beginPath(); ctx.ellipse(x, y + 30, 30, 9, 0, 0, 2 * Math.PI); ctx.fill(); ctx.fillStyle = '#fff'; ctx.font = 'bold 14px Inter, sans-serif'; ctx.textAlign = 'center'; ctx.fillText(label, x, y + 34); }
pan(cx - dx, pivotY - dy, '' + f.L, f.eq ? '#059669' : '#0891b2');
pan(cx + dx, pivotY + dy, '' + f.R, f.eq ? '#059669' : '#e11d48');
ctx.fillStyle = f.eq ? '#059669' : mut; ctx.font = 'bold 13px Inter, sans-serif'; ctx.textAlign = 'center';
ctx.fillText('a·d = ' + f.L + (f.eq ? ' = ' : ' ≠ ') + f.R + ' = b·c' + (f.eq ? ' ✓ пропорция верна' : ''), cx, 22);
}
var L = loop(host, draw);
cap.innerHTML = 'Пропорция $a:b = c:d$ верна, когда произведение крайних равно произведению средних: $a\\cdot d = b\\cdot c$ (весы в равновесии).';
if (W.renderMathInElement) try { W.renderMathInElement(cap, { delimiters: [{ left: '$', right: '$', display: false }], throwOnError: false }); } catch (e) {}
return { stop: L.stop, set: function (a, b, c, d) { st.a = a; st.b = b; st.c = c; st.d = d; setTarget(); } };
};
/* ============================ ДЕМО 15: ОБРАТНАЯ ПРОПОРЦИЯ = ПОСТОЯННАЯ ПЛОЩАДЬ ============================ */
M.constAreaRect = function (host, opts) {
opts = opts || {}; var K = opts.area || 12; var W0 = 360, H0 = 250; var sc = sceneCanvas(host, W0, H0); var cap = caption(host, '');
var st = { w: 3, tw: 3 };
function draw() {
var ctx = sc.ctx; if (!ctx) return; st.w += (st.tw - st.w) * 0.18; var w = st.w, h = K / w;
var pri = cssVar('--pri', '#0891b2'), acc = cssVar('--pri2', '#0e7490'); var pad = 40, unit = Math.min((W0 - 2 * pad) / K, (H0 - 2 * pad) / K), x0 = pad, y0 = H0 - pad;
ctx.clearRect(0, 0, W0, H0);
ctx.fillStyle = 'rgba(8,145,178,0.25)'; ctx.fillRect(x0, y0 - h * unit, w * unit, h * unit);
ctx.strokeStyle = pri; ctx.lineWidth = 2.5; ctx.strokeRect(x0, y0 - h * unit, w * unit, h * unit);
ctx.fillStyle = acc; ctx.font = '13px JetBrains Mono, monospace'; ctx.textAlign = 'center'; ctx.fillText('x = ' + (Math.round(w * 10) / 10), x0 + w * unit / 2, y0 + 20);
ctx.save(); ctx.translate(x0 - 16, y0 - h * unit / 2); ctx.rotate(-Math.PI / 2); ctx.fillText('y = ' + (Math.round(h * 10) / 10), 0, 0); ctx.restore();
ctx.fillStyle = acc; ctx.font = 'bold 15px Inter, sans-serif'; ctx.fillText('x · y = ' + K + ' — площадь постоянна', W0 / 2, 22);
}
var L = loop(host, draw);
cap.innerHTML = 'Обратная пропорциональность: чем больше $x$, тем меньше $y$, но произведение (площадь) не меняется: $x\\cdot y = ' + K + '$.';
if (W.renderMathInElement) try { W.renderMathInElement(cap, { delimiters: [{ left: '$', right: '$', display: false }], throwOnError: false }); } catch (e) {}
return { stop: L.stop, set: function (w) { st.tw = w; } };
};
/* ============================ КОМПОНЕНТ: ПОШАГОВЫЙ ПЛЕЕР (DOM, не canvas) ============================ */
M.stepPlayer = function (host, opts) {
opts = opts || {}; var steps = opts.steps || []; if (!steps.length) return { stop: function () {} };
+19 -4
View File
@@ -269,9 +269,16 @@ function buildP3(){
+'<div id="p3-vq" class="qbox"></div>'
+'<div style="display:flex;gap:10px;justify-content:center;flex-wrap:wrap"><button class="btn primary" data-v="1">Верна</button><button class="btn primary" data-v="0">Неверна</button></div>'
+'<div class="feedback" id="p3-vfb"></div></div>';
h+='<div class="wg" id="p3-bal"><div class="wg-header"><span class="wg-badge">Анимация</span><div class="wg-title">Весы пропорции</div></div>'
+'<div class="wg-help">Пропорция верна, когда произведение крайних равно произведению средних: $a\\cdot d = b\\cdot c$ — весы в равновесии. Жми «другой пример».</div>'
+'<div id="p3-balfig"></div><div style="text-align:center;margin-top:6px"><button class="btn primary" id="p3-bal-next">Другой пример</button></div></div>';
h+=secNav('p2','p4')+readBtn('p3');
box.innerHTML=h; renderMath(box);
(function(){ if(!window.Math6Anim) return; var EX=[[3,4,6,8],[2,5,4,10],[3,4,5,8],[6,9,2,3],[2,3,5,8]];
var bal=Math6Anim.balanceScale(document.getElementById('p3-balfig'),{a:EX[0][0],b:EX[0][1],c:EX[0][2],d:EX[0][3]}); var k=0;
document.getElementById('p3-bal-next').addEventListener('click',function(){ k=(k+1)%EX.length; var e=EX[k]; bal.set(e[0],e[1],e[2],e[3]); }); })();
(function(){
var i=0,score=0,cur=null;
function gen(){ var a=_ri(2,6), b=_ri(2,9), m=_ri(2,6); cur={a:a,b:b,c:a*m,x:b*m}; }
@@ -333,9 +340,16 @@ function buildP4(){
+'<div id="p4-tq" class="qbox"></div>'
+'<div style="display:flex;gap:10px;justify-content:center;align-items:center;flex-wrap:wrap"><input type="text" id="p4-ta" class="tinp" style="width:100px;text-align:center"><button class="btn primary" id="p4-tgo">Проверить</button></div>'
+'<div class="feedback" id="p4-tfb"></div></div>';
h+='<div class="wg" id="p4-car"><div class="wg-header"><span class="wg-badge">Анимация</span><div class="wg-title">Обратная пропорция: постоянная площадь</div></div>'
+'<div class="wg-help">Двигай ширину $x$ — высота $y$ сама подстраивается так, что площадь $x\\cdot y$ остаётся постоянной. Это и есть обратная пропорциональность.</div>'
+'<div class="sliders"><label>Ширина $x$ = <b id="p4-cwv">3</b><input type="range" id="p4-cw" min="1" max="12" value="3"></label></div>'
+'<div id="p4-carfig"></div></div>';
h+=secNav('p3','p5')+readBtn('p4');
box.innerHTML=h; renderMath(box);
(function(){ if(!window.Math6Anim) return; var car=Math6Anim.constAreaRect(document.getElementById('p4-carfig'),{area:12});
var sl=document.getElementById('p4-cw'); sl.oninput=function(){ document.getElementById('p4-cwv').textContent=sl.value; car.set(+sl.value); }; })();
(function(){
var Q=[
{t:'d',q:'Количество тетрадей и их общая стоимость (цена постоянна).'},
@@ -551,11 +565,12 @@ function buildP7(){
{name:'Оценки класса', segs:[{label:'«910»',value:30},{label:'«78»',value:45},{label:'«56»',value:25}]},
{name:'Сутки школьника', segs:[{label:'Сон',value:33},{label:'Учёба',value:25},{label:'Отдых',value:42}]}
];
var pick=document.getElementById('p7-pick'), fig=document.getElementById('p7-fig'), leg=document.getElementById('p7-leg');
var pal=['#4f46e5','#0891b2','#e11d48','#059669','#d97706'];
var pick=document.getElementById('p7-pick'), fig=document.getElementById('p7-fig'), leg=document.getElementById('p7-leg'), grow=null;
var pal=['#0891b2','#f59e0b','#e11d48','#059669','#7c3aed'];
pick.innerHTML=DS.map(function(d,k){ return '<button class="btn'+(k===0?' primary':'')+'" data-k="'+k+'">'+d.name+'</button>'; }).join('');
function render(k){ var d=DS[k]; fig.innerHTML=Math6.pie(d.segs,{size:200});
leg.innerHTML=d.segs.map(function(s,j){ return '<div style="display:flex;align-items:center;gap:8px;margin:4px 0;font-size:.9rem"><span style="width:14px;height:14px;border-radius:3px;background:'+pal[j%pal.length]+';display:inline-block"></span>'+s.label+' — <b>'+s.value+'%</b></div>'; }).join(''); }
function render(k){ var d=DS[k]; var segs=d.segs.map(function(s,j){ return {label:s.label,value:s.value,color:pal[j%pal.length]}; });
if(window.Math6Anim){ if(!grow) grow=Math6Anim.pieGrow(fig,{segs:segs}); else grow.set(segs); } else { fig.innerHTML=Math6.pie(d.segs,{size:200}); }
leg.innerHTML=segs.map(function(s){ return '<div style="display:flex;align-items:center;gap:8px;margin:4px 0;font-size:.9rem"><span style="width:14px;height:14px;border-radius:3px;background:'+s.color+';display:inline-block"></span>'+s.label+' — <b>'+s.value+'%</b></div>'; }).join(''); }
pick.querySelectorAll('[data-k]').forEach(function(b){ b.addEventListener('click',function(){ pick.querySelectorAll('button').forEach(function(x){x.classList.remove('primary');}); b.classList.add('primary'); render(+b.getAttribute('data-k')); }); });
render(0);
})();