Files
Learn_System/frontend/textbooks/geometry_11_ch4.html
Maxim Dolgolyov 8786cf5e20 fix(textbooks): убраны лишние слэши в LaTeX-формулах (over-escaping)
Формулы в JS-литералах имели \\\\dfrac / \\\\\\\\dfrac (4/8 слэшей) вместо
\\dfrac (2). После JS-анескейпа KaTeX получал \\dfrac, трактовал \\ как
перенос строки и печатал dfrac/cdot/sqrt/pi как текст (карточка пирамиды и
конуса в geometry_11_ch2, и др.).

Схлопнуты прогоны слэшей кратные 4 перед LaTeX-командой -> 2. Прогоны из
3 слэшей (\\ перенос строки + \cmd в \begin{cases}) и перед x/цифрой не
тронуты. 150 правок в 7 файлах (algebra_11_ch1/ch2/ch3, geometry_11_ch1..ch4).

БД чиста: questions (1398) text/explanation/correct_text + options (5187) -
0 багов. Скрипт: backend/scripts/fix_overescaped_latex.js (идемпотентный,
dry-run по умолчанию, --apply, с KaTeX-валидацией).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 11:53:17 +03:00

2273 lines
152 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>Геометрия 11 · Раздел 4 · «Повторение»</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"
onload="renderMathInElement(document.body,{delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false})"></script>
<script src="/js/api.js" defer></script>
<script src="/js/xp.js" defer></script>
<script src="/js/g3d.js" defer></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Manrope:wght@600;700;800;900&family=Unbounded:wght@700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
<style>
:root{
--bg:#fafafa; --card:#fff; --card-soft:#f8fafc; --text:#0f172a; --ink:#0f172a; --muted:#64748b;
--border:#e2e8f0; --sh:0 1px 3px rgba(0,0,0,.06); --sh2:0 4px 14px rgba(0,0,0,.08);
--pri:#e11d48; --pri2:#be123c; --pri-soft:#ffe4e6;
--acc:#f43f5e; --acc2:#e11d48; --acc-soft:#fecdd3;
--ok:#10b981; --ok-bg:#d1fae5; --warn:#f59e0b; --warn-bg:#fef3c7;
--bad:#ef4444; --fail:#dc2626; --fail-bg:#fee2e2;
}
.dark{--bg:#1a0510; --card:#2a081a; --card-soft:#36102a; --text:#ffe4e6; --ink:#ffe4e6; --muted:#fda4af; --border:#4a1029}
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
html,body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.55;font-size:15px}
button,input,select,textarea{font-family:inherit;font-size:inherit}
button{cursor:pointer;border:0;background:transparent;color:inherit}
a{color:inherit;text-decoration:none}
.ic{width:16px;height:16px;display:inline-block;flex-shrink:0;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}
.hdr{position:relative;background:linear-gradient(110deg,#881337 0%,#e11d48 55%,#fb7185 100%);color:#fff;padding:46px 22px 30px;overflow:hidden;border-bottom:2px solid rgba(251,113,133,.2);min-height:130px}
.hdr::before{content:'РАЗДЕЛ 4';position:absolute;right:-12px;top:50%;transform:translateY(-50%);font-family:'Unbounded',sans-serif;font-size:clamp(5rem,15vw,11rem);font-weight:900;letter-spacing:-.04em;color:transparent;-webkit-text-stroke:1.5px rgba(255,255,255,.12);line-height:1;pointer-events:none;user-select:none;z-index:0}
.hdr-row{position:relative;z-index:1;display:flex;align-items:center;gap:14px;flex-wrap:wrap}
.hdr h1{font-family:'Unbounded',sans-serif;font-size:1.5rem;font-weight:900;letter-spacing:-.01em;line-height:1.3;padding-top:4px}
.hdr-sub{font-size:.85rem;opacity:.88;margin-top:6px;font-weight:500;line-height:1.4}
.hdr-side{margin-left:auto;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
.hdr-btn{padding:7px 12px;border-radius:9px;background:rgba(255,255,255,.14);color:#fff;font-weight:600;font-size:.82rem;display:inline-flex;align-items:center;gap:6px;transition:background .15s;text-decoration:none}
.hdr-btn:hover{background:rgba(255,255,255,.24)}
.main{max-width:1240px;margin:0 auto;padding:22px;width:100%;display:grid;grid-template-columns:1fr 280px;gap:24px}
@media(max-width:980px){.main{grid-template-columns:1fr;padding:14px}}
.col-main{min-width:0}
.hero{background:linear-gradient(135deg,var(--pri-soft) 0%,var(--acc-soft) 50%,var(--pri-soft) 100%);background-size:200% 200%;animation:heroShift 12s ease-in-out infinite;border:1px solid var(--border);border-radius:18px;padding:24px 22px;margin-bottom:24px;position:relative;overflow:hidden}
@keyframes heroShift{0%,100%{background-position:0% 50%}50%{background-position:100% 50%}}
.hero::before{content:'\25C7';position:absolute;right:0;top:-30px;font-size:clamp(2rem,12vw,8rem);font-weight:900;color:var(--pri);opacity:.10;line-height:1;pointer-events:none;font-family:'Unbounded',sans-serif}
.hero h2{font-family:'Unbounded',sans-serif;font-size:1.55rem;font-weight:800;color:var(--pri2);margin-bottom:10px;letter-spacing:-.01em}
.hero p{font-size:.95rem;color:var(--text);opacity:.88;margin-bottom:14px;max-width:640px}
.hero-row{display:flex;gap:14px;flex-wrap:wrap;align-items:center}
.btn-primary{padding:11px 22px;background:linear-gradient(135deg,var(--pri),var(--pri2));color:#fff;border-radius:11px;font-weight:700;font-size:.92rem;display:inline-flex;align-items:center;gap:8px;box-shadow:var(--sh2);transition:transform .15s,box-shadow .15s}
.btn-primary:hover{transform:translateY(-1px);box-shadow:0 8px 28px rgba(0,0,0,.18)}
.hero-progress{flex:1;min-width:200px;max-width:280px}
.hp-label{font-size:.74rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;display:block;margin-bottom:5px}
.hp-bar{height:8px;background:rgba(0,0,0,.12);border-radius:5px;overflow:hidden}
.hp-fill{height:100%;background:linear-gradient(90deg,var(--pri),var(--acc));border-radius:5px;width:0%;transition:width .6s cubic-bezier(.16,1,.3,1)}
.hp-text{font-size:.78rem;color:var(--muted);font-weight:700;margin-top:4px;display:block}
.hero-xp-badge{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:linear-gradient(135deg,var(--warn,#f59e0b),var(--pri));color:#fff;border-radius:99px;font-size:.82rem;font-weight:800;letter-spacing:.02em;box-shadow:0 4px 12px rgba(0,0,0,.18);font-family:'Unbounded',sans-serif}
.psel{margin-bottom:24px}
.psel-title{font-size:.72rem;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:.08em;margin-bottom:10px}
.psel-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:10px}
.psel-card{background:var(--card);border:1.5px solid var(--border);border-radius:13px;padding:14px;cursor:pointer;transition:transform .2s,box-shadow .2s,border-color .2s;text-align:left;position:relative}
.psel-card:hover{transform:translateY(-3px);box-shadow:var(--sh2);border-color:var(--pri)}
.psel-card.active{border-color:var(--pri);background:linear-gradient(135deg,var(--pri-soft),var(--card));box-shadow:var(--sh2)}
.psel-card.active::after{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,var(--pri),var(--acc));border-radius:13px 13px 0 0}
.psel-num{font-family:'Unbounded',sans-serif;font-size:.72rem;font-weight:800;color:var(--pri);text-transform:uppercase;letter-spacing:.08em;margin-bottom:5px}
.psel-name{font-size:.86rem;font-weight:700;color:var(--text);line-height:1.3;margin-bottom:8px}
.psel-prog{height:4px;background:rgba(0,0,0,.10);border-radius:3px;overflow:hidden}
.psel-prog-fill{height:100%;background:var(--pri);width:0%;transition:width .4s}
.psel-card.final{background:linear-gradient(135deg,var(--acc-soft),var(--pri-soft))}
.psel-card.final .psel-num{color:var(--warn)}
.sec[id="sec-p8"]{ --sec-acc:#e11d48; --sec-acc-d:#be123c; --sec-acc-soft:#ffe4e6; }
.sec[id="sec-p9"]{ --sec-acc:#e11d48; --sec-acc-d:#be123c; --sec-acc-soft:#ffe4e6; }
.sec[id="sec-p10"]{ --sec-acc:#e11d48; --sec-acc-d:#be123c; --sec-acc-soft:#ffe4e6; }
.sec[id="sec-p11"]{ --sec-acc:#e11d48; --sec-acc-d:#be123c; --sec-acc-soft:#ffe4e6; }
.sec[id="sec-final4"]{ --sec-acc:#e11d48; --sec-acc-d:#be123c; --sec-acc-soft:#ffe4e6; }
.sec{display:none;position:relative;animation:fadeIn .35s ease}
.sec.active{display:block}
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
.sec::before{content:attr(data-watermark);position:absolute;right:-20px;top:10%;font-family:'Unbounded',sans-serif;font-size:clamp(6rem,18vw,14rem);font-weight:900;color:transparent;-webkit-text-stroke:1.5px var(--sec-acc-soft,var(--pri-soft));line-height:1;pointer-events:none;user-select:none;z-index:0;opacity:.35}
.sec-header{margin-bottom:22px;padding-bottom:14px;border-bottom:2px solid var(--sec-acc-soft,var(--pri-soft));position:relative;z-index:1}
.sec-num{display:inline-block;padding:4px 10px;background:linear-gradient(135deg,var(--sec-acc,var(--pri)),var(--sec-acc-d,var(--pri2)));color:#fff;border-radius:7px;font-family:'Unbounded',sans-serif;font-size:.78rem;font-weight:800;letter-spacing:.04em;margin-bottom:8px}
.sec-h{font-family:'Unbounded',sans-serif;font-size:1.6rem;font-weight:800;color:var(--sec-acc-d,var(--pri2));letter-spacing:-.01em;line-height:1.25}
.card{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:18px 20px;margin-bottom:16px;box-shadow:0 1px 3px rgba(0,0,0,.04),0 8px 24px rgba(0,0,0,.04);position:relative;z-index:1;transition:transform .25s cubic-bezier(.16,1,.3,1),box-shadow .25s}
.card:hover{transform:translateY(-2px);box-shadow:0 4px 10px rgba(0,0,0,.06),0 16px 36px rgba(0,0,0,.08)}
.card-header{display:flex;align-items:center;gap:10px;margin-bottom:12px;padding-bottom:10px;border-bottom:1px dashed var(--border)}
.card-icon{width:32px;height:32px;border-radius:9px;display:flex;align-items:center;justify-content:center;flex-shrink:0;color:#fff}
.card-icon.repeat{background:#0ea5e9}.card-icon.theory{background:#8b5cf6}.card-icon.algo{background:#f59e0b}.card-icon.rule{background:#ec4899}.card-icon.example{background:#10b981}.card-icon.oral{background:#06b6d4}
.card-icon .ic{width:18px;height:18px}
.card-title{font-family:'Unbounded',sans-serif;font-size:.82rem;font-weight:800;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);flex:1}
.card-num{font-size:.74rem;font-weight:700;color:var(--muted);background:var(--sec-acc-soft,var(--pri-soft));padding:3px 7px;border-radius:5px}
.card-body{font-size:.94rem;line-height:1.65}
.card-body p{margin-bottom:8px}
.card-body p:last-child{margin-bottom:0}
.btn{padding:8px 16px;border-radius:8px;background:var(--card);color:var(--text);border:1.5px solid var(--border);font-weight:600;font-size:.88rem;transition:background .15s,border-color .15s,transform .1s}
.btn:hover{background:var(--sec-acc-soft,var(--pri-soft));border-color:var(--sec-acc,var(--pri))}
.btn:active{transform:scale(.96)}
.btn.primary{background:var(--sec-acc,var(--pri));color:#fff;border-color:var(--sec-acc,var(--pri))}
.btn.primary:hover{background:var(--sec-acc-d,var(--pri2));border-color:var(--sec-acc-d,var(--pri2))}
.feedback{padding:10px 14px;border-radius:9px;font-weight:600;font-size:.88rem;margin-top:8px;display: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 #ffe4e6;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)}
.sidecard-row{margin-bottom:8px;font-size:.86rem;line-height:1.6}
.sidecard-row b{color:var(--pri);font-weight:700}
.sidecard-row:last-child{margin-bottom:0}
@media(max-width:980px){.col-side{position:static;max-height:none}}
.xp-card{background:linear-gradient(135deg,var(--acc-soft),var(--pri-soft));border:1.5px solid var(--acc);border-radius:12px;padding:14px;margin-bottom:14px}
.xp-card-title{font-size:.68rem;font-weight:800;color:var(--acc2);text-transform:uppercase;letter-spacing:.07em;margin-bottom:8px;display:flex;align-items:center;justify-content:space-between}
.xp-level{font-size:1.1rem;font-weight:900;color:var(--acc2);font-family:'Unbounded',sans-serif}
.xp-bar{height:9px;background:rgba(0,0,0,.10);border-radius:6px;overflow:hidden;margin:7px 0}
.xp-fill{height:100%;background:linear-gradient(90deg,var(--acc),var(--pri));border-radius:6px;transition:width .5s cubic-bezier(.4,0,.2,1)}
.xp-nums{font-size:.74rem;color:var(--muted);display:flex;justify-content:space-between}
.sec-nav{display:flex;gap:10px;margin-top:24px;padding-top:20px;border-top:1px solid var(--border);justify-content:space-between;flex-wrap:wrap}
.foot{text-align:center;padding:30px 16px;color:var(--muted);font-size:.78rem;border-top:1px solid var(--border);margin-top:30px}
.ach-popup{position:fixed;top:80px;right:18px;background:linear-gradient(135deg,var(--pri),var(--acc));color:#fff;padding:12px 18px;border-radius:11px;font-weight:700;font-size:.9rem;box-shadow:0 8px 28px rgba(0,0,0,.32);z-index:1002;display:none;align-items:center;gap:8px;max-width:340px}
.ach-popup.show{display:flex}
.col-side-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.42);z-index:9990;display:none}
.col-side-backdrop.show{display:block}
@media(min-width:981px){#sidebar-btn{display:none}.col-side-backdrop.show{display:none}}
@media(max-width:980px){
.col-side{position:fixed;top:0;right:0;height:100vh;width:300px;max-width:88vw;background:var(--bg);box-shadow:-12px 0 24px rgba(0,0,0,.18);padding:18px 16px;overflow-y:auto;transform:translateX(100%);transition:transform .25s ease;z-index:9991;max-height:none}
.col-side.open{transform:none}
}
.search-modal{position:fixed;inset:0;background:rgba(15,23,42,.55);backdrop-filter:blur(4px);z-index:9993;display:none;align-items:flex-start;justify-content:center;padding-top:14vh}
.search-modal.show{display:flex}
.search-box{background:var(--bg);border:1px solid var(--border);border-radius:14px;width:560px;max-width:92vw;max-height:70vh;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 24px 64px rgba(0,0,0,.4)}
.search-input{padding:14px 16px;font-size:1rem;border:0;border-bottom:1px solid var(--border);background:transparent;color:var(--text);outline:none}
.search-results{flex:1;overflow-y:auto;padding:6px 0}
.search-row{display:block;padding:8px 16px;cursor:pointer;border-bottom:1px solid var(--border);text-align:left;background:transparent;border:0;width:100%;color:var(--text)}
.search-row:hover,.search-row.active{background:var(--sec-acc-soft,var(--pri-soft))}
.search-row .sr-kind{font-size:.7rem;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:2px}
.search-row .sr-title{font-weight:700;font-size:.92rem;color:var(--text)}
.search-row .sr-desc{font-size:.8rem;color:var(--muted);margin-top:2px}
.search-empty{padding:20px;text-align:center;color:var(--muted);font-size:.88rem}
.search-foot{padding:8px 14px;border-top:1px solid var(--border);font-size:.74rem;color:var(--muted);display:flex;gap:14px}
.search-foot kbd{padding:2px 6px;background:var(--card);border:1px solid var(--border);border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:.72rem}
/* === GEOM11 POLISH === */
@keyframes wgFadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
.sec.active .wg{animation:wgFadeIn .35s cubic-bezier(.16,1,.3,1) backwards}
.sec.active .wg:nth-of-type(1){animation-delay:.02s}
.sec.active .wg:nth-of-type(2){animation-delay:.08s}
.sec.active .wg:nth-of-type(3){animation-delay:.14s}
.sec.active .wg:nth-of-type(4){animation-delay:.20s}
.sec.active .wg:nth-of-type(5){animation-delay:.26s}
.sec.active .wg:nth-of-type(6){animation-delay:.32s}
.wg svg{transition:filter .25s ease}
.wg:hover svg{filter:drop-shadow(0 4px 16px rgba(0,0,0,.10))}
input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
.wg input[type=range]{cursor:ew-resize}
.score-display b{transition:transform .22s cubic-bezier(.16,1,.3,1),color .22s;display:inline-block;transform-origin:center}
.score-display b.bump{transform:scale(1.28);color:var(--pri)}
.katex{transition:color .2s}
.wg-help .katex:hover,.card-body .katex:hover{color:var(--pri2,var(--pri));cursor:help}
.hp-fill,.psel-prog-fill,.xp-fill,[id$="-overall-fill"]{transition:width .6s cubic-bezier(.16,1,.3,1)!important}
.boss-card,.btn.primary,.btn-primary{position:relative;overflow:hidden}
.btn.primary::after,.btn-primary::after{content:'';position:absolute;inset:0;background:radial-gradient(circle at center,rgba(255,255,255,.42) 0%,transparent 60%);opacity:0;transition:opacity .25s;pointer-events:none}
.btn.primary:hover::after,.btn-primary:hover::after{opacity:1}
.psel-card{position:relative}
.psel-card .psel-done{position:absolute;top:6px;right:6px;width:18px;height:18px;border-radius:50%;background:#10b981;display:none;align-items:center;justify-content:center;box-shadow:0 2px 6px rgba(16,185,129,.45);z-index:2}
.psel-card .psel-done svg{width:11px;height:11px;stroke:#fff;fill:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:round}
.psel-card.done .psel-done{display:flex}
.sec{transition:opacity .25s}
/* g3d toolbar */
.g3d-tools{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px}
.g3d-tools .btn{padding:5px 10px;font-size:.78rem}
.stub-note{padding:18px 22px;background:linear-gradient(135deg,var(--pri-soft),var(--sec-acc-soft));border:1.5px dashed var(--pri);border-radius:13px;text-align:center;color:var(--text);margin-bottom:14px}
.stub-note h3{font-family:'Unbounded',sans-serif;color:var(--pri2);margin-bottom:8px;font-size:1.05rem}
.stub-note p{color:var(--muted);font-size:.9rem;line-height:1.55}
</style>
</head>
<body>
<header class="hdr">
<div class="hdr-row">
<div>
<h1>Геометрия 11 · Раздел 4</h1>
<div class="hdr-sub">Планиметрия · величины · координаты и векторы в 3D · построения</div>
</div>
<div class="hdr-side">
<a href="/textbook/geometry-11" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> К геометрии 11</a>
<button id="search-btn" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m21 21-4-4"/></svg> Поиск</button>
<button id="sidebar-btn" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="18" x2="14" y2="18"/></svg> Шпаргалка</button>
<button id="theme-btn" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg><span id="theme-lab">Тёмная</span></button>
</div>
</div>
</header>
<main class="main">
<div class="col-main">
<section class="hero">
<h2>Повторение всей геометрии</h2>
<p>Повторение всей геометрии: планиметрия, площади и объёмы, координаты и векторы в 3D, классические построения.</p>
<div class="hero-row">
<button class="btn-primary" onclick="goTo('p8')"><svg class="ic" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4" fill="currentColor" stroke="none"/></svg> Начать § 8</button>
<div class="hero-progress">
<span class="hp-label">Прогресс по разделу</span>
<div class="hp-bar"><div id="hero-hp-fill" class="hp-fill"></div></div>
<span id="hero-hp-text" class="hp-text">0%</span>
</div>
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
</div>
</section>
<section class="psel">
<div class="psel-title">Параграфы раздела</div>
<div id="psel-grid" class="psel-grid"></div>
</section>
<section id="sec-p8" class="sec" data-watermark="\square"><div class="sec-header"><span class="sec-num">§ 8</span><h2 class="sec-h">Геометрические фигуры и их свойства</h2></div><div id="p8-body"></div></section>
<section id="sec-p9" class="sec" data-watermark="S="><div class="sec-header"><span class="sec-num">§ 9</span><h2 class="sec-h">Геометрические величины</h2></div><div id="p9-body"></div></section>
<section id="sec-p10" class="sec" data-watermark="\vec{v}"><div class="sec-header"><span class="sec-num">§ 10</span><h2 class="sec-h">Координаты и векторы</h2></div><div id="p10-body"></div></section>
<section id="sec-p11" class="sec" data-watermark="\circlearrowleft"><div class="sec-header"><span class="sec-num">§ 11</span><h2 class="sec-h">Геометрические построения</h2></div><div id="p11-body"></div></section>
<section id="sec-final4" class="sec" data-watermark="&#9733;"><div class="sec-header"><span class="sec-num" style="background:linear-gradient(135deg,#e11d48,#f43f5e)">&#9733;</span><h2 class="sec-h">Финал раздела</h2></div><div id="final4-body"></div></section>
</div>
<aside class="col-side" id="col-side"><div id="sidebar-content"></div></aside>
<div class="col-side-backdrop" id="col-side-backdrop"></div>
</main>
<footer class="foot">Интерактивный учебник «Геометрия 11» · Раздел 4 · «Повторение» · LearnSpace</footer>
<div id="ach-popup" class="ach-popup"><svg class="ic" viewBox="0 0 24 24" style="width:22px;height:22px"><polygon points="12,2 22,20 2,20"/></svg><span id="ach-text">Достижение!</span></div>
<div id="search-modal" class="search-modal" role="dialog">
<div class="search-box">
<input type="text" id="search-input" class="search-input" placeholder="Поиск…" autocomplete="off">
<div id="search-results" class="search-results"></div>
<div class="search-foot"><span><kbd>↑↓</kbd> навигация</span><span><kbd>Enter</kbd> открыть</span><span><kbd>Esc</kbd> закрыть</span></div>
</div>
</div>
<script>
'use strict';
const STATE = { current:'p8', progress:{}, achievements:new Map(), xp:0, level:1 };
const TOTAL_PARAS = 5;
const _TB_SLUG = 'geometry-11-ch4';
const PARAS = [
{ id:'p8', num:'§ 8', name:"Геометрические фигуры и их свойства", sub:'планиметрия' },
{ id:'p9', num:'§ 9', name:"Геометрические величины", sub:'площади, объёмы' },
{ id:'p10', num:'§ 10', name:"Координаты и векторы", sub:'3D: $\\vec{a}=(x;y;z)$' },
{ id:'p11', num:'§ 11', name:"Геометрические построения", sub:'циркуль и линейка' },
{ id:'final4', num:'&#9733;', name:"Финал раздела", sub:'Итоги · боссы раздела 4', final:true }
];
PARAS.forEach(p => { STATE.progress[p.id] = 0; });
function calcLevel(xp){ return Math.floor(Math.sqrt((xp||0)/100))+1; }
function _xpForLevel(lv){ return (lv-1)*(lv-1)*100; }
const ACH_LABELS = {
p8_done:"Геометрические фигуры и их свойства освоено!",
p9_done:"Геометрические величины освоено!",
p10_done:"Координаты и векторы освоено!",
p11_done:"Геометрические построения освоено!",
start:"Начало раздела 4!",
ch4_done:"Раздел 4 пройден!",
r4_done:"Магистр повторения"
};
function loadProgress(){
try{
const s=localStorage.getItem('geometry11_ch4_progress'); if(s) Object.assign(STATE.progress, JSON.parse(s));
const a=localStorage.getItem('geometry11_ch4_achievements');
if(a){ const p=JSON.parse(a); if(Array.isArray(p)) p.forEach(id=>STATE.achievements.set(id, ACH_LABELS[id]||id)); else if(p&&typeof p==='object'){ for(const[id,t] of Object.entries(p)) STATE.achievements.set(id,(t&&t!==id)?t:(ACH_LABELS[id]||id)); } }
STATE.xp=+(localStorage.getItem('geometry11_xp')||0); STATE.level=calcLevel(STATE.xp);
}catch(e){}
}
function saveProgress(){
try{
localStorage.setItem('geometry11_ch4_progress', JSON.stringify(STATE.progress));
localStorage.setItem('geometry11_ch4_achievements', JSON.stringify(Object.fromEntries(STATE.achievements)));
localStorage.setItem('geometry11_xp', String(STATE.xp));
}catch(e){}
}
function bumpProgress(key, delta){
STATE.progress[key]=Math.max(0,Math.min(100,(STATE.progress[key]||0)+delta));
saveProgress(); refreshProgressUI();
if(STATE.progress[key]>=50) markParaRead(key);
}
const _markedRead=new Set();
let _pendingProgressBody=null, _progressTimer=null;
function _flushProgress(){
const body=_pendingProgressBody; _pendingProgressBody=null; if(!body) return;
const tok=(window.LS&&LS.getToken)?LS.getToken():''; if(!tok) return;
fetch('/api/textbooks/'+_TB_SLUG+'/progress',{method:'POST',headers:{'Content-Type':'application/json','Authorization':'Bearer '+tok},body:JSON.stringify(body),keepalive:true}).catch(()=>{});
}
function _queueProgress(patch){ _pendingProgressBody=Object.assign(_pendingProgressBody||{},patch); if(_progressTimer) clearTimeout(_progressTimer); _progressTimer=setTimeout(_flushProgress, 600); }
function markLastPara(id){ _queueProgress({last_para:id}); }
function markParaRead(id){ if(_markedRead.has(id)) return; _markedRead.add(id); _queueProgress({mark_read:id}); }
window.addEventListener('beforeunload', _flushProgress);
function loadServerReadState(){
const tok=(window.LS&&LS.getToken)?LS.getToken():''; if(!tok) return;
fetch('/api/textbooks/'+_TB_SLUG,{headers:{'Authorization':'Bearer '+tok}}).then(r=>r.ok?r.json():null).then(d=>{
if(!d||!d.progress) return;
(d.progress.read||[]).forEach(k=>{_markedRead.add(k); if((STATE.progress[k]||0)<50) STATE.progress[k]=100;});
saveProgress(); refreshProgressUI();
}).catch(()=>{});
}
function addXp(n,src){
if(!n) return;
const prev=STATE.level; STATE.xp=Math.max(0,(STATE.xp||0)+n); STATE.level=calcLevel(STATE.xp);
saveProgress(); refreshProgressUI();
if(window.LS&&window.LS.xp) window.LS.xp.add(n,'geometry11-ch4-'+(src||'misc'));
if(STATE.level>prev){
const pop=document.getElementById('ach-popup');
if(pop){ document.getElementById('ach-text').textContent='Уровень '+STATE.level+'!'; pop.classList.add('show'); setTimeout(()=>pop.classList.remove('show'),2600); }
}
}
function refreshProgressUI(){
const total=Math.round(Object.values(STATE.progress).reduce((a,b)=>a+b,0)/TOTAL_PARAS);
const f=document.getElementById('hero-hp-fill'); if(f) f.style.width=total+'%';
const t=document.getElementById('hero-hp-text'); if(t) t.textContent=total+'% пройдено';
document.querySelectorAll('[data-prog-card]').forEach(el=>{ const k=el.dataset.progCard; const fl=el.querySelector('.psel-prog-fill'); if(fl) fl.style.width=(STATE.progress[k]||0)+'%'; });
const xpBadge=document.getElementById('hero-xp-badge');
if(xpBadge){ xpBadge.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:13px;height:13px"><polygon points="12 2 22 20 2 20"/></svg> Ур. '+STATE.level+' \xb7 '+(STATE.xp||0)+' XP'; }
if(STATE.current && document.getElementById('sidebar-content')){ try{ buildSidebar(STATE.current); }catch(e){} }
}
function achievement(id,text){
if(STATE.achievements.has(id)) return;
STATE.achievements.set(id, text||ACH_LABELS[id]||id); saveProgress();
const pop=document.getElementById('ach-popup');
if(pop){ document.getElementById('ach-text').textContent=text||ACH_LABELS[id]||id; pop.classList.add('show'); setTimeout(()=>pop.classList.remove('show'),3300); }
addXp(20,'ach-'+id);
}
function buildParaSelector(){
const g=document.getElementById('psel-grid'); g.innerHTML='';
PARAS.forEach(p=>{
const card=document.createElement('div');
card.className='psel-card'+(p.final?' final':'');
card.dataset.id=p.id; card.dataset.progCard=p.id;
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>';
card.addEventListener('click', ()=>goTo(p.id));
g.appendChild(card);
});
}
const BUILT=new Set();
const BUILDERS = { p8:()=>buildP8(), p9:()=>buildP9(), p10:()=>buildP10(), p11:()=>buildP11(), final4:()=>buildFinal4() };
function ensureBuilt(id){ if(BUILT.has(id)) return; const fn=BUILDERS[id]; if(fn){ fn(); BUILT.add(id); } }
function goTo(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');
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(()=>renderMath(el), 0);
markLastPara(id);
}
const SIDEBARS = {
p8:{title:"Шпаргалка § 8", rows:[["Сумма углов $\\triangle$","$180°$"],["Пифагор","$a^2+b^2=c^2$"],["Синусов","$\\dfrac{a}{\\sin A}=2R$"],["Косинусов","$c^2=a^2+b^2-2ab\\cos C$"],["Длина окр.","$C=2\\pi R$"],["$S$ круга","$\\pi R^2$"]]},
p9:{title:"Шпаргалка § 9", rows:[["$S_\\triangle$","$\\frac{1}{2}ab\\sin C$"],["Герон","$\\sqrt{p(p-a)(p-b)(p-c)}$"],["$S$ трап.","$\\frac{a+b}{2}h$"],["$V$ призмы","$S_{осн}h$"],["$V$ пир.","$\\frac{1}{3}S_{осн}h$"],["$V$ шара","$\\frac{4}{3}\\pi R^3$"]]},
p10:{title:"Шпаргалка § 10", rows:[["Тема", "Координаты и векторы"],["Формула","3D: $\\vec{a}=(x;y;z)$"]]},
p11:{title:"Шпаргалка § 11", rows:[["Тема", "Геометрические построения"],["Формула","циркуль и линейка"]]},
final4:{title:"Финал раздела 4", rows:[["§ 8–§ 11","теория раздела 4"],["Награда","+50 XP"]]}
};
const TIPS=[
{sec:'p8',html:"§ 8: обзор планиметрии. Теоремы Пифагора, синусов, косинусов; виды треугольников и четырёхугольников; окружность."},
{sec:'p9',html:"§ 9: площади плоских фигур и объёмы тел. Формулы Герона, $S=\\frac{1}{2}ab\\sin C$, $V=\\frac{1}{3}S_{осн}h$, шар."},
{sec:'p10',html:"§ 10 «Координаты и векторы» — содержание в разработке. 3D: $\\vec{a}=(x;y;z)$"},
{sec:'p11',html:"§ 11 «Геометрические построения» — содержание в разработке. циркуль и линейка"},
{sec:'final4',html:"Финал раздела 4 — интегрированные задачи по разделу."}
];
function buildSidebar(id){
const box=document.getElementById('sidebar-content');
const sb=SIDEBARS[id]||SIDEBARS[PARAS[0].id];
let html='';
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><span class="xp-level">Ур. '+STATE.level+'</span></div><div class="xp-bar"><div class="xp-fill" style="width:'+xpPct+'%"></div></div><div class="xp-nums"><span>'+STATE.xp+' XP</span><span>'+xpNext+' XP</span></div></div>';
html+='<div class="sidecard"><h4>'+sb.title+'</h4>';
sb.rows.forEach(([k,v])=>{ html+='<div class="sidecard-row"><b>'+k+'</b>'+(v?' \u2014 '+v:'')+'</div>'; });
html+='</div>';
const tip=TIPS.find(t=>t.sec===id)||TIPS[0];
if(tip){
html+='<div class="sidecard" style="background:linear-gradient(135deg,var(--warn-bg,#fef3c7),var(--pri-soft));border-color:var(--warn,#f59e0b)"><h4 style="color:#92400e;display:flex;align-items:center;gap:6px"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px"><polygon points="12,2 22,20 2,20"/></svg>Подсказка</h4><div class="sidecard-row" style="margin-bottom:0;font-size:.84rem;line-height:1.55">'+tip.html+'</div></div>';
}
if(STATE.achievements.size>0){
html+='<div class="sidecard"><h4>Достижения <span style="color:var(--warn);float:right">'+STATE.achievements.size+'</span></h4>';
[...STATE.achievements.values()].slice(-4).forEach(text=>{ html+='<div class="sidecard-row" style="font-size:.78rem;color:var(--ok)">&#10003; '+text+'</div>'; });
html+='</div>';
}
box.innerHTML=html;
if(window.renderMathInElement) try{ renderMath(box); }catch(e){}
}
function initTheme(){
const t=localStorage.getItem('geometry11_ch4_theme')||'light';
if(t==='dark') document.documentElement.classList.add('dark');
document.getElementById('theme-lab').textContent=t==='dark'?'Светлая':'Тёмная';
document.getElementById('theme-btn').addEventListener('click', ()=>{
document.documentElement.classList.toggle('dark');
const dark=document.documentElement.classList.contains('dark');
localStorage.setItem('geometry11_ch4_theme', dark?'dark':'light');
document.getElementById('theme-lab').textContent=dark?'Светлая':'Тёмная';
});
}
function renderMath(root){ if(window.renderMathInElement){ try{ renderMathInElement(root, {delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false}); }catch(e){} } }
function feedback(elm, ok, text){ if(!elm) return; elm.className='feedback '+(ok?'ok':'fail'); elm.innerHTML=text||(ok?'&#10003; Верно!':'&#10007; Неверно'); elm.style.display='block'; try{renderMath(elm);}catch(e){} }
function fmt(n){ if(!isFinite(n)) return '?'; if(Number.isInteger(n)) return String(n); return Math.abs(n-Math.round(n))<1e-9?String(Math.round(n)):(+n.toFixed(6)).toString(); }
function ipow(base, exp){ let r=1; for(let i=0;i<Math.abs(exp);i++) r*=base; return exp<0 ? 1/r : r; }
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-хелперы 2D (axes, plotFunc, pointWithDrop, asymptote, snapToValue, геом.) === */
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 };
}
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"/>';
}
function pointWithDrop(x, fx, toX, toY, color, label){
const px = toX(x), py = toY(fx);
let s = '';
s += '<line x1="'+px+'" y1="'+py+'" x2="'+px+'" y2="'+toY(0)+'" stroke="'+color+'" stroke-width="1.2" stroke-dasharray="3 3" opacity=".7"/>';
s += '<line x1="'+px+'" y1="'+py+'" x2="'+toX(0)+'" y2="'+py+'" stroke="'+color+'" stroke-width="1.2" stroke-dasharray="3 3" opacity=".7"/>';
s += '<circle cx="'+px+'" cy="'+py+'" r="4.5" fill="'+color+'" stroke="#fff" stroke-width="2"/>';
if (label){
s += '<text x="'+(px+8)+'" y="'+(py-8)+'" font-family="Inter,sans-serif" font-size="12" font-weight="700" fill="'+color+'">'+label+'</text>';
}
return s;
}
function asymptote(orientation, value, toX, toY, xmin, xmax, ymin, ymax, color){
color = color || '#94a3b8';
if (orientation === 'h'){
const y = toY(value);
return '<line x1="'+toX(xmin)+'" y1="'+y+'" x2="'+toX(xmax)+'" y2="'+y+'" stroke="'+color+'" stroke-width="1.3" stroke-dasharray="6 4"/>';
} else {
const x = toX(value);
return '<line x1="'+x+'" y1="'+toY(ymin)+'" x2="'+x+'" y2="'+toY(ymax)+'" stroke="'+color+'" stroke-width="1.3" stroke-dasharray="6 4"/>';
}
}
function snapToValue(value, snapPoints, tolerance){
tolerance = tolerance || 0.1;
for (const sp of snapPoints){
if (Math.abs(value - sp) < tolerance) return sp;
}
return value;
}
function rightAngleMark(V, uIn, wIn, s){
s = s || 9;
const p1 = {x: V.x + s*uIn.x, y: V.y + s*uIn.y};
const c = {x: p1.x + s*wIn.x, y: p1.y + s*wIn.y};
const p2 = {x: V.x + s*wIn.x, y: V.y + s*wIn.y};
return p1.x+','+p1.y+' '+c.x+','+c.y+' '+p2.x+','+p2.y;
}
function angleArcAuto(V, uA, uB, R){
const sA = {x: V.x + R*uA.x, y: V.y + R*uA.y};
const eB = {x: V.x + R*uB.x, y: V.y + R*uB.y};
const cross = uA.x*uB.y - uA.y*uB.x;
const sweep = cross > 0 ? 1 : 0;
return 'M'+sA.x+','+sA.y+' A'+R+','+R+' 0 0,'+sweep+' '+eB.x+','+eB.y;
}
function unitVec(p1, p2){
const dx = p2.x - p1.x, dy = p2.y - p1.y;
const len = Math.sqrt(dx*dx + dy*dy) || 1;
return {x: dx/len, y: dy/len};
}
function deg2rad(d){ return d * Math.PI / 180; }
const ICONS = {
repeat:'<svg class="ic" viewBox="0 0 24 24"><polyline points="9 11 12 14 22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>',
theory:'<svg class="ic" viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>',
algo:'<svg class="ic" viewBox="0 0 24 24"><polyline points="17 11 21 7 17 3"/><line x1="21" y1="7" x2="9" y2="7"/><polyline points="7 13 3 17 7 21"/><line x1="3" y1="17" x2="15" y2="17"/></svg>',
rule:'<svg class="ic" viewBox="0 0 24 24"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></svg>',
example:'<svg class="ic" viewBox="0 0 24 24"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M12 2a7 7 0 0 0-4 13c1 1 2 2 2 4h4c0-2 1-3 2-4a7 7 0 0 0-4-13z"/></svg>',
oral:'<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
};
function secNavFor(curId){
const idx = PARAS.findIndex(p => p.id === curId);
const prev = idx > 0 ? PARAS[idx-1].id : null;
const next = idx < PARAS.length - 1 ? PARAS[idx+1].id : null;
return secNav(prev, next);
}
function secNav(prev, next){
const NAMES = {p8:'\xA78',p9:'\xA79',p10:'\xA710',p11:'\xA711',final4:'Финал'};
let h='<div class="sec-nav">';
h+=prev?'<button class="btn" onclick="goTo(\''+prev+'\')"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> '+NAMES[prev]+'</button>':'<span></span>';
h+=next?'<button class="btn primary" onclick="goTo(\''+next+'\')">'+NAMES[next]+' <svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></button>':'<span></span>';
h+='</div>'; return h;
}
function readButton(paraId){
const p = PARAS.find(x => x.id === paraId);
const labelTail = p && p.final ? 'финал' : (p ? p.num : '\xA7?');
return '<div style="margin-top:18px;display:flex;justify-content:center">'
+'<button class="btn primary" id="'+paraId+'-read-btn">'
+'<svg class="ic" viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>'
+' Я прочитал — '+labelTail+' (+10 XP)'
+'</button></div>';
}
function wireReadBtn(paraId){
const btn = document.getElementById(paraId+'-read-btn'); if(!btn) return;
btn.addEventListener('click', ()=>{
addXp(10, paraId+'-read'); bumpProgress(paraId, 30);
btn.textContent='Прочитано! +10 XP'; btn.disabled=true; btn.style.opacity=.6;
const aId = paraId+'_done';
if(ACH_LABELS[aId]) achievement(aId);
});
}
/* ===== STUB BUILDER — единый для всех параграфов раздела (Phase 0) ===== */
function buildStub(id){
const p = PARAS.find(x => x.id === id);
if(!p) return;
const box = document.getElementById(id + '-body');
if(!box) return;
let html = '';
html += '<div class="stub-note">'
+ '<h3>' + p.num + ' «' + p.name + '» — в разработке</h3>'
+ '<p>Это параграф раздела ' + 4 + '. Полное наполнение (теория + 3 интерактива + DnD + тренажёр) появится в Phase 1+. Сейчас доступны только базовая навигация, прогресс-бар и подсказка в боковой панели.</p>'
+ '</div>';
html += makeCard('theory', 'План параграфа', p.num, '<p>Тема: <b>' + p.name + '</b>.</p>' + (p.sub ? '<p>Ключевая формула: ' + p.sub + '</p>' : '') + '<p>Содержание будет реализовано в следующих фазах разработки.</p>');
/* Демо-интерактив с G3D (если доступен) — показываем разработчику, что движок работает */
if(window.G3D && !p.final){
html += '<div class="wg" id="' + id + '-iv-demo">'
+ '<div class="wg-header"><span class="wg-badge">DEMO 3D</span><div class="wg-title">Превью мини-3D движка</div></div>'
+ '<div class="wg-help">Скелет-демо: тело можно вращать мышью. В Phase 1+ здесь появятся полноценные интерактивы с сечениями, развёртками и формулами.</div>'
+ '<div class="g3d-tools">'
+ '<button class="btn" data-view="iso">Изо</button>'
+ '<button class="btn" data-view="front">Спереди</button>'
+ '<button class="btn" data-view="top">Сверху</button>'
+ '<button class="btn" data-view="side">Сбоку</button>'
+ '</div>'
+ '<div style="background:var(--card);border-radius:9px;padding:10px;text-align:center"><svg id="' + id + '-iv-svg" viewBox="0 0 480 360" width="100%" style="max-width:480px;height:auto"></svg></div>'
+ '</div>';
}
html += secNavFor(id);
html += readButton(id);
box.innerHTML = html;
renderMath(box);
/* Установка демо-3D */
if(window.G3D && !p.final){
const svg = document.getElementById(id + '-iv-svg');
if(svg){
const scene = G3D.createScene({W:480, H:360, scale:42, camDist:8, rotX:-0.35, rotY:0.7});
/* выбираем фигуру по id параграфа */
let mesh;
if(id === 'p1') mesh = G3D.prismMesh(4, 1.6, 2.4); /* куб/призма */
else if(id === 'p2') mesh = G3D.cylinderMesh(1.5, 2.6, 32);
else if(id === 'p3') mesh = G3D.pyramidMesh(4, 1.8, 2.6);
else if(id === 'p4') mesh = G3D.coneMesh(1.5, 2.6, 32);
else if(id === 'p5' || id === 'p6') mesh = null; /* сфера — отдельный wireframe */
else if(id === 'p7') mesh = G3D.prismMesh(3, 1.6, 1.8); /* тетраэдр-подобно */
else if(id === 'p10') mesh = G3D.prismMesh(4, 1.6, 2.0); /* куб для координат */
else mesh = G3D.prismMesh(6, 1.5, 2.2);
function draw(){
const M = G3D.buildRotMatrix(scene);
let inner = '';
if(mesh){
inner = G3D.renderMesh(mesh, M, scene);
} else {
/* сфера */
const sph = G3D.sphereWireframe(1.7, 5, 10);
inner = G3D.renderSphereWireframe(sph, M, scene);
}
svg.innerHTML = inner;
}
draw();
G3D.attachOrbit(svg, scene, draw);
const tools = document.querySelectorAll('#' + id + '-iv-demo .g3d-tools .btn');
tools.forEach(b => b.addEventListener('click', () => {
G3D.presetView(scene, b.dataset.view, draw);
}));
}
}
wireReadBtn(id);
}
/* ===== §8 «Геометрические фигуры и их свойства» (повторение планиметрии) ===== */
function buildP8(){
const box = document.getElementById('p8-body');
if(!box) return;
let html = '';
/* === ТЕОРИЯ === */
html += makeCard('theory', 'Треугольники и четырёхугольники', '§ 8.1',
'<p><b>Треугольник.</b> Сумма углов любого треугольника равна $180°$. Виды по углам: <i>остроугольный, прямоугольный, тупоугольный</i>. По сторонам: <i>разносторонний, равнобедренный, равносторонний</i>.</p>'
+ '<p><b>Замечательные точки треугольника:</b> центроид $G$ (точка пересечения медиан, делит каждую медиану $2{:}1$ от вершины), ортоцентр $H$ (пересечение высот), центр описанной окружности $O$ (пересечение серединных перпендикуляров), центр вписанной окружности $I$ (пересечение биссектрис).</p>'
+ '<p><b>Прямоугольный треугольник:</b> теорема Пифагора $a^2 + b^2 = c^2$. Тригонометрия острого угла $A$:</p>'
+ '<p style="text-align:center;margin:6px 0">$\\sin A = \\dfrac{a}{c},\\quad \\cos A = \\dfrac{b}{c},\\quad \\tan A = \\dfrac{a}{b}$</p>'
+ '<p><b>Теорема синусов</b> (для любого треугольника):</p>'
+ '<p style="text-align:center;margin:6px 0">$\\dfrac{a}{\\sin A} = \\dfrac{b}{\\sin B} = \\dfrac{c}{\\sin C} = 2R$</p>'
+ '<p><b>Теорема косинусов:</b></p>'
+ '<p style="text-align:center;margin:6px 0">$c^2 = a^2 + b^2 - 2ab\\cos C$</p>'
+ '<p><b>Четырёхугольники.</b> Сумма углов выпуклого четырёхугольника равна $360°$. Иерархия: <b>параллелограмм</b> $\\supset$ <b>прямоугольник</b>, <b>ромб</b> $\\supset$ <b>квадрат</b>. У параллелограмма противоположные стороны равны и параллельны, диагонали точкой пересечения делятся пополам.</p>'
+ '<p><b>Трапеция:</b> две параллельные стороны (основания) разной длины. Средняя линия $m = \\dfrac{a+b}{2}$.</p>');
html += makeCard('example', 'Окружность и её свойства', '§ 8.2',
'<p><b>Окружность.</b> Длина окружности: $C = 2\\pi R$. Длина дуги в $\\alpha$ градусов: $\\ell = \\dfrac{\\pi R \\alpha}{180°}$.</p>'
+ '<p><b>Круг:</b> площадь $S = \\pi R^2$. Площадь сектора с центральным углом $\\alpha$: $S_{сект} = \\dfrac{\\pi R^2 \\alpha}{360°}$.</p>'
+ '<p><b>Вписанный угол.</b> Угол, вершина которого лежит на окружности, а стороны — хорды, равен <i>половине</i> центрального угла, опирающегося на ту же дугу. Все вписанные углы, опирающиеся на один диаметр, прямые (теорема Фалеса).</p>'
+ '<p><b>Касательная.</b> Прямая, имеющая с окружностью ровно одну общую точку. <i>Касательная перпендикулярна радиусу, проведённому в точку касания.</i> Отрезки касательных из одной внешней точки равны.</p>'
+ '<p><b>Вписанная и описанная окружности треугольника:</b></p>'
+ '<p style="text-align:center;margin:6px 0">$r = \\dfrac{S}{p},\\qquad R = \\dfrac{abc}{4S}$</p>'
+ '<p>где $p = \\dfrac{a+b+c}{2}$ — полупериметр, $S$ — площадь треугольника.</p>'
+ '<details class="spoiler"><summary>Пример: треугольник $a=3$, $b=4$, $c=5$</summary><div class="spoiler-body">'
+ '<p>Прямоугольный (так как $3^2+4^2=5^2$). $S = \\frac{1}{2}\\cdot 3 \\cdot 4 = 6$. $p = 6$.</p>'
+ '<p>$r = S/p = 6/6 = 1$. $R = (3\\cdot 4\\cdot 5)/(4\\cdot 6) = 60/24 = 2{,}5$ (= $c/2$ — гипотенуза диаметр).</p>'
+ '</div></details>');
/* === ИНТЕРАКТИВ 1 — Квикфайр === */
html += '<div class="wg" id="p8-iv1">'
+ '<div class="wg-header"><span class="wg-badge">квикфайр · 8 вопросов</span><div class="wg-title">Какая теорема нужна?</div></div>'
+ '<div class="wg-help">Для каждой ситуации выбери теорему: Пифагор, синусов или косинусов. После 8 верных — +10 XP.</div>'
+ '<div id="p8-iv1-area"></div>'
+ '<div class="score-display" style="margin-top:10px">Верно: <b id="p8-iv1-score">0</b> / 8</div>'
+ '</div>';
/* === ИНТЕРАКТИВ 2 — DnD сортер === */
html += '<div class="wg" id="p8-iv2">'
+ '<div class="wg-header"><span class="wg-badge">сортер</span><div class="wg-title">Тип четырёхугольника</div></div>'
+ '<div class="wg-help">Перетащи описание в нужный ящик. После всех верных — +15 XP.</div>'
+ '<div class="dnd-hint"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/></svg>6 описаний → 4 типа</div>'
+ '<div id="p8-iv2-pool" class="dnd-pool"></div>'
+ '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px">'
+ '<div class="drop-box"><h5>Квадрат</h5><div class="drop-items" data-cat="square"></div></div>'
+ '<div class="drop-box"><h5>Ромб</h5><div class="drop-items" data-cat="rhomb"></div></div>'
+ '<div class="drop-box"><h5>Прям-к / Парал-м</h5><div class="drop-items" data-cat="rect"></div></div>'
+ '<div class="drop-box"><h5>Трапеция</h5><div class="drop-items" data-cat="trap"></div></div>'
+ '</div>'
+ '<div class="actions"><button class="btn primary" id="p8-iv2-check">Проверить</button><button class="btn" id="p8-iv2-reset">Сброс</button></div>'
+ '<div class="feedback" id="p8-iv2-fb"></div>'
+ '</div>';
/* === ИНТЕРАКТИВ 3 — Тренажёр повторения === */
html += '<div class="wg" id="p8-iv3">'
+ '<div class="wg-header"><span class="wg-badge">тренажёр · 5 задач</span><div class="wg-title">Повторение планиметрии</div></div>'
+ '<div class="wg-help">Введи числовой ответ. Допуск $\\pm 0{,}05$. Для $\\pi$ используй $3{,}14$.</div>'
+ '<div id="p8-iv3-list"></div>'
+ '<div class="score-display" style="margin-top:10px">Решено: <b id="p8-iv3-score">0</b> / 5</div>'
+ '</div>';
html += secNavFor('p8');
html += readButton('p8');
box.innerHTML = html;
renderMath(box);
/* IV1 — Квикфайр */
(function(){
const tasks = [
{ q:'В прямоугольном треугольнике даны <b>оба катета</b>. Найти гипотенузу.', a:'pyth' },
{ q:'В произвольном треугольнике даны <b>две стороны и угол между ними</b>. Найти третью сторону.', a:'cos' },
{ q:'Дана сторона и <b>противолежащий ей угол</b>. Найти радиус описанной окружности.', a:'sin' },
{ q:'В треугольнике даны <b>все три стороны</b>. Найти один из углов.', a:'cos' },
{ q:'Даны <b>два угла и сторона</b>. Найти другую сторону.', a:'sin' },
{ q:'В прямоугольном треугольнике даны <b>катет и гипотенуза</b>. Найти второй катет.', a:'pyth' },
{ q:'Даны <b>две стороны и угол между ними</b>. Найти третью сторону.', a:'cos' },
{ q:'Сторона и противолежащий угол в произвольном треугольнике. Найти радиус $R$.', a:'sin' }
];
const LABELS = {pyth:'Пифагор', sin:'Синусов', cos:'Косинусов'};
const area = document.getElementById('p8-iv1-area');
const scoreEl = document.getElementById('p8-iv1-score');
const solved = new Set();
let xpGiven = false;
area.innerHTML = tasks.map(function(t, i){
return '<div style="background:var(--card);border:1px solid var(--border);border-radius:9px;padding:10px 12px;margin-bottom:8px" id="p8-iv1-q-'+i+'">'
+ '<div style="margin-bottom:6px"><b>Вопрос '+(i+1)+'.</b> '+t.q+'</div>'
+ '<div style="display:flex;gap:6px;flex-wrap:wrap">'
+ '<button class="btn" data-i="'+i+'" data-v="pyth">Пифагор</button>'
+ '<button class="btn" data-i="'+i+'" data-v="sin">Синусов</button>'
+ '<button class="btn" data-i="'+i+'" data-v="cos">Косинусов</button>'
+ '</div>'
+ '<div class="feedback" id="p8-iv1-fb-'+i+'"></div>'
+ '</div>';
}).join('');
renderMath(area);
area.querySelectorAll('button[data-i]').forEach(function(b){
b.addEventListener('click', function(){
const i = +b.dataset.i, v = b.dataset.v, t = tasks[i];
const fb = document.getElementById('p8-iv1-fb-'+i);
if(v === t.a){
feedback(fb, true, '&#10003; Верно — теорема '+LABELS[t.a]+'!');
if(!solved.has(i)){
solved.add(i);
scoreEl.textContent = solved.size;
if(solved.size === tasks.length && !xpGiven){
xpGiven = true;
addXp(10, 'p8-iv1');
bumpProgress('p8', 20);
}
}
} else {
feedback(fb, false, '&#10007; Неверно. Подумай: что дано — прямоуг. $\\triangle$? Угол между двумя сторонами? Сторона + противолежащий угол?');
}
});
});
})();
/* IV2 — DnD сортер */
(function(){
const items = [
{ id:'q1', html:'4 стороны равны, все углы прямые', cat:'square' },
{ id:'q2', html:'4 стороны равны, углы не обязательно прямые', cat:'rhomb' },
{ id:'q3', html:'Противоположные стороны равны и параллельны', cat:'rect' },
{ id:'q4', html:'2 параллельные стороны (основания) разной длины', cat:'trap' },
{ id:'q5', html:'4 угла по $90°$, противоположные стороны равны', cat:'rect' },
{ id:'q6', html:'Все 4 угла $90°$ и все стороны равны', cat:'square' }
];
const sorter = setupSorter({
poolId:'p8-iv2-pool',
scopeSelector:'#p8-iv2',
items: items,
cats: ['square','rhomb','rect','trap']
});
let xpGiven = false;
document.getElementById('p8-iv2-check').addEventListener('click', function(){
const fb = document.getElementById('p8-iv2-fb');
let correct = 0;
items.forEach(function(it){ if(sorter.placed[it.id] === it.cat) correct++; });
if(correct === items.length){
feedback(fb, true, '&#10003; Все 6 правильно! +15 XP');
if(!xpGiven){
xpGiven = true;
addXp(15, 'p8-iv2');
bumpProgress('p8', 30);
}
} else {
feedback(fb, false, '&#10007; Правильно: '+correct+' из '+items.length+'. Попробуй ещё раз.');
}
});
document.getElementById('p8-iv2-reset').addEventListener('click', function(){
sorter.reset();
const fb = document.getElementById('p8-iv2-fb'); fb.style.display = 'none';
});
})();
/* IV3 — Тренажёр повторения */
(function(){
const tasks = [
{ q:'Треугольник со сторонами 3, 4, 5. Какой угол (в градусах) лежит против стороны 5?', a:90, tol:0.05 },
{ q:'В прямоугольном треугольнике катеты 6 и 8. Найди гипотенузу.', a:10, tol:0.05 },
{ q:'Чему равна сумма углов выпуклого 5-угольника (в градусах)?', a:540, tol:0.05 },
{ q:'В круге $R=6$. Длина окружности ($\\pi\\approx 3{,}14$)?', a:37.68, tol:0.05 },
{ q:'Треугольник со сторонами 7, 8, 9. Найди площадь по формуле Герона (точность 0,01).', a:26.83, tol:0.05 }
];
const list = document.getElementById('p8-iv3-list');
const scoreEl = document.getElementById('p8-iv3-score');
const solved = new Set();
let xpGiven = false;
list.innerHTML = tasks.map(function(t, i){
return '<div style="background:var(--card);border:1px solid var(--border);border-radius:9px;padding:10px 12px;margin-bottom:8px">'
+ '<div style="margin-bottom:6px"><b>Задача '+(i+1)+'.</b> '+t.q+'</div>'
+ '<div style="display:flex;gap:6px;flex-wrap:wrap;align-items:center">'
+ '<input type="text" class="tinp" id="p8-iv3-inp-'+i+'" placeholder="число" style="width:140px">'
+ '<button class="btn primary" data-i="'+i+'">Проверить</button>'
+ '</div>'
+ '<div class="feedback" id="p8-iv3-fb-'+i+'"></div>'
+ '</div>';
}).join('');
renderMath(list);
list.querySelectorAll('button[data-i]').forEach(function(b){
b.addEventListener('click', function(){
const i = +b.dataset.i, t = tasks[i];
const inp = document.getElementById('p8-iv3-inp-'+i);
const fb = document.getElementById('p8-iv3-fb-'+i);
const raw = (inp.value || '').replace(',', '.').trim();
const val = parseFloat(raw);
if(!isFinite(val)){ feedback(fb, false, '&#10007; Введи число'); return; }
if(Math.abs(val - t.a) <= t.tol){
feedback(fb, true, '&#10003; Верно!');
if(!solved.has(i)){
solved.add(i);
scoreEl.textContent = solved.size;
if(solved.size === tasks.length && !xpGiven){
xpGiven = true;
addXp(15, 'p8-iv3');
bumpProgress('p8', 30);
setTimeout(function(){ achievement('p8_done'); }, 400);
}
}
} else {
feedback(fb, false, '&#10007; Не точно. Пересчитай аккуратно.');
}
});
});
})();
wireReadBtn('p8');
}
/* ===== §9 «Геометрические величины» (площади + объёмы — обзор) ===== */
function buildP9(){
const box = document.getElementById('p9-body');
if(!box) return;
let html = '';
/* === ТЕОРИЯ === */
html += makeCard('theory', 'Площади плоских фигур', '§ 9.1',
'<p><b>Треугольник.</b> Несколько эквивалентных формул:</p>'
+ '<p style="text-align:center;margin:6px 0">$S = \\dfrac{1}{2} a h_a = \\dfrac{1}{2} a b \\sin C = \\sqrt{p(p-a)(p-b)(p-c)} = \\dfrac{abc}{4R} = p r$</p>'
+ '<p>где $p = \\dfrac{a+b+c}{2}$, $R$ — описанная, $r$ — вписанная окружности.</p>'
+ '<p><b>Четырёхугольники:</b></p>'
+ '<ul style="margin:6px 0 10px 22px;line-height:1.7">'
+ '<li>Прямоугольник: $S = a b$.</li>'
+ '<li>Параллелограмм: $S = a h = a b \\sin\\alpha$.</li>'
+ '<li>Ромб: $S = \\dfrac{1}{2} d_1 d_2 = a^2 \\sin\\alpha$.</li>'
+ '<li>Трапеция: $S = \\dfrac{a + b}{2} \\cdot h$ (где $a$, $b$ — основания).</li>'
+ '</ul>'
+ '<p><b>Круг и сектор:</b></p>'
+ '<p style="text-align:center;margin:6px 0">$S_{круг} = \\pi R^2,\\qquad S_{сект} = \\dfrac{\\pi R^2 \\alpha}{360°}$</p>');
html += makeCard('example', 'Объёмы тел в пространстве', '§ 9.2',
'<p>Сводная таблица формул объёмов (повторение §§ 16):</p>'
+ '<ul style="margin:6px 0 10px 22px;line-height:1.7">'
+ '<li><b>Куб</b> со стороной $a$: $V = a^3$.</li>'
+ '<li><b>Прямоугольный параллелепипед</b> $a \\times b \\times c$: $V = a b c$.</li>'
+ '<li><b>Призма</b>: $V = S_{осн} \\cdot h$.</li>'
+ '<li><b>Цилиндр</b>: $V = \\pi R^2 h$.</li>'
+ '<li><b>Пирамида</b>: $V = \\dfrac{1}{3} S_{осн} \\cdot h$.</li>'
+ '<li><b>Конус</b>: $V = \\dfrac{1}{3} \\pi R^2 h$.</li>'
+ '<li><b>Шар</b>: $V = \\dfrac{4}{3} \\pi R^3$, $S_{пов} = 4\\pi R^2$.</li>'
+ '</ul>'
+ '<p><b>Ключевая идея.</b> Объёмы «остроконечных» тел (пирамида, конус) в <b>3 раза меньше</b> объёма «призменных» аналогов с той же основой и высотой: $V_{пир} = \\frac{1}{3} V_{призмы}$, $V_{конус} = \\frac{1}{3} V_{цил}$.</p>'
+ '<details class="spoiler"><summary>Пример сравнения: $R=3$, $h=4$</summary><div class="spoiler-body">'
+ '<ul style="margin:6px 0 0 22px;line-height:1.7">'
+ '<li>Цилиндр: $V = \\pi \\cdot 9 \\cdot 4 = 36\\pi \\approx 113{,}0$.</li>'
+ '<li>Конус с теми же $R$, $h$: $V = \\frac{1}{3}\\cdot 36\\pi = 12\\pi \\approx 37{,}7$ — ровно треть.</li>'
+ '<li>Шар $R=3$: $V = \\frac{4}{3}\\pi \\cdot 27 = 36\\pi \\approx 113{,}0$ — столько же, сколько у цилиндра $R=3$, $h=4$!</li>'
+ '</ul>'
+ '</div></details>');
/* === ИНТЕРАКТИВ 1 — Универсальный калькулятор === */
html += '<div class="wg" id="p9-iv1">'
+ '<div class="wg-header"><span class="wg-badge">калькулятор</span><div class="wg-title">$S$ и $V$ — универсальный</div></div>'
+ '<div class="wg-help">Выбери фигуру, введи параметры. После <b>5 разных фигур</b> — +10 XP.</div>'
+ '<div class="sliders">'
+ '<label>Фигура:<select id="p9-iv1-fig" style="margin-left:8px;padding:5px 8px;border-radius:7px;border:1px solid var(--border);background:var(--card);color:var(--text)">'
+ '<option value="rect">Прямоугольник (a, b)</option>'
+ '<option value="tri">Треугольник (a, b, C°)</option>'
+ '<option value="par">Параллелограмм (a, b, α°)</option>'
+ '<option value="rhomb">Ромб (d₁, d₂)</option>'
+ '<option value="trap">Трапеция (a, b, h)</option>'
+ '<option value="circ">Круг (R)</option>'
+ '<option value="prism">Призма (S осн, h)</option>'
+ '<option value="cyl">Цилиндр (R, h)</option>'
+ '<option value="pyr">Пирамида (S осн, h)</option>'
+ '<option value="cone">Конус (R, h)</option>'
+ '<option value="sph">Шар (R)</option>'
+ '</select></label>'
+ '</div>'
+ '<div id="p9-iv1-fields" style="display:flex;gap:10px;flex-wrap:wrap;margin-top:8px;align-items:center"></div>'
+ '<div style="margin-top:10px"><button class="btn primary" id="p9-iv1-calc">Вычислить</button></div>'
+ '<div id="p9-iv1-out" style="margin-top:10px;padding:10px 14px;background:var(--card);border:1px solid var(--border);border-radius:9px;font-size:.92rem;line-height:1.7;min-height:40px">Выбери фигуру и параметры, затем нажми «Вычислить».</div>'
+ '<div style="font-size:.78rem;color:var(--muted);margin-top:6px">Фигур изучено: <b id="p9-iv1-cnt">0</b> / 5</div>'
+ '</div>';
/* === ИНТЕРАКТИВ 2 — Тренажёр площадей === */
html += '<div class="wg" id="p9-iv2">'
+ '<div class="wg-header"><span class="wg-badge">тренажёр площадей · 6 задач</span><div class="wg-title">Найди площадь $S$</div></div>'
+ '<div class="wg-help">Введи числовой ответ. Допуск $\\pm 0{,}05$. Для $\\pi$ используй $3{,}14$.</div>'
+ '<div id="p9-iv2-list"></div>'
+ '<div class="score-display" style="margin-top:10px">Решено: <b id="p9-iv2-score">0</b> / 6</div>'
+ '</div>';
/* === ИНТЕРАКТИВ 3 — Тренажёр объёмов === */
html += '<div class="wg" id="p9-iv3">'
+ '<div class="wg-header"><span class="wg-badge">тренажёр объёмов · 6 задач</span><div class="wg-title">Найди объём $V$</div></div>'
+ '<div class="wg-help">Введи числовой ответ. Допуск $\\pm 0{,}05$. Для $\\pi$ используй $3{,}14$.</div>'
+ '<div id="p9-iv3-list"></div>'
+ '<div class="score-display" style="margin-top:10px">Решено: <b id="p9-iv3-score">0</b> / 6</div>'
+ '</div>';
html += secNavFor('p9');
html += readButton('p9');
box.innerHTML = html;
renderMath(box);
/* IV1 — Универсальный калькулятор */
(function(){
const PI = 3.14;
/* Спецификация полей: { key, label, defValue } */
const SPEC = {
rect: { name:'Прямоугольник', fields:[['a','$a$',5],['b','$b$',8]], calc:p=>({fx:'S = a\\cdot b = '+p.a+'\\cdot '+p.b+' = '+(p.a*p.b).toFixed(2), val:p.a*p.b, unit:'S'}) },
tri: { name:'Треугольник', fields:[['a','$a$',5],['b','$b$',8],['C','$C°$',30]], calc:p=>{const v=0.5*p.a*p.b*Math.sin(p.C*Math.PI/180); return {fx:'S = \\tfrac{1}{2}ab\\sin C = \\tfrac{1}{2}\\cdot '+p.a+'\\cdot '+p.b+'\\cdot \\sin '+p.C+'° = '+v.toFixed(2), val:v, unit:'S'};} },
par: { name:'Параллелограмм', fields:[['a','$a$',6],['b','$b$',10],['A','$\\alpha°$',30]], calc:p=>{const v=p.a*p.b*Math.sin(p.A*Math.PI/180); return {fx:'S = ab\\sin\\alpha = '+p.a+'\\cdot '+p.b+'\\cdot \\sin '+p.A+'° = '+v.toFixed(2), val:v, unit:'S'};} },
rhomb: { name:'Ромб', fields:[['d1','$d_1$',6],['d2','$d_2$',8]], calc:p=>({fx:'S = \\tfrac{1}{2}d_1 d_2 = \\tfrac{1}{2}\\cdot '+p.d1+'\\cdot '+p.d2+' = '+(0.5*p.d1*p.d2).toFixed(2), val:0.5*p.d1*p.d2, unit:'S'}) },
trap: { name:'Трапеция', fields:[['a','$a$',4],['b','$b$',8],['h','$h$',5]], calc:p=>({fx:'S = \\tfrac{a+b}{2}\\cdot h = \\tfrac{'+p.a+'+'+p.b+'}{2}\\cdot '+p.h+' = '+(0.5*(p.a+p.b)*p.h).toFixed(2), val:0.5*(p.a+p.b)*p.h, unit:'S'}) },
circ: { name:'Круг', fields:[['R','$R$',4]], calc:p=>({fx:'S = \\pi R^2 \\approx 3{,}14\\cdot '+p.R+'^2 = '+(PI*p.R*p.R).toFixed(2), val:PI*p.R*p.R, unit:'S'}) },
prism: { name:'Призма', fields:[['So','$S_{осн}$',12],['h','$h$',5]], calc:p=>({fx:'V = S_{осн}\\cdot h = '+p.So+'\\cdot '+p.h+' = '+(p.So*p.h).toFixed(2), val:p.So*p.h, unit:'V'}) },
cyl: { name:'Цилиндр', fields:[['R','$R$',3],['h','$h$',5]], calc:p=>({fx:'V = \\pi R^2 h \\approx 3{,}14\\cdot '+p.R+'^2\\cdot '+p.h+' = '+(PI*p.R*p.R*p.h).toFixed(2), val:PI*p.R*p.R*p.h, unit:'V'}) },
pyr: { name:'Пирамида', fields:[['So','$S_{осн}$',24],['h','$h$',5]], calc:p=>({fx:'V = \\tfrac{1}{3}S_{осн}h = \\tfrac{1}{3}\\cdot '+p.So+'\\cdot '+p.h+' = '+(p.So*p.h/3).toFixed(2), val:p.So*p.h/3, unit:'V'}) },
cone: { name:'Конус', fields:[['R','$R$',6],['h','$h$',5]], calc:p=>({fx:'V = \\tfrac{1}{3}\\pi R^2 h \\approx \\tfrac{1}{3}\\cdot 3{,}14\\cdot '+p.R+'^2\\cdot '+p.h+' = '+(PI*p.R*p.R*p.h/3).toFixed(2), val:PI*p.R*p.R*p.h/3, unit:'V'}) },
sph: { name:'Шар', fields:[['R','$R$',3]], calc:p=>({fx:'V = \\tfrac{4}{3}\\pi R^3 \\approx \\tfrac{4}{3}\\cdot 3{,}14\\cdot '+p.R+'^3 = '+(4*PI*p.R*p.R*p.R/3).toFixed(2), val:4*PI*p.R*p.R*p.R/3, unit:'V'}) }
};
const sel = document.getElementById('p9-iv1-fig');
const fields = document.getElementById('p9-iv1-fields');
const out = document.getElementById('p9-iv1-out');
const oCnt = document.getElementById('p9-iv1-cnt');
const seen = new Set();
let xpGiven = false;
function renderFields(){
const sp = SPEC[sel.value];
fields.innerHTML = sp.fields.map(function(f){
return '<label style="font-size:.88rem;display:flex;align-items:center;gap:6px">'
+ '<span class="kx">'+f[1]+'</span> = <input type="text" class="tinp" id="p9-iv1-f-'+f[0]+'" value="'+f[2]+'" style="width:80px">'
+ '</label>';
}).join('');
renderMath(fields);
}
sel.addEventListener('change', function(){ renderFields(); out.innerHTML = 'Параметры обновлены. Нажми «Вычислить».'; });
renderFields();
document.getElementById('p9-iv1-calc').addEventListener('click', function(){
const key = sel.value;
const sp = SPEC[key];
const p = {};
let bad = false;
sp.fields.forEach(function(f){
const el = document.getElementById('p9-iv1-f-'+f[0]);
const raw = (el.value || '').replace(',', '.').trim();
const v = parseFloat(raw);
if(!isFinite(v) || v <= 0){ bad = true; }
p[f[0]] = v;
});
if(bad){ out.innerHTML = '<span style="color:var(--bad,#ef4444)">Введи положительные числа во все поля.</span>'; return; }
const r = sp.calc(p);
out.innerHTML = '<p><b>'+sp.name+'</b></p>'
+ '<p>$$ '+r.fx+' $$</p>'
+ '<p><b>Ответ:</b> $'+r.unit+' \\approx '+r.val.toFixed(2)+'$</p>';
renderMath(out);
seen.add(key);
oCnt.textContent = seen.size;
if(seen.size >= 5 && !xpGiven){
xpGiven = true;
addXp(10, 'p9-iv1');
bumpProgress('p9', 20);
}
});
})();
/* IV2 — Тренажёр площадей */
(function(){
const tasks = [
{ q:'Прямоугольник $5 \\times 8$. Найди площадь.', a:40, tol:0.05 },
{ q:'Круг $R=4$. Найди площадь ($\\pi\\approx 3{,}14$).', a:50.24, tol:0.05 },
{ q:'Треугольник со сторонами 5, 12, 13. Найди площадь.', a:30, tol:0.05 },
{ q:'Трапеция: основания 4 и 8, высота 5. Найди площадь.', a:30, tol:0.05 },
{ q:'Параллелограмм со сторонами 6 и 10, угол $30°$. Площадь?', a:30, tol:0.05 },
{ q:'Ромб с диагоналями 6 и 8. Площадь?', a:24, tol:0.05 }
];
const list = document.getElementById('p9-iv2-list');
const scoreEl = document.getElementById('p9-iv2-score');
const solved = new Set();
let xpGiven = false;
list.innerHTML = tasks.map(function(t, i){
return '<div style="background:var(--card);border:1px solid var(--border);border-radius:9px;padding:10px 12px;margin-bottom:8px">'
+ '<div style="margin-bottom:6px"><b>Задача '+(i+1)+'.</b> '+t.q+'</div>'
+ '<div style="display:flex;gap:6px;flex-wrap:wrap;align-items:center">'
+ '<input type="text" class="tinp" id="p9-iv2-inp-'+i+'" placeholder="число" style="width:140px">'
+ '<button class="btn primary" data-i="'+i+'">Проверить</button>'
+ '</div>'
+ '<div class="feedback" id="p9-iv2-fb-'+i+'"></div>'
+ '</div>';
}).join('');
renderMath(list);
list.querySelectorAll('button[data-i]').forEach(function(b){
b.addEventListener('click', function(){
const i = +b.dataset.i, t = tasks[i];
const inp = document.getElementById('p9-iv2-inp-'+i);
const fb = document.getElementById('p9-iv2-fb-'+i);
const raw = (inp.value || '').replace(',', '.').trim();
const val = parseFloat(raw);
if(!isFinite(val)){ feedback(fb, false, '&#10007; Введи число'); return; }
if(Math.abs(val - t.a) <= t.tol){
feedback(fb, true, '&#10003; Верно!');
if(!solved.has(i)){
solved.add(i);
scoreEl.textContent = solved.size;
if(solved.size === tasks.length && !xpGiven){
xpGiven = true;
addXp(15, 'p9-iv2');
bumpProgress('p9', 30);
}
}
} else {
feedback(fb, false, '&#10007; Не точно. Пересчитай аккуратно.');
}
});
});
})();
/* IV3 — Тренажёр объёмов */
(function(){
const tasks = [
{ q:'Куб со стороной 4. Найди $V$.', a:64, tol:0.05 },
{ q:'Прямоугольный параллелепипед $3\\times 4\\times 5$. Найди $V$.', a:60, tol:0.05 },
{ q:'Цилиндр $R=3$, $h=5$. Найди $V$ ($\\pi\\approx 3{,}14$).', a:141.3, tol:0.05 },
{ q:'Пирамида: $S_{осн}=24$, $h=5$. Найди $V$.', a:40, tol:0.05 },
{ q:'Конус $R=6$, $h=5$. Найди $V$ ($\\pi\\approx 3{,}14$).', a:188.4, tol:0.05 },
{ q:'Шар $R=3$. Найди $V$ ($\\pi\\approx 3{,}14$).', a:113.04, tol:0.05 }
];
const list = document.getElementById('p9-iv3-list');
const scoreEl = document.getElementById('p9-iv3-score');
const solved = new Set();
let xpGiven = false;
list.innerHTML = tasks.map(function(t, i){
return '<div style="background:var(--card);border:1px solid var(--border);border-radius:9px;padding:10px 12px;margin-bottom:8px">'
+ '<div style="margin-bottom:6px"><b>Задача '+(i+1)+'.</b> '+t.q+'</div>'
+ '<div style="display:flex;gap:6px;flex-wrap:wrap;align-items:center">'
+ '<input type="text" class="tinp" id="p9-iv3-inp-'+i+'" placeholder="число" style="width:140px">'
+ '<button class="btn primary" data-i="'+i+'">Проверить</button>'
+ '</div>'
+ '<div class="feedback" id="p9-iv3-fb-'+i+'"></div>'
+ '</div>';
}).join('');
renderMath(list);
list.querySelectorAll('button[data-i]').forEach(function(b){
b.addEventListener('click', function(){
const i = +b.dataset.i, t = tasks[i];
const inp = document.getElementById('p9-iv3-inp-'+i);
const fb = document.getElementById('p9-iv3-fb-'+i);
const raw = (inp.value || '').replace(',', '.').trim();
const val = parseFloat(raw);
if(!isFinite(val)){ feedback(fb, false, '&#10007; Введи число'); return; }
if(Math.abs(val - t.a) <= t.tol){
feedback(fb, true, '&#10003; Верно!');
if(!solved.has(i)){
solved.add(i);
scoreEl.textContent = solved.size;
if(solved.size === tasks.length && !xpGiven){
xpGiven = true;
addXp(15, 'p9-iv3');
bumpProgress('p9', 30);
setTimeout(function(){ achievement('p9_done'); }, 400);
}
}
} else {
feedback(fb, false, '&#10007; Не точно. Пересчитай аккуратно.');
}
});
});
})();
wireReadBtn('p9');
}
/* ===== §10 «Координаты и векторы» ===== */
function buildP10(){
const box = document.getElementById('p10-body');
if(!box) return;
let html = '';
/* === ТЕОРИЯ === */
html += makeCard('theory', 'Координаты в пространстве и длина вектора', '§ 10.1',
'<p><b>Декартова система координат.</b> Три взаимно перпендикулярные оси $Ox$, $Oy$, $Oz$ с общим началом $O$. Каждая точка пространства задаётся тройкой координат $(x;\\, y;\\, z)$.</p>'
+ '<p><b>Вектор</b> $\\vec{v}$ в пространстве — упорядоченная тройка координат:</p>'
+ '<p style="text-align:center;margin:6px 0">$\\vec{v} = (x;\\, y;\\, z)$</p>'
+ '<p><b>Длина вектора:</b></p>'
+ '<p style="text-align:center;margin:6px 0">$|\\vec{v}| = \\sqrt{x^2 + y^2 + z^2}$</p>'
+ '<p><b>Расстояние между точками</b> $A(x_1, y_1, z_1)$ и $B(x_2, y_2, z_2)$:</p>'
+ '<p style="text-align:center;margin:6px 0">$|AB| = \\sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2 + (z_2 - z_1)^2}$</p>'
+ '<p><b>Координаты середины отрезка</b> $AB$:</p>'
+ '<p style="text-align:center;margin:6px 0">$M = \\left(\\dfrac{x_1+x_2}{2};\\, \\dfrac{y_1+y_2}{2};\\, \\dfrac{z_1+z_2}{2}\\right)$</p>'
+ '<details class="spoiler"><summary>Пример: $A(1, 2, 3)$, $B(4, 6, 7)$</summary><div class="spoiler-body">'
+ '<p>$|AB| = \\sqrt{(4-1)^2 + (6-2)^2 + (7-3)^2} = \\sqrt{9 + 16 + 16} = \\sqrt{41} \\approx 6{,}40$.</p>'
+ '<p>Середина: $M = \\left(\\dfrac{5}{2};\\, 4;\\, 5\\right) = (2{,}5;\\, 4;\\, 5)$.</p>'
+ '</div></details>');
html += makeCard('rule', 'Скалярное произведение и угол между векторами', '§ 10.2',
'<p><b>Операции с векторами</b> (поэлементно):</p>'
+ '<ul style="margin:6px 0 10px 22px;line-height:1.7">'
+ '<li>Сумма: $\\vec{a} + \\vec{b} = (a_1 + b_1;\\, a_2 + b_2;\\, a_3 + b_3)$.</li>'
+ '<li>Разность: $\\vec{a} - \\vec{b} = (a_1 - b_1;\\, a_2 - b_2;\\, a_3 - b_3)$.</li>'
+ '<li>Умножение на число $k$: $k\\vec{a} = (ka_1;\\, ka_2;\\, ka_3)$.</li>'
+ '</ul>'
+ '<p><b>Скалярное произведение</b> (две эквивалентные формулы):</p>'
+ '<p style="text-align:center;margin:6px 0">$\\vec{a} \\cdot \\vec{b} = a_1 b_1 + a_2 b_2 + a_3 b_3 = |\\vec{a}| \\cdot |\\vec{b}| \\cdot \\cos\\alpha$</p>'
+ '<p><b>Косинус угла</b> между векторами:</p>'
+ '<p style="text-align:center;margin:6px 0">$\\cos\\alpha = \\dfrac{\\vec{a} \\cdot \\vec{b}}{|\\vec{a}| \\cdot |\\vec{b}|}$</p>'
+ '<p><b>Условие перпендикулярности:</b> $\\vec{a} \\perp \\vec{b} \\Leftrightarrow \\vec{a} \\cdot \\vec{b} = 0$.</p>'
+ '<details class="spoiler"><summary>Пример: $\\vec{a} = (1, 2, 2)$, $\\vec{b} = (2, -1, 2)$</summary><div class="spoiler-body">'
+ '<p>$\\vec{a} \\cdot \\vec{b} = 1\\cdot 2 + 2\\cdot(-1) + 2\\cdot 2 = 2 - 2 + 4 = 4$.</p>'
+ '<p>$|\\vec{a}| = \\sqrt{1 + 4 + 4} = 3$, $|\\vec{b}| = \\sqrt{4 + 1 + 4} = 3$.</p>'
+ '<p>$\\cos\\alpha = \\dfrac{4}{3 \\cdot 3} = \\dfrac{4}{9} \\approx 0{,}444$, откуда $\\alpha \\approx 63{,}6°$.</p>'
+ '</div></details>');
/* === ИНТЕРАКТИВ 1 — 3D-визуализатор векторов === */
html += '<div class="wg" id="p10-iv1">'
+ '<div class="wg-header"><span class="wg-badge">3D · визуализатор</span><div class="wg-title">Векторы $\\vec{a}$ и $\\vec{b}$ в пространстве</div></div>'
+ '<div class="wg-help">Перетащи мышью, чтобы вращать сцену. Меняй координаты слайдерами. После <b>4 разных конфигураций</b> — +10 XP.</div>'
+ '<div class="g3d-tools">'
+ '<button class="btn" data-view="iso">Изо</button>'
+ '<button class="btn" data-view="front">Спереди</button>'
+ '<button class="btn" data-view="top">Сверху</button>'
+ '<button class="btn" data-view="side">Сбоку</button>'
+ '</div>'
+ '<div style="background:var(--card);border-radius:9px;padding:10px;text-align:center;margin-top:6px"><svg id="p10-iv1-svg" viewBox="0 0 480 400" width="100%" style="max-width:480px;height:auto;display:block;margin:0 auto"></svg></div>'
+ '<div class="sliders" style="margin-top:10px;display:grid;grid-template-columns:1fr 1fr;gap:14px">'
+ '<div style="background:rgba(234,88,12,0.08);border:1px solid rgba(234,88,12,0.3);border-radius:9px;padding:8px 10px">'
+ '<div style="font-weight:700;color:#ea580c;margin-bottom:6px">$\\vec{a} = (a_1;\\, a_2;\\, a_3)$</div>'
+ '<label style="display:block;font-size:.85rem;margin:3px 0">$a_1$: <input type="range" id="p10-iv1-a1" min="-3" max="3" step="0.5" value="2" style="width:55%;vertical-align:middle"> <span id="p10-iv1-a1-v" class="kx">2</span></label>'
+ '<label style="display:block;font-size:.85rem;margin:3px 0">$a_2$: <input type="range" id="p10-iv1-a2" min="-3" max="3" step="0.5" value="1" style="width:55%;vertical-align:middle"> <span id="p10-iv1-a2-v" class="kx">1</span></label>'
+ '<label style="display:block;font-size:.85rem;margin:3px 0">$a_3$: <input type="range" id="p10-iv1-a3" min="-3" max="3" step="0.5" value="0" style="width:55%;vertical-align:middle"> <span id="p10-iv1-a3-v" class="kx">0</span></label>'
+ '</div>'
+ '<div style="background:rgba(147,51,234,0.08);border:1px solid rgba(147,51,234,0.3);border-radius:9px;padding:8px 10px">'
+ '<div style="font-weight:700;color:#9333ea;margin-bottom:6px">$\\vec{b} = (b_1;\\, b_2;\\, b_3)$</div>'
+ '<label style="display:block;font-size:.85rem;margin:3px 0">$b_1$: <input type="range" id="p10-iv1-b1" min="-3" max="3" step="0.5" value="0" style="width:55%;vertical-align:middle"> <span id="p10-iv1-b1-v" class="kx">0</span></label>'
+ '<label style="display:block;font-size:.85rem;margin:3px 0">$b_2$: <input type="range" id="p10-iv1-b2" min="-3" max="3" step="0.5" value="2" style="width:55%;vertical-align:middle"> <span id="p10-iv1-b2-v" class="kx">2</span></label>'
+ '<label style="display:block;font-size:.85rem;margin:3px 0">$b_3$: <input type="range" id="p10-iv1-b3" min="-3" max="3" step="0.5" value="1.5" style="width:55%;vertical-align:middle"> <span id="p10-iv1-b3-v" class="kx">1.5</span></label>'
+ '</div>'
+ '</div>'
+ '<div id="p10-iv1-out" style="margin-top:10px;padding:10px 14px;background:var(--card);border:1px solid var(--border);border-radius:9px;font-size:.92rem;line-height:1.75"></div>'
+ '<div style="font-size:.78rem;color:var(--muted);margin-top:6px">Изучено конфигураций: <b id="p10-iv1-cnt">0</b> / 4</div>'
+ '</div>';
/* === ИНТЕРАКТИВ 2 — Калькулятор векторов === */
html += '<div class="wg" id="p10-iv2">'
+ '<div class="wg-header"><span class="wg-badge">калькулятор</span><div class="wg-title">Операции над векторами</div></div>'
+ '<div class="wg-help">Введи координаты, выбери операцию. После <b>4 операций</b> — +15 XP.</div>'
+ '<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:8px">'
+ '<div style="background:rgba(234,88,12,0.08);border:1px solid rgba(234,88,12,0.3);border-radius:9px;padding:8px 10px">'
+ '<div style="font-weight:700;color:#ea580c;margin-bottom:6px">$\\vec{a}$</div>'
+ '<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">'
+ '<span class="kx">$a_1$</span>=<input type="text" class="tinp" id="p10-iv2-a1" value="1" style="width:55px">'
+ '<span class="kx">$a_2$</span>=<input type="text" class="tinp" id="p10-iv2-a2" value="2" style="width:55px">'
+ '<span class="kx">$a_3$</span>=<input type="text" class="tinp" id="p10-iv2-a3" value="2" style="width:55px">'
+ '</div>'
+ '</div>'
+ '<div style="background:rgba(147,51,234,0.08);border:1px solid rgba(147,51,234,0.3);border-radius:9px;padding:8px 10px">'
+ '<div style="font-weight:700;color:#9333ea;margin-bottom:6px">$\\vec{b}$</div>'
+ '<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">'
+ '<span class="kx">$b_1$</span>=<input type="text" class="tinp" id="p10-iv2-b1" value="2" style="width:55px">'
+ '<span class="kx">$b_2$</span>=<input type="text" class="tinp" id="p10-iv2-b2" value="-1" style="width:55px">'
+ '<span class="kx">$b_3$</span>=<input type="text" class="tinp" id="p10-iv2-b3" value="2" style="width:55px">'
+ '</div>'
+ '</div>'
+ '</div>'
+ '<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:10px">'
+ '<button class="btn primary" data-op="sum">Сумма $\\vec{a}+\\vec{b}$</button>'
+ '<button class="btn primary" data-op="diff">Разность $\\vec{a}-\\vec{b}$</button>'
+ '<button class="btn primary" data-op="dot">Скалярное произв.</button>'
+ '<button class="btn primary" data-op="cos">Угол ($\\cos\\alpha$, $\\alpha°$)</button>'
+ '<button class="btn primary" data-op="len">Длина $|\\vec{a}|$</button>'
+ '</div>'
+ '<div id="p10-iv2-out" style="margin-top:10px;padding:10px 14px;background:var(--card);border:1px solid var(--border);border-radius:9px;font-size:.92rem;line-height:1.75;min-height:40px">Выбери операцию.</div>'
+ '<div style="font-size:.78rem;color:var(--muted);margin-top:6px">Операций использовано: <b id="p10-iv2-cnt">0</b> / 4</div>'
+ '</div>';
/* === ИНТЕРАКТИВ 3 — Тренажёр векторов === */
html += '<div class="wg" id="p10-iv3">'
+ '<div class="wg-header"><span class="wg-badge">тренажёр · 5 задач</span><div class="wg-title">Векторы и координаты</div></div>'
+ '<div class="wg-help">Введи числовой ответ. Допуск $\\pm 0{,}05$. После всех — +15 XP.</div>'
+ '<div id="p10-iv3-list"></div>'
+ '<div class="score-display" style="margin-top:10px">Решено: <b id="p10-iv3-score">0</b> / 5</div>'
+ '</div>';
html += secNavFor('p10');
html += readButton('p10');
box.innerHTML = html;
renderMath(box);
/* === IV1 — 3D-визуализатор === */
(function(){
const svg = document.getElementById('p10-iv1-svg');
if(!svg) return;
/* Если G3D недоступен — graceful fallback */
if(!window.G3D){
svg.innerHTML = '<text x="240" y="200" text-anchor="middle" fill="#888">G3D engine not loaded</text>';
return;
}
const scene = G3D.createScene({W:480, H:400, scale:38, camDist:9, rotX:-0.42, rotY:0.78});
const AX_COL = { X:'#dc2626', Y:'#10b981', Z:'#2563eb' };
const VA_COL = '#ea580c'; /* оранжевый */
const VB_COL = '#9333ea'; /* фиолетовый */
const AX_LEN = 4;
const GRID_LEN = 4;
let a = { x:2, y:1, z:0 };
let b = { x:0, y:2, z:1.5 };
const seen = new Set();
let xpGiven = false;
/* Проекция 3D-точки на 2D-плоскость SVG */
function P(v, M){
const r = G3D.vApply(M, v);
return G3D.projectPersp(r, scene.camDist, scene.cx, scene.cy, scene.scale);
}
/* Рендер одной координатной оси (двусторонняя линия + стрелка + подпись) */
function renderAxis(M, dir, col, label){
const len = AX_LEN;
const pPos = P({x:dir.x*len, y:dir.y*len, z:dir.z*len}, M);
const pNeg = P({x:-dir.x*len*0.6, y:-dir.y*len*0.6, z:-dir.z*len*0.6}, M);
const pO = P({x:0,y:0,z:0}, M);
if(!pPos || !pNeg || !pO) return '';
let s = '';
/* отрицательная часть — тонко, пунктир */
s += '<line x1="'+pNeg.x.toFixed(1)+'" y1="'+pNeg.y.toFixed(1)+'" x2="'+pO.x.toFixed(1)+'" y2="'+pO.y.toFixed(1)+'" stroke="'+col+'" stroke-width="1" stroke-dasharray="3,3" opacity=".55"/>';
/* положительная часть — толсто */
s += '<line x1="'+pO.x.toFixed(1)+'" y1="'+pO.y.toFixed(1)+'" x2="'+pPos.x.toFixed(1)+'" y2="'+pPos.y.toFixed(1)+'" stroke="'+col+'" stroke-width="2" opacity=".9"/>';
/* стрелка-треугольник на конце положительной части */
s += renderArrowHead(M, {x:0,y:0,z:0}, {x:dir.x*len, y:dir.y*len, z:dir.z*len}, col, 0.18);
/* подпись */
s += '<text x="'+(pPos.x + dir.x*8).toFixed(1)+'" y="'+(pPos.y - dir.y*10 + 4).toFixed(1)+'" fill="'+col+'" font-size="14" font-weight="700">'+label+'</text>';
return s;
}
/* Стрелка-треугольник в конце вектора */
function renderArrowHead(M, tail, head, col, size){
size = size || 0.22;
/* направление в 3D */
const dx = head.x - tail.x, dy = head.y - tail.y, dz = head.z - tail.z;
const L = Math.sqrt(dx*dx + dy*dy + dz*dz) || 1;
const ux = dx/L, uy = dy/L, uz = dz/L;
/* выбираем «вспомогательную» ось, не параллельную направлению */
let helper = (Math.abs(uy) < 0.9) ? {x:0,y:1,z:0} : {x:1,y:0,z:0};
/* perp1 = u x helper */
const px = uy*helper.z - uz*helper.y;
const py = uz*helper.x - ux*helper.z;
const pz = ux*helper.y - uy*helper.x;
const pl = Math.sqrt(px*px+py*py+pz*pz) || 1;
const npx = px/pl, npy = py/pl, npz = pz/pl;
/* основание стрелки = head - u*size */
const baseX = head.x - ux*size, baseY = head.y - uy*size, baseZ = head.z - uz*size;
/* две точки сбоку */
const sx1 = baseX + npx*size*0.55, sy1 = baseY + npy*size*0.55, sz1 = baseZ + npz*size*0.55;
const sx2 = baseX - npx*size*0.55, sy2 = baseY - npy*size*0.55, sz2 = baseZ - npz*size*0.55;
const pHead = P(head, M);
const pS1 = P({x:sx1,y:sy1,z:sz1}, M);
const pS2 = P({x:sx2,y:sy2,z:sz2}, M);
if(!pHead || !pS1 || !pS2) return '';
return '<polygon points="'+pHead.x.toFixed(1)+','+pHead.y.toFixed(1)+' '+pS1.x.toFixed(1)+','+pS1.y.toFixed(1)+' '+pS2.x.toFixed(1)+','+pS2.y.toFixed(1)+'" fill="'+col+'" stroke="'+col+'" stroke-width="1"/>';
}
/* Рендер вектора-стрелки от O до v */
function renderVector(M, v, col, label){
const L = Math.sqrt(v.x*v.x + v.y*v.y + v.z*v.z);
if(L < 0.01) return '';
const pO = P({x:0,y:0,z:0}, M);
const pV = P(v, M);
if(!pO || !pV) return '';
let s = '';
s += '<line x1="'+pO.x.toFixed(1)+'" y1="'+pO.y.toFixed(1)+'" x2="'+pV.x.toFixed(1)+'" y2="'+pV.y.toFixed(1)+'" stroke="'+col+'" stroke-width="3" stroke-linecap="round"/>';
s += renderArrowHead(M, {x:0,y:0,z:0}, v, col, 0.28);
/* подпись чуть в стороне от наконечника */
s += '<text x="'+(pV.x + 10).toFixed(1)+'" y="'+(pV.y - 6).toFixed(1)+'" fill="'+col+'" font-size="13" font-weight="700" font-style="italic">'+label+'</text>';
return s;
}
/* Рендер сетки на плоскости XOZ (пол) */
function renderGrid(M){
let s = '';
const step = 1, N = GRID_LEN;
for(let i = -N; i <= N; i++){
const p1 = P({x:i,y:-0.001,z:-N}, M);
const p2 = P({x:i,y:-0.001,z: N}, M);
const q1 = P({x:-N,y:-0.001,z:i}, M);
const q2 = P({x: N,y:-0.001,z:i}, M);
if(p1 && p2) s += '<line x1="'+p1.x.toFixed(1)+'" y1="'+p1.y.toFixed(1)+'" x2="'+p2.x.toFixed(1)+'" y2="'+p2.y.toFixed(1)+'" stroke="#94a3b8" stroke-width=".5" opacity=".25"/>';
if(q1 && q2) s += '<line x1="'+q1.x.toFixed(1)+'" y1="'+q1.y.toFixed(1)+'" x2="'+q2.x.toFixed(1)+'" y2="'+q2.y.toFixed(1)+'" stroke="#94a3b8" stroke-width=".5" opacity=".25"/>';
}
return s;
}
function draw(){
const M = G3D.buildRotMatrix(scene);
let s = '';
s += renderGrid(M);
s += renderAxis(M, {x:1,y:0,z:0}, AX_COL.X, 'X');
s += renderAxis(M, {x:0,y:1,z:0}, AX_COL.Y, 'Y');
s += renderAxis(M, {x:0,y:0,z:1}, AX_COL.Z, 'Z');
s += renderVector(M, a, VA_COL, 'a⃗');
s += renderVector(M, b, VB_COL, 'b⃗');
svg.innerHTML = s;
}
function updateOut(){
const out = document.getElementById('p10-iv1-out');
const la = Math.sqrt(a.x*a.x + a.y*a.y + a.z*a.z);
const lb = Math.sqrt(b.x*b.x + b.y*b.y + b.z*b.z);
const dot = a.x*b.x + a.y*b.y + a.z*b.z;
const denom = la * lb;
const cosA = denom > 1e-9 ? dot / denom : null;
const ang = cosA !== null ? Math.acos(Math.max(-1, Math.min(1, cosA))) * 180 / Math.PI : null;
const perp = Math.abs(dot) < 1e-6;
let h = '';
h += '<p><b style="color:'+VA_COL+'">$\\vec{a}$</b> = ('+fmt(a.x)+'; '+fmt(a.y)+'; '+fmt(a.z)+'), &nbsp; <b style="color:'+VB_COL+'">$\\vec{b}$</b> = ('+fmt(b.x)+'; '+fmt(b.y)+'; '+fmt(b.z)+')</p>';
h += '<p>$|\\vec{a}| = \\sqrt{'+fmt(a.x*a.x)+'+'+fmt(a.y*a.y)+'+'+fmt(a.z*a.z)+'} \\approx '+la.toFixed(3)+'$, &nbsp; $|\\vec{b}| \\approx '+lb.toFixed(3)+'$</p>';
h += '<p>$\\vec{a} \\cdot \\vec{b} = '+fmt(a.x)+'\\cdot '+fmt(b.x)+' + '+fmt(a.y)+'\\cdot '+fmt(b.y)+' + '+fmt(a.z)+'\\cdot '+fmt(b.z)+' = '+dot.toFixed(3)+'$</p>';
if(cosA !== null){
h += '<p>$\\cos\\alpha = \\dfrac{'+dot.toFixed(3)+'}{'+la.toFixed(3)+'\\cdot '+lb.toFixed(3)+'} \\approx '+cosA.toFixed(3)+'$, &nbsp; $\\alpha \\approx '+ang.toFixed(1)+'°$</p>';
} else {
h += '<p style="color:var(--muted)">Один из векторов нулевой — угол не определён.</p>';
}
if(perp && la > 1e-6 && lb > 1e-6){
h += '<p style="color:#10b981;font-weight:700">$\\vec{a} \\perp \\vec{b}$ &#10003; (скалярное произведение равно 0)</p>';
}
out.innerHTML = h;
renderMath(out);
}
function bumpSeen(){
const key = [a.x,a.y,a.z,b.x,b.y,b.z].map(v=>v.toFixed(1)).join('|');
seen.add(key);
const cnt = document.getElementById('p10-iv1-cnt');
if(cnt) cnt.textContent = Math.min(seen.size, 4);
if(seen.size >= 4 && !xpGiven){
xpGiven = true;
addXp(10, 'p10-iv1');
bumpProgress('p10', 20);
}
}
function bindSlider(id, axis, key){
const el = document.getElementById('p10-iv1-'+id);
const lbl = document.getElementById('p10-iv1-'+id+'-v');
if(!el || !lbl) return;
el.addEventListener('input', function(){
const v = parseFloat(el.value);
lbl.textContent = (v === Math.round(v)) ? String(v) : v.toFixed(1);
if(axis === 'a') a[key] = v; else b[key] = v;
draw(); updateOut(); bumpSeen();
});
}
bindSlider('a1','a','x'); bindSlider('a2','a','y'); bindSlider('a3','a','z');
bindSlider('b1','b','x'); bindSlider('b2','b','y'); bindSlider('b3','b','z');
draw(); updateOut();
G3D.attachOrbit(svg, scene, draw);
document.querySelectorAll('#p10-iv1 .g3d-tools .btn').forEach(function(btn){
btn.addEventListener('click', function(){
G3D.presetView(scene, btn.dataset.view, draw);
});
});
})();
/* === IV2 — Калькулятор векторов === */
(function(){
const out = document.getElementById('p10-iv2-out');
const cntEl = document.getElementById('p10-iv2-cnt');
const used = new Set();
let xpGiven = false;
function rd(id){
const raw = (document.getElementById('p10-iv2-'+id).value || '').replace(',', '.').trim();
return parseFloat(raw);
}
function readVec(p){
return { x:rd(p+'1'), y:rd(p+'2'), z:rd(p+'3') };
}
function vecOk(v){ return isFinite(v.x) && isFinite(v.y) && isFinite(v.z); }
function vecStr(v){ return '('+fmt(v.x)+';\\, '+fmt(v.y)+';\\, '+fmt(v.z)+')'; }
document.querySelectorAll('#p10-iv2 button[data-op]').forEach(function(btn){
btn.addEventListener('click', function(){
const a = readVec('a'), b = readVec('b');
if(!vecOk(a) || !vecOk(b)){
out.innerHTML = '<span style="color:var(--bad,#ef4444)">Введи числовые координаты во все поля.</span>';
return;
}
const op = btn.dataset.op;
let h = '';
if(op === 'sum'){
const r = { x:a.x+b.x, y:a.y+b.y, z:a.z+b.z };
h = '<p>$\\vec{a} + \\vec{b} = ('+fmt(a.x)+'+'+fmt(b.x)+';\\, '+fmt(a.y)+'+'+fmt(b.y)+';\\, '+fmt(a.z)+'+'+fmt(b.z)+') = '+vecStr(r)+'$</p>';
} else if(op === 'diff'){
const r = { x:a.x-b.x, y:a.y-b.y, z:a.z-b.z };
h = '<p>$\\vec{a} - \\vec{b} = ('+fmt(a.x)+'-('+fmt(b.x)+');\\, '+fmt(a.y)+'-('+fmt(b.y)+');\\, '+fmt(a.z)+'-('+fmt(b.z)+')) = '+vecStr(r)+'$</p>';
} else if(op === 'dot'){
const d = a.x*b.x + a.y*b.y + a.z*b.z;
h = '<p>$\\vec{a} \\cdot \\vec{b} = '+fmt(a.x)+'\\cdot '+fmt(b.x)+' + '+fmt(a.y)+'\\cdot '+fmt(b.y)+' + '+fmt(a.z)+'\\cdot '+fmt(b.z)+' = '+d.toFixed(3)+'$</p>';
if(Math.abs(d) < 1e-6) h += '<p style="color:#10b981;font-weight:700">$\\vec{a} \\perp \\vec{b}$ &#10003;</p>';
} else if(op === 'cos'){
const d = a.x*b.x + a.y*b.y + a.z*b.z;
const la = Math.sqrt(a.x*a.x + a.y*a.y + a.z*a.z);
const lb = Math.sqrt(b.x*b.x + b.y*b.y + b.z*b.z);
if(la < 1e-9 || lb < 1e-9){ h = '<p style="color:var(--bad,#ef4444)">Один из векторов нулевой — угол не определён.</p>'; }
else {
const c = d/(la*lb);
const ang = Math.acos(Math.max(-1,Math.min(1,c))) * 180/Math.PI;
h = '<p>$|\\vec{a}| \\approx '+la.toFixed(3)+'$, &nbsp; $|\\vec{b}| \\approx '+lb.toFixed(3)+'$, &nbsp; $\\vec{a}\\cdot\\vec{b} = '+d.toFixed(3)+'$</p>'
+ '<p>$\\cos\\alpha = \\dfrac{'+d.toFixed(3)+'}{'+la.toFixed(3)+'\\cdot '+lb.toFixed(3)+'} \\approx '+c.toFixed(3)+'$, &nbsp; $\\alpha \\approx '+ang.toFixed(2)+'°$</p>';
}
} else if(op === 'len'){
const la = Math.sqrt(a.x*a.x + a.y*a.y + a.z*a.z);
h = '<p>$|\\vec{a}| = \\sqrt{'+fmt(a.x*a.x)+'+'+fmt(a.y*a.y)+'+'+fmt(a.z*a.z)+'} = \\sqrt{'+(a.x*a.x+a.y*a.y+a.z*a.z).toFixed(3)+'} \\approx '+la.toFixed(3)+'$</p>';
}
out.innerHTML = h;
renderMath(out);
used.add(op);
cntEl.textContent = Math.min(used.size, 4);
if(used.size >= 4 && !xpGiven){
xpGiven = true;
addXp(15, 'p10-iv2');
bumpProgress('p10', 30);
}
});
});
})();
/* === IV3 — Тренажёр === */
(function(){
const tasks = [
{ q:'Найди длину вектора $\\vec{a} = (2,\\, 3,\\, 6)$.', a:7, tol:0.05 },
{ q:'Скалярное произведение $\\vec{a} = (1,\\, 2,\\, 3)$ и $\\vec{b} = (4,\\, 5,\\, 6)$.', a:32, tol:0.05 },
{ q:'Расстояние между точками $A(1, 2, 3)$ и $B(4, 6, 7)$.', a:6.40, tol:0.05 },
{ q:'Если $\\vec{a} = (3,\\, 0,\\, 4)$, найди $|\\vec{a}|$.', a:5, tol:0.05 },
{ q:'Найди $\\cos\\alpha$ между $\\vec{a} = (1,\\, 0,\\, 0)$ и $\\vec{b} = (1,\\, 1,\\, 0)$.', a:0.71, tol:0.05 }
];
const list = document.getElementById('p10-iv3-list');
const scoreEl = document.getElementById('p10-iv3-score');
const solved = new Set();
let xpGiven = false;
list.innerHTML = tasks.map(function(t, i){
return '<div style="background:var(--card);border:1px solid var(--border);border-radius:9px;padding:10px 12px;margin-bottom:8px">'
+ '<div style="margin-bottom:6px"><b>Задача '+(i+1)+'.</b> '+t.q+'</div>'
+ '<div style="display:flex;gap:6px;flex-wrap:wrap;align-items:center">'
+ '<input type="text" class="tinp" id="p10-iv3-inp-'+i+'" placeholder="число" style="width:140px">'
+ '<button class="btn primary" data-i="'+i+'">Проверить</button>'
+ '</div>'
+ '<div class="feedback" id="p10-iv3-fb-'+i+'"></div>'
+ '</div>';
}).join('');
renderMath(list);
list.querySelectorAll('button[data-i]').forEach(function(b){
b.addEventListener('click', function(){
const i = +b.dataset.i, t = tasks[i];
const inp = document.getElementById('p10-iv3-inp-'+i);
const fb = document.getElementById('p10-iv3-fb-'+i);
const raw = (inp.value || '').replace(',', '.').trim();
const val = parseFloat(raw);
if(!isFinite(val)){ feedback(fb, false, '&#10007; Введи число'); return; }
if(Math.abs(val - t.a) <= t.tol){
feedback(fb, true, '&#10003; Верно!');
if(!solved.has(i)){
solved.add(i);
scoreEl.textContent = solved.size;
if(solved.size === tasks.length && !xpGiven){
xpGiven = true;
addXp(15, 'p10-iv3');
bumpProgress('p10', 30);
setTimeout(function(){ achievement('p10_done'); }, 400);
}
}
} else {
feedback(fb, false, '&#10007; Не точно. Пересчитай аккуратно.');
}
});
});
})();
wireReadBtn('p10');
}
/* ===== §11 «Геометрические построения» (циркуль и линейка) ===== */
function buildP11(){
const box = document.getElementById('p11-body');
if(!box) return;
let html = '';
/* === ТЕОРИЯ === */
html += makeCard('theory', 'Основные построения циркулем и линейкой', '§ 11.1',
'<p><b>Инструменты:</b> циркуль (для окружностей и переноса длин) и линейка <i>без делений</i> (только проводить прямые через две точки). С их помощью решается удивительно широкий класс задач.</p>'
+ '<p><b>6 базовых построений</b> — основа всего курса:</p>'
+ '<ul style="margin:6px 0 10px 22px;line-height:1.75">'
+ '<li><b>Серединный перпендикуляр к отрезку $AB$.</b> Из $A$ и $B$ радиусом $r > |AB|/2$ проводим окружности — они пересекутся в двух точках $P_1$ (сверху) и $P_2$ (снизу). Прямая $P_1 P_2$ — искомая.</li>'
+ '<li><b>Биссектриса угла $\\angle AOB$.</b> Из вершины $O$ — окружность; точки пересечения с лучами назовём $X$ и $Y$. Из $X$ и $Y$ — окружности равного радиуса; их пересечение $Z$ — внутри угла. Луч $OZ$ — биссектриса.</li>'
+ '<li><b>Перпендикуляр к прямой через точку</b> (на прямой или вне неё) — вариация серединного перпендикуляра.</li>'
+ '<li><b>Параллельная прямая через точку</b> — через построение равных накрест лежащих углов.</li>'
+ '<li><b>Касательная к окружности</b> из внешней точки $P$ — строим окружность с диаметром $OP$; её пересечения с данной окружностью — точки касания.</li>'
+ '<li><b>Описанная окружность</b> треугольника — центр в пересечении серединных перпендикуляров сторон. <b>Вписанная</b> — центр в пересечении биссектрис углов.</li>'
+ '</ul>'
+ '<details class="spoiler"><summary>Почему серединный перпендикуляр работает?</summary><div class="spoiler-body">'
+ '<p>Точки $P_1$ и $P_2$ равноудалены от $A$ и $B$ (по построению $|P_i A| = |P_i B| = r$). Множество точек, равноудалённых от концов отрезка, — это серединный перпендикуляр (это его геометрическое определение). Значит, прямая $P_1 P_2$ и есть искомый перпендикуляр.</p>'
+ '</div></details>');
html += makeCard('rule', 'Алгоритм решения задач на построение', '§ 11.2',
'<p>Любая «настоящая» задача на построение решается по <b>4 этапам</b>:</p>'
+ '<ol style="margin:8px 0 10px 22px;line-height:1.75">'
+ '<li><b>Анализ.</b> Предположим, искомая фигура <i>уже построена</i>. Делаем «эскиз результата», ищем связи между известными и искомыми элементами. Часто отсюда «всплывает» вспомогательное построение (серединный $\\perp$, биссектриса, дуга...).</li>'
+ '<li><b>Построение.</b> Пошагово, циркулем и линейкой. Каждый шаг — точное действие: «Проведём окружность радиуса $r$ с центром в точке $M$», «Проведём прямую через точки $X$ и $Y$».</li>'
+ '<li><b>Доказательство.</b> Показываем, что построенная фигура удовлетворяет всем условиям задачи (опираясь на теоремы планиметрии).</li>'
+ '<li><b>Исследование.</b> При каких условиях задача имеет решение? Сколько решений? Единственно ли оно? Например: «построить треугольник по трём сторонам» возможно только при выполнении неравенства треугольника.</li>'
+ '</ol>'
+ '<p style="font-size:.88rem;color:var(--muted)">В школьной практике этапы 1 и 4 часто опускают, но именно они отличают задачу <i>на построение</i> от простого <i>описания</i> построения.</p>');
/* === ИНТЕРАКТИВ 1 — Анимация серединного перпендикуляра === */
html += '<div class="wg" id="p11-iv1">'
+ '<div class="wg-header"><span class="wg-badge">анимация · 5 шагов</span><div class="wg-title">Построение серединного $\\perp$ к отрезку $AB$</div></div>'
+ '<div class="wg-help">Двигай слайдер шагов — увидишь, как из двух дуг рождается серединный перпендикуляр. Дойди до шага 5 — +10 XP.</div>'
+ '<div style="background:var(--card);border-radius:9px;padding:10px;text-align:center"><svg id="p11-iv1-svg" viewBox="0 0 480 280" width="100%" style="max-width:480px;height:auto;display:block;margin:0 auto"></svg></div>'
+ '<div style="margin-top:10px">'
+ '<label style="display:block;font-size:.9rem;margin-bottom:6px">Шаг построения: <b id="p11-iv1-step-v">1</b> / 5</label>'
+ '<input type="range" id="p11-iv1-step" min="1" max="5" step="1" value="1" style="width:100%">'
+ '</div>'
+ '<div id="p11-iv1-desc" style="margin-top:10px;padding:10px 14px;background:var(--card);border:1px solid var(--border);border-radius:9px;font-size:.92rem;line-height:1.6;min-height:38px"></div>'
+ '</div>';
/* === ИНТЕРАКТИВ 2 — Анимация биссектрисы === */
html += '<div class="wg" id="p11-iv2">'
+ '<div class="wg-header"><span class="wg-badge">анимация · 4 шага</span><div class="wg-title">Построение биссектрисы угла $\\angle AOB$</div></div>'
+ '<div class="wg-help">Двигай слайдер шагов — биссектриса появится из пересечения двух дуг. Дойди до шага 4 — +10 XP.</div>'
+ '<div style="background:var(--card);border-radius:9px;padding:10px;text-align:center"><svg id="p11-iv2-svg" viewBox="0 0 480 280" width="100%" style="max-width:480px;height:auto;display:block;margin:0 auto"></svg></div>'
+ '<div style="margin-top:10px">'
+ '<label style="display:block;font-size:.9rem;margin-bottom:6px">Шаг построения: <b id="p11-iv2-step-v">1</b> / 4</label>'
+ '<input type="range" id="p11-iv2-step" min="1" max="4" step="1" value="1" style="width:100%">'
+ '</div>'
+ '<div id="p11-iv2-desc" style="margin-top:10px;padding:10px 14px;background:var(--card);border:1px solid var(--border);border-radius:9px;font-size:.92rem;line-height:1.6;min-height:38px"></div>'
+ '</div>';
/* === ИНТЕРАКТИВ 3 — Квикфайр «Какое построение?» === */
html += '<div class="wg" id="p11-iv3">'
+ '<div class="wg-header"><span class="wg-badge">квикфайр · 6 вопросов</span><div class="wg-title">Какое базовое построение нужно?</div></div>'
+ '<div class="wg-help">Для каждой ситуации выбери базовое построение. После 6 верных — +15 XP.</div>'
+ '<div id="p11-iv3-area"></div>'
+ '<div class="score-display" style="margin-top:10px">Верно: <b id="p11-iv3-score">0</b> / 6</div>'
+ '</div>';
html += secNavFor('p11');
html += readButton('p11');
box.innerHTML = html;
renderMath(box);
/* === IV1 — анимация серединного перпендикуляра === */
(function(){
const svg = document.getElementById('p11-iv1-svg');
const sl = document.getElementById('p11-iv1-step');
const sv = document.getElementById('p11-iv1-step-v');
const desc = document.getElementById('p11-iv1-desc');
let xpGiven = false, maxStep = 1;
/* координаты в SVG */
const A = {x:130, y:160}, B = {x:350, y:160};
const Mx = (A.x + B.x) / 2;
const r = 130; /* радиус > |AB|/2 = 110 */
/* пересечения двух окружностей */
const dx = (B.x - A.x);
const half = dx / 2;
const hY = Math.sqrt(Math.max(0, r*r - half*half));
const P1 = {x:Mx, y:160 - hY};
const P2 = {x:Mx, y:160 + hY};
const M = {x:Mx, y:160};
const TXT = [
'Шаг 1. Дан отрезок $AB$. Его нужно разделить пополам перпендикуляром.',
'Шаг 2. Проводим окружность радиуса $r > |AB|/2$ с центром в точке $A$.',
'Шаг 3. Тем же радиусом — окружность с центром в точке $B$.',
'Шаг 4. Окружности пересеклись в точках $P_1$ (сверху) и $P_2$ (снизу).',
'Шаг 5. Прямая $P_1 P_2$ — серединный перпендикуляр; она пересекает $AB$ в середине $M$.'
];
function draw(step){
let g = '';
/* фон-сетка */
g += '<rect x="0" y="0" width="480" height="280" fill="#fafafa"/>';
/* отрезок AB всегда */
g += '<line x1="'+A.x+'" y1="'+A.y+'" x2="'+B.x+'" y2="'+B.y+'" stroke="#0f172a" stroke-width="2.2"/>';
g += '<circle cx="'+A.x+'" cy="'+A.y+'" r="4.5" fill="#0f172a"/>';
g += '<circle cx="'+B.x+'" cy="'+B.y+'" r="4.5" fill="#0f172a"/>';
g += '<text x="'+(A.x-14)+'" y="'+(A.y+5)+'" font-family="Inter" font-size="14" font-weight="700" fill="#0f172a">A</text>';
g += '<text x="'+(B.x+8)+'" y="'+(B.y+5)+'" font-family="Inter" font-size="14" font-weight="700" fill="#0f172a">B</text>';
if(step >= 2){
g += '<circle cx="'+A.x+'" cy="'+A.y+'" r="'+r+'" fill="none" stroke="#2563eb" stroke-width="1.6" opacity="0.75"/>';
}
if(step >= 3){
g += '<circle cx="'+B.x+'" cy="'+B.y+'" r="'+r+'" fill="none" stroke="#dc2626" stroke-width="1.6" opacity="0.75"/>';
}
if(step >= 4){
g += '<circle cx="'+P1.x+'" cy="'+P1.y+'" r="5" fill="#10b981" stroke="#fff" stroke-width="2"/>';
g += '<circle cx="'+P2.x+'" cy="'+P2.y+'" r="5" fill="#10b981" stroke="#fff" stroke-width="2"/>';
g += '<text x="'+(P1.x+8)+'" y="'+(P1.y-4)+'" font-family="Inter" font-size="13" font-weight="700" fill="#10b981">P1</text>';
g += '<text x="'+(P2.x+8)+'" y="'+(P2.y+14)+'" font-family="Inter" font-size="13" font-weight="700" fill="#10b981">P2</text>';
}
if(step >= 5){
g += '<line x1="'+P1.x+'" y1="'+(P1.y-12)+'" x2="'+P2.x+'" y2="'+(P2.y+12)+'" stroke="#7c3aed" stroke-width="2.4"/>';
g += '<circle cx="'+M.x+'" cy="'+M.y+'" r="4.5" fill="#7c3aed" stroke="#fff" stroke-width="2"/>';
g += '<text x="'+(M.x+7)+'" y="'+(M.y+20)+'" font-family="Inter" font-size="13" font-weight="700" fill="#7c3aed">M</text>';
/* пометка прямого угла */
g += '<polyline points="'+(M.x+10)+','+M.y+' '+(M.x+10)+','+(M.y-10)+' '+M.x+','+(M.y-10)+'" fill="none" stroke="#7c3aed" stroke-width="1.5"/>';
}
svg.innerHTML = g;
}
function update(){
const s = +sl.value;
sv.textContent = s;
desc.innerHTML = TXT[s-1];
try{ renderMath(desc); }catch(e){}
draw(s);
if(s > maxStep) maxStep = s;
if(maxStep >= 5 && !xpGiven){
xpGiven = true;
addXp(10, 'p11-iv1');
bumpProgress('p11', 25);
}
}
sl.addEventListener('input', update);
update();
})();
/* === IV2 — анимация биссектрисы === */
(function(){
const svg = document.getElementById('p11-iv2-svg');
const sl = document.getElementById('p11-iv2-step');
const sv = document.getElementById('p11-iv2-step-v');
const desc = document.getElementById('p11-iv2-desc');
let xpGiven = false, maxStep = 1;
/* угол с вершиной в O */
const O = {x:120, y:220};
const angA = -10 * Math.PI/180; /* луч OA — почти горизонтальный */
const angB = -55 * Math.PI/180; /* луч OB — выше */
const angBis = (angA + angB) / 2;
const Ld = 320; /* длина лучей */
const Aend = {x:O.x + Ld*Math.cos(angA), y:O.y + Ld*Math.sin(angA)};
const Bend = {x:O.x + Ld*Math.cos(angB), y:O.y + Ld*Math.sin(angB)};
const r1 = 110;
const X = {x:O.x + r1*Math.cos(angA), y:O.y + r1*Math.sin(angA)};
const Y = {x:O.x + r1*Math.cos(angB), y:O.y + r1*Math.sin(angB)};
/* пересечение Z двух одинаковых дуг из X и Y радиуса r2 — на биссектрисе */
const r2 = 90;
/* Z на луче биссектрисы от O */
const Zd = r1*Math.cos((angB-angA)/2) + Math.sqrt(Math.max(0, r2*r2 - (r1*Math.sin((angB-angA)/2))*(r1*Math.sin((angB-angA)/2))));
const Z = {x:O.x + Zd*Math.cos(angBis), y:O.y + Zd*Math.sin(angBis)};
const TXT = [
'Шаг 1. Дан угол $\\angle AOB$ с вершиной в $O$. Нужно построить его биссектрису.',
'Шаг 2. Окружность с центром $O$ — пересекает лучи в точках $X$ (на $OA$) и $Y$ (на $OB$).',
'Шаг 3. Из $X$ и $Y$ — окружности равного радиуса. Они пересекаются в точке $Z$ внутри угла.',
'Шаг 4. Луч $OZ$ — биссектриса; он делит угол $\\angle AOB$ ровно пополам.'
];
function draw(step){
let g = '';
g += '<rect x="0" y="0" width="480" height="280" fill="#fafafa"/>';
/* лучи угла */
g += '<line x1="'+O.x+'" y1="'+O.y+'" x2="'+Aend.x+'" y2="'+Aend.y+'" stroke="#0f172a" stroke-width="2.2"/>';
g += '<line x1="'+O.x+'" y1="'+O.y+'" x2="'+Bend.x+'" y2="'+Bend.y+'" stroke="#0f172a" stroke-width="2.2"/>';
g += '<circle cx="'+O.x+'" cy="'+O.y+'" r="4.5" fill="#0f172a"/>';
g += '<text x="'+(O.x-16)+'" y="'+(O.y+6)+'" font-family="Inter" font-size="14" font-weight="700" fill="#0f172a">O</text>';
g += '<text x="'+(Aend.x-20)+'" y="'+(Aend.y+18)+'" font-family="Inter" font-size="13" font-weight="700" fill="#0f172a">A</text>';
g += '<text x="'+(Bend.x-12)+'" y="'+(Bend.y-6)+'" font-family="Inter" font-size="13" font-weight="700" fill="#0f172a">B</text>';
if(step >= 2){
g += '<circle cx="'+O.x+'" cy="'+O.y+'" r="'+r1+'" fill="none" stroke="#2563eb" stroke-width="1.6" opacity="0.75"/>';
g += '<circle cx="'+X.x+'" cy="'+X.y+'" r="4.5" fill="#2563eb" stroke="#fff" stroke-width="2"/>';
g += '<circle cx="'+Y.x+'" cy="'+Y.y+'" r="4.5" fill="#2563eb" stroke="#fff" stroke-width="2"/>';
g += '<text x="'+(X.x+6)+'" y="'+(X.y+16)+'" font-family="Inter" font-size="12" font-weight="700" fill="#2563eb">X</text>';
g += '<text x="'+(Y.x+6)+'" y="'+(Y.y-6)+'" font-family="Inter" font-size="12" font-weight="700" fill="#2563eb">Y</text>';
}
if(step >= 3){
g += '<circle cx="'+X.x+'" cy="'+X.y+'" r="'+r2+'" fill="none" stroke="#dc2626" stroke-width="1.4" opacity="0.65"/>';
g += '<circle cx="'+Y.x+'" cy="'+Y.y+'" r="'+r2+'" fill="none" stroke="#dc2626" stroke-width="1.4" opacity="0.65"/>';
g += '<circle cx="'+Z.x+'" cy="'+Z.y+'" r="5" fill="#10b981" stroke="#fff" stroke-width="2"/>';
g += '<text x="'+(Z.x+7)+'" y="'+(Z.y-4)+'" font-family="Inter" font-size="13" font-weight="700" fill="#10b981">Z</text>';
}
if(step >= 4){
const Bend2 = {x:O.x + Ld*Math.cos(angBis), y:O.y + Ld*Math.sin(angBis)};
g += '<line x1="'+O.x+'" y1="'+O.y+'" x2="'+Bend2.x+'" y2="'+Bend2.y+'" stroke="#7c3aed" stroke-width="2.6"/>';
/* стрелка */
const tipx = Bend2.x, tipy = Bend2.y;
const back = 14;
const bx = O.x + (Ld-back)*Math.cos(angBis), by = O.y + (Ld-back)*Math.sin(angBis);
const nx = -Math.sin(angBis), ny = Math.cos(angBis);
g += '<polygon points="'+tipx+','+tipy+' '+(bx + 6*nx)+','+(by + 6*ny)+' '+(bx - 6*nx)+','+(by - 6*ny)+'" fill="#7c3aed"/>';
}
svg.innerHTML = g;
}
function update(){
const s = +sl.value;
sv.textContent = s;
desc.innerHTML = TXT[s-1];
try{ renderMath(desc); }catch(e){}
draw(s);
if(s > maxStep) maxStep = s;
if(maxStep >= 4 && !xpGiven){
xpGiven = true;
addXp(10, 'p11-iv2');
bumpProgress('p11', 25);
}
}
sl.addEventListener('input', update);
update();
})();
/* === IV3 — квикфайр === */
(function(){
const tasks = [
{ q:'Нужно разделить отрезок $AB$ пополам.', a:'midperp' },
{ q:'Нужно разделить угол $\\angle AOB$ пополам.', a:'bisect' },
{ q:'Из внешней точки $P$ провести прямую, касающуюся данной окружности.', a:'tangent' },
{ q:'Через точку $P$ провести прямую, параллельную данной прямой $l$.', a:'parallel' },
{ q:'Найти центр окружности, <b>описанной</b> около треугольника $ABC$.', a:'midperp' },
{ q:'Найти центр окружности, <b>вписанной</b> в треугольник $ABC$.', a:'bisect' }
];
const LABELS = {midperp:'Серединный $\\perp$', bisect:'Биссектриса', tangent:'Касательная', parallel:'Параллельная'};
const HINTS = {
midperp:'центр описанной окружности лежит в точке пересечения серединных перпендикуляров сторон',
bisect:'центр вписанной окружности — в точке пересечения биссектрис углов',
tangent:'строится через окружность с диаметром на отрезке от центра до внешней точки',
parallel:'строится через копирование угла или равное расстояние'
};
const area = document.getElementById('p11-iv3-area');
const scoreEl = document.getElementById('p11-iv3-score');
const solved = new Set();
let xpGiven = false;
area.innerHTML = tasks.map(function(t, i){
return '<div style="background:var(--card);border:1px solid var(--border);border-radius:9px;padding:10px 12px;margin-bottom:8px" id="p11-iv3-q-'+i+'">'
+ '<div style="margin-bottom:6px"><b>Вопрос '+(i+1)+'.</b> '+t.q+'</div>'
+ '<div style="display:flex;gap:6px;flex-wrap:wrap">'
+ '<button class="btn" data-i="'+i+'" data-v="midperp">Серединный $\\perp$</button>'
+ '<button class="btn" data-i="'+i+'" data-v="bisect">Биссектриса</button>'
+ '<button class="btn" data-i="'+i+'" data-v="tangent">Касательная</button>'
+ '<button class="btn" data-i="'+i+'" data-v="parallel">Параллельная</button>'
+ '</div>'
+ '<div class="feedback" id="p11-iv3-fb-'+i+'"></div>'
+ '</div>';
}).join('');
renderMath(area);
area.querySelectorAll('button[data-i]').forEach(function(b){
b.addEventListener('click', function(){
const i = +b.dataset.i, v = b.dataset.v, t = tasks[i];
const fb = document.getElementById('p11-iv3-fb-'+i);
if(v === t.a){
feedback(fb, true, '&#10003; Верно — '+LABELS[t.a]+'!');
if(!solved.has(i)){
solved.add(i);
scoreEl.textContent = solved.size;
if(solved.size === tasks.length && !xpGiven){
xpGiven = true;
addXp(15, 'p11-iv3');
bumpProgress('p11', 30);
setTimeout(function(){ achievement('p11_done'); }, 400);
}
}
} else {
feedback(fb, false, '&#10007; Не то. Подумай: '+HINTS[t.a]+'.');
}
});
});
})();
wireReadBtn('p11');
}
/* ===== Финал Раздела 4 «Повторение» ===== */
function buildFinal4(){
const box = document.getElementById('final4-body');
if(!box) return;
let html = '';
/* === Часть А — Шпаргалка раздела 4 (4 mini-карточки) === */
html += '<div class="card">'
+ '<div class="card-header">'
+ '<div class="card-icon theory">' + ICONS.theory + '</div>'
+ '<div class="card-title">Шпаргалка раздела 4</div>'
+ '<div class="card-num">Итог</div>'
+ '</div>'
+ '<div class="card-body">'
+ '<p>Ключевые формулы и идеи четырёх параграфов раздела — пробеги глазами перед битвой с боссами.</p>'
+ '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:12px;margin-top:10px">'
/* §8 Планиметрия */
+ '<div style="padding:12px 14px;background:var(--sec-acc-soft);border-radius:11px;border-left:3px solid var(--pri)">'
+ '<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">'
+ '<svg viewBox="0 0 24 24" fill="none" stroke="#2563eb" stroke-width="2" style="width:18px;height:18px"><polygon points="12,3 22,21 2,21"/></svg>'
+ '<div style="font-family:\'Unbounded\',sans-serif;font-weight:700;color:var(--pri2);font-size:.92rem">§ 8 · Планиметрия</div>'
+ '</div>'
+ '<div style="font-size:.94rem;line-height:1.55">Пифагор $a^2+b^2=c^2$. Синусов $\\dfrac{a}{\\sin A}=2R$. Косинусов $a^2=b^2+c^2-2bc\\cos A$. Замечательные точки: $G$ (медианы), $H$ (высоты), $O$ (серединные $\\perp$), $I$ (биссектрисы). Параллелограмм, ромб, трапеция.</div>'
+ '</div>'
/* §9 Площади и объёмы */
+ '<div style="padding:12px 14px;background:var(--sec-acc-soft);border-radius:11px;border-left:3px solid var(--pri)">'
+ '<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">'
+ '<svg viewBox="0 0 24 24" fill="none" stroke="#dc2626" stroke-width="2" style="width:18px;height:18px"><rect x="3" y="9" width="18" height="12" rx="1"/><polyline points="3,9 12,3 21,9"/></svg>'
+ '<div style="font-family:\'Unbounded\',sans-serif;font-weight:700;color:var(--pri2);font-size:.92rem">§ 9 · Площади и объёмы</div>'
+ '</div>'
+ '<div style="font-size:.94rem;line-height:1.55">$S_\\triangle=\\tfrac{1}{2}ab\\sin C$, Герон $S=\\sqrt{p(p-a)(p-b)(p-c)}$. Круг $S=\\pi R^2$. Призма $V=S_{осн}h$, цилиндр $V=\\pi R^2 h$. Пирамида $V=\\tfrac{1}{3}S_{осн}h$. Шар $V=\\tfrac{4}{3}\\pi R^3$, $S=4\\pi R^2$.</div>'
+ '</div>'
/* §10 Векторы 3D */
+ '<div style="padding:12px 14px;background:var(--sec-acc-soft);border-radius:11px;border-left:3px solid var(--pri)">'
+ '<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">'
+ '<svg viewBox="0 0 24 24" fill="none" stroke="#7c3aed" stroke-width="2" style="width:18px;height:18px"><line x1="3" y1="21" x2="21" y2="3"/><polyline points="14,3 21,3 21,10"/></svg>'
+ '<div style="font-family:\'Unbounded\',sans-serif;font-weight:700;color:var(--pri2);font-size:.92rem">§ 10 · Векторы 3D</div>'
+ '</div>'
+ '<div style="font-size:.94rem;line-height:1.55">$|\\vec{v}|=\\sqrt{x^2+y^2+z^2}$. $\\vec{a}\\cdot\\vec{b}=a_1 b_1 + a_2 b_2 + a_3 b_3$. $\\cos\\alpha=\\dfrac{\\vec{a}\\cdot\\vec{b}}{|\\vec{a}||\\vec{b}|}$. Перпендикулярность: $\\vec{a}\\cdot\\vec{b}=0$.</div>'
+ '</div>'
/* §11 Построения */
+ '<div style="padding:12px 14px;background:var(--sec-acc-soft);border-radius:11px;border-left:3px solid var(--pri)">'
+ '<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">'
+ '<svg viewBox="0 0 24 24" fill="none" stroke="#0891b2" stroke-width="2" style="width:18px;height:18px"><circle cx="12" cy="12" r="9"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="12" y1="3" x2="12" y2="21"/></svg>'
+ '<div style="font-family:\'Unbounded\',sans-serif;font-weight:700;color:var(--pri2);font-size:.92rem">§ 11 · Построения</div>'
+ '</div>'
+ '<div style="font-size:.94rem;line-height:1.55">6 базовых: серединный $\\perp$, биссектриса, перпендикуляр, параллельная, касательная, описанная / вписанная окружность. Алгоритм: <b>анализ → построение → доказательство → исследование</b>.</div>'
+ '</div>'
+ '</div>'
+ '</div>'
+ '</div>';
/* === Часть Б — анонс 5 боссов === */
html += '<div class="card">'
+ '<div class="card-header">'
+ '<div class="card-icon rule">' + ICONS.rule + '</div>'
+ '<div class="card-title">Боссы раздела 4</div>'
+ '<div class="card-num">5</div>'
+ '</div>'
+ '<div class="card-body">'
+ '<p>5 интегрированных задач — каждая опирается на темы § 8, § 9, § 10 или их синтез. За каждого побеждённого босса: <b>+10 XP, +18% к прогрессу</b>. Победишь всех — ачивка <b>«Магистр повторения»</b> и <b>+50 XP бонус</b>.</p>'
+ '<p style="font-size:.88rem;color:var(--muted);margin-top:6px">Для расчётов с $\\pi$ используй $\\pi\\approx 3{,}14$. Допуск — $\\pm 0{,}05$ (для больших чисел — $\\pm 0{,}5$).</p>'
+ '</div>'
+ '</div>';
html += '<div id="r4-bosses-container"></div>';
/* Прогресс-итог + ачивка */
html += '<div style="margin-top:18px;padding:18px 20px;background:linear-gradient(135deg,var(--pri-soft),var(--sec-acc-soft));border-radius:14px;border:1.5px solid var(--pri);text-align:center" id="r4-final-summary">'
+ '<div style="font-family:\'Unbounded\',sans-serif;font-weight:800;color:var(--pri2);font-size:1.1rem;margin-bottom:6px">Прогресс по боссам</div>'
+ '<div id="r4-boss-overall" style="font-size:.95rem;color:var(--text);margin-bottom:10px">0 / 5 боссов побеждено</div>'
+ '<div style="height:12px;background:var(--card);border-radius:8px;overflow:hidden;border:1px solid var(--border)">'
+ '<div id="r4-boss-overall-fill" style="height:100%;width:0%;background:linear-gradient(90deg,#e11d48,#fb7185)"></div>'
+ '</div>'
+ '<div id="r4-final-reward" style="margin-top:14px;display:none;padding:14px;background:var(--card);border-radius:11px;border:2px solid #f59e0b">'
+ '<div style="font-family:\'Unbounded\',sans-serif;font-weight:800;color:#92400e;font-size:1.1rem;margin-bottom:6px">Геометрия 11 пройдена!</div>'
+ '<div style="font-size:.92rem;margin-bottom:10px">Все 4 раздела изучены. Готовы к финалу курса! +50 XP бонус, ачивка «Магистр повторения».</div>'
+ '<a class="btn primary" href="/textbook/geometry-11" style="text-decoration:none">К хабу Геометрии 11 <svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></a>'
+ '</div>'
+ '</div>';
html += secNav('p11', null);
box.innerHTML = html;
renderMath(box);
/* === Боссы === */
const BOSSES = [
{
n:1, color:'#2563eb',
title:'Циклоп Планиметрии',
tag:'§ 8',
q:'В треугольнике стороны равны 6, 8 и 10. Найдите наибольший угол (в градусах).',
ans:90, tol:0.05,
hint:'$6^2+8^2=36+64=100=10^2$ — треугольник прямоугольный (теорема, обратная Пифагору). Наибольший угол — против гипотенузы $= 90°$.'
},
{
n:2, color:'#dc2626',
title:'Минотавр Объёмов',
tag:'§ 9',
q:'Шар с радиусом $R=3$. Найдите объём $V$ (используй $\\pi\\approx 3{,}14$).',
ans:113.04, tol:0.5,
hint:'$V=\\tfrac{4}{3}\\pi R^3=\\tfrac{4}{3}\\cdot 3{,}14\\cdot 27 \\approx 113{,}04$.'
},
{
n:3, color:'#7c3aed',
title:'Гарпия Векторов',
tag:'§ 10',
q:'Найдите скалярное произведение $\\vec{a}=(2;\\, -1;\\, 3)$ и $\\vec{b}=(1;\\, 4;\\, 2)$.',
ans:4, tol:0.05,
hint:'$\\vec{a}\\cdot\\vec{b}=2\\cdot 1 + (-1)\\cdot 4 + 3\\cdot 2 = 2-4+6 = 4$.'
},
{
n:4, color:'#0891b2',
title:'Дракон Угла',
tag:'§ 10',
q:'Векторы $\\vec{a}=(1;\\, 0;\\, 1)$ и $\\vec{b}=(0;\\, 1;\\, 0)$. Найдите угол между ними (в градусах).',
ans:90, tol:0.05,
hint:'$\\vec{a}\\cdot\\vec{b}=1\\cdot 0+0\\cdot 1+1\\cdot 0 = 0$, значит $\\vec{a}\\perp\\vec{b}$ — угол $= 90°$.'
},
{
n:5, color:'#f59e0b',
title:'Мастер Геометрии',
tag:'синтез § 9 + § 10',
q:'Прямоугольный параллелепипед со сторонами $3$, $4$, $12$. Найдите длину его главной диагонали.',
ans:13, tol:0.05,
hint:'$d=\\sqrt{3^2+4^2+12^2}=\\sqrt{9+16+144}=\\sqrt{169}=13$.'
}
];
const cont = document.getElementById('r4-bosses-container');
const STATE_KEY = 'geometry11_ch4_bosses';
const BOSS_STATE = (function(){
try{ const s = localStorage.getItem(STATE_KEY); if(s){ const p = JSON.parse(s); if(Array.isArray(p) && p.length === BOSSES.length) return p; } }catch(e){}
return BOSSES.map(()=>({defeated:false}));
})();
function saveBosses(){ try{ localStorage.setItem(STATE_KEY, JSON.stringify(BOSS_STATE)); }catch(e){} }
cont.innerHTML = BOSSES.map(b=>{
return '<div class="boss-card" id="boss4-'+b.n+'-card" style="padding:16px;background:var(--card);border-radius:12px;border:2px solid '+b.color+';margin-bottom:14px">'
+ '<div style="display:flex;align-items:center;gap:10px;margin-bottom:10px;flex-wrap:wrap">'
+ '<svg viewBox="0 0 24 24" fill="none" stroke="'+b.color+'" stroke-width="2.2" style="width:28px;height:28px;flex-shrink:0"><polygon points="12,2 22,20 2,20"/><circle cx="12" cy="14" r="3"/></svg>'
+ '<div style="font-family:\'Unbounded\',sans-serif;font-weight:800;color:'+b.color+';font-size:1.05rem">Босс '+b.n+': '+b.title+'</div>'
+ '<div style="margin-left:auto;font-size:.78rem;color:var(--muted);padding:3px 8px;background:var(--sec-acc-soft);border-radius:6px">'+b.tag+'</div>'
+ '</div>'
+ '<div class="boss-q" id="boss4-'+b.n+'-q" style="padding:12px 14px;background:var(--sec-acc-soft);border-radius:9px;font-size:1rem;line-height:1.5;margin-bottom:10px">'+b.q+'</div>'
+ '<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">'
+ '<span style="font-family:\'JetBrains Mono\',monospace;font-size:.92rem">ответ =</span>'
+ '<input type="number" id="boss4-'+b.n+'-ans" class="tinp" style="width:140px;text-align:center" step="0.01" placeholder="число">'
+ '<button class="btn primary" id="boss4-'+b.n+'-go" style="background:'+b.color+';border-color:'+b.color+'">Атаковать</button>'
+ '<button class="btn" id="boss4-'+b.n+'-hint">Подсказка</button>'
+ '</div>'
+ '<div class="feedback" id="boss4-'+b.n+'-fb"></div>'
+ '</div>';
}).join('');
renderMath(cont);
function refreshOverall(){
const won = BOSS_STATE.filter(s => s.defeated).length;
const txt = document.getElementById('r4-boss-overall');
const fill = document.getElementById('r4-boss-overall-fill');
if(txt) txt.textContent = won + ' / ' + BOSSES.length + ' боссов побеждено';
if(fill) fill.style.width = (won * 100 / BOSSES.length) + '%';
if(won >= BOSSES.length){
const reward = document.getElementById('r4-final-reward');
if(reward && reward.style.display === 'none'){
reward.style.display = 'block';
if(!STATE.achievements.has('r4_done')){
achievement('r4_done','Магистр повторения');
addXp(50, 'r4-bonus');
bumpProgress('final4', 30);
if(window.confetti){ try{ confetti(); }catch(e){} }
}
}
}
}
BOSSES.forEach((b, idx)=>{
const card = document.getElementById('boss4-'+b.n+'-card');
const goBtn = document.getElementById('boss4-'+b.n+'-go');
const hintBtn= document.getElementById('boss4-'+b.n+'-hint');
const ansInp = document.getElementById('boss4-'+b.n+'-ans');
if(BOSS_STATE[idx].defeated){
card.style.background = 'linear-gradient(135deg,var(--sec-acc-soft),var(--pri-soft))';
card.classList.add('glow');
goBtn.disabled = true; goBtn.style.opacity = .55; goBtn.innerHTML = '&#10003; Повержен';
ansInp.disabled = true;
}
goBtn.addEventListener('click', ()=>{
if(BOSS_STATE[idx].defeated) return;
const fb = document.getElementById('boss4-'+b.n+'-fb');
const raw = (ansInp.value||'').replace(',', '.').trim();
const val = parseFloat(raw);
if(!isFinite(val)){ feedback(fb, false, '&#10007; Введи число.'); return; }
if(Math.abs(val - b.ans) <= b.tol){
BOSS_STATE[idx].defeated = true; saveBosses();
feedback(fb, true, '&#10003; Босс '+b.n+' повержен! +10 XP. '+b.hint);
addXp(10, 'boss-r4-'+b.n);
bumpProgress('final4', 18);
goBtn.disabled = true; goBtn.style.opacity = .55; goBtn.innerHTML = '&#10003; Повержен';
ansInp.disabled = true;
card.style.background = 'linear-gradient(135deg,var(--sec-acc-soft),var(--pri-soft))';
card.classList.add('glow','pulse');
setTimeout(()=>card.classList.remove('pulse'), 900);
refreshOverall();
} else {
feedback(fb, false, '&#10007; Промах. Попробуй ещё. Подсказка доступна.');
}
});
hintBtn.addEventListener('click', ()=>{
const fb = document.getElementById('boss4-'+b.n+'-fb');
fb.className = 'feedback ok';
fb.innerHTML = '<b>Подсказка:</b> '+b.hint;
fb.style.display = 'block';
fb.style.background = 'var(--warn-bg,#fef3c7)';
fb.style.color = '#92400e';
fb.style.borderLeftColor = 'var(--warn,#f59e0b)';
try{ renderMath(fb); }catch(e){}
});
ansInp.addEventListener('keydown', e=>{ if(e.key === 'Enter') goBtn.click(); });
});
refreshOverall();
}
/* ===== Search ===== */
const SEARCH_INDEX = (function(){
const arr=[];
PARAS.forEach(p=>arr.push({kind:'Параграф',title:p.num+' '+p.name,desc:p.sub||'',sec:p.id}));
return arr;
})();
function initSearch(){
const modal=document.getElementById('search-modal'),inp=document.getElementById('search-input'),out=document.getElementById('search-results'),btn=document.getElementById('search-btn');
if(!modal||!inp||!out) return;
let cur=0,rows=[];
function score(q,it){ const t=(it.title+' '+it.desc).toLowerCase(); if(t.includes(q)) return 100+(it.title.toLowerCase().startsWith(q)?50:0); let s=0; q.split(/\s+/).forEach(w=>{if(w&&t.includes(w))s+=10;}); return s; }
function rank(q){ q=q.trim().toLowerCase(); if(!q) return SEARCH_INDEX.slice(0,12); return SEARCH_INDEX.map(it=>({it,s:score(q,it)})).filter(x=>x.s>0).sort((a,b)=>b.s-a.s).slice(0,20).map(x=>x.it); }
function render(){ cur=0; if(!rows.length){out.innerHTML='<div class="search-empty">Ничего не найдено</div>';return;} out.innerHTML=rows.map((r,i)=>'<button class="search-row'+(i===0?' active':'')+'" data-i="'+i+'"><div class="sr-kind">'+r.kind+'</div><div class="sr-title">'+r.title+'</div>'+(r.desc?'<div class="sr-desc">'+(r.desc.length>90?r.desc.slice(0,90)+'\u2026':r.desc)+'</div>':'')+'</button>').join(''); out.querySelectorAll('.search-row').forEach(b=>b.addEventListener('click',()=>{cur=+b.dataset.i;pick();})); }
function pick(){ const r=rows[cur]; if(!r) return; close(); goTo(r.sec); }
function move(d){ const items=out.querySelectorAll('.search-row'); if(!items.length) return; items[cur]&&items[cur].classList.remove('active'); cur=(cur+d+items.length)%items.length; items[cur].classList.add('active'); items[cur].scrollIntoView({block:'nearest'}); }
function open(){ modal.classList.add('show'); inp.value=''; rows=rank(''); render(); setTimeout(()=>inp.focus(),50); }
function close(){ modal.classList.remove('show'); }
btn&&btn.addEventListener('click',open);
modal.addEventListener('click',e=>{if(e.target===modal)close();});
inp.addEventListener('input',()=>{rows=rank(inp.value);render();});
inp.addEventListener('keydown',e=>{ if(e.key==='ArrowDown'){e.preventDefault();move(1);}else if(e.key==='ArrowUp'){e.preventDefault();move(-1);}else if(e.key==='Enter'){e.preventDefault();pick();}else if(e.key==='Escape'){e.preventDefault();close();} });
document.addEventListener('keydown',e=>{ if((e.ctrlKey||e.metaKey)&&(e.key==='k'||e.key==='K')){ e.preventDefault(); if(modal.classList.contains('show')) close(); else open(); } });
}
function initSidebarToggle(){
const side=document.getElementById('col-side'),back=document.getElementById('col-side-backdrop'),btn=document.getElementById('sidebar-btn');
if(!side||!btn) return;
function open(){ side.classList.add('open'); back.classList.add('show'); }
function close(){ side.classList.remove('open'); back.classList.remove('show'); }
btn.addEventListener('click',()=>{ if(side.classList.contains('open')) close(); else open(); });
back.addEventListener('click',close);
document.addEventListener('keydown',e=>{ if(e.key==='Escape') close(); });
}
function init(){
loadProgress(); initTheme(); initSidebarToggle(); initSearch();
buildParaSelector(); refreshProgressUI(); loadServerReadState(); goTo(PARAS[0].id);
setTimeout(()=>achievement('start'), 600);
if(window.LS&&window.LS.xp){
window.LS.xp.load().then(function(s){ if(s&&s.xp>STATE.xp){ STATE.xp=s.xp; STATE.level=calcLevel(STATE.xp); saveProgress(); refreshProgressUI(); if(STATE.current) buildSidebar(STATE.current); } });
}
}
document.addEventListener('DOMContentLoaded', init);
/* === GEOM11 POLISH JS === */
(function(){
function bumpScore(el){
if(!el) return;
el.classList.remove('bump');
void el.offsetWidth;
el.classList.add('bump');
setTimeout(function(){ try{ el.classList.remove('bump'); }catch(e){} }, 270);
}
window.__geom11BumpScore = bumpScore;
function observeScores(root){
root = root || document;
var nodes = root.querySelectorAll('.score-display b');
nodes.forEach(function(b){
if(b.__scoreObs) return;
b.__scoreObs = true;
var last = b.textContent;
try{
var mo = new MutationObserver(function(){
var nv = b.textContent;
if(nv !== last){ last = nv; bumpScore(b); }
});
mo.observe(b, {childList:true, characterData:true, subtree:true});
}catch(e){}
});
}
function rescanScores(){ try{ observeScores(document); }catch(e){} }
if(document.readyState === 'loading') document.addEventListener('DOMContentLoaded', rescanScores);
else rescanScores();
try{
var rootObs = new MutationObserver(function(muts){
var need = false;
for(var i=0;i<muts.length && !need;i++){
var m = muts[i];
for(var j=0;j<m.addedNodes.length;j++){
var n = m.addedNodes[j];
if(n.nodeType===1){
if(n.classList && n.classList.contains('score-display')) { need = true; break; }
if(n.querySelector && n.querySelector('.score-display b')) { need = true; break; }
}
}
}
if(need) rescanScores();
});
rootObs.observe(document.body, {childList:true, subtree:true});
}catch(e){}
function refreshDoneMarks(){
try{
if(typeof STATE === 'undefined' || !STATE.progress) return;
document.querySelectorAll('.psel-card').forEach(function(c){
var id = c.dataset.id || c.dataset.progCard;
if(!id) return;
var pct = +STATE.progress[id] || 0;
if(!c.querySelector('.psel-done')){
var s = document.createElement('span');
s.className = 'psel-done';
s.setAttribute('title','Прочитано');
s.innerHTML = '<svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg>';
c.appendChild(s);
}
c.classList.toggle('done', pct >= 50);
});
}catch(e){}
}
try{
if(typeof window.refreshProgressUI === 'function'){
var _origRP = window.refreshProgressUI;
window.refreshProgressUI = function(){ var r = _origRP.apply(this, arguments); setTimeout(refreshDoneMarks, 0); return r; };
}
}catch(e){}
setTimeout(refreshDoneMarks, 600);
setTimeout(refreshDoneMarks, 1800);
window.addEventListener('focus', function(){ setTimeout(refreshDoneMarks, 200); });
document.addEventListener('click', function(e){
var card = e.target.closest && e.target.closest('.psel-card');
if(!card) return;
var id = card.dataset.id;
if(!id) return;
setTimeout(function(){
var sec = document.getElementById('sec-' + id);
if(sec) try{ sec.scrollIntoView({behavior:'smooth', block:'start'}); }catch(e){}
}, 60);
});
})();
</script>
</body>
</html>