From f4ece6f5b11702b0c6042a38912079ba1dc74fe4 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Tue, 2 Jun 2026 21:47:22 +0300 Subject: [PATCH] =?UTF-8?q?feat(math6):=20=D1=82=D0=B5=D1=80=D0=BC=D0=BE?= =?UTF-8?q?=D0=BC=D0=B5=D1=82=D1=80=20(=D0=93=D0=BB.4=20=C2=A71)=20?= =?UTF-8?q?=E2=80=94=20=C2=B1=D1=87=D0=B8=D1=81=D0=BB=D0=B0=20=D0=B8=20?= =?UTF-8?q?=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8C=20=D0=BD=D0=B0=D0=B3=D0=BB?= =?UTF-8?q?=D1=8F=D0=B4=D0=BD=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Math6Anim.thermometer: вертикальный термометр на canvas, ртуть плавно поднимается/опускается к значению (easing), выше нуля — красный, ниже — синий; подпись поясняет знак и |x| как расстояние до нуля. Ползунок −10..10. Вшит в Гл.4 §1. Headless-safe. Тесты math6: 20/20. Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/tests/math6-page.test.js | 2 ++ frontend/js/math6_anim.js | 27 +++++++++++++++++++++++++++ frontend/textbooks/math_6_ch4.html | 7 +++++++ 3 files changed, 36 insertions(+) diff --git a/backend/tests/math6-page.test.js b/backend/tests/math6-page.test.js index 8832898..cbfcc9d 100644 --- a/backend/tests/math6-page.test.js +++ b/backend/tests/math6-page.test.js @@ -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'); diff --git a/frontend/js/math6_anim.js b/frontend/js/math6_anim.js index 2508745..7e44f60 100644 --- a/frontend/js/math6_anim.js +++ b/frontend/js/math6_anim.js @@ -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 () {} }; diff --git a/frontend/textbooks/math_6_ch4.html b/frontend/textbooks/math_6_ch4.html index 337275e..b794a6d 100644 --- a/frontend/textbooks/math_6_ch4.html +++ b/frontend/textbooks/math_6_ch4.html @@ -125,6 +125,10 @@ function buildP1(){ +'
  • $A$ правее $B$, значит $3 > -4$. Любое положительное больше любого отрицательного.
  • '); h+=makeCard('theory','А знаешь ли ты?','1.4', '

    Отрицательные числа официально признали математики лишь в XVII веке — до этого их называли «абсурдными» или «мнимыми долгами». Индийский математик Брахмагупта ещё в VII веке описал правила работы с ними, но в Европе их долго отвергали!

    '); + h+='
    Анимация
    Термометр: тепло и мороз
    ' + +'
    Двигай ползунок. Выше нуля — тепло (красный), ниже — мороз (синий). Модуль $|x|$ — это расстояние числа до нуля.
    ' + +'
    ' + +'
    '; h+='
    Интерактив 1
    Прочитай координату
    ' +'
    Определи координату отмеченной точки (может быть отрицательной).
    ' +'
    Вопрос 1 / 6Очки: 0 / 6
    ' @@ -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='Готово! '+score+' / 6'; if(score>=5){addXp(15,'p1-iv1');bumpProgress('p1',30);}else if(score>=3){addXp(8,'p1-iv1');bumpProgress('p1',16);} return; }