Files
Learn_System/frontend/textbooks/geometry_9_ch4.html
T
Maxim Dolgolyov 660e7e2747 feat(gamification): Phase 1 — full kill-switch + textbook XP wrapping
Until now the 'gamification' feature flag did nothing: it had no row in
app_settings, the admin couldn't toggle it, awardXP/awardCoins ignored
it, and the CSS only hid three dashboard widgets — XP bars in textbooks
stayed visible regardless.

Phase 1 closes every hole.

Backend (source of truth):
  • migration 029 seeds feature_gamification_enabled=1
  • new isGamificationEnabled() helper in gamification/_shared.js with a
    30s cache + invalidateGamificationCache() for instant admin toggles
  • awardXP / awardCoins / updateStreak / unlockAchievement /
    checkAchievements all bail out when the flag is off
  • /api/gamification/* and /api/shop/* (user routes) return 404 when
    disabled; admin routes remain open so the switch itself is reachable
  • adminController.updateFeatures gains 'gamification' in the allow-list
    and invalidates the cache on flip

Frontend:
  • LS.isGamificationEnabled() (synchronous, populated by loadFeatures)
    so xp.js + applyCosmetics can bail without a round-trip
  • xp.js load/add/flush become no-ops when the flag is off
  • applyCosmetics skips the round-trip when off
  • CSS .no-gamification rule expanded to cover .hero-xp-badge, .po-xp,
    .xp-card, .xp-bar, #frames-section, and a universal [data-gamified]
    hook for future blocks

Textbooks (Variant 2 of the plan):
  • backend/scripts/wrap_textbook_xp.py — idempotent script that adds
    data-gamified to 167 XP tags across 63 textbook files (chapters +
    hubs, all subjects/grades). Single CSS rule now hides everything.

Verified end-to-end: with the flag off, awardXP/awardCoins write nothing;
flipping back restores normal behavior.

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

2083 lines
141 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>Геометрия 9 · Глава 4 · Правильные многоугольники</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"
onload="renderMathInElement(document.body,{delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false})"></script>
<script src="/js/api.js" defer></script>
<script src="/js/xp.js" defer></script>
<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:#0891b2; --pri2:#0e7490; --pri-soft:#cffafe;
--acc:#22d3ee; --acc2:#0891b2; --acc-soft:#ecfeff;
--ok:#10b981; --ok-bg:#d1fae5; --warn:#f59e0b; --warn-bg:#fef3c7;
--bad:#ef4444; --fail:#dc2626; --fail-bg:#fee2e2;
}
.dark{--bg:#04141a; --card:#0a1b22; --card-soft:#0d2229; --text:#e0fcff; --ink:#e0fcff; --muted:#7aa8b3; --border:#163842}
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
html,body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.55;font-size:15px}
button,input,select,textarea{font-family:inherit;font-size:inherit}
button{cursor:pointer;border:0;background:transparent;color:inherit}
a{color:inherit;text-decoration:none}
.ic{width:16px;height:16px;display:inline-block;flex-shrink:0;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}
.hdr{position:relative;background:linear-gradient(110deg,#164e63 0%,#0891b2 55%,#22d3ee 100%);color:#fff;padding:46px 22px 30px;overflow:hidden;border-bottom:2px solid rgba(165,243,252,.2);min-height:130px}
.hdr::before{content:'ГЛАВА 4';position:absolute;right:-12px;top:50%;transform:translateY(-50%);font-family:'Unbounded',sans-serif;font-size:clamp(5rem,15vw,11rem);font-weight:900;letter-spacing:-.04em;color:transparent;-webkit-text-stroke:1.5px rgba(209,250,255,.12);line-height:1;pointer-events:none;user-select:none;z-index:0}
.hdr-row{position:relative;z-index:1;display:flex;align-items:center;gap:14px;flex-wrap:wrap}
.hdr h1{font-family:'Unbounded',sans-serif;font-size:1.5rem;font-weight:900;letter-spacing:-.01em;line-height:1.3;padding-top:4px}
.hdr-sub{font-size:.85rem;opacity:.88;margin-top:6px;font-weight:500;line-height:1.4}
.hdr-side{margin-left:auto;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
.hdr-btn{padding:7px 12px;border-radius:9px;background:rgba(255,255,255,.14);color:#fff;font-weight:600;font-size:.82rem;display:inline-flex;align-items:center;gap:6px;transition:background .15s;text-decoration:none}
.hdr-btn:hover{background:rgba(255,255,255,.24)}
.main{max-width:1240px;margin:0 auto;padding:22px;width:100%;display:grid;grid-template-columns:1fr 280px;gap:24px}
@media(max-width:980px){.main{grid-template-columns:1fr;padding:14px}}
.col-main{min-width:0}
.hero{background:linear-gradient(135deg,var(--pri-soft) 0%,var(--acc-soft) 50%,var(--pri-soft) 100%);background-size:200% 200%;animation:heroShift 12s ease-in-out infinite;border:1px solid var(--border);border-radius:18px;padding:24px 22px;margin-bottom:24px;position:relative;overflow:hidden}
@keyframes heroShift{0%,100%{background-position:0% 50%}50%{background-position:100% 50%}}
.hero::before{content:'⬢';position:absolute;right:0;top:-30px;font-size:clamp(2rem,12vw,8rem);font-weight:900;color:var(--pri);opacity:.10;line-height:1;pointer-events:none;font-family:'Unbounded',sans-serif}
.hero h2{font-family:'Unbounded',sans-serif;font-size:1.55rem;font-weight:800;color:var(--pri2);margin-bottom:10px;letter-spacing:-.01em}
.hero p{font-size:.95rem;color:var(--text);opacity:.88;margin-bottom:14px;max-width:640px}
.hero-row{display:flex;gap:14px;flex-wrap:wrap;align-items:center}
.btn-primary{padding:11px 22px;background:linear-gradient(135deg,var(--pri),var(--pri2));color:#fff;border-radius:11px;font-weight:700;font-size:.92rem;display:inline-flex;align-items:center;gap:8px;box-shadow:var(--sh2);transition:transform .15s,box-shadow .15s}
.btn-primary:hover{transform:translateY(-1px);box-shadow:0 8px 28px rgba(0,0,0,.18)}
.hero-progress{flex:1;min-width:200px;max-width:280px}
.hp-label{font-size:.74rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;display:block;margin-bottom:5px}
.hp-bar{height:8px;background:rgba(0,0,0,.12);border-radius:5px;overflow:hidden}
.hp-fill{height:100%;background:linear-gradient(90deg,var(--pri),var(--acc));border-radius:5px;width:0%;transition:width .6s cubic-bezier(.16,1,.3,1)}
.hp-text{font-size:.78rem;color:var(--muted);font-weight:700;margin-top:4px;display:block}
.hero-xp-badge{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:linear-gradient(135deg,var(--warn,#f59e0b),var(--pri));color:#fff;border-radius:99px;font-size:.82rem;font-weight:800;letter-spacing:.02em;box-shadow:0 4px 12px rgba(0,0,0,.18);font-family:'Unbounded',sans-serif}
.psel{margin-bottom:24px}
.psel-title{font-size:.72rem;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:.08em;margin-bottom:10px}
.psel-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:10px}
.psel-card{background:var(--card);border:1.5px solid var(--border);border-radius:13px;padding:14px;cursor:pointer;transition:transform .2s,box-shadow .2s,border-color .2s;text-align:left;position:relative}
.psel-card:hover{transform:translateY(-3px);box-shadow:var(--sh2);border-color:var(--pri)}
.psel-card.active{border-color:var(--pri);background:linear-gradient(135deg,var(--pri-soft),var(--card));box-shadow:var(--sh2)}
.psel-card.active::after{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,var(--pri),var(--acc));border-radius:13px 13px 0 0}
.psel-num{font-family:'Unbounded',sans-serif;font-size:.72rem;font-weight:800;color:var(--pri);text-transform:uppercase;letter-spacing:.08em;margin-bottom:5px}
.psel-name{font-size:.86rem;font-weight:700;color:var(--text);line-height:1.3;margin-bottom:8px}
.psel-prog{height:4px;background:rgba(0,0,0,.10);border-radius:3px;overflow:hidden}
.psel-prog-fill{height:100%;background:var(--pri);width:0%;transition:width .4s}
.psel-card.final{background:linear-gradient(135deg,#fff5e1,#fef3c7)}
.psel-card.final .psel-num{color:var(--warn)}
.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(--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(--pri-soft);position:relative;z-index:1}
.sec-num{display:inline-block;padding:4px 10px;background:linear-gradient(135deg,var(--pri),var(--pri2));color:#fff;border-radius:7px;font-family:'Unbounded',sans-serif;font-size:.78rem;font-weight:800;letter-spacing:.04em;margin-bottom:8px}
.sec-h{font-family:'Unbounded',sans-serif;font-size:1.6rem;font-weight:800;color:var(--pri2);letter-spacing:-.01em;line-height:1.25}
.card{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:18px 20px;margin-bottom:16px;box-shadow:0 1px 3px rgba(0,0,0,.04),0 8px 24px rgba(0,0,0,.04);position:relative;z-index:1;transition:transform .25s cubic-bezier(.16,1,.3,1),box-shadow .25s}
.card:hover{transform:translateY(-2px);box-shadow:0 4px 10px rgba(0,0,0,.06),0 16px 36px rgba(0,0,0,.08)}
.card-header{display:flex;align-items:center;gap:10px;margin-bottom:12px;padding-bottom:10px;border-bottom:1px dashed var(--border)}
.card-icon{width:32px;height:32px;border-radius:9px;display:flex;align-items:center;justify-content:center;flex-shrink:0;color:#fff}
.card-icon.repeat{background:#0ea5e9}.card-icon.theory{background:#8b5cf6}.card-icon.algo{background:#f59e0b}.card-icon.rule{background:#ec4899}.card-icon.example{background:#10b981}.card-icon.oral{background:#06b6d4}
.card-icon .ic{width:18px;height:18px}
.card-title{font-family:'Unbounded',sans-serif;font-size:.82rem;font-weight:800;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);flex:1}
.card-num{font-size:.74rem;font-weight:700;color:var(--muted);background:var(--pri-soft);padding:3px 7px;border-radius:5px}
.card-body{font-size:.94rem;line-height:1.65}
.card-body p{margin-bottom:8px}
.card-body p:last-child{margin-bottom:0}
.btn{padding:8px 16px;border-radius:8px;background:var(--card);color:var(--text);border:1.5px solid var(--border);font-weight:600;font-size:.88rem;transition:background .15s,border-color .15s,transform .1s}
.btn:hover{background:var(--pri-soft);border-color:var(--pri)}
.btn:active{transform:scale(.96)}
.btn.primary{background:var(--pri);color:#fff;border-color:var(--pri)}
.btn.primary:hover{background:var(--pri2);border-color:var(--pri2)}
.feedback{padding:10px 14px;border-radius:9px;font-weight:600;font-size:.88rem;margin-top:8px;display:none}
.feedback.ok{display:block;background:var(--ok-bg);color:#065f46;border-left:4px solid var(--ok)}
.feedback.fail{display:block;background:var(--fail-bg);color:#7f1d1d;border-left:4px solid var(--fail)}
.col-side{position:sticky;top:14px;align-self:start;height:fit-content;max-height:calc(100vh - 28px);overflow-y:auto}
.sidecard{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:16px;margin-bottom:14px;box-shadow:var(--sh)}
.sidecard h4{font-family:'Unbounded',sans-serif;font-size:.74rem;font-weight:800;color:var(--pri2);text-transform:uppercase;letter-spacing:.07em;margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid var(--border)}
.sidecard-row{margin-bottom:8px;font-size:.86rem;line-height:1.6}
.sidecard-row b{color:var(--pri);font-weight:700}
.sidecard-row:last-child{margin-bottom:0}
@media(max-width:980px){.col-side{position:static;max-height:none}}
.xp-card{background:linear-gradient(135deg,var(--acc-soft),var(--pri-soft));border:1.5px solid var(--acc);border-radius:12px;padding:14px;margin-bottom:14px}
.xp-card-title{font-size:.68rem;font-weight:800;color:var(--acc2);text-transform:uppercase;letter-spacing:.07em;margin-bottom:8px;display:flex;align-items:center;justify-content:space-between}
.xp-level{font-size:1.1rem;font-weight:900;color:var(--acc2);font-family:'Unbounded',sans-serif}
.xp-bar{height:9px;background:rgba(0,0,0,.10);border-radius:6px;overflow:hidden;margin:7px 0}
.xp-fill{height:100%;background:linear-gradient(90deg,var(--acc),var(--pri));border-radius:6px;transition:width .5s cubic-bezier(.4,0,.2,1)}
.xp-nums{font-size:.74rem;color:var(--muted);display:flex;justify-content:space-between}
.sec-nav{display:flex;gap:10px;margin-top:24px;padding-top:20px;border-top:1px solid var(--border);justify-content:space-between;flex-wrap:wrap}
.foot{text-align:center;padding:30px 16px;color:var(--muted);font-size:.78rem;border-top:1px solid var(--border);margin-top:30px}
.ach-popup{position:fixed;top:80px;right:18px;background:linear-gradient(135deg,var(--pri),var(--acc));color:#fff;padding:12px 18px;border-radius:11px;font-weight:700;font-size:.9rem;box-shadow:0 8px 28px rgba(0,0,0,.32);z-index:1002;display:none;align-items:center;gap:8px;max-width:340px}
.ach-popup.show{display:flex}
.col-side-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.42);z-index:9990;display:none}
.col-side-backdrop.show{display:block}
@media(max-width:980px){
.col-side{position:fixed;top:0;right:0;height:100vh;width:300px;max-width:88vw;background:var(--bg);box-shadow:-12px 0 24px rgba(0,0,0,.18);padding:18px 16px;overflow-y:auto;transform:translateX(100%);transition:transform .25s ease;z-index:9991;max-height:none}
.col-side.open{transform:none}
}
.search-modal{position:fixed;inset:0;background:rgba(15,23,42,.55);backdrop-filter:blur(4px);z-index:9993;display:none;align-items:flex-start;justify-content:center;padding-top:14vh}
.search-modal.show{display:flex}
.search-box{background:var(--bg);border:1px solid var(--border);border-radius:14px;width:560px;max-width:92vw;max-height:70vh;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 24px 64px rgba(0,0,0,.4)}
.search-input{padding:14px 16px;font-size:1rem;border:0;border-bottom:1px solid var(--border);background:transparent;color:var(--text);outline:none}
.search-results{flex:1;overflow-y:auto;padding:6px 0}
.search-row{display:block;padding:8px 16px;cursor:pointer;border-bottom:1px solid var(--border);text-align:left;background:transparent;border:0;width:100%;color:var(--text)}
.search-row:hover,.search-row.active{background:var(--pri-soft)}
.search-row .sr-kind{font-size:.7rem;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:2px}
.search-row .sr-title{font-weight:700;font-size:.92rem;color:var(--text)}
.search-row .sr-desc{font-size:.8rem;color:var(--muted);margin-top:2px}
.search-empty{padding:20px;text-align:center;color:var(--muted);font-size:.88rem}
.search-foot{padding:8px 14px;border-top:1px solid var(--border);font-size:.74rem;color:var(--muted);display:flex;gap:14px}
.search-foot kbd{padding:2px 6px;background:var(--card);border:1px solid var(--border);border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:.72rem}
.wg{background:linear-gradient(135deg,var(--card),var(--pri-soft));border:1.5px solid 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(--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(--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),var(--pri-soft));border-left:4px solid var(--warn);padding:9px 14px;border-radius:9px}
.tinp{padding:8px 12px;border:1.5px solid var(--border);border-radius:8px;background:var(--card);color:var(--text);transition:border-color .15s;font-family:'JetBrains Mono',monospace}
.tinp:focus{outline:0;border-color:var(--pri);box-shadow:0 0 0 3px var(--pri-soft)}
.sliders{display:flex;gap:14px;flex-wrap:wrap;margin-bottom:12px}
.sliders label{flex:1;min-width:180px;font-size:.85rem;color:var(--text);font-weight:600}
.sliders label b{color:var(--pri);font-family:'JetBrains Mono',monospace;margin-left:6px}
.sliders input[type=range]{width:100%;margin-top:6px;accent-color:var(--pri)}
.score-display{display:flex;gap:14px;flex-wrap:wrap;align-items:center;padding:10px 14px;background:var(--pri-soft);border-radius:10px;margin-bottom:12px}
.score-display b{color:var(--pri2);font-size:1.15rem}
.spoiler{border:1px solid var(--border);border-radius:10px;background:var(--card);margin:10px 0;overflow:hidden}
.spoiler summary{padding:8px 14px;background:var(--pri-soft);font-weight:700;cursor:pointer;font-size:.88rem;color: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(--pri);width:18px}
.spoiler[open] summary::before{content:'\2212'}
.spoiler-body{padding:10px 14px;font-size:.92rem;line-height:1.6}
.dnd-pool{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:14px;padding:10px;border:1.5px dashed var(--border);border-radius:10px;min-height:54px;transition:border-color .18s,background .18s}
.dnd-pool.over{border-color:var(--pri);background: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(--pri);box-shadow:var(--sh)}
.dnd-chip:active{cursor:grabbing}
.dnd-chip.armed{border-color:var(--pri);background:var(--pri-soft);box-shadow:0 0 0 3px rgba(8,145,178,.22);transform:translateY(-1px)}
.dnd-chip.dragging{opacity:.28}
.dnd-chip.placed{background:var(--pri-soft);border-color: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);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(--pri);background:var(--pri-soft)}
.drop-box h5{font-family:'Unbounded',sans-serif;font-size:.78rem;color:var(--pri2);margin-bottom:8px;text-transform:uppercase;letter-spacing:.05em}
.drop-box.over{border-color:var(--pri);background:var(--pri-soft);border-style:solid;transform:scale(1.015)}
/* === GEOM9 POLISH (visual + micro-interactions) === */
@keyframes wgFadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
.sec.active .wg{animation:wgFadeIn .35s cubic-bezier(.16,1,.3,1) backwards}
.sec.active .wg:nth-of-type(1){animation-delay:.02s}
.sec.active .wg:nth-of-type(2){animation-delay:.08s}
.sec.active .wg:nth-of-type(3){animation-delay:.14s}
.sec.active .wg:nth-of-type(4){animation-delay:.20s}
.sec.active .wg:nth-of-type(5){animation-delay:.26s}
.sec.active .wg:nth-of-type(6){animation-delay:.32s}
.wg svg{transition:filter .25s ease}
.wg:hover svg{filter:drop-shadow(0 4px 16px rgba(0,0,0,.10))}
input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
.wg input[type=range]{cursor:ew-resize}
.score-display b{transition:transform .22s cubic-bezier(.16,1,.3,1),color .22s;display:inline-block;transform-origin:center}
.score-display b.bump{transform:scale(1.28);color:var(--pri)}
.katex{transition:color .2s}
.wg-help .katex:hover,.card-body .katex:hover{color:var(--pri2,var(--pri));cursor:help}
.hp-fill,.psel-prog-fill,.xp-fill,[id$="-overall-fill"]{transition:width .6s cubic-bezier(.16,1,.3,1)!important}
.boss-card,.btn.primary,.btn-primary{position:relative;overflow:hidden}
.btn.primary::after,.btn-primary::after{content:'';position:absolute;inset:0;background:radial-gradient(circle at center,rgba(255,255,255,.42) 0%,transparent 60%);opacity:0;transition:opacity .25s;pointer-events:none}
.btn.primary:hover::after,.btn-primary:hover::after{opacity:1}
.psel-card{position:relative}
.psel-card .psel-done{position:absolute;top:6px;right:6px;width:18px;height:18px;border-radius:50%;background:#10b981;display:none;align-items:center;justify-content:center;box-shadow:0 2px 6px rgba(16,185,129,.45);z-index:2}
.psel-card .psel-done svg{width:11px;height:11px;stroke:#fff;fill:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:round}
.psel-card.done .psel-done{display:flex}
.boss-card{transition:border-color .35s,box-shadow .6s,background .3s,transform .2s}
.boss-card.glow{box-shadow:0 0 24px rgba(16,185,129,.6),0 0 0 2px rgba(16,185,129,.45)!important}
@keyframes bossPulse{0%{box-shadow:0 0 0 0 rgba(16,185,129,.55)}70%{box-shadow:0 0 0 14px rgba(16,185,129,0)}100%{box-shadow:0 0 0 0 rgba(16,185,129,0)}}
.boss-card.pulse{animation:bossPulse .8s ease-out}
.psel-card{transition:transform .2s,box-shadow .2s,border-color .2s,background .25s}
.sec{transition:opacity .25s}
</style>
</head>
<body>
<header class="hdr">
<div class="hdr-row">
<div>
<h1>Геометрия 9 · Глава 4</h1>
<div class="hdr-sub">Угол · радиусы · длина окружности · площадь круга</div>
</div>
<div class="hdr-side">
<a href="/textbook/geometry-9" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> К геометрии 9</a>
<button id="search-btn" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m21 21-4-4"/></svg> Поиск</button>
<button id="sidebar-btn" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="18" x2="14" y2="18"/></svg> Шпаргалка</button>
<button id="theme-btn" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg><span id="theme-lab">Тёмная</span></button>
</div>
</div>
</header>
<main class="main">
<div class="col-main">
<section class="hero">
<h2>Правильные многоугольники</h2>
<p>Здесь мы изучаем <b>правильные многоугольники</b>, формулу внутреннего угла $\beta = \tfrac{180^\circ(n-2)}{n}$, связи стороны и радиуса описанной окружности, частные случаи (треугольник, квадрат, шестиугольник) и формулы $C = 2\pi R$, $S = \pi R^2$.</p>
<div class="hero-row">
<button class="btn-primary" onclick="goTo('p13')"><svg class="ic" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4" fill="currentColor" stroke="none"/></svg> Начать § 13</button>
<div class="hero-progress">
<span class="hp-label">Прогресс по главе</span>
<div class="hp-bar"><div id="hero-hp-fill" class="hp-fill"></div></div>
<span id="hero-hp-text" class="hp-text">0%</span>
</div>
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
</div>
</section>
<section class="psel">
<div class="psel-title">Параграфы главы</div>
<div id="psel-grid" class="psel-grid"></div>
</section>
<section id="sec-p13" class="sec" data-watermark="⬢"><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="R"><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="△□⬡"><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="⊙"><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-final4" class="sec" data-watermark="★"><div class="sec-header"><span class="sec-num" style="background:linear-gradient(135deg,#0891b2,#22d3ee)"></span><h2 class="sec-h">Итоги главы 4</h2></div><div id="final4-body"></div></section>
</div>
<aside class="col-side" id="col-side"><div id="sidebar-content"></div></aside>
<div class="col-side-backdrop" id="col-side-backdrop"></div>
</main>
<footer class="foot">Интерактивный учебник «Геометрия 9» · Глава 4 · Правильные многоугольники · LearnSpace</footer>
<div id="ach-popup" class="ach-popup"><svg class="ic" viewBox="0 0 24 24" style="width:22px;height:22px"><polygon points="12,2 22,20 2,20"/></svg><span id="ach-text">Достижение!</span></div>
<div id="search-modal" class="search-modal" role="dialog">
<div class="search-box">
<input type="text" id="search-input" class="search-input" placeholder="Поиск…" autocomplete="off">
<div id="search-results" class="search-results"></div>
<div class="search-foot"><span><kbd>↑↓</kbd> навигация</span><span><kbd>Enter</kbd> открыть</span><span><kbd>Esc</kbd> закрыть</span></div>
</div>
</div>
<script>
'use strict';
const STATE = { current:'p13', progress:{p13:0,p14:0,p15:0,p16:0,final4:0}, achievements:new Map(), xp:0, level:1 };
const TOTAL_PARAS = 5;
const _TB_SLUG = 'geometry-9-ch4';
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:'Начало главы 4!',
p13_done:'Правильные многоугольники освоены!',
p15_done:'Треугольник, квадрат, шестиугольник освоены!',
p16_done:'Длина окружности и площадь круга освоены!',
ch4_done:'Глава 4 пройдена! Геометрия 9 — финал!'
};
function loadProgress(){
try{
const s=localStorage.getItem('geometry9_ch4_progress'); if(s) Object.assign(STATE.progress, JSON.parse(s));
const a=localStorage.getItem('geometry9_ch4_achievements');
if(a){ const p=JSON.parse(a); if(Array.isArray(p)) p.forEach(id=>STATE.achievements.set(id, ACH_LABELS[id]||id)); else if(p&&typeof p==='object'){ for(const[id,t] of Object.entries(p)) STATE.achievements.set(id,(t&&t!==id)?t:(ACH_LABELS[id]||id)); } }
STATE.xp=+(localStorage.getItem('geometry9_xp')||0); STATE.level=calcLevel(STATE.xp);
}catch(e){}
}
function saveProgress(){
try{
localStorage.setItem('geometry9_ch4_progress', JSON.stringify(STATE.progress));
localStorage.setItem('geometry9_ch4_achievements', JSON.stringify(Object.fromEntries(STATE.achievements)));
localStorage.setItem('geometry9_xp', String(STATE.xp));
}catch(e){}
}
function bumpProgress(key, delta){
STATE.progress[key]=Math.max(0,Math.min(100,(STATE.progress[key]||0)+delta));
saveProgress(); refreshProgressUI();
if(STATE.progress[key]>=50) markParaRead(key);
}
const _markedRead=new Set();
let _pendingProgressBody=null, _progressTimer=null;
function _flushProgress(){
const body=_pendingProgressBody; _pendingProgressBody=null; if(!body) return;
const tok=(window.LS&&LS.getToken)?LS.getToken():''; if(!tok) return;
fetch('/api/textbooks/'+_TB_SLUG+'/progress',{method:'POST',headers:{'Content-Type':'application/json','Authorization':'Bearer '+tok},body:JSON.stringify(body),keepalive:true}).catch(()=>{});
}
function _queueProgress(patch){ _pendingProgressBody=Object.assign(_pendingProgressBody||{},patch); if(_progressTimer) clearTimeout(_progressTimer); _progressTimer=setTimeout(_flushProgress, 600); }
function markLastPara(id){ _queueProgress({last_para:id}); }
function markParaRead(id){ if(_markedRead.has(id)) return; _markedRead.add(id); _queueProgress({mark_read:id}); }
window.addEventListener('beforeunload', _flushProgress);
function loadServerReadState(){
const tok=(window.LS&&LS.getToken)?LS.getToken():''; if(!tok) return;
fetch('/api/textbooks/'+_TB_SLUG,{headers:{'Authorization':'Bearer '+tok}}).then(r=>r.ok?r.json():null).then(d=>{
if(!d||!d.progress) return;
(d.progress.read||[]).forEach(k=>{_markedRead.add(k); if((STATE.progress[k]||0)<50) STATE.progress[k]=100;});
saveProgress(); refreshProgressUI();
}).catch(()=>{});
}
function addXp(n,src){
if(!n) return;
const prev=STATE.level; STATE.xp=Math.max(0,(STATE.xp||0)+n); STATE.level=calcLevel(STATE.xp);
saveProgress(); refreshProgressUI();
if(window.LS&&window.LS.xp) window.LS.xp.add(n,'geometry9-ch4-'+(src||'misc'));
if(STATE.level>prev){
const pop=document.getElementById('ach-popup');
if(pop){ document.getElementById('ach-text').textContent='Уровень '+STATE.level+'!'; pop.classList.add('show'); setTimeout(()=>pop.classList.remove('show'),2600); }
}
}
function refreshProgressUI(){
const total=Math.round(Object.values(STATE.progress).reduce((a,b)=>a+b,0)/TOTAL_PARAS);
const f=document.getElementById('hero-hp-fill'); if(f) f.style.width=total+'%';
const t=document.getElementById('hero-hp-text'); if(t) t.textContent=total+'% пройдено';
document.querySelectorAll('[data-prog-card]').forEach(el=>{ const k=el.dataset.progCard; const fl=el.querySelector('.psel-prog-fill'); if(fl) fl.style.width=(STATE.progress[k]||0)+'%'; });
const xpBadge=document.getElementById('hero-xp-badge');
if(xpBadge){ xpBadge.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:13px;height:13px"><polygon points="12 2 22 20 2 20"/></svg> Ур. '+STATE.level+' \xb7 '+(STATE.xp||0)+' XP'; }
if(STATE.current && document.getElementById('sidebar-content')){ try{ buildSidebar(STATE.current); }catch(e){} }
}
function achievement(id,text){
if(STATE.achievements.has(id)) return;
STATE.achievements.set(id, text||ACH_LABELS[id]||id); saveProgress();
const pop=document.getElementById('ach-popup');
if(pop){ document.getElementById('ach-text').textContent=text||ACH_LABELS[id]||id; pop.classList.add('show'); setTimeout(()=>pop.classList.remove('show'),3300); }
addXp(20,'ach-'+id);
}
const PARAS = [
{ id:'p13', num:'§ 13', name:'Правильные многоугольники', sub:'$\\beta = \\tfrac{180^\\circ(n-2)}{n}$' },
{ id:'p14', num:'§ 14', name:'Формулы радиусов', sub:'$\\tfrac{a}{2} = R\\sin\\tfrac{180^\\circ}{n}$' },
{ id:'p15', num:'§ 15', name:'Треугольник, квадрат, шестиугольник', sub:'$a = R\\sqrt{3}, R\\sqrt{2}, R$' },
{ id:'p16', num:'§ 16', name:'Длина окружности и площадь круга', sub:'$C = 2\\pi R$, $S = \\pi R^2$' },
{ id:'final4', num:'★', name:'Финал главы', sub:'Итоги главы 4 · Геометрия 9 пройдена!', 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 = { p13:()=>buildP13(), p14:()=>buildP14(), p15:()=>buildP15(), p16:()=>buildP16(), final4:()=>buildFinal4() };
function ensureBuilt(id){ if(BUILT.has(id)) return; const fn=BUILDERS[id]; if(fn){ fn(); BUILT.add(id); } }
function goTo(id){
STATE.current=id; ensureBuilt(id);
document.querySelectorAll('.sec').forEach(s=>s.classList.remove('active'));
const el=document.getElementById('sec-'+id); if(el) el.classList.add('active');
document.querySelectorAll('.psel-card').forEach(c=>c.classList.toggle('active', c.dataset.id===id));
buildSidebar(id);
window.scrollTo({top:0,behavior:'smooth'});
if((STATE.progress[id]||0)<10) bumpProgress(id, 10);
if(window.renderMathInElement) setTimeout(()=>renderMath(el), 0);
markLastPara(id);
}
const SIDEBARS = {
p13:{title:'Шпаргалка \xA713',rows:[['Внутренний угол','$\\beta = \\tfrac{180^\\circ(n-2)}{n}$'],['Центральный угол','$\\tfrac{360^\\circ}{n}$']]},
p14:{title:'Шпаргалка \xA714',rows:[['Сторона','$a = 2R\\sin\\tfrac{180^\\circ}{n}$'],['Радиус вписанной','$r = R\\cos\\tfrac{180^\\circ}{n}$']]},
p15:{title:'Шпаргалка \xA715',rows:[['Треугольник','$a = R\\sqrt{3}$'],['Квадрат','$a = R\\sqrt{2}$'],['Шестиугольник','$a = R$']]},
p16:{title:'Шпаргалка \xA716',rows:[['Длина','$C = 2\\pi R$'],['Площадь','$S = \\pi R^2$'],['Сектор','$S = \\tfrac{\\pi R^2 \\alpha}{360^\\circ}$']]},
final4:{title:'Финал главы',rows:[['§§1316','теория главы 4'],['Геометрия 9','полностью пройдена!']]}
};
const TIPS=[
{sec:'p13',html:'В правильном $n$-угольнике все стороны и углы равны. Внутренний угол $\\beta = \\dfrac{180^\\circ(n-2)}{n}$.'},
{sec:'p14',html:'$\\dfrac{a}{2} = R\\sin\\dfrac{180^\\circ}{n}$ — половина стороны через радиус описанной окружности.'},
{sec:'p15',html:'Запомни: в правильном треугольнике $a = R\\sqrt{3}$, в квадрате $a = R\\sqrt{2}$, в шестиугольнике $a = R$.'},
{sec:'p16',html:'$C = 2\\pi R$ — длина окружности; $S = \\pi R^2$ — площадь круга.'},
{sec:'final4',html:'Главные результаты главы 4: формулы правильных многоугольников и круга. Вся Геометрия 9 в твоём арсенале!'}
];
function buildSidebar(id){
const box=document.getElementById('sidebar-content');
const sb=SIDEBARS[id]||SIDEBARS['p13'];
let html='';
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><span class="xp-level">Ур. '+STATE.level+'</span></div><div class="xp-bar"><div class="xp-fill" style="width:'+xpPct+'%"></div></div><div class="xp-nums"><span>'+STATE.xp+' XP</span><span>'+xpNext+' XP</span></div></div>';
html+='<div class="sidecard"><h4>'+sb.title+'</h4>';
sb.rows.forEach(([k,v])=>{ html+='<div class="sidecard-row"><b>'+k+'</b>'+(v?' — '+v:'')+'</div>'; });
html+='</div>';
const tip=TIPS.find(t=>t.sec===id)||TIPS[0];
if(tip){
html+='<div class="sidecard" style="background:linear-gradient(135deg,var(--warn-bg,#fef3c7),var(--pri-soft));border-color:var(--warn,#f59e0b)"><h4 style="color:#92400e;display:flex;align-items:center;gap:6px"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px"><polygon points="12,2 22,20 2,20"/></svg>Подсказка</h4><div class="sidecard-row" style="margin-bottom:0;font-size:.84rem;line-height:1.55">'+tip.html+'</div></div>';
}
if(STATE.achievements.size>0){
html+='<div class="sidecard"><h4>Достижения <span style="color:var(--warn);float:right">'+STATE.achievements.size+'</span></h4>';
[...STATE.achievements.values()].slice(-4).forEach(text=>{ html+='<div class="sidecard-row" style="font-size:.78rem;color:var(--ok)">&#10003; '+text+'</div>'; });
html+='</div>';
}
box.innerHTML=html;
if(window.renderMathInElement) try{ renderMath(box); }catch(e){}
}
function initTheme(){
const t=localStorage.getItem('geometry9_ch4_theme')||'light';
if(t==='dark') document.documentElement.classList.add('dark');
document.getElementById('theme-lab').textContent=t==='dark'?'Светлая':'Тёмная';
document.getElementById('theme-btn').addEventListener('click', ()=>{
document.documentElement.classList.toggle('dark');
const dark=document.documentElement.classList.contains('dark');
localStorage.setItem('geometry9_ch4_theme', dark?'dark':'light');
document.getElementById('theme-lab').textContent=dark?'Светлая':'Тёмная';
});
}
function renderMath(root){ if(window.renderMathInElement){ try{ renderMathInElement(root, {delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false}); }catch(e){} } }
function feedback(elm, ok, text){ if(!elm) return; elm.className='feedback '+(ok?'ok':'fail'); elm.innerHTML=text||(ok?'&#10003; Верно!':'&#10007; Неверно'); elm.style.display='block'; try{renderMath(elm);}catch(e){} }
function fmt(n){ if(!isFinite(n)) return '?'; if(Number.isInteger(n)) return String(n); return Math.abs(n-Math.round(n))<1e-9?String(Math.round(n)):(+n.toFixed(6)).toString(); }
function deg2rad(d){ return d * Math.PI / 180; }
function unitVec(p1, p2){ const dx=p2.x-p1.x, dy=p2.y-p1.y; const len=Math.sqrt(dx*dx+dy*dy)||1; return {x:dx/len, y:dy/len}; }
function regularPoly(cx, cy, R, n, theta0){
theta0 = (theta0 === undefined) ? -Math.PI/2 : theta0;
const pts = [];
for (let i = 0; i < n; i++){
const a = theta0 + 2*Math.PI*i/n;
pts.push({x: cx + R*Math.cos(a), y: cy + R*Math.sin(a)});
}
return pts;
}
/* ===== DnD сортер ===== */
function setupSorter(cfg){
const placed = {}; const pool = document.getElementById(cfg.poolId); const scope = document.querySelector(cfg.scopeSelector);
if(!pool||!scope) return {placed,render:()=>{},reset:()=>{}};
pool.classList.add('dnd-pool'); if(cfg.columnLayout) pool.classList.add('col');
let armed = null;
function buildChip(it,isPlaced){ const e=document.createElement('div'); e.className='dnd-chip'+(isPlaced?' placed':''); e.dataset.id=it.id; e.innerHTML='<span class="dnd-txt">'+it.html+'</span><span class="dnd-x" title="Убрать">\xd7</span>'; attach(e,it.id); return e; }
function attach(elm,itId){ let ghost=null,dragging=false,sx=0,sy=0; elm.addEventListener('pointerdown',ev=>{ if(ev.button!==undefined&&ev.button!==0) return;
ev.preventDefault(); if(ev.target.classList&&ev.target.classList.contains('dnd-x')){ ev.stopPropagation(); if(placed[itId]){delete placed[itId];render();}else if(armed===itId){armed=null;render();} return; } sx=ev.clientX;sy=ev.clientY; const r=elm.getBoundingClientRect(); const ox=ev.clientX-r.left,oy=ev.clientY-r.top; try{elm.setPointerCapture(ev.pointerId);}catch(e){} function onMove(e){ const dx=e.clientX-sx,dy=e.clientY-sy; if(!dragging&&Math.hypot(dx,dy)>8){ dragging=true; ghost=elm.cloneNode(true); ghost.classList.remove('armed'); ghost.style.cssText='position:fixed;z-index:9999;pointer-events:none;opacity:.9;transform:rotate(-2.5deg);box-shadow:0 14px 36px rgba(0,0,0,.32);width:'+r.width+'px;left:'+(e.clientX-ox)+'px;top:'+(e.clientY-oy)+'px'; document.body.appendChild(ghost); elm.classList.add('dragging'); } if(dragging&&ghost){ ghost.style.left=(e.clientX-ox)+'px';ghost.style.top=(e.clientY-oy)+'px'; const under=document.elementsFromPoint(e.clientX,e.clientY); scope.querySelectorAll('.drop-box.over,.dnd-pool.over').forEach(n=>n.classList.remove('over')); const tgt=under.find(n=>n.classList&&(n.classList.contains('drop-box')||n.classList.contains('dnd-pool'))); if(tgt)tgt.classList.add('over'); } } function onUp(e){ elm.removeEventListener('pointermove',onMove);elm.removeEventListener('pointerup',onUp);elm.removeEventListener('pointercancel',onUp);elm.classList.remove('dragging'); if(ghost){ghost.remove();ghost=null;} scope.querySelectorAll('.drop-box.over,.dnd-pool.over').forEach(n=>n.classList.remove('over')); if(dragging){ const under=document.elementsFromPoint(e.clientX,e.clientY); const box=under.find(n=>n.classList&&n.classList.contains('drop-box')); const pl=under.find(n=>n.classList&&n.classList.contains('dnd-pool')); if(box){const di=box.querySelector('[data-cat]');if(di){placed[itId]=di.dataset.cat;armed=null;render();return;}}else if(pl){delete placed[itId];armed=null;render();return;} }else{ if(placed[itId]){delete placed[itId];armed=null;render();}else{armed=(armed===itId)?null:itId;render();} } dragging=false; } elm.addEventListener('pointermove',onMove);elm.addEventListener('pointerup',onUp);elm.addEventListener('pointercancel',onUp); }); }
function attachBoxTaps(){ scope.querySelectorAll('.drop-box').forEach(box=>{ box.addEventListener('click',ev=>{ if(!armed)return; if(ev.target.closest('.dnd-chip'))return; const di=box.querySelector('[data-cat]'); if(di){placed[armed]=di.dataset.cat;armed=null;render();} }); }); }
function render(){ pool.innerHTML=''; cfg.items.forEach(it=>{if(placed[it.id])return;const c=buildChip(it,false);if(armed===it.id)c.classList.add('armed');pool.appendChild(c);}); cfg.cats.forEach(cat=>{const box=scope.querySelector('.drop-items[data-cat="'+cat+'"]');if(!box)return;box.innerHTML='';cfg.items.forEach(it=>{if(placed[it.id]!==cat)return;box.appendChild(buildChip(it,true));});}); if(window.renderMathInElement)try{renderMath(scope);}catch(_){} }
attachBoxTaps(); render();
return {placed,render,reset(){ for(const k in placed)delete placed[k];armed=null;render(); }};
}
const ICONS = {
repeat:'<svg class="ic" viewBox="0 0 24 24"><polyline points="9 11 12 14 22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>',
theory:'<svg class="ic" viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>',
algo:'<svg class="ic" viewBox="0 0 24 24"><polyline points="17 11 21 7 17 3"/><line x1="21" y1="7" x2="9" y2="7"/><polyline points="7 13 3 17 7 21"/><line x1="3" y1="17" x2="15" y2="17"/></svg>',
rule:'<svg class="ic" viewBox="0 0 24 24"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></svg>',
example:'<svg class="ic" viewBox="0 0 24 24"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M12 2a7 7 0 0 0-4 13c1 1 2 2 2 4h4c0-2 1-3 2-4a7 7 0 0 0-4-13z"/></svg>',
oral:'<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>'
};
function makeCard(kind, title, num, body){
const labels = {repeat:'Повторение',theory:'Теория',algo:'Алгоритм',rule:'Правило',example:'Пример',oral:'Устно'};
return '<div class="card"><div class="card-header"><div class="card-icon '+kind+'">'+ICONS[kind]+'</div><div class="card-title">'+(labels[kind]||'')+(title&&title!==labels[kind]?' \xb7 '+title:'')+'</div>'+(num?'<div class="card-num">'+num+'</div>':'')+'</div><div class="card-body">'+body+'</div></div>';
}
function secNav(prev, next){
const NAMES={p13:'\xA713',p14:'\xA714',p15:'\xA715',p16:'\xA716',final4:'Финал'};
let h='<div class="sec-nav">';
h+=prev?'<button class="btn" onclick="goTo(\''+prev+'\')"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> '+NAMES[prev]+'</button>':'<span></span>';
h+=next?'<button class="btn primary" onclick="goTo(\''+next+'\')">'+NAMES[next]+' <svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></button>':'<span></span>';
h+='</div>'; return h;
}
function readButton(paraId){
return '<div style="margin-top:18px;display:flex;justify-content:center">'
+'<button class="btn primary" id="'+paraId+'-read-btn">'
+'<svg class="ic" viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>'
+' Я прочитал — '+(paraId.startsWith('final')?'финал':'\xA7'+paraId.replace('p',''))+' (+10 XP)'
+'</button></div>';
}
function wireReadBtn(paraId){
const btn = document.getElementById(paraId+'-read-btn'); if(!btn) return;
btn.addEventListener('click', ()=>{
addXp(10, paraId+'-read'); bumpProgress(paraId, 100);
btn.textContent='Прочитано! +10 XP'; btn.disabled=true; btn.style.opacity=.6;
if(paraId==='final4') achievement('ch4_done');
});
}
/* ===== STUB BUILDERS — наполнение в Phase 7+ ===== */
function _stubBuilder(paraId, num, name, prev, next){
const body = document.getElementById(paraId+'-body');
let html = '';
html += makeCard('theory', 'В разработке', num, `
<p>Содержание параграфа <b>«${name}»</b> будет добавлено в следующих обновлениях.</p>
<p style="color:var(--muted);font-size:.9rem">Раздел Phase 7.</p>`);
html += readButton(paraId);
html += secNav(prev, next);
body.innerHTML = html;
wireReadBtn(paraId);
if(window.renderMathInElement) renderMath(body);
}
/* ===== §15 Треугольник, квадрат, шестиугольник ===== */
function buildP15(){
const box = document.getElementById('p15-body');
let html = '';
html += makeCard('theory', 'Эталонные значения для $n=3,4,6$', '15.1', `
<p>Из общей формулы $\\dfrac{a}{2} = R\\sin\\dfrac{180^\\circ}{n}$ (§14) для трёх «именных» правильных многоугольников получаются красивые ответы.</p>
<p><b>Равносторонний треугольник</b> ($n = 3$, $180^\\circ/3 = 60^\\circ$):</p>
<p style="text-align:center">$a = 2R\\sin 60^\\circ = R\\sqrt{3}$,&nbsp; $r = R\\cos 60^\\circ = \\dfrac{R}{2}$,&nbsp; $S = \\dfrac{3\\sqrt{3}}{4} R^2$.</p>
<p><b>Квадрат</b> ($n = 4$, $180^\\circ/4 = 45^\\circ$):</p>
<p style="text-align:center">$a = 2R\\sin 45^\\circ = R\\sqrt{2}$,&nbsp; $r = R\\cos 45^\\circ = R\\dfrac{\\sqrt{2}}{2}$,&nbsp; $S = 2R^2$.</p>
<p><b>Правильный шестиугольник</b> ($n = 6$, $180^\\circ/6 = 30^\\circ$):</p>
<p style="text-align:center">$a = 2R\\sin 30^\\circ = R$,&nbsp; $r = R\\cos 30^\\circ = R\\dfrac{\\sqrt{3}}{2}$,&nbsp; $S = \\dfrac{3\\sqrt{3}}{2} R^2$.</p>
<details class="spoiler"><summary>Откуда $S$?</summary><div class="spoiler-body">
Берём $S = \\tfrac{1}{2} n R^2 \\sin\\dfrac{360^\\circ}{n}$. Для $n=3$: $S = \\tfrac{3}{2} R^2 \\sin 120^\\circ = \\tfrac{3}{2}\\cdot\\tfrac{\\sqrt{3}}{2} R^2 = \\tfrac{3\\sqrt{3}}{4} R^2$. Для $n=4$: $S = 2 R^2 \\sin 90^\\circ = 2R^2$. Для $n=6$: $S = 3 R^2 \\sin 60^\\circ = \\tfrac{3\\sqrt{3}}{2} R^2$.
</div></details>`);
html += makeCard('rule', 'Как это запомнить', '15.2', `
<p>Сторона выражается через $R$ корнем (или без него) — главное правило главы:</p>
<ul style="padding-left:22px;line-height:1.95">
<li><b>3-угольник:</b>&nbsp; $a = R\\sqrt{3}$ — «три-корень-три».</li>
<li><b>4-угольник:</b>&nbsp; $a = R\\sqrt{2}$ — «квадрат-корень-два». Кстати, диагональ квадрата $= a\\sqrt{2} = 2R$ (это диаметр описанной окружности).</li>
<li><b>6-угольник:</b>&nbsp; $a = R$ — «шестиугольник = радиус». Правильный 6-угольник состоит из <b>6 равносторонних треугольников</b> со стороной $R$ (с вершиной в центре).</li>
</ul>
<p style="margin-top:10px"><b>Совет:</b> запомни только эти три случая — на ЦТ/экзамене они встречаются чаще всего, и формулы для них короче «общих».</p>`);
html += makeCard('example', 'Обратные формулы (от $a$ к $R$)', '15.3', `
<p>Если дана сторона $a$, радиус описанной окружности находится так:</p>
<ul style="padding-left:22px;line-height:1.95">
<li><b>3-угольник:</b>&nbsp; $R = \\dfrac{a}{\\sqrt{3}} = \\dfrac{a\\sqrt{3}}{3}$. Пример: $a = 6 \\Rightarrow R = \\dfrac{6}{\\sqrt{3}} = 2\\sqrt{3} \\approx 3{,}46$.</li>
<li><b>4-угольник:</b>&nbsp; $R = \\dfrac{a}{\\sqrt{2}} = \\dfrac{a\\sqrt{2}}{2}$. Пример: $a = 4 \\Rightarrow R = 2\\sqrt{2} \\approx 2{,}83$.</li>
<li><b>6-угольник:</b>&nbsp; $R = a$. Пример: $a = 5 \\Rightarrow R = 5$.</li>
</ul>
<details class="spoiler"><summary>А апофема $r$?</summary><div class="spoiler-body">
Для треугольника $r = R/2 = \\dfrac{a}{2\\sqrt{3}} = \\dfrac{a\\sqrt{3}}{6}$. Для квадрата $r = R\\dfrac{\\sqrt{2}}{2} = \\dfrac{a}{2}$ — апофема квадрата равна половине стороны. Для шестиугольника $r = R\\dfrac{\\sqrt{3}}{2} = \\dfrac{a\\sqrt{3}}{2}$.
</div></details>`);
/* IV1 — Три эталонных многоугольника */
html += `<div class="wg" id="p15-iv1">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Три эталонных многоугольника</div></div>
<div class="wg-help">Меняй радиус $R$ — увидишь сразу 3 правильных многоугольника с одинаковой описанной окружностью.</div>
<div class="sliders">
<label>Радиус $R$<b id="p15-iv1-rval">3.5</b><input type="range" id="p15-iv1-r" min="2" max="5" step="0.1" value="3.5"></label>
</div>
<div style="background:var(--card);border-radius:10px;padding:10px;overflow-x:auto">
<svg id="p15-iv1-svg" viewBox="0 0 700 260" style="width:100%;max-width:700px;min-width:340px;height:auto;display:block;margin:0 auto"></svg>
</div>
<div id="p15-iv1-out" style="margin-top:10px;padding:10px 14px;background:var(--pri-soft);border-radius:9px;font-size:.92rem;overflow-x:auto"></div>
</div>`;
/* IV2 — Калькулятор для эталонов */
html += `<div class="wg" id="p15-iv2">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Калькулятор для эталонов</div></div>
<div class="wg-help">Выбери многоугольник, введи радиус $R$ — получишь $a$, $r$, $S$ по «короткой» формуле.</div>
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px;justify-content:center" id="p15-iv2-modes">
<button class="btn primary" data-m="3">Треугольник</button>
<button class="btn" data-m="4">Квадрат</button>
<button class="btn" data-m="6">6-угольник</button>
</div>
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;justify-content:center;margin-bottom:10px">
<span style="font-family:'JetBrains Mono',monospace">$R$ =</span>
<input type="number" id="p15-iv2-r" class="tinp" style="width:100px;text-align:center" value="4" step="0.01" min="0.01">
<button class="btn primary" id="p15-iv2-go">Вычислить</button>
</div>
<div id="p15-iv2-out" style="padding:12px 14px;background:var(--card);border-radius:9px;text-align:center;font-size:1rem;min-height:50px"></div>
<div class="feedback" id="p15-iv2-fb"></div>
</div>`;
/* IV3 — Какая сторона? */
html += `<div class="wg" id="p15-iv3">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 3</span><div class="wg-title">Какая сторона?</div></div>
<div class="wg-help">Выбери правильную формулу для стороны $a$ через радиус $R$.</div>
<div class="score-display"><span>Задача <b id="p15-iv3-i">1</b> / 6</span><span>Очки: <b id="p15-iv3-s">0</b> / 6</span></div>
<div id="p15-iv3-q" style="padding:14px;background:var(--pri-soft);border-radius:10px;font-size:1.1rem;text-align:center;margin-bottom:10px"></div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:8px">
<button class="btn primary" data-ans="3" id="p15-iv3-a3">$a = R\\sqrt{3}$</button>
<button class="btn primary" data-ans="2" id="p15-iv3-a2">$a = R\\sqrt{2}$</button>
<button class="btn primary" data-ans="1" id="p15-iv3-a1">$a = R$</button>
</div>
<div class="feedback" id="p15-iv3-fb"></div>
</div>`;
/* IV4 — Тренажёр */
html += `<div class="wg" id="p15-iv4">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 4</span><div class="wg-title">Тренажёр эталонных формул</div></div>
<div class="wg-help">Реши задачу и введи число (округли до двух знаков, допуск 0.05).</div>
<div class="score-display"><span>Задача <b id="p15-iv4-i">1</b> / 6</span><span>Очки: <b id="p15-iv4-s">0</b> / 6</span></div>
<div id="p15-iv4-q" style="padding:14px;background:var(--pri-soft);border-radius:10px;font-size:1.05rem;margin-bottom:10px;text-align:center"></div>
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;justify-content:center">
<span style="font-family:'JetBrains Mono',monospace">ответ =</span>
<input type="number" id="p15-iv4-ans" class="tinp" style="width:110px;text-align:center" step="0.01">
<button class="btn primary" id="p15-iv4-go">Проверить</button>
<button class="btn" id="p15-iv4-start">Заново</button>
</div>
<div class="feedback" id="p15-iv4-fb"></div>
</div>`;
html += readButton('p15');
html += secNav('p14', 'p16');
box.innerHTML = html;
renderMath(box);
/* IV1 — три эталонных */
(function(){
const sl = document.getElementById('p15-iv1-r');
const lab = document.getElementById('p15-iv1-rval');
const svg = document.getElementById('p15-iv1-svg');
const out = document.getElementById('p15-iv1-out');
const seen = new Set();
const centers = [{x:120, y:130, n:3, label:'3-угольник', aStr:'R\\sqrt{3}'},
{x:350, y:130, n:4, label:'Квадрат', aStr:'R\\sqrt{2}'},
{x:580, y:130, n:6, label:'6-угольник', aStr:'R'}];
function draw(){
const R = +sl.value; // в условных единицах (2..5)
lab.textContent = R.toFixed(1);
const K = 20; // px / ед.
const Rpx = R * K;
let s = '';
centers.forEach(c=>{
const pts = regularPoly(c.x, c.y, Rpx, c.n);
// описанная окр
s += '<circle cx="'+c.x+'" cy="'+c.y+'" r="'+Rpx.toFixed(2)+'" fill="none" stroke="#94a3b8" stroke-width="1.3" stroke-dasharray="4 4"/>';
// многоугольник
s += '<polygon points="'+pts.map(p=>p.x.toFixed(2)+','+p.y.toFixed(2)).join(' ')+'" fill="rgba(8,145,178,.12)" stroke="#0e7490" stroke-width="2.2" stroke-linejoin="round"/>';
// одна сторона (выделим)
const P0 = pts[0], P1 = pts[1];
s += '<line x1="'+P0.x.toFixed(2)+'" y1="'+P0.y.toFixed(2)+'" x2="'+P1.x.toFixed(2)+'" y2="'+P1.y.toFixed(2)+'" stroke="#dc2626" stroke-width="3.2"/>';
// центр
s += '<circle cx="'+c.x+'" cy="'+c.y+'" r="3" fill="#0f172a"/>';
// подпись названия снизу
s += '<text x="'+c.x+'" y="240" text-anchor="middle" font-family="Inter,sans-serif" font-size="13" font-weight="700" fill="#0f172a">'+c.label+'</text>';
s += '<text x="'+c.x+'" y="255" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" fill="#0e7490">a = '+c.aStr+'</text>';
});
svg.innerHTML = s;
const a3 = R*Math.sqrt(3), a4 = R*Math.sqrt(2), a6 = R;
const r3 = R/2, r4 = R*Math.sqrt(2)/2, r6 = R*Math.sqrt(3)/2;
const S3 = 3*Math.sqrt(3)/4*R*R, S4 = 2*R*R, S6 = 3*Math.sqrt(3)/2*R*R;
out.innerHTML =
'<table style="margin:0 auto;border-collapse:collapse;font-size:.9rem">'
+'<tr style="border-bottom:1px solid var(--border)"><th style="padding:4px 10px;text-align:left">$n$</th><th style="padding:4px 10px">$a$</th><th style="padding:4px 10px">$r$</th><th style="padding:4px 10px">$S$</th></tr>'
+'<tr><td style="padding:4px 10px"><b>3</b></td><td style="padding:4px 10px">$R\\sqrt{3} \\approx '+a3.toFixed(2)+'$</td><td style="padding:4px 10px">$R/2 = '+r3.toFixed(2)+'$</td><td style="padding:4px 10px">$\\tfrac{3\\sqrt{3}}{4} R^2 \\approx '+S3.toFixed(2)+'$</td></tr>'
+'<tr><td style="padding:4px 10px"><b>4</b></td><td style="padding:4px 10px">$R\\sqrt{2} \\approx '+a4.toFixed(2)+'$</td><td style="padding:4px 10px">$R\\tfrac{\\sqrt{2}}{2} \\approx '+r4.toFixed(2)+'$</td><td style="padding:4px 10px">$2R^2 = '+S4.toFixed(2)+'$</td></tr>'
+'<tr><td style="padding:4px 10px"><b>6</b></td><td style="padding:4px 10px">$R = '+a6.toFixed(2)+'$</td><td style="padding:4px 10px">$R\\tfrac{\\sqrt{3}}{2} \\approx '+r6.toFixed(2)+'$</td><td style="padding:4px 10px">$\\tfrac{3\\sqrt{3}}{2} R^2 \\approx '+S6.toFixed(2)+'$</td></tr>'
+'</table>';
renderMath(out);
seen.add(R);
if(seen.size >= 4 && !seen.has('_done')){ addXp(10,'p15-iv1'); bumpProgress('p15', 15); seen.add('_done'); }
}
sl.addEventListener('input', draw);
draw();
})();
/* IV2 — Калькулятор для эталонов */
(function(){
const modes = document.querySelectorAll('#p15-iv2-modes button');
const inpR = document.getElementById('p15-iv2-r');
const go = document.getElementById('p15-iv2-go');
const out = document.getElementById('p15-iv2-out');
const fb = document.getElementById('p15-iv2-fb');
let mode = 3;
let solved = 0;
function calc(){
const R = parseFloat(inpR.value);
if(isNaN(R) || R <= 0){ feedback(fb, false, '&#10007; Введи положительное $R$.'); return; }
let a, r, S, formA, formR, formS;
if(mode === 3){
a = R*Math.sqrt(3); r = R/2; S = 3*Math.sqrt(3)/4*R*R;
formA = 'a = R\\sqrt{3} = '+R+'\\sqrt{3} \\approx '+a.toFixed(3);
formR = 'r = R/2 = '+r.toFixed(3);
formS = 'S = \\dfrac{3\\sqrt{3}}{4} R^2 \\approx '+S.toFixed(3);
} else if(mode === 4){
a = R*Math.sqrt(2); r = R*Math.sqrt(2)/2; S = 2*R*R;
formA = 'a = R\\sqrt{2} = '+R+'\\sqrt{2} \\approx '+a.toFixed(3);
formR = 'r = R\\dfrac{\\sqrt{2}}{2} \\approx '+r.toFixed(3);
formS = 'S = 2R^2 = '+S.toFixed(3);
} else {
a = R; r = R*Math.sqrt(3)/2; S = 3*Math.sqrt(3)/2*R*R;
formA = 'a = R = '+a.toFixed(3);
formR = 'r = R\\dfrac{\\sqrt{3}}{2} \\approx '+r.toFixed(3);
formS = 'S = \\dfrac{3\\sqrt{3}}{2} R^2 \\approx '+S.toFixed(3);
}
out.innerHTML = '<b>$'+formA+'$</b><br><b>$'+formR+'$</b><br><b>$'+formS+'$</b>';
renderMath(out);
feedback(fb, true, '&#10003; Готово.');
solved++;
if(solved === 1){ addXp(10,'p15-iv2'); bumpProgress('p15', 10); }
}
modes.forEach(b=>{
b.addEventListener('click', ()=>{
mode = +b.dataset.m;
modes.forEach(x=>{ x.classList.remove('primary'); x.classList.add('btn'); });
b.classList.remove('btn'); b.classList.add('btn','primary');
out.innerHTML = ''; fb.style.display = 'none';
});
});
go.addEventListener('click', calc);
inpR.addEventListener('keydown', e=>{ if(e.key==='Enter') calc(); });
calc();
})();
/* IV3 — Какая сторона? */
(function(){
const Q = [
{ q:'Равносторонний треугольник', ans:3, why:'$a = R\\sqrt{3}$ (n = 3)' },
{ q:'Квадрат', ans:2, why:'$a = R\\sqrt{2}$ (n = 4)' },
{ q:'Правильный 6-угольник', ans:1, why:'$a = R$ (n = 6)' },
{ q:'Правильный 3-угольник', ans:3, why:'Это равносторонний: $a = R\\sqrt{3}$' },
{ q:'4-угольник правильный', ans:2, why:'Это квадрат: $a = R\\sqrt{2}$' },
{ q:'Гексагон (6-угольник)', ans:1, why:'$a = R$ — шестиугольник = радиус' },
];
let i = 0, score = 0;
function show(){
if(i >= Q.length){
document.getElementById('p15-iv3-q').innerHTML = '<b>Готово!</b> Результат: ' + score + ' / ' + Q.length;
if(score === Q.length){ addXp(15,'p15-iv3'); bumpProgress('p15', 25); }
else if(score >= Q.length - 1){ addXp(8,'p15-iv3'); bumpProgress('p15', 15); }
return;
}
document.getElementById('p15-iv3-i').textContent = (i+1);
document.getElementById('p15-iv3-s').textContent = score;
document.getElementById('p15-iv3-q').innerHTML = 'Какая формула для стороны $a$ через $R$?<br><b style="font-size:1.15rem">'+Q[i].q+'</b>';
renderMath(document.getElementById('p15-iv3-q'));
document.getElementById('p15-iv3-fb').style.display = 'none';
}
function answer(a){
if(i >= Q.length) return;
const fb = document.getElementById('p15-iv3-fb');
if(+a === Q[i].ans){ score++; feedback(fb, true, '&#10003; Верно! '+Q[i].why+'. Дальше'); }
else feedback(fb, false, '&#10007; Нет. '+Q[i].why+'. Дальше');
document.getElementById('p15-iv3-s').textContent = score;
i++;
setTimeout(show, 1100);
}
['1','2','3'].forEach(k=>{
const b = document.getElementById('p15-iv3-a'+k); if(b) b.addEventListener('click', ()=>answer(k));
});
show();
})();
/* IV4 — Тренажёр */
(function(){
const Q = [
{ q:'Равносторонний треугольник с $R = 4$. Найди сторону $a$.',
ans:6.93, hint:'$a = R\\sqrt{3} = 4\\sqrt{3} \\approx 6{,}93$' },
{ q:'Квадрат с $R = 5$. Найди сторону $a$.',
ans:7.07, hint:'$a = R\\sqrt{2} = 5\\sqrt{2} \\approx 7{,}07$' },
{ q:'Правильный 6-угольник с $R = 7$. Найди сторону $a$.',
ans:7, hint:'$a = R = 7$ (шестиугольник = радиус)' },
{ q:'Квадрат со стороной $a = 6$. Найди радиус описанной окружности $R$.',
ans:4.24, hint:'$R = a/\\sqrt{2} = 6/\\sqrt{2} = 3\\sqrt{2} \\approx 4{,}24$' },
{ q:'Равносторонний треугольник со стороной $a = 9$. Найди $R$.',
ans:5.20, hint:'$R = a/\\sqrt{3} = 9/\\sqrt{3} = 3\\sqrt{3} \\approx 5{,}20$' },
{ q:'Правильный 6-угольник со стороной $a = 8$. Найди $R$.',
ans:8, hint:'$R = a = 8$' },
];
let i = 0, score = 0;
function show(){
if(i >= Q.length){
document.getElementById('p15-iv4-q').innerHTML = '<b>Готово!</b> Результат: ' + score + ' / ' + Q.length;
if(score === Q.length){ addXp(15,'p15-iv4'); bumpProgress('p15', 25); }
else if(score >= 4){ addXp(8,'p15-iv4'); bumpProgress('p15', 15); }
return;
}
document.getElementById('p15-iv4-i').textContent = (i+1);
document.getElementById('p15-iv4-s').textContent = score;
document.getElementById('p15-iv4-q').innerHTML = Q[i].q;
document.getElementById('p15-iv4-ans').value = '';
renderMath(document.getElementById('p15-iv4-q'));
document.getElementById('p15-iv4-fb').style.display = 'none';
}
function go(){
if(i >= Q.length) return;
const fb = document.getElementById('p15-iv4-fb');
const ans = parseFloat(document.getElementById('p15-iv4-ans').value);
if(isNaN(ans)){ feedback(fb, false, '&#10007; Введи число.'); return; }
if(Math.abs(ans - Q[i].ans) < 0.05){ score++; feedback(fb, true, '&#10003; Верно! '+Q[i].hint+'. Дальше'); }
else feedback(fb, false, '&#10007; Неверно. Ответ: '+Q[i].ans+' ('+Q[i].hint+'). Дальше');
document.getElementById('p15-iv4-s').textContent = score;
i++;
setTimeout(show, 1300);
}
document.getElementById('p15-iv4-go').addEventListener('click', go);
document.getElementById('p15-iv4-ans').addEventListener('keydown', e=>{ if(e.key==='Enter') go(); });
document.getElementById('p15-iv4-start').addEventListener('click', ()=>{ i=0; score=0; show(); });
show();
})();
wireReadBtn('p15');
}
/* ===== §16 Длина окружности и площадь круга ===== */
function buildP16(){
const box = document.getElementById('p16-body');
let html = '';
html += makeCard('theory', 'Длина окружности', '16.1', `
<p>Длина окружности радиуса $R$ (диаметр $d = 2R$):</p>
<p style="text-align:center;font-size:1.1rem">$$C = 2\\pi R = \\pi d$$</p>
<p>Число $\\pi$ (отношение длины окружности к её диаметру) — иррациональное:</p>
<p style="text-align:center">$\\pi \\approx 3{,}14159265\\ldots$&nbsp; (для оценок берут $\\pi \\approx 3{,}14$ или $\\pi \\approx \\dfrac{22}{7}$).</p>
<p><b>Примеры:</b></p>
<ul style="padding-left:22px;line-height:1.95">
<li>$R = 5$: $C = 10\\pi \\approx 31{,}4$.</li>
<li>$d = 10$: $C = 10\\pi \\approx 31{,}4$ (тот же ответ, т.к. $d = 2R$).</li>
<li>$R = 1$: $C = 2\\pi \\approx 6{,}28$ — длина единичной окружности.</li>
</ul>`);
html += makeCard('rule', 'Площадь круга', '16.2', `
<p><b>Круг</b> — это вся часть плоскости, ограниченная окружностью (плюс сама окружность). Его площадь:</p>
<p style="text-align:center;font-size:1.1rem">$$S = \\pi R^2$$</p>
<p>Через диаметр: $S = \\pi \\left(\\dfrac{d}{2}\\right)^2 = \\dfrac{\\pi d^2}{4}$.</p>
<p><b>Примеры:</b></p>
<ul style="padding-left:22px;line-height:1.95">
<li>$R = 4$: $S = 16\\pi \\approx 50{,}24$.</li>
<li>$d = 10$: $S = \\dfrac{\\pi \\cdot 100}{4} = 25\\pi \\approx 78{,}5$.</li>
<li>$R = 1$: $S = \\pi \\approx 3{,}14$ — площадь единичного круга.</li>
</ul>
<details class="spoiler"><summary>Откуда $S = \\pi R^2$?</summary><div class="spoiler-body">
«Разрежем» круг на много узких секторов и сложим их в прямоугольник: ширина $\\approx R$, длина $\\approx \\tfrac{1}{2} \\cdot 2\\pi R = \\pi R$. Площадь $\\approx R \\cdot \\pi R = \\pi R^2$. В пределе (бесконечно тонкие секторы) — точное равенство.
</div></details>`);
html += makeCard('example', 'Дуга и сектор', '16.3', `
<p>Дуге, на которую опирается центральный угол $\\alpha^\\circ$, соответствует доля $\\dfrac{\\alpha}{360^\\circ}$ всей окружности. Поэтому:</p>
<p style="text-align:center">$$\\ell = 2\\pi R \\cdot \\dfrac{\\alpha}{360^\\circ} = \\dfrac{\\pi R \\alpha}{180^\\circ}$$</p>
<p>Сектор с углом $\\alpha^\\circ$ занимает ту же долю круга:</p>
<p style="text-align:center">$$S_{\\text{сек}} = \\pi R^2 \\cdot \\dfrac{\\alpha}{360^\\circ} = \\dfrac{\\pi R^2 \\alpha}{360^\\circ}$$</p>
<p><b>Примеры:</b></p>
<ul style="padding-left:22px;line-height:1.95">
<li>Длина дуги четверти круга радиуса $R = 2$: $\\ell = \\dfrac{\\pi \\cdot 2 \\cdot 90}{180} = \\pi \\approx 3{,}14$.</li>
<li>Площадь сектора $60^\\circ$ в круге радиуса $6$: $S = \\dfrac{\\pi \\cdot 36 \\cdot 60}{360} = 6\\pi \\approx 18{,}85$.</li>
</ul>`);
/* IV1 — Slider R и сектор */
html += `<div class="wg" id="p16-iv1">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Окружность, круг и сектор</div></div>
<div class="wg-help">Меняй радиус $R$ и угол сектора $\\alpha$ — следи за длиной окружности, площадью круга, длиной дуги и площадью сектора.</div>
<div class="sliders">
<label>Радиус $R$<b id="p16-iv1-rval">4</b><input type="range" id="p16-iv1-r" min="2" max="6" step="0.1" value="4"></label>
<label>Угол $\\alpha$ (°)<b id="p16-iv1-aval">90</b><input type="range" id="p16-iv1-a" min="0" max="360" step="1" value="90"></label>
</div>
<div style="background:var(--card);border-radius:10px;padding:10px;overflow-x:auto">
<svg id="p16-iv1-svg" viewBox="0 0 400 400" style="width:100%;max-width:420px;min-width:280px;height:auto;display:block;margin:0 auto"></svg>
</div>
<div id="p16-iv1-out" style="margin-top:10px;padding:10px 14px;background:var(--pri-soft);border-radius:9px;font-size:.92rem;text-align:center;line-height:1.9"></div>
</div>`;
/* IV2 — Калькулятор C и S */
html += `<div class="wg" id="p16-iv2">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Калькулятор: $C$, $S$, дуга, сектор</div></div>
<div class="wg-help">Выбери, что дано ($R$ или $d$). Введи значение — посчитаем $C$ и $S$ круга, а также длину дуги и площадь сектора для угла $\\alpha$.</div>
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px;justify-content:center" id="p16-iv2-modes">
<button class="btn primary" data-m="R">Дано $R$</button>
<button class="btn" data-m="d">Дано $d$</button>
</div>
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;justify-content:center;margin-bottom:10px">
<span style="font-family:'JetBrains Mono',monospace" id="p16-iv2-lab">$R$ =</span>
<input type="number" id="p16-iv2-val" class="tinp" style="width:90px;text-align:center" value="5" step="0.01" min="0.01">
<span style="font-family:'JetBrains Mono',monospace">$\\alpha$ =</span>
<input type="number" id="p16-iv2-a" class="tinp" style="width:80px;text-align:center" value="90" step="1" min="0" max="360">°
<button class="btn primary" id="p16-iv2-go">Вычислить</button>
</div>
<div id="p16-iv2-out" style="padding:12px 14px;background:var(--card);border-radius:9px;text-align:center;font-size:.98rem;min-height:50px;line-height:1.9"></div>
<div class="feedback" id="p16-iv2-fb"></div>
</div>`;
/* IV3 — Какая формула? */
html += `<div class="wg" id="p16-iv3">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 3</span><div class="wg-title">Какая формула?</div></div>
<div class="wg-help">Выбери формулу, которая подходит под описание.</div>
<div class="score-display"><span>Задача <b id="p16-iv3-i">1</b> / 6</span><span>Очки: <b id="p16-iv3-s">0</b> / 6</span></div>
<div id="p16-iv3-q" style="padding:14px;background:var(--pri-soft);border-radius:10px;font-size:1.1rem;text-align:center;margin-bottom:10px"></div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:8px">
<button class="btn primary" data-ans="C" id="p16-iv3-aC">$2\\pi R$</button>
<button class="btn primary" data-ans="S" id="p16-iv3-aS">$\\pi R^2$</button>
<button class="btn primary" data-ans="L" id="p16-iv3-aL">$\\dfrac{\\pi R \\alpha}{180}$</button>
<button class="btn primary" data-ans="Ss" id="p16-iv3-aSs">$\\dfrac{\\pi R^2 \\alpha}{360}$</button>
</div>
<div class="feedback" id="p16-iv3-fb"></div>
</div>`;
/* IV4 — Тренажёр */
html += `<div class="wg" id="p16-iv4">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 4</span><div class="wg-title">Тренажёр окружности и круга</div></div>
<div class="wg-help">Для всех задач используй $\\pi \\approx 3{,}14$. Ответы округляй до двух знаков (допуск 0.05).</div>
<div class="score-display"><span>Задача <b id="p16-iv4-i">1</b> / 6</span><span>Очки: <b id="p16-iv4-s">0</b> / 6</span></div>
<div id="p16-iv4-q" style="padding:14px;background:var(--pri-soft);border-radius:10px;font-size:1.05rem;margin-bottom:10px;text-align:center"></div>
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;justify-content:center">
<span style="font-family:'JetBrains Mono',monospace">ответ =</span>
<input type="number" id="p16-iv4-ans" class="tinp" style="width:110px;text-align:center" step="0.01">
<button class="btn primary" id="p16-iv4-go">Проверить</button>
<button class="btn" id="p16-iv4-start">Заново</button>
</div>
<div class="feedback" id="p16-iv4-fb"></div>
</div>`;
html += readButton('p16');
html += secNav('p15', 'final4');
box.innerHTML = html;
renderMath(box);
/* IV1 — Slider R + сектор */
(function(){
const slR = document.getElementById('p16-iv1-r');
const slA = document.getElementById('p16-iv1-a');
const labR = document.getElementById('p16-iv1-rval');
const labA = document.getElementById('p16-iv1-aval');
const svg = document.getElementById('p16-iv1-svg');
const out = document.getElementById('p16-iv1-out');
const seen = new Set();
const cx = 200, cy = 200;
function draw(){
const R = +slR.value, alpha = +slA.value; // R в условных единицах (2..6)
labR.textContent = R.toFixed(1); labA.textContent = alpha;
const K = 25; // px / ед.
const Rpx = R * K;
let s = '';
// основная окружность (контур)
s += '<circle cx="'+cx+'" cy="'+cy+'" r="'+Rpx.toFixed(2)+'" fill="rgba(34,211,238,.06)" stroke="#94a3b8" stroke-width="1.4"/>';
// сектор — рисуем path
if(alpha > 0){
if(alpha >= 360){
// целый круг
s += '<circle cx="'+cx+'" cy="'+cy+'" r="'+Rpx.toFixed(2)+'" fill="rgba(8,145,178,.30)" stroke="#0e7490" stroke-width="2"/>';
} else {
const rad = alpha * Math.PI / 180;
const x1 = cx + Rpx, y1 = cy;
const x2 = cx + Rpx*Math.cos(rad), y2 = cy + Rpx*Math.sin(rad);
const large = alpha > 180 ? 1 : 0;
s += '<path d="M '+cx+' '+cy+' L '+x1.toFixed(2)+' '+y1+' A '+Rpx.toFixed(2)+' '+Rpx.toFixed(2)+' 0 '+large+' 1 '+x2.toFixed(2)+' '+y2.toFixed(2)+' Z" fill="rgba(8,145,178,.30)" stroke="#0e7490" stroke-width="2"/>';
}
}
// радиус (на 0°)
s += '<line x1="'+cx+'" y1="'+cy+'" x2="'+(cx+Rpx).toFixed(2)+'" y2="'+cy+'" stroke="#0891b2" stroke-width="2.2"/>';
// радиус на углу alpha
const ax = cx + Rpx*Math.cos(alpha*Math.PI/180), ay = cy + Rpx*Math.sin(alpha*Math.PI/180);
s += '<line x1="'+cx+'" y1="'+cy+'" x2="'+ax.toFixed(2)+'" y2="'+ay.toFixed(2)+'" stroke="#0891b2" stroke-width="2.2"/>';
// центр O
s += '<circle cx="'+cx+'" cy="'+cy+'" r="3.5" fill="#0f172a"/>';
s += '<text x="'+(cx-10)+'" y="'+(cy-8)+'" text-anchor="end" font-family="Inter,sans-serif" font-size="14" font-weight="700" fill="#0f172a">O</text>';
// подпись R
s += '<text x="'+(cx + Rpx/2).toFixed(2)+'" y="'+(cy-6)+'" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="12" font-weight="700" fill="#0891b2">R='+R.toFixed(1)+'</text>';
svg.innerHTML = s;
const PI = Math.PI;
const C = 2*PI*R;
const Scircle = PI*R*R;
const L = PI*R*alpha/180;
const Ssec = PI*R*R*alpha/360;
out.innerHTML =
'$R = '+R.toFixed(1)+'$,&nbsp; $\\alpha = '+alpha+'^\\circ$<br>'
+'$C = 2\\pi R \\approx '+C.toFixed(2)+'$,&nbsp; $S_{\\text{круга}} = \\pi R^2 \\approx '+Scircle.toFixed(2)+'$<br>'
+'$\\ell_{\\text{дуги}} = \\dfrac{\\pi R \\alpha}{180} \\approx '+L.toFixed(2)+'$,&nbsp; $S_{\\text{сектора}} = \\dfrac{\\pi R^2 \\alpha}{360} \\approx '+Ssec.toFixed(2)+'$';
renderMath(out);
seen.add(R+'_'+alpha);
if(seen.size >= 5 && !seen.has('_done')){ addXp(10,'p16-iv1'); bumpProgress('p16', 15); seen.add('_done'); }
}
slR.addEventListener('input', draw);
slA.addEventListener('input', draw);
draw();
})();
/* IV2 — Калькулятор C, S, дуга, сектор */
(function(){
const modes = document.querySelectorAll('#p16-iv2-modes button');
const inpV = document.getElementById('p16-iv2-val');
const inpA = document.getElementById('p16-iv2-a');
const lab = document.getElementById('p16-iv2-lab');
const go = document.getElementById('p16-iv2-go');
const out = document.getElementById('p16-iv2-out');
const fb = document.getElementById('p16-iv2-fb');
let mode = 'R';
let solved = 0;
function updateLabel(){
lab.innerHTML = mode === 'R' ? '$R$ =' : '$d$ =';
renderMath(lab);
}
function calc(){
const v = parseFloat(inpV.value);
const alpha = parseFloat(inpA.value);
if(isNaN(v) || v <= 0){ feedback(fb, false, '&#10007; Введи положительное значение.'); return; }
if(isNaN(alpha) || alpha < 0 || alpha > 360){ feedback(fb, false, '&#10007; Угол $\\alpha$ — от 0° до 360°.'); return; }
const R = mode === 'R' ? v : v/2;
const d = 2*R;
const PI = Math.PI;
const C = 2*PI*R;
const S = PI*R*R;
const L = PI*R*alpha/180;
const Ss = PI*R*R*alpha/360;
out.innerHTML =
'<b>$R = '+R.toFixed(3)+'$, $d = '+d.toFixed(3)+'$</b><br>'
+'$C = 2\\pi R \\approx '+C.toFixed(3)+'$<br>'
+'$S_{\\text{круга}} = \\pi R^2 \\approx '+S.toFixed(3)+'$<br>'
+'$\\ell_{\\text{дуги}}('+alpha+'^\\circ) = \\dfrac{\\pi R \\alpha}{180} \\approx '+L.toFixed(3)+'$<br>'
+'$S_{\\text{сектора}}('+alpha+'^\\circ) = \\dfrac{\\pi R^2 \\alpha}{360} \\approx '+Ss.toFixed(3)+'$';
renderMath(out);
feedback(fb, true, '&#10003; Готово.');
solved++;
if(solved === 1){ addXp(10,'p16-iv2'); bumpProgress('p16', 10); }
}
modes.forEach(b=>{
b.addEventListener('click', ()=>{
mode = b.dataset.m;
modes.forEach(x=>{ x.classList.remove('primary'); x.classList.add('btn'); });
b.classList.remove('btn'); b.classList.add('btn','primary');
updateLabel();
out.innerHTML = ''; fb.style.display = 'none';
});
});
go.addEventListener('click', calc);
inpV.addEventListener('keydown', e=>{ if(e.key==='Enter') calc(); });
inpA.addEventListener('keydown', e=>{ if(e.key==='Enter') calc(); });
updateLabel(); calc();
})();
/* IV3 — Какая формула? */
(function(){
const Q = [
{ q:'Длина окружности', ans:'C', why:'$C = 2\\pi R$' },
{ q:'Площадь круга', ans:'S', why:'$S = \\pi R^2$' },
{ q:'Длина дуги, на которую опирается угол $\\alpha$', ans:'L', why:'$\\ell = \\dfrac{\\pi R \\alpha}{180}$' },
{ q:'Площадь сектора с углом $\\alpha$', ans:'Ss', why:'$S_{\\text{сек}} = \\dfrac{\\pi R^2 \\alpha}{360}$' },
{ q:'Половина окружности (дуга с углом 180°)', ans:'L', why:'Это $\\ell = \\dfrac{\\pi R \\alpha}{180}$ при $\\alpha = 180^\\circ$, выходит $\\pi R$' },
{ q:'Четверть круга (сектор с углом 90°)', ans:'Ss', why:'Это $S_{\\text{сек}} = \\dfrac{\\pi R^2 \\alpha}{360}$ при $\\alpha = 90^\\circ$, выходит $\\pi R^2 / 4$' },
];
let i = 0, score = 0;
function show(){
if(i >= Q.length){
document.getElementById('p16-iv3-q').innerHTML = '<b>Готово!</b> Результат: ' + score + ' / ' + Q.length;
if(score === Q.length){ addXp(15,'p16-iv3'); bumpProgress('p16', 25); }
else if(score >= Q.length - 1){ addXp(8,'p16-iv3'); bumpProgress('p16', 15); }
return;
}
document.getElementById('p16-iv3-i').textContent = (i+1);
document.getElementById('p16-iv3-s').textContent = score;
document.getElementById('p16-iv3-q').innerHTML = 'Какая формула?<br><b>'+Q[i].q+'</b>';
renderMath(document.getElementById('p16-iv3-q'));
document.getElementById('p16-iv3-fb').style.display = 'none';
}
function answer(a){
if(i >= Q.length) return;
const fb = document.getElementById('p16-iv3-fb');
if(a === Q[i].ans){ score++; feedback(fb, true, '&#10003; Верно! '+Q[i].why+'. Дальше'); }
else feedback(fb, false, '&#10007; Нет. '+Q[i].why+'. Дальше');
document.getElementById('p16-iv3-s').textContent = score;
i++;
setTimeout(show, 1200);
}
['C','S','L','Ss'].forEach(k=>{
const b = document.getElementById('p16-iv3-a'+k); if(b) b.addEventListener('click', ()=>answer(k));
});
show();
})();
/* IV4 — Тренажёр (π = 3.14) */
(function(){
const PI = 3.14;
const Q = [
{ q:'Длина окружности с $R = 5$. ($\\pi \\approx 3{,}14$)',
ans: 10*PI, hint:'$C = 2\\pi R = 10\\pi \\approx 31{,}4$' },
{ q:'Площадь круга с $R = 4$. ($\\pi \\approx 3{,}14$)',
ans: 16*PI, hint:'$S = \\pi R^2 = 16\\pi \\approx 50{,}24$' },
{ q:'Длина окружности с $d = 10$. ($\\pi \\approx 3{,}14$)',
ans: 10*PI, hint:'$C = \\pi d = 10\\pi \\approx 31{,}4$' },
{ q:'Длина дуги: $R = 6$, $\\alpha = 90^\\circ$. ($\\pi \\approx 3{,}14$)',
ans: 3*PI, hint:'$\\ell = \\dfrac{\\pi R \\alpha}{180} = \\dfrac{\\pi \\cdot 6 \\cdot 90}{180} = 3\\pi \\approx 9{,}42$' },
{ q:'Площадь сектора: $R = 4$, $\\alpha = 90^\\circ$. ($\\pi \\approx 3{,}14$)',
ans: 4*PI, hint:'$S = \\dfrac{\\pi R^2 \\alpha}{360} = \\dfrac{\\pi \\cdot 16 \\cdot 90}{360} = 4\\pi \\approx 12{,}56$' },
{ q:'Площадь круга с $d = 10$. ($\\pi \\approx 3{,}14$)',
ans: 25*PI, hint:'$S = \\dfrac{\\pi d^2}{4} = \\dfrac{100\\pi}{4} = 25\\pi \\approx 78{,}5$' },
];
let i = 0, score = 0;
function show(){
if(i >= Q.length){
document.getElementById('p16-iv4-q').innerHTML = '<b>Готово!</b> Результат: ' + score + ' / ' + Q.length;
if(score === Q.length){ addXp(15,'p16-iv4'); bumpProgress('p16', 25); }
else if(score >= 4){ addXp(8,'p16-iv4'); bumpProgress('p16', 15); }
return;
}
document.getElementById('p16-iv4-i').textContent = (i+1);
document.getElementById('p16-iv4-s').textContent = score;
document.getElementById('p16-iv4-q').innerHTML = Q[i].q;
document.getElementById('p16-iv4-ans').value = '';
renderMath(document.getElementById('p16-iv4-q'));
document.getElementById('p16-iv4-fb').style.display = 'none';
}
function go(){
if(i >= Q.length) return;
const fb = document.getElementById('p16-iv4-fb');
const ans = parseFloat(document.getElementById('p16-iv4-ans').value);
if(isNaN(ans)){ feedback(fb, false, '&#10007; Введи число.'); return; }
if(Math.abs(ans - Q[i].ans) < 0.05){ score++; feedback(fb, true, '&#10003; Верно! '+Q[i].hint+'. Дальше'); }
else feedback(fb, false, '&#10007; Неверно. Ответ: '+Q[i].ans.toFixed(2)+' ('+Q[i].hint+'). Дальше');
document.getElementById('p16-iv4-s').textContent = score;
i++;
setTimeout(show, 1400);
}
document.getElementById('p16-iv4-go').addEventListener('click', go);
document.getElementById('p16-iv4-ans').addEventListener('keydown', e=>{ if(e.key==='Enter') go(); });
document.getElementById('p16-iv4-start').addEventListener('click', ()=>{ i=0; score=0; show(); });
show();
})();
wireReadBtn('p16');
}
/* ===== §13 Правильные многоугольники ===== */
function buildP13(){
const box = document.getElementById('p13-body');
let html = '';
html += makeCard('theory', 'Определение', '13.1', `
<p><b>Правильный многоугольник</b> — это выпуклый многоугольник, у которого <b>все стороны равны</b> И <b>все углы равны</b>.</p>
<p>Примеры:</p>
<ul style="padding-left:22px;line-height:1.95">
<li>правильный <b>треугольник</b> (равносторонний) — 3 стороны, все углы по $60^\\circ$;</li>
<li>правильный <b>4-угольник</b> (квадрат) — 4 стороны, все углы по $90^\\circ$;</li>
<li>правильный <b>5-угольник</b> (пентагон) — 5 сторон, углы по $108^\\circ$;</li>
<li>правильный <b>6-угольник</b> (гексагон) — 6 сторон, углы по $120^\\circ$;</li>
<li>правильный <b>8-угольник</b> (октагон) — 8 сторон, углы по $135^\\circ$.</li>
</ul>
<details class="spoiler"><summary>Почему нужно требовать и стороны, и углы?</summary><div class="spoiler-body">
Если только стороны равны — получится, например, <b>ромб</b> (не квадрат). Если только углы — <b>прямоугольник</b>. Чтобы был «правильный» — нужны оба условия одновременно.
</div></details>`);
html += makeCard('rule', 'Внутренний угол', '13.2', `
<p>В правильном $n$-угольнике каждый <b>внутренний угол</b> равен:</p>
<p style="text-align:center;font-size:1.05rem">$$\\beta = \\dfrac{180^\\circ \\cdot (n - 2)}{n}$$</p>
<p>Это следует из суммы внутренних углов выпуклого $n$-угольника: $(n-2) \\cdot 180^\\circ$, поделённой на число углов $n$ (все они равны).</p>
<p><b>Примеры:</b></p>
<ul style="padding-left:22px;line-height:1.95">
<li>$n = 3$: $\\beta = \\dfrac{180^\\circ \\cdot 1}{3} = 60^\\circ$;</li>
<li>$n = 4$: $\\beta = \\dfrac{180^\\circ \\cdot 2}{4} = 90^\\circ$;</li>
<li>$n = 6$: $\\beta = \\dfrac{180^\\circ \\cdot 4}{6} = 120^\\circ$;</li>
<li>$n = 12$: $\\beta = \\dfrac{180^\\circ \\cdot 10}{12} = 150^\\circ$.</li>
</ul>`);
html += makeCard('example', 'Окружности правильного многоугольника', '13.3', `
<p>В любой правильный $n$-угольник можно <b>вписать окружность</b> (касается всех сторон, радиус $r$ — апофема) и <b>описать окружность</b> (проходит через все вершины, радиус $R$). <b>Оба центра совпадают</b> — это центр многоугольника $O$.</p>
<ul style="padding-left:22px;line-height:1.95">
<li><b>Центральный угол</b>, опирающийся на одну сторону: $\\dfrac{360^\\circ}{n}$ (или $\\dfrac{2\\pi}{n}$ радиан).</li>
<li>Центр $O$ — точка пересечения серединных перпендикуляров к сторонам, биссектрис углов и осей симметрии.</li>
<li><b>Апофема</b> $r$ — это перпендикуляр из $O$ к стороне (он попадает в её середину).</li>
</ul>
<details class="spoiler"><summary>Связь $R$ и $r$ через центральный угол</summary><div class="spoiler-body">
Рассмотрим равнобедренный треугольник с вершиной в $O$ и основанием — стороной многоугольника. Угол при $O$ равен $\\dfrac{360^\\circ}{n}$, апофема $r$ — высота к основанию. Тогда $r = R\\cos\\dfrac{180^\\circ}{n}$ (половинный угол при $O$).
</div></details>`);
/* IV1 — Slider n */
html += `<div class="wg" id="p13-iv1">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Правильный n-угольник</div></div>
<div class="wg-help">Меняй число сторон $n$ — увидишь, как меняется форма, внутренний угол $\\beta$ и апофема $r = R\\cos(180^\\circ/n)$.</div>
<div class="sliders">
<label>Число сторон $n$<b id="p13-iv1-nval">6</b><input type="range" id="p13-iv1-n" min="3" max="12" step="1" value="6"></label>
</div>
<div style="background:var(--card);border-radius:10px;padding:10px;overflow-x:auto">
<svg id="p13-iv1-svg" viewBox="0 0 360 360" style="width:100%;max-width:420px;min-width:280px;height:auto;display:block;margin:0 auto"></svg>
</div>
<div id="p13-iv1-out" style="margin-top:10px;padding:10px 14px;background:var(--pri-soft);border-radius:9px;font-size:.95rem;text-align:center;line-height:1.9"></div>
</div>`;
/* IV2 — Калькулятор β */
html += `<div class="wg" id="p13-iv2">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Калькулятор внутреннего угла</div></div>
<div class="wg-help">Введи $n$ — посчитаем внутренний угол $\\beta = 180^\\circ(n-2)/n$, сумму всех углов и центральный угол.</div>
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;justify-content:center;margin-bottom:10px">
<span style="font-family:'JetBrains Mono',monospace">$n$ =</span>
<input type="number" id="p13-iv2-n" class="tinp" style="width:90px;text-align:center" value="6" min="3" max="20">
<button class="btn primary" id="p13-iv2-go">Найти угол</button>
</div>
<div id="p13-iv2-out" style="padding:12px 14px;background:var(--card);border-radius:9px;text-align:center;font-size:1rem;min-height:50px"></div>
<div class="feedback" id="p13-iv2-fb"></div>
</div>`;
/* IV3 — Quickfire «Какой угол?» */
html += `<div class="wg" id="p13-iv3">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 3</span><div class="wg-title">Какой угол?</div></div>
<div class="wg-help">Выбери внутренний угол $\\beta$ правильного $n$-угольника с заданным $n$.</div>
<div class="score-display"><span>Задача <b id="p13-iv3-i">1</b> / 6</span><span>Очки: <b id="p13-iv3-s">0</b> / 6</span></div>
<div id="p13-iv3-q" style="padding:14px;background:var(--pri-soft);border-radius:10px;font-size:1.15rem;text-align:center;margin-bottom:10px"></div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(110px,1fr));gap:8px">
<button class="btn primary" data-ans="60" id="p13-iv3-a60">$60^\\circ$</button>
<button class="btn primary" data-ans="90" id="p13-iv3-a90">$90^\\circ$</button>
<button class="btn primary" data-ans="108" id="p13-iv3-a108">$108^\\circ$</button>
<button class="btn primary" data-ans="120" id="p13-iv3-a120">$120^\\circ$</button>
</div>
<div class="feedback" id="p13-iv3-fb"></div>
</div>`;
/* IV4 — Тренажёр углов */
html += `<div class="wg" id="p13-iv4">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 4</span><div class="wg-title">Тренажёр углов</div></div>
<div class="wg-help">Введи внутренний угол $\\beta$ правильного $n$-угольника (число градусов, целое).</div>
<div class="score-display"><span>Задача <b id="p13-iv4-i">1</b> / 6</span><span>Очки: <b id="p13-iv4-s">0</b> / 6</span></div>
<div id="p13-iv4-q" style="padding:14px;background:var(--pri-soft);border-radius:10px;font-size:1.05rem;margin-bottom:10px;text-align:center"></div>
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;justify-content:center">
<span style="font-family:'JetBrains Mono',monospace">$\\beta$ =</span>
<input type="number" id="p13-iv4-ans" class="tinp" style="width:110px;text-align:center" step="1">
<span style="font-family:'JetBrains Mono',monospace">°</span>
<button class="btn primary" id="p13-iv4-go">Проверить</button>
<button class="btn" id="p13-iv4-start">Заново</button>
</div>
<div class="feedback" id="p13-iv4-fb"></div>
</div>`;
html += readButton('p13');
html += secNav(null, 'p14');
box.innerHTML = html;
renderMath(box);
/* IV1 — slider n */
(function(){
const sl = document.getElementById('p13-iv1-n');
const lab = document.getElementById('p13-iv1-nval');
const svg = document.getElementById('p13-iv1-svg');
const out = document.getElementById('p13-iv1-out');
const seen = new Set();
const cx = 180, cy = 180, R = 130;
function draw(){
const n = +sl.value;
lab.textContent = n;
const r = R * Math.cos(Math.PI/n);
const pts = regularPoly(cx, cy, R, n);
let s = '';
// описанная окружность
s += '<circle cx="'+cx+'" cy="'+cy+'" r="'+R+'" fill="none" stroke="#94a3b8" stroke-width="1.4" stroke-dasharray="4 4"/>';
// вписанная окружность
s += '<circle cx="'+cx+'" cy="'+cy+'" r="'+r.toFixed(2)+'" fill="none" stroke="#10b981" stroke-width="1.4" stroke-dasharray="3 3"/>';
// многоугольник
s += '<polygon points="'+pts.map(p=>p.x.toFixed(2)+','+p.y.toFixed(2)).join(' ')+'" fill="rgba(8,145,178,.10)" stroke="#0e7490" stroke-width="2.2" stroke-linejoin="round"/>';
// первая сторона (выделим)
const P0 = pts[0], P1 = pts[1];
s += '<line x1="'+P0.x.toFixed(2)+'" y1="'+P0.y.toFixed(2)+'" x2="'+P1.x.toFixed(2)+'" y2="'+P1.y.toFixed(2)+'" stroke="#dc2626" stroke-width="3.6"/>';
// радиус R (из O в P0)
s += '<line x1="'+cx+'" y1="'+cy+'" x2="'+P0.x.toFixed(2)+'" y2="'+P0.y.toFixed(2)+'" stroke="#0891b2" stroke-width="2"/>';
// апофема r (из O в середину первой стороны)
const Mx = (P0.x+P1.x)/2, My = (P0.y+P1.y)/2;
s += '<line x1="'+cx+'" y1="'+cy+'" x2="'+Mx.toFixed(2)+'" y2="'+My.toFixed(2)+'" stroke="#10b981" stroke-width="2" stroke-dasharray="5 3"/>';
// центр O
s += '<circle cx="'+cx+'" cy="'+cy+'" r="3.5" fill="#0f172a"/>';
s += '<text x="'+(cx-10)+'" y="'+(cy-8)+'" text-anchor="end" font-family="Inter,sans-serif" font-size="14" font-weight="700" fill="#0f172a">O</text>';
// вершины
pts.forEach((p,i)=>{ s += '<circle cx="'+p.x.toFixed(2)+'" cy="'+p.y.toFixed(2)+'" r="3" fill="#0e7490"/>'; });
// подписи R и r
const Rmx = (cx+P0.x)/2, Rmy = (cy+P0.y)/2;
s += '<text x="'+(Rmx+8).toFixed(2)+'" y="'+(Rmy-4).toFixed(2)+'" font-family="JetBrains Mono,monospace" font-size="12" font-weight="700" fill="#0891b2">R</text>';
s += '<text x="'+((cx+Mx)/2+8).toFixed(2)+'" y="'+((cy+My)/2-4).toFixed(2)+'" font-family="JetBrains Mono,monospace" font-size="12" font-weight="700" fill="#10b981">r</text>';
svg.innerHTML = s;
const beta = 180*(n-2)/n;
// R = 130 px ↔ R = 10 ед., K = 13 px/ед.
const K = 13;
const Ru = R / K, ru = r / K;
out.innerHTML = '$n = '+n+'$,&nbsp; $\\beta = \\dfrac{180^\\circ \\cdot '+(n-2)+'}{'+n+'} = '+beta.toFixed(2)+'^\\circ$<br>'
+ '$R = '+Ru.toFixed(2)+'$,&nbsp; $r = R\\cos\\dfrac{180^\\circ}{'+n+'} \\approx '+ru.toFixed(2)+'$';
renderMath(out);
seen.add(n);
if(seen.size >= 5 && !seen.has('_done')){ addXp(10,'p13-iv1'); bumpProgress('p13', 15); seen.add('_done'); }
}
sl.addEventListener('input', draw);
draw();
})();
/* IV2 — Калькулятор β */
(function(){
const inp = document.getElementById('p13-iv2-n');
const go = document.getElementById('p13-iv2-go');
const out = document.getElementById('p13-iv2-out');
const fb = document.getElementById('p13-iv2-fb');
let solved = 0;
function calc(){
const n = parseInt(inp.value, 10);
if(isNaN(n)){ feedback(fb, false, '&#10007; Введи целое число $n$.'); return; }
if(n < 3){ feedback(fb, false, '&#10007; У многоугольника должно быть хотя бы 3 стороны.'); return; }
if(n > 100){ feedback(fb, false, '&#10007; Слишком много — попробуй $n \\le 100$.'); return; }
const beta = 180*(n-2)/n;
const sum = (n-2)*180;
const center = 360/n;
out.innerHTML = '<b>$\\beta = \\dfrac{180^\\circ \\cdot ('+n+' - 2)}{'+n+'} = \\dfrac{'+sum+'^\\circ}{'+n+'} = '+beta.toFixed(2)+'^\\circ$</b><br>'
+ 'Сумма всех углов: $('+n+' - 2) \\cdot 180^\\circ = '+sum+'^\\circ$<br>'
+ 'Центральный угол: $\\dfrac{360^\\circ}{'+n+'} = '+center.toFixed(2)+'^\\circ$';
renderMath(out);
feedback(fb, true, '&#10003; Готово.');
solved++;
if(solved === 1){ addXp(10,'p13-iv2'); bumpProgress('p13', 15); }
}
go.addEventListener('click', calc);
inp.addEventListener('keydown', e=>{ if(e.key==='Enter') calc(); });
calc();
})();
/* IV3 — Quickfire «Какой угол?» */
(function(){
const Q = [
{ n:3, ans:60, why:'$\\beta = 180^\\circ \\cdot 1 / 3 = 60^\\circ$' },
{ n:4, ans:90, why:'$\\beta = 180^\\circ \\cdot 2 / 4 = 90^\\circ$' },
{ n:5, ans:108, why:'$\\beta = 180^\\circ \\cdot 3 / 5 = 108^\\circ$' },
{ n:6, ans:120, why:'$\\beta = 180^\\circ \\cdot 4 / 6 = 120^\\circ$' },
{ n:3, ans:60, why:'$\\beta = 180^\\circ \\cdot 1 / 3 = 60^\\circ$ — равносторонний треугольник' },
{ n:4, ans:90, why:'$\\beta = 180^\\circ \\cdot 2 / 4 = 90^\\circ$ — квадрат' },
];
let i = 0, score = 0;
function show(){
if(i >= Q.length){
document.getElementById('p13-iv3-q').innerHTML = '<b>Готово!</b> Результат: ' + score + ' / ' + Q.length;
if(score === Q.length){ addXp(15,'p13-iv3'); bumpProgress('p13', 25); }
else if(score >= Q.length - 1){ addXp(8,'p13-iv3'); bumpProgress('p13', 15); }
return;
}
document.getElementById('p13-iv3-i').textContent = (i+1);
document.getElementById('p13-iv3-s').textContent = score;
document.getElementById('p13-iv3-q').innerHTML = 'Чему равен внутренний угол правильного $n$-угольника при $n = '+Q[i].n+'$?';
renderMath(document.getElementById('p13-iv3-q'));
document.getElementById('p13-iv3-fb').style.display = 'none';
}
function answer(a){
if(i >= Q.length) return;
const fb = document.getElementById('p13-iv3-fb');
if(+a === Q[i].ans){ score++; feedback(fb, true, '&#10003; Верно! '+Q[i].why+'. Дальше ▶'); }
else feedback(fb, false, '&#10007; Нет. '+Q[i].why+'. Дальше ▶');
document.getElementById('p13-iv3-s').textContent = score;
i++;
setTimeout(show, 1100);
}
['60','90','108','120'].forEach(k=>{
const b = document.getElementById('p13-iv3-a'+k); if(b) b.addEventListener('click', ()=>answer(k));
});
show();
})();
/* IV4 — Тренажёр углов */
(function(){
const Q = [
{ n:3, ans:60, hint:'$180^\\circ \\cdot 1 / 3 = 60^\\circ$' },
{ n:4, ans:90, hint:'$180^\\circ \\cdot 2 / 4 = 90^\\circ$' },
{ n:6, ans:120, hint:'$180^\\circ \\cdot 4 / 6 = 120^\\circ$' },
{ n:8, ans:135, hint:'$180^\\circ \\cdot 6 / 8 = 135^\\circ$' },
{ n:9, ans:140, hint:'$180^\\circ \\cdot 7 / 9 = 140^\\circ$' },
{ n:10, ans:144, hint:'$180^\\circ \\cdot 8 / 10 = 144^\\circ$' },
];
let i = 0, score = 0;
function show(){
if(i >= Q.length){
document.getElementById('p13-iv4-q').innerHTML = '<b>Готово!</b> Результат: ' + score + ' / ' + Q.length;
if(score === Q.length){ addXp(15,'p13-iv4'); bumpProgress('p13', 25); }
else if(score >= 4){ addXp(8,'p13-iv4'); bumpProgress('p13', 15); }
return;
}
document.getElementById('p13-iv4-i').textContent = (i+1);
document.getElementById('p13-iv4-s').textContent = score;
document.getElementById('p13-iv4-q').innerHTML = 'Правильный $n$-угольник, $n = '+Q[i].n+'$. Найди внутренний угол $\\beta$ в градусах.';
document.getElementById('p13-iv4-ans').value = '';
renderMath(document.getElementById('p13-iv4-q'));
document.getElementById('p13-iv4-fb').style.display = 'none';
}
function go(){
if(i >= Q.length) return;
const fb = document.getElementById('p13-iv4-fb');
const ans = parseFloat(document.getElementById('p13-iv4-ans').value);
if(isNaN(ans)){ feedback(fb, false, '&#10007; Введи число.'); return; }
if(Math.abs(ans - Q[i].ans) < 0.5){ score++; feedback(fb, true, '&#10003; Верно! '+Q[i].hint+'. Дальше ▶'); }
else feedback(fb, false, '&#10007; Неверно. Ответ: '+Q[i].ans+'° ('+Q[i].hint+'). Дальше ▶');
document.getElementById('p13-iv4-s').textContent = score;
i++;
setTimeout(show, 1300);
}
document.getElementById('p13-iv4-go').addEventListener('click', go);
document.getElementById('p13-iv4-ans').addEventListener('keydown', e=>{ if(e.key==='Enter') go(); });
document.getElementById('p13-iv4-start').addEventListener('click', ()=>{ i=0; score=0; show(); });
show();
})();
wireReadBtn('p13');
}
/* ===== §14 Формулы радиусов ===== */
function buildP14(){
const box = document.getElementById('p14-body');
let html = '';
html += makeCard('theory', 'Сторона, $R$, $r$ и $n$', '14.1', `
<p>Рассмотрим правильный $n$-угольник со стороной $a$, описанной окружностью радиуса $R$ и вписанной радиуса $r$ (это апофема). Соединим центр $O$ с двумя соседними вершинами и серединой стороны — получим прямоугольный треугольник с катетами $a/2$ и $r$, гипотенузой $R$ и углом $180^\\circ / n$ при $O$.</p>
<p>Отсюда:</p>
<p style="text-align:center;font-size:1.05rem">$$\\dfrac{a}{2} = R \\sin\\dfrac{180^\\circ}{n} = r \\tan\\dfrac{180^\\circ}{n}$$</p>
<p>Все остальные формулы — алгебраические следствия:</p>
<ul style="padding-left:22px;line-height:1.95">
<li>$a = 2R \\sin\\dfrac{180^\\circ}{n}$, &nbsp;&nbsp; $R = \\dfrac{a}{2 \\sin(180^\\circ/n)}$;</li>
<li>$a = 2r \\tan\\dfrac{180^\\circ}{n}$, &nbsp;&nbsp; $r = \\dfrac{a}{2 \\tan(180^\\circ/n)}$;</li>
<li>$r = R \\cos\\dfrac{180^\\circ}{n}$, &nbsp;&nbsp; $R = \\dfrac{r}{\\cos(180^\\circ/n)}$.</li>
</ul>`);
html += makeCard('rule', 'Периметр и площадь', '14.2', `
<p><b>Периметр</b> правильного $n$-угольника со стороной $a$:</p>
<p style="text-align:center">$P_n = n \\cdot a$</p>
<p><b>Площадь</b> — сумма $n$ равнобедренных треугольников с основанием $a$ и высотой $r$:</p>
<p style="text-align:center">$$S_n = \\dfrac{1}{2} P_n \\cdot r = \\dfrac{1}{2} n \\, a \\, r$$</p>
<p>Через радиус $R$ (используя $a = 2R\\sin(180^\\circ/n)$ и $r = R\\cos(180^\\circ/n)$):</p>
<p style="text-align:center">$$S_n = n R^2 \\sin\\dfrac{180^\\circ}{n} \\cos\\dfrac{180^\\circ}{n} = \\dfrac{1}{2} n R^2 \\sin\\dfrac{360^\\circ}{n}$$</p>
<p>Последнее равенство получается из формулы двойного угла $\\sin 2\\alpha = 2\\sin\\alpha\\cos\\alpha$.</p>`);
html += makeCard('example', 'Пример: правильный 6-угольник', '14.3', `
<p><b>Дано:</b> правильный 6-угольник со стороной $a = 4$. <b>Найти:</b> $R$, $r$, $P$, $S$.</p>
<p><b>1.</b> $R = \\dfrac{a}{2 \\sin(180^\\circ / 6)} = \\dfrac{4}{2 \\sin 30^\\circ} = \\dfrac{4}{2 \\cdot 0{,}5} = 4$.</p>
<p><b>2.</b> $r = R \\cos 30^\\circ = 4 \\cdot \\dfrac{\\sqrt{3}}{2} = 2\\sqrt{3} \\approx 3{,}46$.</p>
<p><b>3.</b> $P = 6 \\cdot 4 = 24$.</p>
<p><b>4.</b> $S = \\dfrac{1}{2} \\cdot 24 \\cdot 2\\sqrt{3} = 24\\sqrt{3} \\approx 41{,}57$.</p>
<details class="spoiler"><summary>Проверка через $R$</summary><div class="spoiler-body">
$S = \\tfrac{1}{2} n R^2 \\sin(360^\\circ/n) = \\tfrac{1}{2} \\cdot 6 \\cdot 16 \\cdot \\sin 60^\\circ = 48 \\cdot \\tfrac{\\sqrt{3}}{2} = 24\\sqrt{3}$ — совпадает.
</div></details>`);
/* IV1 — Slider n и R */
html += `<div class="wg" id="p14-iv1">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Радиусы по двум ползункам</div></div>
<div class="wg-help">Меняй $n$ и $R$ — увидишь, как пересчитываются сторона $a$, апофема $r$, периметр $P$ и площадь $S$.</div>
<div class="sliders">
<label>Число сторон $n$<b id="p14-iv1-nval">6</b><input type="range" id="p14-iv1-n" min="3" max="12" step="1" value="6"></label>
<label>Радиус $R$<b id="p14-iv1-rval">6</b><input type="range" id="p14-iv1-r" min="2" max="8" step="0.1" value="6"></label>
</div>
<div style="background:var(--card);border-radius:10px;padding:10px;overflow-x:auto">
<svg id="p14-iv1-svg" viewBox="0 0 360 360" style="width:100%;max-width:420px;min-width:280px;height:auto;display:block;margin:0 auto"></svg>
</div>
<div id="p14-iv1-out" style="margin-top:10px;padding:10px 14px;background:var(--pri-soft);border-radius:9px;font-size:.95rem;text-align:center;line-height:1.9"></div>
</div>`;
/* IV2 — Калькулятор сторон */
html += `<div class="wg" id="p14-iv2">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Калькулятор: сторона, $R$, $r$</div></div>
<div class="wg-help">Выбери, какая величина дана. Введи $n$ и значение — получишь два оставшихся.</div>
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px" id="p14-iv2-modes">
<button class="btn primary" data-m="R">Дано $R$</button>
<button class="btn" data-m="r">Дано $r$</button>
<button class="btn" data-m="a">Дано $a$</button>
</div>
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;justify-content:center;margin-bottom:10px">
<span style="font-family:'JetBrains Mono',monospace">$n$ =</span>
<input type="number" id="p14-iv2-n" class="tinp" style="width:80px;text-align:center" value="6" min="3" max="50">
<span style="font-family:'JetBrains Mono',monospace" id="p14-iv2-lab">$R$ =</span>
<input type="number" id="p14-iv2-val" class="tinp" style="width:90px;text-align:center" value="4" step="0.01" min="0.01">
<button class="btn primary" id="p14-iv2-go">Найти остальные</button>
</div>
<div id="p14-iv2-out" style="padding:12px 14px;background:var(--card);border-radius:9px;text-align:center;font-size:1rem;min-height:50px"></div>
<div class="feedback" id="p14-iv2-fb"></div>
</div>`;
/* IV3 — DnD «Подбери формулу» */
html += `<div class="wg" id="p14-iv3">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 3</span><div class="wg-title">Подбери формулу</div></div>
<div class="wg-help">Перетащи каждую формулу в правильный ящик: что именно она вычисляет — сторону $a$, радиус $R$ или апофему $r$.</div>
<div id="p14-iv3-pool"></div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px">
<div class="drop-box"><h5 data-cat="a">Сторона $a$</h5><div class="drop-items" data-cat="a"></div></div>
<div class="drop-box"><h5 data-cat="R">Радиус $R$</h5><div class="drop-items" data-cat="R"></div></div>
<div class="drop-box"><h5 data-cat="r">Апофема $r$</h5><div class="drop-items" data-cat="r"></div></div>
</div>
<div style="display:flex;gap:10px;margin-top:12px;justify-content:center">
<button class="btn primary" id="p14-iv3-check">Проверить</button>
<button class="btn" id="p14-iv3-reset">Сбросить</button>
</div>
<div class="feedback" id="p14-iv3-fb"></div>
</div>`;
/* IV4 — Тренажёр радиусов */
html += `<div class="wg" id="p14-iv4">
<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="p14-iv4-i">1</b> / 6</span><span>Очки: <b id="p14-iv4-s">0</b> / 6</span></div>
<div id="p14-iv4-q" style="padding:14px;background:var(--pri-soft);border-radius:10px;font-size:1.05rem;margin-bottom:10px;text-align:center"></div>
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;justify-content:center">
<span style="font-family:'JetBrains Mono',monospace">ответ =</span>
<input type="number" id="p14-iv4-ans" class="tinp" style="width:110px;text-align:center" step="0.01">
<button class="btn primary" id="p14-iv4-go">Проверить</button>
<button class="btn" id="p14-iv4-start">Заново</button>
</div>
<div class="feedback" id="p14-iv4-fb"></div>
</div>`;
html += readButton('p14');
html += secNav('p13', 'p15');
box.innerHTML = html;
renderMath(box);
/* IV1 — slider n и R */
(function(){
const sln = document.getElementById('p14-iv1-n');
const slr = document.getElementById('p14-iv1-r');
const labN = document.getElementById('p14-iv1-nval');
const labR = document.getElementById('p14-iv1-rval');
const svg = document.getElementById('p14-iv1-svg');
const out = document.getElementById('p14-iv1-out');
const seen = new Set();
const cx = 180, cy = 180;
function draw(){
const n = +sln.value;
const R = +slr.value; // R в условных единицах (2..8)
labN.textContent = n; labR.textContent = R.toFixed(1);
const K = 18; // px на единицу
const Rpx = R * K;
const a = 2*R*Math.sin(Math.PI/n);
const r = R*Math.cos(Math.PI/n);
const P = n*a;
const S = 0.5*n*R*R*Math.sin(2*Math.PI/n);
const pts = regularPoly(cx, cy, Rpx, n);
const rPx = Rpx*Math.cos(Math.PI/n);
let s = '';
s += '<circle cx="'+cx+'" cy="'+cy+'" r="'+Rpx.toFixed(2)+'" fill="none" stroke="#94a3b8" stroke-width="1.4" stroke-dasharray="4 4"/>';
s += '<circle cx="'+cx+'" cy="'+cy+'" r="'+rPx.toFixed(2)+'" fill="none" stroke="#10b981" stroke-width="1.4" stroke-dasharray="3 3"/>';
s += '<polygon points="'+pts.map(p=>p.x.toFixed(2)+','+p.y.toFixed(2)).join(' ')+'" fill="rgba(8,145,178,.10)" stroke="#0e7490" stroke-width="2.2" stroke-linejoin="round"/>';
const P0 = pts[0], P1 = pts[1];
s += '<line x1="'+P0.x.toFixed(2)+'" y1="'+P0.y.toFixed(2)+'" x2="'+P1.x.toFixed(2)+'" y2="'+P1.y.toFixed(2)+'" stroke="#dc2626" stroke-width="3.6"/>';
s += '<line x1="'+cx+'" y1="'+cy+'" x2="'+P0.x.toFixed(2)+'" y2="'+P0.y.toFixed(2)+'" stroke="#0891b2" stroke-width="2"/>';
const Mx = (P0.x+P1.x)/2, My = (P0.y+P1.y)/2;
s += '<line x1="'+cx+'" y1="'+cy+'" x2="'+Mx.toFixed(2)+'" y2="'+My.toFixed(2)+'" stroke="#10b981" stroke-width="2" stroke-dasharray="5 3"/>';
s += '<circle cx="'+cx+'" cy="'+cy+'" r="3.5" fill="#0f172a"/>';
svg.innerHTML = s;
out.innerHTML = '$n = '+n+'$,&nbsp; $R = '+R.toFixed(1)+'$<br>'
+ '$a = 2R\\sin\\dfrac{180^\\circ}{'+n+'} \\approx '+a.toFixed(2)+'$,&nbsp; $r = R\\cos\\dfrac{180^\\circ}{'+n+'} \\approx '+r.toFixed(2)+'$<br>'
+ '$P = n a \\approx '+P.toFixed(2)+'$,&nbsp; $S = \\tfrac{1}{2} n R^2 \\sin\\dfrac{360^\\circ}{'+n+'} \\approx '+S.toFixed(2)+'$';
renderMath(out);
seen.add(n+'_'+R);
if(seen.size >= 5 && !seen.has('_done')){ addXp(10,'p14-iv1'); bumpProgress('p14', 15); seen.add('_done'); }
}
sln.addEventListener('input', draw);
slr.addEventListener('input', draw);
draw();
})();
/* IV2 — Калькулятор сторон */
(function(){
const modes = document.querySelectorAll('#p14-iv2-modes button');
const inpN = document.getElementById('p14-iv2-n');
const inpV = document.getElementById('p14-iv2-val');
const lab = document.getElementById('p14-iv2-lab');
const go = document.getElementById('p14-iv2-go');
const out = document.getElementById('p14-iv2-out');
const fb = document.getElementById('p14-iv2-fb');
let mode = 'R';
let solved = 0;
function updateLabel(){
lab.innerHTML = mode === 'R' ? '$R$ =' : (mode === 'r' ? '$r$ =' : '$a$ =');
renderMath(lab);
}
function calc(){
const n = parseInt(inpN.value, 10);
const v = parseFloat(inpV.value);
if(isNaN(n) || n < 3){ feedback(fb, false, '&#10007; $n$ должно быть целым, $n \\ge 3$.'); return; }
if(isNaN(v) || v <= 0){ feedback(fb, false, '&#10007; Введи положительное значение.'); return; }
const sn = Math.sin(Math.PI/n), cs = Math.cos(Math.PI/n), tn = Math.tan(Math.PI/n);
let a, R, r;
if(mode === 'R'){ R = v; a = 2*R*sn; r = R*cs; }
else if(mode === 'r'){ r = v; R = r/cs; a = 2*r*tn; }
else { a = v; R = a/(2*sn); r = a/(2*tn); }
out.innerHTML = '<b>$a = '+a.toFixed(3)+'$</b>,&nbsp; <b>$R = '+R.toFixed(3)+'$</b>,&nbsp; <b>$r = '+r.toFixed(3)+'$</b><br>'
+ '<span style="font-size:.86rem;color:var(--muted)">Формулы: $a = 2R\\sin(180^\\circ/n)$, $r = R\\cos(180^\\circ/n)$, $R = a / (2\\sin(180^\\circ/n))$.</span>';
renderMath(out);
feedback(fb, true, '&#10003; Готово.');
solved++;
if(solved === 1){ addXp(10,'p14-iv2'); bumpProgress('p14', 15); }
}
modes.forEach(b=>{
b.addEventListener('click', ()=>{
mode = b.dataset.m;
modes.forEach(x=>{ x.classList.remove('primary'); x.classList.add('btn'); });
b.classList.remove('btn'); b.classList.add('btn','primary');
updateLabel();
out.innerHTML = ''; fb.style.display = 'none';
});
});
go.addEventListener('click', calc);
inpV.addEventListener('keydown', e=>{ if(e.key==='Enter') calc(); });
updateLabel(); calc();
})();
/* IV3 — DnD «Подбери формулу» */
(function(){
const items = [
{ id:'f1', cat:'a', html:'$a = 2R \\sin(180^\\circ/n)$' },
{ id:'f2', cat:'a', html:'$a = 2r \\tan(180^\\circ/n)$' },
{ id:'f3', cat:'R', html:'$R = a / (2\\sin(180^\\circ/n))$' },
{ id:'f4', cat:'R', html:'$R = r / \\cos(180^\\circ/n)$' },
{ id:'f5', cat:'r', html:'$r = R \\cos(180^\\circ/n)$' },
{ id:'f6', cat:'r', html:'$r = a / (2\\tan(180^\\circ/n))$' },
];
const sorter = setupSorter({
poolId:'p14-iv3-pool',
scopeSelector:'#p14-iv3',
items: items,
cats:['a','R','r'],
columnLayout:false,
});
document.getElementById('p14-iv3-check').addEventListener('click', ()=>{
const fb = document.getElementById('p14-iv3-fb');
const placedCount = items.filter(it => sorter.placed[it.id]).length;
const correct = items.filter(it => sorter.placed[it.id] === it.cat).length;
if(placedCount < items.length){ feedback(fb, false, '&#10007; Размести все 6 формул.'); return; }
if(correct === items.length){ feedback(fb, true, '&#10003; Все 6 на месте! +15 XP'); addXp(15,'p14-iv3'); bumpProgress('p14', 25); }
else feedback(fb, false, '&#10007; Правильно ' + correct + ' из 6. Попробуй ещё.');
});
document.getElementById('p14-iv3-reset').addEventListener('click', ()=>{ sorter.reset(); document.getElementById('p14-iv3-fb').style.display = 'none'; });
})();
/* IV4 — Тренажёр радиусов */
(function(){
const Q = [
{ q:'Правильный 6-угольник со стороной $a = 4$. Найди радиус описанной окружности $R$.',
ans:4, hint:'$R = a / (2\\sin 30^\\circ) = 4 / 1 = 4$' },
{ q:'Правильный 4-угольник (квадрат) со стороной $a = 4$. Найди $R$.',
ans:2.83, hint:'$R = a / (2\\sin 45^\\circ) = 4 / \\sqrt{2} = 2\\sqrt{2} \\approx 2{,}83$' },
{ q:'Правильный 3-угольник со стороной $a = 6$. Найди радиус вписанной окружности $r$.',
ans:1.73, hint:'$r = a / (2\\tan 60^\\circ) = 6/(2\\sqrt{3}) = \\sqrt{3} \\approx 1{,}73$' },
{ q:'Правильный 6-угольник со стороной $a = 4$. Найди апофему $r$.',
ans:3.46, hint:'$r = R\\cos 30^\\circ = 4 \\cdot \\tfrac{\\sqrt{3}}{2} = 2\\sqrt{3} \\approx 3{,}46$' },
{ q:'Правильный 4-угольник (квадрат) со стороной $a = 4$. Найди $r$.',
ans:2, hint:'$r = a / (2\\tan 45^\\circ) = 4 / 2 = 2$' },
{ q:'Правильный 12-угольник с $R = 10$. Найди сторону $a$.',
ans:5.18, hint:'$a = 2R\\sin 15^\\circ = 20 \\sin 15^\\circ \\approx 5{,}18$' },
];
let i = 0, score = 0;
function show(){
if(i >= Q.length){
document.getElementById('p14-iv4-q').innerHTML = '<b>Готово!</b> Результат: ' + score + ' / ' + Q.length;
if(score === Q.length){ addXp(15,'p14-iv4'); bumpProgress('p14', 25); }
else if(score >= 4){ addXp(8,'p14-iv4'); bumpProgress('p14', 15); }
return;
}
document.getElementById('p14-iv4-i').textContent = (i+1);
document.getElementById('p14-iv4-s').textContent = score;
document.getElementById('p14-iv4-q').innerHTML = Q[i].q;
document.getElementById('p14-iv4-ans').value = '';
renderMath(document.getElementById('p14-iv4-q'));
document.getElementById('p14-iv4-fb').style.display = 'none';
}
function go(){
if(i >= Q.length) return;
const fb = document.getElementById('p14-iv4-fb');
const ans = parseFloat(document.getElementById('p14-iv4-ans').value);
if(isNaN(ans)){ feedback(fb, false, '&#10007; Введи число.'); return; }
if(Math.abs(ans - Q[i].ans) < 0.05){ score++; feedback(fb, true, '&#10003; Верно! '+Q[i].hint+'. Дальше ▶'); }
else feedback(fb, false, '&#10007; Неверно. Ответ: '+Q[i].ans+' ('+Q[i].hint+'). Дальше ▶');
document.getElementById('p14-iv4-s').textContent = score;
i++;
setTimeout(show, 1300);
}
document.getElementById('p14-iv4-go').addEventListener('click', go);
document.getElementById('p14-iv4-ans').addEventListener('keydown', e=>{ if(e.key==='Enter') go(); });
document.getElementById('p14-iv4-start').addEventListener('click', ()=>{ i=0; score=0; show(); });
show();
})();
wireReadBtn('p14');
}
function buildFinal4(){
const box = document.getElementById('final4-body');
let html = '';
/* Часть А — Шпаргалка главы (4 mini-карточки) */
html += `<div class="card">
<div class="card-header">
<span class="card-icon theory">${ICONS.theory}</span>
<span class="card-title">Шпаргалка главы 4</span>
<span class="card-num">Итог</span>
</div>
<div class="card-body">
<p>Все ключевые формулы главы <b>«Правильные многоугольники»</b> — в одном месте. Просмотри перед боссами!</p>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px;margin-top:10px">
<div style="padding:12px 14px;background:var(--acc-soft);border-radius:11px;border-left:3px solid var(--pri)">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
<svg viewBox="0 0 24 24" fill="none" stroke="var(--pri2)" stroke-width="2" style="width:18px;height:18px"><polygon points="12 2 22 8 22 16 12 22 2 16 2 8"/></svg>
<span style="font-family:'Unbounded',sans-serif;font-weight:700;color:var(--pri2);font-size:.92rem">§ 13 · Правильные многоуг.</span>
</div>
<div style="font-size:.92rem">Внутренний угол $\\beta = \\dfrac{180°(n-2)}{n}$. Центральный угол $= \\dfrac{360°}{n}$.</div>
</div>
<div style="padding:12px 14px;background:var(--acc-soft);border-radius:11px;border-left:3px solid var(--pri)">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
<svg viewBox="0 0 24 24" fill="none" stroke="var(--pri2)" stroke-width="2" style="width:18px;height:18px"><circle cx="12" cy="12" r="9"/><line x1="12" y1="12" x2="20" y2="12"/><line x1="12" y1="12" x2="12" y2="4"/></svg>
<span style="font-family:'Unbounded',sans-serif;font-weight:700;color:var(--pri2);font-size:.92rem">§ 14 · Радиусы</span>
</div>
<div style="font-size:.92rem">$\\dfrac{a}{2} = R\\sin\\dfrac{180°}{n} = r\\tan\\dfrac{180°}{n}$. $r = R\\cos\\dfrac{180°}{n}$.</div>
</div>
<div style="padding:12px 14px;background:var(--acc-soft);border-radius:11px;border-left:3px solid var(--pri)">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
<svg viewBox="0 0 24 24" fill="none" stroke="var(--pri2)" stroke-width="2" style="width:18px;height:18px"><polygon points="3 20 21 20 12 4"/></svg>
<span style="font-family:'Unbounded',sans-serif;font-weight:700;color:var(--pri2);font-size:.92rem">§ 15 · Эталоны $n=3,4,6$</span>
</div>
<div style="font-size:.92rem">3-уг: $a = R\\sqrt{3}$. 4-уг: $a = R\\sqrt{2}$. 6-уг: $a = R$.</div>
</div>
<div style="padding:12px 14px;background:var(--acc-soft);border-radius:11px;border-left:3px solid var(--pri)">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
<svg viewBox="0 0 24 24" fill="none" stroke="var(--pri2)" stroke-width="2" style="width:18px;height:18px"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="2.5"/></svg>
<span style="font-family:'Unbounded',sans-serif;font-weight:700;color:var(--pri2);font-size:.92rem">§ 16 · Окружность и круг</span>
</div>
<div style="font-size:.92rem">$C = 2\\pi R$, $S = \\pi R^2$. Дуга $\\ell = \\dfrac{\\pi R \\alpha}{180}$, сектор $S = \\dfrac{\\pi R^2 \\alpha}{360}$.</div>
</div>
</div>
</div>
</div>`;
/* Часть Б — 5 боссов */
html += `<div class="card">
<div class="card-header">
<span class="card-icon rule">${ICONS.rule}</span>
<span class="card-title">Боссы главы 4</span>
<span class="card-num">5</span>
</div>
<div class="card-body">
<p>5 интегрированных задач — каждая комбинирует темы главы. За каждого побеждённого босса — <b>+10 XP</b> и +18% к прогрессу. Победишь всех — <b>+50 XP бонус</b> и ачивка «Магистр многоугольников и круга»!</p>
</div>
</div>`;
html += '<div id="ch4G-bosses-container"></div>';
html += `<div style="margin-top:18px;padding:18px 20px;background:linear-gradient(135deg,var(--pri-soft),var(--acc-soft));border-radius:14px;border:1.5px solid var(--pri);text-align:center" id="ch4G-final-summary">
<div style="font-family:'Unbounded',sans-serif;font-weight:800;color:var(--pri2);font-size:1.1rem;margin-bottom:6px">Прогресс по боссам</div>
<div id="ch4G-boss-overall" style="font-size:.95rem;color:var(--text);margin-bottom:10px">0 / 5 боссов побеждено</div>
<div style="height:12px;background:var(--card);border-radius:8px;overflow:hidden;border:1px solid var(--border)">
<div id="ch4G-boss-overall-fill" style="height:100%;width:0%;background:linear-gradient(90deg,var(--pri),var(--acc));transition:width .35s"></div>
</div>
<div id="ch4G-final-reward" style="margin-top:14px;display:none;padding:14px;background:var(--card);border-radius:11px;border:2px solid var(--warn)">
<div style="display:flex;align-items:center;justify-content:center;gap:8px;margin-bottom:6px">
<svg viewBox="0 0 24 24" fill="none" stroke="var(--warn)" stroke-width="2.2" style="width:22px;height:22px"><polygon points="12 2 15 9 22 9 17 14 19 22 12 18 5 22 7 14 2 9 9 9"/></svg>
<div style="font-family:'Unbounded',sans-serif;font-weight:800;color:var(--pri2);font-size:1.05rem">Магистр многоугольников и круга</div>
</div>
<div style="font-size:.92rem;margin-bottom:10px">Глава 4 пройдена! Все 5 боссов повержены. +50 XP бонус.</div>
<div style="margin-top:10px;padding:12px;background:linear-gradient(135deg,var(--acc-soft),var(--pri-soft));border-radius:10px;border:1px solid var(--pri)">
<div style="font-family:'Unbounded',sans-serif;font-weight:800;color:var(--pri2);font-size:1.02rem;margin-bottom:6px">Геометрия 9 пройдена!</div>
<div style="font-size:.9rem;color:var(--text);margin-bottom:10px">Ты прошёл все 4 главы курса «Геометрия 9». Соотношения, окружности, площади и правильные многоугольники — в твоём арсенале. Возвращайся в хаб, чтобы выбрать следующий учебник!</div>
<a class="btn primary" href="/textbook/geometry-9" style="text-decoration:none">К хабу Геометрии 9 <svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></a>
</div>
</div>
</div>`;
html += secNav('p16', null);
box.innerHTML = html;
renderMath(box);
/* Боссы */
const BOSSES = [
{
n:1, color:'#10b981',
title:'Циклоп Углов',
tag:'§ 13',
q:'Чему равен <b>внутренний угол правильного 12-угольника</b> (в градусах)?',
ans:150, decimal:false,
hint:'$\\beta = \\dfrac{180°(n-2)}{n} = \\dfrac{180° \\cdot 10}{12} = 150°$.'
},
{
n:2, color:'#0891b2',
title:'Минотавр Сторон',
tag:'§ 14 + § 15',
q:'В правильный шестиугольник с описанной окружностью радиуса $R = 5$ вписан правильный треугольник (та же окружность). Найди <b>сторону треугольника</b> (округли до сотых).',
ans:8.66, decimal:true,
hint:'У треугольника та же $R = 5$. По эталону: $a = R\\sqrt{3} = 5\\sqrt{3} \\approx 8{,}66$.'
},
{
n:3, color:'#7c3aed',
title:'Гарпия Окружности',
tag:'§ 16',
q:'Длина окружности равна $C = 31{,}4$ см. Найди <b>радиус</b> (используй $\\pi = 3{,}14$).',
ans:5, decimal:false,
hint:'$R = \\dfrac{C}{2\\pi} = \\dfrac{31{,}4}{6{,}28} = 5$.'
},
{
n:4, color:'#dc2626',
title:'Дракон Сектора',
tag:'§ 16 + § 13',
q:'Правильный 6-угольник вписан в окружность радиуса $R = 6$. Найди <b>площадь сектора</b>, центральный угол которого равен углу, опирающемуся на одну сторону (используй $\\pi = 3{,}14$, округли до сотых).',
ans:18.84, decimal:true,
hint:'Центральный угол 6-уг $= \\dfrac{360°}{6} = 60°$. $S = \\dfrac{\\pi R^2 \\cdot 60}{360} = \\dfrac{\\pi \\cdot 36}{6} = 6\\pi \\approx 18{,}84$.'
},
{
n:5, color:'#f59e0b',
title:'Мастер Многоугольников',
tag:'§§ 13-16 — синтез',
q:'В круг радиуса $R = 10$ вписан правильный 4-угольник (квадрат). Найди <b>разницу</b> между площадью круга и площадью квадрата (используй $\\pi = 3{,}14$).',
ans:114, decimal:false,
hint:'$S_{круга} = \\pi R^2 = 100\\pi = 314$. Для квадрата $a = R\\sqrt{2}$, $S_{кв} = a^2 = 2R^2 = 200$. Разница $= 314 - 200 = 114$.'
},
];
const cont = document.getElementById('ch4G-bosses-container');
const STATE_KEY = 'geometry9_ch4_bosses';
const BOSS_STATE = (function(){
try{ const s = localStorage.getItem(STATE_KEY); if(s){ const p = JSON.parse(s); if(Array.isArray(p) && p.length === BOSSES.length) return p; } }catch(e){}
return BOSSES.map(()=>({defeated:false}));
})();
function saveBosses(){ try{ localStorage.setItem(STATE_KEY, JSON.stringify(BOSS_STATE)); }catch(e){} }
cont.innerHTML = BOSSES.map((b, idx)=>{
const stepAttr = b.decimal ? 'step="0.01"' : 'step="1"';
const ph = b.decimal ? 'число (можно с запятой)' : 'целое число';
return '<div class="boss-card" id="bossG4-'+b.n+'-card" style="padding:16px;background:var(--card);border-radius:12px;border:2px solid '+b.color+';margin-bottom:14px;transition:background .3s,box-shadow .3s">'
+'<div style="display:flex;align-items:center;gap:10px;margin-bottom:10px;flex-wrap:wrap">'
+'<svg viewBox="0 0 24 24" fill="none" stroke="'+b.color+'" stroke-width="2.2" style="width:28px;height:28px;flex-shrink:0"><polygon points="12,2 22,20 2,20"/></svg>'
+'<div style="font-family:\'Unbounded\',sans-serif;font-weight:800;color:'+b.color+';font-size:1.05rem">Босс '+b.n+': '+b.title+'</div>'
+'<div style="margin-left:auto;font-size:.78rem;color:var(--muted);padding:3px 8px;background:var(--acc-soft);border-radius:6px">'+b.tag+'</div>'
+'</div>'
+'<div id="bossG4-'+b.n+'-q" style="padding:12px 14px;background:var(--acc-soft);border-radius:9px;font-size:1rem;line-height:1.55;margin-bottom:10px">'+b.q+'</div>'
+'<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">'
+'<span style="font-family:\'JetBrains Mono\',monospace;font-size:.92rem">ответ =</span>'
+'<input type="number" '+stepAttr+' id="bossG4-'+b.n+'-ans" class="tinp" style="width:150px;text-align:center" placeholder="'+ph+'">'
+'<button class="btn primary" id="bossG4-'+b.n+'-go" style="background:'+b.color+';border-color:'+b.color+'">Атаковать</button>'
+'<button class="btn" id="bossG4-'+b.n+'-hint">Подсказка</button>'
+'</div>'
+'<div class="feedback" id="bossG4-'+b.n+'-fb"></div>'
+'</div>';
}).join('');
renderMath(cont);
function markDefeatedUI(b, idx){
const card = document.getElementById('bossG4-'+b.n+'-card');
const goBtn = document.getElementById('bossG4-'+b.n+'-go');
const ansInp = document.getElementById('bossG4-'+b.n+'-ans');
if(!card || !goBtn || !ansInp) return;
card.style.background = 'linear-gradient(135deg,var(--acc-soft),var(--pri-soft))';
card.style.boxShadow = '0 0 0 2px '+b.color+'33, 0 8px 24px rgba(16,185,129,.12)';
goBtn.disabled = true; goBtn.style.opacity = .55;
goBtn.innerHTML = '<svg class="ic" viewBox="0 0 24 24" style="margin-right:4px"><polyline points="20 6 9 17 4 12"/></svg> Повержен';
ansInp.disabled = true;
}
function refreshOverall(){
const won = BOSS_STATE.filter(s => s.defeated).length;
const txt = document.getElementById('ch4G-boss-overall');
const fill = document.getElementById('ch4G-boss-overall-fill');
if(txt) txt.textContent = won + ' / ' + BOSSES.length + ' боссов побеждено';
if(fill) fill.style.width = (won * 100 / BOSSES.length) + '%';
if(won >= BOSSES.length){
const reward = document.getElementById('ch4G-final-reward');
if(reward && reward.style.display === 'none'){
reward.style.display = 'block';
if(!STATE.achievements.has('ch4_done')){
achievement('ch4_done','Магистр многоугольников и круга');
addXp(50, 'ch4-bonus');
bumpProgress('final4', 10);
if(window.confetti){ try{ window.confetti(); }catch(e){} }
}
}
}
}
BOSSES.forEach((b, idx)=>{
const goBtn = document.getElementById('bossG4-'+b.n+'-go');
const hintBtn = document.getElementById('bossG4-'+b.n+'-hint');
const ansInp = document.getElementById('bossG4-'+b.n+'-ans');
if(BOSS_STATE[idx].defeated) markDefeatedUI(b, idx);
goBtn.addEventListener('click', ()=>{
if(BOSS_STATE[idx].defeated) return;
const fb = document.getElementById('bossG4-'+b.n+'-fb');
const raw = (ansInp.value||'').replace(',', '.').trim();
const val = parseFloat(raw);
if(isNaN(val) || raw === ''){ feedback(fb, false, '&#10007; Введи число.'); return; }
const ok = Math.abs(val - b.ans) < 0.05;
if(ok){
BOSS_STATE[idx].defeated = true; saveBosses();
feedback(fb, true, '&#10003; Босс '+b.n+' повержен! +10 XP. '+b.hint);
addXp(10, 'boss-ch4-'+b.n);
bumpProgress('final4', 18);
markDefeatedUI(b, idx);
refreshOverall();
} else {
feedback(fb, false, '&#10007; Промах. Попробуй ещё. Подсказка доступна.');
}
});
hintBtn.addEventListener('click', ()=>{
const fb = document.getElementById('bossG4-'+b.n+'-fb');
fb.className = 'feedback ok';
fb.innerHTML = '<b>Подсказка:</b> '+b.hint;
fb.style.display = 'block';
fb.style.background = 'var(--warn-bg)';
fb.style.color = '#92400e';
fb.style.borderLeftColor = 'var(--warn)';
renderMath(fb);
});
ansInp.addEventListener('keydown', e=>{ if(e.key === 'Enter') goBtn.click(); });
});
refreshOverall();
}
/* ===== Search ===== */
const SEARCH_INDEX = (function(){
const arr=[];
PARAS.forEach(p=>arr.push({kind:'Параграф',title:p.num+' '+p.name,desc:p.sub||'',sec:p.id}));
return arr;
})();
function initSearch(){
const modal=document.getElementById('search-modal'),inp=document.getElementById('search-input'),out=document.getElementById('search-results'),btn=document.getElementById('search-btn');
if(!modal||!inp||!out) return;
let cur=0,rows=[];
function score(q,it){ const t=(it.title+' '+it.desc).toLowerCase(); if(t.includes(q)) return 100+(it.title.toLowerCase().startsWith(q)?50:0); let s=0; q.split(/\s+/).forEach(w=>{if(w&&t.includes(w))s+=10;}); return s; }
function rank(q){ q=q.trim().toLowerCase(); if(!q) return SEARCH_INDEX.slice(0,12); return SEARCH_INDEX.map(it=>({it,s:score(q,it)})).filter(x=>x.s>0).sort((a,b)=>b.s-a.s).slice(0,20).map(x=>x.it); }
function render(){ cur=0; if(!rows.length){out.innerHTML='<div class="search-empty">Ничего не найдено</div>';return;} out.innerHTML=rows.map((r,i)=>'<button class="search-row'+(i===0?' active':'')+'" data-i="'+i+'"><div class="sr-kind">'+r.kind+'</div><div class="sr-title">'+r.title+'</div>'+(r.desc?'<div class="sr-desc">'+(r.desc.length>90?r.desc.slice(0,90)+'…':r.desc)+'</div>':'')+'</button>').join(''); out.querySelectorAll('.search-row').forEach(b=>b.addEventListener('click',()=>{cur=+b.dataset.i;pick();})); }
function pick(){ const r=rows[cur]; if(!r) return; close(); goTo(r.sec); }
function move(d){ const items=out.querySelectorAll('.search-row'); if(!items.length) return; items[cur]&&items[cur].classList.remove('active'); cur=(cur+d+items.length)%items.length; items[cur].classList.add('active'); items[cur].scrollIntoView({block:'nearest'}); }
function open(){ modal.classList.add('show'); inp.value=''; rows=rank(''); render(); setTimeout(()=>inp.focus(),50); }
function close(){ modal.classList.remove('show'); }
btn&&btn.addEventListener('click',open);
modal.addEventListener('click',e=>{if(e.target===modal)close();});
inp.addEventListener('input',()=>{rows=rank(inp.value);render();});
inp.addEventListener('keydown',e=>{ if(e.key==='ArrowDown'){e.preventDefault();move(1);}else if(e.key==='ArrowUp'){e.preventDefault();move(-1);}else if(e.key==='Enter'){e.preventDefault();pick();}else if(e.key==='Escape'){e.preventDefault();close();} });
document.addEventListener('keydown',e=>{ if((e.ctrlKey||e.metaKey)&&(e.key==='k'||e.key==='K')){ e.preventDefault(); if(modal.classList.contains('show')) close(); else open(); } });
}
function initSidebarToggle(){
const side=document.getElementById('col-side'),back=document.getElementById('col-side-backdrop'),btn=document.getElementById('sidebar-btn');
if(!side||!btn) return;
function open(){ side.classList.add('open'); back.classList.add('show'); }
function close(){ side.classList.remove('open'); back.classList.remove('show'); }
btn.addEventListener('click',()=>{ if(side.classList.contains('open')) close(); else open(); });
back.addEventListener('click',close);
document.addEventListener('keydown',e=>{ if(e.key==='Escape') close(); });
}
function init(){
loadProgress(); initTheme(); initSidebarToggle(); initSearch();
buildParaSelector(); refreshProgressUI(); loadServerReadState(); goTo('p13');
setTimeout(()=>achievement('start'), 600);
if(window.LS&&window.LS.xp){
window.LS.xp.load().then(function(s){ if(s&&s.xp>STATE.xp){ STATE.xp=s.xp; STATE.level=calcLevel(STATE.xp); saveProgress(); refreshProgressUI(); if(STATE.current) buildSidebar(STATE.current); } });
}
}
document.addEventListener('DOMContentLoaded', init);
/* === GEOM9 POLISH JS === */
(function(){
function bumpScore(el){
if(!el) return;
el.classList.remove('bump');
void el.offsetWidth;
el.classList.add('bump');
setTimeout(function(){ try{ el.classList.remove('bump'); }catch(e){} }, 270);
}
window.__geom9BumpScore = bumpScore;
function observeScores(root){
root = root || document;
var nodes = root.querySelectorAll('.score-display b');
nodes.forEach(function(b){
if(b.__scoreObs) return;
b.__scoreObs = true;
var last = b.textContent;
try{
var mo = new MutationObserver(function(){
var nv = b.textContent;
if(nv !== last){ last = nv; bumpScore(b); }
});
mo.observe(b, {childList:true, characterData:true, subtree:true});
}catch(e){}
});
}
function rescanScores(){ try{ observeScores(document); }catch(e){} }
if(document.readyState === 'loading') document.addEventListener('DOMContentLoaded', rescanScores);
else rescanScores();
try{
var rootObs = new MutationObserver(function(muts){
var need = false;
for(var i=0;i<muts.length && !need;i++){
var m = muts[i];
for(var j=0;j<m.addedNodes.length;j++){
var n = m.addedNodes[j];
if(n.nodeType===1){
if(n.classList && n.classList.contains('score-display')) { need = true; break; }
if(n.querySelector && n.querySelector('.score-display b')) { need = true; break; }
}
}
}
if(need) rescanScores();
});
rootObs.observe(document.body, {childList:true, subtree:true});
}catch(e){}
function refreshDoneMarks(){
try{
if(typeof STATE === 'undefined' || !STATE.progress) return;
document.querySelectorAll('.psel-card').forEach(function(c){
var id = c.dataset.id || c.dataset.progCard;
if(!id) return;
var pct = +STATE.progress[id] || 0;
if(!c.querySelector('.psel-done')){
var s = document.createElement('span');
s.className = 'psel-done';
s.setAttribute('title','Прочитано');
s.innerHTML = '<svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg>';
c.appendChild(s);
}
c.classList.toggle('done', pct >= 50);
});
}catch(e){}
}
try{
if(typeof window.refreshProgressUI === 'function'){
var _origRP = window.refreshProgressUI;
window.refreshProgressUI = function(){ var r = _origRP.apply(this, arguments); setTimeout(refreshDoneMarks, 0); return r; };
}
}catch(e){}
setTimeout(refreshDoneMarks, 600);
setTimeout(refreshDoneMarks, 1800);
window.addEventListener('focus', function(){ setTimeout(refreshDoneMarks, 200); });
document.addEventListener('click', function(e){
var card = e.target.closest && e.target.closest('.psel-card');
if(!card) return;
var id = card.dataset.id;
if(!id) return;
setTimeout(function(){
var sec = document.getElementById('sec-' + id);
if(sec) try{ sec.scrollIntoView({behavior:'smooth', block:'start'}); }catch(e){}
}, 60);
});
function observeBossGlow(){
var cards = document.querySelectorAll('.boss-card');
cards.forEach(function(card){
if(card.__bossObs) return;
card.__bossObs = true;
try{
var mo = new MutationObserver(function(){
if(card.__glowed) return;
var go = card.querySelector('button[id*="-go"]');
var fb = card.querySelector('.feedback.ok, .boss-fb.ok');
var defeatedByBtn = go && go.disabled;
var defeatedByFb = fb && fb.textContent && /Побед|повержен/i.test(fb.textContent);
if(defeatedByBtn || defeatedByFb){
card.__glowed = true;
card.classList.add('glow','pulse');
setTimeout(function(){ try{ card.classList.remove('pulse'); }catch(e){} }, 900);
setTimeout(function(){ try{ card.classList.remove('glow'); }catch(e){} }, 1400);
}
});
mo.observe(card, {childList:true, subtree:true, attributes:true, attributeFilter:['class','style','disabled']});
var goNow = card.querySelector('button[id*="-go"]');
if(goNow && goNow.disabled) card.__glowed = true;
}catch(e){}
});
}
var bossRescan = function(){ try{ observeBossGlow(); }catch(e){} };
setTimeout(bossRescan, 800);
var bossPoll = setInterval(bossRescan, 2500);
setTimeout(function(){ try{ clearInterval(bossPoll); }catch(e){} }, 90000);
})();
</script>
</body>
</html>