/* alg7-fx.js — универсальные эффекты для всех глав Алгебры 7 * Подключается всеми ch1..ch4. Автоматически: * - наблюдает за элементами .feedback, при появлении .ok → pulse + комбо, * при .fail → shake + сброс комбо * - показывает значок комбо при сериях 3/5/10 правильных подряд * - даёт бонусный XP через global addXp(), если он определён * * Публичный API на window.ALG7: * ALG7.shake(el), ALG7.pulse(el) * ALG7.combo, ALG7.maxCombo, ALG7.resetCombo() * ALG7.buildQuadSumViz(container) — виз. квадрата суммы (Ch2 §12) * ALG7.buildDiffSquaresViz(container)— виз. разности квадратов (Ch2 §13) */ (function(){ 'use strict'; if (window.ALG7 && window.ALG7.__installed) return; const ALG7 = window.ALG7 = window.ALG7 || {}; ALG7.__installed = true; ALG7.combo = 0; ALG7.maxCombo = 0; /* === KATEX HELPER (использует те же делимитеры, что и страницы) === */ const _KATEX_OPTS = { delimiters: [ { left:'$$', right:'$$', display:true }, { left:'$', right:'$', display:false }, { left:'\\[', right:'\\]', display:true }, { left:'\\(', right:'\\)', display:false } ], throwOnError: false }; ALG7.renderMath = function(root){ if(!root || !window.renderMathInElement) return; try{ window.renderMathInElement(root, _KATEX_OPTS); }catch(e){} }; /* === ANIMATIONS === */ ALG7.shake = function(el){ if(!el) return; el.classList.remove('alg7-shake'); void el.offsetWidth; el.classList.add('alg7-shake'); setTimeout(()=>el.classList.remove('alg7-shake'), 600); }; ALG7.pulse = function(el){ if(!el) return; el.classList.remove('alg7-pulse'); void el.offsetWidth; el.classList.add('alg7-pulse'); setTimeout(()=>el.classList.remove('alg7-pulse'), 950); }; ALG7.resetCombo = function(silent){ if(ALG7.combo > 0 && !silent){ /* fade streak indicator */ const s = document.querySelector('.alg7-streak'); if(s){ s.style.opacity = '0'; setTimeout(()=>{ s.classList.remove('show'); s.style.opacity = ''; }, 300); } } ALG7.combo = 0; }; function _ensureStreak(){ let s = document.querySelector('.alg7-streak'); if(!s){ s = document.createElement('div'); s.className = 'alg7-streak'; s.innerHTML = '3'; document.body.appendChild(s); } return s; } function _showStreak(n){ const s = _ensureStreak(); s.classList.add('show'); s.querySelector('.streak-text').textContent = '×' + n; } function _showComboBadge(n, bonusXp){ const badge = document.createElement('div'); badge.className = 'alg7-combo-badge'; badge.innerHTML = ' КОМБО ×' + n + ' +' + bonusXp + ' XP'; /* position center top-ish */ const vw = window.innerWidth, vh = window.innerHeight; badge.style.left = (vw / 2 - 130) + 'px'; badge.style.top = (vh / 2 - 80) + 'px'; document.body.appendChild(badge); setTimeout(()=>badge.remove(), 2300); } function _addSparkles(el){ if(!el) return; const r = el.getBoundingClientRect(); for(let i = 0; i < 6; i++){ const sp = document.createElement('div'); sp.className = 'alg7-sparkle'; sp.style.left = (r.left + r.width * Math.random()) + 'px'; sp.style.top = (r.top + r.height * Math.random()) + 'px'; sp.style.position = 'fixed'; sp.style.zIndex = '9999'; sp.style.animationDelay = (i * 60) + 'ms'; document.body.appendChild(sp); setTimeout(()=>sp.remove(), 1000); } } ALG7.onCorrect = function(elm){ ALG7.combo++; if(ALG7.combo > ALG7.maxCombo) ALG7.maxCombo = ALG7.combo; if(elm) { ALG7.pulse(elm); _addSparkles(elm); } if(ALG7.combo >= 3) _showStreak(ALG7.combo); /* милестоны: 3, 5, 10 — бонусные XP */ const milestones = { 3:5, 5:15, 10:50, 15:75, 20:100 }; if(milestones[ALG7.combo]){ const bonus = milestones[ALG7.combo]; _showComboBadge(ALG7.combo, bonus); if(typeof window.addXp === 'function') window.addXp(bonus, 'combo-' + ALG7.combo); } }; ALG7.onWrong = function(elm){ if(elm) ALG7.shake(elm); ALG7.resetCombo(); }; /* === АВТО-ХУК НА FEEDBACK ЧЕРЕЗ MUTATIONOBSERVER === */ const _seenFeedback = new WeakMap(); function _processFeedback(el){ const display = window.getComputedStyle(el).display; if(display === 'none') return; const isOk = el.classList.contains('ok'); const isFail = el.classList.contains('fail'); if(!isOk && !isFail) return; /* Дебаунс — каждый элемент обрабатываем не чаще раза в 700мс */ const now = Date.now(); const last = _seenFeedback.get(el) || 0; if(now - last < 700) return; _seenFeedback.set(el, now); /* Найти ближайший .wg или родительскую карточку */ const target = el.closest('.wg') || el.closest('.card') || el.parentElement; if(isOk) ALG7.onCorrect(target); else ALG7.onWrong(target); } function _initObserver(){ if(!document.body) { setTimeout(_initObserver, 50); return; } const obs = new MutationObserver(muts => { for(const m of muts){ if(m.type === 'attributes'){ const t = m.target; if(t && t.classList && t.classList.contains('feedback')) _processFeedback(t); } else if(m.type === 'childList'){ m.addedNodes && m.addedNodes.forEach(n => { if(n.nodeType === 1 && n.classList && n.classList.contains('feedback')) _processFeedback(n); }); } } }); obs.observe(document.body, { subtree:true, attributes:true, childList:true, attributeFilter:['class','style'] }); } if(document.readyState === 'loading') document.addEventListener('DOMContentLoaded', _initObserver); else _initObserver(); /* ============================================================ ВИЗУАЛИЗАТОР КВАДРАТА СУММЫ (Ch2 §12) Геометрическое доказательство (a+b)² = a² + 2ab + b² ============================================================ */ ALG7.buildQuadSumViz = function(container){ if(!container) return; container.innerHTML = '' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '' + '
' + '
Кликни цветную область — она «подсветится» в формуле
' + '
'; const aSl = container.querySelector('#alg7-qsum-a-sl'); const bSl = container.querySelector('#alg7-qsum-b-sl'); const aV = container.querySelector('#alg7-qsum-a'); const bV = container.querySelector('#alg7-qsum-b'); const svg = container.querySelector('#alg7-qsum-svg'); const formula = container.querySelector('#alg7-qsum-formula'); const modeButtons = container.querySelectorAll('.alg7-qsum-mode button'); let mode = 'sum'; function render(){ const a = +aSl.value; const b = +bSl.value; aV.textContent = a; bV.textContent = b; const isSum = mode === 'sum'; /* Масштаб: общая сторона = a + b (для sum) или a (для diff) */ const side = isSum ? (a + b) : a; const PX = 300; /* размер квадрата в px */ const offX = 10, offY = 10; const unit = PX / side; const aPx = a * unit; const bPx = b * unit; let html = ''; if(isSum){ /* Большой квадрат (a+b) × (a+b) разбит на 4 области: a×a (top-left), a×b (top-right), b×a (bottom-left), b×b (bottom-right) */ /* 1. a² (top-left, красный) */ html += ''; /* 2. a*b (top-right, оранжевый) */ html += ''; /* 3. b*a (bottom-left, оранжевый) */ html += ''; /* 4. b² (bottom-right, зелёный) */ html += ''; /* Подписи */ if(aPx > 32) html += 'a²='+(a*a)+''; if(bPx > 22 && aPx > 28) html += 'ab='+(a*b)+''; if(bPx > 22 && aPx > 28) html += 'ab='+(a*b)+''; if(bPx > 32) html += 'b²='+(b*b)+''; /* Стороны (внешняя разметка) */ html += 'a='+a+''; html += 'b='+b+''; html += 'сторона = a+b = '+(a+b)+''; /* Скобка снизу */ html += ''; html += ''; html += ''; /* Формула */ formula.innerHTML = '$(' + a + ' + ' + b + ')^2 = ' + '\\underbrace{' + a + '^2}_{\\color{#dc2626}{' + (a*a) + '}} + ' + '\\underbrace{2 \\cdot ' + a + ' \\cdot ' + b + '}_{\\color{#d97706}{' + (2*a*b) + '}} + ' + '\\underbrace{' + b + '^2}_{\\color{#047857}{' + (b*b) + '}} = ' + ((a+b)*(a+b)) + '$'; } else { /* DIFF mode: показываем (a-b)² внутри a×a, выделяя верхний-левый угол */ if(b >= a){ html += '$b \\ge a$ — выбери $b < a$'; formula.innerHTML = 'Условие: $b < a$ для $(a-b)^2$'; } else { const diff = a - b; const dPx = diff * unit; /* (a-b)² в углу (красный) */ html += ''; /* Полосы 2ab внутри */ html += ''; html += ''; /* b² в углу */ html += ''; /* Подписи */ if(dPx > 30) html += '(a-b)²='+(diff*diff)+''; if(bPx > 22 && dPx > 25) html += ''+(diff*b)+''; if(bPx > 22 && dPx > 25) html += ''+(diff*b)+''; if(bPx > 30) html += 'b²='+(b*b)+''; html += 'сторона a='+a+'; b='+b+'; a−b='+diff+''; /* Формула */ formula.innerHTML = '$(' + a + ' - ' + b + ')^2 = a^2 - 2ab + b^2 = ' + a + '^2 - 2 \\cdot ' + a + ' \\cdot ' + b + ' + ' + b + '^2 = ' + (a*a) + ' - ' + (2*a*b) + ' + ' + (b*b) + ' = ' + (diff*diff) + '$'; } } svg.innerHTML = html; ALG7.renderMath(formula); /* Hover-эффект: при клике подсветить термин */ svg.querySelectorAll('rect[data-part]').forEach(r=>{ r.style.cursor = 'pointer'; r.addEventListener('click', ()=>{ const part = r.dataset.part; svg.querySelectorAll('rect.area-aa, rect.area-ab, rect.area-bb').forEach(x=>x.classList.add('dim')); const sameClass = part==='aa' ? 'area-aa' : (part==='bb' ? 'area-bb' : 'area-ab'); svg.querySelectorAll('.' + sameClass).forEach(x=>x.classList.remove('dim')); setTimeout(()=>{ svg.querySelectorAll('rect.area-aa, rect.area-ab, rect.area-bb').forEach(x=>x.classList.remove('dim')); }, 1500); }); }); } aSl.addEventListener('input', render); bSl.addEventListener('input', render); modeButtons.forEach(btn => btn.addEventListener('click', ()=>{ modeButtons.forEach(b=>b.classList.remove('active')); btn.classList.add('active'); mode = btn.dataset.mode; render(); })); render(); }; /* ============================================================ ВИЗУАЛИЗАТОР РАЗНОСТИ КВАДРАТОВ (Ch2 §13) a² - b² = (a-b)(a+b) — анимация перестроения ============================================================ */ ALG7.buildDiffSquaresViz = function(container){ if(!container) return; container.innerHTML = '' + '
' + '
' + '' + '' + '
' + '' + '
' + '' + '
'; const aSl = container.querySelector('#alg7-dsq-a-sl'); const bSl = container.querySelector('#alg7-dsq-b-sl'); const aV = container.querySelector('#alg7-dsq-a'); const bV = container.querySelector('#alg7-dsq-b'); const svg = container.querySelector('#alg7-dsq-svg'); const formula = container.querySelector('#alg7-dsq-formula'); const stepBtn = container.querySelector('#alg7-dsq-step'); const stepText = container.querySelector('#alg7-dsq-step-text'); let stage = 0; /* 0 = большой a² с вырезанной b²; 1 = две части L-формы; 2 = собранный прямоугольник */ function render(){ const a = +aSl.value; const b = +bSl.value; aV.textContent = a; bV.textContent = b; if(b >= a){ svg.innerHTML = 'Выбери $b < a$'; formula.innerHTML = 'Условие: $b < a$'; stepBtn.disabled = true; return; } stepBtn.disabled = false; const diff = a - b; const sum = a + b; /* Площади 200x200 для исходного, 200x200 для финального — рядом */ const PX = 200; const unit = PX / Math.max(a, sum); /* единичная клетка */ const aPx = a * unit; const bPx = b * unit; const dPx = diff * unit; const sumPx = sum * unit; const offX = 20, offY = 30; const off2X = 250, off2Y = 30; let html = ''; /* === ЭТАП 0 — исходное состояние === */ /* Левая сторона: большой квадрат a² */ html += 'a² (сторона a='+a+')'; /* Большой квадрат (рыжий) */ html += ''; /* Голубой квадрат b² в правом верхнем углу */ if(stage >= 0){ html += ''; html += 'b²='+(b*b)+''; } /* Подпись большой стороны */ html += 'a²='+(a*a)+''; /* === ЭТАП 1+ — L-форма (большой квадрат минус b²) разделена на 2 прямоугольника === */ /* L-форма состоит из двух частей: - Нижний прямоугольник: a × (a-b) = a × diff - Верхний-левый прямоугольник: (a-b) × b = diff × b Они собираются в прямоугольник (a-b) × (a+b) */ /* Этап 1: показываем линию разреза в исходном квадрате */ if(stage === 1){ const cutY = offY + dPx; /* горизонтальная линия разреза на высоте a-b */ html += ''; html += 'разрез'; } /* Этап 2: показываем «улетающие» части и финальный прямоугольник справа */ if(stage === 2){ /* Призрак исходного квадрата */ html += ''; /* Финальный прямоугольник (a-b) × (a+b) справа */ html += '(a-b)(a+b) = '+(diff*sum)+''; /* Часть 1: нижний прямоугольник a × (a-b) — поворачиваем в финале на правую часть */ html += ''; html += 'a·(a-b)='+(a*diff)+''; /* Часть 2: верхний-левый прямоугольник (a-b) × b — приклеиваем к правому концу */ html += ''; html += 'b(a-b)='+(b*diff)+''; /* Подписи сторон финального прямоугольника */ html += 'a+b='+sum+''; html += 'a-b='+diff+''; /* Линия разделения */ html += ''; } svg.innerHTML = html; /* Формула */ let formulaText; if(stage === 0){ formulaText = '$a^2 - b^2 = ' + a + '^2 - ' + b + '^2 = ' + (a*a) + ' - ' + (b*b) + ' = ' + (a*a - b*b) + '$'; stepText.textContent = 'Шаг 1: разрезать L-форму'; } else if(stage === 1){ formulaText = 'Разрезаем L-форму горизонтально: $a \\cdot (a-b)$ + $(a-b) \\cdot b$'; stepText.textContent = 'Шаг 2: собрать в прямоугольник'; } else { formulaText = '$a^2 - b^2 = (a-b)(a+b) = ' + diff + ' \\cdot ' + sum + ' = ' + (diff*sum) + '$ ✓'; stepText.textContent = 'Сбросить'; } formula.innerHTML = formulaText; ALG7.renderMath(formula); } stepBtn.addEventListener('click', ()=>{ stage = (stage + 1) % 3; render(); }); aSl.addEventListener('input', ()=>{ stage = 0; render(); }); bSl.addEventListener('input', ()=>{ stage = 0; render(); }); render(); }; })();