feat(phys8 ch2): Phase 2.2 — 6 флагман-интерактивов

§12 Charge Sandbox: canvas с динамическим добавлением зарядов.
Click → +заряд (или - через кнопку), drag для перемещения,
стрелки взаимодействия по Кулону (красные=отталкивание,
зелёные=притяжение). Кнопки '+/-', 'Очистить'.

§17 Field Visualizer: drag-зарядов с live перерисовкой
силовых линий. От каждого + рисуются 16 линий, идущих
по полю E через интегрирование шагами. Линии останавливаются
у − зарядов или вылетают за canvas.

§22 Закон Ома: SVG цепь батарея + резистор + лампа.
Scrubbers U (0.5-12 В), R (1-100 Ом). I=U/R обновляется
live, яркость лампы ∝ I (glow при I>0.3).

§25 Параллельные резисторы: SVG цепь с разветвлением.
Scrubbers R₁, R₂. Live расчёт R_общ = R₁R₂/(R₁+R₂),
I₁, I₂ для каждой ветви, общий I.

§28 Магниты: canvas с 2 drag-магнитами (N-S полюса).
При сближении inner полюсов (S-N) рисуются стрелки
притяжения с величиной по F~1/d².

§30 Опыт Эрстеда: SVG провод с током (scrubber -5..+5 А)
и компас под ним. Силовые линии магн. поля вокруг провода
(концентрические штриховые круги) с opacity ∝ |I|.
Стрелка компаса отклоняется по arctan(I), угол выводится.
This commit is contained in:
Maxim Dolgolyov
2026-05-30 10:17:23 +03:00
parent 1f82a980de
commit da6dd96aac
2 changed files with 1046 additions and 37 deletions
+575
View File
@@ -0,0 +1,575 @@
// Phase 2.2 — флагман-интерактивы для критических §:
// §12 Charge sandbox, §17 Field visualizer, §22 Ohm's law,
// §25 Parallel resistors, §28 Magnet polarity, §30 Эрстед.
'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;
}
// ============================================================
// §12 — Charge sandbox: click anywhere to add charge
// ============================================================
const P12_HTML = `/* IV6 — Charge Sandbox (Phase 2.2) */
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">Песочница зарядов — наблюдай взаимодействие</div></div>'
+'<div class="wg-help">Клик ЛКМ → добавить +заряд, клик ПКМ → добавить -заряд. Перетаскивай существующие. Стрелки показывают силы взаимодействия (закон Кулона $F = k|q_1 q_2|/r^2$).</div>'
+'<div class="p8-sandbox" id="p12-iv6-sandbox" style="height:300px"></div>'
+'<div style="margin-top:10px;display:flex;gap:10px;flex-wrap:wrap">'
+'<button class="btn primary" id="p12-iv6-add-pos">+ Добавить +</button>'
+'<button class="btn primary" id="p12-iv6-add-neg" style="background:#2563eb;border-color:#2563eb">+ Добавить </button>'
+'<button class="btn" id="p12-iv6-clear">Очистить</button>'
+'<div class="p8-readout"><span class="p8-readout-label">Зарядов</span><span class="p8-readout-value" id="p12-iv6-count">0</span></div>'
+'</div>'
+'</div>';`;
const P12_INIT = `
function _initP12_iv6(){
const sb = document.getElementById('p12-iv6-sandbox');
if (!sb || !window.P8Helpers || !window.P8Drag) return;
const W = 560, H = 300;
const canvas = document.createElement('canvas');
canvas.width = W; canvas.height = H;
canvas.style.width = '100%'; canvas.style.height = '100%'; canvas.style.display = 'block';
sb.appendChild(canvas);
const ctx = canvas.getContext('2d');
const charges = [];
let nextSign = 1;
function draw(){
ctx.fillStyle = '#fafafa';
ctx.fillRect(0, 0, W, H);
/* Forces between pairs */
for (let i = 0; i < charges.length; i++) {
for (let j = i + 1; j < charges.length; j++) {
const a = charges[i], b = charges[j];
const dx = b.x - a.x, dy = b.y - a.y;
const r2 = dx*dx + dy*dy;
if (r2 < 100) continue;
const r = Math.sqrt(r2);
const F = 4e6 * a.sign * b.sign / r2;
const fx = F * dx / r, fy = F * dy / r;
/* Arrow from a in direction (-fx, -fy) means: force on a from b */
const len = Math.min(80, Math.abs(F) * 5);
const dir = a.sign * b.sign > 0 ? -1 : 1;
const aex = a.x + dir * fx / Math.abs(F) * len;
const aey = a.y + dir * fy / Math.abs(F) * len;
ctx.strokeStyle = a.sign * b.sign > 0 ? '#dc2626' : '#16a34a';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(aex, aey);
ctx.stroke();
/* Arrowhead */
const ang = Math.atan2(aey - a.y, aex - a.x);
ctx.beginPath();
ctx.moveTo(aex, aey);
ctx.lineTo(aex - 7 * Math.cos(ang - 0.3), aey - 7 * Math.sin(ang - 0.3));
ctx.lineTo(aex - 7 * Math.cos(ang + 0.3), aey - 7 * Math.sin(ang + 0.3));
ctx.closePath();
ctx.fillStyle = ctx.strokeStyle;
ctx.fill();
}
}
/* Charges */
charges.forEach(c => {
const color = c.sign > 0 ? '#dc2626' : '#2563eb';
const fill = c.sign > 0 ? '#fecaca' : '#bfdbfe';
ctx.fillStyle = fill;
ctx.strokeStyle = color;
ctx.lineWidth = 2.5;
ctx.beginPath();
ctx.arc(c.x, c.y, 18, 0, 2 * Math.PI);
ctx.fill();
ctx.stroke();
ctx.fillStyle = color;
ctx.font = "bold 18px sans-serif";
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(c.sign > 0 ? '+' : '', c.x, c.y + 1);
});
document.getElementById('p12-iv6-count').textContent = charges.length;
}
const drag = P8Drag.attachCanvas(canvas, {
objects: charges.map(c => ({ ...c, r: 22 })),
onPickup: c => {},
onDrag: (c, pos) => {
/* Sync back to charges by id */
const orig = charges.find(ch => ch === c || (ch.id === c.id));
if (orig) { orig.x = pos.x; orig.y = pos.y; }
draw();
},
onClick: (pos) => {
charges.push({ x: pos.x, y: pos.y, sign: nextSign, id: Date.now() + Math.random() });
drag.updateObjects(charges.map(c => ({ ...c, r: 22 })));
draw();
if (window.addXp && charges.length === 2) addXp(10, 'p12-iv6-first');
}
});
document.getElementById('p12-iv6-add-pos').onclick = () => {
nextSign = 1;
charges.push({ x: 80 + Math.random() * (W - 160), y: 80 + Math.random() * (H - 160), sign: 1, id: Date.now() + Math.random() });
drag.updateObjects(charges.map(c => ({ ...c, r: 22 })));
draw();
};
document.getElementById('p12-iv6-add-neg').onclick = () => {
nextSign = -1;
charges.push({ x: 80 + Math.random() * (W - 160), y: 80 + Math.random() * (H - 160), sign: -1, id: Date.now() + Math.random() });
drag.updateObjects(charges.map(c => ({ ...c, r: 22 })));
draw();
};
document.getElementById('p12-iv6-clear').onclick = () => {
charges.length = 0;
drag.updateObjects([]);
draw();
};
draw();
}
`;
replaceStub('p12', 12, P12_HTML, P12_INIT);
// ============================================================
// §17 — Field visualizer
// ============================================================
const P17_HTML = `/* IV6 — Field Visualizer (Phase 2.2) */
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">Силовые линии — карта поля</div></div>'
+'<div class="wg-help">Перетаскивай заряды. Силовые линии рисуются live: выходят из + и заходят в −. Густота линий = напряжённость $E$.</div>'
+'<div class="p8-sandbox" id="p17-iv6-sandbox" style="height:320px"></div>'
+'<div style="margin-top:10px;display:flex;gap:10px;flex-wrap:wrap">'
+'<button class="btn" id="p17-iv6-add-pos">+ Заряд</button>'
+'<button class="btn" id="p17-iv6-add-neg"> Заряд</button>'
+'<button class="btn" id="p17-iv6-clear">Сброс</button>'
+'</div>'
+'</div>';`;
const P17_INIT = `
function _initP17_iv6(){
const sb = document.getElementById('p17-iv6-sandbox');
if (!sb || !window.P8Drag) return;
const W = 560, H = 320;
const canvas = document.createElement('canvas');
canvas.width = W; canvas.height = H;
canvas.style.width = '100%'; canvas.style.height = '100%'; canvas.style.display = 'block';
sb.appendChild(canvas);
const ctx = canvas.getContext('2d');
let charges = [
{ x: 200, y: 160, sign: 1, r: 22 },
{ x: 360, y: 160, sign: -1, r: 22 }
];
function E(x, y) {
let ex = 0, ey = 0;
charges.forEach(c => {
const dx = x - c.x, dy = y - c.y;
const r2 = dx*dx + dy*dy;
if (r2 < 200) return;
const r = Math.sqrt(r2);
const k = 5000 * c.sign / r2;
ex += k * dx / r; ey += k * dy / r;
});
return { ex, ey, mag: Math.sqrt(ex*ex + ey*ey) };
}
function draw(){
ctx.fillStyle = '#fafafa';
ctx.fillRect(0, 0, W, H);
/* Draw field lines starting from + charges */
charges.filter(c => c.sign > 0).forEach(c => {
for (let i = 0; i < 16; i++) {
const a = i * 2 * Math.PI / 16;
let x = c.x + 25 * Math.cos(a);
let y = c.y + 25 * Math.sin(a);
ctx.strokeStyle = '#dc2626';
ctx.lineWidth = 1.2;
ctx.globalAlpha = 0.75;
ctx.beginPath();
ctx.moveTo(x, y);
for (let step = 0; step < 200; step++) {
const e = E(x, y);
if (e.mag < 0.01) break;
const dx = e.ex / e.mag * 3;
const dy = e.ey / e.mag * 3;
x += dx; y += dy;
if (x < 0 || x > W || y < 0 || y > H) break;
/* Stop near - charge */
let nearNeg = false;
for (const neg of charges) {
if (neg.sign < 0 && (x - neg.x)**2 + (y - neg.y)**2 < 600) { nearNeg = true; break; }
}
ctx.lineTo(x, y);
if (nearNeg) break;
}
ctx.stroke();
ctx.globalAlpha = 1;
}
});
/* Charges */
charges.forEach(c => {
const color = c.sign > 0 ? '#dc2626' : '#2563eb';
const fill = c.sign > 0 ? '#fecaca' : '#bfdbfe';
ctx.fillStyle = fill;
ctx.strokeStyle = color;
ctx.lineWidth = 2.5;
ctx.beginPath();
ctx.arc(c.x, c.y, 20, 0, 2 * Math.PI);
ctx.fill();
ctx.stroke();
ctx.fillStyle = color;
ctx.font = "bold 20px sans-serif";
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(c.sign > 0 ? '+' : '', c.x, c.y + 1);
});
}
const drag = P8Drag.attachCanvas(canvas, {
objects: charges,
onDrag: () => draw()
});
document.getElementById('p17-iv6-add-pos').onclick = () => {
charges.push({ x: 100 + Math.random() * (W - 200), y: 80 + Math.random() * (H - 160), sign: 1, r: 22 });
drag.updateObjects(charges);
draw();
};
document.getElementById('p17-iv6-add-neg').onclick = () => {
charges.push({ x: 100 + Math.random() * (W - 200), y: 80 + Math.random() * (H - 160), sign: -1, r: 22 });
drag.updateObjects(charges);
draw();
};
document.getElementById('p17-iv6-clear').onclick = () => {
charges.length = 0;
charges.push({ x: 200, y: 160, sign: 1, r: 22 }, { x: 360, y: 160, sign: -1, r: 22 });
drag.updateObjects(charges);
draw();
};
draw();
}
`;
replaceStub('p17', 17, P17_HTML, P17_INIT);
// ============================================================
// §22 — Ohm's law sandbox
// ============================================================
const P22_HTML = `/* IV6 — Ohm's Law (Phase 2.2) */
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">Закон Ома: $I = U/R$</div></div>'
+'<div class="wg-help">Двигай напряжение $U$ и сопротивление $R$. Ток $I = U/R$ обновляется в реальном времени. Лампочка светится ярче с ростом тока.</div>'
+'<div class="p8-sandbox" id="p22-iv6-sandbox" style="height:220px"></div>'
+'<div style="margin-top:10px;display:grid;grid-template-columns:1fr 1fr;gap:8px">'
+'<div class="p8-scrubber"><span class="p8-scrubber-label">U</span><input type="range" id="p22-iv6-u" min="0.5" max="12" step="0.1" value="6"><span class="p8-scrubber-value"><span id="p22-iv6-u-val">6.0</span><span class="p8-unit">В</span></span></div>'
+'<div class="p8-scrubber"><span class="p8-scrubber-label">R</span><input type="range" id="p22-iv6-r" min="1" max="100" step="1" value="12"><span class="p8-scrubber-value"><span id="p22-iv6-r-val">12</span><span class="p8-unit">Ом</span></span></div>'
+'</div>'
+'<div style="margin-top:8px"><div class="p8-readout"><span class="p8-readout-label">I = U/R</span><span class="p8-readout-value" id="p22-iv6-i">0.50</span><span class="p8-readout-unit">А</span></div></div>'
+'</div>';`;
const P22_INIT = `
function _initP22_iv6(){
const sb = document.getElementById('p22-iv6-sandbox');
if (!sb || !window.P8Helpers) return;
const svg = P8Helpers.svg.create(560, 220);
svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block';
sb.appendChild(svg);
let U = 6, R = 12;
function render(){
svg.innerHTML = '';
const I = U / R;
/* Circuit */
/* Battery */
svg.appendChild(P8Helpers.em.circuitComponent('battery', 120, 110, 'h', U+' В'));
/* Resistor */
svg.appendChild(P8Helpers.em.circuitComponent('resistor', 280, 110, 'h', R+' Ом'));
/* Lamp (brightness varies with I) */
const lampG = P8Helpers.svg.el('g', { transform: 'translate(440, 110)' });
const brightness = Math.min(1, I / 1.5);
lampG.appendChild(P8Helpers.svg.el('circle', { cx: 0, cy: 0, r: 26, fill: '#fef3c7', opacity: brightness * 0.6 + 0.1 }));
lampG.appendChild(P8Helpers.svg.el('circle', { cx: 0, cy: 0, r: 16, fill: '#fef3c7', stroke: '#0f172a', 'stroke-width': 2 }));
if (brightness > 0.3) {
lampG.appendChild(P8Helpers.svg.el('circle', { cx: 0, cy: 0, r: 30, fill: 'none', stroke: '#facc15', 'stroke-width': 3, opacity: brightness }));
}
lampG.appendChild(P8Helpers.svg.el('line', { x1: -10, y1: -10, x2: 10, y2: 10, stroke: '#0f172a', 'stroke-width': 1.5 }));
lampG.appendChild(P8Helpers.svg.el('line', { x1: -10, y1: 10, x2: 10, y2: -10, stroke: '#0f172a', 'stroke-width': 1.5 }));
svg.appendChild(lampG);
/* Connect wires */
svg.appendChild(P8Helpers.svg.el('line', { x1: 150, y1: 110, x2: 250, y2: 110, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 310, y1: 110, x2: 414, y2: 110, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 466, y1: 110, x2: 510, y2: 110, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 510, y1: 110, x2: 510, y2: 170, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 90, y1: 110, x2: 90, y2: 170, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 90, y1: 170, x2: 510, y2: 170, stroke: '#0f172a', 'stroke-width': 2 }));
/* Current label */
svg.appendChild(P8Helpers.svg.el('text', { x: 300, y: 195, 'font-family':"'JetBrains Mono',monospace", 'font-size':14, 'font-weight':800, fill:'var(--el-mid,#06b6d4)', 'text-anchor':'middle', text: 'I = '+I.toFixed(2)+' А' }));
document.getElementById('p22-iv6-i').textContent = I.toFixed(2);
}
document.getElementById('p22-iv6-u').oninput = ev => { U = +ev.target.value; document.getElementById('p22-iv6-u-val').textContent = U.toFixed(1); render(); };
document.getElementById('p22-iv6-r').oninput = ev => { R = +ev.target.value; document.getElementById('p22-iv6-r-val').textContent = R; render(); };
render();
}
`;
replaceStub('p22', 22, P22_HTML, P22_INIT);
// ============================================================
// §25 — Parallel resistors
// ============================================================
const P25_HTML = `/* IV6 — Parallel resistors (Phase 2.2) */
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">Параллельные резисторы: $1/R = 1/R_1 + 1/R_2$</div></div>'
+'<div class="wg-help">Двигай $R_1, R_2$ — наблюдай как ток делится между ветвями ($I = I_1 + I_2$) и какое получается общее $R$.</div>'
+'<div class="p8-sandbox" id="p25-iv6-sandbox" style="height:240px"></div>'
+'<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="p25-iv6-r1" min="1" max="100" step="1" value="20"><span class="p8-scrubber-value"><span id="p25-iv6-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="p25-iv6-r2" min="1" max="100" step="1" value="30"><span class="p8-scrubber-value"><span id="p25-iv6-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">R_общ</span><span class="p8-readout-value" id="p25-iv6-r">12</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="p25-iv6-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="p25-iv6-i2">0.4</span><span class="p8-readout-unit">А</span></div>'
+'</div>'
+'</div>';`;
const P25_INIT = `
function _initP25_iv6(){
const sb = document.getElementById('p25-iv6-sandbox');
if (!sb || !window.P8Helpers) return;
const svg = P8Helpers.svg.create(560, 240);
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 R = 1 / (1/R1 + 1/R2);
const I1 = U / R1, I2 = U / R2, I = I1 + I2;
/* Battery left */
svg.appendChild(P8Helpers.em.circuitComponent('battery', 80, 120, 'h', U+' В'));
/* Branch split */
svg.appendChild(P8Helpers.svg.el('line', { x1: 110, y1: 120, x2: 200, y2: 120, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 200, y1: 60, x2: 200, y2: 180, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 380, y1: 60, x2: 380, y2: 180, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 200, y1: 60, x2: 290, y2: 60, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 320, y1: 60, x2: 380, y2: 60, 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 }));
/* R1 (top) */
svg.appendChild(P8Helpers.em.circuitComponent('resistor', 305, 60, 'h', R1+' Ом'));
/* R2 (bottom) */
svg.appendChild(P8Helpers.em.circuitComponent('resistor', 305, 180, 'h', R2+' Ом'));
/* Right wire */
svg.appendChild(P8Helpers.svg.el('line', { x1: 380, y1: 120, x2: 510, y2: 120, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 510, y1: 120, x2: 510, y2: 210, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 50, y1: 120, x2: 50, y2: 210, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 50, y1: 210, x2: 510, y2: 210, stroke: '#0f172a', 'stroke-width': 2 }));
/* Current labels */
svg.appendChild(P8Helpers.svg.el('text', { x: 290, y: 48, 'font-family':"'JetBrains Mono',monospace", 'font-size':11, 'font-weight':700, fill:'var(--el-mid,#06b6d4)', 'text-anchor':'middle', text: 'I₁ = '+I1.toFixed(2)+' А' }));
svg.appendChild(P8Helpers.svg.el('text', { x: 290, y: 218, 'font-family':"'JetBrains Mono',monospace", 'font-size':11, 'font-weight':700, fill:'var(--el-mid,#06b6d4)', 'text-anchor':'middle', text: 'I₂ = '+I2.toFixed(2)+' А' }));
svg.appendChild(P8Helpers.svg.el('text', { x: 150, y: 138, 'font-family':"'JetBrains Mono',monospace", 'font-size':11, 'font-weight':700, fill:'#dc2626', 'text-anchor':'middle', text: 'I = '+I.toFixed(2)+' А' }));
document.getElementById('p25-iv6-r').textContent = R.toFixed(1);
document.getElementById('p25-iv6-i1').textContent = I1.toFixed(2);
document.getElementById('p25-iv6-i2').textContent = I2.toFixed(2);
}
document.getElementById('p25-iv6-r1').oninput = ev => { R1 = +ev.target.value; document.getElementById('p25-iv6-r1-val').textContent = R1; render(); };
document.getElementById('p25-iv6-r2').oninput = ev => { R2 = +ev.target.value; document.getElementById('p25-iv6-r2-val').textContent = R2; render(); };
render();
}
`;
replaceStub('p25', 25, P25_HTML, P25_INIT);
// ============================================================
// §28 — Magnet polarity demo
// ============================================================
const P28_HTML = `/* IV6 — Magnet polarity (Phase 2.2) */
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">Магниты: разноимённые притягиваются</div></div>'
+'<div class="wg-help">Перетаскивай магниты. При сближении одноимённых полюсов (N-N или S-S) — отталкивание (зелёные стрелки). Разноимённых (N-S) — притяжение (красные стрелки).</div>'
+'<div class="p8-sandbox" id="p28-iv6-sandbox" style="height:240px"></div>'
+'</div>';`;
const P28_INIT = `
function _initP28_iv6(){
const sb = document.getElementById('p28-iv6-sandbox');
if (!sb || !window.P8Drag) return;
const W = 560, H = 240;
const canvas = document.createElement('canvas');
canvas.width = W; canvas.height = H;
canvas.style.width='100%'; canvas.style.height='100%'; canvas.style.display='block';
sb.appendChild(canvas);
const ctx = canvas.getContext('2d');
const magnets = [
{ x: 140, y: 120, angle: 0, r: 50 },
{ x: 420, y: 120, angle: 0, r: 50 }
];
function drawMagnet(m){
const w = 100, h = 32;
ctx.save();
ctx.translate(m.x, m.y);
ctx.rotate(m.angle);
/* N half (red) */
ctx.fillStyle = '#dc2626';
ctx.fillRect(-w/2, -h/2, w/2, h);
/* S half (blue) */
ctx.fillStyle = '#2563eb';
ctx.fillRect(0, -h/2, w/2, h);
ctx.strokeStyle = '#0f172a';
ctx.lineWidth = 2;
ctx.strokeRect(-w/2, -h/2, w, h);
ctx.fillStyle = '#fff';
ctx.font = "bold 18px sans-serif";
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('N', -w/4, 0);
ctx.fillText('S', w/4, 0);
ctx.restore();
}
function draw(){
ctx.fillStyle = '#fafafa';
ctx.fillRect(0, 0, W, H);
/* Compute interaction between the two magnets — their inner poles */
/* Magnet 1: right side is S (blue, at +50), Magnet 2: left side is N (red, at -50) */
const m1S_x = magnets[0].x + 50 * Math.cos(magnets[0].angle);
const m1S_y = magnets[0].y + 50 * Math.sin(magnets[0].angle);
const m2N_x = magnets[1].x - 50 * Math.cos(magnets[1].angle);
const m2N_y = magnets[1].y - 50 * Math.sin(magnets[1].angle);
const dx = m2N_x - m1S_x;
const dy = m2N_y - m1S_y;
const dist = Math.sqrt(dx*dx + dy*dy);
if (dist < 250 && dist > 30) {
/* N-S → attraction */
const F = 5000 / (dist * dist);
const ux = dx / dist, uy = dy / dist;
const len = Math.min(50, F * 50);
const color = '#dc2626';
/* Arrow 1 from m1S toward m2N */
ctx.strokeStyle = color; ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(m1S_x, m1S_y);
ctx.lineTo(m1S_x + ux * len, m1S_y + uy * len);
ctx.stroke();
/* Arrow 2 from m2N back */
ctx.beginPath();
ctx.moveTo(m2N_x, m2N_y);
ctx.lineTo(m2N_x - ux * len, m2N_y - uy * len);
ctx.stroke();
ctx.fillStyle = color;
ctx.font = "bold 12px sans-serif";
ctx.textAlign = 'center';
ctx.fillText('притяжение', (m1S_x + m2N_x)/2, (m1S_y + m2N_y)/2 - 12);
}
magnets.forEach(drawMagnet);
}
/* Drag */
const dragObjs = magnets.map((m, i) => ({ x: m.x, y: m.y, r: 50, idx: i }));
const drag = P8Drag.attachCanvas(canvas, {
objects: dragObjs,
onDrag: (obj, pos) => {
magnets[obj.idx].x = pos.x;
magnets[obj.idx].y = pos.y;
draw();
}
});
draw();
}
`;
replaceStub('p28', 28, P28_HTML, P28_INIT);
// ============================================================
// §30 — Эрстед: wire + compass
// ============================================================
const P30_HTML = `/* IV6 — Эрстед (Phase 2.2) */
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">Опыт Эрстеда: ток отклоняет стрелку</div></div>'
+'<div class="wg-help">Включи ток в проводнике скрубером — стрелка компаса отклоняется. Направление поля вокруг провода определяется правилом правой руки.</div>'
+'<div class="p8-sandbox" id="p30-iv6-sandbox" style="height:240px"></div>'
+'<div style="margin-top:10px;display:flex;gap:14px;flex-wrap:wrap">'
+'<div class="p8-scrubber" style="flex:1;min-width:240px"><span class="p8-scrubber-label">Ток</span><input type="range" id="p30-iv6-i" min="-5" max="5" step="0.1" value="0"><span class="p8-scrubber-value"><span id="p30-iv6-i-val">0.0</span><span class="p8-unit">А</span></span></div>'
+'<div class="p8-readout"><span class="p8-readout-label">Угол</span><span class="p8-readout-value" id="p30-iv6-ang">0</span><span class="p8-readout-unit">°</span></div>'
+'</div>'
+'</div>';`;
const P30_INIT = `
function _initP30_iv6(){
const sb = document.getElementById('p30-iv6-sandbox');
if (!sb || !window.P8Helpers) return;
const svg = P8Helpers.svg.create(560, 240);
svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block';
sb.appendChild(svg);
let I = 0;
function render(){
svg.innerHTML = '';
/* Wire (horizontal) */
svg.appendChild(P8Helpers.svg.el('line', { x1: 40, y1: 120, x2: 520, y2: 120, stroke: '#0f172a', 'stroke-width': 5 }));
/* Current arrow direction */
if (Math.abs(I) > 0.05) {
const dir = I > 0 ? 1 : -1;
const arrowX = 320;
svg.appendChild(P8Helpers.svg.el('polygon', {
points: dir > 0 ? (arrowX+8)+',120 '+(arrowX-12)+',114 '+(arrowX-12)+',126' : (arrowX-8)+',120 '+(arrowX+12)+',114 '+(arrowX+12)+',126',
fill: '#dc2626'
}));
svg.appendChild(P8Helpers.svg.el('text', { x: 100, y: 110, 'font-family':"'JetBrains Mono',monospace", 'font-size':11, 'font-weight':700, fill:'#dc2626', text: 'I = '+I.toFixed(1)+' А' }));
}
/* Field lines around wire (concentric circles) */
const intensity = Math.abs(I) / 5;
if (intensity > 0.05) {
[30, 50, 70, 90].forEach((r, i) => {
svg.appendChild(P8Helpers.svg.el('circle', { cx: 280, cy: 120, r, fill: 'none', stroke: '#7c3aed', 'stroke-width': 1.5, opacity: intensity * (1 - i * 0.15), 'stroke-dasharray': '5 3' }));
});
}
/* Compass below wire (initially N up = 0°) */
const angle = Math.atan2(0, 1) * 180 / Math.PI; /* baseline */
/* Angle deflection ∝ I (sign determines direction) */
const deflection = Math.atan(I * 0.5) * 60; /* approx */
/* Compass body */
svg.appendChild(P8Helpers.svg.el('circle', { cx: 280, cy: 195, r: 28, fill: '#fff', stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('text', { x: 280, y: 172, 'font-family':"'Unbounded',sans-serif", 'font-size':10, 'font-weight':800, fill:'#dc2626', 'text-anchor':'middle', text: 'N' }));
/* Needle */
const needleG = P8Helpers.svg.el('g', { transform: 'translate(280, 195) rotate('+deflection+')' });
needleG.appendChild(P8Helpers.svg.el('polygon', { points: '-2,-22 2,-22 0,-2', fill: '#dc2626' }));
needleG.appendChild(P8Helpers.svg.el('polygon', { points: '-2,22 2,22 0,2', fill: '#475569' }));
needleG.appendChild(P8Helpers.svg.el('circle', { cx: 0, cy: 0, r: 3, fill: '#0f172a' }));
svg.appendChild(needleG);
document.getElementById('p30-iv6-ang').textContent = Math.round(deflection);
}
document.getElementById('p30-iv6-i').oninput = ev => { I = +ev.target.value; document.getElementById('p30-iv6-i-val').textContent = I.toFixed(1); render(); };
render();
}
`;
replaceStub('p30', 30, P30_HTML, P30_INIT);
fs.writeFileSync(DST, h);
console.log('ch2 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, fns);
+471 -37
View File
@@ -1,4 +1,421 @@
<!
function _initP30_iv6(){
const sb = document.getElementById('p30-iv6-sandbox');
if (!sb || !window.P8Helpers) return;
const svg = P8Helpers.svg.create(560, 240);
svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block';
sb.appendChild(svg);
let I = 0;
function render(){
svg.innerHTML = '';
/* Wire (horizontal) */
svg.appendChild(P8Helpers.svg.el('line', { x1: 40, y1: 120, x2: 520, y2: 120, stroke: '#0f172a', 'stroke-width': 5 }));
/* Current arrow direction */
if (Math.abs(I) > 0.05) {
const dir = I > 0 ? 1 : -1;
const arrowX = 320;
svg.appendChild(P8Helpers.svg.el('polygon', {
points: dir > 0 ? (arrowX+8)+',120 '+(arrowX-12)+',114 '+(arrowX-12)+',126' : (arrowX-8)+',120 '+(arrowX+12)+',114 '+(arrowX+12)+',126',
fill: '#dc2626'
}));
svg.appendChild(P8Helpers.svg.el('text', { x: 100, y: 110, 'font-family':"'JetBrains Mono',monospace", 'font-size':11, 'font-weight':700, fill:'#dc2626', text: 'I = '+I.toFixed(1)+' А' }));
}
/* Field lines around wire (concentric circles) */
const intensity = Math.abs(I) / 5;
if (intensity > 0.05) {
[30, 50, 70, 90].forEach((r, i) => {
svg.appendChild(P8Helpers.svg.el('circle', { cx: 280, cy: 120, r, fill: 'none', stroke: '#7c3aed', 'stroke-width': 1.5, opacity: intensity * (1 - i * 0.15), 'stroke-dasharray': '5 3' }));
});
}
/* Compass below wire (initially N up = 0°) */
const angle = Math.atan2(0, 1) * 180 / Math.PI; /* baseline */
/* Angle deflection ∝ I (sign determines direction) */
const deflection = Math.atan(I * 0.5) * 60; /* approx */
/* Compass body */
svg.appendChild(P8Helpers.svg.el('circle', { cx: 280, cy: 195, r: 28, fill: '#fff', stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('text', { x: 280, y: 172, 'font-family':"'Unbounded',sans-serif", 'font-size':10, 'font-weight':800, fill:'#dc2626', 'text-anchor':'middle', text: 'N' }));
/* Needle */
const needleG = P8Helpers.svg.el('g', { transform: 'translate(280, 195) rotate('+deflection+')' });
needleG.appendChild(P8Helpers.svg.el('polygon', { points: '-2,-22 2,-22 0,-2', fill: '#dc2626' }));
needleG.appendChild(P8Helpers.svg.el('polygon', { points: '-2,22 2,22 0,2', fill: '#475569' }));
needleG.appendChild(P8Helpers.svg.el('circle', { cx: 0, cy: 0, r: 3, fill: '#0f172a' }));
svg.appendChild(needleG);
document.getElementById('p30-iv6-ang').textContent = Math.round(deflection);
}
document.getElementById('p30-iv6-i').oninput = ev => { I = +ev.target.value; document.getElementById('p30-iv6-i-val').textContent = I.toFixed(1); render(); };
render();
}
function _initP28_iv6(){
const sb = document.getElementById('p28-iv6-sandbox');
if (!sb || !window.P8Drag) return;
const W = 560, H = 240;
const canvas = document.createElement('canvas');
canvas.width = W; canvas.height = H;
canvas.style.width='100%'; canvas.style.height='100%'; canvas.style.display='block';
sb.appendChild(canvas);
const ctx = canvas.getContext('2d');
const magnets = [
{ x: 140, y: 120, angle: 0, r: 50 },
{ x: 420, y: 120, angle: 0, r: 50 }
];
function drawMagnet(m){
const w = 100, h = 32;
ctx.save();
ctx.translate(m.x, m.y);
ctx.rotate(m.angle);
/* N half (red) */
ctx.fillStyle = '#dc2626';
ctx.fillRect(-w/2, -h/2, w/2, h);
/* S half (blue) */
ctx.fillStyle = '#2563eb';
ctx.fillRect(0, -h/2, w/2, h);
ctx.strokeStyle = '#0f172a';
ctx.lineWidth = 2;
ctx.strokeRect(-w/2, -h/2, w, h);
ctx.fillStyle = '#fff';
ctx.font = "bold 18px sans-serif";
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('N', -w/4, 0);
ctx.fillText('S', w/4, 0);
ctx.restore();
}
function draw(){
ctx.fillStyle = '#fafafa';
ctx.fillRect(0, 0, W, H);
/* Compute interaction between the two magnets — their inner poles */
/* Magnet 1: right side is S (blue, at +50), Magnet 2: left side is N (red, at -50) */
const m1S_x = magnets[0].x + 50 * Math.cos(magnets[0].angle);
const m1S_y = magnets[0].y + 50 * Math.sin(magnets[0].angle);
const m2N_x = magnets[1].x - 50 * Math.cos(magnets[1].angle);
const m2N_y = magnets[1].y - 50 * Math.sin(magnets[1].angle);
const dx = m2N_x - m1S_x;
const dy = m2N_y - m1S_y;
const dist = Math.sqrt(dx*dx + dy*dy);
if (dist < 250 && dist > 30) {
/* N-S → attraction */
const F = 5000 / (dist * dist);
const ux = dx / dist, uy = dy / dist;
const len = Math.min(50, F * 50);
const color = '#dc2626';
/* Arrow 1 from m1S toward m2N */
ctx.strokeStyle = color; ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(m1S_x, m1S_y);
ctx.lineTo(m1S_x + ux * len, m1S_y + uy * len);
ctx.stroke();
/* Arrow 2 from m2N back */
ctx.beginPath();
ctx.moveTo(m2N_x, m2N_y);
ctx.lineTo(m2N_x - ux * len, m2N_y - uy * len);
ctx.stroke();
ctx.fillStyle = color;
ctx.font = "bold 12px sans-serif";
ctx.textAlign = 'center';
ctx.fillText('притяжение', (m1S_x + m2N_x)/2, (m1S_y + m2N_y)/2 - 12);
}
magnets.forEach(drawMagnet);
}
/* Drag */
const dragObjs = magnets.map((m, i) => ({ x: m.x, y: m.y, r: 50, idx: i }));
const drag = P8Drag.attachCanvas(canvas, {
objects: dragObjs,
onDrag: (obj, pos) => {
magnets[obj.idx].x = pos.x;
magnets[obj.idx].y = pos.y;
draw();
}
});
draw();
}
function _initP25_iv6(){
const sb = document.getElementById('p25-iv6-sandbox');
if (!sb || !window.P8Helpers) return;
const svg = P8Helpers.svg.create(560, 240);
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 R = 1 / (1/R1 + 1/R2);
const I1 = U / R1, I2 = U / R2, I = I1 + I2;
/* Battery left */
svg.appendChild(P8Helpers.em.circuitComponent('battery', 80, 120, 'h', U+' В'));
/* Branch split */
svg.appendChild(P8Helpers.svg.el('line', { x1: 110, y1: 120, x2: 200, y2: 120, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 200, y1: 60, x2: 200, y2: 180, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 380, y1: 60, x2: 380, y2: 180, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 200, y1: 60, x2: 290, y2: 60, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 320, y1: 60, x2: 380, y2: 60, 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 }));
/* R1 (top) */
svg.appendChild(P8Helpers.em.circuitComponent('resistor', 305, 60, 'h', R1+' Ом'));
/* R2 (bottom) */
svg.appendChild(P8Helpers.em.circuitComponent('resistor', 305, 180, 'h', R2+' Ом'));
/* Right wire */
svg.appendChild(P8Helpers.svg.el('line', { x1: 380, y1: 120, x2: 510, y2: 120, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 510, y1: 120, x2: 510, y2: 210, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 50, y1: 120, x2: 50, y2: 210, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 50, y1: 210, x2: 510, y2: 210, stroke: '#0f172a', 'stroke-width': 2 }));
/* Current labels */
svg.appendChild(P8Helpers.svg.el('text', { x: 290, y: 48, 'font-family':"'JetBrains Mono',monospace", 'font-size':11, 'font-weight':700, fill:'var(--el-mid,#06b6d4)', 'text-anchor':'middle', text: 'I₁ = '+I1.toFixed(2)+' А' }));
svg.appendChild(P8Helpers.svg.el('text', { x: 290, y: 218, 'font-family':"'JetBrains Mono',monospace", 'font-size':11, 'font-weight':700, fill:'var(--el-mid,#06b6d4)', 'text-anchor':'middle', text: 'I₂ = '+I2.toFixed(2)+' А' }));
svg.appendChild(P8Helpers.svg.el('text', { x: 150, y: 138, 'font-family':"'JetBrains Mono',monospace", 'font-size':11, 'font-weight':700, fill:'#dc2626', 'text-anchor':'middle', text: 'I = '+I.toFixed(2)+' А' }));
document.getElementById('p25-iv6-r').textContent = R.toFixed(1);
document.getElementById('p25-iv6-i1').textContent = I1.toFixed(2);
document.getElementById('p25-iv6-i2').textContent = I2.toFixed(2);
}
document.getElementById('p25-iv6-r1').oninput = ev => { R1 = +ev.target.value; document.getElementById('p25-iv6-r1-val').textContent = R1; render(); };
document.getElementById('p25-iv6-r2').oninput = ev => { R2 = +ev.target.value; document.getElementById('p25-iv6-r2-val').textContent = R2; render(); };
render();
}
function _initP22_iv6(){
const sb = document.getElementById('p22-iv6-sandbox');
if (!sb || !window.P8Helpers) return;
const svg = P8Helpers.svg.create(560, 220);
svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block';
sb.appendChild(svg);
let U = 6, R = 12;
function render(){
svg.innerHTML = '';
const I = U / R;
/* Circuit */
/* Battery */
svg.appendChild(P8Helpers.em.circuitComponent('battery', 120, 110, 'h', U+' В'));
/* Resistor */
svg.appendChild(P8Helpers.em.circuitComponent('resistor', 280, 110, 'h', R+' Ом'));
/* Lamp (brightness varies with I) */
const lampG = P8Helpers.svg.el('g', { transform: 'translate(440, 110)' });
const brightness = Math.min(1, I / 1.5);
lampG.appendChild(P8Helpers.svg.el('circle', { cx: 0, cy: 0, r: 26, fill: '#fef3c7', opacity: brightness * 0.6 + 0.1 }));
lampG.appendChild(P8Helpers.svg.el('circle', { cx: 0, cy: 0, r: 16, fill: '#fef3c7', stroke: '#0f172a', 'stroke-width': 2 }));
if (brightness > 0.3) {
lampG.appendChild(P8Helpers.svg.el('circle', { cx: 0, cy: 0, r: 30, fill: 'none', stroke: '#facc15', 'stroke-width': 3, opacity: brightness }));
}
lampG.appendChild(P8Helpers.svg.el('line', { x1: -10, y1: -10, x2: 10, y2: 10, stroke: '#0f172a', 'stroke-width': 1.5 }));
lampG.appendChild(P8Helpers.svg.el('line', { x1: -10, y1: 10, x2: 10, y2: -10, stroke: '#0f172a', 'stroke-width': 1.5 }));
svg.appendChild(lampG);
/* Connect wires */
svg.appendChild(P8Helpers.svg.el('line', { x1: 150, y1: 110, x2: 250, y2: 110, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 310, y1: 110, x2: 414, y2: 110, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 466, y1: 110, x2: 510, y2: 110, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 510, y1: 110, x2: 510, y2: 170, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 90, y1: 110, x2: 90, y2: 170, stroke: '#0f172a', 'stroke-width': 2 }));
svg.appendChild(P8Helpers.svg.el('line', { x1: 90, y1: 170, x2: 510, y2: 170, stroke: '#0f172a', 'stroke-width': 2 }));
/* Current label */
svg.appendChild(P8Helpers.svg.el('text', { x: 300, y: 195, 'font-family':"'JetBrains Mono',monospace", 'font-size':14, 'font-weight':800, fill:'var(--el-mid,#06b6d4)', 'text-anchor':'middle', text: 'I = '+I.toFixed(2)+' А' }));
document.getElementById('p22-iv6-i').textContent = I.toFixed(2);
}
document.getElementById('p22-iv6-u').oninput = ev => { U = +ev.target.value; document.getElementById('p22-iv6-u-val').textContent = U.toFixed(1); render(); };
document.getElementById('p22-iv6-r').oninput = ev => { R = +ev.target.value; document.getElementById('p22-iv6-r-val').textContent = R; render(); };
render();
}
function _initP17_iv6(){
const sb = document.getElementById('p17-iv6-sandbox');
if (!sb || !window.P8Drag) return;
const W = 560, H = 320;
const canvas = document.createElement('canvas');
canvas.width = W; canvas.height = H;
canvas.style.width = '100%'; canvas.style.height = '100%'; canvas.style.display = 'block';
sb.appendChild(canvas);
const ctx = canvas.getContext('2d');
let charges = [
{ x: 200, y: 160, sign: 1, r: 22 },
{ x: 360, y: 160, sign: -1, r: 22 }
];
function E(x, y) {
let ex = 0, ey = 0;
charges.forEach(c => {
const dx = x - c.x, dy = y - c.y;
const r2 = dx*dx + dy*dy;
if (r2 < 200) return;
const r = Math.sqrt(r2);
const k = 5000 * c.sign / r2;
ex += k * dx / r; ey += k * dy / r;
});
return { ex, ey, mag: Math.sqrt(ex*ex + ey*ey) };
}
function draw(){
ctx.fillStyle = '#fafafa';
ctx.fillRect(0, 0, W, H);
/* Draw field lines starting from + charges */
charges.filter(c => c.sign > 0).forEach(c => {
for (let i = 0; i < 16; i++) {
const a = i * 2 * Math.PI / 16;
let x = c.x + 25 * Math.cos(a);
let y = c.y + 25 * Math.sin(a);
ctx.strokeStyle = '#dc2626';
ctx.lineWidth = 1.2;
ctx.globalAlpha = 0.75;
ctx.beginPath();
ctx.moveTo(x, y);
for (let step = 0; step < 200; step++) {
const e = E(x, y);
if (e.mag < 0.01) break;
const dx = e.ex / e.mag * 3;
const dy = e.ey / e.mag * 3;
x += dx; y += dy;
if (x < 0 || x > W || y < 0 || y > H) break;
/* Stop near - charge */
let nearNeg = false;
for (const neg of charges) {
if (neg.sign < 0 && (x - neg.x)**2 + (y - neg.y)**2 < 600) { nearNeg = true; break; }
}
ctx.lineTo(x, y);
if (nearNeg) break;
}
ctx.stroke();
ctx.globalAlpha = 1;
}
});
/* Charges */
charges.forEach(c => {
const color = c.sign > 0 ? '#dc2626' : '#2563eb';
const fill = c.sign > 0 ? '#fecaca' : '#bfdbfe';
ctx.fillStyle = fill;
ctx.strokeStyle = color;
ctx.lineWidth = 2.5;
ctx.beginPath();
ctx.arc(c.x, c.y, 20, 0, 2 * Math.PI);
ctx.fill();
ctx.stroke();
ctx.fillStyle = color;
ctx.font = "bold 20px sans-serif";
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(c.sign > 0 ? '+' : '', c.x, c.y + 1);
});
}
const drag = P8Drag.attachCanvas(canvas, {
objects: charges,
onDrag: () => draw()
});
document.getElementById('p17-iv6-add-pos').onclick = () => {
charges.push({ x: 100 + Math.random() * (W - 200), y: 80 + Math.random() * (H - 160), sign: 1, r: 22 });
drag.updateObjects(charges);
draw();
};
document.getElementById('p17-iv6-add-neg').onclick = () => {
charges.push({ x: 100 + Math.random() * (W - 200), y: 80 + Math.random() * (H - 160), sign: -1, r: 22 });
drag.updateObjects(charges);
draw();
};
document.getElementById('p17-iv6-clear').onclick = () => {
charges.length = 0;
charges.push({ x: 200, y: 160, sign: 1, r: 22 }, { x: 360, y: 160, sign: -1, r: 22 });
drag.updateObjects(charges);
draw();
};
draw();
}
function _initP12_iv6(){
const sb = document.getElementById('p12-iv6-sandbox');
if (!sb || !window.P8Helpers || !window.P8Drag) return;
const W = 560, H = 300;
const canvas = document.createElement('canvas');
canvas.width = W; canvas.height = H;
canvas.style.width = '100%'; canvas.style.height = '100%'; canvas.style.display = 'block';
sb.appendChild(canvas);
const ctx = canvas.getContext('2d');
const charges = [];
let nextSign = 1;
function draw(){
ctx.fillStyle = '#fafafa';
ctx.fillRect(0, 0, W, H);
/* Forces between pairs */
for (let i = 0; i < charges.length; i++) {
for (let j = i + 1; j < charges.length; j++) {
const a = charges[i], b = charges[j];
const dx = b.x - a.x, dy = b.y - a.y;
const r2 = dx*dx + dy*dy;
if (r2 < 100) continue;
const r = Math.sqrt(r2);
const F = 4e6 * a.sign * b.sign / r2;
const fx = F * dx / r, fy = F * dy / r;
/* Arrow from a in direction (-fx, -fy) means: force on a from b */
const len = Math.min(80, Math.abs(F) * 5);
const dir = a.sign * b.sign > 0 ? -1 : 1;
const aex = a.x + dir * fx / Math.abs(F) * len;
const aey = a.y + dir * fy / Math.abs(F) * len;
ctx.strokeStyle = a.sign * b.sign > 0 ? '#dc2626' : '#16a34a';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(aex, aey);
ctx.stroke();
/* Arrowhead */
const ang = Math.atan2(aey - a.y, aex - a.x);
ctx.beginPath();
ctx.moveTo(aex, aey);
ctx.lineTo(aex - 7 * Math.cos(ang - 0.3), aey - 7 * Math.sin(ang - 0.3));
ctx.lineTo(aex - 7 * Math.cos(ang + 0.3), aey - 7 * Math.sin(ang + 0.3));
ctx.closePath();
ctx.fillStyle = ctx.strokeStyle;
ctx.fill();
}
}
/* Charges */
charges.forEach(c => {
const color = c.sign > 0 ? '#dc2626' : '#2563eb';
const fill = c.sign > 0 ? '#fecaca' : '#bfdbfe';
ctx.fillStyle = fill;
ctx.strokeStyle = color;
ctx.lineWidth = 2.5;
ctx.beginPath();
ctx.arc(c.x, c.y, 18, 0, 2 * Math.PI);
ctx.fill();
ctx.stroke();
ctx.fillStyle = color;
ctx.font = "bold 18px sans-serif";
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(c.sign > 0 ? '+' : '', c.x, c.y + 1);
});
document.getElementById('p12-iv6-count').textContent = charges.length;
}
const drag = P8Drag.attachCanvas(canvas, {
objects: charges.map(c => ({ ...c, r: 22 })),
onPickup: c => {},
onDrag: (c, pos) => {
/* Sync back to charges by id */
const orig = charges.find(ch => ch === c || (ch.id === c.id));
if (orig) { orig.x = pos.x; orig.y = pos.y; }
draw();
},
onClick: (pos) => {
charges.push({ x: pos.x, y: pos.y, sign: nextSign, id: Date.now() + Math.random() });
drag.updateObjects(charges.map(c => ({ ...c, r: 22 })));
draw();
if (window.addXp && charges.length === 2) addXp(10, 'p12-iv6-first');
}
});
document.getElementById('p12-iv6-add-pos').onclick = () => {
nextSign = 1;
charges.push({ x: 80 + Math.random() * (W - 160), y: 80 + Math.random() * (H - 160), sign: 1, id: Date.now() + Math.random() });
drag.updateObjects(charges.map(c => ({ ...c, r: 22 })));
draw();
};
document.getElementById('p12-iv6-add-neg').onclick = () => {
nextSign = -1;
charges.push({ x: 80 + Math.random() * (W - 160), y: 80 + Math.random() * (H - 160), sign: -1, id: Date.now() + Math.random() });
drag.updateObjects(charges.map(c => ({ ...c, r: 22 })));
draw();
};
document.getElementById('p12-iv6-clear').onclick = () => {
charges.length = 0;
drag.updateObjects([]);
draw();
};
draw();
}
function _initp31_iv5(){
const TASKS = [{"q":"У электромагнита было $N_1 = 100$ витков, его магнитное поле $B_1$. После добавления стало $N_2 = 500$ витков (тот же ток). Во сколько раз вырастет $B$?","ans":5,"tol":0.2,"why":"$B \\propto N$, поэтому $B_2/B_1 = N_2/N_1 = 5$."},{"q":"Ток в катушке вырос с $I_1 = 0{,}2$ А до $I_2 = 1$ А. Во сколько раз увеличилось магнитное поле электромагнита?","ans":5,"tol":0.2,"why":"$B \\propto I$, поэтому $B_2/B_1 = I_2/I_1 = 5$."},{"q":"Без сердечника поле электромагнита $B_0 = 1$ мТл. С железным сердечником стало $B = 1000$ мТл. Во сколько раз сердечник усилил поле?","ans":1000,"tol":10,"why":"Железо имеет магнитную проницаемость $\\mu \\sim 1000$. $B/B_0 = 1000$."},{"q":"Электромагнит подняет груз массой $m = 50$ кг с силой $F = 500$ Н. Какова перегрузка $F/(mg)$? ($g = 10$ м/с²)","ans":1,"tol":0.05,"why":"$F/(mg) = 500/(50 \\cdot 10) = 500/500 = 1$ — сила в точности уравновешивает вес."},{"q":"Если ток отключить, что произойдёт с магн. полем электромагнита? ($1$ — останется, $0$ — исчезнет)","ans":0,"tol":0.1,"why":"Магнитное поле электромагнита создаётся током. Нет тока — нет поля. (В отличие от постоянного магнита.)"}];
let i = 0, ok = 0, awarded = false;
@@ -1407,19 +1824,23 @@ function build_p12(){
+'<div class="score-display" style="margin-top:10px"><span>Задача: <b id="p12-tasks5-i">1</b> / 5</span><span>Правильно: <b id="p12-tasks5-ok">0</b></span></div>'
+'</div>';
/* IV6 — flagship интерактив (заглушка Phase 2, наполнение в Phase 2.12) */
/* IV6 — Charge Sandbox (Phase 2.2) */
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">Новый интерактив §12</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.12 — coming soon</div>'
+'<div class="wg-header"><span class="wg-badge p8-badge p8-badge-electric">IV-6</span><div class="wg-title">Песочница зарядов — наблюдай взаимодействие</div></div>'
+'<div class="wg-help">Клик ЛКМ → добавить +заряд, клик ПКМ → добавить -заряд. Перетаскивай существующие. Стрелки показывают силы взаимодействия (закон Кулона $F = k|q_1 q_2|/r^2$).</div>'
+'<div class="p8-sandbox" id="p12-iv6-sandbox" style="height:300px"></div>'
+'<div style="margin-top:10px;display:flex;gap:10px;flex-wrap:wrap">'
+'<button class="btn primary" id="p12-iv6-add-pos">+ Добавить +</button>'
+'<button class="btn primary" id="p12-iv6-add-neg" style="background:#2563eb;border-color:#2563eb">+ Добавить </button>'
+'<button class="btn" id="p12-iv6-clear">Очистить</button>'
+'<div class="p8-readout"><span class="p8-readout-label">Зарядов</span><span class="p8-readout-value" id="p12-iv6-count">0</span></div>'
+'</div>'
+'</div>';
box.innerHTML = h + secNavFor('p12') + readButton('p12');
renderMath(box);
wireReadBtn('p12');
_initP12_iv6();
_initp12_iv5();
_initP12_sim();
@@ -2600,19 +3021,22 @@ function build_p17(){
+'<div class="score-display" style="margin-top:10px"><span>Задача: <b id="p17-tasks5-i">1</b> / 5</span><span>Правильно: <b id="p17-tasks5-ok">0</b></span></div>'
+'</div>';
/* IV6 — flagship интерактив (заглушка Phase 2, наполнение в Phase 2.17) */
/* IV6 — Field Visualizer (Phase 2.2) */
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">Новый интерактив §17</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.17 — coming soon</div>'
+'<div class="wg-header"><span class="wg-badge p8-badge p8-badge-electric">IV-6</span><div class="wg-title">Силовые линии — карта поля</div></div>'
+'<div class="wg-help">Перетаскивай заряды. Силовые линии рисуются live: выходят из + и заходят в −. Густота линий = напряжённость $E$.</div>'
+'<div class="p8-sandbox" id="p17-iv6-sandbox" style="height:320px"></div>'
+'<div style="margin-top:10px;display:flex;gap:10px;flex-wrap:wrap">'
+'<button class="btn" id="p17-iv6-add-pos">+ Заряд</button>'
+'<button class="btn" id="p17-iv6-add-neg"> Заряд</button>'
+'<button class="btn" id="p17-iv6-clear">Сброс</button>'
+'</div>'
+'</div>';
box.innerHTML = h + secNavFor('p17') + readButton('p17');
renderMath(box);
wireReadBtn('p17');
_initP17_iv6();
_initp17_iv5();
_initP17_field();
@@ -3761,19 +4185,22 @@ function build_p22(){
+'<div class="score-display" style="margin-top:10px"><span>Задача: <b id="p22-task-i">1</b> / 6</span><span>Правильно: <b id="p22-task-ok">0</b></span></div>'
+'</div>';
/* IV6 — flagship интерактив (заглушка Phase 2, наполнение в Phase 2.22) */
/* IV6 — Ohm's Law (Phase 2.2) */
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">Новый интерактив §22</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.22 — coming soon</div>'
+'<div class="wg-header"><span class="wg-badge p8-badge p8-badge-electric">IV-6</span><div class="wg-title">Закон Ома: $I = U/R$</div></div>'
+'<div class="wg-help">Двигай напряжение $U$ и сопротивление $R$. Ток $I = U/R$ обновляется в реальном времени. Лампочка светится ярче с ростом тока.</div>'
+'<div class="p8-sandbox" id="p22-iv6-sandbox" style="height:220px"></div>'
+'<div style="margin-top:10px;display:grid;grid-template-columns:1fr 1fr;gap:8px">'
+'<div class="p8-scrubber"><span class="p8-scrubber-label">U</span><input type="range" id="p22-iv6-u" min="0.5" max="12" step="0.1" value="6"><span class="p8-scrubber-value"><span id="p22-iv6-u-val">6.0</span><span class="p8-unit">В</span></span></div>'
+'<div class="p8-scrubber"><span class="p8-scrubber-label">R</span><input type="range" id="p22-iv6-r" min="1" max="100" step="1" value="12"><span class="p8-scrubber-value"><span id="p22-iv6-r-val">12</span><span class="p8-unit">Ом</span></span></div>'
+'</div>'
+'<div style="margin-top:8px"><div class="p8-readout"><span class="p8-readout-label">I = U/R</span><span class="p8-readout-value" id="p22-iv6-i">0.50</span><span class="p8-readout-unit">А</span></div></div>'
+'</div>';
box.innerHTML = h + secNavFor('p22') + readButton('p22');
renderMath(box);
wireReadBtn('p22');
_initP22_iv6();
_initP22_vah();
_initP22_calc();
@@ -4454,19 +4881,26 @@ function build_p25(){
+'<div class="score-display" style="margin-top:10px"><span>Задача: <b id="p25-task-i">1</b> / 6</span><span>Правильно: <b id="p25-task-ok">0</b></span></div>'
+'</div>';
/* IV6 — flagship интерактив (заглушка Phase 2, наполнение в Phase 2.25) */
/* IV6 — Parallel resistors (Phase 2.2) */
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">Новый интерактив §25</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.25 — coming soon</div>'
+'<div class="wg-header"><span class="wg-badge p8-badge p8-badge-electric">IV-6</span><div class="wg-title">Параллельные резисторы: $1/R = 1/R_1 + 1/R_2$</div></div>'
+'<div class="wg-help">Двигай $R_1, R_2$ — наблюдай как ток делится между ветвями ($I = I_1 + I_2$) и какое получается общее $R$.</div>'
+'<div class="p8-sandbox" id="p25-iv6-sandbox" style="height:240px"></div>'
+'<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="p25-iv6-r1" min="1" max="100" step="1" value="20"><span class="p8-scrubber-value"><span id="p25-iv6-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="p25-iv6-r2" min="1" max="100" step="1" value="30"><span class="p8-scrubber-value"><span id="p25-iv6-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">R_общ</span><span class="p8-readout-value" id="p25-iv6-r">12</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="p25-iv6-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="p25-iv6-i2">0.4</span><span class="p8-readout-unit">А</span></div>'
+'</div>'
+'</div>';
box.innerHTML = h + secNavFor('p25') + readButton('p25');
renderMath(box);
wireReadBtn('p25');
_initP25_iv6();
_initP25_calc();
_initP25_quiz();
@@ -5161,19 +5595,17 @@ function build_p28(){
+'<div class="score-display" style="margin-top:10px"><span>Задача: <b id="p28-tasks5-i">1</b> / 5</span><span>Правильно: <b id="p28-tasks5-ok">0</b></span></div>'
+'</div>';
/* IV6 — flagship интерактив (заглушка Phase 2, наполнение в Phase 2.28) */
/* IV6 — Magnet polarity (Phase 2.2) */
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">Новый интерактив §28</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.28 — coming soon</div>'
+'</div>'
+'<div class="wg-header"><span class="wg-badge p8-badge p8-badge-electric">IV-6</span><div class="wg-title">Магниты: разноимённые притягиваются</div></div>'
+'<div class="wg-help">Перетаскивай магниты. При сближении одноимённых полюсов (N-N или S-S) — отталкивание (зелёные стрелки). Разноимённых (N-S) — притяжение (красные стрелки).</div>'
+'<div class="p8-sandbox" id="p28-iv6-sandbox" style="height:240px"></div>'
+'</div>';
box.innerHTML = h + secNavFor('p28') + readButton('p28');
renderMath(box);
wireReadBtn('p28');
_initP28_iv6();
_initp28_iv5();
_initP28_two();
@@ -5611,19 +6043,21 @@ function build_p30(){
+'<div class="score-display" style="margin-top:10px"><span>Задача: <b id="p30-tasks5-i">1</b> / 5</span><span>Правильно: <b id="p30-tasks5-ok">0</b></span></div>'
+'</div>';
/* IV6 — flagship интерактив (заглушка Phase 2, наполнение в Phase 2.30) */
/* IV6 — Эрстед (Phase 2.2) */
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">Новый интерактив §30</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.30 — coming soon</div>'
+'<div class="wg-header"><span class="wg-badge p8-badge p8-badge-electric">IV-6</span><div class="wg-title">Опыт Эрстеда: ток отклоняет стрелку</div></div>'
+'<div class="wg-help">Включи ток в проводнике скрубером — стрелка компаса отклоняется. Направление поля вокруг провода определяется правилом правой руки.</div>'
+'<div class="p8-sandbox" id="p30-iv6-sandbox" style="height:240px"></div>'
+'<div style="margin-top:10px;display:flex;gap:14px;flex-wrap:wrap">'
+'<div class="p8-scrubber" style="flex:1;min-width:240px"><span class="p8-scrubber-label">Ток</span><input type="range" id="p30-iv6-i" min="-5" max="5" step="0.1" value="0"><span class="p8-scrubber-value"><span id="p30-iv6-i-val">0.0</span><span class="p8-unit">А</span></span></div>'
+'<div class="p8-readout"><span class="p8-readout-label">Угол</span><span class="p8-readout-value" id="p30-iv6-ang">0</span><span class="p8-readout-unit">°</span></div>'
+'</div>'
+'</div>';
box.innerHTML = h + secNavFor('p30') + readButton('p30');
renderMath(box);
wireReadBtn('p30');
_initP30_iv6();
_initp30_iv5();
_initP30_oersted();