From 21c18ce47743c2e291f61c7ee83d63c7f62a7e07 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Tue, 2 Jun 2026 22:13:01 +0300 Subject: [PATCH] =?UTF-8?q?feat(math6):=20=D0=BF=D0=BE=D0=BB=D0=B8=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=BA=D0=B0=20=D0=93=D0=BB.6=20=C2=A73=20=E2=80=94?= =?UTF-8?q?=20=D0=BF=D0=B5=D1=80=D0=B5=D1=82=D0=B0=D1=81=D0=BA=D0=B8=D0=B2?= =?UTF-8?q?=D0=B0=D0=B5=D0=BC=D1=8B=D0=B9=20=D1=82=D1=80=D0=B5=D1=83=D0=B3?= =?UTF-8?q?=D0=BE=D0=BB=D1=8C=D0=BD=D0=B8=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Math6Anim.triangleDrag (SVG): тащишь вершины A/B/C — тип пересчитывается вживую по сторонам и по углам, штрихи равных сторон + метка прямого угла. Блок «Песочница» перед интерактивами §3. Тесты math6: 20/20. Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/tests/math6-page.test.js | 2 ++ frontend/js/math6_anim.js | 46 ++++++++++++++++++++++++++++++ frontend/textbooks/math_6_ch6.html | 5 ++++ 3 files changed, 53 insertions(+) diff --git a/backend/tests/math6-page.test.js b/backend/tests/math6-page.test.js index 9fcce77..23a3df3 100644 --- a/backend/tests/math6-page.test.js +++ b/backend/tests/math6-page.test.js @@ -171,6 +171,8 @@ test('ch6: наглядная геометрия — интерактивы §1 test('анимации: canvas-демо монтируются (headless-safe)', async () => { // Глава 6 §2: колесо + заметание площади const r6 = await loadDom('math_6_ch6.html'); + r6.doc.defaultView.goTo('p3'); await wait(100); + assert.ok(r6.doc.querySelector('#p3-tri svg polygon'), 'svg «перетаскиваемый треугольник» §6.3'); 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'); diff --git a/frontend/js/math6_anim.js b/frontend/js/math6_anim.js index 0c03784..e324568 100644 --- a/frontend/js/math6_anim.js +++ b/frontend/js/math6_anim.js @@ -540,6 +540,52 @@ M.constAreaRect = function (host, opts) { return { stop: L.stop, set: function (w) { st.tw = w; } }; }; +/* ============================ ДЕМО 16: ПЕРЕТАСКИВАЕМЫЙ ТРЕУГОЛЬНИК (SVG) ============================ */ +M.triangleDrag = function (host, opts) { + var ns = 'http://www.w3.org/2000/svg', W0 = 300, H0 = 250; + host.innerHTML = ''; + var svg = D.createElementNS(ns, 'svg'); svg.setAttribute('viewBox', '0 0 ' + W0 + ' ' + H0); svg.setAttribute('width', '100%'); + svg.style.maxWidth = W0 + 'px'; svg.style.height = 'auto'; svg.style.touchAction = 'none'; svg.style.border = '1px solid var(--border,#e2e8f0)'; svg.style.borderRadius = '10px'; svg.style.background = 'var(--card,#fff)'; svg.style.display = 'block'; svg.style.margin = '0 auto'; + host.appendChild(svg); + var label = D.createElement('div'); label.style.cssText = 'text-align:center;font-weight:700;color:var(--pri2,#3730a3);margin-top:8px;font-size:1.02rem'; host.appendChild(label); + var V = [{ x: 150, y: 40 }, { x: 55, y: 200 }, { x: 245, y: 200 }]; + function dist(p, q) { return Math.hypot(p.x - q.x, p.y - q.y); } + function classify() { + var A = V[0], B = V[1], C = V[2], c = dist(A, B), a = dist(B, C), b = dist(C, A), arr = [a, b, c].slice().sort(function (x, y) { return x - y; }), eq = function (u, v) { return Math.abs(u - v) < 12; }; + var side = (eq(a, b) && eq(b, c)) ? 'равносторонний' : ((eq(a, b) || eq(b, c) || eq(a, c)) ? 'равнобедренный' : 'разносторонний'); + var mx = arr[2], rest = arr[0] * arr[0] + arr[1] * arr[1] - mx * mx, angle = Math.abs(rest) < 350 ? 'прямоугольный' : (rest > 0 ? 'остроугольный' : 'тупоугольный'); + var rv = null; if (angle === 'прямоугольный') rv = (mx === a ? A : (mx === b ? B : C)); + var ticks = []; if (side === 'равносторонний') ticks = [[A, B, 1], [B, C, 1], [C, A, 1]]; else if (side === 'равнобедренный') { if (eq(a, b)) ticks = [[B, C, 2], [C, A, 2]]; else if (eq(b, c)) ticks = [[C, A, 2], [A, B, 2]]; else ticks = [[A, B, 2], [B, C, 2]]; } + return { side: side, angle: angle, rv: rv, ticks: ticks }; + } + function render() { + var info = classify(), A = V[0], B = V[1], C = V[2]; + var s = ''; + info.ticks.forEach(function (t) { var P = t[0], Q = t[1], n = t[2], mxp = (P.x + Q.x) / 2, myp = (P.y + Q.y) / 2, dx = Q.x - P.x, dy = Q.y - P.y, L = Math.hypot(dx, dy) || 1, ux = dx / L, uy = dy / L, nx = -uy, ny = ux; for (var k = 0; k < n; k++) { var off = (k - (n - 1) / 2) * 5, cx = mxp + ux * off, cy = myp + uy * off; s += ''; } }); + if (info.rv) { var o1 = info.rv === A ? B : A, o2 = info.rv === C ? B : C, u1x = o1.x - info.rv.x, u1y = o1.y - info.rv.y, l1 = Math.hypot(u1x, u1y) || 1; u1x /= l1; u1y /= l1; var u2x = o2.x - info.rv.x, u2y = o2.y - info.rv.y, l2 = Math.hypot(u2x, u2y) || 1; u2x /= l2; u2y /= l2; var m = 14; s += ''; } + ['A', 'B', 'C'].forEach(function (nm, i) { s += ''; s += '' + nm + ''; }); + svg.innerHTML = s; + label.innerHTML = 'По сторонам: ' + info.side + '  ·  по углам: ' + info.angle + ''; + attach(); + } + function attach() { + var nodes = svg.querySelectorAll('.m6tv'); + for (var n = 0; n < nodes.length; n++) { + (function (c) { + c.addEventListener('pointerdown', function (ev) { + ev.preventDefault(); var i = +c.getAttribute('data-i'); + function pt(e) { var r = svg.getBoundingClientRect(); return { x: Math.max(20, Math.min(W0 - 20, (e.clientX - r.left) / r.width * W0)), y: Math.max(20, Math.min(H0 - 20, (e.clientY - r.top) / r.height * H0)) }; } + function mv(e) { var p = pt(e); V[i].x = p.x; V[i].y = p.y; render(); } + function up() { D.removeEventListener('pointermove', mv); D.removeEventListener('pointerup', up); } + D.addEventListener('pointermove', mv); D.addEventListener('pointerup', up); + }); + })(nodes[n]); + } + } + render(); + return { stop: function () {} }; +}; + /* ============================ КОМПОНЕНТ: ПОШАГОВЫЙ ПЛЕЕР (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_ch6.html b/frontend/textbooks/math_6_ch6.html index aaa46c5..67d909b 100644 --- a/frontend/textbooks/math_6_ch6.html +++ b/frontend/textbooks/math_6_ch6.html @@ -323,6 +323,9 @@ function buildP3(){ +''); h+=makeCard('theory','А знаешь ли ты?','3.2t', '

Треугольник со сторонами $3$, $4$, $5$ называют «египетским» — строители Древнего Египта натягивали верёвку с 12 узлами (3+4+5) в виде треугольника, чтобы получить идеальный прямой угол для кладки стен пирамид. Этим приёмом пользуются строители до сих пор!

'); + h+='
Песочница
Тащи вершины — тип меняется вживую
' + +'
Перетаскивай вершины $A$, $B$, $C$. Штрихи отмечают равные стороны, красный уголок — прямой угол. Сделай равносторонний, прямоугольный или тупоугольный треугольник.
' + +'
'; h+='
Интерактив 1
Вид по сторонам
' +'
Определи вид треугольника по сторонам (штрихи отмечают равные стороны).
' +'
Вопрос 1 / 5Очки: 0 / 5
' @@ -342,6 +345,8 @@ function buildP3(){ h+=secNav('p2','p4')+readBtn('p3'); box.innerHTML=h; renderMath(box); + if(window.Math6Anim&&Math6Anim.triangleDrag){ try{ Math6Anim.triangleDrag(document.getElementById('p3-tri')); }catch(e){} } + setupSorter('p3-sorter',{ items:['Равносторонний','Равнобедренный','Разносторонний','Остроугольный','Прямоугольный','Тупоугольный'], groups:['По сторонам','По углам'],