feat(math6): полировка Гл.6 §3 — перетаскиваемый треугольник
Math6Anim.triangleDrag (SVG): тащишь вершины A/B/C — тип пересчитывается вживую по сторонам и по углам, штрихи равных сторон + метка прямого угла. Блок «Песочница» перед интерактивами §3. Тесты math6: 20/20. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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');
|
||||
|
||||
@@ -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 = '<polygon points="' + V.map(function (p) { return p.x + ',' + p.y; }).join(' ') + '" fill="rgba(217,119,6,0.12)" stroke="#d97706" stroke-width="2.5" stroke-linejoin="round"/>';
|
||||
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 += '<line x1="' + (cx + nx * 6) + '" y1="' + (cy + ny * 6) + '" x2="' + (cx - nx * 6) + '" y2="' + (cy - ny * 6) + '" stroke="#d97706" stroke-width="2"/>'; } });
|
||||
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 += '<polyline points="' + (info.rv.x + u1x * m) + ',' + (info.rv.y + u1y * m) + ' ' + (info.rv.x + u1x * m + u2x * m) + ',' + (info.rv.y + u1y * m + u2y * m) + ' ' + (info.rv.x + u2x * m) + ',' + (info.rv.y + u2y * m) + '" fill="none" stroke="#dc2626" stroke-width="1.6"/>'; }
|
||||
['A', 'B', 'C'].forEach(function (nm, i) { s += '<circle class="m6tv" data-i="' + i + '" cx="' + V[i].x + '" cy="' + V[i].y + '" r="9" fill="var(--card,#fff)" stroke="#d97706" stroke-width="2.5" style="cursor:grab"/>'; s += '<text x="' + (V[i].x + (i === 0 ? 0 : (i === 1 ? -16 : 16))) + '" y="' + (V[i].y + (i === 0 ? -14 : 20)) + '" font-size="14" font-weight="800" fill="#b45309" text-anchor="middle">' + nm + '</text>'; });
|
||||
svg.innerHTML = s;
|
||||
label.innerHTML = 'По сторонам: <b>' + info.side + '</b> · по углам: <b>' + info.angle + '</b>';
|
||||
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 () {} };
|
||||
|
||||
@@ -323,6 +323,9 @@ function buildP3(){
|
||||
+'</ol>');
|
||||
h+=makeCard('theory','А знаешь ли ты?','3.2t',
|
||||
'<p>Треугольник со сторонами $3$, $4$, $5$ называют «египетским» — строители Древнего Египта натягивали верёвку с 12 узлами (3+4+5) в виде треугольника, чтобы получить идеальный прямой угол для кладки стен пирамид. Этим приёмом пользуются строители до сих пор!</p>');
|
||||
h+='<div class="wg" id="p3-play"><div class="wg-header"><span class="wg-badge">Песочница</span><div class="wg-title">Тащи вершины — тип меняется вживую</div></div>'
|
||||
+'<div class="wg-help">Перетаскивай вершины $A$, $B$, $C$. Штрихи отмечают равные стороны, красный уголок — прямой угол. Сделай равносторонний, прямоугольный или тупоугольный треугольник.</div>'
|
||||
+'<div id="p3-tri"></div></div>';
|
||||
h+='<div class="wg" id="p3-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="p3-i">1</b> / 5</span><span>Очки: <b id="p3-s">0</b> / 5</span></div>'
|
||||
@@ -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:['По сторонам','По углам'],
|
||||
|
||||
Reference in New Issue
Block a user