Files
Learn_System/backend/scripts/redesign_p8_ch2_3.cjs
T
Maxim Dolgolyov 0d9226f6d5 feat(phys8 ch2): Phase 2.3 — оставшиеся 14 IV-6 (Ch2 завершена)
scrubberWidget() helper в скрипте — генерирует виджет с N
scrubbers + 1 readout + live SVG render.

§13 Проводники/диэлектрики: 2 стержня, движущиеся электроны в меди,
застрявшие в стекле. Анимация через setInterval.

§14 Электростатическая индукция: + палочка + проводник, при
сближении видны индуцированные −/+ заряды на сторонах.

§15 q=ne: сфера тела с радиально размещёнными ±зарядами,
расчёт n = q/e.

§16 Строение атома: ядро + N электронов на orbitals (2-8-8-2 shells).

§18 A=qU: 2 пластины + drag-arrow с подписью работы.

§19 ЭДС: батарея с readout ε = A/q.

§20 I=q/t: провод с анимированными носителями.

§21 Замкнутая цепь: батарея + switch + лампа, кнопка открыть/замкнуть.

§23 R=ρl/S: динамический wire с длиной/толщиной по scrubberу,
R вычисляется для меди.

§24 Последовательное: 2 резистора, общее R=R1+R2, I.

§26 P=UI: светящаяся лампа brightness ∝ P, лучи при P>120 Вт.

§27 A=UIt: time-bar 0-24 ч, A в кВт·ч.

§29 B≈I вокруг провода: концентрические штриховые круги, opacity ∝ |I|.

§31 Электромагнит: соленоид (число катушек по N), железный сердечник,
полевые линии-параболы с интенсивностью по B=NI.
2026-05-30 10:20:49 +03:00

508 lines
32 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.
// Phase 2.3 — оставшиеся 14 IV-6 для Ch2.
'use strict';
const fs = require('fs');
const path = require('path');
const DST = path.join(__dirname, '..', '..', 'frontend', 'textbooks', 'physics_8_ch2.html');
let h = fs.readFileSync(DST, 'utf8');
function makeStubText(n) {
return `/* IV6 — flagship интерактив (заглушка Phase 2, наполнение в Phase 2.${n}) */
h += '<div class="wg p8-iv6">'
+'<div class="wg-header"><span class="wg-badge p8-badge p8-badge-electric">IV-6</span><div class="wg-title">Новый интерактив §${n}</div></div>'
+'<div class="wg-help">Готовится: интерактивная визуализация с drag-and-drop для углубления темы. Скоро будет доступна.</div>'
+'<div style="padding:30px;text-align:center;color:var(--p8-muted);font-style:italic">'
+'<svg viewBox="0 0 24 24" style="width:32px;height:32px;stroke:currentColor;fill:none;stroke-width:1.5;opacity:.4"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>'
+'<div style="margin-top:8px;font-size:.86rem">Phase 2.${n} — coming soon</div>'
+'</div>'
+'</div>';`;
}
function replaceStub(pid, n, widgetHtml, initFn) {
const stubLF = makeStubText(n);
const stubCRLF = stubLF.replace(/\n/g, '\r\n');
let stubText = null;
if (h.includes(stubLF)) stubText = stubLF;
else if (h.includes(stubCRLF)) stubText = stubCRLF;
if (!stubText) { console.warn(`${pid}: stub not found`); return false; }
const eol = stubText === stubCRLF ? '\r\n' : '\n';
const widget = widgetHtml.trim().replace(/\n/g, eol);
h = h.replace(stubText, widget);
h = h.replace(`wireReadBtn('${pid}');`, `wireReadBtn('${pid}');\n _init${pid.toUpperCase()}_iv6();`);
const fnStart = h.indexOf(`function build_${pid}()`);
const fnEnd = h.indexOf('\n}\n', fnStart);
h = h.slice(0, fnEnd + 3) + '\n' + initFn.trim() + '\n' + h.slice(fnEnd + 3);
console.log(`${pid}: replaced`);
return true;
}
// Compact builder: scrubber-driven calculator with live SVG.
function scrubberWidget(pid, n, title, helpHtml, inputs, formula, svgRender) {
const html = `/* IV6 — ${title} (Phase 2.3) */
h += '<div class="wg p8-iv6">'
+'<div class="wg-header"><span class="wg-badge p8-badge p8-badge-electric">IV-6</span><div class="wg-title">${title}</div></div>'
+'<div class="wg-help">${helpHtml}</div>'
+'<div class="p8-sandbox" id="${pid}-iv6-sandbox" style="height:200px"></div>'
+'<div style="margin-top:10px;display:flex;gap:10px;flex-wrap:wrap">'
${inputs.map(inp => ` +'<div class="p8-scrubber" style="flex:1;min-width:170px"><span class="p8-scrubber-label">${inp.label}</span><input type="range" id="${pid}-iv6-${inp.id}" min="${inp.min}" max="${inp.max}" step="${inp.step}" value="${inp.value}"><span class="p8-scrubber-value"><span id="${pid}-iv6-${inp.id}-val">${inp.value}</span><span class="p8-unit">${inp.unit}</span></span></div>'`).join('\n')}
+'<div class="p8-readout"><span class="p8-readout-label">${formula.label}</span><span class="p8-readout-value" id="${pid}-iv6-out">${formula.initial}</span><span class="p8-readout-unit">${formula.unit}</span></div>'
+'</div>'
+'</div>';`;
const inputBindings = inputs.map(inp =>
` state.${inp.id} = ${inp.value};\n const ${inp.id}Inp = document.getElementById('${pid}-iv6-${inp.id}');\n const ${inp.id}Lab = document.getElementById('${pid}-iv6-${inp.id}-val');\n ${inp.id}Inp.oninput = ev => { state.${inp.id} = +ev.target.value; ${inp.id}Lab.textContent = (${inp.id == 'a' ? 'state.'+inp.id : 'state.'+inp.id}).toFixed(${inp.step >= 1 ? 0 : 2}); render(); };`
).join('\n');
const init = `
function _init${pid.toUpperCase()}_iv6(){
const sb = document.getElementById('${pid}-iv6-sandbox');
if (!sb || !window.P8Helpers) return;
const svg = P8Helpers.svg.create(560, 200);
svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block';
sb.appendChild(svg);
const state = {};
function render(){
svg.innerHTML = '';
${svgRender}
}
${inputBindings}
render();
}
`;
replaceStub(pid, n, html, init);
}
// ============================================================
// §13 — Проводники / диэлектрики
// ============================================================
scrubberWidget('p13', 13,
'Проводники vs диэлектрики',
'Двигай напряжение — в проводнике (медь, $n \\\\sim 10^{29}$/м³) свободные электроны легко движутся, в диэлектрике (стекло) — нет.',
[{ id: 'u', label: 'U', min: 0, max: 100, step: 1, value: 0, unit: 'В' }],
{ label: 'Ток', initial: '0', unit: 'А' },
`
const U = state.u;
/* Conductor (left) — copper bar */
svg.appendChild(P8Helpers.svg.el('rect', { x: 50, y: 60, width: 180, height: 60, fill: '#b45309', stroke: '#0f172a', 'stroke-width': 2, rx: 5 }));
svg.appendChild(P8Helpers.svg.el('text', { x: 140, y: 50, 'font-family':"'Inter',sans-serif", 'font-size':12, 'font-weight':700, fill:'#0f172a', 'text-anchor':'middle', text: 'Проводник (медь)' }));
/* Moving electrons in conductor */
const numE = 8;
for (let i = 0; i < numE; i++) {
const t = (Date.now() / 100 + i * 20) % 100 / 100;
const x = 60 + t * 160;
svg.appendChild(P8Helpers.svg.el('circle', { cx: x, cy: 80 + (i % 2) * 20, r: 4, fill: '#dc2626', opacity: U > 5 ? 1 : 0.3 }));
}
/* Insulator (right) — glass bar */
svg.appendChild(P8Helpers.svg.el('rect', { x: 320, y: 60, width: 180, height: 60, fill: '#bae6fd', stroke: '#0f172a', 'stroke-width': 2, rx: 5 }));
svg.appendChild(P8Helpers.svg.el('text', { x: 410, y: 50, 'font-family':"'Inter',sans-serif", 'font-size':12, 'font-weight':700, fill:'#0f172a', 'text-anchor':'middle', text: 'Диэлектрик (стекло)' }));
/* Stuck electrons */
for (let i = 0; i < 8; i++) {
svg.appendChild(P8Helpers.svg.el('circle', { cx: 335 + i * 22, cy: 80 + (i % 2) * 20, r: 4, fill: '#475569' }));
}
/* Current ↦ in conductor only */
const I = U > 1 ? U / 10 : 0;
svg.appendChild(P8Helpers.svg.el('text', { x: 140, y: 145, 'font-family':"'JetBrains Mono',monospace", 'font-size':12, 'font-weight':800, fill:'#dc2626', 'text-anchor':'middle', text: 'I = '+I.toFixed(2)+' А' }));
svg.appendChild(P8Helpers.svg.el('text', { x: 410, y: 145, 'font-family':"'JetBrains Mono',monospace", 'font-size':12, 'font-weight':800, fill:'#94a3b8', 'text-anchor':'middle', text: 'I = 0 А' }));
document.getElementById('p13-iv6-out').textContent = I.toFixed(2);
/* Animate by re-render every 50ms */
if (!sb._anim) sb._anim = setInterval(() => { if (sb.isConnected) render(); else { clearInterval(sb._anim); } }, 100);
`
);
// ============================================================
// §14 — Электростатическая индукция
// ============================================================
scrubberWidget('p14', 14,
'Электростатическая индукция',
'Двигай заряженную палочку к незаряженному проводнику. Свободные электроны притягиваются к + или отталкиваются от −, на дальней стороне возникает противоположный заряд.',
[{ id: 'd', label: 'Расстояние', min: 50, max: 300, step: 5, value: 200, unit: 'мм' }],
{ label: 'Индукция', initial: '0', unit: 'отн.' },
`
const d = state.d;
/* Charged rod (left) */
svg.appendChild(P8Helpers.svg.el('rect', { x: 30, y: 80, width: 60, height: 30, fill: '#fecaca', stroke: '#dc2626', 'stroke-width': 2, rx: 5 }));
svg.appendChild(P8Helpers.svg.el('text', { x: 60, y: 100, 'font-family':"'Unbounded',sans-serif", 'font-size':18, 'font-weight':900, fill: '#dc2626', 'text-anchor':'middle', text: '+++' }));
/* Conductor (right at position 90 + d) */
const condX = 90 + d;
svg.appendChild(P8Helpers.svg.el('rect', { x: condX, y: 70, width: 140, height: 50, fill: '#fef3c7', stroke: '#0f172a', 'stroke-width': 2, rx: 5 }));
/* Distribution: near side , far side + (induction) */
const intensity = Math.max(0, Math.min(1, (300 - d) / 250));
if (intensity > 0.1) {
svg.appendChild(P8Helpers.svg.el('text', { x: condX + 25, y: 100, 'font-family':"'Unbounded',sans-serif", 'font-size':16, 'font-weight':900, fill: '#2563eb', 'text-anchor':'middle', text: '−−' }));
svg.appendChild(P8Helpers.svg.el('text', { x: condX + 115, y: 100, 'font-family':"'Unbounded',sans-serif", 'font-size':16, 'font-weight':900, fill: '#dc2626', 'text-anchor':'middle', text: '++' }));
}
document.getElementById('p14-iv6-out').textContent = intensity.toFixed(2);
`
);
// ============================================================
// §15 — Элементарный заряд (n = q/e)
// ============================================================
scrubberWidget('p15', 15,
'Элементарный заряд: $q = ne$',
'Любой заряд кратен $e = 1{,}6 \\\\cdot 10^{-19}$ Кл. Двигай заряд тела — посчитаем число избыточных электронов.',
[{ id: 'q', label: 'q', min: -10, max: 10, step: 0.1, value: 1, unit: 'нКл' }],
{ label: 'n электронов', initial: '6.25', unit: '×10¹⁰' },
`
const q = state.q * 1e-9;
const n = Math.abs(q / 1.6e-19);
const sign = q > 0 ? 1 : -1;
/* Body (sphere) */
svg.appendChild(P8Helpers.svg.el('circle', { cx: 200, cy: 100, r: 50, fill: sign > 0 ? '#fecaca' : '#bfdbfe', stroke: sign > 0 ? '#dc2626' : '#2563eb', 'stroke-width': 3 }));
/* +/- charges around */
const numE = Math.min(12, Math.round(n / 1e10) + 1);
for (let i = 0; i < numE; i++) {
const a = i * 2 * Math.PI / numE;
svg.appendChild(P8Helpers.svg.el('text', { x: 200 + 38 * Math.cos(a), y: 105 + 38 * Math.sin(a), 'font-family':"'Inter',sans-serif", 'font-size':14, 'font-weight':900, fill: sign > 0 ? '#dc2626' : '#2563eb', 'text-anchor':'middle', text: sign > 0 ? '+' : '' }));
}
/* Counter */
svg.appendChild(P8Helpers.svg.el('text', { x: 380, y: 90, 'font-family':"'Unbounded',sans-serif", 'font-size':14, 'font-weight':800, fill:'#0f172a', 'text-anchor':'middle', text: 'n = q/e' }));
svg.appendChild(P8Helpers.svg.el('text', { x: 380, y: 115, 'font-family':"'JetBrains Mono',monospace", 'font-size':16, 'font-weight':700, fill:'var(--el-mid,#06b6d4)', 'text-anchor':'middle', text: '≈ '+(n/1e10).toFixed(2)+'·10¹⁰' }));
document.getElementById('p15-iv6-out').textContent = (n / 1e10).toFixed(2);
`
);
// ============================================================
// §16 — Строение атома
// ============================================================
scrubberWidget('p16', 16,
'Строение атома',
'Атом нейтрален: число протонов $Z$ = число электронов. Двигай Z, наблюдай орбиту электронов вокруг ядра.',
[{ id: 'z', label: 'Z (протоны)', min: 1, max: 20, step: 1, value: 6, unit: '' }],
{ label: 'Заряд ядра', initial: '+6e', unit: '' },
`
const Z = Math.round(state.z);
/* Nucleus */
svg.appendChild(P8Helpers.svg.el('circle', { cx: 280, cy: 100, r: 14, fill: '#dc2626', stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('text', { x: 280, y: 105, 'font-family':"'Unbounded',sans-serif", 'font-size':12, 'font-weight':900, fill:'#fff', 'text-anchor':'middle', text: '+'+Z }));
/* Orbits — fill shell by shell: 2, 8, 8, 2 */
const shells = [];
let remaining = Z;
[2, 8, 8, 2].forEach(cap => { if (remaining > 0) { shells.push(Math.min(cap, remaining)); remaining -= cap; }});
shells.forEach((electrons, shellIdx) => {
const radius = 35 + shellIdx * 20;
svg.appendChild(P8Helpers.svg.el('circle', { cx: 280, cy: 100, r: radius, fill: 'none', stroke: '#94a3b8', 'stroke-width': 1, 'stroke-dasharray': '3 3' }));
for (let i = 0; i < electrons; i++) {
const a = i * 2 * Math.PI / electrons;
svg.appendChild(P8Helpers.svg.el('circle', { cx: 280 + radius * Math.cos(a), cy: 100 + radius * Math.sin(a), r: 4.5, fill: '#2563eb', stroke: '#0f172a', 'stroke-width': 1 }));
}
});
document.getElementById('p16-iv6-out').textContent = '+'+Z+'e';
`
);
// ============================================================
// §18 — A = qU
// ============================================================
scrubberWidget('p18', 18,
'Работа поля: $A = qU$',
'Двигай заряд $q$ и напряжение $U$. Работа $A = qU$ обновляется live.',
[
{ id: 'q', label: 'q', min: 0.1, max: 10, step: 0.1, value: 1, unit: 'мкКл' },
{ id: 'u', label: 'U', min: 1, max: 100, step: 1, value: 12, unit: 'В' }
],
{ label: 'A', initial: '12', unit: 'мкДж' },
`
const q = state.q, U = state.u;
const A = q * U;
/* Two plates */
svg.appendChild(P8Helpers.svg.el('line', { x1: 100, y1: 40, x2: 100, y2: 160, stroke: '#dc2626', 'stroke-width': 6 }));
svg.appendChild(P8Helpers.svg.el('text', { x: 100, y: 30, 'font-family':"'Unbounded',sans-serif", 'font-size':14, 'font-weight':900, fill:'#dc2626', 'text-anchor':'middle', text: '+' }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 400, y1: 40, x2: 400, y2: 160, stroke: '#2563eb', 'stroke-width': 6 }));
svg.appendChild(P8Helpers.svg.el('text', { x: 400, y: 30, 'font-family':"'Unbounded',sans-serif", 'font-size':14, 'font-weight':900, fill:'#2563eb', 'text-anchor':'middle', text: '' }));
/* Charge moving */
svg.appendChild(P8Helpers.svg.el('circle', { cx: 200, cy: 100, r: 14, fill: '#fecaca', stroke: '#dc2626', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('text', { x: 200, y: 105, 'font-family':"'Inter',sans-serif", 'font-size':14, 'font-weight':900, fill:'#dc2626', 'text-anchor':'middle', text: '+q' }));
/* Arrow direction of work */
svg.appendChild(P8Helpers.svg.gradientArrow(svg, 220, 100, 380, 100, { colorFrom: '#facc15', colorTo: '#dc2626', width: 3, headSize: 12, glow: true }));
svg.appendChild(P8Helpers.svg.el('text', { x: 300, y: 90, 'font-family':"'JetBrains Mono',monospace", 'font-size':14, 'font-weight':800, fill:'#0f172a', 'text-anchor':'middle', text: 'A = qU = '+A.toFixed(1)+' мкДж' }));
svg.appendChild(P8Helpers.svg.el('text', { x: 250, y: 180, 'font-family':"'Inter',sans-serif", 'font-size':11, fill:'var(--p8-muted,#64748b)', 'text-anchor':'middle', text: 'U = '+U+' В, между пластинами' }));
document.getElementById('p18-iv6-out').textContent = A.toFixed(1);
`
);
// ============================================================
// §19 — Источники тока
// ============================================================
scrubberWidget('p19', 19,
'ЭДС источника: $\\\\mathcal{E} = A/q$',
'Источник тока совершает работу $A$ над зарядом $q$. ЭДС $\\\\mathcal{E} = A/q$.',
[
{ id: 'a', label: 'A', min: 1, max: 100, step: 1, value: 24, unit: 'Дж' },
{ id: 'q', label: 'q', min: 0.5, max: 20, step: 0.5, value: 2, unit: 'Кл' }
],
{ label: 'ЭДС', initial: '12', unit: 'В' },
`
const A = state.a, q = state.q;
const E = A / q;
/* Battery shape */
svg.appendChild(P8Helpers.svg.el('rect', { x: 200, y: 60, width: 160, height: 80, fill: '#facc15', stroke: '#0f172a', 'stroke-width': 3, rx: 8 }));
svg.appendChild(P8Helpers.svg.el('rect', { x: 215, y: 50, width: 30, height: 10, fill: '#475569' }));
svg.appendChild(P8Helpers.svg.el('rect', { x: 320, y: 50, width: 30, height: 10, fill: '#475569' }));
svg.appendChild(P8Helpers.svg.el('text', { x: 280, y: 100, 'font-family':"'Unbounded',sans-serif", 'font-size':22, 'font-weight':900, fill:'#0f172a', 'text-anchor':'middle', text: E.toFixed(1)+' В' }));
svg.appendChild(P8Helpers.svg.el('text', { x: 280, y: 125, 'font-family':"'Inter',sans-serif", 'font-size':11, fill:'#0f172a', 'text-anchor':'middle', text: 'A = '+A+' Дж, q = '+q+' Кл' }));
document.getElementById('p19-iv6-out').textContent = E.toFixed(1);
`
);
// ============================================================
// §20 — I = q/t
// ============================================================
scrubberWidget('p20', 20,
'Сила тока: $I = q/t$',
'Двигай заряд и время — найдём ток.',
[
{ id: 'q', label: 'q', min: 0.1, max: 100, step: 0.1, value: 6, unit: 'Кл' },
{ id: 't', label: 't', min: 0.1, max: 60, step: 0.1, value: 2, unit: 'с' }
],
{ label: 'I', initial: '3.0', unit: 'А' },
`
const q = state.q, t = state.t;
const I = q / t;
/* Wire with flowing charges */
svg.appendChild(P8Helpers.svg.el('rect', { x: 80, y: 90, width: 400, height: 20, fill: '#cbd5e1', stroke: '#0f172a', 'stroke-width': 2 }));
const numE = Math.min(20, Math.round(q));
for (let i = 0; i < numE; i++) {
const t0 = (Date.now() / 200 + i / numE) % 1;
const x = 90 + t0 * 380;
svg.appendChild(P8Helpers.svg.el('circle', { cx: x, cy: 100, r: 4, fill: '#dc2626' }));
}
/* Arrow direction */
svg.appendChild(P8Helpers.svg.gradientArrow(svg, 480, 100, 530, 100, { colorFrom: '#dc2626', colorTo: '#7f1d1d', width: 3, headSize: 12 }));
svg.appendChild(P8Helpers.svg.el('text', { x: 280, y: 140, 'font-family':"'JetBrains Mono',monospace", 'font-size':14, 'font-weight':800, fill:'var(--el-mid,#06b6d4)', 'text-anchor':'middle', text: 'I = '+q+'/'+t+' = '+I.toFixed(2)+' А' }));
document.getElementById('p20-iv6-out').textContent = I.toFixed(2);
if (!sb._anim) sb._anim = setInterval(() => { if (sb.isConnected) render(); else clearInterval(sb._anim); }, 100);
`
);
// ============================================================
// §21 — Электрическая цепь
// ============================================================
scrubberWidget('p21', 21,
'Замкнутая электрическая цепь',
'Состоит из источника, потребителя и соединительных проводов. Переключи выключатель — пойдёт ток.',
[{ id: 's', label: 'Замкнут', min: 0, max: 1, step: 1, value: 1, unit: '' }],
{ label: 'Цепь', initial: 'замкнута', unit: '' },
`
const closed = state.s > 0.5;
/* Battery */
svg.appendChild(P8Helpers.em.circuitComponent('battery', 120, 100, 'h', '6 В'));
/* Switch */
svg.appendChild(P8Helpers.em.circuitComponent('switch', 270, 100, 'h'));
if (closed) {
svg.appendChild(P8Helpers.svg.el('line', { x1: 258, y1: 100, x2: 282, y2: 100, stroke: '#0f172a', 'stroke-width': 2 }));
}
/* Lamp */
svg.appendChild(P8Helpers.em.circuitComponent('lamp', 420, 100, 'h'));
if (closed) {
svg.appendChild(P8Helpers.svg.el('circle', { cx: 420, cy: 100, r: 22, fill: '#fef3c7', opacity: 0.5 }));
}
/* Wires */
svg.appendChild(P8Helpers.svg.el('line', { x1: 150, y1: 100, x2: 240, y2: 100, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 300, y1: 100, x2: 394, y2: 100, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 446, y1: 100, x2: 500, y2: 100, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 500, y1: 100, x2: 500, y2: 160, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 90, y1: 100, x2: 90, y2: 160, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 90, y1: 160, x2: 500, y2: 160, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('text', { x: 280, y: 185, 'font-family':"'Inter',sans-serif", 'font-size':11, 'font-weight':700, fill: closed ? '#16a34a' : '#dc2626', 'text-anchor':'middle', text: closed ? '✓ Цепь замкнута — ток идёт' : '✗ Цепь разомкнута' }));
document.getElementById('p21-iv6-out').textContent = closed ? 'замкнута' : 'разомкнута';
`
);
// ============================================================
// §23 — R = ρl/S
// ============================================================
scrubberWidget('p23', 23,
'Сопротивление: $R = \\\\rho l / S$',
'Длина увеличивает $R$ пропорционально, площадь — обратно пропорционально. Удельное $\\\\rho$ — для меди 1.7·10⁻⁸ Ом·м.',
[
{ id: 'l', label: 'l', min: 0.1, max: 10, step: 0.1, value: 1, unit: 'м' },
{ id: 's', label: 'S', min: 0.5, max: 10, step: 0.1, value: 1, unit: 'мм²' }
],
{ label: 'R (медь)', initial: '0.017', unit: 'Ом' },
`
const l = state.l, S = state.s * 1e-6;
const rho = 1.7e-8;
const R = rho * l / S;
/* Wire shape: длина = l*60 max, толщина = sqrt(S)*8 max */
const wireL = Math.min(440, 50 + l * 40);
const wireH = Math.min(40, 6 + Math.sqrt(state.s) * 8);
svg.appendChild(P8Helpers.svg.el('rect', { x: (560 - wireL) / 2, y: (200 - wireH) / 2, width: wireL, height: wireH, fill: '#b45309', stroke: '#0f172a', 'stroke-width': 2, rx: 4 }));
svg.appendChild(P8Helpers.svg.el('text', { x: 280, y: (200 - wireH)/2 - 8, 'font-family':"'JetBrains Mono',monospace", 'font-size':11, 'font-weight':700, fill:'#0f172a', 'text-anchor':'middle', text: 'l = '+l.toFixed(1)+' м, S = '+state.s.toFixed(1)+' мм²' }));
svg.appendChild(P8Helpers.svg.el('text', { x: 280, y: 170, 'font-family':"'Unbounded',sans-serif", 'font-size':14, 'font-weight':800, fill:'var(--el-mid,#06b6d4)', 'text-anchor':'middle', text: 'R = '+R.toFixed(4)+' Ом' }));
document.getElementById('p23-iv6-out').textContent = R.toFixed(4);
`
);
// ============================================================
// §24 — Последовательные резисторы
// ============================================================
scrubberWidget('p24', 24,
'Последовательное соединение: $R = R_1 + R_2$',
'Сложи $R_1$ и $R_2$ — получишь общее $R$. Ток через них одинаков, напряжения складываются: $U = U_1 + U_2$.',
[
{ id: 'r1', label: 'R₁', min: 1, max: 100, step: 1, value: 20, unit: 'Ом' },
{ id: 'r2', label: 'R₂', min: 1, max: 100, step: 1, value: 30, unit: 'Ом' }
],
{ label: 'R_общ', initial: '50', unit: 'Ом' },
`
const R1 = state.r1, R2 = state.r2;
const R = R1 + R2;
const U = 12;
const I = U / R;
/* Battery */
svg.appendChild(P8Helpers.em.circuitComponent('battery', 80, 100, 'h', U+' В'));
/* R1 */
svg.appendChild(P8Helpers.em.circuitComponent('resistor', 240, 100, 'h', R1+' Ом'));
/* R2 */
svg.appendChild(P8Helpers.em.circuitComponent('resistor', 400, 100, 'h', R2+' Ом'));
/* Wires */
svg.appendChild(P8Helpers.svg.el('line', { x1: 110, y1: 100, x2: 210, y2: 100, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 270, y1: 100, x2: 370, y2: 100, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 430, y1: 100, x2: 510, y2: 100, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 510, y1: 100, x2: 510, y2: 160, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 50, y1: 100, x2: 50, y2: 160, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 50, y1: 160, x2: 510, y2: 160, stroke: '#0f172a', 'stroke-width': 2 }));
/* Labels */
svg.appendChild(P8Helpers.svg.el('text', { x: 280, y: 180, 'font-family':"'JetBrains Mono',monospace", 'font-size':12, 'font-weight':800, fill:'var(--el-mid,#06b6d4)', 'text-anchor':'middle', text: 'R = R₁+R₂ = '+R+' Ом, I = U/R = '+I.toFixed(3)+' А' }));
document.getElementById('p24-iv6-out').textContent = R;
`
);
// ============================================================
// §26 — P = UI
// ============================================================
scrubberWidget('p26', 26,
'Мощность: $P = UI$',
'Двигай напряжение и ток — мощность.',
[
{ id: 'u', label: 'U', min: 1, max: 220, step: 1, value: 220, unit: 'В' },
{ id: 'i', label: 'I', min: 0.01, max: 10, step: 0.01, value: 0.5, unit: 'А' }
],
{ label: 'P', initial: '110', unit: 'Вт' },
`
const U = state.u, I = state.i;
const P = U * I;
/* Lamp brightness */
const brightness = Math.min(1, P / 200);
svg.appendChild(P8Helpers.svg.el('circle', { cx: 280, cy: 100, r: 50, fill: '#fef3c7', opacity: brightness * 0.5 + 0.2 }));
svg.appendChild(P8Helpers.svg.el('circle', { cx: 280, cy: 100, r: 30, fill: '#fde047', stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('text', { x: 280, y: 105, 'font-family':"'Unbounded',sans-serif", 'font-size':16, 'font-weight':900, fill: '#0f172a', 'text-anchor':'middle', text: P.toFixed(0) }));
svg.appendChild(P8Helpers.svg.el('text', { x: 280, y: 122, 'font-family':"'Inter',sans-serif", 'font-size':11, 'font-weight':700, fill: '#0f172a', 'text-anchor':'middle', text: 'Вт' }));
if (brightness > 0.6) {
/* Rays */
for (let i = 0; i < 8; i++) {
const a = i * Math.PI / 4;
const x1 = 280 + 38 * Math.cos(a), y1 = 100 + 38 * Math.sin(a);
const x2 = 280 + 58 * Math.cos(a), y2 = 100 + 58 * Math.sin(a);
svg.appendChild(P8Helpers.svg.el('line', { x1, y1, x2, y2, stroke: '#facc15', 'stroke-width': 3 }));
}
}
svg.appendChild(P8Helpers.svg.el('text', { x: 280, y: 180, 'font-family':"'JetBrains Mono',monospace", 'font-size':12, 'font-weight':700, fill:'#0f172a', 'text-anchor':'middle', text: 'P = U·I = '+U+'·'+I.toFixed(2)+' = '+P.toFixed(1)+' Вт' }));
document.getElementById('p26-iv6-out').textContent = P.toFixed(1);
`
);
// ============================================================
// §27 — A = UIt (электроэнергия)
// ============================================================
scrubberWidget('p27', 27,
'Электроэнергия: $A = UIt$',
'За время $t$ потребитель потратит $A = UIt$ или $A = Pt$.',
[
{ id: 'p', label: 'P', min: 1, max: 3000, step: 1, value: 100, unit: 'Вт' },
{ id: 't', label: 't', min: 0.1, max: 24, step: 0.1, value: 5, unit: 'ч' }
],
{ label: 'A', initial: '0.5', unit: 'кВт·ч' },
`
const P = state.p, t = state.t;
const A = P * t / 1000; /* kWh */
/* Time bar */
const barW = (t / 24) * 460;
svg.appendChild(P8Helpers.svg.el('rect', { x: 50, y: 80, width: 460, height: 40, fill: '#e5e7eb', stroke: '#0f172a' }));
svg.appendChild(P8Helpers.svg.el('rect', { x: 50, y: 80, width: barW, height: 40, fill: 'var(--el-mid,#06b6d4)', opacity: 0.7 }));
svg.appendChild(P8Helpers.svg.el('text', { x: 280, y: 105, 'font-family':"'JetBrains Mono',monospace", 'font-size':14, 'font-weight':800, fill:'#fff', 'text-anchor':'middle', text: t.toFixed(1)+' ч из 24' }));
/* A display */
svg.appendChild(P8Helpers.svg.el('text', { x: 280, y: 160, 'font-family':"'Unbounded',sans-serif", 'font-size':18, 'font-weight':900, fill:'#0f172a', 'text-anchor':'middle', text: 'A = '+A.toFixed(3)+' кВт·ч' }));
document.getElementById('p27-iv6-out').textContent = A.toFixed(3);
`
);
// ============================================================
// §29 — Магнитное поле тока (видим B-grid)
// ============================================================
scrubberWidget('p29', 29,
'Магнитное поле тока: $B \\\\propto I$',
'Чем больше ток — тем сильнее поле вокруг проводника. Густота линий ∝ $|I|$.',
[{ id: 'i', label: 'I', min: -10, max: 10, step: 0.1, value: 3, unit: 'А' }],
{ label: 'B (отн.)', initial: '3', unit: '' },
`
const I = state.i;
/* Wire (vertical center) */
svg.appendChild(P8Helpers.svg.el('line', { x1: 280, y1: 20, x2: 280, y2: 180, stroke: '#0f172a', 'stroke-width': 5 }));
/* Current direction */
if (Math.abs(I) > 0.05) {
const dir = I > 0 ? 1 : -1;
svg.appendChild(P8Helpers.svg.el('polygon', { points: '280,'+(dir>0?20:180)+' 274,'+(dir>0?30:170)+' 286,'+(dir>0?30:170), fill: '#dc2626' }));
}
/* Field circles around wire */
const intensity = Math.abs(I) / 10;
[25, 45, 65, 90, 115].forEach((r, k) => {
const opacity = intensity * (1 - k * 0.15);
if (opacity > 0.05) {
svg.appendChild(P8Helpers.svg.el('circle', { cx: 280, cy: 100, r, fill: 'none', stroke: '#7c3aed', 'stroke-width': 1.5, opacity, 'stroke-dasharray': '5 3' }));
}
});
svg.appendChild(P8Helpers.svg.el('text', { x: 280, y: 195, 'font-family':"'JetBrains Mono',monospace", 'font-size':12, 'font-weight':800, fill:'var(--el-mid,#06b6d4)', 'text-anchor':'middle', text: 'I = '+I.toFixed(1)+' А, B ∝ |I|' }));
document.getElementById('p29-iv6-out').textContent = Math.abs(I).toFixed(1);
`
);
// ============================================================
// §31 — Электромагнит (B ∝ NI)
// ============================================================
scrubberWidget('p31', 31,
'Электромагнит: $B \\\\propto NI$',
'Соленоид с $N$ витками и током $I$ — поле растёт пропорционально и тому, и другому. Сердечник из железа усиливает в $\\\\mu \\\\sim 1000$ раз.',
[
{ id: 'n', label: 'Витки N', min: 10, max: 1000, step: 10, value: 100, unit: '' },
{ id: 'i', label: 'I', min: 0, max: 5, step: 0.1, value: 1, unit: 'А' }
],
{ label: 'B (отн.)', initial: '100', unit: '' },
`
const N = state.n, I = state.i;
const B = N * I;
/* Solenoid coils */
const coils = Math.min(20, Math.round(N / 50) + 4);
const coilW = 16;
for (let k = 0; k < coils; k++) {
const x = 130 + k * coilW;
svg.appendChild(P8Helpers.svg.el('ellipse', { cx: x, cy: 100, rx: 6, ry: 32, fill: 'none', stroke: '#b45309', 'stroke-width': 2 }));
}
/* Iron core */
svg.appendChild(P8Helpers.svg.el('rect', { x: 120, y: 90, width: coils * coilW + 20, height: 20, fill: '#64748b', stroke: '#0f172a', 'stroke-width': 1 }));
/* Field lines */
const intensity = Math.min(1, B / 2000);
if (intensity > 0.05) {
[40, 70, 100].forEach((dy, k) => {
const op = intensity * (1 - k * 0.25);
if (op < 0.05) return;
svg.appendChild(P8Helpers.svg.el('path', { d: 'M 50 '+(100-dy)+' Q 280 '+(100-dy*1.5)+', 510 '+(100-dy), fill: 'none', stroke: '#7c3aed', 'stroke-width': 1.5, opacity: op }));
svg.appendChild(P8Helpers.svg.el('path', { d: 'M 50 '+(100+dy)+' Q 280 '+(100+dy*1.5)+', 510 '+(100+dy), fill: 'none', stroke: '#7c3aed', 'stroke-width': 1.5, opacity: op }));
});
}
svg.appendChild(P8Helpers.svg.el('text', { x: 280, y: 180, 'font-family':"'JetBrains Mono',monospace", 'font-size':12, 'font-weight':800, fill:'var(--el-mid,#06b6d4)', 'text-anchor':'middle', text: 'B ∝ N·I = '+B+' (отн.)' }));
document.getElementById('p31-iv6-out').textContent = B;
`
);
fs.writeFileSync(DST, h);
console.log('ch2 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');
const fns = [...h.matchAll(/function build_p(\d+)\(\)/g)].map(m => parseInt(m[1]));
console.log('Builders:', fns.length);