diff --git a/backend/scripts/redesign_p8_ch2_2.cjs b/backend/scripts/redesign_p8_ch2_2.cjs new file mode 100644 index 0000000..def72c8 --- /dev/null +++ b/backend/scripts/redesign_p8_ch2_2.cjs @@ -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 += '
' + +'
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(/