feat(textbooks): переделать 'Связка x² ↔ √x' в наглядный конвейер

Было: два изолированных блока (квадрат и линия), связь неявная.

Стало: конвейер из трёх шагов со стрелками:
  [x] →(возвести в квадрат)→ [x² с площадью квадрата] →(извлечь корень)→ [|x|]

Ключевое улучшение: ползунок теперь от -8 до +8. При отрицательном x:
- площадь всё равно положительная (x²)
- корень даёт |x|, не x
- формула снизу подсвечивается янтарным предупреждением 'это |x| ≠ x'

Под конвейером: живая формула KaTeX типа 'x = 3 → x² = 9 → √9 = 3 ✓'. При отрицательном x текст явно показывает: 'x = -3 → x² = 9 → √9 = 3 ≠ -3 → это |x| = 3'.

Мобайл: вертикальная компоновка со стрелками вниз.
This commit is contained in:
Maxim Dolgolyov
2026-05-27 13:04:37 +03:00
parent 10ba4978cf
commit 718772a2aa
+85 -23
View File
@@ -454,6 +454,28 @@ input,select,textarea{font-family:inherit}
/* hover-preview карточек выключен — мешал перекрытием соседних рядов */
.psel-card-preview{display:none!important}
/* Конвейер x → x² → √(x²) в §1 */
.dual-pipeline{display:flex;align-items:center;justify-content:space-between;gap:6px;flex-wrap:wrap;margin-top:14px;padding:14px 8px;background:var(--card);border-radius:12px;border:1px solid var(--border)}
.dual-step{flex:1;min-width:90px;text-align:center;padding:10px 8px;border-radius:10px;background:rgba(233,30,99,0.04);border:1.5px solid var(--border)}
.dual-step.dual-input{background:rgba(233,30,99,0.12);border-color:rgba(233,30,99,0.4)}
.dual-step.dual-square{background:rgba(155,93,229,0.10);border-color:rgba(155,93,229,0.35)}
.dual-step.dual-output{background:rgba(3,169,244,0.12);border-color:rgba(3,169,244,0.4)}
.dual-step-lab{font-size:.66rem;font-weight:800;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);margin-bottom:4px}
.dual-step-val{font-size:1.8rem;font-weight:900;color:var(--text);font-family:'JetBrains Mono',monospace;line-height:1}
.dual-step-val.small{font-size:1.3rem;margin-top:4px}
.dual-step-cap{font-size:.78rem;color:var(--muted);margin-top:4px;font-family:'JetBrains Mono',monospace}
.dual-arrow{flex:0 0 auto;display:flex;flex-direction:column;align-items:center;min-width:60px}
.dual-arrow svg{width:54px;height:24px}
.dual-arrow-lab{font-size:.68rem;color:var(--pri2);font-weight:600;margin-top:2px;text-align:center;line-height:1.2}
.dual-formula{margin-top:14px;padding:10px 14px;background:linear-gradient(135deg,var(--pri-soft),var(--acc-soft));border-radius:9px;text-align:center;font-size:.95rem;line-height:1.7;border:1px solid var(--border)}
.dual-formula.mod-active{background:linear-gradient(135deg,#fef3c7,#fce7f3);border-color:var(--warn)}
@media(max-width:680px){
.dual-pipeline{flex-direction:column;gap:8px}
.dual-step{width:100%}
.dual-arrow{flex-direction:row;gap:8px}
.dual-arrow svg{transform:rotate(90deg);width:24px;height:54px}
}
/* Task 8: section fade transitions */
.sec.fade-out{animation:secFadeOut .18s ease forwards}
.sec.fade-in{animation:secFadeIn .22s ease forwards}
@@ -1697,25 +1719,40 @@ function buildP1(){
</details>
`)}
${widget('Связкаx', 'VISUAL', 'Слева — площадь квадрата (x²). Справа — длина стороны (√x). Меняйте x ползунком — оба зеркалят друг друга.', `
<div class="row">
${widget('Конвейер: x →(x²)', 'VISUAL', 'Двигайте ползунок. Слева — возведение в квадрат, справа — извлечение корня. Попробуйте отрицательное x — результат всё равно положительный!', `
<div class="row" style="margin-bottom:14px">
<span class="lab">x =</span>
<input id="dual-x" type="range" class="slider" min="0" max="12" step="0.1" value="5" style="max-width:280px">
<span id="dual-x-val" class="lab-mono" style="font-size:1.05rem">5.0</span>
<input id="dual-x" type="range" class="slider" min="-8" max="8" step="0.5" value="3" style="max-width:300px">
<span id="dual-x-val" class="lab-mono" style="font-size:1.3rem;font-weight:800;color:var(--pri2);min-width:54px;text-align:center">3.0</span>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:14px">
<div style="text-align:center">
<div class="lab" style="margin-bottom:6px">x²: квадрат со стороной x</div>
<svg viewBox="0 0 160 160" style="width:100%;max-width:160px"><rect id="dual-sq" x="20" y="20" width="120" height="120" fill="rgba(233,30,99,0.18)" stroke="#e91e63" stroke-width="2"/></svg>
<div class="chip" style="margin-top:6px">S = <b id="dual-area">25.0</b></div>
<div class="dual-pipeline">
<div class="dual-step dual-input">
<div class="dual-step-lab">Вход</div>
<div class="dual-step-val" id="dual-in">3</div>
<div class="dual-step-cap">x</div>
</div>
<div style="text-align:center">
<div class="lab" style="margin-bottom:6px">√(x²): сторона возвращается</div>
<svg viewBox="0 0 160 160" style="width:100%;max-width:160px"><line x1="20" y1="80" x2="140" y2="80" stroke="#03a9f4" stroke-width="3"/><line x1="20" y1="70" x2="20" y2="90" stroke="#03a9f4" stroke-width="3"/><line x1="140" y1="70" x2="140" y2="90" stroke="#03a9f4" stroke-width="3"/></svg>
<div class="chip acc">сторона = <b id="dual-side">5.0</b></div>
<div class="dual-arrow">
<svg viewBox="0 0 60 30"><path d="M 5 15 L 50 15" stroke="#e91e63" stroke-width="2.5" fill="none"/><polyline points="42 8 50 15 42 22" stroke="#e91e63" stroke-width="2.5" fill="none"/></svg>
<div class="dual-arrow-lab">возвести в квадрат</div>
</div>
<div class="dual-step dual-square">
<div class="dual-step-lab">Площадь</div>
<svg id="dual-sq-svg" viewBox="0 0 80 80" style="display:block;margin:0 auto"><rect id="dual-sq" x="10" y="10" width="60" height="60" fill="rgba(233,30,99,0.22)" stroke="#e91e63" stroke-width="2"/></svg>
<div class="dual-step-val small" id="dual-area">9</div>
<div class="dual-step-cap">x² = $x \\cdot x$</div>
</div>
<div class="dual-arrow">
<svg viewBox="0 0 60 30"><path d="M 5 15 L 50 15" stroke="#03a9f4" stroke-width="2.5" fill="none"/><polyline points="42 8 50 15 42 22" stroke="#03a9f4" stroke-width="2.5" fill="none"/></svg>
<div class="dual-arrow-lab" style="color:#03a9f4">извлечь корень</div>
</div>
<div class="dual-step dual-output">
<div class="dual-step-lab">Выход</div>
<div class="dual-step-val" id="dual-out">3</div>
<div class="dual-step-cap">$\\sqrt{x^2} = |x|$</div>
</div>
</div>
<p style="margin-top:10px;font-size:.88rem;color:var(--muted);text-align:center"><b>√(x²) = |x|</b> — корень и квадрат «отменяют» друг друга для неотрицательных чисел.</p>
<div id="dual-formula" class="dual-formula">$x = 3$ → $x^2 = 9$ → $\\sqrt{9} = 3$ ✓ (вернулись к исходному)</div>
<p style="margin-top:8px;font-size:.86rem;color:var(--muted);text-align:center">При <b>отрицательном x</b> результат всё равно <b>положительный</b> — это и есть смысл $\\sqrt{x^2} = |x|$</p>
`)}
${makeCard('home','Домашнее задание','1.111.15',`
@@ -2059,19 +2096,44 @@ function initDual(){
const x = document.getElementById('dual-x');
if(!x) return;
const xv = document.getElementById('dual-x-val');
const inEl = document.getElementById('dual-in');
const sq = document.getElementById('dual-sq');
const area = document.getElementById('dual-area');
const side = document.getElementById('dual-side');
const out = document.getElementById('dual-out');
const formula = document.getElementById('dual-formula');
function fmt(n){ return Number.isInteger(n) ? String(n) : n.toFixed(1); }
function upd(){
const v = +x.value;
xv.textContent = v.toFixed(1);
const sz = Math.min(120, v * 10);
sq.setAttribute('x', 80 - sz/2);
sq.setAttribute('y', 80 - sz/2);
sq.setAttribute('width', Math.max(2, sz));
sq.setAttribute('height', Math.max(2, sz));
area.textContent = (v*v).toFixed(1);
side.textContent = v.toFixed(1);
const sq2 = v * v;
const root = Math.sqrt(sq2);
xv.textContent = fmt(v);
if(inEl) inEl.textContent = fmt(v);
// Размер квадрата: |v|·8, max 60
const sz = Math.min(60, Math.abs(v) * 8 + 2);
if(sq){
sq.setAttribute('x', 40 - sz/2);
sq.setAttribute('y', 40 - sz/2);
sq.setAttribute('width', sz);
sq.setAttribute('height', sz);
}
if(area) area.textContent = fmt(sq2);
if(out) out.textContent = fmt(root);
// Подсветка: если v < 0 показать что вернулось |v|, а не v
if(formula){
if(v < 0){
formula.innerHTML = `$x = ${fmt(v)}$ → $x^2 = ${fmt(sq2)}$ → $\\sqrt{${fmt(sq2)}} = ${fmt(root)}$ ≠ ${fmt(v)} → это <b style="color:var(--warn)">|x| = ${fmt(root)}</b>`;
formula.classList.add('mod-active');
} else if(v === 0){
formula.innerHTML = `$x = 0$ → $x^2 = 0$ → $\\sqrt{0} = 0$ — особый случай`;
formula.classList.remove('mod-active');
} else {
formula.innerHTML = `$x = ${fmt(v)}$ → $x^2 = ${fmt(sq2)}$ → $\\sqrt{${fmt(sq2)}} = ${fmt(root)}$ ✓ (вернулись к исходному)`;
formula.classList.remove('mod-active');
}
if(window.renderMathInElement){
try{ renderMathInElement(formula, {delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false}],throwOnError:false}); }catch(e){}
}
}
}
x.addEventListener('input', upd);
upd();