Files
Learn_System/frontend/textbooks/geometry_11_ch1.html
T
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

1831 lines
122 KiB
HTML
Raw 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 · Раздел 1 · «Призма и цилиндр»</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:#d97706; --pri2:#b45309; --pri-soft:#fef3c7;
--acc:#f59e0b; --acc2:#d97706; --acc-soft:#fef9c3;
--ok:#10b981; --ok-bg:#d1fae5; --warn:#f59e0b; --warn-bg:#fef3c7;
--bad:#ef4444; --fail:#dc2626; --fail-bg:#fee2e2;
}
.dark{--bg:#0a0a0e; --card:#13120a; --card-soft:#18160a; --text:#fef9e7; --ink:#fef9e7; --muted:#a39070; --border:#2a2512}
*{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,#92400e 0%,#d97706 55%,#fbbf24 100%);color:#fff;padding:46px 22px 30px;overflow:hidden;border-bottom:2px solid rgba(251,191,36,.2);min-height:130px}
.hdr::before{content:'РАЗДЕЛ 1';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-p1"]{ --sec-acc:#d97706; --sec-acc-d:#b45309; --sec-acc-soft:#fef3c7; }
.sec[id="sec-p2"]{ --sec-acc:#d97706; --sec-acc-d:#b45309; --sec-acc-soft:#fef3c7; }
.sec[id="sec-final1"]{ --sec-acc:#d97706; --sec-acc-d:#b45309; --sec-acc-soft:#fef3c7; }
.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 #fef3c7;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 · Раздел 1</h1>
<div class="hdr-sub">Призма (правильная, прямая, наклонная, параллелепипед, куб) · цилиндр и его сечения</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('p1')"><svg class="ic" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4" fill="currentColor" stroke="none"/></svg> Начать § 1</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-p1" class="sec" data-watermark="\triangle"><div class="sec-header"><span class="sec-num">§ 1</span><h2 class="sec-h">Призма</h2></div><div id="p1-body"></div></section>
<section id="sec-p2" class="sec" data-watermark="\bigcirc"><div class="sec-header"><span class="sec-num">§ 2</span><h2 class="sec-h">Цилиндр</h2></div><div id="p2-body"></div></section>
<section id="sec-final1" class="sec" data-watermark="&#9733;"><div class="sec-header"><span class="sec-num" style="background:linear-gradient(135deg,#d97706,#f59e0b)">&#9733;</span><h2 class="sec-h">Финал раздела</h2></div><div id="final1-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» · Раздел 1 · «Призма и цилиндр» · 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:'p1', progress:{}, achievements:new Map(), xp:0, level:1 };
const TOTAL_PARAS = 3;
const _TB_SLUG = 'geometry-11-ch1';
const PARAS = [
{ id:'p1', num:'§ 1', name:"Призма", sub:'$S_{бок}=Pl$, $V=S_{осн}h$' },
{ id:'p2', num:'§ 2', name:"Цилиндр", sub:'$S_{бок}=2\\pi Rh$, $V=\\pi R^2h$' },
{ id:'final1', num:'&#9733;', name:"Финал раздела", sub:'Итоги · боссы раздела 1', 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 = {
p1_done:"Призма освоено!",
p2_done:"Цилиндр освоено!",
start:"Начало раздела 1!",
ch1_done:"Раздел 1 пройден!",
r1_done:"Мастер призмы и цилиндра"
};
function loadProgress(){
try{
const s=localStorage.getItem('geometry11_ch1_progress'); if(s) Object.assign(STATE.progress, JSON.parse(s));
const a=localStorage.getItem('geometry11_ch1_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_ch1_progress', JSON.stringify(STATE.progress));
localStorage.setItem('geometry11_ch1_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-ch1-'+(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 = { p1:()=>buildP1(), p2:()=>buildP2(), final1:()=>buildFinal1() };
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 = {
p1:{title:"Шпаргалка § 1", rows:[["Тема","Призма"],["Прямая","$S_{бок}=P_{осн}\\cdot h$"],["Наклонная","$S_{бок}=P_{пер}\\cdot l$"],["Объём","$V=S_{осн}\\cdot h$"],["Диагональ пар.","$d=\\sqrt{a^2+b^2+c^2}$"]]},
p2:{title:"Шпаргалка § 2", rows:[["Тема", "Цилиндр"],["$S_{осн}$","$\\pi R^2$"],["$S_{бок}$","$2\\pi Rh$"],["$S_{полн}$","$2\\pi R(R+h)$"],["$V$","$\\pi R^2 h$"],["Развёртка","прямоуг. $2\\pi R \\times h$"],["Осевое сеч.","прямоуг. $2R \\times h$"],["Наклон. сеч.","эллипс, $a=R/\\cos\\alpha$, $b=R$"]]},
final1:{title:"Финал раздела 1", rows:[["§ 1","Призма"],["§ 2","Цилиндр"],["Боссы","5 интегрированных"],["Награда","+50 XP + ачивка"]]}
};
const TIPS=[
{sec:'p1',html:"§ 1 «Призма» — крути 3D-модель в интерактиве 1, проверь формулы в калькуляторе. Главное: $V=S_{осн}\\cdot h$, $S_{бок}=P_{осн}\\cdot h$ (для прямой)."},
{sec:'p2',html:"§ 2 «Цилиндр» — крути 3D-модель в интерактиве 1, разбирай сечения в IV2 (круг/прямоугольник/эллипс). Главное: $S_{бок}=2\\pi Rh$, $V=\\pi R^2 h$, развёртка боковой поверхности — прямоугольник $2\\pi R \\times h$."},
{sec:'final1',html:"Финал раздела 1 — интегрированные задачи по разделу."}
];
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_ch1_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_ch1_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 = {p1:'\xA71',p2:'\xA72',final1:'Финал'};
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);
});
}
/* ===== §1 «Призма» — Wave 1 ===== */
function buildP1(){
const box = document.getElementById('p1-body');
if(!box) return;
let html = '';
/* === ТЕОРИЯ === */
html += makeCard('theory', 'Определение и виды призм', '§ 1.1',
'<p><b>Призма</b> — многогранник, две грани которого (основания) — равные многоугольники, лежащие в параллельных плоскостях, а остальные грани (боковые) — параллелограммы.</p>'
+ '<p><b>Виды призм:</b></p>'
+ '<ul style="margin:6px 0 10px 22px;line-height:1.7">'
+ '<li><b>Прямая</b> — боковые рёбра $\\perp$ основанию. Боковые грани — прямоугольники.</li>'
+ '<li><b>Наклонная</b> — боковые рёбра не перпендикулярны основанию.</li>'
+ '<li><b>Правильная</b> — прямая призма, основание которой — правильный многоугольник.</li>'
+ '</ul>'
+ '<p><b>Параллелепипед</b> — призма с параллелограммом в основании. У неё $6$ граней, $12$ рёбер, $8$ вершин.</p>'
+ '<p><b>Прямой параллелепипед</b> — боковые рёбра $\\perp$ основанию. <b>Прямоугольный параллелепипед</b> — прямой, у которого основание — прямоугольник (значит, все грани — прямоугольники). <b>Куб</b> — прямоугольный параллелепипед с равными рёбрами.</p>'
+ '<p style="margin-top:10px"><b>Свойства параллелепипеда:</b></p>'
+ '<ul style="margin:6px 0 0 22px;line-height:1.7">'
+ '<li>Противоположные грани равны и параллельны.</li>'
+ '<li>Все четыре диагонали пересекаются в одной точке и делятся ею пополам.</li>'
+ '<li>Диагональ прямоугольного параллелепипеда: $d=\\sqrt{a^2+b^2+c^2}$.</li>'
+ '</ul>');
html += makeCard('rule', 'Площадь поверхности и объём', '§ 1.2',
'<p><b>Боковая поверхность.</b></p>'
+ '<ul style="margin:6px 0 10px 22px;line-height:1.7">'
+ '<li>Прямой призмы: $S_{бок}=P_{осн}\\cdot h$, где $P_{осн}$ — периметр основания, $h$ — высота (= боковое ребро).</li>'
+ '<li>Наклонной призмы: $S_{бок}=P_{пер}\\cdot l$, где $P_{пер}$ — периметр <i>перпендикулярного сечения</i>, $l$ — боковое ребро.</li>'
+ '</ul>'
+ '<p><b>Полная поверхность:</b> $S_{полн}=S_{бок}+2\\cdot S_{осн}$.</p>'
+ '<p><b>Объём</b> (для любой призмы):</p>'
+ '<p style="text-align:center;margin:8px 0">$$V=S_{осн}\\cdot h$$</p>'
+ '<p style="font-size:.9rem;color:var(--muted)">где $h$ — высота призмы (расстояние между плоскостями оснований).</p>'
+ '<details class="spoiler"><summary>Пример: правильная 6-угольная призма</summary><div class="spoiler-body">'
+ '<p>Сторона основания $a=4$, высота $h=10$:</p>'
+ '<ul style="margin:6px 0 0 22px;line-height:1.7">'
+ '<li>$S_{осн}=\\dfrac{3\\sqrt{3}}{2}\\,a^2=24\\sqrt{3}\\approx 41{,}57$</li>'
+ '<li>$P_{осн}=6a=24$</li>'
+ '<li>$S_{бок}=24\\cdot 10=240$</li>'
+ '<li>$V=24\\sqrt{3}\\cdot 10=240\\sqrt{3}\\approx 415{,}7$</li>'
+ '</ul>'
+ '</div></details>');
html += makeCard('example', 'Диагональ прямоугольного параллелепипеда', '§ 1.3',
'<p>Пусть у прямоугольного параллелепипеда измерения $a$, $b$, $c$ (длина, ширина, высота).</p>'
+ '<p><b>Диагональ грани</b> (например, нижнего основания): $d_{грани}=\\sqrt{a^2+b^2}$.</p>'
+ '<p><b>Главная диагональ</b> — отрезок, соединяющий противоположные вершины. Тогда:</p>'
+ '<p style="text-align:center;margin:8px 0">$$d^2=a^2+b^2+c^2$$</p>'
+ '<p>Это <b>трёхмерная теорема Пифагора</b>.</p>'
+ '<details class="spoiler"><summary>Доказательство</summary><div class="spoiler-body">'
+ '<p>Возьмём вершину $A$ нижнего основания и противоположную $A_1$ верхнего. Соединим $A$ с вершиной $C$ нижнего основания, потом $C$ с $A_1$.</p>'
+ '<p>В треугольнике $ACA_1$ угол при $C$ прямой (ребро $CC_1=c$ перпендикулярно плоскости основания). По Пифагору в нижнем основании: $AC^2=a^2+b^2$. В $\\triangle ACA_1$: $AA_1^2=AC^2+CA_1^2=a^2+b^2+c^2$.</p>'
+ '</div></details>'
+ '<p style="margin-top:10px"><b>Пример:</b> куб со стороной $3 \\Rightarrow d=\\sqrt{9+9+9}=3\\sqrt{3}\\approx 5{,}20$.</p>');
/* === ИНТЕРАКТИВ 1 — 3D-конструктор === */
html += '<div class="wg" id="p1-iv1">'
+ '<div class="wg-header"><span class="wg-badge">3D · конструктор</span><div class="wg-title">Правильная n-угольная призма</div></div>'
+ '<div class="wg-help">Меняй число сторон основания $n$, радиус описанной окружности $R$ и высоту $h$. Вращай мышью или выбирай вид. После <b>4 разных конфигураций</b> — +10 XP.</div>'
+ '<div class="sliders">'
+ '<label>$n$ (сторон):<b id="p1-iv1-n-v">5</b><input type="range" id="p1-iv1-n" min="3" max="8" step="1" value="5"></label>'
+ '<label>$R$ (радиус):<b id="p1-iv1-R-v">1.5</b><input type="range" id="p1-iv1-R" min="1" max="4" step="0.1" value="1.5"></label>'
+ '<label>$h$ (высота):<b id="p1-iv1-h-v">2.4</b><input type="range" id="p1-iv1-h" min="1" max="6" step="0.1" value="2.4"></label>'
+ '</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:1px solid var(--border);border-radius:9px;padding:8px;text-align:center"><svg id="p1-iv1-svg" viewBox="0 0 480 400" width="100%" style="max-width:480px;height:auto"></svg></div>'
+ '<div class="score-display" style="margin-top:10px;flex-wrap:wrap">'
+ '<span>$P_{осн}=$<b id="p1-iv1-P">—</b></span>'
+ '<span>$S_{осн}=$<b id="p1-iv1-Sb">—</b></span>'
+ '<span>$S_{бок}=$<b id="p1-iv1-Ss">—</b></span>'
+ '<span>$S_{полн}=$<b id="p1-iv1-St">—</b></span>'
+ '<span>$V=$<b id="p1-iv1-V">—</b></span>'
+ '</div>'
+ '<div style="font-size:.78rem;color:var(--muted);margin-top:6px">Конфигураций изучено: <b id="p1-iv1-cnt">0</b> / 4</div>'
+ '</div>';
/* === ИНТЕРАКТИВ 2 — Калькулятор === */
html += '<div class="wg" id="p1-iv2">'
+ '<div class="wg-header"><span class="wg-badge">калькулятор</span><div class="wg-title">$V$, $S_{полн}$ и диагональ</div></div>'
+ '<div class="wg-help">Выбери тип призмы и задай размеры. Получишь полный разбор формул.</div>'
+ '<div style="display:flex;gap:14px;margin-bottom:10px;flex-wrap:wrap">'
+ '<label style="display:flex;align-items:center;gap:6px;cursor:pointer"><input type="radio" name="p1-iv2-type" value="reg" checked> Правильная $n$-угольная</label>'
+ '<label style="display:flex;align-items:center;gap:6px;cursor:pointer"><input type="radio" name="p1-iv2-type" value="rect"> Прямоугольный параллелепипед</label>'
+ '</div>'
+ '<div id="p1-iv2-fields-reg" class="sliders">'
+ '<label>$n$:<b id="p1-iv2-n-v">6</b><input type="range" id="p1-iv2-n" min="3" max="10" step="1" value="6"></label>'
+ '<label>$a$ (сторона):<b id="p1-iv2-a-v">4.0</b><input type="range" id="p1-iv2-a" min="1" max="8" step="0.1" value="4"></label>'
+ '<label>$h$ (высота):<b id="p1-iv2-h-v">10.0</b><input type="range" id="p1-iv2-h" min="1" max="15" step="0.1" value="10"></label>'
+ '</div>'
+ '<div id="p1-iv2-fields-rect" class="sliders" style="display:none">'
+ '<label>$a$:<b id="p1-iv2-ra-v">3.0</b><input type="range" id="p1-iv2-ra" min="1" max="6" step="0.1" value="3"></label>'
+ '<label>$b$:<b id="p1-iv2-rb-v">4.0</b><input type="range" id="p1-iv2-rb" min="1" max="6" step="0.1" value="4"></label>'
+ '<label>$c$:<b id="p1-iv2-rc-v">5.0</b><input type="range" id="p1-iv2-rc" min="1" max="6" step="0.1" value="5"></label>'
+ '</div>'
+ '<div class="actions"><button class="btn primary" id="p1-iv2-calc">Вычислить</button></div>'
+ '<div id="p1-iv2-out" style="margin-top:10px;font-size:.92rem;line-height:1.7"></div>'
+ '</div>';
/* === ИНТЕРАКТИВ 3 — DnD сортер === */
html += '<div class="wg" id="p1-iv3">'
+ '<div class="wg-header"><span class="wg-badge">сортер</span><div class="wg-title">Какая это призма?</div></div>'
+ '<div class="wg-help">Перетащи описание в нужный ящик. Кликни чип, затем ящик — тоже сработает.</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 описаний → 3 типа</div>'
+ '<div id="p1-iv3-pool" class="dnd-pool"></div>'
+ '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px">'
+ '<div class="drop-box"><h5>Прямая</h5><div class="drop-items" data-cat="straight"></div></div>'
+ '<div class="drop-box"><h5>Правильная</h5><div class="drop-items" data-cat="regular"></div></div>'
+ '<div class="drop-box"><h5>Наклонная</h5><div class="drop-items" data-cat="oblique"></div></div>'
+ '</div>'
+ '<div class="actions"><button class="btn primary" id="p1-iv3-check">Проверить</button><button class="btn" id="p1-iv3-reset">Сброс</button></div>'
+ '<div class="feedback" id="p1-iv3-fb"></div>'
+ '</div>';
/* === ИНТЕРАКТИВ 4 — Тренажёр === */
html += '<div class="wg" id="p1-iv4">'
+ '<div class="wg-header"><span class="wg-badge">тренажёр · 6 задач</span><div class="wg-title">$V$, $S$ и диагональ</div></div>'
+ '<div class="wg-help">Введи числовой ответ. Допуск $\\pm 0{,}05$ для дробных значений.</div>'
+ '<div id="p1-iv4-list"></div>'
+ '<div class="score-display" style="margin-top:10px">Решено: <b id="p1-iv4-score">0</b> / 6</div>'
+ '</div>';
html += secNavFor('p1');
html += readButton('p1');
box.innerHTML = html;
renderMath(box);
/* ====== JS-логика интерактивов ====== */
/* IV1 — 3D конструктор */
(function(){
if(!window.G3D) return;
const svg = document.getElementById('p1-iv1-svg');
const elN = document.getElementById('p1-iv1-n');
const elR = document.getElementById('p1-iv1-R');
const elH = document.getElementById('p1-iv1-h');
const vN = document.getElementById('p1-iv1-n-v');
const vR = document.getElementById('p1-iv1-R-v');
const vH = document.getElementById('p1-iv1-h-v');
const oP = document.getElementById('p1-iv1-P');
const oSb = document.getElementById('p1-iv1-Sb');
const oSs = document.getElementById('p1-iv1-Ss');
const oSt = document.getElementById('p1-iv1-St');
const oV = document.getElementById('p1-iv1-V');
const oCnt = document.getElementById('p1-iv1-cnt');
if(!svg) return;
const scene = G3D.createScene({W:480, H:400, scale:50, camDist:8, rotX:-0.35, rotY:0.7});
const seen = new Set();
let xpGiven = false;
function draw(){
const n = +elN.value, R = +elR.value, h = +elH.value;
vN.textContent = n;
vR.textContent = R.toFixed(1);
vH.textContent = h.toFixed(1);
const mesh = G3D.prismMesh(n, R, h);
const M = G3D.buildRotMatrix(scene);
svg.innerHTML = G3D.renderMesh(mesh, M, scene, {
fillBase:'rgba(252,231,243,.55)',
fillSide:'rgba(219,234,254,.55)',
strokeVisible:'#0f172a',
strokeHidden:'#94a3b8'
});
/* Формулы для правильного n-угольника, вписанного в окружность радиуса R */
const a = 2*R*Math.sin(Math.PI/n);
const P = n*a;
const Sb = 0.5*n*R*R*Math.sin(2*Math.PI/n);
const Ss = P*h;
const St = Ss + 2*Sb;
const V = Sb*h;
oP.textContent = P.toFixed(2);
oSb.textContent = Sb.toFixed(2);
oSs.textContent = Ss.toFixed(2);
oSt.textContent = St.toFixed(2);
oV.textContent = V.toFixed(2);
const key = n+'|'+R.toFixed(1)+'|'+h.toFixed(1);
seen.add(key);
oCnt.textContent = Math.min(seen.size, 4);
if(seen.size >= 4 && !xpGiven){
xpGiven = true;
addXp(10, 'p1-iv1');
bumpProgress('p1', 15);
const note = document.createElement('div');
note.className = 'feedback ok';
note.innerHTML = '&#10003; +10 XP за изучение 4 разных призм!';
note.style.cssText = 'display:block;margin-top:8px';
const host = document.getElementById('p1-iv1');
if(host) host.appendChild(note);
setTimeout(function(){ try{ note.remove(); }catch(e){} }, 3000);
}
}
draw();
G3D.attachOrbit(svg, scene, draw);
[elN, elR, elH].forEach(function(el){ el.addEventListener('input', draw); });
document.querySelectorAll('#p1-iv1 .g3d-tools .btn').forEach(function(b){
b.addEventListener('click', function(){ G3D.presetView(scene, b.dataset.view, draw); });
});
})();
/* IV2 — Калькулятор */
(function(){
const radios = document.querySelectorAll('input[name="p1-iv2-type"]');
const fldReg = document.getElementById('p1-iv2-fields-reg');
const fldRect = document.getElementById('p1-iv2-fields-rect');
const elN = document.getElementById('p1-iv2-n');
const elA = document.getElementById('p1-iv2-a');
const elH = document.getElementById('p1-iv2-h');
const elRa = document.getElementById('p1-iv2-ra');
const elRb = document.getElementById('p1-iv2-rb');
const elRc = document.getElementById('p1-iv2-rc');
const labels = {
'p1-iv2-n':'p1-iv2-n-v', 'p1-iv2-a':'p1-iv2-a-v', 'p1-iv2-h':'p1-iv2-h-v',
'p1-iv2-ra':'p1-iv2-ra-v', 'p1-iv2-rb':'p1-iv2-rb-v', 'p1-iv2-rc':'p1-iv2-rc-v'
};
Object.keys(labels).forEach(function(k){
const el = document.getElementById(k), lab = document.getElementById(labels[k]);
if(!el || !lab) return;
const upd = function(){ lab.textContent = el.step === '1' ? el.value : (+el.value).toFixed(1); };
el.addEventListener('input', upd); upd();
});
radios.forEach(function(r){ r.addEventListener('change', function(){
const v = document.querySelector('input[name="p1-iv2-type"]:checked').value;
fldReg.style.display = v === 'reg' ? '' : 'none';
fldRect.style.display = v === 'rect' ? '' : 'none';
}); });
let xpGiven = false;
document.getElementById('p1-iv2-calc').addEventListener('click', function(){
const out = document.getElementById('p1-iv2-out');
const v = document.querySelector('input[name="p1-iv2-type"]:checked').value;
let html = '';
if(v === 'reg'){
const n = +elN.value, a = +elA.value, h = +elH.value;
const P = n*a;
const apo = a / (2*Math.tan(Math.PI/n));
const Sb = 0.5 * P * apo;
const Ss = P * h;
const St = Ss + 2*Sb;
const V = Sb * h;
html = '<div style="background:var(--card);border:1px solid var(--border);border-radius:9px;padding:10px 14px">'
+ '<p><b>Правильная '+n+'-угольная призма, $a='+a.toFixed(2)+'$, $h='+h.toFixed(2)+'$</b></p>'
+ '<p>$P_{осн}=n\\cdot a='+n+'\\cdot '+a.toFixed(2)+'='+P.toFixed(2)+'$</p>'
+ '<p>$S_{осн}=\\dfrac{1}{2}P\\cdot a_{апо}=\\dfrac{n a^2}{4}\\cot\\dfrac{\\pi}{n}\\approx '+Sb.toFixed(2)+'$</p>'
+ '<p>$S_{бок}=P_{осн}\\cdot h='+P.toFixed(2)+'\\cdot '+h.toFixed(2)+'='+Ss.toFixed(2)+'$</p>'
+ '<p>$S_{полн}=S_{бок}+2S_{осн}\\approx '+St.toFixed(2)+'$</p>'
+ '<p style="font-weight:700;color:var(--pri2)">$V=S_{осн}\\cdot h\\approx '+V.toFixed(2)+'$</p>'
+ '</div>';
} else {
const a = +elRa.value, b = +elRb.value, c = +elRc.value;
const V = a*b*c;
const St = 2*(a*b + b*c + a*c);
const d = Math.sqrt(a*a + b*b + c*c);
html = '<div style="background:var(--card);border:1px solid var(--border);border-radius:9px;padding:10px 14px">'
+ '<p><b>Прямоугольный параллелепипед, $a='+a.toFixed(2)+'$, $b='+b.toFixed(2)+'$, $c='+c.toFixed(2)+'$</b></p>'
+ '<p>$V=abc='+a.toFixed(2)+'\\cdot '+b.toFixed(2)+'\\cdot '+c.toFixed(2)+'='+V.toFixed(2)+'$</p>'
+ '<p>$S_{полн}=2(ab+bc+ac)='+St.toFixed(2)+'$</p>'
+ '<p style="font-weight:700;color:var(--pri2)">$d=\\sqrt{a^2+b^2+c^2}=\\sqrt{'+(a*a+b*b+c*c).toFixed(2)+'}\\approx '+d.toFixed(2)+'$</p>'
+ '</div>';
}
out.innerHTML = html;
renderMath(out);
if(!xpGiven){
xpGiven = true;
addXp(10, 'p1-iv2');
bumpProgress('p1', 15);
}
});
})();
/* IV3 — DnD сортер */
(function(){
const items = [
{ id:'i1', html:'Основание — правильный 6-угольник, боковые рёбра $\\perp$ основанию', cat:'regular' },
{ id:'i2', html:'Боковые рёбра наклонены к основанию под углом 60°', cat:'oblique' },
{ id:'i3', html:'Все 6 граней — прямоугольники, все рёбра равны (куб)', cat:'straight' },
{ id:'i4', html:'Боковые рёбра $\\perp$ основанию, основание — параллелограмм', cat:'straight' },
{ id:'i5', html:'Основание — квадрат со стороной 3, высота 5, боковые рёбра $\\perp$', cat:'regular' },
{ id:'i6', html:'Боковые грани — параллелограммы, не прямоугольники', cat:'oblique' }
];
const sorter = setupSorter({
poolId:'p1-iv3-pool',
scopeSelector:'#p1-iv3',
items: items,
cats: ['straight','regular','oblique']
});
let xpGiven = false;
document.getElementById('p1-iv3-check').addEventListener('click', function(){
const fb = document.getElementById('p1-iv3-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, 'p1-iv3');
bumpProgress('p1', 25);
}
} else {
feedback(fb, false, '&#10007; Правильно: '+correct+' из '+items.length+'. Попробуй ещё раз.');
}
});
document.getElementById('p1-iv3-reset').addEventListener('click', function(){
sorter.reset();
const fb = document.getElementById('p1-iv3-fb'); fb.style.display = 'none';
});
})();
/* IV4 — Тренажёр */
(function(){
const tasks = [
{ q:'Прямоугольный параллелепипед $3\\times 4\\times 5$. $V=\\,?$', a:60, tol:0.05 },
{ q:'Куб со стороной 2. $V=\\,?$', a:8, tol:0.05 },
{ q:'Куб со стороной 3. Диагональ $d=\\,?$ (точность 0,01)', a:5.20, tol:0.05 },
{ q:'Прямоугольный параллелепипед $2\\times 3\\times 6$. Главная диагональ?', a:7, tol:0.05 },
{ q:'Правильная 4-угольная призма со стороной основания 4 и высотой 5. $V=\\,?$', a:80, tol:0.05 },
{ q:'Прямоугольный параллелепипед $3\\times 4\\times 5$. $S_{полн}=\\,?$', a:94, tol:0.05 }
];
const list = document.getElementById('p1-iv4-list');
const scoreEl = document.getElementById('p1-iv4-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="p1-iv4-inp-'+i+'" placeholder="число" style="width:140px">'
+ '<button class="btn primary" data-i="'+i+'">Проверить</button>'
+ '</div>'
+ '<div class="feedback" id="p1-iv4-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('p1-iv4-inp-'+i);
const fb = document.getElementById('p1-iv4-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, 'p1-iv4');
bumpProgress('p1', 25);
setTimeout(function(){ achievement('p1_done'); }, 400);
}
}
} else {
feedback(fb, false, '&#10007; Не точно. Пересчитай аккуратно.');
}
});
});
})();
wireReadBtn('p1');
}
/* ===== §2 «Цилиндр» — Wave 2 ===== */
function buildP2(){
const box = document.getElementById('p2-body');
if(!box) return;
let html = '';
/* === ТЕОРИЯ === */
html += makeCard('theory', 'Определение и элементы', '§ 2.1',
'<p><b>Цилиндр</b> (точнее — <b>прямой круговой цилиндр</b>) — тело, полученное вращением прямоугольника вокруг одной из его сторон.</p>'
+ '<p><b>Элементы цилиндра:</b></p>'
+ '<ul style="margin:6px 0 10px 22px;line-height:1.7">'
+ '<li><b>Основания</b> — два равных круга радиуса $R$, лежащих в параллельных плоскостях.</li>'
+ '<li><b>Ось</b> — отрезок, соединяющий центры оснований.</li>'
+ '<li><b>Образующие</b> — отрезки длины $h$, параллельные оси и соединяющие соответствующие точки окружностей оснований. Все образующие равны и $\\perp$ основаниям.</li>'
+ '<li><b>Боковая поверхность</b> — объединение всех образующих.</li>'
+ '<li><b>Высота</b> $h$ — расстояние между основаниями (= длина образующей).</li>'
+ '<li><b>Радиус</b> $R$ — радиус основания.</li>'
+ '</ul>'
+ '<p>Цилиндр можно рассматривать как <b>предельный случай правильной $n$-угольной призмы</b>: если число сторон основания $n \\to \\infty$, призма «выпрямляется» в цилиндр, а её боковая поверхность — в боковую поверхность цилиндра.</p>');
html += makeCard('rule', 'Площадь поверхности и объём', '§ 2.2',
'<p>Основные формулы цилиндра радиуса $R$ и высоты $h$:</p>'
+ '<p style="text-align:center;margin:8px 0">$$S_{осн}=\\pi R^2 \\qquad S_{бок}=2\\pi R h$$</p>'
+ '<p style="text-align:center;margin:8px 0">$$S_{полн}=2\\pi R(R+h) \\qquad V=\\pi R^2 h$$</p>'
+ '<p><b>Развёртка боковой поверхности</b> — прямоугольник со сторонами $2\\pi R$ (длина окружности основания) и $h$ (высота). Отсюда $S_{бок}=2\\pi R \\cdot h$.</p>'
+ '<details class="spoiler"><summary>Пример: $R=3$, $h=5$</summary><div class="spoiler-body">'
+ '<ul style="margin:6px 0 0 22px;line-height:1.7">'
+ '<li>$S_{осн}=9\\pi \\approx 28{,}3$</li>'
+ '<li>$S_{бок}=2\\pi\\cdot 3\\cdot 5=30\\pi \\approx 94{,}2$</li>'
+ '<li>$S_{полн}=2\\pi\\cdot 3(3+5)=48\\pi \\approx 150{,}8$</li>'
+ '<li>$V=\\pi\\cdot 9\\cdot 5=45\\pi \\approx 141{,}4$</li>'
+ '</ul>'
+ '</div></details>');
html += makeCard('example', 'Сечения цилиндра', '§ 2.3',
'<p>Какие фигуры получаются в сечении цилиндра плоскостью:</p>'
+ '<ul style="margin:6px 0 10px 22px;line-height:1.7">'
+ '<li><b>Перпендикулярное оси</b> (= параллельное основанию): <b>круг</b> радиуса $R$.</li>'
+ '<li><b>Осевое</b> (через ось): <b>прямоугольник</b> со сторонами $2R$ и $h$.</li>'
+ '<li><b>Параллельное оси, не осевое</b> (на расстоянии $d<R$ от оси): <b>прямоугольник</b> со сторонами $2\\sqrt{R^2-d^2}$ и $h$.</li>'
+ '<li><b>Наклонное</b> (плоскость пересекает все образующие, угол с основанием $\\alpha$): <b>эллипс</b> с полуосями $b=R$ (малая) и $a=R/\\cos\\alpha$ (большая).</li>'
+ '</ul>'
+ '<p><b>Касательная плоскость</b> к цилиндру проходит через одну образующую и не пересекает цилиндр в других точках. Через любую образующую можно провести <b>ровно одну</b> касательную плоскость; она перпендикулярна осевому сечению, проходящему через эту образующую.</p>'
+ '<details class="spoiler"><summary>Почему наклонное сечение — эллипс?</summary><div class="spoiler-body">'
+ '<p>Пусть плоскость $\\pi$ наклонена к основанию под углом $\\alpha$. Спроецируем сечение на плоскость основания вдоль оси цилиндра. Проекция — круг радиуса $R$ (граница цилиндра, видимая сверху).</p>'
+ '<p>В направлении линии пересечения с основанием размеры сохраняются ($b=R$), а в перпендикулярном направлении сечение «растянуто» в $1/\\cos\\alpha$ раз — получается $a=R/\\cos\\alpha$. Это и есть эллипс с полуосями $R$ и $R/\\cos\\alpha$.</p>'
+ '</div></details>');
/* === ИНТЕРАКТИВ 1 — 3D-конструктор === */
html += '<div class="wg" id="p2-iv1">'
+ '<div class="wg-header"><span class="wg-badge">3D · конструктор</span><div class="wg-title">Цилиндр $R \\times h$</div></div>'
+ '<div class="wg-help">Меняй радиус $R$ и высоту $h$. Вращай мышью или выбирай вид. После <b>4 разных конфигураций</b> — +10 XP.</div>'
+ '<div class="sliders">'
+ '<label>$R$ (радиус):<b id="p2-iv1-R-v">2.0</b><input type="range" id="p2-iv1-R" min="1" max="4" step="0.1" value="2"></label>'
+ '<label>$h$ (высота):<b id="p2-iv1-h-v">3.0</b><input type="range" id="p2-iv1-h" min="1" max="8" step="0.1" value="3"></label>'
+ '</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:1px solid var(--border);border-radius:9px;padding:8px;text-align:center"><svg id="p2-iv1-svg" viewBox="0 0 480 400" width="100%" style="max-width:480px;height:auto"></svg></div>'
+ '<div class="score-display" style="margin-top:10px;flex-wrap:wrap">'
+ '<span>$P_{осн}=$<b id="p2-iv1-P">—</b></span>'
+ '<span>$S_{осн}=$<b id="p2-iv1-Sb">—</b></span>'
+ '<span>$S_{бок}=$<b id="p2-iv1-Ss">—</b></span>'
+ '<span>$S_{полн}=$<b id="p2-iv1-St">—</b></span>'
+ '<span>$V=$<b id="p2-iv1-V">—</b></span>'
+ '</div>'
+ '<div style="font-size:.78rem;color:var(--muted);margin-top:6px">Конфигураций изучено: <b id="p2-iv1-cnt">0</b> / 4</div>'
+ '</div>';
/* === ИНТЕРАКТИВ 2 — Сечения === */
html += '<div class="wg" id="p2-iv2">'
+ '<div class="wg-header"><span class="wg-badge">визуализатор</span><div class="wg-title">4 типа сечений цилиндра</div></div>'
+ '<div class="wg-help">Выбери тип сечения и крути ползунки. После просмотра всех <b>4 типов</b> — +10 XP.</div>'
+ '<div class="sliders">'
+ '<label>Тип сечения:<b id="p2-iv2-t-v">1</b><input type="range" id="p2-iv2-t" min="1" max="4" step="1" value="1"></label>'
+ '<label>$R$:<b id="p2-iv2-R-v">2.0</b><input type="range" id="p2-iv2-R" min="1" max="4" step="0.1" value="2"></label>'
+ '<label>$h$:<b id="p2-iv2-h-v">3.0</b><input type="range" id="p2-iv2-h" min="1" max="8" step="0.1" value="3"></label>'
+ '<label id="p2-iv2-d-wrap" style="display:none">$d$:<b id="p2-iv2-d-v">0.5</b><input type="range" id="p2-iv2-d" min="0" max="3.9" step="0.1" value="0.5"></label>'
+ '<label id="p2-iv2-a-wrap" style="display:none">$\\alpha$ (°):<b id="p2-iv2-a-v">30</b><input type="range" id="p2-iv2-a" min="0" max="80" step="1" value="30"></label>'
+ '</div>'
+ '<div style="background:var(--card);border:1px solid var(--border);border-radius:9px;padding:8px;text-align:center"><svg id="p2-iv2-svg" viewBox="0 0 380 280" width="100%" style="max-width:380px;height:auto"></svg></div>'
+ '<div id="p2-iv2-info" 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"></div>'
+ '<div style="font-size:.78rem;color:var(--muted);margin-top:6px">Типов сечений просмотрено: <b id="p2-iv2-cnt">0</b> / 4</div>'
+ '</div>';
/* === ИНТЕРАКТИВ 3 — Квикфайр === */
html += '<div class="wg" id="p2-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="p2-iv3-area"></div>'
+ '<div class="score-display" style="margin-top:10px">Верно: <b id="p2-iv3-score">0</b> / 6</div>'
+ '</div>';
/* === ИНТЕРАКТИВ 4 — Тренажёр === */
html += '<div class="wg" id="p2-iv4">'
+ '<div class="wg-header"><span class="wg-badge">тренажёр · 6 задач</span><div class="wg-title">$V$ и $S$ цилиндра</div></div>'
+ '<div class="wg-help">Введи числовой ответ. Допуск $\\pm 0{,}05$. Используй $\\pi \\approx 3{,}14$.</div>'
+ '<div id="p2-iv4-list"></div>'
+ '<div class="score-display" style="margin-top:10px">Решено: <b id="p2-iv4-score">0</b> / 6</div>'
+ '</div>';
html += secNavFor('p2');
html += readButton('p2');
box.innerHTML = html;
renderMath(box);
/* ===== JS-логика интерактивов ===== */
/* IV1 — 3D конструктор */
(function(){
if(!window.G3D) return;
const svg = document.getElementById('p2-iv1-svg');
const elR = document.getElementById('p2-iv1-R');
const elH = document.getElementById('p2-iv1-h');
const vR = document.getElementById('p2-iv1-R-v');
const vH = document.getElementById('p2-iv1-h-v');
const oP = document.getElementById('p2-iv1-P');
const oSb = document.getElementById('p2-iv1-Sb');
const oSs = document.getElementById('p2-iv1-Ss');
const oSt = document.getElementById('p2-iv1-St');
const oV = document.getElementById('p2-iv1-V');
const oCnt = document.getElementById('p2-iv1-cnt');
if(!svg) return;
const scene = G3D.createScene({W:480, H:400, scale:50, camDist:8, rotX:-0.35, rotY:0.7});
const seen = new Set();
let xpGiven = false;
function draw(){
const R = +elR.value, h = +elH.value;
vR.textContent = R.toFixed(1);
vH.textContent = h.toFixed(1);
const mesh = G3D.cylinderMesh(R, h, 32);
const M = G3D.buildRotMatrix(scene);
svg.innerHTML = G3D.renderMesh(mesh, M, scene, {
fillBase:'rgba(252,231,243,.55)',
fillSide:'rgba(219,234,254,.55)',
strokeVisible:'#0f172a',
strokeHidden:'#94a3b8'
});
const P = 2*Math.PI*R;
const Sb = Math.PI*R*R;
const Ss = 2*Math.PI*R*h;
const St = 2*Math.PI*R*(R+h);
const V = Math.PI*R*R*h;
oP.textContent = P.toFixed(2);
oSb.textContent = Sb.toFixed(2);
oSs.textContent = Ss.toFixed(2);
oSt.textContent = St.toFixed(2);
oV.textContent = V.toFixed(2);
const key = R.toFixed(1)+'|'+h.toFixed(1);
seen.add(key);
oCnt.textContent = Math.min(seen.size, 4);
if(seen.size >= 4 && !xpGiven){
xpGiven = true;
addXp(10, 'p2-iv1');
bumpProgress('p2', 15);
const note = document.createElement('div');
note.className = 'feedback ok';
note.innerHTML = '&#10003; +10 XP за изучение 4 разных цилиндров!';
note.style.cssText = 'display:block;margin-top:8px';
const host = document.getElementById('p2-iv1');
if(host) host.appendChild(note);
setTimeout(function(){ try{ note.remove(); }catch(e){} }, 3000);
}
}
draw();
G3D.attachOrbit(svg, scene, draw);
[elR, elH].forEach(function(el){ el.addEventListener('input', draw); });
document.querySelectorAll('#p2-iv1 .g3d-tools .btn').forEach(function(b){
b.addEventListener('click', function(){ G3D.presetView(scene, b.dataset.view, draw); });
});
})();
/* IV2 — Сечения */
(function(){
const svg = document.getElementById('p2-iv2-svg');
const info = document.getElementById('p2-iv2-info');
const elT = document.getElementById('p2-iv2-t');
const elR = document.getElementById('p2-iv2-R');
const elH = document.getElementById('p2-iv2-h');
const elD = document.getElementById('p2-iv2-d');
const elA = document.getElementById('p2-iv2-a');
const vT = document.getElementById('p2-iv2-t-v');
const vR = document.getElementById('p2-iv2-R-v');
const vH = document.getElementById('p2-iv2-h-v');
const vD = document.getElementById('p2-iv2-d-v');
const vA = document.getElementById('p2-iv2-a-v');
const wrapD = document.getElementById('p2-iv2-d-wrap');
const wrapA = document.getElementById('p2-iv2-a-wrap');
const oCnt = document.getElementById('p2-iv2-cnt');
if(!svg) return;
const TYPE_NAMES = {
1:'Параллельное основанию',
2:'Осевое',
3:'Параллельное оси (на расстоянии $d$ от оси)',
4:'Наклонное (под углом $\\alpha$ к основанию)'
};
const seenTypes = new Set();
let xpGiven = false;
function draw(){
const t = +elT.value;
const R = +elR.value, h = +elH.value;
let d = +elD.value, a = +elA.value;
vT.textContent = t;
vR.textContent = R.toFixed(1);
vH.textContent = h.toFixed(1);
vD.textContent = d.toFixed(1);
vA.textContent = a;
/* Динамически ограничиваем d ≤ R - 0.1 */
elD.max = (R - 0.1).toFixed(1);
if(d > +elD.max){ d = +elD.max; elD.value = d; vD.textContent = d.toFixed(1); }
wrapD.style.display = (t === 3) ? '' : 'none';
wrapA.style.display = (t === 4) ? '' : 'none';
/* Масштаб: используем единичный масштаб, чтобы вписать в 380x280 */
const W = 380, H = 280;
const cx = W/2, cy = H/2;
let body = '';
let infoHtml = '<p><b>Тип '+t+': '+TYPE_NAMES[t]+'</b></p>';
if(t === 1){
/* Круг радиуса R */
const sc = Math.min((W-60)/(2*R), (H-60)/(2*R));
const rPx = R*sc;
body += '<circle cx="'+cx+'" cy="'+cy+'" r="'+rPx.toFixed(2)+'" fill="rgba(252,231,243,.55)" stroke="#db2777" stroke-width="2"/>';
/* радиус */
body += '<line x1="'+cx+'" y1="'+cy+'" x2="'+(cx+rPx).toFixed(2)+'" y2="'+cy+'" stroke="#0f172a" stroke-width="1.4" stroke-dasharray="4 3"/>';
body += '<circle cx="'+cx+'" cy="'+cy+'" r="3" fill="#0f172a"/>';
body += '<text x="'+(cx + rPx/2).toFixed(2)+'" y="'+(cy-6)+'" font-size="13" fill="#0f172a" text-anchor="middle" font-weight="700">R = '+R.toFixed(1)+'</text>';
infoHtml += '<p>Фигура — <b>круг</b> радиуса $R='+R.toFixed(1)+'$.</p>';
infoHtml += '<p>Площадь сечения: $S=\\pi R^2 \\approx '+(Math.PI*R*R).toFixed(2)+'$.</p>';
} else if(t === 2){
/* Осевое — прямоугольник 2R × h */
const sc = Math.min((W-60)/(2*R), (H-60)/h);
const wPx = 2*R*sc, hPx = h*sc;
const x0 = cx - wPx/2, y0 = cy - hPx/2;
body += '<rect x="'+x0.toFixed(2)+'" y="'+y0.toFixed(2)+'" width="'+wPx.toFixed(2)+'" height="'+hPx.toFixed(2)+'" fill="rgba(219,234,254,.55)" stroke="#0891b2" stroke-width="2"/>';
/* ось */
body += '<line x1="'+cx+'" y1="'+y0.toFixed(2)+'" x2="'+cx+'" y2="'+(y0+hPx).toFixed(2)+'" stroke="#db2777" stroke-width="1.4" stroke-dasharray="4 3"/>';
body += '<text x="'+cx+'" y="'+(y0-6)+'" font-size="12" fill="#db2777" text-anchor="middle" font-weight="700">ось</text>';
body += '<text x="'+cx+'" y="'+(y0+hPx+16).toFixed(2)+'" font-size="13" fill="#0f172a" text-anchor="middle" font-weight="700">2R = '+(2*R).toFixed(1)+'</text>';
body += '<text x="'+(x0-6).toFixed(2)+'" y="'+(cy+4)+'" font-size="13" fill="#0f172a" text-anchor="end" font-weight="700">h = '+h.toFixed(1)+'</text>';
infoHtml += '<p>Фигура — <b>прямоугольник</b> со сторонами $2R='+(2*R).toFixed(1)+'$ и $h='+h.toFixed(1)+'$.</p>';
infoHtml += '<p>Площадь сечения: $S=2Rh = '+(2*R*h).toFixed(2)+'$.</p>';
} else if(t === 3){
/* Параллельное оси, не осевое — прямоугольник со сторонами 2√(R²-d²) и h */
const w = 2*Math.sqrt(Math.max(0, R*R - d*d));
const sc = Math.min((W-60)/(2*R), (H-60)/h);
const wPx = w*sc, hPx = h*sc;
/* нарисуем «пунктиром» полный осевой прямоугольник 2R×h для сравнения и закрасим нашу полосу */
const fullW = 2*R*sc;
const fx0 = cx - fullW/2, fy0 = cy - hPx/2;
body += '<rect x="'+fx0.toFixed(2)+'" y="'+fy0.toFixed(2)+'" width="'+fullW.toFixed(2)+'" height="'+hPx.toFixed(2)+'" fill="none" stroke="#94a3b8" stroke-width="1.2" stroke-dasharray="4 3"/>';
const x0 = cx - wPx/2, y0 = cy - hPx/2;
body += '<rect x="'+x0.toFixed(2)+'" y="'+y0.toFixed(2)+'" width="'+wPx.toFixed(2)+'" height="'+hPx.toFixed(2)+'" fill="rgba(219,234,254,.55)" stroke="#0891b2" stroke-width="2"/>';
body += '<text x="'+cx+'" y="'+(y0+hPx+16).toFixed(2)+'" font-size="12" fill="#0f172a" text-anchor="middle" font-weight="700">2√(R²−d²) = '+w.toFixed(2)+'</text>';
body += '<text x="'+(x0-6).toFixed(2)+'" y="'+(cy+4)+'" font-size="13" fill="#0f172a" text-anchor="end" font-weight="700">h = '+h.toFixed(1)+'</text>';
infoHtml += '<p>Фигура — <b>прямоугольник</b> со сторонами $2\\sqrt{R^2-d^2}\\approx '+w.toFixed(2)+'$ и $h='+h.toFixed(1)+'$.</p>';
infoHtml += '<p>При $d='+d.toFixed(1)+'$, $R='+R.toFixed(1)+'$: ширина = $2\\sqrt{'+(R*R).toFixed(2)+'-'+(d*d).toFixed(2)+'}='+w.toFixed(2)+'$.</p>';
infoHtml += '<p>Площадь сечения: $S='+w.toFixed(2)+' \\cdot '+h.toFixed(1)+' = '+(w*h).toFixed(2)+'$.</p>';
} else {
/* Наклонное — эллипс с полуосями R и R/cos(α) */
const alpha = a * Math.PI/180;
const aMaj = R / Math.cos(alpha); /* большая полуось */
const sc = Math.min((W-60)/(2*aMaj), (H-60)/(2*R));
const aPx = aMaj*sc, bPx = R*sc;
body += '<ellipse cx="'+cx+'" cy="'+cy+'" rx="'+aPx.toFixed(2)+'" ry="'+bPx.toFixed(2)+'" fill="rgba(254,243,199,.55)" stroke="#d97706" stroke-width="2"/>';
/* большая ось */
body += '<line x1="'+(cx-aPx).toFixed(2)+'" y1="'+cy+'" x2="'+(cx+aPx).toFixed(2)+'" y2="'+cy+'" stroke="#0f172a" stroke-width="1.2" stroke-dasharray="4 3"/>';
/* малая ось */
body += '<line x1="'+cx+'" y1="'+(cy-bPx).toFixed(2)+'" x2="'+cx+'" y2="'+(cy+bPx).toFixed(2)+'" stroke="#0f172a" stroke-width="1.2" stroke-dasharray="4 3"/>';
body += '<text x="'+(cx + aPx/2).toFixed(2)+'" y="'+(cy-6)+'" font-size="12" fill="#0f172a" text-anchor="middle" font-weight="700">a = '+aMaj.toFixed(2)+'</text>';
body += '<text x="'+(cx+6)+'" y="'+(cy - bPx/2).toFixed(2)+'" font-size="12" fill="#0f172a" font-weight="700">b = '+R.toFixed(1)+'</text>';
infoHtml += '<p>Фигура — <b>эллипс</b> с полуосями $b=R='+R.toFixed(1)+'$ и $a=R/\\cos\\alpha = '+R.toFixed(1)+'/\\cos '+a+'° \\approx '+aMaj.toFixed(2)+'$.</p>';
infoHtml += '<p>Площадь эллипса: $S=\\pi ab \\approx '+(Math.PI*R*aMaj).toFixed(2)+'$.</p>';
}
svg.innerHTML = body;
info.innerHTML = infoHtml;
try{ renderMath(info); }catch(e){}
seenTypes.add(t);
oCnt.textContent = seenTypes.size;
if(seenTypes.size >= 4 && !xpGiven){
xpGiven = true;
addXp(10, 'p2-iv2');
bumpProgress('p2', 15);
const note = document.createElement('div');
note.className = 'feedback ok';
note.innerHTML = '&#10003; +10 XP за разбор всех 4 типов сечений!';
note.style.cssText = 'display:block;margin-top:8px';
const host = document.getElementById('p2-iv2');
if(host) host.appendChild(note);
setTimeout(function(){ try{ note.remove(); }catch(e){} }, 3000);
}
}
draw();
[elT, elR, elH, elD, elA].forEach(function(el){ el.addEventListener('input', draw); });
})();
/* IV3 — Квикфайр */
(function(){
const tasks = [
{ q:'Сечение цилиндра плоскостью, <b>параллельной основанию</b>:', a:'circle' },
{ q:'Сечение цилиндра <b>осевой</b> плоскостью:', a:'rect' },
{ q:'Сечение цилиндра плоскостью, <b>перпендикулярной оси</b>:', a:'circle' },
{ q:'Сечение цилиндра плоскостью, <b>параллельной оси, но не проходящей через ось</b>:', a:'rect' },
{ q:'Сечение цилиндра плоскостью, <b>наклонной к основанию под углом $60°$</b> (пересекает все образующие):', a:'ellipse' },
{ q:'Сечение цилиндра плоскостью, проходящей <b>через ось и наклонённой к основанию</b>:', a:'rect' }
];
const LABELS = {circle:'Круг', rect:'Прямоугольник', ellipse:'Эллипс'};
const area = document.getElementById('p2-iv3-area');
const scoreEl = document.getElementById('p2-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="p2-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="circle">Круг</button>'
+ '<button class="btn" data-i="'+i+'" data-v="rect">Прямоугольник</button>'
+ '<button class="btn" data-i="'+i+'" data-v="ellipse">Эллипс</button>'
+ '</div>'
+ '<div class="feedback" id="p2-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('p2-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, 'p2-iv3');
bumpProgress('p2', 25);
}
}
} else {
feedback(fb, false, '&#10007; Неверно. Подсказка: подумай, как плоскость пересекает образующие.');
}
});
});
})();
/* IV4 — Тренажёр */
(function(){
const PI = 3.14;
const tasks = [
{ q:'Цилиндр $R=2$, $h=5$. Найди $V$ (используй $\\pi\\approx 3{,}14$).', a:+(PI*4*5).toFixed(2), tol:0.05 }, /* 62.80 */
{ q:'Цилиндр $R=3$, $h=4$. Найди $S_{бок}$ ($\\pi\\approx 3{,}14$).', a:+(2*PI*3*4).toFixed(2), tol:0.05 }, /* 75.36 */
{ q:'Цилиндр $R=5$, $h=2$. Найди $S_{полн}$ ($\\pi\\approx 3{,}14$).', a:+(2*PI*5*7).toFixed(2), tol:0.05 }, /* 219.80 */
{ q:'$V$ цилиндра равен $100\\pi$, высота $h=4$. Найди радиус $R$.', a:5, tol:0.05 },
{ q:'Цилиндр $R=2$, $h=3$. Найди длину окружности основания ($\\pi\\approx 3{,}14$).', a:+(2*PI*2).toFixed(2), tol:0.05 }, /* 12.56 */
{ q:'Цилиндр $R=4$ пересечён плоскостью под углом $60°$ к основанию. Найди <b>большую полуось</b> эллипса в сечении.', a:8, tol:0.05 }
];
const list = document.getElementById('p2-iv4-list');
const scoreEl = document.getElementById('p2-iv4-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="p2-iv4-inp-'+i+'" placeholder="число" style="width:140px">'
+ '<button class="btn primary" data-i="'+i+'">Проверить</button>'
+ '</div>'
+ '<div class="feedback" id="p2-iv4-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('p2-iv4-inp-'+i);
const fb = document.getElementById('p2-iv4-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, 'p2-iv4');
bumpProgress('p2', 25);
setTimeout(function(){ achievement('p2_done'); }, 400);
}
}
} else {
feedback(fb, false, '&#10007; Не точно. Пересчитай аккуратно ($\\pi\\approx 3{,}14$).');
}
});
});
})();
wireReadBtn('p2');
}
/* ===== ФИНАЛ РАЗДЕЛА 1 — Шпаргалка + 5 боссов + ачивка ===== */
function buildFinal1(){
const box = document.getElementById('final1-body');
if(!box) return;
let html = '';
/* Часть А — Шпаргалка раздела (2 mini-карточки по числу § в разделе) */
html += '<div class="card">'
+ '<div class="card-header">'
+ '<div class="card-icon theory">' + ICONS.theory + '</div>'
+ '<div class="card-title">Шпаргалка раздела 1</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">'
+ '<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"><path d="M3 7l9-4 9 4v10l-9 4-9-4z"/><path d="M3 7l9 4 9-4"/><path d="M12 11v10"/></svg>'
+ '<div style="font-family:\'Unbounded\',sans-serif;font-weight:700;color:var(--pri2);font-size:.92rem">§ 1 · Призма</div>'
+ '</div>'
+ '<div style="font-size:.94rem;line-height:1.55">$V = S_{осн}\\cdot h$ — для любой призмы. Для прямой: $S_{бок}=P_{осн}\\cdot h$. Прямоугольный параллелепипед: $d^2=a^2+b^2+c^2$. Виды: прямая, правильная, наклонная, параллелепипед, куб.</div>'
+ '</div>'
+ '<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"><ellipse cx="12" cy="5" rx="8" ry="3"/><path d="M4 5v14a8 3 0 0 0 16 0V5"/></svg>'
+ '<div style="font-family:\'Unbounded\',sans-serif;font-weight:700;color:var(--pri2);font-size:.92rem">§ 2 · Цилиндр</div>'
+ '</div>'
+ '<div style="font-size:.94rem;line-height:1.55">$V=\\pi R^2 h$, $S_{бок}=2\\pi Rh$, $S_{полн}=2\\pi R(R+h)$. Сечения: круг ($\\perp$ оси), прямоугольник (через ось или $\\parallel$ оси), эллипс (наклонная плоскость).</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">Боссы раздела 1</div>'
+ '<div class="card-num">5</div>'
+ '</div>'
+ '<div class="card-body">'
+ '<p>5 интегрированных задач — каждая комбинирует темы § 1 и § 2. За каждого побеждённого босса: <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$ (для боссов 2 и 5 указан в условии).</p>'
+ '</div>'
+ '</div>';
html += '<div id="r1-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="r1-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="r1-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="r1-boss-overall-fill" style="height:100%;width:0%;background:linear-gradient(90deg,#0891b2,#7c3aed);transition:width .35s"></div>'
+ '</div>'
+ '<div id="r1-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.05rem;margin-bottom:6px">Мастер призмы и цилиндра</div>'
+ '<div style="font-size:.92rem;margin-bottom:10px">Раздел 1 пройден! Все 5 боссов повержены. +50 XP бонус.</div>'
+ '<a class="btn primary" href="/textbook/geometry-11-ch2" style="text-decoration:none">Дальше: Раздел 2 <svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></a>'
+ '</div>'
+ '</div>';
html += secNav('p2', null);
box.innerHTML = html;
renderMath(box);
/* === Боссы === */
const BOSSES = [
{
n:1, color:'#0891b2',
title:'Циклоп Параллелепипеда',
tag:'§ 1',
q:'В прямоугольном параллелепипеде $a=3$, $b=4$, $c=12$. Найдите длину главной диагонали.',
ans:13, tol:0.05,
hint:'$d=\\sqrt{a^2+b^2+c^2}=\\sqrt{9+16+144}=\\sqrt{169}=13$.'
},
{
n:2, color:'#10b981',
title:'Минотавр Куба',
tag:'§ 1',
q:'Куб со стороной $4$. Найдите длину диагонали куба (допуск $\\pm 0{,}05$).',
ans:6.93, tol:0.05,
hint:'Диагональ куба: $d=a\\sqrt{3}=4\\sqrt{3}\\approx 6{,}928$.'
},
{
n:3, color:'#7c3aed',
title:'Гарпия Цилиндра',
tag:'§ 2',
q:'Цилиндр с $R=5$, $h=10$. Найдите объём (используйте $\\pi=3{,}14$).',
ans:785, tol:0.05,
hint:'$V=\\pi R^2 h = 3{,}14\\cdot 25\\cdot 10 = 785$.'
},
{
n:4, color:'#dc2626',
title:'Дракон Сечений',
tag:'§ 2 + § 1',
q:'Цилиндр с $R=6$ пересечён плоскостью под углом $60°$ к плоскости основания (плоскость пересекает обе окружности оснований). Найдите большую полуось эллипса в сечении.',
ans:12, tol:0.05,
hint:'При наклоне на угол $\\alpha$ малая полуось эллипса равна $R$, большая — $a=R/\\cos\\alpha = 6/\\cos 60° = 6/0{,}5 = 12$.'
},
{
n:5, color:'#f59e0b',
title:'Мастер 3D-форм',
tag:'синтез § 1 + § 2',
q:'В цилиндр с $R=6$ и $h=8$ вписана правильная шестиугольная призма (вершины основания лежат на окружности). Найдите объём призмы (допуск $\\pm 0{,}1$).',
ans:748.25, tol:0.1,
hint:'Сторона правильного 6-угольника, вписанного в окружность радиуса $R$, равна $R=6$. Его площадь: $S=\\tfrac{3\\sqrt{3}}{2}\\cdot 36 = 54\\sqrt{3}$. Объём: $V=S\\cdot h=54\\sqrt{3}\\cdot 8 = 432\\sqrt{3}\\approx 748{,}25$.'
}
];
const cont = document.getElementById('r1-bosses-container');
const STATE_KEY = 'geometry11_ch1_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="boss1-'+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"/></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="boss1-'+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="boss1-'+b.n+'-ans" class="tinp" style="width:140px;text-align:center" step="0.01" placeholder="число">'
+ '<button class="btn primary" id="boss1-'+b.n+'-go" style="background:'+b.color+';border-color:'+b.color+'">Атаковать</button>'
+ '<button class="btn" id="boss1-'+b.n+'-hint">Подсказка</button>'
+ '</div>'
+ '<div class="feedback" id="boss1-'+b.n+'-fb"></div>'
+ '</div>';
}).join('');
renderMath(cont);
function refreshOverall(){
const won = BOSS_STATE.filter(s => s.defeated).length;
const txt = document.getElementById('r1-boss-overall');
const fill = document.getElementById('r1-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('r1-final-reward');
if(reward && reward.style.display === 'none'){
reward.style.display = 'block';
if(!STATE.achievements.has('r1_done')){
achievement('r1_done','Мастер призмы и цилиндра');
addXp(50, 'r1-bonus');
bumpProgress('final1', 30);
if(window.confetti){ try{ confetti(); }catch(e){} }
}
}
}
}
BOSSES.forEach((b, idx)=>{
const card = document.getElementById('boss1-'+b.n+'-card');
const goBtn = document.getElementById('boss1-'+b.n+'-go');
const hintBtn= document.getElementById('boss1-'+b.n+'-hint');
const ansInp = document.getElementById('boss1-'+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('boss1-'+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-r1-'+b.n);
bumpProgress('final1', 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('boss1-'+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();
}
/* ===== 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>Это параграф раздела ' + 1 + '. Полное наполнение (теория + 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);
}
/* ===== 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>