Files
Learn_System/backend/scripts/redesign_p8_lab.cjs
Maxim Dolgolyov 382dff3879 feat(phys8 lab): Phase 4 — Лабораторный практикум (визуал + 7 IV-6)
Hero: emerald-зелёный градиент (стиль 'химической лаборатории'),
flask SVG-watermark, live meter '7/7 ЛР'.

7 section watermarks: термометр, печь, цепь, посл/парал, P, угол.

7 IV-6 интерактивов:
ЛР1 Теплообмен: 2 ёмкости (0.5 кг + 1 кг), scrubbers T₁/T₂,
кнопка 'Смешать' с tween-анимацией, формула баланса.
ЛР2 Удельная теплоёмкость: scrubbers P/m/t, нагреватель,
термометр с цветовой картой, c=Q/(mΔT) для воды (4200).
ЛР3 Простейшая цепь: батарея+амперметр+лампа+вольтметр,
scrubber U, live показания приборов.
ЛР4 Последовательное соединение: U=U₁+U₂, I одинаков.
ЛР5 Параллельное соединение: U одинаков, I=I₁+I₂.
ЛР6 Работа и мощность: U·I·t, лампа brightness ∝ P,
лучи при P>100 Вт.
ЛР7 Закон отражения: луч + нормаль + угловые дуги,
verdict 'α=β'.
2026-05-30 10:29:50 +03:00

385 lines
33 KiB
JavaScript
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.
// Phase 4 — Lab redesign: hero + section watermarks + 7 IV-6 sandboxes.
'use strict';
const fs = require('fs');
const path = require('path');
const DST = path.join(__dirname, '..', '..', 'frontend', 'textbooks', 'physics_8_lab.html');
let h = fs.readFileSync(DST, 'utf8');
// === 1. Hero replacement ===
const FLASK_WM = `<svg viewBox="0 0 100 100" aria-hidden="true">
<path d="M40 12 L40 38 L18 80 C 15 88, 20 92, 26 92 L74 92 C 80 92, 85 88, 82 80 L60 38 L60 12 Z M 35 12 L65 12" stroke="currentColor" stroke-width="4" fill="none"/>
<circle cx="38" cy="64" r="3" fill="currentColor"/>
<circle cx="55" cy="76" r="3" fill="currentColor"/>
<circle cx="44" cy="80" r="2" fill="currentColor"/>
</svg>`;
const NEW_HERO = `<header class="p8-hero" style="background:linear-gradient(115deg,#064e3b 0%,#10b981 60%,#6ee7b7 100%)">
<div class="p8-hero-wm">${FLASK_WM}</div>
<div class="p8-hero-meter" id="p8-meter-lab"><span id="p8-meter-val">7</span>/7 ЛР</div>
<div class="p8-hero-inner">
<div class="p8-hero-eyebrow">Лабораторный практикум · 7 ЛР</div>
<h1 class="p8-hero-title">Виртуальная лаборатория</h1>
<div class="p8-hero-sub">Перетаскивайте термометры, нагреватели, динамометры и измерительные приборы. Собирайте установки и записывайте результаты.</div>
<div class="hdr-side" style="margin-top:18px;display:flex;gap:8px;flex-wrap:wrap;position:relative;z-index:1">
<a href="/textbook/physics-8" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> К физике 8</a>
<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>`;
const oldHdrRegex = /<header class="hdr">[\s\S]*?<\/header>/;
if (h.match(oldHdrRegex)) {
h = h.replace(oldHdrRegex, NEW_HERO);
console.log('Hero replaced');
}
// === 2. Section watermarks ===
const SEC_SYMBOLS = {
lr1: '<svg viewBox="0 0 100 100"><rect x="30" y="20" width="40" height="60" fill="none" stroke="currentColor" stroke-width="4" rx="4"/><rect x="36" y="40" width="28" height="35" fill="currentColor" opacity="0.6"/></svg>',
lr2: '<svg viewBox="0 0 100 100"><rect x="20" y="40" width="60" height="40" fill="none" stroke="currentColor" stroke-width="3"/><circle cx="50" cy="60" r="10" fill="currentColor"/></svg>',
lr3: '<svg viewBox="0 0 100 100"><rect x="20" y="40" width="60" height="30" fill="none" stroke="currentColor" stroke-width="3"/><circle cx="50" cy="55" r="8" fill="none" stroke="currentColor" stroke-width="3"/></svg>',
lr4: '<svg viewBox="0 0 100 100"><path d="M20 50 L30 50 L35 40 L45 60 L55 40 L65 60 L70 50 L80 50" stroke="currentColor" stroke-width="3" fill="none"/></svg>',
lr5: '<svg viewBox="0 0 100 100"><path d="M20 30 L80 30 M20 70 L80 70 M50 30 L50 70" stroke="currentColor" stroke-width="3" fill="none"/></svg>',
lr6: '<svg viewBox="0 0 100 100"><text x="50" y="60" font-family="Unbounded" font-size="32" font-weight="900" fill="currentColor" text-anchor="middle">P</text></svg>',
lr7: '<svg viewBox="0 0 100 100"><line x1="20" y1="20" x2="50" y2="50" stroke="currentColor" stroke-width="4"/><line x1="50" y1="50" x2="80" y2="20" stroke="currentColor" stroke-width="4"/></svg>'
};
let secWmInjected = 0;
for (const pid of Object.keys(SEC_SYMBOLS)) {
const symbol = SEC_SYMBOLS[pid];
const secOpenRegex = new RegExp(`(<section[^>]+id="sec-${pid}"[^>]*>)`);
if (h.match(secOpenRegex) && !h.includes(`p8-sec-wm-${pid}`)) {
const wmDiv = `<div class="p8-sec-wm" id="p8-sec-wm-${pid}" aria-hidden="true">${symbol}</div>`;
h = h.replace(secOpenRegex, '$1\n ' + wmDiv);
secWmInjected++;
}
}
console.log('Section watermarks:', secWmInjected);
// === 3. Inject IV-6 widgets into each lr-builder ===
function injectIV6(lrid, title, helpHtml, body, init) {
const widgetHtml = `
/* IV6 — ${title} (Phase 4) */
h += '<div class="wg p8-iv6">'
+'<div class="wg-header"><span class="wg-badge p8-badge" style="background:#d1fae5;color:#047857">IV-6</span><div class="wg-title">${title}</div></div>'
+'<div class="wg-help">${helpHtml}</div>'
+'<div class="p8-sandbox" id="${lrid}-iv6-sandbox" style="height:260px"></div>'
${body}
+'</div>';
`;
const initFn = `
function _init${lrid.toUpperCase()}_iv6(){
const sb = document.getElementById('${lrid}-iv6-sandbox');
if (!sb || !window.P8Helpers) return;
${init}
}
`;
const marker = `box.innerHTML = h + secNavFor('${lrid}') + readButton('${lrid}');`;
if (!h.includes(marker)) { console.warn(lrid+': no marker'); return; }
if (h.includes(`${lrid}-iv6-sandbox`)) { console.log(lrid+': already injected'); return; }
const eol = (h.indexOf('\r\n') >= 0) ? '\r\n' : '\n';
const indented = widgetHtml.trim().replace(/\n/g, eol);
h = h.replace(marker, indented + eol + eol + ' ' + marker);
h = h.replace(`wireReadBtn('${lrid}');`, `wireReadBtn('${lrid}');${eol} _init${lrid.toUpperCase()}_iv6();`);
const fnStart = h.indexOf(`function build_${lrid}()`);
const fnEnd = h.indexOf('\n}\n', fnStart);
h = h.slice(0, fnEnd + 3) + eol + initFn.trim() + eol + h.slice(fnEnd + 3);
console.log(lrid+': injected IV-6');
}
// ============================================================
// ЛР1 — Теплообмен (drag термометр + смешать жидкости)
// ============================================================
injectIV6('lr1', 'Теплообмен — смешивание жидкостей',
'Задавай начальные T₁ и T₂ скрубберами. Кнопка «Смешать» — итоговая T рассчитывается через тепловой баланс.',
'+\'<div style="margin-top:10px;display:grid;grid-template-columns:1fr 1fr;gap:8px"><div class="p8-scrubber"><span class="p8-scrubber-label">T₁ (0.5 кг)</span><input type="range" id="lr1-t1" min="0" max="100" step="1" value="80"><span class="p8-scrubber-value"><span id="lr1-t1-val">80</span><span class="p8-unit">°C</span></span></div><div class="p8-scrubber"><span class="p8-scrubber-label">T₂ (1 кг)</span><input type="range" id="lr1-t2" min="0" max="100" step="1" value="20"><span class="p8-scrubber-value"><span id="lr1-t2-val">20</span><span class="p8-unit">°C</span></span></div></div>\'+\'<div style="margin-top:8px;display:flex;gap:10px;flex-wrap:wrap"><button class="btn primary" id="lr1-mix">Смешать</button><div class="p8-readout"><span class="p8-readout-label">T_итог</span><span class="p8-readout-value" id="lr1-tf">—</span><span class="p8-readout-unit">°C</span></div></div>\'',
`
const svg = P8Helpers.svg.create(560, 260);
svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block';
sb.appendChild(svg);
let T1=80, T2=20, mixed=false, Tf=50;
function vessel(x, y, T, m){
const g = P8Helpers.svg.el('g', { transform: 'translate('+x+','+y+')' });
const ht = 40 + m*50;
g.appendChild(P8Helpers.svg.el('rect', { x:-32, y:-ht, width:64, height:ht, rx:5, fill:'rgba(255,255,255,.7)', stroke:'#0f172a', 'stroke-width':2 }));
g.appendChild(P8Helpers.svg.el('rect', { x:-29, y:-ht+3, width:58, height:ht-5, rx:3, fill: P8Helpers.thermal.tempColor(T/100) }));
g.appendChild(P8Helpers.svg.el('text', { x:0, y:18, 'font-family':"'JetBrains Mono',monospace", 'font-size':12, 'font-weight':700, fill:'#0f172a', 'text-anchor':'middle', text: 'T='+Math.round(T)+'°C' }));
return g;
}
function render(){
svg.innerHTML='';
if (!mixed){
svg.appendChild(vessel(160, 180, T1, 0.5));
svg.appendChild(vessel(400, 180, T2, 1));
svg.appendChild(P8Helpers.svg.el('text', { x:160, y:235, 'font-family':"'Inter',sans-serif", 'font-size':11, fill:'var(--p8-muted,#64748b)', 'text-anchor':'middle', text:'m₁=0.5 кг' }));
svg.appendChild(P8Helpers.svg.el('text', { x:400, y:235, 'font-family':"'Inter',sans-serif", 'font-size':11, fill:'var(--p8-muted,#64748b)', 'text-anchor':'middle', text:'m₂=1 кг' }));
} else {
svg.appendChild(vessel(280, 180, Tf, 1.5));
svg.appendChild(P8Helpers.svg.el('text', { x:280, y:70, 'font-family':"'Unbounded',sans-serif", 'font-size':18, 'font-weight':900, fill:'#10b981', 'text-anchor':'middle', text: 'T_итог = '+Math.round(Tf)+' °C' }));
}
}
document.getElementById('lr1-t1').oninput = ev => { T1 = +ev.target.value; document.getElementById('lr1-t1-val').textContent = T1; mixed = false; document.getElementById('lr1-tf').textContent='—'; render(); };
document.getElementById('lr1-t2').oninput = ev => { T2 = +ev.target.value; document.getElementById('lr1-t2-val').textContent = T2; mixed = false; document.getElementById('lr1-tf').textContent='—'; render(); };
document.getElementById('lr1-mix').onclick = () => {
Tf = (0.5*T1 + 1*T2)/(0.5+1);
mixed = true;
if (window.P8Anim) P8Anim.tween({ from: T1, to: Tf, duration: 1200, easing: 'cubicInOut', onUpdate: t => { Tf = t; render(); document.getElementById('lr1-tf').textContent = Math.round(t); } });
else { render(); document.getElementById('lr1-tf').textContent = Math.round(Tf); }
if (window.addXp) addXp(15, 'lr1-iv6');
};
render();
`);
// ============================================================
// ЛР2 — Удельная теплоёмкость (нагреватель + ёмкость)
// ============================================================
injectIV6('lr2', 'Измерение удельной теплоёмкости',
'Нагреватель мощности P подаёт Q=Pt в массу m. Из ΔT находим $c = Q/(m\\\\Delta T)$.',
'+\'<div style="margin-top:10px;display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px"><div class="p8-scrubber"><span class="p8-scrubber-label">P</span><input type="range" id="lr2-p" min="50" max="1000" step="10" value="500"><span class="p8-scrubber-value"><span id="lr2-p-val">500</span><span class="p8-unit">Вт</span></span></div><div class="p8-scrubber"><span class="p8-scrubber-label">m</span><input type="range" id="lr2-m" min="0.1" max="2" step="0.1" value="0.5"><span class="p8-scrubber-value"><span id="lr2-m-val">0.5</span><span class="p8-unit">кг</span></span></div><div class="p8-scrubber"><span class="p8-scrubber-label">t</span><input type="range" id="lr2-t" min="10" max="600" step="5" value="120"><span class="p8-scrubber-value"><span id="lr2-t-val">120</span><span class="p8-unit">с</span></span></div></div>\'+\'<div style="margin-top:8px;display:flex;gap:10px;flex-wrap:wrap"><div class="p8-readout"><span class="p8-readout-label">Q</span><span class="p8-readout-value" id="lr2-q">60</span><span class="p8-readout-unit">кДж</span></div><div class="p8-readout"><span class="p8-readout-label">ΔT</span><span class="p8-readout-value" id="lr2-dt">29</span><span class="p8-readout-unit">К</span></div><div class="p8-readout"><span class="p8-readout-label">c</span><span class="p8-readout-value" id="lr2-c">4200</span><span class="p8-readout-unit">Дж/(кг·К)</span></div></div>\'',
`
const svg = P8Helpers.svg.create(560, 260);
svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block';
sb.appendChild(svg);
let P=500, m=0.5, t=120;
const c_const = 4200; /* предположим вода */
function render(){
svg.innerHTML='';
const Q = P*t;
const dT = Q/(c_const*m);
/* Vessel */
const ht = 50+m*60;
svg.appendChild(P8Helpers.svg.el('rect', { x: 200, y: 200-ht, width: 160, height: ht, rx: 5, fill: P8Helpers.thermal.tempColor(Math.min(1, (20+dT)/120)), stroke: '#0f172a', 'stroke-width': 2, opacity: 0.85 }));
svg.appendChild(P8Helpers.svg.el('text', { x: 280, y: 200-ht+22, 'font-family':"'JetBrains Mono',monospace", 'font-size':12, 'font-weight':700, fill:'#fff', 'text-anchor':'middle', text: 'm='+m+' кг воды' }));
/* Heater */
svg.appendChild(P8Helpers.svg.el('rect', { x: 240, y: 205, width: 80, height: 14, fill: '#dc2626', rx: 3 }));
svg.appendChild(P8Helpers.svg.el('text', { x: 280, y: 235, 'font-family':"'Inter',sans-serif", 'font-size':11, 'font-weight':700, fill:'#dc2626', 'text-anchor':'middle', text: 'нагреватель P='+P+' Вт' }));
/* Thermometer */
svg.appendChild(P8Helpers.thermal.thermometerSVG(120, 60, 110, Math.min(1, (20+dT)/120)));
svg.appendChild(P8Helpers.svg.el('text', { x: 120, y: 50, 'font-family':"'JetBrains Mono',monospace", 'font-size':11, 'font-weight':800, fill:'#0f172a', 'text-anchor':'middle', text: 'T='+Math.round(20+dT)+'°C' }));
/* Updates */
document.getElementById('lr2-q').textContent = (Q/1000).toFixed(1);
document.getElementById('lr2-dt').textContent = Math.round(dT);
document.getElementById('lr2-c').textContent = c_const;
}
document.getElementById('lr2-p').oninput = ev => { P = +ev.target.value; document.getElementById('lr2-p-val').textContent = P; render(); };
document.getElementById('lr2-m').oninput = ev => { m = +ev.target.value; document.getElementById('lr2-m-val').textContent = m.toFixed(1); render(); };
document.getElementById('lr2-t').oninput = ev => { t = +ev.target.value; document.getElementById('lr2-t-val').textContent = t; render(); };
render();
`);
// ============================================================
// ЛР3 — Простейшая цепь (батарея + лампа + А + V)
// ============================================================
injectIV6('lr3', 'Сборка простейшей цепи',
'Цепь: батарея → амперметр → лампа → вольтметр (параллельно). Двигай U — показания приборов обновляются.',
'+\'<div class="p8-scrubber" style="margin-top:10px"><span class="p8-scrubber-label">U батареи</span><input type="range" id="lr3-u" min="1" max="12" step="0.1" value="6"><span class="p8-scrubber-value"><span id="lr3-u-val">6.0</span><span class="p8-unit">В</span></span></div>\'+\'<div style="margin-top:8px;display:flex;gap:10px;flex-wrap:wrap"><div class="p8-readout"><span class="p8-readout-label">A показывает</span><span class="p8-readout-value" id="lr3-a">0.50</span><span class="p8-readout-unit">А</span></div><div class="p8-readout"><span class="p8-readout-label">V показывает</span><span class="p8-readout-value" id="lr3-v">6.0</span><span class="p8-readout-unit">В</span></div></div>\'',
`
const svg = P8Helpers.svg.create(560, 260);
svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block';
sb.appendChild(svg);
let U=6;
const R=12;
function render(){
svg.innerHTML='';
const I = U/R;
/* Battery left */
svg.appendChild(P8Helpers.em.circuitComponent('battery', 80, 120, 'h', U.toFixed(1)+' В'));
/* Ammeter */
svg.appendChild(P8Helpers.em.circuitComponent('ammeter', 220, 120, 'h'));
svg.appendChild(P8Helpers.svg.el('text', { x: 220, y: 100, 'font-family':"'JetBrains Mono',monospace", 'font-size':10, 'font-weight':800, fill:'#dc2626', 'text-anchor':'middle', text: I.toFixed(2)+' А' }));
/* Lamp */
const lampG = P8Helpers.svg.el('g', { transform: 'translate(380, 120)' });
const br = Math.min(1, I/1.2);
lampG.appendChild(P8Helpers.svg.el('circle', { cx: 0, cy: 0, r: 26, fill: '#fef3c7', opacity: br*0.6+0.1 }));
lampG.appendChild(P8Helpers.svg.el('circle', { cx: 0, cy: 0, r: 16, fill: '#fef3c7', stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(lampG);
/* Voltmeter (parallel above lamp) */
svg.appendChild(P8Helpers.svg.el('line', { x1: 380, y1: 90, x2: 380, y2: 50, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 380, y1: 50, x2: 480, y2: 50, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.em.circuitComponent('voltmeter', 480, 50, 'h'));
svg.appendChild(P8Helpers.svg.el('line', { x1: 480, y1: 50, x2: 480, y2: 120, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('text', { x: 480, y: 30, 'font-family':"'JetBrains Mono',monospace", 'font-size':10, 'font-weight':800, fill:'#2563eb', 'text-anchor':'middle', text: U.toFixed(1)+' В' }));
/* Wires */
svg.appendChild(P8Helpers.svg.el('line', { x1: 110, y1: 120, x2: 190, y2: 120, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 250, y1: 120, x2: 354, y2: 120, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 380, y1: 136, x2: 510, y2: 136, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 510, y1: 136, x2: 510, y2: 190, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 50, y1: 120, x2: 50, y2: 190, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 50, y1: 190, x2: 510, y2: 190, stroke: '#0f172a', 'stroke-width': 2 }));
/* Updates */
document.getElementById('lr3-a').textContent = I.toFixed(2);
document.getElementById('lr3-v').textContent = U.toFixed(1);
}
document.getElementById('lr3-u').oninput = ev => { U = +ev.target.value; document.getElementById('lr3-u-val').textContent = U.toFixed(1); render(); };
render();
`);
// ============================================================
// ЛР4 — Последовательное соединение
// ============================================================
injectIV6('lr4', 'Последовательное соединение проводников',
'Двигай R₁, R₂. Проверь: ток одинаков везде; напряжения складываются U = U₁ + U₂.',
'+\'<div style="margin-top:10px;display:grid;grid-template-columns:1fr 1fr;gap:8px"><div class="p8-scrubber"><span class="p8-scrubber-label">R₁</span><input type="range" id="lr4-r1" min="1" max="50" step="1" value="10"><span class="p8-scrubber-value"><span id="lr4-r1-val">10</span><span class="p8-unit">Ом</span></span></div><div class="p8-scrubber"><span class="p8-scrubber-label">R₂</span><input type="range" id="lr4-r2" min="1" max="50" step="1" value="20"><span class="p8-scrubber-value"><span id="lr4-r2-val">20</span><span class="p8-unit">Ом</span></span></div></div>\'+\'<div style="margin-top:8px;display:flex;gap:10px;flex-wrap:wrap"><div class="p8-readout"><span class="p8-readout-label">U₁</span><span class="p8-readout-value" id="lr4-u1">4</span><span class="p8-readout-unit">В</span></div><div class="p8-readout"><span class="p8-readout-label">U₂</span><span class="p8-readout-value" id="lr4-u2">8</span><span class="p8-readout-unit">В</span></div><div class="p8-readout"><span class="p8-readout-label">I</span><span class="p8-readout-value" id="lr4-i">0.4</span><span class="p8-readout-unit">А</span></div></div>\'',
`
const svg = P8Helpers.svg.create(560, 260);
svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block';
sb.appendChild(svg);
const U = 12;
let R1=10, R2=20;
function render(){
svg.innerHTML='';
const R = R1+R2, I = U/R, U1 = I*R1, U2 = I*R2;
/* Battery */
svg.appendChild(P8Helpers.em.circuitComponent('battery', 80, 130, 'h', U+' В'));
/* R1 */
svg.appendChild(P8Helpers.em.circuitComponent('resistor', 230, 130, 'h', R1+' Ом'));
svg.appendChild(P8Helpers.svg.el('text', { x: 230, y: 110, 'font-family':"'JetBrains Mono',monospace", 'font-size':10, 'font-weight':800, fill:'#dc2626', 'text-anchor':'middle', text: 'U₁='+U1.toFixed(1)+' В' }));
/* R2 */
svg.appendChild(P8Helpers.em.circuitComponent('resistor', 400, 130, 'h', R2+' Ом'));
svg.appendChild(P8Helpers.svg.el('text', { x: 400, y: 110, 'font-family':"'JetBrains Mono',monospace", 'font-size':10, 'font-weight':800, fill:'#dc2626', 'text-anchor':'middle', text: 'U₂='+U2.toFixed(1)+' В' }));
/* Wires */
svg.appendChild(P8Helpers.svg.el('line', { x1: 110, y1: 130, x2: 200, y2: 130, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 260, y1: 130, x2: 370, y2: 130, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 430, y1: 130, x2: 510, y2: 130, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 510, y1: 130, x2: 510, y2: 200, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 50, y1: 130, x2: 50, y2: 200, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 50, y1: 200, x2: 510, y2: 200, stroke: '#0f172a', 'stroke-width': 2 }));
/* I label */
svg.appendChild(P8Helpers.svg.el('text', { x: 280, y: 220, 'font-family':"'JetBrains Mono',monospace", 'font-size':12, 'font-weight':800, fill:'#10b981', 'text-anchor':'middle', text: 'I = '+I.toFixed(3)+' А (одинаков везде)' }));
/* Updates */
document.getElementById('lr4-u1').textContent = U1.toFixed(1);
document.getElementById('lr4-u2').textContent = U2.toFixed(1);
document.getElementById('lr4-i').textContent = I.toFixed(3);
}
document.getElementById('lr4-r1').oninput = ev => { R1 = +ev.target.value; document.getElementById('lr4-r1-val').textContent = R1; render(); };
document.getElementById('lr4-r2').oninput = ev => { R2 = +ev.target.value; document.getElementById('lr4-r2-val').textContent = R2; render(); };
render();
`);
// ============================================================
// ЛР5 — Параллельное соединение
// ============================================================
injectIV6('lr5', 'Параллельное соединение проводников',
'Двигай R₁, R₂. Проверь: напряжение одинаково на обоих, токи складываются I = I₁ + I₂.',
'+\'<div style="margin-top:10px;display:grid;grid-template-columns:1fr 1fr;gap:8px"><div class="p8-scrubber"><span class="p8-scrubber-label">R₁</span><input type="range" id="lr5-r1" min="1" max="50" step="1" value="20"><span class="p8-scrubber-value"><span id="lr5-r1-val">20</span><span class="p8-unit">Ом</span></span></div><div class="p8-scrubber"><span class="p8-scrubber-label">R₂</span><input type="range" id="lr5-r2" min="1" max="50" step="1" value="30"><span class="p8-scrubber-value"><span id="lr5-r2-val">30</span><span class="p8-unit">Ом</span></span></div></div>\'+\'<div style="margin-top:8px;display:flex;gap:10px;flex-wrap:wrap"><div class="p8-readout"><span class="p8-readout-label">I₁</span><span class="p8-readout-value" id="lr5-i1">0.6</span><span class="p8-readout-unit">А</span></div><div class="p8-readout"><span class="p8-readout-label">I₂</span><span class="p8-readout-value" id="lr5-i2">0.4</span><span class="p8-readout-unit">А</span></div><div class="p8-readout"><span class="p8-readout-label">I</span><span class="p8-readout-value" id="lr5-i">1.0</span><span class="p8-readout-unit">А</span></div></div>\'',
`
const svg = P8Helpers.svg.create(560, 260);
svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block';
sb.appendChild(svg);
const U = 12;
let R1=20, R2=30;
function render(){
svg.innerHTML='';
const I1=U/R1, I2=U/R2, I=I1+I2;
svg.appendChild(P8Helpers.em.circuitComponent('battery', 80, 130, 'h', U+' В'));
svg.appendChild(P8Helpers.svg.el('line', { x1: 110, y1: 130, x2: 200, y2: 130, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 200, y1: 80, x2: 200, y2: 180, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 380, y1: 80, x2: 380, y2: 180, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 200, y1: 80, x2: 290, y2: 80, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 320, y1: 80, x2: 380, y2: 80, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 200, y1: 180, x2: 290, y2: 180, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 320, y1: 180, x2: 380, y2: 180, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.em.circuitComponent('resistor', 305, 80, 'h', R1+' Ом'));
svg.appendChild(P8Helpers.em.circuitComponent('resistor', 305, 180, 'h', R2+' Ом'));
svg.appendChild(P8Helpers.svg.el('text', { x: 290, y: 68, 'font-family':"'JetBrains Mono',monospace", 'font-size':10, 'font-weight':800, fill:'#dc2626', 'text-anchor':'middle', text: 'I₁='+I1.toFixed(2)+' А' }));
svg.appendChild(P8Helpers.svg.el('text', { x: 290, y: 200, 'font-family':"'JetBrains Mono',monospace", 'font-size':10, 'font-weight':800, fill:'#dc2626', 'text-anchor':'middle', text: 'I₂='+I2.toFixed(2)+' А' }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 380, y1: 130, x2: 510, y2: 130, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 510, y1: 130, x2: 510, y2: 220, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 50, y1: 130, x2: 50, y2: 220, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 50, y1: 220, x2: 510, y2: 220, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('text', { x: 150, y: 250, 'font-family':"'JetBrains Mono',monospace", 'font-size':12, 'font-weight':800, fill:'#10b981', 'text-anchor':'middle', text: 'I='+I.toFixed(2)+' А' }));
document.getElementById('lr5-i1').textContent = I1.toFixed(2);
document.getElementById('lr5-i2').textContent = I2.toFixed(2);
document.getElementById('lr5-i').textContent = I.toFixed(2);
}
document.getElementById('lr5-r1').oninput = ev => { R1 = +ev.target.value; document.getElementById('lr5-r1-val').textContent = R1; render(); };
document.getElementById('lr5-r2').oninput = ev => { R2 = +ev.target.value; document.getElementById('lr5-r2-val').textContent = R2; render(); };
render();
`);
// ============================================================
// ЛР6 — Работа и мощность (P=UI, A=Pt)
// ============================================================
injectIV6('lr6', 'Работа и мощность тока',
'Задавай U, I и время t — рассчитаются P=UI, A=Pt. Лампа светится ярче с ростом P.',
'+\'<div style="margin-top:10px;display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px"><div class="p8-scrubber"><span class="p8-scrubber-label">U</span><input type="range" id="lr6-u" min="1" max="220" step="1" value="220"><span class="p8-scrubber-value"><span id="lr6-u-val">220</span><span class="p8-unit">В</span></span></div><div class="p8-scrubber"><span class="p8-scrubber-label">I</span><input type="range" id="lr6-i" min="0.01" max="5" step="0.01" value="0.5"><span class="p8-scrubber-value"><span id="lr6-i-val">0.50</span><span class="p8-unit">А</span></span></div><div class="p8-scrubber"><span class="p8-scrubber-label">t</span><input type="range" id="lr6-t" min="1" max="3600" step="1" value="60"><span class="p8-scrubber-value"><span id="lr6-t-val">60</span><span class="p8-unit">с</span></span></div></div>\'+\'<div style="margin-top:8px;display:flex;gap:10px;flex-wrap:wrap"><div class="p8-readout"><span class="p8-readout-label">P</span><span class="p8-readout-value" id="lr6-p">110</span><span class="p8-readout-unit">Вт</span></div><div class="p8-readout"><span class="p8-readout-label">A</span><span class="p8-readout-value" id="lr6-a">6.6</span><span class="p8-readout-unit">кДж</span></div></div>\'',
`
const svg = P8Helpers.svg.create(560, 260);
svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block';
sb.appendChild(svg);
let U=220, I=0.5, t=60;
function render(){
svg.innerHTML='';
const P = U*I, A = P*t;
const br = Math.min(1, P/200);
/* Lamp */
svg.appendChild(P8Helpers.svg.el('circle', { cx: 280, cy: 110, r: 55, fill: '#fef3c7', opacity: br*0.5+0.15 }));
svg.appendChild(P8Helpers.svg.el('circle', { cx: 280, cy: 110, r: 35, fill: '#fde047', stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('text', { x: 280, y: 113, 'font-family':"'Unbounded',sans-serif", 'font-size':18, 'font-weight':900, fill:'#0f172a', 'text-anchor':'middle', text: P.toFixed(0)+' Вт' }));
if (br > 0.5) {
for (let i = 0; i < 8; i++) {
const a = i*Math.PI/4;
svg.appendChild(P8Helpers.svg.el('line', { x1: 280+45*Math.cos(a), y1: 110+45*Math.sin(a), x2: 280+68*Math.cos(a), y2: 110+68*Math.sin(a), stroke: '#facc15', 'stroke-width': 3 }));
}
}
svg.appendChild(P8Helpers.svg.el('text', { x: 280, y: 220, 'font-family':"'JetBrains Mono',monospace", 'font-size':13, 'font-weight':800, fill:'#0f172a', 'text-anchor':'middle', text: 'P = UI = '+P.toFixed(1)+' Вт, A = Pt = '+(A/1000).toFixed(1)+' кДж' }));
document.getElementById('lr6-p').textContent = P.toFixed(1);
document.getElementById('lr6-a').textContent = (A/1000).toFixed(2);
}
document.getElementById('lr6-u').oninput = ev => { U = +ev.target.value; document.getElementById('lr6-u-val').textContent = U; render(); };
document.getElementById('lr6-i').oninput = ev => { I = +ev.target.value; document.getElementById('lr6-i-val').textContent = I.toFixed(2); render(); };
document.getElementById('lr6-t').oninput = ev => { t = +ev.target.value; document.getElementById('lr6-t-val').textContent = t; render(); };
render();
`);
// ============================================================
// ЛР7 — Отражение (закон отражения с протрактором)
// ============================================================
injectIV6('lr7', 'Закон отражения света',
'Двигай угол падения α — угол отражения β равен ему. Луч идёт по правилу: «угол падения = углу отражения».',
'+\'<div class="p8-scrubber" style="margin-top:10px"><span class="p8-scrubber-label">Угол падения</span><input type="range" id="lr7-a" min="0" max="80" step="1" value="40"><span class="p8-scrubber-value"><span id="lr7-a-val">40</span><span class="p8-unit">°</span></span></div>\'+\'<div style="margin-top:8px;display:flex;gap:10px;flex-wrap:wrap"><div class="p8-readout"><span class="p8-readout-label">α</span><span class="p8-readout-value" id="lr7-a-out">40</span><span class="p8-readout-unit">°</span></div><div class="p8-readout"><span class="p8-readout-label">β</span><span class="p8-readout-value" id="lr7-b">40</span><span class="p8-readout-unit">°</span></div></div>\'',
`
const svg = P8Helpers.svg.create(560, 260);
svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block';
sb.appendChild(svg);
let alpha = 40;
function render(){
svg.innerHTML='';
const cx = 280, cy = 210;
svg.appendChild(P8Helpers.optics.mirrorPlane(60, 210, 500, 210));
svg.appendChild(P8Helpers.svg.el('line', { x1: cx, y1: 210, x2: cx, y2: 30, stroke: '#475569', 'stroke-width': 1.5, 'stroke-dasharray': '5 3' }));
const aRad = alpha*Math.PI/180;
const len = 170;
/* Incident */
const inX = cx - len*Math.sin(aRad), inY = cy - len*Math.cos(aRad);
svg.appendChild(P8Helpers.optics.rayLine(inX, inY, cx, cy, { color: '#facc15', width: 3.5, glow: true }));
/* Reflected */
const rX = cx + len*Math.sin(aRad), rY = cy - len*Math.cos(aRad);
svg.appendChild(P8Helpers.optics.rayLine(cx, cy, rX, rY, { color: '#facc15', width: 3.5, glow: true }));
/* Angle arcs */
svg.appendChild(P8Helpers.svg.el('path', { d: 'M '+(cx-25*Math.sin(aRad/2))+' '+(cy-25*Math.cos(aRad/2))+' A 25 25 0 0 1 '+cx+' '+(cy-25)+' ', fill: 'none', stroke: '#dc2626', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('text', { x: cx-22, y: cy-40, 'font-family':"'JetBrains Mono',monospace", 'font-size':13, 'font-weight':800, fill:'#dc2626', 'text-anchor':'middle', text: 'α='+alpha+'°' }));
svg.appendChild(P8Helpers.svg.el('path', { d: 'M '+cx+' '+(cy-25)+' A 25 25 0 0 1 '+(cx+25*Math.sin(aRad/2))+' '+(cy-25*Math.cos(aRad/2))+' ', fill: 'none', stroke: '#16a34a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('text', { x: cx+22, y: cy-40, 'font-family':"'JetBrains Mono',monospace", 'font-size':13, 'font-weight':800, fill:'#16a34a', 'text-anchor':'middle', text: 'β='+alpha+'°' }));
/* Verdict */
svg.appendChild(P8Helpers.svg.el('text', { x: 280, y: 250, 'font-family':"'JetBrains Mono',monospace", 'font-size':12, 'font-weight':700, fill:'#10b981', 'text-anchor':'middle', text: '✓ α = β — закон отражения' }));
document.getElementById('lr7-a-out').textContent = alpha;
document.getElementById('lr7-b').textContent = alpha;
}
document.getElementById('lr7-a').oninput = ev => { alpha = +ev.target.value; document.getElementById('lr7-a-val').textContent = alpha; render(); };
render();
`);
fs.writeFileSync(DST, h);
console.log('lab final size:', h.length);
const scripts = [...h.matchAll(/<script>([\s\S]*?)<\/script>/g)];
for (const m of scripts) {
try { new Function(m[1]); }
catch (e) { console.error('JS PARSE FAIL:', e.message.slice(0, 200)); process.exit(1); }
}
console.log('inline JS parses OK');