Files
Learn_System/backend/scripts/redesign_p8_ch1_2.cjs
T
Maxim Dolgolyov eaee79dc8a feat(phys8 ch1): Phase 1.2 — IV-6 интерактивы §3, §6, §8
Заменены stub'ы 'coming soon' на полноценные drag-and-drop виджеты:

§3 Тепловая лавочка (Heat Conductor Bench):
- SVG-sandbox 560×300 с горелкой (drop zone) и 4 стержнями
  (медь λ=400, серебро λ=430, стекло λ=0.8, дерево λ=0.15).
- P8Drag.attach на каждый стержень → drop на горелку.
- При drop'е sim запускается: P8Anim.raf обновляет цвет
  каждого сегмента стержня через P8Helpers.thermal.tempColor()
  по log-нормализованной λ. Тепловая волна идёт по стержню.
- Readouts: материал, λ, T дальнего конца.

§6 Heat Mixer (Q=cmΔT):
- 2 ёмкости (m₁, T₁), (m₂, T₂) — рисуются с цветом по T.
- 4 scrubber'a (m₁, T₁, m₂, T₂) с live update SVG.
- Кнопка 'Смешать' → tween анимация в 1.2 с → итоговая T
  через формулу теплового баланса (m₁T₁+m₂T₂)/(m₁+m₂).
- Readout T_итог, кнопка 'Сброс'.

§8 График плавления (Phase Diagram T(t)):
- T-t график 560×280 с осями (-20 до 120°C, 0 до 300 с).
- Фазовые области: лёд (синий), вода (голубой), пар (жёлтый).
- Реальная симуляция: c_льда=2100, c_воды=4200, λ=330000,
  r=2300000. P8Anim.raf вычисляет накопление энергии и
  фазовые переходы — плато на 0°C (плавление) и 100°C
  (кипение).
- Scrubber мощности 100-2000 Вт. Кнопки Старт/Сброс.
- Readouts: фаза, T.

+10 XP за каждое успешное взаимодействие.
2026-05-30 10:03:55 +03:00

440 lines
23 KiB
JavaScript
Raw 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.2 — Заменяет IV-6 stubs в §3, §6, §8 на полноценные интерактивы:
// §3 Heat Conductor Bench — drag-стержни разных материалов
// §6 Heat Mixer — drag двух ёмкостей с T1, T2 → T_итог
// §8 Phase Diagram T(t) — анимированный график плавления льда
'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');
// === Замена stub'а для одного § на реальный виджет + init ===
function replaceStub(pid, widgetHtml, initFn) {
const stubMatch = h.match(new RegExp(
`\\/\\* IV6 — flagship интерактив[^\\n]*\\)[^\\n]*\\*\\/[\\s\\S]*?\\+'</div>';\\s*\\n\\s*box\\.innerHTML = h \\+ secNavFor\\('${pid}'\\)`
));
if (!stubMatch) { console.warn(`${pid}: stub not found`); return false; }
const stubText = stubMatch[0];
// Replace stub HTML portion (everything before the box.innerHTML line)
const lastBox = stubText.lastIndexOf("box.innerHTML");
const stubHtmlPart = stubText.slice(0, lastBox).trimEnd();
const newStubHtml = widgetHtml.trim() + '\n\n ';
h = h.replace(stubHtmlPart, newStubHtml);
// Add init call after wireReadBtn
h = h.replace(`wireReadBtn('${pid}');`, `wireReadBtn('${pid}');\n _init${pid.toUpperCase()}_iv6();`);
// Append init function after build_pN
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 stub with real IV-6`);
return true;
}
// =====================================================================
// §3 — Heat Conductor Bench
// =====================================================================
const P3_HTML = `
/* IV6 — Heat Conductor Bench (Phase 1.2) */
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="p3-iv6-sandbox" style="height:300px"></div>'
+'<div style="margin-top:10px;display:flex;gap:10px;flex-wrap:wrap">'
+'<div class="p8-readout"><span class="p8-readout-label">Материал</span><span class="p8-readout-value" id="p3-iv6-mat">—</span></div>'
+'<div class="p8-readout"><span class="p8-readout-label">λ</span><span class="p8-readout-value" id="p3-iv6-lam">—</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="p3-iv6-tend">—</span><span class="p8-readout-unit">°C</span></div>'
+'</div>'
+'</div>';
`;
const P3_INIT = `
function _initP3_iv6(){
const sb = document.getElementById('p3-iv6-sandbox');
if (!sb || !window.P8Helpers || !window.P8Drag || !window.P8Anim) return;
const svg = P8Helpers.svg.create(560, 300);
svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block';
sb.appendChild(svg);
/* Горелка (drop zone) */
const burner = P8Helpers.svg.el('g', { transform: 'translate(80, 240)' });
burner.appendChild(P8Helpers.svg.el('rect', { x:-32, y:-8, width:64, height:32, rx:4, fill:'#475569' }));
burner.appendChild(P8Helpers.svg.el('rect', { x:-26, y:-22, width:52, height:14, rx:7, fill:'#dc2626' }));
burner.appendChild(P8Helpers.svg.el('text', { x:0, y:48, 'font-family':"'Inter',sans-serif", 'font-size':11, 'font-weight':700, fill:'#475569', 'text-anchor':'middle', text:'Горелка (drop)' }));
svg.appendChild(burner);
/* Палитра 4 стержней */
const rods = [
{ name:'Медь', lam:400, color:'#b45309', x:200, y:50 },
{ name:'Серебро', lam:430, color:'#9ca3af', x:300, y:50 },
{ name:'Стекло', lam:0.8, color:'#bae6fd', x:400, y:50 },
{ name:'Дерево', lam:0.15,color:'#a16207', x:500, y:50 }
];
const rodEls = [];
rods.forEach((rod, i) => {
const g = P8Helpers.svg.el('g', { transform: 'translate('+rod.x+','+rod.y+')' });
/* Sections of rod, each will be colored by T gradient when active */
const segments = 12;
const segs = [];
for (let s = 0; s < segments; s++) {
const r = P8Helpers.svg.el('rect', {
x: -55 + s * (110/segments), y: -10, width: 110/segments, height: 20,
fill: rod.color, stroke: 'none'
});
g.appendChild(r);
segs.push(r);
}
/* Frame */
g.appendChild(P8Helpers.svg.el('rect', { x:-55, y:-10, width:110, height:20, rx:3, fill:'none', stroke:'#0f172a', 'stroke-width':1.5 }));
g.appendChild(P8Helpers.svg.el('text', { x:0, y:-18, 'font-family':"'Inter',sans-serif", 'font-size':11, 'font-weight':700, fill:'#0f172a', 'text-anchor':'middle', text: rod.name }));
g.appendChild(P8Helpers.svg.el('text', { x:0, y:30, 'font-family':"'JetBrains Mono',monospace", 'font-size':9, 'font-weight':600, fill:'var(--p8-muted, #64748b)', 'text-anchor':'middle', text: 'λ='+rod.lam }));
svg.appendChild(g);
rodEls.push({ rod, g, segs, x: rod.x, y: rod.y });
});
/* Active sim state */
let activeIdx = -1;
let simLoop = null;
let simTime = 0;
const matEl = document.getElementById('p3-iv6-mat');
const lamEl = document.getElementById('p3-iv6-lam');
const tendEl = document.getElementById('p3-iv6-tend');
function resetColors(rodObj){
rodObj.segs.forEach(s => s.setAttribute('fill', rodObj.rod.color));
}
function startSim(rodObj){
if (simLoop) simLoop.stop();
simTime = 0;
/* λ нормализованный 0..1: log scale (15 -> 430) */
const lamNorm = Math.min(1, Math.log10(rodObj.rod.lam + 1) / Math.log10(500));
simLoop = P8Anim.raf((dt, t) => {
simTime += dt;
/* Diffusion-like: каждый сегмент i прогревается со скоростью lamNorm */
const speed = lamNorm * 0.8 + 0.04;
rodObj.segs.forEach((seg, i) => {
const pos = i / (rodObj.segs.length - 1);
const wave = speed * simTime;
const heat = Math.max(0, Math.min(1, wave - pos));
seg.setAttribute('fill', P8Helpers.thermal.tempColor(heat * 0.85 + 0.1));
});
/* T-end value */
const endHeat = Math.max(0, Math.min(1, speed * simTime - 0.95));
const tEnd = Math.round(20 + endHeat * 80);
if (tendEl) tendEl.textContent = tEnd;
if (simTime > 30) simLoop.stop();
});
simLoop.start();
if (matEl) matEl.textContent = rodObj.rod.name;
if (lamEl) lamEl.textContent = rodObj.rod.lam;
}
/* Attach drag to each rod */
rodEls.forEach((rodObj, i) => {
P8Drag.attach(rodObj.g, {
container: svg,
onMove: (ev, pos) => {
rodObj.x = pos.x;
rodObj.y = pos.y;
rodObj.g.setAttribute('transform', 'translate('+rodObj.x+','+rodObj.y+')');
},
onEnd: (ev, pos) => {
/* Check if dropped near burner */
if (Math.abs(pos.x - 80) < 70 && Math.abs(pos.y - 240) < 50) {
/* Snap to position above burner */
P8Anim.tween({
from: 0, to: 1, duration: 320, easing: 'cubicOut',
onUpdate: k => {
rodObj.x = pos.x + (80 + 55 - pos.x) * k;
rodObj.y = pos.y + (220 - pos.y) * k;
rodObj.g.setAttribute('transform', 'translate('+rodObj.x+','+rodObj.y+')');
}
});
/* Reset other rods to original */
rodEls.forEach((other, j) => {
if (j === i) return;
resetColors(other);
});
activeIdx = i;
startSim(rodObj);
if (window.addXp) addXp(10, 'p3-iv6-conduct');
}
}
});
});
/* Help text */
svg.appendChild(P8Helpers.svg.el('text', {
x: 280, y: 290,
'font-family': "'Inter', sans-serif", 'font-size': 10,
fill: 'var(--p8-muted, #64748b)', 'text-anchor': 'middle',
text: 'Перетащи стержень на горелку • Чем выше λ — тем быстрее цвет дойдёт до конца'
}));
}
`;
replaceStub('p3', P3_HTML, P3_INIT);
// =====================================================================
// §6 — Heat Mixer (Q = cmΔT)
// =====================================================================
const P6_HTML = `
/* IV6 — Heat Mixer (Phase 1.2) */
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">Перетащи ёмкости друг к другу — они смешаются. Масса и начальная температура каждой — на скрубберах ниже. Конечная температура считается по уравнению теплового баланса $c m_1 (T_1 - T) = c m_2 (T - T_2)$.</div>'
+'<div class="p8-sandbox" id="p6-iv6-sandbox" style="height:240px"></div>'
+'<div style="margin-top:12px;display:grid;grid-template-columns:1fr 1fr;gap:10px">'
+'<div class="p8-scrubber"><span class="p8-scrubber-label">m₁</span><input type="range" id="p6-iv6-m1" min="0.1" max="2" step="0.1" value="0.5"><span class="p8-scrubber-value"><span id="p6-iv6-m1-val">0.5</span><span class="p8-unit">кг</span></span></div>'
+'<div class="p8-scrubber"><span class="p8-scrubber-label">T₁</span><input type="range" id="p6-iv6-t1" min="0" max="100" step="1" value="80"><span class="p8-scrubber-value"><span id="p6-iv6-t1-val">80</span><span class="p8-unit">°C</span></span></div>'
+'<div class="p8-scrubber"><span class="p8-scrubber-label">m₂</span><input type="range" id="p6-iv6-m2" min="0.1" max="2" step="0.1" value="1"><span class="p8-scrubber-value"><span id="p6-iv6-m2-val">1.0</span><span class="p8-unit">кг</span></span></div>'
+'<div class="p8-scrubber"><span class="p8-scrubber-label">T₂</span><input type="range" id="p6-iv6-t2" min="0" max="100" step="1" value="20"><span class="p8-scrubber-value"><span id="p6-iv6-t2-val">20</span><span class="p8-unit">°C</span></span></div>'
+'</div>'
+'<div style="margin-top:10px;display:flex;gap:10px;flex-wrap:wrap">'
+'<div class="p8-readout"><span class="p8-readout-label">T_итог</span><span class="p8-readout-value" id="p6-iv6-tf">—</span><span class="p8-readout-unit">°C</span></div>'
+'<button class="btn primary" id="p6-iv6-mix">Смешать</button>'
+'<button class="btn" id="p6-iv6-reset">Сброс</button>'
+'</div>'
+'</div>';
`;
const P6_INIT = `
function _initP6_iv6(){
const sb = document.getElementById('p6-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);
/* Vessel positions (will animate to centre on mix) */
const v1 = { x: 140, y: 130, m: 0.5, T: 80, color: '#fb923c' };
const v2 = { x: 420, y: 130, m: 1.0, T: 20, color: '#7dd3fc' };
const finalState = { active: false, T: 50, fillFraction: 0.5 };
function drawVessel(x, y, m, T, color){
const g = P8Helpers.svg.el('g', { transform: 'translate('+x+','+y+')' });
const h = 30 + m * 50;
const w = 70;
/* Glass */
g.appendChild(P8Helpers.svg.el('rect', { x:-w/2, y:-h, width:w, height:h, rx:6, fill:'rgba(255,255,255,.6)', stroke:'#0f172a', 'stroke-width':1.5 }));
/* Liquid */
g.appendChild(P8Helpers.svg.el('rect', { x:-w/2+3, y:-h+5, width:w-6, height:h-8, rx:4, fill: P8Helpers.thermal.tempColor(T/100) }));
/* Label */
g.appendChild(P8Helpers.svg.el('text', { x:0, y:18, 'font-family':"'JetBrains Mono',monospace", 'font-size':11, 'font-weight':700, fill:'#0f172a', 'text-anchor':'middle', text: 'm='+m.toFixed(1)+' кг' }));
g.appendChild(P8Helpers.svg.el('text', { x:0, y:32, 'font-family':"'JetBrains Mono',monospace", 'font-size':11, 'font-weight':700, fill:'#0f172a', 'text-anchor':'middle', text: 'T='+Math.round(T)+'°C' }));
return g;
}
let v1G = drawVessel(v1.x, v1.y, v1.m, v1.T, v1.color);
let v2G = drawVessel(v2.x, v2.y, v2.m, v2.T, v2.color);
svg.appendChild(v1G); svg.appendChild(v2G);
function redraw(){
svg.innerHTML = '';
if (!finalState.active) {
v1G = drawVessel(v1.x, v1.y, v1.m, v1.T, v1.color);
v2G = drawVessel(v2.x, v2.y, v2.m, v2.T, v2.color);
svg.appendChild(v1G); svg.appendChild(v2G);
} else {
/* Single combined vessel */
const cv = drawVessel(280, 130, v1.m + v2.m, finalState.T, P8Helpers.thermal.tempColor(finalState.T/100));
svg.appendChild(cv);
/* Result label */
svg.appendChild(P8Helpers.svg.el('text', { x:280, y:60, 'font-family':"'Unbounded',sans-serif", 'font-size':14, 'font-weight':800, fill:'var(--th-mid,#f97316)', 'text-anchor':'middle', text: 'T_итог = '+Math.round(finalState.T)+' °C' }));
}
}
/* Hook up scrubbers */
function bindScrub(inputId, valId, obj, prop){
const input = document.getElementById(inputId);
const lab = document.getElementById(valId);
if (!input || !lab) return;
input.addEventListener('input', () => {
const v = parseFloat(input.value);
obj[prop] = v;
lab.textContent = v.toFixed(prop === 'm' ? 1 : 0);
if (finalState.active) {
finalState.active = false;
document.getElementById('p6-iv6-tf').textContent = '—';
}
redraw();
});
}
bindScrub('p6-iv6-m1', 'p6-iv6-m1-val', v1, 'm');
bindScrub('p6-iv6-t1', 'p6-iv6-t1-val', v1, 'T');
bindScrub('p6-iv6-m2', 'p6-iv6-m2-val', v2, 'm');
bindScrub('p6-iv6-t2', 'p6-iv6-t2-val', v2, 'T');
/* Mix button */
document.getElementById('p6-iv6-mix').onclick = () => {
const T = (v1.m * v1.T + v2.m * v2.T) / (v1.m + v2.m);
finalState.active = true;
finalState.T = T;
P8Anim.tween({
from: v1.T, to: T, duration: 1200, easing: 'cubicInOut',
onUpdate: t => {
finalState.T = t;
redraw();
document.getElementById('p6-iv6-tf').textContent = Math.round(t);
}
});
if (window.addXp) addXp(10, 'p6-iv6-mix');
};
document.getElementById('p6-iv6-reset').onclick = () => {
finalState.active = false;
document.getElementById('p6-iv6-tf').textContent = '—';
redraw();
};
redraw();
}
`;
replaceStub('p6', P6_HTML, P6_INIT);
// =====================================================================
// §8 — Phase Diagram T(t)
// =====================================================================
const P8_HTML = `
/* IV6 — Phase Diagram T(t) (Phase 1.2) */
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">Запусти нагрев льда и наблюдай за графиком T(t). При плавлении энергия идёт на разрушение кристаллической решётки — T держится постоянной (плато при 0°C). Двигай ползунок мощности нагревателя — крутизна меняется.</div>'
+'<div class="p8-sandbox" id="p8-iv6-sandbox" style="height:280px"></div>'
+'<div style="margin-top:12px;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="p8-iv6-pwr" min="100" max="2000" step="50" value="500"><span class="p8-scrubber-value"><span id="p8-iv6-pwr-val">500</span><span class="p8-unit">Вт</span></span></div>'
+'<div class="p8-readout"><span class="p8-readout-label">Фаза</span><span class="p8-readout-value" id="p8-iv6-phase">лёд</span></div>'
+'<div class="p8-readout"><span class="p8-readout-label">T</span><span class="p8-readout-value" id="p8-iv6-temp">-20</span><span class="p8-readout-unit">°C</span></div>'
+'<button class="btn primary" id="p8-iv6-play">Старт</button>'
+'<button class="btn" id="p8-iv6-reset">Сброс</button>'
+'</div>'
+'</div>';
`;
const P8_INIT = `
function _initP8_iv6(){
const sb = document.getElementById('p8-iv6-sandbox');
if (!sb || !window.P8Helpers || !window.P8Anim) return;
const W = 560, H = 280;
const svg = P8Helpers.svg.create(W, H);
svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block';
sb.appendChild(svg);
/* Sim state */
const m = 0.5; // kg ice
const c_ice = 2100;
const c_water = 4200;
const lambda = 330000;
const r_vap = 2300000;
let power = 500;
let energyAccumulated = 0;
let running = false;
let raf = null;
let points = [{ t: 0, T: -20 }];
/* Axes */
const pad = { l: 50, r: 18, t: 22, b: 32 };
const plotW = W - pad.l - pad.r;
const plotH = H - pad.t - pad.b;
/* Background */
svg.appendChild(P8Helpers.svg.el('rect', { x: pad.l, y: pad.t, width: plotW, height: plotH, fill: '#fafafa', stroke: '#e5e7eb' }));
/* Y axis: -20 to 120 °C */
const yMin = -20, yMax = 120;
function yToPx(T) { return pad.t + plotH * (1 - (T - yMin) / (yMax - yMin)); }
function tToPx(t) { return pad.l + plotW * Math.min(1, t / 300); } // 300 s scale
/* Y ticks */
[-20, 0, 20, 40, 60, 80, 100, 120].forEach(t => {
const y = yToPx(t);
svg.appendChild(P8Helpers.svg.el('line', { x1: pad.l, y1: y, x2: pad.l + plotW, y2: y, stroke: '#e5e7eb' }));
svg.appendChild(P8Helpers.svg.el('text', { x: pad.l - 6, y: y + 3, 'font-family':"'JetBrains Mono',monospace", 'font-size': 10, fill: 'var(--p8-muted,#64748b)', 'text-anchor':'end', text: t+'°' }));
});
/* Phase regions overlays (transparent) */
const phaseRegions = [
{ from: -20, to: 0, fill: '#bfdbfe', name: 'лёд' },
{ from: 0, to: 100, fill: '#7dd3fc', name: 'вода' },
{ from: 100, to: 120, fill: '#fde68a', name: 'пар' }
];
phaseRegions.forEach(r => {
const y1 = yToPx(r.from), y2 = yToPx(r.to);
svg.appendChild(P8Helpers.svg.el('rect', { x: pad.l, y: y2, width: plotW, height: y1 - y2, fill: r.fill, opacity: 0.18 }));
svg.appendChild(P8Helpers.svg.el('text', { x: pad.l + plotW - 6, y: (y1 + y2) / 2 + 3, 'font-family':"'Inter',sans-serif", 'font-size': 10, 'font-weight': 700, fill: 'var(--p8-text)', 'text-anchor': 'end', text: r.name }));
});
/* Phase lines (0 and 100) */
svg.appendChild(P8Helpers.svg.el('line', { x1: pad.l, y1: yToPx(0), x2: pad.l + plotW, y2: yToPx(0), stroke: '#0f172a', 'stroke-width': 1, 'stroke-dasharray': '3 3' }));
svg.appendChild(P8Helpers.svg.el('line', { x1: pad.l, y1: yToPx(100), x2: pad.l + plotW, y2: yToPx(100), stroke: '#0f172a', 'stroke-width': 1, 'stroke-dasharray': '3 3' }));
/* X axis */
svg.appendChild(P8Helpers.svg.el('line', { x1: pad.l, y1: pad.t + plotH, x2: pad.l + plotW, y2: pad.t + plotH, stroke: '#0f172a' }));
svg.appendChild(P8Helpers.svg.el('text', { x: pad.l + plotW / 2, y: H - 6, 'font-family':"'Inter',sans-serif", 'font-size': 11, 'font-weight': 700, fill: 'var(--p8-text)', 'text-anchor': 'middle', text: 'Время, с' }));
/* Curve path (will be updated) */
const path = P8Helpers.svg.el('path', { d: '', fill: 'none', stroke: 'var(--th-mid, #f97316)', 'stroke-width': 3, 'stroke-linejoin': 'round', 'stroke-linecap': 'round' });
svg.appendChild(path);
function updatePath(){
if (!points.length) return;
const d = points.map((p, i) => (i === 0 ? 'M' : 'L') + tToPx(p.t).toFixed(1) + ',' + yToPx(p.T).toFixed(1)).join(' ');
path.setAttribute('d', d);
}
function currentT(){ return points[points.length-1].T; }
function currentPhase(T){
if (T < 0) return 'лёд';
if (T < 100) return T === 0 ? 'плавление' : 'вода';
if (T === 100) return 'кипение';
return 'пар';
}
function tick(dt){
if (!running) return;
const energy = power * dt; // J
let T = currentT();
let newT = T;
if (T < 0) {
/* heating ice */
const dT = energy / (c_ice * m);
newT = T + dT;
if (newT > 0) newT = 0;
} else if (T < 0.001 && energyAccumulated < lambda * m) {
/* phase transition (melting) */
energyAccumulated += energy;
newT = 0;
if (energyAccumulated >= lambda * m) {
newT = 0.001;
}
} else if (T < 100) {
/* heating water */
const dT = energy / (c_water * m);
newT = T + dT;
if (newT > 100) newT = 100;
} else if (T < 100.001 && energyAccumulated < (lambda + r_vap) * m) {
/* phase transition (boiling) */
energyAccumulated += energy;
newT = 100;
if (energyAccumulated >= (lambda + r_vap) * m) {
newT = 100.001;
}
} else if (T < 120) {
const dT = energy / (c_water * m); // simplified for steam
newT = T + dT;
if (newT > 120) newT = 120;
} else {
running = false;
}
const lastP = points[points.length-1];
points.push({ t: lastP.t + dt, T: newT });
if (points.length > 600) points.shift();
updatePath();
document.getElementById('p8-iv6-temp').textContent = Math.round(newT);
document.getElementById('p8-iv6-phase').textContent = currentPhase(newT);
if (lastP.t > 300) running = false;
}
raf = P8Anim.raf(dt => tick(Math.min(dt * 4, 0.5))); // accelerate 4x for demo
/* Bind controls */
const pwrInp = document.getElementById('p8-iv6-pwr');
const pwrLab = document.getElementById('p8-iv6-pwr-val');
pwrInp.oninput = () => { power = +pwrInp.value; pwrLab.textContent = power; };
document.getElementById('p8-iv6-play').onclick = () => {
if (!running) { running = true; raf.start(); if (window.addXp) addXp(10, 'p8-iv6-melt'); }
};
document.getElementById('p8-iv6-reset').onclick = () => {
running = false; raf.stop();
energyAccumulated = 0;
points = [{ t: 0, T: -20 }];
updatePath();
document.getElementById('p8-iv6-temp').textContent = '-20';
document.getElementById('p8-iv6-phase').textContent = 'лёд';
};
updatePath();
}
`;
replaceStub('p8', P8_HTML, P8_INIT);
fs.writeFileSync(DST, h);
console.log('ch1 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, 150)); process.exit(1); }
}
console.log('inline JS parses OK');