feat(alg9 ch2 wave1): §6 «Функция, D(f), E(f)» + §7 «Свойства» + axes2D/plotFunc

This commit is contained in:
Maxim Dolgolyov
2026-05-29 08:21:14 +03:00
parent 31b40b0e99
commit bb40776fa8
+676 -29
View File
@@ -109,6 +109,46 @@ a{color:inherit;text-decoration:none}
.feedback.ok{display:block;background:var(--ok-bg);color:#065f46;border-left:4px solid var(--ok)}
.feedback.fail{display:block;background:var(--fail-bg);color:#7f1d1d;border-left:4px solid var(--fail)}
.wg{background:linear-gradient(135deg,var(--card),var(--sec-acc-soft,var(--pri-soft)));border:1.5px solid var(--sec-acc,var(--pri));border-radius:14px;padding:18px 20px;margin-bottom:18px;box-shadow:var(--sh2);position:relative;z-index:1}
.wg-header{display:flex;align-items:center;gap:8px;margin-bottom:14px}
.wg-badge{padding:4px 9px;background:var(--sec-acc,var(--pri));color:#fff;border-radius:6px;font-family:'Unbounded',sans-serif;font-size:.68rem;font-weight:800;text-transform:uppercase;letter-spacing:.06em}
.wg-title{font-family:'Unbounded',sans-serif;font-size:1.05rem;font-weight:800;color:var(--sec-acc-d,var(--pri2));flex:1}
.wg-help{font-size:.88rem;color:var(--text);margin-bottom:12px;line-height:1.55;background:linear-gradient(135deg,var(--warn-bg,#fef3c7),var(--sec-acc-soft,var(--pri-soft)));border-left:4px solid var(--warn,#f59e0b);padding:9px 14px;border-radius:9px}
.tinp{padding:8px 12px;border:1.5px solid var(--border);border-radius:8px;background:var(--card);color:var(--text);transition:border-color .15s;font-family:'JetBrains Mono',monospace}
.tinp:focus{outline:0;border-color:var(--sec-acc,var(--pri));box-shadow:0 0 0 3px var(--sec-acc-soft,var(--pri-soft))}
.actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}
.sliders{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-bottom:10px}
.sliders label{display:block;font-size:.92rem;color:var(--muted);background:var(--card);padding:8px 12px;border-radius:8px;border:1px solid var(--border);line-height:1.5}
.sliders label b{font-family:'JetBrains Mono',monospace;font-size:1.05rem;color:var(--sec-acc-d,var(--pri2));margin-left:4px}
.sliders label input[type="range"]{display:block;width:100%;margin-top:6px;accent-color:var(--sec-acc,var(--pri))}
.score-display{display:flex;gap:14px;flex-wrap:wrap;align-items:center;padding:10px 14px;background:var(--sec-acc-soft,var(--pri-soft));border-radius:10px;margin-bottom:12px}
.score-display b{color:var(--sec-acc-d,var(--pri2));font-size:1.15rem}
.spoiler{border:1px solid var(--border);border-radius:10px;background:var(--card);margin:10px 0;overflow:hidden}
.spoiler summary{padding:8px 14px;background:var(--sec-acc-soft,var(--pri-soft));font-weight:700;cursor:pointer;font-size:.88rem;color:var(--sec-acc-d,var(--pri2));list-style:none;display:flex;align-items:center;gap:8px}
.spoiler summary::-webkit-details-marker{display:none}
.spoiler summary::before{content:'+';font-size:1.2rem;font-weight:900;color:var(--sec-acc,var(--pri));width:18px}
.spoiler[open] summary::before{content:'\2212'}
.spoiler-body{padding:10px 14px;font-size:.92rem;line-height:1.6}
.dnd-pool{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:14px;padding:10px;border:1.5px dashed var(--border);border-radius:10px;min-height:54px;transition:border-color .18s,background .18s}
.dnd-pool.over{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft));border-style:solid}
.dnd-pool.col{flex-direction:column;align-items:stretch}
.dnd-pool.col .dnd-chip{width:auto}
.dnd-chip{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:var(--card);border:1.5px solid var(--border);border-radius:10px;cursor:grab;user-select:none;font-size:.92rem;line-height:1.4;transition:transform .12s,box-shadow .12s,border-color .12s;touch-action:none;max-width:100%}
.dnd-chip:hover{transform:translateY(-1px);border-color:var(--sec-acc,var(--pri));box-shadow:var(--sh)}
.dnd-chip:active{cursor:grabbing}
.dnd-chip.armed{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft));box-shadow:0 0 0 3px rgba(217,119,6,.22);transform:translateY(-1px)}
.dnd-chip.dragging{opacity:.28}
.dnd-chip.placed{background:var(--sec-acc-soft,var(--pri-soft));border-color:var(--sec-acc,var(--pri))}
.dnd-chip .dnd-x{padding:0 5px;color:var(--muted);font-weight:700;font-size:1.05rem;border-radius:4px;cursor:pointer}
.dnd-chip .dnd-x:hover{color:var(--bad,var(--fail));background:var(--fail-bg)}
.drop-box{background:var(--card);border:1.5px dashed var(--border);border-radius:10px;padding:10px;min-height:90px;transition:border-color .15s,background .15s}
.drop-box:hover{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft))}
.drop-box h5{font-family:'Unbounded',sans-serif;font-size:.78rem;color:var(--sec-acc-d,var(--pri2));margin-bottom:8px;text-transform:uppercase;letter-spacing:.05em}
.drop-box.over{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft));border-style:solid;transform:scale(1.015)}
.drop-items{display:flex;flex-wrap:wrap;gap:6px;min-height:32px}
.dnd-hint{font-size:.78rem;color:var(--muted);margin-bottom:8px;display:flex;align-items:center;gap:6px}
.dnd-hint svg{width:14px;height:14px;flex-shrink:0}
.col-side{position:sticky;top:14px;align-self:start;height:fit-content;max-height:calc(100vh - 28px);overflow-y:auto}
.sidecard{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:16px;margin-bottom:14px;box-shadow:var(--sh)}
.sidecard h4{font-family:'Unbounded',sans-serif;font-size:.74rem;font-weight:800;color:var(--pri2);text-transform:uppercase;letter-spacing:.07em;margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid var(--border)}
@@ -426,47 +466,654 @@ function readButton(paraId){
function wireReadBtn(paraId){
const btn = document.getElementById(paraId+'-read-btn'); if(!btn) return;
btn.addEventListener('click', ()=>{
addXp(10, paraId+'-read'); bumpProgress(paraId, 100);
addXp(10, paraId+'-read'); bumpProgress(paraId, 30);
btn.textContent='Прочитано! +10 XP'; btn.disabled=true; btn.style.opacity=.6;
if(paraId==='p7') achievement('p7_done');
if(paraId==='p8') achievement('p8_done');
if(paraId==='p9') achievement('p9_done');
if(paraId==='final2') achievement('ch2_done');
});
}
function gcd(a,b){ a=Math.abs(a|0); b=Math.abs(b|0); while(b){ const t=b; b=a%b; a=t; } return a||1; }
function makeCard(kind, title, num, body){
const labels = {repeat:'Повторение',theory:'Теория',algo:'Алгоритм',rule:'Правило',example:'Пример',oral:'Устно'};
return '<div class="card"><div class="card-header"><div class="card-icon '+kind+'">'+ICONS[kind]+'</div><div class="card-title">'+(labels[kind]||'')+(title&&title!==labels[kind]?' \xb7 '+title:'')+'</div>'+(num?'<div class="card-num">'+num+'</div>':'')+'</div><div class="card-body">'+body+'</div></div>';
}
function setupSorter(cfg){
const placed = {}; const pool = document.getElementById(cfg.poolId); const scope = document.querySelector(cfg.scopeSelector);
if(!pool||!scope) return {placed,render:()=>{},reset:()=>{}};
pool.classList.add('dnd-pool'); if(cfg.columnLayout) pool.classList.add('col');
let armed = null;
function buildChip(it,isPlaced){ const e=document.createElement('div'); e.className='dnd-chip'+(isPlaced?' placed':''); e.dataset.id=it.id; e.innerHTML='<span class="dnd-txt">'+it.html+'</span><span class="dnd-x" title="Убрать">\xd7</span>'; attach(e,it.id); return e; }
function attach(elm,itId){ let ghost=null,dragging=false,sx=0,sy=0; elm.addEventListener('pointerdown',ev=>{ if(ev.button!==undefined&&ev.button!==0) return;
ev.preventDefault(); if(ev.target.classList&&ev.target.classList.contains('dnd-x')){ ev.stopPropagation(); if(placed[itId]){delete placed[itId];render();}else if(armed===itId){armed=null;render();} return; } sx=ev.clientX;sy=ev.clientY; const r=elm.getBoundingClientRect(); const ox=ev.clientX-r.left,oy=ev.clientY-r.top; try{elm.setPointerCapture(ev.pointerId);}catch(e){} function onMove(e){ const dx=e.clientX-sx,dy=e.clientY-sy; if(!dragging&&Math.hypot(dx,dy)>8){ dragging=true; ghost=elm.cloneNode(true); ghost.classList.remove('armed'); ghost.style.cssText='position:fixed;z-index:9999;pointer-events:none;opacity:.9;transform:rotate(-2.5deg);box-shadow:0 14px 36px rgba(0,0,0,.32);width:'+r.width+'px;left:'+(e.clientX-ox)+'px;top:'+(e.clientY-oy)+'px'; document.body.appendChild(ghost); elm.classList.add('dragging'); } if(dragging&&ghost){ ghost.style.left=(e.clientX-ox)+'px';ghost.style.top=(e.clientY-oy)+'px'; const under=document.elementsFromPoint(e.clientX,e.clientY); scope.querySelectorAll('.drop-box.over,.dnd-pool.over').forEach(n=>n.classList.remove('over')); const tgt=under.find(n=>n.classList&&(n.classList.contains('drop-box')||n.classList.contains('dnd-pool'))); if(tgt)tgt.classList.add('over'); } } function onUp(e){ elm.removeEventListener('pointermove',onMove);elm.removeEventListener('pointerup',onUp);elm.removeEventListener('pointercancel',onUp);elm.classList.remove('dragging'); if(ghost){ghost.remove();ghost=null;} scope.querySelectorAll('.drop-box.over,.dnd-pool.over').forEach(n=>n.classList.remove('over')); if(dragging){ const under=document.elementsFromPoint(e.clientX,e.clientY); const box=under.find(n=>n.classList&&n.classList.contains('drop-box')); const pl=under.find(n=>n.classList&&n.classList.contains('dnd-pool')); if(box){const di=box.querySelector('[data-cat]');if(di){placed[itId]=di.dataset.cat;armed=null;render();return;}}else if(pl){delete placed[itId];armed=null;render();return;} }else{ if(placed[itId]){delete placed[itId];armed=null;render();}else{armed=(armed===itId)?null:itId;render();} } dragging=false; } elm.addEventListener('pointermove',onMove);elm.addEventListener('pointerup',onUp);elm.addEventListener('pointercancel',onUp); }); }
function attachBoxTaps(){ scope.querySelectorAll('.drop-box').forEach(box=>{ box.addEventListener('click',ev=>{ if(!armed)return; if(ev.target.closest('.dnd-chip'))return; const di=box.querySelector('[data-cat]'); if(di){placed[armed]=di.dataset.cat;armed=null;render();} }); }); }
function render(){ pool.innerHTML=''; cfg.items.forEach(it=>{if(placed[it.id])return;const c=buildChip(it,false);if(armed===it.id)c.classList.add('armed');pool.appendChild(c);}); cfg.cats.forEach(cat=>{const box=scope.querySelector('.drop-items[data-cat="'+cat+'"]');if(!box)return;box.innerHTML='';cfg.items.forEach(it=>{if(placed[it.id]!==cat)return;box.appendChild(buildChip(it,true));});}); if(window.renderMathInElement)try{renderMath(scope);}catch(_){} }
attachBoxTaps(); render();
return {placed,render,reset(){ for(const k in placed)delete placed[k];armed=null;render(); }};
}
/* Координатная плоскость в SVG */
function axes2D(W, H, pad, xmin, xmax, ymin, ymax){
const ux = (W - 2*pad) / (xmax - xmin);
const uy = (H - 2*pad) / (ymax - ymin);
const toX = v => pad + (v - xmin) * ux;
const toY = v => H - pad - (v - ymin) * uy;
let g = '';
g += '<g stroke="#e5e7eb" stroke-width="1">';
for (let x = Math.ceil(xmin); x <= xmax; x++){
g += '<line x1="'+toX(x)+'" y1="'+pad+'" x2="'+toX(x)+'" y2="'+(H-pad)+'"/>';
}
for (let y = Math.ceil(ymin); y <= ymax; y++){
g += '<line x1="'+pad+'" y1="'+toY(y)+'" x2="'+(W-pad)+'" y2="'+toY(y)+'"/>';
}
g += '</g>';
const y0 = toY(0), x0 = toX(0);
g += '<line x1="'+pad+'" y1="'+y0+'" x2="'+(W-pad)+'" y2="'+y0+'" stroke="#0f172a" stroke-width="1.5"/>';
g += '<line x1="'+x0+'" y1="'+pad+'" x2="'+x0+'" y2="'+(H-pad)+'" stroke="#0f172a" stroke-width="1.5"/>';
g += '<text x="'+(W-pad+2)+'" y="'+(y0-4)+'" font-size="11" fill="#0f172a">x</text>';
g += '<text x="'+(x0+4)+'" y="'+(pad-2)+'" font-size="11" fill="#0f172a">y</text>';
g += '<g font-size="10" fill="#64748b">';
for (let x = Math.ceil(xmin); x <= xmax; x++){
if (x !== 0) g += '<text x="'+(toX(x)-3)+'" y="'+(y0+12)+'">'+x+'</text>';
}
for (let y = Math.ceil(ymin); y <= ymax; y++){
if (y !== 0) g += '<text x="'+(x0+4)+'" y="'+(toY(y)+3)+'">'+y+'</text>';
}
g += '<text x="'+(x0+4)+'" y="'+(y0+12)+'">0</text>';
g += '</g>';
return { content: g, toX, toY, ux, uy };
}
/* График функции y=f(x) — возвращает строку <path ...> */
function plotFunc(f, xmin, xmax, toX, toY, color, N){
N = N || 200;
let d = '';
let prevValid = false;
for (let i = 0; i <= N; i++){
const x = xmin + (xmax - xmin) * i / N;
let y;
try { y = f(x); } catch(e){ y = NaN; }
if (!isFinite(y) || isNaN(y) || y < -1e4 || y > 1e4){ prevValid = false; continue; }
d += (prevValid ? ' L' : ' M') + toX(x).toFixed(2) + ',' + toY(y).toFixed(2);
prevValid = true;
}
return '<path d="'+d+'" stroke="'+color+'" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>';
}
/* ===== STUB BUILDERS — наполнение в Phase 1+ ===== */
function buildP6(){
const root = document.getElementById('p6-body');
root.innerHTML = `
<div class="card">
<div class="card-header">
<span class="card-icon theory">${ICONS.theory}</span>
<span class="card-title">В разработке</span>
<span class="card-num">§ 6</span>
</div>
<div class="card-body">
<p>Содержание параграфа <b>«Функция числового аргумента»</b> будет добавлено в следующих обновлениях.</p>
<p style="color:var(--muted);font-size:.9rem">Раздел Phase 1.</p>
</div>
</div>` + secNav(null, 'p7') + readButton('p6');
renderMath(root);
const box = document.getElementById('p6-body');
let html = '';
html += makeCard('theory', 'Определение функции', '6.1', `
<p>Если каждому значению $x$ из некоторого множества $X$ поставлено в соответствие <b>единственное</b> значение $y$, то говорят, что $y$ есть <b>функция</b> от $x$. Записывают $y = f(x)$.</p>
<p>Переменную $x$ называют <b>аргументом</b> (или независимой переменной), а $y$ — <b>значением функции</b> (зависимой переменной).</p>
<ul style="padding-left:22px;line-height:1.9">
<li>Множество $X$ — <b>область определения</b> функции, обозначается $D(f)$.</li>
<li>Множество всех значений $y = f(x)$ — <b>область значений</b>, обозначается $E(f)$.</li>
</ul>
<details class="spoiler"><summary>А что если двум разным $x$ соответствует одно $y$?</summary><div class="spoiler-body">
Это нормально! Главное требование: одному $x$ — ровно одно $y$. А разные $x$ могут давать одно и то же $y$ — например, $f(x) = x^2$: при $x = -2$ и $x = 2$ получаем одно значение $y = 4$.
</div></details>`);
html += makeCard('rule', 'Три способа задания функции', '6.2', `
<p>Функцию можно задать одним из трёх основных способов:</p>
<ol style="padding-left:22px;line-height:2">
<li><b>Формулой (аналитически):</b> $y = 2x + 1$, $y = \\dfrac{1}{x}$, $y = \\sqrt{x}$.</li>
<li><b>Таблицей:</b> две строки — значения $x$ и соответствующие $y$.</li>
<li><b>Графиком:</b> точки $(x; f(x))$ в координатной плоскости.</li>
</ol>
<p>Иногда функцию задают <b>словесным описанием</b>: «каждому натуральному числу поставлен в соответствие его последний разряд».</p>`);
html += makeCard('example', 'Нахождение области определения', '6.3', `
<p><b>а)</b> $f(x) = \\dfrac{1}{x - 3}$. Знаменатель не равен нулю: $x - 3 \\ne 0 \\Rightarrow x \\ne 3$. <b>$D(f) = (-\\infty; 3) \\cup (3; +\\infty)$.</b></p>
<p><b>б)</b> $f(x) = \\sqrt{x - 2}$. Подкоренное выражение неотрицательно: $x - 2 \\ge 0 \\Rightarrow x \\ge 2$. <b>$D(f) = [2; +\\infty)$.</b></p>
<p><b>в)</b> $f(x) = x^2 + 1$. Никаких ограничений нет. <b>$D(f) = \\mathbb{R}$.</b></p>
<p><b>г)</b> $f(x) = \\dfrac{\\sqrt{x}}{x - 5}$. Сразу два условия: $x \\ge 0$ и $x \\ne 5$. <b>$D(f) = [0; 5) \\cup (5; +\\infty)$.</b></p>`);
/* INTERACTIVE 1 — график + точка */
html += `<div class="wg" id="p6-iv1">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">График и точка $(x_0; f(x_0))$</div></div>
<div class="wg-help">Выбери функцию ползунком, а потом — значение $x_0$. На графике появится точка $(x_0; f(x_0))$ и пунктирные линии к осям.</div>
<div class="sliders">
<label>Функция №<b id="p6-iv1-fi">1</b> / 5<input type="range" id="p6-iv1-fn" min="1" max="5" step="1" value="1"></label>
<label>$x_0$ =<b id="p6-iv1-xv">0</b><input type="range" id="p6-iv1-xs" min="-5" max="5" step="0.25" value="0"></label>
</div>
<div id="p6-iv1-formula" style="text-align:center;font-size:1.1rem;padding:10px;background:var(--card);border-radius:9px;margin-bottom:10px"></div>
<div style="background:var(--card);border-radius:10px;padding:10px;overflow-x:auto">
<svg id="p6-iv1-svg" viewBox="0 0 360 280" style="width:100%;max-width:480px;height:auto;display:block;margin:0 auto"></svg>
</div>
<div id="p6-iv1-out" style="margin-top:10px;padding:10px 14px;background:var(--sec-acc-soft);border-radius:9px;font-size:.95rem;text-align:center"></div>
</div>`;
/* INTERACTIVE 2 — функция или нет */
html += `<div class="wg" id="p6-iv2">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Функция или нет?</div></div>
<div class="wg-help">Помни главное правило: одному $x$ — ровно одно $y$. Если хотя бы одному $x$ соответствует два разных $y$ — это уже не функция.</div>
<div class="score-display">Задача: <b id="p6-iv2-idx">1</b> / 6 &middot; Очки: <b id="p6-iv2-sc">0</b></div>
<div id="p6-iv2-q" style="text-align:center;font-size:1.05rem;padding:14px;background:var(--card);border-radius:9px;margin-bottom:12px;min-height:60px"></div>
<div class="actions" style="justify-content:center">
<button class="btn primary" id="p6-iv2-y">Да, функция</button>
<button class="btn" id="p6-iv2-n">Нет, не функция</button>
</div>
<div class="feedback" id="p6-iv2-fb"></div>
</div>`;
/* INTERACTIVE 3 — DnD сортер D(f) */
html += `<div class="wg" id="p6-iv3">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 3</span><div class="wg-title">Найди $D(f)$</div></div>
<div class="wg-help">Перетащи каждую функцию в подходящий ящик с её областью определения.</div>
<div id="p6-iv3-pool"></div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px">
<div class="drop-box"><h5>$\\mathbb{R}$</h5><div class="drop-items" data-cat="R"></div></div>
<div class="drop-box"><h5>Все, кроме точки</h5><div class="drop-items" data-cat="hole"></div></div>
<div class="drop-box"><h5>Луч $[a; +\\infty)$</h5><div class="drop-items" data-cat="ray"></div></div>
</div>
<div class="actions"><button class="btn primary" id="p6-iv3-check">Проверить</button><button class="btn" id="p6-iv3-reset">Сбросить</button></div>
<div class="feedback" id="p6-iv3-fb"></div>
</div>`;
/* INTERACTIVE 4 — калькулятор f(x) */
html += `<div class="wg" id="p6-iv4">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 4</span><div class="wg-title">Калькулятор $f(x)$</div></div>
<div class="wg-help">Выбери функцию из списка и значение $x$. Калькулятор проверит принадлежность $D(f)$ и посчитает $y$.</div>
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;justify-content:center;margin-bottom:10px">
<select id="p6-iv4-fn" class="tinp" style="min-width:200px">
<option value="0">y = x²</option>
<option value="1">y = 2x + 5</option>
<option value="2">y = 1/x</option>
<option value="3">y = √x</option>
<option value="4">y = |x|</option>
<option value="5">y = (x+1)/(x-3)</option>
</select>
<span style="font-family:'JetBrains Mono',monospace">$x$ =</span>
<input type="number" id="p6-iv4-x" class="tinp" style="width:90px;text-align:center" value="2" step="0.5">
<button class="btn primary" id="p6-iv4-go">Вычислить</button>
</div>
<div id="p6-iv4-out" style="padding:12px 14px;background:var(--card);border-radius:9px;text-align:center;font-size:1rem;min-height:48px"></div>
</div>`;
box.innerHTML = html + secNav(null, 'p7') + readButton('p6');
renderMath(box);
/* ===== IV1 wiring ===== */
(function(){
const fns = [
{ label: 'y = 2x - 1', f: x=>2*x-1, ok: x=>true },
{ label: 'y = x²', f: x=>x*x, ok: x=>true },
{ label: 'y = -x + 3', f: x=>-x+3, ok: x=>true },
{ label: 'y = 6/x', f: x=>6/x, ok: x=>x!==0 },
{ label: 'y = √x', f: x=>Math.sqrt(x), ok: x=>x>=0 }
];
const svg = document.getElementById('p6-iv1-svg');
const fnSl = document.getElementById('p6-iv1-fn');
const xSl = document.getElementById('p6-iv1-xs');
const fi = document.getElementById('p6-iv1-fi');
const xv = document.getElementById('p6-iv1-xv');
const formula = document.getElementById('p6-iv1-formula');
const out = document.getElementById('p6-iv1-out');
let bumped = false;
function redraw(){
const idx = (+fnSl.value)-1;
const x0 = +xSl.value;
const fobj = fns[idx];
fi.textContent = (idx+1);
xv.textContent = fmt(x0);
const ax = axes2D(360, 280, 28, -6, 6, -6, 6);
let g = ax.content;
g += plotFunc(fobj.f, -6, 6, ax.toX, ax.toY, '#059669', 300);
let outHtml = '';
if (fobj.ok(x0)){
const y0 = fobj.f(x0);
if (isFinite(y0) && y0>=-6 && y0<=6){
const px = ax.toX(x0), py = ax.toY(y0);
g += '<line x1="'+px+'" y1="'+py+'" x2="'+px+'" y2="'+ax.toY(0)+'" stroke="#ef4444" stroke-width="1.2" stroke-dasharray="4 3"/>';
g += '<line x1="'+px+'" y1="'+py+'" x2="'+ax.toX(0)+'" y2="'+py+'" stroke="#ef4444" stroke-width="1.2" stroke-dasharray="4 3"/>';
g += '<circle cx="'+px+'" cy="'+py+'" r="5" fill="#ef4444" stroke="#fff" stroke-width="2"/>';
}
outHtml = '$f(' + fmt(x0) + ') = ' + fmt(+y0.toFixed(4)) + '$';
} else {
outHtml = '$f(' + fmt(x0) + ')$ — не определена';
}
svg.innerHTML = g;
formula.innerHTML = '$' + fobj.label.replace('y =','y =').replace('²','^2').replace('√','\\sqrt') + '$';
// отрисовать формулу из набора: подменим на корректные KaTeX-варианты
const tex = ['y = 2x - 1','y = x^2','y = -x + 3','y = \\dfrac{6}{x}','y = \\sqrt{x}'];
formula.innerHTML = '$' + tex[idx] + '$';
out.innerHTML = outHtml;
renderMath(formula); renderMath(out);
if (!bumped){ bumped = true; bumpProgress('p6', 15); addXp(10,'p6-iv1'); }
}
fnSl.addEventListener('input', redraw);
xSl.addEventListener('input', redraw);
redraw();
})();
/* ===== IV2 wiring ===== */
(function(){
const items = [
{ q: 'Соответствие $\\{(1,2),\\ (2,4),\\ (3,6)\\}$', ans: true, hint: 'Каждому $x$ — один $y$. Это функция.' },
{ q: 'Соответствие $\\{(1,2),\\ (1,3),\\ (2,4)\\}$', ans: false, hint: 'При $x = 1$ получаем сразу 2 и 3. Не функция.' },
{ q: '$y = x^2$', ans: true, hint: 'Каждому $x$ соответствует ровно одно $y$.' },
{ q: '$y^2 = x$', ans: false, hint: 'При $x = 4$ получаем $y = 2$ и $y = -2$. Не функция.' },
{ q: '$y = |x|$', ans: true, hint: 'Один $x$ → один $y$.' },
{ q: 'Окружность $x^2 + y^2 = 9$', ans: false, hint: 'При $x = 0$ есть $y = 3$ и $y = -3$. Не функция.' }
];
let i = 0, sc = 0;
const idxEl = document.getElementById('p6-iv2-idx');
const scEl = document.getElementById('p6-iv2-sc');
const qEl = document.getElementById('p6-iv2-q');
const fb = document.getElementById('p6-iv2-fb');
const yBtn = document.getElementById('p6-iv2-y');
const nBtn = document.getElementById('p6-iv2-n');
let bumped = false;
function render(){
idxEl.textContent = Math.min(i+1, items.length);
scEl.textContent = sc;
if (i >= items.length){
qEl.innerHTML = '<b>Готово!</b> Результат: ' + sc + ' / ' + items.length;
yBtn.disabled = true; nBtn.disabled = true;
yBtn.style.opacity = .5; nBtn.style.opacity = .5;
if (!bumped){ bumped = true; bumpProgress('p6', 15); addXp(10,'p6-iv2'); }
return;
}
qEl.innerHTML = items[i].q;
fb.style.display = 'none';
renderMath(qEl);
}
function answer(v){
if (i >= items.length) return;
const it = items[i];
const ok = (v === it.ans);
if (ok) sc++;
feedback(fb, ok, (ok?'&#10003; Верно. ':'&#10007; Неверно. ') + it.hint);
i++;
setTimeout(render, 900);
}
yBtn.addEventListener('click', ()=>answer(true));
nBtn.addEventListener('click', ()=>answer(false));
render();
})();
/* ===== IV3 wiring — DnD sorter ===== */
(function(){
const items = [
{ id:'a', html:'$f(x) = x^2 + 1$', cat:'R' },
{ id:'b', html:'$f(x) = \\dfrac{1}{x}$', cat:'hole' },
{ id:'c', html:'$f(x) = \\sqrt{x}$', cat:'ray' },
{ id:'d', html:'$f(x) = \\dfrac{1}{x-2}$', cat:'hole' },
{ id:'e', html:'$f(x) = \\sqrt{x-3}$', cat:'ray' },
{ id:'f', html:'$f(x) = 5x - 7$', cat:'R' }
];
const sorter = setupSorter({
poolId: 'p6-iv3-pool',
scopeSelector: '#p6-iv3',
cats: ['R','hole','ray'],
items: items
});
let bumped = false;
document.getElementById('p6-iv3-check').addEventListener('click', ()=>{
const fb = document.getElementById('p6-iv3-fb');
const total = items.length;
let correct = 0, placed = 0;
items.forEach(it=>{ if(sorter.placed[it.id]){ placed++; if(sorter.placed[it.id]===it.cat) correct++; } });
if (placed < total){
feedback(fb, false, 'Размещены не все: ' + placed + ' / ' + total + '.');
return;
}
const ok = (correct === total);
feedback(fb, ok, ok ? '&#10003; Все верно! ' + correct + ' / ' + total : '&#10007; Правильно: ' + correct + ' / ' + total);
if (ok && !bumped){ bumped = true; bumpProgress('p6', 25); addXp(15,'p6-iv3'); }
});
document.getElementById('p6-iv3-reset').addEventListener('click', ()=>{
sorter.reset();
const fb = document.getElementById('p6-iv3-fb'); fb.style.display='none';
});
})();
/* ===== IV4 wiring — calculator ===== */
(function(){
const fns = [
{ tex:'y = x^2', f:x=>x*x, ok:x=>true, bad:'' },
{ tex:'y = 2x + 5', f:x=>2*x+5, ok:x=>true, bad:'' },
{ tex:'y = \\dfrac{1}{x}', f:x=>1/x, ok:x=>x!==0, bad:'$x = 0$' },
{ tex:'y = \\sqrt{x}', f:x=>Math.sqrt(x), ok:x=>x>=0, bad:'$x < 0$' },
{ tex:'y = |x|', f:x=>Math.abs(x), ok:x=>true, bad:'' },
{ tex:'y = \\dfrac{x+1}{x-3}', f:x=>(x+1)/(x-3), ok:x=>x!==3, bad:'$x = 3$' }
];
const out = document.getElementById('p6-iv4-out');
let bumped = false;
document.getElementById('p6-iv4-go').addEventListener('click', ()=>{
const idx = +document.getElementById('p6-iv4-fn').value;
const x = +document.getElementById('p6-iv4-x').value;
const fn = fns[idx];
if (!fn.ok(x)){
out.innerHTML = 'Функция $' + fn.tex + '$ <b>не определена</b> при ' + fn.bad + '.';
} else {
const y = fn.f(x);
out.innerHTML = '$' + fn.tex + '$, при $x = ' + fmt(x) + '$: $y = ' + fmt(+y.toFixed(6)) + '$.';
}
renderMath(out);
if (!bumped){ bumped = true; bumpProgress('p6', 25); addXp(15,'p6-iv4'); }
});
})();
wireReadBtn('p6');
}
function buildP7(){
const root = document.getElementById('p7-body');
root.innerHTML = `
<div class="card">
<div class="card-header">
<span class="card-icon theory">${ICONS.theory}</span>
<span class="card-title">В разработке</span>
<span class="card-num">§ 7</span>
</div>
<div class="card-body">
<p>Содержание параграфа <b>«Свойства функции»</b> будет добавлено в следующих обновлениях.</p>
<p style="color:var(--muted);font-size:.9rem">Раздел Phase 1.</p>
</div>
</div>` + secNav('p6', 'p8') + readButton('p7');
renderMath(root);
const box = document.getElementById('p7-body');
let html = '';
html += makeCard('theory', 'Возрастание и убывание', '7.1', `
<p>Функция $f$ <b>возрастает</b> на промежутке $I$, если для любых $x_1, x_2 \\in I$ из $x_1 < x_2$ следует $f(x_1) < f(x_2)$ (бо́льшему аргументу — бо́льшее значение).</p>
<p>Функция $f$ <b>убывает</b> на промежутке $I$, если для любых $x_1, x_2 \\in I$ из $x_1 < x_2$ следует $f(x_1) > f(x_2)$ (бо́льшему аргументу — меньшее значение).</p>
<p>Функция называется <b>монотонной</b> на $I$, если она возрастает или убывает на $I$.</p>
<details class="spoiler"><summary>Пример</summary><div class="spoiler-body">
$y = x^2$ убывает на $(-\\infty; 0]$ и возрастает на $[0; +\\infty)$. На всём $\\mathbb{R}$ — не монотонна.
</div></details>`);
html += makeCard('rule', 'Нули и знакопостоянство', '7.2', `
<p><b>Нуль</b> функции — такое значение $x_0$, при котором $f(x_0) = 0$. Графически — точка пересечения графика с осью $Ox$.</p>
<p>Чтобы найти нули, решают уравнение $f(x) = 0$.</p>
<p><b>Промежутки знакопостоянства</b> — те, на которых функция сохраняет знак:</p>
<ul style="padding-left:22px;line-height:1.9">
<li>$f(x) > 0$ — график над осью $Ox$;</li>
<li>$f(x) < 0$ — график под осью $Ox$.</li>
</ul>
<p>Пример. Для $f(x) = x^2 - 4$: нули $x = \\pm 2$; $f(x) > 0$ на $(-\\infty;-2) \\cup (2;+\\infty)$; $f(x) < 0$ на $(-2; 2)$.</p>`);
html += makeCard('example', 'Экстремумы функции', '7.3', `
<p>Точка $x_0$ — <b>точка максимума</b>, если в некоторой её окрестности $f(x_0) \\ge f(x)$ для всех $x$. Значение $f(x_0)$ называют <b>максимумом</b> и пишут $f_{\\max}$.</p>
<p>Аналогично, $x_0$ — <b>точка минимума</b>, если $f(x_0) \\le f(x)$ в окрестности. Значение — $f_{\\min}$.</p>
<p>Примеры:</p>
<ul style="padding-left:22px;line-height:1.9">
<li>$y = x^2$ — точка минимума $x = 0$, $f_{\\min} = 0$.</li>
<li>$y = -x^2 + 1$ — точка максимума $x = 0$, $f_{\\max} = 1$.</li>
<li>$y = x^3$ — экстремумов нет (функция возрастает всюду).</li>
</ul>`);
/* INTERACTIVE 1 — анализ графика */
html += `<div class="wg" id="p7-iv1">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Анализ графика</div></div>
<div class="wg-help">Выбери функцию ползунком. На графике видны нули (красные точки) и экстремум (синяя звёздочка), а под графиком — все свойства функции.</div>
<div class="sliders">
<label>Функция №<b id="p7-iv1-fi">1</b> / 5<input type="range" id="p7-iv1-fn" min="1" max="5" step="1" value="1"></label>
</div>
<div id="p7-iv1-formula" style="text-align:center;font-size:1.1rem;padding:10px;background:var(--card);border-radius:9px;margin-bottom:10px"></div>
<div style="background:var(--card);border-radius:10px;padding:10px;overflow-x:auto">
<svg id="p7-iv1-svg" viewBox="0 0 400 280" style="width:100%;max-width:520px;height:auto;display:block;margin:0 auto"></svg>
</div>
<div id="p7-iv1-out" style="margin-top:10px;padding:12px 14px;background:var(--sec-acc-soft);border-radius:9px;font-size:.92rem;line-height:1.7"></div>
</div>`;
/* INTERACTIVE 2 — нули функции */
html += `<div class="wg" id="p7-iv2">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Найди нули функции</div></div>
<div class="wg-help">Введи <b>сумму</b> всех нулей функции. Если нулей нет — введи <b>-999</b>. Если бесконечно много — введи <b>999</b>.</div>
<div class="score-display">Задача: <b id="p7-iv2-idx">1</b> / 6 &middot; Очки: <b id="p7-iv2-sc">0</b></div>
<div id="p7-iv2-q" style="text-align:center;font-size:1.1rem;padding:14px;background:var(--card);border-radius:9px;margin-bottom:12px"></div>
<div style="display:flex;gap:10px;justify-content:center;flex-wrap:wrap;align-items:center">
<span>Сумма нулей =</span>
<input type="number" id="p7-iv2-in" class="tinp" style="width:110px;text-align:center" step="1">
<button class="btn primary" id="p7-iv2-go">Проверить</button>
</div>
<div class="feedback" id="p7-iv2-fb"></div>
</div>`;
/* INTERACTIVE 3 — возрастает или убывает */
html += `<div class="wg" id="p7-iv3">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 3</span><div class="wg-title">Возрастает или убывает?</div></div>
<div class="wg-help">Для каждой функции и промежутка выбери: возрастает она или убывает.</div>
<div class="score-display">Задача: <b id="p7-iv3-idx">1</b> / 6 &middot; Очки: <b id="p7-iv3-sc">0</b></div>
<div id="p7-iv3-q" style="text-align:center;font-size:1.05rem;padding:14px;background:var(--card);border-radius:9px;margin-bottom:12px;min-height:60px"></div>
<div class="actions" style="justify-content:center">
<button class="btn primary" id="p7-iv3-u">Возрастает</button>
<button class="btn" id="p7-iv3-d">Убывает</button>
</div>
<div class="feedback" id="p7-iv3-fb"></div>
</div>`;
/* INTERACTIVE 4 — DnD сортер */
html += `<div class="wg" id="p7-iv4">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 4</span><div class="wg-title">Свойства: возрастает / убывает</div></div>
<div class="wg-help">Перетащи каждую карточку с функцией и промежутком в нужный ящик.</div>
<div id="p7-iv4-pool"></div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<div class="drop-box"><h5>Возрастает</h5><div class="drop-items" data-cat="up"></div></div>
<div class="drop-box"><h5>Убывает</h5><div class="drop-items" data-cat="down"></div></div>
</div>
<div class="actions"><button class="btn primary" id="p7-iv4-check">Проверить</button><button class="btn" id="p7-iv4-reset">Сбросить</button></div>
<div class="feedback" id="p7-iv4-fb"></div>
</div>`;
box.innerHTML = html + secNav('p6', 'p8') + readButton('p7');
renderMath(box);
/* ===== IV1 wiring — анализ графика ===== */
(function(){
const fns = [
{
tex: 'y = x^2 - 4',
f: x=>x*x - 4,
zeros: [-2, 2],
ext: {x:0, y:-4, kind:'min'},
desc: 'Нули: $x = -2,\\ x = 2$. Точка минимума $x = 0$, $f_{\\min} = -4$. Убывает на $(-\\infty; 0]$, возрастает на $[0; +\\infty)$.'
},
{
tex: 'y = -x^2 + 3',
f: x=>-x*x + 3,
zeros: [-Math.sqrt(3), Math.sqrt(3)],
ext: {x:0, y:3, kind:'max'},
desc: 'Нули: $x = \\pm\\sqrt{3}$. Точка максимума $x = 0$, $f_{\\max} = 3$. Возрастает на $(-\\infty; 0]$, убывает на $[0; +\\infty)$.'
},
{
tex: 'y = x^3',
f: x=>x*x*x,
zeros: [0],
ext: null,
desc: 'Нуль: $x = 0$. Экстремумов нет. Возрастает на всей числовой прямой $\\mathbb{R}$.'
},
{
tex: 'y = (x - 2)^2 - 1',
f: x=>(x-2)*(x-2) - 1,
zeros: [1, 3],
ext: {x:2, y:-1, kind:'min'},
desc: 'Нули: $x = 1,\\ x = 3$. Точка минимума $x = 2$, $f_{\\min} = -1$. Убывает на $(-\\infty; 2]$, возрастает на $[2; +\\infty)$.'
},
{
tex: 'y = -|x| + 2',
f: x=>-Math.abs(x) + 2,
zeros: [-2, 2],
ext: {x:0, y:2, kind:'max'},
desc: 'Нули: $x = \\pm 2$. Точка максимума $x = 0$, $f_{\\max} = 2$. Возрастает на $(-\\infty; 0]$, убывает на $[0; +\\infty)$.'
}
];
const sl = document.getElementById('p7-iv1-fn');
const fi = document.getElementById('p7-iv1-fi');
const svg = document.getElementById('p7-iv1-svg');
const formula = document.getElementById('p7-iv1-formula');
const out = document.getElementById('p7-iv1-out');
let bumped = false;
function redraw(){
const idx = (+sl.value)-1;
const fobj = fns[idx];
fi.textContent = (idx+1);
const ax = axes2D(400, 280, 30, -5, 5, -4, 6);
let g = ax.content;
g += plotFunc(fobj.f, -5, 5, ax.toX, ax.toY, '#10b981', 300);
// нули
fobj.zeros.forEach(z=>{
if (z>=-5 && z<=5){
const cx = ax.toX(z), cy = ax.toY(0);
g += '<circle cx="'+cx+'" cy="'+cy+'" r="5" fill="#ef4444" stroke="#fff" stroke-width="2"/>';
}
});
// экстремум — звёздочка
if (fobj.ext){
const ex = fobj.ext;
if (ex.x>=-5 && ex.x<=5 && ex.y>=-4 && ex.y<=6){
const cx = ax.toX(ex.x), cy = ax.toY(ex.y);
// 5-конечная звезда
let pts = '';
for (let i=0;i<10;i++){
const r = (i%2===0) ? 7 : 3.2;
const a = -Math.PI/2 + i*Math.PI/5;
pts += (cx + r*Math.cos(a)).toFixed(2) + ',' + (cy + r*Math.sin(a)).toFixed(2) + ' ';
}
g += '<polygon points="'+pts.trim()+'" fill="#2563eb" stroke="#fff" stroke-width="1.5"/>';
}
}
svg.innerHTML = g;
formula.innerHTML = '$' + fobj.tex + '$';
out.innerHTML = fobj.desc;
renderMath(formula); renderMath(out);
if (!bumped){ bumped = true; bumpProgress('p7', 15); addXp(10,'p7-iv1'); }
}
sl.addEventListener('input', redraw);
redraw();
})();
/* ===== IV2 wiring — нули ===== */
(function(){
const items = [
{ q: '$f(x) = x - 5$', ans: 5 },
{ q: '$f(x) = x^2 - 9$', ans: 0 },
{ q: '$f(x) = (x+1)(x-4)$', ans: 3 },
{ q: '$f(x) = x^2 + 1$', ans: -999 },
{ q: '$f(x) = x^2 - 6x + 9$', ans: 3 },
{ q: '$f(x) = x^3 - 8$', ans: 2 }
];
let i = 0, sc = 0;
const idxEl = document.getElementById('p7-iv2-idx');
const scEl = document.getElementById('p7-iv2-sc');
const qEl = document.getElementById('p7-iv2-q');
const inEl = document.getElementById('p7-iv2-in');
const fb = document.getElementById('p7-iv2-fb');
const goBtn = document.getElementById('p7-iv2-go');
let bumped = false;
function render(){
idxEl.textContent = Math.min(i+1, items.length);
scEl.textContent = sc;
if (i >= items.length){
qEl.innerHTML = '<b>Готово!</b> Результат: ' + sc + ' / ' + items.length;
inEl.disabled = true; goBtn.disabled = true; goBtn.style.opacity = .5;
if (!bumped){ bumped = true; bumpProgress('p7', 15); addXp(10,'p7-iv2'); }
return;
}
qEl.innerHTML = 'Функция: ' + items[i].q;
inEl.value = '';
fb.style.display = 'none';
renderMath(qEl);
inEl.focus();
}
function check(){
if (i >= items.length) return;
const v = +inEl.value;
if (!Number.isFinite(v) || inEl.value === '') { feedback(fb, false, 'Введи число.'); return; }
const it = items[i];
const ok = (v === it.ans);
if (ok) sc++;
let hint = '';
if (it.ans === -999) hint = 'Нулей нет.';
else if (it.ans === 999) hint = 'Нулей бесконечно много.';
else hint = 'Сумма нулей = ' + it.ans + '.';
feedback(fb, ok, (ok?'&#10003; Верно. ':'&#10007; Неверно. ') + hint);
i++;
setTimeout(render, 1100);
}
goBtn.addEventListener('click', check);
inEl.addEventListener('keydown', e=>{ if (e.key === 'Enter') check(); });
render();
})();
/* ===== IV3 wiring — возрастает / убывает ===== */
(function(){
const items = [
{ q: '$y = 2x + 1$ на $\\mathbb{R}$', up: true, hint: 'Линейная с положительным $k$. Возрастает.' },
{ q: '$y = -3x + 5$ на $\\mathbb{R}$', up: false, hint: 'Линейная с отрицательным $k$. Убывает.' },
{ q: '$y = x^2$ на $(-\\infty; 0]$', up: false, hint: 'Левая ветвь параболы. Убывает.' },
{ q: '$y = x^2$ на $[0; +\\infty)$', up: true, hint: 'Правая ветвь параболы. Возрастает.' },
{ q: '$y = \\dfrac{1}{x}$ на $(0; +\\infty)$', up: false, hint: 'Гипербола в I четверти. Убывает.' },
{ q: '$y = x^3$ на $\\mathbb{R}$', up: true, hint: 'Кубическая. Возрастает всюду.' }
];
let i = 0, sc = 0;
const idxEl = document.getElementById('p7-iv3-idx');
const scEl = document.getElementById('p7-iv3-sc');
const qEl = document.getElementById('p7-iv3-q');
const fb = document.getElementById('p7-iv3-fb');
const uBtn = document.getElementById('p7-iv3-u');
const dBtn = document.getElementById('p7-iv3-d');
let bumped = false;
function render(){
idxEl.textContent = Math.min(i+1, items.length);
scEl.textContent = sc;
if (i >= items.length){
qEl.innerHTML = '<b>Готово!</b> Результат: ' + sc + ' / ' + items.length;
uBtn.disabled = true; dBtn.disabled = true;
uBtn.style.opacity = .5; dBtn.style.opacity = .5;
if (!bumped){ bumped = true; bumpProgress('p7', 25); addXp(15,'p7-iv3'); }
return;
}
qEl.innerHTML = items[i].q;
fb.style.display = 'none';
renderMath(qEl);
}
function answer(v){
if (i >= items.length) return;
const it = items[i];
const ok = (v === it.up);
if (ok) sc++;
feedback(fb, ok, (ok?'&#10003; Верно. ':'&#10007; Неверно. ') + it.hint);
i++;
setTimeout(render, 900);
}
uBtn.addEventListener('click', ()=>answer(true));
dBtn.addEventListener('click', ()=>answer(false));
render();
})();
/* ===== IV4 wiring — DnD sorter ===== */
(function(){
const items = [
{ id:'a', html:'$y = 5x$ на $\\mathbb{R}$', cat:'up' },
{ id:'b', html:'$y = -2x$ на $\\mathbb{R}$', cat:'down' },
{ id:'c', html:'$y = x^2$ на $[0;\\,3]$', cat:'up' },
{ id:'d', html:'$y = x^2$ на $[-3;\\,0]$', cat:'down' },
{ id:'e', html:'$y = x^3$ на $\\mathbb{R}$', cat:'up' },
{ id:'f', html:'$y = \\dfrac{1}{x}$ на $(0;\\,+\\infty)$', cat:'down' }
];
const sorter = setupSorter({
poolId: 'p7-iv4-pool',
scopeSelector: '#p7-iv4',
cats: ['up','down'],
items: items
});
let bumped = false;
document.getElementById('p7-iv4-check').addEventListener('click', ()=>{
const fb = document.getElementById('p7-iv4-fb');
const total = items.length;
let correct = 0, placed = 0;
items.forEach(it=>{ if(sorter.placed[it.id]){ placed++; if(sorter.placed[it.id]===it.cat) correct++; } });
if (placed < total){ feedback(fb, false, 'Размещены не все: ' + placed + ' / ' + total + '.'); return; }
const ok = (correct === total);
feedback(fb, ok, ok ? '&#10003; Все верно! ' + correct + ' / ' + total : '&#10007; Правильно: ' + correct + ' / ' + total);
if (ok && !bumped){ bumped = true; bumpProgress('p7', 25); addXp(15,'p7-iv4'); }
});
document.getElementById('p7-iv4-reset').addEventListener('click', ()=>{
sorter.reset();
const fb = document.getElementById('p7-iv4-fb'); fb.style.display='none';
});
})();
wireReadBtn('p7');
}