Files
Learn_System/frontend/textbooks/geometry_8_ch1.html
T
Maxim Dolgolyov 99d7bf3d03 feat(geom8): Wave 1 Главы 1 — §1-§4 с интерактивами
§1 Многоугольники: SVG-конструктор с drag-вершинами, калькулятор диагоналей,
DnD-сортер фигур, тренажёр периметра, босс (4 задачи).
§2 Сумма углов: анимация триангуляции, калькулятор, обратная задача, DnD
правильные ↔ углы, босс.
§3 Внешние углы: SVG свёртка в точку (360°), калькулятор, тренажёр, mini-quiz, босс.
§4 Параллелограмм: SVG-конструктор (drag B/D), DnD, пошаговое доказательство,
тренажёр углы/периметр, босс.

File: 766 → 1910 LOC.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 19:07:22 +03:00

1911 lines
153 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>Геометрия 8 · Глава 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>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<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}
/* HEADER */
.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,235,180,.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 GRID */
.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 */
.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:'&#9632;';position:absolute;right:-10px;top:-20px;font-size:clamp(2rem,8vw,5.5rem);font-weight:900;color:var(--pri);opacity:.10;line-height:1;pointer-events:none}
.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(217,119,6,.32)}
.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(217,119,6,.18);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(217,119,6,.22);font-family:'Unbounded',sans-serif}
/* PARA SELECTOR */
.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(160px,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:.88rem;font-weight:700;color:var(--text);line-height:1.3;margin-bottom:8px}
.psel-prog{height:4px;background:rgba(217,119,6,.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,#fff5e1,#fef3c7)}
.psel-card.final .psel-num{color:var(--warn)}
/* SECTION COLORS — amber/orange/warm palette */
.sec[id="sec-p1"] { --sec-acc:#d97706; --sec-acc-d:#b45309; --sec-acc-soft:#fef3c7; }
.sec[id="sec-p2"] { --sec-acc:#f59e0b; --sec-acc-d:#d97706; --sec-acc-soft:#fef9c3; }
.sec[id="sec-p3"] { --sec-acc:#f97316; --sec-acc-d:#ea580c; --sec-acc-soft:#ffedd5; }
.sec[id="sec-p4"] { --sec-acc:#dc2626; --sec-acc-d:#b91c1c; --sec-acc-soft:#fee2e2; }
.sec[id="sec-p5"] { --sec-acc:#e11d48; --sec-acc-d:#be123c; --sec-acc-soft:#ffe4e6; }
.sec[id="sec-p6"] { --sec-acc:#c026d3; --sec-acc-d:#a21caf; --sec-acc-soft:#fae8ff; }
.sec[id="sec-p7"] { --sec-acc:#7c3aed; --sec-acc-d:#6d28d9; --sec-acc-soft:#ede9fe; }
.sec[id="sec-p8"] { --sec-acc:#2563eb; --sec-acc-d:#1d4ed8; --sec-acc-soft:#dbeafe; }
.sec[id="sec-p9"] { --sec-acc:#0891b2; --sec-acc-d:#0e7490; --sec-acc-soft:#cffafe; }
.sec[id="sec-p10"] { --sec-acc:#059669; --sec-acc-d:#047857; --sec-acc-soft:#d1fae5; }
.sec[id="sec-p11"] { --sec-acc:#16a34a; --sec-acc-d:#15803d; --sec-acc-soft:#dcfce7; }
.sec[id="sec-p12"] { --sec-acc:#d97706; --sec-acc-d:#b45309; --sec-acc-soft:#fef3c7; }
.sec[id="sec-p13"] { --sec-acc:#f59e0b; --sec-acc-d:#d97706; --sec-acc-soft:#fef9c3; }
.sec[id="sec-p14"] { --sec-acc:#f97316; --sec-acc-d:#ea580c; --sec-acc-soft:#ffedd5; }
.sec[id="sec-p15"] { --sec-acc:#dc2626; --sec-acc-d:#b91c1c; --sec-acc-soft:#fee2e2; }
.sec[id="sec-p16"] { --sec-acc:#e11d48; --sec-acc-d:#be123c; --sec-acc-soft:#ffe4e6; }
.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.7rem;font-weight:800;color:var(--sec-acc-d,var(--pri2));letter-spacing:-.01em;line-height:1.25}
/* CARDS */
.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(217,119,6,.06);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(217,119,6,.12)}
.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.class{background:#3b82f6}.card-icon.home{background:#f97316}
.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}
/* WIDGET */
.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}
/* BUTTONS */
.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))}
.btn.small{padding:5px 11px;font-size:.78rem}
/* INPUTS */
.tinp{padding:8px 12px;border:1.5px solid var(--border);border-radius:8px;background:var(--card);color:var(--text);transition:border-color .15s}
.tinp:focus{outline:0;border-color:var(--sec-acc,var(--pri));box-shadow:0 0 0 3px var(--sec-acc-soft,var(--pri-soft))}
/* FEEDBACK */
.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)}
/* SIDEBAR */
.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 */
.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(245,158,11,.15);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}
/* SPOILER */
.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}
.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}
/* TABLES */
.tbl{width:100%;border-collapse:collapse;margin:12px 0;font-size:.88rem}
.tbl th,.tbl td{padding:7px 10px;border:1px solid var(--border);text-align:center}
.tbl th{background:var(--sec-acc-soft,var(--pri-soft));color:var(--sec-acc-d,var(--pri2));font-weight:700}
/* ACH popup */
.ach-popup{position:fixed;top:80px;right:18px;background:linear-gradient(135deg,#d97706,#f59e0b);color:#fff;padding:12px 18px;border-radius:11px;font-weight:700;font-size:.9rem;box-shadow:0 8px 28px rgba(217,119,6,.45);z-index:1002;display:none;align-items:center;gap:8px;animation:achIn .45s cubic-bezier(.34,1.56,.64,1) forwards;max-width:340px}
.ach-popup.show{display:flex}
@keyframes achIn{from{opacity:0;transform:translateX(40px)}to{opacity:1;transform:none}}
/* DRAG & DROP */
.dnd-pool{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:14px;padding:10px;border:1.5px dashed var(--border);border-radius:10px;min-height:54px;transition:border-color .18s,background .18s}
.dnd-pool.over{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft));border-style:solid}
.dnd-pool.col{flex-direction:column;align-items:stretch}
.dnd-pool.col .dnd-chip{width:auto}
.dnd-chip{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:var(--card);border:1.5px solid var(--border);border-radius:10px;cursor:grab;user-select:none;font-size:.92rem;line-height:1.4;transition:transform .12s,box-shadow .12s,border-color .12s;touch-action:none;max-width:100%}
.dnd-chip:hover{transform:translateY(-1px);border-color:var(--sec-acc,var(--pri));box-shadow:var(--sh)}
.dnd-chip:active{cursor:grabbing}
.dnd-chip.armed{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft));box-shadow:0 0 0 3px rgba(217,119,6,.22);transform:translateY(-1px)}
.dnd-chip.dragging{opacity:.28}
.dnd-chip.placed{background:var(--sec-acc-soft,var(--pri-soft));border-color:var(--sec-acc,var(--pri))}
.dnd-chip .dnd-x{padding:0 5px;color:var(--muted);font-weight:700;font-size:1.05rem;border-radius:4px;cursor:pointer}
.dnd-chip .dnd-x:hover{color:var(--bad,var(--fail));background:var(--fail-bg)}
.drop-box{background:var(--card);border:1.5px dashed var(--border);border-radius:10px;padding:10px;min-height:90px;transition:border-color .15s,background .15s}
.drop-box:hover{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft))}
.drop-box h5{font-family:'Unbounded',sans-serif;font-size:.78rem;color:var(--sec-acc-d,var(--pri2));margin-bottom:8px;text-transform:uppercase;letter-spacing:.05em}
.drop-box.over{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft));border-style:solid;transform:scale(1.015)}
.drop-items{display:flex;flex-wrap:wrap;gap:6px;min-height:32px}
.dnd-hint{font-size:.78rem;color:var(--muted);margin-bottom:8px;display:flex;align-items:center;gap:6px}
.dnd-hint svg{width:14px;height:14px;flex-shrink:0}
.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}
/* SIDEBAR DRAWER for narrow viewports */
.col-side-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.42);z-index:9990;display:none;animation:fadeIn .18s ease}
.col-side-backdrop.show{display:block}
@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}
}
/* GLOSSARY tooltip */
.gloss-term{border-bottom:1.5px dotted var(--sec-acc,var(--pri));cursor:help;color:var(--sec-acc-d,var(--pri2));font-weight:600;padding:0 1px}
.gloss-term:hover{background:var(--sec-acc-soft,var(--pri-soft));border-radius:3px}
.gloss-tip{position:fixed;max-width:320px;padding:11px 14px;background:var(--card);border:1.5px solid var(--sec-acc,var(--pri));border-radius:11px;font-size:.84rem;line-height:1.55;box-shadow:0 12px 32px rgba(0,0,0,.18);z-index:9994;display:none;pointer-events:none;color:var(--text)}
.gloss-tip.show{display:block;animation:tipIn .15s ease}
.gloss-tip b{color:var(--sec-acc-d,var(--pri2));font-size:.92rem}
@keyframes tipIn{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
/* SEARCH MODAL */
.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;animation:fadeIn .15s ease}
.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-left:0;border-right:0;border-top: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;background:var(--card-soft,transparent)}
.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}
</style>
</head>
<body>
<header class="hdr">
<div class="hdr-row">
<div>
<h1>Геометрия 8 · Глава 1</h1>
<div class="hdr-sub">Многоугольники</div>
</div>
<div class="hdr-side">
<a href="/textbook/geometry-8" class="hdr-btn" title="К Геометрии 8 — все главы">
<svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>
К геометрии 8
</a>
<button id="search-btn" class="hdr-btn" title="Поиск (Ctrl+K)">
<svg class="ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m21 21-4-4"/></svg>
<span>Поиск</span>
</button>
<button id="sidebar-btn" class="hdr-btn" title="Шпаргалка">
<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" title="Сменить тему">
<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>В этой главе мы изучаем <b>выпуклые многоугольники</b> и их диагонали, доказываем формулу суммы углов, исследуем параллелограммы, ромбы, прямоугольники и квадраты. Кульминация — <b>теорема Фалеса</b> и <b>средние линии</b>.</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" title="Опыт"></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="&#9653;"><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="&#8721;&#8736;"><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-p3" class="sec" data-watermark="360"><div class="sec-header"><span class="sec-num">§ 3</span><h2 class="sec-h">Сумма внешних углов выпуклого многоугольника</h2></div><div id="p3-body"></div></section>
<section id="sec-p4" class="sec" data-watermark="&#9649;"><div class="sec-header"><span class="sec-num">§ 4</span><h2 class="sec-h">Параллелограмм</h2></div><div id="p4-body"></div></section>
<section id="sec-p5" class="sec" data-watermark="&#8741;&#8741;"><div class="sec-header"><span class="sec-num">§ 5</span><h2 class="sec-h">Свойства параллелограмма</h2></div><div id="p5-body"></div></section>
<section id="sec-p6" class="sec" data-watermark="&#8660;"><div class="sec-header"><span class="sec-num">§ 6</span><h2 class="sec-h">Признаки параллелограмма</h2></div><div id="p6-body"></div></section>
<section id="sec-p7" class="sec" data-watermark="&#9645;"><div class="sec-header"><span class="sec-num">§ 7</span><h2 class="sec-h">Прямоугольник. Свойство диагоналей</h2></div><div id="p7-body"></div></section>
<section id="sec-p8" class="sec" data-watermark="90&#176;"><div class="sec-header"><span class="sec-num">§ 8</span><h2 class="sec-h">Признак прямоугольника</h2></div><div id="p8-body"></div></section>
<section id="sec-p9" class="sec" data-watermark="&#9671;"><div class="sec-header"><span class="sec-num">§ 9</span><h2 class="sec-h">Ромб. Свойство диагоналей ромба. Признаки ромба</h2></div><div id="p9-body"></div></section>
<section id="sec-p10" class="sec" data-watermark="&#9632;"><div class="sec-header"><span class="sec-num">§ 10</span><h2 class="sec-h">Квадрат. Свойства квадрата</h2></div><div id="p10-body"></div></section>
<section id="sec-p11" class="sec" data-watermark="&#8942;"><div class="sec-header"><span class="sec-num">§ 11</span><h2 class="sec-h">Теорема Фалеса. Деление отрезка на равные части</h2></div><div id="p11-body"></div></section>
<section id="sec-p12" class="sec" data-watermark="m"><div class="sec-header"><span class="sec-num">§ 12</span><h2 class="sec-h">Свойство медиан треугольника</h2></div><div id="p12-body"></div></section>
<section id="sec-p13" class="sec" data-watermark="&#8869;"><div class="sec-header"><span class="sec-num">§ 13</span><h2 class="sec-h">Свойство средней линии треугольника</h2></div><div id="p13-body"></div></section>
<section id="sec-p14" class="sec" data-watermark="&#8767;"><div class="sec-header"><span class="sec-num">§ 14</span><h2 class="sec-h">Трапеция. Свойство средней линии</h2></div><div id="p14-body"></div></section>
<section id="sec-p15" class="sec" data-watermark="&#8736;&#8736;"><div class="sec-header"><span class="sec-num">§ 15</span><h2 class="sec-h">Свойство углов и диагоналей равнобедренной трапеции</h2></div><div id="p15-body"></div></section>
<section id="sec-p16" class="sec" data-watermark="&#8801;"><div class="sec-header"><span class="sec-num">§ 16</span><h2 class="sec-h">Признаки равнобедренной трапеции</h2></div><div id="p16-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)">Финал главы</span><h2 class="sec-h">Итоги. Боссы главы 1</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">Интерактивный учебник «Геометрия 8» · Глава 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="gloss-tip" class="gloss-tip"></div>
<div id="search-modal" class="search-modal" role="dialog" aria-label="Поиск по главе">
<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>&#8593;&#8595;</kbd> навигация</span>
<span><kbd>Enter</kbd> открыть</span>
<span><kbd>Esc</kbd> закрыть</span>
</div>
</div>
</div>
<script>
'use strict';
/* STATE */
const STATE = {
current: 'p1',
progress: { p1:0,p2:0,p3:0,p4:0,p5:0,p6:0,p7:0,p8:0,p9:0,p10:0,p11:0,p12:0,p13:0,p14:0,p15:0,p16:0,final1:0 },
achievements: new Map(),
xp: 0,
level: 1,
};
function calcLevel(xp){ return Math.floor(Math.sqrt((xp || 0) / 100)) + 1; }
function _xpForLevel(lv){ return (lv - 1) * (lv - 1) * 100; }
const ACH_LABELS = {
start: 'Начало главы 1!',
ch1_done: 'Многоугольники изучены!',
};
function loadProgress(){
try{
const s = localStorage.getItem('geometry8_ch1_progress');
if(s) Object.assign(STATE.progress, JSON.parse(s));
const a = localStorage.getItem('geometry8_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('geometry8_xp') || 0);
STATE.level = calcLevel(STATE.xp);
}catch(e){}
}
function saveProgress(){
try{
localStorage.setItem('geometry8_ch1_progress', JSON.stringify(STATE.progress));
localStorage.setItem('geometry8_ch1_achievements', JSON.stringify(Object.fromEntries(STATE.achievements)));
localStorage.setItem('geometry8_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);
}
/* Server sync */
const _TB_SLUG = 'geometry-8-ch1';
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, 'geometry8-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); }
if(window.confetti) try{ confetti(); }catch(e){}
}
}
const TOTAL_PARAS = 17; /* 16 параграфов + финал */
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);
}
/* PARAGRAPH LIST */
const PARAS = [
{ id:'p1', num:'§ 1', name:'Выпуклые многоугольники', sub:'Диагональ и периметр' },
{ id:'p2', num:'§ 2', name:'Сумма углов', sub:'Формула (n-2)·180°' },
{ id:'p3', num:'§ 3', name:'Сумма внешних углов', sub:'Всегда 360°' },
{ id:'p4', num:'§ 4', name:'Параллелограмм', sub:'Определение и виды' },
{ id:'p5', num:'§ 5', name:'Свойства параллелограмма', sub:'Стороны и диагонали' },
{ id:'p6', num:'§ 6', name:'Признаки параллелограмма', sub:'3 признака' },
{ id:'p7', num:'§ 7', name:'Прямоугольник', sub:'Свойство диагоналей' },
{ id:'p8', num:'§ 8', name:'Признак прямоугольника', sub:'Равные диагонали' },
{ id:'p9', num:'§ 9', name:'Ромб', sub:'Диагонали ромба' },
{ id:'p10', num:'§ 10', name:'Квадрат', sub:'Свойства квадрата' },
{ id:'p11', num:'§ 11', name:'Теорема Фалеса', sub:'Деление отрезка' },
{ id:'p12', num:'§ 12', name:'Медианы треугольника', sub:'Точка пересечения' },
{ id:'p13', num:'§ 13', name:'Средняя линия треугольника', sub:'Параллельность и длина' },
{ id:'p14', num:'§ 14', name:'Трапеция', sub:'Средняя линия трапеции' },
{ id:'p15', num:'§ 15', name:'Равнобедренная трапеция', sub:'Углы и диагонали' },
{ id:'p16', num:'§ 16', name:'Признаки равнобедр. трапеции', sub:'Два признака' },
{ id:'final1', num:'★', name:'Финал главы', sub:'Итоги · Боссы', final:true },
];
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(), p3:()=>buildP3(), p4:()=>buildP4(),
p5:()=>buildP5stub(), p6:()=>buildP6stub(), p7:()=>buildP7stub(), p8:()=>buildP8stub(),
p9:()=>buildP9stub(), p10:()=>buildP10stub(), p11:()=>buildP11stub(), p12:()=>buildP12stub(),
p13:()=>buildP13stub(), p14:()=>buildP14stub(), p15:()=>buildP15stub(), p16:()=>buildP16stub(),
final1:()=>buildFinal1stub(),
};
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);
setTimeout(()=>{ try{ wrapGlossary(el); }catch(e){} }, 60);
markLastPara(id);
}
/* SIDEBAR DATA */
const SIDEBARS = {
p1: { title:'Шпаргалка § 1', rows:[['Многоугольник','замкнутая ломаная из $n$ звеньев'],['Диагональ','отрезок, соединяющий несмежные вершины'],['Число диагоналей','$\\dfrac{n(n-3)}{2}'],['Периметр','сумма всех сторон']] },
p2: { title:'Шпаргалка § 2', rows:[['Сумма углов','$(n-2)\\cdot 180^{\\circ}$'],['Треугольник','$180^{\\circ}$'],['Четырёхугольник','$360^{\\circ}$'],['Пятиугольник','$540^{\\circ}$']] },
p3: { title:'Шпаргалка § 3', rows:[['Сумма внешних углов','всегда $360^{\\circ}$'],['Независимо от $n$','для любого выпуклого многоугольника']] },
p4: { title:'Шпаргалка § 4', rows:[['Параллелограмм','четырёхугольник с двумя парами параллельных сторон'],['Обозначение','$\\Box ABCD$']] },
p5: { title:'Шпаргалка § 5', rows:[['Противоположные стороны','равны'],['Противоположные углы','равны'],['Диагонали','делятся точкой пересечения пополам']] },
p6: { title:'Шпаргалка § 6', rows:[['Признак 1','две пары равных противоположных сторон'],['Признак 2','одна пара параллельных и равных сторон'],['Признак 3','диагонали делятся пополам']] },
p7: { title:'Шпаргалка § 7', rows:[['Прямоугольник','параллелограмм с прямым углом'],['Диагонали','равны между собой']] },
p8: { title:'Шпаргалка § 8', rows:[['Признак прямоугольника','параллелограмм с равными диагоналями']] },
p9: { title:'Шпаргалка § 9', rows:[['Ромб','параллелограмм с равными сторонами'],['Диагонали ромба','взаимно перпендикулярны и делят углы пополам']] },
p10: { title:'Шпаргалка § 10', rows:[['Квадрат','прямоугольник и ромб одновременно'],['Диагональ квадрата','$d = a\\sqrt{2}$']] },
p11: { title:'Шпаргалка § 11', rows:[['Теорема Фалеса','параллельные прямые отсекают равные отрезки'],['Применение','деление отрезка на $n$ равных частей']] },
p12: { title:'Шпаргалка § 12', rows:[['Медиана','отрезок от вершины до середины стороны'],['Точка пересечения','делит каждую медиану в отношении $2:1$']] },
p13: { title:'Шпаргалка § 13', rows:[['Средняя линия','соединяет середины двух сторон'],['Длина','равна половине основания'],['Параллельна','основанию треугольника']] },
p14: { title:'Шпаргалка § 14', rows:[['Трапеция','один угол 180°? нет — четырёхугольник с одной парой параллельных сторон'],['Средняя линия','параллельна основаниям, равна их полусумме: $m=\\dfrac{a+b}{2}$']] },
p15: { title:'Шпаргалка § 15', rows:[['Равнобедренная','углы при основании равны'],['Диагонали','равны']] },
p16: { title:'Шпаргалка § 16', rows:[['Признак 1','углы при одном основании равны'],['Признак 2','диагонали равны']] },
final1: { title:'Финал главы', rows:[['16 параграфов','многоугольники изучены'],['Боссы','по одному на каждую тему']] },
};
const TIPS = [
{ sec:'p1', html:'Число диагоналей $n$-угольника: $\\dfrac{n(n-3)}{2}$. Проверь: у четырёхугольника $\\dfrac{4\\cdot1}{2}=2$.' },
{ sec:'p2', html:'Сумма углов $= (n-2)\\cdot180°$. Разбиение на треугольники даёт доказательство.' },
{ sec:'p3', html:'Сумма внешних углов <b>любого</b> выпуклого многоугольника равна $360°$.' },
{ sec:'p4', html:'Параллелограмм — это обобщение: прямоугольник, ромб, квадрат — его частные случаи.' },
{ sec:'p5', html:'Диагонали параллелограмма <b>делятся пополам</b>, а не равны (это у прямоугольника).' },
{ sec:'p6', html:'Три признака: равные противоположные стороны; одна пара параллельных и равных; диагонали пополам.' },
{ sec:'p7', html:'Прямоугольник: диагонали равны. Ромб: диагонали перпендикулярны. Квадрат: оба свойства.' },
{ sec:'p8', html:'Если у параллелограмма диагонали равны — это прямоугольник.' },
{ sec:'p9', html:'Диагонали ромба взаимно перпендикулярны и являются биссектрисами углов.' },
{ sec:'p10', html:'Квадрат совмещает все свойства ромба и прямоугольника.' },
{ sec:'p11', html:'Теорема Фалеса: если параллельные прямые пересекают две прямые, они отсекают пропорциональные отрезки.' },
{ sec:'p12', html:'Медианы пересекаются в точке, делящей каждую в отношении $2:1$ от вершины.' },
{ sec:'p13', html:'Средняя линия треугольника $\\parallel$ основанию и равна его <b>половине</b>.' },
{ sec:'p14', html:'Средняя линия трапеции $= \\dfrac{a+b}{2}$, параллельна основаниям.' },
{ sec:'p15', html:'В равнобедренной трапеции углы при каждом основании равны, диагонали равны.' },
{ sec:'p16', html:'Два признака равнобедренной трапеции: равные углы при основании или равные диагонали.' },
{ sec:'final1', html:'Повторите все формулы: сумма углов, диагонали параллелограмма, средние линии.' },
];
function buildSidebar(id){
const box = document.getElementById('sidebar-content');
const sb = SIDEBARS[id] || SIDEBARS.p1;
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"><div class="xp-card-title"><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 ? ' — ' + v : ''}</div>`; });
html += '</div>';
const tip = TIPS.find(t=>t.sec === id) || TIPS[0];
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)">✓ ${text}</div>`; });
html += '</div>';
}
box.innerHTML = html;
if(window.renderMathInElement) try{ renderMath(box); }catch(e){}
}
/* THEME */
function initTheme(){
const t = localStorage.getItem('geometry8_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('geometry8_ch1_theme', dark ? 'dark' : 'light');
document.getElementById('theme-lab').textContent = dark ? 'Светлая' : 'Тёмная';
});
}
/* HELPERS */
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){ elm.className = 'feedback ' + (ok ? 'ok' : 'fail'); elm.innerHTML = text || (ok ? '&#10003; Верно!' : '&#10007; Неверно'); }
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(4)).toString(); }
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>',
class: '<svg class="ic" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="14" rx="1"/><line x1="3" y1="21" x2="21" y2="21"/><polyline points="7 14 10 11 13 14 17 10"/></svg>',
home: '<svg class="ic" viewBox="0 0 24 24"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>',
};
function makeCard(kind, title, num, body){
const labels = {repeat:'Повторение',theory:'Теория',algo:'Алгоритм',rule:'Правило',example:'Пример',oral:'Устно',class:'Класс',home:'Домашка'};
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 secNav(prev, next){
const NAMES = {p1:'§1',p2:'§2',p3:'§3',p4:'§4',p5:'§5',p6:'§6',p7:'§7',p8:'§8',p9:'§9',p10:'§10',p11:'§11',p12:'§12',p13:'§13',p14:'§14',p15:'§15',p16:'§16',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;
}
/* CONFETTI */
let _confettiCanvas = null, _confettiParticles = [], _confettiRaf = null;
function confetti(){
if(!_confettiCanvas){ _confettiCanvas = document.createElement('canvas'); _confettiCanvas.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:9999'; document.body.appendChild(_confettiCanvas); }
const c = _confettiCanvas; c.width = window.innerWidth; c.height = window.innerHeight;
const ctx = c.getContext('2d');
const colors = ['#d97706','#f59e0b','#fbbf24','#10b981','#0891b2'];
for(let i = 0; i < 80; i++){ _confettiParticles.push({ x:window.innerWidth/2+(Math.random()-.5)*200, y:window.innerHeight/2, vx:(Math.random()-.5)*14, vy:-10-Math.random()*10, g:.4, life:100, color:colors[i%colors.length], r:4+Math.random()*4, rot:0, vRot:(Math.random()-.5)*.3 }); }
if(_confettiRaf) cancelAnimationFrame(_confettiRaf);
function frame(){ ctx.clearRect(0,0,c.width,c.height); _confettiParticles=_confettiParticles.filter(p=>{ p.x+=p.vx;p.y+=p.vy;p.vy+=p.g;p.life--;p.rot+=p.vRot;ctx.save();ctx.translate(p.x,p.y);ctx.rotate(p.rot);ctx.fillStyle=p.color;ctx.fillRect(-p.r,-p.r/2,p.r*2,p.r);ctx.restore();return p.life>0&&p.y<c.height+50; }); if(_confettiParticles.length>0) _confettiRaf=requestAnimationFrame(frame); else{ ctx.clearRect(0,0,c.width,c.height);_confettiRaf=null; } }
frame();
}
/* DnD sorter */
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; 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(); }};
}
const DND_HINT_HTML = '<div class="dnd-hint"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11V6a3 3 0 0 1 6 0v5"/><path d="M9 11h6v8a4 4 0 0 1-8 0z"/></svg> Перетащивайте или нажмите карточку, затем — на нужный ящик.</div>';
/* GLOSSARY */
const GLOSSARY = [
{ term:'многоугольник', def:'Замкнутая ломаная линия, образующая плоскую фигуру.', sec:'p1', aliases:['многоугольник','многоугольника','многоугольнике','многоугольников','многоугольники','многоугольникам'] },
{ term:'выпуклый многоугольник', def:'Многоугольник, у которого все диагонали лежат внутри фигуры.', sec:'p1', aliases:['выпуклый многоугольник','выпуклого многоугольника','выпуклые многоугольники','выпуклых многоугольников'] },
{ term:'диагональ', def:'Отрезок, соединяющий две несмежные (нессоседние) вершины многоугольника.', sec:'p1', aliases:['диагональ','диагонали','диагоналей','диагональю','диагоналями'] },
{ term:'периметр', def:'Сумма длин всех сторон многоугольника.', sec:'p1', aliases:['периметр','периметра','периметром'] },
{ term:'параллелограмм', def:'Четырёхугольник, у которого противоположные стороны попарно параллельны.', sec:'p4', aliases:['параллелограмм','параллелограмма','параллелограмме','параллелограммов','параллелограммы'] },
{ term:'прямоугольник', def:'Параллелограмм, у которого все углы прямые.', sec:'p7', aliases:['прямоугольник','прямоугольника','прямоугольнике','прямоугольников','прямоугольники'] },
{ term:'ромб', def:'Параллелограмм, у которого все стороны равны.', sec:'p9', aliases:['ромб','ромба','ромбе','ромбов','ромбы'] },
{ term:'квадрат', def:'Прямоугольник, у которого все стороны равны (одновременно ромб и прямоугольник).', sec:'p10', aliases:['квадрат','квадрата','квадрате','квадратов','квадраты'] },
{ term:'трапеция', def:'Четырёхугольник, у которого ровно одна пара параллельных сторон (основания).', sec:'p14', aliases:['трапеция','трапеции','трапецию','трапеций','трапеций'] },
{ term:'равнобедренная трапеция', def:'Трапеция, у которой боковые стороны равны.', sec:'p15', aliases:['равнобедренная трапеция','равнобедренной трапеции','равнобедренную трапецию'] },
{ term:'средняя линия треугольника', def:'Отрезок, соединяющий середины двух сторон треугольника. Параллелен третьей стороне и равен её половине.', sec:'p13', aliases:['средняя линия треугольника','средней линии треугольника','средние линии треугольника'] },
{ term:'средняя линия трапеции', def:'Отрезок, соединяющий середины боковых сторон трапеции. Равен полусумме оснований.', sec:'p14', aliases:['средняя линия трапеции','средней линии трапеции'] },
{ term:'медиана', def:'Отрезок, проведённый из вершины треугольника к середине противоположной стороны.', sec:'p12', aliases:['медиана','медианы','медиан','медианам','медиану'] },
{ term:'теорема Фалеса', def:'Если параллельные прямые пересекают две прямые, они отсекают на них пропорциональные отрезки.', sec:'p11', aliases:['теорема Фалеса','теореме Фалеса','теоремы Фалеса'] },
{ term:'подобные треугольники', def:'Треугольники, у которых углы равны попарно и стороны пропорциональны.', sec:'p11', aliases:['подобные треугольники','подобных треугольников','подобными треугольниками'] },
{ term:'касательная', def:'Прямая, имеющая одну общую точку с окружностью и перпендикулярная радиусу в точке касания.', sec:'p11', aliases:['касательная','касательной','касательную','касательных'] },
{ term:'центральный угол', def:'Угол с вершиной в центре окружности, стороны которого — радиусы.', sec:'p11', aliases:['центральный угол','центрального угла','центральному углу'] },
{ term:'вписанный угол', def:'Угол с вершиной на окружности, стороны которого — хорды.', sec:'p11', aliases:['вписанный угол','вписанного угла','вписанном угле'] },
{ term:'теорема Пифагора', def:'В прямоугольном треугольнике: $a^2 + b^2 = c^2$, где $c$ — гипотенуза.', sec:'p11', aliases:['теорема Пифагора','теореме Пифагора','теоремы Пифагора'] },
{ term:'площадь', def:'Числовая характеристика плоской фигуры, выражающая её размер.', sec:'p11', aliases:['площадь','площади','площадью'] },
];
function wrapGlossary(root){
if(!root||root.__glossDone) return;
const allAliases = [];
GLOSSARY.forEach((g,i)=>g.aliases.forEach(a=>allAliases.push({a,i})));
allAliases.sort((x,y)=>y.a.length-x.a.length);
const re = new RegExp('(?<![\\w\\u0400-\\u04ff-])(' + allAliases.map(x=>x.a.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')).join('|') + ')(?![\\w\\u0400-\\u04ff-])', 'iu');
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, { acceptNode(node){ const p=node.parentElement; if(!p) return NodeFilter.FILTER_REJECT; if(p.closest('.katex,.gloss-term,button,input,select,.wg-badge,.card-icon,.sec-num,.psel-num,.hdr,.ach-popup,script,style,.search-modal,.sidecard,.gloss-tip')) return NodeFilter.FILTER_REJECT; if(!re.test(node.nodeValue)) return NodeFilter.FILTER_REJECT; return NodeFilter.FILTER_ACCEPT; } });
const nodes = []; let n; while((n=walker.nextNode())) nodes.push(n);
nodes.forEach(node=>{ const text=node.nodeValue; const out=document.createDocumentFragment(); let cursor=0; const global=new RegExp(re.source,'giu'); let m; while((m=global.exec(text))!==null){ if(m.index>cursor) out.appendChild(document.createTextNode(text.slice(cursor,m.index))); const found=m[0].toLowerCase(); const hit=allAliases.find(x=>x.a.toLowerCase()===found); const g=hit?GLOSSARY[hit.i]:null; const sp=document.createElement('span'); sp.className='gloss-term'; sp.dataset.gloss=g?g.term:''; sp.textContent=m[0]; out.appendChild(sp); cursor=m.index+m[0].length; } if(cursor<text.length) out.appendChild(document.createTextNode(text.slice(cursor))); node.parentNode.replaceChild(out,node); });
root.__glossDone = true;
}
function initGlossaryTip(){
const tip = document.getElementById('gloss-tip'); if(!tip) return;
let lockOpen = null;
function show(elm){ const g=GLOSSARY.find(x=>x.term===elm.dataset.gloss); if(!g) return; tip.innerHTML='<b>'+g.term[0].toUpperCase()+g.term.slice(1)+'</b><div style="margin-top:4px">'+g.def+'</div><div style="margin-top:6px;font-size:.72rem;color:var(--muted);text-transform:uppercase;letter-spacing:.06em">См. § '+g.sec.replace('p','')+'</div>'; if(window.renderMathInElement) renderMath(tip); const r=elm.getBoundingClientRect(); tip.classList.add('show'); const tw=tip.offsetWidth,th=tip.offsetHeight; let left=r.left,top=r.bottom+8; if(left+tw>window.innerWidth-12) left=window.innerWidth-tw-12; if(top+th>window.innerHeight-12) top=r.top-th-8; tip.style.left=Math.max(8,left)+'px'; tip.style.top=Math.max(8,top)+'px'; }
function hide(){ tip.classList.remove('show'); }
document.addEventListener('mouseover',e=>{ const elm=e.target.closest&&e.target.closest('.gloss-term'); if(elm&&!lockOpen) show(elm); });
document.addEventListener('mouseout',e=>{ const elm=e.target.closest&&e.target.closest('.gloss-term'); if(elm&&!lockOpen) hide(); });
document.addEventListener('click',e=>{ const elm=e.target.closest&&e.target.closest('.gloss-term'); if(elm){ if(lockOpen===elm){lockOpen=null;hide();}else{lockOpen=elm;show(elm);} }else if(lockOpen&&!e.target.closest('.gloss-tip')){lockOpen=null;hide();} });
}
/* SEARCH */
const SEARCH_INDEX = (function(){
const arr = [];
PARAS.forEach(p=>arr.push({kind:'Параграф',title:p.num+' '+p.name,desc:p.sub||'',sec:p.id}));
GLOSSARY.forEach(g=>arr.push({kind:'Понятие',title:g.term,desc:g.def.replace(/\$/g,''),sec:g.sec,gloss:g.term}));
[
['Формула','Число диагоналей n-угольника: n(n-3)/2','§1','p1'],
['Формула','Сумма углов: (n-2)·180°','§2','p2'],
['Формула','Сумма внешних углов: 360°','§3','p3'],
['Формула','Средняя линия треугольника = половина основания','§13','p13'],
['Формула','Средняя линия трапеции = (a+b)/2','§14','p14'],
].forEach(([k,t,d,s])=>arr.push({kind:k,title:t,desc:d,sec:s}));
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)+'…':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); if(r.gloss){ setTimeout(()=>{ const sec=document.getElementById('sec-'+r.sec); const elm=sec&&sec.querySelector('[data-gloss="'+r.gloss+'"]'); if(elm){elm.scrollIntoView({behavior:'smooth',block:'center'});elm.style.transition='background .3s';elm.style.background='var(--warn,#f59e0b)';setTimeout(()=>{elm.style.background='';},1400);} },400); } }
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(); } });
}
/* SIDEBAR TOGGLE */
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(); });
}
/* INIT */
function init(){
loadProgress(); initTheme(); initSidebarToggle(); initGlossaryTip(); initSearch();
buildParaSelector(); refreshProgressUI(); loadServerReadState(); goTo('p1');
setTimeout(()=>achievement('start','Начало главы 1!'), 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);
/* ============================================================
§ 1 — ВЫПУКЛЫЕ МНОГОУГОЛЬНИКИ. ДИАГОНАЛЬ. ПЕРИМЕТР
============================================================ */
function buildP1(){
const box = document.getElementById('p1-body');
let html = '';
html += makeCard('theory','Многоугольник — определение','1.1',`
<p><b>Многоугольник</b> — это замкнутая ломаная без самопересечений. Она делит плоскость на внутреннюю часть (многоугольник) и внешнюю.</p>
<p>Элементы: <b>вершины</b> (точки излома), <b>стороны</b> (звенья ломаной), <b>углы</b> (при каждой вершине).</p>
<p>Названия: <b>3</b> стороны — треугольник, <b>4</b> — четырёхугольник, <b>5</b> — пятиугольник, <b>6</b> — шестиугольник, …, <b>n</b> — n-угольник.</p>`);
html += makeCard('rule','Выпуклый многоугольник','1.2',`
<p><b>Выпуклый многоугольник</b> — многоугольник, у которого каждая сторона (её прямая) не разделяет оставшиеся вершины на две части: все они лежат по одну сторону от этой прямой.</p>
<p>Эквивалентно: все диагонали лежат <b>внутри</b> фигуры.</p>
<p>Невыпуклый (вогнутый) — если хотя бы одна диагональ выходит наружу.</p>`);
html += makeCard('rule','Диагональ. Число диагоналей','1.3',`
<p><b>Диагональ</b> — отрезок, соединяющий две <em>несмежные</em> (несоседние) вершины многоугольника.</p>
<p>В n-угольнике число диагоналей:</p>
\\[D = \\dfrac{n(n-3)}{2}\\]
<p style="font-size:.88rem;color:var(--muted)">Объяснение: из каждой из $n$ вершин можно провести $(n-3)$ диагонали (не к самой себе и не к двум соседним). Делим на 2, т.к. каждая диагональ считается дважды.</p>
<table class="tbl" style="margin-top:10px">
<thead><tr><th>$n$</th><th>3</th><th>4</th><th>5</th><th>6</th><th>7</th><th>8</th><th>10</th></tr></thead>
<tbody><tr><th>Диагоналей</th><td>0</td><td>2</td><td>5</td><td>9</td><td>14</td><td>20</td><td>35</td></tr></tbody>
</table>`);
html += makeCard('rule','Периметр','1.4',`
<p><b>Периметр</b> многоугольника — сумма длин всех его сторон:</p>
\\[P = a_1 + a_2 + \\cdots + a_n\\]
<p>Для правильного n-угольника со стороной $a$: $P = n \\cdot a$.</p>`);
html += makeCard('example','Пример','1.5',`
<p><b>Сколько диагоналей у восьмиугольника?</b></p>
<p>$n = 8$: $D = \\dfrac{8 \\cdot (8-3)}{2} = \\dfrac{8 \\cdot 5}{2} = \\dfrac{40}{2} = 20$. Ответ: 20 диагоналей.</p>
<p style="margin-top:8px"><b>Периметр четырёхугольника</b> со сторонами 5, 7, 4, 6 равен $5+7+4+6=22$.</p>`);
/* --- INTERACTIVE 1: SVG-конструктор многоугольника --- */
html += `<div class="wg" id="p1-polycon">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Конструктор многоугольника</div></div>
<div class="wg-help">Тащи вершины мышью (или пальцем). Меняй количество вершин слайдером. Смотри: периметр, диагонали, выпуклость.</div>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:10px">
<label style="font-size:.92rem;color:var(--muted)">Вершин: <b id="p1-poly-n-val">5</b>
<input type="range" min="3" max="10" value="5" id="p1-poly-n-sl" style="vertical-align:middle;width:120px;accent-color:var(--sec-acc,var(--pri))">
</label>
<button class="btn small" id="p1-poly-reset">Сбросить</button>
</div>
<div id="p1-poly-svg-wrap" style="display:flex;justify-content:center"></div>
<div id="p1-poly-info" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:8px;margin-top:10px"></div>
</div>`;
/* --- INTERACTIVE 2: Калькулятор диагоналей --- */
html += `<div class="wg" id="p1-diagcalc">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Калькулятор диагоналей</div></div>
<div class="wg-help">Выбери n — увидишь пошаговое вычисление формулы $D = \\dfrac{n(n-3)}{2}$.</div>
<div class="sliders">
<label>$n$ = <b id="p1-dc-nval">7</b><input type="range" min="3" max="20" value="7" id="p1-dc-n" style="accent-color:var(--sec-acc,var(--pri))"></label>
</div>
<div id="p1-dc-out" style="padding:14px;background:var(--sec-acc-soft,var(--pri-soft));border-radius:10px;font-size:.95rem;line-height:1.8"></div>
</div>`;
/* --- INTERACTIVE 3: DnD — выпуклый/невыпуклый/не многоугольник --- */
html += `<div class="wg" id="p1-dnd-wrap">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 3</span><div class="wg-title">Выпуклый / Невыпуклый / Не многоугольник</div></div>
<div class="wg-help">Разложи фигуры по категориям. Нажми карточку, затем — на нужный ящик. Или перетащи.</div>
<div id="p1-dnd-hint" class="dnd-hint"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px"><path d="M9 11V6a3 3 0 0 1 6 0v5"/><path d="M9 11h6v8a4 4 0 0 1-8 0z"/></svg> Перетащивайте или нажмите карточку, затем — на нужный ящик.</div>
<div id="p1-dnd-pool"></div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-top:8px">
<div class="drop-box"><h5 data-cat="conv">Выпуклый</h5><div class="drop-items" data-cat="conv"></div></div>
<div class="drop-box"><h5 data-cat="conc">Невыпуклый</h5><div class="drop-items" data-cat="conc"></div></div>
<div class="drop-box"><h5 data-cat="not">Не многоугольник</h5><div class="drop-items" data-cat="not"></div></div>
</div>
<div class="actions"><button class="btn primary" id="p1-dnd-check">Проверить</button><button class="btn" id="p1-dnd-reset">Сначала</button></div>
<div class="feedback" id="p1-dnd-fb" style="display:none"></div>
</div>`;
/* --- INTERACTIVE 4: Тренажёр периметра --- */
html += `<div class="wg">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 4</span><div class="wg-title">Тренажёр периметра</div></div>
<div class="wg-help">Вычисли периметр многоугольника по данным сторонам и введи ответ.</div>
<div class="score-display"><span>Задача <b id="p1-pt-i">1</b> / 5</span><span>Очки: <b id="p1-pt-score">0</b></span></div>
<div id="p1-pt-task" style="padding:14px;background:var(--sec-acc-soft,var(--pri-soft));border-radius:10px;font-size:1.05rem;margin-bottom:10px"></div>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<input type="number" id="p1-pt-ans" class="tinp" placeholder="Ответ" style="width:110px">
<button class="btn primary" id="p1-pt-go">Проверить</button>
<button class="btn" id="p1-pt-start">Начать</button>
</div>
<div class="feedback" id="p1-pt-fb" style="display:none"></div>
</div>`;
/* --- INTERACTIVE 5: Босс §1 --- */
html += `<div class="wg" style="border-color:var(--sec-acc-d,var(--pri2));background:linear-gradient(135deg,var(--card),var(--sec-acc-soft,var(--pri-soft)))">
<div class="wg-header"><span class="wg-badge" style="background:var(--sec-acc-d,var(--pri2))">БОСС §1</span><div class="wg-title">Итоговые задачи</div></div>
<div class="wg-help">Реши все 4 задачи — получишь XP. Каждая правильно решённая задача даёт +5 XP.</div>
<div id="p1-boss-tasks"></div>
</div>`;
html += `<div style="margin-top:18px;display:flex;justify-content:center">
<button class="btn primary" id="p1-read-btn" onclick="addXp(10,'p1-read');bumpProgress('p1',40);document.getElementById('p1-read-btn').textContent='Прочитано!';document.getElementById('p1-read-btn').disabled=true;">
<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>
Я прочитал §1 (+10 XP)
</button>
</div>`;
html += secNav(null,'p2');
box.innerHTML = html;
if(window.renderMathInElement) setTimeout(()=>renderMath(box), 0);
/* == INIT: SVG-конструктор многоугольника == */
(function(){
const W = 360, H = 320, cx = W/2, cy = H/2, R = 120;
let n = 5;
let verts = [];
function defaultVerts(nv){
const v = [];
for(let i=0;i<nv;i++){
const a = -Math.PI/2 + 2*Math.PI*i/nv;
v.push({x: cx + R*Math.cos(a), y: cy + R*Math.sin(a)});
}
return v;
}
function dist(a,b){ return Math.hypot(b.x-a.x,b.y-a.y); }
function cross2d(o,a,b){ return (a.x-o.x)*(b.y-o.y)-(a.y-o.y)*(b.x-o.x); }
function segIntersect(p,r,q,s){
const rxs = r.x*s.y-r.y*s.x;
if(Math.abs(rxs)<1e-10) return false;
const qp = {x:q.x-p.x,y:q.y-p.y};
const t=(qp.x*s.y-qp.y*s.x)/rxs;
const u=(qp.x*r.y-qp.y*r.x)/rxs;
return t>1e-6&&t<1-1e-6&&u>1e-6&&u<1-1e-6;
}
function hasSelfIntersection(vs){
const n=vs.length;
for(let i=0;i<n;i++){
const p=vs[i],r={x:vs[(i+1)%n].x-p.x,y:vs[(i+1)%n].y-p.y};
for(let j=i+2;j<n;j++){
if(i===0&&j===n-1) continue;
const q=vs[j],s={x:vs[(j+1)%n].x-q.x,y:vs[(j+1)%n].y-q.y};
if(segIntersect(p,r,q,s)) return true;
}
}
return false;
}
function isConvex(vs){
const n=vs.length; let sign=0;
for(let i=0;i<n;i++){
const c=cross2d(vs[i],vs[(i+1)%n],vs[(i+2)%n]);
if(Math.abs(c)<1e-9) continue;
const s=c>0?1:-1;
if(!sign) sign=s;
else if(sign!==s) return false;
}
return true;
}
function interiorAngle(vs,i){
const n=vs.length;
const prev=vs[(i-1+n)%n],cur=vs[i],next=vs[(i+1)%n];
const ax=prev.x-cur.x,ay=prev.y-cur.y;
const bx=next.x-cur.x,by=next.y-cur.y;
const dot=ax*bx+ay*by,cross=ax*by-ay*bx;
let a=Math.atan2(Math.abs(cross),dot)*180/Math.PI;
if(cross<0) a=360-a;
return a;
}
function redraw(){
const svg=document.getElementById('p1-poly-svg');
if(!svg) return;
const nv=verts.length;
const selfX=hasSelfIntersection(verts);
const conv=!selfX&&isConvex(verts);
const pts=verts.map(v=>v.x+','+v.y).join(' ');
const col=selfX?'#ef4444':(conv?'#10b981':'#f59e0b');
const fillCol=selfX?'rgba(239,68,68,.12)':(conv?'rgba(16,185,129,.12)':'rgba(245,158,11,.12)');
let s='';
s+='<polygon points="'+pts+'" fill="'+fillCol+'" stroke="'+col+'" stroke-width="2.5" stroke-linejoin="round"/>';
// диагонали
for(let i=0;i<nv;i++) for(let j=i+2;j<nv;j++){
if(i===0&&j===nv-1) continue;
s+='<line x1="'+verts[i].x+'" y1="'+verts[i].y+'" x2="'+verts[j].x+'" y2="'+verts[j].y+'" stroke="#94a3b8" stroke-width="1" stroke-dasharray="4 3" opacity=".7"/>';
}
// метки сторон
for(let i=0;i<nv;i++){
const a=verts[i],b=verts[(i+1)%nv];
const mx=(a.x+b.x)/2,my=(a.y+b.y)/2;
const d=dist(a,b);
s+='<text x="'+mx+'" y="'+my+'" text-anchor="middle" dominant-baseline="middle" font-size="10" fill="#64748b" font-family="JetBrains Mono,monospace" dy="-7">'+d.toFixed(1)+'</text>';
}
// вершины
for(let i=0;i<nv;i++){
const v=verts[i];
s+='<circle cx="'+v.x+'" cy="'+v.y+'" r="10" fill="'+col+'" opacity=".22" class="p1-poly-vh" data-i="'+i+'"/>';
s+='<circle cx="'+v.x+'" cy="'+v.y+'" r="6" fill="'+col+'" stroke="#fff" stroke-width="2" class="p1-poly-vh" data-i="'+i+'"/>';
const letter=String.fromCharCode(65+i);
const lx=v.x+(v.x-cx)*0.22+2,ly=v.y+(v.y-cy)*0.22+2;
s+='<text x="'+lx+'" y="'+ly+'" text-anchor="middle" dominant-baseline="middle" font-size="13" font-weight="700" fill="'+col+'" font-family="Unbounded,sans-serif">'+letter+'</text>';
}
svg.innerHTML=s;
// drag handlers
svg.querySelectorAll('.p1-poly-vh').forEach(el=>{
el.style.cursor='grab';
el.addEventListener('pointerdown',ev=>{
if(ev.button!==undefined&&ev.button!==0) return;
const idx=+el.dataset.i;
el.style.cursor='grabbing';
try{el.setPointerCapture(ev.pointerId);}catch(e){}
function onMove(e){
const rect=svg.getBoundingClientRect();
const sx=svg.viewBox.baseVal.width/rect.width;
const sy=svg.viewBox.baseVal.height/rect.height;
const nx=Math.max(10,Math.min(W-10,(e.clientX-rect.left)*sx));
const ny=Math.max(10,Math.min(H-10,(e.clientY-rect.top)*sy));
verts[idx]={x:nx,y:ny};
redraw(); updateInfo();
}
function onUp(){ el.removeEventListener('pointermove',onMove);el.removeEventListener('pointerup',onUp);el.removeEventListener('pointercancel',onUp);el.style.cursor='grab'; }
el.addEventListener('pointermove',onMove);
el.addEventListener('pointerup',onUp);
el.addEventListener('pointercancel',onUp);
});
});
const statusEl=document.getElementById('p1-poly-status');
if(statusEl){
statusEl.textContent=selfX?'Самопересечение!':(conv?'Выпуклый':'Невыпуклый');
statusEl.style.color=col;
}
}
function updateInfo(){
const nv=verts.length;
const perimeter=verts.reduce((s,v,i)=>s+dist(v,verts[(i+1)%nv]),0);
const diags=nv*(nv-3)/2;
const selfX=hasSelfIntersection(verts);
const conv=!selfX&&isConvex(verts);
const angStr=verts.map((v,i)=>interiorAngle(verts,i).toFixed(1)+'°').join(', ');
const angSum=verts.reduce((s,v,i)=>s+interiorAngle(verts,i),0);
const col=selfX?'#ef4444':(conv?'#10b981':'#f59e0b');
document.getElementById('p1-poly-info').innerHTML=`
<div style="padding:8px 12px;background:var(--card);border-radius:8px;border:1px solid var(--border);font-size:.88rem"><div style="color:var(--muted);font-size:.72rem;font-weight:700;text-transform:uppercase;margin-bottom:4px">Вид</div><b style="color:${col}">${selfX?'Самопересечение!':(conv?'Выпуклый':'Невыпуклый')}</b></div>
<div style="padding:8px 12px;background:var(--card);border-radius:8px;border:1px solid var(--border);font-size:.88rem"><div style="color:var(--muted);font-size:.72rem;font-weight:700;text-transform:uppercase;margin-bottom:4px">Периметр</div><b>${perimeter.toFixed(2)}</b></div>
<div style="padding:8px 12px;background:var(--card);border-radius:8px;border:1px solid var(--border);font-size:.88rem"><div style="color:var(--muted);font-size:.72rem;font-weight:700;text-transform:uppercase;margin-bottom:4px">Диагоналей</div><b>${Math.max(0,diags)}</b></div>
<div style="padding:8px 12px;background:var(--card);border-radius:8px;border:1px solid var(--border);font-size:.88rem"><div style="color:var(--muted);font-size:.72rem;font-weight:700;text-transform:uppercase;margin-bottom:4px">Сумма углов</div><b>${angSum.toFixed(1)}°</b></div>`;
}
function build(){
verts=defaultVerts(n);
const wrap=document.getElementById('p1-poly-svg-wrap');
wrap.innerHTML='<svg id="p1-poly-svg" viewBox="0 0 '+W+' '+H+'" style="width:100%;max-width:380px;background:var(--card);border:1px solid var(--border);border-radius:14px;touch-action:none"><text id="p1-poly-status" x="'+(W/2)+'" y="18" text-anchor="middle" font-size="13" font-weight="700" font-family="Unbounded,sans-serif"></text></svg>';
redraw(); updateInfo();
}
build();
document.getElementById('p1-poly-n-sl').addEventListener('input',function(){
n=+this.value; document.getElementById('p1-poly-n-val').textContent=n; build();
});
document.getElementById('p1-poly-reset').addEventListener('click',()=>build());
})();
/* == INIT: Калькулятор диагоналей == */
(function(){
const sl=document.getElementById('p1-dc-n'),val=document.getElementById('p1-dc-nval'),out=document.getElementById('p1-dc-out');
function update(){
const n=+sl.value; val.textContent=n;
const d=n*(n-3)/2;
out.innerHTML=`
<div style="margin-bottom:6px">$n = ${n}$, формула: $D = \\dfrac{n(n-3)}{2}$</div>
<div style="margin-bottom:6px">Подставляем: $D = \\dfrac{${n} \\cdot (${n} - 3)}{2} = \\dfrac{${n} \\cdot ${n-3}}{2} = \\dfrac{${n*(n-3)}}{2}$</div>
<div style="font-size:1.15rem;font-weight:800;color:var(--sec-acc-d,var(--pri2))">Ответ: $D = ${d}$ диагоналей</div>`;
renderMath(out);
}
sl.addEventListener('input',update); update();
})();
/* == INIT: DnD сортировка == */
(function(){
const items=[
{id:'tri', html:'<svg style="width:36px;height:30px" viewBox="0 0 36 30"><polygon points="18,2 34,28 2,28" fill="rgba(16,185,129,.2)" stroke="#10b981" stroke-width="1.8"/></svg><span style="font-size:.8rem">Треугольник</span>', ans:'conv'},
{id:'square',html:'<svg style="width:30px;height:30px" viewBox="0 0 30 30"><rect x="2" y="2" width="26" height="26" fill="rgba(16,185,129,.2)" stroke="#10b981" stroke-width="1.8"/></svg><span style="font-size:.8rem">Квадрат</span>', ans:'conv'},
{id:'hex', html:'<svg style="width:36px;height:32px" viewBox="0 0 36 32"><polygon points="18,2 34,10 34,22 18,30 2,22 2,10" fill="rgba(16,185,129,.2)" stroke="#10b981" stroke-width="1.8"/></svg><span style="font-size:.8rem">Правильный шестиугольник</span>', ans:'conv'},
{id:'star', html:'<svg style="width:36px;height:36px" viewBox="0 0 36 36"><polygon points="18,2 21,13 33,13 23,20 27,32 18,25 9,32 13,20 3,13 15,13" fill="rgba(245,158,11,.2)" stroke="#f59e0b" stroke-width="1.8"/></svg><span style="font-size:.8rem">Звезда</span>', ans:'conc'},
{id:'arrow', html:'<svg style="width:36px;height:30px" viewBox="0 0 36 30"><polygon points="2,10 20,10 20,2 34,15 20,28 20,20 2,20" fill="rgba(245,158,11,.2)" stroke="#f59e0b" stroke-width="1.8"/></svg><span style="font-size:.8rem">Стрелка</span>', ans:'conc'},
{id:'circle',html:'<svg style="width:30px;height:30px" viewBox="0 0 30 30"><circle cx="15" cy="15" r="13" fill="rgba(148,163,184,.2)" stroke="#94a3b8" stroke-width="1.8"/></svg><span style="font-size:.8rem">Круг</span>', ans:'not'},
{id:'open', html:'<svg style="width:36px;height:30px" viewBox="0 0 36 30"><polyline points="2,28 10,2 18,28 26,2 34,28" fill="none" stroke="#94a3b8" stroke-width="1.8"/></svg><span style="font-size:.8rem">Незамкнутая ломаная</span>', ans:'not'},
];
const sorter=setupSorter({poolId:'p1-dnd-pool',scopeSelector:'#p1-dnd-wrap',items,cats:['conv','conc','not']});
document.getElementById('p1-dnd-reset').addEventListener('click',()=>{ sorter.reset(); document.getElementById('p1-dnd-fb').style.display='none'; });
document.getElementById('p1-dnd-check').addEventListener('click',()=>{
let ok=0,total=items.length;
items.forEach(it=>{ if(sorter.placed[it.id]===it.ans) ok++; });
const fb=document.getElementById('p1-dnd-fb');
if(ok===total){ feedback(fb,true,'Все '+total+' фигур разложены верно! +5 XP'); addXp(5,'p1-dnd'); bumpProgress('p1',15); }
else feedback(fb,false,'Верно: '+ok+' из '+total+'. Попробуй ещё раз.');
});
})();
/* == INIT: Тренажёр периметра == */
(function(){
const tasks=[
{sides:[5,7,4,6], ans:22, text:'Четырёхугольник со сторонами 5, 7, 4, 6.'},
{sides:[3,4,5], ans:12, text:'Треугольник со сторонами 3, 4, 5.'},
{sides:[8,8,8,8,8], ans:40, text:'Правильный пятиугольник со стороной 8.'},
{sides:[6,9,6,9], ans:30, text:'Прямоугольник со сторонами 6 и 9.'},
{sides:[7,5,8,4,6], ans:30, text:'Пятиугольник со сторонами 7, 5, 8, 4, 6.'},
];
let idx=0,score=0;
function show(){
const t=tasks[idx];
document.getElementById('p1-pt-i').textContent=idx+1;
document.getElementById('p1-pt-task').innerHTML='<b>'+t.text+'</b><br>Найди периметр.';
document.getElementById('p1-pt-ans').value='';
document.getElementById('p1-pt-fb').style.display='none';
}
document.getElementById('p1-pt-start').addEventListener('click',()=>{ idx=0;score=0;document.getElementById('p1-pt-score').textContent=0;show(); });
document.getElementById('p1-pt-go').addEventListener('click',()=>{
if(idx>=tasks.length) return;
const ans=+document.getElementById('p1-pt-ans').value;
const fb=document.getElementById('p1-pt-fb');
if(ans===tasks[idx].ans){
score++; document.getElementById('p1-pt-score').textContent=score;
addXp(3,'p1-perim'); bumpProgress('p1',5);
if(idx<tasks.length-1){ feedback(fb,true,'Верно! +3 XP'); idx++; setTimeout(()=>show(),900); }
else{ feedback(fb,true,'Все задачи решены! +5 XP'); addXp(5,'p1-perim-all'); bumpProgress('p1',10); }
} else {
feedback(fb,false,'Неверно. Ответ: '+tasks[idx].ans+'. Периметр = сумма всех сторон.');
}
});
document.getElementById('p1-pt-ans').addEventListener('keydown',e=>{ if(e.key==='Enter') document.getElementById('p1-pt-go').click(); });
show();
})();
/* == INIT: Босс §1 == */
(function(){
const tasks=[
{ q:'Сколько диагоналей у <b>7-угольника</b>?', ans:14, hint:'Формула: n(n-3)/2, n=7.' },
{ q:'Найди периметр прямоугольника со сторонами <b>AB=12</b> и <b>BC=7</b>.', ans:38, hint:'P = 2(12+7) = 38.' },
{ q:'В многоугольнике <b>20 диагоналей</b>. Сколько в нём вершин? (Число сторон)', ans:8, hint:'n(n-3)/2=20 → n=8.' },
{ q:'Периметр правильного шестиугольника равен <b>54</b>. Чему равна одна сторона?', ans:9, hint:'P = 6a → a = 54/6 = 9.' },
];
const box=document.getElementById('p1-boss-tasks');
let solved=new Set();
box.innerHTML=tasks.map((t,i)=>`
<div style="padding:14px;background:var(--card);border-radius:10px;border:1px solid var(--border);margin-bottom:10px" id="p1-boss-t${i}">
<div style="margin-bottom:8px;font-size:.95rem">${t.q}</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
<input type="number" class="tinp" id="p1-boss-a${i}" placeholder="Ответ" style="width:100px">
<button class="btn primary small" onclick="(function(){
const v=+document.getElementById('p1-boss-a${i}').value;
const fb=document.getElementById('p1-boss-fb${i}');
if(v===${t.ans}){
feedback(fb,true,'Верно! +5 XP');
if(!p1BossSolved.has(${i})){ p1BossSolved.add(${i}); addXp(5,'p1-boss${i}'); bumpProgress('p1',10); }
} else feedback(fb,false,'Неверно. Подсказка: ${t.hint}');
})()">Проверить</button>
</div>
<div class="feedback" id="p1-boss-fb${i}" style="display:none;margin-top:8px"></div>
</div>`).join('');
window.p1BossSolved=new Set();
})();
}
/* ============================================================
§ 2 — СУММА УГЛОВ ВЫПУКЛОГО МНОГОУГОЛЬНИКА
============================================================ */
function buildP2(){
const box = document.getElementById('p2-body');
let html = '';
html += makeCard('theory','Теорема о сумме углов','2.1',`
<p><b>Теорема.</b> Сумма внутренних углов выпуклого $n$-угольника равна:</p>
\\[(n-2)\\cdot 180^{\\circ}\\]
<p><b>Доказательство.</b> Из любой вершины $A_1$ проводим диагонали ко всем несмежным вершинам. Получаем $(n-2)$ треугольника. Сумма углов каждого треугольника равна $180°$, и все эти углы вместе составляют углы многоугольника. Итого: $(n-2) \\cdot 180°$.</p>
<table class="tbl" style="margin-top:10px">
<thead><tr><th>$n$</th><th>3</th><th>4</th><th>5</th><th>6</th><th>7</th><th>8</th><th>10</th></tr></thead>
<tbody><tr><th>Сумма углов</th><td>$180°$</td><td>$360°$</td><td>$540°$</td><td>$720°$</td><td>$900°$</td><td>$1080°$</td><td>$1440°$</td></tr></tbody>
</table>`);
html += makeCard('rule','Правильный многоугольник','2.2',`
<p><b>Правильный n-угольник</b> — все стороны равны и все углы равны.</p>
<p>Каждый внутренний угол правильного $n$-угольника:</p>
\\[\\alpha = \\dfrac{(n-2)\\cdot 180^{\\circ}}{n}\\]
<table class="tbl" style="margin-top:8px">
<thead><tr><th>Фигура</th><th>$n$</th><th>Угол $\\alpha$</th></tr></thead>
<tbody>
<tr><td>Треугольник</td><td>3</td><td>$60°$</td></tr>
<tr><td>Квадрат</td><td>4</td><td>$90°$</td></tr>
<tr><td>Пятиугольник</td><td>5</td><td>$108°$</td></tr>
<tr><td>Шестиугольник</td><td>6</td><td>$120°$</td></tr>
<tr><td>Восьмиугольник</td><td>8</td><td>$135°$</td></tr>
<tr><td>Двенадцатиугольник</td><td>12</td><td>$150°$</td></tr>
</tbody>
</table>`);
html += makeCard('example','Примеры','2.3',`
<p><b>Один угол девятиугольника.</b> $n=9$: сумма $= (9-2)\\cdot 180°=7\\cdot 180°=1260°$. Один угол правильного 9-угольника: $1260°/9=140°$.</p>
<p style="margin-top:6px"><b>Найти n по сумме углов.</b> Сумма $=1440°$: $(n-2)\\cdot 180°=1440° \\Rightarrow n-2=8 \\Rightarrow n=10$. Ответ: десятиугольник.</p>`);
/* --- INTERACTIVE 1: Анимация триангуляции --- */
html += `<div class="wg" id="p2-triang-wrap">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Триангуляция и сумма углов</div></div>
<div class="wg-help">Выбери n-угольник и нажми «Триангулировать» — увидишь разбиение на треугольники и подсчёт суммы углов.</div>
<div class="sliders">
<label>$n$ = <b id="p2-tr-nval">5</b><input type="range" min="3" max="12" value="5" id="p2-tr-n" style="accent-color:var(--sec-acc,var(--pri))"></label>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px">
<button class="btn primary" id="p2-tr-go">Триангулировать</button>
<button class="btn" id="p2-tr-reset">Сбросить</button>
</div>
<div id="p2-tr-svg-wrap" style="display:flex;justify-content:center"></div>
<div id="p2-tr-out" style="padding:12px;background:var(--sec-acc-soft,var(--pri-soft));border-radius:10px;margin-top:10px;font-size:.95rem;display:none"></div>
</div>`;
/* --- INTERACTIVE 2: Калькулятор суммы и угла --- */
html += `<div class="wg">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Калькулятор: сумма и угол правильного</div></div>
<div class="wg-help">Задай $n$ — мгновенно увидишь сумму всех углов и величину одного угла правильного $n$-угольника.</div>
<div class="sliders">
<label>$n$ = <b id="p2-cl-nval">6</b><input type="range" min="3" max="20" value="6" id="p2-cl-n" style="accent-color:var(--sec-acc,var(--pri))"></label>
</div>
<div id="p2-cl-out" style="padding:14px;background:var(--sec-acc-soft,var(--pri-soft));border-radius:10px;font-size:.95rem;line-height:1.9"></div>
</div>`;
/* --- INTERACTIVE 3: Обратная задача --- */
html += `<div class="wg">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 3</span><div class="wg-title">Обратная задача: по сумме углов найти n</div></div>
<div class="wg-help">Введи сумму внутренних углов (кратную 180°, больше 180°) — система найдёт n или скажет, что такого многоугольника нет.</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
<input type="number" id="p2-rev-inp" class="tinp" placeholder="Сумма углов в °" style="width:160px" value="1440">
<button class="btn primary" id="p2-rev-go">Найти n</button>
</div>
<div id="p2-rev-out" style="padding:12px;background:var(--sec-acc-soft,var(--pri-soft));border-radius:10px;margin-top:10px;display:none"></div>
</div>`;
/* --- INTERACTIVE 4: DnD правильные многоугольники ↔ углы --- */
html += `<div class="wg" id="p2-dnd-wrap">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 4</span><div class="wg-title">Правильные многоугольники и их углы</div></div>
<div class="wg-help">Разложи названия правильных многоугольников по значению угла.</div>
<div id="p2-dnd-hint" class="dnd-hint"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px"><path d="M9 11V6a3 3 0 0 1 6 0v5"/><path d="M9 11h6v8a4 4 0 0 1-8 0z"/></svg> Перетащивайте или нажмите карточку, затем — на нужный ящик.</div>
<div id="p2-dnd-pool"></div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px;margin-top:8px">
<div class="drop-box"><h5>$60°$</h5><div class="drop-items" data-cat="60"></div></div>
<div class="drop-box"><h5>$90°$</h5><div class="drop-items" data-cat="90"></div></div>
<div class="drop-box"><h5>$108°$</h5><div class="drop-items" data-cat="108"></div></div>
<div class="drop-box"><h5>$120°$</h5><div class="drop-items" data-cat="120"></div></div>
<div class="drop-box"><h5>$135°$</h5><div class="drop-items" data-cat="135"></div></div>
<div class="drop-box"><h5>$140°$</h5><div class="drop-items" data-cat="140"></div></div>
</div>
<div class="actions"><button class="btn primary" id="p2-dnd-check">Проверить</button><button class="btn" id="p2-dnd-reset">Сначала</button></div>
<div class="feedback" id="p2-dnd-fb" style="display:none"></div>
</div>`;
/* --- INTERACTIVE 5: Босс §2 --- */
html += `<div class="wg" style="border-color:var(--sec-acc-d,var(--pri2))">
<div class="wg-header"><span class="wg-badge" style="background:var(--sec-acc-d,var(--pri2))">БОСС §2</span><div class="wg-title">Итоговые задачи по §2</div></div>
<div class="wg-help">Реши задачи — каждая верная даёт +5 XP.</div>
<div id="p2-boss-tasks"></div>
</div>`;
html += `<div style="margin-top:18px;display:flex;justify-content:center">
<button class="btn primary" id="p2-read-btn" onclick="addXp(10,'p2-read');bumpProgress('p2',40);document.getElementById('p2-read-btn').textContent='Прочитано!';document.getElementById('p2-read-btn').disabled=true;">
<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>
Я прочитал §2 (+10 XP)
</button>
</div>`;
html += secNav('p1','p3');
box.innerHTML = html;
if(window.renderMathInElement) setTimeout(()=>renderMath(box), 0);
/* == INIT: Триангуляция == */
(function(){
const W=360, H=300, cx=W/2, cy=H/2, R=110;
let n=5, shown=false;
const colors=['#d97706','#10b981','#0891b2','#8b5cf6','#ef4444','#f97316','#14b8a6','#a855f7','#e11d48','#6366f1'];
function pts(nv,r,ox,oy){ const a=[]; for(let i=0;i<nv;i++){const ang=-Math.PI/2+2*Math.PI*i/nv;a.push({x:ox+r*Math.cos(ang),y:oy+r*Math.sin(ang)});}return a; }
function draw(triangulate){
const vs=pts(n,R,cx,cy);
const wrap=document.getElementById('p2-tr-svg-wrap');
let s='<svg viewBox="0 0 '+W+' '+H+'" style="width:100%;max-width:380px;background:var(--card);border:1px solid var(--border);border-radius:14px">';
if(triangulate){
for(let i=1;i<n-1;i++){
const col=colors[i%colors.length]+'33';
const sc=colors[i%colors.length];
s+='<polygon points="'+vs[0].x+','+vs[0].y+' '+vs[i].x+','+vs[i].y+' '+vs[i+1].x+','+vs[i+1].y+'" fill="'+col+'" stroke="'+sc+'" stroke-width="1.5" stroke-dasharray="5 3"/>';
}
const out=document.getElementById('p2-tr-out');
out.style.display='block';
out.innerHTML='$n='+n+'$, треугольников: $'+(n-2)+'$.<br>Сумма углов $= ('+n+'-2)\\cdot 180° = '+(n-2)+'\\cdot 180° = '+((n-2)*180)+'°$';
renderMath(out);
}
const ptstr=vs.map(v=>v.x+','+v.y).join(' ');
s+='<polygon points="'+ptstr+'" fill="'+(!triangulate?'rgba(217,119,6,.1)':'none')+'" stroke="#d97706" stroke-width="2.5" stroke-linejoin="round"/>';
vs.forEach((v,i)=>{
s+='<circle cx="'+v.x+'" cy="'+v.y+'" r="5" fill="#d97706" stroke="#fff" stroke-width="2"/>';
const lx=v.x+(v.x-cx)*0.22,ly=v.y+(v.y-cy)*0.22;
s+='<text x="'+lx+'" y="'+ly+'" text-anchor="middle" dominant-baseline="middle" font-size="12" font-weight="700" fill="#b45309" font-family="Unbounded,sans-serif">'+ String.fromCharCode(65+i)+'</text>';
});
if(triangulate){
for(let i=1;i<n-1;i++){
s+='<line x1="'+vs[0].x+'" y1="'+vs[0].y+'" x2="'+vs[i+1].x+'" y2="'+vs[i+1].y+'" stroke="#94a3b8" stroke-width="1.5" stroke-dasharray="4 3"/>';
}
for(let i=1;i<n-1;i++){
const tx=(vs[0].x+vs[i].x+vs[i+1].x)/3,ty=(vs[0].y+vs[i].y+vs[i+1].y)/3;
s+='<text x="'+tx+'" y="'+ty+'" text-anchor="middle" dominant-baseline="middle" font-size="11" font-weight="700" fill="'+colors[i%colors.length]+'" font-family="Unbounded,sans-serif">T'+(i)+'</text>';
}
}
s+='</svg>';
wrap.innerHTML=s;
}
const nlSl=document.getElementById('p2-tr-n');
const nlVal=document.getElementById('p2-tr-nval');
nlSl.addEventListener('input',function(){ n=+this.value; nlVal.textContent=n; shown=false; document.getElementById('p2-tr-out').style.display='none'; draw(false); });
document.getElementById('p2-tr-go').addEventListener('click',()=>{ shown=true; draw(true); addXp(2,'p2-triang'); bumpProgress('p2',10); });
document.getElementById('p2-tr-reset').addEventListener('click',()=>{ shown=false; document.getElementById('p2-tr-out').style.display='none'; draw(false); });
draw(false);
})();
/* == INIT: Калькулятор == */
(function(){
const sl=document.getElementById('p2-cl-n'),val=document.getElementById('p2-cl-nval'),out=document.getElementById('p2-cl-out');
function update(){
const n=+sl.value; val.textContent=n;
const sum=(n-2)*180;
const ang=sum/n;
out.innerHTML='$n='+n+'$<br>Сумма всех углов: $('+n+'-2)\\cdot 180° = '+sum+'°$<br>Один угол правильного $'+n+'$-угольника: $\\dfrac{'+sum+'°}{'+n+'} = '+ang.toFixed(2).replace(/\.00$/,'')+'°$';
renderMath(out);
}
sl.addEventListener('input',update); update();
})();
/* == INIT: Обратная задача == */
(function(){
document.getElementById('p2-rev-go').addEventListener('click',()=>{
const s=+document.getElementById('p2-rev-inp').value;
const out=document.getElementById('p2-rev-out');
out.style.display='block';
if(s<=0||isNaN(s)){ out.innerHTML='<span style="color:var(--bad)">Введи положительное число.</span>'; return; }
if(s%180!==0){ out.innerHTML='<span style="color:var(--bad)">Сумма углов должна делиться на 180°.</span>'; return; }
const n=(s/180)+2;
if(!Number.isInteger(n)||n<3){ out.innerHTML='<span style="color:var(--bad)">Нет такого многоугольника (n должен быть целым ≥ 3).</span>'; return; }
out.innerHTML='$(n-2)\\cdot 180°='+s+'° \\Rightarrow n-2='+s/180+' \\Rightarrow n='+n+'$<br><b style="color:var(--sec-acc-d,var(--pri2))">Ответ: '+n+'-угольник</b>';
renderMath(out);
addXp(3,'p2-rev');
});
document.getElementById('p2-rev-inp').addEventListener('keydown',e=>{ if(e.key==='Enter') document.getElementById('p2-rev-go').click(); });
})();
/* == INIT: DnD правильные ↔ углы == */
(function(){
const items=[
{id:'tri3', html:'Треугольник', ans:'60'},
{id:'sq4', html:'Квадрат', ans:'90'},
{id:'pent5', html:'Пятиугольник', ans:'108'},
{id:'hex6', html:'Шестиугольник', ans:'120'},
{id:'oct8', html:'Восьмиугольник', ans:'135'},
{id:'n9', html:'Девятиугольник', ans:'140'},
];
const sorter=setupSorter({poolId:'p2-dnd-pool',scopeSelector:'#p2-dnd-wrap',items,cats:['60','90','108','120','135','140']});
document.getElementById('p2-dnd-reset').addEventListener('click',()=>{ sorter.reset(); document.getElementById('p2-dnd-fb').style.display='none'; });
document.getElementById('p2-dnd-check').addEventListener('click',()=>{
let ok=0;
items.forEach(it=>{ if(sorter.placed[it.id]===it.ans) ok++; });
const fb=document.getElementById('p2-dnd-fb');
if(ok===items.length){ feedback(fb,true,'Все фигуры сопоставлены верно! +5 XP'); addXp(5,'p2-dnd'); bumpProgress('p2',15); }
else feedback(fb,false,'Верно: '+ok+' из '+items.length+'. Используй формулу α=(n-2)·180°/n.');
});
})();
/* == INIT: Босс §2 == */
(function(){
const tasks=[
{ q:'Найди сумму углов <b>выпуклого десятиугольника</b>.', ans:1440, hint:'(10-2)·180=1440' },
{ q:'Один угол правильного <b>восьмиугольника</b> равен … °', ans:135, hint:'(8-2)·180/8=135' },
{ q:'Сумма углов многоугольника равна <b>900°</b>. Сколько сторон?', ans:7, hint:'(n-2)·180=900→n=7' },
{ q:'Сумма углов правильного многоугольника = <b>2520°</b>. Найди один угол.', ans:168, hint:'n=(2520/180)+2=16; угол=2520/16=157.5? нет: (16-2)*180=2520, угол=2520/16=157.5. Проверь: n=16' },
];
const bossBox=document.getElementById('p2-boss-tasks');
bossBox.innerHTML=tasks.map((t,i)=>`
<div style="padding:14px;background:var(--card);border-radius:10px;border:1px solid var(--border);margin-bottom:10px">
<div style="margin-bottom:8px;font-size:.95rem">${t.q}</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
<input type="number" class="tinp" id="p2-boss-a${i}" placeholder="Ответ" style="width:100px">
<button class="btn primary small" onclick="(function(){
const v=+document.getElementById('p2-boss-a${i}').value;
const fb=document.getElementById('p2-boss-fb${i}');
if(v===${t.ans}||Math.abs(v-${t.ans})<0.5){
feedback(fb,true,'Верно! +5 XP');
if(!p2BossSolved.has(${i})){ p2BossSolved.add(${i}); addXp(5,'p2-boss${i}'); bumpProgress('p2',10); }
} else feedback(fb,false,'Неверно. Подсказка: ${t.hint}');
})()">Проверить</button>
</div>
<div class="feedback" id="p2-boss-fb${i}" style="display:none;margin-top:8px"></div>
</div>`).join('');
window.p2BossSolved=new Set();
})();
}
/* ============================================================
§ 3 — СУММА ВНЕШНИХ УГЛОВ ВЫПУКЛОГО МНОГОУГОЛЬНИКА
============================================================ */
function buildP3(){
const box = document.getElementById('p3-body');
let html = '';
html += makeCard('theory','Внешний угол. Теорема','3.1',`
<p><b>Внешний угол</b> выпуклого многоугольника при вершине — угол между стороной и продолжением соседней стороны. Внешний угол при вершине $A_i$:</p>
\\[\\beta_i = 180^{\\circ} - \\alpha_i\\]
<p>где $\\alpha_i$ — соответствующий внутренний угол.</p>
<p><b>Теорема.</b> Сумма всех внешних углов выпуклого многоугольника (по одному у каждой вершины) равна $360°$ — при любом $n$.</p>
<p><b>Объяснение:</b> если обойти многоугольник по периметру, на каждой вершине повернёшься на внешний угол. За полный обход — ровно один полный оборот $= 360°$.</p>`);
html += makeCard('rule','Правильный n-угольник','3.2',`
<p>Внешний угол правильного $n$-угольника:</p>
\\[\\beta = \\dfrac{360^{\\circ}}{n}\\]
<p>Внутренний угол: $\\alpha = 180° - \\dfrac{360°}{n} = \\dfrac{(n-2)\\cdot 180°}{n}$.</p>
<p>Отсюда: зная внешний угол $\\beta$, можно найти $n = \\dfrac{360°}{\\beta}$.</p>`);
html += makeCard('example','Примеры','3.3',`
<p><b>Внешний угол правильного шестиугольника:</b> $\\beta = 360°/6 = 60°$, внутренний $= 180°-60°=120°$.</p>
<p style="margin-top:6px"><b>Внешний угол правильного многоугольника = 24°. Найти n:</b> $n = 360°/24° = 15$. Ответ: 15-угольник.</p>
<p style="margin-top:6px"><b>Сумма внешних углов правильного 100-угольника:</b> всегда $360°$, независимо от $n$!</p>`);
/* --- INTERACTIVE 1: SVG внешние углы + анимация "свернуть в точку" --- */
html += `<div class="wg" id="p3-ext-wrap">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Внешние углы: визуализация</div></div>
<div class="wg-help">Выбери $n$. Посмотри на внешние углы (жёлтые дуги). Нажми «Свернуть» — углы образуют полный оборот $360°$.</div>
<div class="sliders">
<label>$n$ = <b id="p3-ex-nval">6</b><input type="range" min="3" max="10" value="6" id="p3-ex-n" style="accent-color:var(--sec-acc,var(--pri))"></label>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px">
<button class="btn primary" id="p3-ex-fold">Свернуть в точку</button>
<button class="btn" id="p3-ex-reset">Сбросить</button>
</div>
<div id="p3-ex-svg-wrap" style="display:flex;justify-content:center"></div>
<div id="p3-ex-info" style="padding:10px;background:var(--sec-acc-soft,var(--pri-soft));border-radius:10px;margin-top:10px;text-align:center;font-size:.95rem"></div>
</div>`;
/* --- INTERACTIVE 2: Калькулятор правильного n-угольника --- */
html += `<div class="wg">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Калькулятор правильного многоугольника</div></div>
<div class="wg-help">Задай $n$ — получишь и внешний, и внутренний угол.</div>
<div class="sliders">
<label>$n$ = <b id="p3-cl-nval">6</b><input type="range" min="3" max="20" value="6" id="p3-cl-n" style="accent-color:var(--sec-acc,var(--pri))"></label>
</div>
<div id="p3-cl-out" style="padding:14px;background:var(--sec-acc-soft,var(--pri-soft));border-radius:10px;line-height:1.9"></div>
</div>`;
/* --- INTERACTIVE 3: Тренажёр — найти n по внешнему углу --- */
html += `<div class="wg">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 3</span><div class="wg-title">Тренажёр: найди n по внешнему углу</div></div>
<div class="wg-help">Дан один внешний угол правильного n-угольника. Найди n.</div>
<div class="score-display"><span>Задача <b id="p3-tr-i">1</b> / 4</span><span>Очки: <b id="p3-tr-score">0</b></span></div>
<div id="p3-tr-task" style="padding:14px;background:var(--sec-acc-soft,var(--pri-soft));border-radius:10px;font-size:1.05rem;margin-bottom:10px"></div>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<input type="number" id="p3-tr-ans" class="tinp" placeholder="n = ?" style="width:100px">
<button class="btn primary" id="p3-tr-go">Ответить</button>
<button class="btn" id="p3-tr-start">Начать</button>
</div>
<div class="feedback" id="p3-tr-fb" style="display:none"></div>
</div>`;
/* --- INTERACTIVE 4: Mini-quiz --- */
html += `<div class="wg">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 4</span><div class="wg-title">Мини-квиз по §3</div></div>
<div id="p3-quiz-container"></div>
</div>`;
/* --- INTERACTIVE 5: Босс §3 --- */
html += `<div class="wg" style="border-color:var(--sec-acc-d,var(--pri2))">
<div class="wg-header"><span class="wg-badge" style="background:var(--sec-acc-d,var(--pri2))">БОСС §3</span><div class="wg-title">Итоговые задачи §3</div></div>
<div class="wg-help">Реши задачи — +5 XP за каждую верную.</div>
<div id="p3-boss-tasks"></div>
</div>`;
html += `<div style="margin-top:18px;display:flex;justify-content:center">
<button class="btn primary" id="p3-read-btn" onclick="addXp(10,'p3-read');bumpProgress('p3',40);document.getElementById('p3-read-btn').textContent='Прочитано!';document.getElementById('p3-read-btn').disabled=true;">
<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>
Я прочитал §3 (+10 XP)
</button>
</div>`;
html += secNav('p2','p4');
box.innerHTML = html;
if(window.renderMathInElement) setTimeout(()=>renderMath(box), 0);
/* == INIT: SVG внешние углы == */
(function(){
const W=380, H=320, cx=W/2, cy=H/2, R=100, Rext=30;
let n=6, folded=false;
const colors=['#d97706','#10b981','#0891b2','#8b5cf6','#ef4444','#f97316','#14b8a6','#a855f7','#e11d48','#6366f1'];
function pts(nv){ const v=[];for(let i=0;i<nv;i++){const a=-Math.PI/2+2*Math.PI*i/nv;v.push({x:cx+R*Math.cos(a),y:cy+R*Math.sin(a)});}return v; }
function drawNormal(){
const vs=pts(n);
let s='<svg viewBox="0 0 '+W+' '+H+'" style="width:100%;max-width:400px;background:var(--card);border:1px solid var(--border);border-radius:14px">';
s+='<polygon points="'+vs.map(v=>v.x+','+v.y).join(' ')+'" fill="rgba(217,119,6,.10)" stroke="#d97706" stroke-width="2.5" stroke-linejoin="round"/>';
vs.forEach((v,i)=>{
const nxt=vs[(i+1)%n];
const ext=40;
const dx=v.x-nxt.x,dy=v.y-nxt.y;
const len=Math.hypot(dx,dy);
const ex=v.x+ext*dx/len,ey=v.y+ext*dy/len;
s+='<line x1="'+nxt.x+'" y1="'+nxt.y+'" x2="'+ex+'" y2="'+ey+'" stroke="#f59e0b" stroke-width="1.5" stroke-dasharray="4 2"/>';
const extAngle=360/n;
const startAng=Math.atan2(v.y-nxt.y,v.x-nxt.x)*180/Math.PI;
const endAng=startAng-extAngle;
const ra=Rext,sa=startAng*(Math.PI/180),ea=endAng*(Math.PI/180);
const x1=nxt.x+ra*Math.cos(sa),y1=nxt.y+ra*Math.sin(sa);
const x2=nxt.x+ra*Math.cos(ea),y2=nxt.y+ra*Math.sin(ea);
const large=extAngle>180?1:0;
s+='<path d="M '+x1+' '+y1+' A '+ra+' '+ra+' 0 '+large+' 0 '+x2+' '+y2+'" fill="rgba(245,158,11,.35)" stroke="#f59e0b" stroke-width="1.5"/>';
s+='<text x="'+nxt.x+'" y="'+nxt.y+'" text-anchor="middle" dominant-baseline="middle" font-size="9" fill="#92400e" dx="'+(nxt.x-cx)*0.4+'" dy="'+(nxt.y-cy)*0.4+'">'+(extAngle.toFixed(1))+'°</text>';
});
vs.forEach((v,i)=>{
s+='<circle cx="'+v.x+'" cy="'+v.y+'" r="5" fill="#d97706" stroke="#fff" stroke-width="2"/>';
});
s+='</svg>';
document.getElementById('p3-ex-svg-wrap').innerHTML=s;
document.getElementById('p3-ex-info').innerHTML='$n='+n+'$, внешний угол $= \\dfrac{360°}{'+n+'} = '+(360/n).toFixed(2).replace(/\.00$/,'')+'°$, сумма $= '+n+' \\times '+(360/n).toFixed(1)+'° = 360°$';
renderMath(document.getElementById('p3-ex-info'));
}
function drawFolded(){
let s='<svg viewBox="0 0 '+W+' '+H+'" style="width:100%;max-width:400px;background:var(--card);border:1px solid var(--border);border-radius:14px">';
const R2=14;
let cumAngle=0;
for(let i=0;i<n;i++){
const extA=360/n*(Math.PI/180);
const sa=cumAngle,ea=cumAngle+extA;
const x1=cx+80*Math.cos(sa),y1=cy+80*Math.sin(sa);
const x2=cx+80*Math.cos(ea),y2=cy+80*Math.sin(ea);
s+='<path d="M '+cx+' '+cy+' L '+x1+' '+y1+' A 80 80 0 0 1 '+x2+' '+y2+' Z" fill="'+colors[i%colors.length]+'55" stroke="'+colors[i%colors.length]+'" stroke-width="1.5"/>';
const mid=(sa+ea)/2;
s+='<text x="'+(cx+55*Math.cos(mid))+'" y="'+(cy+55*Math.sin(mid))+'" text-anchor="middle" dominant-baseline="middle" font-size="10" fill="'+colors[i%colors.length]+'" font-weight="700">'+(360/n).toFixed(0)+'°</text>';
cumAngle=ea;
}
s+='<circle cx="'+cx+'" cy="'+cy+'" r="4" fill="#d97706"/>';
s+='<text x="'+cx+'" y="'+(cy-95)+'" text-anchor="middle" font-size="14" font-weight="700" fill="#d97706" font-family="Unbounded,sans-serif">360°</text>';
s+='</svg>';
document.getElementById('p3-ex-svg-wrap').innerHTML=s;
document.getElementById('p3-ex-info').innerHTML='Все $'+n+'$ внешних углов по $'+(360/n).toFixed(1)+'°$ образуют <b>полный оборот 360°</b>!';
renderMath(document.getElementById('p3-ex-info'));
addXp(2,'p3-fold'); bumpProgress('p3',10);
}
const nlSl=document.getElementById('p3-ex-n'),nlVal=document.getElementById('p3-ex-nval');
nlSl.addEventListener('input',function(){ n=+this.value; nlVal.textContent=n; folded=false; drawNormal(); });
document.getElementById('p3-ex-fold').addEventListener('click',()=>{ folded=true; drawFolded(); });
document.getElementById('p3-ex-reset').addEventListener('click',()=>{ folded=false; drawNormal(); });
drawNormal();
})();
/* == INIT: Калькулятор == */
(function(){
const sl=document.getElementById('p3-cl-n'),val=document.getElementById('p3-cl-nval'),out=document.getElementById('p3-cl-out');
function update(){
const n=+sl.value; val.textContent=n;
const ext=360/n, int=180-ext;
out.innerHTML='$n='+n+'$<br>Внешний угол: $\\dfrac{360°}{'+n+'} = '+ext.toFixed(2).replace(/\.00$/,'')+'°$<br>Внутренний угол: $180° - '+ext.toFixed(2).replace(/\.00$/,'')+'° = '+int.toFixed(2).replace(/\.00$/,'')+'°$';
renderMath(out);
}
sl.addEventListener('input',update); update();
})();
/* == INIT: Тренажёр == */
(function(){
const tasks=[
{ext:30, n:12, text:'Внешний угол правильного многоугольника равен <b>30°</b>. Найди n.'},
{ext:45, n:8, text:'Внешний угол правильного многоугольника равен <b>45°</b>. Найди n.'},
{ext:60, n:6, text:'Внешний угол правильного многоугольника равен <b>60°</b>. Найди n.'},
{ext:72, n:5, text:'Внешний угол правильного многоугольника равен <b>72°</b>. Найди n.'},
];
let idx=0,score=0;
function show(){
document.getElementById('p3-tr-i').textContent=idx+1;
document.getElementById('p3-tr-task').innerHTML=tasks[idx].text+'<br><small style="color:var(--muted)">Подсказка: n = 360°/внешний угол</small>';
document.getElementById('p3-tr-ans').value='';
document.getElementById('p3-tr-fb').style.display='none';
}
document.getElementById('p3-tr-start').addEventListener('click',()=>{ idx=0;score=0;document.getElementById('p3-tr-score').textContent=0;show(); });
document.getElementById('p3-tr-go').addEventListener('click',()=>{
const ans=+document.getElementById('p3-tr-ans').value;
const fb=document.getElementById('p3-tr-fb');
if(ans===tasks[idx].n){
score++; document.getElementById('p3-tr-score').textContent=score; addXp(3,'p3-train'); bumpProgress('p3',8);
if(idx<tasks.length-1){ feedback(fb,true,'Верно! +3 XP'); idx++; setTimeout(()=>show(),900); }
else{ feedback(fb,true,'Все задачи решены! +5 XP'); addXp(5,'p3-train-all'); bumpProgress('p3',10); }
} else feedback(fb,false,'Неверно. Используй: n=360°/'+tasks[idx].ext+'°='+tasks[idx].n);
});
document.getElementById('p3-tr-ans').addEventListener('keydown',e=>{ if(e.key==='Enter') document.getElementById('p3-tr-go').click(); });
show();
})();
/* == INIT: Мини-квиз == */
(function(){
const qs=[
{q:'Сумма внешних углов выпуклого <b>семиугольника</b> равна...', opts:['360°','1260°','900°','Зависит от n'], ans:0},
{q:'Внешний угол правильного <b>пятиугольника</b> равен...', opts:['60°','72°','108°','120°'], ans:1},
{q:'Если внешний угол правильного многоугольника = 40°, то сторон...', opts:['7','8','9','12'], ans:2},
];
let qi=0,qscore=0,answered=false;
const cont=document.getElementById('p3-quiz-container');
function showQ(){
const q=qs[qi];
cont.innerHTML=`<div style="margin-bottom:10px;font-size:.95rem">${q.q}</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px" id="p3-q-opts">
${q.opts.map((o,i)=>'<button class="btn" id="p3-qopt-'+i+'" onclick="p3QuizAns('+i+')">'+o+'</button>').join('')}
</div>
<div class="feedback" id="p3-q-fb" style="display:none"></div>
<div style="margin-top:8px;display:flex;gap:8px;justify-content:flex-end">
<span style="font-size:.78rem;color:var(--muted)">Вопрос ${qi+1} / ${qs.length} · Очки: <b>${qscore}</b></span>
</div>`;
answered=false;
}
window.p3QuizAns=function(i){
if(answered) return;
answered=true;
const q=qs[qi];
const fb=document.getElementById('p3-q-fb');
if(i===q.ans){ qscore++; feedback(fb,true,'Верно! +3 XP'); addXp(3,'p3-quiz'); bumpProgress('p3',7); }
else feedback(fb,false,'Неверно. Правильный ответ: '+q.opts[q.ans]);
document.querySelectorAll('[id^="p3-qopt-"]').forEach((b,bi)=>{ b.disabled=true; if(bi===q.ans) b.style.background='var(--ok-bg)'; else if(bi===i) b.style.background='var(--fail-bg)'; });
setTimeout(()=>{
qi++;
if(qi<qs.length) showQ();
else cont.innerHTML='<div style="padding:14px;text-align:center;font-size:1.05rem"><b>Квиз завершён! Очки: '+qscore+'/'+qs.length+'</b></div>';
},1300);
};
showQ();
})();
/* == INIT: Босс §3 == */
(function(){
const tasks=[
{ q:'Внешний угол правильного многоугольника равен <b>20°</b>. Сколько сторон?', ans:18, hint:'n=360/20=18' },
{ q:'Внутренний угол правильного многоугольника равен <b>150°</b>. Сколько сторон?', ans:12, hint:'внешний=180-150=30°, n=360/30=12' },
{ q:'Сумма внешних углов выпуклого <b>двадцатиугольника</b> в градусах?', ans:360, hint:'Всегда 360°!' },
{ q:'Внешний угол правильного многоугольника равен <b>36°</b>. Чему равен внутренний угол?', ans:144, hint:'внутренний = 180-36 = 144°' },
];
const bossBox=document.getElementById('p3-boss-tasks');
bossBox.innerHTML=tasks.map((t,i)=>`
<div style="padding:14px;background:var(--card);border-radius:10px;border:1px solid var(--border);margin-bottom:10px">
<div style="margin-bottom:8px;font-size:.95rem">${t.q}</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
<input type="number" class="tinp" id="p3-boss-a${i}" placeholder="Ответ" style="width:100px">
<button class="btn primary small" onclick="(function(){
const v=+document.getElementById('p3-boss-a${i}').value;
const fb=document.getElementById('p3-boss-fb${i}');
if(v===${t.ans}){
feedback(fb,true,'Верно! +5 XP');
if(!p3BossSolved.has(${i})){ p3BossSolved.add(${i}); addXp(5,'p3-boss${i}'); bumpProgress('p3',10); }
} else feedback(fb,false,'Неверно. Подсказка: ${t.hint}');
})()">Проверить</button>
</div>
<div class="feedback" id="p3-boss-fb${i}" style="display:none;margin-top:8px"></div>
</div>`).join('');
window.p3BossSolved=new Set();
})();
}
/* ============================================================
§ 4 — ПАРАЛЛЕЛОГРАММ
============================================================ */
function buildP4(){
const box = document.getElementById('p4-body');
let html = '';
html += makeCard('theory','Определение и элементы','4.1',`
<p><b>Параллелограмм</b> — четырёхугольник, у которого <em>противоположные стороны попарно параллельны</em>:</p>
\\[AB \\parallel CD \\quad \\text{и} \\quad BC \\parallel AD\\]
<p>Обозначение: $\\square ABCD$ или просто $ABCD$.</p>
<p><b>Элементы параллелограмма:</b></p>
<ul style="margin-left:18px;line-height:1.8">
<li><b>Стороны:</b> $AB$, $BC$, $CD$, $DA$ (противоположные: $AB = CD$, $BC = DA$)</li>
<li><b>Углы:</b> противоположные углы равны; смежные углы — дополнение до $180°$</li>
<li><b>Диагонали:</b> $AC$ и $BD$ — пересекаются и делятся точкой пересечения пополам</li>
</ul>`);
html += makeCard('rule','Основные свойства','4.2',`
<p>В параллелограмме $ABCD$:</p>
\\[AB = CD, \\quad BC = AD\\]
\\[\\angle A = \\angle C, \\quad \\angle B = \\angle D\\]
\\[\\angle A + \\angle B = 180^{\\circ}\\]
<p>Эти свойства доказываются через равенство треугольников, на которые диагональ делит параллелограмм.</p>`);
html += makeCard('example','Примеры','4.3',`
<p><b>Квадрат, прямоугольник, ромб</b> — это частные случаи параллелограмма.</p>
<p style="margin-top:6px"><b>Трапеция</b> — НЕ параллелограмм (только одна пара параллельных сторон).</p>
<p style="margin-top:6px"><b>Задача:</b> в параллелограмме $AB = 8$, $BC = 5$. Найти периметр. Решение: $P = 2(8+5) = 26$.</p>
<p style="margin-top:6px"><b>Задача:</b> $\\angle A = 65°$. Найти остальные углы. $\\angle C = 65°$, $\\angle B = \\angle D = 115°$.</p>`);
/* --- INTERACTIVE 1: SVG-конструктор параллелограмма --- */
html += `<div class="wg" id="p4-pgram-wrap">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Конструктор параллелограмма</div></div>
<div class="wg-help">Тащи вершины <b>B</b> и <b>D</b>. Параллелограмм строится автоматически: AB∥CD и BC∥AD. Следи за значениями сторон и углов.</div>
<div id="p4-pgram-svg-wrap" style="display:flex;justify-content:center"></div>
<div id="p4-pgram-info" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:8px;margin-top:10px"></div>
</div>`;
/* --- INTERACTIVE 2: DnD — какие из фигур параллелограммы --- */
html += `<div class="wg" id="p4-dnd-wrap">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Параллелограмм или нет?</div></div>
<div class="wg-help">Разложи фигуры: является ли она параллелограммом (или его частным случаем)?</div>
<div id="p4-dnd-pool"></div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:8px">
<div class="drop-box"><h5 data-cat="yes">Параллелограмм</h5><div class="drop-items" data-cat="yes"></div></div>
<div class="drop-box"><h5 data-cat="no">Не параллелограмм</h5><div class="drop-items" data-cat="no"></div></div>
</div>
<div class="actions"><button class="btn primary" id="p4-dnd-check">Проверить</button><button class="btn" id="p4-dnd-reset">Сначала</button></div>
<div class="feedback" id="p4-dnd-fb" style="display:none"></div>
</div>`;
/* --- INTERACTIVE 3: Пошаговое доказательство --- */
html += `<div class="wg">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 3</span><div class="wg-title">Доказательство: противоположные стороны равны</div></div>
<div class="wg-help">Нажимай «Дальше» — идёт пошаговое доказательство через треугольники.</div>
<div id="p4-proof-svg" style="display:flex;justify-content:center;margin-bottom:10px"></div>
<div id="p4-proof-step" style="padding:12px;background:var(--sec-acc-soft,var(--pri-soft));border-radius:10px;font-size:.95rem;min-height:60px;line-height:1.65"></div>
<div style="display:flex;gap:8px;margin-top:10px">
<button class="btn primary" id="p4-proof-next">Дальше</button>
<button class="btn" id="p4-proof-restart">Сначала</button>
</div>
</div>`;
/* --- INTERACTIVE 4: Тренажёр --- */
html += `<div class="wg">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 4</span><div class="wg-title">Тренажёр задач на параллелограмм</div></div>
<div class="score-display"><span>Задача <b id="p4-tr-i">1</b> / 4</span><span>Очки: <b id="p4-tr-score">0</b></span></div>
<div id="p4-tr-task" style="padding:14px;background:var(--sec-acc-soft,var(--pri-soft));border-radius:10px;font-size:1.05rem;margin-bottom:10px"></div>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<input type="number" id="p4-tr-ans" class="tinp" placeholder="Ответ" style="width:110px">
<button class="btn primary" id="p4-tr-go">Проверить</button>
<button class="btn" id="p4-tr-start">Начать</button>
</div>
<div class="feedback" id="p4-tr-fb" style="display:none"></div>
</div>`;
/* --- INTERACTIVE 5: Босс §4 --- */
html += `<div class="wg" style="border-color:var(--sec-acc-d,var(--pri2))">
<div class="wg-header"><span class="wg-badge" style="background:var(--sec-acc-d,var(--pri2))">БОСС §4</span><div class="wg-title">Итоговые задачи §4</div></div>
<div class="wg-help">+5 XP за каждую верную задачу.</div>
<div id="p4-boss-tasks"></div>
</div>`;
html += `<div style="margin-top:18px;display:flex;justify-content:center">
<button class="btn primary" id="p4-read-btn" onclick="addXp(10,'p4-read');bumpProgress('p4',40);document.getElementById('p4-read-btn').textContent='Прочитано!';document.getElementById('p4-read-btn').disabled=true;">
<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>
Я прочитал §4 (+10 XP)
</button>
</div>`;
html += secNav('p3','p5');
box.innerHTML = html;
if(window.renderMathInElement) setTimeout(()=>renderMath(box), 0);
/* == INIT: SVG-конструктор параллелограмма == */
(function(){
const W=380, H=300;
let A={x:60,y:220}, B={x:180,y:220}, D={x:100,y:100};
function getC(){ return {x:D.x+(B.x-A.x),y:D.y+(B.y-A.y)}; }
function dist(a,b){ return Math.hypot(b.x-a.x,b.y-a.y); }
function angle(O,P,Q){
const ax=P.x-O.x,ay=P.y-O.y,bx=Q.x-O.x,by=Q.y-O.y;
return Math.acos(Math.max(-1,Math.min(1,(ax*bx+ay*by)/(Math.hypot(ax,ay)*Math.hypot(bx,by)))))*180/Math.PI;
}
function redraw(){
const C=getC();
const vs={A,B,C,D};
const pts=[A,B,C,D];
let s='<svg viewBox="0 0 '+W+' '+H+'" style="width:100%;max-width:400px;background:var(--card);border:1px solid var(--border);border-radius:14px;touch-action:none">';
s+='<polygon points="'+pts.map(v=>v.x+','+v.y).join(' ')+'" fill="rgba(220,38,38,.10)" stroke="#dc2626" stroke-width="2.5" stroke-linejoin="round"/>';
s+='<line x1="'+A.x+'" y1="'+A.y+'" x2="'+C.x+'" y2="'+C.y+'" stroke="#94a3b8" stroke-width="1" stroke-dasharray="5 3"/>';
s+='<line x1="'+B.x+'" y1="'+B.y+'" x2="'+D.x+'" y2="'+D.y+'" stroke="#94a3b8" stroke-width="1" stroke-dasharray="5 3"/>';
const labels=['A','B','C','D'];
const cx=(A.x+B.x+C.x+D.x)/4, cy=(A.y+B.y+C.y+D.y)/4;
pts.forEach((v,i)=>{
const movable=(i===1||i===3);
s+='<circle cx="'+v.x+'" cy="'+v.y+'" r="'+(movable?10:7)+'" fill="'+(movable?'#dc2626':'#dc2626')+'" opacity="'+(movable?.25:.15)+'" class="p4-vh" data-v="'+labels[i]+'"/>';
s+='<circle cx="'+v.x+'" cy="'+v.y+'" r="'+(movable?6:5)+'" fill="#dc2626" stroke="#fff" stroke-width="2" '+(movable?'class="p4-vh" data-v="'+labels[i]+'"':'')+'style="cursor:'+(movable?'grab':'default')+'"/>';
const lx=v.x+(v.x-cx)*0.25,ly=v.y+(v.y-cy)*0.25;
s+='<text x="'+lx+'" y="'+ly+'" text-anchor="middle" dominant-baseline="middle" font-size="13" font-weight="700" fill="#b91c1c" font-family="Unbounded,sans-serif">'+labels[i]+'</text>';
});
const sides=[['AB',A,B],['BC',B,C],['CD',C,D],['DA',D,A]];
sides.forEach(([name,p1,p2])=>{
const mx=(p1.x+p2.x)/2,my=(p1.y+p2.y)/2;
const d=dist(p1,p2);
s+='<text x="'+mx+'" y="'+my+'" text-anchor="middle" dominant-baseline="middle" font-size="10" fill="#64748b" font-family="JetBrains Mono,monospace" dy="-7">'+d.toFixed(1)+'</text>';
});
s+='</svg>';
const svg=document.createElement('div');
svg.innerHTML=s;
const svgEl=svg.firstElementChild;
document.getElementById('p4-pgram-svg-wrap').innerHTML='';
document.getElementById('p4-pgram-svg-wrap').appendChild(svgEl);
svgEl.querySelectorAll('.p4-vh').forEach(el=>{
el.style.cursor='grab';
el.addEventListener('pointerdown',ev=>{
if(ev.button!==undefined&&ev.button!==0) return;
const vname=el.dataset.v;
try{el.setPointerCapture(ev.pointerId);}catch(e){}
function onMove(e){
const rect=svgEl.getBoundingClientRect();
const sx=W/rect.width,sy=H/rect.height;
const nx=Math.max(10,Math.min(W-10,(e.clientX-rect.left)*sx));
const ny=Math.max(10,Math.min(H-10,(e.clientY-rect.top)*sy));
if(vname==='B') B={x:nx,y:ny};
else if(vname==='D') D={x:nx,y:ny};
redraw(); updateInfo();
}
function onUp(){ el.removeEventListener('pointermove',onMove);el.removeEventListener('pointerup',onUp);el.removeEventListener('pointercancel',onUp); }
el.addEventListener('pointermove',onMove);
el.addEventListener('pointerup',onUp);
el.addEventListener('pointercancel',onUp);
});
});
updateInfo();
}
function updateInfo(){
const C=getC();
const ab=dist(A,B),bc=dist(B,C),angA=angle(A,D,B),angB=angle(B,A,C);
document.getElementById('p4-pgram-info').innerHTML=`
<div style="padding:8px 12px;background:var(--card);border-radius:8px;border:1px solid var(--border);font-size:.88rem"><div style="color:var(--muted);font-size:.72rem;font-weight:700;text-transform:uppercase;margin-bottom:4px">AB = CD</div><b>${ab.toFixed(1)}</b></div>
<div style="padding:8px 12px;background:var(--card);border-radius:8px;border:1px solid var(--border);font-size:.88rem"><div style="color:var(--muted);font-size:.72rem;font-weight:700;text-transform:uppercase;margin-bottom:4px">BC = DA</div><b>${bc.toFixed(1)}</b></div>
<div style="padding:8px 12px;background:var(--card);border-radius:8px;border:1px solid var(--border);font-size:.88rem"><div style="color:var(--muted);font-size:.72rem;font-weight:700;text-transform:uppercase;margin-bottom:4px">∠A = ∠C</div><b>${angA.toFixed(1)}°</b></div>
<div style="padding:8px 12px;background:var(--card);border-radius:8px;border:1px solid var(--border);font-size:.88rem"><div style="color:var(--muted);font-size:.72rem;font-weight:700;text-transform:uppercase;margin-bottom:4px">∠A + ∠B</div><b>${(angA+angB).toFixed(1)}°</b></div>`;
}
redraw();
})();
/* == INIT: DnD параллелограмм или нет == */
(function(){
const items=[
{id:'rect', html:'Прямоугольник', ans:'yes'},
{id:'rhombus',html:'Ромб', ans:'yes'},
{id:'square', html:'Квадрат', ans:'yes'},
{id:'pgram', html:'Произвольный параллелограмм', ans:'yes'},
{id:'trap', html:'Трапеция', ans:'no'},
{id:'kite', html:'Дельтоид (воздушный змей)', ans:'no'},
{id:'arb4', html:'Произвольный четырёхугольник', ans:'no'},
{id:'tri', html:'Треугольник', ans:'no'},
];
const sorter=setupSorter({poolId:'p4-dnd-pool',scopeSelector:'#p4-dnd-wrap',items,cats:['yes','no']});
document.getElementById('p4-dnd-reset').addEventListener('click',()=>{ sorter.reset(); document.getElementById('p4-dnd-fb').style.display='none'; });
document.getElementById('p4-dnd-check').addEventListener('click',()=>{
let ok=0;
items.forEach(it=>{ if(sorter.placed[it.id]===it.ans) ok++; });
const fb=document.getElementById('p4-dnd-fb');
if(ok===items.length){ feedback(fb,true,'Все '+items.length+' фигур верно! +5 XP'); addXp(5,'p4-dnd'); bumpProgress('p4',15); }
else feedback(fb,false,'Верно: '+ok+' из '+items.length+'. Прямоугольник, ромб, квадрат — частные случаи параллелограмма.');
});
})();
/* == INIT: Пошаговое доказательство == */
(function(){
const steps=[
{ text:'<b>Дано:</b> $ABCD$ — параллелограмм, $AB \\parallel CD$, $BC \\parallel AD$.<br>Проведём диагональ $AC$.', highlight:'' },
{ text:'<b>Шаг 1.</b> $AB \\parallel CD$ и $AC$ — секущая.<br>По свойству параллельных прямых: $\\angle BAC = \\angle DCA$ (накрест лежащие углы).', highlight:'diag' },
{ text:'<b>Шаг 2.</b> $BC \\parallel AD$ и $AC$ — секущая.<br>Аналогично: $\\angle BCA = \\angle DAC$ (накрест лежащие углы).', highlight:'diag' },
{ text:'<b>Шаг 3.</b> $AC$ — общая сторона треугольников $\\triangle ABC$ и $\\triangle CDA$.<br>По признаку «угол-сторона-угол»: $\\triangle ABC = \\triangle CDA$.', highlight:'both' },
{ text:'<b>Вывод.</b> Из равенства треугольников следует: $AB = CD$ и $BC = DA$.<br>Противоположные стороны параллелограмма равны. $\\square$', highlight:'both' },
];
let step=0;
const W=300,H=200;
function drawProof(highlight){
const A={x:30,y:160},B={x:180,y:160},C={x:260,y:50},D={x:110,y:50};
const cx=(A.x+B.x+C.x+D.x)/4;
let s='<svg viewBox="0 0 '+W+' '+H+'" style="width:100%;max-width:320px;background:var(--card);border:1px solid var(--border);border-radius:12px">';
const t1fill=highlight==='diag'||highlight==='both'?'rgba(220,38,38,.15)':'rgba(220,38,38,.07)';
const t2fill=highlight==='both'?'rgba(16,185,129,.15)':'rgba(220,38,38,.07)';
s+='<polygon points="'+A.x+','+A.y+' '+B.x+','+B.y+' '+C.x+','+C.y+' '+D.x+','+D.y+'" fill="rgba(220,38,38,.07)" stroke="#dc2626" stroke-width="2"/>';
if(highlight){
s+='<polygon points="'+A.x+','+A.y+' '+B.x+','+B.y+' '+C.x+','+C.y+'" fill="'+t1fill+'" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="4 2"/>';
s+='<polygon points="'+C.x+','+C.y+' '+D.x+','+D.y+' '+A.x+','+A.y+'" fill="'+t2fill+'" stroke="#10b981" stroke-width="1.5" stroke-dasharray="4 2"/>';
s+='<line x1="'+A.x+'" y1="'+A.y+'" x2="'+C.x+'" y2="'+C.y+'" stroke="#8b5cf6" stroke-width="2"/>';
}
['A','B','C','D'].forEach((lbl,i)=>{
const v=[A,B,C,D][i];
s+='<circle cx="'+v.x+'" cy="'+v.y+'" r="5" fill="#dc2626" stroke="#fff" stroke-width="2"/>';
const lx=v.x+(v.x-cx)*0.28,ly=v.y+(v.y-cy)*0.28;
s+='<text x="'+lx+'" y="'+ly+'" text-anchor="middle" dominant-baseline="middle" font-size="12" font-weight="700" fill="#b91c1c" font-family="Unbounded,sans-serif">'+lbl+'</text>';
});
s+='</svg>';
document.getElementById('p4-proof-svg').innerHTML=s;
}
function showStep(){
const st=steps[step];
document.getElementById('p4-proof-step').innerHTML=st.text;
renderMath(document.getElementById('p4-proof-step'));
drawProof(st.highlight);
document.getElementById('p4-proof-next').textContent=step<steps.length-1?'Дальше':'Готово';
}
document.getElementById('p4-proof-next').addEventListener('click',()=>{
if(step<steps.length-1){ step++; showStep(); }
else{ addXp(5,'p4-proof'); bumpProgress('p4',12); document.getElementById('p4-proof-next').textContent='Доказательство изучено! +5 XP'; document.getElementById('p4-proof-next').disabled=true; }
});
document.getElementById('p4-proof-restart').addEventListener('click',()=>{ step=0; showStep(); document.getElementById('p4-proof-next').disabled=false; document.getElementById('p4-proof-next').textContent='Дальше'; });
showStep();
})();
/* == INIT: Тренажёр задач == */
(function(){
const tasks=[
{ q:'В параллелограмме <b>AB = 11</b>, <b>BC = 7</b>. Найди периметр.', ans:36, hint:'P=2(AB+BC)=2(11+7)=36' },
{ q:'В параллелограмме <b>∠A = 72°</b>. Найди <b>∠B</b> (смежный угол).', ans:108, hint:'∠A+∠B=180°, ∠B=108°' },
{ q:'В параллелограмме <b>∠A = 72°</b>. Найди <b>∠C</b> (противоположный).', ans:72, hint:'∠A=∠C в параллелограмме' },
{ q:'Периметр параллелограмма равен <b>56</b>, одна сторона равна <b>18</b>. Найди вторую сторону.', ans:10, hint:'2(18+x)=56, x=10' },
];
let idx=0,score=0;
function show(){
document.getElementById('p4-tr-i').textContent=idx+1;
document.getElementById('p4-tr-task').innerHTML=tasks[idx].q;
document.getElementById('p4-tr-ans').value='';
document.getElementById('p4-tr-fb').style.display='none';
}
document.getElementById('p4-tr-start').addEventListener('click',()=>{ idx=0;score=0;document.getElementById('p4-tr-score').textContent=0;show(); });
document.getElementById('p4-tr-go').addEventListener('click',()=>{
const ans=+document.getElementById('p4-tr-ans').value;
const fb=document.getElementById('p4-tr-fb');
if(ans===tasks[idx].ans){
score++; document.getElementById('p4-tr-score').textContent=score; addXp(3,'p4-train'); bumpProgress('p4',6);
if(idx<tasks.length-1){ feedback(fb,true,'Верно! +3 XP'); idx++; setTimeout(()=>show(),900); }
else{ feedback(fb,true,'Все задачи решены! +5 XP'); addXp(5,'p4-train-all'); bumpProgress('p4',10); }
} else feedback(fb,false,'Неверно. Подсказка: '+tasks[idx].hint);
});
document.getElementById('p4-tr-ans').addEventListener('keydown',e=>{ if(e.key==='Enter') document.getElementById('p4-tr-go').click(); });
show();
})();
/* == INIT: Босс §4 == */
(function(){
const tasks=[
{ q:'В параллелограмме $AB = 13$, $BC = 9$. Найди периметр.', ans:44, hint:'P=2(13+9)=44' },
{ q:'В параллелограмме $\\angle A = 55°$. Найди $\\angle D$ (смежный с $\\angle A$).', ans:125, hint:'∠A+∠D=180°, ∠D=125°' },
{ q:'В параллелограмме $\\angle B = 130°$. Найди $\\angle A$.', ans:50, hint:'∠A+∠B=180°, ∠A=50°' },
{ q:'Периметр параллелограмма равен 80, одна сторона = 25. Найди вторую сторону.', ans:15, hint:'2(25+x)=80, x=15' },
];
const bossBox=document.getElementById('p4-boss-tasks');
bossBox.innerHTML=tasks.map((t,i)=>`
<div style="padding:14px;background:var(--card);border-radius:10px;border:1px solid var(--border);margin-bottom:10px">
<div style="margin-bottom:8px;font-size:.95rem">${t.q}</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
<input type="number" class="tinp" id="p4-boss-a${i}" placeholder="Ответ" style="width:100px">
<button class="btn primary small" onclick="(function(){
const v=+document.getElementById('p4-boss-a${i}').value;
const fb=document.getElementById('p4-boss-fb${i}');
if(v===${t.ans}){
feedback(fb,true,'Верно! +5 XP');
if(!p4BossSolved.has(${i})){ p4BossSolved.add(${i}); addXp(5,'p4-boss${i}'); bumpProgress('p4',10); }
} else feedback(fb,false,'Неверно. Подсказка: ${t.hint}');
})()">Проверить</button>
</div>
<div class="feedback" id="p4-boss-fb${i}" style="display:none;margin-top:8px"></div>
</div>`).join('');
window.p4BossSolved=new Set();
})();
renderMath(box);
}
function buildP5stub(){ document.getElementById('p5-body').innerHTML = '<div class="card"><div class="card-body"><p><b>§5 — Волна 1</b>: содержимое появится в следующем обновлении.</p></div></div>' + secNav('p4','p6'); }
function buildP6stub(){ document.getElementById('p6-body').innerHTML = '<div class="card"><div class="card-body"><p><b>§6 — Волна 1</b>: содержимое появится в следующем обновлении.</p></div></div>' + secNav('p5','p7'); }
function buildP7stub(){ document.getElementById('p7-body').innerHTML = '<div class="card"><div class="card-body"><p><b>§7 — Волна 1</b>: содержимое появится в следующем обновлении.</p></div></div>' + secNav('p6','p8'); }
function buildP8stub(){ document.getElementById('p8-body').innerHTML = '<div class="card"><div class="card-body"><p><b>§8 — Волна 1</b>: содержимое появится в следующем обновлении.</p></div></div>' + secNav('p7','p9'); }
function buildP9stub(){ document.getElementById('p9-body').innerHTML = '<div class="card"><div class="card-body"><p><b>§9 — Волна 1</b>: содержимое появится в следующем обновлении.</p></div></div>' + secNav('p8','p10'); }
function buildP10stub(){ document.getElementById('p10-body').innerHTML = '<div class="card"><div class="card-body"><p><b>§10 — Волна 1</b>: содержимое появится в следующем обновлении.</p></div></div>' + secNav('p9','p11'); }
function buildP11stub(){ document.getElementById('p11-body').innerHTML = '<div class="card"><div class="card-body"><p><b>§11 — Волна 1</b>: содержимое появится в следующем обновлении.</p></div></div>' + secNav('p10','p12'); }
function buildP12stub(){ document.getElementById('p12-body').innerHTML = '<div class="card"><div class="card-body"><p><b>§12 — Волна 1</b>: содержимое появится в следующем обновлении.</p></div></div>' + secNav('p11','p13'); }
function buildP13stub(){ document.getElementById('p13-body').innerHTML = '<div class="card"><div class="card-body"><p><b>§13 — Волна 1</b>: содержимое появится в следующем обновлении.</p></div></div>' + secNav('p12','p14'); }
function buildP14stub(){ document.getElementById('p14-body').innerHTML = '<div class="card"><div class="card-body"><p><b>§14 — Волна 1</b>: содержимое появится в следующем обновлении.</p></div></div>' + secNav('p13','p15'); }
function buildP15stub(){ document.getElementById('p15-body').innerHTML = '<div class="card"><div class="card-body"><p><b>§15 — Волна 1</b>: содержимое появится в следующем обновлении.</p></div></div>' + secNav('p14','p16'); }
function buildP16stub(){ document.getElementById('p16-body').innerHTML = '<div class="card"><div class="card-body"><p><b>§16 — Волна 1</b>: содержимое появится в следующем обновлении.</p></div></div>' + secNav('p15','final1'); }
function buildFinal1stub(){ document.getElementById('final1-body').innerHTML = '<div class="card"><div class="card-body"><p><b>Финал главы 1 — Волна 1</b>: боссы и итоги появятся в следующем обновлении.</p></div></div>' + secNav('p16',null); }
</script>
</body>
</html>