feat(phys7): Phase 0 — фундамент учебника Физики 7

Полная инфраструктура: hub, 5 ch-скелетов, lab-скелет, миграция 039,
расширение phys.js на 11 хелперов + 2 класса симуляций для новых тем 7-го класса.

ФАЙЛЫ:
- backend/src/db/migrations/039_physics_7_hub.sql — self-sufficient миграция
  (parent physics-7 + 6 children: ch1..ch5 + lab). Palette: sky/blue для hub,
  глав: indigo/violet/red/amber/emerald/cyan.
- frontend/textbooks/physics_7_hub.html (862 строки) — hub с прогресс-картами
  6 разделов, шпаргалкой курса в 5 mini-карточках, 10 интегрированных боссов
  финала курса (через ачивку «Магистр физики 7», +150 XP), темой/lang storage
  через ключи physics7_*. Sidebar-фикс на десктопе встроен.
- frontend/textbooks/physics_7_ch1..ch5.html (350-390 строк каждый) —
  скелеты глав с header, paragraph selector, sidebar, прогресс/XP, goTo,
  search-модалом, KaTeX с delimiters, sidebar-фиксом, cache-busting ?v=20260530.
  Каждая глава имеет правильное число параграфов (7/6/14/8/7) + sec-finalN.
- frontend/textbooks/physics_7_lab.html (306 строк) — скелет лаб. практикума
  на 6 ЛР с teal/cyan палитрой и ачивкой «Лаборант 7 класса» (+80 XP).
- backend/scripts/gen_phys7_ch.js / gen_phys7_lab.js — генераторы из единого
  шаблона (для регенерации при правках инфраструктуры).

PHYS.JS НОВЫЕ ХЕЛПЕРЫ (всё работает, smoke-test пройден):
- forceVector(x,y,F,angle,color,label) — стрелка силы с подписью
- dynamometer(x,y,h,Fmax,F) — динамометр с пружиной и шкалой
- blockOnSurface(x,y,w,h,label,weights) — брусок со стопкой гирь
- connectedVessels(x,y,kindA,kindB,levelY) — сообщающиеся сосуды
- hydraulicPress(x,y,sSmall,sLarge,fSmall) — гидравлический пресс
- mercuryBarometer(x,y,hMm) — ртутный барометр Торричелли
- aneroidBarometer(cx,cy,r,p) — стрелочный барометр-анероид
- uManometer(x,y,w,h,deltaH) — U-образный жидкостный манометр
- rulerWithError(x,y,lenCm,mmPerDiv) — линейка со шкалой и ценой деления
- bimetal(x,y,w,h,deltaT) — биметаллическая пластина (гнётся от ΔT)
- expandingRod(x,y,l0,alpha,deltaT) — стержень с тепловым расширением
- class HillSlideSim — тележка на горке (§42, закон сохранения; графики Ek/Ep/Etot)
- class PendulumSim — математический маятник (§42, осцилляции)

Все 13 экспортированы в window.PHYS, smoke-test показал физически разумные
значения энергий. Parse-check + node --check проходят.

Уроки phys 9 учтены сразу: cache-busting на phys.js, sidebar-фикс @media
min-width:981px, delimiters для renderMathInElement.

PHASE 0 DONE. Дальше: Phase 1 Wave 1 — §§1-2 (Физика как наука + Тело/явление/величина).
This commit is contained in:
Maxim Dolgolyov
2026-05-30 10:32:37 +03:00
parent 29a2bae7d9
commit e76485cadc
11 changed files with 4210 additions and 1 deletions
+457
View File
@@ -0,0 +1,457 @@
#!/usr/bin/env node
// Генератор скелетов глав Физики 7. Создаёт physics_7_ch1..ch5.html из единого шаблона.
// Phase 0: скелет с инфраструктурой (header, navigator, sidebar, KaTeX, прогресс/XP, goTo),
// без §-контента — наполняется в Phase 1+.
const fs = require('fs');
const path = require('path');
const VER = '20260530';
const OUT = path.join(__dirname, '..', '..', 'frontend', 'textbooks');
const CHAPTERS = [
{
n: 1, slug: 'physics-7-ch1',
title: 'Физические методы познания природы',
range: '§§17',
accent: '#4f46e5', accentD: '#3730a3', accentSoft: '#e0e7ff',
coverGrad: 'linear-gradient(135deg,#312e81,#4f46e5 60%,#a5b4fc)',
paras: [
{ id:'p1', num:'§ 1', title:'Физика — наука о природе', wm:'?' },
{ id:'p2', num:'§ 2', title:'Тело, явление, величина', wm:'×' },
{ id:'p3', num:'§ 3', title:'Методы исследования в физике', wm:'⚙' },
{ id:'p4', num:'§ 4', title:'Прямые и косвенные измерения', wm:'=' },
{ id:'p5', num:'§ 5', title:'Единицы измерения. СИ', wm:'м' },
{ id:'p6', num:'§ 6', title:'Действия над физическими величинами', wm:'±' },
{ id:'p7', num:'§ 7', title:'Цена деления. Погрешность', wm:'∇' },
{ id:'final1', num:'Финал', title:'Итоги главы 1', wm:'★' },
],
achTitle: 'Юный физик',
},
{
n: 2, slug: 'physics-7-ch2',
title: 'Строение вещества',
range: '§§813',
accent: '#7c3aed', accentD: '#5b21b6', accentSoft: '#ede9fe',
coverGrad: 'linear-gradient(135deg,#4c1d95,#7c3aed 60%,#c4b5fd)',
paras: [
{ id:'p8', num:'§ 8', title:'Дискретное строение вещества', wm:'•' },
{ id:'p9', num:'§ 9', title:'Тепловое движение частиц', wm:'~' },
{ id:'p10', num:'§ 10', title:'Взаимодействие частиц', wm:'⇌' },
{ id:'p11', num:'§ 11', title:'Газ, жидкость, твёрдое', wm:'△' },
{ id:'p12', num:'§ 12', title:'Тепловое расширение', wm:'↔' },
{ id:'p13', num:'§ 13', title:'Температура. Термометры', wm:'°' },
{ id:'final2', num:'Финал', title:'Итоги главы 2', wm:'★' },
],
achTitle: 'Знаток вещества',
},
{
n: 3, slug: 'physics-7-ch3',
title: 'Движение и силы',
range: '§§1427',
accent: '#dc2626', accentD: '#991b1b', accentSoft: '#fee2e2',
coverGrad: 'linear-gradient(135deg,#7f1d1d,#dc2626 60%,#f87171)',
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:'★' },
],
achTitle: 'Мастер движения',
},
{
n: 4, slug: 'physics-7-ch4',
title: 'Давление',
range: '§§2835',
accent: '#d97706', accentD: '#92400e', accentSoft: '#fef3c7',
coverGrad: 'linear-gradient(135deg,#78350f,#d97706 60%,#fbbf24)',
paras: [
{ id:'p28', num:'§ 28', title:'Давление. Единицы давления', wm:'p' },
{ id:'p29', num:'§ 29', title:'Давление газа', wm:'∴' },
{ id:'p30', num:'§ 30', title:'Закон Паскаля', wm:'⊕' },
{ id:'p31', num:'§ 31', title:'Гидростатическое давление', wm:'≡' },
{ id:'p32', num:'§ 32', title:'Сообщающиеся сосуды', wm:'U' },
{ id:'p33', num:'§ 33', title:'Газы и их вес', wm:'⌒' },
{ id:'p34', num:'§ 34', title:'Атмосферное давление', wm:'' },
{ id:'p35', num:'§ 35', title:'Барометры и манометры', wm:'⏚' },
{ id:'final4', num:'Финал', title:'Итоги главы 4', wm:'★' },
],
achTitle: 'Властелин давления',
},
{
n: 5, slug: 'physics-7-ch5',
title: 'Работа. Мощность. Энергия',
range: '§§3642',
accent: '#10b981', accentD: '#047857', accentSoft: '#d1fae5',
coverGrad: 'linear-gradient(135deg,#064e3b,#10b981 60%,#6ee7b7)',
paras: [
{ id:'p36', num:'§ 36', title:'Механическая работа', wm:'A' },
{ id:'p37', num:'§ 37', title:'КПД', wm:'η' },
{ id:'p38', num:'§ 38', title:'Мощность', wm:'P' },
{ id:'p39', num:'§ 39', title:'Кинетическая энергия',wm:'Eк' },
{ id:'p40', num:'§ 40', title:'Потенциальная энергия',wm:'Eп' },
{ id:'p41', num:'§ 41', title:'Расчёт Eп = mgh', wm:'h' },
{ id:'p42', num:'§ 42', title:'Закон сохранения энергии',wm:'∑' },
{ id:'final5', num:'Финал', title:'Итоги главы 5', wm:'★' },
],
achTitle: 'Энергетик',
},
];
function makeHTML(C) {
const parasJs = C.paras.map(p => `{id:'${p.id}',num:'${p.num}',title:${JSON.stringify(p.title)},wm:'${p.wm}'}`).join(',');
const sections = C.paras.map(p =>
` <section id="sec-${p.id}" class="sec" data-watermark="${p.wm}">
<div class="sec-header"><span class="sec-num">${p.num}</span><h2 class="sec-h">${p.title}</h2></div>
<div id="${p.id}-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
</section>`).join('\n');
return `<!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 · Глава ${C.n} · ${C.title}</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=${VER}" 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:${C.accent}; --acc-d:${C.accentD}; --acc-soft:${C.accentSoft};
--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:${C.coverGrad};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 &middot; Глава ${C.n}</h1>
<div class="hdr-sub">${C.title} &middot; ${C.range}</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">Параграфы главы ${C.n}</div>
<div class="psel-grid" id="psel-grid"></div>
</div>
${sections}
</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 класс» &middot; Глава ${C.n} &middot; LearnSpace</footer>
<script>
'use strict';
const LS_PREFIX = 'physics7_ch${C.n}';
const _TB_SLUG = '${C.slug}';
const PARAS = [${parasJs}];
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: 'Начало главы ${C.n}', ch_done: '${C.achTitle}' };
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', '${C.achTitle}');
}
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-ch${C.n}-' + (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);
}
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 ? ' &mdash; ' + 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)">&#10003; ' + 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>
`;
}
CHAPTERS.forEach(C => {
const html = makeHTML(C);
const file = path.join(OUT, `physics_7_ch${C.n}.html`);
fs.writeFileSync(file, html, 'utf8');
console.log(`[gen_phys7_ch] ${file}${html.split('\n').length} lines`);
});
console.log('Done.');
+291
View File
@@ -0,0 +1,291 @@
#!/usr/bin/env node
// Генератор скелета лабораторного практикума Физики 7. Phase 0: только инфраструктура.
const fs = require('fs');
const path = require('path');
const VER = '20260530';
const OUT = path.join(__dirname, '..', '..', 'frontend', 'textbooks', 'physics_7_lab.html');
const LABS = [
{ id:'lr1', num:'ЛР 1', title:'Определение цены деления шкалы измерительного прибора', wm:'1', tag:'§ 7' },
{ id:'lr2', num:'ЛР 2', title:'Измерение длины', wm:'2', tag:'§ 4 · § 7' },
{ id:'lr3', num:'ЛР 3', title:'Измерение объёма', wm:'3', tag:'§ 4' },
{ id:'lr4', num:'ЛР 4', title:'Изучение неравномерного движения', wm:'4', tag:'§ 18' },
{ id:'lr5', num:'ЛР 5', title:'Измерение плотности вещества', wm:'5', tag:'§ 20' },
{ id:'lr6', num:'ЛР 6', title:'Изучение силы трения', wm:'6', tag:'§ 27' },
];
const labsJs = LABS.map(l => `{id:'${l.id}',num:'${l.num}',title:${JSON.stringify(l.title)},wm:'${l.wm}',tag:'${l.tag}'}`).join(',');
const sections = LABS.map(l =>
` <section id="sec-${l.id}" class="sec" data-watermark="${l.wm}">
<div class="sec-header">
<span class="sec-num">${l.num}</span>
<h2 class="sec-h">${l.title}</h2>
<span class="sec-tag">${l.tag}</span>
</div>
<div id="${l.id}-body"><div class="placeholder">Виртуальная лабораторная работа появится в Phase 7 (после контента глав).</div></div>
</section>`).join('\n');
const 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 · Лабораторный практикум</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}],throwOnError:false})"></script>
<script src="/js/api.js" defer></script>
<script src="/js/xp.js" defer></script>
<script src="/js/phys.js?v=${VER}" 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; --muted:#475569;
--border:#a5f3fc; --pri:#0891b2; --pri2:#0e7490; --pri-soft:#cffafe;
--acc:#06b6d4; --acc-d:#0e7490; --acc-soft:#cffafe;
--ok:#10b981; --ok-bg:#d1fae5; --fail:#dc2626; --fail-bg:#fee2e2; --warn:#f59e0b; --warn-bg:#fef3c7;
--sh:0 4px 16px rgba(8,145,178,.08); --sh-h:0 12px 36px rgba(8,145,178,.16);
}
html.dark{--bg:#0c2030;--card:#0e2436;--card-soft:#0b1a28;--text:#cffafe;--muted:#67e8f9;--border:#155e75;--pri-soft:rgba(8,145,178,.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,#164e63,#0891b2 60%,#22d3ee);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(190px,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:-8px;font-family:'Unbounded',sans-serif;font-size:5.6rem;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;flex-wrap:wrap}
.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.3rem;font-weight:800;color:var(--text);flex:1;min-width:0}
.sec-tag{font-size:.74rem;font-weight:700;color:var(--pri2);background:var(--pri-soft);padding:3px 9px;border-radius:99px;text-transform:uppercase;letter-spacing:.04em}
.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}}
.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}
</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 &middot; Лабораторный практикум</h1>
<div class="hdr-sub">6 виртуальных лабораторных работ</div>
</div>
<div class="hdr-side">
<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">Лабораторные работы</div>
<div class="psel-grid" id="psel-grid"></div>
</div>
${sections}
</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>
<footer class="foot">Интерактивный учебник «Физика 7 класс» &middot; Лабораторный практикум &middot; LearnSpace</footer>
<script>
'use strict';
const LS_PREFIX = 'physics7_lab';
const _TB_SLUG = 'physics-7-lab';
const LABS = [${labsJs}];
const TOTAL_LABS = LABS.length;
const SIDEBARS = {};
LABS.forEach(l => { SIDEBARS[l.id] = { title: 'Шпаргалка ' + l.num, rows: [['В разработке','симуляция и таблицы измерений появятся в Phase 7']] }; });
const ACH_LABELS = { start: 'Начало практикума', all_labs: 'Лаборант 7 класса' };
const STATE = { current: null, progress: {}, xp: 0, level: 1, achievements: new Map() };
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();
const done = LABS.every(l => (STATE.progress[l.id]||0) >= 100);
if(done && !STATE.achievements.has('all_labs')) achievement('all_labs');
}
function addXp(n, src){
if(!n) return;
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-lab-' + (src||'misc'));
}
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(id === 'all_labs' ? 80 : 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 buildSelector(){
const grid = document.getElementById('psel-grid');
if(!grid) return;
grid.innerHTML = LABS.map(l =>
'<button class="psel-card" data-id="' + l.id + '" data-prog-card="' + l.id + '">'
+ '<div class="psel-num">' + l.num + ' &middot; ' + l.tag + '</div>'
+ '<div class="psel-title">' + l.title + '</div>'
+ '<div class="psel-prog"><div class="psel-prog-fill" style="width:' + (STATE.progress[l.id]||0) + '%"></div></div>'
+ '</button>'
).join('');
grid.querySelectorAll('.psel-card').forEach(c => c.addEventListener('click', () => goTo(c.dataset.id)));
}
function goTo(id){
STATE.current = 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);
}
function buildSidebar(id){
const box = document.getElementById('sidebar-content');
if(!box) return;
const sb = SIDEBARS[id] || SIDEBARS[LABS[0].id];
const xpForLv = _xpForLevel(STATE.level), xpNext = _xpForLevel(STATE.level+1);
const xpPct = (xpNext - xpForLv) > 0 ? Math.round((STATE.xp - xpForLv) / (xpNext - xpForLv) * 100) : 100;
let html = '';
html += '<div class="sidecard" style="background:linear-gradient(135deg,var(--pri-soft),var(--acc-soft));border-color:var(--acc)"><h4>XP-прогресс <span style="float:right">Ур. ' + STATE.level + '</span></h4><div class="sidecard-row"><div style="height:8px;background:rgba(0,0,0,.07);border-radius:5px;overflow:hidden"><div style="height:100%;background:linear-gradient(90deg,var(--acc),var(--pri));width:' + xpPct + '%"></div></div><div style="display:flex;justify-content:space-between;font-size:.78rem;color:var(--muted);margin-top:5px"><span>' + STATE.xp + ' XP</span><span>' + xpNext + ' XP</span></div></div></div>';
html += '<div class="sidecard"><h4>' + sb.title + '</h4>';
sb.rows.forEach(([k,v]) => { html += '<div class="sidecard-row"><b>' + k + '</b>' + (v ? ' &mdash; ' + v : '') + '</div>'; });
html += '</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)">&#10003; ' + t + '</div>'; });
html += '</div>';
}
box.innerHTML = html;
}
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 init(){
loadProgress(); initTheme(); initSidebarToggle();
buildSelector(); refreshProgressUI(); goTo(LABS[0].id);
setTimeout(() => achievement('start'), 600);
}
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>
`;
fs.writeFileSync(OUT, html, 'utf8');
console.log(`[gen_phys7_lab] ${OUT}${html.split('\n').length} lines`);
@@ -0,0 +1,60 @@
-- Physics 7 hub migration (self-sufficient).
-- Creates physics-7 as a full 5-chapter + lab textbook in the style of physics-8/9/10:
-- physics-7 (hub, html_path = physics_7_hub.html)
-- physics-7-ch1 (Физические методы познания природы, §§1–7) → physics_7_ch1.html
-- physics-7-ch2 (Строение вещества, §§8–13) → physics_7_ch2.html
-- physics-7-ch3 (Движение и силы, §§14–27) → physics_7_ch3.html
-- physics-7-ch4 (Давление, §§28–35) → physics_7_ch4.html
-- physics-7-ch5 (Работа. Мощность. Энергия, §§36–42) → physics_7_ch5.html
-- physics-7-lab (Лабораторный практикум, 6 ЛР) → physics_7_lab.html
--
-- This migration is the single source of truth for physics-7 textbook structure.
-- It is idempotent and does not depend on any prior physics-7 migration.
-- 1. Ensure the parent physics-7 hub row exists.
INSERT OR IGNORE INTO textbooks
(slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active)
VALUES
('physics-7', 'physics', 7, 'Физика — 7 класс',
'',
'Первый курс физики: методы познания, строение вещества, движение и силы, давление, работа/мощность/энергия. 42 параграфа в 5 главах + 6 виртуальных лабораторных работ.',
'physics_7_hub.html', 48, 'sky', 3, 1);
-- 2. Update the parent physics-7 hub row (idempotent on re-runs).
UPDATE textbooks
SET
author = '',
para_count = 48,
html_path = 'physics_7_hub.html',
description = 'Первый курс физики: методы познания, строение вещества, движение и силы, давление, работа/мощность/энергия. 42 параграфа в 5 главах + 6 виртуальных лабораторных работ.',
color = 'sky'
WHERE slug = 'physics-7';
-- 3. Insert the 6 children (5 chapters + lab).
INSERT INTO textbooks
(slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active, parent_slug)
VALUES
('physics-7-ch1', 'physics', 7, 'Физика 7 · Физические методы познания природы',
'',
'§§1–7: физика как наука, физическое тело и явление, методы исследования, измерения, СИ, цена деления, погрешность.',
'physics_7_ch1.html', 7, 'sky', 1, 1, 'physics-7'),
('physics-7-ch2', 'physics', 7, 'Физика 7 · Строение вещества',
'',
'§§8–13: дискретное строение, тепловое движение, взаимодействие частиц, три состояния, тепловое расширение, термометр.',
'physics_7_ch2.html', 6, 'indigo', 2, 1, 'physics-7'),
('physics-7-ch3', 'physics', 7, 'Физика 7 · Движение и силы',
'',
'§§14–27: механическое движение, равномерное и неравномерное, плотность, силы тяжести/упругости/трения, динамометр, равнодействующая.',
'physics_7_ch3.html', 14, 'red', 3, 1, 'physics-7'),
('physics-7-ch4', 'physics', 7, 'Физика 7 · Давление',
'',
'§§28–35: давление, закон Паскаля, гидравлический пресс, гидростатика, сообщающиеся сосуды, атмосферное давление, барометры.',
'physics_7_ch4.html', 8, 'amber', 4, 1, 'physics-7'),
('physics-7-ch5', 'physics', 7, 'Физика 7 · Работа. Мощность. Энергия',
'',
'§§36–42: механическая работа, КПД, мощность, кинетическая и потенциальная энергия, закон сохранения механической энергии.',
'physics_7_ch5.html', 7, 'emerald', 5, 1, 'physics-7'),
('physics-7-lab', 'physics', 7, 'Физика 7 · Лабораторный практикум',
'',
'6 виртуальных лабораторных работ: цена деления, измерение длины, объёма, неравномерное движение, плотность, сила трения.',
'physics_7_lab.html', 6, 'cyan', 6, 1, 'physics-7');
+328 -1
View File
@@ -443,6 +443,319 @@ function Rparallel() {
return inv > 0 ? 1 / inv : Infinity;
}
// ======================================================================
// PHYSICS 7 HELPERS — силы, давление, гидростатика, барометры, энергия.
// Все размеры в SVG-пикселях; физические величины в СИ.
// ======================================================================
function forceVector(x, y, F, angleDeg, color, label, scale) {
scale = scale || 1.5;
const a = -angleDeg * Math.PI / 180;
const x2 = x + F * scale * Math.cos(a);
const y2 = y + F * scale * Math.sin(a);
let s = drawArrow(x, y, x2, y2, color || '#0f172a', 2.5, 9);
if (label) {
const lx = x2 + 8 * Math.cos(a);
const ly = y2 + 8 * Math.sin(a) + 4;
s += `<text x="${lx.toFixed(1)}" y="${ly.toFixed(1)}" font-family="Inter,sans-serif" font-size="12" font-weight="700" fill="${color || '#0f172a'}">${label}</text>`;
}
return s;
}
function dynamometer(x, y, h, Fmax, Fcurr) {
const w = 30;
const springTop = y + 14, springBot = y + h - 28;
const stretch = Math.max(0, Math.min(1, Fcurr / Fmax)) * (springBot - springTop);
const pointerY = springTop + stretch;
let s = `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="#fef3c7" stroke="#92400e" stroke-width="1.5" rx="4"/>`;
const coils = 8;
const coilH = (pointerY - springTop) / coils;
let path = `M ${x + w/2} ${springTop}`;
for (let i = 0; i < coils; i++) {
path += ` L ${x + (i%2 ? w-6 : 6)} ${springTop + (i+0.5)*coilH}`;
}
path += ` L ${x + w/2} ${pointerY}`;
s += `<path d="${path}" fill="none" stroke="#92400e" stroke-width="1.5"/>`;
const ticks = 10;
for (let i = 0; i <= ticks; i++) {
const ty = springTop + (springBot - springTop) * i / ticks;
s += `<line x1="${x + w}" y1="${ty}" x2="${x + w + 6}" y2="${ty}" stroke="#92400e" stroke-width="1.2"/>`;
if (i % 2 === 0) {
s += `<text x="${x + w + 10}" y="${ty + 4}" font-family="Inter,sans-serif" font-size="9" fill="#92400e">${(Fmax * i / ticks).toFixed(Fmax < 5 ? 1 : 0)}</text>`;
}
}
s += `<line x1="${x + 5}" y1="${pointerY}" x2="${x + w - 5}" y2="${pointerY}" stroke="#dc2626" stroke-width="2.5"/>`;
s += `<circle cx="${x + w/2}" cy="${pointerY + 8}" r="4" fill="#374151"/>`;
s += `<text x="${x + w/2}" y="${y + h + 18}" text-anchor="middle" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="#374151">F = ${Fcurr.toFixed(1)} Н</text>`;
return s;
}
function blockOnSurface(x, y, w, h, label, weights) {
let s = `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="#a78bfa" stroke="#5b21b6" stroke-width="1.5" rx="3"/>`;
if (label) s += `<text x="${x + w/2}" y="${y + h/2 + 4}" text-anchor="middle" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="#fff">${label}</text>`;
if (weights && weights > 0) {
const wH = 10, wW = w * 0.7;
for (let i = 0; i < weights; i++) {
const wy = y - (i + 1) * (wH + 2);
s += `<rect x="${x + (w - wW)/2}" y="${wy}" width="${wW}" height="${wH}" fill="#475569" stroke="#1e293b" stroke-width="1" rx="2"/>`;
}
}
return s;
}
function connectedVessels(x, y, kindA, kindB, levelY, fluidColor) {
fluidColor = fluidColor || '#60a5fa';
const widths = { cylinder: 60, wide: 100, narrow: 35 };
const wA = widths[kindA] || 60, wB = widths[kindB] || 60;
const h = 140, gap = 50;
const xA = x, xB = x + wA + gap;
let s = '';
s += `<line x1="${xA}" y1="${y}" x2="${xA}" y2="${y + h}" stroke="#0f172a" stroke-width="2"/>`;
s += `<line x1="${xA + wA}" y1="${y}" x2="${xA + wA}" y2="${y + h}" stroke="#0f172a" stroke-width="2"/>`;
s += `<line x1="${xB}" y1="${y}" x2="${xB}" y2="${y + h}" stroke="#0f172a" stroke-width="2"/>`;
s += `<line x1="${xB + wB}" y1="${y}" x2="${xB + wB}" y2="${y + h}" stroke="#0f172a" stroke-width="2"/>`;
s += `<line x1="${xA}" y1="${y + h}" x2="${xA + wA}" y2="${y + h}" stroke="#0f172a" stroke-width="2"/>`;
s += `<line x1="${xB}" y1="${y + h}" x2="${xB + wB}" y2="${y + h}" stroke="#0f172a" stroke-width="2"/>`;
s += `<line x1="${xA + wA}" y1="${y + h - 6}" x2="${xB}" y2="${y + h - 6}" stroke="#0f172a" stroke-width="2"/>`;
s += `<line x1="${xA + wA}" y1="${y + h}" x2="${xB}" y2="${y + h}" stroke="#0f172a" stroke-width="2"/>`;
const fillH = (y + h) - levelY;
s += `<rect x="${xA + 1}" y="${levelY}" width="${wA - 2}" height="${fillH}" fill="${fluidColor}" opacity="0.75"/>`;
s += `<rect x="${xB + 1}" y="${levelY}" width="${wB - 2}" height="${fillH}" fill="${fluidColor}" opacity="0.75"/>`;
s += `<rect x="${xA + wA}" y="${y + h - 6}" width="${gap}" height="6" fill="${fluidColor}" opacity="0.75"/>`;
s += `<line x1="${xA - 8}" y1="${levelY}" x2="${xB + wB + 8}" y2="${levelY}" stroke="#dc2626" stroke-width="1" stroke-dasharray="4 3"/>`;
return s;
}
function hydraulicPress(x, y, sSmall, sLarge, fSmall, fluidColor) {
fluidColor = fluidColor || '#60a5fa';
const fLarge = fSmall * sLarge / sSmall;
const scale = 6;
const wSmall = Math.max(20, Math.sqrt(sSmall) * scale);
const wLarge = Math.max(40, Math.sqrt(sLarge) * scale);
const cylH = 60, baseY = y + 110, gap = 40;
const xS = x, xL = x + wSmall + gap;
let s = '';
s += `<rect x="${xS}" y="${y + 50}" width="${wSmall}" height="${cylH}" fill="${fluidColor}" stroke="#0f172a" stroke-width="1.5" opacity="0.7"/>`;
s += `<rect x="${xS - 3}" y="${y + 40}" width="${wSmall + 6}" height="14" fill="#374151" stroke="#0f172a" stroke-width="1.5" rx="2"/>`;
s += `<rect x="${xL}" y="${y + 50}" width="${wLarge}" height="${cylH}" fill="${fluidColor}" stroke="#0f172a" stroke-width="1.5" opacity="0.7"/>`;
s += `<rect x="${xL - 3}" y="${y + 40}" width="${wLarge + 6}" height="14" fill="#374151" stroke="#0f172a" stroke-width="1.5" rx="2"/>`;
s += `<rect x="${xS}" y="${baseY}" width="${(xL + wLarge) - xS}" height="10" fill="${fluidColor}" stroke="#0f172a" stroke-width="1.5" opacity="0.7"/>`;
s += drawArrow(xS + wSmall/2, y + 20, xS + wSmall/2, y + 40, '#dc2626', 2.5, 8);
s += drawArrow(xL + wLarge/2, y + 20, xL + wLarge/2, y + 40, '#10b981', 2.5, 8);
s += `<text x="${xS + wSmall/2}" y="${y + 12}" text-anchor="middle" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="#dc2626">F_1 = ${fSmall.toFixed(0)} Н</text>`;
s += `<text x="${xL + wLarge/2}" y="${y + 12}" text-anchor="middle" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="#10b981">F_2 = ${fLarge.toFixed(0)} Н</text>`;
s += `<text x="${xS + wSmall/2}" y="${y + 80}" text-anchor="middle" font-family="Inter,sans-serif" font-size="10" fill="#0f172a">S_1 = ${sSmall} см²</text>`;
s += `<text x="${xL + wLarge/2}" y="${y + 80}" text-anchor="middle" font-family="Inter,sans-serif" font-size="10" fill="#0f172a">S_2 = ${sLarge} см²</text>`;
return s;
}
function mercuryBarometer(x, y, hMm) {
hMm = hMm == null ? 760 : hMm;
const maxMm = 800, scale = 0.4;
const tubeH = maxMm * scale, tubeW = 14;
const cupY = y + tubeH + 10, cupW = 70, cupH = 26;
const colH = hMm * scale, colTop = y + tubeH - colH;
let s = '';
s += `<rect x="${x - cupW/2 + tubeW/2}" y="${cupY}" width="${cupW}" height="${cupH}" fill="#94a3b8" stroke="#0f172a" stroke-width="1.5" rx="3"/>`;
s += `<rect x="${x - cupW/2 + tubeW/2 + 3}" y="${cupY + 5}" width="${cupW - 6}" height="${cupH - 8}" fill="#475569" opacity="0.85"/>`;
s += `<rect x="${x}" y="${y}" width="${tubeW}" height="${tubeH + 12}" fill="#e0f2fe" stroke="#0f172a" stroke-width="1.5"/>`;
s += `<rect x="${x + 1}" y="${colTop}" width="${tubeW - 2}" height="${colH + 12}" fill="#475569"/>`;
s += `<text x="${x + tubeW + 8}" y="${y + 14}" font-family="Inter,sans-serif" font-size="10" font-style="italic" fill="#64748b">вакуум</text>`;
for (let mm = 0; mm <= maxMm; mm += 50) {
const ty = y + tubeH - mm * scale;
s += `<line x1="${x + tubeW}" y1="${ty}" x2="${x + tubeW + 6}" y2="${ty}" stroke="#0f172a" stroke-width="1"/>`;
if (mm % 100 === 0) s += `<text x="${x + tubeW + 10}" y="${ty + 3}" font-family="Inter,sans-serif" font-size="9" fill="#0f172a">${mm}</text>`;
}
s += `<line x1="${x - 12}" y1="${colTop}" x2="${x}" y2="${colTop}" stroke="#dc2626" stroke-width="2"/>`;
s += `<text x="${x - 14}" y="${colTop + 4}" text-anchor="end" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="#dc2626">${hMm} мм</text>`;
return s;
}
function aneroidBarometer(cx, cy, r, pressurePa) {
const pMm = pressurePa / 133.322;
const pMin = 720, pMax = 800;
const angDeg = -210 + 240 * Math.max(0, Math.min(1, (pMm - pMin) / (pMax - pMin)));
const angRad = angDeg * Math.PI / 180;
let s = '';
s += `<circle cx="${cx}" cy="${cy}" r="${r}" fill="#fff" stroke="#0f172a" stroke-width="2"/>`;
s += `<circle cx="${cx}" cy="${cy}" r="${r - 8}" fill="none" stroke="#cbd5e1" stroke-width="1"/>`;
for (let p = pMin; p <= pMax; p += 10) {
const a = (-210 + 240 * (p - pMin) / (pMax - pMin)) * Math.PI / 180;
const r1 = r - 4, r2 = r - 12;
const x1 = cx + r1 * Math.cos(a), y1 = cy + r1 * Math.sin(a);
const x2 = cx + r2 * Math.cos(a), y2 = cy + r2 * Math.sin(a);
s += `<line x1="${x1.toFixed(1)}" y1="${y1.toFixed(1)}" x2="${x2.toFixed(1)}" y2="${y2.toFixed(1)}" stroke="#0f172a" stroke-width="${p % 20 === 0 ? 2 : 1}"/>`;
if (p % 20 === 0) {
const rT = r - 22;
const xT = cx + rT * Math.cos(a), yT = cy + rT * Math.sin(a) + 3;
s += `<text x="${xT.toFixed(1)}" y="${yT.toFixed(1)}" text-anchor="middle" font-family="Inter,sans-serif" font-size="9" fill="#0f172a">${p}</text>`;
}
}
const tipX = cx + (r - 18) * Math.cos(angRad), tipY = cy + (r - 18) * Math.sin(angRad);
s += `<line x1="${cx}" y1="${cy}" x2="${tipX.toFixed(1)}" y2="${tipY.toFixed(1)}" stroke="#dc2626" stroke-width="3" stroke-linecap="round"/>`;
s += `<circle cx="${cx}" cy="${cy}" r="4" fill="#374151"/>`;
s += `<text x="${cx}" y="${cy + r + 16}" text-anchor="middle" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="#374151">${pMm.toFixed(0)} мм рт. ст.</text>`;
return s;
}
function uManometer(x, y, w, h, deltaH, fluidColor) {
fluidColor = fluidColor || '#0891b2';
const tube = 14;
const xL = x, xR = x + w - tube;
const baseY = y + h;
let s = `<path d="M ${xL} ${y} L ${xL} ${baseY - tube/2} Q ${xL} ${baseY} ${xL + tube/2} ${baseY} L ${xR + tube/2} ${baseY} Q ${xR + tube} ${baseY} ${xR + tube} ${baseY - tube/2} L ${xR + tube} ${y}" fill="none" stroke="#0f172a" stroke-width="1.5"/>`;
s += `<path d="M ${xL + tube} ${y} L ${xL + tube} ${baseY - tube/2 - 8}" fill="none" stroke="#0f172a" stroke-width="1.5" opacity="0.4"/>`;
s += `<path d="M ${xR} ${y} L ${xR} ${baseY - tube/2 - 8}" fill="none" stroke="#0f172a" stroke-width="1.5" opacity="0.4"/>`;
const lLevel = y + h * 0.45 + deltaH / 2;
const rLevel = y + h * 0.45 - deltaH / 2;
s += `<rect x="${xL + 1}" y="${lLevel}" width="${tube - 2}" height="${baseY - lLevel}" fill="${fluidColor}" opacity="0.85"/>`;
s += `<rect x="${xR + 1}" y="${rLevel}" width="${tube - 2}" height="${baseY - rLevel}" fill="${fluidColor}" opacity="0.85"/>`;
if (Math.abs(deltaH) > 1) {
s += `<line x1="${xR + tube + 6}" y1="${lLevel}" x2="${xR + tube + 6}" y2="${rLevel}" stroke="#dc2626" stroke-width="1.5"/>`;
s += `<text x="${xR + tube + 10}" y="${(lLevel + rLevel)/2 + 4}" font-family="Inter,sans-serif" font-size="10" font-weight="700" fill="#dc2626">Δh</text>`;
}
return s;
}
function rulerWithError(x, y, lenCm, mmPerDiv) {
mmPerDiv = mmPerDiv || 1;
const pxPerCm = 30;
const w = lenCm * pxPerCm, h = 28;
let s = `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="#fef3c7" stroke="#92400e" stroke-width="1.5" rx="2"/>`;
const totalDivs = (lenCm * 10) / mmPerDiv;
for (let i = 0; i <= totalDivs; i++) {
const tx = x + (i * mmPerDiv / 10) * pxPerCm;
const mm = i * mmPerDiv;
const isCm = mm % 10 === 0, isHalfCm = mm % 5 === 0;
const tickH = isCm ? 12 : (isHalfCm ? 8 : 5);
s += `<line x1="${tx}" y1="${y}" x2="${tx}" y2="${y + tickH}" stroke="#0f172a" stroke-width="${isCm ? 1.5 : 1}"/>`;
if (isCm) s += `<text x="${tx}" y="${y + h - 4}" text-anchor="middle" font-family="Inter,sans-serif" font-size="9" font-weight="600" fill="#0f172a">${mm/10}</text>`;
}
s += `<text x="${x + w + 6}" y="${y + h/2 + 4}" font-family="Inter,sans-serif" font-size="10" font-weight="700" fill="#92400e">см</text>`;
return s;
}
function bimetal(x, y, w, h, deltaT) {
const ang = Math.max(-30, Math.min(30, deltaT * 0.3));
let s = `<g transform="translate(${x},${y}) rotate(${ang} 0 ${h/2})">`;
s += `<rect x="0" y="0" width="${w}" height="${h/2}" fill="#fbbf24" stroke="#92400e" stroke-width="1"/>`;
s += `<rect x="0" y="${h/2}" width="${w}" height="${h/2}" fill="#94a3b8" stroke="#374151" stroke-width="1"/>`;
s += `<text x="${w/2}" y="${h + 14}" text-anchor="middle" font-family="Inter,sans-serif" font-size="10" fill="#374151">ΔT = ${deltaT.toFixed(0)} °C</text>`;
s += `</g>`;
return s;
}
function expandingRod(x, y, l0, alpha, deltaT) {
const dl = l0 * alpha * deltaT * 1000;
const len = l0 + dl;
const h = 16;
const color = tempColor(deltaT, 0, 100);
let s = `<rect x="${x}" y="${y}" width="${len.toFixed(1)}" height="${h}" fill="${color}" stroke="#0f172a" stroke-width="1.5" rx="2"/>`;
s += `<line x1="${x + l0}" y1="${y - 4}" x2="${x + l0}" y2="${y + h + 4}" stroke="#0f172a" stroke-width="1" stroke-dasharray="3 2"/>`;
if (dl > 1) {
s += drawArrow(x + l0, y + h + 14, x + len, y + h + 14, '#dc2626', 1.8, 6);
s += `<text x="${x + l0 + dl/2}" y="${y + h + 28}" text-anchor="middle" font-family="Inter,sans-serif" font-size="10" font-weight="700" fill="#dc2626">Δl = ${dl.toFixed(1)} px</text>`;
}
return s;
}
// === HillSlideSim — тележка скатывается с горки (для §42, закон сохранения) ===
class HillSlideSim {
constructor(opts) {
opts = opts || {};
this.x0 = opts.x0 || 30;
this.y0 = opts.y0 || 200;
this.hStart = opts.hStart != null ? opts.hStart : 5;
this.m = opts.mass || 1;
this.g = opts.g || 9.8;
this.friction = opts.friction || 0;
this.scale = opts.scale || 30;
this.reset();
}
reset() { this.t = 0; this.x = 0; this.v = 0; this.h = this.hStart; }
step(dt) {
const gEff = this.g * (1 - this.friction);
this.t += dt;
const dropped = this.hStart - this.h;
if (this.h <= 0) { this.h = 0; this.v = Math.sqrt(2 * gEff * this.hStart); return; }
this.v = Math.sqrt(2 * gEff * Math.max(0, dropped));
this.x += this.v * dt;
const L = this.hStart * 4;
const xRel = Math.min(this.x, L) / L;
this.h = this.hStart * Math.pow(1 - xRel, 2);
}
getEnergies() {
return {
Ek: 0.5 * this.m * this.v * this.v,
Ep: this.m * this.g * this.h,
Etot: 0.5 * this.m * this.v * this.v + this.m * this.g * this.h
};
}
renderProfile() {
const L = this.hStart * 4;
const pxL = L * this.scale, pxH = this.hStart * this.scale;
const baseY = this.y0;
let path = `M ${this.x0} ${baseY - pxH}`;
const N = 40;
for (let i = 1; i <= N; i++) {
const xRel = i / N;
const yRel = Math.pow(1 - xRel, 2);
path += ` L ${(this.x0 + pxL * xRel).toFixed(1)} ${(baseY - pxH * yRel).toFixed(1)}`;
}
let s = '';
s += `<line x1="${this.x0 - 10}" y1="${baseY}" x2="${this.x0 + pxL + 30}" y2="${baseY}" stroke="#0f172a" stroke-width="2"/>`;
s += `<path d="${path} L ${this.x0 + pxL} ${baseY} L ${this.x0} ${baseY} Z" fill="#86efac" opacity="0.6" stroke="#10b981" stroke-width="1.5"/>`;
const cartX = this.x0 + this.x * this.scale;
const cartY = baseY - this.h * this.scale;
s += `<rect x="${(cartX - 10).toFixed(1)}" y="${(cartY - 14).toFixed(1)}" width="20" height="12" fill="#dc2626" stroke="#0f172a" stroke-width="1.5" rx="2"/>`;
s += `<circle cx="${(cartX - 5).toFixed(1)}" cy="${(cartY - 2).toFixed(1)}" r="3" fill="#0f172a"/>`;
s += `<circle cx="${(cartX + 5).toFixed(1)}" cy="${(cartY - 2).toFixed(1)}" r="3" fill="#0f172a"/>`;
return s;
}
}
// === PendulumSim — математический маятник (для §42) ===
class PendulumSim {
constructor(opts) {
opts = opts || {};
this.cx = opts.cx || 150;
this.cy = opts.cy || 40;
this.L = opts.length || 1.2;
this.m = opts.mass || 0.5;
this.g = opts.g || 9.8;
this.phi0 = (opts.angleDeg || 25) * Math.PI / 180;
this.scale = opts.scale || 80;
this.reset();
}
reset() { this.t = 0; this.phi = this.phi0; this.omega = 0; }
step(dt) {
const alpha = -(this.g / this.L) * Math.sin(this.phi);
this.omega += alpha * dt;
this.phi += this.omega * dt;
this.t += dt;
}
getEnergies() {
const h = this.L * (1 - Math.cos(this.phi));
const v = Math.abs(this.omega) * this.L;
return {
Ek: 0.5 * this.m * v * v,
Ep: this.m * this.g * h,
Etot: 0.5 * this.m * v * v + this.m * this.g * h
};
}
render() {
const pxL = this.L * this.scale;
const bx = this.cx + pxL * Math.sin(this.phi);
const by = this.cy + pxL * Math.cos(this.phi);
let s = '';
s += `<rect x="${this.cx - 30}" y="${this.cy - 8}" width="60" height="8" fill="#475569" stroke="#0f172a" stroke-width="1"/>`;
s += `<line x1="${this.cx}" y1="${this.cy}" x2="${bx.toFixed(1)}" y2="${by.toFixed(1)}" stroke="#0f172a" stroke-width="1.5"/>`;
s += `<circle cx="${bx.toFixed(1)}" cy="${by.toFixed(1)}" r="9" fill="#dc2626" stroke="#7f1d1d" stroke-width="1.5"/>`;
s += `<circle cx="${this.cx}" cy="${this.cy}" r="3" fill="#0f172a"/>`;
return s;
}
}
// === Экспорт ===
window.PHYS = {
tempColor: tempColor,
@@ -473,6 +786,20 @@ window.PHYS = {
atmToPa: atmToPa,
paToAtm: paToAtm,
litersToM3: litersToM3,
m3ToLiters: m3ToLiters
m3ToLiters: m3ToLiters,
// Physics 7 — Phase 0
forceVector: forceVector,
dynamometer: dynamometer,
blockOnSurface: blockOnSurface,
connectedVessels: connectedVessels,
hydraulicPress: hydraulicPress,
mercuryBarometer: mercuryBarometer,
aneroidBarometer: aneroidBarometer,
uManometer: uManometer,
rulerWithError: rulerWithError,
bimetal: bimetal,
expandingRod: expandingRod,
HillSlideSim: HillSlideSim,
PendulumSim: PendulumSim
};
})();
+359
View File
@@ -0,0 +1,359 @@
<!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 · Глава 1 · Физические методы познания природы</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>
<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:#4f46e5; --acc-d:#3730a3; --acc-soft:#e0e7ff;
--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,#312e81,#4f46e5 60%,#a5b4fc);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 &middot; Глава 1</h1>
<div class="hdr-sub">Физические методы познания природы &middot; §§17</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">Параграфы главы 1</div>
<div class="psel-grid" id="psel-grid"></div>
</div>
<section id="sec-p1" class="sec" data-watermark="?">
<div class="sec-header"><span class="sec-num">§ 1</span><h2 class="sec-h">Физика — наука о природе</h2></div>
<div id="p1-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
</section>
<section id="sec-p2" class="sec" data-watermark="×">
<div class="sec-header"><span class="sec-num">§ 2</span><h2 class="sec-h">Тело, явление, величина</h2></div>
<div id="p2-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
</section>
<section id="sec-p3" class="sec" data-watermark="⚙">
<div class="sec-header"><span class="sec-num">§ 3</span><h2 class="sec-h">Методы исследования в физике</h2></div>
<div id="p3-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
</section>
<section id="sec-p4" class="sec" data-watermark="=">
<div class="sec-header"><span class="sec-num">§ 4</span><h2 class="sec-h">Прямые и косвенные измерения</h2></div>
<div id="p4-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
</section>
<section id="sec-p5" class="sec" data-watermark="м">
<div class="sec-header"><span class="sec-num">§ 5</span><h2 class="sec-h">Единицы измерения. СИ</h2></div>
<div id="p5-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
</section>
<section id="sec-p6" class="sec" data-watermark="±">
<div class="sec-header"><span class="sec-num">§ 6</span><h2 class="sec-h">Действия над физическими величинами</h2></div>
<div id="p6-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
</section>
<section id="sec-p7" class="sec" data-watermark="∇">
<div class="sec-header"><span class="sec-num">§ 7</span><h2 class="sec-h">Цена деления. Погрешность</h2></div>
<div id="p7-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
</section>
<section id="sec-final1" class="sec" data-watermark="★">
<div class="sec-header"><span class="sec-num">Финал</span><h2 class="sec-h">Итоги главы 1</h2></div>
<div id="final1-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 класс» &middot; Глава 1 &middot; LearnSpace</footer>
<script>
'use strict';
const LS_PREFIX = 'physics7_ch1';
const _TB_SLUG = 'physics-7-ch1';
const PARAS = [{id:'p1',num:'§ 1',title:"Физика — наука о природе",wm:'?'},{id:'p2',num:'§ 2',title:"Тело, явление, величина",wm:'×'},{id:'p3',num:'§ 3',title:"Методы исследования в физике",wm:'⚙'},{id:'p4',num:'§ 4',title:"Прямые и косвенные измерения",wm:'='},{id:'p5',num:'§ 5',title:"Единицы измерения. СИ",wm:'м'},{id:'p6',num:'§ 6',title:"Действия над физическими величинами",wm:'±'},{id:'p7',num:'§ 7',title:"Цена деления. Погрешность",wm:'∇'},{id:'final1',num:'Финал',title:"Итоги главы 1",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: 'Начало главы 1', 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-ch1-' + (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);
}
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 ? ' &mdash; ' + 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)">&#10003; ' + 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>
+355
View File
@@ -0,0 +1,355 @@
<!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 · Глава 2 · Строение вещества</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>
<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:#7c3aed; --acc-d:#5b21b6; --acc-soft:#ede9fe;
--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,#4c1d95,#7c3aed 60%,#c4b5fd);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 &middot; Глава 2</h1>
<div class="hdr-sub">Строение вещества &middot; §§813</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">Параграфы главы 2</div>
<div class="psel-grid" id="psel-grid"></div>
</div>
<section id="sec-p8" class="sec" data-watermark="•">
<div class="sec-header"><span class="sec-num">§ 8</span><h2 class="sec-h">Дискретное строение вещества</h2></div>
<div id="p8-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
</section>
<section id="sec-p9" class="sec" data-watermark="~">
<div class="sec-header"><span class="sec-num">§ 9</span><h2 class="sec-h">Тепловое движение частиц</h2></div>
<div id="p9-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
</section>
<section id="sec-p10" class="sec" data-watermark="⇌">
<div class="sec-header"><span class="sec-num">§ 10</span><h2 class="sec-h">Взаимодействие частиц</h2></div>
<div id="p10-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
</section>
<section id="sec-p11" class="sec" data-watermark="△">
<div class="sec-header"><span class="sec-num">§ 11</span><h2 class="sec-h">Газ, жидкость, твёрдое</h2></div>
<div id="p11-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
</section>
<section id="sec-p12" class="sec" data-watermark="↔">
<div class="sec-header"><span class="sec-num">§ 12</span><h2 class="sec-h">Тепловое расширение</h2></div>
<div id="p12-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
</section>
<section id="sec-p13" class="sec" data-watermark="°">
<div class="sec-header"><span class="sec-num">§ 13</span><h2 class="sec-h">Температура. Термометры</h2></div>
<div id="p13-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
</section>
<section id="sec-final2" class="sec" data-watermark="★">
<div class="sec-header"><span class="sec-num">Финал</span><h2 class="sec-h">Итоги главы 2</h2></div>
<div id="final2-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 класс» &middot; Глава 2 &middot; LearnSpace</footer>
<script>
'use strict';
const LS_PREFIX = 'physics7_ch2';
const _TB_SLUG = 'physics-7-ch2';
const PARAS = [{id:'p8',num:'§ 8',title:"Дискретное строение вещества",wm:'•'},{id:'p9',num:'§ 9',title:"Тепловое движение частиц",wm:'~'},{id:'p10',num:'§ 10',title:"Взаимодействие частиц",wm:'⇌'},{id:'p11',num:'§ 11',title:"Газ, жидкость, твёрдое",wm:'△'},{id:'p12',num:'§ 12',title:"Тепловое расширение",wm:'↔'},{id:'p13',num:'§ 13',title:"Температура. Термометры",wm:'°'},{id:'final2',num:'Финал',title:"Итоги главы 2",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: 'Начало главы 2', 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-ch2-' + (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);
}
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 ? ' &mdash; ' + 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)">&#10003; ' + 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>
+387
View File
@@ -0,0 +1,387 @@
<!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>
<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 &middot; Глава 3</h1>
<div class="hdr-sub">Движение и силы &middot; §§1427</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 класс» &middot; Глава 3 &middot; 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);
}
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 ? ' &mdash; ' + 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)">&#10003; ' + 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>
+363
View File
@@ -0,0 +1,363 @@
<!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 · Глава 4 · Давление</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
<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>
<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:#d97706; --acc-d:#92400e; --acc-soft:#fef3c7;
--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,#78350f,#d97706 60%,#fbbf24);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 &middot; Глава 4</h1>
<div class="hdr-sub">Давление &middot; §§2835</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">Параграфы главы 4</div>
<div class="psel-grid" id="psel-grid"></div>
</div>
<section id="sec-p28" class="sec" data-watermark="p">
<div class="sec-header"><span class="sec-num">§ 28</span><h2 class="sec-h">Давление. Единицы давления</h2></div>
<div id="p28-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
</section>
<section id="sec-p29" class="sec" data-watermark="∴">
<div class="sec-header"><span class="sec-num">§ 29</span><h2 class="sec-h">Давление газа</h2></div>
<div id="p29-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
</section>
<section id="sec-p30" class="sec" data-watermark="⊕">
<div class="sec-header"><span class="sec-num">§ 30</span><h2 class="sec-h">Закон Паскаля</h2></div>
<div id="p30-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
</section>
<section id="sec-p31" class="sec" data-watermark="≡">
<div class="sec-header"><span class="sec-num">§ 31</span><h2 class="sec-h">Гидростатическое давление</h2></div>
<div id="p31-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
</section>
<section id="sec-p32" class="sec" data-watermark="U">
<div class="sec-header"><span class="sec-num">§ 32</span><h2 class="sec-h">Сообщающиеся сосуды</h2></div>
<div id="p32-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
</section>
<section id="sec-p33" class="sec" data-watermark="⌒">
<div class="sec-header"><span class="sec-num">§ 33</span><h2 class="sec-h">Газы и их вес</h2></div>
<div id="p33-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
</section>
<section id="sec-p34" class="sec" data-watermark="">
<div class="sec-header"><span class="sec-num">§ 34</span><h2 class="sec-h">Атмосферное давление</h2></div>
<div id="p34-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
</section>
<section id="sec-p35" class="sec" data-watermark="⏚">
<div class="sec-header"><span class="sec-num">§ 35</span><h2 class="sec-h">Барометры и манометры</h2></div>
<div id="p35-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
</section>
<section id="sec-final4" class="sec" data-watermark="★">
<div class="sec-header"><span class="sec-num">Финал</span><h2 class="sec-h">Итоги главы 4</h2></div>
<div id="final4-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 класс» &middot; Глава 4 &middot; LearnSpace</footer>
<script>
'use strict';
const LS_PREFIX = 'physics7_ch4';
const _TB_SLUG = 'physics-7-ch4';
const PARAS = [{id:'p28',num:'§ 28',title:"Давление. Единицы давления",wm:'p'},{id:'p29',num:'§ 29',title:"Давление газа",wm:'∴'},{id:'p30',num:'§ 30',title:"Закон Паскаля",wm:'⊕'},{id:'p31',num:'§ 31',title:"Гидростатическое давление",wm:'≡'},{id:'p32',num:'§ 32',title:"Сообщающиеся сосуды",wm:'U'},{id:'p33',num:'§ 33',title:"Газы и их вес",wm:'⌒'},{id:'p34',num:'§ 34',title:"Атмосферное давление",wm:''},{id:'p35',num:'§ 35',title:"Барометры и манометры",wm:'⏚'},{id:'final4',num:'Финал',title:"Итоги главы 4",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: 'Начало главы 4', 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-ch4-' + (src||'misc'));
if(STATE.level > prev){
const pop = document.getElementById('ach-popup');
if(pop){ document.getElementById('ach-text').textContent = 'Уровень ' + STATE.level + '!'; pop.classList.add('show'); setTimeout(()=>pop.classList.remove('show'), 2600); }
}
}
function 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);
}
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 ? ' &mdash; ' + 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)">&#10003; ' + 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>
+359
View File
@@ -0,0 +1,359 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>Физика 7 · Глава 5 · Работа. Мощность. Энергия</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
<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>
<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:#10b981; --acc-d:#047857; --acc-soft:#d1fae5;
--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,#064e3b,#10b981 60%,#6ee7b7);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 &middot; Глава 5</h1>
<div class="hdr-sub">Работа. Мощность. Энергия &middot; §§3642</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">Параграфы главы 5</div>
<div class="psel-grid" id="psel-grid"></div>
</div>
<section id="sec-p36" class="sec" data-watermark="A">
<div class="sec-header"><span class="sec-num">§ 36</span><h2 class="sec-h">Механическая работа</h2></div>
<div id="p36-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
</section>
<section id="sec-p37" class="sec" data-watermark="η">
<div class="sec-header"><span class="sec-num">§ 37</span><h2 class="sec-h">КПД</h2></div>
<div id="p37-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
</section>
<section id="sec-p38" class="sec" data-watermark="P">
<div class="sec-header"><span class="sec-num">§ 38</span><h2 class="sec-h">Мощность</h2></div>
<div id="p38-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
</section>
<section id="sec-p39" class="sec" data-watermark="Eк">
<div class="sec-header"><span class="sec-num">§ 39</span><h2 class="sec-h">Кинетическая энергия</h2></div>
<div id="p39-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
</section>
<section id="sec-p40" class="sec" data-watermark="Eп">
<div class="sec-header"><span class="sec-num">§ 40</span><h2 class="sec-h">Потенциальная энергия</h2></div>
<div id="p40-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
</section>
<section id="sec-p41" class="sec" data-watermark="h">
<div class="sec-header"><span class="sec-num">§ 41</span><h2 class="sec-h">Расчёт Eп = mgh</h2></div>
<div id="p41-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
</section>
<section id="sec-p42" class="sec" data-watermark="∑">
<div class="sec-header"><span class="sec-num">§ 42</span><h2 class="sec-h">Закон сохранения энергии</h2></div>
<div id="p42-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
</section>
<section id="sec-final5" class="sec" data-watermark="★">
<div class="sec-header"><span class="sec-num">Финал</span><h2 class="sec-h">Итоги главы 5</h2></div>
<div id="final5-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 класс» &middot; Глава 5 &middot; LearnSpace</footer>
<script>
'use strict';
const LS_PREFIX = 'physics7_ch5';
const _TB_SLUG = 'physics-7-ch5';
const PARAS = [{id:'p36',num:'§ 36',title:"Механическая работа",wm:'A'},{id:'p37',num:'§ 37',title:"КПД",wm:'η'},{id:'p38',num:'§ 38',title:"Мощность",wm:'P'},{id:'p39',num:'§ 39',title:"Кинетическая энергия",wm:'Eк'},{id:'p40',num:'§ 40',title:"Потенциальная энергия",wm:'Eп'},{id:'p41',num:'§ 41',title:"Расчёт Eп = mgh",wm:'h'},{id:'p42',num:'§ 42',title:"Закон сохранения энергии",wm:'∑'},{id:'final5',num:'Финал',title:"Итоги главы 5",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: 'Начало главы 5', 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-ch5-' + (src||'misc'));
if(STATE.level > prev){
const pop = document.getElementById('ach-popup');
if(pop){ document.getElementById('ach-text').textContent = 'Уровень ' + STATE.level + '!'; pop.classList.add('show'); setTimeout(()=>pop.classList.remove('show'), 2600); }
}
}
function 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);
}
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 ? ' &mdash; ' + 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)">&#10003; ' + 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>
+946
View File
@@ -0,0 +1,946 @@
<!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">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Физика 7 класс — учебник</title>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;800;900&family=Inter:wght@400;500;600;700&family=Unbounded:wght@400;700;800;900&display=swap" rel="stylesheet">
<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"></script>
<script src="/js/api.js" defer></script>
<script src="/js/xp.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>
<style>
:root{
--bg:#f0f9ff; --card:#fff;
--text:#0f172a; --muted:#475569;
--border:#bae6fd;
--pri:#0284c7; --pri-d:#0c4a6e;
--pri-soft:#e0f2fe;
--ch1:#4f46e5; --ch1-d:#3730a3;
--ch2:#7c3aed; --ch2-d:#5b21b6;
--ch3:#dc2626; --ch3-d:#991b1b;
--ch4:#d97706; --ch4-d:#92400e;
--ch5:#10b981; --ch5-d:#047857;
--ch6:#0891b2; --ch6-d:#0e7490;
--sh:0 4px 16px rgba(2,132,199,.10);
--sh-h:0 12px 36px rgba(2,132,199,.18);
}
html.dark{
--bg:#0c1e2e; --card:#0e2436;
--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.55;transition:background .25s,color .25s}
/* HEADER */
.hdr{position:relative;background:linear-gradient(110deg,#0c4a6e 0%,#0284c7 55%,#bae6fd 100%);color:#fff;padding:32px 24px 28px;overflow:hidden;border-bottom:2px solid rgba(237,233,254,.18)}
.hdr::before{content:'ФИЗИКА';position:absolute;right:-14px;top:-18%;font-family:'Outfit',sans-serif;font-size:clamp(5rem,16vw,13rem);font-weight:900;letter-spacing:-.04em;color:transparent;-webkit-text-stroke:1.5px rgba(237,233,254,.14);line-height:1;pointer-events:none;user-select:none}
.hdr-inner{position:relative;z-index:1;max-width:1100px;margin:0 auto;display:flex;align-items:center;gap:18px;flex-wrap:wrap}
.hdr-back{display:inline-flex;align-items:center;gap:8px;padding:8px 14px;background:rgba(255,255,255,.14);border-radius:9px;color:#fff;text-decoration:none;font-size:.85rem;font-weight:600;transition:background .15s}
.hdr-back:hover{background:rgba(255,255,255,.24)}
.hdr h1{font-family:'Outfit',sans-serif;font-size:1.85rem;font-weight:900;letter-spacing:-.01em}
.hdr-sub{font-size:.92rem;opacity:.88;margin-top:4px}
.hdr-side{margin-left:auto;display:flex;gap:8px}
.hdr-btn{padding:8px 12px;background:rgba(255,255,255,.14);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}
.hdr-btn:hover{background:rgba(255,255,255,.24)}
.ic{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
main{max-width:1100px;margin:0 auto;padding:32px 24px 60px}
/* OVERALL PROGRESS */
.prog-overall{background:linear-gradient(135deg,var(--pri-soft),rgba(186,230,253,.18));border:1px solid var(--border);border-radius:14px;padding:14px 18px;margin-bottom:28px;display:flex;gap:14px;align-items:center;flex-wrap:wrap}
.po-icon{width:46px;height:46px;border-radius:12px;background:linear-gradient(135deg,#0284c7,#bae6fd);color:#fff;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-family:'Outfit',sans-serif;font-size:1.4rem;font-weight:900;font-style:italic}
.po-text{flex:1;min-width:160px}
.po-label{font-size:.78rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:4px}
.po-bar{height:8px;background:rgba(2,132,199,.14);border-radius:5px;overflow:hidden;margin-top:6px}
.po-fill{height:100%;background:linear-gradient(90deg,var(--pri),#bae6fd);border-radius:5px;transition:width .5s}
.po-xp{display:inline-flex;align-items:center;gap:6px;padding:6px 14px;background:linear-gradient(135deg,#7dd3fc,var(--pri));color:#fff;border-radius:99px;font-size:.8rem;font-weight:800;font-family:'Unbounded',sans-serif;letter-spacing:.02em;box-shadow:0 4px 12px rgba(2,132,199,.24)}
/* CHAPTER GRID — 4 sections: 3 chapters + lab */
.ch-grid{display:grid;grid-template-columns:1fr;gap:18px;margin-bottom:30px}
@media(min-width:680px){.ch-grid{grid-template-columns:1fr 1fr}}
.ch-card{background:var(--card);border:1.5px solid var(--border);border-radius:18px;overflow:hidden;display:flex;flex-direction:column;transition:transform .2s,box-shadow .2s,border-color .2s;cursor:pointer;text-decoration:none;color:inherit}
.ch-card:hover{transform:translateY(-4px);box-shadow:var(--sh-h)}
.ch-cover{padding:22px 22px 18px;color:#fff;position:relative;overflow:hidden}
.ch-cover-wm{position:absolute;right:-8px;top:-22px;font-size:5.2rem;font-weight:900;font-family:'Outfit',sans-serif;line-height:1;color:rgba(255,255,255,.20);pointer-events:none;letter-spacing:-.04em}
.ch-num{display:inline-block;padding:4px 10px;background:rgba(255,255,255,.22);border-radius:99px;font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;margin-bottom:8px;position:relative;z-index:1}
.ch-title{font-family:'Outfit',sans-serif;font-size:1.1rem;font-weight:800;letter-spacing:-.01em;position:relative;z-index:1;line-height:1.3}
.ch-range{font-size:.84rem;opacity:.88;margin-top:4px;position:relative;z-index:1;font-weight:500}
.ch-cover.ch1{background:linear-gradient(135deg,#312e81,#4f46e5 60%,#a5b4fc)}
.ch-cover.ch2{background:linear-gradient(135deg,#4c1d95,#7c3aed 60%,#c4b5fd)}
.ch-cover.ch3{background:linear-gradient(135deg,#7f1d1d,#dc2626 60%,#f87171)}
.ch-cover.ch4{background:linear-gradient(135deg,#78350f,#d97706 60%,#fbbf24)}
.ch-cover.ch5{background:linear-gradient(135deg,#064e3b,#10b981 60%,#6ee7b7)}
.ch-cover.ch6{background:linear-gradient(135deg,#164e63,#0891b2 60%,#22d3ee)}
.ch-body{padding:16px 20px 18px;display:flex;flex-direction:column;flex:1}
.ch-desc{font-size:.88rem;color:var(--text);opacity:.84;flex:1;margin-bottom:12px;line-height:1.55}
.ch-prog{margin-bottom:12px}
.ch-prog-label{display:flex;justify-content:space-between;font-size:.74rem;color:var(--muted);font-weight:600;margin-bottom:4px}
.ch-prog-bar{height:6px;background:rgba(0,0,0,.07);border-radius:4px;overflow:hidden}
.ch-prog-fill{height:100%;border-radius:4px;transition:width .5s}
.ch-card.ch1-card .ch-prog-fill{background:linear-gradient(90deg,var(--ch1),var(--ch1-d))}
.ch-card.ch2-card .ch-prog-fill{background:linear-gradient(90deg,var(--ch2),var(--ch2-d))}
.ch-card.ch3-card .ch-prog-fill{background:linear-gradient(90deg,var(--ch3),var(--ch3-d))}
.ch-card.ch4-card .ch-prog-fill{background:linear-gradient(90deg,var(--ch4),var(--ch4-d))}
.ch-card.ch5-card .ch-prog-fill{background:linear-gradient(90deg,var(--ch5),var(--ch5-d))}
.ch-card.ch6-card .ch-prog-fill{background:linear-gradient(90deg,var(--ch6),var(--ch6-d))}
.ch-action{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;border-radius:11px;font-weight:700;font-size:.9rem;color:#fff;transition:filter .15s}
.ch-action:hover{filter:brightness(1.08)}
.ch-card.ch1-card .ch-action{background:linear-gradient(135deg,var(--ch1),#a5b4fc)}
.ch-card.ch2-card .ch-action{background:linear-gradient(135deg,var(--ch2),#c4b5fd)}
.ch-card.ch3-card .ch-action{background:linear-gradient(135deg,var(--ch3),#f87171)}
.ch-card.ch4-card .ch-action{background:linear-gradient(135deg,var(--ch4),#fbbf24)}
.ch-card.ch5-card .ch-action{background:linear-gradient(135deg,var(--ch5),#6ee7b7)}
.ch-card.ch6-card .ch-action{background:linear-gradient(135deg,var(--ch6),#22d3ee)}
/* ACHIEVEMENT STRIP */
.ach-strip{background:var(--card);border:1.5px solid var(--border);border-radius:16px;padding:18px 22px;margin-bottom:28px;display:flex;align-items:center;gap:16px;transition:border-color .4s,box-shadow .4s}
.ach-strip.lit{border-color:#0ea5e9;box-shadow:0 0 0 3px rgba(14,165,233,.22)}
.ach-icon{width:52px;height:52px;border-radius:14px;background:rgba(0,0,0,.06);display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background .4s}
.ach-strip.lit .ach-icon{background:linear-gradient(135deg,#7dd3fc,#0284c7)}
.ach-icon svg{width:28px;height:28px;stroke:var(--muted);fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.ach-strip.lit .ach-icon svg{stroke:#fff}
.ach-text{flex:1}
.ach-title{font-weight:800;font-size:1.02rem;color:var(--text)}
.ach-sub{font-size:.85rem;color:var(--muted);margin-top:2px}
.ach-strip.lit .ach-title{color:#0c4a6e}
.foot{text-align:center;padding:24px 16px;color:var(--muted);font-size:.78rem;border-top:1px solid var(--border)}
/* COURSE FINAL */
.final-wrap{margin:0 0 28px;background:var(--card);border:1.5px solid var(--border);border-radius:18px;overflow:hidden;box-shadow:var(--sh)}
.final-head{padding:18px 22px;background:linear-gradient(135deg,#0c4a6e 0%,#0284c7 55%,#7dd3fc 100%);color:#fff;cursor:pointer;display:flex;align-items:center;gap:14px;user-select:none;transition:filter .15s}
.final-head:hover{filter:brightness(1.06)}
.final-head-icon{width:46px;height:46px;border-radius:12px;background:rgba(255,255,255,.18);display:flex;align-items:center;justify-content:center;flex-shrink:0}
.final-head-icon svg{width:26px;height:26px;stroke:#fff;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.final-head-text{flex:1;min-width:0}
.final-head-tag{display:inline-block;padding:3px 9px;background:rgba(255,255,255,.22);border-radius:99px;font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;margin-bottom:4px}
.final-head-title{font-family:'Outfit',sans-serif;font-size:1.18rem;font-weight:800;letter-spacing:-.01em;line-height:1.25}
.final-head-sub{font-size:.84rem;opacity:.9;margin-top:2px}
.final-chevron{flex-shrink:0;transition:transform .25s}
.final-chevron svg{width:24px;height:24px;stroke:#fff;fill:none;stroke-width:2.4;stroke-linecap:round;stroke-linejoin:round}
.final-wrap.open .final-chevron{transform:rotate(180deg)}
.final-body{display:none;padding:22px}
.final-wrap.open .final-body{display:block}
.fin-section-title{font-family:'Outfit',sans-serif;font-size:1.18rem;font-weight:800;color:var(--text);margin:8px 0 14px;letter-spacing:-.005em;display:flex;align-items:center;gap:9px}
.fin-section-title svg{width:20px;height:20px;stroke:var(--pri);fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
/* CHEAT SHEET */
.cheat-grid{display:grid;grid-template-columns:1fr;gap:14px;margin-bottom:28px}
@media(min-width:680px){.cheat-grid{grid-template-columns:1fr 1fr}}
@media(min-width:1000px){.cheat-grid{grid-template-columns:repeat(3,1fr)}}
.cheat-card{border:1.5px solid var(--border);border-radius:13px;padding:14px 16px;background:var(--card);position:relative;overflow:hidden}
.cheat-card::before{content:'';position:absolute;left:0;top:0;bottom:0;width:4px}
.cheat-card.c1::before{background:linear-gradient(180deg,var(--ch1),var(--ch1-d))}
.cheat-card.c2::before{background:linear-gradient(180deg,var(--ch2),var(--ch2-d))}
.cheat-card.c3::before{background:linear-gradient(180deg,var(--ch3),var(--ch3-d))}
.cheat-card.c4::before{background:linear-gradient(180deg,var(--ch4),var(--ch4-d))}
.cheat-card.c5::before{background:linear-gradient(180deg,var(--ch5),var(--ch5-d))}
.cheat-head{display:flex;align-items:center;gap:9px;margin-bottom:9px;padding-left:6px}
.cheat-badge{font-size:.7rem;font-weight:800;padding:2px 8px;border-radius:99px;color:#fff;letter-spacing:.05em;text-transform:uppercase}
.cheat-card.c1 .cheat-badge{background:var(--ch1)}
.cheat-card.c2 .cheat-badge{background:var(--ch2)}
.cheat-card.c3 .cheat-badge{background:var(--ch3)}
.cheat-card.c4 .cheat-badge{background:var(--ch4)}
.cheat-card.c5 .cheat-badge{background:var(--ch5)}
.cheat-title{font-weight:800;color:var(--text);font-size:.98rem}
.cheat-list{list-style:none;padding-left:6px;margin:0}
.cheat-list li{padding:6px 0;border-bottom:1px dashed var(--border);font-size:.92rem;line-height:1.5;color:var(--text)}
.cheat-list li:last-child{border-bottom:0}
/* BOSS PROGRESS */
.boss-overall-bar{background:linear-gradient(135deg,rgba(2,132,199,.08),rgba(186,230,253,.10));border:1px solid var(--border);border-radius:12px;padding:13px 16px;margin:6px 0 18px;display:flex;gap:14px;align-items:center;flex-wrap:wrap}
.boss-overall-bar .lab{font-weight:700;font-size:.95rem;color:var(--text);min-width:200px}
.boss-overall-bar .bar{flex:1;min-width:160px;height:9px;background:rgba(2,132,199,.14);border-radius:5px;overflow:hidden}
.boss-overall-bar .fill{height:100%;background:linear-gradient(90deg,var(--pri),#bae6fd,#7dd3fc);transition:width .5s;border-radius:5px}
/* BOSS CARDS */
.boss-card{background:var(--card);border:2px solid var(--border);border-radius:14px;padding:16px;margin-bottom:14px;transition:border-color .35s,box-shadow .35s,transform .2s}
.boss-card.solved{border-color:#10b981;box-shadow:0 0 0 3px rgba(16,185,129,.18)}
.boss-head{display:flex;align-items:center;gap:10px;margin-bottom:10px;flex-wrap:wrap}
.boss-tag{font-size:.7rem;font-weight:800;padding:3px 9px;border-radius:99px;background:rgba(2,132,199,.14);color:var(--pri-d);letter-spacing:.04em;text-transform:uppercase}
html.dark .boss-tag{color:#bae6fd}
.boss-title{font-family:'Outfit',sans-serif;font-weight:800;color:var(--text);font-size:1.02rem;flex:1;min-width:0}
.boss-q{padding:12px 14px;background:rgba(2,132,199,.06);border-radius:10px;font-size:.96rem;line-height:1.55;margin-bottom:10px;color:var(--text)}
.boss-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:6px}
.boss-input{padding:8px 12px;border:1.5px solid var(--border);border-radius:8px;background:var(--card);color:var(--text);font-family:'JetBrains Mono',monospace;width:130px;text-align:center;font-size:.95rem;transition:border-color .15s}
.boss-input:focus{outline:0;border-color:var(--pri);box-shadow:0 0 0 3px var(--pri-soft)}
.boss-btn{padding:8px 16px;border-radius:9px;background:var(--card);color:var(--text);border:1.5px solid var(--border);font-weight:700;font-size:.88rem;cursor:pointer;font-family:inherit;transition:background .15s,border-color .15s,transform .1s}
.boss-btn:hover{background:var(--pri-soft);border-color:var(--pri)}
.boss-btn:active{transform:scale(.96)}
.boss-btn.primary{background:linear-gradient(135deg,var(--pri),#7dd3fc);color:#fff;border-color:transparent}
.boss-btn.primary:hover{filter:brightness(1.08)}
.boss-fb{padding:10px 14px;border-radius:9px;font-weight:600;font-size:.88rem;margin-top:8px;display:none;line-height:1.45}
.boss-fb.ok{display:block;background:#d1fae5;color:#065f46;border-left:4px solid #10b981}
.boss-fb.fail{display:block;background:#fee2e2;color:#7f1d1d;border-left:4px solid #dc2626}
html.dark .boss-fb.ok{background:rgba(16,185,129,.18);color:#a7f3d0}
html.dark .boss-fb.fail{background:rgba(220,38,38,.18);color:#fecaca}
.boss-hint-txt{margin-top:8px;padding:9px 13px;background:rgba(245,158,11,.12);border-left:3px solid #f59e0b;border-radius:6px;font-size:.86rem;color:var(--text);display:none;line-height:1.5}
.boss-hint-txt.show{display:block}
/* FINAL CTA */
.final-cta{margin-top:24px;padding:18px 20px;border-radius:14px;background:linear-gradient(135deg,#e0f2fe,#bae6fd);border:1.5px solid #7dd3fc;display:none;align-items:center;gap:14px;flex-wrap:wrap}
.final-cta.show{display:flex}
html.dark .final-cta{background:linear-gradient(135deg,rgba(2,132,199,.22),rgba(91,33,182,.18));border-color:#0284c7}
.final-cta-icon{width:48px;height:48px;border-radius:12px;background:linear-gradient(135deg,#7dd3fc,#0284c7);display:flex;align-items:center;justify-content:center;flex-shrink:0}
.final-cta-icon svg{width:28px;height:28px;stroke:#fff;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.final-cta-txt{flex:1;min-width:180px}
.final-cta-title{font-weight:800;color:#0c4a6e;font-size:1.05rem;font-family:'Outfit',sans-serif}
html.dark .final-cta-title{color:#bae6fd}
.final-cta-sub{font-size:.86rem;color:#0369a1;margin-top:2px}
html.dark .final-cta-sub{color:#bae6fd}
.final-cta-btn{padding:10px 18px;border-radius:10px;background:linear-gradient(135deg,var(--pri),#7dd3fc);color:#fff;text-decoration:none;font-weight:800;font-size:.9rem;display:inline-flex;align-items:center;gap:7px;transition:filter .15s}
.final-cta-btn:hover{filter:brightness(1.1)}
.final-cta-btn svg{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
</style>
</head>
<body>
<header class="hdr">
<div class="hdr-inner">
<div>
<a href="/textbooks" class="hdr-back">
<svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>
К каталогу
</a>
</div>
<div>
<h1>Физика &mdash; 7 класс</h1>
<div class="hdr-sub">Первый курс физики: методы познания природы, строение вещества, движение и силы, давление, работа и энергия, лабораторный практикум</div>
</div>
<div class="hdr-side">
<button id="theme-btn" class="hdr-btn" title="Сменить тему">
<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>
<section class="prog-overall">
<div class="po-icon">f</div>
<div class="po-text">
<div class="po-label">Общий прогресс по курсу</div>
<div id="overall-text" style="font-size:1.05rem;font-weight:700">Загрузка...</div>
<div class="po-bar"><div id="overall-fill" class="po-fill" style="width:0%"></div></div>
</div>
<div id="hero-xp-badge" class="po-xp" style="display:none" data-gamified>0 XP</div>
</section>
<div class="ch-grid">
<a href="/textbook/physics-7-ch1" class="ch-card ch1-card" id="ch-1">
<div class="ch-cover ch1">
<div class="ch-cover-wm">&Sigma;</div>
<div class="ch-num">Глава 1</div>
<div class="ch-title">Методы познания природы</div>
<div class="ch-range">&sect;1&ndash;&sect;7 + Финал</div>
</div>
<div class="ch-body">
<div class="ch-desc">Физика как наука. Тело, явление, величина. Методы исследования. Прямые и косвенные измерения. СИ. Цена деления. Погрешность.</div>
<div class="ch-prog">
<div class="ch-prog-label"><span>Прогресс</span><span id="prog-1">0%</span></div>
<div class="ch-prog-bar"><div class="ch-prog-fill" id="fill-1" style="width:0%"></div></div>
</div>
<div class="ch-action">
<span id="btn-1">Открыть главу</span>
<svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg>
</div>
</div>
</a>
<a href="/textbook/physics-7-ch2" class="ch-card ch2-card" id="ch-2">
<div class="ch-cover ch2">
<div class="ch-cover-wm">&deg;</div>
<div class="ch-num">Глава 2</div>
<div class="ch-title">Строение вещества</div>
<div class="ch-range">&sect;8&ndash;&sect;13 + Финал</div>
</div>
<div class="ch-body">
<div class="ch-desc">Дискретное строение. Тепловое движение. Взаимодействие частиц. Три состояния вещества. Тепловое расширение. Температура и термометры.</div>
<div class="ch-prog">
<div class="ch-prog-label"><span>Прогресс</span><span id="prog-2">0%</span></div>
<div class="ch-prog-bar"><div class="ch-prog-fill" id="fill-2" style="width:0%"></div></div>
</div>
<div class="ch-action">
<span id="btn-2">Открыть главу</span>
<svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg>
</div>
</div>
</a>
<a href="/textbook/physics-7-ch3" class="ch-card ch3-card" id="ch-3">
<div class="ch-cover ch3">
<div class="ch-cover-wm">F</div>
<div class="ch-num">Глава 3</div>
<div class="ch-title">Движение и силы</div>
<div class="ch-range">&sect;14&ndash;&sect;27 + Финал</div>
</div>
<div class="ch-body">
<div class="ch-desc">Механическое движение. Равномерное и неравномерное движение. Скорость. Масса. Плотность. Силы тяжести, упругости, трения. Динамометр. Равнодействующая.</div>
<div class="ch-prog">
<div class="ch-prog-label"><span>Прогресс</span><span id="prog-3">0%</span></div>
<div class="ch-prog-bar"><div class="ch-prog-fill" id="fill-3" style="width:0%"></div></div>
</div>
<div class="ch-action">
<span id="btn-3">Открыть главу</span>
<svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg>
</div>
</div>
</a>
<a href="/textbook/physics-7-ch4" class="ch-card ch4-card" id="ch-4">
<div class="ch-cover ch4">
<div class="ch-cover-wm">P</div>
<div class="ch-num">Глава 4</div>
<div class="ch-title">Давление</div>
<div class="ch-range">&sect;28&ndash;&sect;35 + Финал</div>
</div>
<div class="ch-body">
<div class="ch-desc">Давление твёрдых тел, газов, жидкостей. Закон Паскаля. Гидравлический пресс. Гидростатика. Сообщающиеся сосуды. Атмосферное давление. Барометры и манометры.</div>
<div class="ch-prog">
<div class="ch-prog-label"><span>Прогресс</span><span id="prog-4">0%</span></div>
<div class="ch-prog-bar"><div class="ch-prog-fill" id="fill-4" style="width:0%"></div></div>
</div>
<div class="ch-action">
<span id="btn-4">Открыть главу</span>
<svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg>
</div>
</div>
</a>
<a href="/textbook/physics-7-ch5" class="ch-card ch5-card" id="ch-5">
<div class="ch-cover ch5">
<div class="ch-cover-wm">E</div>
<div class="ch-num">Глава 5</div>
<div class="ch-title">Работа. Мощность. Энергия</div>
<div class="ch-range">&sect;36&ndash;&sect;42 + Финал</div>
</div>
<div class="ch-body">
<div class="ch-desc">Механическая работа. КПД. Мощность. Кинетическая и потенциальная энергия. Закон сохранения механической энергии.</div>
<div class="ch-prog">
<div class="ch-prog-label"><span>Прогресс</span><span id="prog-5">0%</span></div>
<div class="ch-prog-bar"><div class="ch-prog-fill" id="fill-5" style="width:0%"></div></div>
</div>
<div class="ch-action">
<span id="btn-5">Открыть главу</span>
<svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg>
</div>
</div>
</a>
<a href="/textbook/physics-7-lab" class="ch-card ch6-card" id="ch-6">
<div class="ch-cover ch6">
<div class="ch-cover-wm">ЛР</div>
<div class="ch-num">Лаборатория</div>
<div class="ch-title">Лабораторный практикум</div>
<div class="ch-range">6 виртуальных ЛР</div>
</div>
<div class="ch-body">
<div class="ch-desc">6 виртуальных лабораторных работ: цена деления, измерение длины и объёма, неравномерное движение, плотность вещества, сила трения.</div>
<div class="ch-prog">
<div class="ch-prog-label"><span>Прогресс</span><span id="prog-6">0%</span></div>
<div class="ch-prog-bar"><div class="ch-prog-fill" id="fill-6" style="width:0%"></div></div>
</div>
<div class="ch-action">
<span id="btn-6">Открыть практикум</span>
<svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg>
</div>
</div>
</a>
</div>
<section class="final-wrap" id="course-final">
<div class="final-head" id="final-head" tabindex="0" role="button" aria-expanded="false" aria-controls="final-body">
<div class="final-head-icon">
<svg viewBox="0 0 24 24"><path d="M7 4h10v6a5 5 0 0 1-10 0V4z"/><path d="M5 4h2v2H5a2 2 0 0 1 0-4M19 4h-2v2h2a2 2 0 0 0 0-4M9 20h6M12 15v5"/></svg>
</div>
<div class="final-head-text">
<div class="final-head-tag">Финал курса</div>
<div class="final-head-title">Босс-проверка по всему курсу</div>
<div class="final-head-sub">Шпаргалка курса и 10 интегрированных боссов по всем 3 главам. Победи всех — получи «Магистр физики 7» и +150 XP.</div>
</div>
<div class="final-chevron"><svg viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"/></svg></div>
</div>
<div class="final-body" id="final-body">
<div class="fin-section-title">
<svg viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h10"/></svg>
Шпаргалка курса
</div>
<div class="cheat-grid">
<div class="cheat-card c1">
<div class="cheat-head">
<span class="cheat-badge">Гл. 1</span>
<span class="cheat-title">Методы познания</span>
</div>
<ul class="cheat-list">
<li>$C = (X_2 - X_1)/N$ — цена деления</li>
<li>$\Delta X = C/2$ — погрешность</li>
<li>СИ: м, кг, с, А, К, моль, кд</li>
<li>Кратные: к (10³), М (10⁶); дольные: м (10⁻³), мк (10⁻⁶)</li>
<li>$S = ab$, $V = abc$ — косвенные измерения</li>
</ul>
</div>
<div class="cheat-card c2">
<div class="cheat-head">
<span class="cheat-badge">Гл. 2</span>
<span class="cheat-title">Строение вещества</span>
</div>
<ul class="cheat-list">
<li>$d_{мол} \sim 10^{-10}$ м</li>
<li>Тверд.: фиксированная форма и объём</li>
<li>Жидк.: объём фиксирован, форма — нет</li>
<li>Газ: ни форма, ни объём не фиксированы</li>
<li>$\Delta l \sim \Delta T$ (расширение)</li>
<li>Шкала Цельсия: 0 — лёд, 100 — кипение</li>
</ul>
</div>
<div class="cheat-card c3">
<div class="cheat-head">
<span class="cheat-badge">Гл. 3</span>
<span class="cheat-title">Движение и силы</span>
</div>
<ul class="cheat-list">
<li>$v = s/t$, $[v] = $ м/с</li>
<li>$\langle v\rangle = s_{полн}/t_{полн}$</li>
<li>$\rho = m/V$, $[\rho] = $ кг/м³</li>
<li>$F_т = mg$, $g \approx 10$ Н/кг</li>
<li>$P = mg$ (вес на опоре)</li>
<li>$R = F_1 + F_2$ (сонапр.), $R = |F_1 - F_2|$ (противопол.)</li>
<li>$F_{тр} \sim N$</li>
</ul>
</div>
<div class="cheat-card c4">
<div class="cheat-head">
<span class="cheat-badge">Гл. 4</span>
<span class="cheat-title">Давление</span>
</div>
<ul class="cheat-list">
<li>$p = F/S$, $[p] = $ Па</li>
<li>Закон Паскаля: $p$ передаётся одинаково</li>
<li>Гидравлика: $F_2/F_1 = S_2/S_1$</li>
<li>$p = \rho g h$ — гидростатика</li>
<li>Сообщ. сосуды → один уровень (однородная жидкость)</li>
<li>$p_0 = 101\,325$ Па $= 760$ мм рт. ст.</li>
</ul>
</div>
<div class="cheat-card c5">
<div class="cheat-head">
<span class="cheat-badge">Гл. 5</span>
<span class="cheat-title">Работа и энергия</span>
</div>
<ul class="cheat-list">
<li>$A = Fs$ (при $\vec F \parallel \vec s$), $[A] = $ Дж</li>
<li>$\eta = A_{полез}/A_{полн} \cdot 100\%$</li>
<li>$P = A/t$, $[P] = $ Вт</li>
<li>$E_к = mv^2/2$</li>
<li>$E_п = mgh$</li>
<li>$E_к + E_п = \text{const}$ (без трения)</li>
</ul>
</div>
</div>
<div class="fin-section-title">
<svg viewBox="0 0 24 24"><path d="M14.5 3.5l-5 5L4 4l1.5 6L3 12l5 1 1 5 2.5-2.5 6 1.5-4.5-5.5 5-5"/></svg>
10 интегрированных боссов
</div>
<div class="boss-overall-bar">
<div class="lab" id="fin-boss-lab">Боссов побеждено: 0 / 10</div>
<div class="bar"><div class="fill" id="fin-boss-fill" style="width:0%"></div></div>
</div>
<div id="fin-bosses-container"></div>
<div class="final-cta" id="final-cta">
<div class="final-cta-icon">
<svg viewBox="0 0 24 24"><path d="M6 9H4l-1-3h18l-1 3h-2M6 9l1 6h10l1-6M6 9h12"/><path d="M9 21h6M12 15v6"/></svg>
</div>
<div class="final-cta-txt">
<div class="final-cta-title">Курс Физика 8 пройден!</div>
<div class="final-cta-sub">Вы прошли всю итоговую проверку курса. +150 XP, ачивка «Магистр физики 7» получена.</div>
</div>
<a href="/textbooks" class="final-cta-btn">
К каталогу учебников
<svg viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg>
</a>
</div>
</div>
</section>
<div class="ach-strip" id="ach-strip">
<div class="ach-icon">
<svg viewBox="0 0 24 24">
<path d="M6 9H4l-1-3h18l-1 3h-2M6 9l1 6h10l1-6M6 9h12"/><path d="M9 21h6M12 15v6"/>
</svg>
</div>
<div class="ach-text">
<div class="ach-title">Магистр физики 7</div>
<div class="ach-sub" id="ach-sub">Прочитайте все 40 параграфов и выполните все 7 лабораторных работ, чтобы получить достижение</div>
</div>
</div>
</main>
<footer class="foot">
Интерактивный учебник «Физика &mdash; 7 класс» &middot; LearnSpace
</footer>
<script>
'use strict';
/* THEME */
(function(){
var saved = localStorage.getItem('physics7_theme') || localStorage.getItem('theme') || 'light';
if (saved === 'dark') document.documentElement.classList.add('dark');
var lab = document.getElementById('theme-lab');
if (lab) lab.textContent = saved === 'dark' ? 'Светлая' : 'Тёмная';
document.getElementById('theme-btn').addEventListener('click', function(){
document.documentElement.classList.toggle('dark');
var dark = document.documentElement.classList.contains('dark');
localStorage.setItem('physics7_theme', dark ? 'dark' : 'light');
localStorage.setItem('theme', dark ? 'dark' : 'light');
if (lab) lab.textContent = dark ? 'Светлая' : 'Тёмная';
});
})();
/* PROGRESS */
var TOTAL = 48; // 7 + 6 + 14 + 8 + 7 + 6
var CH_PARA = {
'physics-7-ch1': 7,
'physics-7-ch2': 6,
'physics-7-ch3': 14,
'physics-7-ch4': 8,
'physics-7-ch5': 7,
'physics-7-lab': 6
};
var CH_IDX = {
'physics-7-ch1': 1,
'physics-7-ch2': 2,
'physics-7-ch3': 3,
'physics-7-ch4': 4,
'physics-7-ch5': 5,
'physics-7-lab': 6
};
function setChProg(idx, readCount, total) {
var pct = total ? Math.round(readCount * 100 / total) : 0;
var labelEl = document.getElementById('prog-' + idx);
var fillEl = document.getElementById('fill-' + idx);
var btnEl = document.getElementById('btn-' + idx);
if (labelEl) labelEl.textContent = pct + '%';
if (fillEl) fillEl.style.width = pct + '%';
if (btnEl) {
if (readCount > 0 && readCount < total) btnEl.textContent = (idx === 6 ? 'Продолжить ЛР' : 'Продолжить');
else if (readCount >= total) btnEl.textContent = 'Открыть снова';
else btnEl.textContent = (idx === 6 ? 'Открыть практикум' : 'Открыть главу');
}
return pct;
}
var FIN_ACH_KEY = 'physics7_course_master';
function renderProgress(children) {
var totalRead = 0;
for (var i = 0; i < children.length; i++) {
var ch = children[i];
var idx = CH_IDX[ch.slug];
if (!idx) continue;
var read = ch.progress ? ch.progress.read.length : 0;
var total = ch.para_count || CH_PARA[ch.slug] || 1;
totalRead += read;
setChProg(idx, read, total);
}
var pct = Math.round(totalRead * 100 / TOTAL);
var overallEl = document.getElementById('overall-text');
var fillEl = document.getElementById('overall-fill');
if (overallEl) overallEl.textContent = totalRead + ' из ' + TOTAL + ' пунктов \xb7 ' + pct + '%';
if (fillEl) fillEl.style.width = pct + '%';
var xpBadge = document.getElementById('hero-xp-badge');
var xp = parseInt(localStorage.getItem('physics7_xp') || '0', 10) || 0;
if (xpBadge && xp > 0) {
xpBadge.style.display = '';
xpBadge.textContent = xp + ' XP';
}
var mastered = localStorage.getItem(FIN_ACH_KEY) === '1';
if (totalRead >= TOTAL || mastered) {
var strip = document.getElementById('ach-strip');
var sub = document.getElementById('ach-sub');
if (strip) strip.classList.add('lit');
if (sub) {
if (mastered) sub.textContent = 'Выполнено! Вы — Магистр физики 7.';
else sub.textContent = 'Выполнено! Вы прошли весь курс физики 7 класса.';
}
}
}
/* COURSE FINAL — lazy bosses */
var FIN_BOSS_KEY = 'physics7_course_bosses';
var FIN_BOSSES = [
{
n: 1,
title: 'Цена деления линейки',
tag: 'Гл. 1',
q: 'На линейке между отметками $0$ и $1$ см нанесено $10$ маленьких делений. Найди цену деления в мм.',
hint: '$C = (X_2 - X_1)/N = 10\\,\\text{мм}/10 = 1$ мм.',
ans: 1,
tol: 0.05,
step: '0.1'
},
{
n: 2,
title: 'Плотность и масса',
tag: 'Гл. 3',
q: 'Алюминиевый брусок имеет массу $m = 270$ г. Плотность алюминия $\\rho = 2700$ кг/м³. Найди объём бруска в см³.',
hint: '$m = 0{,}270$ кг. $V = m/\\rho = 0{,}270/2700 = 10^{-4}$ м³ $= 100$ см³.',
ans: 100,
tol: 1,
step: '1'
},
{
n: 3,
title: 'Сила тяжести на Луне',
tag: 'Гл. 3',
q: 'Тело массой $m = 250$ г. Что покажет динамометр на Луне, где $g_Л = 1{,}6$ Н/кг? Ответ в Н (округли до сотых).',
hint: '$F = mg = 0{,}25 \\cdot 1{,}6 = 0{,}40$ Н.',
ans: 0.40,
tol: 0.02,
step: '0.01'
},
{
n: 4,
title: 'Равнодействующая трёх сил',
tag: 'Гл. 3',
q: 'На тело действуют три силы по одной прямой: $F_1 = 30$ Н вправо, $F_2 = 20$ Н вправо, $F_3 = 40$ Н влево. Найди модуль равнодействующей в Н.',
hint: 'Вправо: $30 + 20 = 50$ Н. Влево: $40$ Н. $|R| = 50 - 40 = 10$ Н (направлена вправо).',
ans: 10,
tol: 0.5,
step: '1'
},
{
n: 5,
title: 'Давление на дне бассейна',
tag: 'Гл. 4',
q: 'Глубина бассейна $h = 3$ м. Найди гидростатическое давление воды на дно в кПа. ($g = 9{,}8$ Н/кг, $\\rho_{воды} = 1000$ кг/м³)',
hint: '$p = \\rho g h = 1000 \\cdot 9{,}8 \\cdot 3 = 29\\,400$ Па $= 29{,}4$ кПа.',
ans: 29.4,
tol: 0.5,
step: '0.1'
},
{
n: 6,
title: 'Гидравлический пресс',
tag: 'Гл. 4',
q: 'Малый поршень имеет площадь $S_1 = 2$ см², большой — $S_2 = 200$ см². На малый поршень давит сила $F_1 = 50$ Н. Какую силу в Н создаёт большой поршень?',
hint: 'По закону Паскаля $p_1 = p_2$, $F_2 = F_1 \\cdot S_2/S_1 = 50 \\cdot 200/2 = 5000$ Н.',
ans: 5000,
tol: 50,
step: '1'
},
{
n: 7,
title: 'Атмосфера на 30-м этаже',
tag: 'Гл. 4',
q: 'Высота 30-этажного дома около $H = 90$ м. На каждые $12$ м подъёма атм. давление падает на $1$ мм рт. ст. На сколько мм рт. ст. отличается давление на крыше от первого этажа?',
hint: '$\\Delta p = H/12 = 90/12 = 7{,}5$ мм рт. ст.',
ans: 7.5,
tol: 0.2,
step: '0.1'
},
{
n: 8,
title: 'Работа силы трения',
tag: 'Гл. 5',
q: 'Брусок переместили на $s = 5$ м, преодолевая силу трения $F_{тр} = 20$ Н. Какую работу совершила сила трения? Ответ в Дж со знаком (трение направлено против движения).',
hint: '$A_{тр} = -F_{тр} \\cdot s = -20 \\cdot 5 = -100$ Дж (отрицательная — энергия рассеивается в тепло).',
ans: -100,
tol: 1,
step: '1'
},
{
n: 9,
title: 'КПД наклонной плоскости',
tag: 'Гл. 5',
q: 'Груз массой $m = 50$ кг поднимают по наклону длиной $l = 4$ м на высоту $h = 1$ м, прикладывая силу $F = 150$ Н вдоль наклона. ($g = 10$ Н/кг). Найди КПД в %.',
hint: '$A_{полез} = mgh = 50 \\cdot 10 \\cdot 1 = 500$ Дж. $A_{полн} = F \\cdot l = 150 \\cdot 4 = 600$ Дж. $\\eta = 500/600 \\cdot 100\\% \\approx 83$ %.',
ans: 83,
tol: 1,
step: '1'
},
{
n: 10,
title: 'Магистр физики 7',
tag: 'синтез курса',
q: 'Тело массой $m = 0{,}5$ кг падает с высоты $h = 10$ м без сопротивления воздуха. Найди скорость $v$ у земли в м/с (округли до десятых). ($g = 9{,}8$ Н/кг)',
hint: 'По закону сохранения: $mgh = mv^2/2 \\Rightarrow v = \\sqrt{2gh} = \\sqrt{2 \\cdot 9{,}8 \\cdot 10} = \\sqrt{196} = 14{,}0$ м/с.',
ans: 14.0,
tol: 0.2,
step: '0.1'
}
];
function loadFinBossState(){
try { return JSON.parse(localStorage.getItem(FIN_BOSS_KEY) || '{}') || {}; }
catch(e) { return {}; }
}
function saveFinBossState(s){
try { localStorage.setItem(FIN_BOSS_KEY, JSON.stringify(s)); } catch(e){}
}
function finRenderKatex(root){
if (typeof window.renderMathInElement !== 'function') return;
try {
window.renderMathInElement(root, {
delimiters: [
{left: '$$', right: '$$', display: true},
{left: '$', right: '$', display: false}
],
throwOnError: false
});
} catch(e){}
}
function updateFinBossBar(state){
var won = 0;
for (var k in state) if (state[k]) won++;
var lab = document.getElementById('fin-boss-lab');
var fill = document.getElementById('fin-boss-fill');
if (lab) lab.textContent = 'Боссов побеждено: ' + won + ' / ' + FIN_BOSSES.length;
if (fill) fill.style.width = Math.round(won * 100 / FIN_BOSSES.length) + '%';
return won;
}
function maybeUnlockMaster(state){
if (localStorage.getItem(FIN_ACH_KEY) === '1') return;
var won = 0;
for (var k in state) if (state[k]) won++;
if (won < FIN_BOSSES.length) return;
localStorage.setItem(FIN_ACH_KEY, '1');
/* +150 XP */
var xp = parseInt(localStorage.getItem('physics7_xp') || '0', 10) || 0;
localStorage.setItem('physics7_xp', String(xp + 150));
try {
if (window.LS && typeof window.LS.addXp === 'function') {
window.LS.addXp(150, 'physics7-master');
} else if (typeof window.addXp === 'function') {
window.addXp(150, 'physics7-master');
}
} catch(e){}
try { if (typeof window.confetti === 'function') window.confetti({particleCount: 220, spread: 110, origin: {y: .6}}); } catch(e){}
var strip = document.getElementById('ach-strip');
var sub = document.getElementById('ach-sub');
if (strip) strip.classList.add('lit');
if (sub) sub.textContent = 'Выполнено! Вы — Магистр физики 7.';
var cta = document.getElementById('final-cta');
if (cta) cta.classList.add('show');
var xpBadge = document.getElementById('hero-xp-badge');
if (xpBadge) {
var newXp = parseInt(localStorage.getItem('physics7_xp') || '0', 10) || 0;
xpBadge.style.display = '';
xpBadge.textContent = newXp + ' XP';
}
}
function buildFinBoss(b, state){
var solvedClass = state[b.n] ? ' solved' : '';
var step = b.step || '1';
var displayAns = (typeof b.ans === 'number' && step !== '1') ? b.ans.toFixed(2) : b.ans;
return '<div class="boss-card' + solvedClass + '" id="fin-boss-' + b.n + '-card">'
+ '<div class="boss-head">'
+ '<span class="boss-tag">' + b.tag + '</span>'
+ '<span class="boss-title">Босс ' + b.n + '. ' + b.title + '</span>'
+ '</div>'
+ '<div class="boss-q" id="fin-boss-' + b.n + '-q">' + b.q + '</div>'
+ '<div class="boss-row">'
+ '<input type="number" step="' + step + '" class="boss-input" id="fin-boss-' + b.n + '-inp" placeholder="число"' + (state[b.n] ? ' value="' + displayAns + '" disabled' : '') + '>'
+ '<button class="boss-btn primary" id="fin-boss-' + b.n + '-go"' + (state[b.n] ? ' disabled' : '') + '>Атаковать</button>'
+ '<button class="boss-btn" id="fin-boss-' + b.n + '-hint">Подсказка</button>'
+ '</div>'
+ '<div class="boss-hint-txt" id="fin-boss-' + b.n + '-hinttxt">' + b.hint + '</div>'
+ '<div class="boss-fb' + (state[b.n] ? ' ok' : '') + '" id="fin-boss-' + b.n + '-fb">' + (state[b.n] ? 'Победа! +15 XP. Босс уже повержен.' : '') + '</div>'
+ '</div>';
}
function bindFinBoss(b){
var state = loadFinBossState();
var goBtn = document.getElementById('fin-boss-' + b.n + '-go');
var hintBtn = document.getElementById('fin-boss-' + b.n + '-hint');
var inp = document.getElementById('fin-boss-' + b.n + '-inp');
var fb = document.getElementById('fin-boss-' + b.n + '-fb');
var hintTx = document.getElementById('fin-boss-' + b.n + '-hinttxt');
var card = document.getElementById('fin-boss-' + b.n + '-card');
if (!goBtn) return;
if (hintBtn) hintBtn.addEventListener('click', function(){
if (hintTx) hintTx.classList.toggle('show');
});
if (state[b.n]) return;
goBtn.addEventListener('click', function(){
var v = parseFloat((inp.value || '').replace(',', '.'));
if (isNaN(v)) {
fb.className = 'boss-fb fail';
fb.textContent = 'Введите число.';
return;
}
var tol = (typeof b.tol === 'number') ? b.tol : 1e-9;
if (Math.abs(v - b.ans) < tol) {
fb.className = 'boss-fb ok';
fb.textContent = 'Победа! +15 XP. Босс повержен.';
card.classList.add('solved');
goBtn.disabled = true;
inp.disabled = true;
var s = loadFinBossState();
if (!s[b.n]) {
s[b.n] = true;
saveFinBossState(s);
var xp = parseInt(localStorage.getItem('physics7_xp') || '0', 10) || 0;
localStorage.setItem('physics7_xp', String(xp + 15));
try {
if (window.LS && typeof window.LS.addXp === 'function') window.LS.addXp(15, 'fin-boss-' + b.n);
else if (typeof window.addXp === 'function') window.addXp(15, 'fin-boss-' + b.n);
} catch(e){}
var xpBadge = document.getElementById('hero-xp-badge');
if (xpBadge) {
var nXp = parseInt(localStorage.getItem('physics7_xp') || '0', 10) || 0;
xpBadge.style.display = '';
xpBadge.textContent = nXp + ' XP';
}
updateFinBossBar(s);
maybeUnlockMaster(s);
}
} else {
fb.className = 'boss-fb fail';
fb.textContent = 'Не то. Перепроверь решение и попробуй снова.';
}
});
inp.addEventListener('keydown', function(e){
if (e.key === 'Enter') { e.preventDefault(); goBtn.click(); }
});
}
var FIN_BOSSES_RENDERED = false;
function renderFinBosses(){
if (FIN_BOSSES_RENDERED) return;
var cont = document.getElementById('fin-bosses-container');
if (!cont) return;
var state = loadFinBossState();
var html = '';
for (var i = 0; i < FIN_BOSSES.length; i++) html += buildFinBoss(FIN_BOSSES[i], state);
cont.innerHTML = html;
for (var j = 0; j < FIN_BOSSES.length; j++) bindFinBoss(FIN_BOSSES[j]);
var wrap = document.getElementById('course-final');
finRenderKatex(wrap);
updateFinBossBar(state);
if (localStorage.getItem(FIN_ACH_KEY) === '1') {
var cta = document.getElementById('final-cta');
if (cta) cta.classList.add('show');
var strip = document.getElementById('ach-strip');
var sub = document.getElementById('ach-sub');
if (strip) strip.classList.add('lit');
if (sub) sub.textContent = 'Выполнено! Вы — Магистр физики 7.';
}
FIN_BOSSES_RENDERED = true;
}
/* FINAL ACCORDION */
(function bindFinalAccordion(){
var head = document.getElementById('final-head');
var wrap = document.getElementById('course-final');
if (!head || !wrap) return;
function toggle(){
var willOpen = !wrap.classList.contains('open');
wrap.classList.toggle('open');
head.setAttribute('aria-expanded', willOpen ? 'true' : 'false');
if (willOpen) {
renderFinBosses();
finRenderKatex(wrap);
}
}
head.addEventListener('click', toggle);
head.addEventListener('keydown', function(e){
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); }
});
})();
(function syncMasterOnLoad(){
if (localStorage.getItem(FIN_ACH_KEY) === '1') {
var strip = document.getElementById('ach-strip');
var sub = document.getElementById('ach-sub');
if (strip) strip.classList.add('lit');
if (sub) sub.textContent = 'Выполнено! Вы — Магистр физики 7.';
}
})();
function loadProgress() {
if (typeof window.LS === 'undefined' || typeof window.LS.api !== 'function') {
renderProgress([]);
return;
}
window.LS.api('/api/textbooks/physics-7/children')
.then(function(data) {
if (data && data.children) renderProgress(data.children);
else renderProgress([]);
})
.catch(function() { renderProgress([]); });
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadProgress);
} else {
loadProgress();
}
window.addEventListener('focus', loadProgress);
</script>
</body>
</html>
+305
View File
@@ -0,0 +1,305 @@
<!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 · Лабораторный практикум</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}],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>
<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; --muted:#475569;
--border:#a5f3fc; --pri:#0891b2; --pri2:#0e7490; --pri-soft:#cffafe;
--acc:#06b6d4; --acc-d:#0e7490; --acc-soft:#cffafe;
--ok:#10b981; --ok-bg:#d1fae5; --fail:#dc2626; --fail-bg:#fee2e2; --warn:#f59e0b; --warn-bg:#fef3c7;
--sh:0 4px 16px rgba(8,145,178,.08); --sh-h:0 12px 36px rgba(8,145,178,.16);
}
html.dark{--bg:#0c2030;--card:#0e2436;--card-soft:#0b1a28;--text:#cffafe;--muted:#67e8f9;--border:#155e75;--pri-soft:rgba(8,145,178,.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,#164e63,#0891b2 60%,#22d3ee);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(190px,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:-8px;font-family:'Unbounded',sans-serif;font-size:5.6rem;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;flex-wrap:wrap}
.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.3rem;font-weight:800;color:var(--text);flex:1;min-width:0}
.sec-tag{font-size:.74rem;font-weight:700;color:var(--pri2);background:var(--pri-soft);padding:3px 9px;border-radius:99px;text-transform:uppercase;letter-spacing:.04em}
.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}}
.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}
</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 &middot; Лабораторный практикум</h1>
<div class="hdr-sub">6 виртуальных лабораторных работ</div>
</div>
<div class="hdr-side">
<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">Лабораторные работы</div>
<div class="psel-grid" id="psel-grid"></div>
</div>
<section id="sec-lr1" class="sec" data-watermark="1">
<div class="sec-header">
<span class="sec-num">ЛР 1</span>
<h2 class="sec-h">Определение цены деления шкалы измерительного прибора</h2>
<span class="sec-tag">§ 7</span>
</div>
<div id="lr1-body"><div class="placeholder">Виртуальная лабораторная работа появится в Phase 7 (после контента глав).</div></div>
</section>
<section id="sec-lr2" class="sec" data-watermark="2">
<div class="sec-header">
<span class="sec-num">ЛР 2</span>
<h2 class="sec-h">Измерение длины</h2>
<span class="sec-tag">§ 4 · § 7</span>
</div>
<div id="lr2-body"><div class="placeholder">Виртуальная лабораторная работа появится в Phase 7 (после контента глав).</div></div>
</section>
<section id="sec-lr3" class="sec" data-watermark="3">
<div class="sec-header">
<span class="sec-num">ЛР 3</span>
<h2 class="sec-h">Измерение объёма</h2>
<span class="sec-tag">§ 4</span>
</div>
<div id="lr3-body"><div class="placeholder">Виртуальная лабораторная работа появится в Phase 7 (после контента глав).</div></div>
</section>
<section id="sec-lr4" class="sec" data-watermark="4">
<div class="sec-header">
<span class="sec-num">ЛР 4</span>
<h2 class="sec-h">Изучение неравномерного движения</h2>
<span class="sec-tag">§ 18</span>
</div>
<div id="lr4-body"><div class="placeholder">Виртуальная лабораторная работа появится в Phase 7 (после контента глав).</div></div>
</section>
<section id="sec-lr5" class="sec" data-watermark="5">
<div class="sec-header">
<span class="sec-num">ЛР 5</span>
<h2 class="sec-h">Измерение плотности вещества</h2>
<span class="sec-tag">§ 20</span>
</div>
<div id="lr5-body"><div class="placeholder">Виртуальная лабораторная работа появится в Phase 7 (после контента глав).</div></div>
</section>
<section id="sec-lr6" class="sec" data-watermark="6">
<div class="sec-header">
<span class="sec-num">ЛР 6</span>
<h2 class="sec-h">Изучение силы трения</h2>
<span class="sec-tag">§ 27</span>
</div>
<div id="lr6-body"><div class="placeholder">Виртуальная лабораторная работа появится в Phase 7 (после контента глав).</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>
<footer class="foot">Интерактивный учебник «Физика 7 класс» &middot; Лабораторный практикум &middot; LearnSpace</footer>
<script>
'use strict';
const LS_PREFIX = 'physics7_lab';
const _TB_SLUG = 'physics-7-lab';
const LABS = [{id:'lr1',num:'ЛР 1',title:"Определение цены деления шкалы измерительного прибора",wm:'1',tag:'§ 7'},{id:'lr2',num:'ЛР 2',title:"Измерение длины",wm:'2',tag:'§ 4 · § 7'},{id:'lr3',num:'ЛР 3',title:"Измерение объёма",wm:'3',tag:'§ 4'},{id:'lr4',num:'ЛР 4',title:"Изучение неравномерного движения",wm:'4',tag:'§ 18'},{id:'lr5',num:'ЛР 5',title:"Измерение плотности вещества",wm:'5',tag:'§ 20'},{id:'lr6',num:'ЛР 6',title:"Изучение силы трения",wm:'6',tag:'§ 27'}];
const TOTAL_LABS = LABS.length;
const SIDEBARS = {};
LABS.forEach(l => { SIDEBARS[l.id] = { title: 'Шпаргалка ' + l.num, rows: [['В разработке','симуляция и таблицы измерений появятся в Phase 7']] }; });
const ACH_LABELS = { start: 'Начало практикума', all_labs: 'Лаборант 7 класса' };
const STATE = { current: null, progress: {}, xp: 0, level: 1, achievements: new Map() };
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();
const done = LABS.every(l => (STATE.progress[l.id]||0) >= 100);
if(done && !STATE.achievements.has('all_labs')) achievement('all_labs');
}
function addXp(n, src){
if(!n) return;
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-lab-' + (src||'misc'));
}
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(id === 'all_labs' ? 80 : 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 buildSelector(){
const grid = document.getElementById('psel-grid');
if(!grid) return;
grid.innerHTML = LABS.map(l =>
'<button class="psel-card" data-id="' + l.id + '" data-prog-card="' + l.id + '">'
+ '<div class="psel-num">' + l.num + ' &middot; ' + l.tag + '</div>'
+ '<div class="psel-title">' + l.title + '</div>'
+ '<div class="psel-prog"><div class="psel-prog-fill" style="width:' + (STATE.progress[l.id]||0) + '%"></div></div>'
+ '</button>'
).join('');
grid.querySelectorAll('.psel-card').forEach(c => c.addEventListener('click', () => goTo(c.dataset.id)));
}
function goTo(id){
STATE.current = 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);
}
function buildSidebar(id){
const box = document.getElementById('sidebar-content');
if(!box) return;
const sb = SIDEBARS[id] || SIDEBARS[LABS[0].id];
const xpForLv = _xpForLevel(STATE.level), xpNext = _xpForLevel(STATE.level+1);
const xpPct = (xpNext - xpForLv) > 0 ? Math.round((STATE.xp - xpForLv) / (xpNext - xpForLv) * 100) : 100;
let html = '';
html += '<div class="sidecard" style="background:linear-gradient(135deg,var(--pri-soft),var(--acc-soft));border-color:var(--acc)"><h4>XP-прогресс <span style="float:right">Ур. ' + STATE.level + '</span></h4><div class="sidecard-row"><div style="height:8px;background:rgba(0,0,0,.07);border-radius:5px;overflow:hidden"><div style="height:100%;background:linear-gradient(90deg,var(--acc),var(--pri));width:' + xpPct + '%"></div></div><div style="display:flex;justify-content:space-between;font-size:.78rem;color:var(--muted);margin-top:5px"><span>' + STATE.xp + ' XP</span><span>' + xpNext + ' XP</span></div></div></div>';
html += '<div class="sidecard"><h4>' + sb.title + '</h4>';
sb.rows.forEach(([k,v]) => { html += '<div class="sidecard-row"><b>' + k + '</b>' + (v ? ' &mdash; ' + v : '') + '</div>'; });
html += '</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)">&#10003; ' + t + '</div>'; });
html += '</div>';
}
box.innerHTML = html;
}
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 init(){
loadProgress(); initTheme(); initSidebarToggle();
buildSelector(); refreshProgressUI(); goTo(LABS[0].id);
setTimeout(() => achievement('start'), 600);
}
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>