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:
@@ -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);
|
||||
Reference in New Issue
Block a user