Files
Learn_System/frontend/textbooks/geometry_7_ch5.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

1398 lines
104 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>Геометрия 7 · Глава 5 · Задачи на построение</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"
onload="renderMathInElement(document.body,{delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false})"></script>
<script src="/js/api.js" defer></script>
<script src="/js/xp.js" defer></script>
<script src="/js/geom7_svg.js?v=6" 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; --muted:#64748b;
--border:#e2e8f0; --sh:0 1px 3px rgba(0,0,0,.06); --sh2:0 4px 14px rgba(0,0,0,.08);
--pri:#db2777; --pri2:#be185d; --pri-soft:#fce7f3;
--acc:#ec4899; --acc2:#db2777; --acc-soft:#fbcfe8;
--ok:#10b981; --ok-bg:#d1fae5; --warn:#f59e0b; --warn-bg:#fef3c7;
--bad:#ef4444; --fail:#dc2626; --fail-bg:#fee2e2;
}
.dark{--bg:#0e0612; --card:#180a18; --card-soft:#1f0e1f; --text:#fee0f0; --muted:#a0708a; --border:#321632}
*{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,#831843 0%,#be185d 55%,#f472b6 100%);color:#fff;padding:46px 22px 30px;overflow:hidden;border-bottom:2px solid rgba(252,231,243,.2);min-height:130px}
.hdr::before{content:'ГЛАВА 5';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(252,230,243,.12);line-height:1;pointer-events: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:'\25CB';position:absolute;right:-10px;top:-20px;font-size:clamp(2rem,8vw,5.5rem);font-weight:900;color:var(--pri);opacity:.10;line-height:1;pointer-events:none;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}
.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)}
.btn-primary:hover{transform:translateY(-1px);box-shadow:0 8px 28px rgba(219,39,119,.32)}
.hero-progress{flex:1;min-width:200px;max-width:280px}
.hp-label{font-size:.74rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;display:block;margin-bottom:5px}
.hp-bar{height:8px;background:rgba(219,39,119,.18);border-radius:5px;overflow:hidden}
.hp-fill{height:100%;background:linear-gradient(90deg,var(--pri),var(--acc));border-radius:5px;width:0%;transition:width .6s cubic-bezier(.16,1,.3,1)}
.hp-text{font-size:.78rem;color:var(--muted);font-weight:700;margin-top:4px;display:block}
.hero-xp-badge{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:linear-gradient(135deg,var(--warn,#f59e0b),var(--pri));color:#fff;border-radius:99px;font-size:.82rem;font-weight:800;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(170px,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(219,39,119,.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)}
.sec[id="sec-p27"] { --sec-acc:#db2777; --sec-acc-d:#be185d; --sec-acc-soft:#fce7f3; }
.sec[id="sec-p28"] { --sec-acc:#c026d3; --sec-acc-d:#a21caf; --sec-acc-soft:#fae8ff; }
.sec[id="sec-p29"] { --sec-acc:#7c3aed; --sec-acc-d:#6d28d9; --sec-acc-soft:#ede9fe; }
.sec[id="sec-p30"] { --sec-acc:#0891b2; --sec-acc-d:#0e7490; --sec-acc-soft:#cffafe; }
.sec[id="sec-p31"] { --sec-acc:#059669; --sec-acc-d:#047857; --sec-acc-soft:#d1fae5; }
.sec[id="sec-final5"]{ --sec-acc:#db2777; --sec-acc-d:#be185d; --sec-acc-soft:#fce7f3; }
.sec{display:none;position:relative;animation:fadeIn .35s ease}
.sec.active{display:block}
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
.sec::before{content:attr(data-watermark);position:absolute;right:-20px;top:10%;font-family:'Unbounded',sans-serif;font-size:clamp(6rem,18vw,14rem);font-weight:900;color:transparent;-webkit-text-stroke:1.5px var(--sec-acc-soft,var(--pri-soft));line-height:1;pointer-events:none;z-index:0;opacity:.35}
.sec-header{margin-bottom:22px;padding-bottom:14px;border-bottom:2px solid var(--sec-acc-soft,var(--pri-soft));position:relative;z-index:1}
.sec-num{display:inline-block;padding:4px 10px;background:linear-gradient(135deg,var(--sec-acc,var(--pri)),var(--sec-acc-d,var(--pri2)));color:#fff;border-radius:7px;font-family:'Unbounded',sans-serif;font-size:.78rem;font-weight:800;letter-spacing:.04em;margin-bottom:8px}
.sec-h{font-family:'Unbounded',sans-serif;font-size:1.55rem;font-weight:800;color:var(--sec-acc-d,var(--pri2));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(219,39,119,.06);position:relative;z-index:1}
.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.theory{background:#8b5cf6}.card-icon.rule{background:#ec4899}.card-icon.algo{background:#f59e0b}.card-icon.example{background:#10b981}
.card-icon .ic{width:18px;height:18px}
.card-title{font-family:'Unbounded',sans-serif;font-size:.82rem;font-weight:800;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);flex:1}
.card-num{font-size:.74rem;font-weight:700;color:var(--muted);background:var(--sec-acc-soft,var(--pri-soft));padding:3px 7px;border-radius:5px}
.card-body{font-size:.94rem;line-height:1.65}
.card-body p{margin-bottom:8px}
.wg{background:linear-gradient(135deg,var(--card),var(--sec-acc-soft,var(--pri-soft)));border:1.5px solid var(--sec-acc,var(--pri));border-radius:14px;padding:18px 20px;margin-bottom:18px;box-shadow:var(--sh2);position:relative;z-index:1}
.wg-header{display:flex;align-items:center;gap:8px;margin-bottom:14px}
.wg-badge{padding:4px 9px;background:var(--sec-acc,var(--pri));color:#fff;border-radius:6px;font-family:'Unbounded',sans-serif;font-size:.68rem;font-weight:800;text-transform:uppercase;letter-spacing:.06em}
.wg-title{font-family:'Unbounded',sans-serif;font-size:1.05rem;font-weight:800;color:var(--sec-acc-d,var(--pri2));flex:1}
.wg-help{font-size:.88rem;color:var(--text);margin-bottom:12px;line-height:1.55;background:linear-gradient(135deg,var(--warn-bg,#fef3c7),var(--sec-acc-soft,var(--pri-soft)));border-left:4px solid var(--warn,#f59e0b);padding:9px 14px;border-radius:9px}
.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}
.btn:hover{background:var(--sec-acc-soft,var(--pri-soft));border-color:var(--sec-acc,var(--pri))}
.btn.primary{background:var(--sec-acc,var(--pri));color:#fff;border-color:var(--sec-acc,var(--pri))}
.btn.primary:hover{background:var(--sec-acc-d,var(--pri2));border-color:var(--sec-acc-d,var(--pri2))}
.tinp{padding:8px 12px;border:1.5px solid var(--border);border-radius:8px;background:var(--card);color:var(--text);font-family:'JetBrains Mono',monospace}
.tinp:focus{outline:0;border-color:var(--sec-acc,var(--pri));box-shadow:0 0 0 3px var(--sec-acc-soft,var(--pri-soft))}
.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}
@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(219,39,119,.18);border-radius:6px;overflow:hidden;margin:7px 0}
.xp-fill{height:100%;background:linear-gradient(90deg,var(--acc),var(--pri));border-radius:6px;transition:width .5s cubic-bezier(.4,0,.2,1)}
.xp-nums{font-size:.74rem;color:var(--muted);display:flex;justify-content:space-between}
.spoiler{border:1px solid var(--border);border-radius:10px;background:var(--card);margin:10px 0;overflow:hidden}
.spoiler summary{padding:8px 14px;background:var(--sec-acc-soft,var(--pri-soft));font-weight:700;cursor:pointer;font-size:.88rem;color:var(--sec-acc-d,var(--pri2));list-style:none;display:flex;align-items:center;gap:8px}
.spoiler summary::-webkit-details-marker{display:none}
.spoiler summary::before{content:'+';font-size:1.2rem;font-weight:900;color:var(--sec-acc,var(--pri));width:18px}
.spoiler[open] summary::before{content:'\2212'}
.spoiler-body{padding:10px 14px;font-size:.92rem;line-height:1.6}
.sec-nav{display:flex;gap:10px;margin-top:24px;padding-top:20px;border-top:1px solid var(--border);justify-content:space-between;flex-wrap:wrap}
.foot{text-align:center;padding:30px 16px;color:var(--muted);font-size:.78rem;border-top:1px solid var(--border);margin-top:30px}
.ach-popup{position:fixed;top:80px;right:18px;background:linear-gradient(135deg,#db2777,#f472b6);color:#fff;padding:12px 18px;border-radius:11px;font-weight:700;font-size:.9rem;box-shadow:0 8px 28px rgba(219,39,119,.45);z-index:1002;display:none;align-items:center;gap:8px;max-width:340px}
.ach-popup.show{display:flex}
.score-display{display:flex;gap:14px;flex-wrap:wrap;align-items:center;padding:10px 14px;background:var(--sec-acc-soft,var(--pri-soft));border-radius:10px;margin-bottom:12px}
.score-display b{color:var(--sec-acc-d,var(--pri2));font-size:1.15rem}
.hp-boss{height:14px;background:rgba(220,38,38,.12);border-radius:9px;overflow:hidden;border:1px solid #fecaca;margin:8px 0}
.hp-boss-fill{height:100%;background:linear-gradient(90deg,#dc2626,#f59e0b);border-radius:9px;transition:width .5s cubic-bezier(.4,0,.2,1)}
.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}
}
.boss-card{padding:16px;background:var(--card);border-radius:12px;border:2px solid var(--bad,#dc2626);margin-bottom:14px}
.boss-head{display:flex;align-items:center;gap:10px;margin-bottom:10px}
.boss-title{font-family:'Unbounded',sans-serif;font-weight:800;color:#7f1d1d;font-size:1.04rem;flex:1}
.boss-stage{font-size:.85rem;color:var(--muted)}
.boss-q{font-size:1rem;line-height:1.55;padding:11px 13px;background:var(--card-soft);border-radius:8px;margin-bottom:9px;border-left:3px solid var(--bad,#dc2626)}
.svg-host{display:flex;justify-content:center;margin:12px 0}
.svg-host-row{display:flex;gap:8px;flex-wrap:wrap;justify-content:center;margin:12px 0}
.steps{counter-reset:step;margin:14px 0}
.step{counter-increment:step;display:flex;gap:12px;align-items:flex-start;padding:10px 14px;background:var(--sec-acc-soft);border-radius:10px;margin-bottom:8px;border-left:4px solid var(--sec-acc)}
.step::before{content:"Шаг " counter(step);flex-shrink:0;font-family:'Unbounded',sans-serif;font-size:.72rem;font-weight:800;color:var(--sec-acc-d);background:#fff;padding:4px 10px;border-radius:7px;letter-spacing:.04em;text-transform:uppercase;align-self:flex-start;min-width:64px;text-align:center}
.step-text{flex:1;font-size:.92rem;line-height:1.5}
</style>
</head>
<body>
<header class="hdr">
<div class="hdr-row">
<div>
<h1>Геометрия 7 · Глава 5</h1>
<div class="hdr-sub">Задачи на построение циркулем и линейкой · алгоритмы · ГМТ</div>
</div>
<div class="hdr-side">
<a href="/textbook/geometry-7" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> К геометрии 7</a>
<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> проводит прямые. <b>Циркуль</b> рисует окружности. Этих двух инструментов хватит, чтобы построить <b>середину отрезка</b>, <b>биссектрису угла</b>, <b>перпендикуляр</b>, <b>треугольник по 3 сторонам</b> и решать сложные задачи методом <b>геометрических мест точек</b>.</p>
<div class="hero-row">
<button class="btn-primary" onclick="goTo('p27')"><svg class="ic" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4" fill="currentColor" stroke="none"/></svg> Начать § 27</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-p27" class="sec" data-watermark="○"><div class="sec-header"><span class="sec-num">§ 27</span><h2 class="sec-h">Простейшие построения</h2></div><div id="p27-body"></div></section>
<section id="sec-p28" class="sec" data-watermark="△"><div class="sec-header"><span class="sec-num">§ 28</span><h2 class="sec-h">Построение треугольника по 3 сторонам</h2></div><div id="p28-body"></div></section>
<section id="sec-p29" class="sec" data-watermark="↗"><div class="sec-header"><span class="sec-num">§ 29</span><h2 class="sec-h">Построение биссектрисы угла</h2></div><div id="p29-body"></div></section>
<section id="sec-p30" class="sec" data-watermark="⊥"><div class="sec-header"><span class="sec-num">§ 30</span><h2 class="sec-h">Середина и перпендикуляр</h2></div><div id="p30-body"></div></section>
<section id="sec-p31" class="sec" data-watermark="ГМТ"><div class="sec-header"><span class="sec-num">§ 31</span><h2 class="sec-h">Метод геометрических мест точек</h2></div><div id="p31-body"></div></section>
<section id="sec-final5" class="sec" data-watermark="★"><div class="sec-header"><span class="sec-num" style="background:linear-gradient(135deg,#db2777,#f472b6)">Финал главы</span><h2 class="sec-h">Итоги. 5 боссов главы 5</h2></div><div id="final5-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">Интерактивный учебник «Геометрия 7» · Глава 5 · Задачи на построение · 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>
<script>
'use strict';
const STATE = { current:'p27', progress:{p27:0,p28:0,p29:0,p30:0,p31:0,final5:0}, achievements:new Map(), xp:0, level:1 };
const TOTAL_PARAS = 6;
const _TB_SLUG = 'geometry-7-ch5';
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:'Начало главы 5!',
p27_done:'Базовые построения освоены!',
p28_done:'Треугольник по 3 сторонам — твой!',
p29_done:'Биссектриса циркулем построена!',
p30_done:'Середина и перпендикуляр — мастер!',
p31_done:'Метод ГМТ — освоен!',
ch5_done:'Глава 5 пройдена!',
geom7_done:'Геометрия 7 полностью пройдена!',
};
function loadProgress(){
try{
const s=localStorage.getItem('geometry7_ch5_progress'); if(s) Object.assign(STATE.progress, JSON.parse(s));
const a=localStorage.getItem('geometry7_ch5_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('geometry7_xp')||0); STATE.level=calcLevel(STATE.xp);
}catch(e){}
}
function saveProgress(){
try{
localStorage.setItem('geometry7_ch5_progress', JSON.stringify(STATE.progress));
localStorage.setItem('geometry7_ch5_achievements', JSON.stringify(Object.fromEntries(STATE.achievements)));
localStorage.setItem('geometry7_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);
if(STATE.progress[key]>=100){
if(key==='p27') achievement('p27_done');
else if(key==='p28') achievement('p28_done');
else if(key==='p29') achievement('p29_done');
else if(key==='p30') achievement('p30_done');
else if(key==='p31') achievement('p31_done');
else if(key==='final5'){ achievement('ch5_done'); setTimeout(()=>achievement('geom7_done','Геометрия 7 полностью пройдена!'),1500); }
}
}
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 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,'geometry7-ch5-'+(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:'p27', num:'§ 27', name:'Простейшие построения', sub:'циркуль + линейка' },
{ id:'p28', num:'§ 28', name:'Треугольник по 3 сторонам', sub:'отрезок + 2 окружн.' },
{ id:'p29', num:'§ 29', name:'Биссектриса угла', sub:'3 дуги + точка M' },
{ id:'p30', num:'§ 30', name:'Середина и ⊥', sub:'2 окружн. → 2 точки' },
{ id:'p31', num:'§ 31', name:'Метод ГМТ', sub:'2 ГМТ → пересечение' },
{ id:'final5', num:'★', name:'Финал главы', sub:'Итоги \xB7 5 боссов', 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);
});
if(window.renderMathInElement) try{ renderMath(g); }catch(e){}
}
const BUILT=new Set();
const BUILDERS = { p27:()=>buildP27(), p28:()=>buildP28(), p29:()=>buildP29(), p30:()=>buildP30(), p31:()=>buildP31(), final5:()=>buildFinal5() };
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 = {
p27:{title:'Шпаргалка \xA727',rows:[
['Линейка','прямая через 2 точки (без делений!)'],
['Циркуль','окружность по центру и радиусу'],
['Базовых опер.','3: прямая, окружность, пересеч.'],
['Этапы решения','Анализ → Построение → Доказ. → Исслед.'],
]},
p28:{title:'Шпаргалка \xA728',rows:[
['Дано','3 отрезка $a, b, c$'],
['Построить','$\\triangle ABC$'],
['Условие','$|b-c| < a < b+c$'],
['Алгоритм','отрезок $c$ + 2 окружн.'],
]},
p29:{title:'Шпаргалка \xA729',rows:[
['Дано','$\\angle AOB$'],
['Построить','биссектрису'],
['Шагов','4: 3 окружн. + луч'],
['Основа доказ.','ССС'],
]},
p30:{title:'Шпаргалка \xA730',rows:[
['Радиус','$r > \\frac{1}{2}AB$'],
['Окружностей','2 (из $A$ и $B$ равных радиусов)'],
['Точек пересеч.','2 ($P_1$ и $P_2$)'],
['Серед. $\\perp$','прямая $P_1 P_2$'],
]},
p31:{title:'Шпаргалка \xA731',rows:[
['Метод ГМТ','точка $=$ ГМТ$_1 \\cap$ ГМТ$_2$'],
['Окружность','$\\{X : OX = r\\}$'],
['Серед. $\\perp$','$\\{X : XA = XB\\}$'],
['Биссектриса','$\\{X : d(X,a) = d(X,b)\\}$'],
]},
final5:{title:'Финал главы',rows:[
['\xA727\xA731','теория главы 5'],
['Боссов','5'],
['Награда','+100 XP + ачивка «Геометрия 7 пройдена»'],
]},
};
const TIPS=[
{sec:'p27',html:'<b>Линейка</b> у нас <b>без делений</b> — она только проводит прямую через 2 точки. <b>Циркуль</b> переносит длины и рисует окружности.'},
{sec:'p28',html:'Без неравенства треугольника <b>построение невозможно</b>: окружности не пересекутся. Сначала проверь $|b-c| < a < b+c$.'},
{sec:'p29',html:'Биссектриса строится через <b>3 окружности</b>: одна из вершины, две из точек пересечения со сторонами. Доказательство — по ССС.'},
{sec:'p30',html:'Серединный $\\perp$ = ось симметрии отрезка. Две окружности <b>равного радиуса</b> ($r > \\frac{1}{2}AB$) из концов.'},
{sec:'p31',html:'<b>Метод ГМТ:</b> ищешь точку с двумя свойствами → строишь ГМТ для каждого → пересекаешь.'},
{sec:'final5',html:'Победишь всех 5 боссов — получишь <b>финальную ачивку</b> «Геометрия 7 полностью пройдена»!'},
];
function buildSidebar(id){
const box=document.getElementById('sidebar-content');
const sb=SIDEBARS[id]||SIDEBARS.p27;
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];
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">Подсказка</h4><div class="sidecard-row" style="font-size:.84rem;line-height:1.55">'+tip.html+'</div></div>';
if(STATE.achievements.size>0){
html+='<div class="sidecard"><h4>Достижения</h4>';
[...STATE.achievements.values()].slice(-4).forEach(text=>{ html+='<div class="sidecard-row" style="font-size:.78rem;color:var(--ok)">✓ '+text+'</div>'; });
html+='</div>';
}
box.innerHTML=html;
if(window.renderMathInElement) try{ renderMath(box); }catch(e){}
}
function initTheme(){
const t=localStorage.getItem('geometry7_ch5_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('geometry7_ch5_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){} }
const ICONS = {
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>',
};
function makeCard(kind, title, num, body){
const labels={theory:'Теория',algo:'Алгоритм',rule:'Правило',example:'Пример'};
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={p27:'\xA727',p28:'\xA728',p29:'\xA729',p30:'\xA730',p31:'\xA731',final5:'Финал'};
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 makeTrainer(opts){
let i=0, score=0;
const Q=opts.questions;
const parser = opts.parser || (v => parseFloat(String(v).replace(',','.')));
function show(){
if(i >= Q.length){
document.getElementById(opts.idPrefix+'-q').innerHTML = '<b>Готово!</b> Результат: '+score+' / '+Q.length;
if(opts.onComplete) opts.onComplete(score, Q.length);
return;
}
document.getElementById(opts.idPrefix+'-i').textContent = (i+1);
document.getElementById(opts.idPrefix+'-s').textContent = score;
document.getElementById(opts.idPrefix+'-q').innerHTML = Q[i].q;
document.getElementById(opts.idPrefix+'-ans').value = '';
renderMath(document.getElementById(opts.idPrefix+'-q'));
document.getElementById(opts.idPrefix+'-fb').style.display = 'none';
}
function go(){
if(i >= Q.length) return;
const fb = document.getElementById(opts.idPrefix+'-fb');
const raw = document.getElementById(opts.idPrefix+'-ans').value.trim();
if(raw === ''){ feedback(fb, false, '&#10007; Введи ответ.'); return; }
const expected = Q[i].a;
let ok = false;
if(typeof expected === 'function') ok = expected(raw);
else { const got = parser(raw); ok = !isNaN(got) && Math.abs(got - expected) < 1e-6; }
if(ok){ score++; feedback(fb, true, '&#10003; Верно! Дальше ▶'); }
else feedback(fb, false, '&#10007; Неверно. Правильно: <b>'+(Q[i].show||expected)+'</b>. Дальше ▶');
document.getElementById(opts.idPrefix+'-s').textContent = score;
i++; setTimeout(show, 1100);
}
document.getElementById(opts.idPrefix+'-go').addEventListener('click', go);
document.getElementById(opts.idPrefix+'-ans').addEventListener('keydown', e=>{ if(e.key==='Enter') go(); });
const restart = document.getElementById(opts.idPrefix+'-start');
if(restart) restart.addEventListener('click', ()=>{ i=0; score=0; show(); });
show();
}
function trainerHTML(idPrefix, total, placeholder){
return '<div class="score-display"><span>Задача <b id="'+idPrefix+'-i">1</b> / '+total+'</span><span>Очки: <b id="'+idPrefix+'-s">0</b> / '+total+'</span></div>'
+'<div id="'+idPrefix+'-q" style="padding:14px;background:var(--sec-acc-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">'
+'<input type="text" id="'+idPrefix+'-ans" class="tinp" placeholder="'+(placeholder||'Ответ')+'" style="width:140px;text-align:center">'
+'<button class="btn primary" id="'+idPrefix+'-go">Проверить</button>'
+'<button class="btn" id="'+idPrefix+'-start">Заново</button>'
+'</div><div class="feedback" id="'+idPrefix+'-fb"></div>';
}
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>'
+' Я прочитал \xA7'+paraId.replace('p','')+' (+10 XP)'
+'</button></div>';
}
function wireReadBtn(paraId){
document.getElementById(paraId+'-read-btn').addEventListener('click', ()=>{
addXp(10, paraId+'-read'); bumpProgress(paraId, 30);
const b=document.getElementById(paraId+'-read-btn'); b.textContent='Прочитано! +10 XP'; b.disabled=true; b.style.opacity=.6;
});
}
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();
buildParaSelector(); refreshProgressUI(); goTo('p27');
setTimeout(()=>achievement('start','Начало главы 5!'), 600);
}
document.addEventListener('DOMContentLoaded', init);
/* ============================================================
\xA7 27 — Простейшие построения циркулем и линейкой
============================================================ */
function buildP27(){
const box = document.getElementById('p27-body');
const G = window.GEOM7;
let html = '';
let svgTools='';
if(G){
/* Две панели: ЛИНЕЙКА слева, ЦИРКУЛЬ справа.
В каждой панели: инструмент сверху → стрелка ↓ → результат снизу. */
const b=G.svgBox(340,210,{id:'p27-tools',cell:0,grid:false,bg:'#fff'});
/* Вертикальный разделитель */
let s=b.open
+ '<line x1="170" y1="15" x2="170" y2="195" stroke="#e2e8f0" stroke-width="1" stroke-dasharray="3 4"/>'
/* ===== ЛЕВАЯ ПАНЕЛЬ — ЛИНЕЙКА ===== */
+ '<text x="85" y="25" text-anchor="middle" font-size="11" font-family="Unbounded,Inter,sans-serif" font-weight="800" fill="#be185d">ЛИНЕЙКА</text>'
/* Корпус линейки */
+ '<rect x="22" y="33" width="126" height="20" fill="#fbcfe8" stroke="#be185d" stroke-width="1.8" rx="2"/>';
/* Маленькие штрихи на верхнем крае линейки (без чисел) */
for(let i=0;i<=9;i++){ const x=22+i*14; s+='<line x1="'+x+'" y1="33" x2="'+x+'" y2="'+(33+(i%2===0?7:4))+'" stroke="#be185d" stroke-width="1"/>'; }
s += '<text x="85" y="70" text-anchor="middle" font-size="8" fill="#7c2d52">(без делений)</text>'
/* Стрелка ↓ */
+ '<path d="M 85 80 L 85 100 M 80 95 L 85 100 L 90 95" fill="none" stroke="#10b981" stroke-width="2.2" stroke-linecap="round"/>'
/* Результат: 2 точки и прямая через них */
+ '<line x1="30" y1="148" x2="140" y2="158" stroke="#0891b2" stroke-width="2.2" stroke-linecap="round"/>'
+ G.point(30,148,'A',{color:'#1e293b',dx:-12,dy:-2,fontSize:11,r:3.5})
+ G.point(140,158,'B',{color:'#1e293b',dx:6,dy:-2,fontSize:11,r:3.5})
+ '<text x="85" y="188" text-anchor="middle" font-size="10" font-family="Unbounded,Inter,sans-serif" font-weight="700" fill="#0891b2">прямая через 2 точки</text>'
/* ===== ПРАВАЯ ПАНЕЛЬ — ЦИРКУЛЬ ===== */
+ '<text x="255" y="25" text-anchor="middle" font-size="11" font-family="Unbounded,Inter,sans-serif" font-weight="800" fill="#be185d">ЦИРКУЛЬ</text>'
/* Две ноги от шарнира вниз */
+ '<line x1="255" y1="33" x2="228" y2="78" stroke="#be185d" stroke-width="3" stroke-linecap="round"/>'
+ '<line x1="255" y1="33" x2="282" y2="78" stroke="#be185d" stroke-width="3" stroke-linecap="round"/>'
/* Шарнир */
+ '<circle cx="255" cy="33" r="3.5" fill="#fbcfe8" stroke="#be185d" stroke-width="1.5"/>'
/* Игла (треугольник) на левой ноге */
+ '<polygon points="228,78 224,85 232,85" fill="#dc2626"/>'
/* Грифель на правой ноге */
+ '<polygon points="282,78 278,85 286,85" fill="#0f172a"/>'
/* Стрелка ↓ */
+ '<path d="M 255 92 L 255 110 M 250 105 L 255 110 L 260 105" fill="none" stroke="#10b981" stroke-width="2.2" stroke-linecap="round"/>'
/* Результат: окружность с центром O и пунктирным радиусом */
+ '<circle cx="255" cy="148" r="26" fill="none" stroke="#7c3aed" stroke-width="2"/>'
+ '<line x1="255" y1="148" x2="273" y2="129" stroke="#7c3aed" stroke-width="1.3" stroke-dasharray="3 2"/>'
+ '<text x="265" y="138" font-size="9" fill="#6d28d9" font-weight="700">r</text>'
+ G.point(255,148,'O',{color:'#1e293b',dx:-10,dy:-2,fontSize:11,r:3})
+ '<text x="255" y="188" text-anchor="middle" font-size="10" font-family="Unbounded,Inter,sans-serif" font-weight="700" fill="#7c3aed">окружность (O, r)</text>'
+ b.close;
svgTools = s;
}
html += makeCard('theory', 'Что такое задача на построение?', '27.1', `
<p><b>Задача на построение</b> — это задача, в которой требуется построить некоторую фигуру (точку, отрезок, окружность, треугольник, ...) с заданными свойствами, используя только два инструмента: <b>циркуль</b> и <b>линейку</b>.</p>
<div class="svg-host">`+svgTools+`</div>
<p style="background:var(--sec-acc-soft);padding:10px 14px;border-radius:8px"><b>Важно.</b> Линейка <b>без делений</b>. Она лишь проводит прямую через две данные точки. Циркуль рисует окружность по заданным центру и радиусу.</p>`);
html += makeCard('rule', '3 базовых операции', '27.2', `
<p>Любое построение состоит из последовательного применения 3 простых операций:</p>
<ol style="padding-left:22px;line-height:1.9">
<li><b>Через 2 точки провести прямую</b> (отрезок, луч) — это даёт линейка.</li>
<li><b>Из точки $O$ радиусом $r$ нарисовать окружность</b> — это даёт циркуль.</li>
<li><b>Отметить точку пересечения</b> двух построенных линий (прямых или окружностей).</li>
</ol>
<p>Всё! Из этих трёх «кирпичиков» строятся <b>все</b> задачи на построение.</p>`);
html += makeCard('algo', 'Структура решения задачи', '27.3', `
<p>Полное решение задачи на построение состоит из <b>4 этапов</b>:</p>
<ol style="padding-left:22px;line-height:1.9">
<li><b>Анализ.</b> Предполагаем, что задача решена. Делаем чертёж «как должно получиться» и ищем связи.</li>
<li><b>Построение.</b> Записываем шаги: «Шаг 1: ..., Шаг 2: ...»</li>
<li><b>Доказательство.</b> Объясняем, почему получившаяся фигура удовлетворяет условию.</li>
<li><b>Исследование.</b> Когда задача имеет решение и сколько решений (одно, несколько, ни одного).</li>
</ol>
<p>В школе мы обычно сосредотачиваемся на этапах <b>Построение</b> и <b>Доказательство</b>.</p>`);
html += makeCard('example', 'Простая задача', '27.4', `
<p><b>Задача.</b> Дан отрезок $AB$. Построить отрезок $CD$ длиной $AB$ на данном луче от его начала $C$.</p>
<div class="steps">
<div class="step"><div class="step-text">На луче отмечаем начало — точку $C$.</div></div>
<div class="step"><div class="step-text">Циркулем «измеряем» $AB$: ставим иглу в $A$, грифель в $B$.</div></div>
<div class="step"><div class="step-text">Не меняя раствор циркуля, ставим иглу в $C$ и проводим окружность.</div></div>
<div class="step"><div class="step-text">Точку пересечения окружности с лучом обозначаем $D$. Тогда $CD = AB$.</div></div>
</div>
<p><b>Доказательство.</b> $CD$ — радиус построенной окружности, $AB$ — тоже её радиус (мы взяли именно такой раствор). Значит $CD = AB$.</p>`);
html += '<div class="wg" id="p27-iv1">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Можно ли так делать?</div></div>'
+'<div class="wg-help">Допускается ли действие только циркулем и линейкой (без делений)?</div>'
+'<div class="score-display"><span>Задача <b id="p27-iv1-i">1</b> / 6</span><span>Очки: <b id="p27-iv1-s">0</b> / 6</span></div>'
+'<div id="p27-iv1-q" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.02rem;text-align:center;margin-bottom:10px"></div>'
+'<div style="display:flex;gap:8px;justify-content:center"><button class="btn primary" id="p27-iv1-y" style="background:#10b981;border-color:#10b981">Можно</button><button class="btn primary" id="p27-iv1-n" style="background:#dc2626;border-color:#dc2626">Нельзя</button></div>'
+'<div class="feedback" id="p27-iv1-fb"></div></div>';
html += '<div class="wg" id="p27-iv2">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Терминология</div></div>'
+'<div class="wg-help">Введи слово или число.</div>'
+trainerHTML('p27-iv2', 5, 'ответ')
+'</div>';
html += secNav(null, 'p28') + readButton('p27');
box.innerHTML = html; renderMath(box);
(function(){
const Q=[
{ e:'Провести прямую через 2 данные точки.', ok:true, why:'Базовая операция линейкой.' },
{ e:'Измерить длину отрезка линейкой в сантиметрах.', ok:false, why:'Линейка <b>без делений</b>.' },
{ e:'Нарисовать окружность с центром в $O$ радиусом $r = AB$.', ok:true, why:'Циркулем переносим длину.' },
{ e:'Отметить точку пересечения двух построенных линий.', ok:true, why:'Это базовая операция.' },
{ e:'Транспортиром отложить угол $37°$.', ok:false, why:'Транспортир не входит в инструменты.' },
{ e:'Из $A$ провести окружность радиуса $5$ см, измерив циркулем по линейке.', ok:false, why:'У линейки нет делений — мы не можем взять «5 см».' },
];
let i=0,score=0;
function show(){
if(i>=Q.length){ document.getElementById('p27-iv1-q').innerHTML='<b>Готово!</b> '+score+' / '+Q.length; if(score===Q.length){addXp(15,'p27-iv1');bumpProgress('p27',25);} else if(score>=4){addXp(8,'p27-iv1');bumpProgress('p27',12);} return; }
document.getElementById('p27-iv1-i').textContent=(i+1);
document.getElementById('p27-iv1-s').textContent=score;
document.getElementById('p27-iv1-q').innerHTML=Q[i].e;
renderMath(document.getElementById('p27-iv1-q'));
document.getElementById('p27-iv1-fb').style.display='none';
}
function ans(yes){
if(i>=Q.length) return;
const fb=document.getElementById('p27-iv1-fb');
if(yes===Q[i].ok){ score++; feedback(fb,true,'&#10003; Верно! '+Q[i].why); }
else feedback(fb,false,'&#10007; '+(Q[i].ok?'Можно: ':'Нельзя: ')+Q[i].why);
document.getElementById('p27-iv1-s').textContent=score;
i++; setTimeout(show,1500);
}
document.getElementById('p27-iv1-y').addEventListener('click',()=>ans(true));
document.getElementById('p27-iv1-n').addEventListener('click',()=>ans(false));
show();
})();
makeTrainer({
idPrefix:'p27-iv2',
parser:(v)=>v,
questions:[
{ q:'Сколько инструментов используется в задачах на построение?', a:(v)=>+v===2, show:'2 (циркуль + линейка)' },
{ q:'Какой инструмент проводит прямую через 2 точки?', a:(v)=>String(v).trim().toLowerCase().startsWith('линей'), show:'линейка' },
{ q:'Какой инструмент рисует окружность?', a:(v)=>String(v).trim().toLowerCase().startsWith('цирк'), show:'циркуль' },
{ q:'Имеются ли деления на линейке? «да» или «нет»', a:(v)=>String(v).trim().toLowerCase().startsWith('н'), show:'нет' },
{ q:'Сколько этапов в полном решении задачи?', a:(v)=>+v===4, show:'4 (Анализ, Построение, Доказательство, Исследование)' },
],
onComplete:(s,n)=>{ if(s===n){addXp(15,'p27-iv2');bumpProgress('p27',25);} else if(s>=3){addXp(8,'p27-iv2');bumpProgress('p27',12);} }
});
wireReadBtn('p27');
}
/* ============================================================
\xA7 28 — Построение треугольника по 3 сторонам
============================================================ */
function buildP28(){
const box = document.getElementById('p28-body');
const G = window.GEOM7;
let html = '';
let svgBuild='';
if(G){
/* Масштаб 25 px/ед; a=5 (BC), b=4 (AC), c=6 (AB)
A={30,160}, B={180,160}, C найден из системы */
const b=G.svgBox(280,200,{id:'p28-build',cell:20});
const A={x:30,y:160}, B={x:180,y:160};
/* AC=100, BC=125. Решаем:
Cx = (100²-125²+(180²-30²))/(2*(180-30)) = (10000-15625+(32400-900))/300 = (-5625+31500)/300 = 25875/300 ≈ 86.25
Cy = 160 - sqrt(100² - (86.25-30)²) = 160 - sqrt(10000 - 3164) = 160 - sqrt(6836) ≈ 160 - 82.68 = 77.3 */
const C={x:86,y:77};
svgBuild = b.open
/* Отрезок AB */
+ G.segment(A,B,{color:'#be185d',width:2.5,label:'c = AB',labelOffset:18,labelColor:'#be185d'})
/* Окружность из A радиуса b=100 */
+ G.circle(A,100,{color:'#7c3aed',width:1.5,dash:'4 3'})
+ '<text x="55" y="75" font-size="10" fill="#7c3aed" font-weight="700">r = b</text>'
/* Окружность из B радиуса a=125 */
+ G.circle(B,125,{color:'#0891b2',width:1.5,dash:'4 3'})
+ '<text x="200" y="55" font-size="10" fill="#0891b2" font-weight="700">r = a</text>'
/* Стороны от A и B к C */
+ G.segment(A,C,{color:'#7c3aed',width:2.5})
+ G.segment(B,C,{color:'#0891b2',width:2.5})
+ G.polygon([A,B,C],{color:'#be185d',width:0.01,fill:'rgba(219,39,119,.06)'})
+ G.point(A.x,A.y,'A',{color:'#1e293b',dx:-12,dy:14,fontSize:12})
+ G.point(B.x,B.y,'B',{color:'#1e293b',dx:8,dy:14,fontSize:12})
+ G.point(C.x,C.y,'C',{color:'#1e293b',dx:-4,dy:-8,fontSize:12})
+ b.close;
}
html += makeCard('theory', 'Задача', '28.1', `
<p><b>Дано:</b> три отрезка $a$, $b$, $c$.</p>
<p><b>Построить:</b> треугольник $ABC$, у которого $BC = a$, $AC = b$, $AB = c$.</p>
<p><b>Условие существования:</b> неравенство треугольника $|b-c| < a < b+c$ (и аналогично для $b$, $c$). Без него окружности не пересекутся.</p>`);
html += makeCard('algo', 'Построение', '28.2', `
<div class="steps">
<div class="step"><div class="step-text">На произвольной прямой откладываем отрезок $AB$ длиной $c$ (используя циркуль для переноса длины).</div></div>
<div class="step"><div class="step-text">Из точки $A$ как центра проводим <b>окружность радиуса $b$</b>.</div></div>
<div class="step"><div class="step-text">Из точки $B$ как центра проводим <b>окружность радиуса $a$</b>.</div></div>
<div class="step"><div class="step-text">Точку пересечения окружностей обозначаем $C$. Проводим отрезки $AC$ и $BC$.</div></div>
</div>
<div class="svg-host">`+svgBuild+`</div>
<p><b>Готово!</b> Треугольник $ABC$ построен.</p>`);
html += makeCard('rule', 'Доказательство', '28.3', `
<p>В построенном $\\triangle ABC$:</p>
<ul style="padding-left:22px;line-height:1.85">
<li>$AB = c$ — построено на шаге 1;</li>
<li>$AC = b$ — $C$ лежит на окружности с центром $A$ радиуса $b$;</li>
<li>$BC = a$ — $C$ лежит на окружности с центром $B$ радиуса $a$.</li>
</ul>
<p>Все три стороны соответствуют требуемым. ■</p>
<p style="background:var(--warn-bg);padding:9px 13px;border-radius:8px;border-left:4px solid var(--warn)"><b>Исследование.</b> Если $a + b > c$, $a + c > b$, $b + c > a$ — задача имеет ровно <b>1 решение</b> (с точностью до симметрии относительно $AB$). Если одно из неравенств нарушено — <b>решений нет</b>.</p>`);
html += makeCard('example', 'Когда построение невозможно', '28.4', `
<p><b>Задача.</b> Построить треугольник со сторонами $3, 4, 8$.</p>
<p><b>Проверка.</b> $3 + 4 = 7 < 8$ — неравенство треугольника <b>нарушено</b>.</p>
<p><b>Что произойдёт?</b> После построения $AB = 8$ окружности радиусов $3$ и $4$ <b>не пересекутся</b> — между ними останется зазор (см. §22). Построить треугольник <b>невозможно</b>.</p>`);
html += '<div class="wg" id="p28-iv1">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Можно ли построить?</div></div>'
+'<div class="wg-help">По 3 сторонам реши: можно ли построить треугольник.</div>'
+'<div class="score-display"><span>Задача <b id="p28-iv1-i">1</b> / 6</span><span>Очки: <b id="p28-iv1-s">0</b> / 6</span></div>'
+'<div id="p28-iv1-q" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.05rem;text-align:center;margin-bottom:10px"></div>'
+'<div style="display:flex;gap:8px;justify-content:center"><button class="btn primary" id="p28-iv1-y" style="background:#10b981;border-color:#10b981">Можно</button><button class="btn primary" id="p28-iv1-n" style="background:#dc2626;border-color:#dc2626">Нельзя</button></div>'
+'<div class="feedback" id="p28-iv1-fb"></div></div>';
html += '<div class="wg" id="p28-iv2">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Какой радиус циркуля?</div></div>'
+'<div class="wg-help">В построении $\\triangle$ со сторонами $a, b, c$.</div>'
+trainerHTML('p28-iv2', 5, 'число / слово')
+'</div>';
html += secNav('p27', 'p29') + readButton('p28');
box.innerHTML = html; renderMath(box);
(function(){
const Q=[
{ e:'$a = 5, b = 6, c = 7$', ok:true, why:'Все неравенства выполнены.' },
{ e:'$a = 3, b = 4, c = 8$', ok:false, why:'$3+4 < 8$.' },
{ e:'$a = 7, b = 7, c = 7$', ok:true, why:'Равносторонний.' },
{ e:'$a = 2, b = 5, c = 10$', ok:false, why:'$2+5 < 10$.' },
{ e:'$a = 4, b = 4, c = 7$', ok:true, why:'$4+4 > 7$ ✓.' },
{ e:'$a = 1, b = 1, c = 3$', ok:false, why:'$1+1 < 3$.' },
];
let i=0,score=0;
function show(){
if(i>=Q.length){ document.getElementById('p28-iv1-q').innerHTML='<b>Готово!</b> '+score+' / '+Q.length; if(score===Q.length){addXp(15,'p28-iv1');bumpProgress('p28',28);} else if(score>=4){addXp(8,'p28-iv1');bumpProgress('p28',14);} return; }
document.getElementById('p28-iv1-i').textContent=(i+1);
document.getElementById('p28-iv1-s').textContent=score;
document.getElementById('p28-iv1-q').innerHTML=Q[i].e;
renderMath(document.getElementById('p28-iv1-q'));
document.getElementById('p28-iv1-fb').style.display='none';
}
function ans(yes){
if(i>=Q.length) return;
const fb=document.getElementById('p28-iv1-fb');
if(yes===Q[i].ok){ score++; feedback(fb,true,'&#10003; Верно! '+Q[i].why); }
else feedback(fb,false,'&#10007; '+(Q[i].ok?'Можно: ':'Нельзя: ')+Q[i].why);
document.getElementById('p28-iv1-s').textContent=score;
i++; setTimeout(show,1400);
}
document.getElementById('p28-iv1-y').addEventListener('click',()=>ans(true));
document.getElementById('p28-iv1-n').addEventListener('click',()=>ans(false));
show();
})();
makeTrainer({
idPrefix:'p28-iv2',
parser:(v)=>v,
questions:[
{ q:'$\\triangle ABC$: $a=5$, $b=4$, $c=6$. Откладываем $AB = c$. Радиус 1-й окружности из $A$?', a:(v)=>+v===4, show:'4 ($= b$)' },
{ q:'Тот же $\\triangle$. Радиус 2-й окружности из $B$?', a:(v)=>+v===5, show:'5 ($= a$)' },
{ q:'Сколько окружностей нужно нарисовать?', a:(v)=>+v===2, show:'2' },
{ q:'Сколько точек пересечения у двух окружностей (если задача разрешима)?', a:(v)=>+v===2, show:'2 (симметричные)' },
{ q:'Сколько <b>неравных</b> $\\triangle$ можно получить?', a:(v)=>+v===1, show:'1 (симметричный — тот же)' },
],
onComplete:(s,n)=>{ if(s===n){addXp(15,'p28-iv2');bumpProgress('p28',25);} else if(s>=3){addXp(8,'p28-iv2');bumpProgress('p28',12);} }
});
wireReadBtn('p28');
}
/* ============================================================
\xA7 29 — Построение биссектрисы угла
============================================================ */
function buildP29(){
const box = document.getElementById('p29-body');
const G = window.GEOM7;
let html = '';
let svgBis='';
if(G){
const b=G.svgBox(300,200,{id:'p29-bis',cell:20});
const O={x:40,y:100};
const ang=30*Math.PI/180;
const len=220;
const Aend={x:O.x+len*Math.cos(-ang), y:O.y+len*Math.sin(-ang)};
const Bend={x:O.x+len*Math.cos(ang), y:O.y+len*Math.sin(ang)};
/* Шаг 1: окружность из O радиусом r1 — пересекает стороны в X и Y */
const r1=80;
const X={x:O.x+r1*Math.cos(-ang), y:O.y+r1*Math.sin(-ang)};
const Y={x:O.x+r1*Math.cos(ang), y:O.y+r1*Math.sin(ang)};
/* Шаг 2: из X и Y окружн. радиуса r2 — точка M на оси симметрии (биссектрисе) */
const r2=70;
const dy2 = r1*Math.sin(ang);
const dx2 = Math.sqrt(r2*r2 - dy2*dy2);
const Mx = X.x + dx2;
const M = {x:Mx, y:O.y};
svgBis = b.open
/* Стороны угла */
+ G.segment(O,Aend,{color:'#7c3aed',width:2.5})
+ G.segment(O,Bend,{color:'#7c3aed',width:2.5})
/* Шаг 1: окружность из O */
+ G.circle(O,r1,{color:'#0891b2',width:1.3,dash:'4 3'})
+ G.point(X.x,X.y,'X',{color:'#dc2626',dx:8,dy:-2,fontSize:12,r:3.5})
+ G.point(Y.x,Y.y,'Y',{color:'#dc2626',dx:8,dy:14,fontSize:12,r:3.5})
/* Шаги 2-3: окружности из X и Y */
+ G.circle(X,r2,{color:'#f59e0b',width:1.2,dash:'3 2'})
+ G.circle(Y,r2,{color:'#f59e0b',width:1.2,dash:'3 2'})
/* Шаг 4: точка M */
+ G.point(M.x,M.y,'M',{color:'#059669',dx:6,dy:-6,fontSize:13,r:4})
/* Шаг 5: луч OM */
+ G.segment(O,{x:O.x+(M.x-O.x)*1.5,y:O.y},{color:'#dc2626',width:2.5,dash:'8 3'})
+ '<text x="'+(O.x+90)+'" y="'+(O.y-6)+'" font-size="11" font-family="Unbounded,Inter,sans-serif" font-weight="700" fill="#dc2626">биссектриса</text>'
+ G.point(O.x,O.y,'O',{color:'#1e293b',dx:-14,dy:5,fontSize:13})
+ '<text x="'+(Aend.x-2)+'" y="'+(Aend.y-2)+'" font-size="12" font-family="Unbounded,Inter,sans-serif" font-weight="700" fill="#7c3aed">A</text>'
+ '<text x="'+(Bend.x-2)+'" y="'+(Bend.y+14)+'" font-size="12" font-family="Unbounded,Inter,sans-serif" font-weight="700" fill="#7c3aed">B</text>'
+ b.close;
}
html += makeCard('theory', 'Задача', '29.1', `
<p><b>Дано:</b> $\\angle AOB$.</p>
<p><b>Построить:</b> биссектрису этого угла — луч $OM$ такой, что $\\angle AOM = \\angle MOB$.</p>`);
html += makeCard('algo', 'Построение', '29.2', `
<div class="steps">
<div class="step"><div class="step-text">Из вершины $O$ проведём окружность <b>любого радиуса $r_1$</b>. Она пересекает стороны угла в точках $X$ и $Y$.</div></div>
<div class="step"><div class="step-text">Из $X$ и $Y$ как из центров проводим <b>две окружности равного радиуса $r_2$</b> (так, чтобы они пересекались).</div></div>
<div class="step"><div class="step-text">Их точку пересечения внутри угла обозначаем $M$.</div></div>
<div class="step"><div class="step-text">Проводим <b>луч $OM$</b>. Это и есть биссектриса.</div></div>
</div>
<div class="svg-host">`+svgBis+`</div>`);
html += makeCard('rule', 'Доказательство', '29.3', `
<p>Рассмотрим треугольники $\\triangle OXM$ и $\\triangle OYM$:</p>
<ul style="padding-left:22px;line-height:1.85">
<li>$OX = OY = r_1$ — обе точки лежат на окружности с центром $O$ радиуса $r_1$;</li>
<li>$XM = YM = r_2$ — точка $M$ лежит на обеих окружностях радиуса $r_2$;</li>
<li>$OM$ — общая сторона.</li>
</ul>
<p>По <b>3-му признаку (ССС)</b> $\\triangle OXM = \\triangle OYM$. Отсюда $\\angle XOM = \\angle YOM$, то есть $OM$ — биссектриса. ■</p>`);
html += '<div class="wg" id="p29-iv1">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Что делаем на каждом шаге?</div></div>'
+'<div class="wg-help">Порядок шагов в построении биссектрисы.</div>'
+trainerHTML('p29-iv1', 5, 'номер шага')
+'</div>';
html += '<div class="wg" id="p29-iv2">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Доказательство</div></div>'
+'<div class="wg-help">Почему $OM$ — биссектриса.</div>'
+trainerHTML('p29-iv2', 4, 'число / слово')
+'</div>';
html += secNav('p28', 'p30') + readButton('p29');
box.innerHTML = html; renderMath(box);
makeTrainer({
idPrefix:'p29-iv1',
questions:[
{ q:'Шаг $?$: провести окружность из $O$ — она пересекает стороны в $X, Y$.', a:1 },
{ q:'Шаг $?$: провести окружность из $X$ радиуса $r_2$.', a:2 },
{ q:'Шаг $?$: точка пересечения окружностей внутри угла — это $M$.', a:3 },
{ q:'Шаг $?$: провести луч $OM$.', a:4 },
{ q:'Сколько окружностей рисуется в полном построении?', a:3 },
],
onComplete:(s,n)=>{ if(s===n){addXp(15,'p29-iv1');bumpProgress('p29',28);} else if(s>=3){addXp(8,'p29-iv1');bumpProgress('p29',14);} }
});
makeTrainer({
idPrefix:'p29-iv2',
parser:(v)=>v,
questions:[
{ q:'По какому признаку равны $\\triangle OXM$ и $\\triangle OYM$? «ССС», «СУС» или «УСУ»', a:(v)=>String(v).trim().toUpperCase().startsWith('ССС'), show:'ССС' },
{ q:'Сколько окружностей равного радиуса $r_2$ нужно?', a:(v)=>+v===2, show:'2 (из $X$ и из $Y$)' },
{ q:'$OX = OY$. Они равны как ... окружности (введи слово).', a:(v)=>String(v).trim().toLowerCase().startsWith('радиус'), show:'радиусы' },
{ q:'Биссектриса делит угол на $?$ равных частей', a:(v)=>+v===2, show:'2' },
],
onComplete:(s,n)=>{ if(s===n){addXp(15,'p29-iv2');bumpProgress('p29',28);} else if(s>=2){addXp(8,'p29-iv2');bumpProgress('p29',14);} }
});
wireReadBtn('p29');
}
/* ============================================================
\xA7 30 — Середина и перпендикуляр
============================================================ */
function buildP30(){
const box = document.getElementById('p30-body');
const G = window.GEOM7;
let html = '';
let svgMid='';
if(G){
const b=G.svgBox(300,220,{id:'p30-mid',cell:20});
const A={x:50,y:110}, B={x:230,y:110};
/* Радиус больше половины AB. AB = 180, r = 120 */
const r=120;
const Mx=(A.x+B.x)/2;
const d=Math.sqrt(r*r - ((B.x-A.x)/2)*((B.x-A.x)/2));
const P1={x:Mx, y:A.y - d};
const P2={x:Mx, y:A.y + d};
const Mid={x:Mx, y:A.y};
svgMid = b.open
+ G.segment(A,B,{color:'#0891b2',width:2.5})
+ G.circle(A,r,{color:'#7c3aed',width:1.3,dash:'4 3'})
+ G.circle(B,r,{color:'#f59e0b',width:1.3,dash:'4 3'})
+ G.segment(P1,P2,{color:'#dc2626',width:2.5,dash:'8 3'})
+ G.rightAngleMark(Mid,B,P1,{color:'#dc2626',size:10})
+ G.point(P1.x,P1.y,'P₁',{color:'#dc2626',dx:6,dy:-4,fontSize:11,r:3.5})
+ G.point(P2.x,P2.y,'P₂',{color:'#dc2626',dx:6,dy:12,fontSize:11,r:3.5})
+ G.point(Mid.x,Mid.y,'M',{color:'#059669',dx:-8,dy:-8,fontSize:13,r:4})
+ G.point(A.x,A.y,'A',{color:'#1e293b',dx:-14,dy:14,fontSize:12})
+ G.point(B.x,B.y,'B',{color:'#1e293b',dx:8,dy:14,fontSize:12})
+ '<text x="'+Mx+'" y="22" text-anchor="middle" font-size="11" font-family="Unbounded,Inter,sans-serif" font-weight="700" fill="#dc2626">серединный перпендикуляр</text>'
+ b.close;
}
html += makeCard('theory', 'Две задачи в одной', '30.1', `
<p>Одно построение решает сразу три задачи:</p>
<ol style="padding-left:22px;line-height:1.9">
<li>Построить <b>середину $M$</b> отрезка $AB$.</li>
<li>Построить <b>серединный перпендикуляр</b> к отрезку $AB$.</li>
<li>Построить <b>перпендикуляр</b> к прямой через данную точку.</li>
</ol>
<p>Идея: серединный перпендикуляр = ось симметрии отрезка. Любая точка на нём равноудалена от концов.</p>`);
html += makeCard('algo', 'Построение серединного перпендикуляра (и середины)', '30.2', `
<div class="steps">
<div class="step"><div class="step-text">Из точки $A$ проводим окружность <b>радиуса $r$</b>, где $r > \\frac{1}{2}AB$ (например, $r = AB$).</div></div>
<div class="step"><div class="step-text">Из точки $B$ проводим окружность <b>того же радиуса $r$</b>.</div></div>
<div class="step"><div class="step-text">Окружности пересекаются в двух точках $P_1$ и $P_2$ (выше и ниже отрезка $AB$).</div></div>
<div class="step"><div class="step-text">Прямая $P_1 P_2$ — серединный перпендикуляр. Её точка пересечения с $AB$ — середина $M$.</div></div>
</div>
<div class="svg-host">`+svgMid+`</div>`);
html += makeCard('rule', 'Доказательство', '30.3', `
<p>Точки $P_1$ и $P_2$ лежат на обеих окружностях $\\Rightarrow$ $AP_1 = BP_1 = r$ и $AP_2 = BP_2 = r$.</p>
<p>Значит $P_1$ и $P_2$ — точки, <b>равноудалённые</b> от $A$ и $B$. По теореме о серединном перпендикуляре они лежат на нём (§14).</p>
<p>Через 2 точки проходит единственная прямая $\\Rightarrow$ $P_1 P_2$ — это и есть серединный перпендикуляр.</p>
<p>Его пересечение с $AB$ — точка $M$, середина отрезка ($AM = MB$). ■</p>`);
html += makeCard('example', 'Перпендикуляр через данную точку', '30.4', `
<p><b>Задача.</b> Дана прямая $a$ и точка $K$ <b>не на ней</b>. Построить перпендикуляр из $K$ к $a$.</p>
<div class="steps">
<div class="step"><div class="step-text">Из $K$ проводим окружность так, чтобы она пересекла $a$ в двух точках. Назовём их $A$ и $B$.</div></div>
<div class="step"><div class="step-text">$KA = KB$ (радиусы) $\\Rightarrow$ $K$ лежит на серединном перпендикуляре к $AB$.</div></div>
<div class="step"><div class="step-text">Строим серединный перпендикуляр к $AB$ (по алгоритму выше).</div></div>
<div class="step"><div class="step-text">Прямая $KM$ (где $M$ — середина $AB$) и есть искомый перпендикуляр из $K$ к $a$.</div></div>
</div>`);
html += '<div class="wg" id="p30-iv1">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Параметры построения</div></div>'
+'<div class="wg-help">Условие на радиус, число точек пересечения.</div>'
+trainerHTML('p30-iv1', 5, 'число / знак')
+'</div>';
html += '<div class="wg" id="p30-iv2">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Верно / Неверно</div></div>'
+'<div class="wg-help">Свойства построения.</div>'
+'<div class="score-display"><span>Задача <b id="p30-iv2-i">1</b> / 5</span><span>Очки: <b id="p30-iv2-s">0</b> / 5</span></div>'
+'<div id="p30-iv2-q" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.02rem;text-align:center;margin-bottom:10px"></div>'
+'<div style="display:flex;gap:8px;justify-content:center"><button class="btn primary" id="p30-iv2-y" style="background:#10b981;border-color:#10b981">Да</button><button class="btn primary" id="p30-iv2-n" style="background:#dc2626;border-color:#dc2626">Нет</button></div>'
+'<div class="feedback" id="p30-iv2-fb"></div></div>';
html += secNav('p29', 'p31') + readButton('p30');
box.innerHTML = html; renderMath(box);
makeTrainer({
idPrefix:'p30-iv1',
parser:(v)=>v,
questions:[
{ q:'$AB = 10$. Минимальный <b>целый</b> радиус $r$ для пересечения окружностей?', a:(v)=>+v===6, show:'6 ($r > 5$)' },
{ q:'$AB = 8$. Подходит ли радиус $r = 4$? «да»/«нет»', a:(v)=>String(v).trim().toLowerCase().startsWith('н'), show:'нет ($r$ должен быть $> 4$)' },
{ q:'Сколько точек пересечения у окружностей?', a:(v)=>+v===2, show:'2 ($P_1$ и $P_2$)' },
{ q:'$P_1$ и $P_2$ равноудалены от $A$ и $B$? «да»/«нет»', a:(v)=>String(v).trim().toLowerCase().startsWith('д'), show:'да' },
{ q:'Угол между серединным перпендикуляром и $AB$ равен $?$ градусов.', a:(v)=>+v===90, show:'90°' },
],
onComplete:(s,n)=>{ if(s===n){addXp(15,'p30-iv1');bumpProgress('p30',25);} else if(s>=3){addXp(8,'p30-iv1');bumpProgress('p30',12);} }
});
(function(){
const Q=[
{ e:'Радиусы окружностей в построении серединного $\\perp$ должны быть равны.', ok:true },
{ e:'Радиус каждой окружности должен быть меньше половины $AB$.', ok:false },
{ e:'Через 2 точки $P_1, P_2$ проходит ровно одна прямая.', ok:true },
{ e:'Серединный $\\perp$ проходит через середину $AB$.', ok:true },
{ e:'Серединный $\\perp$ параллелен отрезку $AB$.', ok:false },
];
let i=0,score=0;
function show(){
if(i>=Q.length){ document.getElementById('p30-iv2-q').innerHTML='<b>Готово!</b> '+score+' / '+Q.length; if(score===Q.length){addXp(12,'p30-iv2');bumpProgress('p30',22);} else if(score>=3){addXp(6,'p30-iv2');bumpProgress('p30',12);} return; }
document.getElementById('p30-iv2-i').textContent=(i+1);
document.getElementById('p30-iv2-s').textContent=score;
document.getElementById('p30-iv2-q').innerHTML=Q[i].e;
renderMath(document.getElementById('p30-iv2-q'));
document.getElementById('p30-iv2-fb').style.display='none';
}
function ans(yes){
if(i>=Q.length) return;
const fb=document.getElementById('p30-iv2-fb');
if(yes===Q[i].ok){ score++; feedback(fb,true,'&#10003; Верно!'); }
else feedback(fb,false,'&#10007; '+(Q[i].ok?'Верно.':'Неверно.'));
document.getElementById('p30-iv2-s').textContent=score;
i++; setTimeout(show,1300);
}
document.getElementById('p30-iv2-y').addEventListener('click',()=>ans(true));
document.getElementById('p30-iv2-n').addEventListener('click',()=>ans(false));
show();
})();
wireReadBtn('p30');
}
/* ============================================================
\xA7 31 — Метод геометрических мест точек
============================================================ */
function buildP31(){
const box = document.getElementById('p31-body');
const G = window.GEOM7;
let html = '';
let svgGMT='';
if(G){
/* Чистый рисунок: угол ∠AOB слева, биссектриса (ГМТ 1, красная)
и окружность (ГМТ 2, синяя) пересекаются в двух точках K₁, K₂. */
const b=G.svgBox(320,230,{id:'p31-gmt',cell:20});
const O={x:50,y:135};
const ang=20*Math.PI/180; /* половина раствора угла */
const len=220;
const Aend={x:O.x+len*Math.cos(-ang), y:O.y+len*Math.sin(-ang)};
const Bend={x:O.x+len*Math.cos(ang), y:O.y+len*Math.sin(ang)};
/* Окружность (ГМТ 2): центр Cc, радиус R.
Биссектриса — горизонталь y=O.y от O.
Пересечение: (x-Cc.x)² + (O.y-Cc.y)² = R² → x = Cc.x ± √(R² - dy²) */
const Cc={x:180,y:90}, R=55;
const dy = O.y - Cc.y;
const dx = Math.sqrt(R*R - dy*dy);
const K1={x:Cc.x-dx, y:O.y}, K2={x:Cc.x+dx, y:O.y};
/* Конец биссектрисы — заходит чуть за K2 */
const Bs={x:O.x+len,y:O.y};
svgGMT = b.open
/* Стороны угла */
+ G.segment(O,Aend,{color:'#7c3aed',width:2.5})
+ G.segment(O,Bend,{color:'#7c3aed',width:2.5})
+ G.point(O.x,O.y,'O',{color:'#1e293b',dx:-14,dy:5,fontSize:13})
+ '<text x="'+(Aend.x+3)+'" y="'+(Aend.y-1)+'" font-size="12" font-family="Unbounded,Inter,sans-serif" font-weight="700" fill="#7c3aed">A</text>'
+ '<text x="'+(Bend.x+3)+'" y="'+(Bend.y+12)+'" font-size="12" font-family="Unbounded,Inter,sans-serif" font-weight="700" fill="#7c3aed">B</text>'
/* Маленькие дуги показывают, что биссектриса делит угол пополам */
+ G.arc(O,18,-ang,0,{color:'#dc2626',width:1.4})
+ G.arc(O,18,0,ang,{color:'#dc2626',width:1.4})
/* ГМТ 1 — биссектриса (красная пунктирная) */
+ G.segment(O,Bs,{color:'#dc2626',width:2.2,dash:'7 4'})
/* ГМТ 2 — окружность (синяя пунктирная) */
+ G.circle(Cc,R,{color:'#0891b2',width:2,dash:'5 3'})
+ G.point(Cc.x,Cc.y,'C',{color:'#0891b2',dx:6,dy:-6,fontSize:11,r:3})
/* Точки пересечения K₁, K₂ — крупные зелёные с обводкой */
+ '<circle cx="'+K1.x+'" cy="'+K1.y+'" r="5.5" fill="#10b981" stroke="#fff" stroke-width="2"/>'
+ '<circle cx="'+K2.x+'" cy="'+K2.y+'" r="5.5" fill="#10b981" stroke="#fff" stroke-width="2"/>'
+ '<text x="'+(K1.x-5)+'" y="'+(K1.y+25)+'" text-anchor="middle" font-size="13" font-family="Unbounded,Inter,sans-serif" font-weight="800" fill="#047857">K₁</text>'
+ '<text x="'+(K2.x+5)+'" y="'+(K2.y+25)+'" text-anchor="middle" font-size="13" font-family="Unbounded,Inter,sans-serif" font-weight="800" fill="#047857">K₂</text>'
/* Подпись ГМТ 1 (плашка справа от биссектрисы, за K₂) */
+ '<rect x="'+(Bs.x-65)+'" y="'+(Bs.y-11)+'" width="62" height="20" rx="4" fill="#fee2e2" stroke="#dc2626" stroke-width="1"/>'
+ '<text x="'+(Bs.x-34)+'" y="'+(Bs.y+3)+'" text-anchor="middle" font-size="10" font-family="Unbounded,Inter,sans-serif" font-weight="800" fill="#dc2626">ГМТ 1: бис.</text>'
/* Подпись ГМТ 2 (плашка сверху над окружностью) */
+ '<rect x="'+(Cc.x-45)+'" y="'+(Cc.y-R-22)+'" width="90" height="20" rx="4" fill="#cffafe" stroke="#0891b2" stroke-width="1"/>'
+ '<text x="'+Cc.x+'" y="'+(Cc.y-R-8)+'" text-anchor="middle" font-size="10" font-family="Unbounded,Inter,sans-serif" font-weight="800" fill="#0891b2">ГМТ 2: окружн.</text>'
/* Итог снизу */
+ '<text x="160" y="218" text-anchor="middle" font-size="11" font-weight="800" fill="#047857">K₁, K₂ = ГМТ 1 ∩ ГМТ 2</text>'
+ b.close;
}
html += makeCard('theory', 'Идея метода', '31.1', `
<p><b>Метод геометрических мест точек</b> (метод ГМТ) — мощный способ решать задачи на построение, когда искомая точка обладает <b>двумя</b> свойствами.</p>
<p style="background:var(--sec-acc-soft);padding:10px 14px;border-radius:8px"><b>Алгоритм.</b></p>
<ol style="padding-left:22px;line-height:1.9">
<li>Найди <b>ГМТ 1</b> — множество всех точек, обладающих свойством 1.</li>
<li>Найди <b>ГМТ 2</b> — множество всех точек, обладающих свойством 2.</li>
<li>Искомая точка лежит на <b>пересечении</b> $\\text{ГМТ 1} \\cap \\text{ГМТ 2}$.</li>
</ol>
<div class="svg-host">`+svgGMT+`</div>`);
html += makeCard('rule', 'Знакомые ГМТ', '31.2', `
<p>Из предыдущих глав мы уже знаем 4 важных ГМТ:</p>
<ul style="padding-left:22px;line-height:1.85">
<li><b>Окружность</b> с центром $O$ радиуса $r$ — ГМТ точек, находящихся на расстоянии $r$ от $O$.</li>
<li><b>Серединный перпендикуляр</b> к $AB$ — ГМТ точек, равноудалённых от $A$ и $B$.</li>
<li><b>Биссектриса</b> $\\angle AOB$ — ГМТ точек внутри угла, равноудалённых от его сторон.</li>
<li><b>Прямая</b>, параллельная данной — ГМТ точек, находящихся от неё на заданном расстоянии (с одной стороны).</li>
</ul>`);
html += makeCard('example', 'Окружность через 3 точки', '31.3', `
<p><b>Задача.</b> Через 3 точки $A, B, C$, не лежащие на одной прямой, провести окружность.</p>
<p><b>Решение методом ГМТ.</b> Центр $O$ окружности должен быть:</p>
<ul style="padding-left:22px;line-height:1.85">
<li>равноудалён от $A$ и $B$ $\\Rightarrow$ $O \\in$ серединного $\\perp$ к $AB$ (<b>ГМТ 1</b>);</li>
<li>равноудалён от $B$ и $C$ $\\Rightarrow$ $O \\in$ серединного $\\perp$ к $BC$ (<b>ГМТ 2</b>).</li>
</ul>
<p>Точка пересечения этих 2 перпендикуляров — искомый центр $O$. Радиус $= OA = OB = OC$.</p>`);
html += makeCard('example', 'Точка, равноуд. от 2 прямых, на расст. $r$ от 3-й', '31.4', `
<p><b>Задача.</b> Даны пересекающиеся прямые $a$ и $b$. Найти точку $K$, равноудалённую от $a$ и $b$ и находящуюся на расстоянии $r$ от данной прямой $c$.</p>
<p><b>Решение.</b></p>
<ul style="padding-left:22px;line-height:1.9">
<li><b>ГМТ 1</b> (равноуд. от $a$ и $b$): <b>биссектрисы</b> углов между $a$ и $b$ — их $2$.</li>
<li><b>ГМТ 2</b> (на расст. $r$ от $c$): <b>2 прямые</b>, параллельные $c$ на расстоянии $r$.</li>
<li><b>Искомые точки</b> $K$ — пересечения биссектрис с этими параллельными прямыми. Всего до $2 \\times 2 = 4$ точек.</li>
</ul>`);
html += '<div class="wg" id="p31-iv1">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Узнай ГМТ</div></div>'
+'<div class="wg-help">Какая фигура — это ГМТ точек с указанным свойством?</div>'
+'<div class="score-display"><span>Задача <b id="p31-iv1-i">1</b> / 6</span><span>Очки: <b id="p31-iv1-s">0</b> / 6</span></div>'
+'<div id="p31-iv1-q" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.02rem;text-align:center;margin-bottom:10px"></div>'
+'<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:8px;max-width:380px;margin:0 auto"><button class="btn primary" id="p31-iv1-c">Окружность</button><button class="btn primary" id="p31-iv1-b">Биссектриса</button><button class="btn primary" id="p31-iv1-m">Серед. перп.</button><button class="btn primary" id="p31-iv1-p">Парал. прямая</button></div>'
+'<div class="feedback" id="p31-iv1-fb"></div></div>';
html += '<div class="wg" id="p31-iv2">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Сколько решений?</div></div>'
+'<div class="wg-help">Сколько точек на пересечении двух ГМТ?</div>'
+trainerHTML('p31-iv2', 5, 'число')
+'</div>';
html += secNav('p30', 'final5') + readButton('p31');
box.innerHTML = html; renderMath(box);
(function(){
const Q=[
{ e:'Точки на расстоянии $r$ от $O$.', ans:'c' },
{ e:'Точки, равноудалённые от концов отрезка $AB$.', ans:'m' },
{ e:'Точки внутри $\\angle AOB$, равноудалённые от его сторон.', ans:'b' },
{ e:'Точки на расстоянии $h$ от прямой $\\ell$ (с одной стороны).', ans:'p' },
{ e:'Точки, равноудалённые от точек $A$ и $B$.', ans:'m' },
{ e:'Точки, до которых от $O$ ровно $5$ см.', ans:'c' },
];
let i=0,score=0;
function show(){
if(i>=Q.length){ document.getElementById('p31-iv1-q').innerHTML='<b>Готово!</b> '+score+' / '+Q.length; if(score===Q.length){addXp(15,'p31-iv1');bumpProgress('p31',28);} else if(score>=4){addXp(8,'p31-iv1');bumpProgress('p31',14);} return; }
document.getElementById('p31-iv1-i').textContent=(i+1);
document.getElementById('p31-iv1-s').textContent=score;
document.getElementById('p31-iv1-q').innerHTML=Q[i].e;
renderMath(document.getElementById('p31-iv1-q'));
document.getElementById('p31-iv1-fb').style.display='none';
}
function ans(a){
if(i>=Q.length) return;
const fb=document.getElementById('p31-iv1-fb');
if(a===Q[i].ans){ score++; feedback(fb,true,'&#10003; Верно!'); }
else{ const lab={c:'окружность',b:'биссектриса',m:'серединный перпендикуляр',p:'парал. прямая'}; feedback(fb,false,'&#10007; Правильно: <b>'+lab[Q[i].ans]+'</b>'); }
document.getElementById('p31-iv1-s').textContent=score;
i++; setTimeout(show,1300);
}
document.getElementById('p31-iv1-c').addEventListener('click',()=>ans('c'));
document.getElementById('p31-iv1-b').addEventListener('click',()=>ans('b'));
document.getElementById('p31-iv1-m').addEventListener('click',()=>ans('m'));
document.getElementById('p31-iv1-p').addEventListener('click',()=>ans('p'));
show();
})();
makeTrainer({
idPrefix:'p31-iv2',
questions:[
{ q:'Две пересекающиеся прямые. Сколько точек их пересечения?', a:1 },
{ q:'Прямая <b>пересекает</b> окружность (секущая). Сколько точек?', a:2 },
{ q:'Прямая <b>касается</b> окружности. Сколько точек?', a:1 },
{ q:'Прямая <b>не пересекает</b> окружность. Сколько точек?', a:0 },
{ q:'Две окружности с разными центрами, пересекаются. Сколько точек?', a:2 },
],
onComplete:(s,n)=>{ if(s===n){addXp(15,'p31-iv2');bumpProgress('p31',28);} else if(s>=3){addXp(8,'p31-iv2');bumpProgress('p31',14);} }
});
wireReadBtn('p31');
}
/* ============================================================
FINAL 5 — 5 БОССОВ
============================================================ */
const BOSSES = [
{
n:1, title:'Босс \xA727 — Базовые построения', color:'#db2777',
steps:[
{ q:'Сколько инструментов используется в задачах на построение?', verify:(v)=>+v===2, hint:'Циркуль и линейка.' },
{ q:'Есть ли деления на линейке? «да»/«нет»', verify:(v)=>String(v).trim().toLowerCase().startsWith('н'), hint:'Нет — линейка без делений.' },
{ q:'Сколько этапов полного решения задачи на построение?', verify:(v)=>+v===4, hint:'Анализ, Построение, Доказ., Исслед.' },
{ q:'Что делает циркуль: «прямая», «окружность» или «угол»?', verify:(v)=>String(v).trim().toLowerCase().startsWith('окруж'), hint:'Окружность.' },
{ q:'Что делает линейка: «прямая», «окружность» или «угол»?', verify:(v)=>String(v).trim().toLowerCase().startsWith('прям'), hint:'Прямая.' },
]
},
{
n:2, title:'Босс \xA728 — Треугольник по 3 сторонам', color:'#c026d3',
steps:[
{ q:'$a=4, b=5, c=6$. Возможно ли построить? «да»/«нет»', verify:(v)=>String(v).trim().toLowerCase().startsWith('д'), hint:'Все неравенства выполнены.' },
{ q:'$a=2, b=3, c=10$. Возможно ли построить? «да»/«нет»', verify:(v)=>String(v).trim().toLowerCase().startsWith('н'), hint:'$2+3 < 10$.' },
{ q:'Сколько окружностей рисуется при построении $\\triangle$ по 3 сторонам?', verify:(v)=>+v===2, hint:'Из $A$ и из $B$.' },
{ q:'Сколько точек пересечения у этих окружностей (если задача разрешима)?', verify:(v)=>+v===2, hint:'2 симметричные.' },
{ q:'Сколько <b>неравных</b> треугольников получаем?', verify:(v)=>+v===1, hint:'1 — симметричный считаем тем же.' },
]
},
{
n:3, title:'Босс \xA729-30 — Биссектриса, середина, ⊥', color:'#7c3aed',
steps:[
{ q:'По какому признаку доказываем равенство $\\triangle OXM = \\triangle OYM$ в построении бис.? «ССС», «СУС», «УСУ»', verify:(v)=>String(v).trim().toUpperCase().startsWith('ССС'), hint:'ССС — три стороны.' },
{ q:'Сколько окружностей в построении серединного $\\perp$?', verify:(v)=>+v===2, hint:'Из $A$ и $B$.' },
{ q:'$AB = 12$. Минимальный целый радиус $r$ для серединного перп.?', verify:(v)=>+v===7, hint:'$r > 6$.' },
{ q:'Угол между серединным $\\perp$ и $AB$ равен $?$ градусов.', verify:(v)=>+v===90, hint:'Перпендикулярно.' },
{ q:'Биссектриса делит угол $80°$ на 2 угла по $?$ градусов.', verify:(v)=>+v===40, hint:'$80/2$.' },
]
},
{
n:4, title:'Босс \xA731 — Метод ГМТ', color:'#0891b2',
steps:[
{ q:'Точки на расст. $r$ от $O$ — это $?$ Введи слово.', verify:(v)=>String(v).trim().toLowerCase().startsWith('окруж'), hint:'Окружность.' },
{ q:'Точки, равноуд. от концов отрезка $AB$ — это $?$ Введи: «бис.» или «серед.перп.»', verify:(v)=>String(v).trim().toLowerCase().startsWith('серед'), hint:'Серединный $\\perp$.' },
{ q:'Точки внутри $\\angle$, равноуд. от его сторон — это $?$ Введи: «бис.» или «серед.перп.»', verify:(v)=>String(v).trim().toLowerCase().startsWith('бис'), hint:'Биссектриса.' },
{ q:'Окружность через 3 точки: её центр = пересеч. $?$ серединных $\\perp$. (число)', verify:(v)=>+v===2, hint:'К 2 сторонам $\\triangle$, 3-й автоматически.' },
{ q:'Две окружности касаются. Сколько точек пересечения?', verify:(v)=>+v===1, hint:'Касание — 1 точка.' },
]
},
{
n:5, title:'Финальный босс — Сборная по всей Геометрии 7', color:'#dc2626',
steps:[
{ q:'Сумма углов $\\triangle$ = $?$ градусов.', verify:(v)=>+v===180, hint:'$180°$.' },
{ q:'Сколько признаков равенства треугольников?', verify:(v)=>+v===3, hint:'СУС, УСУ, ССС.' },
{ q:'В прямоуг. $\\triangle$ катет напротив $30°$ равен $\\frac{c}{?}$ (число)', verify:(v)=>+v===2, hint:'Половина гипотенузы.' },
{ q:'$a \\parallel b$, секущая. $\\angle 1 = 60°$ — соотв. угол $\\angle 5 = ?$', verify:(v)=>+v===60, hint:'Равны.' },
{ q:'Сколько биссектрис в треугольнике?', verify:(v)=>+v===3, hint:'По одной в каждой вершине.' },
]
},
];
function buildFinal5(){
const box = document.getElementById('final5-body');
let html = '';
html += makeCard('theory', 'Что мы изучили', 'Итог', `
<p>Глава 5 — основы конструктивной геометрии:</p>
<ul style="padding-left:22px;line-height:1.85">
<li>познакомились с <b>циркулем и линейкой</b> как инструментами и узнали о 4 этапах решения;</li>
<li>научились строить <b>треугольник по 3 сторонам</b> и проверять неравенство треугольника;</li>
<li>освоили построение <b>биссектрисы угла</b> через 3 окружности (доказательство — ССС);</li>
<li>построили <b>середину отрезка и серединный перпендикуляр</b> двумя одинаковыми окружностями;</li>
<li>овладели <b>методом ГМТ</b> — искомая точка как пересечение двух геометрических мест.</li>
</ul>
<p style="background:var(--warn-bg);padding:10px 14px;border-radius:8px;border-left:4px solid var(--warn)"><b>Финальный босс</b> — сборная задача по <b>всей</b> Геометрии 7. После победы — финальная ачивка <b>«Геометрия 7 полностью пройдена!»</b>.</p>`);
html += '<div id="bosses-container"></div>';
html += '<div style="margin-top:22px;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">'
+'<div style="font-family:\'Unbounded\',sans-serif;font-weight:800;color:var(--pri2);font-size:1.1rem;margin-bottom:6px">Прогресс по боссам</div>'
+'<div id="boss-overall" style="font-size:.95rem;color:var(--text);margin-bottom:10px">0 / 5 боссов побеждено</div>'
+'<div style="height:14px;background:rgba(219,39,119,.12);border-radius:9px;overflow:hidden"><div id="boss-overall-fill" style="height:100%;width:0%;background:linear-gradient(90deg,#db2777,#f472b6);transition:width .5s"></div></div>'
+'</div>';
html += secNav('p31', null);
box.innerHTML = html; renderMath(box);
const cont = document.getElementById('bosses-container');
const BOSS_STATE = (function(){
try{ const s=localStorage.getItem('geometry7_ch5_bosses'); if(s) return JSON.parse(s); }catch(e){}
return BOSSES.map(()=>({stage:0,defeated:false}));
})();
function saveBosses(){ try{ localStorage.setItem('geometry7_ch5_bosses', JSON.stringify(BOSS_STATE)); }catch(e){} }
function refreshOverall(){
const won=BOSS_STATE.filter(b=>b.defeated).length;
const txt=document.getElementById('boss-overall'); if(txt) txt.textContent=won+' / '+BOSSES.length+' боссов побеждено';
const fill=document.getElementById('boss-overall-fill'); if(fill) fill.style.width=(won*100/BOSSES.length)+'%';
if(won>=BOSSES.length){ bumpProgress('final5',60); achievement('ch5_done','Глава 5 пройдена!'); }
}
cont.innerHTML = BOSSES.map((b,idx)=>{
return '<div class="boss-card" id="boss-card-'+idx+'" style="border-color:'+b.color+'">'
+'<div class="boss-head">'
+'<svg viewBox="0 0 24 24" fill="none" stroke="'+b.color+'" stroke-width="2.2" style="width:28px;height:28px"><polygon points="12,2 22,20 2,20"/></svg>'
+'<div class="boss-title" style="color:'+b.color+'">'+b.title+'</div>'
+'<div class="boss-stage" id="boss-'+idx+'-stage">Этап 1 / '+b.steps.length+'</div>'
+'</div>'
+'<div class="hp-boss" style="border-color:'+b.color+'66;background:'+b.color+'1a"><div class="hp-boss-fill" id="boss-'+idx+'-fill" style="width:0%;background:linear-gradient(90deg,'+b.color+',#f59e0b)"></div></div>'
+'<div class="boss-q" id="boss-'+idx+'-q" style="border-color:'+b.color+'"></div>'
+'<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">'
+'<input type="text" id="boss-'+idx+'-input" class="tinp" placeholder="Ответ" style="width:160px;text-align:center">'
+'<button class="btn primary" id="boss-'+idx+'-go" style="background:'+b.color+';border-color:'+b.color+'">Атака</button>'
+'<button class="btn" id="boss-'+idx+'-hint">Подсказка</button>'
+'<button class="btn" id="boss-'+idx+'-restart">↻</button>'
+'</div>'
+'<div class="feedback" id="boss-'+idx+'-fb"></div>'
+'</div>';
}).join('');
if(window.renderMathInElement) try{ renderMath(cont); }catch(e){}
BOSSES.forEach((b,idx)=>{
function show(){
const st=BOSS_STATE[idx];
const stageEl=document.getElementById('boss-'+idx+'-stage');
const fill=document.getElementById('boss-'+idx+'-fill');
const q=document.getElementById('boss-'+idx+'-q');
const fb=document.getElementById('boss-'+idx+'-fb');
if(st.defeated){
stageEl.textContent='✓ Побеждён'; fill.style.width='100%';
q.innerHTML='<b style="color:'+b.color+'">Босс повержен!</b>';
document.getElementById('boss-'+idx+'-go').disabled=true;
document.getElementById('boss-'+idx+'-go').style.opacity=.5;
return;
}
stageEl.textContent='Этап '+(st.stage+1)+' / '+b.steps.length;
fill.style.width=(st.stage*100/b.steps.length)+'%';
q.innerHTML=b.steps[st.stage].q;
document.getElementById('boss-'+idx+'-input').value='';
fb.style.display='none';
renderMath(q);
}
document.getElementById('boss-'+idx+'-go').addEventListener('click',()=>{
const st=BOSS_STATE[idx]; if(st.defeated) return;
const step=b.steps[st.stage];
const val=document.getElementById('boss-'+idx+'-input').value;
const fb=document.getElementById('boss-'+idx+'-fb');
if(!val.trim()){ feedback(fb,false,'&#10007; Введи ответ.'); return; }
if(step.verify(val)){
st.stage++;
if(st.stage>=b.steps.length){
st.defeated=true; saveBosses();
feedback(fb,true,'&#10003; Босс '+b.n+' побеждён! +20 XP');
addXp(20,'boss-'+b.n); bumpProgress('final5',18); refreshOverall();
setTimeout(show,1400);
}else{
saveBosses(); feedback(fb,true,'&#10003; Верно! +3 XP'); addXp(3,'boss-step'); setTimeout(show,1100);
}
}else{ feedback(fb,false,'&#10007; Промах.'); }
});
document.getElementById('boss-'+idx+'-hint').addEventListener('click',()=>{
const st=BOSS_STATE[idx]; if(st.defeated) return;
const fb=document.getElementById('boss-'+idx+'-fb');
fb.className='feedback ok';
fb.innerHTML='<span style="color:#92400e">\u{1F4A1} Подсказка:</span> '+b.steps[st.stage].hint;
fb.style.display='block';
fb.style.background='var(--warn-bg)'; fb.style.color='#92400e'; fb.style.borderLeftColor='var(--warn)';
renderMath(fb);
});
document.getElementById('boss-'+idx+'-restart').addEventListener('click',()=>{
BOSS_STATE[idx]={stage:0,defeated:false}; saveBosses();
document.getElementById('boss-'+idx+'-go').disabled=false;
document.getElementById('boss-'+idx+'-go').style.opacity=1;
show(); refreshOverall();
});
show();
});
refreshOverall();
}
</script>
</body>
</html>