Files
Learn_System/frontend/textbooks/physics_7_lab.html
Maxim Dolgolyov 2bf7ff7ef1 feat(phys7 lab): Phase 7 — Лабораторный практикум, 6 виртуальных ЛР
Все 6 ЛР физики 7 закрыты. Файл phys7_lab_widgets.js (726 строк, 6 экспортов:
lr1..lr6). Палитра cyan. Подключение через обновлённый gen_phys7_lab.js:
script-тег + hook в goTo (удаление placeholder + вызов widgets).

Каждая ЛР содержит:
- Цель (goal card, голубая)
- Оборудование (equip card, оранжевая)
- Ход работы (steps card, фиолетовая) — пронумерованный список
- СИМ-виджет (интерактивная симуляция прибора)
- ТБЛ-виджет (таблица измерений)
- ВОПР-виджет (3 контрольных вопроса с авто-проверкой)
- Вывод (concl card, зелёная)
- Кнопка «Сдать ЛР» (+30 XP, localStorage-фиксация)

ЛР-1 «Цена деления» (§7):
- 4 виртуальных прибора (линейка/термометр/мензурка/динамометр) с SVG-шкалами
- Таблица C для всех 4
- 3 контрольных вопроса

ЛР-2 «Измерение длины» (§4, §7):
- 3 предмета на выбор (карандаш/тетрадь/брусок), SVG с линейкой ниже,
  риска на длине + запись (l ± 0,5) мм
- Таблица 3 измерений

ЛР-3 «Объём вытеснением» (§4):
- 3 тела (камень/гайка/болт), 2 SVG-мензурки рядом (V1=100 и V2=100+V),
  стрелка «опускаем» между ними, авто-расчёт V = V2 − V1
- Таблица 3 измерений

ЛР-4 «Неравномерное движение» (§18):
- Шарик на наклонной плоскости, slider угла 10..60°, кнопка «Запустить»,
  анимация скатывания (квадратичная по времени, эмпирически быстрее на больших углах)
- Таблица 3 углов с разной средней скоростью

ЛР-5 «Плотность» (§20):
- 3 образца на выбор (54г/156г/272г, V=20 см³ каждый), SVG-весы+мензурка,
  расчёт ρ = m/V и автоопределение материала (алюминий/железо/золото)
- Таблица плотностей 9 веществ

ЛР-6 «Сила трения» (§27):
- SVG: брусок с грузами, динамометр, разные поверхности из <select>
  (дерево/пластик/резина/лёд: μ от 0.04 до 0.5)
- slider массы 100..500 г → авто N и Ftr через динамометр
- Таблица 5 измерений с разными грузами → видно Ftr ~ N

АЧИВКА «Лаборант 7 класса» +80 XP — автоматически при сдаче всех 6 ЛР
(проверка через localStorage в wireSubmit).

Парсинг OK, smoke (6 экспортов) OK.
2026-05-30 11:53:51 +03:00

318 lines
20 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>Физика 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>
<script src="/js/phys7_lab_widgets.js?v=20260530" defer></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Manrope:wght@600;700;800;900&family=Unbounded:wght@700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
<style>
:root{
--bg:#ecfeff; --card:#fff; --card-soft:#f8fafc; --text:#0f172a; --muted:#475569;
--border:#a5f3fc; --pri:#0891b2; --pri2:#0e7490; --pri-soft:#cffafe;
--acc:#06b6d4; --acc-d:#0e7490; --acc-soft:#cffafe;
--ok:#10b981; --ok-bg:#d1fae5; --fail:#dc2626; --fail-bg:#fee2e2; --warn:#f59e0b; --warn-bg:#fef3c7;
--sh:0 4px 16px rgba(8,145,178,.08); --sh-h:0 12px 36px rgba(8,145,178,.16);
}
html.dark{--bg:#0c2030;--card:#0e2436;--card-soft:#0b1a28;--text:#cffafe;--muted:#67e8f9;--border:#155e75;--pri-soft:rgba(8,145,178,.18)}
*{margin:0;padding:0;box-sizing:border-box}
html,body{min-height:100vh}
body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.6;transition:background .25s,color .25s}
.hdr{position:relative;background:linear-gradient(135deg,#164e63,#0891b2 60%,#22d3ee);color:#fff;padding:24px 22px 22px;overflow:hidden;border-bottom:2px solid rgba(255,255,255,.18)}
.hdr-inner{position:relative;z-index:1;max-width:1240px;margin:0 auto;display:flex;align-items:center;gap:14px;flex-wrap:wrap}
.hdr h1{font-family:'Unbounded',sans-serif;font-size:1.55rem;font-weight:800;letter-spacing:-.01em}
.hdr-sub{font-size:.88rem;opacity:.9;margin-top:3px}
.hdr-side{margin-left:auto;display:flex;gap:8px;flex-wrap:wrap}
.hdr-btn{padding:8px 12px;background:rgba(255,255,255,.16);border:none;color:#fff;border-radius:9px;cursor:pointer;font-weight:600;font-size:.82rem;display:inline-flex;align-items:center;gap:6px;transition:background .15s;font-family:inherit;text-decoration:none}
.hdr-btn:hover{background:rgba(255,255,255,.26)}
.ic{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.main{max-width:1240px;margin:0 auto;padding:22px;width:100%;display:grid;grid-template-columns:1fr 280px;gap:24px}
@media(max-width:980px){.main{grid-template-columns:1fr;padding:14px}}
.col-main{min-width:0}
.psel{background:var(--card);border:1.5px solid var(--border);border-radius:14px;padding:16px;margin-bottom:18px;box-shadow:var(--sh)}
.psel-head{font-family:'Unbounded',sans-serif;font-size:.78rem;font-weight:800;color:var(--pri2);text-transform:uppercase;letter-spacing:.07em;margin-bottom:10px}
.psel-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:10px}
.psel-card{padding:12px;background:var(--card-soft);border:1.5px solid var(--border);border-radius:10px;cursor:pointer;transition:transform .15s,border-color .15s,box-shadow .15s;text-align:left}
.psel-card:hover{border-color:var(--acc);transform:translateY(-2px);box-shadow:0 4px 14px rgba(0,0,0,.06)}
.psel-card.active{border-color:var(--acc);background:var(--acc-soft)}
.psel-num{font-size:.7rem;font-weight:800;color:var(--acc-d);letter-spacing:.04em;text-transform:uppercase;margin-bottom:3px}
.psel-title{font-size:.86rem;font-weight:700;line-height:1.35}
.psel-prog{height:4px;background:rgba(0,0,0,.07);border-radius:3px;overflow:hidden;margin-top:7px}
.psel-prog-fill{height:100%;background:linear-gradient(90deg,var(--acc),var(--acc-d));border-radius:3px;transition:width .4s}
.sec{display:none;background:var(--card);border:1.5px solid var(--border);border-radius:14px;padding:22px;box-shadow:var(--sh);position:relative}
.sec.active{display:block}
.sec[data-watermark]::before{content:attr(data-watermark);position:absolute;right:18px;top:-8px;font-family:'Unbounded',sans-serif;font-size:5.6rem;font-weight:900;color:var(--acc-soft);pointer-events:none;line-height:1;user-select:none}
.sec-header{display:flex;align-items:baseline;gap:14px;margin-bottom:18px;padding-bottom:14px;border-bottom:1.5px solid var(--border);position:relative;z-index:1;flex-wrap:wrap}
.sec-num{background:linear-gradient(135deg,var(--acc),var(--acc-d));color:#fff;padding:5px 12px;border-radius:9px;font-family:'Unbounded',sans-serif;font-weight:800;font-size:.86rem;letter-spacing:.04em}
.sec-h{font-family:'Unbounded',sans-serif;font-size:1.3rem;font-weight:800;color:var(--text);flex:1;min-width:0}
.sec-tag{font-size:.74rem;font-weight:700;color:var(--pri2);background:var(--pri-soft);padding:3px 9px;border-radius:99px;text-transform:uppercase;letter-spacing:.04em}
.placeholder{padding:32px 20px;text-align:center;color:var(--muted);font-size:.95rem;background:var(--card-soft);border:1.5px dashed var(--border);border-radius:10px}
.col-side{position:sticky;top:14px;align-self:start;height:fit-content;max-height:calc(100vh - 28px);overflow-y:auto}
.sidecard{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:16px;margin-bottom:14px;box-shadow:var(--sh)}
.sidecard h4{font-family:'Unbounded',sans-serif;font-size:.74rem;font-weight:800;color:var(--pri2);text-transform:uppercase;letter-spacing:.07em;margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid var(--border)}
.sidecard-row{margin-bottom:8px;font-size:.86rem;line-height:1.6}
.sidecard-row b{color:var(--pri);font-weight:700}
@media(max-width:980px){.col-side{position:static;max-height:none}}
.col-side-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.42);z-index:9990;display:none}
.col-side-backdrop.show{display:block}
@media(min-width:981px){#sidebar-btn{display:none}.col-side-backdrop.show{display:none}}
@media(max-width:980px){
.col-side{position:fixed;top:0;right:0;height:100vh;width:300px;max-width:88vw;background:var(--bg);box-shadow:-12px 0 24px rgba(0,0,0,.18);padding:18px 16px;overflow-y:auto;transform:translateX(100%);transition:transform .25s ease;z-index:9991;max-height:none}
.col-side.open{transform:none}
}
.ach-popup{position:fixed;top:80px;right:18px;background:linear-gradient(135deg,var(--acc-d),var(--acc));color:#fff;padding:12px 18px;border-radius:11px;font-weight:700;font-size:.9rem;box-shadow:0 8px 28px rgba(0,0,0,.25);z-index:1002;display:none;align-items:center;gap:8px;max-width:340px}
.ach-popup.show{display:flex}
.foot{text-align:center;padding:30px 16px;color:var(--muted);font-size:.78rem;border-top:1px solid var(--border);margin-top:30px}
</style>
</head>
<body>
<header class="hdr">
<div class="hdr-inner">
<div>
<a href="/textbook/physics-7" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> К физике 7</a>
</div>
<div>
<h1>Физика 7 &middot; Лабораторный практикум</h1>
<div class="hdr-sub">6 виртуальных лабораторных работ</div>
</div>
<div class="hdr-side">
<button id="sidebar-btn" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="18" x2="14" y2="18"/></svg> Шпаргалка</button>
<button id="theme-btn" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg><span id="theme-lab">Тёмная</span></button>
</div>
</div>
</header>
<main class="main">
<div class="col-main">
<div class="psel">
<div class="psel-head">Лабораторные работы</div>
<div class="psel-grid" id="psel-grid"></div>
</div>
<section id="sec-lr1" class="sec" data-watermark="1">
<div class="sec-header">
<span class="sec-num">ЛР 1</span>
<h2 class="sec-h">Определение цены деления шкалы измерительного прибора</h2>
<span class="sec-tag">§ 7</span>
</div>
<div id="lr1-body"><div class="placeholder">Виртуальная лабораторная работа появится в Phase 7 (после контента глав).</div></div>
</section>
<section id="sec-lr2" class="sec" data-watermark="2">
<div class="sec-header">
<span class="sec-num">ЛР 2</span>
<h2 class="sec-h">Измерение длины</h2>
<span class="sec-tag">§ 4 · § 7</span>
</div>
<div id="lr2-body"><div class="placeholder">Виртуальная лабораторная работа появится в Phase 7 (после контента глав).</div></div>
</section>
<section id="sec-lr3" class="sec" data-watermark="3">
<div class="sec-header">
<span class="sec-num">ЛР 3</span>
<h2 class="sec-h">Измерение объёма</h2>
<span class="sec-tag">§ 4</span>
</div>
<div id="lr3-body"><div class="placeholder">Виртуальная лабораторная работа появится в Phase 7 (после контента глав).</div></div>
</section>
<section id="sec-lr4" class="sec" data-watermark="4">
<div class="sec-header">
<span class="sec-num">ЛР 4</span>
<h2 class="sec-h">Изучение неравномерного движения</h2>
<span class="sec-tag">§ 18</span>
</div>
<div id="lr4-body"><div class="placeholder">Виртуальная лабораторная работа появится в Phase 7 (после контента глав).</div></div>
</section>
<section id="sec-lr5" class="sec" data-watermark="5">
<div class="sec-header">
<span class="sec-num">ЛР 5</span>
<h2 class="sec-h">Измерение плотности вещества</h2>
<span class="sec-tag">§ 20</span>
</div>
<div id="lr5-body"><div class="placeholder">Виртуальная лабораторная работа появится в Phase 7 (после контента глав).</div></div>
</section>
<section id="sec-lr6" class="sec" data-watermark="6">
<div class="sec-header">
<span class="sec-num">ЛР 6</span>
<h2 class="sec-h">Изучение силы трения</h2>
<span class="sec-tag">§ 27</span>
</div>
<div id="lr6-body"><div class="placeholder">Виртуальная лабораторная работа появится в Phase 7 (после контента глав).</div></div>
</section>
</div>
<aside class="col-side" id="col-side"><div id="sidebar-content"></div></aside>
<div class="col-side-backdrop" id="col-side-backdrop"></div>
</main>
<div class="ach-popup" id="ach-popup"><svg class="ic" viewBox="0 0 24 24"><polygon points="12,2 15,9 22,9.3 17,14 18.5,21 12,17 5.5,21 7,14 2,9.3 9,9"/></svg><span id="ach-text"></span></div>
<footer class="foot">Интерактивный учебник «Физика 7 класс» &middot; Лабораторный практикум &middot; LearnSpace</footer>
<script>
'use strict';
const LS_PREFIX = 'physics7_lab';
const _TB_SLUG = 'physics-7-lab';
const LABS = [{id:'lr1',num:'ЛР 1',title:"Определение цены деления шкалы измерительного прибора",wm:'1',tag:'§ 7'},{id:'lr2',num:'ЛР 2',title:"Измерение длины",wm:'2',tag:'§ 4 · § 7'},{id:'lr3',num:'ЛР 3',title:"Измерение объёма",wm:'3',tag:'§ 4'},{id:'lr4',num:'ЛР 4',title:"Изучение неравномерного движения",wm:'4',tag:'§ 18'},{id:'lr5',num:'ЛР 5',title:"Измерение плотности вещества",wm:'5',tag:'§ 20'},{id:'lr6',num:'ЛР 6',title:"Изучение силы трения",wm:'6',tag:'§ 27'}];
const TOTAL_LABS = LABS.length;
const SIDEBARS = {};
LABS.forEach(l => { SIDEBARS[l.id] = { title: 'Шпаргалка ' + l.num, rows: [['В разработке','симуляция и таблицы измерений появятся в Phase 7']] }; });
const ACH_LABELS = { start: 'Начало практикума', all_labs: 'Лаборант 7 класса' };
const STATE = { current: null, progress: {}, xp: 0, level: 1, achievements: new Map() };
function _xpForLevel(lv){ return Math.round(100 * Math.pow(lv-1, 1.6)); }
function calcLevel(xp){ let lv = 1; while(_xpForLevel(lv+1) <= xp) lv++; return lv; }
function loadProgress(){
try{
const s = localStorage.getItem(LS_PREFIX + '_progress'); if(s) Object.assign(STATE.progress, JSON.parse(s));
const a = localStorage.getItem(LS_PREFIX + '_achievements');
if(a){ const p = JSON.parse(a); if(p && typeof p === 'object') for(const [id,t] of Object.entries(p)) STATE.achievements.set(id, (t && t !== id) ? t : (ACH_LABELS[id] || id)); }
STATE.xp = +(localStorage.getItem('physics7_xp') || 0); STATE.level = calcLevel(STATE.xp);
}catch(e){}
}
function saveProgress(){
try{
localStorage.setItem(LS_PREFIX + '_progress', JSON.stringify(STATE.progress));
localStorage.setItem(LS_PREFIX + '_achievements', JSON.stringify(Object.fromEntries(STATE.achievements)));
localStorage.setItem('physics7_xp', String(STATE.xp));
}catch(e){}
}
function bumpProgress(key, delta){
STATE.progress[key] = Math.max(0, Math.min(100, (STATE.progress[key]||0) + delta));
saveProgress(); refreshProgressUI();
const done = LABS.every(l => (STATE.progress[l.id]||0) >= 100);
if(done && !STATE.achievements.has('all_labs')) achievement('all_labs');
}
function addXp(n, src){
if(!n) return;
STATE.xp = Math.max(0, (STATE.xp||0) + n); STATE.level = calcLevel(STATE.xp);
saveProgress(); refreshProgressUI();
if(window.LS && window.LS.xp) window.LS.xp.add(n, 'physics7-lab-' + (src||'misc'));
}
function achievement(id, label){
if(STATE.achievements.has(id)) return;
STATE.achievements.set(id, label || ACH_LABELS[id] || id);
saveProgress();
const pop = document.getElementById('ach-popup');
if(pop){ document.getElementById('ach-text').textContent = 'Ачивка: ' + (label || ACH_LABELS[id] || id); pop.classList.add('show'); setTimeout(()=>pop.classList.remove('show'), 3000); }
addXp(id === 'all_labs' ? 80 : 20, 'ach-' + id);
}
function refreshProgressUI(){
document.querySelectorAll('[data-prog-card]').forEach(el => {
const k = el.dataset.progCard;
const fl = el.querySelector('.psel-prog-fill');
if(fl) fl.style.width = (STATE.progress[k]||0) + '%';
});
if(STATE.current && document.getElementById('sidebar-content')){ try{ buildSidebar(STATE.current); }catch(e){} }
}
function buildSelector(){
const grid = document.getElementById('psel-grid');
if(!grid) return;
grid.innerHTML = LABS.map(l =>
'<button class="psel-card" data-id="' + l.id + '" data-prog-card="' + l.id + '">'
+ '<div class="psel-num">' + l.num + ' &middot; ' + l.tag + '</div>'
+ '<div class="psel-title">' + l.title + '</div>'
+ '<div class="psel-prog"><div class="psel-prog-fill" style="width:' + (STATE.progress[l.id]||0) + '%"></div></div>'
+ '</button>'
).join('');
grid.querySelectorAll('.psel-card').forEach(c => c.addEventListener('click', () => goTo(c.dataset.id)));
}
function goTo(id){
STATE.current = id;
document.querySelectorAll('.sec').forEach(s => s.classList.remove('active'));
const el = document.getElementById('sec-' + id); if(el) el.classList.add('active');
document.querySelectorAll('.psel-card').forEach(c => c.classList.toggle('active', c.dataset.id === id));
buildSidebar(id);
window.scrollTo({ top: 0, behavior: 'smooth' });
if((STATE.progress[id]||0) < 10) bumpProgress(id, 10);
const W = window['PHYS7_LAB_WIDGETS'];
if(W && typeof W[id] === 'function' && !STATE._built){ STATE._built = {}; }
if(W && typeof W[id] === 'function' && !STATE._built[id]){
STATE._built[id] = true;
const body = document.getElementById(id + '-body');
if(body){
const ph = body.querySelector('.placeholder');
if(ph) ph.remove();
}
try{ W[id](); }catch(e){ console.warn('phys7 lab ' + id + ':', e.message); }
}
}
function buildSidebar(id){
const box = document.getElementById('sidebar-content');
if(!box) return;
const sb = SIDEBARS[id] || SIDEBARS[LABS[0].id];
const xpForLv = _xpForLevel(STATE.level), xpNext = _xpForLevel(STATE.level+1);
const xpPct = (xpNext - xpForLv) > 0 ? Math.round((STATE.xp - xpForLv) / (xpNext - xpForLv) * 100) : 100;
let html = '';
html += '<div class="sidecard" style="background:linear-gradient(135deg,var(--pri-soft),var(--acc-soft));border-color:var(--acc)"><h4>XP-прогресс <span style="float:right">Ур. ' + STATE.level + '</span></h4><div class="sidecard-row"><div style="height:8px;background:rgba(0,0,0,.07);border-radius:5px;overflow:hidden"><div style="height:100%;background:linear-gradient(90deg,var(--acc),var(--pri));width:' + xpPct + '%"></div></div><div style="display:flex;justify-content:space-between;font-size:.78rem;color:var(--muted);margin-top:5px"><span>' + STATE.xp + ' XP</span><span>' + xpNext + ' XP</span></div></div></div>';
html += '<div class="sidecard"><h4>' + sb.title + '</h4>';
sb.rows.forEach(([k,v]) => { html += '<div class="sidecard-row"><b>' + k + '</b>' + (v ? ' &mdash; ' + v : '') + '</div>'; });
html += '</div>';
if(STATE.achievements.size > 0){
html += '<div class="sidecard"><h4>Достижения <span style="color:var(--warn);float:right">' + STATE.achievements.size + '</span></h4>';
[...STATE.achievements.values()].slice(-4).forEach(t => { html += '<div class="sidecard-row" style="font-size:.78rem;color:var(--ok)">&#10003; ' + t + '</div>'; });
html += '</div>';
}
box.innerHTML = html;
}
function initTheme(){
const t = localStorage.getItem(LS_PREFIX + '_theme') || localStorage.getItem('physics7_theme') || 'light';
if(t === 'dark') document.documentElement.classList.add('dark');
document.getElementById('theme-lab').textContent = t === 'dark' ? 'Светлая' : 'Тёмная';
document.getElementById('theme-btn').addEventListener('click', () => {
document.documentElement.classList.toggle('dark');
const dark = document.documentElement.classList.contains('dark');
localStorage.setItem(LS_PREFIX + '_theme', dark ? 'dark' : 'light');
localStorage.setItem('physics7_theme', dark ? 'dark' : 'light');
document.getElementById('theme-lab').textContent = dark ? 'Светлая' : 'Тёмная';
});
}
function initSidebarToggle(){
const side = document.getElementById('col-side'), back = document.getElementById('col-side-backdrop'), btn = document.getElementById('sidebar-btn');
if(!side || !btn) return;
function open(){ side.classList.add('open'); back.classList.add('show'); }
function close(){ side.classList.remove('open'); back.classList.remove('show'); }
btn.addEventListener('click', () => { if(side.classList.contains('open')) close(); else open(); });
back.addEventListener('click', close);
document.addEventListener('keydown', e => { if(e.key === 'Escape') close(); });
}
function init(){
loadProgress(); initTheme(); initSidebarToggle();
buildSelector(); refreshProgressUI(); goTo(LABS[0].id);
setTimeout(() => achievement('start'), 600);
}
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>