65c2e7dac1
GEN: gen_phys7_ch.js теперь подключает <script src=phys7_chN_widgets.js> и
вызывает PHYS7_CHN_WIDGETS[id] в ensureBuilt, удаляя placeholder. Все 5 chN
регенерированы под этот hook.
WIDGETS (frontend/js/phys7_ch1_widgets.js, 402 строки, экспорт p1+p2):
- §1 «Физика — наука о природе»:
* 3 теор. карточки (что изучает / связь с науками / 6 примеров явлений)
* IV-1 СИМ: галерея 8 областей физики с hover-эффектом
* IV-2 КВИЗ: 3 вопроса о предмете физики и слове «фюзис»
* IV-3 DnD: 8 карточек → 4 науки (астро/химия/био/физика)
* IV-4 ТРН: 5 вопросов тренажёр
- §2 «Физическое тело, явление, величина»:
* 3 теор. карточки (4-понятийная таблица / как отличать / стакан-пример)
* IV-1 СИМ (главный): DnD 12 карточек → 4 корзины (тело/вещество/явление/величина),
в т.ч. KaTeX-величины (=5$ кг, =-10$ °C, =30$ км/ч)
* IV-2 КВИЗ: «найди величину/явление/вещество» (3 вопроса)
* IV-3 ТЕСТ: 5 быстрых вопросов на классификацию
* IV-4 ТРН: 4 расчётных + концептуальных вопроса
- Кнопка «Я прочитал §» (+10 XP), localStorage-фиксация, серая «Прочитано»
после первого нажатия.
ИНФРАСТРУКТУРА:
- Общие хелперы внутри файла: makeCard (theory/rule/example), wgWrap, dndPool,
wireDnd, quizQuestion, wireQuiz, readButton, wireReadBtn, renderMath с правильными
delimiters $..$ и $$..$$.
- XP: DnD +15, квиз +10, тренажёр +15, прочитал +10. Прогресс параграфа +30 при
«прочитал», +10 базово при открытии. Цвета §1+§2 единые с темой главы 1 (indigo).
- Parse-check, KaTeX-аудит ($$ только двойной backslash), smoke-test пройдены.
398 lines
28 KiB
HTML
398 lines
28 KiB
HTML
<!doctype html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||
<meta http-equiv="Pragma" content="no-cache">
|
||
<meta http-equiv="Expires" content="0">
|
||
<title>Физика 7 · Глава 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/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/phys.js?v=20260530" defer></script>
|
||
<script src="/js/phys7_ch3_widgets.js?v=20260530" 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:#f0f9ff; --card:#fff; --card-soft:#f8fafc; --text:#0f172a; --muted:#475569;
|
||
--border:#bae6fd; --pri:#0284c7; --pri2:#0c4a6e; --pri-soft:#e0f2fe;
|
||
--acc:#dc2626; --acc-d:#991b1b; --acc-soft:#fee2e2;
|
||
--ok:#10b981; --ok-bg:#d1fae5; --fail:#dc2626; --fail-bg:#fee2e2; --warn:#f59e0b; --warn-bg:#fef3c7;
|
||
--sh:0 4px 16px rgba(2,132,199,.08); --sh-h:0 12px 36px rgba(2,132,199,.16);
|
||
}
|
||
html.dark{--bg:#0c1e2e;--card:#0e2436;--card-soft:#0b1a28;--text:#e0f2fe;--muted:#7dd3fc;--border:#1e3a5f;--pri-soft:rgba(2,132,199,.18)}
|
||
*{margin:0;padding:0;box-sizing:border-box}
|
||
html,body{min-height:100vh}
|
||
body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.6;transition:background .25s,color .25s}
|
||
|
||
.hdr{position:relative;background:linear-gradient(135deg,#7f1d1d,#dc2626 60%,#f87171);color:#fff;padding:24px 22px 22px;overflow:hidden;border-bottom:2px solid rgba(255,255,255,.18)}
|
||
.hdr-inner{position:relative;z-index:1;max-width:1240px;margin:0 auto;display:flex;align-items:center;gap:14px;flex-wrap:wrap}
|
||
.hdr h1{font-family:'Unbounded',sans-serif;font-size:1.55rem;font-weight:800;letter-spacing:-.01em}
|
||
.hdr-sub{font-size:.88rem;opacity:.9;margin-top:3px}
|
||
.hdr-side{margin-left:auto;display:flex;gap:8px;flex-wrap:wrap}
|
||
.hdr-btn{padding:8px 12px;background:rgba(255,255,255,.16);border:none;color:#fff;border-radius:9px;cursor:pointer;font-weight:600;font-size:.82rem;display:inline-flex;align-items:center;gap:6px;transition:background .15s;font-family:inherit;text-decoration:none}
|
||
.hdr-btn:hover{background:rgba(255,255,255,.26)}
|
||
.ic{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
||
|
||
.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}
|
||
.psel{background:var(--card);border:1.5px solid var(--border);border-radius:14px;padding:16px;margin-bottom:18px;box-shadow:var(--sh)}
|
||
.psel-head{font-family:'Unbounded',sans-serif;font-size:.78rem;font-weight:800;color:var(--pri2);text-transform:uppercase;letter-spacing:.07em;margin-bottom:10px}
|
||
.psel-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:10px}
|
||
.psel-card{padding:12px;background:var(--card-soft);border:1.5px solid var(--border);border-radius:10px;cursor:pointer;transition:transform .15s,border-color .15s,box-shadow .15s;text-align:left}
|
||
.psel-card:hover{border-color:var(--acc);transform:translateY(-2px);box-shadow:0 4px 14px rgba(0,0,0,.06)}
|
||
.psel-card.active{border-color:var(--acc);background:var(--acc-soft)}
|
||
.psel-num{font-size:.7rem;font-weight:800;color:var(--acc-d);letter-spacing:.04em;text-transform:uppercase;margin-bottom:3px}
|
||
.psel-title{font-size:.86rem;font-weight:700;line-height:1.35}
|
||
.psel-prog{height:4px;background:rgba(0,0,0,.07);border-radius:3px;overflow:hidden;margin-top:7px}
|
||
.psel-prog-fill{height:100%;background:linear-gradient(90deg,var(--acc),var(--acc-d));border-radius:3px;transition:width .4s}
|
||
|
||
.sec{display:none;background:var(--card);border:1.5px solid var(--border);border-radius:14px;padding:22px;box-shadow:var(--sh);position:relative}
|
||
.sec.active{display:block}
|
||
.sec[data-watermark]::before{content:attr(data-watermark);position:absolute;right:18px;top:-12px;font-family:'Unbounded',sans-serif;font-size:5.2rem;font-weight:900;color:var(--acc-soft);pointer-events:none;line-height:1;user-select:none}
|
||
.sec-header{display:flex;align-items:baseline;gap:14px;margin-bottom:18px;padding-bottom:14px;border-bottom:1.5px solid var(--border);position:relative;z-index:1}
|
||
.sec-num{background:linear-gradient(135deg,var(--acc),var(--acc-d));color:#fff;padding:5px 12px;border-radius:9px;font-family:'Unbounded',sans-serif;font-weight:800;font-size:.86rem;letter-spacing:.04em}
|
||
.sec-h{font-family:'Unbounded',sans-serif;font-size:1.35rem;font-weight:800;color:var(--text)}
|
||
.placeholder{padding:32px 20px;text-align:center;color:var(--muted);font-size:.95rem;background:var(--card-soft);border:1.5px dashed var(--border);border-radius:10px}
|
||
|
||
.col-side{position:sticky;top:14px;align-self:start;height:fit-content;max-height:calc(100vh - 28px);overflow-y:auto}
|
||
.sidecard{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:16px;margin-bottom:14px;box-shadow:var(--sh)}
|
||
.sidecard h4{font-family:'Unbounded',sans-serif;font-size:.74rem;font-weight:800;color:var(--pri2);text-transform:uppercase;letter-spacing:.07em;margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid var(--border)}
|
||
.sidecard-row{margin-bottom:8px;font-size:.86rem;line-height:1.6}
|
||
.sidecard-row b{color:var(--pri);font-weight:700}
|
||
@media(max-width:980px){.col-side{position:static;max-height:none}}
|
||
|
||
.xp-card{background:linear-gradient(135deg,var(--acc-soft),var(--pri-soft));border:1.5px solid var(--acc);border-radius:12px;padding:14px;margin-bottom:14px}
|
||
.xp-card-title{font-size:.68rem;font-weight:800;color:var(--acc-d);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(--acc-d);font-family:'Unbounded',sans-serif}
|
||
.xp-bar{height:9px;background:rgba(245,158,11,.15);border-radius:6px;overflow:hidden;margin:7px 0}
|
||
.xp-fill{height:100%;background:linear-gradient(90deg,var(--acc),var(--pri));border-radius:6px;transition:width .5s cubic-bezier(.4,0,.2,1)}
|
||
.xp-nums{font-size:.74rem;color:var(--muted);display:flex;justify-content:space-between}
|
||
|
||
.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}
|
||
}
|
||
|
||
.ach-popup{position:fixed;top:80px;right:18px;background:linear-gradient(135deg,var(--acc-d),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,.25);z-index:1002;display:none;align-items:center;gap:8px;max-width:340px}
|
||
.ach-popup.show{display:flex}
|
||
|
||
.foot{text-align:center;padding:30px 16px;color:var(--muted);font-size:.78rem;border-top:1px solid var(--border);margin-top:30px}
|
||
|
||
/* Search modal */
|
||
.search-modal{position:fixed;inset:0;background:rgba(15,23,42,.55);backdrop-filter:blur(4px);z-index:9993;display:none;align-items:flex-start;justify-content:center;padding-top:80px}
|
||
.search-modal.show{display:flex}
|
||
.search-box{background:var(--card);border-radius:14px;width:520px;max-width:92vw;padding:14px;box-shadow:0 24px 64px rgba(0,0,0,.35);border:1.5px solid var(--border)}
|
||
.search-inp{width:100%;padding:11px 14px;background:var(--card-soft);border:1.5px solid var(--border);border-radius:9px;color:var(--text);font-size:.95rem;font-family:inherit;outline:0}
|
||
.search-inp:focus{border-color:var(--acc)}
|
||
.search-list{margin-top:12px;max-height:320px;overflow-y:auto}
|
||
.search-item{padding:10px 12px;border-radius:9px;cursor:pointer;border:1px solid transparent;font-size:.9rem}
|
||
.search-item:hover,.search-item.cur{background:var(--acc-soft);border-color:var(--acc)}
|
||
.search-item .num{display:inline-block;padding:2px 8px;background:var(--acc);color:#fff;border-radius:99px;font-size:.7rem;font-weight:700;margin-right:8px}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<header class="hdr">
|
||
<div class="hdr-inner">
|
||
<div>
|
||
<a href="/textbook/physics-7" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> К физике 7</a>
|
||
</div>
|
||
<div>
|
||
<h1>Физика 7 · Глава 3</h1>
|
||
<div class="hdr-sub">Движение и силы · §§14–27</div>
|
||
</div>
|
||
<div class="hdr-side">
|
||
<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">
|
||
<div class="psel">
|
||
<div class="psel-head">Параграфы главы 3</div>
|
||
<div class="psel-grid" id="psel-grid"></div>
|
||
</div>
|
||
|
||
<section id="sec-p14" class="sec" data-watermark="→">
|
||
<div class="sec-header"><span class="sec-num">§ 14</span><h2 class="sec-h">Механическое движение. Относительность</h2></div>
|
||
<div id="p14-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
|
||
</section>
|
||
<section id="sec-p15" class="sec" data-watermark="s">
|
||
<div class="sec-header"><span class="sec-num">§ 15</span><h2 class="sec-h">Траектория, путь, время</h2></div>
|
||
<div id="p15-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
|
||
</section>
|
||
<section id="sec-p16" class="sec" data-watermark="v">
|
||
<div class="sec-header"><span class="sec-num">§ 16</span><h2 class="sec-h">Равномерное движение. Скорость</h2></div>
|
||
<div id="p16-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
|
||
</section>
|
||
<section id="sec-p17" class="sec" data-watermark="∠">
|
||
<div class="sec-header"><span class="sec-num">§ 17</span><h2 class="sec-h">Графики s(t) и v(t)</h2></div>
|
||
<div id="p17-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
|
||
</section>
|
||
<section id="sec-p18" class="sec" data-watermark="⟨⟩">
|
||
<div class="sec-header"><span class="sec-num">§ 18</span><h2 class="sec-h">Средняя скорость</h2></div>
|
||
<div id="p18-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
|
||
</section>
|
||
<section id="sec-p19" class="sec" data-watermark="∞">
|
||
<div class="sec-header"><span class="sec-num">§ 19</span><h2 class="sec-h">Инерция</h2></div>
|
||
<div id="p19-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
|
||
</section>
|
||
<section id="sec-p20" class="sec" data-watermark="ρ">
|
||
<div class="sec-header"><span class="sec-num">§ 20</span><h2 class="sec-h">Масса. Плотность</h2></div>
|
||
<div id="p20-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
|
||
</section>
|
||
<section id="sec-p21" class="sec" data-watermark="F">
|
||
<div class="sec-header"><span class="sec-num">§ 21</span><h2 class="sec-h">Сила</h2></div>
|
||
<div id="p21-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
|
||
</section>
|
||
<section id="sec-p22" class="sec" data-watermark="↓">
|
||
<div class="sec-header"><span class="sec-num">§ 22</span><h2 class="sec-h">Сила тяжести</h2></div>
|
||
<div id="p22-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
|
||
</section>
|
||
<section id="sec-p23" class="sec" data-watermark="≈">
|
||
<div class="sec-header"><span class="sec-num">§ 23</span><h2 class="sec-h">Сила упругости</h2></div>
|
||
<div id="p23-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
|
||
</section>
|
||
<section id="sec-p24" class="sec" data-watermark="P">
|
||
<div class="sec-header"><span class="sec-num">§ 24</span><h2 class="sec-h">Вес тела</h2></div>
|
||
<div id="p24-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
|
||
</section>
|
||
<section id="sec-p25" class="sec" data-watermark="⊥">
|
||
<div class="sec-header"><span class="sec-num">§ 25</span><h2 class="sec-h">Динамометр</h2></div>
|
||
<div id="p25-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
|
||
</section>
|
||
<section id="sec-p26" class="sec" data-watermark="+">
|
||
<div class="sec-header"><span class="sec-num">§ 26</span><h2 class="sec-h">Сложение сил</h2></div>
|
||
<div id="p26-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
|
||
</section>
|
||
<section id="sec-p27" class="sec" data-watermark="~">
|
||
<div class="sec-header"><span class="sec-num">§ 27</span><h2 class="sec-h">Сила трения</h2></div>
|
||
<div id="p27-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
|
||
</section>
|
||
<section id="sec-final3" class="sec" data-watermark="★">
|
||
<div class="sec-header"><span class="sec-num">Финал</span><h2 class="sec-h">Итоги главы 3</h2></div>
|
||
<div id="final3-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></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>
|
||
|
||
<div class="ach-popup" id="ach-popup"><svg class="ic" viewBox="0 0 24 24"><polygon points="12,2 15,9 22,9.3 17,14 18.5,21 12,17 5.5,21 7,14 2,9.3 9,9"/></svg><span id="ach-text"></span></div>
|
||
|
||
<div class="search-modal" id="search-modal"><div class="search-box">
|
||
<input type="text" class="search-inp" id="search-inp" placeholder="Поиск по параграфам... (Esc — закрыть, Ctrl+K)">
|
||
<div class="search-list" id="search-list"></div>
|
||
</div></div>
|
||
|
||
<footer class="foot">Интерактивный учебник «Физика 7 класс» · Глава 3 · LearnSpace</footer>
|
||
|
||
<script>
|
||
'use strict';
|
||
|
||
const LS_PREFIX = 'physics7_ch3';
|
||
const _TB_SLUG = 'physics-7-ch3';
|
||
|
||
const PARAS = [{id:'p14',num:'§ 14',title:"Механическое движение. Относительность",wm:'→'},{id:'p15',num:'§ 15',title:"Траектория, путь, время",wm:'s'},{id:'p16',num:'§ 16',title:"Равномерное движение. Скорость",wm:'v'},{id:'p17',num:'§ 17',title:"Графики s(t) и v(t)",wm:'∠'},{id:'p18',num:'§ 18',title:"Средняя скорость",wm:'⟨⟩'},{id:'p19',num:'§ 19',title:"Инерция",wm:'∞'},{id:'p20',num:'§ 20',title:"Масса. Плотность",wm:'ρ'},{id:'p21',num:'§ 21',title:"Сила",wm:'F'},{id:'p22',num:'§ 22',title:"Сила тяжести",wm:'↓'},{id:'p23',num:'§ 23',title:"Сила упругости",wm:'≈'},{id:'p24',num:'§ 24',title:"Вес тела",wm:'P'},{id:'p25',num:'§ 25',title:"Динамометр",wm:'⊥'},{id:'p26',num:'§ 26',title:"Сложение сил",wm:'+'},{id:'p27',num:'§ 27',title:"Сила трения",wm:'~'},{id:'final3',num:'Финал',title:"Итоги главы 3",wm:'★'}];
|
||
const TOTAL_PARAS = PARAS.length;
|
||
|
||
const SIDEBARS = {};
|
||
PARAS.forEach(p => { SIDEBARS[p.id] = { title: 'Шпаргалка ' + p.num, rows: [['В разработке','контент появится с волной соответствующего §']] }; });
|
||
const TIPS = [{ sec: PARAS[0].id, html: 'Скелет главы готов. Контент параграфов выйдет в одной из ближайших фаз.' }];
|
||
const ACH_LABELS = { start: 'Начало главы 3', ch_done: 'Мастер движения' };
|
||
|
||
const STATE = { current: null, progress: {}, xp: 0, level: 1, achievements: new Map(), _built: new Set() };
|
||
|
||
function _xpForLevel(lv){ return Math.round(100 * Math.pow(lv-1, 1.6)); }
|
||
function calcLevel(xp){ let lv = 1; while(_xpForLevel(lv+1) <= xp) lv++; return lv; }
|
||
|
||
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(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('physics7_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('physics7_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] >= 100 && key === PARAS[PARAS.length-1].id) achievement('ch_done', 'Мастер движения');
|
||
}
|
||
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, 'physics7-ch3-' + (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 achievement(id, label){
|
||
if(STATE.achievements.has(id)) return;
|
||
STATE.achievements.set(id, label || ACH_LABELS[id] || id);
|
||
saveProgress();
|
||
const pop = document.getElementById('ach-popup');
|
||
if(pop){ document.getElementById('ach-text').textContent = 'Ачивка: ' + (label || ACH_LABELS[id] || id); pop.classList.add('show'); setTimeout(()=>pop.classList.remove('show'), 3000); }
|
||
addXp(20, 'ach-' + id);
|
||
}
|
||
|
||
function refreshProgressUI(){
|
||
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) + '%';
|
||
});
|
||
if(STATE.current && document.getElementById('sidebar-content')){ try{ buildSidebar(STATE.current); }catch(e){} }
|
||
}
|
||
|
||
function buildParaSelector(){
|
||
const grid = document.getElementById('psel-grid');
|
||
if(!grid) return;
|
||
grid.innerHTML = PARAS.map(p =>
|
||
'<button class="psel-card" data-id="' + p.id + '" data-prog-card="' + p.id + '">'
|
||
+ '<div class="psel-num">' + p.num + '</div>'
|
||
+ '<div class="psel-title">' + p.title + '</div>'
|
||
+ '<div class="psel-prog"><div class="psel-prog-fill" style="width:' + (STATE.progress[p.id]||0) + '%"></div></div>'
|
||
+ '</button>'
|
||
).join('');
|
||
grid.querySelectorAll('.psel-card').forEach(c => c.addEventListener('click', () => goTo(c.dataset.id)));
|
||
}
|
||
|
||
function ensureBuilt(id){
|
||
if(STATE._built.has(id)) return;
|
||
STATE._built.add(id);
|
||
const W = window['PHYS7_CH3_WIDGETS'];
|
||
if(W && typeof W[id] === 'function'){
|
||
const body = document.getElementById(id + '-body');
|
||
if(body){
|
||
const ph = body.querySelector('.placeholder');
|
||
if(ph) ph.remove();
|
||
}
|
||
try{ W[id](); }catch(e){ console.warn('phys7 widget ' + id + ':', e.message); }
|
||
}
|
||
}
|
||
|
||
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 && el){
|
||
setTimeout(() => {
|
||
try{ renderMathInElement(el, { delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false}], throwOnError:false }); }catch(e){}
|
||
}, 0);
|
||
}
|
||
}
|
||
|
||
function buildSidebar(id){
|
||
const box = document.getElementById('sidebar-content');
|
||
if(!box) return;
|
||
const sb = SIDEBARS[id] || SIDEBARS[PARAS[0].id];
|
||
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;
|
||
let html = '';
|
||
html += '<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><span class="xp-level">Ур. ' + STATE.level + '</span></div><div class="xp-bar"><div class="xp-fill" style="width:' + xpPct + '%"></div></div><div class="xp-nums"><span>' + STATE.xp + ' XP</span><span>' + xpNext + ' XP</span></div></div>';
|
||
html += '<div class="sidecard"><h4>' + sb.title + '</h4>';
|
||
sb.rows.forEach(([k,v]) => { html += '<div class="sidecard-row"><b>' + k + '</b>' + (v ? ' — ' + v : '') + '</div>'; });
|
||
html += '</div>';
|
||
const tip = TIPS.find(t => t.sec === id) || TIPS[0];
|
||
if(tip){
|
||
html += '<div class="sidecard" style="background:linear-gradient(135deg,var(--warn-bg),var(--pri-soft));border-color:var(--warn)"><h4 style="color:#92400e">Подсказка</h4><div class="sidecard-row" style="margin-bottom:0;font-size:.84rem">' + 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(t => { html += '<div class="sidecard-row" style="font-size:.78rem;color:var(--ok)">✓ ' + t + '</div>'; });
|
||
html += '</div>';
|
||
}
|
||
box.innerHTML = html;
|
||
if(window.renderMathInElement){
|
||
try{ renderMathInElement(box, { delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false}], throwOnError:false }); }catch(e){}
|
||
}
|
||
}
|
||
|
||
function initTheme(){
|
||
const t = localStorage.getItem(LS_PREFIX + '_theme') || localStorage.getItem('physics7_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');
|
||
localStorage.setItem('physics7_theme', dark ? 'dark' : 'light');
|
||
document.getElementById('theme-lab').textContent = dark ? 'Светлая' : 'Тёмная';
|
||
});
|
||
}
|
||
|
||
function initSidebarToggle(){
|
||
const side = document.getElementById('col-side'), back = document.getElementById('col-side-backdrop'), btn = document.getElementById('sidebar-btn');
|
||
if(!side || !btn) return;
|
||
function open(){ side.classList.add('open'); back.classList.add('show'); }
|
||
function close(){ side.classList.remove('open'); back.classList.remove('show'); }
|
||
btn.addEventListener('click', () => { if(side.classList.contains('open')) close(); else open(); });
|
||
back.addEventListener('click', close);
|
||
document.addEventListener('keydown', e => { if(e.key === 'Escape') close(); });
|
||
}
|
||
|
||
function initSearch(){
|
||
const btn = document.getElementById('search-btn'), modal = document.getElementById('search-modal'), inp = document.getElementById('search-inp'), list = document.getElementById('search-list');
|
||
if(!btn || !modal) return;
|
||
let cur = 0;
|
||
function render(q){
|
||
const ql = (q||'').toLowerCase().trim();
|
||
const items = PARAS.filter(p => !ql || p.title.toLowerCase().includes(ql) || p.num.toLowerCase().includes(ql));
|
||
list.innerHTML = items.map((p,i) => '<div class="search-item' + (i === cur ? ' cur' : '') + '" data-id="' + p.id + '"><span class="num">' + p.num + '</span>' + p.title + '</div>').join('');
|
||
list.querySelectorAll('.search-item').forEach(el => el.addEventListener('click', () => { goTo(el.dataset.id); close(); }));
|
||
}
|
||
function open(){ modal.classList.add('show'); inp.value = ''; cur = 0; render(''); setTimeout(() => inp.focus(), 50); }
|
||
function close(){ modal.classList.remove('show'); }
|
||
btn.addEventListener('click', open);
|
||
modal.addEventListener('click', e => { if(e.target === modal) close(); });
|
||
inp.addEventListener('input', () => { cur = 0; render(inp.value); });
|
||
inp.addEventListener('keydown', e => {
|
||
const items = list.querySelectorAll('.search-item');
|
||
if(e.key === 'ArrowDown'){ e.preventDefault(); cur = Math.min(items.length-1, cur+1); render(inp.value); }
|
||
else if(e.key === 'ArrowUp'){ e.preventDefault(); cur = Math.max(0, cur-1); render(inp.value); }
|
||
else if(e.key === 'Enter'){ e.preventDefault(); const sel = items[cur]; if(sel){ goTo(sel.dataset.id); close(); } }
|
||
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 init(){
|
||
loadProgress(); initTheme(); initSidebarToggle(); initSearch();
|
||
buildParaSelector(); refreshProgressUI(); goTo(PARAS[0].id);
|
||
setTimeout(() => achievement('start'), 600);
|
||
}
|
||
document.addEventListener('DOMContentLoaded', init);
|
||
</script>
|
||
</body>
|
||
</html>
|