feat(math6): термометр (Гл.4 §1) — ±числа и модуль наглядно

Math6Anim.thermometer: вертикальный термометр на canvas, ртуть плавно
поднимается/опускается к значению (easing), выше нуля — красный, ниже — синий;
подпись поясняет знак и |x| как расстояние до нуля. Ползунок −10..10.
Вшит в Гл.4 §1. 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:47:22 +03:00
parent 8edab2196f
commit f4ece6f5b1
3 changed files with 36 additions and 0 deletions
+2
View File
@@ -184,6 +184,8 @@ test('анимации: canvas-демо монтируются (headless-safe)',
const r4 = await loadDom('math_6_ch4.html');
r4.doc.defaultView.goTo('p4'); await wait(100);
assert.ok(r4.doc.querySelector('#p4-walk canvas'), 'canvas «прыжки по прямой» §4.4');
r4.doc.defaultView.goTo('p1'); await wait(100);
assert.ok(r4.doc.querySelector('#p1-therm-fig canvas'), 'canvas «термометр» §4.1');
assert.deepEqual(r4.errors, [], 'ch4 без ошибок: ' + r4.errors.join(' | '));
// Глава 5 §2: машинка + график
const r5 = await loadDom('math_6_ch5.html');
+27
View File
@@ -278,6 +278,33 @@ M.plotLive = function (host, opts) {
};
};
/* ============================ ДЕМО 7: ТЕРМОМЕТР (±числа, модуль) ============================ */
M.thermometer = function (host, opts) {
opts = opts || {}; var W0 = 220, H0 = 320; var sc = sceneCanvas(host, W0, H0); var cap = caption(host, '');
var st = { v: opts.value != null ? opts.value : 5, target: opts.value != null ? opts.value : 5 };
var MIN = -10, MAX = 10, cx = 64, top = 26, bot = H0 - 56, bulbR = 18;
function Y(v) { return bot - (v - MIN) / (MAX - MIN) * (bot - top); }
function draw() {
var ctx = sc.ctx; if (!ctx) return; st.v += (st.target - st.v) * 0.15;
var mut = cssVar('--muted', '#64748b'), txt = cssVar('--text', '#0f172a');
ctx.clearRect(0, 0, W0, H0);
ctx.strokeStyle = mut; ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(cx - 9, top); ctx.arc(cx, top, 9, Math.PI, 0); ctx.lineTo(cx + 9, bot); ctx.moveTo(cx - 9, top); ctx.lineTo(cx - 9, bot); ctx.stroke();
ctx.beginPath(); ctx.arc(cx, bot + bulbR - 4, bulbR, 0, 2 * Math.PI); ctx.fillStyle = '#fee2e2'; ctx.fill(); ctx.strokeStyle = mut; ctx.stroke();
ctx.font = '11px JetBrains Mono, monospace'; ctx.textAlign = 'left';
for (var v = MIN; v <= MAX; v += 2) { var y = Y(v); ctx.strokeStyle = mut; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(cx + 12, y); ctx.lineTo(cx + 18, y); ctx.stroke(); ctx.fillStyle = (v === 0 ? txt : mut); ctx.fillText(v + '°', cx + 22, y + 4); }
var col = st.v >= 0 ? '#dc2626' : '#2563eb', y0 = Y(0), yv = Y(st.v);
ctx.fillStyle = col; ctx.fillRect(cx - 5, Math.min(y0, yv), 10, Math.abs(yv - y0));
ctx.beginPath(); ctx.arc(cx, bot + bulbR - 4, bulbR - 3, 0, 2 * Math.PI); ctx.fill();
ctx.beginPath(); ctx.arc(cx, yv, 4, 0, 2 * Math.PI); ctx.fill();
ctx.fillStyle = txt; ctx.font = 'bold 17px Inter, sans-serif'; ctx.textAlign = 'center'; ctx.fillText(Math.round(st.v) + '°', cx, H0 - 14);
}
var L = loop(host, draw);
function setCap(v) { if (!cap) return; cap.innerHTML = '$' + v + '°$ — это ' + (v > 0 ? 'тепло, выше нуля' : (v < 0 ? 'мороз, ниже нуля' : 'ровно ноль')) + '. Модуль $|' + v + '| = ' + Math.abs(v) + '$ — это расстояние до нуля.'; if (W.renderMathInElement) try { W.renderMathInElement(cap, { delimiters: [{ left: '$', right: '$', display: false }], throwOnError: false }); } catch (e) {} }
setCap(st.target);
return { stop: L.stop, set: function (v) { st.target = v; setCap(v); } };
};
/* ============================ КОМПОНЕНТ: ПОШАГОВЫЙ ПЛЕЕР (DOM, не canvas) ============================ */
M.stepPlayer = function (host, opts) {
opts = opts || {}; var steps = opts.steps || []; if (!steps.length) return { stop: function () {} };
+7
View File
@@ -125,6 +125,10 @@ function buildP1(){
+'<li>$A$ правее $B$, значит $3 > -4$. Любое положительное больше любого отрицательного.</li></ol>');
h+=makeCard('theory','А знаешь ли ты?','1.4',
'<p>Отрицательные числа официально признали математики лишь в XVII веке — до этого их называли «абсурдными» или «мнимыми долгами». Индийский математик Брахмагупта ещё в VII веке описал правила работы с ними, но в Европе их долго отвергали!</p>');
h+='<div class="wg" id="p1-therm"><div class="wg-header"><span class="wg-badge">Анимация</span><div class="wg-title">Термометр: тепло и мороз</div></div>'
+'<div class="wg-help">Двигай ползунок. Выше нуля — тепло (красный), ниже — мороз (синий). Модуль $|x|$ — это расстояние числа до нуля.</div>'
+'<div class="sliders"><label>Температура = <b id="p1-tv">5</b>°<input type="range" id="p1-ts" min="-10" max="10" value="5"></label></div>'
+'<div id="p1-therm-fig"></div></div>';
h+='<div class="wg" id="p1-iv1"><div class="wg-header"><span class="wg-badge">Интерактив 1</span><div class="wg-title">Прочитай координату</div></div>'
+'<div class="wg-help">Определи координату отмеченной точки (может быть отрицательной).</div>'
+'<div class="score-display"><span>Вопрос <b id="p1-i">1</b> / 6</span><span>Очки: <b id="p1-s">0</b> / 6</span></div>'
@@ -140,6 +144,9 @@ function buildP1(){
h+=secNav(null,'p2')+readBtn('p1');
box.innerHTML=h; renderMath(box);
(function(){ if(!window.Math6Anim) return; var t=Math6Anim.thermometer(document.getElementById('p1-therm-fig'),{value:5});
var sl=document.getElementById('p1-ts'); sl.oninput=function(){ document.getElementById('p1-tv').textContent=sl.value; t.set(+sl.value); }; })();
(function(){
var i=0,score=0,cur=0;
function show(){ if(i>=6){ document.getElementById('p1-fig').innerHTML='<b>Готово!</b> '+score+' / 6'; if(score>=5){addXp(15,'p1-iv1');bumpProgress('p1',30);}else if(score>=3){addXp(8,'p1-iv1');bumpProgress('p1',16);} return; }