From cd14e1326fbf65f710575a37ec0b35d940469acb Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sat, 30 May 2026 10:08:49 +0300 Subject: [PATCH] =?UTF-8?q?fix(phys8=20ch1):=20Phase=201.2=20redo=20?= =?UTF-8?q?=E2=80=94=20CRLF-aware=20stub=20replace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Предыдущий коммит eaee79d удалил builders §3, §5, §6, §8 из-за greedy regex, который пересекал границы параграфов. Фактически жалкие 211 КБ файла вместо 280 КБ. redesign_p8_ch1_2.cjs переписан: - Использует точный stub-text per-paragraph (с 'Новый интерактив §N' в title — уникальный маркер). - Нормализует CRLF/LF (ch1.html на диске CRLF, шаблон — LF). - Делает простой h.replace(stubText, widget) без regex с greedy. - Sanity-чек: все 11 builders должны остаться на месте после patch. Восстановлены §3 Heat Conductor Bench, §6 Heat Mixer, §8 Phase Diagram T(t) — full IV-6 interactives с drag/scrubbers/Anim.raf. Размер ch1: 295851 байт. Все 11 builders + 5 IVs in каждом + IV-6 flagship в §1, §3, §6, §8. --- backend/scripts/redesign_p8_ch1_2.cjs | 238 +++++++------------- frontend/textbooks/physics_8_ch1.html | 306 ++++++++++++++++++++++++-- 2 files changed, 371 insertions(+), 173 deletions(-) diff --git a/backend/scripts/redesign_p8_ch1_2.cjs b/backend/scripts/redesign_p8_ch1_2.cjs index 7bc90bf..4eb5c8f 100644 --- a/backend/scripts/redesign_p8_ch1_2.cjs +++ b/backend/scripts/redesign_p8_ch1_2.cjs @@ -1,7 +1,6 @@ -// 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) — анимированный график плавления льда +// Phase 1.2 — Заменяет IV-6 stubs в §3, §6, §8 на полноценные интерактивы. +// Использует точный per-paragraph anchor — текст 'Новый интерактив §N' — для +// замены ровно одного стуба за раз. Без greedy match через границы. 'use strict'; const fs = require('fs'); const path = require('path'); @@ -9,18 +8,34 @@ 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]*?\\+'';\\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); +// Stub-HTML per paragraph N (та же форма что в redesign_p8_ch1.cjs). +// Заменяем эту точную строку на новый widgetHtml. +function makeStubText(n) { + return `/* IV6 — flagship интерактив (заглушка Phase 1, наполнение в Phase 1.${n}) */ + h += '
' + +'
IV-6
Новый интерактив §${n}
' + +'
Готовится: интерактивная визуализация с drag-and-drop для углубления темы. Скоро будет доступна.
' + +'
' + +'' + +'
Phase 1.${n} — coming soon
' + +'
' + +'
';`; +} + +function replaceStub(pid, n, widgetHtml, initFn) { + // File uses CRLF, my template uses LF — normalize stub to file's EOL style. + 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 text not found in file`); + return false; + } + const eol = stubText === stubCRLF ? '\r\n' : '\n'; + const widget = widgetHtml.trim().replace(/\n/g, eol); + h = h.replace(stubText, widget); // Add init call after wireReadBtn h = h.replace(`wireReadBtn('${pid}');`, `wireReadBtn('${pid}');\n _init${pid.toUpperCase()}_iv6();`); // Append init function after build_pN @@ -31,11 +46,8 @@ function replaceStub(pid, widgetHtml, initFn) { return true; } -// ===================================================================== -// §3 — Heat Conductor Bench -// ===================================================================== -const P3_HTML = ` - /* IV6 — Heat Conductor Bench (Phase 1.2) */ +// === §3 — Heat Conductor Bench === +const P3_HTML = `/* IV6 — Heat Conductor Bench (Phase 1.2) */ h += '
' +'
IV-6
Тепловая лавочка — какой материал быстрее проводит тепло?
' +'
Перетащи один из стержней (медь, дерево, стекло, серебро) на горелку. Цветовая карта покажет, как тепло движется по стержню. Чем больше λ — тем быстрее.
' @@ -45,8 +57,7 @@ const P3_HTML = ` +'
λВт/(м·К)
' +'
T дальнего конца°C
' +'
' - +''; -`; + +'';`; const P3_INIT = ` function _initP3_iv6(){ @@ -55,13 +66,11 @@ function _initP3_iv6(){ 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 }, @@ -69,9 +78,8 @@ function _initP3_iv6(){ { name:'Дерево', lam:0.15,color:'#a16207', x:500, y:50 } ]; const rodEls = []; - rods.forEach((rod, i) => { + rods.forEach(rod => { 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++) { @@ -82,39 +90,30 @@ function _initP3_iv6(){ 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 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) => { + simLoop = P8Anim.raf(dt => { 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)); + const heat = Math.max(0, Math.min(1, speed * simTime - 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; @@ -124,7 +123,6 @@ function _initP3_iv6(){ 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, @@ -134,30 +132,14 @@ function _initP3_iv6(){ 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; + rodEls.forEach((other, j) => { if (j !== i) resetColors(other); }); 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, @@ -166,16 +148,13 @@ function _initP3_iv6(){ })); } `; -replaceStub('p3', P3_HTML, P3_INIT); +replaceStub('p3', 3, P3_HTML, P3_INIT); -// ===================================================================== -// §6 — Heat Mixer (Q = cmΔT) -// ===================================================================== -const P6_HTML = ` - /* IV6 — Heat Mixer (Phase 1.2) */ +// === §6 — Heat Mixer === +const P6_HTML = `/* IV6 — Heat Mixer (Phase 1.2) */ h += '
' +'
IV-6
Смесь двух жидкостей — рассчитай конечную T
' - +'
Перетащи ёмкости друг к другу — они смешаются. Масса и начальная температура каждой — на скрубберах ниже. Конечная температура считается по уравнению теплового баланса $c m_1 (T_1 - T) = c m_2 (T - T_2)$.
' + +'
Установи массы и начальные T двух ёмкостей скрубберами, нажми «Смешать» и наблюдай за итоговой температурой по уравнению теплового баланса $c m_1 (T_1 - T) = c m_2 (T - T_2)$.
' +'
' +'
' +'
m₁0.5кг
' @@ -188,8 +167,7 @@ const P6_HTML = ` +'' +'' +'
' - +'
'; -`; + +'';`; const P6_INIT = ` function _initP6_iv6(){ @@ -198,41 +176,28 @@ function _initP6_iv6(){ 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 v1 = { x: 140, y: 130, m: 0.5, T: 80 }; + const v2 = { x: 420, y: 130, m: 1.0, T: 20 }; + const finalState = { active: false, T: 50 }; + function drawVessel(x, y, m, T){ 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 */ + const ht = 30 + m * 50; const w = 70; + g.appendChild(P8Helpers.svg.el('rect', { x:-w/2, y:-ht, width:w, height:ht, rx:6, fill:'rgba(255,255,255,.6)', stroke:'#0f172a', 'stroke-width':1.5 })); + g.appendChild(P8Helpers.svg.el('rect', { x:-w/2+3, y:-ht+5, width:w-6, height:ht-8, rx:4, fill: P8Helpers.thermal.tempColor(T/100) })); 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); + svg.appendChild(drawVessel(v1.x, v1.y, v1.m, v1.T)); + svg.appendChild(drawVessel(v2.x, v2.y, v2.m, v2.T)); } 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(drawVessel(280, 130, v1.m + v2.m, finalState.T)); 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); @@ -241,10 +206,7 @@ function _initP6_iv6(){ 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 = '—'; - } + if (finalState.active) { finalState.active = false; document.getElementById('p6-iv6-tf').textContent = '—'; } redraw(); }); } @@ -252,39 +214,28 @@ function _initP6_iv6(){ 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); - } + 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(); + finalState.active = false; document.getElementById('p6-iv6-tf').textContent = '—'; redraw(); }; redraw(); } `; -replaceStub('p6', P6_HTML, P6_INIT); +replaceStub('p6', 6, P6_HTML, P6_INIT); -// ===================================================================== -// §8 — Phase Diagram T(t) -// ===================================================================== -const P8_HTML = ` - /* IV6 — Phase Diagram T(t) (Phase 1.2) */ +// === §8 — Phase Diagram T(t) === +const P8_HTML = `/* IV6 — Phase Diagram T(t) (Phase 1.2) */ h += '
' +'
IV-6
График плавления — почему T не растёт?
' - +'
Запусти нагрев льда и наблюдай за графиком T(t). При плавлении энергия идёт на разрушение кристаллической решётки — T держится постоянной (плато при 0°C). Двигай ползунок мощности нагревателя — крутизна меняется.
' + +'
Запусти нагрев льда и наблюдай T(t). При плавлении энергия идёт на разрушение решётки — T держится постоянной (плато при 0°C). Двигай мощность нагревателя — крутизна меняется.
' +'
' +'
' +'
Мощность500Вт
' @@ -293,8 +244,7 @@ const P8_HTML = ` +'' +'' +'
' - +'
'; -`; + +'';`; const P8_INIT = ` function _initP8_iv6(){ @@ -304,34 +254,22 @@ function _initP8_iv6(){ 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; + const m = 0.5; + const c_ice = 2100, c_water = 4200, lambda = 330000, r_vap = 2300000; + let power = 500, energyAccumulated = 0, running = false; 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 */ + function tToPx(t) { return pad.l + plotW * Math.min(1, t / 300); } [-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: 'вода' }, @@ -342,13 +280,10 @@ function _initP8_iv6(){ 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(){ @@ -359,46 +294,37 @@ function _initP8_iv6(){ 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 'кипение'; + if (T < 0.5 && energyAccumulated < lambda * m) return 'плавление'; + if (T < 100) return 'вода'; + if (T < 100.5 && energyAccumulated < (lambda + r_vap) * m) return 'кипение'; return 'пар'; } function tick(dt){ if (!running) return; - const energy = power * dt; // J + const energy = power * dt; 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) */ + } else if (T < 0.5 && energyAccumulated < lambda * m) { energyAccumulated += energy; newT = 0; - if (energyAccumulated >= lambda * m) { - newT = 0.001; - } + if (energyAccumulated >= lambda * m) newT = 0.5; } 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) */ + } else if (T < 100.5 && energyAccumulated < (lambda + r_vap) * m) { energyAccumulated += energy; newT = 100; - if (energyAccumulated >= (lambda + r_vap) * m) { - newT = 100.001; - } + if (energyAccumulated >= (lambda + r_vap) * m) newT = 100.5; } else if (T < 120) { - const dT = energy / (c_water * m); // simplified for steam + const dT = energy / (c_water * m); newT = T + dT; if (newT > 120) newT = 120; - } else { - running = false; - } + } else { running = false; } const lastP = points[points.length-1]; points.push({ t: lastP.t + dt, T: newT }); if (points.length > 600) points.shift(); @@ -407,8 +333,7 @@ function _initP8_iv6(){ 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 raf = P8Anim.raf(dt => tick(Math.min(dt * 4, 0.5))); const pwrInp = document.getElementById('p8-iv6-pwr'); const pwrLab = document.getElementById('p8-iv6-pwr-val'); pwrInp.oninput = () => { power = +pwrInp.value; pwrLab.textContent = power; }; @@ -426,10 +351,10 @@ function _initP8_iv6(){ updatePath(); } `; -replaceStub('p8', P8_HTML, P8_INIT); +replaceStub('p8', 8, P8_HTML, P8_INIT); fs.writeFileSync(DST, h); -console.log('ch1 size:', h.length); +console.log('ch1 final size:', h.length); const scripts = [...h.matchAll(/