// 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 += '
' +'
IV-6
Новый интерактив §${n}
' +'
Готовится: интерактивная визуализация с drag-and-drop для углубления темы. Скоро будет доступна.
' +'
' +'' +'
Phase 2.${n} — coming soon
' +'
' +'
';`; } 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 += '
' +'
IV-6
Песочница зарядов — наблюдай взаимодействие
' +'
Клик ЛКМ → добавить +заряд, клик ПКМ → добавить -заряд. Перетаскивай существующие. Стрелки показывают силы взаимодействия (закон Кулона $F = k|q_1 q_2|/r^2$).
' +'
' +'
' +'' +'' +'' +'
Зарядов0
' +'
' +'
';`; 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 += '
' +'
IV-6
Силовые линии — карта поля
' +'
Перетаскивай заряды. Силовые линии рисуются live: выходят из + и заходят в −. Густота линий = напряжённость $E$.
' +'
' +'
' +'' +'' +'' +'
' +'
';`; 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 += '
' +'
IV-6
Закон Ома: $I = U/R$
' +'
Двигай напряжение $U$ и сопротивление $R$. Ток $I = U/R$ обновляется в реальном времени. Лампочка светится ярче с ростом тока.
' +'
' +'
' +'
U6.0В
' +'
R12Ом
' +'
' +'
I = U/R0.50А
' +'
';`; 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 += '
' +'
IV-6
Параллельные резисторы: $1/R = 1/R_1 + 1/R_2$
' +'
Двигай $R_1, R_2$ — наблюдай как ток делится между ветвями ($I = I_1 + I_2$) и какое получается общее $R$.
' +'
' +'
' +'
R₁20Ом
' +'
R₂30Ом
' +'
' +'
' +'
R_общ12Ом
' +'
I₁0.6А
' +'
I₂0.4А
' +'
' +'
';`; 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 += '
' +'
IV-6
Магниты: разноимённые притягиваются
' +'
Перетаскивай магниты. При сближении одноимённых полюсов (N-N или S-S) — отталкивание (зелёные стрелки). Разноимённых (N-S) — притяжение (красные стрелки).
' +'
' +'
';`; 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 += '
' +'
IV-6
Опыт Эрстеда: ток отклоняет стрелку
' +'
Включи ток в проводнике скрубером — стрелка компаса отклоняется. Направление поля вокруг провода определяется правилом правой руки.
' +'
' +'
' +'
Ток0.0А
' +'
Угол0°
' +'
' +'
';`; 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(/