feat(textbooks): Wave Depth — 4 прокачанных интерактива (+474 строки)

1. §1 «Извлечение в столбик» — пошаговая анимация
   - Поле ввода числа + пресеты 1296/2916/7744
   - Async-функция clStart() рендерит классическое 'деление в столбик'
   - JetBrains Mono шрифт, подсветка текущей грани цветом секции
   - Поясняющий текст для каждого шага рядом
   - При остатке 0: confetti + 15 XP + ачивка 'col-root'
   - Для нецелых корней — корректно показывает остаток

2. §4 «Сравнение через квадраты» — визуальное доказательство
   - 5 пар: 3√2 vs 2√3, 4√3 vs 3√5, √17 vs 4, ...
   - SVG с двумя анимированно растущими квадратами (transform scale 0→1, spring)
   - Победитель — бейдж в верхней части
   - Под квадратами: (3√2)² = 18 > 12 = (2√3)²

3. §5 «Эйлеровы диаграммы» — альтернатива линейной визуализации
   - 4 слайдера для границ A и B
   - Два эллипса (pink/blue) с пересечением
   - Режимы: 'Показать ∪' (золотой контур), '∩' (зелёная штриховка), 'Оба'
   - Дополняет существующую линейную визуализацию

4. §6 «Решатель систем 3+ неравенств» — расширен с 2 до 5
   - Динамический контейнер #sys-list с массивом _sysRows
   - Кнопка '+ Добавить неравенство' (до 5)
   - Кнопка '×' удаляет (кроме первой)
   - SVG-прямая динамически масштабируется под N строк
   - Совместимость с sysMode/solveLin сохранена
This commit is contained in:
Maxim Dolgolyov
2026-05-27 13:39:11 +03:00
parent aed820c2d1
commit beebdadca0
+619 -76
View File
@@ -660,6 +660,26 @@ input,select,textarea{font-family:inherit}
/* Sound indicator */
#sound-muted-hint{display:none}
/* ═══════════════════════════════════════════════
WAVE DEPTH — 4 new widgets
═══════════════════════════════════════════════ */
/* Widget 1: Column root extraction */
.cl-workspace{font-family:'JetBrains Mono',monospace;font-size:1.05rem;background:var(--card);border:1px solid var(--border);border-radius:11px;padding:18px 22px;margin-top:14px;line-height:1.7;min-height:120px;white-space:pre}
.cl-workspace .cl-active{background:var(--sec-acc-soft,#fce7f3);padding:0 4px;border-radius:3px;font-weight:700;color:var(--sec-acc,var(--pri))}
.cl-workspace .cl-result{color:var(--ok);font-weight:800}
.cl-explain{margin-top:10px;padding:10px 14px;background:linear-gradient(135deg,var(--pri-soft),var(--acc-soft));border-radius:9px;font-size:.92rem;line-height:1.5;min-height:60px}
.cl-final-badge{display:inline-block;padding:8px 16px;margin:14px auto;background:linear-gradient(135deg,var(--ok),#34d399);color:#fff;border-radius:99px;font-weight:800;font-size:1.1rem;box-shadow:0 6px 18px rgba(16,185,129,.3);animation:simpPop .5s ease}
/* Widget 2: Square comparison SVG */
.sq-comp-conclusion{margin-top:10px;text-align:center;font-size:1rem;min-height:36px;padding:8px 12px;border-radius:8px;background:var(--card);border:1px solid var(--border)}
/* Widget 3: Euler-Venn diagrams */
.ev-result{margin-top:10px;text-align:center;font-size:.95rem;padding:8px 12px;background:var(--card);border-radius:9px;border:1px solid var(--border);min-height:36px}
/* Widget 4: Multi-row system solver */
.sys-row-card{padding:10px;background:var(--card);border-radius:9px;border:1.5px solid var(--border);margin-bottom:8px}
</style>
</head>
@@ -1676,6 +1696,19 @@ function buildP1(){
</details>
`)}
${widget('Извлечение в столбик — пошаговая анимация', 'STEPS', 'Введите положительное число (точный квадрат до 10000). Нажмите «Извлечь по шагам» — увидите классический алгоритм.', `
<div class="row" style="justify-content:center;gap:10px;flex-wrap:wrap">
<span class="lab">Число под корнем:</span>
<input id="cl-n" class="inp num" type="number" value="5184" min="1" max="999999" step="1" style="width:100px;font-size:1.1rem">
<button class="btn primary" onclick="clStart()">Извлечь по шагам</button>
<button class="btn" onclick="clPreset(1296)">1296</button>
<button class="btn" onclick="clPreset(2916)">2916</button>
<button class="btn" onclick="clPreset(7744)">7744</button>
</div>
<div id="cl-workspace" class="cl-workspace"></div>
<div id="cl-explain" class="cl-explain"></div>
`)}
${widget('Игра «Таблица квадратов 1099»', 'GAME', 'Показано число — выберите его квадратный корень. Точность важнее скорости, но скорость даёт бонус. Лучший результат сохраняется!', `
<div class="score-display">
<div>Раунд: <b id="sq-round">1</b>/10</div>
@@ -3235,6 +3268,20 @@ function buildP4(){
<div id="comp-fb" class="feedback"></div>
`)}
${widget('Сравнение через возведение в квадрат', 'VISUAL', 'Чтобы сравнить два выражения с корнями — возведи их в квадрат. У большего выражения квадрат больше.', `
<div class="row" style="justify-content:center;gap:18px;font-family:'JetBrains Mono',monospace;font-size:1.3rem;margin-bottom:14px">
<span id="sq-a-expr" style="color:var(--acc2);font-weight:700">3√2</span>
<span style="color:var(--muted)">vs</span>
<span id="sq-b-expr" style="color:var(--pri2);font-weight:700">2√3</span>
</div>
<svg id="sq-svg" viewBox="0 0 600 220" style="width:100%;max-width:560px;display:block;margin:0 auto"></svg>
<div class="row-c" style="margin-top:12px">
<button class="btn primary" onclick="sqAnimate()">Возвести в квадрат и сравнить</button>
<button class="btn" onclick="sqNext()">Другая пара</button>
</div>
<div id="sq-conclusion" class="sq-comp-conclusion"></div>
`)}
${widget('Тренажёр «Упрости»', 'GAME', 'Введите упрощённое выражение в виде a√b. Например, для √72 ответ: 6√2.', `
<div id="simp4-card" style="padding:14px;background:var(--card);border-radius:9px;text-align:center">
<div class="lab">Упростите:</div>
@@ -3610,6 +3657,28 @@ function buildP5(){
</div>
`)}
${widget('Эйлеровы диаграммы для ∪ и ∩', 'VISUAL', 'Альтернативная визуализация: каждый промежуток — капсула (по точкам). Пересечение — общая часть. Объединение — оба вместе.', `
<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:14px">
<div>
<div class="lab">A = [<span id="ev-a-lo">2</span>; <span id="ev-a-hi">6</span>]</div>
<input id="ev-a-lo-s" type="range" class="slider" min="-5" max="10" step="0.5" value="2">
<input id="ev-a-hi-s" type="range" class="slider" min="-5" max="10" step="0.5" value="6" style="margin-top:4px">
</div>
<div>
<div class="lab">B = [<span id="ev-b-lo">4</span>; <span id="ev-b-hi">9</span>]</div>
<input id="ev-b-lo-s" type="range" class="slider" min="-5" max="10" step="0.5" value="4">
<input id="ev-b-hi-s" type="range" class="slider" min="-5" max="10" step="0.5" value="9" style="margin-top:4px">
</div>
</div>
<svg id="ev-svg" viewBox="0 0 520 280" style="width:100%;max-width:560px;display:block;margin:0 auto"></svg>
<div class="row-c" style="margin-top:12px">
<button class="btn" id="ev-show-union" onclick="evMode('union')">Показать A B</button>
<button class="btn" id="ev-show-inter" onclick="evMode('inter')">Показать A ∩ B</button>
<button class="btn" onclick="evMode('both')">Оба</button>
</div>
<div id="ev-result" class="ev-result"></div>
`)}
${makeCard('example','Примеры',null,`
<ul>
<li>$(-\\infty; 3) \\cup [3; 7] = (-\\infty; 7]$</li>
@@ -3983,34 +4052,10 @@ function buildP6(){
<p><b>Пример 2.</b> Двойное неравенство $-3 < 2x + 1 \\leq 7$. Решим как систему: $-3 < 2x + 1$ и $2x + 1 \\leq 7$. Получаем $x > -2$ и $x \\leq 3$, то есть $x \\in (-2; 3]$.</p>
`)}
${widget('Решатель системы линейных неравенств', 'CALC', 'Введите две формы $ax+b$ ≷ $c$ для двух неравенств. Получите пересечение.', `
<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:14px">
<div style="padding:12px;background:var(--card);border:1.5px solid var(--pri);border-radius:9px">
<div class="lab">Неравенство 1</div>
<div class="row" style="margin-top:6px">
<input id="s1-a" class="inp num" type="number" value="3" style="width:50px">
<span class="lab">x +</span>
<input id="s1-b" class="inp num" type="number" value="6" style="width:50px">
<select id="s1-r" class="inp" style="width:auto">
<option></option><option>></option><option></option><option>&lt;</option>
</select>
<input id="s1-c" class="inp num" type="number" value="0" style="width:50px">
</div>
<div id="s1-out" class="chip" style="margin-top:10px">x ≥ -2</div>
</div>
<div style="padding:12px;background:var(--card);border:1.5px solid var(--acc);border-radius:9px">
<div class="lab">Неравенство 2</div>
<div class="row" style="margin-top:6px">
<input id="s2-a" class="inp num" type="number" value="-2" style="width:50px">
<span class="lab">x +</span>
<input id="s2-b" class="inp num" type="number" value="5" style="width:50px">
<select id="s2-r" class="inp" style="width:auto">
<option>></option><option></option><option></option><option>&lt;</option>
</select>
<input id="s2-c" class="inp num" type="number" value="1" style="width:50px">
</div>
<div id="s2-out" class="chip acc" style="margin-top:10px">x &lt; 2</div>
</div>
${widget('Решатель системы линейных неравенств', 'CALC', 'Введите до 5 неравенств $ax+b$ ≷ $c$. Используйте кнопки «+ Добавить» / «×» для управления строками.', `
<div id="sys-list"></div>
<div class="row-c" style="margin-bottom:14px">
<button class="btn ok" onclick="sysAddRow(1,0,'≥',0)">+ Добавить неравенство</button>
</div>
<div id="sys-line" style="position:relative;height:170px;background:var(--card);border:1px solid var(--border);border-radius:9px"></div>
<div class="row-c" style="margin-top:12px">
@@ -4095,14 +4140,69 @@ function buildP6(){
setTimeout(()=>{ initSysSolver(); initDoubleIneq(); initFindInt(); }, 50);
}
/* ──── Sys Solver ──── */
/* ──── Sys Solver (multi-row) ──── */
let sysCurMode = 'sys';
let _sysRows = [];
let _sysDefaults = [{a:3,b:6,op:'≥',c:0},{a:-2,b:5,op:'>',c:1}];
function initSysSolver(){
['s1-a','s1-b','s1-c','s1-r','s2-a','s2-b','s2-c','s2-r'].forEach(id=>{
const e = document.getElementById(id);
if(e) e.addEventListener('input', sysUpdate);
if(e && e.tagName === 'SELECT') e.addEventListener('change', sysUpdate);
_sysRows = [];
_sysDefaults.forEach((d,i) => { _sysRows.push({id:'sr'+(i+1), a:d.a, b:d.b, op:d.op, c:d.c}); });
_sysRebuildHTML();
sysUpdate();
}
function sysAddRow(a, b, op, c){
if(_sysRows.length >= 5) return;
const id = 'sr' + (_sysRows.length + 1);
_sysRows.push({id, a:a||1, b:b||0, op:op||'≥', c:c||0});
_sysRebuildHTML();
sysUpdate();
}
function _sysRebuildHTML(){
const list = document.getElementById('sys-list');
if(!list) return;
const COLORS = ['var(--pri)','var(--acc)','var(--ok)','var(--warn)','var(--sec-acc,#9333ea)'];
list.innerHTML = _sysRows.map((r,i)=>`
<div class="sys-row-card" style="border-color:${COLORS[i%COLORS.length]}">
<div class="lab" style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
<span>Неравенство ${i+1}</span>
${i > 0 ? `<button class="btn small" onclick="sysRemoveRow(${i})">&#215;</button>` : ''}
</div>
<div class="row" style="margin-top:2px">
<input id="${r.id}-a" class="inp num" type="number" value="${r.a}" style="width:50px">
<span class="lab">x +</span>
<input id="${r.id}-b" class="inp num" type="number" value="${r.b}" style="width:50px">
<select id="${r.id}-r" class="inp" style="width:auto">
<option${r.op==='>' ? ' selected' : ''}>&gt;</option>
<option${r.op==='≥' ? ' selected' : ''}></option>
<option${r.op==='≤' ? ' selected' : ''}></option>
<option${r.op==='<' ? ' selected' : ''}>&lt;</option>
</select>
<input id="${r.id}-c" class="inp num" type="number" value="${r.c}" style="width:50px">
</div>
<div id="${r.id}-out" class="chip" style="margin-top:8px;border-color:${COLORS[i%COLORS.length]};color:${COLORS[i%COLORS.length]}">x ?</div>
</div>
`).join('');
_sysRebindHandlers();
}
function _sysRebindHandlers(){
_sysRows.forEach(r => {
[r.id+'-a', r.id+'-b', r.id+'-c', r.id+'-r'].forEach(id => {
const e = document.getElementById(id);
if(e){ e.addEventListener('input', sysUpdate); e.addEventListener('change', sysUpdate); }
});
});
}
function sysRemoveRow(i){
if(_sysRows.length <= 1) return;
_sysRows.splice(i, 1);
// re-assign ids
_sysRows = _sysRows.map((r,idx) => ({...r, id:'sr'+(idx+1)}));
_sysRebuildHTML();
sysUpdate();
}
function solveLin(a, b, op, c){
@@ -4121,81 +4221,97 @@ function solveLin(a, b, op, c){
return { bound, strict, dir };
}
function sysUpdate(){
const s1 = solveLin(+document.getElementById('s1-a').value, +document.getElementById('s1-b').value, document.getElementById('s1-r').value, +document.getElementById('s1-c').value);
const s2 = solveLin(+document.getElementById('s2-a').value, +document.getElementById('s2-b').value, document.getElementById('s2-r').value, +document.getElementById('s2-c').value);
function describe(s){
const rel = s.dir + (s.strict ? '' : '=');
return 'x ' + (s.dir === '>' ? (s.strict ? '>' : '≥') : (s.strict ? '<' : '≤')) + ' ' + s.bound;
if(!_sysRows.length) return;
function getRowVal(r){
const aEl = document.getElementById(r.id+'-a');
const bEl = document.getElementById(r.id+'-b');
const cEl = document.getElementById(r.id+'-c');
const rEl = document.getElementById(r.id+'-r');
if(!aEl) return null;
const op = rEl ? rEl.value : '≥';
// handle &lt; being stored as <
const opClean = op === '&lt;' ? '<' : op;
return solveLin(+aEl.value||0, +bEl.value||0, opClean, +cEl.value||0);
}
document.getElementById('s1-out').innerHTML = describe(s1);
document.getElementById('s2-out').innerHTML = describe(s2);
// intersect or union
function toInterval(s){
function describe(s){
return 'x ' + (s.dir === '>' ? (s.strict ? '>' : '≥') : (s.strict ? '<' : '≤')) + ' ' + (Number.isFinite(s.bound) ? +s.bound.toFixed(4) : s.bound);
}
function toIntervalSys(s){
if(s.dir === '>') return { l:s.bound, r:Infinity, lOp:s.strict, rOp:true };
else return { l:-Infinity, r:s.bound, lOp:true, rOp:s.strict };
}
const A = toInterval(s1), B = toInterval(s2);
// intersection of two intervals
let inter = null;
const lo = Math.max(A.l, B.l);
const hi = Math.min(A.r, B.r);
const loOp = (A.l > B.l) ? A.lOp : (A.l < B.l) ? B.lOp : (A.lOp || B.lOp);
const hiOp = (A.r < B.r) ? A.rOp : (A.r > B.r) ? B.rOp : (A.rOp || B.rOp);
if(lo < hi || (lo === hi && !loOp && !hiOp)) inter = {l:lo, r:hi, lOp:loOp, rOp:hiOp};
function intersectTwo(A, B){
const lo = Math.max(A.l, B.l);
const hi = Math.min(A.r, B.r);
const loOp = (A.l > B.l) ? A.lOp : (A.l < B.l) ? B.lOp : (A.lOp || B.lOp);
const hiOp = (A.r < B.r) ? A.rOp : (A.r > B.r) ? B.rOp : (A.rOp || B.rOp);
if(lo < hi || (lo === hi && !loOp && !hiOp)) return {l:lo, r:hi, lOp:loOp, rOp:hiOp};
return null;
}
const COLORS = ['#e91e63','#03a9f4','#10b981','#f59e0b','#9333ea'];
const intervals = [];
_sysRows.forEach((r) => {
const s = getRowVal(r);
if(!s || !Number.isFinite(s.bound)) return;
const outEl = document.getElementById(r.id+'-out');
if(outEl) outEl.textContent = describe(s);
intervals.push(toIntervalSys(s));
});
let inter = intervals.length ? intervals[0] : null;
for(let i = 1; i < intervals.length; i++) inter = inter ? intersectTwo(inter, intervals[i]) : null;
// visualize
const vis = document.getElementById('sys-line');
if(!vis) return;
vis.innerHTML='';
const VLO = -8, VHI = 12;
vis.appendChild(el('div',{style:'position:absolute;top:140px;left:3%;right:3%;height:2px;background:var(--text)'}));
const axisY = 20 + _sysRows.length * 25 + 20;
vis.style.height = (axisY + 36) + 'px';
vis.appendChild(el('div',{style:`position:absolute;top:${axisY}px;left:3%;right:3%;height:2px;background:var(--text)`}));
for(let i = VLO; i <= VHI; i++){
const x = 3 + (i-VLO)/(VHI-VLO)*94;
vis.appendChild(el('div',{style:`position:absolute;top:152px;left:${x}%;transform:translateX(-50%);font-size:.72rem;color:var(--muted);font-family:'JetBrains Mono',monospace`}, ''+i));
vis.appendChild(el('div',{style:`position:absolute;top:${axisY+12}px;left:${x}%;transform:translateX(-50%);font-size:.72rem;color:var(--muted);font-family:'JetBrains Mono',monospace`}, ''+i));
}
function drawInt(y, iv, col, lbl){
if(!iv) return;
const l = iv.l === -Infinity ? VLO : iv.l;
const r = iv.r === Infinity ? VHI : iv.r;
if(l > VHI || r < VLO) return;
const x1 = 3 + Math.max(VLO, l - VLO) / (VHI-VLO) * 94 - (VLO < 0 ? VLO/(VHI-VLO)*94 : 0);
const xL = 3 + (Math.max(VLO,l) - VLO)/(VHI-VLO)*94;
const xR = 3 + (Math.min(VHI,r) - VLO)/(VHI-VLO)*94;
vis.appendChild(el('div',{style:`position:absolute;top:${y}px;left:${xL}%;width:${xR-xL}%;height:10px;background:${col};border-radius:5px`}));
vis.appendChild(el('div',{style:`position:absolute;top:${y}px;left:${xL}%;width:${Math.max(0,xR-xL)}%;height:10px;background:${col};border-radius:5px`}));
if(iv.l !== -Infinity) vis.appendChild(el('div',{style:`position:absolute;top:${y-2}px;left:${xL}%;width:14px;height:14px;border-radius:50%;background:${iv.lOp?'var(--card)':col};border:2.5px solid ${col};transform:translateX(-50%)`}));
if(iv.r !== Infinity) vis.appendChild(el('div',{style:`position:absolute;top:${y-2}px;left:${xR}%;width:14px;height:14px;border-radius:50%;background:${iv.rOp?'var(--card)':col};border:2.5px solid ${col};transform:translateX(-50%)`}));
vis.appendChild(el('div',{style:`position:absolute;top:${y-2}px;left:3px;font-size:.74rem;font-weight:700;color:${col}`}, lbl));
}
drawInt(20, A, '#e91e63', '1)');
drawInt(50, B, '#03a9f4', '2)');
let ans, lbl, col;
if(sysCurMode === 'sys'){
ans = inter; lbl = '∩ Система:'; col = '#10b981';
} else {
// union
if(A.r < B.l || B.r < A.l){ ans = null; lbl = '∪ Совокупность (2 части):'; col = '#10b981'; }
else { ans = {l:Math.min(A.l,B.l), r:Math.max(A.r,B.r), lOp:Math.min(A.l,B.l)===A.l?A.lOp:B.lOp, rOp:Math.max(A.r,B.r)===A.r?A.rOp:B.rOp}; lbl = ' Совокупность:'; col = '#10b981'; }
}
if(sysCurMode === 'un' && !ans){
drawInt(80, A, col, '');
drawInt(110, B, col, '');
} else {
drawInt(95, ans, col, lbl);
}
// answer
intervals.forEach((iv, i) => drawInt(10 + i*25, iv, COLORS[i%COLORS.length], (i+1)+')'));
function fmt(iv){
if(!iv) return '∅';
const lA = iv.lOp ? '(' : '[';
const rA = iv.rOp ? ')' : ']';
const l = iv.l === -Infinity ? '-∞' : iv.l;
const r = iv.r === Infinity ? '+∞' : iv.r;
const l = iv.l === -Infinity ? '-∞' : +iv.l.toFixed(4);
const r = iv.r === Infinity ? '+∞' : +iv.r.toFixed(4);
return lA + l + '; ' + r + (iv.r === Infinity ? ')' : rA);
}
let ansText;
if(sysCurMode === 'sys'){
ansText = inter ? fmt(inter) : '∅';
} else if(ans){
ansText = fmt(ans);
if(inter) drawInt(axisY - 18, inter, '#10b981', '∩');
} else {
ansText = fmt(A) + ' ' + fmt(B);
// union: just show all individually in green
intervals.forEach(iv => drawInt(axisY - 18, iv, '#10b981', ''));
if(intervals.length === 2){
const A2 = intervals[0], B2 = intervals[1];
if(A2.r >= B2.l && B2.r >= A2.l){
const merged = {l:Math.min(A2.l,B2.l), r:Math.max(A2.r,B2.r),
lOp:Math.min(A2.l,B2.l)===A2.l?A2.lOp:B2.lOp,
rOp:Math.max(A2.r,B2.r)===A2.r?A2.rOp:B2.rOp};
ansText = fmt(merged);
} else {
ansText = fmt(A2) + ' ' + fmt(B2);
}
} else {
ansText = intervals.map(fmt).join(' ');
}
}
document.getElementById('sys-answer').textContent = ansText;
}
@@ -5756,5 +5872,432 @@ function initWave4(){
document.addEventListener('DOMContentLoaded', ()=>setTimeout(initWave4, 200));
</script>
<script>
'use strict';
/* ════════════════════════════════════════════════════════
WAVE DEPTH — 4 new interactive widgets
════════════════════════════════════════════════════════ */
/* ══════════════════════════════════════════════
WIDGET 1 — Column root extraction (§1)
══════════════════════════════════════════════ */
let _clRunning = false;
function clPreset(n){
const inp = document.getElementById('cl-n');
if(inp){ inp.value = n; }
}
async function clStart(){
if(_clRunning) return;
const inp = document.getElementById('cl-n');
const ws = document.getElementById('cl-workspace');
const ex = document.getElementById('cl-explain');
if(!inp || !ws || !ex) return;
const N = Math.abs(Math.round(+inp.value)) || 5184;
if(N < 1 || N > 999999){ ex.textContent = 'Введите число от 1 до 999999.'; return; }
_clRunning = true;
ws.innerHTML = '';
ex.textContent = 'Начинаем...';
// Step 1: Split into pairs from right
const digits = '' + N;
const pairs = [];
for(let i = digits.length; i > 0; i -= 2){
pairs.unshift(digits.slice(Math.max(0, i-2), i));
}
const pairsStr = pairs.join(' | ');
// Step 2: compute actual integer sqrt for display
const sqrtN = Math.sqrt(N);
const isExact = Math.abs(sqrtN - Math.round(sqrtN)) < 1e-9;
const answerInt = isExact ? Math.round(sqrtN) : Math.floor(sqrtN);
// Build steps
let remainder = 0;
let result = '';
let lines = [];
lines.push(` Число: ${N}`);
lines.push(` Грани: ${pairsStr}`);
lines.push(` ${'─'.repeat(30)}`);
ws.innerHTML = '<span>' + lines.join('\n') + '</span>';
ex.innerHTML = '<b>Шаг 1.</b> Разбиваем число на грани по 2 цифры справа налево: <b>' + pairsStr + '</b>';
await sleep(1000);
for(let pi = 0; pi < pairs.length; pi++){
const pairVal = parseInt(pairs[pi], 10);
// Bring down the pair
const current = remainder * 100 + pairVal;
// Double current answer for divisor base
const doubleResult = (result === '') ? 0 : parseInt(result, 10) * 2;
// Find next digit d: (doubleResult*10 + d) * d <= current
let d = 0;
for(let t = 9; t >= 0; t--){
if((doubleResult * 10 + t) * t <= current){ d = t; break; }
}
const subtract = (doubleResult * 10 + d) * d;
const newRemainder = current - subtract;
result += '' + d;
// Add to display
if(pi === 0){
lines.push(` <span class="cl-active">${pairVal}</span> | √${N}`);
lines.push(` <span class="cl-active">-${subtract}</span> | <span class="cl-result">${d}</span>`);
lines.push(` ${'─'.repeat(18)}`);
lines.push(` ${newRemainder}`);
} else {
lines.push(` <span class="cl-active">${current}</span>`);
lines.push(` <span class="cl-active">-${subtract}</span> | <span class="cl-result">${result}</span> ← (${doubleResult}·${d} = ${doubleResult*d}; добавили ${d}: ${doubleResult*10+d}×${d}=${subtract})`);
lines.push(` ${'─'.repeat(18)}`);
lines.push(` ${newRemainder}`);
}
ws.innerHTML = '<span>' + lines.join('\n') + '</span>';
if(pi === 0){
ex.innerHTML = `<b>Шаг ${pi*3+2}.</b> Берём первую грань <b>${pairVal}</b>. Ищем наибольшее <b>d</b> такое что <b>d² ≤ ${pairVal}</b>: d = <b>${d}</b> (${d}² = ${d*d}). Записываем в ответ. Вычитаем: ${pairVal} ${subtract} = <b>${newRemainder}</b>.`;
} else {
ex.innerHTML = `<b>Шаг ${pi*3+2}.</b> Сносим грань <b>${pairs[pi]}</b> → получаем <b>${current}</b>. Удваиваем ответ: ${parseInt(result.slice(0,-1)||'0',10)*2} → заготовка делителя <b>${doubleResult}</b>. Подбираем d: (${doubleResult*10}+d)×d ≤ ${current}. Подходит d = <b>${d}</b>. Вычитаем: ${current} ${subtract} = <b>${newRemainder}</b>.`;
}
remainder = newRemainder;
await sleep(1100);
}
// Final
if(remainder === 0){
lines.push('');
lines.push(` <span class="cl-result">√${N} = ${result}</span> (остаток 0 — точный квадрат)`);
ws.innerHTML = '<span>' + lines.join('\n') + '</span>';
ex.innerHTML = `<b>Готово!</b> Остаток равен 0 — корень извлечён точно. <b>√${N} = ${result}</b>.`;
await sleep(600);
// Badge
ex.innerHTML += ` <span class="cl-final-badge">√${N} = ${result}</span>`;
confetti();
addXp(15, 'col-root');
bumpProgress('p1', 8);
achievement('col-root', 'Извлёк корень в столбик');
} else {
lines.push('');
lines.push(` Остаток: <span class="cl-active">${remainder}</span> (не 0 — ${N} не точный квадрат)`);
lines.push(` <span class="cl-result">⌊√${N}⌋ ≈ ${answerInt}</span>`);
ws.innerHTML = '<span>' + lines.join('\n') + '</span>';
ex.innerHTML = `<b>Внимание:</b> остаток ${remainder} ≠ 0. Число <b>${N}</b> — не точный квадрат. Целая часть корня: <b>⌊√${N}⌋ = ${answerInt}</b>. Точный ответ иррационален.`;
addXp(8, 'col-root-approx');
bumpProgress('p1', 4);
}
_clRunning = false;
}
/* ══════════════════════════════════════════════
WIDGET 2 — Square comparison SVG (§4)
══════════════════════════════════════════════ */
const SQ_PAIRS = [
{ aExpr:'3√2', aSq:18, bExpr:'2√3', bSq:12 },
{ aExpr:'4√3', aSq:48, bExpr:'3√5', bSq:45 },
{ aExpr:'5√2', aSq:50, bExpr:'7', bSq:49 },
{ aExpr:'2√7', aSq:28, bExpr:'3√3', bSq:27 },
{ aExpr:'√17', aSq:17, bExpr:'4', bSq:16 },
];
let sqPairIdx = 0;
let _sqAnimating = false;
function sqRender(){
const p = SQ_PAIRS[sqPairIdx];
const ae = document.getElementById('sq-a-expr');
const be = document.getElementById('sq-b-expr');
if(ae) ae.textContent = p.aExpr;
if(be) be.textContent = p.bExpr;
const svg = document.getElementById('sq-svg');
if(svg){
svg.innerHTML = '<text x="300" y="110" text-anchor="middle" font-size="18" fill="currentColor" opacity="0.5">Нажмите «Возвести в квадрат и сравнить»</text>';
}
const con = document.getElementById('sq-conclusion');
if(con) con.innerHTML = '';
}
async function sqAnimate(){
if(_sqAnimating) return;
_sqAnimating = true;
const p = SQ_PAIRS[sqPairIdx];
const svg = document.getElementById('sq-svg');
const con = document.getElementById('sq-conclusion');
if(!svg) { _sqAnimating = false; return; }
const W = 600, H = 220;
const maxSq = Math.max(p.aSq, p.bSq);
const maxSide = 100;
const sideA = maxSide * Math.sqrt(p.aSq / maxSq);
const sideB = maxSide * Math.sqrt(p.bSq / maxSq);
const cy = H / 2;
const cxA = 140, cxB = 460;
svg.innerHTML = '';
// Draw grid helper function as tiny squares
function makeRect(cx, cy, side, col, label, sqVal, expr){
const x = cx - side/2, y = cy - side/2;
const g = document.createElementNS('http://www.w3.org/2000/svg','g');
g.style.transformOrigin = `${cx}px ${cy}px`;
g.style.transform = 'scale(0)';
g.style.transition = 'transform 0.7s cubic-bezier(0.34,1.56,0.64,1)';
const rect = document.createElementNS('http://www.w3.org/2000/svg','rect');
rect.setAttribute('x', x); rect.setAttribute('y', y);
rect.setAttribute('width', side); rect.setAttribute('height', side);
rect.setAttribute('fill', col + '33');
rect.setAttribute('stroke', col);
rect.setAttribute('stroke-width', '2.5');
rect.setAttribute('rx', '4');
g.appendChild(rect);
// Side label — top
const tSide = document.createElementNS('http://www.w3.org/2000/svg','text');
tSide.setAttribute('x', cx); tSide.setAttribute('y', y - 8);
tSide.setAttribute('text-anchor','middle');
tSide.setAttribute('font-size','14');
tSide.setAttribute('font-weight','700');
tSide.setAttribute('fill', col);
tSide.textContent = expr;
g.appendChild(tSide);
// Area label — center
const tArea = document.createElementNS('http://www.w3.org/2000/svg','text');
tArea.setAttribute('x', cx); tArea.setAttribute('y', cy + 5);
tArea.setAttribute('text-anchor','middle');
tArea.setAttribute('font-size','20');
tArea.setAttribute('font-weight','900');
tArea.setAttribute('fill', col);
tArea.textContent = '(' + expr + ')² = ' + sqVal;
g.appendChild(tArea);
// Expression bottom
const tExpr = document.createElementNS('http://www.w3.org/2000/svg','text');
tExpr.setAttribute('x', cx); tExpr.setAttribute('y', y + side + 20);
tExpr.setAttribute('text-anchor','middle');
tExpr.setAttribute('font-size','14');
tExpr.setAttribute('fill','currentColor');
tExpr.setAttribute('opacity','0.7');
tExpr.textContent = label;
g.appendChild(tExpr);
return g;
}
const colA = '#0288d1', colB = '#c2185b';
const gA = makeRect(cxA, cy, sideA, colA, 'A = ' + p.aExpr, p.aSq, p.aExpr);
const gB = makeRect(cxB, cy, sideB, colB, 'B = ' + p.bExpr, p.bSq, p.bExpr);
// VS text
const vsText = document.createElementNS('http://www.w3.org/2000/svg','text');
vsText.setAttribute('x', W/2); vsText.setAttribute('y', cy + 6);
vsText.setAttribute('text-anchor','middle');
vsText.setAttribute('font-size','22');
vsText.setAttribute('font-weight','700');
vsText.setAttribute('fill','#888');
vsText.textContent = 'vs';
svg.appendChild(vsText);
svg.appendChild(gA);
svg.appendChild(gB);
await sleep(50);
gA.style.transform = 'scale(1)';
await sleep(200);
gB.style.transform = 'scale(1)';
await sleep(800);
// Winner arrow/badge
const bigger = p.aSq > p.bSq ? 'A' : (p.bSq > p.aSq ? 'B' : 'equal');
const winX = bigger === 'A' ? cxA : bigger === 'B' ? cxB : W/2;
const winCol = bigger === 'A' ? colA : bigger === 'B' ? colB : '#10b981';
const winText = p.aSq > p.bSq
? p.aExpr + ' > ' + p.bExpr
: (p.bSq > p.aSq ? p.bExpr + ' > ' + p.aExpr : p.aExpr + ' = ' + p.bExpr);
const badge = document.createElementNS('http://www.w3.org/2000/svg','g');
const br = document.createElementNS('http://www.w3.org/2000/svg','rect');
br.setAttribute('x', winX - 80); br.setAttribute('y', 10);
br.setAttribute('width', 160); br.setAttribute('height', 34);
br.setAttribute('rx', 17); br.setAttribute('fill', winCol);
badge.appendChild(br);
const bt = document.createElementNS('http://www.w3.org/2000/svg','text');
bt.setAttribute('x', winX); bt.setAttribute('y', 33);
bt.setAttribute('text-anchor','middle');
bt.setAttribute('font-size','15');
bt.setAttribute('font-weight','800');
bt.setAttribute('fill','#fff');
bt.textContent = winText;
badge.appendChild(bt);
badge.style.opacity = '0';
badge.style.transition = 'opacity 0.4s ease';
svg.appendChild(badge);
await sleep(50);
badge.style.opacity = '1';
if(con){
const sign = p.aSq > p.bSq ? '>' : (p.bSq > p.aSq ? '<' : '=');
const conclusion = `${p.aExpr} ${sign} ${p.bExpr}, потому что (${p.aExpr})² = ${p.aSq} ${sign} ${p.bSq} = (${p.bExpr})²`;
con.innerHTML = '<b style="color:var(--ok)">' + conclusion + '</b>';
bumpProgress('p4', 4);
addXp(8, 'sq-compare');
achievement('sq-compare', 'Сравнил через квадрат');
confetti();
}
_sqAnimating = false;
}
function sqNext(){
sqPairIdx = (sqPairIdx + 1) % SQ_PAIRS.length;
_sqAnimating = false;
sqRender();
}
function initSqCompare(){
sqPairIdx = 0;
sqRender();
}
/* ══════════════════════════════════════════════
WIDGET 3 — EulerVenn diagrams (§5)
══════════════════════════════════════════════ */
let _evMode = 'both';
function _evGetVals(){
const aLo = +document.getElementById('ev-a-lo-s').value;
const aHi = +document.getElementById('ev-a-hi-s').value;
const bLo = +document.getElementById('ev-b-lo-s').value;
const bHi = +document.getElementById('ev-b-hi-s').value;
return {
aLo: Math.min(aLo, aHi),
aHi: Math.max(aLo, aHi),
bLo: Math.min(bLo, bHi),
bHi: Math.max(bLo, bHi),
};
}
function evMode(m){
_evMode = m;
_evDraw();
}
function _evDraw(){
const svg = document.getElementById('ev-svg');
const res = document.getElementById('ev-result');
if(!svg) return;
const {aLo, aHi, bLo, bHi} = _evGetVals();
// Update labels
const aLoLbl = document.getElementById('ev-a-lo'); if(aLoLbl) aLoLbl.textContent = aLo;
const aHiLbl = document.getElementById('ev-a-hi'); if(aHiLbl) aHiLbl.textContent = aHi;
const bLoLbl = document.getElementById('ev-b-lo'); if(bLoLbl) bLoLbl.textContent = bLo;
const bHiLbl = document.getElementById('ev-b-hi'); if(bHiLbl) bHiLbl.textContent = bHi;
const W = 520, H = 280;
// Map values to x coordinates in SVG
const VMIN = -5, VMAX = 10;
function toX(v){ return 50 + (v - VMIN) / (VMAX - VMIN) * (W - 100); }
const xA1 = toX(aLo), xA2 = toX(aHi);
const xB1 = toX(bLo), xB2 = toX(bHi);
const ry = 40; // ellipse y-radius
const cyA = 100, cyB = 180;
const rxA = Math.max(10, (xA2 - xA1) / 2);
const rxB = Math.max(10, (xB2 - xB1) / 2);
const cxA = (xA1 + xA2) / 2;
const cxB = (xB1 + xB2) / 2;
// Intersection
const interLo = Math.max(aLo, bLo);
const interHi = Math.min(aHi, bHi);
const hasInter = interLo <= interHi;
const interX1 = toX(interLo), interX2 = toX(interHi);
const interRx = Math.max(0, (interX2 - interX1) / 2);
const interCx = (interX1 + interX2) / 2;
// Union
const unionLo = Math.min(aLo, bLo);
const unionHi = Math.max(aHi, bHi);
let html = `<defs>
<clipPath id="clip-a"><ellipse cx="${cxA}" cy="${cyA}" rx="${rxA}" ry="${ry}"/></clipPath>
<clipPath id="clip-b"><ellipse cx="${cxB}" cy="${cyB}" rx="${rxB}" ry="${ry}"/></clipPath>
</defs>`;
// Base ellipses
const opA = _evMode === 'inter' ? '0.35' : '0.7';
const opB = _evMode === 'inter' ? '0.35' : '0.7';
html += `<ellipse cx="${cxA}" cy="${cyA}" rx="${rxA}" ry="${ry}" fill="rgba(3,169,244,0.18)" stroke="#0288d1" stroke-width="2.5" opacity="${opA}"/>`;
html += `<ellipse cx="${cxB}" cy="${cyB}" rx="${rxB}" ry="${ry}" fill="rgba(233,30,99,0.18)" stroke="#c2185b" stroke-width="2.5" opacity="${opB}"/>`;
// Labels
html += `<text x="${cxA}" y="${cyA+5}" text-anchor="middle" font-size="16" font-weight="800" fill="#0288d1">A</text>`;
html += `<text x="${cxB}" y="${cyB+5}" text-anchor="middle" font-size="16" font-weight="800" fill="#c2185b">B</text>`;
// Range labels below each ellipse
html += `<text x="${cxA}" y="${cyA + ry + 14}" text-anchor="middle" font-size="11" fill="currentColor" opacity="0.7">[${aLo}; ${aHi}]</text>`;
html += `<text x="${cxB}" y="${cyB + ry + 14}" text-anchor="middle" font-size="11" fill="currentColor" opacity="0.7">[${bLo}; ${bHi}]</text>`;
// Mode-specific highlights
if(_evMode === 'inter' || _evMode === 'both'){
if(hasInter && interRx > 0){
// Check if ellipses actually overlap in y (they're on different y levels but show as overlapping if x-ranges cross)
// We just highlight intersection region as a vertical band between both ellipses
html += `<rect x="${interX1}" y="${cyA - ry}" width="${Math.max(0,interX2-interX1)}" height="${cyB + ry - (cyA - ry)}" fill="rgba(16,185,129,0.28)" stroke="#10b981" stroke-width="2" stroke-dasharray="5,3" rx="4"/>`;
html += `<text x="${interCx}" y="${(cyA+cyB)/2+5}" text-anchor="middle" font-size="13" font-weight="700" fill="#10b981"></text>`;
}
}
if(_evMode === 'union' || _evMode === 'both'){
// Draw union outline — thick gold border around combined span
const ux1 = toX(unionLo), ux2 = toX(unionHi);
html += `<rect x="${ux1 - 4}" y="${cyA - ry - 4}" width="${ux2 - ux1 + 8}" height="${cyB + ry + 8 - (cyA - ry - 4)}" fill="none" stroke="#f59e0b" stroke-width="3" rx="8" opacity="0.85"/>`;
html += `<text x="${(ux1+ux2)/2}" y="${cyA - ry - 10}" text-anchor="middle" font-size="12" font-weight="700" fill="#f59e0b"></text>`;
}
svg.innerHTML = html;
// Result text
if(res){
const interStr = hasInter ? `[${interLo}; ${interHi}]` : '∅';
const unionStr = `[${unionLo}; ${unionHi}]`;
if(_evMode === 'inter') res.innerHTML = `$A \\cap B = ${interStr}$`;
else if(_evMode === 'union') res.innerHTML = `$A \\cup B = ${unionStr}$`;
else res.innerHTML = `$A \\cup B = ${unionStr}$, &nbsp; $A \\cap B = ${interStr}$`;
if(typeof renderMath === 'function') renderMath(res);
bumpProgress('p5', 2);
}
}
function initEulerVenn(){
['ev-a-lo-s','ev-a-hi-s','ev-b-lo-s','ev-b-hi-s'].forEach(id=>{
const e = document.getElementById(id);
if(e) e.addEventListener('input', _evDraw);
});
_evMode = 'both';
_evDraw();
}
/* ══════════════════════════════════════════════
PATCH buildP4 / buildP5 to init new widgets
══════════════════════════════════════════════ */
document.addEventListener('DOMContentLoaded', function(){
// Patch _goToFinish after all other DOMContentLoaded hooks have registered
// We use a late timeout so Wave3 patch (at +100ms) has already run
setTimeout(function(){
const _origFinish = window._goToFinish;
window._goToFinish = function(id){
_origFinish(id);
if(id === 'p4') setTimeout(initSqCompare, 80);
if(id === 'p5') setTimeout(initEulerVenn, 80);
};
}, 300);
});
</script>
</body>
</html>