Files
Learn_System/frontend/textbooks/physics_8_ch3.html

2764 lines
219 KiB
HTML
Raw Permalink 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">
<link rel="stylesheet" href="/css/phys8-interactives.css">
<link rel="stylesheet" href="/css/phys8-design-system.css">
<link rel="stylesheet" href="/css/phys-textbook-widgets.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/phys8-helpers.js" defer></script>
<script src="/js/phys8-drag.js" defer></script>
<script src="/js/phys8-anim.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(min-width:981px){#sidebar-btn{display:none}.col-side-backdrop.show{display:none}}
@media(max-width:980px){
.col-side{position:fixed;top:0;right:0;height:100vh;width:300px;max-width:88vw;background:var(--bg);box-shadow:-12px 0 24px rgba(0,0,0,.18);padding:18px 16px;overflow-y:auto;transform:translateX(100%);transition:transform .25s ease;z-index:9991;max-height:none}
.col-side.open{transform:none}
}
.search-modal{position:fixed;inset:0;background:rgba(15,23,42,.55);backdrop-filter:blur(4px);z-index:9993;display:none;align-items:flex-start;justify-content:center;padding-top:14vh}
.search-modal.show{display:flex}
.search-box{background:var(--bg);border:1px solid var(--border);border-radius:14px;width:560px;max-width:92vw;max-height:70vh;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 24px 64px rgba(0,0,0,.4)}
.search-input{padding:14px 16px;font-size:1rem;border:0;border-bottom:1px solid var(--border);background:transparent;color:var(--text);outline:none}
.search-results{flex:1;overflow-y:auto;padding:6px 0}
.search-row{display:block;padding:8px 16px;cursor:pointer;border-bottom:1px solid var(--border);text-align:left;background:transparent;border:0;width:100%;color:var(--text)}
.search-row:hover,.search-row.active{background:var(--sec-acc-soft,var(--pri-soft))}
.search-row .sr-kind{font-size:.7rem;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:2px}
.search-row .sr-title{font-weight:700;font-size:.92rem;color:var(--text)}
.search-row .sr-desc{font-size:.8rem;color:var(--muted);margin-top:2px}
.search-empty{padding:20px;text-align:center;color:var(--muted);font-size:.88rem}
.search-foot{padding:8px 14px;border-top:1px solid var(--border);font-size:.74rem;color:var(--muted);display:flex;gap:14px}
.search-foot kbd{padding:2px 6px;background:var(--card);border:1px solid var(--border);border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:.72rem}
.sec{transition:opacity .25s}
</style>
</head>
<body class="p8-theme-spectrum">
<header class="p8-hero">
<div class="p8-hero-wm"><svg viewBox="0 0 100 100" aria-hidden="true">
<circle cx="50" cy="50" r="22" />
<g><line x1="50" y1="8" x2="50" y2="22" stroke="currentColor" stroke-width="4" stroke-linecap="round"/>
<line x1="50" y1="78" x2="50" y2="92" stroke="currentColor" stroke-width="4" stroke-linecap="round"/>
<line x1="8" y1="50" x2="22" y2="50" stroke="currentColor" stroke-width="4" stroke-linecap="round"/>
<line x1="78" y1="50" x2="92" y2="50" stroke="currentColor" stroke-width="4" stroke-linecap="round"/>
<line x1="20" y1="20" x2="30" y2="30" stroke="currentColor" stroke-width="4" stroke-linecap="round"/>
<line x1="70" y1="70" x2="80" y2="80" stroke="currentColor" stroke-width="4" stroke-linecap="round"/>
<line x1="80" y1="20" x2="70" y2="30" stroke="currentColor" stroke-width="4" stroke-linecap="round"/>
<line x1="30" y1="70" x2="20" y2="80" stroke="currentColor" stroke-width="4" stroke-linecap="round"/></g>
</svg></div>
<div class="p8-hero-meter" id="p8-meter-ch3"><span id="p8-meter-val">λ=550</span> нм</div>
<div class="p8-hero-inner">
<div class="p8-hero-eyebrow">Глава 3 · 9 параграфов</div>
<h1 class="p8-hero-title">Световые явления</h1>
<div class="p8-hero-sub">Лучи, тени, отражение, преломление, линзы, дисперсия, глаз. Перетаскивайте источники света и зеркала, наблюдайте за лучами и спектром.</div>
<div class="hdr-side" style="margin-top:18px;display:flex;gap:8px;flex-wrap:wrap;position:relative;z-index:1">
<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="p8-sec-wm" id="p8-sec-wm-p32" aria-hidden="true"><svg viewBox="0 0 100 100"><circle cx="50" cy="50" r="14" fill="currentColor"/><g stroke="currentColor" stroke-width="4" stroke-linecap="round"><line x1="50" y1="14" x2="50" y2="26"/><line x1="50" y1="74" x2="50" y2="86"/><line x1="14" y1="50" x2="26" y2="50"/><line x1="74" y1="50" x2="86" y2="50"/></g></svg></div><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="p8-sec-wm" id="p8-sec-wm-p33" aria-hidden="true"><svg viewBox="0 0 100 100"><circle cx="32" cy="40" r="10" fill="currentColor"/><rect x="50" y="34" width="14" height="40" fill="currentColor"/><polygon points="68,40 92,30 92,80 68,70" fill="currentColor" opacity="0.5"/></svg></div><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="p8-sec-wm" id="p8-sec-wm-p34" aria-hidden="true"><svg viewBox="0 0 100 100"><line x1="20" y1="20" x2="50" y2="50" stroke="currentColor" stroke-width="5"/><line x1="50" y1="50" x2="80" y2="20" stroke="currentColor" stroke-width="5"/><line x1="50" y1="50" x2="50" y2="90" stroke="currentColor" stroke-width="2" stroke-dasharray="4 4"/></svg></div><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="p8-sec-wm" id="p8-sec-wm-p35" aria-hidden="true"><svg viewBox="0 0 100 100"><line x1="30" y1="20" x2="30" y2="80" stroke="currentColor" stroke-width="4"/><g stroke="currentColor" stroke-width="1.5"><line x1="30" y1="30" x2="22" y2="34"/><line x1="30" y1="45" x2="22" y2="49"/><line x1="30" y1="60" x2="22" y2="64"/><line x1="30" y1="75" x2="22" y2="79"/></g><circle cx="60" cy="50" r="6" fill="currentColor"/></svg></div><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="p8-sec-wm" id="p8-sec-wm-p36" aria-hidden="true"><svg viewBox="0 0 100 100"><path d="M30 20 Q 20 50, 30 80" stroke="currentColor" stroke-width="5" fill="none"/><line x1="48" y1="50" x2="70" y2="50" stroke="currentColor" stroke-width="2" stroke-dasharray="3 3"/><circle cx="70" cy="50" r="3" fill="currentColor"/></svg></div><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="p8-sec-wm" id="p8-sec-wm-p37" aria-hidden="true"><svg viewBox="0 0 100 100"><line x1="20" y1="20" x2="50" y2="50" stroke="currentColor" stroke-width="5"/><line x1="50" y1="50" x2="80" y2="80" stroke="currentColor" stroke-width="5" stroke-dasharray="0"/><line x1="0" y1="50" x2="100" y2="50" stroke="currentColor" stroke-width="2"/></svg></div><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="p8-sec-wm" id="p8-sec-wm-p38" aria-hidden="true"><svg viewBox="0 0 100 100"><ellipse cx="50" cy="50" rx="10" ry="36" fill="currentColor" opacity="0.4"/><line x1="0" y1="50" x2="100" y2="50" stroke="currentColor" stroke-width="2"/><circle cx="25" cy="50" r="2" fill="currentColor"/><circle cx="75" cy="50" r="2" fill="currentColor"/></svg></div><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="p8-sec-wm" id="p8-sec-wm-p39" aria-hidden="true"><svg viewBox="0 0 100 100"><polygon points="40,20 80,50 40,80" stroke="currentColor" stroke-width="4" fill="none"/><line x1="20" y1="50" x2="40" y2="50" stroke="currentColor" stroke-width="3"/><g stroke-width="2.5" fill="none"><line x1="60" y1="40" x2="90" y2="30" stroke="#dc2626"/><line x1="60" y1="50" x2="90" y2="50" stroke="#16a34a"/><line x1="60" y1="60" x2="90" y2="70" stroke="#2563eb"/></g></svg></div><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="p8-sec-wm" id="p8-sec-wm-p40" aria-hidden="true"><svg viewBox="0 0 100 100"><ellipse cx="50" cy="50" rx="36" ry="22" fill="none" stroke="currentColor" stroke-width="3"/><circle cx="50" cy="50" r="12" fill="currentColor"/><circle cx="50" cy="50" r="5" fill="#fff"/></svg></div><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';
function _initP40_iv6(){
const sb = document.getElementById('p40-iv6-sandbox');
if (!sb || !window.P8Helpers) return;
const svg = P8Helpers.svg.create(560, 240);
svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block';
sb.appendChild(svg);
let mode = 'normal';
function render(){
svg.innerHTML = '';
/* Eye outline */
svg.appendChild(P8Helpers.svg.el('ellipse', { cx: 380, cy: 120, rx: 90, ry: 70, fill: '#fff', stroke: '#0f172a', 'stroke-width': 2 }));
/* Cornea */
svg.appendChild(P8Helpers.svg.el('path', { d: 'M 290 105 Q 270 120, 290 135', fill: '#bae6fd', stroke: '#0284c7', 'stroke-width': 2 }));
/* Lens */
svg.appendChild(P8Helpers.svg.el('ellipse', { cx: 310, cy: 120, rx: 8, ry: 26, fill: 'rgba(125,211,252,.55)', stroke: '#0284c7', 'stroke-width': 1.5 }));
/* Retina */
svg.appendChild(P8Helpers.svg.el('path', { d: 'M 440 80 Q 470 120, 440 160', stroke: '#dc2626', 'stroke-width': 3, fill: 'none' }));
svg.appendChild(P8Helpers.svg.el('text', { x: 465, y: 85, 'font-family':"'Inter',sans-serif", 'font-size':10, 'font-weight':700, fill:'#dc2626', text: 'сетчатка' }));
/* Rays */
const focusX = mode === 'normal' ? 440 : (mode === 'myop' ? 420 : 480);
const colorFocus = mode === 'normal' ? '#16a34a' : '#dc2626';
/* 3 incoming rays */
[80, 120, 160].forEach(y => {
svg.appendChild(P8Helpers.svg.el('line', { x1: 50, y1: y, x2: 305, y2: y, stroke: '#facc15', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 315, y1: y, x2: focusX, y2: 120, stroke: '#facc15', 'stroke-width': 2 }));
});
/* Focus point */
svg.appendChild(P8Helpers.svg.el('circle', { cx: focusX, cy: 120, r: 5, fill: colorFocus }));
/* Correction lens if needed */
if (mode === 'myop') {
svg.appendChild(P8Helpers.optics.lensSVG(180, 120, 70, 'diverging'));
svg.appendChild(P8Helpers.svg.el('text', { x: 180, y: 200, 'font-family':"'Inter',sans-serif", 'font-size':10, 'font-weight':700, fill:'#2563eb', 'text-anchor':'middle', text: '−дптр (рассеивающая)' }));
} else if (mode === 'hyper') {
svg.appendChild(P8Helpers.optics.lensSVG(180, 120, 70, 'converging'));
svg.appendChild(P8Helpers.svg.el('text', { x: 180, y: 200, 'font-family':"'Inter',sans-serif", 'font-size':10, 'font-weight':700, fill:'#dc2626', 'text-anchor':'middle', text: '+дптр (собирающая)' }));
}
/* Label */
const labels = { normal: 'Норма: фокус на сетчатке', myop: 'Близорукость: фокус перед сетчаткой', hyper: 'Дальнозоркость: фокус за сетчаткой' };
svg.appendChild(P8Helpers.svg.el('text', { x: 280, y: 222, 'font-family':"'JetBrains Mono',monospace", 'font-size':11, 'font-weight':700, fill:'#0f172a', 'text-anchor':'middle', text: labels[mode] }));
}
document.getElementById('p40-iv6-normal').onclick = () => { mode = 'normal'; render(); };
document.getElementById('p40-iv6-myop').onclick = () => { mode = 'myop'; render(); };
document.getElementById('p40-iv6-hyper').onclick = () => { mode = 'hyper'; render(); };
render();
}
function _initP39_iv6(){
const sb = document.getElementById('p39-iv6-sandbox');
if (!sb || !window.P8Helpers) return;
const svg = P8Helpers.svg.create(560, 240);
svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block';
sb.appendChild(svg);
function render(){
svg.innerHTML = '';
/* Incident white */
svg.appendChild(P8Helpers.svg.el('line', { x1: 30, y1: 120, x2: 200, y2: 120, stroke: '#fff', 'stroke-width': 5 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 30, y1: 120, x2: 200, y2: 120, stroke: '#facc15', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('text', { x: 30, y: 105, 'font-family':"'Inter',sans-serif", 'font-size':11, 'font-weight':700, fill:'#0f172a', text: 'Белый свет' }));
/* Prism */
svg.appendChild(P8Helpers.svg.el('polygon', { points: '200,180 280,40 360,180', fill: 'rgba(125,211,252,.35)', stroke: '#0284c7', 'stroke-width': 2 }));
/* Spectrum out */
const colors = [
{ c: '#dc2626', off: 0, label: 'красный' },
{ c: '#f97316', off: 8, label: 'оранжевый' },
{ c: '#facc15', off: 16, label: 'жёлтый' },
{ c: '#16a34a', off: 24, label: 'зелёный' },
{ c: '#0ea5e9', off: 32, label: 'голубой' },
{ c: '#2563eb', off: 40, label: 'синий' },
{ c: '#7c3aed', off: 48, label: 'фиолетовый' }
];
colors.forEach((cl, i) => {
svg.appendChild(P8Helpers.svg.el('line', { x1: 290, y1: 120, x2: 530, y2: 100 + cl.off, stroke: cl.c, 'stroke-width': 2.5, 'stroke-linecap': 'round' }));
svg.appendChild(P8Helpers.svg.el('text', { x: 535, y: 104 + cl.off, 'font-family':"'Inter',sans-serif", 'font-size':9, 'font-weight':700, fill: cl.c, text: cl.label }));
});
}
render();
}
function _initP38_iv6(){
const sb = document.getElementById('p38-iv6-sandbox');
if (!sb || !window.P8Helpers) return;
const svg = P8Helpers.svg.create(560, 280);
svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block';
sb.appendChild(svg);
let d = 180;
const F = 100, lensX = 320, axisY = 150;
function render(){
svg.innerHTML = '';
/* Axis */
svg.appendChild(P8Helpers.svg.el('line', { x1: 30, y1: axisY, x2: 530, y2: axisY, stroke: '#94a3b8', 'stroke-width': 1, 'stroke-dasharray': '4 3' }));
/* Lens */
svg.appendChild(P8Helpers.optics.lensSVG(lensX, axisY, 140, 'converging'));
/* F marks */
svg.appendChild(P8Helpers.svg.el('circle', { cx: lensX - F, cy: axisY, r: 3, fill: '#16a34a' }));
svg.appendChild(P8Helpers.svg.el('circle', { cx: lensX + F, cy: axisY, r: 3, fill: '#16a34a' }));
svg.appendChild(P8Helpers.svg.el('text', { x: lensX - F - 8, y: axisY + 18, 'font-family':"'JetBrains Mono',monospace", 'font-size':10, 'font-weight':700, fill:'#16a34a', text: 'F' }));
svg.appendChild(P8Helpers.svg.el('text', { x: lensX + F + 8, y: axisY + 18, 'font-family':"'JetBrains Mono',monospace", 'font-size':10, 'font-weight':700, fill:'#16a34a', text: 'F' }));
/* Object */
const objX = lensX - d;
const objH = 50;
svg.appendChild(P8Helpers.svg.el('line', { x1: objX, y1: axisY, x2: objX, y2: axisY - objH, stroke: '#dc2626', 'stroke-width': 3 }));
svg.appendChild(P8Helpers.svg.el('polygon', { points: objX+','+(axisY-objH-6)+' '+(objX-5)+','+(axisY-objH+2)+' '+(objX+5)+','+(axisY-objH+2), fill: '#dc2626' }));
/* Thin lens equation: 1/v - 1/(-d) = 1/F → v = dF/(d-F) (object on left, d>0) */
const v = (d * F) / (d - F);
const imgX = lensX + v;
const imgH = objH * v / d;
/* Three principal rays */
/* Ray 1: parallel to axis, refracts through far F */
svg.appendChild(P8Helpers.optics.rayLine(objX, axisY - objH, lensX, axisY - objH, { color: '#facc15', width: 1.5, arrow: false }));
svg.appendChild(P8Helpers.optics.rayLine(lensX, axisY - objH, imgX, axisY + imgH, { color: '#facc15', width: 1.5, arrow: false }));
/* Ray 2: through optic center */
svg.appendChild(P8Helpers.optics.rayLine(objX, axisY - objH, imgX, axisY + imgH, { color: '#16a34a', width: 1.5, arrow: false }));
/* Ray 3: through near F, refracts parallel */
svg.appendChild(P8Helpers.optics.rayLine(objX, axisY - objH, lensX, axisY + ((lensX - objX) / (lensX - F - objX)) * (-objH) - (-objH) * ((lensX - F - objX) / (lensX - F - objX) - 1), { color: '#2563eb', width: 1.5, arrow: false }));
/* Image */
svg.appendChild(P8Helpers.svg.el('line', { x1: imgX, y1: axisY, x2: imgX, y2: axisY + imgH, stroke: '#2563eb', 'stroke-width': 3 }));
svg.appendChild(P8Helpers.svg.el('polygon', { points: imgX+','+(axisY+imgH+6)+' '+(imgX-5)+','+(axisY+imgH-2)+' '+(imgX+5)+','+(axisY+imgH-2), fill: '#2563eb' }));
svg.appendChild(P8Helpers.svg.el('text', { x: 280, y: 260, 'font-family':"'JetBrains Mono',monospace", 'font-size':11, 'font-weight':700, fill:'#0f172a', 'text-anchor':'middle', text: 'd='+d+' мм, F='+F+', v='+v.toFixed(0)+' мм' }));
}
document.getElementById('p38-iv6-d').oninput = ev => { d = +ev.target.value; document.getElementById('p38-iv6-d-val').textContent = d; render(); };
render();
}
function _initP37_iv6(){
const sb = document.getElementById('p37-iv6-sandbox');
if (!sb || !window.P8Helpers) return;
const svg = P8Helpers.svg.create(560, 240);
svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block';
sb.appendChild(svg);
let alpha = 40;
const n1 = 1, n2 = 1.33;
function render(){
svg.innerHTML = '';
const cx = 280, cy = 120;
/* Water region */
svg.appendChild(P8Helpers.svg.el('rect', { x: 0, y: 120, width: 560, height: 120, fill: '#7dd3fc', opacity: 0.35 }));
/* Interface */
svg.appendChild(P8Helpers.svg.el('line', { x1: 30, y1: cy, x2: 530, y2: cy, stroke: '#0f172a', 'stroke-width': 2 }));
/* Normal */
svg.appendChild(P8Helpers.svg.el('line', { x1: cx, y1: 20, x2: cx, y2: 220, stroke: '#475569', 'stroke-width': 1.5, 'stroke-dasharray': '4 3' }));
/* Incident */
const aRad = alpha * Math.PI / 180;
const len = 120;
const inX = cx - len * Math.sin(aRad);
const inY = cy - len * Math.cos(aRad);
svg.appendChild(P8Helpers.optics.rayLine(inX, inY, cx, cy, { color: '#facc15', width: 3, glow: true }));
/* Snell: n1 sin α = n2 sin β */
const beta = Math.asin(Math.min(1, n1 / n2 * Math.sin(aRad)));
const rX = cx + len * Math.sin(beta);
const rY = cy + len * Math.cos(beta);
svg.appendChild(P8Helpers.optics.rayLine(cx, cy, rX, rY, { color: '#facc15', width: 3, glow: true }));
/* Labels */
svg.appendChild(P8Helpers.svg.el('text', { x: cx - 25, y: 70, 'font-family':"'JetBrains Mono',monospace", 'font-size':13, 'font-weight':800, fill:'#dc2626', text: 'α='+alpha+'°' }));
svg.appendChild(P8Helpers.svg.el('text', { x: cx + 8, y: 175, 'font-family':"'JetBrains Mono',monospace", 'font-size':13, 'font-weight':800, fill:'#16a34a', text: 'β='+(beta * 180 / Math.PI).toFixed(1)+'°' }));
svg.appendChild(P8Helpers.svg.el('text', { x: 50, y: 50, 'font-family':"'Inter',sans-serif", 'font-size':11, 'font-weight':700, fill:'#475569', text: 'n₁=1 (воздух)' }));
svg.appendChild(P8Helpers.svg.el('text', { x: 50, y: 215, 'font-family':"'Inter',sans-serif", 'font-size':11, 'font-weight':700, fill:'#0f172a', text: 'n₂=1.33 (вода)' }));
}
document.getElementById('p37-iv6-a').oninput = ev => { alpha = +ev.target.value; document.getElementById('p37-iv6-a-val').textContent = alpha; render(); };
render();
}
function _initP36_iv6(){
const sb = document.getElementById('p36-iv6-sandbox');
if (!sb || !window.P8Helpers) return;
const svg = P8Helpers.svg.create(560, 240);
svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block';
sb.appendChild(svg);
const F = 100, mirX = 480;
let d = 180;
function render(){
svg.innerHTML = '';
/* Mirror curve */
svg.appendChild(P8Helpers.svg.el('path', { d: 'M '+mirX+' 60 Q '+(mirX-30)+' 120, '+mirX+' 180', stroke: '#0f172a', 'stroke-width': 4, fill: 'none' }));
/* Axis */
svg.appendChild(P8Helpers.svg.el('line', { x1: 50, y1: 120, x2: mirX, y2: 120, stroke: '#94a3b8', 'stroke-width': 1, 'stroke-dasharray': '4 3' }));
/* Focus */
svg.appendChild(P8Helpers.svg.el('circle', { cx: mirX - F, cy: 120, r: 3, fill: '#16a34a' }));
svg.appendChild(P8Helpers.svg.el('text', { x: mirX - F, y: 138, 'font-family':"'JetBrains Mono',monospace", 'font-size':10, 'font-weight':700, fill:'#16a34a', 'text-anchor':'middle', text: 'F' }));
/* Object */
const objX = mirX - d;
svg.appendChild(P8Helpers.svg.el('line', { x1: objX, y1: 120, x2: objX, y2: 80, stroke: '#dc2626', 'stroke-width': 2.5 }));
svg.appendChild(P8Helpers.svg.el('polygon', { points: objX+',75 '+(objX-4)+',82 '+(objX+4)+',82', fill: '#dc2626' }));
/* Lens formula: 1/v - 1/d = 1/F, here mirror equation: 1/v + 1/d = 1/F (using d positive in front) */
const v = 1 / (1 / F - 1 / d);
const imgX = mirX - v;
const h_img = 40 * v / d * -1;
svg.appendChild(P8Helpers.svg.el('line', { x1: imgX, y1: 120, x2: imgX, y2: 120 + h_img, stroke: '#2563eb', 'stroke-width': 2.5, 'stroke-dasharray': v < 0 ? '4 3' : '0' }));
svg.appendChild(P8Helpers.svg.el('polygon', { points: imgX+','+(120 + h_img - Math.sign(h_img) * 4)+' '+(imgX-4)+','+(120 + h_img + 1)+' '+(imgX+4)+','+(120 + h_img + 1), fill: '#2563eb' }));
svg.appendChild(P8Helpers.svg.el('text', { x: 280, y: 220, 'font-family':"'JetBrains Mono',monospace", 'font-size':11, 'font-weight':700, fill:'#0f172a', 'text-anchor':'middle', text: 'd='+d+' мм, F='+F+', v='+v.toFixed(0)+' мм' }));
}
document.getElementById('p36-iv6-d').oninput = ev => { d = +ev.target.value; document.getElementById('p36-iv6-d-val').textContent = d; render(); };
render();
}
function _initP35_iv6(){
const sb = document.getElementById('p35-iv6-sandbox');
if (!sb || !window.P8Helpers) return;
const svg = P8Helpers.svg.create(560, 240);
svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block';
sb.appendChild(svg);
let d = 100;
function render(){
svg.innerHTML = '';
const mirX = 280;
/* Mirror */
svg.appendChild(P8Helpers.svg.el('line', { x1: mirX, y1: 40, x2: mirX, y2: 200, stroke: '#0f172a', 'stroke-width': 4 }));
/* Hatch */
for (let i = 0; i < 12; i++) {
svg.appendChild(P8Helpers.svg.el('line', { x1: mirX, y1: 45 + i * 14, x2: mirX + 8, y2: 49 + i * 14, stroke: '#475569', 'stroke-width': 1.5 }));
}
/* Object (arrow) */
const objX = mirX - d;
svg.appendChild(P8Helpers.svg.el('line', { x1: objX, y1: 180, x2: objX, y2: 90, stroke: '#dc2626', 'stroke-width': 3 }));
svg.appendChild(P8Helpers.svg.el('polygon', { points: objX+',85 '+(objX-6)+',95 '+(objX+6)+',95', fill: '#dc2626' }));
svg.appendChild(P8Helpers.svg.el('text', { x: objX, y: 220, 'font-family':"'Inter',sans-serif", 'font-size':11, 'font-weight':700, fill:'#dc2626', 'text-anchor':'middle', text: 'объект' }));
/* Virtual image */
const imgX = mirX + d;
svg.appendChild(P8Helpers.svg.el('line', { x1: imgX, y1: 180, x2: imgX, y2: 90, stroke: '#94a3b8', 'stroke-width': 3, 'stroke-dasharray': '4 3' }));
svg.appendChild(P8Helpers.svg.el('polygon', { points: imgX+',85 '+(imgX-6)+',95 '+(imgX+6)+',95', fill: '#94a3b8' }));
svg.appendChild(P8Helpers.svg.el('text', { x: imgX, y: 220, 'font-family':"'Inter',sans-serif", 'font-size':11, 'font-weight':700, fill:'#94a3b8', 'text-anchor':'middle', text: 'мнимое изображение' }));
/* Distance arrows */
svg.appendChild(P8Helpers.svg.el('line', { x1: objX, y1: 60, x2: mirX, y2: 60, stroke: '#475569', 'stroke-width': 1.5 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: mirX, y1: 60, x2: imgX, y2: 60, stroke: '#475569', 'stroke-width': 1.5 }));
svg.appendChild(P8Helpers.svg.el('text', { x: (objX + mirX) / 2, y: 52, 'font-family':"'JetBrains Mono',monospace", 'font-size':11, 'font-weight':700, fill:'#475569', 'text-anchor':'middle', text: 'd='+d }));
svg.appendChild(P8Helpers.svg.el('text', { x: (mirX + imgX) / 2, y: 52, 'font-family':"'JetBrains Mono',monospace", 'font-size':11, 'font-weight':700, fill:'#475569', 'text-anchor':'middle', text: 'd='+d }));
}
document.getElementById('p35-iv6-d').oninput = ev => { d = +ev.target.value; document.getElementById('p35-iv6-d-val').textContent = d; render(); };
render();
}
function _initP34_iv6(){
const sb = document.getElementById('p34-iv6-sandbox');
if (!sb || !window.P8Helpers) return;
const svg = P8Helpers.svg.create(560, 240);
svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block';
sb.appendChild(svg);
let alpha = 35;
function render(){
svg.innerHTML = '';
const cx = 280, cy = 200;
/* Mirror */
svg.appendChild(P8Helpers.optics.mirrorPlane(80, 200, 480, 200));
/* Normal */
svg.appendChild(P8Helpers.svg.el('line', { x1: cx, y1: 200, x2: cx, y2: 30, stroke: '#475569', 'stroke-width': 1.5, 'stroke-dasharray': '5 3' }));
svg.appendChild(P8Helpers.svg.el('text', { x: cx + 8, y: 40, 'font-family':"'Inter',sans-serif", 'font-size':11, 'font-weight':700, fill:'#475569', text: 'нормаль' }));
/* Incident ray */
const rad = alpha * Math.PI / 180;
const len = 150;
const inX = cx - len * Math.sin(rad);
const inY = cy - len * Math.cos(rad);
svg.appendChild(P8Helpers.optics.rayLine(inX, inY, cx, cy, { color: '#facc15', width: 3, glow: true }));
/* Reflected ray */
const rX = cx + len * Math.sin(rad);
const rY = cy - len * Math.cos(rad);
svg.appendChild(P8Helpers.optics.rayLine(cx, cy, rX, rY, { color: '#facc15', width: 3, glow: true }));
/* Angle labels */
svg.appendChild(P8Helpers.svg.el('text', { x: cx - 25, y: 130, 'font-family':"'JetBrains Mono',monospace", 'font-size':13, 'font-weight':800, fill:'#dc2626', text: 'α='+alpha+'°' }));
svg.appendChild(P8Helpers.svg.el('text', { x: cx + 8, y: 130, 'font-family':"'JetBrains Mono',monospace", 'font-size':13, 'font-weight':800, fill:'#16a34a', text: 'β='+alpha+'°' }));
}
document.getElementById('p34-iv6-a').oninput = ev => { alpha = +ev.target.value; document.getElementById('p34-iv6-a-val').textContent = alpha; render(); };
render();
}
function _initP33_iv6(){
const sb = document.getElementById('p33-iv6-sandbox');
if (!sb || !window.P8Helpers) return;
const svg = P8Helpers.svg.create(560, 240);
svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block';
sb.appendChild(svg);
let lampX = 80;
function render(){
svg.innerHTML = '';
/* Light */
svg.appendChild(P8Helpers.svg.el('circle', { cx: lampX, cy: 120, r: 12, fill: '#facc15', stroke: '#0f172a', 'stroke-width': 2 }));
/* Object */
const objX = 300;
svg.appendChild(P8Helpers.svg.el('rect', { x: objX - 12, y: 90, width: 24, height: 60, fill: '#475569' }));
/* Rays + shadow */
const wallX = 510;
const t = (wallX - lampX) / (objX - lampX);
const yTop = 120 + (90 - 120) * t;
const yBot = 120 + (150 - 120) * t;
svg.appendChild(P8Helpers.svg.el('line', { x1: lampX + 12, y1: 110, x2: wallX, y2: yTop, stroke: '#facc15', 'stroke-width': 1.5, 'stroke-dasharray': '3 3' }));
svg.appendChild(P8Helpers.svg.el('line', { x1: lampX + 12, y1: 130, x2: wallX, y2: yBot, stroke: '#facc15', 'stroke-width': 1.5, 'stroke-dasharray': '3 3' }));
/* Wall */
svg.appendChild(P8Helpers.svg.el('line', { x1: wallX, y1: 20, x2: wallX, y2: 220, stroke: '#0f172a', 'stroke-width': 3 }));
/* Shadow on wall */
svg.appendChild(P8Helpers.svg.el('rect', { x: wallX, y: yTop, width: 28, height: yBot - yTop, fill: '#0f172a', opacity: 0.7 }));
svg.appendChild(P8Helpers.svg.el('text', { x: wallX + 14, y: yTop - 5, 'font-family':"'JetBrains Mono',monospace", 'font-size':10, 'font-weight':700, fill:'#0f172a', 'text-anchor':'middle', text: 'h='+(yBot-yTop).toFixed(0) }));
}
document.getElementById('p33-iv6-x').oninput = ev => { lampX = +ev.target.value; document.getElementById('p33-iv6-x-val').textContent = lampX; render(); };
render();
}
function _initP32_iv6(){
const sb = document.getElementById('p32-iv6-sandbox');
if (!sb || !window.P8Helpers) return;
const svg = P8Helpers.svg.create(560, 240);
svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block';
sb.appendChild(svg);
let mode = 'point';
function render(){
svg.innerHTML = '';
/* Object */
svg.appendChild(P8Helpers.svg.el('rect', { x: 230, y: 90, width: 30, height: 60, fill: '#475569' }));
if (mode === 'point') {
svg.appendChild(P8Helpers.svg.el('circle', { cx: 80, cy: 120, r: 12, fill: '#facc15', stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 92, y1: 110, x2: 230, y2: 90, stroke: '#facc15', 'stroke-width': 2, 'stroke-dasharray': '3 3' }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 92, y1: 130, x2: 230, y2: 150, stroke: '#facc15', 'stroke-width': 2, 'stroke-dasharray': '3 3' }));
/* Sharp shadow */
svg.appendChild(P8Helpers.svg.el('polygon', { points: '260,90 460,30 460,210 260,150', fill: '#0f172a', opacity: 0.7 }));
svg.appendChild(P8Helpers.svg.el('text', { x: 380, y: 220, 'font-family':"'Inter',sans-serif", 'font-size':12, 'font-weight':700, fill: '#fff', 'text-anchor':'middle', text: 'Чёткая тень' }));
} else {
/* Sun */
svg.appendChild(P8Helpers.svg.el('circle', { cx: 80, cy: 120, r: 30, fill: '#facc15', stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 110, y1: 90, x2: 230, y2: 90, stroke: '#facc15', 'stroke-width': 1.5, 'stroke-dasharray': '3 3' }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 110, y1: 150, x2: 230, y2: 150, stroke: '#facc15', 'stroke-width': 1.5, 'stroke-dasharray': '3 3' }));
/* Sharp inner shadow (umbra) */
svg.appendChild(P8Helpers.svg.el('polygon', { points: '260,100 380,80 380,160 260,140', fill: '#0f172a', opacity: 0.7 }));
/* Penumbra */
svg.appendChild(P8Helpers.svg.el('polygon', { points: '260,90 460,30 380,80 260,100', fill: '#0f172a', opacity: 0.3 }));
svg.appendChild(P8Helpers.svg.el('polygon', { points: '260,150 380,160 460,210 260,150', fill: '#0f172a', opacity: 0.3 }));
svg.appendChild(P8Helpers.svg.el('text', { x: 320, y: 220, 'font-family':"'Inter',sans-serif", 'font-size':12, 'font-weight':700, fill: '#0f172a', 'text-anchor':'middle', text: 'Тень + полутень' }));
}
}
document.getElementById('p32-iv6-point').onclick = () => { mode = 'point'; render(); };
document.getElementById('p32-iv6-ext').onclick = () => { mode = 'ext'; render(); };
render();
}
function _initp39_iv5(){
const TASKS = [{"q":"Из скольких основных цветов состоит спектр белого света (радуга)?","ans":7,"tol":0.1,"why":"$7$ цветов: красный, оранжевый, жёлтый, зелёный, голубой, синий, фиолетовый."},{"q":"У какого цвета света наибольшая длина волны: ($1$ — красный, $2$ — синий, $3$ — фиолетовый, $4$ — зелёный)?","ans":1,"tol":0.1,"why":"Красный свет имеет наибольшую длину волны ($\\sim 700$ нм) среди видимого спектра."},{"q":"У какого цвета света наименьшая длина волны: ($1$ — красный, $2$ — жёлтый, $3$ — зелёный, $4$ — фиолетовый)?","ans":4,"tol":0.1,"why":"Фиолетовый свет имеет наименьшую длину волны ($\\sim 400$ нм)."},{"q":"Линза с фокусом $F = 25$ см. Оптическая сила $D = 1/F$ (дптр). Найдите $D$ при $F$ в метрах.","ans":4,"tol":0.1,"why":"$F = 0{,}25$ м, $D = 1/F = 1/0{,}25 = 4$ дптр."},{"q":"У близорукого человека очки $-2$ дптр. Найдите фокусное расстояние линзы $F$ в м.","ans":-0.5,"tol":0.02,"why":"$F = 1/D = 1/(-2) = -0{,}5$ м (рассеивающая линза)."}];
let i = 0, ok = 0, awarded = false;
function render(){
const t = TASKS[i]; const wrap = document.getElementById('p39-tasks5'); if(!wrap) return;
wrap.innerHTML =
'<div style="padding:10px 14px;background:rgba(15,23,42,.04);border-radius:9px;margin-bottom:10px;font-size:.95rem;line-height:1.55"><b>Задача '+(i+1)+'.</b> '+t.q+'</div>'
+'<div class="actions"><input type="number" step="0.001" class="tinp" id="p39-iv5-inp" placeholder="число" style="width:140px">'
+'<button class="btn primary" id="p39-iv5-go">Ответ</button>'
+'<button class="btn" id="p39-iv5-hint">Подсказка</button>'
+'<button class="btn" id="p39-iv5-next">Следующая</button></div>'
+'<details class="spoiler" id="p39-iv5-why-wrap" style="margin-top:8px;display:none"><summary>Решение</summary><div class="spoiler-body">'+t.why+'</div></details>'
+'<div class="feedback" id="p39-iv5-fb"></div>';
if (window.renderMathInElement) try { renderMathInElement(wrap, {delimiters:[{left:'$',right:'$',display:false}],throwOnError:false}); } catch(e){}
document.getElementById('p39-iv5-go').onclick = () => {
const v = parseFloat(document.getElementById('p39-iv5-inp').value.replace(',','.'));
const fb = document.getElementById('p39-iv5-fb');
const wh = document.getElementById('p39-iv5-why-wrap');
if (Math.abs(v - t.ans) <= t.tol) {
fb.className = 'feedback ok'; fb.innerHTML = 'Верно!'; ok++;
document.getElementById('p39-tasks5-ok').textContent = ok;
wh.style.display = 'block';
} else {
fb.className = 'feedback fail'; fb.innerHTML = 'Не совсем. Ожидался $' + t.ans + '$. Загляни в подсказку.';
if (window.renderMathInElement) try { renderMathInElement(fb, {delimiters:[{left:'$',right:'$',display:false}],throwOnError:false}); } catch(e){}
}
};
document.getElementById('p39-iv5-hint').onclick = () => {
const wh = document.getElementById('p39-iv5-why-wrap');
wh.style.display = wh.style.display === 'block' ? 'none' : 'block';
};
document.getElementById('p39-iv5-next').onclick = () => {
i = (i + 1) % TASKS.length;
document.getElementById('p39-tasks5-i').textContent = i + 1;
render();
if (ok === TASKS.length && !awarded) { awarded = true; if (typeof addXp === 'function') addXp(20, 'p39-iv5'); }
};
}
render();
}
function _initp32_iv5(){
const TASKS = [{"q":"Скорость света в вакууме $c = 3 \\cdot 10^{8}$ м/с. За какое время (мкс) свет пройдёт $L = 300$ км?","ans":1000,"tol":10,"why":"$t = L/c = 3 \\cdot 10^{5} / 3 \\cdot 10^{8} = 10^{-3}$ с $= 1000$ мкс."},{"q":"Свет от Солнца достигает Земли за $t = 500$ с. Какое расстояние (в км)? Ответ $\\times 10^{8}$.","ans":1.5,"tol":0.05,"why":"$L = ct = 3 \\cdot 10^{8} \\cdot 500 = 1{,}5 \\cdot 10^{11}$ м $= 1{,}5 \\cdot 10^{8}$ км."},{"q":"Сколько секунд лётит свет от Луны до Земли, если расстояние $L = 384\\,000$ км?","ans":1.28,"tol":0.05,"why":"$t = L/c = 3{,}84 \\cdot 10^{8} / 3 \\cdot 10^{8} = 1{,}28$ с."},{"q":"Свет звезды доходит до нас за $4$ года. Сколько $4$ световых лет в км? Ответ $\\times 10^{13}$.","ans":3.78,"tol":0.05,"why":"$1$ год $\\approx 3{,}15 \\cdot 10^{7}$ с. $L = c \\cdot 4 \\cdot 3{,}15 \\cdot 10^{7} = 3{,}78 \\cdot 10^{16}$ м $= 3{,}78 \\cdot 10^{13}$ км."},{"q":"Какой из источников света — точечный с практической точки зрения: ($1$ — Солнце на небе для нас, $2$ — лампа в комнате с метра, $3$ — звезда)?","ans":3,"tol":0.1,"why":"Звёзды настолько далеки, что их можно считать точечными источниками света. Солнце и лампа — нет."}];
let i = 0, ok = 0, awarded = false;
function render(){
const t = TASKS[i]; const wrap = document.getElementById('p32-tasks5'); if(!wrap) return;
wrap.innerHTML =
'<div style="padding:10px 14px;background:rgba(15,23,42,.04);border-radius:9px;margin-bottom:10px;font-size:.95rem;line-height:1.55"><b>Задача '+(i+1)+'.</b> '+t.q+'</div>'
+'<div class="actions"><input type="number" step="0.001" class="tinp" id="p32-iv5-inp" placeholder="число" style="width:140px">'
+'<button class="btn primary" id="p32-iv5-go">Ответ</button>'
+'<button class="btn" id="p32-iv5-hint">Подсказка</button>'
+'<button class="btn" id="p32-iv5-next">Следующая</button></div>'
+'<details class="spoiler" id="p32-iv5-why-wrap" style="margin-top:8px;display:none"><summary>Решение</summary><div class="spoiler-body">'+t.why+'</div></details>'
+'<div class="feedback" id="p32-iv5-fb"></div>';
if (window.renderMathInElement) try { renderMathInElement(wrap, {delimiters:[{left:'$',right:'$',display:false}],throwOnError:false}); } catch(e){}
document.getElementById('p32-iv5-go').onclick = () => {
const v = parseFloat(document.getElementById('p32-iv5-inp').value.replace(',','.'));
const fb = document.getElementById('p32-iv5-fb');
const wh = document.getElementById('p32-iv5-why-wrap');
if (Math.abs(v - t.ans) <= t.tol) {
fb.className = 'feedback ok'; fb.innerHTML = 'Верно!'; ok++;
document.getElementById('p32-tasks5-ok').textContent = ok;
wh.style.display = 'block';
} else {
fb.className = 'feedback fail'; fb.innerHTML = 'Не совсем. Ожидался $' + t.ans + '$. Загляни в подсказку.';
if (window.renderMathInElement) try { renderMathInElement(fb, {delimiters:[{left:'$',right:'$',display:false}],throwOnError:false}); } catch(e){}
}
};
document.getElementById('p32-iv5-hint').onclick = () => {
const wh = document.getElementById('p32-iv5-why-wrap');
wh.style.display = wh.style.display === 'block' ? 'none' : 'block';
};
document.getElementById('p32-iv5-next').onclick = () => {
i = (i + 1) % TASKS.length;
document.getElementById('p32-tasks5-i').textContent = i + 1;
render();
if (ok === TASKS.length && !awarded) { awarded = true; if (typeof addXp === 'function') addXp(20, 'p32-iv5'); }
};
}
render();
}
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>';
/* IV5 — Расчётные задачи (auto-injected) */
h += '<div class="wg">'
+'<div class="wg-header"><span class="wg-badge">IV-5</span><div class="wg-title">Тренажёр: 5 расчётных задач</div></div>'
+'<div class="wg-help">Введи числовой ответ (точка как разделитель). Решено все верно — +20 XP.</div>'
+'<div id="p32-tasks5"></div>'
+'<div class="score-display" style="margin-top:10px"><span>Задача: <b id="p32-tasks5-i">1</b> / 5</span><span>Правильно: <b id="p32-tasks5-ok">0</b></span></div>'
+'</div>';
/* IV6 — Точечные и протяжённые источники (Phase 3) */
h += '<div class="wg p8-iv6">'
+'<div class="wg-header"><span class="wg-badge p8-badge p8-badge-spectrum">IV-6</span><div class="wg-title">Точечные и протяжённые источники</div></div>'
+'<div class="wg-help">Точечный источник (свеча издалека) даёт чёткие тени. Протяжённый (Солнце) — размытые с полутенью.</div>'
+'<div class="p8-sandbox" id="p32-iv6-sandbox" style="height:240px"></div>'
+'<div style="margin-top:10px;display:flex;gap:10px;flex-wrap:wrap"><button class="btn primary" id="p32-iv6-point">Точечный</button><button class="btn" id="p32-iv6-ext">Протяжённый</button></div>'
+'</div>';
box.innerHTML = h + secNavFor('p32') + readButton('p32');
renderMath(box); wireReadBtn('p32');
_initP32_iv6();
_initp32_iv5();
_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>';
/* IV6 — Тень и её размер (Phase 3) */
h += '<div class="wg p8-iv6">'
+'<div class="wg-header"><span class="wg-badge p8-badge p8-badge-spectrum">IV-6</span><div class="wg-title">Тень и её размер</div></div>'
+'<div class="wg-help">Двигай источник света — наблюдай, как меняется размер тени. Чем ближе источник — тем больше тень.</div>'
+'<div class="p8-sandbox" id="p33-iv6-sandbox" style="height:240px"></div>'
+'<div class="p8-scrubber" style="margin-top:10px"><span class="p8-scrubber-label">Источник X</span><input type="range" id="p33-iv6-x" min="20" max="200" step="2" value="80"><span class="p8-scrubber-value"><span id="p33-iv6-x-val">80</span><span class="p8-unit">px</span></span></div>'
+'</div>';
box.innerHTML = h + secNavFor('p33') + readButton('p33');
renderMath(box); wireReadBtn('p33');
_initP33_iv6();
_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>';
/* IV6 — Закон отражения (Phase 3) */
h += '<div class="wg p8-iv6">'
+'<div class="wg-header"><span class="wg-badge p8-badge p8-badge-spectrum">IV-6</span><div class="wg-title">Закон отражения</div></div>'
+'<div class="wg-help">Двигай угол падения — угол отражения равен ему.</div>'
+'<div class="p8-sandbox" id="p34-iv6-sandbox" style="height:240px"></div>'
+'<div class="p8-scrubber" style="margin-top:10px"><span class="p8-scrubber-label">Угол α</span><input type="range" id="p34-iv6-a" min="0" max="80" step="1" value="35"><span class="p8-scrubber-value"><span id="p34-iv6-a-val">35</span><span class="p8-unit">°</span></span></div>'
+'</div>';
box.innerHTML = h + secNavFor('p34') + readButton('p34');
renderMath(box); wireReadBtn('p34');
_initP34_iv6();
_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>';
/* IV6 — Плоское зеркало (Phase 3) */
h += '<div class="wg p8-iv6">'
+'<div class="wg-header"><span class="wg-badge p8-badge p8-badge-spectrum">IV-6</span><div class="wg-title">Плоское зеркало</div></div>'
+'<div class="wg-help">Двигай объект — мнимое изображение появляется за зеркалом на том же расстоянии.</div>'
+'<div class="p8-sandbox" id="p35-iv6-sandbox" style="height:240px"></div>'
+'<div class="p8-scrubber" style="margin-top:10px"><span class="p8-scrubber-label">Дистанция d</span><input type="range" id="p35-iv6-d" min="50" max="200" step="2" value="100"><span class="p8-scrubber-value"><span id="p35-iv6-d-val">100</span><span class="p8-unit">px</span></span></div>'
+'</div>';
box.innerHTML = h + secNavFor('p35') + readButton('p35');
renderMath(box); wireReadBtn('p35');
_initP35_iv6();
_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 — Преломление света ======== */
function build_p36(){
const box = document.getElementById('p36-body'); let h = '';
h += makeCard('theory', 'Закон преломления', '§ 36.1',
'<p>При переходе из одной среды в другую луч света <b>меняет направление</b>. Это <b>преломление</b>.</p>'
+'<p>Закон Снеллиуса:</p>'
+'<p style="text-align:center;margin:8px 0">$$\\dfrac{\\sin\\alpha}{\\sin\\beta} = \\dfrac{n_2}{n_1} = n_{2/1}$$</p>'
+'<p>$n$ — <b>показатель преломления</b> среды. Чем больше $n$, тем сильнее замедляется свет.</p>'
);
h += makeCard('rule', 'Когда $\\alpha > \\beta$, а когда наоборот', '§ 36.2',
'<ul style="padding-left:20px;margin:6px 0">'
+'<li><b>Из менее плотной в более плотную</b> ($n_1 < n_2$): $\\alpha > \\beta$, луч приближается к нормали.</li>'
+'<li><b>Из более плотной в менее плотную</b>: $\\alpha < \\beta$, луч удаляется от нормали.</li>'
+'</ul>'
+'<p>При большом $\\alpha$ из плотной среды возможно <b>полное внутреннее отражение</b>: луч вообще не выходит.</p>'
+'<p>Таблица $n$:</p>'
+'<ul style="padding-left:20px;margin:6px 0"><li>воздух — $1{,}00$;</li><li>вода — $1{,}33$;</li><li>стекло — $1{,}5$;</li><li>алмаз — $2{,}42$.</li></ul>'
);
h += makeCard('example', 'Примеры', '§ 36.3',
'<ul style="padding-left:20px;margin:6px 0">'
+'<li>Карандаш в стакане воды кажется «сломанным».</li>'
+'<li>Рыба в воде кажется ближе, чем на самом деле.</li>'
+'<li>Мираж в пустыне — лучи от неба преломляются в тёплом воздухе.</li>'
+'<li>Призма раскладывает белый свет в радугу — разные цвета имеют разный $n$.</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">Меняй $\\alpha$ и $n$ — увидь, как меняется $\\beta$. При большом $\\alpha$ из плотной среды наступает полное внутреннее отражение.</div>'
+'<div class="sliders" style="margin-bottom:10px"><label>$\\alpha$, &#176;: <b id="p36-av">30</b><input type="range" id="p36-a" min="0" max="80" step="1" value="30"></label>'
+'<label>Из: <select id="p36-n1" class="tinp" style="width:auto;padding:6px 10px"><option value="1">воздух (n=1)</option><option value="1.33">вода (n=1.33)</option><option value="1.5">стекло (n=1.5)</option></select></label>'
+'<label>В: <select id="p36-n2" class="tinp" style="width:auto;padding:6px 10px"><option value="1">воздух (n=1)</option><option value="1.33" selected>вода (n=1.33)</option><option value="1.5">стекло (n=1.5)</option><option value="2.42">алмаз (n=2.42)</option></select></label></div>'
+'<svg id="p36-sim" viewBox="0 0 460 280" 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>$\\beta$ = <b id="p36-bv">22</b>&#176;</span><span id="p36-tir-lab" style="color:#dc2626;display:none;font-weight:700">Полное внутреннее отражение!</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">При переходе в указанную среду угол $\\alpha$ — больше или меньше $\\beta$?</div>'
+'<div id="p36-quiz"></div>'
+'<div class="actions"><button class="btn" id="p36-quiz-next">Следующий</button></div>'
+'<div class="score-display" style="margin-top:10px"><span>Раунд: <b id="p36-quiz-r">1</b>/5</span><span>Правильно: <b id="p36-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">Расставь по возрастанию $n$</div></div>'
+'<div id="p36-dnd-pool"></div>'
+'<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-top:10px"><div class="drop-box"><h5>1</h5><div class="drop-items" data-cat="r1"></div></div><div class="drop-box"><h5>2</h5><div class="drop-items" data-cat="r2"></div></div><div class="drop-box"><h5>3</h5><div class="drop-items" data-cat="r3"></div></div><div class="drop-box"><h5>4</h5><div class="drop-items" data-cat="r4"></div></div></div>'
+'<div class="actions"><button class="btn primary" id="p36-dnd-check">Проверить</button><button class="btn" id="p36-dnd-reset">Сброс</button></div>'
+'<div class="feedback" id="p36-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="p36-task"></div>'
+'<div class="score-display" style="margin-top:10px"><span>Задача: <b id="p36-task-i">1</b>/5</span><span>Правильно: <b id="p36-task-ok">0</b></span></div></div>';
/* IV6 — Сферическое зеркало (Phase 3) */
h += '<div class="wg p8-iv6">'
+'<div class="wg-header"><span class="wg-badge p8-badge p8-badge-spectrum">IV-6</span><div class="wg-title">Сферическое зеркало</div></div>'
+'<div class="wg-help">Двигай расстояние объекта до фокуса — изображение меняет тип (увеличенное/уменьшенное, прямое/перевёрнутое).</div>'
+'<div class="p8-sandbox" id="p36-iv6-sandbox" style="height:240px"></div>'
+'<div class="p8-scrubber" style="margin-top:10px"><span class="p8-scrubber-label">Объект → зеркало</span><input type="range" id="p36-iv6-d" min="50" max="250" step="2" value="180"><span class="p8-scrubber-value"><span id="p36-iv6-d-val">180</span><span class="p8-unit">мм</span></span></div>'
+'</div>';
box.innerHTML = h + secNavFor('p36') + readButton('p36');
renderMath(box); wireReadBtn('p36');
_initP36_iv6();
_p36_snell(); _p36_quiz(); _p36_dnd(); _p36_tasks();
}
function _p36_snell(){
const svg = document.getElementById('p36-sim'); if(!svg) return;
function d(){
const a = +document.getElementById('p36-a').value;
const n1 = +document.getElementById('p36-n1').value;
const n2 = +document.getElementById('p36-n2').value;
document.getElementById('p36-av').textContent = a;
const sinB = (n1/n2) * Math.sin(a*Math.PI/180);
const tir = Math.abs(sinB) > 1;
document.getElementById('p36-tir-lab').style.display = tir ? 'inline' : 'none';
if(tir){
document.getElementById('p36-bv').textContent = '—';
} else {
const b = Math.asin(sinB) * 180/Math.PI;
document.getElementById('p36-bv').textContent = b.toFixed(1);
}
const result = window.OPTICS.refractRay(230, 140, a, n1, n2, 90);
let bg = '';
/* верхняя среда */
bg += '<rect x="0" y="0" width="460" height="140" fill="rgba(186,230,253,0.25)"/>';
/* нижняя среда */
bg += '<rect x="0" y="140" width="460" height="140" fill="rgba(59,130,246,0.18)"/>';
bg += '<text x="20" y="30" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="#0f172a">n₁ = '+n1+'</text>';
bg += '<text x="20" y="270" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="#0f172a">n₂ = '+n2+'</text>';
svg.innerHTML = bg + result.svg;
}
['p36-a','p36-n1','p36-n2'].forEach(id => document.getElementById(id).addEventListener('input', d));
document.getElementById('p36-n1').addEventListener('change', d);
document.getElementById('p36-n2').addEventListener('change', d);
d();
}
function _p36_quiz(){
const QS = [
{sit:'воздух → вода', ans:'L', why:'В более плотную: $\\alpha > \\beta$, луч ближе к нормали.'},
{sit:'вода → воздух', ans:'B', why:'Из более плотной: $\\alpha < \\beta$.'},
{sit:'воздух → стекло', ans:'L', why:'$\\alpha > \\beta$.'},
{sit:'стекло → воздух', ans:'B', why:'$\\alpha < \\beta$.'},
{sit:'воздух → алмаз', ans:'L', why:'$n_{алмаза} = 2{,}42$, очень плотный — $\\alpha \\gg \\beta$.'}
];
let i = 0, ok = 0;
function r(){
const q = QS[i]; const w = document.getElementById('p36-quiz');
w.innerHTML = '<div style="padding:10px 14px;background:rgba(15,23,42,.04);border-radius:9px;margin:8px 0">'+q.sit+'</div>'
+'<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px"><button class="btn" data-p="L"><b>$\\alpha > \\beta$</b></button><button class="btn" data-p="B"><b>$\\alpha < \\beta$</b></button></div>'
+'<div class="feedback" id="p36-q-fb"></div>';
document.getElementById('p36-quiz-r').textContent = (i+1);
document.getElementById('p36-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('p36-q-fb');
if(b.dataset.p === q.ans){ ok++; fb.className='feedback ok'; fb.innerHTML='&#10003; '+q.why; addXp(3,'p36-q'); bumpProgress('p36',4); }
else { fb.className='feedback fail'; fb.innerHTML='&#10007; '+q.why; }
document.getElementById('p36-quiz-ok').textContent = ok;
renderMath(w);
});
});
renderMath(w);
}
document.getElementById('p36-quiz-next').addEventListener('click', ()=>{ i=(i+1)%QS.length; r(); });
r();
}
function _p36_dnd(){
/* по возрастанию n: воздух 1, вода 1.33, стекло 1.5, алмаз 2.42 */
const items = [
{id:'a',cat:'r1',html:'воздух (1)'},
{id:'w',cat:'r2',html:'вода (1{,}33)'},
{id:'g',cat:'r3',html:'стекло (1{,}5)'},
{id:'d',cat:'r4',html:'алмаз (2{,}42)'}
];
const dnd = setupSorter({ poolId:'p36-dnd-pool', scopeSelector:'#sec-p36', cats:['r1','r2','r3','r4'], items, columnLayout:false });
document.getElementById('p36-dnd-check').addEventListener('click', ()=>{
const fb = document.getElementById('p36-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,'p36-dnd'); bumpProgress('p36',20); }
else { fb.className='feedback fail'; fb.innerHTML='&#10007; Ошибок: '+wr+'.'; }
});
document.getElementById('p36-dnd-reset').addEventListener('click', ()=>{ dnd.reset(); document.getElementById('p36-dnd-fb').style.display='none'; });
}
function _p36_tasks(){
const TASKS = [
{q:'$n_1 = 1$, $n_2 = 1{,}5$, $\\alpha = 30$&#176;. Найди $\\beta$ (&#176;, до 0,1).', ans:19.5, tol:0.5, why:'$\\sin\\beta = \\sin30/1{,}5 = 0{,}333$, $\\beta = 19{,}5$&#176;.'},
{q:'$n_1 = 1{,}33$ (вода), $n_2 = 1$, $\\alpha = 45$&#176;. Найди $\\beta$ (&#176;).', ans:70.5, tol:1, why:'$\\sin\\beta = 1{,}33 \\sin45/1 = 0{,}941$, $\\beta \\approx 70{,}5$&#176;.'},
{q:'$\\alpha = 60$&#176;, воздух → стекло (n=1,5). Найди $\\beta$ (&#176;).', ans:35.3, tol:1, why:'$\\sin\\beta = \\sin60/1{,}5 = 0{,}577$, $\\beta \\approx 35{,}3$&#176;.'},
{q:'В алмазе ($n=2{,}42$) полное внутр. отражение начинается при $\\alpha > $? (&#176;)', ans:24.4, tol:1, why:'$\\sin\\alpha_{кр} = 1/n = 1/2{,}42 = 0{,}413$, $\\alpha_{кр} = 24{,}4$&#176;.'},
{q:'Свет идёт перпендикулярно поверхности ($\\alpha = 0$). Куда пойдёт?', ans:0, tol:0.5, why:'Без преломления, $\\beta = 0$.'}
];
let i = 0, ok = 0, done = 0, aw = false;
function r(){
const t = TASKS[i]; const w = document.getElementById('p36-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="p36-tinp" style="width:140px"><button class="btn primary" id="p36-tgo">Ответ</button><button class="btn" id="p36-thn">Подск.</button><button class="btn" id="p36-tn">След.</button></div>'
+'<div class="boss-hint-txt" id="p36-tht">'+t.why+'</div><div class="feedback" id="p36-tfb"></div>';
document.getElementById('p36-task-i').textContent = (i+1);
document.getElementById('p36-task-ok').textContent = ok;
document.getElementById('p36-tgo').addEventListener('click', ()=>{
const v = parseFloat((document.getElementById('p36-tinp').value || '').replace(',','.'));
const fb = document.getElementById('p36-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,'p36-t'); bumpProgress('p36',6); }
else { fb.className='feedback fail'; fb.innerHTML='&#10007; '+t.ans+'. '+t.why; }
document.getElementById('p36-task-ok').textContent = ok;
renderMath(w);
if(done >= TASKS.length && !aw && ok >= 4){ aw = true; setTimeout(()=>{ const f=document.getElementById('p36-tfb'); f.className='feedback ok'; f.innerHTML='&#10003; +15 XP'; addXp(15,'p36-bonus'); bumpProgress('p36',15); }, 500); }
});
document.getElementById('p36-thn').addEventListener('click', ()=>{ document.getElementById('p36-tht').classList.toggle('show'); });
document.getElementById('p36-tn').addEventListener('click', ()=>{ i=(i+1)%TASKS.length; r(); });
renderMath(w);
}
r();
}
/* ======== §37 — Линзы. Оптическая сила ======== */
function build_p37(){
const box = document.getElementById('p37-body'); let h = '';
h += makeCard('theory', 'Что такое линза', '§ 37.1',
'<p><b>Линза</b> — прозрачное тело, ограниченное двумя сферическими (или одной плоской и одной сферической) поверхностями.</p>'
+'<p>Два типа:</p>'
+'<ul style="padding-left:20px;margin:6px 0">'
+'<li><b>Собирающая (выпуклая)</b>: толще в середине. Параллельные лучи собираются в точке $F$ — фокусе.</li>'
+'<li><b>Рассеивающая (вогнутая)</b>: тоньше в середине. Параллельные лучи расходятся, как будто исходят из мнимого фокуса.</li>'
+'</ul>'
+'<p><b>Главная оптическая ось</b> — прямая через центры обеих сфер.</p>'
);
h += makeCard('rule', 'Оптическая сила', '§ 37.2',
'<p><b>Оптическая сила линзы</b>:</p>'
+'<p style="text-align:center;margin:8px 0">$$D = \\dfrac{1}{F}$$</p>'
+'<p>Единица — <b>диоптрия</b> (дптр) = $1/$м.</p>'
+'<ul style="padding-left:20px;margin:6px 0">'
+'<li>$D > 0$ — собирающая линза;</li>'
+'<li>$D < 0$ — рассеивающая;</li>'
+'<li>чем больше $|D|$, тем сильнее линза.</li>'
+'</ul>'
+'<p>Очки $+2$ дптр $\\to F = 0{,}5$ м (собирающая). Очки $-3$ дптр $\\to F = -0{,}33$ м (рассеивающая).</p>'
);
h += makeCard('example', 'Применение', '§ 37.3',
'<ul style="padding-left:20px;margin:6px 0">'
+'<li>Очки.</li>'
+'<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>Тип: <select id="p37-k" class="tinp" style="width:auto;padding:6px 10px"><option value="converging">собирающая</option><option value="diverging">рассеивающая</option></select></label>'
+'<label>$F$ (px): <b id="p37-fv">80</b><input type="range" id="p37-f" min="40" max="160" step="10" value="80"></label></div>'
+'<svg id="p37-sim" viewBox="0 0 460 200" 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">Калькулятор $D = 1/F$</div></div>'
+'<div class="sliders" style="margin-bottom:10px"><label>$F$, м: <b id="p37-fmv">0.5</b><input type="range" id="p37-fm" min="-2" max="2" step="0.05" value="0.5"></label></div>'
+'<div class="score-display" style="margin-top:8px"><span>$D$ = <b id="p37-dv">2.0</b> дптр</span><span>Тип: <b id="p37-tk">собирающая</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="p37-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="c"></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="p37-dnd-check">Проверить</button><button class="btn" id="p37-dnd-reset">Сброс</button></div>'
+'<div class="feedback" id="p37-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="p37-task"></div>'
+'<div class="score-display" style="margin-top:10px"><span>Задача: <b id="p37-task-i">1</b>/5</span><span>Правильно: <b id="p37-task-ok">0</b></span></div></div>';
/* IV6 — Преломление света (Phase 3) */
h += '<div class="wg p8-iv6">'
+'<div class="wg-header"><span class="wg-badge p8-badge p8-badge-spectrum">IV-6</span><div class="wg-title">Преломление света</div></div>'
+'<div class="wg-help">Двигай угол падения. На границе двух сред (воздух/вода n=1.33) угол преломления меньше.</div>'
+'<div class="p8-sandbox" id="p37-iv6-sandbox" style="height:240px"></div>'
+'<div class="p8-scrubber" style="margin-top:10px"><span class="p8-scrubber-label">Угол α</span><input type="range" id="p37-iv6-a" min="0" max="80" step="1" value="40"><span class="p8-scrubber-value"><span id="p37-iv6-a-val">40</span><span class="p8-unit">°</span></span></div>'
+'</div>';
box.innerHTML = h + secNavFor('p37') + readButton('p37');
renderMath(box); wireReadBtn('p37');
_initP37_iv6();
_p37_lens(); _p37_calc(); _p37_dnd(); _p37_tasks();
}
function _p37_lens(){
const svg = document.getElementById('p37-sim'); if(!svg) return;
function d(){
const k = document.getElementById('p37-k').value;
const F = +document.getElementById('p37-f').value;
document.getElementById('p37-fv').textContent = F;
svg.innerHTML = window.OPTICS.thinLens(230, 100, 70, F, k);
}
['p37-k','p37-f'].forEach(id => document.getElementById(id).addEventListener('input', d));
document.getElementById('p37-k').addEventListener('change', d);
d();
}
function _p37_calc(){
function u(){
const F = +document.getElementById('p37-fm').value;
document.getElementById('p37-fmv').textContent = F.toFixed(2);
if(F === 0){
document.getElementById('p37-dv').textContent = '∞';
document.getElementById('p37-tk').textContent = '—';
return;
}
const D = 1/F;
document.getElementById('p37-dv').textContent = D.toFixed(2);
document.getElementById('p37-tk').textContent = F > 0 ? 'собирающая' : 'рассеивающая';
}
document.getElementById('p37-fm').addEventListener('input', u); u();
}
function _p37_dnd(){
const items = [
{id:'a',cat:'c',html:'выпуклая (+)'},
{id:'b',cat:'c',html:'$D = +3$ дптр'},
{id:'c',cat:'c',html:'лупа'},
{id:'d',cat:'c',html:'хрусталик глаза'},
{id:'e',cat:'d',html:'вогнутая ()'},
{id:'f',cat:'d',html:'$D = -2$ дптр'},
{id:'g',cat:'d',html:'очки для близоруких'},
{id:'h',cat:'d',html:'рассеивает лучи'}
];
const dnd = setupSorter({ poolId:'p37-dnd-pool', scopeSelector:'#sec-p37', cats:['c','d'], items, columnLayout:false });
document.getElementById('p37-dnd-check').addEventListener('click', ()=>{
const fb = document.getElementById('p37-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,'p37-dnd'); bumpProgress('p37',20); renderMath(fb); }
else { fb.className='feedback fail'; fb.innerHTML='&#10007; Ошибок: '+wr+'.'; renderMath(fb); }
});
document.getElementById('p37-dnd-reset').addEventListener('click', ()=>{ dnd.reset(); document.getElementById('p37-dnd-fb').style.display='none'; });
}
function _p37_tasks(){
const TASKS = [
{q:'$F = 25$ см $= 0{,}25$ м. Найди $D$ (дптр).', ans:4, tol:0.1, why:'$D = 1/0{,}25 = 4$ дптр.'},
{q:'$D = +5$ дптр. Найди $F$ (см).', ans:20, tol:0.5, why:'$F = 1/5 = 0{,}2$ м $= 20$ см.'},
{q:'$D = -2$ дптр. Какой $F$ (м, с знаком)?', ans:-0.5, tol:0.05, why:'$F = 1/(-2) = -0{,}5$ м.'},
{q:'Очки +1.5 дптр. Какое фокусное расстояние (см)?', ans:67, tol:2, why:'$F = 1/1{,}5 \\approx 0{,}67$ м $= 67$ см.'},
{q:'У хрусталика глаза $D = 60$ дптр. Найди $F$ в мм.', ans:17, tol:1, why:'$F = 1/60 \\approx 0{,}017$ м = $17$ мм.'}
];
let i = 0, ok = 0, done = 0, aw = false;
function r(){
const t = TASKS[i]; const w = document.getElementById('p37-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.01" class="tinp" id="p37-tinp" style="width:140px"><button class="btn primary" id="p37-tgo">Ответ</button><button class="btn" id="p37-thn">Подск.</button><button class="btn" id="p37-tn">След.</button></div>'
+'<div class="boss-hint-txt" id="p37-tht">'+t.why+'</div><div class="feedback" id="p37-tfb"></div>';
document.getElementById('p37-task-i').textContent = (i+1);
document.getElementById('p37-task-ok').textContent = ok;
document.getElementById('p37-tgo').addEventListener('click', ()=>{
const v = parseFloat((document.getElementById('p37-tinp').value || '').replace(',','.'));
const fb = document.getElementById('p37-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,'p37-t'); bumpProgress('p37',6); }
else { fb.className='feedback fail'; fb.innerHTML='&#10007; '+t.ans+'. '+t.why; }
document.getElementById('p37-task-ok').textContent = ok;
renderMath(w);
if(done >= TASKS.length && !aw && ok >= 4){ aw = true; setTimeout(()=>{ const f=document.getElementById('p37-tfb'); f.className='feedback ok'; f.innerHTML='&#10003; +15 XP'; addXp(15,'p37-bonus'); bumpProgress('p37',15); }, 500); }
});
document.getElementById('p37-thn').addEventListener('click', ()=>{ document.getElementById('p37-tht').classList.toggle('show'); });
document.getElementById('p37-tn').addEventListener('click', ()=>{ i=(i+1)%TASKS.length; r(); });
renderMath(w);
}
r();
}
/* ======== §38 — Построение изображений в линзах ======== */
function build_p38(){
const box = document.getElementById('p38-body'); let h = '';
h += makeCard('theory', 'Формула тонкой линзы', '§ 38.1',
'<p>Расстояния от предмета $d$ и от изображения $f$ до линзы связаны:</p>'
+'<p style="text-align:center;margin:8px 0">$$\\dfrac{1}{F} = \\dfrac{1}{d} + \\dfrac{1}{f}$$</p>'
+'<p>Из неё $f = \\dfrac{dF}{d - F}$.</p>'
+'<p>Если $f > 0$ — изображение <b>действительное</b> (с той стороны линзы, на экране). Если $f < 0$ — <b>мнимое</b> (с той же стороны, где предмет).</p>'
);
h += makeCard('rule', '3 «золотых» луча', '§ 38.2',
'<ol style="padding-left:20px;margin:6px 0">'
+'<li>Луч <b>через центр</b> линзы — не преломляется.</li>'
+'<li>Луч <b>параллельный оси</b> — после линзы проходит через дальний $F$.</li>'
+'<li>Луч <b>через ближний $F$</b> — после линзы идёт параллельно оси.</li>'
+'</ol>'
+'<p>Любые два из этих лучей пересекутся в точке изображения.</p>'
);
h += makeCard('example', 'Зависимость от расстояния', '§ 38.3',
'<p>В собирающей линзе ($F > 0$):</p>'
+'<ul style="padding-left:20px;margin:6px 0">'
+'<li>$d > 2F$ — изображение <b>уменьшенное</b>, перевёрнутое, действительное (фотоаппарат).</li>'
+'<li>$d = 2F$ — равное, перевёрнутое, действительное.</li>'
+'<li>$F < d < 2F$ — <b>увеличенное</b>, перевёрнутое, действительное (проектор).</li>'
+'<li>$d = F$ — на бесконечности (параллельные лучи).</li>'
+'<li>$d < F$ — <b>увеличенное</b>, прямое, мнимое (лупа).</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">Меняй $F$ и $d$ — наблюдай, как меняется изображение. Зелёный — действительное, фиолетовый пунктир — мнимое.</div>'
+'<div class="sliders" style="margin-bottom:10px"><label>$F$, см: <b id="p38-fv">10</b><input type="range" id="p38-f" min="5" max="20" step="1" value="10"></label>'
+'<label>$d$, см: <b id="p38-dv">15</b><input type="range" id="p38-d" min="3" max="40" step="1" value="15"></label></div>'
+'<svg id="p38-sim" viewBox="0 0 600 240" style="width:100%;height:auto;background:#f8fafc;border-radius:9px;border:1px solid var(--border)"></svg>'
+'<div class="score-display" style="margin-top:8px;flex-direction:column;align-items:flex-start;gap:3px">'
+'<span>$f$ = <b id="p38-fs">30</b> см</span>'
+'<span>Увеличение: <b id="p38-mg">2.0</b>x</span>'
+'<span>Тип: <b id="p38-tp">действительное, перевёрнутое</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="p38-quiz"></div>'
+'<div class="actions"><button class="btn" id="p38-quiz-next">Следующий</button></div>'
+'<div class="score-display" style="margin-top:10px"><span>Раунд: <b id="p38-quiz-r">1</b>/5</span><span>Правильно: <b id="p38-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">DnD устройств</div></div>'
+'<div class="wg-help">К какому случаю относится прибор?</div>'
+'<div id="p38-dnd-pool"></div>'
+'<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-top:10px"><div class="drop-box"><h5>$d > 2F$ (фото)</h5><div class="drop-items" data-cat="far"></div></div><div class="drop-box"><h5>$F < d < 2F$ (проектор)</h5><div class="drop-items" data-cat="mid"></div></div><div class="drop-box"><h5>$d < F$ (лупа)</h5><div class="drop-items" data-cat="lupa"></div></div></div>'
+'<div class="actions"><button class="btn primary" id="p38-dnd-check">Проверить</button><button class="btn" id="p38-dnd-reset">Сброс</button></div>'
+'<div class="feedback" id="p38-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="p38-task"></div>'
+'<div class="score-display" style="margin-top:10px"><span>Задача: <b id="p38-task-i">1</b>/5</span><span>Правильно: <b id="p38-task-ok">0</b></span></div></div>';
/* IV6 — Собирающая линза — построение изображения (Phase 3) */
h += '<div class="wg p8-iv6">'
+'<div class="wg-header"><span class="wg-badge p8-badge p8-badge-spectrum">IV-6</span><div class="wg-title">Собирающая линза — построение изображения</div></div>'
+'<div class="wg-help">Двигай объект. Три главных луча: через центр (прямо), параллельно главной оси (через F), через F (параллельно оси).</div>'
+'<div class="p8-sandbox" id="p38-iv6-sandbox" style="height:280px"></div>'
+'<div class="p8-scrubber" style="margin-top:10px"><span class="p8-scrubber-label">Дистанция d</span><input type="range" id="p38-iv6-d" min="50" max="280" step="2" value="180"><span class="p8-scrubber-value"><span id="p38-iv6-d-val">180</span><span class="p8-unit">мм</span></span></div>'
+'</div>';
box.innerHTML = h + secNavFor('p38') + readButton('p38');
renderMath(box); wireReadBtn('p38');
_initP38_iv6();
_p38_constr(); _p38_quiz(); _p38_dnd(); _p38_tasks();
}
function _p38_constr(){
const svg = document.getElementById('p38-sim'); if(!svg) return;
function d(){
const F = +document.getElementById('p38-f').value;
const dist = +document.getElementById('p38-d').value;
document.getElementById('p38-fv').textContent = F;
document.getElementById('p38-dv').textContent = dist;
const res = window.OPTICS.buildLensImage(F, dist, 30);
const f = res.f, h2 = res.h2, virtual = res.virtual;
document.getElementById('p38-fs').textContent = isFinite(f) ? f.toFixed(1) : '∞';
document.getElementById('p38-mg').textContent = isFinite(f) ? Math.abs(h2/30).toFixed(2) : '∞';
const type = virtual ? (h2 > 0 ? 'мнимое, прямое, увеличенное' : 'мнимое, прямое') : (h2 < 0 ? 'действительное, перевёрнутое' : 'действительное');
document.getElementById('p38-tp').textContent = type;
/* Рисуем SVG */
const cx = 300, cy = 130;
const sc = 4; /* px per см */
let s = '';
/* ось */
s += '<line x1="50" y1="'+cy+'" x2="550" y2="'+cy+'" stroke="#cbd5e1" stroke-width="1.2"/>';
/* линза */
s += window.OPTICS.thinLens(cx, cy, 60, F*sc, 'converging');
/* предмет — слева */
const objX = cx - dist*sc;
const objH = 60; /* высота 30 см = 60 px */
s += '<line x1="'+objX+'" y1="'+cy+'" x2="'+objX+'" y2="'+(cy-objH)+'" stroke="#0f172a" stroke-width="3"/>';
s += '<polygon points="'+objX+','+(cy-objH)+' '+(objX-4)+','+(cy-objH+7)+' '+(objX+4)+','+(cy-objH+7)+'" fill="#0f172a"/>';
/* изображение */
if(isFinite(f) && !virtual){
const imgX = cx + f*sc;
const imgY = cy - h2*sc/15; /* h2 от исходн. h=30 — масштабируем */
s += '<line x1="'+imgX+'" y1="'+cy+'" x2="'+imgX+'" y2="'+(cy - h2*2)+'" stroke="#7c3aed" stroke-width="3"/>';
const tip = cy - h2*2;
s += '<polygon points="'+imgX+','+tip+' '+(imgX-4)+','+(tip+(h2>0?7:-7))+' '+(imgX+4)+','+(tip+(h2>0?7:-7))+'" fill="#7c3aed"/>';
} else if(isFinite(f) && virtual){
const imgX = cx + f*sc; /* отрицательное f → слева */
s += '<line x1="'+imgX+'" y1="'+cy+'" x2="'+imgX+'" y2="'+(cy - h2*2)+'" stroke="#a78bfa" stroke-width="2.5" stroke-dasharray="5 3"/>';
const tip = cy - h2*2;
s += '<polygon points="'+imgX+','+tip+' '+(imgX-4)+','+(tip+7)+' '+(imgX+4)+','+(tip+7)+'" fill="#a78bfa" opacity="0.6"/>';
}
svg.innerHTML = s;
}
document.getElementById('p38-f').addEventListener('input', d);
document.getElementById('p38-d').addEventListener('input', d);
d();
}
function _p38_quiz(){
const QS = [
{sit:'$d > 2F$ (например, $d = 30$, $F = 10$)', ans:'A', why:'Уменьшенное, перевёрнутое, действительное.'},
{sit:'$d = 2F$', ans:'B', why:'Равное, перевёрнутое, действительное.'},
{sit:'$F < d < 2F$ (проектор)', ans:'C', why:'Увеличенное, перевёрнутое, действительное.'},
{sit:'$d < F$ (лупа)', ans:'D', why:'Увеличенное, прямое, мнимое.'},
{sit:'$d = F$', ans:'E', why:'На бесконечности — параллельные лучи.'}
];
let i = 0, ok = 0;
function r(){
const q = QS[i]; const w = document.getElementById('p38-quiz');
w.innerHTML = '<div style="padding:10px 14px;background:rgba(15,23,42,.04);border-radius:9px;margin:8px 0">'+q.sit+'</div>'
+'<div style="display:grid;grid-template-columns:1fr;gap:6px">'
+'<button class="btn" data-p="A">A. Уменьш., перевёрн., действ.</button>'
+'<button class="btn" data-p="B">B. Равное, перевёрн., действ.</button>'
+'<button class="btn" data-p="C">C. Увелич., перевёрн., действ.</button>'
+'<button class="btn" data-p="D">D. Увелич., прямое, мнимое</button>'
+'<button class="btn" data-p="E">E. На бесконечности</button>'
+'</div><div class="feedback" id="p38-q-fb"></div>';
document.getElementById('p38-quiz-r').textContent = (i+1);
document.getElementById('p38-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('p38-q-fb');
if(b.dataset.p === q.ans){ ok++; fb.className='feedback ok'; fb.innerHTML='&#10003; '+q.why; addXp(3,'p38-q'); bumpProgress('p38',4); }
else { fb.className='feedback fail'; fb.innerHTML='&#10007; '+q.why; }
document.getElementById('p38-quiz-ok').textContent = ok;
});
});
}
document.getElementById('p38-quiz-next').addEventListener('click', ()=>{ i=(i+1)%QS.length; r(); });
r();
}
function _p38_dnd(){
const items = [
{id:'p',cat:'far',html:'фотоаппарат'},
{id:'eye',cat:'far',html:'глаз (далёкий предмет)'},
{id:'mp',cat:'mid',html:'проектор'},
{id:'film',cat:'mid',html:'кинопроектор'},
{id:'l1',cat:'lupa',html:'лупа'},
{id:'l2',cat:'lupa',html:'часы под лупой'}
];
const dnd = setupSorter({ poolId:'p38-dnd-pool', scopeSelector:'#sec-p38', cats:['far','mid','lupa'], items, columnLayout:false });
document.getElementById('p38-dnd-check').addEventListener('click', ()=>{
const fb = document.getElementById('p38-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,'p38-dnd'); bumpProgress('p38',20); }
else { fb.className='feedback fail'; fb.innerHTML='&#10007; Ошибок: '+wr+'.'; }
});
document.getElementById('p38-dnd-reset').addEventListener('click', ()=>{ dnd.reset(); document.getElementById('p38-dnd-fb').style.display='none'; });
}
function _p38_tasks(){
const TASKS = [
{q:'$F = 20$ см, $d = 30$ см. Найди $f$ (см).', ans:60, tol:1, why:'$f = dF/(d-F) = 30\\cdot20/(30-20) = 60$ см.'},
{q:'$F = 10$ см, $d = 15$ см. Найди $f$ (см).', ans:30, tol:1, why:'$f = 15\\cdot10/5 = 30$ см.'},
{q:'$F = 20$ см, $d = 60$ см. Найди $f$ (см).', ans:30, tol:1, why:'$f = 60\\cdot20/40 = 30$ см.'},
{q:'Предмет высотой 5 см, $d = 30$ см, $f = 60$ см. Найди высоту изображения (см).', ans:10, tol:0.5, why:'$h_2 = h \\cdot f/d = 5 \\cdot 60/30 = 10$ см.'},
{q:'$F = 15$ см, $d = 10$ см (лупа). Найди $|f|$ (см).', ans:30, tol:1, why:'$f = 10\\cdot15/(10-15) = -30$, |f| = 30 см. Мнимое.'}
];
let i = 0, ok = 0, done = 0, aw = false;
function r(){
const t = TASKS[i]; const w = document.getElementById('p38-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="p38-tinp" style="width:140px"><button class="btn primary" id="p38-tgo">Ответ</button><button class="btn" id="p38-thn">Подск.</button><button class="btn" id="p38-tn">След.</button></div>'
+'<div class="boss-hint-txt" id="p38-tht">'+t.why+'</div><div class="feedback" id="p38-tfb"></div>';
document.getElementById('p38-task-i').textContent = (i+1);
document.getElementById('p38-task-ok').textContent = ok;
document.getElementById('p38-tgo').addEventListener('click', ()=>{
const v = parseFloat((document.getElementById('p38-tinp').value || '').replace(',','.'));
const fb = document.getElementById('p38-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,'p38-t'); bumpProgress('p38',6); }
else { fb.className='feedback fail'; fb.innerHTML='&#10007; '+t.ans+'. '+t.why; }
document.getElementById('p38-task-ok').textContent = ok;
renderMath(w);
if(done >= TASKS.length && !aw && ok >= 4){ aw = true; setTimeout(()=>{ const f=document.getElementById('p38-tfb'); f.className='feedback ok'; f.innerHTML='&#10003; +15 XP'; addXp(15,'p38-bonus'); bumpProgress('p38',15); }, 500); }
});
document.getElementById('p38-thn').addEventListener('click', ()=>{ document.getElementById('p38-tht').classList.toggle('show'); });
document.getElementById('p38-tn').addEventListener('click', ()=>{ i=(i+1)%TASKS.length; r(); });
renderMath(w);
}
r();
}
/* ======== §39 — Глаз как оптическая система ======== */
function build_p39(){
const box = document.getElementById('p39-body'); let h = '';
h += makeCard('theory', 'Устройство глаза', '§ 39.1',
'<p>Глаз — сложная оптическая система:</p>'
+'<ul style="padding-left:20px;margin:6px 0">'
+'<li><b>Роговица</b> — прозрачная передняя оболочка, преломляет свет.</li>'
+'<li><b>Зрачок</b> — отверстие в радужной оболочке, регулирует количество света.</li>'
+'<li><b>Хрусталик</b> — биологическая собирающая линза с переменным $F$.</li>'
+'<li><b>Сетчатка</b> — задняя стенка глаза, на ней формируется изображение.</li>'
+'<li><b>Зрительный нерв</b> — передаёт сигнал в мозг.</li>'
+'</ul>'
);
h += makeCard('rule', 'Аккомодация', '§ 39.2',
'<p>Расстояние от хрусталика до сетчатки <b>фиксировано</b>. А расстояние до предметов меняется. Как глаз приспосабливается?</p>'
+'<p>Мышцы вокруг хрусталика меняют его кривизну (форму):</p>'
+'<ul style="padding-left:20px;margin:6px 0">'
+'<li>Далёкий предмет: хрусталик <b>уплощается</b>, $F$ растёт.</li>'
+'<li>Близкий предмет: хрусталик <b>округляется</b>, $F$ уменьшается.</li>'
+'</ul>'
+'<p>Это и есть <b>аккомодация</b>.</p>'
);
h += makeCard('example', 'Расстояние наилучшего зрения', '§ 39.3',
'<p>Расстояние, на котором глаз видит чётко без напряжения, — около <b>25 см</b>. На таком расстоянии обычно держат книгу при чтении.</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="p39-dv">далёкий предмет</b><input type="range" id="p39-d" min="0" max="100" step="10" value="0"></label></div>'
+'<svg id="p39-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="p39-quiz"></div>'
+'<div class="actions"><button class="btn" id="p39-quiz-next">Следующий</button></div>'
+'<div class="score-display" style="margin-top:10px"><span>Раунд: <b id="p39-quiz-r">1</b>/5</span><span>Правильно: <b id="p39-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="p39-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="op"></div></div><div class="drop-box"><h5>Регистрация / нерв</h5><div class="drop-items" data-cat="nr"></div></div></div>'
+'<div class="actions"><button class="btn primary" id="p39-dnd-check">Проверить</button><button class="btn" id="p39-dnd-reset">Сброс</button></div>'
+'<div class="feedback" id="p39-dnd-fb"></div></div>';
h += '<div class="wg"><div class="wg-header"><span class="wg-badge">IV-4</span><div class="wg-title">Тренажёр: 6 MCQ</div></div>'
+'<div id="p39-mcq"></div>'
+'<div class="score-display" style="margin-top:10px"><span>Вопрос: <b id="p39-mcq-i">1</b>/6</span><span>Правильно: <b id="p39-mcq-ok">0</b></span></div></div>';
/* IV5 — Расчётные задачи (auto-injected) */
h += '<div class="wg">'
+'<div class="wg-header"><span class="wg-badge">IV-5</span><div class="wg-title">Тренажёр: 5 расчётных задач</div></div>'
+'<div class="wg-help">Введи числовой ответ (точка как разделитель). Решено все верно — +20 XP.</div>'
+'<div id="p39-tasks5"></div>'
+'<div class="score-display" style="margin-top:10px"><span>Задача: <b id="p39-tasks5-i">1</b> / 5</span><span>Правильно: <b id="p39-tasks5-ok">0</b></span></div>'
+'</div>';
/* IV6 — Дисперсия — разложение белого света (Phase 3) */
h += '<div class="wg p8-iv6">'
+'<div class="wg-header"><span class="wg-badge p8-badge p8-badge-spectrum">IV-6</span><div class="wg-title">Дисперсия — разложение белого света</div></div>'
+'<div class="wg-help">Через призму белый свет разлагается на спектр. Угол отклонения зависит от длины волны: красный отклоняется меньше, фиолетовый больше.</div>'
+'<div class="p8-sandbox" id="p39-iv6-sandbox" style="height:240px"></div>'
+'</div>';
box.innerHTML = h + secNavFor('p39') + readButton('p39');
renderMath(box); wireReadBtn('p39');
_initP39_iv6();
_initp39_iv5();
_p39_eye(); _p39_quiz(); _p39_dnd(); _p39_mcq();
}
function _p39_eye(){
const svg = document.getElementById('p39-sim'); if(!svg) return;
function d(){
const dval = +document.getElementById('p39-d').value;
/* 0 — далёкий, 100 — близкий */
document.getElementById('p39-dv').textContent = dval < 30 ? 'далёкий (бесконечность)' : dval < 70 ? 'на расстоянии 1 м' : 'близкий (25 см)';
const acc = dval / 100;
let s = '';
/* «предмет» слева */
s += window.OPTICS.lightObject(60, 130, 40, 'arrow');
s += '<text x="60" y="170" text-anchor="middle" font-family="Inter,sans-serif" font-size="11" fill="#475569">предмет</text>';
/* линии лучей */
s += '<line x1="60" y1="130" x2="260" y2="110" stroke="#fbbf24" stroke-width="1.3"/>';
s += '<line x1="60" y1="90" x2="260" y2="120" stroke="#fbbf24" stroke-width="1.3"/>';
/* глаз справа */
s += window.OPTICS.eyeDiagram(330, 115, 70, acc);
/* подпись аккомодации */
s += '<text x="330" y="200" text-anchor="middle" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="#7c3aed">хрусталик '+(acc < 0.3 ? 'уплощён' : acc < 0.7 ? 'средний' : 'округлён')+'</text>';
svg.innerHTML = s;
}
document.getElementById('p39-d').addEventListener('input', d); d();
}
function _p39_quiz(){
const QS = [
{sit:'Эта часть преломляет свет на входе.', opts:['Сетчатка','Роговица','Зрачок','Нерв'], ans:1, why:'Роговица — главный преломляющий элемент.'},
{sit:'Она меняет $F$ при изменении расстояния.', opts:['Сетчатка','Хрусталик','Радужка','Зрачок'], ans:1, why:'Хрусталик меняет форму — аккомодация.'},
{sit:'На ней формируется изображение.', opts:['Радужка','Сетчатка','Зрачок','Зр. нерв'], ans:1, why:'Сетчатка — «экран» глаза.'},
{sit:'Она регулирует количество света.', opts:['Радужка/зрачок','Хрусталик','Роговица','Сетчатка'], ans:0, why:'На ярком свету зрачок сужается.'},
{sit:'Это сетчатка передаёт сигнал в мозг через…', opts:['артерию','зрительный нерв','лимфу','аккомодацию'], ans:1, why:'Зрительный нерв.'}
];
let i = 0, ok = 0;
function r(){
const q = QS[i]; const w = document.getElementById('p39-quiz');
let html = '<div style="padding:10px 14px;background:rgba(15,23,42,.04);border-radius:9px;margin-bottom:10px"><b>'+(i+1)+'.</b> '+q.sit+'</div><div style="display:grid;grid-template-columns:1fr;gap:6px">';
q.opts.forEach((o,k)=>{ html += '<button class="btn" data-k="'+k+'" style="text-align:left;padding:10px 14px">'+String.fromCharCode(65+k)+'. '+o+'</button>'; });
html += '</div><div class="feedback" id="p39-q-fb"></div>';
w.innerHTML = html;
document.getElementById('p39-quiz-r').textContent = (i+1);
document.getElementById('p39-quiz-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('p39-q-fb');
if(k===q.ans){ ok++; fb.className='feedback ok'; fb.innerHTML='&#10003; '+q.why; addXp(3,'p39-q'); bumpProgress('p39',4); }
else { fb.className='feedback fail'; fb.innerHTML='&#10007; '+q.why; }
document.getElementById('p39-quiz-ok').textContent = ok;
});
});
}
document.getElementById('p39-quiz-next').addEventListener('click', ()=>{ i=(i+1)%QS.length; r(); });
r();
}
function _p39_dnd(){
const items = [
{id:'r',cat:'op',html:'роговица'},{id:'h',cat:'op',html:'хрусталик'},{id:'z',cat:'op',html:'зрачок'},{id:'i',cat:'op',html:'радужка'},
{id:'s',cat:'nr',html:'сетчатка'},{id:'n',cat:'nr',html:'зрительный нерв'},{id:'c',cat:'nr',html:'колбочки и палочки'},{id:'br',cat:'nr',html:'мозг (анализ)'}
];
const dnd = setupSorter({ poolId:'p39-dnd-pool', scopeSelector:'#sec-p39', cats:['op','nr'], items, columnLayout:false });
document.getElementById('p39-dnd-check').addEventListener('click', ()=>{
const fb = document.getElementById('p39-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,'p39-dnd'); bumpProgress('p39',20); }
else { fb.className='feedback fail'; fb.innerHTML='&#10007; Ошибок: '+wr+'.'; }
});
document.getElementById('p39-dnd-reset').addEventListener('click', ()=>{ dnd.reset(); document.getElementById('p39-dnd-fb').style.display='none'; });
}
function _p39_mcq(){
const QS = [
{q:'Что играет роль линзы в глазу?', opts:['роговица','хрусталик','сетчатка','зрачок'], ans:1, why:'Хрусталик с переменным F.'},
{q:'Что изменяется при аккомодации?', opts:['расст. до сетчатки','форма хрусталика','цвет радужки','диаметр зрачка'], ans:1, why:'Мышцы меняют форму хрусталика.'},
{q:'Где формируется изображение?', opts:['в зрачке','на роговице','на сетчатке','в нерве'], ans:2, why:'На сетчатке.'},
{q:'Расстояние наилучшего зрения для здорового глаза?', opts:['10 см','25 см','1 м','5 м'], ans:1, why:'25 см.'},
{q:'Каким будет изображение на сетчатке?', opts:['прямое, мнимое','перевёрн., действ.','прямое, действ.','мнимое'], ans:1, why:'Глаз — собирающая система: перевёрнутое действительное (мозг переворачивает).'},
{q:'Что НЕ оптический элемент глаза?', opts:['роговица','хрусталик','сетчатка','нерв'], ans:3, why:'Нерв передаёт сигнал, не преломляет свет.'}
];
let i = 0, ok = 0, done = 0, aw = false;
function r(){
const q = QS[i]; const w = document.getElementById('p39-mcq');
let h = '<div style="padding:10px 14px;background:rgba(15,23,42,.04);border-radius:9px;margin-bottom:10px"><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="p39-mcq-fb"></div><div class="actions"><button class="btn" id="p39-mcq-n">Следующий</button></div>';
w.innerHTML = h;
document.getElementById('p39-mcq-i').textContent = (i+1);
document.getElementById('p39-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('p39-mcq-fb');
if(k===q.ans){ ok++; done++; fb.className='feedback ok'; fb.innerHTML='&#10003; '+q.why; addXp(2,'p39-mcq'); bumpProgress('p39',3); }
else { done++; fb.className='feedback fail'; fb.innerHTML='&#10007; '+q.why; }
document.getElementById('p39-mcq-ok').textContent = ok;
if(done >= QS.length && !aw && ok >= 4){ aw = true; setTimeout(()=>{ const f=document.getElementById('p39-mcq-fb'); f.className='feedback ok'; f.innerHTML='&#10003; +15 XP'; addXp(15,'p39-bonus'); bumpProgress('p39',15); }, 500); }
});
});
document.getElementById('p39-mcq-n').addEventListener('click', ()=>{ i=(i+1)%QS.length; r(); });
}
r();
}
/* ======== §40 — Дефекты зрения. Очки ======== */
function build_p40(){
const box = document.getElementById('p40-body'); let h = '';
h += makeCard('theory', 'Близорукость', '§ 40.1',
'<p><b>Близорукость (миопия)</b> — изображение далёких предметов фокусируется <b>перед</b> сетчаткой.</p>'
+'<p>Причина: глаз слегка вытянут, или хрусталик слишком сильный. Человек хорошо видит вблизи, плохо — вдаль.</p>'
+'<p>Лечение: <b>рассеивающие</b> линзы ($D < 0$). Они «отодвигают» фокус на сетчатку.</p>'
);
h += makeCard('rule', 'Дальнозоркость', '§ 40.2',
'<p><b>Дальнозоркость (гиперметропия)</b> — изображение фокусируется <b>за</b> сетчаткой.</p>'
+'<p>Причина: глаз короче нормы, или хрусталик слабоват. Человек плохо видит вблизи.</p>'
+'<p>Лечение: <b>собирающие</b> линзы ($D > 0$). Они «приближают» фокус к сетчатке.</p>'
);
h += makeCard('example', 'Как подобрать очки', '§ 40.3',
'<p>Сила линзы $D$ подбирается врачом-офтальмологом по таблицам.</p>'
+'<ul style="padding-left:20px;margin:6px 0">'
+'<li>$-1$ … $-3$ дптр — лёгкая близорукость.</li>'
+'<li>$-3$ … $-6$ дптр — средняя.</li>'
+'<li>$-6$ дптр и больше — сильная.</li>'
+'</ul>'
+'<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>Дефект: <select id="p40-def" class="tinp" style="width:auto;padding:6px 10px"><option value="m">близорукость</option><option value="h">дальнозоркость</option><option value="n">норма</option></select></label></div>'
+'<svg id="p40-sim" viewBox="0 0 460 200" 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>Нужны очки: <b id="p40-lens">собирающие, $D > 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="p40-quiz"></div>'
+'<div class="actions"><button class="btn" id="p40-quiz-next">Следующий</button></div>'
+'<div class="score-display" style="margin-top:10px"><span>Раунд: <b id="p40-quiz-r">1</b>/5</span><span>Правильно: <b id="p40-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">DnD признаков</div></div>'
+'<div id="p40-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="h"></div></div></div>'
+'<div class="actions"><button class="btn primary" id="p40-dnd-check">Проверить</button><button class="btn" id="p40-dnd-reset">Сброс</button></div>'
+'<div class="feedback" id="p40-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="p40-task"></div>'
+'<div class="score-display" style="margin-top:10px"><span>Задача: <b id="p40-task-i">1</b>/5</span><span>Правильно: <b id="p40-task-ok">0</b></span></div></div>';
/* IV6 — Глаз: аккомодация и очки (Phase 3) */
h += '<div class="wg p8-iv6">'
+'<div class="wg-header"><span class="wg-badge p8-badge p8-badge-spectrum">IV-6</span><div class="wg-title">Глаз: аккомодация и очки</div></div>'
+'<div class="wg-help">Нормальный глаз: лучи фокусируются на сетчатке. Близорукий — перед сетчаткой (нужна рассеивающая). Дальнозоркий — за сетчаткой (нужна собирающая).</div>'
+'<div class="p8-sandbox" id="p40-iv6-sandbox" style="height:240px"></div>'
+'<div style="margin-top:10px;display:flex;gap:10px;flex-wrap:wrap"><button class="btn primary" id="p40-iv6-normal">Норма</button><button class="btn" id="p40-iv6-myop">Близорукость</button><button class="btn" id="p40-iv6-hyper">Дальнозоркость</button></div>'
+'</div>';
box.innerHTML = h + secNavFor('p40') + readButton('p40');
renderMath(box); wireReadBtn('p40');
_initP40_iv6();
_p40_glass(); _p40_quiz(); _p40_dnd(); _p40_tasks();
}
function _p40_glass(){
const svg = document.getElementById('p40-sim'); if(!svg) return;
function d(){
const def = document.getElementById('p40-def').value;
let s = '';
const cx = 320, cy = 100;
/* глаз */
s += window.OPTICS.eyeDiagram(cx, cy, 50, 0.3);
/* лучи входящие */
const labelLens = def === 'm' ? 'рассеивающие, $D &lt; 0$' : def === 'h' ? 'собирающие, $D &gt; 0$' : 'не нужны';
document.getElementById('p40-lens').innerHTML = labelLens;
/* для близорукости: фокус ПЕРЕД сетчаткой; рассеивающая линза «отодвигает» */
/* для дальнозоркости: фокус ЗА; собирающая «приближает» */
/* нарисуем фокусное пятно условно */
let focusX = 0;
let lensType = '';
if(def === 'm'){ focusX = cx - 5; lensType = 'diverging'; }
else if(def === 'h'){ focusX = cx + 60; lensType = 'converging'; }
else { focusX = cx + 40; }
/* очки спереди глаза */
if(def !== 'n'){
s += window.OPTICS.thinLens(200, cy, 50, 80, lensType);
}
/* лучи */
s += '<line x1="80" y1="80" x2="200" y2="80" stroke="#fbbf24" stroke-width="1.4"/>';
s += '<line x1="80" y1="120" x2="200" y2="120" stroke="#fbbf24" stroke-width="1.4"/>';
s += '<line x1="200" y1="80" x2="'+focusX+'" y2="'+cy+'" stroke="#fbbf24" stroke-width="1.4"/>';
s += '<line x1="200" y1="120" x2="'+focusX+'" y2="'+cy+'" stroke="#fbbf24" stroke-width="1.4"/>';
s += '<circle cx="'+focusX+'" cy="'+cy+'" r="4" fill="#dc2626"/>';
s += '<text x="'+focusX+'" y="'+(cy+18)+'" text-anchor="middle" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="#dc2626">фокус</text>';
if(def === 'n') s += '<text x="200" y="40" text-anchor="middle" font-family="Inter,sans-serif" font-size="12" font-weight="700" fill="#10b981">нормальное зрение</text>';
if(def === 'm') s += '<text x="200" y="40" text-anchor="middle" font-family="Inter,sans-serif" font-size="12" font-weight="700" fill="#dc2626">близорукость: фокус до сетчатки</text>';
if(def === 'h') s += '<text x="200" y="40" text-anchor="middle" font-family="Inter,sans-serif" font-size="12" font-weight="700" fill="#dc2626">дальнозоркость: фокус за сетчаткой</text>';
svg.innerHTML = s;
}
document.getElementById('p40-def').addEventListener('change', d); d();
}
function _p40_quiz(){
const QS = [
{sit:'У человека близорукость', ans:'-', why:'Нужны рассеивающие, $D < 0$.'},
{sit:'Дальнозоркость', ans:'+', why:'Нужны собирающие, $D > 0$.'},
{sit:'У школьника плохо видит вдаль', ans:'-', why:'Признак близорукости.'},
{sit:'У бабушки плохо видит вблизи (мелкий шрифт)', ans:'+', why:'Возрастная дальнозоркость.'},
{sit:'$D = +2$ дптр прописал врач', ans:'+', why:'Собирающие — для дальнозоркости.'}
];
let i = 0, ok = 0;
function r(){
const q = QS[i]; const w = document.getElementById('p40-quiz');
w.innerHTML = '<div style="padding:10px 14px;background:rgba(15,23,42,.04);border-radius:9px;margin:8px 0">'+q.sit+'</div>'
+'<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px"><button class="btn" data-p="-"><b>$D &lt; 0$ (рассеив.)</b></button><button class="btn" data-p="+"><b>$D &gt; 0$ (собир.)</b></button></div>'
+'<div class="feedback" id="p40-q-fb"></div>';
document.getElementById('p40-quiz-r').textContent = (i+1);
document.getElementById('p40-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('p40-q-fb');
if(b.dataset.p === q.ans){ ok++; fb.className='feedback ok'; fb.innerHTML='&#10003; '+q.why; addXp(3,'p40-q'); bumpProgress('p40',4); }
else { fb.className='feedback fail'; fb.innerHTML='&#10007; '+q.why; }
document.getElementById('p40-quiz-ok').textContent = ok;
renderMath(w);
});
});
renderMath(w);
}
document.getElementById('p40-quiz-next').addEventListener('click', ()=>{ i=(i+1)%QS.length; r(); });
r();
}
function _p40_dnd(){
const items = [
{id:'a',cat:'m',html:'фокус ПЕРЕД сетчаткой'},
{id:'b',cat:'m',html:'плохо видит вдаль'},
{id:'c',cat:'m',html:'нужны рассеив. линзы'},
{id:'d',cat:'m',html:'$D < 0$'},
{id:'e',cat:'h',html:'фокус ЗА сетчаткой'},
{id:'f',cat:'h',html:'плохо видит вблизи'},
{id:'g',cat:'h',html:'нужны собир. линзы'},
{id:'i',cat:'h',html:'$D > 0$'}
];
const dnd = setupSorter({ poolId:'p40-dnd-pool', scopeSelector:'#sec-p40', cats:['m','h'], items, columnLayout:false });
document.getElementById('p40-dnd-check').addEventListener('click', ()=>{
const fb = document.getElementById('p40-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,'p40-dnd'); bumpProgress('p40',20); renderMath(fb); }
else { fb.className='feedback fail'; fb.innerHTML='&#10007; Ошибок: '+wr+'.'; renderMath(fb); }
});
document.getElementById('p40-dnd-reset').addEventListener('click', ()=>{ dnd.reset(); document.getElementById('p40-dnd-fb').style.display='none'; });
}
function _p40_tasks(){
const TASKS = [
{q:'$F = -0{,}5$ м. Найди $D$ (дптр).', ans:-2, tol:0.1, why:'$D = 1/-0{,}5 = -2$ дптр.'},
{q:'Очки $D = +1{,}5$ дптр. Какой $F$ (см)?', ans:67, tol:2, why:'$F = 1/1{,}5 \\approx 0{,}67$ м = 67 см.'},
{q:'Близорукому нужны $D = -3$ дптр. Какой |F| (см)?', ans:33, tol:1, why:'|F| = 1/3 м = 33 см.'},
{q:'Какой знак $D$ для очков от близорукости? (введи +1 или -1)', ans:-1, tol:0.1, why:'Рассеивающие, $D < 0$.'},
{q:'У бабушки прописали +2 дптр. Какой её дефект? (введи 1=близорукость, 2=дальнозоркость)', ans:2, tol:0.1, why:'$D > 0$ → собирающие → дальнозоркость.'}
];
let i = 0, ok = 0, done = 0, aw = false;
function r(){
const t = TASKS[i]; const w = document.getElementById('p40-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="p40-tinp" style="width:140px"><button class="btn primary" id="p40-tgo">Ответ</button><button class="btn" id="p40-thn">Подск.</button><button class="btn" id="p40-tn">След.</button></div>'
+'<div class="boss-hint-txt" id="p40-tht">'+t.why+'</div><div class="feedback" id="p40-tfb"></div>';
document.getElementById('p40-task-i').textContent = (i+1);
document.getElementById('p40-task-ok').textContent = ok;
document.getElementById('p40-tgo').addEventListener('click', ()=>{
const v = parseFloat((document.getElementById('p40-tinp').value || '').replace(',','.'));
const fb = document.getElementById('p40-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,'p40-t'); bumpProgress('p40',6); }
else { fb.className='feedback fail'; fb.innerHTML='&#10007; '+t.ans+'. '+t.why; }
document.getElementById('p40-task-ok').textContent = ok;
renderMath(w);
if(done >= TASKS.length && !aw && ok >= 4){ aw = true; setTimeout(()=>{ const f=document.getElementById('p40-tfb'); f.className='feedback ok'; f.innerHTML='&#10003; +15 XP'; addXp(15,'p40-bonus'); bumpProgress('p40',15); }, 500); }
});
document.getElementById('p40-thn').addEventListener('click', ()=>{ document.getElementById('p40-tht').classList.toggle('show'); });
document.getElementById('p40-tn').addEventListener('click', ()=>{ i=(i+1)%TASKS.length; r(); });
renderMath(w);
}
r();
}
/* ======== ФИНАЛ ГЛАВЫ 3 ======== */
function build_final3(){
const box = document.getElementById('final3-body'); let h = '';
h += '<div class="card" style="background:linear-gradient(135deg,var(--sec-acc-soft),var(--card));border:1.5px solid var(--sec-acc)">'
+'<div class="card-header"><div class="card-icon rule">'+ICONS.rule+'</div><div class="card-title">Шпаргалка главы 3</div></div>'
+'<div class="card-body" style="display:grid;grid-template-columns:1fr 1fr;gap:14px">'
+'<div><b>$c$:</b> $3 \\cdot 10^8$ м/с</div>'
+'<div><b>Тень:</b> точечный источник $\\to$ только тень</div>'
+'<div><b>Отражение:</b> $\\alpha = \\beta$</div>'
+'<div><b>Плоское зеркало:</b> мнимое, прямое, равное</div>'
+'<div><b>Снеллиус:</b> $\\sin\\alpha/\\sin\\beta = n_2/n_1$</div>'
+'<div><b>$n$ воды:</b> 1,33; <b>стекла:</b> 1,5</div>'
+'<div><b>$D$:</b> $D = 1/F$, дптр</div>'
+'<div><b>Линза:</b> $1/F = 1/d + 1/f$</div>'
+'<div><b>Глаз:</b> хрусталик меняет $F$ (аккомодация)</div>'
+'<div><b>Близорукость:</b> $D < 0$; <b>дальнозоркость:</b> $D > 0$</div>'
+'</div></div>';
const BOSSES = [
{n:1, title:'Скорость света', q:'За какое время свет пройдёт 600 000 км (мин)?', hint:'$t = 6\\cdot10^8 / 3\\cdot10^8 = 2$ с. В минутах: $1/30 \\approx 0{,}033$ мин.', ans:0.033, tol:0.005, step:'0.001'},
{n:2, title:'Закон отражения', q:'$\\alpha = 25$&#176;. Найди $\\beta$ (&#176;).', hint:'$\\beta = \\alpha = 25$&#176;.', ans:25, tol:0.5, step:'0.5'},
{n:3, title:'Преломление', q:'$\\alpha = 30$&#176;, воздух → стекло (n=1{,}5). Найди $\\beta$ (&#176;, до 0,1).', hint:'$\\sin\\beta = \\sin30/1{,}5 = 0{,}333$, $\\beta \\approx 19{,}5$&#176;.', ans:19.5, tol:0.5, step:'0.1'},
{n:4, title:'$D$ линзы', q:'$F = 0{,}25$ м. Найди $D$ (дптр).', hint:'$D = 1/0{,}25 = 4$ дптр.', ans:4, tol:0.1, step:'0.1'},
{n:5, title:'Тонкая линза', q:'$F = 20$ см, $d = 30$ см. Найди $f$ (см).', hint:'$f = 30\\cdot20/(30-20) = 60$ см.', ans:60, tol:1, step:'1'},
{n:6, title:'Очки', q:'Близорукому прописали $|F| = 50$ см. Какое $D$ (дптр)?', hint:'$D = 1/F = 1/(-0{,}5) = -2$ дптр.', ans:-2, tol:0.1, step:'0.1'},
{n:7, title:'Магистр света', q:'Угол падения на стекло $\\alpha = 60$&#176;, $n = 1{,}5$. Угол между падающим и преломлённым (&#176;, до 0,1).', hint:'$\\sin\\beta = \\sin60/1{,}5 \\approx 0{,}577$, $\\beta \\approx 35{,}3$&#176;. Угол между падающим (продолж.) и преломлённым: $\\alpha - \\beta \\approx 24{,}7$&#176; в относ. направ-х. Но угол между фактич. (падающий до границы) и преломлённым (после) — $180 - \\alpha - \\beta = 180 - 95{,}3 \\approx 84{,}7$&#176;.', ans:84.7, tol:1, step:'0.1'}
];
h += '<div class="card" style="margin-top:14px"><div class="card-header"><div class="card-icon example">'+ICONS.example+'</div><div class="card-title">7 боссов главы 3</div></div><div class="card-body">'
+'<div style="background:linear-gradient(135deg,rgba(15,23,42,.04),rgba(8,145,178,.06));border-radius:11px;padding:12px;display:flex;gap:14px;align-items:center;flex-wrap:wrap;margin-bottom:14px">'
+'<span style="font-weight:700">Боссов побеждено: <b id="f3-won">0</b> / 7</span>'
+'<div style="flex:1;min-width:160px;height:8px;background:rgba(0,0,0,.08);border-radius:4px;overflow:hidden">'
+'<div id="f3-bar" style="height:100%;background:linear-gradient(90deg,var(--sec-acc),var(--sec-acc-d));width:0%;transition:width .4s"></div>'
+'</div></div>'
+'<div id="f3-bosses"></div>'
+'</div></div>';
box.innerHTML = h + secNavFor('final3') + readButton('final3');
renderMath(box); wireReadBtn('final3');
_initFinal3_bosses(BOSSES);
}
function _initFinal3_bosses(BOSSES){
const KEY = 'physics8_ch3_bosses';
function loadState(){ try { return JSON.parse(localStorage.getItem(KEY) || '{}') || {}; } catch(e){ return {}; } }
function saveState(s){ try { localStorage.setItem(KEY, JSON.stringify(s)); } catch(e){} }
function updateBar(){
const s = loadState();
let won = 0; for(const k in s) if(s[k]) won++;
document.getElementById('f3-won').textContent = won;
document.getElementById('f3-bar').style.width = Math.round(won*100/BOSSES.length)+'%';
if(won >= BOSSES.length && !STATE.achievements.has('light_master')){
addXp(50, 'light-master');
achievement('light_master');
}
return won;
}
function renderAll(){
const cont = document.getElementById('f3-bosses');
const state = loadState();
let html = '';
BOSSES.forEach(b=>{
const solved = state[b.n];
html += '<div class="boss-card'+(solved?' solved':'')+'" id="f3-boss-'+b.n+'" style="border:2px solid '+(solved?'#10b981':'var(--border)')+';border-radius:12px;padding:14px;margin-bottom:10px;background:var(--card)">'
+'<div style="display:flex;gap:10px;align-items:center;margin-bottom:8px"><span style="font-family:Unbounded,sans-serif;font-size:.7rem;font-weight:800;padding:3px 8px;border-radius:99px;background:var(--sec-acc-soft);color:var(--sec-acc-d);text-transform:uppercase">Босс '+b.n+'</span><span style="font-weight:700">'+b.title+'</span></div>'
+'<div style="padding:10px 12px;background:rgba(15,23,42,.04);border-radius:8px;margin-bottom:8px;font-size:.94rem;line-height:1.5">'+b.q+'</div>'
+'<div class="boss-row">'
+'<input type="number" step="'+b.step+'" class="tinp" id="f3-b'+b.n+'-inp" placeholder="число" style="width:140px"'+(solved?' value="'+b.ans+'" disabled':'')+'>'
+'<button class="btn primary" id="f3-b'+b.n+'-go"'+(solved?' disabled':'')+'>Атаковать</button>'
+'<button class="btn" id="f3-b'+b.n+'-hint">Подсказка</button>'
+'</div>'
+'<div class="boss-hint-txt" id="f3-b'+b.n+'-ht" style="margin-top:8px;padding:9px 13px;background:rgba(245,158,11,.12);border-left:3px solid #f59e0b;border-radius:6px;font-size:.86rem;display:none;line-height:1.5">'+b.hint+'</div>'
+'<div class="feedback'+(solved?' ok':'')+'" id="f3-b'+b.n+'-fb" style="display:'+(solved?'block':'none')+'">'+(solved?'&#10003; Победа! +10 XP. Босс повержен.':'')+'</div>'
+'</div>';
});
cont.innerHTML = html;
BOSSES.forEach(b=>{
const go = document.getElementById('f3-b'+b.n+'-go');
const inp = document.getElementById('f3-b'+b.n+'-inp');
const fb = document.getElementById('f3-b'+b.n+'-fb');
const ht = document.getElementById('f3-b'+b.n+'-ht');
const hintBtn = document.getElementById('f3-b'+b.n+'-hint');
if(hintBtn) hintBtn.addEventListener('click', ()=>{ ht.style.display = ht.style.display==='block'?'none':'block'; });
if(!go || go.disabled) return;
go.addEventListener('click', ()=>{
const v = parseFloat((inp.value || '').replace(',','.'));
if(isNaN(v)){ fb.style.display='block'; fb.className='feedback fail'; fb.innerHTML='Введите число.'; return; }
if(Math.abs(v - b.ans) < b.tol){
fb.style.display='block'; fb.className='feedback ok'; fb.innerHTML='&#10003; Победа! +10 XP. '+b.hint;
go.disabled = true; inp.disabled = true;
document.getElementById('f3-boss-'+b.n).classList.add('solved');
const s = loadState();
if(!s[b.n]){ s[b.n]=true; saveState(s); addXp(10,'f3-boss-'+b.n); bumpProgress('final3', 14); }
updateBar();
renderMath(fb);
} else {
fb.style.display='block'; fb.className='feedback fail'; fb.innerHTML='&#10007; Не то. Перепроверь и попробуй снова.';
}
});
inp.addEventListener('keydown', e=>{ if(e.key === 'Enter') go.click(); });
});
renderMath(cont);
updateBar();
}
renderAll();
}
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>
<script>
/* P8 hero meter — анимация длины волны (Phase 3 spectrum) */
(function(){
function init(){
const el = document.getElementById('p8-meter-val');
if (!el || !window.P8Anim) return;
const targets = [{ l: 400, c:'#7c3aed' }, { l: 470, c:'#2563eb' }, { l: 550, c:'#16a34a' }, { l: 600, c:'#f59e0b' }, { l: 700, c:'#dc2626' }];
let i = 0;
function step(){
const from = parseFloat((el.textContent || '550').replace(/\D/g,'')) || 550;
const target = targets[i % targets.length];
P8Anim.tween({
from, to: target.l, duration: 1100, easing: 'cubicInOut',
onUpdate: v => { el.textContent = 'λ=' + Math.round(v); el.style.color = target.c; },
onComplete: () => { i++; setTimeout(step, 1400); }
});
}
setTimeout(step, 1200);
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script>
</body>
</html>