Files
Learn_System/frontend/textbooks/physics_8_ch3.html
T
Maxim Dolgolyov 0c6618fb38 feat(phys8 ch3): Phase 5 Wave 1+2 — §32 источники + §33 тени + §34 отражение + §35 зеркало
§32 Источники света:
- 3 теории: естеств./искусств., тепловые/люминесц., точечные
- IV-1: 8 раундов «светит/отражает»
- IV-2: 6 раундов «тепловой/люминесцентный»
- IV-3: DnD 8 источников на 2 категории
- IV-4: 6 MCQ

§33 Скорость света и распространение:
- 3 теории: c, прямолинейность, тень/полутень
- IV-1: ГЛАВНЫЙ ВИЗУАЛ — динамическая тень/полутень: slider'ы
  размера источника (0=точечный → 40=протяжённый) и расстояния,
  рисуются зоны тени и полутени на экране
- IV-2: калькулятор времени пролёта света
- IV-3: DnD 8 утверждений правда/ложь
- IV-4: 5 числовых задач (Солнце, Луна, скорость vs звук)

§34 Отражение света:
- 3 теории: закон отражения, зеркальное/диффузное, примеры
- IV-1: динамическая визуализация через OPTICS.reflectRay,
  slider α 0-80°
- IV-2: 6 раундов «зеркало/диффузное»
- IV-3: DnD 8 поверхностей
- IV-4: 5 задач (включая поворот зеркала)

§35 Плоское зеркало:
- 3 теории: свойства изображения, построение, зеркальные надписи
- IV-1: построение мнимого изображения через OPTICS.mirrorPlane
  + OPTICS.lightObject, slider расстояния
- IV-2: 6 True/False
- IV-3: DnD свойств изображения
- IV-4: 5 задач (включая 2 зеркала под 90°)

§36-40 + финал — stub-заглушки, будут реализованы в Wave 3-4.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 08:52:26 +03:00

1294 lines
105 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>Физика 8 · Глава 3 · «Световые явления»</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"
onload="renderMathInElement(document.body,{delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false})"></script>
<script src="/js/api.js" defer></script>
<script src="/js/xp.js" defer></script>
<script src="/js/g3d.js" defer></script>
<script src="/js/phys.js" defer></script>
<script src="/js/optics.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:#ecfeff; --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:#7c3aed; --pri2:#5b21b6; --pri-soft:#ede9fe;
--acc:#a78bfa; --acc2:#7c3aed; --acc-soft:#ede9fe;
--ok:#10b981; --ok-bg:#d1fae5; --warn:#f59e0b; --warn-bg:#fef3c7;
--bad:#ef4444; --fail:#dc2626; --fail-bg:#fee2e2;
}
.dark{--bg:#0a0a0e; --card:#13120a; --card-soft:#18160a; --text:#fef9e7; --ink:#fef9e7; --muted:#a39070; --border:#2a2512}
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
html,body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.55;font-size:15px}
button,input,select,textarea{font-family:inherit;font-size:inherit}
button{cursor:pointer;border:0;background:transparent;color:inherit}
a{color:inherit;text-decoration:none}
.ic{width:16px;height:16px;display:inline-block;flex-shrink:0;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}
.hdr{position:relative;background:linear-gradient(110deg,#164e63 0%,#0891b2 55%,#67e8f9 100%);color:#fff;padding:46px 22px 30px;overflow:hidden;border-bottom:2px solid rgba(255,255,255,.2);min-height:130px}
.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 h2{font-family:'Unbounded',sans-serif;font-size:1.55rem;font-weight:800;color:var(--pri2);margin-bottom:10px;letter-spacing:-.01em}
.hero p{font-size:.95rem;color:var(--text);opacity:.88;margin-bottom:14px;max-width:640px}
.hero-row{display:flex;gap:14px;flex-wrap:wrap;align-items:center}
.btn-primary{padding:11px 22px;background:linear-gradient(135deg,var(--pri),var(--pri2));color:#fff;border-radius:11px;font-weight:700;font-size:.92rem;display:inline-flex;align-items:center;gap:8px;box-shadow:var(--sh2);transition:transform .15s,box-shadow .15s}
.btn-primary:hover{transform:translateY(-1px);box-shadow:0 8px 28px rgba(0,0,0,.18)}
.hero-progress{flex:1;min-width:200px;max-width:280px}
.hp-label{font-size:.74rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;display:block;margin-bottom:5px}
.hp-bar{height:8px;background:rgba(0,0,0,.12);border-radius:5px;overflow:hidden}
.hp-fill{height:100%;background:linear-gradient(90deg,var(--pri),var(--acc));border-radius:5px;width:0%;transition:width .6s cubic-bezier(.16,1,.3,1)}
.hp-text{font-size:.78rem;color:var(--muted);font-weight:700;margin-top:4px;display:block}
.hero-xp-badge{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:linear-gradient(135deg,var(--warn,#f59e0b),var(--pri));color:#fff;border-radius:99px;font-size:.82rem;font-weight:800;letter-spacing:.02em;box-shadow:0 4px 12px rgba(0,0,0,.18);font-family:'Unbounded',sans-serif}
.psel{margin-bottom:24px}
.psel-title{font-size:.72rem;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:.08em;margin-bottom:10px}
.psel-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:10px}
.psel-card{background:var(--card);border:1.5px solid var(--border);border-radius:13px;padding:14px;cursor:pointer;transition:transform .2s,box-shadow .2s,border-color .2s;text-align:left;position:relative}
.psel-card:hover{transform:translateY(-3px);box-shadow:var(--sh2);border-color:var(--pri)}
.psel-card.active{border-color:var(--pri);background:linear-gradient(135deg,var(--pri-soft),var(--card));box-shadow:var(--sh2)}
.psel-card.active::after{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,var(--pri),var(--acc));border-radius:13px 13px 0 0}
.psel-num{font-family:'Unbounded',sans-serif;font-size:.72rem;font-weight:800;color:var(--pri);text-transform:uppercase;letter-spacing:.08em;margin-bottom:5px}
.psel-name{font-size:.86rem;font-weight:700;color:var(--text);line-height:1.3;margin-bottom:8px}
.psel-prog{height:4px;background:rgba(0,0,0,.10);border-radius:3px;overflow:hidden}
.psel-prog-fill{height:100%;background:var(--pri);width:0%;transition:width .4s}
.psel-card.final{background:linear-gradient(135deg,var(--acc-soft),var(--pri-soft))}
.psel-card.final .psel-num{color:var(--warn)}
.sec[id="sec-p32"]{ --sec-acc:#0891b2; --sec-acc-d:#0e7490; --sec-acc-soft:#cffafe; }
.sec[id="sec-p33"]{ --sec-acc:#0891b2; --sec-acc-d:#0e7490; --sec-acc-soft:#cffafe; }
.sec[id="sec-p34"]{ --sec-acc:#0891b2; --sec-acc-d:#0e7490; --sec-acc-soft:#cffafe; }
.sec[id="sec-p35"]{ --sec-acc:#0891b2; --sec-acc-d:#0e7490; --sec-acc-soft:#cffafe; }
.sec[id="sec-p36"]{ --sec-acc:#0891b2; --sec-acc-d:#0e7490; --sec-acc-soft:#cffafe; }
.sec[id="sec-p37"]{ --sec-acc:#0891b2; --sec-acc-d:#0e7490; --sec-acc-soft:#cffafe; }
.sec[id="sec-p38"]{ --sec-acc:#0891b2; --sec-acc-d:#0e7490; --sec-acc-soft:#cffafe; }
.sec[id="sec-p39"]{ --sec-acc:#0891b2; --sec-acc-d:#0e7490; --sec-acc-soft:#cffafe; }
.sec[id="sec-p40"]{ --sec-acc:#0891b2; --sec-acc-d:#0e7490; --sec-acc-soft:#cffafe; }
.sec[id="sec-final3"]{ --sec-acc:#0891b2; --sec-acc-d:#0e7490; --sec-acc-soft:#cffafe; }
.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-header{margin-bottom:22px;padding-bottom:14px;border-bottom:2px solid var(--sec-acc-soft,var(--pri-soft));position:relative;z-index:1}
.sec-num{display:inline-block;padding:4px 10px;background:linear-gradient(135deg,var(--sec-acc,var(--pri)),var(--sec-acc-d,var(--pri2)));color:#fff;border-radius:7px;font-family:'Unbounded',sans-serif;font-size:.78rem;font-weight:800;letter-spacing:.04em;margin-bottom:8px}
.sec-h{font-family:'Unbounded',sans-serif;font-size:1.6rem;font-weight:800;color:var(--sec-acc-d,var(--pri2));letter-spacing:-.01em;line-height:1.25}
.card{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:18px 20px;margin-bottom:16px;box-shadow:0 1px 3px rgba(0,0,0,.04),0 8px 24px rgba(0,0,0,.04);position:relative;z-index:1;transition:transform .25s cubic-bezier(.16,1,.3,1),box-shadow .25s}
.card:hover{transform:translateY(-2px);box-shadow:0 4px 10px rgba(0,0,0,.06),0 16px 36px rgba(0,0,0,.08)}
.card-header{display:flex;align-items:center;gap:10px;margin-bottom:12px;padding-bottom:10px;border-bottom:1px dashed var(--border)}
.card-icon{width:32px;height:32px;border-radius:9px;display:flex;align-items:center;justify-content:center;flex-shrink:0;color:#fff}
.card-icon.repeat{background:#0ea5e9}.card-icon.theory{background:#8b5cf6}.card-icon.algo{background:#f59e0b}.card-icon.rule{background:#ec4899}.card-icon.example{background:#10b981}.card-icon.oral{background:#06b6d4}
.card-icon .ic{width:18px;height:18px}
.card-title{font-family:'Unbounded',sans-serif;font-size:.82rem;font-weight:800;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);flex:1}
.card-num{font-size:.74rem;font-weight:700;color:var(--muted);background:var(--sec-acc-soft,var(--pri-soft));padding:3px 7px;border-radius:5px}
.card-body{font-size:.94rem;line-height:1.65}
.card-body p{margin-bottom:8px}
.card-body p:last-child{margin-bottom:0}
.btn{padding:8px 16px;border-radius:8px;background:var(--card);color:var(--text);border:1.5px solid var(--border);font-weight:600;font-size:.88rem;transition:background .15s,border-color .15s,transform .1s}
.btn:hover{background:var(--sec-acc-soft,var(--pri-soft));border-color:var(--sec-acc,var(--pri))}
.btn:active{transform:scale(.96)}
.btn.primary{background:var(--sec-acc,var(--pri));color:#fff;border-color:var(--sec-acc,var(--pri))}
.btn.primary:hover{background:var(--sec-acc-d,var(--pri2));border-color:var(--sec-acc-d,var(--pri2))}
.feedback{padding:10px 14px;border-radius:9px;font-weight:600;font-size:.88rem;margin-top:8px;display:none}
.feedback.ok{display:block;background:var(--ok-bg);color:#065f46;border-left:4px solid var(--ok)}
.feedback.fail{display:block;background:var(--fail-bg);color:#7f1d1d;border-left:4px solid var(--fail)}
.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(--sec-acc-soft,var(--pri-soft))}
.search-row .sr-kind{font-size:.7rem;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:2px}
.search-row .sr-title{font-weight:700;font-size:.92rem;color:var(--text)}
.search-row .sr-desc{font-size:.8rem;color:var(--muted);margin-top:2px}
.search-empty{padding:20px;text-align:center;color:var(--muted);font-size:.88rem}
.search-foot{padding:8px 14px;border-top:1px solid var(--border);font-size:.74rem;color:var(--muted);display:flex;gap:14px}
.search-foot kbd{padding:2px 6px;background:var(--card);border:1px solid var(--border);border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:.72rem}
.sec{transition:opacity .25s}
</style>
</head>
<body>
<header class="hdr">
<div class="hdr-row">
<div>
<h1>Физика 8 · Глава 3</h1>
<div class="hdr-sub">Свет · отражение · преломление · линзы · глаз</div>
</div>
<div class="hdr-side">
<a href="/textbook/physics-8" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> К физике 8</a>
<button id="search-btn" class="hdr-btn"><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>Свет распространяется прямолинейно со скоростью $c = 3 \cdot 10^8$ м/с. Закон отражения и закон преломления (Снеллиуса) объясняют поведение пучков света. Линзы строят изображения; глаз — это оптическая система.</p>
<div class="hero-row">
<button class="btn-primary" onclick="goTo('p32')"><svg class="ic" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4" fill="currentColor" stroke="none"/></svg> Начать § 32</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-p32" class="sec"><div class="sec-header"><span class="sec-num">&sect; 32</span><h2 class="sec-h">Источники света</h2></div><div id="p32-body"></div></section>
<section id="sec-p33" class="sec"><div class="sec-header"><span class="sec-num">&sect; 33</span><h2 class="sec-h">Скорость света. Прямолинейное распространение света</h2></div><div id="p33-body"></div></section>
<section id="sec-p34" class="sec"><div class="sec-header"><span class="sec-num">&sect; 34</span><h2 class="sec-h">Отражение света</h2></div><div id="p34-body"></div></section>
<section id="sec-p35" class="sec"><div class="sec-header"><span class="sec-num">&sect; 35</span><h2 class="sec-h">Зеркала. Изображение в плоском зеркале</h2></div><div id="p35-body"></div></section>
<section id="sec-p36" class="sec"><div class="sec-header"><span class="sec-num">&sect; 36</span><h2 class="sec-h">Преломление света</h2></div><div id="p36-body"></div></section>
<section id="sec-p37" class="sec"><div class="sec-header"><span class="sec-num">&sect; 37</span><h2 class="sec-h">Линзы. Оптическая сила линзы</h2></div><div id="p37-body"></div></section>
<section id="sec-p38" class="sec"><div class="sec-header"><span class="sec-num">&sect; 38</span><h2 class="sec-h">Построение изображений в тонких линзах</h2></div><div id="p38-body"></div></section>
<section id="sec-p39" class="sec"><div class="sec-header"><span class="sec-num">&sect; 39</span><h2 class="sec-h">Глаз как оптическая система</h2></div><div id="p39-body"></div></section>
<section id="sec-p40" class="sec"><div class="sec-header"><span class="sec-num">&sect; 40</span><h2 class="sec-h">Дефекты зрения. Очки</h2></div><div id="p40-body"></div></section>
<section id="sec-final3" class="sec"><div class="sec-header"><span class="sec-num">&#9733;</span><h2 class="sec-h">Финал главы</h2></div><div id="final3-body"></div></section>
</div>
<aside class="col-side" id="col-side"><div id="sidebar-content"></div></aside>
<div class="col-side-backdrop" id="col-side-backdrop"></div>
</main>
<footer class="foot">Интерактивный учебник «Физика 8» · Глава 3 · «Световые явления» · 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:'p32', progress:{}, achievements:new Map(), xp:0, level:1 };
const TOTAL_PARAS = 10;
const _TB_SLUG = 'physics-8-ch3';
const LS_PREFIX = 'physics8_ch3';
const LS_XP = 'physics8_xp';
const PARAS = [
{ id:'p32', num:'\u00a7 32', name:'Источники света', sub:'Тепловые, люминесцентные' },
{ id:'p33', num:'\u00a7 33', name:'Скорость света. Прямолинейное распространение света', sub:'$c = 3 \\cdot 10^8$ м/с' },
{ id:'p34', num:'\u00a7 34', name:'Отражение света', sub:'$\\alpha = \\beta$' },
{ id:'p35', num:'\u00a7 35', name:'Зеркала. Изображение в плоском зеркале', sub:'Мнимое, симметричное' },
{ id:'p36', num:'\u00a7 36', name:'Преломление света', sub:'$\\sin\\alpha/\\sin\\beta = n$' },
{ id:'p37', num:'\u00a7 37', name:'Линзы. Оптическая сила линзы', sub:'$D = 1/F$' },
{ id:'p38', num:'\u00a7 38', name:'Построение изображений в тонких линзах', sub:'3 «золотых» луча' },
{ id:'p39', num:'\u00a7 39', name:'Глаз как оптическая система', sub:'Аккомодация, $\\geq 25$ см' },
{ id:'p40', num:'\u00a7 40', name:'Дефекты зрения. Очки', sub:'Близо- и дальнозоркость' },
{ id:'final3', num:'\u2605', name:'Финал главы', sub:'Итоги · 7 боссов', final:true }
];
PARAS.forEach(p => { STATE.progress[p.id] = 0; });
const ACH_LABELS = {
start:"Начало главы 3!",
p32_done:"Источники света освоен!",
p33_done:"Скорость света. Прямолинейное распространение света освоен!",
p34_done:"Отражение света освоен!",
p35_done:"Зеркала. Изображение в плоском зеркале освоен!",
p36_done:"Преломление света освоен!",
p37_done:"Линзы. Оптическая сила линзы освоен!",
p38_done:"Построение изображений в тонких линзах освоен!",
p39_done:"Глаз как оптическая система освоен!",
p40_done:"Дефекты зрения. Очки освоен!",
ch3_done:"Глава 3 пройдена!",
light_master:"Мастер света — все боссы главы 3 повержены!"
};
const SIDEBARS = {
p32:{title:"Шпаргалка § 32",rows:[["Источники","естественные / искусствен."],["Тепловые","Солнце, лампа, костёр"],["Люминесцентные","экран, светодиод"],["Точечный","размер $\\ll$ расстояния"]]},
p33:{title:"Шпаргалка § 33",rows:[["$c$","$3 \\cdot 10^8$ м/с"],["В вакууме","максимальна"],["Прямолинейно","в однородн. среде"],["Тень","полное отсутствие света"],["Полутень","точечн. источник $\\to$ тень; протяжённый $\\to$ + полутень"]]},
p34:{title:"Шпаргалка § 34",rows:[["Закон","$\\alpha = \\beta$"],["От нормали","углы измеряют"],["Диффузное","шероховатая поверхность"],["Зеркальное","гладкая, отражает в одном направлении"]]},
p35:{title:"Шпаргалка § 35",rows:[["Изображение","мнимое, прямое, равное"],["Симметрия","относит. плоскости зеркала"],["Расстояние","предмет $=$ изобр. от зеркала"],["Размер","совпадает"]]},
p36:{title:"Шпаргалка § 36",rows:[["Закон Снеллиуса","$\\sin\\alpha/\\sin\\beta = n$"],["Из воздуха в воду","$\\alpha > \\beta$"],["Из воды в воздух","$\\alpha < \\beta$"],["$n$ воды","$1{,}33$"],["$n$ стекла","$1{,}5$"]]},
p37:{title:"Шпаргалка § 37",rows:[["Собирающая","выпукл., $F > 0$"],["Рассеивающая","вогн., $F < 0$"],["Оптическая сила","$D = 1/F$"],["[D]","дптр $=$ 1/м"],["Очки $+1$","$F = 1$ м"]]},
p38:{title:"Шпаргалка § 38",rows:[["Формула","$1/F = 1/d + 1/f$"],["3 «золотых» луча","через центр, парал. оси, через $F$"],["$d > 2F$","умен., перевёрн., действ."],["$d < F$","увел., прямое, мнимое (как лупа)"]]},
p39:{title:"Шпаргалка § 39",rows:[["Хрусталик","биол. линза"],["Аккомодация","изменение $F$ хрусталика"],["Сетчатка","экран"],["Расст. наилуч. зрения","25 см"]]},
p40:{title:"Шпаргалка § 40",rows:[["Близоруков.","изобр. перед сетч., $D &lt; 0$ (рассеив.)"],["Дальнозоркость","изобр. за сетч., $D &gt; 0$ (собир.)"]]},
final3:{title:"Финал главы 3",rows:[["§§32-40","свет"],["Награда","+50 XP + «Мастер света»"]]}
};
const TIPS=[
{sec:'p32',html:"Свет излучают <b>источники</b> — тепловые (Солнце, лампа накаливания) или люминесцентные (светодиоды, экраны). Источник, размер которого много меньше расстояния, называют <b>точечным</b>."},
{sec:'p33',html:"В вакууме свет летит со скоростью $c = 3 \\cdot 10^8$ м/с — это рекорд природы. От Солнца до Земли (150 млн км) свет идёт ~8 минут. В однородной среде свет распространяется <b>прямолинейно</b> — отсюда тени."},
{sec:'p34',html:"Закон отражения: угол падения $=$ угол отражения, оба от нормали. Гладкая поверхность даёт <b>зеркальное</b> отражение, шероховатая — <b>диффузное</b>. Луна светит отражённым светом Солнца — это диффузное отражение от её поверхности."},
{sec:'p35',html:"Зеркало даёт <b>мнимое</b> изображение: оно «за» зеркалом, на том же расстоянии, что и предмет, и тех же размеров. Симметричное относительно плоскости зеркала."},
{sec:'p36',html:"При переходе из одной среды в другую луч меняет направление — это <b>преломление</b>. $\\sin\\alpha/\\sin\\beta = n$. Из воздуха в воду $n = 1{,}33$ — угол $\\beta$ меньше угла $\\alpha$."},
{sec:'p37',html:"Линза — прозрачное тело, ограниченное двумя сферическими поверхностями. <b>Собирающая</b> (двусторонне выпуклая) фокусирует параллельные лучи в точку $F$. <b>Оптическая сила</b> $D = 1/F$, [D] = дптр. У очков $+2$ дптр $F = 0{,}5$ м."},
{sec:'p38',html:"Три «золотых» луча: 1) через центр линзы — без преломления; 2) параллельно оси — после линзы через $F$; 3) через ближний $F$ — после линзы параллельно оси. Точка их пересечения — изображение."},
{sec:'p39',html:"Глаз — оптическая система. Свет проходит через роговицу и <b>хрусталик</b> (биологическая собирающая линза) и фокусируется на <b>сетчатке</b>. При изменении расстояния мышцы меняют форму хрусталика — это <b>аккомодация</b>."},
{sec:'p40',html:"<b>Близорукость</b>: изображение фокусируется перед сетчаткой. Лечится <b>рассеивающими</b> линзами ($D &lt; 0$). <b>Дальнозоркость</b>: фокус за сетчаткой. Лечится <b>собирающими</b> линзами ($D &gt; 0$)."},
{sec:'final3',html:"Финал — 7 боссов по 9 параграфам оптики: отражение, преломление, линзы, очки. +50 XP и ачивка «Мастер света»."}
];
const BUILDERS = {
p32: ()=>{ build_p32(); },
p33: ()=>{ build_p33(); },
p34: ()=>{ build_p34(); },
p35: ()=>{ build_p35(); },
p36: ()=>{ build_p36(); },
p37: ()=>{ build_p37(); },
p38: ()=>{ build_p38(); },
p39: ()=>{ build_p39(); },
p40: ()=>{ build_p40(); },
final3: ()=>{ build_final3(); }
};
function calcLevel(xp){ return Math.floor(Math.sqrt((xp||0)/100))+1; }
function _xpForLevel(lv){ return (lv-1)*(lv-1)*100; }
function loadProgress(){
try{
const s=localStorage.getItem(LS_PREFIX+'_progress'); if(s) Object.assign(STATE.progress, JSON.parse(s));
const a=localStorage.getItem(LS_PREFIX+'_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(LS_XP)||0); STATE.level=calcLevel(STATE.xp);
}catch(e){}
}
function saveProgress(){
try{
localStorage.setItem(LS_PREFIX+'_progress', JSON.stringify(STATE.progress));
localStorage.setItem(LS_PREFIX+'_achievements', JSON.stringify(Object.fromEntries(STATE.achievements)));
localStorage.setItem(LS_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, LS_PREFIX+'-'+(src||'misc'));
if(STATE.level>prev){
const pop=document.getElementById('ach-popup');
if(pop){ document.getElementById('ach-text').textContent='Уровень '+STATE.level+'!'; pop.classList.add('show'); setTimeout(()=>pop.classList.remove('show'),2600); }
}
}
function refreshProgressUI(){
const total=Math.round(Object.values(STATE.progress).reduce((a,b)=>a+b,0)/TOTAL_PARAS);
const f=document.getElementById('hero-hp-fill'); if(f) f.style.width=total+'%';
const t=document.getElementById('hero-hp-text'); if(t) t.textContent=total+'% пройдено';
document.querySelectorAll('[data-prog-card]').forEach(el=>{ const k=el.dataset.progCard; const fl=el.querySelector('.psel-prog-fill'); if(fl) fl.style.width=(STATE.progress[k]||0)+'%'; });
const xpBadge=document.getElementById('hero-xp-badge');
if(xpBadge){ xpBadge.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:13px;height:13px"><polygon points="12 2 22 20 2 20"/></svg> Ур. '+STATE.level+' \xb7 '+(STATE.xp||0)+' XP'; }
if(STATE.current && document.getElementById('sidebar-content')){ try{ buildSidebar(STATE.current); }catch(e){} }
}
function achievement(id,text){
if(STATE.achievements.has(id)) return;
STATE.achievements.set(id, text||ACH_LABELS[id]||id); saveProgress();
const pop=document.getElementById('ach-popup');
if(pop){ document.getElementById('ach-text').textContent=text||ACH_LABELS[id]||id; pop.classList.add('show'); setTimeout(()=>pop.classList.remove('show'),3300); }
addXp(20,'ach-'+id);
}
function buildParaSelector(){
const g=document.getElementById('psel-grid'); g.innerHTML='';
PARAS.forEach(p=>{
const card=document.createElement('div');
card.className='psel-card'+(p.final?' final':'');
card.dataset.id=p.id; card.dataset.progCard=p.id;
card.innerHTML='<div class="psel-num">'+p.num+'</div><div class="psel-name">'+p.name+'</div><div class="psel-prog"><div class="psel-prog-fill"></div></div>';
card.addEventListener('click', ()=>goTo(p.id));
g.appendChild(card);
});
}
const BUILT=new Set();
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);
}
function buildSidebar(id){
const box=document.getElementById('sidebar-content');
const sb=SIDEBARS[id]||SIDEBARS[PARAS[0].id];
let html='';
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><span class="xp-level">Ур. '+STATE.level+'</span></div><div class="xp-bar"><div class="xp-fill" style="width:'+xpPct+'%"></div></div><div class="xp-nums"><span>'+STATE.xp+' XP</span><span>'+xpNext+' XP</span></div></div>';
html+='<div class="sidecard"><h4>'+sb.title+'</h4>';
sb.rows.forEach(([k,v])=>{ html+='<div class="sidecard-row"><b>'+k+'</b>'+(v?' \u2014 '+v:'')+'</div>'; });
html+='</div>';
const tip=TIPS.find(t=>t.sec===id)||TIPS[0];
if(tip){
html+='<div class="sidecard" style="background:linear-gradient(135deg,var(--warn-bg,#fef3c7),var(--pri-soft));border-color:var(--warn,#f59e0b)"><h4 style="color:#92400e;display:flex;align-items:center;gap:6px"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px"><polygon points="12,2 22,20 2,20"/></svg>Подсказка</h4><div class="sidecard-row" style="margin-bottom:0;font-size:.84rem;line-height:1.55">'+tip.html+'</div></div>';
}
if(STATE.achievements.size>0){
html+='<div class="sidecard"><h4>Достижения <span style="color:var(--warn);float:right">'+STATE.achievements.size+'</span></h4>';
[...STATE.achievements.values()].slice(-4).forEach(text=>{ html+='<div class="sidecard-row" style="font-size:.78rem;color:var(--ok)">&#10003; '+text+'</div>'; });
html+='</div>';
}
box.innerHTML=html;
if(window.renderMathInElement) try{ renderMath(box); }catch(e){}
}
function initTheme(){
const t=localStorage.getItem(LS_PREFIX+'_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(LS_PREFIX+'_theme', dark?'dark':'light');
document.getElementById('theme-lab').textContent=dark?'Светлая':'Тёмная';
});
}
function renderMath(root){ if(window.renderMathInElement){ try{ renderMathInElement(root, {delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false}); }catch(e){} } }
function feedback(elm, ok, text){ if(!elm) return; elm.className='feedback '+(ok?'ok':'fail'); elm.innerHTML=text||(ok?'&#10003; Верно!':'&#10007; Неверно'); elm.style.display='block'; try{renderMath(elm);}catch(e){} }
function fmt(n){ if(!isFinite(n)) return '?'; if(Number.isInteger(n)) return String(n); return Math.abs(n-Math.round(n))<1e-9?String(Math.round(n)):(+n.toFixed(6)).toString(); }
function ipow(base, exp){ let r=1; for(let i=0;i<Math.abs(exp);i++) r*=base; return exp<0 ? 1/r : r; }
function gcd(a,b){ a=Math.abs(a|0); b=Math.abs(b|0); while(b){ const t=b; b=a%b; a=t; } return a||1; }
function makeCard(kind, title, num, body){
const labels = {repeat:'Повторение',theory:'Теория',algo:'Алгоритм',rule:'Правило',example:'Пример',oral:'Устно'};
return '<div class="card"><div class="card-header"><div class="card-icon '+kind+'">'+ICONS[kind]+'</div><div class="card-title">'+(labels[kind]||'')+(title&&title!==labels[kind]?' \xb7 '+title:'')+'</div>'+(num?'<div class="card-num">'+num+'</div>':'')+'</div><div class="card-body">'+body+'</div></div>';
}
/* === SVG-хелперы === */
function axes2D(W, H, pad, xmin, xmax, ymin, ymax){
const ux = (W - 2*pad) / (xmax - xmin);
const uy = (H - 2*pad) / (ymax - ymin);
const toX = v => pad + (v - xmin) * ux;
const toY = v => H - pad - (v - ymin) * uy;
let g = '';
g += '<g stroke="#e5e7eb" stroke-width="1">';
for (let x = Math.ceil(xmin); x <= xmax; x++){
g += '<line x1="'+toX(x)+'" y1="'+pad+'" x2="'+toX(x)+'" y2="'+(H-pad)+'"/>';
}
for (let y = Math.ceil(ymin); y <= ymax; y++){
g += '<line x1="'+pad+'" y1="'+toY(y)+'" x2="'+(W-pad)+'" y2="'+toY(y)+'"/>';
}
g += '</g>';
const y0 = toY(0), x0 = toX(0);
g += '<line x1="'+pad+'" y1="'+y0+'" x2="'+(W-pad)+'" y2="'+y0+'" stroke="#0f172a" stroke-width="1.5"/>';
g += '<line x1="'+x0+'" y1="'+pad+'" x2="'+x0+'" y2="'+(H-pad)+'" stroke="#0f172a" stroke-width="1.5"/>';
g += '<text x="'+(W-pad+2)+'" y="'+(y0-4)+'" font-size="11" fill="#0f172a">x</text>';
g += '<text x="'+(x0+4)+'" y="'+(pad-2)+'" font-size="11" fill="#0f172a">y</text>';
g += '<g font-size="10" fill="#64748b">';
for (let x = Math.ceil(xmin); x <= xmax; x++){
if (x !== 0) g += '<text x="'+(toX(x)-3)+'" y="'+(y0+12)+'">'+x+'</text>';
}
for (let y = Math.ceil(ymin); y <= ymax; y++){
if (y !== 0) g += '<text x="'+(x0+4)+'" y="'+(toY(y)+3)+'">'+y+'</text>';
}
g += '<text x="'+(x0+4)+'" y="'+(y0+12)+'">0</text>';
g += '</g>';
return { content: g, toX, toY, ux, uy };
}
function plotFunc(f, xmin, xmax, toX, toY, color, N){
N = N || 200;
let d = '';
let prevValid = false;
for (let i = 0; i <= N; i++){
const x = xmin + (xmax - xmin) * i / N;
let y;
try { y = f(x); } catch(e){ y = NaN; }
if (!isFinite(y) || isNaN(y) || y < -1e4 || y > 1e4){ prevValid = false; continue; }
d += (prevValid ? ' L' : ' M') + toX(x).toFixed(2) + ',' + toY(y).toFixed(2);
prevValid = true;
}
return '<path d="'+d+'" stroke="'+color+'" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>';
}
function pointWithDrop(x, fx, toX, toY, color, label){
const px = toX(x), py = toY(fx);
let s = '';
s += '<line x1="'+px+'" y1="'+py+'" x2="'+px+'" y2="'+toY(0)+'" stroke="'+color+'" stroke-width="1.2" stroke-dasharray="3 3" opacity=".7"/>';
s += '<line x1="'+px+'" y1="'+py+'" x2="'+toX(0)+'" y2="'+py+'" stroke="'+color+'" stroke-width="1.2" stroke-dasharray="3 3" opacity=".7"/>';
s += '<circle cx="'+px+'" cy="'+py+'" r="4.5" fill="'+color+'" stroke="#fff" stroke-width="2"/>';
if (label){
s += '<text x="'+(px+8)+'" y="'+(py-8)+'" font-family="Inter,sans-serif" font-size="12" font-weight="700" fill="'+color+'">'+label+'</text>';
}
return s;
}
function asymptote(orientation, value, toX, toY, xmin, xmax, ymin, ymax, color){
color = color || '#94a3b8';
if (orientation === 'h'){
const y = toY(value);
return '<line x1="'+toX(xmin)+'" y1="'+y+'" x2="'+toX(xmax)+'" y2="'+y+'" stroke="'+color+'" stroke-width="1.3" stroke-dasharray="6 4"/>';
} else {
const x = toX(value);
return '<line x1="'+x+'" y1="'+toY(ymin)+'" x2="'+x+'" y2="'+toY(ymax)+'" stroke="'+color+'" stroke-width="1.3" stroke-dasharray="6 4"/>';
}
}
function snapToValue(value, snapPoints, tolerance){
tolerance = tolerance || 0.1;
for (const sp of snapPoints){
if (Math.abs(value - sp) < tolerance) return sp;
}
return value;
}
function rightAngleMark(V, uIn, wIn, s){
s = s || 9;
const p1 = {x: V.x + s*uIn.x, y: V.y + s*uIn.y};
const c = {x: p1.x + s*wIn.x, y: p1.y + s*wIn.y};
const p2 = {x: V.x + s*wIn.x, y: V.y + s*wIn.y};
return p1.x+','+p1.y+' '+c.x+','+c.y+' '+p2.x+','+p2.y;
}
function angleArcAuto(V, uA, uB, R){
const sA = {x: V.x + R*uA.x, y: V.y + R*uA.y};
const eB = {x: V.x + R*uB.x, y: V.y + R*uB.y};
const cross = uA.x*uB.y - uA.y*uB.x;
const sweep = cross > 0 ? 1 : 0;
return 'M'+sA.x+','+sA.y+' A'+R+','+R+' 0 0,'+sweep+' '+eB.x+','+eB.y;
}
function unitVec(p1, p2){
const dx = p2.x - p1.x, dy = p2.y - p1.y;
const len = Math.sqrt(dx*dx + dy*dy) || 1;
return {x: dx/len, y: dy/len};
}
function deg2rad(d){ return d * Math.PI / 180; }
const ICONS = {
repeat:'<svg class="ic" viewBox="0 0 24 24"><polyline points="9 11 12 14 22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>',
theory:'<svg class="ic" viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>',
algo:'<svg class="ic" viewBox="0 0 24 24"><polyline points="17 11 21 7 17 3"/><line x1="21" y1="7" x2="9" y2="7"/><polyline points="7 13 3 17 7 21"/><line x1="3" y1="17" x2="15" y2="17"/></svg>',
rule:'<svg class="ic" viewBox="0 0 24 24"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></svg>',
example:'<svg class="ic" viewBox="0 0 24 24"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M12 2a7 7 0 0 0-4 13c1 1 2 2 2 4h4c0-2 1-3 2-4a7 7 0 0 0-4-13z"/></svg>',
oral:'<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>'
};
function secNavFor(curId){
const idx = PARAS.findIndex(p => p.id === curId);
const prev = idx > 0 ? PARAS[idx-1].id : null;
const next = idx < PARAS.length - 1 ? PARAS[idx+1].id : null;
return secNav(prev, next);
}
function secNav(prev, next){
function lbl(id){ if(!id) return ''; const p=PARAS.find(x=>x.id===id); return p?p.num:id; }
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> '+lbl(prev)+'</button>':'<span></span>';
h+=next?'<button class="btn primary" onclick="goTo(\''+next+'\')">'+lbl(next)+' <svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></button>':'<span></span>';
h+='</div>'; return h;
}
function readButton(paraId){
const p = PARAS.find(x => x.id === paraId);
const labelTail = p && p.final ? 'финал' : (p ? p.num : '?');
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>'
+' Я прочитал \u2014 '+labelTail+' (+10 XP)'
+'</button></div>';
}
function wireReadBtn(paraId){
const btn = document.getElementById(paraId+'-read-btn'); if(!btn) return;
btn.addEventListener('click', ()=>{
addXp(10, paraId+'-read'); bumpProgress(paraId, 30);
btn.textContent='Прочитано! +10 XP'; btn.disabled=true; btn.style.opacity=.6;
const aId = paraId+'_done';
if(ACH_LABELS[aId]) achievement(aId);
});
}
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(); }};
}
function buildStub(id, name, phase){
return '<div class="card" style="background:linear-gradient(135deg,var(--sec-acc-soft),var(--card));border:1.5px dashed var(--sec-acc)">'
+ '<div class="card-header"><div class="card-icon theory">'+ICONS.theory+'</div><div class="card-title">В разработке</div></div>'
+ '<div class="card-body"><p>Контент <b>'+name+'</b> будет реализован в <b>'+phase+'</b> по плану <code>PLAN_PHYSICS_8.md</code>.</p>'
+ '<p style="margin-top:8px;color:var(--muted);font-size:.9rem">Phase 0 \u2014 это каркас (skeleton). Все 4 интерактива, 3 теоретические карточки и тренажёр задач будут добавлены в волне.</p>'
+ '</div></div>';
}
/* ===== Search ===== */
const SEARCH_INDEX = (function(){
const arr=[];
PARAS.forEach(p=>arr.push({kind:'Параграф',title:p.num+' '+p.name,desc:p.sub||'',sec:p.id}));
return arr;
})();
function initSearch(){
const modal=document.getElementById('search-modal'),inp=document.getElementById('search-input'),out=document.getElementById('search-results'),btn=document.getElementById('search-btn');
if(!modal||!inp||!out) return;
let cur=0,rows=[];
function score(q,it){ const t=(it.title+' '+it.desc).toLowerCase(); if(t.includes(q)) return 100+(it.title.toLowerCase().startsWith(q)?50:0); let s=0; q.split(/\s+/).forEach(w=>{if(w&&t.includes(w))s+=10;}); return s; }
function rank(q){ q=q.trim().toLowerCase(); if(!q) return SEARCH_INDEX.slice(0,12); return SEARCH_INDEX.map(it=>({it,s:score(q,it)})).filter(x=>x.s>0).sort((a,b)=>b.s-a.s).slice(0,20).map(x=>x.it); }
function render(){ cur=0; if(!rows.length){out.innerHTML='<div class="search-empty">Ничего не найдено</div>';return;} out.innerHTML=rows.map((r,i)=>'<button class="search-row'+(i===0?' active':'')+'" data-i="'+i+'"><div class="sr-kind">'+r.kind+'</div><div class="sr-title">'+r.title+'</div>'+(r.desc?'<div class="sr-desc">'+(r.desc.length>90?r.desc.slice(0,90)+'\u2026':r.desc)+'</div>':'')+'</button>').join(''); out.querySelectorAll('.search-row').forEach(b=>b.addEventListener('click',()=>{cur=+b.dataset.i;pick();})); }
function pick(){ const r=rows[cur]; if(!r) return; close(); goTo(r.sec); }
function move(d){ const items=out.querySelectorAll('.search-row'); if(!items.length) return; items[cur]&&items[cur].classList.remove('active'); cur=(cur+d+items.length)%items.length; items[cur].classList.add('active'); items[cur].scrollIntoView({block:'nearest'}); }
function open(){ modal.classList.add('show'); inp.value=''; rows=rank(''); render(); setTimeout(()=>inp.focus(),50); }
function close(){ modal.classList.remove('show'); }
btn&&btn.addEventListener('click',open);
modal.addEventListener('click',e=>{if(e.target===modal)close();});
inp.addEventListener('input',()=>{rows=rank(inp.value);render();});
inp.addEventListener('keydown',e=>{ if(e.key==='ArrowDown'){e.preventDefault();move(1);}else if(e.key==='ArrowUp'){e.preventDefault();move(-1);}else if(e.key==='Enter'){e.preventDefault();pick();}else if(e.key==='Escape'){e.preventDefault();close();} });
document.addEventListener('keydown',e=>{ if((e.ctrlKey||e.metaKey)&&(e.key==='k'||e.key==='K')){ e.preventDefault(); if(modal.classList.contains('show')) close(); else open(); } });
}
function initSidebarToggle(){
const side=document.getElementById('col-side'),back=document.getElementById('col-side-backdrop'),btn=document.getElementById('sidebar-btn');
if(!side||!btn) return;
function open(){ side.classList.add('open'); back.classList.add('show'); }
function close(){ side.classList.remove('open'); back.classList.remove('show'); }
btn.addEventListener('click',()=>{ if(side.classList.contains('open')) close(); else open(); });
back.addEventListener('click',close);
document.addEventListener('keydown',e=>{ if(e.key==='Escape') close(); });
}
/* ======================================================================
PHASE 5 — Глава 3 «Световые явления» (§32-40 + Финал)
====================================================================== */
const _SIMS = {};
function _killSim(key){ if(_SIMS[key] && _SIMS[key].raf){ cancelAnimationFrame(_SIMS[key].raf); _SIMS[key].raf=0; } }
function _isVisible(secId){ const el=document.getElementById('sec-'+secId); return el && el.classList.contains('active'); }
/* ======== §32 — Источники света ======== */
function build_p32(){
const box = document.getElementById('p32-body'); let h = '';
h += makeCard('theory', 'Источники света', '§ 32.1',
'<p><b>Источник света</b> — тело, которое излучает свет. Их делят на 2 группы:</p>'
+'<ul style="padding-left:20px;margin:6px 0">'
+'<li><b>Естественные</b>: Солнце, звёзды, молния, светящиеся насекомые (светляки).</li>'
+'<li><b>Искусственные</b>: лампа, костёр, свеча, светодиод, экран.</li>'
+'</ul>'
+'<p>Сами по себе <b>не светят</b>: Луна (отражает свет Солнца), книги, стены — все они освещены.</p>'
);
h += makeCard('rule', 'Тепловые и люминесцентные', '§ 32.2',
'<p><b>Тепловые источники</b> излучают свет благодаря высокой температуре:</p>'
+'<ul style="padding-left:20px;margin:6px 0"><li>Солнце ($\\sim 6000$ &#176;C);</li><li>лампа накаливания ($\\sim 2500$ &#176;C);</li><li>пламя свечи ($\\sim 1000$ &#176;C).</li></ul>'
+'<p><b>Люминесцентные</b> — холодные, свет за счёт квантовых процессов:</p>'
+'<ul style="padding-left:20px;margin:6px 0"><li>светодиоды, светофоры;</li><li>светляки, гнилое дерево, медузы;</li><li>люминесцентные лампы.</li></ul>'
);
h += makeCard('example', 'Точечный и протяжённый', '§ 32.3',
'<p>Если размер источника много <b>меньше</b> расстояния до объекта — его называют <b>точечным</b>. Дальняя звезда — точечный источник.</p>'
+'<p><b>Протяжённый</b> — например, длинная лампа на потолке. Он даёт мягкий свет без чётких теней.</p>'
);
h += '<div class="wg"><div class="wg-header"><span class="wg-badge">IV-1</span><div class="wg-title">Светит сам или отражает?</div></div>'
+'<div class="wg-help">Определи: источник света или просто освещённое тело.</div>'
+'<div id="p32-quiz"></div>'
+'<div class="actions"><button class="btn" id="p32-quiz-next">Следующий</button></div>'
+'<div class="score-display" style="margin-top:10px"><span>Раунд: <b id="p32-quiz-r">1</b>/8</span><span>Правильно: <b id="p32-quiz-ok">0</b></span></div></div>';
h += '<div class="wg"><div class="wg-header"><span class="wg-badge">IV-2</span><div class="wg-title">Тепловой или люминесцентный?</div></div>'
+'<div class="wg-help">Определи природу источника.</div>'
+'<div id="p32-quiz2"></div>'
+'<div class="actions"><button class="btn" id="p32-q2-next">Следующий</button></div>'
+'<div class="score-display" style="margin-top:10px"><span>Раунд: <b id="p32-q2-r">1</b>/6</span><span>Правильно: <b id="p32-q2-ok">0</b></span></div></div>';
h += '<div class="wg"><div class="wg-header"><span class="wg-badge">IV-3</span><div class="wg-title">Сортировка</div></div>'
+'<div class="wg-help">Распредели источники.</div>'
+'<div id="p32-dnd-pool"></div>'
+'<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:10px"><div class="drop-box"><h5>Естественный</h5><div class="drop-items" data-cat="nat"></div></div><div class="drop-box"><h5>Искусственный</h5><div class="drop-items" data-cat="art"></div></div></div>'
+'<div class="actions"><button class="btn primary" id="p32-dnd-check">Проверить</button><button class="btn" id="p32-dnd-reset">Сброс</button></div>'
+'<div class="feedback" id="p32-dnd-fb"></div></div>';
h += '<div class="wg"><div class="wg-header"><span class="wg-badge">IV-4</span><div class="wg-title">Тренажёр: 6 вопросов</div></div>'
+'<div class="wg-help">4+ — +15 XP.</div>'
+'<div id="p32-mcq"></div>'
+'<div class="score-display" style="margin-top:10px"><span>Вопрос: <b id="p32-mcq-i">1</b>/6</span><span>Правильно: <b id="p32-mcq-ok">0</b></span></div></div>';
box.innerHTML = h + secNavFor('p32') + readButton('p32');
renderMath(box); wireReadBtn('p32');
_p32_quiz1(); _p32_quiz2(); _p32_dnd(); _p32_mcq();
}
function _p32_quiz1(){
const QS = [
{it:'Луна', ans:'O', why:'Луна отражает свет Солнца.'},
{it:'Солнце', ans:'S', why:'Сам излучает.'},
{it:'Светлячок', ans:'S', why:'Биолюминесценция.'},
{it:'Зеркало', ans:'O', why:'Только отражает.'},
{it:'Лампа', ans:'S', why:'Излучает.'},
{it:'Кошачьи глаза в темноте', ans:'O', why:'Отражают свет фонаря.'},
{it:'Молния', ans:'S', why:'Электр. разряд излучает свет.'},
{it:'Венера', ans:'O', why:'Отражает свет Солнца.'}
];
let i = 0, ok = 0;
function r(){
const q = QS[i]; const w = document.getElementById('p32-quiz');
w.innerHTML = '<div style="padding:10px 14px;background:rgba(15,23,42,.04);border-radius:9px;margin:8px 0;line-height:1.5">'+q.it+'</div>'
+'<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px"><button class="btn" data-p="S"><b>Светит сам</b></button><button class="btn" data-p="O"><b>Отражает</b></button></div>'
+'<div class="feedback" id="p32-q1-fb"></div>';
document.getElementById('p32-quiz-r').textContent = (i+1);
document.getElementById('p32-quiz-ok').textContent = ok;
w.querySelectorAll('[data-p]').forEach(b=>{
b.addEventListener('click', ()=>{
if(b.disabled) return; w.querySelectorAll('[data-p]').forEach(x=>x.disabled=true);
const fb = document.getElementById('p32-q1-fb');
if(b.dataset.p === q.ans){ ok++; fb.className='feedback ok'; fb.innerHTML='&#10003; '+q.why; addXp(2,'p32-q1'); bumpProgress('p32',3); }
else { fb.className='feedback fail'; fb.innerHTML='&#10007; '+q.why; }
document.getElementById('p32-quiz-ok').textContent = ok;
});
});
}
document.getElementById('p32-quiz-next').addEventListener('click', ()=>{ i=(i+1)%QS.length; r(); });
r();
}
function _p32_quiz2(){
const QS = [
{it:'Солнце', ans:'T', why:'Высокая температура.'},
{it:'Светодиод', ans:'L', why:'Квантовый эффект.'},
{it:'Костёр', ans:'T', why:'Горение, $T \\sim 1000$ &#176;C.'},
{it:'Светляк', ans:'L', why:'Холодная биолюминесценция.'},
{it:'Лампа накаливания', ans:'T', why:'Нить нагревается до 2500 &#176;C.'},
{it:'Экран смартфона', ans:'L', why:'Светодиоды излучают холодно.'}
];
let i = 0, ok = 0;
function r(){
const q = QS[i]; const w = document.getElementById('p32-quiz2');
w.innerHTML = '<div style="padding:10px 14px;background:rgba(15,23,42,.04);border-radius:9px;margin:8px 0;line-height:1.5">'+q.it+'</div>'
+'<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px"><button class="btn" data-p="T"><b>Тепловой</b></button><button class="btn" data-p="L"><b>Люминесцентный</b></button></div>'
+'<div class="feedback" id="p32-q2-fb"></div>';
document.getElementById('p32-q2-r').textContent = (i+1);
document.getElementById('p32-q2-ok').textContent = ok;
w.querySelectorAll('[data-p]').forEach(b=>{
b.addEventListener('click', ()=>{
if(b.disabled) return; w.querySelectorAll('[data-p]').forEach(x=>x.disabled=true);
const fb = document.getElementById('p32-q2-fb');
if(b.dataset.p === q.ans){ ok++; fb.className='feedback ok'; fb.innerHTML='&#10003; '+q.why; addXp(2,'p32-q2'); bumpProgress('p32',3); }
else { fb.className='feedback fail'; fb.innerHTML='&#10007; '+q.why; }
document.getElementById('p32-q2-ok').textContent = ok;
});
});
}
document.getElementById('p32-q2-next').addEventListener('click', ()=>{ i=(i+1)%QS.length; r(); });
r();
}
function _p32_dnd(){
const items = [
{id:'a',cat:'nat',html:'Солнце'},{id:'b',cat:'nat',html:'молния'},{id:'c',cat:'nat',html:'светляк'},{id:'d',cat:'nat',html:'звезда'},
{id:'e',cat:'art',html:'лампа'},{id:'f',cat:'art',html:'светодиод'},{id:'g',cat:'art',html:'свеча'},{id:'h',cat:'art',html:'экран'}
];
const dnd = setupSorter({ poolId:'p32-dnd-pool', scopeSelector:'#sec-p32', cats:['nat','art'], items, columnLayout:false });
document.getElementById('p32-dnd-check').addEventListener('click', ()=>{
const fb = document.getElementById('p32-dnd-fb'); let wr = 0;
items.forEach(it=>{ if(dnd.placed[it.id] !== it.cat) wr++; });
if(wr===0){ fb.className='feedback ok'; fb.innerHTML='&#10003; +15 XP'; addXp(15,'p32-dnd'); bumpProgress('p32',20); }
else { fb.className='feedback fail'; fb.innerHTML='&#10007; Ошибок: '+wr+'.'; }
});
document.getElementById('p32-dnd-reset').addEventListener('click', ()=>{ dnd.reset(); document.getElementById('p32-dnd-fb').style.display='none'; });
}
function _p32_mcq(){
const QS = [
{q:'Какое тело — источник света?',opts:['Луна','лампа','зеркало','страница'],ans:1,why:'Лампа излучает свет.'},
{q:'Что общего у Солнца и лампы накаливания?',opts:['обе люминесцентные','тепловые','холодные','одинаковая T'],ans:1,why:'Светят за счёт нагрева.'},
{q:'Светодиод — это …',opts:['тепловой','люминесцентный','зеркало','не источник'],ans:1,why:'Холодный квантовый источник.'},
{q:'Точечный источник — это …',opts:['любой','размер $\\ll$ расстояния','шар','точка'],ans:1,why:'Малый по сравнению с расстоянием.'},
{q:'Луна светит потому что …',opts:['горит','отражает свет Солнца','имеет атомную реакцию','остывает'],ans:1,why:'Отражает солнечный свет.'},
{q:'Какой источник самый «холодный»?',opts:['Солнце','свеча','светляк','лампа'],ans:2,why:'Биолюминесценция идёт при обычной T.'}
];
let i = 0, ok = 0, done = 0, aw = false;
function r(){
const q = QS[i]; const w = document.getElementById('p32-mcq');
let h = '<div style="padding:10px 14px;background:rgba(15,23,42,.04);border-radius:9px;margin-bottom:10px;font-size:.95rem;line-height:1.5"><b>'+(i+1)+'.</b> '+q.q+'</div><div style="display:grid;grid-template-columns:1fr;gap:6px">';
q.opts.forEach((o,k)=>{ h += '<button class="btn" data-k="'+k+'" style="text-align:left;padding:10px 14px">'+String.fromCharCode(65+k)+'. '+o+'</button>'; });
h += '</div><div class="feedback" id="p32-mcq-fb"></div><div class="actions"><button class="btn" id="p32-mcq-n">Следующий</button></div>';
w.innerHTML = h;
document.getElementById('p32-mcq-i').textContent = (i+1);
document.getElementById('p32-mcq-ok').textContent = ok;
w.querySelectorAll('[data-k]').forEach(b=>{
b.addEventListener('click', ()=>{
if(b.disabled) return; w.querySelectorAll('[data-k]').forEach(x=>x.disabled=true);
const k = +b.dataset.k; const fb = document.getElementById('p32-mcq-fb');
if(k===q.ans){ ok++; done++; fb.className='feedback ok'; fb.innerHTML='&#10003; '+q.why; addXp(2,'p32-mcq'); bumpProgress('p32',3); }
else { done++; fb.className='feedback fail'; fb.innerHTML='&#10007; '+q.why; }
document.getElementById('p32-mcq-ok').textContent = ok;
if(done >= QS.length && !aw && ok >= 4){ aw = true; setTimeout(()=>{ const f=document.getElementById('p32-mcq-fb'); f.className='feedback ok'; f.innerHTML='&#10003; +15 XP — тренажёр пройден.'; addXp(15,'p32-bonus'); bumpProgress('p32',15); }, 500); }
});
});
document.getElementById('p32-mcq-n').addEventListener('click', ()=>{ i=(i+1)%QS.length; r(); });
}
r();
}
/* ======== §33 — Скорость света. Прямолинейное распространение ======== */
function build_p33(){
const box = document.getElementById('p33-body'); let h = '';
h += makeCard('theory', 'Скорость света', '§ 33.1',
'<p>В вакууме свет распространяется со <b>скоростью</b>:</p>'
+'<p style="text-align:center;margin:8px 0">$$c = 3 \\cdot 10^8 \\text{ м/с} = 300\\,000 \\text{ км/с}$$</p>'
+'<p>Это <b>максимальная</b> скорость в природе. Никакое тело не может двигаться быстрее.</p>'
+'<ul style="padding-left:20px;margin:6px 0">'
+'<li>От Солнца до Земли (150 млн км): <b>$\\sim 8$ минут</b>.</li>'
+'<li>От Луны до Земли (384 тыс. км): <b>$\\sim 1{,}3$ с</b>.</li>'
+'<li>За 1 с свет пролетает в 7,5 раза вокруг Земли.</li>'
+'</ul>'
+'<p>В воде и стекле свет идёт медленнее: $v = c/n$.</p>'
);
h += makeCard('rule', 'Прямолинейное распространение', '§ 33.2',
'<p>В <b>однородной</b> среде свет распространяется по <b>прямой</b> линии. Это видно по лазерному лучу в пыльной комнате.</p>'
+'<p>Следствие: на пути луча возникает <b>тень</b> от непрозрачного предмета. Тень повторяет силуэт предмета.</p>'
);
h += makeCard('example', 'Тень и полутень', '§ 33.3',
'<p>Если источник <b>точечный</b>, то за предметом образуется только <b>тень</b> — резкая.</p>'
+'<p>Если источник <b>протяжённый</b>, то к тени добавляется <b>полутень</b> — переходная зона.</p>'
+'<p>Затмения Солнца и Луны — это «космические» тени:</p>'
+'<ul style="padding-left:20px;margin:6px 0"><li>солнечное затмение — Луна закрывает Солнце для нас;</li><li>лунное — Земля закрывает Солнце для Луны.</li></ul>'
);
h += '<div class="wg"><div class="wg-header"><span class="wg-badge">IV-1</span><div class="wg-title">Тень и полутень</div></div>'
+'<div class="wg-help">Двигай источник света — увидь, как меняется тень. Расширяй источник — появляется полутень.</div>'
+'<div class="sliders" style="margin-bottom:10px">'
+'<label>Размер источника: <b id="p33-sv">точечный</b><input type="range" id="p33-s" min="0" max="40" step="2" value="0"></label>'
+'<label>Расстояние от объекта до источника: <b id="p33-dv">150</b><input type="range" id="p33-d" min="80" max="250" step="10" value="150"></label>'
+'</div>'
+'<svg id="p33-sim" viewBox="0 0 460 220" style="width:100%;height:auto;background:#f8fafc;border-radius:9px;border:1px solid var(--border)"></svg></div>';
h += '<div class="wg"><div class="wg-header"><span class="wg-badge">IV-2</span><div class="wg-title">Калькулятор времени</div></div>'
+'<div class="wg-help">Сколько секунд свет летит до объекта?</div>'
+'<div class="sliders" style="margin-bottom:10px"><label>Расстояние, км: <b id="p33-rv">150000000</b><input type="range" id="p33-r" min="1" max="1500000000" step="100" value="150000000"></label></div>'
+'<div class="score-display" style="margin-top:8px"><span>Время в пути: <b id="p33-tv">500</b> с</span><span><b id="p33-tm">8.3</b> мин</span></div></div>';
h += '<div class="wg"><div class="wg-header"><span class="wg-badge">IV-3</span><div class="wg-title">DnD «правда/ложь о свете»</div></div>'
+'<div id="p33-dnd-pool"></div>'
+'<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:10px"><div class="drop-box"><h5>Правда</h5><div class="drop-items" data-cat="t"></div></div><div class="drop-box"><h5>Ложь</h5><div class="drop-items" data-cat="f"></div></div></div>'
+'<div class="actions"><button class="btn primary" id="p33-dnd-check">Проверить</button><button class="btn" id="p33-dnd-reset">Сброс</button></div>'
+'<div class="feedback" id="p33-dnd-fb"></div></div>';
h += '<div class="wg"><div class="wg-header"><span class="wg-badge">IV-4</span><div class="wg-title">Тренажёр: 5 задач</div></div>'
+'<div class="wg-help">$c = 3 \\cdot 10^8$ м/с. 4+ — +15 XP.</div>'
+'<div id="p33-task"></div>'
+'<div class="score-display" style="margin-top:10px"><span>Задача: <b id="p33-task-i">1</b>/5</span><span>Правильно: <b id="p33-task-ok">0</b></span></div></div>';
box.innerHTML = h + secNavFor('p33') + readButton('p33');
renderMath(box); wireReadBtn('p33');
_p33_shadow(); _p33_calc(); _p33_dnd(); _p33_tasks();
}
function _p33_shadow(){
const svg = document.getElementById('p33-sim'); if(!svg) return;
function draw(){
const sSize = +document.getElementById('p33-s').value;
const dist = +document.getElementById('p33-d').value;
document.getElementById('p33-sv').textContent = sSize === 0 ? 'точечный' : sSize < 20 ? 'малый' : 'большой';
document.getElementById('p33-dv').textContent = dist;
let s = '';
/* источник слева */
const srcX = 50, srcY = 110;
if(sSize === 0){ s += '<circle cx="'+srcX+'" cy="'+srcY+'" r="6" fill="#fbbf24" stroke="#0f172a" stroke-width="1.4"/>'; }
else { s += '<rect x="'+(srcX-sSize/4)+'" y="'+(srcY-sSize/2)+'" width="'+(sSize/2)+'" height="'+sSize+'" fill="#fbbf24" stroke="#0f172a" stroke-width="1.4"/>'; }
s += '<text x="'+srcX+'" y="40" text-anchor="middle" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="#475569">источник</text>';
/* непрозрачный объект */
const objX = srcX + dist*0.5, objY = 110;
s += '<rect x="'+objX+'" y="'+(objY-20)+'" width="14" height="40" fill="#1f2937" stroke="#0f172a" stroke-width="1.5"/>';
/* экран справа */
const screenX = 400;
s += '<line x1="'+screenX+'" y1="40" x2="'+screenX+'" y2="180" stroke="#475569" stroke-width="3"/>';
/* лучи от краёв источника к краям объекта и дальше на экран */
const sTop = srcY - sSize/2, sBot = srcY + sSize/2;
const oTop = objY - 20, oBot = objY + 20;
/* лучи без полутени (от центра источника к краям объекта): тень */
const k1 = (oTop - srcY) / (objX - srcX);
const yShTop = srcY + k1 * (screenX - srcX);
const k2 = (oBot - srcY) / (objX - srcX);
const yShBot = srcY + k2 * (screenX - srcX);
/* лучи полутени (от противоположного края источника к краям объекта) */
const kp1 = (oTop - sBot) / (objX - srcX);
const yPenTop = sBot + kp1 * (screenX - srcX);
const kp2 = (oBot - sTop) / (objX - srcX);
const yPenBot = sTop + kp2 * (screenX - srcX);
/* рисуем зоны */
if(sSize > 0){
/* полутень — между yPenTop и yShTop, и между yShBot и yPenBot */
s += '<rect x="'+(objX+14)+'" y="'+yPenTop+'" width="'+(screenX-objX-14)+'" height="'+(yShTop-yPenTop)+'" fill="rgba(0,0,0,.15)"/>';
s += '<rect x="'+(objX+14)+'" y="'+yShBot+'" width="'+(screenX-objX-14)+'" height="'+(yPenBot-yShBot)+'" fill="rgba(0,0,0,.15)"/>';
}
/* тень */
s += '<rect x="'+(objX+14)+'" y="'+yShTop+'" width="'+(screenX-objX-14)+'" height="'+(yShBot-yShTop)+'" fill="rgba(0,0,0,.6)"/>';
/* лучи света */
s += '<line x1="'+srcX+'" y1="'+srcY+'" x2="'+screenX+'" y2="'+(srcY-80)+'" stroke="#fbbf24" stroke-width="1" stroke-dasharray="3 3" opacity="0.6"/>';
s += '<line x1="'+srcX+'" y1="'+srcY+'" x2="'+screenX+'" y2="'+(srcY+80)+'" stroke="#fbbf24" stroke-width="1" stroke-dasharray="3 3" opacity="0.6"/>';
/* подписи */
s += '<text x="'+screenX+'" y="200" text-anchor="middle" font-family="Inter,sans-serif" font-size="11" fill="#475569">экран</text>';
if(sSize > 0) s += '<text x="320" y="'+(yPenBot+18)+'" text-anchor="middle" font-family="Inter,sans-serif" font-size="11" fill="#475569">тень + полутень</text>';
else s += '<text x="320" y="200" text-anchor="middle" font-family="Inter,sans-serif" font-size="11" fill="#475569">только тень (источник точечный)</text>';
svg.innerHTML = s;
}
document.getElementById('p33-s').addEventListener('input', draw);
document.getElementById('p33-d').addEventListener('input', draw);
draw();
}
function _p33_calc(){
function u(){
const r = +document.getElementById('p33-r').value;
document.getElementById('p33-rv').textContent = r.toLocaleString('ru');
const t = r*1000 / 3e8;
document.getElementById('p33-tv').textContent = t.toFixed(2);
document.getElementById('p33-tm').textContent = (t/60).toFixed(2);
}
document.getElementById('p33-r').addEventListener('input', u); u();
}
function _p33_dnd(){
const items = [
{id:'a',cat:'t',html:'$c = 3 \\cdot 10^8$ м/с'},
{id:'b',cat:'t',html:'свет идёт прямолинейно в однород. среде'},
{id:'c',cat:'t',html:'свет огибает большой объект'},
{id:'d',cat:'t',html:'тень — там, куда свет не попал'},
{id:'e',cat:'f',html:'свет идёт зигзагом'},
{id:'f',cat:'f',html:'свет летит за 1 с до Луны'},
{id:'g',cat:'f',html:'тень светлее объекта'},
{id:'h',cat:'f',html:'$c$ в воде больше, чем в вакууме'}
];
/* исправлю c (cat) — путаница; правильно: «c» как факт — «не правда» */
items[2].cat = 'f'; /* свет НЕ огибает (он не дифрагирует значительно на больших объектах в школе) */
items[5].cat = 'f'; /* до Луны 1.3 с, не 1 с... но это близко, лучше: убираем спорный */
/* для безопасности — переписываем чище */
const items2 = [
{id:'a',cat:'t',html:'$c = 3 \\cdot 10^8$ м/с'},
{id:'b',cat:'t',html:'свет идёт прямолинейно в однород. среде'},
{id:'c',cat:'t',html:'тень — место, куда свет не попал'},
{id:'d',cat:'t',html:'свет от Солнца идёт ~8 мин'},
{id:'e',cat:'f',html:'свет идёт зигзагом'},
{id:'f',cat:'f',html:'свет — это поток жидкости'},
{id:'g',cat:'f',html:'$c$ в воде больше, чем в вакууме'},
{id:'h',cat:'f',html:'свет не отбрасывает тень'}
];
const dnd = setupSorter({ poolId:'p33-dnd-pool', scopeSelector:'#sec-p33', cats:['t','f'], items: items2, columnLayout:false });
document.getElementById('p33-dnd-check').addEventListener('click', ()=>{
const fb = document.getElementById('p33-dnd-fb'); let wr = 0;
items2.forEach(it=>{ if(dnd.placed[it.id] !== it.cat) wr++; });
if(wr===0){ fb.className='feedback ok'; fb.innerHTML='&#10003; +15 XP'; addXp(15,'p33-dnd'); bumpProgress('p33',20); }
else { fb.className='feedback fail'; fb.innerHTML='&#10007; Ошибок: '+wr+'.'; }
});
document.getElementById('p33-dnd-reset').addEventListener('click', ()=>{ dnd.reset(); document.getElementById('p33-dnd-fb').style.display='none'; });
}
function _p33_tasks(){
const TASKS = [
{q:'За какое время свет от Солнца дойдёт до Земли? Расстояние $1{,}5 \\cdot 10^{11}$ м. Ответ в минутах (целое).', ans:8, tol:0.5, why:'$t = r/c = 1{,}5\\cdot10^{11}/3\\cdot10^8 = 500$ с $= 8{,}3$ мин.'},
{q:'Свет от лампы до глаза: 3 м. Время в наносекундах (1 нс = $10^{-9}$ с)?', ans:10, tol:0.5, why:'$t = 3/3\\cdot10^8 = 10^{-8}$ с = $10$ нс.'},
{q:'Луна на расстоянии $384\\,000$ км. Время в секундах (одна цифра после запятой)?', ans:1.3, tol:0.1, why:'$t = 3{,}84\\cdot10^8/3\\cdot10^8 = 1{,}28$ с.'},
{q:'Сколько км свет проходит за 1 минуту? Введи в формате $a \\cdot 10^7$ км.', ans:1.8, tol:0.1, why:'$d = c \\cdot 60 = 1{,}8 \\cdot 10^{10}$ м = $1{,}8 \\cdot 10^7$ км.'},
{q:'Звук в воздухе $\\sim 340$ м/с. Во сколько раз свет быстрее (порядок: $10^a$, введи $a$)?', ans:6, tol:0.5, why:'$c/v_{зв} = 3\\cdot10^8/340 \\approx 9\\cdot10^5 \\approx 10^6$. Порядок $a = 6$.'}
];
let i = 0, ok = 0, done = 0, aw = false;
function r(){
const t = TASKS[i]; const w = document.getElementById('p33-task');
w.innerHTML = '<div style="padding:10px 14px;background:rgba(15,23,42,.04);border-radius:9px;margin-bottom:10px;font-size:.95rem;line-height:1.5"><b>'+(i+1)+'.</b> '+t.q+'</div>'
+'<div class="boss-row"><input type="number" step="0.01" class="tinp" id="p33-tinp" style="width:140px"><button class="btn primary" id="p33-tgo">Ответ</button><button class="btn" id="p33-thn">Подск.</button><button class="btn" id="p33-tn">След.</button></div>'
+'<div class="boss-hint-txt" id="p33-tht">'+t.why+'</div><div class="feedback" id="p33-tfb"></div>';
document.getElementById('p33-task-i').textContent = (i+1);
document.getElementById('p33-task-ok').textContent = ok;
document.getElementById('p33-tgo').addEventListener('click', ()=>{
const v = parseFloat((document.getElementById('p33-tinp').value || '').replace(',','.'));
const fb = document.getElementById('p33-tfb');
if(isNaN(v)){ fb.className='feedback fail'; fb.innerHTML='Введи число.'; return; }
done++;
if(Math.abs(v - t.ans) < t.tol){ ok++; fb.className='feedback ok'; fb.innerHTML='&#10003; '+t.why; addXp(4,'p33-task'); bumpProgress('p33',6); }
else { fb.className='feedback fail'; fb.innerHTML='&#10007; Ответ: '+t.ans+'. '+t.why; }
document.getElementById('p33-task-ok').textContent = ok;
renderMath(w);
if(done >= TASKS.length && !aw && ok >= 4){ aw = true; setTimeout(()=>{ const f=document.getElementById('p33-tfb'); f.className='feedback ok'; f.innerHTML='&#10003; +15 XP — сданы.'; addXp(15,'p33-bonus'); bumpProgress('p33',15); }, 500); }
});
document.getElementById('p33-thn').addEventListener('click', ()=>{ document.getElementById('p33-tht').classList.toggle('show'); });
document.getElementById('p33-tn').addEventListener('click', ()=>{ i=(i+1)%TASKS.length; r(); });
renderMath(w);
}
r();
}
/* ======== §34 — Отражение света ======== */
function build_p34(){
const box = document.getElementById('p34-body'); let h = '';
h += makeCard('theory', 'Закон отражения', '§ 34.1',
'<p>Когда луч света падает на поверхность, часть его отражается. Угол падения $\\alpha$ — между падающим лучом и <b>нормалью</b> к поверхности (перпендикуляром). Угол отражения $\\beta$ — между нормалью и отражённым лучом.</p>'
+'<p><b>Закон отражения:</b></p>'
+'<ol style="padding-left:20px;margin:6px 0">'
+'<li>Падающий, отражённый луч и нормаль лежат в одной плоскости.</li>'
+'<li>$\\alpha = \\beta$ (угол падения = угол отражения).</li>'
+'</ol>'
);
h += makeCard('rule', 'Зеркальное и диффузное отражение', '§ 34.2',
'<p><b>Зеркальное</b> — от гладкой поверхности (зеркало, спокойная вода). Все лучи параллельны после отражения.</p>'
+'<p><b>Диффузное (рассеянное)</b> — от шероховатой поверхности (бумага, стена). Лучи разлетаются во все стороны — поэтому мы видим стену со всех сторон.</p>'
);
h += makeCard('example', 'В жизни', '§ 34.3',
'<ul style="padding-left:20px;margin:6px 0">'
+'<li>Свет фар на мокром асфальте — зеркальное (бликует).</li>'
+'<li>Свет от листа бумаги — диффузное.</li>'
+'<li>Луна светит Солнечным светом, отражая его диффузно.</li>'
+'</ul>'
);
h += '<div class="wg"><div class="wg-header"><span class="wg-badge">IV-1</span><div class="wg-title">Закон отражения</div></div>'
+'<div class="wg-help">Меняй угол падения — увидь равный угол отражения.</div>'
+'<div class="sliders" style="margin-bottom:10px"><label>$\\alpha$, &#176;: <b id="p34-av">30</b><input type="range" id="p34-a" min="0" max="80" step="1" value="30"></label></div>'
+'<svg id="p34-sim" viewBox="0 0 460 220" style="width:100%;height:auto;background:#f8fafc;border-radius:9px;border:1px solid var(--border)"></svg>'
+'<div class="score-display" style="margin-top:8px"><span>$\\alpha$ = <b id="p34-as">30&#176;</b></span><span>$\\beta$ = <b id="p34-bs">30&#176;</b></span></div></div>';
h += '<div class="wg"><div class="wg-header"><span class="wg-badge">IV-2</span><div class="wg-title">Зеркальное или диффузное?</div></div>'
+'<div class="wg-help">Определи тип отражения.</div>'
+'<div id="p34-quiz"></div>'
+'<div class="actions"><button class="btn" id="p34-quiz-next">Следующий</button></div>'
+'<div class="score-display" style="margin-top:10px"><span>Раунд: <b id="p34-quiz-r">1</b>/6</span><span>Правильно: <b id="p34-quiz-ok">0</b></span></div></div>';
h += '<div class="wg"><div class="wg-header"><span class="wg-badge">IV-3</span><div class="wg-title">Сортировка поверхностей</div></div>'
+'<div id="p34-dnd-pool"></div>'
+'<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:10px"><div class="drop-box"><h5>Зеркальное</h5><div class="drop-items" data-cat="m"></div></div><div class="drop-box"><h5>Диффузное</h5><div class="drop-items" data-cat="d"></div></div></div>'
+'<div class="actions"><button class="btn primary" id="p34-dnd-check">Проверить</button><button class="btn" id="p34-dnd-reset">Сброс</button></div>'
+'<div class="feedback" id="p34-dnd-fb"></div></div>';
h += '<div class="wg"><div class="wg-header"><span class="wg-badge">IV-4</span><div class="wg-title">Тренажёр: 5 задач</div></div>'
+'<div class="wg-help">4+ — +15 XP.</div>'
+'<div id="p34-task"></div>'
+'<div class="score-display" style="margin-top:10px"><span>Задача: <b id="p34-task-i">1</b>/5</span><span>Правильно: <b id="p34-task-ok">0</b></span></div></div>';
box.innerHTML = h + secNavFor('p34') + readButton('p34');
renderMath(box); wireReadBtn('p34');
_p34_ref(); _p34_quiz(); _p34_dnd(); _p34_tasks();
}
function _p34_ref(){
const svg = document.getElementById('p34-sim'); if(!svg) return;
function d(){
const a = +document.getElementById('p34-a').value;
document.getElementById('p34-av').textContent = a;
document.getElementById('p34-as').textContent = a+'&#176;';
document.getElementById('p34-bs').textContent = a+'&#176;';
svg.innerHTML = window.OPTICS.reflectRay(230, 150, a, 100);
}
document.getElementById('p34-a').addEventListener('input', d); d();
}
function _p34_quiz(){
const QS = [
{it:'Зеркало', ans:'M', why:'Гладкая поверхность.'},
{it:'Бумага', ans:'D', why:'Шероховатая.'},
{it:'Мокрый асфальт', ans:'M', why:'Тонкий слой воды делает поверхность гладкой.'},
{it:'Снег', ans:'D', why:'Кристаллики рассеивают.'},
{it:'Стекло окна', ans:'M', why:'Гладкое.'},
{it:'Стена побеленная', ans:'D', why:'Микрорельеф рассеивает.'}
];
let i = 0, ok = 0;
function r(){
const q = QS[i]; const w = document.getElementById('p34-quiz');
w.innerHTML = '<div style="padding:10px 14px;background:rgba(15,23,42,.04);border-radius:9px;margin:8px 0">'+q.it+'</div>'
+'<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px"><button class="btn" data-p="M"><b>Зеркальное</b></button><button class="btn" data-p="D"><b>Диффузное</b></button></div>'
+'<div class="feedback" id="p34-q-fb"></div>';
document.getElementById('p34-quiz-r').textContent = (i+1);
document.getElementById('p34-quiz-ok').textContent = ok;
w.querySelectorAll('[data-p]').forEach(b=>{
b.addEventListener('click', ()=>{
if(b.disabled) return; w.querySelectorAll('[data-p]').forEach(x=>x.disabled=true);
const fb = document.getElementById('p34-q-fb');
if(b.dataset.p === q.ans){ ok++; fb.className='feedback ok'; fb.innerHTML='&#10003; '+q.why; addXp(3,'p34-q'); bumpProgress('p34',4); }
else { fb.className='feedback fail'; fb.innerHTML='&#10007; '+q.why; }
document.getElementById('p34-quiz-ok').textContent = ok;
});
});
}
document.getElementById('p34-quiz-next').addEventListener('click', ()=>{ i=(i+1)%QS.length; r(); });
r();
}
function _p34_dnd(){
const items = [
{id:'a',cat:'m',html:'зеркало'},{id:'b',cat:'m',html:'спокойная вода'},{id:'c',cat:'m',html:'полировка металла'},{id:'d',cat:'m',html:'мокрый асфальт'},
{id:'e',cat:'d',html:'бумага'},{id:'f',cat:'d',html:'снег'},{id:'g',cat:'d',html:'ткань'},{id:'h',cat:'d',html:'штукатурка'}
];
const dnd = setupSorter({ poolId:'p34-dnd-pool', scopeSelector:'#sec-p34', cats:['m','d'], items, columnLayout:false });
document.getElementById('p34-dnd-check').addEventListener('click', ()=>{
const fb = document.getElementById('p34-dnd-fb'); let wr = 0;
items.forEach(it=>{ if(dnd.placed[it.id] !== it.cat) wr++; });
if(wr===0){ fb.className='feedback ok'; fb.innerHTML='&#10003; +15 XP'; addXp(15,'p34-dnd'); bumpProgress('p34',20); }
else { fb.className='feedback fail'; fb.innerHTML='&#10007; Ошибок: '+wr+'.'; }
});
document.getElementById('p34-dnd-reset').addEventListener('click', ()=>{ dnd.reset(); document.getElementById('p34-dnd-fb').style.display='none'; });
}
function _p34_tasks(){
const TASKS = [
{q:'$\\alpha = 30$ &#176;. Найди $\\beta$.', ans:30, tol:0.5, why:'$\\alpha = \\beta$.'},
{q:'Угол между падающим и зеркалом 50&#176;. Найди $\\alpha$ (от нормали).', ans:40, tol:0.5, why:'Нормаль к зеркалу, $\\alpha = 90-50 = 40$&#176;.'},
{q:'Угол между падающим и отражённым лучами 60&#176;. Найди $\\alpha$.', ans:30, tol:0.5, why:'$2\\alpha = 60$, $\\alpha = 30$&#176;.'},
{q:'$\\alpha = 0$&#176;. Куда отражается?', ans:0, tol:0.5, why:'Назад по той же нормали, $\\beta = 0$.'},
{q:'Поверхность повернули на 10&#176; (зафиксировав луч). Как изменится $\\beta$?', ans:20, tol:0.5, why:'При повороте зеркала на $\\Delta$, отражённый поворачивается на $2\\Delta = 20$&#176;.'}
];
let i = 0, ok = 0, done = 0, aw = false;
function r(){
const t = TASKS[i]; const w = document.getElementById('p34-task');
w.innerHTML = '<div style="padding:10px 14px;background:rgba(15,23,42,.04);border-radius:9px;margin-bottom:10px"><b>'+(i+1)+'.</b> '+t.q+'</div>'
+'<div class="boss-row"><input type="number" step="0.5" class="tinp" id="p34-tinp" style="width:140px"><button class="btn primary" id="p34-tgo">Ответ</button><button class="btn" id="p34-thn">Подск.</button><button class="btn" id="p34-tn">След.</button></div>'
+'<div class="boss-hint-txt" id="p34-tht">'+t.why+'</div><div class="feedback" id="p34-tfb"></div>';
document.getElementById('p34-task-i').textContent = (i+1);
document.getElementById('p34-task-ok').textContent = ok;
document.getElementById('p34-tgo').addEventListener('click', ()=>{
const v = parseFloat((document.getElementById('p34-tinp').value || '').replace(',','.'));
const fb = document.getElementById('p34-tfb');
if(isNaN(v)){ fb.className='feedback fail'; fb.innerHTML='Число.'; return; }
done++;
if(Math.abs(v - t.ans) < t.tol){ ok++; fb.className='feedback ok'; fb.innerHTML='&#10003; '+t.why; addXp(4,'p34-t'); bumpProgress('p34',6); }
else { fb.className='feedback fail'; fb.innerHTML='&#10007; '+t.ans+'. '+t.why; }
document.getElementById('p34-task-ok').textContent = ok;
renderMath(w);
if(done >= TASKS.length && !aw && ok >= 4){ aw = true; setTimeout(()=>{ const f=document.getElementById('p34-tfb'); f.className='feedback ok'; f.innerHTML='&#10003; +15 XP'; addXp(15,'p34-bonus'); bumpProgress('p34',15); }, 500); }
});
document.getElementById('p34-thn').addEventListener('click', ()=>{ document.getElementById('p34-tht').classList.toggle('show'); });
document.getElementById('p34-tn').addEventListener('click', ()=>{ i=(i+1)%TASKS.length; r(); });
renderMath(w);
}
r();
}
/* ======== §35 — Плоское зеркало ======== */
function build_p35(){
const box = document.getElementById('p35-body'); let h = '';
h += makeCard('theory', 'Изображение в плоском зеркале', '§ 35.1',
'<p>Плоское зеркало даёт <b>мнимое</b> изображение предмета:</p>'
+'<ul style="padding-left:20px;margin:6px 0">'
+'<li>Изображение расположено <b>за</b> зеркалом, на том же расстоянии, что и предмет.</li>'
+'<li>Размеры предмета и изображения <b>равны</b>.</li>'
+'<li>Изображение <b>симметрично</b> предмету относительно плоскости зеркала.</li>'
+'<li>Изображение «прямое» (не перевёрнуто).</li>'
+'</ul>'
+'<p>«Мнимое» значит: лучи света не пересекаются в этой точке, а пересекаются их <b>продолжения</b>.</p>'
);
h += makeCard('rule', 'Как строится изображение', '§ 35.2',
'<p>От каждой точки предмета идут лучи во все стороны. Те, что попадают на зеркало, отражаются по закону отражения. Продолжения отражённых лучей собираются <b>за зеркалом</b> в точке изображения.</p>'
);
h += makeCard('example', 'Зеркало vs текст', '§ 35.3',
'<p>В зеркале правая рука выглядит как левая, а буквы переворачиваются по горизонтали. Это симметрия.</p>'
+'<p>Поэтому надписи на машинах скорой помощи пишут «зеркально» — чтобы водитель впереди мог прочесть «АМБУЛАНС» в зеркале заднего вида.</p>'
);
h += '<div class="wg"><div class="wg-header"><span class="wg-badge">IV-1</span><div class="wg-title">Построение в плоском зеркале</div></div>'
+'<div class="wg-help">Двигай предмет — увидь, как симметрично «отъезжает» изображение.</div>'
+'<div class="sliders" style="margin-bottom:10px"><label>Расстояние от предмета до зеркала, см: <b id="p35-dv">8</b><input type="range" id="p35-d" min="2" max="14" step="1" value="8"></label></div>'
+'<svg id="p35-sim" viewBox="0 0 460 220" style="width:100%;height:auto;background:#f8fafc;border-radius:9px;border:1px solid var(--border)"></svg></div>';
h += '<div class="wg"><div class="wg-header"><span class="wg-badge">IV-2</span><div class="wg-title">Свойства изображения</div></div>'
+'<div class="wg-help">Правда или ложь?</div>'
+'<div id="p35-quiz"></div>'
+'<div class="actions"><button class="btn" id="p35-quiz-next">Следующий</button></div>'
+'<div class="score-display" style="margin-top:10px"><span>Раунд: <b id="p35-quiz-r">1</b>/6</span><span>Правильно: <b id="p35-quiz-ok">0</b></span></div></div>';
h += '<div class="wg"><div class="wg-header"><span class="wg-badge">IV-3</span><div class="wg-title">Свойства изображения в плоском зеркале</div></div>'
+'<div id="p35-dnd-pool"></div>'
+'<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:10px"><div class="drop-box"><h5>Свойственно</h5><div class="drop-items" data-cat="y"></div></div><div class="drop-box"><h5>Не свойственно</h5><div class="drop-items" data-cat="n"></div></div></div>'
+'<div class="actions"><button class="btn primary" id="p35-dnd-check">Проверить</button><button class="btn" id="p35-dnd-reset">Сброс</button></div>'
+'<div class="feedback" id="p35-dnd-fb"></div></div>';
h += '<div class="wg"><div class="wg-header"><span class="wg-badge">IV-4</span><div class="wg-title">Тренажёр: 5 задач</div></div>'
+'<div class="wg-help">4+ — +15 XP.</div>'
+'<div id="p35-task"></div>'
+'<div class="score-display" style="margin-top:10px"><span>Задача: <b id="p35-task-i">1</b>/5</span><span>Правильно: <b id="p35-task-ok">0</b></span></div></div>';
box.innerHTML = h + secNavFor('p35') + readButton('p35');
renderMath(box); wireReadBtn('p35');
_p35_mir(); _p35_quiz(); _p35_dnd(); _p35_tasks();
}
function _p35_mir(){
const svg = document.getElementById('p35-sim'); if(!svg) return;
function d(){
const dist = +document.getElementById('p35-d').value;
document.getElementById('p35-dv').textContent = dist;
const cm = 12; /* px per cm */
const mirrorX = 230;
let s = '';
/* зеркало вертикальное */
s += window.OPTICS.mirrorPlane(mirrorX, 110, 160, 90);
/* предмет — стрелка слева */
const objX = mirrorX - dist*cm;
s += window.OPTICS.lightObject(objX, 150, 50, 'arrow');
s += '<text x="'+objX+'" y="180" text-anchor="middle" font-family="Inter,sans-serif" font-size="11" fill="#475569">предмет</text>';
/* мнимое изображение справа */
const imgX = mirrorX + dist*cm;
/* пунктирная стрелка-изображение */
s += '<line x1="'+imgX+'" y1="150" x2="'+imgX+'" y2="100" stroke="#7c3aed" stroke-width="2" stroke-dasharray="5 3"/>';
s += '<polygon points="'+imgX+',100 '+(imgX-4)+',107 '+(imgX+4)+',107" fill="#7c3aed" opacity="0.6"/>';
s += '<text x="'+imgX+'" y="180" text-anchor="middle" font-family="Inter,sans-serif" font-size="11" fill="#7c3aed">мнимое изобр.</text>';
/* линии-лучи */
/* луч от вершины предмета к зеркалу и обратно */
s += '<line x1="'+objX+'" y1="100" x2="'+mirrorX+'" y2="130" stroke="#fbbf24" stroke-width="1.4"/>';
s += '<line x1="'+mirrorX+'" y1="130" x2="'+(mirrorX-50)+'" y2="170" stroke="#fbbf24" stroke-width="1.4"/>';
/* продолжение в изображение */
s += '<line x1="'+mirrorX+'" y1="130" x2="'+imgX+'" y2="100" stroke="#fbbf24" stroke-width="1.4" stroke-dasharray="3 3" opacity="0.6"/>';
svg.innerHTML = s;
}
document.getElementById('p35-d').addEventListener('input', d); d();
}
function _p35_quiz(){
const QS = [
{st:'Изображение в плоском зеркале мнимое.', ans:'T', why:'Лучи не пересекаются за зеркалом — только их продолжения.'},
{st:'Размер изображения больше предмета.', ans:'F', why:'Размеры равны.'},
{st:'Изображение находится на том же расстоянии от зеркала, что и предмет.', ans:'T', why:'Симметрия.'},
{st:'Изображение перевёрнуто вверх ногами.', ans:'F', why:'Прямое (не перевёрнуто).'},
{st:'Зеркальное изображение нельзя проецировать на экран.', ans:'T', why:'Мнимое — невозможно поймать на экран.'},
{st:'Если предмет приближается к зеркалу, изображение тоже приближается.', ans:'T', why:'Симметрия сохраняется.'}
];
let i = 0, ok = 0;
function r(){
const q = QS[i]; const w = document.getElementById('p35-quiz');
w.innerHTML = '<div style="padding:10px 14px;background:rgba(15,23,42,.04);border-radius:9px;margin:8px 0">"'+q.st+'"</div>'
+'<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px"><button class="btn" data-p="T"><b>Правда</b></button><button class="btn" data-p="F"><b>Ложь</b></button></div>'
+'<div class="feedback" id="p35-q-fb"></div>';
document.getElementById('p35-quiz-r').textContent = (i+1);
document.getElementById('p35-quiz-ok').textContent = ok;
w.querySelectorAll('[data-p]').forEach(b=>{
b.addEventListener('click', ()=>{
if(b.disabled) return; w.querySelectorAll('[data-p]').forEach(x=>x.disabled=true);
const fb = document.getElementById('p35-q-fb');
if(b.dataset.p === q.ans){ ok++; fb.className='feedback ok'; fb.innerHTML='&#10003; '+q.why; addXp(3,'p35-q'); bumpProgress('p35',4); }
else { fb.className='feedback fail'; fb.innerHTML='&#10007; '+q.why; }
document.getElementById('p35-quiz-ok').textContent = ok;
});
});
}
document.getElementById('p35-quiz-next').addEventListener('click', ()=>{ i=(i+1)%QS.length; r(); });
r();
}
function _p35_dnd(){
const items = [
{id:'a',cat:'y',html:'мнимое'},{id:'b',cat:'y',html:'прямое'},{id:'c',cat:'y',html:'равное по размеру'},{id:'d',cat:'y',html:'симметричное'},
{id:'e',cat:'n',html:'действительное'},{id:'f',cat:'n',html:'перевёрнутое'},{id:'g',cat:'n',html:'увеличенное'},{id:'h',cat:'n',html:'на экране'}
];
const dnd = setupSorter({ poolId:'p35-dnd-pool', scopeSelector:'#sec-p35', cats:['y','n'], items, columnLayout:false });
document.getElementById('p35-dnd-check').addEventListener('click', ()=>{
const fb = document.getElementById('p35-dnd-fb'); let wr = 0;
items.forEach(it=>{ if(dnd.placed[it.id] !== it.cat) wr++; });
if(wr===0){ fb.className='feedback ok'; fb.innerHTML='&#10003; +15 XP'; addXp(15,'p35-dnd'); bumpProgress('p35',20); }
else { fb.className='feedback fail'; fb.innerHTML='&#10007; Ошибок: '+wr+'.'; }
});
document.getElementById('p35-dnd-reset').addEventListener('click', ()=>{ dnd.reset(); document.getElementById('p35-dnd-fb').style.display='none'; });
}
function _p35_tasks(){
const TASKS = [
{q:'Предмет на расстоянии 1,5 м от зеркала. На каком расстоянии (м) изображение от предмета?', ans:3, tol:0.05, why:'Изображение в 1,5 м за зеркалом, расстояние предмет-изобр = 3 м.'},
{q:'Высота предмета 30 см. Высота изображения (см)?', ans:30, tol:0.5, why:'Равны.'},
{q:'Человек двигается к зеркалу со скоростью 1 м/с. С какой скоростью он сближается со своим изображением?', ans:2, tol:0.1, why:'Оба двигаются по 1 м/с к плоскости — сближение 2 м/с.'},
{q:'Изображение в плоском зеркале мнимое? (1 = да, 0 = нет)', ans:1, tol:0.1, why:'Да, мнимое.'},
{q:'Угол между двумя плоскими зеркалами 90&#176;. Сколько изображений предмета возникнет?', ans:3, tol:0.1, why:'Два прямых + одно «двойное» = 3.'}
];
let i = 0, ok = 0, done = 0, aw = false;
function r(){
const t = TASKS[i]; const w = document.getElementById('p35-task');
w.innerHTML = '<div style="padding:10px 14px;background:rgba(15,23,42,.04);border-radius:9px;margin-bottom:10px"><b>'+(i+1)+'.</b> '+t.q+'</div>'
+'<div class="boss-row"><input type="number" step="0.1" class="tinp" id="p35-tinp" style="width:140px"><button class="btn primary" id="p35-tgo">Ответ</button><button class="btn" id="p35-thn">Подск.</button><button class="btn" id="p35-tn">След.</button></div>'
+'<div class="boss-hint-txt" id="p35-tht">'+t.why+'</div><div class="feedback" id="p35-tfb"></div>';
document.getElementById('p35-task-i').textContent = (i+1);
document.getElementById('p35-task-ok').textContent = ok;
document.getElementById('p35-tgo').addEventListener('click', ()=>{
const v = parseFloat((document.getElementById('p35-tinp').value || '').replace(',','.'));
const fb = document.getElementById('p35-tfb');
if(isNaN(v)){ fb.className='feedback fail'; fb.innerHTML='Число.'; return; }
done++;
if(Math.abs(v - t.ans) < t.tol){ ok++; fb.className='feedback ok'; fb.innerHTML='&#10003; '+t.why; addXp(4,'p35-t'); bumpProgress('p35',6); }
else { fb.className='feedback fail'; fb.innerHTML='&#10007; '+t.ans+'. '+t.why; }
document.getElementById('p35-task-ok').textContent = ok;
renderMath(w);
if(done >= TASKS.length && !aw && ok >= 4){ aw = true; setTimeout(()=>{ const f=document.getElementById('p35-tfb'); f.className='feedback ok'; f.innerHTML='&#10003; +15 XP'; addXp(15,'p35-bonus'); bumpProgress('p35',15); }, 500); }
});
document.getElementById('p35-thn').addEventListener('click', ()=>{ document.getElementById('p35-tht').classList.toggle('show'); });
document.getElementById('p35-tn').addEventListener('click', ()=>{ i=(i+1)%TASKS.length; r(); });
renderMath(w);
}
r();
}
/* === Заглушки для §36..§40 и финала — будут заполнены ниже === */
function build_p36(){ _stub_phaseN('p36','Преломление света','Phase 5 Wave 3'); }
function build_p37(){ _stub_phaseN('p37','Линзы. Оптическая сила','Phase 5 Wave 3'); }
function build_p38(){ _stub_phaseN('p38','Построение изображений в линзах','Phase 5 Wave 4'); }
function build_p39(){ _stub_phaseN('p39','Глаз как оптическая система','Phase 5 Wave 4'); }
function build_p40(){ _stub_phaseN('p40','Дефекты зрения. Очки','Phase 5 Wave 4'); }
function build_final3(){ _stub_phaseN('final3','Финал главы 3','Phase 5 Wave 4'); }
function _stub_phaseN(id, name, ph){
const box = document.getElementById(id+'-body');
box.innerHTML = buildStub(id, name, ph) + secNavFor(id) + readButton(id);
renderMath(box); wireReadBtn(id);
}
function init(){
loadProgress(); initTheme(); initSidebarToggle(); initSearch();
buildParaSelector(); refreshProgressUI(); loadServerReadState(); goTo(PARAS[0].id);
setTimeout(()=>achievement('start'), 600);
if(window.LS&&window.LS.xp){
window.LS.xp.load().then(function(s){ if(s&&s.xp>STATE.xp){ STATE.xp=s.xp; STATE.level=calcLevel(STATE.xp); saveProgress(); refreshProgressUI(); if(STATE.current) buildSidebar(STATE.current); } });
}
}
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>