fix(phys8 ch1): Phase 1.2 redo — CRLF-aware stub replace
Предыдущий коммит 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.
This commit is contained in:
@@ -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]*?\\+'</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);
|
||||
// Stub-HTML per paragraph N (та же форма что в redesign_p8_ch1.cjs).
|
||||
// Заменяем эту точную строку на новый widgetHtml.
|
||||
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) {
|
||||
// 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 += '<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>'
|
||||
@@ -45,8 +57,7 @@ const P3_HTML = `
|
||||
+'<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>';
|
||||
`;
|
||||
+'</div>';`;
|
||||
|
||||
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 += '<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="wg-help">Установи массы и начальные T двух ёмкостей скрубберами, нажми «Смешать» и наблюдай за итоговой температурой по уравнению теплового баланса $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>'
|
||||
@@ -188,8 +167,7 @@ const P6_HTML = `
|
||||
+'<button class="btn primary" id="p6-iv6-mix">Смешать</button>'
|
||||
+'<button class="btn" id="p6-iv6-reset">Сброс</button>'
|
||||
+'</div>'
|
||||
+'</div>';
|
||||
`;
|
||||
+'</div>';`;
|
||||
|
||||
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 += '<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="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>'
|
||||
@@ -293,8 +244,7 @@ const P8_HTML = `
|
||||
+'<button class="btn primary" id="p8-iv6-play">Старт</button>'
|
||||
+'<button class="btn" id="p8-iv6-reset">Сброс</button>'
|
||||
+'</div>'
|
||||
+'</div>';
|
||||
`;
|
||||
+'</div>';`;
|
||||
|
||||
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(/<script>([\s\S]*?)<\/script>/g)];
|
||||
for (const m of scripts) {
|
||||
@@ -437,3 +362,8 @@ for (const m of scripts) {
|
||||
catch (e) { console.error('JS PARSE FAIL:', e.message.slice(0, 150)); process.exit(1); }
|
||||
}
|
||||
console.log('inline JS parses OK');
|
||||
|
||||
// Verify all 11 builders still present
|
||||
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); }
|
||||
|
||||
@@ -1,4 +1,255 @@
|
||||
<!doctype html>
|
||||
<!
|
||||
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);
|
||||
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 }];
|
||||
const pad = { l: 50, r: 18, t: 22, b: 32 };
|
||||
const plotW = W - pad.l - pad.r;
|
||||
const plotH = H - pad.t - pad.b;
|
||||
svg.appendChild(P8Helpers.svg.el('rect', { x: pad.l, y: pad.t, width: plotW, height: plotH, fill: '#fafafa', stroke: '#e5e7eb' }));
|
||||
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); }
|
||||
[-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+'°' }));
|
||||
});
|
||||
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 }));
|
||||
});
|
||||
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' }));
|
||||
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: 'Время, с' }));
|
||||
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 < 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;
|
||||
let T = currentT();
|
||||
let newT = T;
|
||||
if (T < 0) {
|
||||
const dT = energy / (c_ice * m);
|
||||
newT = T + dT;
|
||||
if (newT > 0) newT = 0;
|
||||
} else if (T < 0.5 && energyAccumulated < lambda * m) {
|
||||
energyAccumulated += energy;
|
||||
newT = 0;
|
||||
if (energyAccumulated >= lambda * m) newT = 0.5;
|
||||
} else if (T < 100) {
|
||||
const dT = energy / (c_water * m);
|
||||
newT = T + dT;
|
||||
if (newT > 100) newT = 100;
|
||||
} else if (T < 100.5 && energyAccumulated < (lambda + r_vap) * m) {
|
||||
energyAccumulated += energy;
|
||||
newT = 100;
|
||||
if (energyAccumulated >= (lambda + r_vap) * m) newT = 100.5;
|
||||
} else if (T < 120) {
|
||||
const dT = energy / (c_water * m);
|
||||
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;
|
||||
}
|
||||
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; };
|
||||
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();
|
||||
}
|
||||
|
||||
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);
|
||||
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 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;
|
||||
}
|
||||
function redraw(){
|
||||
svg.innerHTML = '';
|
||||
if (!finalState.active) {
|
||||
svg.appendChild(drawVessel(v1.x, v1.y, v1.m, v1.T));
|
||||
svg.appendChild(drawVessel(v2.x, v2.y, v2.m, v2.T));
|
||||
} else {
|
||||
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' }));
|
||||
}
|
||||
}
|
||||
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');
|
||||
document.getElementById('p6-iv6-mix').onclick = () => {
|
||||
const T = (v1.m * v1.T + v2.m * v2.T) / (v1.m + v2.m);
|
||||
finalState.active = true;
|
||||
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();
|
||||
}
|
||||
|
||||
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);
|
||||
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);
|
||||
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 => {
|
||||
const g = P8Helpers.svg.el('g', { transform: 'translate('+rod.x+','+rod.y+')' });
|
||||
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);
|
||||
}
|
||||
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 });
|
||||
});
|
||||
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;
|
||||
const lamNorm = Math.min(1, Math.log10(rodObj.rod.lam + 1) / Math.log10(500));
|
||||
simLoop = P8Anim.raf(dt => {
|
||||
simTime += dt;
|
||||
const speed = lamNorm * 0.8 + 0.04;
|
||||
rodObj.segs.forEach((seg, i) => {
|
||||
const pos = i / (rodObj.segs.length - 1);
|
||||
const heat = Math.max(0, Math.min(1, speed * simTime - pos));
|
||||
seg.setAttribute('fill', P8Helpers.thermal.tempColor(heat * 0.85 + 0.1));
|
||||
});
|
||||
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;
|
||||
}
|
||||
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) => {
|
||||
if (Math.abs(pos.x - 80) < 70 && Math.abs(pos.y - 240) < 50) {
|
||||
rodEls.forEach((other, j) => { if (j !== i) resetColors(other); });
|
||||
startSim(rodObj);
|
||||
if (window.addXp) addXp(10, 'p3-iv6-conduct');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
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: 'Перетащи стержень на горелку • Чем выше λ — тем быстрее цвет дойдёт до конца'
|
||||
}));
|
||||
}
|
||||
doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
@@ -1671,19 +1922,22 @@ function build_p3(){
|
||||
+'<div class="score-display" style="margin-top:10px"><span>Задача: <b id="p3-tasks5-i">1</b> / 5</span><span>Правильно: <b id="p3-tasks5-ok">0</b></span></div>'
|
||||
+'</div>';
|
||||
|
||||
/* IV6 — flagship интерактив (заглушка Phase 1, наполнение в Phase 1.3) */
|
||||
/* 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">Новый интерактив §3</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.3 — coming soon</div>'
|
||||
+'<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>';
|
||||
|
||||
box.innerHTML = h + secNavFor('p3') + readButton('p3');
|
||||
renderMath(box);
|
||||
wireReadBtn('p3');
|
||||
_initP3_iv6();
|
||||
_initp3_iv5();
|
||||
|
||||
_initP3_sim();
|
||||
@@ -2610,19 +2864,28 @@ function build_p6(){
|
||||
+'<div class="score-display" style="margin-top:10px"><span>Задача: <b id="p6-task-i">1</b> / 6</span><span>Правильно: <b id="p6-task-ok">0</b></span></div>'
|
||||
+'</div>';
|
||||
|
||||
/* IV6 — flagship интерактив (заглушка Phase 1, наполнение в Phase 1.6) */
|
||||
/* 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">Новый интерактив §6</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.6 — coming soon</div>'
|
||||
+'<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 двух ёмкостей скрубберами, нажми «Смешать» и наблюдай за итоговой температурой по уравнению теплового баланса $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>';
|
||||
|
||||
box.innerHTML = h + secNavFor('p6') + readButton('p6');
|
||||
renderMath(box);
|
||||
wireReadBtn('p6');
|
||||
_initP6_iv6();
|
||||
|
||||
_initP6_calc();
|
||||
_initP6_mix();
|
||||
@@ -3073,19 +3336,24 @@ function build_p8(){
|
||||
+'<div class="score-display" style="margin-top:10px"><span>Задача: <b id="p8-tasks5-i">1</b> / 5</span><span>Правильно: <b id="p8-tasks5-ok">0</b></span></div>'
|
||||
+'</div>';
|
||||
|
||||
/* IV6 — flagship интерактив (заглушка Phase 1, наполнение в Phase 1.8) */
|
||||
/* 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">Новый интерактив §8</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.8 — coming soon</div>'
|
||||
+'<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>';
|
||||
|
||||
box.innerHTML = h + secNavFor('p8') + readButton('p8');
|
||||
renderMath(box);
|
||||
wireReadBtn('p8');
|
||||
_initP8_iv6();
|
||||
_initp8_iv5();
|
||||
|
||||
_initP8_graph();
|
||||
|
||||
Reference in New Issue
Block a user