e85f7135ff
Заменены оставшиеся stub'ы (Phase 1. coming soon) на реальные интерактивы. Все 11 параграфов Ch1 теперь имеют flagship IV-6. §2 Способы изменения U — Drag-piston: - Цилиндр с газом, движущийся поршень (scrubber сжатия 0-100%). - Scrubber Q для подачи тепла. Молекулы рисуются динамически (количество ∝ T). Цвет газа по T через P8Helpers.thermal.tempColor. - Readouts T (°C), U (отн.). §4 Конвекция — Animated convection cell: - Canvas-симуляция с 60 частицами, P8Anim.raf. - Поток вверх по центру (нагретая лёгкая вода), вниз по краям. - Скорость потоков ∝ мощности горелки (scrubber). Цвет частиц по локальной T. Кнопки Пуск/Стоп. §5 Излучение — Radiation balance: - Лампа с 3 телами (чёрное, белое, зеркало) разной поглощающей способности (0.95, 0.20, 0.05). - Scrubber мощности лампы. Симуляция P8Anim.raf: T каждого тела растёт ∝ absorption × power. Glow вокруг тёплых тел. §7 Q=qm — Fuel burn: - 3 кнопки палитры топлива (дрова q=10, уголь q=29, газ q=44 МДж/кг). - Кастрюля с водой 1 кг. Сжигание выбранного топлива + scrubber массы. - Q = qm, ΔT = Q/(c·m_в). Пар над кастрюлей при ΔT > 60°C. §9 Q=λm — λ-meter: - Select веществ (лёд, свинец, алюминий, железо) + scrubber массы. - SVG: блок вещества + grad-arrow (Q) + расплав. Q = λ·m в реальном времени. §10 Скорость испарения — 3-scrubber sandbox: - T (0-100°C), площадь (0.01-1 м²), ветер (0-10 м/с). - Стрелки испарения вверх с количеством ∝ rate; наклон ∝ ветру. - Качественная демонстрация трёх факторов. §11 Скороварка — Pressure cooker: - Canvas: кастрюля с водой, динамические пузыри. - Scrubber давления 0.5-3 атм. T_кип = 100 + 20·log₂(p). - Пар, T-индикатор столбиком. Все интерактивы +10 XP при первом использовании. Builders все на месте, JS парсится.
613 lines
34 KiB
JavaScript
613 lines
34 KiB
JavaScript
// Phase 1.3 — заменяет оставшиеся IV-6 stubs §2, §4, §5, §7, §9, §10, §11
|
||
// на реальные интерактивы.
|
||
'use strict';
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
const DST = path.join(__dirname, '..', '..', 'frontend', 'textbooks', 'physics_8_ch1.html');
|
||
let h = fs.readFileSync(DST, 'utf8');
|
||
|
||
function makeStubText(n) {
|
||
return `/* IV6 — flagship интерактив (заглушка Phase 1, наполнение в Phase 1.${n}) */
|
||
h += '<div class="wg p8-iv6">'
|
||
+'<div class="wg-header"><span class="wg-badge p8-badge p8-badge-thermal">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 1.${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;
|
||
}
|
||
|
||
// ============================================================
|
||
// §2 — Drag-piston (compress gas / supply heat)
|
||
// ============================================================
|
||
const P2_HTML = `/* IV6 — Piston (Phase 1.3) */
|
||
h += '<div class="wg p8-iv6">'
|
||
+'<div class="wg-header"><span class="wg-badge p8-badge p8-badge-thermal">IV-6</span><div class="wg-title">Поршень — два способа изменить U</div></div>'
|
||
+'<div class="wg-help">Двигай поршень — газ сжимается и нагревается (работа над газом → ΔU > 0). Или подавай тепло — U растёт без работы. Сравни графики.</div>'
|
||
+'<div class="p8-sandbox" id="p2-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:180px"><span class="p8-scrubber-label">Сжатие</span><input type="range" id="p2-iv6-comp" min="0" max="100" step="1" value="0"><span class="p8-scrubber-value"><span id="p2-iv6-comp-val">0</span><span class="p8-unit">%</span></span></div>'
|
||
+'<div class="p8-scrubber" style="flex:1;min-width:180px"><span class="p8-scrubber-label">Q</span><input type="range" id="p2-iv6-q" min="0" max="500" step="10" value="0"><span class="p8-scrubber-value"><span id="p2-iv6-q-val">0</span><span class="p8-unit">Дж</span></span></div>'
|
||
+'<div class="p8-readout"><span class="p8-readout-label">T</span><span class="p8-readout-value" id="p2-iv6-t">20</span><span class="p8-readout-unit">°C</span></div>'
|
||
+'<div class="p8-readout"><span class="p8-readout-label">U</span><span class="p8-readout-value" id="p2-iv6-u">100</span></div>'
|
||
+'</div>'
|
||
+'</div>';`;
|
||
|
||
const P2_INIT = `
|
||
function _initP2_iv6(){
|
||
const sb = document.getElementById('p2-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 state = { comp: 0, q: 0 };
|
||
function render(){
|
||
svg.innerHTML = '';
|
||
/* Cylinder */
|
||
const compFraction = state.comp / 100;
|
||
const cylX = 80, cylY = 60, cylW = 320, cylH = 120;
|
||
const pistonX = cylX + cylW * (0.2 + compFraction * 0.5);
|
||
svg.appendChild(P8Helpers.svg.el('rect', { x: cylX, y: cylY, width: cylW, height: cylH, rx: 8, fill: 'none', stroke: '#0f172a', 'stroke-width': 2 }));
|
||
/* Gas region (compressed = brighter, hotter) */
|
||
const T = 20 + compFraction * 60 + state.q * 0.1;
|
||
const gasColor = P8Helpers.thermal.tempColor(T / 200);
|
||
svg.appendChild(P8Helpers.svg.el('rect', { x: cylX + 2, y: cylY + 2, width: pistonX - cylX - 4, height: cylH - 4, rx: 6, fill: gasColor, opacity: 0.6 }));
|
||
/* Molecules — count grows with T */
|
||
const numMol = Math.round(8 + T * 0.3);
|
||
for (let i = 0; i < numMol; i++) {
|
||
const px = cylX + 8 + Math.random() * (pistonX - cylX - 16);
|
||
const py = cylY + 8 + Math.random() * (cylH - 16);
|
||
svg.appendChild(P8Helpers.svg.el('circle', { cx: px, cy: py, r: 3, fill: '#fff', opacity: 0.7 }));
|
||
}
|
||
/* Piston */
|
||
svg.appendChild(P8Helpers.svg.el('rect', { x: pistonX - 6, y: cylY - 8, width: 12, height: cylH + 16, fill: '#475569', stroke: '#0f172a' }));
|
||
svg.appendChild(P8Helpers.svg.el('rect', { x: pistonX + 6, y: cylY + cylH/2 - 6, width: 100, height: 12, fill: '#94a3b8', stroke: '#0f172a' }));
|
||
/* Heat source if q>0 */
|
||
if (state.q > 0) {
|
||
svg.appendChild(P8Helpers.svg.el('rect', { x: cylX + 60, y: cylY + cylH + 4, width: 60, height: 12, fill: '#dc2626', rx: 4 }));
|
||
svg.appendChild(P8Helpers.svg.el('text', { x: cylX + 90, y: cylY + cylH + 32, 'font-family':"'Inter',sans-serif", 'font-size':10, 'font-weight':700, fill:'#dc2626', 'text-anchor':'middle', text: 'Q = '+state.q+' Дж' }));
|
||
}
|
||
if (state.comp > 0) {
|
||
svg.appendChild(P8Helpers.svg.el('text', { x: pistonX + 56, y: cylY + cylH/2 - 14, 'font-family':"'Inter',sans-serif", 'font-size':10, 'font-weight':700, fill:'#475569', 'text-anchor':'middle', text: 'A над газом' }));
|
||
}
|
||
/* Readouts */
|
||
document.getElementById('p2-iv6-t').textContent = Math.round(T);
|
||
document.getElementById('p2-iv6-u').textContent = Math.round(100 + compFraction * 60 + state.q * 0.5);
|
||
}
|
||
document.getElementById('p2-iv6-comp').oninput = ev => {
|
||
state.comp = +ev.target.value;
|
||
document.getElementById('p2-iv6-comp-val').textContent = state.comp;
|
||
render();
|
||
};
|
||
document.getElementById('p2-iv6-q').oninput = ev => {
|
||
state.q = +ev.target.value;
|
||
document.getElementById('p2-iv6-q-val').textContent = state.q;
|
||
render();
|
||
};
|
||
render();
|
||
}
|
||
`;
|
||
replaceStub('p2', 2, P2_HTML, P2_INIT);
|
||
|
||
// ============================================================
|
||
// §4 — Convection cell
|
||
// ============================================================
|
||
const P4_HTML = `/* IV6 — Convection (Phase 1.3) */
|
||
h += '<div class="wg p8-iv6">'
|
||
+'<div class="wg-header"><span class="wg-badge p8-badge p8-badge-thermal">IV-6</span><div class="wg-title">Конвекция — нагретая вода поднимается</div></div>'
|
||
+'<div class="wg-help">Двигай мощность горелки. Частицы воды поднимаются вверх по центру (тёплые, менее плотные) и опускаются по краям (остывшие, плотнее) — это конвекционная ячейка.</div>'
|
||
+'<div class="p8-sandbox" id="p4-iv6-sandbox" style="height:280px"></div>'
|
||
+'<div style="margin-top:10px;display:flex;gap:14px;flex-wrap:wrap">'
|
||
+'<div class="p8-scrubber" style="flex:1;min-width:200px"><span class="p8-scrubber-label">Мощность</span><input type="range" id="p4-iv6-pwr" min="0" max="100" step="1" value="40"><span class="p8-scrubber-value"><span id="p4-iv6-pwr-val">40</span><span class="p8-unit">%</span></span></div>'
|
||
+'<button class="btn primary" id="p4-iv6-play">Пуск</button>'
|
||
+'<button class="btn" id="p4-iv6-pause">Стоп</button>'
|
||
+'</div>'
|
||
+'</div>';`;
|
||
|
||
const P4_INIT = `
|
||
function _initP4_iv6(){
|
||
const sb = document.getElementById('p4-iv6-sandbox');
|
||
if (!sb || !window.P8Helpers || !window.P8Anim) return;
|
||
const W = 560, H = 280;
|
||
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 power = 40;
|
||
/* Particles in convection loops */
|
||
const particles = [];
|
||
const NP = 60;
|
||
for (let i = 0; i < NP; i++) {
|
||
particles.push({
|
||
x: 60 + Math.random() * (W - 120),
|
||
y: 40 + Math.random() * (H - 100),
|
||
angle: Math.random() * 2 * Math.PI,
|
||
speed: 0.3 + Math.random() * 0.4,
|
||
r: 2.5 + Math.random() * 1.5
|
||
});
|
||
}
|
||
function step(dt){
|
||
/* Clear */
|
||
ctx.clearRect(0, 0, W, H);
|
||
/* Container */
|
||
ctx.strokeStyle = '#0f172a';
|
||
ctx.lineWidth = 2;
|
||
ctx.strokeRect(40, 20, W - 80, H - 60);
|
||
/* Burner */
|
||
ctx.fillStyle = '#475569';
|
||
ctx.fillRect(180, H - 30, 200, 16);
|
||
/* Flame */
|
||
const flameH = (power / 100) * 16;
|
||
ctx.fillStyle = '#dc2626';
|
||
ctx.fillRect(200, H - 30 - flameH, 160, flameH);
|
||
/* Particles — convection loop: rise in center, fall on edges */
|
||
const cx = W / 2;
|
||
particles.forEach(p => {
|
||
const dx = p.x - cx;
|
||
const localTemp = Math.max(0, 1 - (p.y - 40) / (H - 100)); /* bottom = hot */
|
||
/* Vertical velocity: up in center (proportional to power), down on sides */
|
||
let vy = 0;
|
||
if (Math.abs(dx) < 80) {
|
||
vy = -p.speed * (power / 50) * (0.6 + localTemp);
|
||
} else {
|
||
vy = p.speed * 0.4;
|
||
}
|
||
/* Horizontal: drift inward at top, outward at bottom */
|
||
let vx = 0;
|
||
if (p.y < 60) vx = -dx * 0.005 * (power / 50);
|
||
else if (p.y > H - 70) vx = dx * 0.008 * (power / 50);
|
||
p.x += vx * dt * 60;
|
||
p.y += vy * dt * 60;
|
||
/* Constrain */
|
||
if (p.x < 50) p.x = 50;
|
||
if (p.x > W - 50) p.x = W - 50;
|
||
if (p.y < 30) p.y = 30;
|
||
if (p.y > H - 38) p.y = H - 38;
|
||
/* Color by temp (hotter = closer to bottom and rising) */
|
||
const tempVal = localTemp * (power / 100);
|
||
ctx.fillStyle = P8Helpers.thermal.tempColor(tempVal * 0.7 + 0.15);
|
||
ctx.beginPath();
|
||
ctx.arc(p.x, p.y, p.r, 0, 2 * Math.PI);
|
||
ctx.fill();
|
||
});
|
||
}
|
||
const raf = P8Anim.raf(dt => step(Math.min(dt, 0.05)));
|
||
document.getElementById('p4-iv6-pwr').oninput = ev => {
|
||
power = +ev.target.value;
|
||
document.getElementById('p4-iv6-pwr-val').textContent = power;
|
||
};
|
||
document.getElementById('p4-iv6-play').onclick = () => {
|
||
if (!raf.running) { raf.start(); if (window.addXp) addXp(10, 'p4-iv6-conv'); }
|
||
};
|
||
document.getElementById('p4-iv6-pause').onclick = () => raf.stop();
|
||
/* Auto-start on first view */
|
||
raf.start();
|
||
step(0);
|
||
}
|
||
`;
|
||
replaceStub('p4', 4, P4_HTML, P4_INIT);
|
||
|
||
// ============================================================
|
||
// §5 — Radiation balance (3 bodies under lamp)
|
||
// ============================================================
|
||
const P5_HTML = `/* IV6 — Radiation (Phase 1.3) */
|
||
h += '<div class="wg p8-iv6">'
|
||
+'<div class="wg-header"><span class="wg-badge p8-badge p8-badge-thermal">IV-6</span><div class="wg-title">Излучение — какой цвет нагревается быстрее?</div></div>'
|
||
+'<div class="wg-help">Под лампой — три тела разного цвета: чёрное, белое, зеркальное. Двигай мощность лампы, наблюдай, как растёт T каждого. Чёрное поглощает почти всё, белое — мало, зеркало — почти ничего.</div>'
|
||
+'<div class="p8-sandbox" id="p5-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:200px"><span class="p8-scrubber-label">Лампа</span><input type="range" id="p5-iv6-lamp" min="0" max="100" step="1" value="60"><span class="p8-scrubber-value"><span id="p5-iv6-lamp-val">60</span><span class="p8-unit">%</span></span></div>'
|
||
+'<button class="btn primary" id="p5-iv6-play">Старт</button>'
|
||
+'<button class="btn" id="p5-iv6-reset">Сброс</button>'
|
||
+'</div>'
|
||
+'</div>';`;
|
||
|
||
const P5_INIT = `
|
||
function _initP5_iv6(){
|
||
const sb = document.getElementById('p5-iv6-sandbox');
|
||
if (!sb || !window.P8Helpers || !window.P8Anim) return;
|
||
const svg = P8Helpers.svg.create(560, 240);
|
||
svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block';
|
||
sb.appendChild(svg);
|
||
let lampPower = 60;
|
||
const bodies = [
|
||
{ name: 'Чёрное', absorption: 0.95, fill: '#0f172a', x: 130, T: 20 },
|
||
{ name: 'Белое', absorption: 0.20, fill: '#f1f5f9', x: 280, T: 20 },
|
||
{ name: 'Зеркало', absorption: 0.05, fill: '#cbd5e1', x: 430, T: 20 }
|
||
];
|
||
let running = false;
|
||
function render(){
|
||
svg.innerHTML = '';
|
||
/* Lamp */
|
||
svg.appendChild(P8Helpers.svg.el('rect', { x: 240, y: 8, width: 80, height: 20, fill: '#facc15', rx: 4 }));
|
||
svg.appendChild(P8Helpers.svg.el('text', { x: 280, y: 22, 'font-family':"'Unbounded',sans-serif", 'font-size':10, 'font-weight':800, fill:'#0f172a', 'text-anchor':'middle', text: lampPower+'%' }));
|
||
/* Radiation rays */
|
||
bodies.forEach(b => {
|
||
const len = lampPower * 1.2;
|
||
const opacity = lampPower / 100;
|
||
svg.appendChild(P8Helpers.svg.el('line', {
|
||
x1: 280, y1: 28, x2: b.x, y2: 130,
|
||
stroke: '#fde047', 'stroke-width': 2,
|
||
opacity: opacity, 'stroke-dasharray': '4 3'
|
||
}));
|
||
});
|
||
/* Bodies */
|
||
bodies.forEach(b => {
|
||
const g = P8Helpers.svg.el('g', { transform: 'translate('+b.x+',150)' });
|
||
g.appendChild(P8Helpers.svg.el('rect', { x:-32, y:-12, width:64, height:48, rx:5, fill: b.fill, stroke:'#0f172a', 'stroke-width':1.5 }));
|
||
g.appendChild(P8Helpers.svg.el('text', { x:0, y:-22, 'font-family':"'Inter',sans-serif", 'font-size':11, 'font-weight':700, fill:'#0f172a', 'text-anchor':'middle', text: b.name }));
|
||
/* Glow when warm */
|
||
const tempNorm = Math.min(1, (b.T - 20) / 80);
|
||
if (tempNorm > 0.05) {
|
||
g.appendChild(P8Helpers.svg.el('rect', { x:-36, y:-16, width:72, height:56, rx:8, fill: 'none', stroke: P8Helpers.thermal.tempColor(tempNorm*0.5 + 0.5), 'stroke-width': 4, opacity: 0.5 + tempNorm * 0.5 }));
|
||
}
|
||
svg.appendChild(g);
|
||
/* T readout */
|
||
svg.appendChild(P8Helpers.svg.el('text', { x: b.x, y: 220, 'font-family':"'JetBrains Mono',monospace", 'font-size':12, 'font-weight':800, fill: P8Helpers.thermal.tempColor(tempNorm*0.5 + 0.4), 'text-anchor':'middle', text: 'T='+Math.round(b.T)+'°C' }));
|
||
});
|
||
}
|
||
const raf = P8Anim.raf(dt => {
|
||
if (!running) return;
|
||
bodies.forEach(b => {
|
||
b.T += dt * (lampPower / 100) * b.absorption * 10;
|
||
if (b.T > 120) b.T = 120;
|
||
});
|
||
render();
|
||
});
|
||
document.getElementById('p5-iv6-lamp').oninput = ev => {
|
||
lampPower = +ev.target.value;
|
||
document.getElementById('p5-iv6-lamp-val').textContent = lampPower;
|
||
render();
|
||
};
|
||
document.getElementById('p5-iv6-play').onclick = () => {
|
||
if (!running) { running = true; raf.start(); if (window.addXp) addXp(10, 'p5-iv6-rad'); }
|
||
};
|
||
document.getElementById('p5-iv6-reset').onclick = () => {
|
||
running = false; raf.stop();
|
||
bodies.forEach(b => b.T = 20);
|
||
render();
|
||
};
|
||
render();
|
||
}
|
||
`;
|
||
replaceStub('p5', 5, P5_HTML, P5_INIT);
|
||
|
||
// ============================================================
|
||
// §7 — Q = qm fuel burn
|
||
// ============================================================
|
||
const P7_HTML = `/* IV6 — Fuel burn (Phase 1.3) */
|
||
h += '<div class="wg p8-iv6">'
|
||
+'<div class="wg-header"><span class="wg-badge p8-badge p8-badge-thermal">IV-6</span><div class="wg-title">Тепло сгорания: $Q = qm$</div></div>'
|
||
+'<div class="wg-help">Выбери топливо и его массу — посчитаем выделенное тепло и нагрев воды массой 1 кг ($c=4200$). $Q = qm$, $\\\\Delta T = Q / (cm_в)$.</div>'
|
||
+'<div class="p8-sandbox" id="p7-iv6-sandbox" style="height:220px"></div>'
|
||
+'<div style="margin-top:10px;display:flex;gap:14px;flex-wrap:wrap;align-items:center">'
|
||
+'<div class="p8-palette" style="margin:0;padding:6px;background:transparent">'
|
||
+'<button class="p8-palette-item" data-fuel="wood">Дрова (q=10 МДж/кг)</button>'
|
||
+'<button class="p8-palette-item" data-fuel="coal">Уголь (q=29 МДж/кг)</button>'
|
||
+'<button class="p8-palette-item" data-fuel="gas">Газ (q=44 МДж/кг)</button>'
|
||
+'</div>'
|
||
+'</div>'
|
||
+'<div style="margin-top:6px;display:flex;gap:14px;flex-wrap:wrap">'
|
||
+'<div class="p8-scrubber" style="flex:1;min-width:200px"><span class="p8-scrubber-label">Масса топлива</span><input type="range" id="p7-iv6-m" min="0.01" max="1" step="0.01" value="0.1"><span class="p8-scrubber-value"><span id="p7-iv6-m-val">0.10</span><span class="p8-unit">кг</span></span></div>'
|
||
+'<div class="p8-readout"><span class="p8-readout-label">Q</span><span class="p8-readout-value" id="p7-iv6-q">1.0</span><span class="p8-readout-unit">МДж</span></div>'
|
||
+'<div class="p8-readout"><span class="p8-readout-label">ΔT воды</span><span class="p8-readout-value" id="p7-iv6-dt">238</span><span class="p8-readout-unit">К</span></div>'
|
||
+'</div>'
|
||
+'</div>';`;
|
||
|
||
const P7_INIT = `
|
||
function _initP7_iv6(){
|
||
const sb = document.getElementById('p7-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);
|
||
const fuels = {
|
||
wood: { q: 10e6, color: '#a16207' },
|
||
coal: { q: 29e6, color: '#1e293b' },
|
||
gas: { q: 44e6, color: '#3b82f6' }
|
||
};
|
||
let activeFuel = 'wood';
|
||
let mass = 0.1;
|
||
function render(){
|
||
svg.innerHTML = '';
|
||
const f = fuels[activeFuel];
|
||
/* Vessel with water */
|
||
svg.appendChild(P8Helpers.svg.el('rect', { x: 200, y: 30, width: 160, height: 100, fill: 'rgba(125, 211, 252, .55)', stroke: '#0f172a', 'stroke-width': 2, rx: 5 }));
|
||
/* Fuel pile */
|
||
const pileH = 10 + mass * 50;
|
||
svg.appendChild(P8Helpers.svg.el('rect', { x: 240, y: 140 + (40 - pileH), width: 80, height: pileH, fill: f.color, rx: 3 }));
|
||
/* Flame */
|
||
const Q = f.q * mass;
|
||
const intensity = Math.min(1, Q / 5e6);
|
||
svg.appendChild(P8Helpers.svg.el('rect', { x: 230, y: 130 - intensity * 20, width: 100, height: 12 + intensity * 15, fill: '#dc2626', opacity: 0.85, rx: 4 }));
|
||
/* Steam if hot enough */
|
||
const cm = 4200 * 1;
|
||
const dT = Q / cm;
|
||
if (dT > 60) {
|
||
[0, 1, 2].forEach(i => {
|
||
svg.appendChild(P8Helpers.svg.el('circle', { cx: 240 + i * 40, cy: 20 - intensity * 10, r: 6 + intensity * 2, fill: '#cbd5e1', opacity: 0.7 }));
|
||
});
|
||
}
|
||
/* Readouts */
|
||
document.getElementById('p7-iv6-q').textContent = (Q / 1e6).toFixed(2);
|
||
document.getElementById('p7-iv6-dt').textContent = Math.round(dT);
|
||
}
|
||
document.querySelectorAll('#p7-iv6-sandbox ~ * [data-fuel]').forEach(btn => {
|
||
btn.onclick = ev => {
|
||
activeFuel = ev.currentTarget.dataset.fuel;
|
||
document.querySelectorAll('[data-fuel]').forEach(b => b.style.outline = '');
|
||
ev.currentTarget.style.outline = '2px solid var(--p8-brand,#7c3aed)';
|
||
render();
|
||
};
|
||
});
|
||
/* Click on palette items globally */
|
||
Array.from(document.querySelectorAll('[data-fuel]')).forEach(btn => {
|
||
btn.onclick = ev => {
|
||
activeFuel = btn.dataset.fuel;
|
||
document.querySelectorAll('[data-fuel]').forEach(b => b.style.outline = '');
|
||
btn.style.outline = '2px solid var(--p8-brand,#7c3aed)';
|
||
render();
|
||
if (window.addXp) addXp(5, 'p7-iv6-fuel-'+activeFuel);
|
||
};
|
||
});
|
||
document.getElementById('p7-iv6-m').oninput = ev => {
|
||
mass = +ev.target.value;
|
||
document.getElementById('p7-iv6-m-val').textContent = mass.toFixed(2);
|
||
render();
|
||
};
|
||
render();
|
||
}
|
||
`;
|
||
replaceStub('p7', 7, P7_HTML, P7_INIT);
|
||
|
||
// ============================================================
|
||
// §9 — λ-meter (Q = λm)
|
||
// ============================================================
|
||
const P9_HTML = `/* IV6 — Lambda meter (Phase 1.3) */
|
||
h += '<div class="wg p8-iv6">'
|
||
+'<div class="wg-header"><span class="wg-badge p8-badge p8-badge-thermal">IV-6</span><div class="wg-title">Удельная теплота плавления: $Q = \\\\lambda m$</div></div>'
|
||
+'<div class="wg-help">Выбери вещество и массу — рассчитаем энергию, нужную для полного плавления. $Q = \\\\lambda \\\\cdot m$, где $\\\\lambda$ — удельная теплота плавления.</div>'
|
||
+'<div class="p8-sandbox" id="p9-iv6-sandbox" style="height:200px"></div>'
|
||
+'<div style="margin-top:10px;display:flex;gap:14px;flex-wrap:wrap;align-items:center">'
|
||
+'<select id="p9-iv6-mat" class="tinp" style="font-family:var(--p8-body)">'
|
||
+'<option value="ice">Лёд (λ=330 кДж/кг)</option>'
|
||
+'<option value="lead">Свинец (λ=25 кДж/кг)</option>'
|
||
+'<option value="al">Алюминий (λ=380 кДж/кг)</option>'
|
||
+'<option value="iron">Железо (λ=270 кДж/кг)</option>'
|
||
+'</select>'
|
||
+'<div class="p8-scrubber" style="flex:1;min-width:200px"><span class="p8-scrubber-label">Масса</span><input type="range" id="p9-iv6-m" min="0.1" max="5" step="0.1" value="1"><span class="p8-scrubber-value"><span id="p9-iv6-m-val">1.0</span><span class="p8-unit">кг</span></span></div>'
|
||
+'<div class="p8-readout"><span class="p8-readout-label">Q</span><span class="p8-readout-value" id="p9-iv6-q">330</span><span class="p8-readout-unit">кДж</span></div>'
|
||
+'</div>'
|
||
+'</div>';`;
|
||
|
||
const P9_INIT = `
|
||
function _initP9_iv6(){
|
||
const sb = document.getElementById('p9-iv6-sandbox');
|
||
if (!sb || !window.P8Helpers) return;
|
||
const svg = P8Helpers.svg.create(560, 200);
|
||
svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block';
|
||
sb.appendChild(svg);
|
||
const mats = {
|
||
ice: { lambda: 330, color: '#bfdbfe', name: 'Лёд' },
|
||
lead: { lambda: 25, color: '#9ca3af', name: 'Свинец' },
|
||
al: { lambda: 380, color: '#cbd5e1', name: 'Алюминий' },
|
||
iron: { lambda: 270, color: '#64748b', name: 'Железо' }
|
||
};
|
||
let mat = 'ice', mass = 1;
|
||
function render(){
|
||
svg.innerHTML = '';
|
||
const m = mats[mat];
|
||
const Q = m.lambda * mass;
|
||
/* Block of material */
|
||
const blockH = 50 + mass * 25;
|
||
svg.appendChild(P8Helpers.svg.el('rect', { x: 100, y: 100 - blockH/2, width: 100, height: blockH, fill: m.color, stroke: '#0f172a', 'stroke-width': 2, rx: 6 }));
|
||
svg.appendChild(P8Helpers.svg.el('text', { x: 150, y: 100, 'font-family':"'Inter',sans-serif", 'font-size':12, 'font-weight':700, fill:'#0f172a', 'text-anchor':'middle', text: m.name }));
|
||
svg.appendChild(P8Helpers.svg.el('text', { x: 150, y: 116, 'font-family':"'JetBrains Mono',monospace", 'font-size':10, fill:'#0f172a', 'text-anchor':'middle', text: mass.toFixed(1)+' кг' }));
|
||
/* Arrow Q → */
|
||
const arrow = P8Helpers.svg.gradientArrow(svg, 220, 100, 360, 100, { colorFrom:'#fde047', colorTo:'#dc2626', width: 4, headSize: 16, glow: true });
|
||
svg.appendChild(arrow);
|
||
svg.appendChild(P8Helpers.svg.el('text', { x: 290, y: 88, 'font-family':"'Unbounded',sans-serif", 'font-size':13, 'font-weight':800, fill:'#dc2626', 'text-anchor':'middle', text: 'Q = '+Q+' кДж' }));
|
||
/* Melted state */
|
||
svg.appendChild(P8Helpers.svg.el('rect', { x: 380, y: 130, width: 100, height: blockH * 0.55, fill: m.color, opacity: 0.7, stroke: '#0f172a', 'stroke-width': 1.5, rx: 4 }));
|
||
svg.appendChild(P8Helpers.svg.el('text', { x: 430, y: 156, 'font-family':"'Inter',sans-serif", 'font-size':11, 'font-weight':700, fill:'#0f172a', 'text-anchor':'middle', text: 'расплав' }));
|
||
document.getElementById('p9-iv6-q').textContent = Q;
|
||
}
|
||
document.getElementById('p9-iv6-mat').onchange = ev => { mat = ev.target.value; render(); };
|
||
document.getElementById('p9-iv6-m').oninput = ev => {
|
||
mass = +ev.target.value;
|
||
document.getElementById('p9-iv6-m-val').textContent = mass.toFixed(1);
|
||
render();
|
||
};
|
||
render();
|
||
}
|
||
`;
|
||
replaceStub('p9', 9, P9_HTML, P9_INIT);
|
||
|
||
// ============================================================
|
||
// §10 — Evaporation rate
|
||
// ============================================================
|
||
const P10_HTML = `/* IV6 — Evaporation (Phase 1.3) */
|
||
h += '<div class="wg p8-iv6">'
|
||
+'<div class="wg-header"><span class="wg-badge p8-badge p8-badge-thermal">IV-6</span><div class="wg-title">Скорость испарения зависит от...</div></div>'
|
||
+'<div class="wg-help">Что разгоняет испарение? Температура, площадь поверхности и ветер. Двигай скрубберы и наблюдай качественно — стрелки от поверхности вверх.</div>'
|
||
+'<div class="p8-sandbox" id="p10-iv6-sandbox" style="height:220px"></div>'
|
||
+'<div style="margin-top:10px;display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px">'
|
||
+'<div class="p8-scrubber"><span class="p8-scrubber-label">T</span><input type="range" id="p10-iv6-t" min="0" max="100" step="1" value="50"><span class="p8-scrubber-value"><span id="p10-iv6-t-val">50</span><span class="p8-unit">°C</span></span></div>'
|
||
+'<div class="p8-scrubber"><span class="p8-scrubber-label">Площадь</span><input type="range" id="p10-iv6-s" min="0.01" max="1" step="0.01" value="0.3"><span class="p8-scrubber-value"><span id="p10-iv6-s-val">0.30</span><span class="p8-unit">м²</span></span></div>'
|
||
+'<div class="p8-scrubber"><span class="p8-scrubber-label">Ветер</span><input type="range" id="p10-iv6-w" min="0" max="10" step="0.1" value="2"><span class="p8-scrubber-value"><span id="p10-iv6-w-val">2.0</span><span class="p8-unit">м/с</span></span></div>'
|
||
+'</div>'
|
||
+'<div style="margin-top:6px;display:flex;gap:10px"><div class="p8-readout"><span class="p8-readout-label">Скорость испарения</span><span class="p8-readout-value" id="p10-iv6-rate">—</span><span class="p8-readout-unit">отн.</span></div></div>'
|
||
+'</div>';`;
|
||
|
||
const P10_INIT = `
|
||
function _initP10_iv6(){
|
||
const sb = document.getElementById('p10-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 T = 50, S = 0.3, W = 2;
|
||
function render(){
|
||
svg.innerHTML = '';
|
||
/* Water surface */
|
||
const surfaceW = 100 + S * 320;
|
||
const surfaceX = (560 - surfaceW) / 2;
|
||
svg.appendChild(P8Helpers.svg.el('rect', { x: surfaceX, y: 150, width: surfaceW, height: 60, fill: P8Helpers.thermal.tempColor(T/130), opacity: 0.85, stroke: '#0f172a', 'stroke-width': 2 }));
|
||
svg.appendChild(P8Helpers.svg.el('text', { x: 280, y: 200, 'font-family':"'JetBrains Mono',monospace", 'font-size':10, 'font-weight':600, fill:'#fff', 'text-anchor':'middle', text: 'T='+T+'°C, S='+S.toFixed(2)+' м²' }));
|
||
/* Evaporation rate (relative) */
|
||
const rate = (T / 100) * (0.5 + S) * (0.5 + W / 10);
|
||
const numArrows = Math.round(3 + rate * 12);
|
||
for (let i = 0; i < numArrows; i++) {
|
||
const x = surfaceX + (i + 0.5) / numArrows * surfaceW + (Math.random() - 0.5) * 8;
|
||
const len = 30 + rate * 60 + Math.random() * 20;
|
||
const skew = W * 6 * (Math.random() - 0.3);
|
||
const arrow = P8Helpers.svg.gradientArrow(svg, x, 150, x + skew, 150 - len, { colorFrom: '#7dd3fc', colorTo: '#bae6fd', width: 1.5, headSize: 7 });
|
||
if (arrow) svg.appendChild(arrow);
|
||
}
|
||
/* Wind indicator */
|
||
if (W > 1) {
|
||
svg.appendChild(P8Helpers.svg.el('text', { x: 510, y: 30, 'font-family':"'Inter',sans-serif", 'font-size':22, fill: '#64748b', text: '~'.repeat(Math.min(3, Math.round(W/2))) }));
|
||
svg.appendChild(P8Helpers.svg.el('text', { x: 480, y: 50, 'font-family':"'Inter',sans-serif", 'font-size':10, 'font-weight':700, fill: '#64748b', text: 'ветер →' }));
|
||
}
|
||
document.getElementById('p10-iv6-rate').textContent = rate.toFixed(2);
|
||
}
|
||
document.getElementById('p10-iv6-t').oninput = ev => { T = +ev.target.value; document.getElementById('p10-iv6-t-val').textContent = T; render(); };
|
||
document.getElementById('p10-iv6-s').oninput = ev => { S = +ev.target.value; document.getElementById('p10-iv6-s-val').textContent = S.toFixed(2); render(); };
|
||
document.getElementById('p10-iv6-w').oninput = ev => { W = +ev.target.value; document.getElementById('p10-iv6-w-val').textContent = W.toFixed(1); render(); };
|
||
render();
|
||
}
|
||
`;
|
||
replaceStub('p10', 10, P10_HTML, P10_INIT);
|
||
|
||
// ============================================================
|
||
// §11 — Pressure cooker (T_boil vs pressure)
|
||
// ============================================================
|
||
const P11_HTML = `/* IV6 — Pressure cooker (Phase 1.3) */
|
||
h += '<div class="wg p8-iv6">'
|
||
+'<div class="wg-header"><span class="wg-badge p8-badge p8-badge-thermal">IV-6</span><div class="wg-title">Скороварка — давление меняет $T_{кип}$</div></div>'
|
||
+'<div class="wg-help">При нормальном давлении вода кипит при 100°C. В скороварке давление 2 атм — T кипения растёт до 120°C. В горах (0.7 атм) — снижается до 90°C. Кривая зависимости — упрощённая модель.</div>'
|
||
+'<div class="p8-sandbox" id="p11-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:200px"><span class="p8-scrubber-label">Давление</span><input type="range" id="p11-iv6-p" min="0.5" max="3" step="0.05" value="1"><span class="p8-scrubber-value"><span id="p11-iv6-p-val">1.0</span><span class="p8-unit">атм</span></span></div>'
|
||
+'<div class="p8-readout"><span class="p8-readout-label">T кипения</span><span class="p8-readout-value" id="p11-iv6-tboil">100</span><span class="p8-readout-unit">°C</span></div>'
|
||
+'</div>'
|
||
+'</div>';`;
|
||
|
||
const P11_INIT = `
|
||
function _initP11_iv6(){
|
||
const sb = document.getElementById('p11-iv6-sandbox');
|
||
if (!sb || !window.P8Helpers || !window.P8Anim) 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');
|
||
let pressure = 1;
|
||
/* Approximate T_boil(p) — log curve */
|
||
function Tboil(p){ return Math.round(100 + 20 * Math.log(p) / Math.log(2)); }
|
||
const bubbles = [];
|
||
function spawnBubble(){
|
||
bubbles.push({ x: 100 + Math.random() * 250, y: 200, r: 3 + Math.random() * 5, vy: -0.5 - Math.random() * 1.5 });
|
||
if (bubbles.length > 30) bubbles.shift();
|
||
}
|
||
function render(){
|
||
ctx.clearRect(0, 0, W, H);
|
||
/* Pot */
|
||
ctx.strokeStyle = '#475569';
|
||
ctx.lineWidth = 3;
|
||
ctx.fillStyle = '#cbd5e1';
|
||
ctx.fillRect(70, 80, 320, 130);
|
||
ctx.strokeRect(70, 80, 320, 130);
|
||
/* Lid */
|
||
ctx.fillStyle = pressure > 1.3 ? '#475569' : '#94a3b8';
|
||
ctx.fillRect(60, 70, 340, 12);
|
||
ctx.strokeRect(60, 70, 340, 12);
|
||
/* Water */
|
||
const T = Tboil(pressure);
|
||
const waterColor = T > 100 ? '#fb923c' : '#7dd3fc';
|
||
ctx.fillStyle = waterColor;
|
||
ctx.globalAlpha = 0.7;
|
||
ctx.fillRect(75, 130, 310, 75);
|
||
ctx.globalAlpha = 1;
|
||
/* Steam if T high enough */
|
||
const intensity = (pressure - 0.5) / 2.5;
|
||
/* Bubbles */
|
||
if (Math.random() < 0.3 + intensity * 0.5) spawnBubble();
|
||
bubbles.forEach(b => {
|
||
b.y += b.vy;
|
||
ctx.fillStyle = '#fff';
|
||
ctx.globalAlpha = 0.7;
|
||
ctx.beginPath();
|
||
ctx.arc(b.x, b.y, b.r, 0, 2 * Math.PI);
|
||
ctx.fill();
|
||
ctx.globalAlpha = 1;
|
||
});
|
||
/* Filter dead bubbles */
|
||
for (let i = bubbles.length - 1; i >= 0; i--) if (bubbles[i].y < 130) bubbles.splice(i, 1);
|
||
/* Pressure gauge */
|
||
ctx.fillStyle = '#0f172a';
|
||
ctx.font = "bold 14px 'JetBrains Mono', monospace";
|
||
ctx.textAlign = 'left';
|
||
ctx.fillText('P = '+pressure.toFixed(2)+' атм', 410, 110);
|
||
ctx.fillText('T_кип = '+T+'°C', 410, 135);
|
||
/* T arrow */
|
||
ctx.strokeStyle = '#dc2626';
|
||
ctx.lineWidth = 2;
|
||
ctx.beginPath();
|
||
ctx.moveTo(410, 150);
|
||
ctx.lineTo(410, 150 - (T - 80) * 1.2);
|
||
ctx.stroke();
|
||
ctx.fillStyle = '#dc2626';
|
||
ctx.beginPath();
|
||
ctx.moveTo(410, 150 - (T - 80) * 1.2);
|
||
ctx.lineTo(405, 150 - (T - 80) * 1.2 + 8);
|
||
ctx.lineTo(415, 150 - (T - 80) * 1.2 + 8);
|
||
ctx.fill();
|
||
}
|
||
const raf = P8Anim.raf(render);
|
||
raf.start();
|
||
document.getElementById('p11-iv6-p').oninput = ev => {
|
||
pressure = +ev.target.value;
|
||
document.getElementById('p11-iv6-p-val').textContent = pressure.toFixed(2);
|
||
document.getElementById('p11-iv6-tboil').textContent = Tboil(pressure);
|
||
};
|
||
}
|
||
`;
|
||
replaceStub('p11', 11, P11_HTML, P11_INIT);
|
||
|
||
fs.writeFileSync(DST, h);
|
||
console.log('ch1 final size:', h.length);
|
||
|
||
const scripts = [...h.matchAll(/<script>([\s\S]*?)<\/script>/g)];
|
||
for (const m of scripts) {
|
||
try { new Function(m[1]); }
|
||
catch (e) { console.error('JS PARSE FAIL:', e.message.slice(0, 200)); process.exit(1); }
|
||
}
|
||
console.log('inline JS parses OK');
|
||
|
||
const fns = [...h.matchAll(/function build_p(\d+)\(\)/g)].map(m => parseInt(m[1]));
|
||
console.log('Builders after:', fns.length, fns);
|
||
if (fns.length !== 11) { console.error('LOST BUILDERS!'); process.exit(1); }
|