diff --git a/frontend/css/alg7-fx.css b/frontend/css/alg7-fx.css new file mode 100644 index 0000000..ff8a588 --- /dev/null +++ b/frontend/css/alg7-fx.css @@ -0,0 +1,290 @@ +/* alg7-fx.css — UX-эффекты для Алгебры 7 (все главы) + shake + pulse + combo badge + visual constructors styles */ + +/* === ANIMATIONS === */ +@keyframes alg7-shake { + 0%,100% { transform: translateX(0); } + 10%,30%,50%,70%,90% { transform: translateX(-6px); } + 20%,40%,60%,80% { transform: translateX(6px); } +} +@keyframes alg7-pulse { + 0% { box-shadow: 0 0 0 0 rgba(16,185,129,.55); } + 50% { box-shadow: 0 0 0 12px rgba(16,185,129,0); } + 100% { box-shadow: 0 0 0 0 rgba(16,185,129,0); } +} +@keyframes alg7-glow { + 0% { filter: drop-shadow(0 0 0 transparent); } + 50% { filter: drop-shadow(0 0 12px var(--sec-acc, #10b981)); } + 100% { filter: drop-shadow(0 0 0 transparent); } +} +@keyframes alg7-combo-pop { + 0% { transform: scale(.4) translateY(20px); opacity: 0; } + 40% { transform: scale(1.18) translateY(0); opacity: 1; } + 60% { transform: scale(1) translateY(0); } + 90% { transform: scale(1) translateY(0); opacity: 1; } + 100% { transform: scale(.85) translateY(-30px); opacity: 0; } +} +@keyframes alg7-sparkle { + 0%,100% { opacity: 0; transform: scale(0); } + 50% { opacity: 1; transform: scale(1); } +} +@keyframes alg7-area-in { + from { opacity: 0; transform: scale(.5); transform-origin: 0 0; } + to { opacity: 1; transform: scale(1); } +} +@keyframes alg7-slide-out { + 0% { transform: translate(0,0); } + 100% { transform: translate(var(--alg7-dx, 100px), var(--alg7-dy, 0)); } +} + +/* === STATE CLASSES === */ +.alg7-shake { animation: alg7-shake .52s cubic-bezier(.36,.07,.19,.97) both; } +.alg7-pulse { animation: alg7-pulse .85s ease-out; } +.alg7-glow { animation: alg7-glow 1.1s ease-out; } + +/* === COMBO BADGE === */ +.alg7-combo-badge { + position: fixed; + z-index: 9998; + pointer-events: none; + display: flex; + align-items: center; + gap: 8px; + padding: 12px 20px; + background: linear-gradient(135deg, #f59e0b, #ef4444, #f59e0b); + background-size: 200% 100%; + color: #fff; + border-radius: 999px; + font-family: 'Unbounded', 'Inter', sans-serif; + font-weight: 900; + font-size: 1.1rem; + letter-spacing: .02em; + box-shadow: 0 8px 28px rgba(239,68,68,.4), 0 0 0 3px rgba(255,255,255,.2) inset; + animation: alg7-combo-pop 2.2s ease-out forwards, alg7-combo-shimmer 1.4s linear infinite; +} +@keyframes alg7-combo-shimmer { + 0% { background-position: 0% 50%; } + 100% { background-position: 200% 50%; } +} +.alg7-combo-badge .ic-fire { + width: 22px; height: 22px; + filter: drop-shadow(0 0 4px rgba(255,255,255,.8)); +} +.alg7-combo-badge .badge-x { + font-size: 1.4rem; + font-weight: 900; + font-family: 'JetBrains Mono', monospace; +} + +/* === STREAK INDICATOR (постоянный, в углу) === */ +.alg7-streak { + position: fixed; + bottom: 18px; + right: 18px; + z-index: 9990; + display: none; + align-items: center; + gap: 6px; + padding: 7px 14px; + background: linear-gradient(135deg, #f59e0b, #ef4444); + color: #fff; + border-radius: 999px; + font-family: 'Unbounded', 'Inter', sans-serif; + font-weight: 800; + font-size: .82rem; + box-shadow: 0 6px 20px rgba(239,68,68,.35); + transition: transform .2s, opacity .3s; + animation: alg7-streak-pulse 1.6s ease-in-out infinite; +} +.alg7-streak.show { display: inline-flex; } +.alg7-streak .ic-fire { width: 16px; height: 16px; } +@keyframes alg7-streak-pulse { + 0%,100% { box-shadow: 0 6px 20px rgba(239,68,68,.35); } + 50% { box-shadow: 0 6px 28px rgba(239,68,68,.7); } +} + +/* === SPARKLES при правильном ответе === */ +.alg7-sparkle { + position: absolute; + width: 8px; height: 8px; + background: radial-gradient(circle, #fbbf24 0%, transparent 70%); + border-radius: 50%; + pointer-events: none; + animation: alg7-sparkle .9s ease-out forwards; +} + +/* === ВИЗУАЛИЗАТОР КВАДРАТА СУММЫ — Ch2 §12 === */ +.alg7-qsum-stage { + display: flex; + flex-direction: column; + align-items: center; + gap: 14px; + padding: 14px; + background: linear-gradient(135deg, rgba(220,38,38,.05), rgba(245,158,11,.05)); + border-radius: 12px; + margin-top: 12px; +} +.alg7-qsum-svg { + width: 100%; + max-width: 380px; + height: auto; +} +.alg7-qsum-svg .area-aa { fill: #dc2626; opacity: .82; transition: opacity .3s; } +.alg7-qsum-svg .area-ab { fill: #f59e0b; opacity: .82; transition: opacity .3s; } +.alg7-qsum-svg .area-bb { fill: #10b981; opacity: .82; transition: opacity .3s; } +.alg7-qsum-svg .area-aa.dim, +.alg7-qsum-svg .area-ab.dim, +.alg7-qsum-svg .area-bb.dim { opacity: .15; } +.alg7-qsum-svg .area-label { + font-family: 'Unbounded','JetBrains Mono', monospace; + font-weight: 800; + font-size: 13px; + fill: #fff; + text-anchor: middle; + text-shadow: 0 1px 2px rgba(0,0,0,.4); + pointer-events: none; +} +.alg7-qsum-formula { + font-family: 'JetBrains Mono', monospace; + font-size: 1.05rem; + text-align: center; + line-height: 2; + padding: 10px 14px; + background: var(--card); + border-radius: 10px; + border: 1px solid var(--border); + width: 100%; +} +.alg7-qsum-formula .term-aa { color: #dc2626; font-weight: 800; } +.alg7-qsum-formula .term-ab { color: #d97706; font-weight: 800; } +.alg7-qsum-formula .term-bb { color: #047857; font-weight: 800; } +.alg7-qsum-controls { + display: flex; + gap: 10px; + align-items: center; + flex-wrap: wrap; + justify-content: center; +} +.alg7-qsum-controls label { + font-size: .88rem; + color: var(--muted); + display: flex; + align-items: center; + gap: 8px; +} +.alg7-qsum-controls input[type="range"] { + width: 140px; + accent-color: var(--sec-acc, #d97706); +} +.alg7-qsum-controls b { + display: inline-block; + min-width: 28px; + padding: 2px 8px; + background: var(--sec-acc-soft, #fef3c7); + color: var(--sec-acc-d, #b45309); + border-radius: 6px; + font-family: 'JetBrains Mono', monospace; + text-align: center; +} +.alg7-qsum-mode { + display: flex; + gap: 6px; + margin-top: 4px; +} +.alg7-qsum-mode button { + padding: 5px 12px; + border-radius: 8px; + background: var(--card); + border: 1.5px solid var(--border); + font-size: .82rem; + font-weight: 700; + cursor: pointer; + transition: all .15s; +} +.alg7-qsum-mode button.active { + background: var(--sec-acc, #d97706); + color: #fff; + border-color: var(--sec-acc, #d97706); +} + +/* === ВИЗУАЛИЗАТОР РАЗНОСТИ КВАДРАТОВ — Ch2 §13 === */ +.alg7-dsq-stage { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + padding: 14px; + background: linear-gradient(135deg, rgba(234,88,12,.05), rgba(245,158,11,.05)); + border-radius: 12px; + margin-top: 12px; +} +.alg7-dsq-svg { + width: 100%; + max-width: 420px; + height: auto; +} +.alg7-dsq-svg .big-square { fill: #ea580c; opacity: .75; transition: opacity .4s; } +.alg7-dsq-svg .small-square { fill: #06b6d4; opacity: .85; } +.alg7-dsq-svg .l-shape { + fill: #ea580c; + opacity: .75; + transition: transform .9s cubic-bezier(.4,1.4,.4,1), opacity .4s; + transform-origin: center; +} +.alg7-dsq-svg .rect-piece { + fill: #ea580c; + opacity: .85; + transition: transform .9s cubic-bezier(.4,1.4,.4,1); +} +.alg7-dsq-svg .label { + font-family: 'Unbounded', monospace; + font-weight: 800; + font-size: 12px; + fill: #fff; + text-anchor: middle; + text-shadow: 0 1px 2px rgba(0,0,0,.5); + pointer-events: none; +} +.alg7-dsq-svg .label.dark { fill: #1e293b; text-shadow: none; } +.alg7-dsq-svg .ghost-rect { + fill: none; + stroke: #94a3b8; + stroke-width: 1.5; + stroke-dasharray: 4 3; + opacity: 0; + transition: opacity .4s; +} +.alg7-dsq-svg.stage-2 .ghost-rect { opacity: 1; } +.alg7-dsq-formula { + font-family: 'JetBrains Mono', monospace; + font-size: 1.05rem; + text-align: center; + line-height: 1.8; + padding: 10px 14px; + background: var(--card); + border-radius: 10px; + border: 1px solid var(--border); + width: 100%; +} +.alg7-dsq-step-btn { + padding: 10px 22px; + background: linear-gradient(135deg, #ea580c, #f59e0b); + color: #fff; + border-radius: 12px; + font-weight: 700; + font-size: .92rem; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 8px; + box-shadow: 0 4px 14px rgba(234,88,12,.3); + transition: transform .15s, box-shadow .15s; +} +.alg7-dsq-step-btn:hover { + transform: translateY(-1px); + box-shadow: 0 8px 22px rgba(234,88,12,.5); +} +.alg7-dsq-step-btn:disabled { + opacity: .55; + cursor: not-allowed; +} diff --git a/frontend/js/alg7-fx.js b/frontend/js/alg7-fx.js new file mode 100644 index 0000000..fd5f129 --- /dev/null +++ b/frontend/js/alg7-fx.js @@ -0,0 +1,416 @@ +/* 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; + +/* === 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 = '' + + '