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:
@@ -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: '§§1–7',
|
||||
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: '§§8–13',
|
||||
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: '§§14–27',
|
||||
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: '§§28–35',
|
||||
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: '§§36–42',
|
||||
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 · Глава ${C.n}</h1>
|
||||
<div class="hdr-sub">${C.title} · ${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 класс» · Глава ${C.n} · 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 ? ' — ' + v : '') + '</div>'; });
|
||||
html += '</div>';
|
||||
const tip = TIPS.find(t => t.sec === id) || TIPS[0];
|
||||
if(tip){
|
||||
html += '<div class="sidecard" style="background:linear-gradient(135deg,var(--warn-bg),var(--pri-soft));border-color:var(--warn)"><h4 style="color:#92400e">Подсказка</h4><div class="sidecard-row" style="margin-bottom:0;font-size:.84rem">' + tip.html + '</div></div>';
|
||||
}
|
||||
if(STATE.achievements.size > 0){
|
||||
html += '<div class="sidecard"><h4>Достижения <span style="color:var(--warn);float:right">' + STATE.achievements.size + '</span></h4>';
|
||||
[...STATE.achievements.values()].slice(-4).forEach(t => { html += '<div class="sidecard-row" style="font-size:.78rem;color:var(--ok)">✓ ' + t + '</div>'; });
|
||||
html += '</div>';
|
||||
}
|
||||
box.innerHTML = html;
|
||||
if(window.renderMathInElement){
|
||||
try{ renderMathInElement(box, { delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false}], throwOnError:false }); }catch(e){}
|
||||
}
|
||||
}
|
||||
|
||||
function initTheme(){
|
||||
const t = localStorage.getItem(LS_PREFIX + '_theme') || localStorage.getItem('physics7_theme') || 'light';
|
||||
if(t === 'dark') document.documentElement.classList.add('dark');
|
||||
document.getElementById('theme-lab').textContent = t === 'dark' ? 'Светлая' : 'Тёмная';
|
||||
document.getElementById('theme-btn').addEventListener('click', () => {
|
||||
document.documentElement.classList.toggle('dark');
|
||||
const dark = document.documentElement.classList.contains('dark');
|
||||
localStorage.setItem(LS_PREFIX + '_theme', dark ? 'dark' : 'light');
|
||||
localStorage.setItem('physics7_theme', dark ? 'dark' : 'light');
|
||||
document.getElementById('theme-lab').textContent = dark ? 'Светлая' : 'Тёмная';
|
||||
});
|
||||
}
|
||||
|
||||
function initSidebarToggle(){
|
||||
const side = document.getElementById('col-side'), back = document.getElementById('col-side-backdrop'), btn = document.getElementById('sidebar-btn');
|
||||
if(!side || !btn) return;
|
||||
function open(){ side.classList.add('open'); back.classList.add('show'); }
|
||||
function close(){ side.classList.remove('open'); back.classList.remove('show'); }
|
||||
btn.addEventListener('click', () => { if(side.classList.contains('open')) close(); else open(); });
|
||||
back.addEventListener('click', close);
|
||||
document.addEventListener('keydown', e => { if(e.key === 'Escape') close(); });
|
||||
}
|
||||
|
||||
function initSearch(){
|
||||
const btn = document.getElementById('search-btn'), modal = document.getElementById('search-modal'), inp = document.getElementById('search-inp'), list = document.getElementById('search-list');
|
||||
if(!btn || !modal) return;
|
||||
let cur = 0;
|
||||
function render(q){
|
||||
const ql = (q||'').toLowerCase().trim();
|
||||
const items = PARAS.filter(p => !ql || p.title.toLowerCase().includes(ql) || p.num.toLowerCase().includes(ql));
|
||||
list.innerHTML = items.map((p,i) => '<div class="search-item' + (i === cur ? ' cur' : '') + '" data-id="' + p.id + '"><span class="num">' + p.num + '</span>' + p.title + '</div>').join('');
|
||||
list.querySelectorAll('.search-item').forEach(el => el.addEventListener('click', () => { goTo(el.dataset.id); close(); }));
|
||||
}
|
||||
function open(){ modal.classList.add('show'); inp.value = ''; cur = 0; render(''); setTimeout(() => inp.focus(), 50); }
|
||||
function close(){ modal.classList.remove('show'); }
|
||||
btn.addEventListener('click', open);
|
||||
modal.addEventListener('click', e => { if(e.target === modal) close(); });
|
||||
inp.addEventListener('input', () => { cur = 0; render(inp.value); });
|
||||
inp.addEventListener('keydown', e => {
|
||||
const items = list.querySelectorAll('.search-item');
|
||||
if(e.key === 'ArrowDown'){ e.preventDefault(); cur = Math.min(items.length-1, cur+1); render(inp.value); }
|
||||
else if(e.key === 'ArrowUp'){ e.preventDefault(); cur = Math.max(0, cur-1); render(inp.value); }
|
||||
else if(e.key === 'Enter'){ e.preventDefault(); const sel = items[cur]; if(sel){ goTo(sel.dataset.id); close(); } }
|
||||
else if(e.key === 'Escape'){ e.preventDefault(); close(); }
|
||||
});
|
||||
document.addEventListener('keydown', e => { if((e.ctrlKey || e.metaKey) && (e.key === 'k' || e.key === 'K')){ e.preventDefault(); if(modal.classList.contains('show')) close(); else open(); } });
|
||||
}
|
||||
|
||||
function init(){
|
||||
loadProgress(); initTheme(); initSidebarToggle(); initSearch();
|
||||
buildParaSelector(); refreshProgressUI(); goTo(PARAS[0].id);
|
||||
setTimeout(() => achievement('start'), 600);
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
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.');
|
||||
@@ -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 · Лабораторный практикум</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 класс» · Лабораторный практикум · 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 + ' · ' + 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 ? ' — ' + 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)">✓ ' + 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
@@ -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
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -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 · Глава 1</h1>
|
||||
<div class="hdr-sub">Физические методы познания природы · §§1–7</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 класс» · Глава 1 · 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 ? ' — ' + v : '') + '</div>'; });
|
||||
html += '</div>';
|
||||
const tip = TIPS.find(t => t.sec === id) || TIPS[0];
|
||||
if(tip){
|
||||
html += '<div class="sidecard" style="background:linear-gradient(135deg,var(--warn-bg),var(--pri-soft));border-color:var(--warn)"><h4 style="color:#92400e">Подсказка</h4><div class="sidecard-row" style="margin-bottom:0;font-size:.84rem">' + tip.html + '</div></div>';
|
||||
}
|
||||
if(STATE.achievements.size > 0){
|
||||
html += '<div class="sidecard"><h4>Достижения <span style="color:var(--warn);float:right">' + STATE.achievements.size + '</span></h4>';
|
||||
[...STATE.achievements.values()].slice(-4).forEach(t => { html += '<div class="sidecard-row" style="font-size:.78rem;color:var(--ok)">✓ ' + t + '</div>'; });
|
||||
html += '</div>';
|
||||
}
|
||||
box.innerHTML = html;
|
||||
if(window.renderMathInElement){
|
||||
try{ renderMathInElement(box, { delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false}], throwOnError:false }); }catch(e){}
|
||||
}
|
||||
}
|
||||
|
||||
function initTheme(){
|
||||
const t = localStorage.getItem(LS_PREFIX + '_theme') || localStorage.getItem('physics7_theme') || 'light';
|
||||
if(t === 'dark') document.documentElement.classList.add('dark');
|
||||
document.getElementById('theme-lab').textContent = t === 'dark' ? 'Светлая' : 'Тёмная';
|
||||
document.getElementById('theme-btn').addEventListener('click', () => {
|
||||
document.documentElement.classList.toggle('dark');
|
||||
const dark = document.documentElement.classList.contains('dark');
|
||||
localStorage.setItem(LS_PREFIX + '_theme', dark ? 'dark' : 'light');
|
||||
localStorage.setItem('physics7_theme', dark ? 'dark' : 'light');
|
||||
document.getElementById('theme-lab').textContent = dark ? 'Светлая' : 'Тёмная';
|
||||
});
|
||||
}
|
||||
|
||||
function initSidebarToggle(){
|
||||
const side = document.getElementById('col-side'), back = document.getElementById('col-side-backdrop'), btn = document.getElementById('sidebar-btn');
|
||||
if(!side || !btn) return;
|
||||
function open(){ side.classList.add('open'); back.classList.add('show'); }
|
||||
function close(){ side.classList.remove('open'); back.classList.remove('show'); }
|
||||
btn.addEventListener('click', () => { if(side.classList.contains('open')) close(); else open(); });
|
||||
back.addEventListener('click', close);
|
||||
document.addEventListener('keydown', e => { if(e.key === 'Escape') close(); });
|
||||
}
|
||||
|
||||
function initSearch(){
|
||||
const btn = document.getElementById('search-btn'), modal = document.getElementById('search-modal'), inp = document.getElementById('search-inp'), list = document.getElementById('search-list');
|
||||
if(!btn || !modal) return;
|
||||
let cur = 0;
|
||||
function render(q){
|
||||
const ql = (q||'').toLowerCase().trim();
|
||||
const items = PARAS.filter(p => !ql || p.title.toLowerCase().includes(ql) || p.num.toLowerCase().includes(ql));
|
||||
list.innerHTML = items.map((p,i) => '<div class="search-item' + (i === cur ? ' cur' : '') + '" data-id="' + p.id + '"><span class="num">' + p.num + '</span>' + p.title + '</div>').join('');
|
||||
list.querySelectorAll('.search-item').forEach(el => el.addEventListener('click', () => { goTo(el.dataset.id); close(); }));
|
||||
}
|
||||
function open(){ modal.classList.add('show'); inp.value = ''; cur = 0; render(''); setTimeout(() => inp.focus(), 50); }
|
||||
function close(){ modal.classList.remove('show'); }
|
||||
btn.addEventListener('click', open);
|
||||
modal.addEventListener('click', e => { if(e.target === modal) close(); });
|
||||
inp.addEventListener('input', () => { cur = 0; render(inp.value); });
|
||||
inp.addEventListener('keydown', e => {
|
||||
const items = list.querySelectorAll('.search-item');
|
||||
if(e.key === 'ArrowDown'){ e.preventDefault(); cur = Math.min(items.length-1, cur+1); render(inp.value); }
|
||||
else if(e.key === 'ArrowUp'){ e.preventDefault(); cur = Math.max(0, cur-1); render(inp.value); }
|
||||
else if(e.key === 'Enter'){ e.preventDefault(); const sel = items[cur]; if(sel){ goTo(sel.dataset.id); close(); } }
|
||||
else if(e.key === 'Escape'){ e.preventDefault(); close(); }
|
||||
});
|
||||
document.addEventListener('keydown', e => { if((e.ctrlKey || e.metaKey) && (e.key === 'k' || e.key === 'K')){ e.preventDefault(); if(modal.classList.contains('show')) close(); else open(); } });
|
||||
}
|
||||
|
||||
function init(){
|
||||
loadProgress(); initTheme(); initSidebarToggle(); initSearch();
|
||||
buildParaSelector(); refreshProgressUI(); goTo(PARAS[0].id);
|
||||
setTimeout(() => achievement('start'), 600);
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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 · Глава 2</h1>
|
||||
<div class="hdr-sub">Строение вещества · §§8–13</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 класс» · Глава 2 · 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 ? ' — ' + v : '') + '</div>'; });
|
||||
html += '</div>';
|
||||
const tip = TIPS.find(t => t.sec === id) || TIPS[0];
|
||||
if(tip){
|
||||
html += '<div class="sidecard" style="background:linear-gradient(135deg,var(--warn-bg),var(--pri-soft));border-color:var(--warn)"><h4 style="color:#92400e">Подсказка</h4><div class="sidecard-row" style="margin-bottom:0;font-size:.84rem">' + tip.html + '</div></div>';
|
||||
}
|
||||
if(STATE.achievements.size > 0){
|
||||
html += '<div class="sidecard"><h4>Достижения <span style="color:var(--warn);float:right">' + STATE.achievements.size + '</span></h4>';
|
||||
[...STATE.achievements.values()].slice(-4).forEach(t => { html += '<div class="sidecard-row" style="font-size:.78rem;color:var(--ok)">✓ ' + t + '</div>'; });
|
||||
html += '</div>';
|
||||
}
|
||||
box.innerHTML = html;
|
||||
if(window.renderMathInElement){
|
||||
try{ renderMathInElement(box, { delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false}], throwOnError:false }); }catch(e){}
|
||||
}
|
||||
}
|
||||
|
||||
function initTheme(){
|
||||
const t = localStorage.getItem(LS_PREFIX + '_theme') || localStorage.getItem('physics7_theme') || 'light';
|
||||
if(t === 'dark') document.documentElement.classList.add('dark');
|
||||
document.getElementById('theme-lab').textContent = t === 'dark' ? 'Светлая' : 'Тёмная';
|
||||
document.getElementById('theme-btn').addEventListener('click', () => {
|
||||
document.documentElement.classList.toggle('dark');
|
||||
const dark = document.documentElement.classList.contains('dark');
|
||||
localStorage.setItem(LS_PREFIX + '_theme', dark ? 'dark' : 'light');
|
||||
localStorage.setItem('physics7_theme', dark ? 'dark' : 'light');
|
||||
document.getElementById('theme-lab').textContent = dark ? 'Светлая' : 'Тёмная';
|
||||
});
|
||||
}
|
||||
|
||||
function initSidebarToggle(){
|
||||
const side = document.getElementById('col-side'), back = document.getElementById('col-side-backdrop'), btn = document.getElementById('sidebar-btn');
|
||||
if(!side || !btn) return;
|
||||
function open(){ side.classList.add('open'); back.classList.add('show'); }
|
||||
function close(){ side.classList.remove('open'); back.classList.remove('show'); }
|
||||
btn.addEventListener('click', () => { if(side.classList.contains('open')) close(); else open(); });
|
||||
back.addEventListener('click', close);
|
||||
document.addEventListener('keydown', e => { if(e.key === 'Escape') close(); });
|
||||
}
|
||||
|
||||
function initSearch(){
|
||||
const btn = document.getElementById('search-btn'), modal = document.getElementById('search-modal'), inp = document.getElementById('search-inp'), list = document.getElementById('search-list');
|
||||
if(!btn || !modal) return;
|
||||
let cur = 0;
|
||||
function render(q){
|
||||
const ql = (q||'').toLowerCase().trim();
|
||||
const items = PARAS.filter(p => !ql || p.title.toLowerCase().includes(ql) || p.num.toLowerCase().includes(ql));
|
||||
list.innerHTML = items.map((p,i) => '<div class="search-item' + (i === cur ? ' cur' : '') + '" data-id="' + p.id + '"><span class="num">' + p.num + '</span>' + p.title + '</div>').join('');
|
||||
list.querySelectorAll('.search-item').forEach(el => el.addEventListener('click', () => { goTo(el.dataset.id); close(); }));
|
||||
}
|
||||
function open(){ modal.classList.add('show'); inp.value = ''; cur = 0; render(''); setTimeout(() => inp.focus(), 50); }
|
||||
function close(){ modal.classList.remove('show'); }
|
||||
btn.addEventListener('click', open);
|
||||
modal.addEventListener('click', e => { if(e.target === modal) close(); });
|
||||
inp.addEventListener('input', () => { cur = 0; render(inp.value); });
|
||||
inp.addEventListener('keydown', e => {
|
||||
const items = list.querySelectorAll('.search-item');
|
||||
if(e.key === 'ArrowDown'){ e.preventDefault(); cur = Math.min(items.length-1, cur+1); render(inp.value); }
|
||||
else if(e.key === 'ArrowUp'){ e.preventDefault(); cur = Math.max(0, cur-1); render(inp.value); }
|
||||
else if(e.key === 'Enter'){ e.preventDefault(); const sel = items[cur]; if(sel){ goTo(sel.dataset.id); close(); } }
|
||||
else if(e.key === 'Escape'){ e.preventDefault(); close(); }
|
||||
});
|
||||
document.addEventListener('keydown', e => { if((e.ctrlKey || e.metaKey) && (e.key === 'k' || e.key === 'K')){ e.preventDefault(); if(modal.classList.contains('show')) close(); else open(); } });
|
||||
}
|
||||
|
||||
function init(){
|
||||
loadProgress(); initTheme(); initSidebarToggle(); initSearch();
|
||||
buildParaSelector(); refreshProgressUI(); goTo(PARAS[0].id);
|
||||
setTimeout(() => achievement('start'), 600);
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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 · Глава 3</h1>
|
||||
<div class="hdr-sub">Движение и силы · §§14–27</div>
|
||||
</div>
|
||||
<div class="hdr-side">
|
||||
<button id="search-btn" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m21 21-4-4"/></svg> Поиск</button>
|
||||
<button id="sidebar-btn" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="18" x2="14" y2="18"/></svg> Шпаргалка</button>
|
||||
<button id="theme-btn" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg><span id="theme-lab">Тёмная</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="main">
|
||||
<div class="col-main">
|
||||
<div class="psel">
|
||||
<div class="psel-head">Параграфы главы 3</div>
|
||||
<div class="psel-grid" id="psel-grid"></div>
|
||||
</div>
|
||||
|
||||
<section id="sec-p14" class="sec" data-watermark="→">
|
||||
<div class="sec-header"><span class="sec-num">§ 14</span><h2 class="sec-h">Механическое движение. Относительность</h2></div>
|
||||
<div id="p14-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
|
||||
</section>
|
||||
<section id="sec-p15" class="sec" data-watermark="s">
|
||||
<div class="sec-header"><span class="sec-num">§ 15</span><h2 class="sec-h">Траектория, путь, время</h2></div>
|
||||
<div id="p15-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
|
||||
</section>
|
||||
<section id="sec-p16" class="sec" data-watermark="v">
|
||||
<div class="sec-header"><span class="sec-num">§ 16</span><h2 class="sec-h">Равномерное движение. Скорость</h2></div>
|
||||
<div id="p16-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
|
||||
</section>
|
||||
<section id="sec-p17" class="sec" data-watermark="∠">
|
||||
<div class="sec-header"><span class="sec-num">§ 17</span><h2 class="sec-h">Графики s(t) и v(t)</h2></div>
|
||||
<div id="p17-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
|
||||
</section>
|
||||
<section id="sec-p18" class="sec" data-watermark="⟨⟩">
|
||||
<div class="sec-header"><span class="sec-num">§ 18</span><h2 class="sec-h">Средняя скорость</h2></div>
|
||||
<div id="p18-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
|
||||
</section>
|
||||
<section id="sec-p19" class="sec" data-watermark="∞">
|
||||
<div class="sec-header"><span class="sec-num">§ 19</span><h2 class="sec-h">Инерция</h2></div>
|
||||
<div id="p19-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
|
||||
</section>
|
||||
<section id="sec-p20" class="sec" data-watermark="ρ">
|
||||
<div class="sec-header"><span class="sec-num">§ 20</span><h2 class="sec-h">Масса. Плотность</h2></div>
|
||||
<div id="p20-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
|
||||
</section>
|
||||
<section id="sec-p21" class="sec" data-watermark="F">
|
||||
<div class="sec-header"><span class="sec-num">§ 21</span><h2 class="sec-h">Сила</h2></div>
|
||||
<div id="p21-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
|
||||
</section>
|
||||
<section id="sec-p22" class="sec" data-watermark="↓">
|
||||
<div class="sec-header"><span class="sec-num">§ 22</span><h2 class="sec-h">Сила тяжести</h2></div>
|
||||
<div id="p22-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
|
||||
</section>
|
||||
<section id="sec-p23" class="sec" data-watermark="≈">
|
||||
<div class="sec-header"><span class="sec-num">§ 23</span><h2 class="sec-h">Сила упругости</h2></div>
|
||||
<div id="p23-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
|
||||
</section>
|
||||
<section id="sec-p24" class="sec" data-watermark="P">
|
||||
<div class="sec-header"><span class="sec-num">§ 24</span><h2 class="sec-h">Вес тела</h2></div>
|
||||
<div id="p24-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
|
||||
</section>
|
||||
<section id="sec-p25" class="sec" data-watermark="⊥">
|
||||
<div class="sec-header"><span class="sec-num">§ 25</span><h2 class="sec-h">Динамометр</h2></div>
|
||||
<div id="p25-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
|
||||
</section>
|
||||
<section id="sec-p26" class="sec" data-watermark="+">
|
||||
<div class="sec-header"><span class="sec-num">§ 26</span><h2 class="sec-h">Сложение сил</h2></div>
|
||||
<div id="p26-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
|
||||
</section>
|
||||
<section id="sec-p27" class="sec" data-watermark="~">
|
||||
<div class="sec-header"><span class="sec-num">§ 27</span><h2 class="sec-h">Сила трения</h2></div>
|
||||
<div id="p27-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
|
||||
</section>
|
||||
<section id="sec-final3" class="sec" data-watermark="★">
|
||||
<div class="sec-header"><span class="sec-num">Финал</span><h2 class="sec-h">Итоги главы 3</h2></div>
|
||||
<div id="final3-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<aside class="col-side" id="col-side"><div id="sidebar-content"></div></aside>
|
||||
<div class="col-side-backdrop" id="col-side-backdrop"></div>
|
||||
</main>
|
||||
|
||||
<div class="ach-popup" id="ach-popup"><svg class="ic" viewBox="0 0 24 24"><polygon points="12,2 15,9 22,9.3 17,14 18.5,21 12,17 5.5,21 7,14 2,9.3 9,9"/></svg><span id="ach-text"></span></div>
|
||||
|
||||
<div class="search-modal" id="search-modal"><div class="search-box">
|
||||
<input type="text" class="search-inp" id="search-inp" placeholder="Поиск по параграфам... (Esc — закрыть, Ctrl+K)">
|
||||
<div class="search-list" id="search-list"></div>
|
||||
</div></div>
|
||||
|
||||
<footer class="foot">Интерактивный учебник «Физика 7 класс» · Глава 3 · LearnSpace</footer>
|
||||
|
||||
<script>
|
||||
'use strict';
|
||||
|
||||
const LS_PREFIX = 'physics7_ch3';
|
||||
const _TB_SLUG = 'physics-7-ch3';
|
||||
|
||||
const PARAS = [{id:'p14',num:'§ 14',title:"Механическое движение. Относительность",wm:'→'},{id:'p15',num:'§ 15',title:"Траектория, путь, время",wm:'s'},{id:'p16',num:'§ 16',title:"Равномерное движение. Скорость",wm:'v'},{id:'p17',num:'§ 17',title:"Графики s(t) и v(t)",wm:'∠'},{id:'p18',num:'§ 18',title:"Средняя скорость",wm:'⟨⟩'},{id:'p19',num:'§ 19',title:"Инерция",wm:'∞'},{id:'p20',num:'§ 20',title:"Масса. Плотность",wm:'ρ'},{id:'p21',num:'§ 21',title:"Сила",wm:'F'},{id:'p22',num:'§ 22',title:"Сила тяжести",wm:'↓'},{id:'p23',num:'§ 23',title:"Сила упругости",wm:'≈'},{id:'p24',num:'§ 24',title:"Вес тела",wm:'P'},{id:'p25',num:'§ 25',title:"Динамометр",wm:'⊥'},{id:'p26',num:'§ 26',title:"Сложение сил",wm:'+'},{id:'p27',num:'§ 27',title:"Сила трения",wm:'~'},{id:'final3',num:'Финал',title:"Итоги главы 3",wm:'★'}];
|
||||
const TOTAL_PARAS = PARAS.length;
|
||||
|
||||
const SIDEBARS = {};
|
||||
PARAS.forEach(p => { SIDEBARS[p.id] = { title: 'Шпаргалка ' + p.num, rows: [['В разработке','контент появится с волной соответствующего §']] }; });
|
||||
const TIPS = [{ sec: PARAS[0].id, html: 'Скелет главы готов. Контент параграфов выйдет в одной из ближайших фаз.' }];
|
||||
const ACH_LABELS = { start: 'Начало главы 3', ch_done: 'Мастер движения' };
|
||||
|
||||
const STATE = { current: null, progress: {}, xp: 0, level: 1, achievements: new Map(), _built: new Set() };
|
||||
|
||||
function _xpForLevel(lv){ return Math.round(100 * Math.pow(lv-1, 1.6)); }
|
||||
function calcLevel(xp){ let lv = 1; while(_xpForLevel(lv+1) <= xp) lv++; return lv; }
|
||||
|
||||
function loadProgress(){
|
||||
try{
|
||||
const s = localStorage.getItem(LS_PREFIX + '_progress'); if(s) Object.assign(STATE.progress, JSON.parse(s));
|
||||
const a = localStorage.getItem(LS_PREFIX + '_achievements');
|
||||
if(a){ const p = JSON.parse(a); if(p && typeof p === 'object'){ for(const [id,t] of Object.entries(p)) STATE.achievements.set(id, (t && t !== id) ? t : (ACH_LABELS[id] || id)); } }
|
||||
STATE.xp = +(localStorage.getItem('physics7_xp') || 0); STATE.level = calcLevel(STATE.xp);
|
||||
}catch(e){}
|
||||
}
|
||||
function saveProgress(){
|
||||
try{
|
||||
localStorage.setItem(LS_PREFIX + '_progress', JSON.stringify(STATE.progress));
|
||||
localStorage.setItem(LS_PREFIX + '_achievements', JSON.stringify(Object.fromEntries(STATE.achievements)));
|
||||
localStorage.setItem('physics7_xp', String(STATE.xp));
|
||||
}catch(e){}
|
||||
}
|
||||
function bumpProgress(key, delta){
|
||||
STATE.progress[key] = Math.max(0, Math.min(100, (STATE.progress[key]||0) + delta));
|
||||
saveProgress(); refreshProgressUI();
|
||||
if(STATE.progress[key] >= 100 && key === PARAS[PARAS.length-1].id) achievement('ch_done', 'Мастер движения');
|
||||
}
|
||||
function addXp(n, src){
|
||||
if(!n) return;
|
||||
const prev = STATE.level; STATE.xp = Math.max(0, (STATE.xp||0) + n); STATE.level = calcLevel(STATE.xp);
|
||||
saveProgress(); refreshProgressUI();
|
||||
if(window.LS && window.LS.xp) window.LS.xp.add(n, 'physics7-ch3-' + (src||'misc'));
|
||||
if(STATE.level > prev){
|
||||
const pop = document.getElementById('ach-popup');
|
||||
if(pop){ document.getElementById('ach-text').textContent = 'Уровень ' + STATE.level + '!'; pop.classList.add('show'); setTimeout(()=>pop.classList.remove('show'), 2600); }
|
||||
}
|
||||
}
|
||||
function achievement(id, label){
|
||||
if(STATE.achievements.has(id)) return;
|
||||
STATE.achievements.set(id, label || ACH_LABELS[id] || id);
|
||||
saveProgress();
|
||||
const pop = document.getElementById('ach-popup');
|
||||
if(pop){ document.getElementById('ach-text').textContent = 'Ачивка: ' + (label || ACH_LABELS[id] || id); pop.classList.add('show'); setTimeout(()=>pop.classList.remove('show'), 3000); }
|
||||
addXp(20, 'ach-' + id);
|
||||
}
|
||||
|
||||
function refreshProgressUI(){
|
||||
document.querySelectorAll('[data-prog-card]').forEach(el => {
|
||||
const k = el.dataset.progCard;
|
||||
const fl = el.querySelector('.psel-prog-fill');
|
||||
if(fl) fl.style.width = (STATE.progress[k]||0) + '%';
|
||||
});
|
||||
if(STATE.current && document.getElementById('sidebar-content')){ try{ buildSidebar(STATE.current); }catch(e){} }
|
||||
}
|
||||
|
||||
function buildParaSelector(){
|
||||
const grid = document.getElementById('psel-grid');
|
||||
if(!grid) return;
|
||||
grid.innerHTML = PARAS.map(p =>
|
||||
'<button class="psel-card" data-id="' + p.id + '" data-prog-card="' + p.id + '">'
|
||||
+ '<div class="psel-num">' + p.num + '</div>'
|
||||
+ '<div class="psel-title">' + p.title + '</div>'
|
||||
+ '<div class="psel-prog"><div class="psel-prog-fill" style="width:' + (STATE.progress[p.id]||0) + '%"></div></div>'
|
||||
+ '</button>'
|
||||
).join('');
|
||||
grid.querySelectorAll('.psel-card').forEach(c => c.addEventListener('click', () => goTo(c.dataset.id)));
|
||||
}
|
||||
|
||||
function ensureBuilt(id){
|
||||
if(STATE._built.has(id)) return;
|
||||
STATE._built.add(id);
|
||||
}
|
||||
|
||||
function goTo(id){
|
||||
STATE.current = id; ensureBuilt(id);
|
||||
document.querySelectorAll('.sec').forEach(s => s.classList.remove('active'));
|
||||
const el = document.getElementById('sec-' + id); if(el) el.classList.add('active');
|
||||
document.querySelectorAll('.psel-card').forEach(c => c.classList.toggle('active', c.dataset.id === id));
|
||||
buildSidebar(id);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
if((STATE.progress[id]||0) < 10) bumpProgress(id, 10);
|
||||
if(window.renderMathInElement && el){
|
||||
setTimeout(() => {
|
||||
try{ renderMathInElement(el, { delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false}], throwOnError:false }); }catch(e){}
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
function buildSidebar(id){
|
||||
const box = document.getElementById('sidebar-content');
|
||||
if(!box) return;
|
||||
const sb = SIDEBARS[id] || SIDEBARS[PARAS[0].id];
|
||||
const xpForLv = _xpForLevel(STATE.level), xpNext = _xpForLevel(STATE.level+1);
|
||||
const xpInLv = STATE.xp - xpForLv, xpRange = xpNext - xpForLv;
|
||||
const xpPct = xpRange > 0 ? Math.round(xpInLv / xpRange * 100) : 100;
|
||||
let html = '';
|
||||
html += '<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><span class="xp-level">Ур. ' + STATE.level + '</span></div><div class="xp-bar"><div class="xp-fill" style="width:' + xpPct + '%"></div></div><div class="xp-nums"><span>' + STATE.xp + ' XP</span><span>' + xpNext + ' XP</span></div></div>';
|
||||
html += '<div class="sidecard"><h4>' + sb.title + '</h4>';
|
||||
sb.rows.forEach(([k,v]) => { html += '<div class="sidecard-row"><b>' + k + '</b>' + (v ? ' — ' + v : '') + '</div>'; });
|
||||
html += '</div>';
|
||||
const tip = TIPS.find(t => t.sec === id) || TIPS[0];
|
||||
if(tip){
|
||||
html += '<div class="sidecard" style="background:linear-gradient(135deg,var(--warn-bg),var(--pri-soft));border-color:var(--warn)"><h4 style="color:#92400e">Подсказка</h4><div class="sidecard-row" style="margin-bottom:0;font-size:.84rem">' + tip.html + '</div></div>';
|
||||
}
|
||||
if(STATE.achievements.size > 0){
|
||||
html += '<div class="sidecard"><h4>Достижения <span style="color:var(--warn);float:right">' + STATE.achievements.size + '</span></h4>';
|
||||
[...STATE.achievements.values()].slice(-4).forEach(t => { html += '<div class="sidecard-row" style="font-size:.78rem;color:var(--ok)">✓ ' + t + '</div>'; });
|
||||
html += '</div>';
|
||||
}
|
||||
box.innerHTML = html;
|
||||
if(window.renderMathInElement){
|
||||
try{ renderMathInElement(box, { delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false}], throwOnError:false }); }catch(e){}
|
||||
}
|
||||
}
|
||||
|
||||
function initTheme(){
|
||||
const t = localStorage.getItem(LS_PREFIX + '_theme') || localStorage.getItem('physics7_theme') || 'light';
|
||||
if(t === 'dark') document.documentElement.classList.add('dark');
|
||||
document.getElementById('theme-lab').textContent = t === 'dark' ? 'Светлая' : 'Тёмная';
|
||||
document.getElementById('theme-btn').addEventListener('click', () => {
|
||||
document.documentElement.classList.toggle('dark');
|
||||
const dark = document.documentElement.classList.contains('dark');
|
||||
localStorage.setItem(LS_PREFIX + '_theme', dark ? 'dark' : 'light');
|
||||
localStorage.setItem('physics7_theme', dark ? 'dark' : 'light');
|
||||
document.getElementById('theme-lab').textContent = dark ? 'Светлая' : 'Тёмная';
|
||||
});
|
||||
}
|
||||
|
||||
function initSidebarToggle(){
|
||||
const side = document.getElementById('col-side'), back = document.getElementById('col-side-backdrop'), btn = document.getElementById('sidebar-btn');
|
||||
if(!side || !btn) return;
|
||||
function open(){ side.classList.add('open'); back.classList.add('show'); }
|
||||
function close(){ side.classList.remove('open'); back.classList.remove('show'); }
|
||||
btn.addEventListener('click', () => { if(side.classList.contains('open')) close(); else open(); });
|
||||
back.addEventListener('click', close);
|
||||
document.addEventListener('keydown', e => { if(e.key === 'Escape') close(); });
|
||||
}
|
||||
|
||||
function initSearch(){
|
||||
const btn = document.getElementById('search-btn'), modal = document.getElementById('search-modal'), inp = document.getElementById('search-inp'), list = document.getElementById('search-list');
|
||||
if(!btn || !modal) return;
|
||||
let cur = 0;
|
||||
function render(q){
|
||||
const ql = (q||'').toLowerCase().trim();
|
||||
const items = PARAS.filter(p => !ql || p.title.toLowerCase().includes(ql) || p.num.toLowerCase().includes(ql));
|
||||
list.innerHTML = items.map((p,i) => '<div class="search-item' + (i === cur ? ' cur' : '') + '" data-id="' + p.id + '"><span class="num">' + p.num + '</span>' + p.title + '</div>').join('');
|
||||
list.querySelectorAll('.search-item').forEach(el => el.addEventListener('click', () => { goTo(el.dataset.id); close(); }));
|
||||
}
|
||||
function open(){ modal.classList.add('show'); inp.value = ''; cur = 0; render(''); setTimeout(() => inp.focus(), 50); }
|
||||
function close(){ modal.classList.remove('show'); }
|
||||
btn.addEventListener('click', open);
|
||||
modal.addEventListener('click', e => { if(e.target === modal) close(); });
|
||||
inp.addEventListener('input', () => { cur = 0; render(inp.value); });
|
||||
inp.addEventListener('keydown', e => {
|
||||
const items = list.querySelectorAll('.search-item');
|
||||
if(e.key === 'ArrowDown'){ e.preventDefault(); cur = Math.min(items.length-1, cur+1); render(inp.value); }
|
||||
else if(e.key === 'ArrowUp'){ e.preventDefault(); cur = Math.max(0, cur-1); render(inp.value); }
|
||||
else if(e.key === 'Enter'){ e.preventDefault(); const sel = items[cur]; if(sel){ goTo(sel.dataset.id); close(); } }
|
||||
else if(e.key === 'Escape'){ e.preventDefault(); close(); }
|
||||
});
|
||||
document.addEventListener('keydown', e => { if((e.ctrlKey || e.metaKey) && (e.key === 'k' || e.key === 'K')){ e.preventDefault(); if(modal.classList.contains('show')) close(); else open(); } });
|
||||
}
|
||||
|
||||
function init(){
|
||||
loadProgress(); initTheme(); initSidebarToggle(); initSearch();
|
||||
buildParaSelector(); refreshProgressUI(); goTo(PARAS[0].id);
|
||||
setTimeout(() => achievement('start'), 600);
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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 · Глава 4</h1>
|
||||
<div class="hdr-sub">Давление · §§28–35</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 класс» · Глава 4 · 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 ? ' — ' + v : '') + '</div>'; });
|
||||
html += '</div>';
|
||||
const tip = TIPS.find(t => t.sec === id) || TIPS[0];
|
||||
if(tip){
|
||||
html += '<div class="sidecard" style="background:linear-gradient(135deg,var(--warn-bg),var(--pri-soft));border-color:var(--warn)"><h4 style="color:#92400e">Подсказка</h4><div class="sidecard-row" style="margin-bottom:0;font-size:.84rem">' + tip.html + '</div></div>';
|
||||
}
|
||||
if(STATE.achievements.size > 0){
|
||||
html += '<div class="sidecard"><h4>Достижения <span style="color:var(--warn);float:right">' + STATE.achievements.size + '</span></h4>';
|
||||
[...STATE.achievements.values()].slice(-4).forEach(t => { html += '<div class="sidecard-row" style="font-size:.78rem;color:var(--ok)">✓ ' + t + '</div>'; });
|
||||
html += '</div>';
|
||||
}
|
||||
box.innerHTML = html;
|
||||
if(window.renderMathInElement){
|
||||
try{ renderMathInElement(box, { delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false}], throwOnError:false }); }catch(e){}
|
||||
}
|
||||
}
|
||||
|
||||
function initTheme(){
|
||||
const t = localStorage.getItem(LS_PREFIX + '_theme') || localStorage.getItem('physics7_theme') || 'light';
|
||||
if(t === 'dark') document.documentElement.classList.add('dark');
|
||||
document.getElementById('theme-lab').textContent = t === 'dark' ? 'Светлая' : 'Тёмная';
|
||||
document.getElementById('theme-btn').addEventListener('click', () => {
|
||||
document.documentElement.classList.toggle('dark');
|
||||
const dark = document.documentElement.classList.contains('dark');
|
||||
localStorage.setItem(LS_PREFIX + '_theme', dark ? 'dark' : 'light');
|
||||
localStorage.setItem('physics7_theme', dark ? 'dark' : 'light');
|
||||
document.getElementById('theme-lab').textContent = dark ? 'Светлая' : 'Тёмная';
|
||||
});
|
||||
}
|
||||
|
||||
function initSidebarToggle(){
|
||||
const side = document.getElementById('col-side'), back = document.getElementById('col-side-backdrop'), btn = document.getElementById('sidebar-btn');
|
||||
if(!side || !btn) return;
|
||||
function open(){ side.classList.add('open'); back.classList.add('show'); }
|
||||
function close(){ side.classList.remove('open'); back.classList.remove('show'); }
|
||||
btn.addEventListener('click', () => { if(side.classList.contains('open')) close(); else open(); });
|
||||
back.addEventListener('click', close);
|
||||
document.addEventListener('keydown', e => { if(e.key === 'Escape') close(); });
|
||||
}
|
||||
|
||||
function initSearch(){
|
||||
const btn = document.getElementById('search-btn'), modal = document.getElementById('search-modal'), inp = document.getElementById('search-inp'), list = document.getElementById('search-list');
|
||||
if(!btn || !modal) return;
|
||||
let cur = 0;
|
||||
function render(q){
|
||||
const ql = (q||'').toLowerCase().trim();
|
||||
const items = PARAS.filter(p => !ql || p.title.toLowerCase().includes(ql) || p.num.toLowerCase().includes(ql));
|
||||
list.innerHTML = items.map((p,i) => '<div class="search-item' + (i === cur ? ' cur' : '') + '" data-id="' + p.id + '"><span class="num">' + p.num + '</span>' + p.title + '</div>').join('');
|
||||
list.querySelectorAll('.search-item').forEach(el => el.addEventListener('click', () => { goTo(el.dataset.id); close(); }));
|
||||
}
|
||||
function open(){ modal.classList.add('show'); inp.value = ''; cur = 0; render(''); setTimeout(() => inp.focus(), 50); }
|
||||
function close(){ modal.classList.remove('show'); }
|
||||
btn.addEventListener('click', open);
|
||||
modal.addEventListener('click', e => { if(e.target === modal) close(); });
|
||||
inp.addEventListener('input', () => { cur = 0; render(inp.value); });
|
||||
inp.addEventListener('keydown', e => {
|
||||
const items = list.querySelectorAll('.search-item');
|
||||
if(e.key === 'ArrowDown'){ e.preventDefault(); cur = Math.min(items.length-1, cur+1); render(inp.value); }
|
||||
else if(e.key === 'ArrowUp'){ e.preventDefault(); cur = Math.max(0, cur-1); render(inp.value); }
|
||||
else if(e.key === 'Enter'){ e.preventDefault(); const sel = items[cur]; if(sel){ goTo(sel.dataset.id); close(); } }
|
||||
else if(e.key === 'Escape'){ e.preventDefault(); close(); }
|
||||
});
|
||||
document.addEventListener('keydown', e => { if((e.ctrlKey || e.metaKey) && (e.key === 'k' || e.key === 'K')){ e.preventDefault(); if(modal.classList.contains('show')) close(); else open(); } });
|
||||
}
|
||||
|
||||
function init(){
|
||||
loadProgress(); initTheme(); initSidebarToggle(); initSearch();
|
||||
buildParaSelector(); refreshProgressUI(); goTo(PARAS[0].id);
|
||||
setTimeout(() => achievement('start'), 600);
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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 · Глава 5</h1>
|
||||
<div class="hdr-sub">Работа. Мощность. Энергия · §§36–42</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 класс» · Глава 5 · 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 ? ' — ' + v : '') + '</div>'; });
|
||||
html += '</div>';
|
||||
const tip = TIPS.find(t => t.sec === id) || TIPS[0];
|
||||
if(tip){
|
||||
html += '<div class="sidecard" style="background:linear-gradient(135deg,var(--warn-bg),var(--pri-soft));border-color:var(--warn)"><h4 style="color:#92400e">Подсказка</h4><div class="sidecard-row" style="margin-bottom:0;font-size:.84rem">' + tip.html + '</div></div>';
|
||||
}
|
||||
if(STATE.achievements.size > 0){
|
||||
html += '<div class="sidecard"><h4>Достижения <span style="color:var(--warn);float:right">' + STATE.achievements.size + '</span></h4>';
|
||||
[...STATE.achievements.values()].slice(-4).forEach(t => { html += '<div class="sidecard-row" style="font-size:.78rem;color:var(--ok)">✓ ' + t + '</div>'; });
|
||||
html += '</div>';
|
||||
}
|
||||
box.innerHTML = html;
|
||||
if(window.renderMathInElement){
|
||||
try{ renderMathInElement(box, { delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false}], throwOnError:false }); }catch(e){}
|
||||
}
|
||||
}
|
||||
|
||||
function initTheme(){
|
||||
const t = localStorage.getItem(LS_PREFIX + '_theme') || localStorage.getItem('physics7_theme') || 'light';
|
||||
if(t === 'dark') document.documentElement.classList.add('dark');
|
||||
document.getElementById('theme-lab').textContent = t === 'dark' ? 'Светлая' : 'Тёмная';
|
||||
document.getElementById('theme-btn').addEventListener('click', () => {
|
||||
document.documentElement.classList.toggle('dark');
|
||||
const dark = document.documentElement.classList.contains('dark');
|
||||
localStorage.setItem(LS_PREFIX + '_theme', dark ? 'dark' : 'light');
|
||||
localStorage.setItem('physics7_theme', dark ? 'dark' : 'light');
|
||||
document.getElementById('theme-lab').textContent = dark ? 'Светлая' : 'Тёмная';
|
||||
});
|
||||
}
|
||||
|
||||
function initSidebarToggle(){
|
||||
const side = document.getElementById('col-side'), back = document.getElementById('col-side-backdrop'), btn = document.getElementById('sidebar-btn');
|
||||
if(!side || !btn) return;
|
||||
function open(){ side.classList.add('open'); back.classList.add('show'); }
|
||||
function close(){ side.classList.remove('open'); back.classList.remove('show'); }
|
||||
btn.addEventListener('click', () => { if(side.classList.contains('open')) close(); else open(); });
|
||||
back.addEventListener('click', close);
|
||||
document.addEventListener('keydown', e => { if(e.key === 'Escape') close(); });
|
||||
}
|
||||
|
||||
function initSearch(){
|
||||
const btn = document.getElementById('search-btn'), modal = document.getElementById('search-modal'), inp = document.getElementById('search-inp'), list = document.getElementById('search-list');
|
||||
if(!btn || !modal) return;
|
||||
let cur = 0;
|
||||
function render(q){
|
||||
const ql = (q||'').toLowerCase().trim();
|
||||
const items = PARAS.filter(p => !ql || p.title.toLowerCase().includes(ql) || p.num.toLowerCase().includes(ql));
|
||||
list.innerHTML = items.map((p,i) => '<div class="search-item' + (i === cur ? ' cur' : '') + '" data-id="' + p.id + '"><span class="num">' + p.num + '</span>' + p.title + '</div>').join('');
|
||||
list.querySelectorAll('.search-item').forEach(el => el.addEventListener('click', () => { goTo(el.dataset.id); close(); }));
|
||||
}
|
||||
function open(){ modal.classList.add('show'); inp.value = ''; cur = 0; render(''); setTimeout(() => inp.focus(), 50); }
|
||||
function close(){ modal.classList.remove('show'); }
|
||||
btn.addEventListener('click', open);
|
||||
modal.addEventListener('click', e => { if(e.target === modal) close(); });
|
||||
inp.addEventListener('input', () => { cur = 0; render(inp.value); });
|
||||
inp.addEventListener('keydown', e => {
|
||||
const items = list.querySelectorAll('.search-item');
|
||||
if(e.key === 'ArrowDown'){ e.preventDefault(); cur = Math.min(items.length-1, cur+1); render(inp.value); }
|
||||
else if(e.key === 'ArrowUp'){ e.preventDefault(); cur = Math.max(0, cur-1); render(inp.value); }
|
||||
else if(e.key === 'Enter'){ e.preventDefault(); const sel = items[cur]; if(sel){ goTo(sel.dataset.id); close(); } }
|
||||
else if(e.key === 'Escape'){ e.preventDefault(); close(); }
|
||||
});
|
||||
document.addEventListener('keydown', e => { if((e.ctrlKey || e.metaKey) && (e.key === 'k' || e.key === 'K')){ e.preventDefault(); if(modal.classList.contains('show')) close(); else open(); } });
|
||||
}
|
||||
|
||||
function init(){
|
||||
loadProgress(); initTheme(); initSidebarToggle(); initSearch();
|
||||
buildParaSelector(); refreshProgressUI(); goTo(PARAS[0].id);
|
||||
setTimeout(() => achievement('start'), 600);
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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>Физика — 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">Σ</div>
|
||||
<div class="ch-num">Глава 1</div>
|
||||
<div class="ch-title">Методы познания природы</div>
|
||||
<div class="ch-range">§1–§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">°</div>
|
||||
<div class="ch-num">Глава 2</div>
|
||||
<div class="ch-title">Строение вещества</div>
|
||||
<div class="ch-range">§8–§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">§14–§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">§28–§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">§36–§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">
|
||||
Интерактивный учебник «Физика — 7 класс» · 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>
|
||||
@@ -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 · Лабораторный практикум</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 класс» · Лабораторный практикум · 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 + ' · ' + 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 ? ' — ' + 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)">✓ ' + 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>
|
||||
Reference in New Issue
Block a user