feat(textbooks): Wave 2 — прокачка интерактивов Алгебры 8 (+422 строки)

1. Боксёрский ринг (§1): SVG-канаты вокруг квадрата + 4 цветные угловые подушки + ковёр-pattern + bell-звук через Web Audio API при S=36 + анимация боксёра-победителя на 2с
2. Доказательство √(ab)=√a·√b (§3): кнопка 'Воспроизвести' запускает 5-шаговую анимацию (подсветка прямоугольника → разрез на единичные клетки → склейка в квадрат → бейдж 'Доказано!')
3. Drag&drop с инерцией (§4): pointer-based DnD с ghost-карточкой следующей за курсором, drop-zone подсветка, неверный → тряска и возврат с инерцией, кнопочный fallback для тача
4. Match-игра (§3): SVG-overlay рисует линии соединения между парами выражений (синяя dashed pending → зелёная при совпадении / красная мигающая при ошибке)
5. Real-time валидация (liveCheck): ✓/✗ индикатор появляется при вводе во всех числовых input'ах без нажатия 'Проверить'
6. Game-over modal (squares): красивая модалка с рекордом, SVG-кубком, confetti
7. Hover-preview карточек §§: tooltip с темами параграфа и прогресс-баром
8. Fade-переходы между секциями: 180ms fadeOut + 220ms fadeIn с translateY
This commit is contained in:
Maxim Dolgolyov
2026-05-27 12:08:30 +03:00
parent 0417f51427
commit 1ee16a3a38
+548 -94
View File
@@ -410,6 +410,71 @@ input,select,textarea{font-family:inherit}
.psel-card{flex:0 0 148px;scroll-snap-align:start}
body{font-size:.94rem}
}
/* ═══════════════════════════════════════════════
WAVE 2 — INTERACTIVE DEPTH
═══════════════════════════════════════════════ */
/* Task 5: live-check indicators */
.live-ind{display:inline-block;margin-left:6px;font-weight:900;font-size:1rem;transition:color .15s}
.live-ind.ok{color:var(--ok)}
.live-ind.fail{color:var(--fail)}
/* Task 3: drag-and-drop DnD card */
.dnd-card{display:inline-flex;align-items:center;justify-content:center;padding:8px 16px;background:linear-gradient(135deg,var(--acc),var(--acc2));color:#fff;border-radius:10px;font-weight:700;font-size:1rem;cursor:grab;user-select:none;font-family:'JetBrains Mono',monospace;box-shadow:var(--sh2);transition:transform .15s,box-shadow .15s;touch-action:none}
.dnd-card:active{cursor:grabbing;transform:scale(1.06);box-shadow:0 8px 24px rgba(3,169,244,.35)}
.dnd-card.dragging-active{opacity:.55;transform:scale(.95)}
.dnd-card.correct-dnd{background:linear-gradient(135deg,var(--ok),#059669);animation:dndCorrect .4s ease}
.dnd-card.wrong-dnd{animation:dndWrong .5s ease}
@keyframes dndCorrect{0%{transform:scale(1.15)}100%{transform:scale(1)}}
@keyframes dndWrong{0%,100%{transform:translateX(0)}25%{transform:translateX(-8px)}75%{transform:translateX(8px)}}
.dnd-dropzone{min-height:60px;border:2.5px dashed var(--sec-acc,var(--pri));border-radius:12px;background:var(--card);padding:10px 14px;display:flex;align-items:center;justify-content:center;transition:border-color .15s,background .15s;font-size:.88rem;color:var(--muted);font-weight:600}
.dnd-dropzone.over{border-color:var(--ok);background:var(--ok-bg)}
/* Task 4: SVG connection lines for match */
#match-svg-overlay{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:5;overflow:visible}
.match-line{stroke-width:2.5;fill:none;stroke-dasharray:none;transition:stroke .25s}
.match-line.pending{stroke:var(--acc);stroke-dasharray:6 3;animation:dashFlow .8s linear infinite}
.match-line.correct-line{stroke:var(--ok)}
.match-line.wrong-line{stroke:var(--fail);animation:lineFlash .3s ease 3}
@keyframes dashFlow{to{stroke-dashoffset:-18}}
@keyframes lineFlash{0%,100%{opacity:1}50%{opacity:.2}}
/* Task 6: game-over modal */
.game-over-modal{position:fixed;inset:0;z-index:2000;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.55);backdrop-filter:blur(4px);animation:modalIn .3s ease}
@keyframes modalIn{from{opacity:0}to{opacity:1}}
.game-over-box{background:var(--card);border-radius:20px;padding:32px 36px;text-align:center;max-width:360px;width:90%;box-shadow:0 24px 64px rgba(0,0,0,.28);animation:boxIn .4s cubic-bezier(.34,1.56,.64,1)}
@keyframes boxIn{from{transform:scale(.75);opacity:0}to{transform:scale(1);opacity:1}}
.game-over-score{font-size:3rem;font-weight:900;color:var(--pri2);font-family:'Unbounded',sans-serif;margin:10px 0}
.game-over-record{display:inline-flex;align-items:center;gap:8px;padding:8px 16px;background:linear-gradient(135deg,#fcd34d,#f59e0b);color:#451a03;border-radius:10px;font-weight:800;font-size:.95rem;margin:10px 0;animation:recordPulse 1s ease infinite}
@keyframes recordPulse{0%,100%{box-shadow:0 0 0 0 rgba(245,158,11,.5)}50%{box-shadow:0 0 0 8px rgba(245,158,11,0)}}
/* Task 7: psel-card hover preview */
.psel-card{overflow:visible}
/* restore border-radius clip for the active stripe via pseudo-element only on non-preview */
.psel-card-preview{display:none;position:absolute;left:105%;top:0;width:200px;background:var(--card);border:1.5px solid var(--pri);border-radius:12px;padding:12px 14px;z-index:100;box-shadow:var(--sh2);pointer-events:none;animation:previewIn .18s ease}
@keyframes previewIn{from{opacity:0;transform:translateX(-8px)}to{opacity:1;transform:none}}
.psel-card:hover .psel-card-preview{display:block}
.psel-preview-title{font-size:.72rem;font-weight:800;color:var(--pri2);text-transform:uppercase;letter-spacing:.06em;margin-bottom:6px}
.psel-preview-topic{font-size:.78rem;color:var(--text);margin-bottom:2px;padding-left:8px;border-left:2px solid var(--sec-acc,var(--pri))}
.psel-preview-bar{height:5px;background:rgba(233,30,99,.12);border-radius:3px;overflow:hidden;margin-top:8px}
.psel-preview-fill{height:100%;background:var(--pri);border-radius:3px;transition:width .4s}
.psel-preview-pct{font-size:.72rem;color:var(--muted);margin-top:2px}
@media(max-width:768px){.psel-card-preview{display:none!important}}
/* Task 8: section fade transitions */
.sec.fade-out{animation:secFadeOut .18s ease forwards}
.sec.fade-in{animation:secFadeIn .22s ease forwards}
@keyframes secFadeOut{from{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(-8px)}}
@keyframes secFadeIn{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
/* Task 1: boxer ring extras */
.ring-boxer{animation:boxerBounce .4s ease infinite alternate}
@keyframes boxerBounce{from{transform:translateY(0)}to{transform:translateY(-4px)}}
/* Task 2: geo proof animation badge */
.proof-badge{display:inline-flex;align-items:center;gap:6px;padding:6px 14px;background:linear-gradient(135deg,var(--ok),#059669);color:#fff;border-radius:8px;font-weight:700;font-size:.85rem;animation:badgeIn .4s cubic-bezier(.34,1.56,.64,1)}
@keyframes badgeIn{from{transform:scale(.5);opacity:0}to{transform:scale(1);opacity:1}}
</style>
</head>
<body>
@@ -636,13 +701,20 @@ function achievement(id, text){
PARA SELECTOR
════════════════════════════════════════════════════════ */
const PARAS = [
{ id:'p1', num:'§ 1', name:'Квадратный корень', sub:'Арифметический корень' },
{ id:'p2', num:'§ 2', name:'Действительные числа', sub:ррациональные числа' },
{ id:'p3', num:3', name:'Свойства корней', sub:'√(ab) = √a·√b' },
{ id:'p4', num:'§ 4', name:'Применение свойств', sub:'Преобразования' },
{ id:'p5', num:5', name:'Числовые промежутки', sub:' и ∩' },
{ id:'p6', num:'§ 6', name:'Системы неравенств', sub:'Двойные неравенства' },
{ id:'final', num:'', name:'Финал главы', sub:'Самооценка · Практика · Увлекательно', final:true },
{ id:'p1', num:'§ 1', name:'Квадратный корень', sub:'Арифметический корень',
topics:['Определение корня','Арифметический корень','Извлечение корня',гра «Таблица квадратов»','Ринг: S=36'] },
{ id:'p2', num:2', name:'Действительные числа', sub:'Иррациональные числа',
topics:['','Иррациональные числа','Числовая ось','Классификация чисел'] },
{ id:'p3', num:3', name:'Свойства корней', sub:'√(ab) = √a·√b',
topics:['√(ab) = √a·√b','√(a/b) = √a/√b','√(a²) = |a|','Match-игра','Тренажёр упрощения'] },
{ id:'p4', num:'§ 4', name:'Применение свойств', sub:'Преобразования',
topics:['Вынесение множителя','Внесение под корень','Освобождение от иррац.','Сравнение корней','Drag «упрости √»'] },
{ id:'p5', num:'§ 5', name:'Числовые промежутки', sub:' и ∩',
topics:['9 типов промежутков','Конструктор промежутка','Объединение A ∪ B','Пересечение A ∩ B'] },
{ id:'p6', num:'§ 6', name:'Системы неравенств', sub:'Двойные неравенства',
topics:['Система неравенств','Совокупность неравенств','Двойные неравенства','Решение на числовой оси'] },
{ id:'final', num:'★', name:'Финал главы', sub:'Самооценка · Практика · Увлекательно', final:true,
topics:['10 задач самооценки','3 практические задачи','История знака √','Олимпиадные задачи'] },
];
function buildParaSelector(){
@@ -653,10 +725,18 @@ function buildParaSelector(){
card.className = 'psel-card' + (p.final ? ' final' : '');
card.dataset.id = p.id;
card.dataset.progCard = p.id;
const topicsHtml = (p.topics||[]).map(t=>`<div class="psel-preview-topic">${t}</div>`).join('');
const progPct = STATE.progress[p.id] || 0;
card.innerHTML = `
<div class="psel-num">${p.num}</div>
<div class="psel-name">${p.name}</div>
<div class="psel-prog"><div class="psel-prog-fill"></div></div>`;
<div class="psel-prog"><div class="psel-prog-fill"></div></div>
<div class="psel-card-preview">
<div class="psel-preview-title">${p.name}</div>
${topicsHtml}
<div class="psel-preview-bar"><div class="psel-preview-fill" style="width:${progPct}%"></div></div>
<div class="psel-preview-pct">${progPct}% пройдено</div>
</div>`;
card.addEventListener('click', ()=>goTo(p.id));
g.appendChild(card);
});
@@ -670,15 +750,29 @@ function ensureBuilt(id){
if(fn){ fn(); BUILT.add(id); }
}
function goTo(id){
const prevEl = document.querySelector('.sec.active');
if(prevEl && prevEl.id !== 'sec-' + id){
prevEl.classList.add('fade-out');
setTimeout(()=>{
prevEl.classList.remove('active','fade-out');
_goToFinish(id);
}, 180);
} else {
if(prevEl) prevEl.classList.remove('active');
_goToFinish(id);
}
}
function _goToFinish(id){
STATE.current = id;
ensureBuilt(id);
document.querySelectorAll('.sec').forEach(s=>s.classList.remove('active'));
const el = document.getElementById('sec-' + id);
if(el) el.classList.add('active');
if(el){
el.classList.add('active','fade-in');
setTimeout(()=>el.classList.remove('fade-in'), 250);
}
document.querySelectorAll('.psel-card').forEach(c=>c.classList.toggle('active', c.dataset.id === id));
buildSidebar(id);
window.scrollTo({top:0, behavior:'smooth'});
// мин прогресс просто за вход
if((STATE.progress[id]||0) < 10) bumpProgress(id, 10);
if(window.renderMathInElement){
setTimeout(()=>renderMathInElement(el, {delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false}],throwOnError:false}),0);
@@ -1019,6 +1113,41 @@ function initMobileSidebar(){
window.addEventListener('resize', check);
}
/* ════════════════════════════════════════════════════════
WAVE 2 — UTILITIES
════════════════════════════════════════════════════════ */
/* Task 5: live input validation */
function liveCheck(inp, expectedFn){
let ind = inp.nextElementSibling;
if(!ind || !ind.classList.contains('live-ind')){
ind = document.createElement('span');
ind.className = 'live-ind';
inp.after(ind);
}
inp.addEventListener('input', ()=>{
const v = inp.value.trim();
if(!v){ ind.className = 'live-ind'; ind.innerHTML=''; return; }
const result = expectedFn(v);
if(result === true){ ind.className = 'live-ind ok'; ind.innerHTML='&#10003;'; }
else if(result === false){ ind.className = 'live-ind fail'; ind.innerHTML='&#10007;'; }
else { ind.className = 'live-ind'; ind.innerHTML='&#8230;'; }
});
}
/* Task 1: bell sound via Web Audio API */
function playBell(){
try{
const ctx = new (window.AudioContext || window.webkitAudioContext)();
const o = ctx.createOscillator(); const g = ctx.createGain();
o.connect(g); g.connect(ctx.destination);
o.frequency.value = 880; o.type = 'sine';
g.gain.setValueAtTime(0.3, ctx.currentTime);
g.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.5);
o.start(); o.stop(ctx.currentTime + 0.5);
}catch(e){}
}
/* Paragraph builders will be defined below */
</script>
@@ -1061,17 +1190,60 @@ function buildP1(){
${widget('Боксёрский ринг — найди сторону', 'INTERACT', 'Тяните угол квадрата, чтобы изменить его сторону. Поставьте площадь точно 36 м² — получите бонус!', `
<svg id="ring-svg" viewBox="0 0 360 220" style="width:100%;max-width:520px;height:240px;background:var(--card);border-radius:10px;border:1px solid var(--border)">
<rect id="ring-rect" x="60" y="40" width="120" height="120" fill="rgba(233,30,99,0.16)" stroke="#e91e63" stroke-width="2.5"/>
<line x1="60" y1="170" x2="180" y2="170" stroke="#6b5b73" stroke-width="1.5"/>
<text id="ring-side-lab" x="120" y="186" text-anchor="middle" fill="#c2185b" font-size="14" font-weight="700">сторона x = 0 м</text>
<text id="ring-area-lab" x="120" y="105" text-anchor="middle" fill="#1a1a2e" font-size="22" font-weight="900">S = 0 м²</text>
<!-- carpet pattern fill -->
<defs>
<pattern id="ring-carpet" patternUnits="userSpaceOnUse" width="10" height="10">
<rect width="10" height="10" fill="rgba(233,30,99,0.04)"/>
<line x1="0" y1="5" x2="10" y2="5" stroke="rgba(233,30,99,0.07)" stroke-width="1"/>
</pattern>
</defs>
<!-- carpet background -->
<rect id="ring-carpet-rect" x="60" y="40" width="120" height="120" fill="url(#ring-carpet)"/>
<!-- main ring mat -->
<rect id="ring-rect" x="60" y="40" width="120" height="120" fill="rgba(233,30,99,0.10)" stroke="#e91e63" stroke-width="2.5"/>
<!-- 3 ropes (extend slightly beyond ring) -->
<g id="ring-ropes">
<line id="rope-t1" x1="52" y1="58" x2="188" y2="58" stroke="#e91e63" stroke-width="3" stroke-linecap="round" opacity=".7"/>
<line id="rope-t2" x1="52" y1="98" x2="188" y2="98" stroke="#e91e63" stroke-width="3" stroke-linecap="round" opacity=".7"/>
<line id="rope-t3" x1="52" y1="138" x2="188" y2="138" stroke="#e91e63" stroke-width="3" stroke-linecap="round" opacity=".55"/>
<!-- vertical rope posts -->
<line id="rope-l" x1="52" y1="40" x2="52" y2="160" stroke="#c2185b" stroke-width="4" stroke-linecap="round" opacity=".5"/>
<line id="rope-r" x1="188" y1="40" x2="188" y2="160" stroke="#c2185b" stroke-width="4" stroke-linecap="round" opacity=".5"/>
</g>
<!-- corner pads -->
<rect id="cp-tl" x="56" y="36" width="10" height="10" rx="2" fill="rgba(239,68,68,0.75)"/>
<rect id="cp-tr" x="174" y="36" width="10" height="10" rx="2" fill="rgba(59,130,246,0.75)"/>
<rect id="cp-bl" x="56" y="154" width="10" height="10" rx="2" fill="rgba(250,250,250,0.9)" stroke="#ccc" stroke-width="1"/>
<rect id="cp-br" x="174" y="154" width="10" height="10" rx="2" fill="rgba(30,30,30,0.75)"/>
<!-- dimension line -->
<line id="ring-dim-line" x1="60" y1="176" x2="180" y2="176" stroke="#6b5b73" stroke-width="1.5"/>
<text id="ring-side-lab" x="120" y="192" text-anchor="middle" fill="#c2185b" font-size="14" font-weight="700">сторона x = 6.0 м</text>
<text id="ring-area-lab" x="120" y="105" text-anchor="middle" fill="#1a1a2e" font-size="22" font-weight="900">S = 36 м²</text>
<!-- drag handle -->
<circle id="ring-handle" cx="180" cy="40" r="9" fill="#03a9f4" stroke="#fff" stroke-width="2" style="cursor:grab"/>
<text x="270" y="50" fill="#6b5b73" font-size="12">← тяните угол</text>
<text x="265" y="50" fill="#6b5b73" font-size="12">← тяните угол</text>
<!-- boxer (hidden initially) -->
<g id="ring-boxer" style="display:none" class="ring-boxer">
<!-- head -->
<circle cx="120" cy="75" r="9" fill="#f9a825" stroke="#e65100" stroke-width="1.5"/>
<!-- body -->
<line x1="120" y1="84" x2="120" y2="108" stroke="#e65100" stroke-width="2.5"/>
<!-- arms with gloves -->
<line x1="120" y1="90" x2="104" y2="100" stroke="#e65100" stroke-width="2.5"/>
<circle cx="102" cy="101" r="5" fill="#e91e63" stroke="#c2185b" stroke-width="1.5"/>
<line x1="120" y1="90" x2="136" y2="100" stroke="#e65100" stroke-width="2.5"/>
<circle cx="138" cy="101" r="5" fill="#e91e63" stroke="#c2185b" stroke-width="1.5"/>
<!-- legs -->
<line x1="120" y1="108" x2="112" y2="124" stroke="#e65100" stroke-width="2.5"/>
<line x1="120" y1="108" x2="128" y2="124" stroke="#e65100" stroke-width="2.5"/>
<!-- victory text -->
<text x="120" y="138" text-anchor="middle" fill="#e91e63" font-size="13" font-weight="900">Победа!</text>
</g>
</svg>
<div class="row" style="margin-top:12px">
<div class="chip">Сторона: <b id="ring-side-chip">0</b> м</div>
<div class="chip acc">Площадь: <b id="ring-area-chip">0</b> м²</div>
<div id="ring-feedback" style="margin-left:auto;font-weight:700;color:var(--ok);display:none"> Сторона = √36 = 6 м!</div>
<div class="chip">Сторона: <b id="ring-side-chip">6.0</b> м</div>
<div class="chip acc">Площадь: <b id="ring-area-chip">36</b> м²</div>
<div id="ring-feedback" style="margin-left:auto;font-weight:700;color:var(--ok);display:none">&#10003; Сторона = √36 = 6 м!</div>
</div>
`)}
@@ -1226,38 +1398,89 @@ function buildP1(){
function initRing(){
const svg = document.getElementById('ring-svg');
if(!svg) return;
const rect = document.getElementById('ring-rect');
const ringRect = document.getElementById('ring-rect');
const carpetRect = document.getElementById('ring-carpet-rect');
const handle = document.getElementById('ring-handle');
const sideLab = document.getElementById('ring-side-lab');
const areaLab = document.getElementById('ring-area-lab');
const sideChip = document.getElementById('ring-side-chip');
const areaChip = document.getElementById('ring-area-chip');
const fb = document.getElementById('ring-feedback');
const boxer = document.getElementById('ring-boxer');
const dimLine = document.getElementById('ring-dim-line');
let dragging = false;
let ringWon = false;
const scale = 20; // 1 м = 20 px
const OX = 60, OY_BASE = 200; // origin x, bottom anchor
function setRope(id, x1, x2, y){ const r = document.getElementById(id); if(r){ r.setAttribute('x1',x1); r.setAttribute('x2',x2); r.setAttribute('y1',y); r.setAttribute('y2',y); } }
function setPad(id, x, y){ const p = document.getElementById(id); if(p){ p.setAttribute('x',x); p.setAttribute('y',y); } }
function setVPost(id, x, y1, y2){ const p = document.getElementById(id); if(p){ p.setAttribute('x1',x); p.setAttribute('x2',x); p.setAttribute('y1',y1); p.setAttribute('y2',y2); } }
function update(sideM){
sideM = Math.max(0.5, Math.min(10, sideM));
const sidePx = sideM * scale;
rect.setAttribute('width', sidePx);
rect.setAttribute('height', sidePx);
rect.setAttribute('y', 200 - sidePx - 30);
handle.setAttribute('cx', 60 + sidePx);
handle.setAttribute('cy', 200 - sidePx - 30);
sideLab.setAttribute('x', 60 + sidePx/2);
sideLab.setAttribute('y', 200 - 14);
const top = OY_BASE - sidePx - 30;
const right = OX + sidePx;
[ringRect, carpetRect].forEach(r=>{
r.setAttribute('width', sidePx); r.setAttribute('height', sidePx);
r.setAttribute('x', OX); r.setAttribute('y', top);
});
handle.setAttribute('cx', right);
handle.setAttribute('cy', top);
// ropes: 3 horizontal at top+offset, mid, near-bottom
const ropePad = 8;
setRope('rope-t1', OX - ropePad, right + ropePad, top + sidePx*0.2);
setRope('rope-t2', OX - ropePad, right + ropePad, top + sidePx*0.5);
setRope('rope-t3', OX - ropePad, right + ropePad, top + sidePx*0.8);
setVPost('rope-l', OX - ropePad, top, top + sidePx);
setVPost('rope-r', right + ropePad, top, top + sidePx);
// corner pads
setPad('cp-tl', OX - ropePad + 2, top - 4);
setPad('cp-tr', right + ropePad - 10, top - 4);
setPad('cp-bl', OX - ropePad + 2, top + sidePx - 6);
setPad('cp-br', right + ropePad - 10, top + sidePx - 6);
// boxer center position
const bx = OX + sidePx/2;
const by = top + sidePx/2;
if(boxer){
boxer.querySelectorAll('circle,line,text').forEach((el,i)=>{
// shift entire boxer group by adjusting first head circle
});
// repositon via transform
boxer.setAttribute('transform', `translate(${bx - 120},${by - 100})`);
}
// dimension line
if(dimLine){
dimLine.setAttribute('x1', OX); dimLine.setAttribute('x2', right);
dimLine.setAttribute('y1', top + sidePx + 6); dimLine.setAttribute('y2', top + sidePx + 6);
}
sideLab.setAttribute('x', OX + sidePx/2);
sideLab.setAttribute('y', top + sidePx + 22);
sideLab.textContent = 'сторона x = ' + sideM.toFixed(1) + ' м';
areaLab.setAttribute('x', 60 + sidePx/2);
areaLab.setAttribute('y', 200 - sidePx/2 - 30);
areaLab.setAttribute('x', OX + sidePx/2);
areaLab.setAttribute('y', top + sidePx/2 + 6);
const area = sideM * sideM;
areaLab.textContent = 'S = ' + area.toFixed(1) + ' м²';
sideChip.textContent = sideM.toFixed(1);
areaChip.textContent = area.toFixed(1);
if(Math.abs(area - 36) < 0.2){
fb.style.display = 'inline';
fb.textContent = '✓ Точно! Сторона = √36 = 6 м';
achievement('ring36','Нашёл сторону ринга');
bumpProgress('p1', 8);
if(!ringWon){
ringWon = true;
playBell();
fb.style.display = 'inline';
fb.innerHTML = '&#10003; Точно! Сторона = √36 = 6 м';
achievement('ring36','Нашёл сторону ринга');
bumpProgress('p1', 8);
// show boxer for 2s
if(boxer){ boxer.style.display=''; setTimeout(()=>{ if(boxer) boxer.style.display='none'; ringWon=false; }, 2200); }
}
} else {
fb.style.display = 'none';
}
@@ -1266,10 +1489,10 @@ function initRing(){
const onMove = (e)=>{
if(!dragging) return;
const rect = svg.getBoundingClientRect();
const x = (e.clientX || (e.touches && e.touches[0].clientX) || 0) - rect.left;
const svgX = x / rect.width * 360;
const sideM = (svgX - 60) / scale;
const bbox = svg.getBoundingClientRect();
const x = (e.clientX || (e.touches && e.touches[0].clientX) || 0) - bbox.left;
const svgX = x / bbox.width * 360;
const sideM = (svgX - OX) / scale;
update(sideM);
};
handle.addEventListener('mousedown', ()=>dragging=true);
@@ -1360,8 +1583,8 @@ function squaresAnswer(picked, btn){
if(sqState.round > 10){
const t = ((performance.now()-sqState.t0)/1000).toFixed(1);
const total = sqState.score + Math.max(0, Math.round((30 - +t)*2));
feedback(fb, true, 'Игра окончена! Итого: ' + total + ' очков, время ' + t + 'с');
if(total > (isFinite(STATE.squaresBest) ? STATE.squaresBest : 0)){
const isRecord = total > (isFinite(STATE.squaresBest) ? STATE.squaresBest : 0);
if(isRecord){
STATE.squaresBest = total;
saveProgress();
achievement('squares','Лучший результат «Таблица квадратов»');
@@ -1369,10 +1592,40 @@ function squaresAnswer(picked, btn){
bumpProgress('p1', 15);
if(sqState.timer) clearInterval(sqState.timer);
sqState = null;
showGameOverModal(total, t, isRecord);
return;
}
setTimeout(squaresNext, 850);
}
function showGameOverModal(total, timeStr, isRecord){
// remove existing
const old = document.getElementById('game-over-modal');
if(old) old.remove();
const best = isFinite(STATE.squaresBest) ? STATE.squaresBest : total;
const recHtml = isRecord
? `<div class="game-over-record">
<svg class="ic" viewBox="0 0 24 24" style="width:20px;height:20px;stroke:#451a03"><path d="M6 9H4l-1-3h18l-1 3h-2M6 9l1 6h10l1-6M6 9h12"/><path d="M9 21h6M12 15v6"/></svg>
НОВЫЙ РЕКОРД!
</div>`
: `<div style="color:var(--muted);font-size:.9rem;margin:8px 0">Рекорд: <b>${best}</b> очков</div>`;
const modal = document.createElement('div');
modal.id = 'game-over-modal';
modal.className = 'game-over-modal';
modal.innerHTML = `<div class="game-over-box">
<div style="font-size:1rem;font-weight:700;color:var(--muted);margin-bottom:4px">Игра окончена!</div>
<div class="game-over-score">${total}</div>
<div style="font-size:.88rem;color:var(--muted);margin-bottom:8px">очков &nbsp;|&nbsp; время ${timeStr} с</div>
${recHtml}
<div style="display:flex;gap:12px;justify-content:center;margin-top:18px">
<button class="btn primary" onclick="document.getElementById('game-over-modal').remove();squaresStart()">&#9654; Заново</button>
<button class="btn" onclick="document.getElementById('game-over-modal').remove()">Закрыть</button>
</div>
</div>`;
document.body.appendChild(modal);
confetti();
modal.addEventListener('click', e=>{ if(e.target === modal) modal.remove(); });
}
function squaresReset(){
if(sqState && sqState.timer) clearInterval(sqState.timer);
sqState = null;
@@ -1906,6 +2159,10 @@ function buildP3(){
</div>
</div>
<p style="margin-top:12px;text-align:center;color:var(--ok);font-weight:700">Площади всегда равны → $\\sqrt{ab}$ — сторона эквивалентного квадрата</p>
<div class="row-c" style="margin-top:14px">
<button class="btn primary" onclick="geoProofAnimate()">&#9654; Воспроизвести доказательство</button>
</div>
<div id="geo-proof-anim" style="min-height:36px;margin-top:10px;text-align:center;font-size:.92rem;color:var(--acc2);font-weight:600"></div>
`)}
${makeCard('rule','Свойство 2: корень из частного',null,`
@@ -1949,14 +2206,17 @@ function buildP3(){
`)}
${widget('Match: выражение ↔ ответ', 'GAME', 'Соедините каждое выражение с его упрощённым значением. Кликните по выражению, затем по ответу.', `
<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px">
<div>
<div class="dz-label">Выражения</div>
<div id="match-left" style="display:flex;flex-direction:column;gap:6px"></div>
</div>
<div>
<div class="dz-label">Ответы</div>
<div id="match-right" style="display:flex;flex-direction:column;gap:6px"></div>
<div id="match-container" style="position:relative">
<svg id="match-svg-overlay" style="position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:5;overflow:visible"></svg>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px">
<div>
<div class="dz-label">Выражения</div>
<div id="match-left" style="display:flex;flex-direction:column;gap:6px"></div>
</div>
<div>
<div class="dz-label">Ответы</div>
<div id="match-right" style="display:flex;flex-direction:column;gap:6px"></div>
</div>
</div>
</div>
<div class="row-c" style="margin-top:12px">
@@ -2045,6 +2305,42 @@ function initGeoProof(){
upd();
}
/* ──── 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: `<b>Шаг 1.</b> Прямоугольник <span style="color:var(--acc2)">${aVal} × ${bVal}</span> подсвечивается рамкой — его площадь <b>S = a·b = ${ab}</b>` },
{ delay:900, html: `<b>Шаг 2.</b> Прямоугольник можно представить как ${ab} единичных клеток (<span style="color:var(--acc2)">${aVal} строк × ${bVal} столбцов</span>)` },
{ delay:2000, html: `<b>Шаг 3.</b> Все ${ab} клеток «перетекают» в квадрат — ищем сторону, при которой сторона² = ${ab}` },
{ delay:3200, html: `<b>Шаг 4.</b> Квадрат со стороной <b style="color:var(--pri2)">√${ab}${side}</b> имеет ту же площадь: S = (√${ab})² = ${ab}` },
{ delay:4400, html: `<span class="proof-badge"><svg class="ic" viewBox="0 0 24 24" style="width:16px;height:16px;stroke:#fff"><polyline points="20 6 9 17 4 12"/></svg> Доказано!</span> &nbsp; √(${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);
}
/* ──── Property check slider ──── */
function initPropCheck(){
const a = document.getElementById('prop-a');
@@ -2074,11 +2370,38 @@ let matchState = null;
function initMatch(){
matchReset();
}
function _matchLineCoords(btnA, btnB, container){
const cRect = container.getBoundingClientRect();
const rA = btnA.getBoundingClientRect();
const rB = btnB.getBoundingClientRect();
return {
x1: rA.right - cRect.left,
y1: rA.top - cRect.top + rA.height/2,
x2: rB.left - cRect.left,
y2: rB.top - cRect.top + rB.height/2,
};
}
function _drawMatchLine(svg, container, btnL, btnR, cls, id){
const c = _matchLineCoords(btnL, btnR, container);
const line = document.createElementNS('http://www.w3.org/2000/svg','line');
line.setAttribute('x1', c.x1); line.setAttribute('y1', c.y1);
line.setAttribute('x2', c.x2); line.setAttribute('y2', c.y2);
line.className.baseVal = 'match-line ' + cls;
if(id) line.id = id;
svg.appendChild(line);
return line;
}
function matchReset(){
const L = document.getElementById('match-left');
const R = document.getElementById('match-right');
L.innerHTML = ''; R.innerHTML = '';
matchState = { selL:null, selR:null, done:[] };
const svgEl = document.getElementById('match-svg-overlay');
if(L) L.innerHTML = '';
if(R) R.innerHTML = '';
if(svgEl) svgEl.innerHTML = '';
matchState = { selL:null, selR:null, done:[], pendingLine:null };
const lArr = [...MATCH_PAIRS].sort(()=>Math.random()-0.5);
const rArr = [...MATCH_PAIRS].sort(()=>Math.random()-0.5);
lArr.forEach(p=>{
@@ -2087,9 +2410,11 @@ function matchReset(){
b.dataset.expr = p.expr;
b.addEventListener('click', ()=>{
if(b.disabled) return;
L.querySelectorAll('button').forEach(x=>x.style.background='');
if(L) L.querySelectorAll('button').forEach(x=>{if(!x.disabled){x.style.background='';x.style.color='';}});
b.style.background='var(--acc-soft)';
matchState.selL = b;
// remove pending line
if(matchState.pendingLine){ matchState.pendingLine.remove(); matchState.pendingLine=null; }
matchCheck();
});
L.appendChild(b);
@@ -2100,9 +2425,15 @@ function matchReset(){
b.dataset.ans = p.ans;
b.addEventListener('click', ()=>{
if(b.disabled) return;
R.querySelectorAll('button').forEach(x=>x.style.background='');
if(R) R.querySelectorAll('button').forEach(x=>{if(!x.disabled){x.style.background='';x.style.color='';}});
b.style.background='var(--pri-soft)';
matchState.selR = b;
// draw pending line
if(matchState.selL && svgEl){
const container = document.getElementById('match-container');
if(matchState.pendingLine) matchState.pendingLine.remove();
matchState.pendingLine = _drawMatchLine(svgEl, container, matchState.selL, b, 'pending', 'match-pending');
}
matchCheck();
});
R.appendChild(b);
@@ -2110,34 +2441,54 @@ function matchReset(){
document.getElementById('match-cnt').textContent = '0';
document.getElementById('match-fb').className = 'feedback';
}
function matchCheck(){
if(!matchState.selL || !matchState.selR) return;
const e = matchState.selL.dataset.expr;
const a = matchState.selR.dataset.ans;
const correct = MATCH_PAIRS.some(p=>p.expr===e && p.ans===a);
const fb = document.getElementById('match-fb');
const svgEl = document.getElementById('match-svg-overlay');
const container = document.getElementById('match-container');
// remove pending line
if(matchState.pendingLine){ matchState.pendingLine.remove(); matchState.pendingLine=null; }
if(correct){
// draw permanent green line
if(svgEl && container) _drawMatchLine(svgEl, container, matchState.selL, matchState.selR, 'correct-line');
// mark buttons
matchState.selL.style.background='var(--ok)';matchState.selL.style.color='#fff';
matchState.selL.style.borderColor='var(--ok)';
matchState.selR.style.background='var(--ok)';matchState.selR.style.color='#fff';
matchState.selR.style.borderColor='var(--ok)';
// add checkmark
matchState.selL.innerHTML = '&#10003; ' + e;
matchState.selR.innerHTML = '&#10003; ' + a;
matchState.selL.disabled = true;
matchState.selR.disabled = true;
matchState.selL.style.background='var(--ok)';matchState.selL.style.color='#fff';
matchState.selR.style.background='var(--ok)';matchState.selR.style.color='#fff';
matchState.done.push(e);
document.getElementById('match-cnt').textContent = matchState.done.length;
feedback(fb, true, e + ' = ' + a + ' ');
feedback(fb, true, e + ' = ' + a + ' &#10003;');
if(matchState.done.length === MATCH_PAIRS.length){
feedback(fb, true, 'Все 5 соединены! Свойства корня — в кармане.');
achievement('match','Match выражений');
bumpProgress('p3', 15);
confetti();
setTimeout(()=>{
feedback(fb, true, 'Все 5 соединены! Свойства корня — в кармане.');
achievement('match','Match выражений');
bumpProgress('p3', 15);
confetti();
}, 100);
}
} else {
// draw red flashing line then remove
if(svgEl && container){
const wrongLine = _drawMatchLine(svgEl, container, matchState.selL, matchState.selR, 'wrong-line');
setTimeout(()=>wrongLine.remove(), 900);
}
matchState.selL.style.background='var(--fail)';matchState.selL.style.color='#fff';
matchState.selR.style.background='var(--fail)';matchState.selR.style.color='#fff';
feedback(fb, false, 'Не совпадает. Попробуйте другую пару.');
setTimeout(()=>{
if(matchState.selL && !matchState.selL.disabled){ matchState.selL.style.background=''; matchState.selL.style.color=''; }
if(matchState.selR && !matchState.selR.disabled){ matchState.selR.style.background=''; matchState.selR.style.color=''; }
}, 700);
}, 750);
}
matchState.selL = null;
matchState.selR = null;
@@ -2165,11 +2516,18 @@ const SIMP_TASKS = [
{q:'√(2.25)', a:1.5}, {q:'√(0.16·9)', a:1.2}, {q:'√(196)', a:14},
];
let simpIdx = 0;
function initSimpTrainer(){ simpIdx = 0; simpRender(); }
function initSimpTrainer(){
simpIdx = 0; simpRender();
const inp = document.getElementById('simp-ans');
if(inp) liveCheck(inp, v=>{ const n=parseFloat(v.replace(',','.')); if(isNaN(n)) return null; return Math.abs(n - SIMP_TASKS[simpIdx].a) < 0.02; });
}
function simpRender(){
const t = SIMP_TASKS[simpIdx];
document.getElementById('simp-q').textContent = t.q;
document.getElementById('simp-ans').value = '';
// reset live-ind
const ind = document.querySelector('#simp-ans + .live-ind');
if(ind){ ind.className='live-ind'; ind.innerHTML=''; }
document.getElementById('simp-fb').className='feedback';
}
function simpCheck(){
@@ -2224,21 +2582,26 @@ function buildP4(){
</ul>
`)}
${widget('Drag «упрости √»', 'GAME', 'Перетащите подходящий множитель за пределы корня. Подсказка: ищите точный квадрат среди множителей.', `
${widget('Drag «упрости √»', 'GAME', 'Перетащите подходящий множитель в зону слева от корня. На мобиле — просто нажмите нужную карточку.', `
<div id="drag-task" style="padding:14px;background:var(--card);border-radius:9px;text-align:center;margin-bottom:12px">
<div class="lab">Упростите:</div>
<div id="drag-q" style="font-size:1.8rem;font-weight:800;color:var(--pri2);margin:10px 0;font-family:'JetBrains Mono',monospace">√72</div>
<div class="row-c">
<span class="lab">Выберите множитель:</span>
<div id="drag-mults" style="display:flex;gap:6px;flex-wrap:wrap"></div>
<div id="drag-q" style="font-size:2rem;font-weight:800;color:var(--pri2);margin:10px 0;font-family:'JetBrains Mono',monospace">√72</div>
<div style="display:flex;gap:12px;align-items:center;justify-content:center;margin:14px 0;flex-wrap:wrap">
<div id="drag-dropzone" class="dnd-dropzone" style="min-width:80px;min-height:54px">
<span style="font-size:1.2rem;font-family:'JetBrains Mono',monospace;color:var(--muted)">?</span>
</div>
<span style="font-size:1.6rem;color:var(--muted);font-family:'JetBrains Mono',monospace">× √</span>
<span id="drag-rest-num" style="font-size:1.6rem;font-weight:700;color:var(--acc2);font-family:'JetBrains Mono',monospace">?</span>
</div>
<div style="margin-top:14px;font-size:1.2rem;font-family:'JetBrains Mono',monospace">
√<span id="drag-out-a" style="color:var(--ok)">36</span> · √<span id="drag-out-b" style="color:var(--acc2)">2</span> = <span id="drag-out-num" style="color:var(--ok)">6</span>√<span id="drag-out-rad" style="color:var(--acc2)">2</span>
</div>
<div class="row-c" style="margin-top:14px">
<div class="lab" style="margin-bottom:8px">Карточки множителей:</div>
<div id="drag-mults" style="display:flex;gap:8px;flex-wrap:wrap;justify-content:center;min-height:48px"></div>
<div id="drag-result" style="margin-top:14px;font-size:1.15rem;font-family:'JetBrains Mono',monospace;min-height:28px"></div>
<div class="row-c" style="margin-top:12px">
<button class="btn primary" onclick="dragNext()">Следующая</button>
</div>
</div>
<!-- floating drag ghost -->
<div id="dnd-ghost" style="position:fixed;pointer-events:none;z-index:8000;display:none;transform:translate(-50%,-50%)"></div>
<div id="drag-fb" class="feedback"></div>
`)}
@@ -2362,59 +2725,133 @@ function buildP4(){
setTimeout(()=>{ initDragSimp(); initConverter(); initFracIrr(); initCompare(); initSimp4(); }, 50);
}
/* ──── Drag simplify (visual) ──── */
/* ──── Drag simplify — pointer-based DnD ──── */
const DRAG_TASKS = [
{n:72, sq:36, rest:2}, {n:50, sq:25, rest:2}, {n:48, sq:16, rest:3},
{n:200, sq:100, rest:2}, {n:75, sq:25, rest:3}, {n:18, sq:9, rest:2},
{n:32, sq:16, rest:2}, {n:98, sq:49, rest:2}, {n:128, sq:64, rest:2},
];
let dragIdx = 0;
let _dndState = null; // active drag
function initDragSimp(){
dragIdx = 0;
dragRender();
// global pointer handlers for ghost
window.addEventListener('pointermove', _dndMove);
window.addEventListener('pointerup', _dndDrop);
}
function dragRender(){
const t = DRAG_TASKS[dragIdx];
document.getElementById('drag-q').textContent = '√' + t.n;
// generate multipliers — true sq + some fakes
const qEl = document.getElementById('drag-q');
if(qEl) qEl.textContent = '√' + t.n;
// reset dropzone
const dz = document.getElementById('drag-dropzone');
if(dz) dz.innerHTML = '<span style="font-size:1.2rem;font-family:\'JetBrains Mono\',monospace;color:var(--muted)">?</span>';
const restEl = document.getElementById('drag-rest-num');
if(restEl) restEl.textContent = '?';
const resEl = document.getElementById('drag-result');
if(resEl) resEl.textContent = '';
// generate multipliers
const mults = new Set([t.sq]);
while(mults.size < 5){
const m = [4, 9, 16, 25, 36, 49, 64, 81][Math.floor(Math.random()*8)];
const m = [4,9,16,25,36,49,64,81][Math.floor(Math.random()*8)];
if(t.n % m === 0) mults.add(m);
else if(mults.size < 3) mults.add(m);
}
const arr = [...mults].sort((a,b)=>a-b);
const g = document.getElementById('drag-mults');
if(!g) return;
g.innerHTML = '';
arr.forEach(m=>{
const b = el('button', {class:'btn'}, m);
b.addEventListener('click', ()=>dragPick(m, t, b));
g.appendChild(b);
const card = document.createElement('div');
card.className = 'dnd-card';
card.textContent = String(m);
card.dataset.val = m;
// pointer down starts drag
card.addEventListener('pointerdown', e=>{
e.preventDefault();
_dndStart(e, card, m, t);
});
// click fallback for mobile/accessibility
card.addEventListener('click', ()=>{
if(!_dndState) dragPick(m, t, card);
});
g.appendChild(card);
});
// reset visual
document.getElementById('drag-out-a').textContent = '?';
document.getElementById('drag-out-b').textContent = '?';
document.getElementById('drag-out-num').textContent = '?';
document.getElementById('drag-out-rad').textContent = '?';
document.getElementById('drag-fb').className = 'feedback';
}
function dragPick(m, t, btn){
function _dndStart(e, card, val, task){
const ghost = document.getElementById('dnd-ghost');
if(!ghost) return;
_dndState = { card, val, task };
card.classList.add('dragging-active');
ghost.textContent = val;
ghost.className = 'dnd-card';
ghost.style.display = 'flex';
ghost.style.left = e.clientX + 'px';
ghost.style.top = e.clientY + 'px';
card.setPointerCapture(e.pointerId);
}
function _dndMove(e){
if(!_dndState) return;
const ghost = document.getElementById('dnd-ghost');
if(ghost){ ghost.style.left = e.clientX + 'px'; ghost.style.top = e.clientY + 'px'; }
const dz = document.getElementById('drag-dropzone');
if(dz){
const r = dz.getBoundingClientRect();
const over = e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom;
dz.classList.toggle('over', over);
}
}
function _dndDrop(e){
if(!_dndState) return;
const ghost = document.getElementById('dnd-ghost');
if(ghost){ ghost.style.display = 'none'; }
_dndState.card.classList.remove('dragging-active');
const dz = document.getElementById('drag-dropzone');
let droppedOnZone = false;
if(dz){
const r = dz.getBoundingClientRect();
droppedOnZone = e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom;
dz.classList.remove('over');
}
if(droppedOnZone){
dragPick(_dndState.val, _dndState.task, _dndState.card);
}
_dndState = null;
}
function dragPick(m, t, card){
const fb = document.getElementById('drag-fb');
if(t.n % m !== 0){ feedback(fb, false, m + ' не делит ' + t.n + ' нацело'); return; }
const rest = t.n / m;
if(m !== t.sq || rest !== t.rest){
// valid but not optimal
feedback(fb, false, '√' + t.n + ' = √(' + m + '·' + rest + ') = ' + Math.sqrt(m).toFixed(2) + '·√' + rest + '. Это не самый компактный вид.');
if(t.n % m !== 0){
if(card){ card.classList.add('wrong-dnd'); setTimeout(()=>card.classList.remove('wrong-dnd'),600); }
feedback(fb, false, m + ' не делит ' + t.n + ' нацело');
return;
}
document.getElementById('drag-out-a').textContent = m;
document.getElementById('drag-out-b').textContent = rest;
document.getElementById('drag-out-num').textContent = Math.sqrt(m);
document.getElementById('drag-out-rad').textContent = rest;
btn.style.background='var(--ok)'; btn.style.color='#fff';
feedback(fb, true, '✓ √' + t.n + ' = ' + Math.sqrt(m) + '√' + rest);
const rest = t.n / m;
if(m !== t.sq || rest !== t.rest){
if(card){ card.classList.add('wrong-dnd'); setTimeout(()=>card.classList.remove('wrong-dnd'),600); }
feedback(fb, false, '√'+t.n+' = √('+m+'·'+rest+') = '+Math.sqrt(m).toFixed(2)+'·√'+rest+'. Не самый компактный вид.');
return;
}
// correct!
const dz = document.getElementById('drag-dropzone');
if(dz){ dz.innerHTML = '<span style="font-family:\'JetBrains Mono\',monospace;font-weight:900;color:var(--ok);font-size:1.2rem">'+Math.sqrt(m)+'</span>'; }
const restEl = document.getElementById('drag-rest-num');
if(restEl) restEl.textContent = rest;
const resEl = document.getElementById('drag-result');
if(resEl) resEl.innerHTML = '<span style="color:var(--ok);font-weight:700">&#10003; √'+t.n+' = '+Math.sqrt(m)+'√'+rest+'</span>';
if(card){ card.classList.add('correct-dnd'); }
feedback(fb, true, '&#10003; √'+t.n+' = '+Math.sqrt(m)+'√'+rest);
confetti();
bumpProgress('p4', 3);
}
function dragNext(){
dragIdx = (dragIdx + 1) % DRAG_TASKS.length;
dragRender();
@@ -2532,6 +2969,10 @@ let simp4State = { idx:0, score:0, cnt:0 };
function initSimp4(){
simp4State = { idx:0, score:0, cnt:0 };
simp4Render();
const ia = document.getElementById('simp4-a');
const ib = document.getElementById('simp4-b');
if(ia) liveCheck(ia, v=>{ const n=+v; if(isNaN(n)||!v) return null; const t=SIMP4_TASKS[simp4State.idx]; return n===t.a ? null : false; });
if(ib) liveCheck(ib, v=>{ const n=+v; if(isNaN(n)||!v) return null; const t=SIMP4_TASKS[simp4State.idx]; const a=+(document.getElementById('simp4-a').value); return (a===t.a && n===t.b) ? true : (n===t.b ? null : false); });
}
function simp4Render(){
const t = SIMP4_TASKS[simp4State.idx];
@@ -3527,7 +3968,20 @@ function buildFinal(){
${secNav('p6', null)}
`;
renderMath(body);
setTimeout(()=>{ buildAssessment(); renderMath(body); }, 50);
setTimeout(()=>{
buildAssessment(); renderMath(body);
// liveCheck for practical tasks
const pr1 = document.getElementById('pr1-a');
if(pr1) liveCheck(pr1, v=>{ const n=+v; return isNaN(n)?null:n===26?true:false; });
const pr2 = document.getElementById('pr2-a');
if(pr2) liveCheck(pr2, v=>{ const n=+v; return isNaN(n)?null:n===81?true:false; });
// dec inputs
const decExpected = [5,18,21,8,2,1];
['dec-1','dec-2','dec-3','dec-4','dec-5','dec-6'].forEach((id,i)=>{
const inp = document.getElementById(id);
if(inp) liveCheck(inp, v=>{ const n=+v; return isNaN(n)||!v?null:n===decExpected[i]?true:false; });
});
}, 50);
// Повторный рендер с задержкой — на случай если KaTeX ещё не успел подгрузиться
setTimeout(()=>renderMath(body), 300);
}