Files
Learn_System/backend/scripts/redesign_p8_ch1_3.cjs
Maxim Dolgolyov e85f7135ff feat(phys8 ch1): Phase 1.3 — IV-6 для §2, §4, §5, §7, §9, §10, §11
Заменены оставшиеся 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 парсится.
2026-05-30 10:12:29 +03:00

613 lines
34 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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); }