Files
Learn_System/frontend/js/phys7_lab_widgets.js
T
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

727 lines
56 KiB
JavaScript
Raw 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.
// Физика 7 · Лабораторный практикум · 6 виртуальных ЛР.
// Палитра cyan (#0891b2). Каждая ЛР: Цель + Оборудование + Ход + Симуляция +
// Таблица измерений + Контрольные вопросы + Кнопка «Сдать ЛР».
// Ачивка «Лаборант 7 класса» (+80 XP) — за прохождение всех 6.
(function(){
'use strict';
const ACCENT = '#0891b2';
const ACCENT_D = '#0e7490';
const ACCENT_SOFT = '#cffafe';
function renderMath(root){
if(window.renderMathInElement){
try{
window.renderMathInElement(root, {
delimiters: [{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false}],
throwOnError: false
});
}catch(e){}
}
}
function makeCard(kind, title, body){
const colorByKind = { goal:ACCENT, equip:'#d97706', steps:'#7c3aed', concl:'#10b981' };
const labelByKind = { goal:'Цель', equip:'Оборудование', steps:'Ход работы', concl:'Вывод' };
const c = colorByKind[kind] || ACCENT;
return '<div style="background:#fff;border:1.5px solid ' + ACCENT_SOFT + ';border-left:5px solid ' + c + ';border-radius:11px;padding:14px 16px;margin-bottom:12px;box-shadow:0 2px 8px rgba(0,0,0,.05)">'
+ '<div style="display:flex;align-items:center;gap:10px;margin-bottom:8px;flex-wrap:wrap">'
+ '<span style="background:' + c + ';color:#fff;padding:3px 10px;border-radius:99px;font-size:.7rem;font-weight:800;text-transform:uppercase;letter-spacing:.05em">' + labelByKind[kind] + '</span>'
+ '<span style="font-family:Unbounded,sans-serif;font-weight:800;font-size:.96rem;color:#0f172a;flex:1;min-width:0">' + title + '</span>'
+ '</div>'
+ '<div style="font-size:.94rem;line-height:1.6;color:#0f172a">' + body + '</div>'
+ '</div>';
}
function wgWrap(id, badge, title, hint, body){
return '<div id="' + id + '" style="background:#fff;border:1.5px solid ' + ACCENT_SOFT + ';border-radius:12px;padding:14px 16px;margin-bottom:14px;box-shadow:0 2px 8px rgba(0,0,0,.05)">'
+ '<div style="display:flex;align-items:center;gap:10px;margin-bottom:8px;flex-wrap:wrap">'
+ '<span style="background:' + ACCENT + ';color:#fff;padding:3px 10px;border-radius:99px;font-size:.7rem;font-weight:800;letter-spacing:.05em">' + badge + '</span>'
+ '<span style="font-family:Unbounded,sans-serif;font-weight:800;font-size:.92rem;color:#0f172a">' + title + '</span>'
+ '</div>'
+ (hint ? '<div style="font-size:.84rem;color:#64748b;background:' + ACCENT_SOFT + ';border-left:3px solid ' + ACCENT + ';border-radius:6px;padding:8px 12px;margin-bottom:10px;line-height:1.5">' + hint + '</div>' : '')
+ body
+ '</div>';
}
function submitBtn(lrId){
return '<div style="text-align:center;margin-top:18px"><button class="ph7-lr-submit" data-lr="' + lrId + '" '
+ 'style="background:linear-gradient(135deg,' + ACCENT + ',' + ACCENT_D + ');color:#fff;border:none;padding:11px 26px;border-radius:11px;font-weight:700;font-size:.96rem;cursor:pointer;font-family:inherit;display:inline-flex;align-items:center;gap:8px;box-shadow:0 4px 14px rgba(8,145,178,.32)">'
+ '<svg viewBox="0 0 24 24" style="width:18px;height:18px;stroke:currentColor;fill:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round"><polyline points="20 6 9 17 4 12"/></svg>'
+ 'Сдать ЛР &nbsp;<span style="opacity:.85;font-size:.84rem">+30 XP</span>'
+ '</button><div id="ph7-lr-fb-' + lrId + '" style="margin-top:10px;font-size:.92rem;font-weight:700"></div></div>';
}
function wireSubmit(lrId){
const btn = document.querySelector('.ph7-lr-submit[data-lr="' + lrId + '"]');
if(!btn) return;
const KEY = 'physics7_lab_done_' + lrId;
if(localStorage.getItem(KEY) === '1'){
btn.innerHTML = '<svg viewBox="0 0 24 24" style="width:18px;height:18px;stroke:currentColor;fill:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round"><polyline points="20 6 9 17 4 12"/></svg> ЛР сдана';
btn.disabled = true; btn.style.background = '#94a3b8'; btn.style.cursor = 'default';
return;
}
btn.addEventListener('click', () => {
if(localStorage.getItem(KEY) === '1') return;
localStorage.setItem(KEY, '1');
if(typeof window.bumpProgress === 'function') window.bumpProgress(lrId, 100);
if(typeof window.addXp === 'function') window.addXp(30, 'lr-' + lrId);
btn.innerHTML = '<svg viewBox="0 0 24 24" style="width:18px;height:18px;stroke:currentColor;fill:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round"><polyline points="20 6 9 17 4 12"/></svg> ЛР сдана';
btn.disabled = true; btn.style.background = '#94a3b8'; btn.style.cursor = 'default';
const fb = document.getElementById('ph7-lr-fb-' + lrId);
if(fb){ fb.style.color = '#047857'; fb.textContent = '+30 XP получено.'; }
// Проверка ачивки «Лаборант»
const allDone = ['lr1','lr2','lr3','lr4','lr5','lr6'].every(k => localStorage.getItem('physics7_lab_done_' + k) === '1');
if(allDone && localStorage.getItem('physics7_lab_master') !== '1'){
localStorage.setItem('physics7_lab_master', '1');
if(typeof window.addXp === 'function') window.addXp(80, 'lab-master');
if(typeof window.achievement === 'function') window.achievement('all_labs', 'Лаборант 7 класса');
if(fb) fb.innerHTML = '+30 XP. <span style="color:#92400e">&#10003; Ачивка «Лаборант 7 класса» +80 XP!</span>';
}
});
}
function quizQuestion(host, idx, q, opts, correctIdx, explain){
const optsHtml = opts.map((o,i) => '<button class="qz-opt" data-i="' + i + '" type="button" style="background:#fff;border:1.5px solid ' + ACCENT_SOFT + ';border-radius:9px;padding:9px 14px;cursor:pointer;font-size:.92rem;font-family:inherit;text-align:left;width:100%;margin-bottom:6px">' + o + '</button>').join('');
return '<div class="qz-q" style="background:' + ACCENT_SOFT + ';border:1.5px solid ' + ACCENT_SOFT + ';border-radius:10px;padding:12px 14px;margin-bottom:10px">'
+ '<div style="font-weight:700;margin-bottom:8px;font-size:.94rem">' + (idx+1) + '. ' + q + '</div>'
+ '<div class="qz-opts" data-correct="' + correctIdx + '" data-explain="' + (explain||'').replace(/"/g,'&quot;') + '">' + optsHtml + '</div>'
+ '<div class="qz-fb" style="padding:9px 12px;border-radius:8px;font-size:.86rem;margin-top:6px;display:none;line-height:1.45"></div>'
+ '</div>';
}
function wireQuiz(host){
const root = document.getElementById(host);
if(!root) return;
const all = root.querySelectorAll('.qz-q');
const done = new Set();
all.forEach(qDiv => {
const opts = qDiv.querySelectorAll('.qz-opt');
const correct = +qDiv.querySelector('.qz-opts').dataset.correct;
const explain = qDiv.querySelector('.qz-opts').dataset.explain;
const fb = qDiv.querySelector('.qz-fb');
opts.forEach(o => o.addEventListener('click', () => {
if(done.has(qDiv)) return;
const i = +o.dataset.i;
opts.forEach(x => x.disabled = true);
if(i === correct){
o.style.background = '#d1fae5'; o.style.borderColor = '#10b981'; o.style.color = '#065f46';
fb.style.display = 'block'; fb.style.background = '#d1fae5'; fb.style.color = '#065f46'; fb.style.borderLeft = '4px solid #10b981';
fb.innerHTML = '&#10003; Верно!' + (explain ? ' ' + explain : '');
} else {
o.style.background = '#fee2e2'; o.style.borderColor = '#dc2626'; o.style.color = '#7f1d1d';
opts[correct].style.background = '#d1fae5'; opts[correct].style.borderColor = '#10b981'; opts[correct].style.color = '#065f46';
fb.style.display = 'block'; fb.style.background = '#fee2e2'; fb.style.color = '#7f1d1d'; fb.style.borderLeft = '4px solid #dc2626';
fb.innerHTML = '&#10007; Правильно: «' + opts[correct].textContent + '».' + (explain ? ' ' + explain : '');
}
done.add(qDiv);
}));
});
}
/* ========================================================== */
/* ЛР-1 — Цена деления шкалы измерительного прибора */
/* ========================================================== */
function build_lr1(){
const body = document.getElementById('lr1-body');
if(!body) return;
let h = '';
h += makeCard('goal', 'Цель',
'Научиться определять цену деления любого измерительного прибора по двум подписанным значениям и числу делений между ними.');
h += makeCard('equip', 'Оборудование (виртуальное)',
'<ul style="padding-left:20px;margin:5px 0"><li>Линейка</li><li>Мензурка</li><li>Термометр</li><li>Динамометр</li></ul>');
h += makeCard('steps', 'Ход работы',
'<ol style="padding-left:20px;margin:6px 0">'
+ '<li>Найди на шкале <b>две соседние подписанные отметки</b> $X_1$ и $X_2$.</li>'
+ '<li>Посчитай число малых делений <b>$N$</b> между ними.</li>'
+ '<li>Вычисли цену деления: $C = (X_2 - X_1)/N$.</li>'
+ '<li>Запиши результаты в таблицу.</li>'
+ '</ol>');
/* Виджет: 4 прибора */
h += wgWrap('lr1-w', 'СИМ', 'Виртуальные приборы',
'Каждый прибор имеет свою цену деления. Подвинь slider — отсчёт пересчитывается.',
'<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px">'
+ ['Линейка','Термометр','Мензурка','Динамометр'].map((nm, i) =>
'<div style="background:#f0f9ff;border:1.5px solid ' + ACCENT_SOFT + ';border-radius:10px;padding:10px">'
+ '<div style="font-weight:700;color:' + ACCENT_D + ';font-size:.88rem;margin-bottom:8px">' + nm + '</div>'
+ '<svg id="lr1-svg-' + i + '" viewBox="0 0 200 90" width="100%" style="background:#fff;border-radius:6px"></svg>'
+ '<div style="margin-top:6px;font-size:.82rem;color:#475569;font-family:JetBrains Mono,monospace">$C = $ <b id="lr1-C-' + i + '" style="color:' + ACCENT_D + '">—</b></div>'
+ '</div>').join('')
+ '</div>');
/* Таблица измерений */
h += wgWrap('lr1-tbl', 'ТБЛ', 'Таблица измерений', '',
'<table style="width:100%;border-collapse:collapse;font-size:.9rem">'
+ '<tr style="background:' + ACCENT_SOFT + '"><th style="padding:6px 10px;text-align:left;border-bottom:2px solid ' + ACCENT + '">Прибор</th><th style="padding:6px 10px;text-align:right;border-bottom:2px solid ' + ACCENT + '">$X_1$</th><th style="padding:6px 10px;text-align:right;border-bottom:2px solid ' + ACCENT + '">$X_2$</th><th style="padding:6px 10px;text-align:right;border-bottom:2px solid ' + ACCENT + '">$N$</th><th style="padding:6px 10px;text-align:right;border-bottom:2px solid ' + ACCENT + '">$C$</th></tr>'
+ [['Линейка','0','1 см','10','0,1 см = 1 мм'],['Термометр','0','10 °C','5','2 °C'],['Мензурка','0','100 мл','5','20 мл'],['Динамометр','0','5 Н','10','0,5 Н']].map(r =>
'<tr><td style="padding:5px 10px;border-bottom:1px solid ' + ACCENT_SOFT + '">' + r[0] + '</td><td style="padding:5px 10px;text-align:right;border-bottom:1px solid ' + ACCENT_SOFT + ';font-family:JetBrains Mono,monospace">' + r[1] + '</td><td style="padding:5px 10px;text-align:right;border-bottom:1px solid ' + ACCENT_SOFT + ';font-family:JetBrains Mono,monospace">' + r[2] + '</td><td style="padding:5px 10px;text-align:right;border-bottom:1px solid ' + ACCENT_SOFT + ';font-family:JetBrains Mono,monospace">' + r[3] + '</td><td style="padding:5px 10px;text-align:right;border-bottom:1px solid ' + ACCENT_SOFT + ';font-family:JetBrains Mono,monospace;font-weight:700;color:' + ACCENT_D + '">' + r[4] + '</td></tr>').join('')
+ '</table>');
/* Контрольные вопросы */
h += wgWrap('lr1-q', 'ВОПР', 'Контрольные вопросы', '',
'<div id="lr1-q-host">'
+ quizQuestion('lr1-q', 0, 'Что такое цена деления?', ['Расстояние между прибором и телом','Значение наименьшего деления шкалы','Размер шкалы','Цена прибора'], 1)
+ quizQuestion('lr1-q', 1, 'Между $0$ и $1$ см на линейке $10$ делений. $C = ?$', ['1 мм','5 мм','10 мм','0,1 мм'], 0, '$C = (10-0)/10 = 1$ мм.')
+ quizQuestion('lr1-q', 2, 'Чем меньше цена деления, тем прибор…', ['Дешевле','Меньше','Точнее','Тяжелее'], 2)
+ '</div>');
h += makeCard('concl', 'Вывод',
'Мы научились определять цену деления любого измерительного прибора. Формула $C = (X_2 - X_1)/N$ '
+ 'универсальна — работает для линейки, термометра, мензурки, динамометра и других приборов со шкалой. '
+ 'Чем меньше $C$, тем точнее прибор.');
h += submitBtn('lr1');
body.innerHTML = h;
// Render 4 instruments
const insts = [
{ ticks:11, lbl:i => i%10===0?(i/10):'', unit:'см', color:'#0284c7' },
{ ticks:6, lbl:i => i*10, unit:'°C', color:'#dc2626' },
{ ticks:6, lbl:i => i*100, unit:'мл', color:'#0891b2' },
{ ticks:11, lbl:i => i%2===0?(i*0.5):'', unit:'Н', color:'#d97706' }
];
insts.forEach((inst, idx) => {
const W = 200, H = 90;
let s = '<rect x="0" y="20" width="' + W + '" height="40" fill="#fef9c3" stroke="' + inst.color + '" stroke-width="1.5" rx="2"/>';
for(let i = 0; i < inst.ticks; i++){
const x = 10 + (i * (W - 20) / (inst.ticks - 1));
const tickH = 14;
s += '<line x1="' + x + '" y1="20" x2="' + x + '" y2="' + (20 + tickH) + '" stroke="#0f172a" stroke-width="1.2"/>';
const lbl = inst.lbl(i);
if(lbl !== '') s += '<text x="' + x + '" y="' + (20 + tickH + 12) + '" text-anchor="middle" font-family="Inter,sans-serif" font-size="10" font-weight="600" fill="#0f172a">' + lbl + '</text>';
}
s += '<text x="' + (W-8) + '" y="35" text-anchor="end" font-family="Inter,sans-serif" font-size="10" fill="' + inst.color + '" font-weight="700">' + inst.unit + '</text>';
document.getElementById('lr1-svg-' + idx).innerHTML = s;
const cVals = ['1 мм','2 °C','20 мл','0,5 Н'];
document.getElementById('lr1-C-' + idx).textContent = cVals[idx];
});
wireQuiz('lr1-q-host');
wireSubmit('lr1');
renderMath(body);
}
/* ========================================================== */
/* ЛР-2 — Измерение длины */
/* ========================================================== */
function build_lr2(){
const body = document.getElementById('lr2-body');
if(!body) return;
let h = '';
h += makeCard('goal', 'Цель',
'Научиться измерять длину предметов линейкой и записывать результат с учётом погрешности измерения.');
h += makeCard('equip', 'Оборудование',
'<ul style="padding-left:20px;margin:5px 0"><li>Линейка с миллиметровой шкалой</li><li>3 предмета разной длины (карандаш, тетрадь, брусок)</li></ul>');
h += makeCard('steps', 'Ход работы',
'<ol style="padding-left:20px;margin:6px 0">'
+ '<li>Положи предмет вдоль линейки. Совмести один край с отметкой $0$.</li>'
+ '<li>Прочитай отсчёт у другого края, округлив до ближайшего деления.</li>'
+ '<li>Цена деления $C = 1$ мм, поэтому погрешность $\\Delta l = C/2 = 0{,}5$ мм.</li>'
+ '<li>Запиши: $l = (l_0 \\pm \\Delta l)$ мм.</li>'
+ '</ol>');
/* Виджет: 3 предмета по линейке */
h += wgWrap('lr2-w', 'СИМ', 'Измерь 3 предмета', 'Выбери предмет — увидь его длину на линейке.',
'<div style="display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap">'
+ [['penc','Карандаш',125],['tetr','Тетрадь',204],['brusk','Брусок',83]].map((o, i) =>
'<button class="lr2-obj" data-l="' + o[2] + '" data-nm="' + o[1] + '" type="button" style="background:' + (i===0 ? ACCENT : '#fff') + ';color:' + (i===0 ? '#fff' : ACCENT) + ';border:2px solid ' + ACCENT + ';padding:7px 14px;border-radius:9px;cursor:pointer;font-weight:700;font-family:inherit;font-size:.86rem">' + o[1] + '</button>').join('')
+ '</div>'
+ '<svg id="lr2-svg" viewBox="0 0 360 110" width="100%" style="max-width:600px;display:block;margin:0 auto;background:#f0f9ff;border-radius:9px;border:1px solid ' + ACCENT_SOFT + '"></svg>'
+ '<div id="lr2-info" style="background:' + ACCENT_SOFT + ';border-radius:9px;padding:10px 14px;margin-top:8px;font-size:.94rem;text-align:center"></div>');
/* Таблица */
h += wgWrap('lr2-tbl', 'ТБЛ', 'Таблица измерений',
'',
'<table style="width:100%;border-collapse:collapse;font-size:.9rem">'
+ '<tr style="background:' + ACCENT_SOFT + '"><th style="padding:6px 10px;text-align:left;border-bottom:2px solid ' + ACCENT + '">Предмет</th><th style="padding:6px 10px;text-align:right;border-bottom:2px solid ' + ACCENT + '">$l_0$, мм</th><th style="padding:6px 10px;text-align:right;border-bottom:2px solid ' + ACCENT + '">$\\Delta l$, мм</th><th style="padding:6px 10px;text-align:right;border-bottom:2px solid ' + ACCENT + '">Запись</th></tr>'
+ [['Карандаш',125,0.5,'(125,0 ± 0,5) мм'],['Тетрадь',204,0.5,'(204,0 ± 0,5) мм'],['Брусок',83,0.5,'(83,0 ± 0,5) мм']].map(r =>
'<tr><td style="padding:5px 10px;border-bottom:1px solid ' + ACCENT_SOFT + '">' + r[0] + '</td><td style="padding:5px 10px;text-align:right;border-bottom:1px solid ' + ACCENT_SOFT + ';font-family:JetBrains Mono,monospace">' + r[1] + '</td><td style="padding:5px 10px;text-align:right;border-bottom:1px solid ' + ACCENT_SOFT + ';font-family:JetBrains Mono,monospace">' + r[2] + '</td><td style="padding:5px 10px;text-align:right;border-bottom:1px solid ' + ACCENT_SOFT + ';font-family:JetBrains Mono,monospace;font-weight:700;color:' + ACCENT_D + '">' + r[3] + '</td></tr>').join('')
+ '</table>');
h += wgWrap('lr2-q', 'ВОПР', 'Контрольные вопросы', '',
'<div id="lr2-q-host">'
+ quizQuestion('lr2-q', 0, 'Чему равна погрешность миллиметровой линейки?', ['1 мм','0,5 мм','0,1 мм','2 мм'], 1, 'Половина цены деления: $1/2 = 0{,}5$ мм.')
+ quizQuestion('lr2-q', 1, 'Зачем записывать $\\Delta l$ в результате?', ['Для красоты','Чтобы показать границы возможной ошибки','По привычке','Не нужно'], 1)
+ quizQuestion('lr2-q', 2, 'Запись $l = (15{,}0 \\pm 0{,}5)$ мм означает, что $l$ может быть…', ['Любым','От 14,5 до 15,5 мм','От 10 до 20 мм','Точно 15 мм'], 1)
+ '</div>');
h += makeCard('concl', 'Вывод',
'Мы научились измерять длину предметов с погрешностью $\\Delta l = C/2 = 0{,}5$ мм. '
+ 'Любое измерение записывается в виде $l = (l_0 \\pm \\Delta l)$ — это указывает на границы, в которых лежит истинная длина.');
h += submitBtn('lr2');
body.innerHTML = h;
// Render ruler with object
function draw2(lenMm, nm){
const W = 360, H = 110;
const xPad = 20;
const rulerW = W - 2 * xPad;
const totalMm = 300;
const pxPerMm = rulerW / totalMm;
let s = '';
// Object
const objW = lenMm * pxPerMm;
s += '<rect x="' + xPad + '" y="20" width="' + objW + '" height="22" fill="' + ACCENT + '" stroke="' + ACCENT_D + '" stroke-width="1.5" rx="2"/>';
s += '<text x="' + (xPad + objW/2) + '" y="35" text-anchor="middle" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="#fff">' + nm + '</text>';
// Ruler
s += '<rect x="' + xPad + '" y="60" width="' + rulerW + '" height="32" fill="#fef9c3" stroke="#92400e" stroke-width="1.5" rx="2"/>';
for(let mm = 0; mm <= totalMm; mm += 10){
const x = xPad + mm * pxPerMm;
const isCm = mm % 10 === 0, isBig = mm % 50 === 0;
const tickH = isBig ? 14 : 10;
s += '<line x1="' + x + '" y1="60" x2="' + x + '" y2="' + (60 + tickH) + '" stroke="#0f172a" stroke-width="' + (isBig ? 1.5 : 1) + '"/>';
if(isBig) s += '<text x="' + x + '" y="' + (60 + 30) + '" text-anchor="middle" font-family="Inter,sans-serif" font-size="9" font-weight="600" fill="#0f172a">' + (mm/10) + '</text>';
}
// Mark of length
const markX = xPad + lenMm * pxPerMm;
s += '<line x1="' + markX + '" y1="40" x2="' + markX + '" y2="95" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="3 2"/>';
s += '<text x="' + (W - 6) + '" y="78" text-anchor="end" font-family="Inter,sans-serif" font-size="10" font-weight="700" fill="#92400e">см</text>';
document.getElementById('lr2-svg').innerHTML = s;
document.getElementById('lr2-info').innerHTML = '<b>' + nm + ':</b> отсчёт $l_0 = ' + lenMm + '$ мм $= ' + (lenMm/10).toFixed(1) + '$ см. С погрешностью: $l = (' + lenMm + ' \\pm 0{,}5)$ мм.';
renderMath(document.getElementById('lr2-info'));
}
body.querySelectorAll('.lr2-obj').forEach(btn => btn.addEventListener('click', () => {
body.querySelectorAll('.lr2-obj').forEach(b => { b.style.background = '#fff'; b.style.color = ACCENT; });
btn.style.background = ACCENT; btn.style.color = '#fff';
draw2(+btn.dataset.l, btn.dataset.nm);
}));
draw2(125, 'Карандаш');
wireQuiz('lr2-q-host');
wireSubmit('lr2');
renderMath(body);
}
/* ========================================================== */
/* ЛР-3 — Измерение объёма (вытеснение жидкости) */
/* ========================================================== */
function build_lr3(){
const body = document.getElementById('lr3-body');
if(!body) return;
let h = '';
h += makeCard('goal', 'Цель',
'Научиться измерять объём тела неправильной формы методом вытеснения жидкости.');
h += makeCard('equip', 'Оборудование',
'<ul style="padding-left:20px;margin:5px 0"><li>Мензурка с водой</li><li>Тело неправильной формы (камень, гайка, болт)</li><li>Нитка</li></ul>');
h += makeCard('steps', 'Ход работы',
'<ol style="padding-left:20px;margin:6px 0">'
+ '<li>Налей в мензурку воды до отметки $V_1$. Запиши.</li>'
+ '<li>Опусти тело в мензурку (полностью под водой!).</li>'
+ '<li>Прочитай новый уровень $V_2$.</li>'
+ '<li>Объём тела: $V = V_2 - V_1$.</li>'
+ '</ol>');
/* Виджет: мензурка с телом */
h += wgWrap('lr3-w', 'СИМ', 'Измерь объём тела', 'Выбери тело и опусти его в мензурку.',
'<div style="display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap">'
+ [['stone','Камень',45],['nut','Гайка',12],['bolt','Болт',23]].map((o, i) =>
'<button class="lr3-obj" data-v="' + o[2] + '" data-nm="' + o[1] + '" type="button" style="background:' + (i===0 ? ACCENT : '#fff') + ';color:' + (i===0 ? '#fff' : ACCENT) + ';border:2px solid ' + ACCENT + ';padding:7px 14px;border-radius:9px;cursor:pointer;font-weight:700;font-family:inherit;font-size:.86rem">' + o[1] + '</button>').join('')
+ '</div>'
+ '<svg id="lr3-svg" viewBox="0 0 320 240" width="100%" style="max-width:400px;display:block;margin:0 auto;background:#f0f9ff;border-radius:9px;border:1px solid ' + ACCENT_SOFT + '"></svg>'
+ '<div id="lr3-info" style="background:' + ACCENT_SOFT + ';border-radius:9px;padding:10px 14px;margin-top:8px;font-size:.94rem;line-height:1.6"></div>');
/* Таблица */
h += wgWrap('lr3-tbl', 'ТБЛ', 'Таблица измерений', '',
'<table style="width:100%;border-collapse:collapse;font-size:.9rem">'
+ '<tr style="background:' + ACCENT_SOFT + '"><th style="padding:6px 10px;text-align:left;border-bottom:2px solid ' + ACCENT + '">Тело</th><th style="padding:6px 10px;text-align:right;border-bottom:2px solid ' + ACCENT + '">$V_1$, мл</th><th style="padding:6px 10px;text-align:right;border-bottom:2px solid ' + ACCENT + '">$V_2$, мл</th><th style="padding:6px 10px;text-align:right;border-bottom:2px solid ' + ACCENT + '">$V$, мл = см³</th></tr>'
+ [['Камень',100,145,45],['Гайка',100,112,12],['Болт',100,123,23]].map(r =>
'<tr><td style="padding:5px 10px;border-bottom:1px solid ' + ACCENT_SOFT + '">' + r[0] + '</td><td style="padding:5px 10px;text-align:right;border-bottom:1px solid ' + ACCENT_SOFT + ';font-family:JetBrains Mono,monospace">' + r[1] + '</td><td style="padding:5px 10px;text-align:right;border-bottom:1px solid ' + ACCENT_SOFT + ';font-family:JetBrains Mono,monospace">' + r[2] + '</td><td style="padding:5px 10px;text-align:right;border-bottom:1px solid ' + ACCENT_SOFT + ';font-family:JetBrains Mono,monospace;font-weight:700;color:' + ACCENT_D + '">' + r[3] + '</td></tr>').join('')
+ '</table>');
h += wgWrap('lr3-q', 'ВОПР', 'Контрольные вопросы', '',
'<div id="lr3-q-host">'
+ quizQuestion('lr3-q', 0, 'Можно ли измерить объём картофелины линейкой?', ['Да','Нет — неправильная форма, нужна мензурка','Только если разрезать','Только большие'], 1)
+ quizQuestion('lr3-q', 1, '$V_1 = 50$ мл, $V_2 = 78$ мл. $V_{тела}$?', ['28 мл','78 мл','128 мл','50 мл'], 0, '$V = V_2 - V_1 = 78 - 50 = 28$ мл.')
+ quizQuestion('lr3-q', 2, '$1$ мл — это сколько см³?', ['0,1','1','10','100'], 1)
+ '</div>');
h += makeCard('concl', 'Вывод',
'Метод вытеснения жидкости позволяет измерить объём тела <b>любой</b> формы. Это пример <b>косвенного измерения</b>: '
+ 'мы напрямую измеряем уровни $V_1$ и $V_2$, а затем по формуле $V = V_2 - V_1$ вычисляем искомый объём.');
h += submitBtn('lr3');
body.innerHTML = h;
function draw3(vTel, nm){
const W = 320, H = 240;
const W1 = 100, W2 = 200; // позиции 2 мензурок
const mWidth = 60, mHeight = 180, mY = 30;
const maxMl = 200;
function drawMenz(x, label, V){
let s = '';
// Стенки
s += '<line x1="' + x + '" y1="' + mY + '" x2="' + x + '" y2="' + (mY + mHeight) + '" stroke="#0f172a" stroke-width="2"/>';
s += '<line x1="' + (x + mWidth) + '" y1="' + mY + '" x2="' + (x + mWidth) + '" y2="' + (mY + mHeight) + '" stroke="#0f172a" stroke-width="2"/>';
s += '<line x1="' + x + '" y1="' + (mY + mHeight) + '" x2="' + (x + mWidth) + '" y2="' + (mY + mHeight) + '" stroke="#0f172a" stroke-width="2"/>';
// Вода
const waterH = (V / maxMl) * mHeight;
s += '<rect x="' + (x + 1) + '" y="' + (mY + mHeight - waterH) + '" width="' + (mWidth - 2) + '" height="' + waterH + '" fill="#60a5fa" opacity="0.7"/>';
// Шкала
for(let ml = 0; ml <= maxMl; ml += 20){
const y = mY + mHeight - (ml / maxMl) * mHeight;
s += '<line x1="' + x + '" y1="' + y + '" x2="' + (x - 6) + '" y2="' + y + '" stroke="#0f172a" stroke-width="1"/>';
if(ml % 40 === 0) s += '<text x="' + (x - 8) + '" y="' + (y + 3) + '" text-anchor="end" font-family="Inter,sans-serif" font-size="9" fill="#0f172a">' + ml + '</text>';
}
// Подпись V
s += '<text x="' + (x + mWidth/2) + '" y="' + (mY + mHeight + 18) + '" text-anchor="middle" font-family="Unbounded,sans-serif" font-size="11" font-weight="800" fill="' + ACCENT_D + '">' + label + '</text>';
s += '<text x="' + (x + mWidth/2) + '" y="' + (mY + mHeight + 32) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" font-weight="700" fill="#0f172a">' + V + ' мл</text>';
return s;
}
let s = '';
s += drawMenz(W1, 'V₁', 100);
s += drawMenz(W2, 'V₂', 100 + vTel);
// Тело во второй мензурке
s += '<circle cx="' + (W2 + mWidth/2) + '" cy="' + (mY + mHeight - 20) + '" r="' + Math.min(15, Math.sqrt(vTel)*1.4) + '" fill="#475569" stroke="#0f172a" stroke-width="1.5"/>';
// Стрелка
s += '<line x1="170" y1="120" x2="195" y2="120" stroke="' + ACCENT + '" stroke-width="2"/>';
s += '<polygon points="195,120 188,116 188,124" fill="' + ACCENT + '"/>';
s += '<text x="182" y="110" text-anchor="middle" font-family="Inter,sans-serif" font-size="10" fill="' + ACCENT_D + '" font-weight="700">опускаем</text>';
s += '<text x="182" y="138" text-anchor="middle" font-family="Inter,sans-serif" font-size="10" fill="' + ACCENT_D + '">тело</text>';
document.getElementById('lr3-svg').innerHTML = s;
document.getElementById('lr3-info').innerHTML = '<b>' + nm + '</b>: $V_1 = 100$ мл, $V_2 = ' + (100 + vTel) + '$ мл &nbsp;→&nbsp; '
+ '<b>$V = V_2 - V_1 = ' + vTel + '$ мл $= ' + vTel + '$ см³</b>';
renderMath(document.getElementById('lr3-info'));
}
body.querySelectorAll('.lr3-obj').forEach(btn => btn.addEventListener('click', () => {
body.querySelectorAll('.lr3-obj').forEach(b => { b.style.background = '#fff'; b.style.color = ACCENT; });
btn.style.background = ACCENT; btn.style.color = '#fff';
draw3(+btn.dataset.v, btn.dataset.nm);
}));
draw3(45, 'Камень');
wireQuiz('lr3-q-host');
wireSubmit('lr3');
renderMath(body);
}
/* ========================================================== */
/* ЛР-4 — Изучение неравномерного движения */
/* ========================================================== */
function build_lr4(){
const body = document.getElementById('lr4-body');
if(!body) return;
let h = '';
h += makeCard('goal', 'Цель',
'Измерить среднюю скорость движения шарика, скатывающегося по наклонной плоскости, для разных углов наклона.');
h += makeCard('equip', 'Оборудование',
'<ul style="padding-left:20px;margin:5px 0"><li>Желоб (наклонная плоскость)</li><li>Шарик</li><li>Секундомер</li><li>Линейка / мерная лента</li></ul>');
h += makeCard('steps', 'Ход работы',
'<ol style="padding-left:20px;margin:6px 0">'
+ '<li>Установи желоб под углом 15°. Отпусти шарик с верха.</li>'
+ '<li>Засеки время $t$ от старта до конца желоба.</li>'
+ '<li>Измерь путь $s$ (длину желоба).</li>'
+ '<li>Вычисли $\\langle v\\rangle = s/t$.</li>'
+ '<li>Повтори для углов 30° и 45°.</li>'
+ '</ol>');
/* Виджет: симуляция шарика */
h += wgWrap('lr4-w', 'СИМ', 'Шарик на наклонной плоскости',
'Меняй угол, нажимай «Запустить» — шарик скатится, время и средняя скорость измерятся автоматически.',
'<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:8px;margin-bottom:10px">'
+ '<label style="display:block;font-size:.86rem;color:#475569;background:#fff;padding:7px 11px;border-radius:8px;border:1px solid ' + ACCENT_SOFT + '">Угол $\\alpha$, °: <b id="lr4-a" style="color:' + ACCENT_D + ';font-family:JetBrains Mono,monospace">15</b><input type="range" id="lr4-a-r" min="10" max="60" step="5" value="15" style="display:block;width:100%;margin-top:5px;accent-color:' + ACCENT + '"></label>'
+ '<div style="display:flex;gap:6px;align-items:end"><button id="lr4-go" type="button" style="background:linear-gradient(135deg,' + ACCENT + ',' + ACCENT_D + ');color:#fff;border:none;padding:8px 18px;border-radius:9px;cursor:pointer;font-weight:700;font-family:inherit;font-size:.88rem">Запустить</button></div>'
+ '</div>'
+ '<svg id="lr4-svg" viewBox="0 0 360 200" width="100%" style="max-width:500px;display:block;margin:0 auto;background:#f0f9ff;border-radius:9px;border:1px solid ' + ACCENT_SOFT + '"></svg>'
+ '<div id="lr4-info" style="background:' + ACCENT_SOFT + ';border-radius:9px;padding:10px 14px;margin-top:8px;font-size:.94rem;text-align:center"></div>');
h += wgWrap('lr4-tbl', 'ТБЛ', 'Таблица измерений', '',
'<table style="width:100%;border-collapse:collapse;font-size:.9rem">'
+ '<tr style="background:' + ACCENT_SOFT + '"><th style="padding:6px 10px;text-align:left;border-bottom:2px solid ' + ACCENT + '">Угол $\\alpha$</th><th style="padding:6px 10px;text-align:right;border-bottom:2px solid ' + ACCENT + '">$s$, м</th><th style="padding:6px 10px;text-align:right;border-bottom:2px solid ' + ACCENT + '">$t$, с</th><th style="padding:6px 10px;text-align:right;border-bottom:2px solid ' + ACCENT + '">$\\langle v\\rangle$, м/с</th></tr>'
+ [['15°','1,0','1,7','0,59'],['30°','1,0','1,1','0,91'],['45°','1,0','0,8','1,25']].map(r =>
'<tr><td style="padding:5px 10px;border-bottom:1px solid ' + ACCENT_SOFT + '">' + r[0] + '</td><td style="padding:5px 10px;text-align:right;border-bottom:1px solid ' + ACCENT_SOFT + ';font-family:JetBrains Mono,monospace">' + r[1] + '</td><td style="padding:5px 10px;text-align:right;border-bottom:1px solid ' + ACCENT_SOFT + ';font-family:JetBrains Mono,monospace">' + r[2] + '</td><td style="padding:5px 10px;text-align:right;border-bottom:1px solid ' + ACCENT_SOFT + ';font-family:JetBrains Mono,monospace;font-weight:700;color:' + ACCENT_D + '">' + r[3] + '</td></tr>').join('')
+ '</table>');
h += wgWrap('lr4-q', 'ВОПР', 'Контрольные вопросы', '',
'<div id="lr4-q-host">'
+ quizQuestion('lr4-q', 0, 'Каков физический смысл $\\langle v\\rangle$?', ['Скорость в средней точке','Скорость, при которой за то же время прошёл бы тот же путь','Полусумма начальной и конечной','Скорость покоя'], 1)
+ quizQuestion('lr4-q', 1, 'Что изменится, если увеличить угол?', ['Время больше, скорость меньше','Время меньше, $\\langle v\\rangle$ больше','Ничего не изменится','Скорость станет нулевой'], 1, 'Чем круче — тем быстрее скатывается.')
+ quizQuestion('lr4-q', 2, 'Шарик прошёл $s = 2$ м за $t = 4$ с. $\\langle v\\rangle$?', ['0,2 м/с','0,5 м/с','1 м/с','2 м/с'], 1, '$\\langle v\\rangle = 2/4 = 0{,}5$ м/с.')
+ '</div>');
h += makeCard('concl', 'Вывод',
'Чем больше угол наклона, тем сильнее ускорение шарика и тем выше его средняя скорость. '
+ 'Движение шарика — <b>неравномерное</b>: его мгновенная скорость растёт от нуля в начале до максимума в конце. '
+ 'Формула $\\langle v\\rangle = s/t$ даёт усреднённую характеристику, удобную для сравнения.');
h += submitBtn('lr4');
body.innerHTML = h;
let lr4Anim = { raf: 0, t: 0, running: false, totalT: 0, ang: 15 };
function drawTrack(){
const ang = +document.getElementById('lr4-a-r').value;
document.getElementById('lr4-a').textContent = ang;
lr4Anim.ang = ang;
const W = 360, H = 200, baseY = 170;
const len = 220;
const a = ang * Math.PI / 180;
const x1 = 40, y1 = baseY;
const x2 = x1 + len * Math.cos(a);
const y2 = baseY - len * Math.sin(a);
let s = '';
s += '<line x1="0" y1="' + baseY + '" x2="' + W + '" y2="' + baseY + '" stroke="#0f172a" stroke-width="1.5"/>';
s += '<polygon points="' + x1 + ',' + y1 + ' ' + (x1 + len*Math.cos(a)) + ',' + y1 + ' ' + x2 + ',' + y2 + '" fill="' + ACCENT_SOFT + '" stroke="' + ACCENT_D + '" stroke-width="1.5"/>';
// Шарик в начальной позиции (на верху)
const t = lr4Anim.t;
const totalT = Math.max(0.5, 2.0 - ang * 0.025); // эмпирическая формула: больше угол — меньше время
lr4Anim.totalT = totalT;
let progress = t / totalT;
if(progress > 1) progress = 1;
// Шарик ускоряется (квадратичная зависимость пути от времени)
const sFrac = progress * progress;
const bx = x2 + (x1 - x2) * sFrac;
const by = y2 + (y1 - y2) * sFrac;
s += '<circle cx="' + bx.toFixed(1) + '" cy="' + (by - 10).toFixed(1) + '" r="9" fill="' + ACCENT + '" stroke="' + ACCENT_D + '" stroke-width="1.5"/>';
// Подпись
s += '<text x="' + W + '" y="20" text-anchor="end" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="' + ACCENT_D + '">α = ' + ang + '°</text>';
s += '<text x="' + W + '" y="36" text-anchor="end" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="' + ACCENT_D + '">s = 1,0 м</text>';
s += '<text x="' + W + '" y="52" text-anchor="end" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="#dc2626">t = ' + t.toFixed(2) + ' с</text>';
document.getElementById('lr4-svg').innerHTML = s;
const vavg = progress >= 1 ? (1.0 / totalT) : 0;
document.getElementById('lr4-info').innerHTML = progress >= 1
? '<b>Шарик скатился!</b> $t = ' + totalT.toFixed(2) + '$ с &middot; $s = 1{,}0$ м &middot; $\\langle v\\rangle = s/t = ' + vavg.toFixed(2) + '$ м/с'
: 'В движении... $t = ' + t.toFixed(2) + '$ с';
renderMath(document.getElementById('lr4-info'));
}
function lr4Loop(){
if(!lr4Anim.running) return;
lr4Anim.t += 0.02;
if(lr4Anim.t >= lr4Anim.totalT){
lr4Anim.t = lr4Anim.totalT;
lr4Anim.running = false;
drawTrack();
return;
}
drawTrack();
lr4Anim.raf = requestAnimationFrame(lr4Loop);
}
document.getElementById('lr4-a-r').addEventListener('input', () => { lr4Anim.t = 0; lr4Anim.running = false; drawTrack(); });
document.getElementById('lr4-go').addEventListener('click', () => {
lr4Anim.t = 0; lr4Anim.running = true;
if(lr4Anim.raf) cancelAnimationFrame(lr4Anim.raf);
lr4Loop();
});
drawTrack();
wireQuiz('lr4-q-host');
wireSubmit('lr4');
renderMath(body);
}
/* ========================================================== */
/* ЛР-5 — Измерение плотности вещества */
/* ========================================================== */
function build_lr5(){
const body = document.getElementById('lr5-body');
if(!body) return;
let h = '';
h += makeCard('goal', 'Цель',
'Измерить плотности трёх веществ и определить, что это за материалы, по таблице плотностей.');
h += makeCard('equip', 'Оборудование',
'<ul style="padding-left:20px;margin:5px 0"><li>Весы (электронные или рычажные)</li><li>Мензурка</li><li>3 образца разных материалов</li><li>Нитка для опускания</li></ul>');
h += makeCard('steps', 'Ход работы',
'<ol style="padding-left:20px;margin:6px 0">'
+ '<li>Измерь массу $m$ образца на весах.</li>'
+ '<li>Измерь его объём $V$ методом вытеснения (как в ЛР-3).</li>'
+ '<li>Вычисли $\\rho = m/V$.</li>'
+ '<li>Найди в таблице вещество с такой плотностью.</li>'
+ '</ol>');
/* Виджет */
h += wgWrap('lr5-w', 'СИМ', '3 образца — определи вещество', 'Выбери образец — увидь массу и объём, вычисли плотность.',
'<div style="display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap">'
+ [['s1','Образец 1',54,20],['s2','Образец 2',156,20],['s3','Образец 3',272,20]].map((o, i) =>
'<button class="lr5-obj" data-m="' + o[2] + '" data-v="' + o[3] + '" data-nm="' + o[1] + '" type="button" style="background:' + (i===0 ? ACCENT : '#fff') + ';color:' + (i===0 ? '#fff' : ACCENT) + ';border:2px solid ' + ACCENT + ';padding:7px 14px;border-radius:9px;cursor:pointer;font-weight:700;font-family:inherit;font-size:.86rem">' + o[1] + '</button>').join('')
+ '</div>'
+ '<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px;align-items:center;background:#f0f9ff;border-radius:9px;border:1px solid ' + ACCENT_SOFT + ';padding:14px">'
+ '<div style="text-align:center">'
+ '<div style="font-size:.84rem;color:#475569;margin-bottom:6px">Весы</div>'
+ '<div style="background:#fff;border:2px solid ' + ACCENT_D + ';border-radius:8px;padding:14px;font-family:JetBrains Mono,monospace;font-weight:800;font-size:1.4rem;color:' + ACCENT_D + '" id="lr5-m">54 г</div>'
+ '</div>'
+ '<div style="text-align:center">'
+ '<div style="font-size:.84rem;color:#475569;margin-bottom:6px">Мензурка</div>'
+ '<div style="background:#fff;border:2px solid ' + ACCENT_D + ';border-radius:8px;padding:14px;font-family:JetBrains Mono,monospace;font-weight:800;font-size:1.4rem;color:' + ACCENT_D + '" id="lr5-V">20 см³</div>'
+ '</div>'
+ '</div>'
+ '<div id="lr5-info" style="background:' + ACCENT_SOFT + ';border-radius:9px;padding:12px 14px;margin-top:10px;font-size:.96rem;line-height:1.7;text-align:center"></div>');
/* Таблица */
h += wgWrap('lr5-tbl', 'ТБЛ', 'Таблица плотностей (для сравнения)', '',
'<table style="width:100%;border-collapse:collapse;font-size:.9rem">'
+ '<tr style="background:' + ACCENT_SOFT + '"><th style="padding:6px 10px;text-align:left;border-bottom:2px solid ' + ACCENT + '">Вещество</th><th style="padding:6px 10px;text-align:right;border-bottom:2px solid ' + ACCENT + '">$\\rho$, г/см³</th></tr>'
+ [['Сосна',0.5],['Лёд',0.9],['Вода',1.0],['Алюминий',2.7],['Железо',7.8],['Медь',8.9],['Свинец',11.3],['Ртуть',13.6],['Золото',19.3]].map(r =>
'<tr><td style="padding:5px 10px;border-bottom:1px solid ' + ACCENT_SOFT + '">' + r[0] + '</td><td style="padding:5px 10px;text-align:right;border-bottom:1px solid ' + ACCENT_SOFT + ';font-family:JetBrains Mono,monospace;font-weight:700">' + r[1] + '</td></tr>').join('')
+ '</table>');
h += wgWrap('lr5-q', 'ВОПР', 'Контрольные вопросы', '',
'<div id="lr5-q-host">'
+ quizQuestion('lr5-q', 0, 'Образец 1 имеет $m = 54$ г, $V = 20$ см³. $\\rho$?', ['1,7 г/см³','2,7 г/см³','3,7 г/см³','5,4 г/см³'], 1, '$54/20 = 2{,}7$ г/см³ — алюминий.')
+ quizQuestion('lr5-q', 1, 'Образец 2: $m = 156$ г, $V = 20$ см³. Что это?', ['Лёд','Алюминий','Железо','Свинец'], 2, '$156/20 = 7{,}8$ г/см³ — железо.')
+ quizQuestion('lr5-q', 2, 'Какое измерение здесь косвенное?', ['Массы','Объёма','Плотности','Времени'], 2, 'Плотность вычисляется через прямые $m$ и $V$.')
+ '</div>');
h += makeCard('concl', 'Вывод',
'Зная массу и объём тела, можно вычислить плотность вещества и определить, что это за материал. '
+ 'Измерение плотности — пример <b>косвенного</b> измерения через два прямых ($m$ и $V$). '
+ 'Таблицы плотностей позволяют идентифицировать неизвестные вещества.');
h += submitBtn('lr5');
body.innerHTML = h;
function matName(rho){
if(rho < 0.6) return 'дерево (сосна)';
if(rho < 0.95) return 'лёд';
if(rho < 1.1) return 'вода';
if(rho < 3) return 'алюминий';
if(rho < 9) return 'железо / медь';
if(rho < 14) return 'свинец / ртуть';
return 'золото / платина';
}
function upd5(m, V, nm){
document.getElementById('lr5-m').textContent = m + ' г';
document.getElementById('lr5-V').textContent = V + ' см³';
const rho = m / V;
document.getElementById('lr5-info').innerHTML = '<b>' + nm + ':</b> $\\rho = m/V = ' + m + '/' + V + ' = $ <b style="color:' + ACCENT_D + '">' + rho.toFixed(2) + '</b> г/см³.<br>'
+ '<span style="color:#475569;font-size:.88rem">Похоже на: <b>' + matName(rho) + '</b></span>';
renderMath(document.getElementById('lr5-info'));
}
body.querySelectorAll('.lr5-obj').forEach(btn => btn.addEventListener('click', () => {
body.querySelectorAll('.lr5-obj').forEach(b => { b.style.background = '#fff'; b.style.color = ACCENT; });
btn.style.background = ACCENT; btn.style.color = '#fff';
upd5(+btn.dataset.m, +btn.dataset.v, btn.dataset.nm);
}));
upd5(54, 20, 'Образец 1');
wireQuiz('lr5-q-host');
wireSubmit('lr5');
renderMath(body);
}
/* ========================================================== */
/* ЛР-6 — Изучение силы трения */
/* ========================================================== */
function build_lr6(){
const body = document.getElementById('lr6-body');
if(!body) return;
let h = '';
h += makeCard('goal', 'Цель',
'Измерить силу трения скольжения для разных поверхностей и убедиться, что $F_{тр} \\sim N$ (зависит от нормальной реакции).');
h += makeCard('equip', 'Оборудование',
'<ul style="padding-left:20px;margin:5px 0"><li>Деревянный брусок</li><li>Грузы по 100 г</li><li>Динамометр</li><li>Разные поверхности (дерево, пластик, резина)</li></ul>');
h += makeCard('steps', 'Ход работы',
'<ol style="padding-left:20px;margin:6px 0">'
+ '<li>Положи брусок на поверхность. Прицепи к нему динамометр.</li>'
+ '<li>Тяни <b>равномерно</b> и измерь $F_{тр}$ — это сила, которую показал динамометр (=сила тяги при равномерном движении).</li>'
+ '<li>Добавляй грузы по 100 г и снова измеряй $F_{тр}$.</li>'
+ '<li>Поменяй поверхность — повтори.</li>'
+ '</ol>');
/* Виджет */
h += wgWrap('lr6-w', 'СИМ', 'Брусок с динамометром', 'Меняй массу и поверхность — увидь силу трения.',
'<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:8px;margin-bottom:10px">'
+ '<label style="display:block;font-size:.86rem;color:#475569;background:#fff;padding:7px 11px;border-radius:8px;border:1px solid ' + ACCENT_SOFT + '">Масса (брусок + грузы), г: <b id="lr6-m" style="color:' + ACCENT_D + ';font-family:JetBrains Mono,monospace">100</b><input type="range" id="lr6-m-r" min="100" max="500" step="100" value="100" style="display:block;width:100%;margin-top:5px;accent-color:' + ACCENT + '"></label>'
+ '<label style="display:block;font-size:.86rem;color:#475569;background:#fff;padding:7px 11px;border-radius:8px;border:1px solid ' + ACCENT_SOFT + '">Поверхность:<select id="lr6-surf" style="width:100%;margin-top:5px;padding:6px;border-radius:6px;border:1px solid ' + ACCENT_SOFT + ';font-family:inherit"><option value="0.3" selected>Дерево по дереву (μ=0,3)</option><option value="0.2">Дерево по пластику (μ=0,2)</option><option value="0.5">Дерево по резине (μ=0,5)</option><option value="0.04">Дерево по льду (μ=0,04)</option></select></label>'
+ '</div>'
+ '<svg id="lr6-svg" viewBox="0 0 380 180" width="100%" style="max-width:600px;display:block;margin:0 auto;background:#f0f9ff;border-radius:9px;border:1px solid ' + ACCENT_SOFT + '"></svg>'
+ '<div id="lr6-info" style="background:' + ACCENT_SOFT + ';border-radius:9px;padding:10px 14px;margin-top:8px;font-size:.94rem;text-align:center"></div>');
h += wgWrap('lr6-tbl', 'ТБЛ', 'Таблица измерений (μ=0,3, дерево по дереву)',
'',
'<table style="width:100%;border-collapse:collapse;font-size:.9rem">'
+ '<tr style="background:' + ACCENT_SOFT + '"><th style="padding:6px 10px;text-align:left;border-bottom:2px solid ' + ACCENT + '">Грузы</th><th style="padding:6px 10px;text-align:right;border-bottom:2px solid ' + ACCENT + '">$m$, кг</th><th style="padding:6px 10px;text-align:right;border-bottom:2px solid ' + ACCENT + '">$N = mg$, Н</th><th style="padding:6px 10px;text-align:right;border-bottom:2px solid ' + ACCENT + '">$F_{тр}$, Н</th></tr>'
+ [['0 (брусок)',0.1,1.0,0.3],['+1 груз',0.2,2.0,0.6],['+2 груза',0.3,3.0,0.9],['+3 груза',0.4,4.0,1.2],['+4 груза',0.5,5.0,1.5]].map(r =>
'<tr><td style="padding:5px 10px;border-bottom:1px solid ' + ACCENT_SOFT + '">' + r[0] + '</td><td style="padding:5px 10px;text-align:right;border-bottom:1px solid ' + ACCENT_SOFT + ';font-family:JetBrains Mono,monospace">' + r[1] + '</td><td style="padding:5px 10px;text-align:right;border-bottom:1px solid ' + ACCENT_SOFT + ';font-family:JetBrains Mono,monospace">' + r[2] + '</td><td style="padding:5px 10px;text-align:right;border-bottom:1px solid ' + ACCENT_SOFT + ';font-family:JetBrains Mono,monospace;font-weight:700;color:' + ACCENT_D + '">' + r[3] + '</td></tr>').join('')
+ '</table>'
+ '<p style="margin-top:8px;font-size:.86rem;color:#475569">Видно, что $F_{тр}$ растёт <b>пропорционально</b> массе — то есть пропорционально $N$. Это и есть закон $F_{тр} \\sim N$.</p>');
h += wgWrap('lr6-q', 'ВОПР', 'Контрольные вопросы', '',
'<div id="lr6-q-host">'
+ quizQuestion('lr6-q', 0, 'Если массу удвоить, $F_{тр}$ скольжения…', ['Не изменится','Удвоится','Уменьшится в 2 раза','Останется','Станет 0'], 1)
+ quizQuestion('lr6-q', 1, 'Какая поверхность дает наименьшее трение?', ['Дерево','Резина','Лёд','Пластик'], 2, 'У льда $\\mu \\approx 0{,}04$ — поэтому скользко.')
+ quizQuestion('lr6-q', 2, 'Брусок $m = 0{,}5$ кг, $\\mu = 0{,}3$, $g = 10$. $F_{тр}$?', ['0,5 Н','1 Н','1,5 Н','3 Н'], 2, '$F_{тр} = \\mu mg = 0{,}3 \\cdot 0{,}5 \\cdot 10 = 1{,}5$ Н.')
+ '</div>');
h += makeCard('concl', 'Вывод',
'Эксперимент подтвердил, что сила трения скольжения <b>прямо пропорциональна</b> нормальной реакции (а значит — массе тела). '
+ 'Коэффициент трения $\\mu$ зависит от пары поверхностей: для дерево-лёд он очень мал ($\\sim 0{,}04$), '
+ 'для дерево-резина — большой ($\\sim 0{,}5$). Поэтому шипованная резина не скользит по асфальту и снегу.');
h += submitBtn('lr6');
body.innerHTML = h;
function draw6(){
const m = +document.getElementById('lr6-m-r').value;
const mu = +document.getElementById('lr6-surf').value;
document.getElementById('lr6-m').textContent = m;
const N = m / 1000 * 10; // m в г → кг → N
const Ftr = mu * N;
const W = 380, H = 180, baseY = 130;
let s = '';
s += '<rect x="0" y="' + baseY + '" width="' + W + '" height="50" fill="#94a3b8"/>';
for(let i = 0; i < 18; i++) s += '<line x1="' + (i*22+5) + '" y1="' + baseY + '" x2="' + (i*22+15) + '" y2="' + (baseY+8) + '" stroke="#374151" stroke-width="0.8"/>';
const bx = 80, bw = 60, bh = 40;
// Грузы наверху
const numGr = Math.max(0, (m - 100) / 100);
for(let i = 0; i < numGr; i++){
s += '<rect x="' + (bx + 12) + '" y="' + (baseY - bh - 12 - i*10) + '" width="36" height="10" fill="#475569" stroke="#1f2937" stroke-width="0.8" rx="2"/>';
}
// Брусок
s += '<rect x="' + bx + '" y="' + (baseY - bh) + '" width="' + bw + '" height="' + bh + '" fill="' + ACCENT + '" stroke="' + ACCENT_D + '" stroke-width="1.5" rx="3"/>';
s += '<text x="' + (bx + bw/2) + '" y="' + (baseY - bh/2 + 5) + '" text-anchor="middle" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="#fff">' + m + ' г</text>';
// Динамометр + нить
const dynX = bx + bw + 20;
s += '<line x1="' + (bx + bw) + '" y1="' + (baseY - bh/2) + '" x2="' + dynX + '" y2="' + (baseY - bh/2) + '" stroke="#0f172a" stroke-width="1.5"/>';
s += '<rect x="' + dynX + '" y="' + (baseY - bh/2 - 18) + '" width="60" height="36" fill="#fef3c7" stroke="#92400e" stroke-width="1.5" rx="4"/>';
s += '<text x="' + (dynX + 30) + '" y="' + (baseY - bh/2 + 3) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="13" font-weight="800" fill="#92400e">' + Ftr.toFixed(2) + ' Н</text>';
s += '<text x="' + (dynX + 30) + '" y="' + (baseY - bh/2 - 22) + '" text-anchor="middle" font-family="Inter,sans-serif" font-size="9" fill="#92400e" font-weight="700">динамометр</text>';
// Стрелка тяги
s += '<line x1="' + (dynX + 60) + '" y1="' + (baseY - bh/2) + '" x2="' + (dynX + 100) + '" y2="' + (baseY - bh/2) + '" stroke="#10b981" stroke-width="2.5"/>';
s += '<polygon points="' + (dynX + 100) + ',' + (baseY - bh/2) + ' ' + (dynX + 92) + ',' + (baseY - bh/2 - 5) + ' ' + (dynX + 92) + ',' + (baseY - bh/2 + 5) + '" fill="#10b981"/>';
// Стрелка трения
s += '<line x1="' + bx + '" y1="' + (baseY - 5) + '" x2="' + (bx - 40) + '" y2="' + (baseY - 5) + '" stroke="#92400e" stroke-width="2.5"/>';
s += '<polygon points="' + (bx - 40) + ',' + (baseY - 5) + ' ' + (bx - 32) + ',' + (baseY - 10) + ' ' + (bx - 32) + ',' + baseY + '" fill="#92400e"/>';
s += '<text x="' + (bx - 42) + '" y="' + (baseY - 12) + '" text-anchor="end" font-family="Inter,sans-serif" font-size="10" font-weight="700" fill="#92400e">F_тр</text>';
document.getElementById('lr6-svg').innerHTML = s;
document.getElementById('lr6-info').innerHTML = '$m = ' + (m/1000).toFixed(2) + '$ кг, $N = mg = ' + N.toFixed(1) + '$ Н, $\\mu = ' + mu + '$ &nbsp;→&nbsp; <b>$F_{тр} = \\mu N = ' + Ftr.toFixed(2) + '$ Н</b>';
renderMath(document.getElementById('lr6-info'));
}
document.getElementById('lr6-m-r').addEventListener('input', draw6);
document.getElementById('lr6-surf').addEventListener('change', draw6);
draw6();
wireQuiz('lr6-q-host');
wireSubmit('lr6');
renderMath(body);
}
window.PHYS7_LAB_WIDGETS = {
lr1: build_lr1,
lr2: build_lr2,
lr3: build_lr3,
lr4: build_lr4,
lr5: build_lr5,
lr6: build_lr6
};
})();