diff --git a/frontend/textbooks/algebra_8.html b/frontend/textbooks/algebra_8.html index cb960e5..141c070 100644 --- a/frontend/textbooks/algebra_8.html +++ b/frontend/textbooks/algebra_8.html @@ -466,6 +466,12 @@ input,select,textarea{font-family:inherit} @keyframes simpPop{0%{transform:scale(1)}50%{transform:scale(1.1)}100%{transform:scale(1)}} @keyframes simpShake{0%,100%{transform:translateX(0)}25%{transform:translateX(-6px)}75%{transform:translateX(6px)}} +/* Геометрическое доказательство §3 */ +.geo-canvas-wrap{background:var(--card);border:1px solid var(--border);border-radius:11px;padding:8px;margin-top:14px} +.geo-cell{filter:drop-shadow(0 1px 1px rgba(0,0,0,.12))} +.geo-formula{text-align:center;font-size:.98rem;line-height:2;padding:10px 14px;background:linear-gradient(135deg,var(--acc-soft),var(--pri-soft));border-radius:9px;border:1px solid var(--border)} +.proof-badge{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:var(--ok);color:#fff;border-radius:99px;font-size:.85rem;font-weight:700} + /* Конвейер x → x² → √(x²) в §1 */ .dual-pipeline{display:flex;align-items:center;justify-content:space-between;gap:6px;flex-wrap:wrap;margin-top:14px;padding:14px 8px;background:var(--card);border-radius:12px;border:1px solid var(--border)} .dual-step{flex:1;min-width:90px;text-align:center;padding:10px 8px;border-radius:10px;background:rgba(233,30,99,0.04);border:1.5px solid var(--border)} @@ -2595,38 +2601,30 @@ function buildP3(){

Пример: $\\sqrt{144 \\cdot 625} = \\sqrt{144} \\cdot \\sqrt{625} = 12 \\cdot 25 = 300$.

`)} - ${widget('Геометрическое доказательство √(a·b) = √a · √b', 'VISUAL', 'Прямоугольник a × b имеет ту же площадь, что и квадрат со стороной √(ab). Меняйте a и b — площади всегда совпадают.', ` -
- a = - - 4 - b = - - 9 -
-
-
-
Прямоугольник a × b
- - - S = a·b - -
S = 36
+ ${widget('Геометрическое доказательство √(a·b) = √a·√b', 'VISUAL', 'Прямоугольник a × b разбивается на a·b единичных клеток. Те же клетки могут собраться в квадрат со стороной √(a·b). Нажмите «Анимировать» — увидите как клетки перетекают.', ` +
+
+ a = + + 4
-
-
Квадрат со стороной √(ab)
- - - S = (√ab)² - -
сторона = 6.00
+
+ b = + + 9
-

Площади всегда равны → $\\sqrt{ab}$ — сторона эквивалентного квадрата

+
+ +
+
- + +
-
`)} ${makeCard('rule','Свойство 2: корень из частного',null,` @@ -2744,67 +2742,162 @@ function buildP3(){ } /* ──── Geometric proof √(ab) ──── */ +/* ──── Geometric proof √(ab)=√a·√b — анимация клеток ──── */ +const GEO_STATE = { animating: false }; + function initGeoProof(){ const a = document.getElementById('geo-a'); if(!a) return; const b = document.getElementById('geo-b'); function upd(){ - const av = +a.value, bv = +b.value; - document.getElementById('geo-a-v').textContent = av; - document.getElementById('geo-b-v').textContent = bv; - const r = document.getElementById('geo-rect'); - // rect: width ~ a, height ~ b (scaled) - const maxDim = 160, base = Math.max(av, bv); - r.setAttribute('width', av/base*maxDim); - r.setAttribute('height', bv/base*maxDim*0.6); - const sq = document.getElementById('geo-sq'); - const s = Math.sqrt(av * bv); - sq.setAttribute('width', s/base*maxDim); - sq.setAttribute('height', s/base*maxDim); - document.getElementById('geo-rect-s').textContent = (av*bv).toFixed(0); - document.getElementById('geo-side').textContent = s.toFixed(2); + if(GEO_STATE.animating) return; + geoRenderRect(); } a.addEventListener('input', upd); b.addEventListener('input', upd); - upd(); + geoRenderRect(); } -/* ──── Geo Proof Animation ──── */ -function geoProofAnimate(){ - const box = document.getElementById('geo-proof-anim'); - if(!box) return; - const aVal = +(document.getElementById('geo-a') ? document.getElementById('geo-a').value : 4); - const bVal = +(document.getElementById('geo-b') ? document.getElementById('geo-b').value : 9); - const ab = aVal * bVal; - const side = Math.sqrt(ab).toFixed(2); - const steps = [ - { delay:0, html: `Шаг 1. Прямоугольник ${aVal} × ${bVal} подсвечивается рамкой — его площадь S = a·b = ${ab}` }, - { delay:900, html: `Шаг 2. Прямоугольник можно представить как ${ab} единичных клеток (${aVal} строк × ${bVal} столбцов)` }, - { delay:2000, html: `Шаг 3. Все ${ab} клеток «перетекают» в квадрат — ищем сторону, при которой сторона² = ${ab}` }, - { delay:3200, html: `Шаг 4. Квадрат со стороной √${ab} ≈ ${side} имеет ту же площадь: S = (√${ab})² = ${ab}` }, - { delay:4400, html: ` Доказано!   √(${aVal}·${bVal}) = √${aVal}·√${bVal} = ${Math.sqrt(aVal).toFixed(2)}·${Math.sqrt(bVal).toFixed(2)} = ${side}` }, - ]; - const rectEl = document.getElementById('geo-rect'); - const sqEl = document.getElementById('geo-sq'); - // highlight steps visually - if(rectEl){ - rectEl.style.transition = 'stroke-width .3s'; - rectEl.setAttribute('stroke-width','4'); - setTimeout(()=>rectEl.setAttribute('stroke-width','2'), 900); - } - if(sqEl){ - setTimeout(()=>{ - sqEl.style.transition = 'stroke-width .3s'; - sqEl.setAttribute('stroke-width','4'); - setTimeout(()=>sqEl.setAttribute('stroke-width','2'), 800); - }, 3200); - } - steps.forEach(s=>{ - setTimeout(()=>{ box.innerHTML = s.html; renderMath(box); }, s.delay); - }); - setTimeout(()=>bumpProgress('p3', 5), 4500); +function geoCurrentAB(){ + const a = +document.getElementById('geo-a').value; + const b = +document.getElementById('geo-b').value; + return [a, b]; } +function geoRenderRect(){ + const [aVal, bVal] = geoCurrentAB(); + document.getElementById('geo-a-v').textContent = aVal; + document.getElementById('geo-b-v').textContent = bVal; + const svg = document.getElementById('geo-svg'); + if(!svg) return; + svg.innerHTML = ''; + const ab = aVal * bVal; + const sqSide = Math.sqrt(ab); + // sizing: cell size based on max dimensions + const W = 600, H = 280; + const halfW = 290, gap = 20; + const maxCol = Math.max(aVal, Math.ceil(sqSide)); + const maxRow = Math.max(bVal, Math.ceil(sqSide)); + const cell = Math.floor(Math.min(halfW/maxCol, (H-50)/maxRow, 30)); + const rectW = aVal * cell, rectH = bVal * cell; + const rectX = (halfW - rectW) / 2; + const rectY = (H - 30 - rectH) / 2; + const sqW = sqSide * cell; + const sqX = halfW + gap + (halfW - sqW) / 2; + const sqY = (H - 30 - sqW) / 2; + // Labels + svg.appendChild(svgEl('text', {x:halfW/2, y:H-8, 'text-anchor':'middle', fill:'#03a9f4', 'font-size':14, 'font-weight':700, 'font-family':'Inter,sans-serif'}, `${aVal} × ${bVal} = ${ab} клеток`)); + svg.appendChild(svgEl('text', {x:halfW + gap + halfW/2, y:H-8, 'text-anchor':'middle', fill:'#e91e63', 'font-size':14, 'font-weight':700, 'font-family':'Inter,sans-serif'}, `сторона ≈ ${sqSide.toFixed(2)} → S = ${ab}`)); + // Arrow between + svg.appendChild(svgEl('path', {d:`M ${halfW + 2} ${H/2 - 18} L ${halfW + gap - 2} ${H/2 - 18}`, stroke:'#7c3aed', 'stroke-width':2.5, fill:'none'})); + svg.appendChild(svgEl('polyline', {points:`${halfW + gap - 10},${H/2 - 24} ${halfW + gap - 2},${H/2 - 18} ${halfW + gap - 10},${H/2 - 12}`, stroke:'#7c3aed', 'stroke-width':2.5, fill:'none'})); + // Hint outline rectangle on the right (where square would be) + svg.appendChild(svgEl('rect', {x:sqX, y:sqY, width:sqW, height:sqW, fill:'none', stroke:'#e91e63', 'stroke-width':2, 'stroke-dasharray':'4,4', opacity:0.5})); + // Cells in rectangle layout + for(let i = 0; i < ab; i++){ + const col = i % aVal; + const row = Math.floor(i / aVal); + const cellEl = svgEl('rect', { + class:'geo-cell', + 'data-i':i, + x: rectX + col*cell + 1, + y: rectY + row*cell + 1, + width: cell - 2, + height: cell - 2, + fill: i % 2 === 0 ? '#03a9f4' : '#0288d1', + opacity: 0.85, + rx: 2, + }); + cellEl.style.transition = 'x .55s cubic-bezier(.4,1.3,.5,1), y .55s cubic-bezier(.4,1.3,.5,1), fill .3s'; + svg.appendChild(cellEl); + } + // Stash layout info + GEO_STATE.cell = cell; + GEO_STATE.rectX = rectX; GEO_STATE.rectY = rectY; + GEO_STATE.sqX = sqX; GEO_STATE.sqY = sqY; + GEO_STATE.sqSide = sqSide; + GEO_STATE.aVal = aVal; GEO_STATE.bVal = bVal; GEO_STATE.ab = ab; + // Formula + geoUpdateFormula(false); +} + +function svgEl(tag, attrs, text){ + const e = document.createElementNS('http://www.w3.org/2000/svg', tag); + if(attrs) for(const k in attrs) e.setAttribute(k, attrs[k]); + if(text != null) e.textContent = text; + return e; +} + +function geoUpdateFormula(done){ + const fb = document.getElementById('geo-formula'); + if(!fb) return; + const a = GEO_STATE.aVal, b = GEO_STATE.bVal, ab = GEO_STATE.ab; + const ra = Math.sqrt(a), rb = Math.sqrt(b); + const rab = Math.sqrt(ab); + const aRound = Number.isInteger(ra) ? ra : ra.toFixed(2); + const bRound = Number.isInteger(rb) ? rb : rb.toFixed(2); + const abRound = Number.isInteger(rab) ? rab : rab.toFixed(2); + fb.innerHTML = `$$\\sqrt{${a} \\cdot ${b}} = \\sqrt{${ab}} = ${abRound}$$ + $$\\sqrt{${a}} \\cdot \\sqrt{${b}} = ${aRound} \\cdot ${bRound} = ${abRound}$$` + + (done ? `
Доказано: $\\sqrt{${a} \\cdot ${b}} = \\sqrt{${a}} \\cdot \\sqrt{${b}}$
` : ''); + renderMath(fb); +} + +async function geoProofAnimate(){ + if(GEO_STATE.animating) return; + GEO_STATE.animating = true; + const btn = document.getElementById('geo-play-btn'); + if(btn) btn.disabled = true; + const svg = document.getElementById('geo-svg'); + if(!svg){ GEO_STATE.animating = false; if(btn) btn.disabled = false; return; } + const cells = [...svg.querySelectorAll('.geo-cell')]; + const cell = GEO_STATE.cell; + const sqX = GEO_STATE.sqX, sqY = GEO_STATE.sqY; + const sqSide = GEO_STATE.sqSide; + // Шаг 1: волна подсветки прямоугольника + for(let i = 0; i < cells.length; i++){ + cells[i].setAttribute('fill', '#fbbf24'); + cells[i].setAttribute('opacity', '1'); + await sleep(20); + } + await sleep(280); + // Шаг 2: клетки летят в квадрат + // если ab - точный квадрат, плотная упаковка + const isSq = Number.isInteger(sqSide); + const cols = isSq ? sqSide : Math.ceil(sqSide); + const targetCellSize = isSq ? cell : (sqSide * cell) / cols; + for(let i = 0; i < cells.length; i++){ + const col = i % cols; + const row = Math.floor(i / cols); + cells[i].setAttribute('x', sqX + col*targetCellSize + 1); + cells[i].setAttribute('y', sqY + row*targetCellSize + 1); + cells[i].setAttribute('width', targetCellSize - 2); + cells[i].setAttribute('height', targetCellSize - 2); + cells[i].setAttribute('fill', i % 2 === 0 ? '#e91e63' : '#c2185b'); + await sleep(35); + } + await sleep(500); + // Шаг 3: пульс на финальном квадрате + cells.forEach(c => { c.setAttribute('opacity', '1'); }); + await sleep(100); + geoUpdateFormula(true); + bumpProgress('p3', 6); + addXp(10, 'geo-proof'); + confetti(); + await sleep(2400); + GEO_STATE.animating = false; + if(btn) btn.disabled = false; +} + +function geoProofReset(){ + GEO_STATE.animating = false; + const btn = document.getElementById('geo-play-btn'); + if(btn) btn.disabled = false; + geoRenderRect(); +} + +function sleep(ms){ return new Promise(r => setTimeout(r, ms)); } + /* ──── Property check slider ──── */ function initPropCheck(){ const a = document.getElementById('prop-a');