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:
@@ -86,7 +86,7 @@ test('ch2: проценты и пропорции — интерактивы +
|
||||
win.goTo('p3'); await wait(80);
|
||||
assert.ok(doc.querySelector('#p3-q') && doc.querySelectorAll('#p3-iv2 [data-v]').length === 2, 'пропорция §3');
|
||||
win.goTo('p7'); await wait(80);
|
||||
assert.ok(doc.querySelector('#p7-fig svg') && doc.querySelector('#p7-pick [data-k]'), 'круговая диаграмма §7');
|
||||
assert.ok(doc.querySelector('#p7-fig canvas') && doc.querySelector('#p7-pick [data-k]'), 'круговая диаграмма §7');
|
||||
win.goTo('final'); await wait(80);
|
||||
assert.ok(doc.querySelector('#fin-go'), 'арена боссов §2');
|
||||
win.bumpProgress('final', 100); await wait(20);
|
||||
@@ -207,6 +207,12 @@ test('анимации: canvas-демо монтируются (headless-safe)',
|
||||
const r2 = await loadDom('math_6_ch2.html');
|
||||
r2.doc.defaultView.goTo('p1'); await wait(100);
|
||||
assert.ok(r2.doc.querySelector('#p1-bar canvas'), 'canvas «полоса процента» §2.1');
|
||||
r2.doc.defaultView.goTo('p3'); await wait(100);
|
||||
assert.ok(r2.doc.querySelector('#p3-balfig canvas'), 'canvas «весы пропорции» §2.3');
|
||||
r2.doc.defaultView.goTo('p4'); await wait(100);
|
||||
assert.ok(r2.doc.querySelector('#p4-carfig canvas'), 'canvas «постоянная площадь» §2.4');
|
||||
r2.doc.defaultView.goTo('p7'); await wait(100);
|
||||
assert.ok(r2.doc.querySelector('#p7-fig canvas'), 'canvas «растущая диаграмма» §2.7');
|
||||
assert.deepEqual(r2.errors, [], 'ch2 без ошибок: ' + r2.errors.join(' | '));
|
||||
// Глава 3 §1 — фильтр множества
|
||||
const r3 = await loadDom('math_6_ch3.html');
|
||||
|
||||
@@ -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 () {} };
|
||||
|
||||
@@ -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:'«9–10»',value:30},{label:'«7–8»',value:45},{label:'«5–6»',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);
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user