a6a9fb858c
Визуальный редизайн ch1 Тепловые явления: - Hero: заменён старый .hdr на новый .p8-hero с анимированным градиентом (thermal-shift 14s), огненным SVG-watermark справа (дышащая анимация 6s), live-meter в углу с пульсацией и плавной анимацией значения 37 → 100 → 0 → -10 → 25 → 80 °C. - Eyebrow 'Глава 1 · 11 параграфов', крупный title, sub-описание. - Section watermarks: в каждой <section sec-pN> добавлены тематические SVG (атом, конвекция, солнце, сосуд, фазовый переход, пузыри и т.д.) с opacity .07 на правой стороне. IV-6 §1 flagship interactive — Drag thermometer: - SVG-sandbox 560×320 с 4 телами (лёд, вода, чай, пар) разной T и относительной U. - Draggable термометр (P8Drag.attach + P8Helpers.svg). - При наведении на тело — изменяется цвет термометра по P8Helpers.thermal.tempColor(), readout табло показывают T (°C) и U (отн.). - +5 XP за 12 сек исследования. IV-6 stubs для §2-§11: 'Coming soon' плашки с тематическим SVG-иконкой clock. Расширим в Phase 1.2.
258 lines
15 KiB
JavaScript
258 lines
15 KiB
JavaScript
// Phase 1 — визуальный редизайн ch1 (Тепловые явления):
|
||
// 1. Hero: заменяет старый <header class="hdr"> на p8-hero с
|
||
// eyebrow, title, sub, live meter, watermark (огонь SVG).
|
||
// 2. Section watermarks: в каждом <section id="sec-pN"> добавляет
|
||
// тематический SVG-watermark (пламя/термометр/снежинка).
|
||
// 3. Inject IV-6 (drag-thermometer) в §1 — flagship интерактив.
|
||
// Остальные §2-11 получают IV-6 stub-placeholder с заголовком.
|
||
'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');
|
||
|
||
// === 1. Replace .hdr block with p8-hero ===
|
||
const FIRE_WM = `<svg viewBox="0 0 100 100" aria-hidden="true">
|
||
<path d="M50 8 C 52 22 65 30 64 46 C 63 56 56 60 55 48 C 53 56 48 60 42 58 C 36 56 32 50 34 42 C 30 52 22 60 24 72 C 26 84 36 92 50 92 C 64 92 76 84 76 70 C 76 50 60 40 56 22 C 54 14 52 10 50 8 Z"/>
|
||
</svg>`;
|
||
|
||
const NEW_HERO = `<header class="p8-hero">
|
||
<div class="p8-hero-wm">${FIRE_WM}</div>
|
||
<div class="p8-hero-meter" id="p8-meter-ch1"><span id="p8-meter-val">37</span>°C</div>
|
||
<div class="p8-hero-inner">
|
||
<div class="p8-hero-eyebrow">Глава 1 · 11 параграфов</div>
|
||
<h1 class="p8-hero-title">Тепловые явления</h1>
|
||
<div class="p8-hero-sub">Внутренняя энергия, способы теплопередачи, плавление и кипение. Перетаскивайте термометры, нагреватели и материалы — наблюдайте поведение тепла в реальном времени.</div>
|
||
<div class="hdr-side" style="margin-top:18px;display:flex;gap:8px;flex-wrap:wrap;position:relative;z-index:1">
|
||
<a href="/textbook/physics-8" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> К физике 8</a>
|
||
<button id="search-btn" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m21 21-4-4"/></svg> Поиск</button>
|
||
<button id="sidebar-btn" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="18" x2="14" y2="18"/></svg> Шпаргалка</button>
|
||
<button id="theme-btn" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg><span id="theme-lab">Тёмная</span></button>
|
||
</div>
|
||
</div>
|
||
</header>`;
|
||
|
||
const oldHdrRegex = /<header class="hdr">[\s\S]*?<\/header>/;
|
||
if (h.match(oldHdrRegex)) {
|
||
h = h.replace(oldHdrRegex, NEW_HERO);
|
||
console.log('Hero replaced');
|
||
}
|
||
|
||
// === 2. Update meter live: добавим скрипт, который анимирует значение в углу ===
|
||
const METER_SCRIPT = `
|
||
<script>
|
||
/* P8 hero meter — анимированный счётчик в углу (Phase 1 thermal) */
|
||
(function(){
|
||
function init(){
|
||
const el = document.getElementById('p8-meter-val');
|
||
if (!el || !window.P8Anim) return;
|
||
const targets = [37, 100, 0, -10, 25, 80];
|
||
let i = 0;
|
||
function step(){
|
||
const from = parseInt(el.textContent) || 0;
|
||
const to = targets[i % targets.length];
|
||
P8Anim.tween({
|
||
from, to, duration: 1400, easing: 'cubicInOut',
|
||
onUpdate: v => { el.textContent = Math.round(v); },
|
||
onComplete: () => { i++; setTimeout(step, 1800); }
|
||
});
|
||
}
|
||
setTimeout(step, 1200);
|
||
}
|
||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
|
||
else init();
|
||
})();
|
||
</script>
|
||
`;
|
||
if (!h.includes('P8 hero meter')) {
|
||
h = h.replace('</body>', METER_SCRIPT + '\n</body>');
|
||
console.log('Meter animation script added');
|
||
}
|
||
|
||
// === 3. Section watermarks — добавить data-attribute, CSS подхватит ===
|
||
// Используем pseudo-element через inline стиль не получится; вместо этого
|
||
// инжектим <div class="p8-sec-wm"> в каждую секцию.
|
||
|
||
// Watermark SVG-символы по §
|
||
const SEC_SYMBOLS = {
|
||
p1: '<svg viewBox="0 0 100 100"><circle cx="50" cy="50" r="28" stroke="currentColor" stroke-width="6" fill="none"/><path d="M50 22 v56 M22 50 h56" stroke="currentColor" stroke-width="3"/></svg>', // атом
|
||
p2: '<svg viewBox="0 0 100 100"><path d="M50 12 v76 M50 12 l-14 16 M50 12 l14 16 M50 88 l-14-16 M50 88 l14-16" stroke="currentColor" stroke-width="4" fill="none"/></svg>', // вверх/вниз
|
||
p3: '<svg viewBox="0 0 100 100"><path d="M14 50 h72 M86 50 l-14-14 M86 50 l-14 14" stroke="currentColor" stroke-width="5" fill="none"/></svg>', // правая стрелка
|
||
p4: '<svg viewBox="0 0 100 100"><path d="M30 80 C 30 50, 70 50, 70 30 M30 30 C 30 60, 70 60, 70 80" stroke="currentColor" stroke-width="4" fill="none"/></svg>', // спирали (конвекция)
|
||
p5: '<svg viewBox="0 0 100 100"><circle cx="50" cy="50" r="14" fill="currentColor"/><g stroke="currentColor" stroke-width="4" fill="none"><line x1="50" y1="6" x2="50" y2="22"/><line x1="50" y1="78" x2="50" y2="94"/><line x1="6" y1="50" x2="22" y2="50"/><line x1="78" y1="50" x2="94" y2="50"/><line x1="18" y1="18" x2="30" y2="30"/><line x1="70" y1="70" x2="82" y2="82"/><line x1="82" y1="18" x2="70" y2="30"/><line x1="30" y1="70" x2="18" y2="82"/></g></svg>', // солнце
|
||
p6: '<svg viewBox="0 0 100 100"><rect x="20" y="35" width="60" height="35" rx="4" stroke="currentColor" stroke-width="4" fill="none"/><path d="M28 35 v-8 M50 35 v-8 M72 35 v-8" stroke="currentColor" stroke-width="3"/></svg>', // сосуд
|
||
p7: '<svg viewBox="0 0 100 100"><path d="M28 78 L50 22 L72 78 Z" stroke="currentColor" stroke-width="4" fill="none"/><path d="M40 60 L60 60" stroke="currentColor" stroke-width="3"/></svg>', // пламя/огонь
|
||
p8: '<svg viewBox="0 0 100 100"><path d="M30 30 L70 30 L70 70 L30 70 Z" stroke="currentColor" stroke-width="4" fill="none"/><path d="M30 50 L70 50" stroke="currentColor" stroke-width="3" stroke-dasharray="4 3"/></svg>', // фазовый переход
|
||
p9: '<svg viewBox="0 0 100 100"><path d="M50 14 L70 50 L50 86 L30 50 Z" stroke="currentColor" stroke-width="4" fill="none"/></svg>', // ромб
|
||
p10: '<svg viewBox="0 0 100 100"><path d="M20 70 Q 35 50, 50 65 T 80 60" stroke="currentColor" stroke-width="4" fill="none"/><circle cx="78" cy="32" r="6" fill="currentColor"/></svg>', // капля + пар
|
||
p11: '<svg viewBox="0 0 100 100"><circle cx="35" cy="55" r="6" fill="currentColor"/><circle cx="55" cy="45" r="8" fill="currentColor"/><circle cx="65" cy="65" r="5" fill="currentColor"/><circle cx="50" cy="75" r="4" fill="currentColor"/></svg>', // пузыри
|
||
};
|
||
|
||
let secWmInjected = 0;
|
||
for (const pid of Object.keys(SEC_SYMBOLS)) {
|
||
const symbol = SEC_SYMBOLS[pid];
|
||
const secOpenRegex = new RegExp(`(<section[^>]+id="sec-${pid}"[^>]*>)`);
|
||
if (h.match(secOpenRegex) && !h.includes(`p8-sec-wm-${pid}`)) {
|
||
const wmDiv = `<div class="p8-sec-wm" id="p8-sec-wm-${pid}" aria-hidden="true">${symbol}</div>`;
|
||
h = h.replace(secOpenRegex, '$1\n ' + wmDiv);
|
||
secWmInjected++;
|
||
}
|
||
}
|
||
console.log('Section watermarks injected:', secWmInjected);
|
||
|
||
// Обеспечиваем что section имеет position:relative — добавляем inline-стиль
|
||
// (или полагаемся на существующий CSS .sec, который позиционирован)
|
||
// Проверим: ищем `.sec{` в style
|
||
// (Это уже есть в существующем CSS, sec — позиционирован относительно)
|
||
|
||
// === 4. IV-6 flagship: для §1 добавляем drag-thermometer интерактив ===
|
||
// Patch build_p1 — добавим IV-6 widget HTML + _initP1_iv6 функцию.
|
||
|
||
const IV6_P1_WIDGET = `
|
||
/* IV6 — Drag thermometer (Phase 1 flagship interactive) */
|
||
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="p1-iv6-sandbox" style="height:320px"></div>'
|
||
+'<div class="actions" 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="p1-iv6-T">—</span><span class="p8-readout-unit">°C</span></div>'
|
||
+'<div class="p8-readout"><span class="p8-readout-label">U отн.</span><span class="p8-readout-value" id="p1-iv6-U">—</span></div>'
|
||
+'</div>'
|
||
+'</div>';
|
||
`;
|
||
|
||
const IV6_P1_INIT = `
|
||
function _initP1_iv6(){
|
||
const sandbox = document.getElementById('p1-iv6-sandbox');
|
||
if (!sandbox || !window.P8Helpers || !window.P8Drag) return;
|
||
const svg = P8Helpers.svg.create(560, 320);
|
||
svg.setAttribute('width', '100%');
|
||
svg.setAttribute('height', '100%');
|
||
svg.style.display = 'block';
|
||
sandbox.appendChild(svg);
|
||
/* 4 тела: имя, T (°C), относительная U */
|
||
const bodies = [
|
||
{ name:'Лёд 1 кг', cx: 95, cy: 200, T: -10, U: 14, color:'#bfdbfe' },
|
||
{ name:'Вода 1 кг', cx: 230, cy: 200, T: 20, U: 100, color:'#7dd3fc' },
|
||
{ name:'Чай 0,3 кг',cx: 365, cy: 200, T: 80, U: 80, color:'#fb923c' },
|
||
{ name:'Пар 0,5 кг',cx: 500, cy: 200, T: 110, U: 200, color:'#ef4444' }
|
||
];
|
||
bodies.forEach(b => {
|
||
const g = P8Helpers.svg.el('g', { transform: 'translate('+b.cx+','+b.cy+')' });
|
||
g.appendChild(P8Helpers.svg.el('rect', {
|
||
x: -50, y: -55, width: 100, height: 110, rx: 12,
|
||
fill: b.color, stroke: '#0f172a', 'stroke-width': 1.5, opacity: 0.88
|
||
}));
|
||
g.appendChild(P8Helpers.svg.el('text', {
|
||
x: 0, y: -68,
|
||
'font-family': "'JetBrains Mono', monospace",
|
||
'font-size': 11, 'font-weight': 700,
|
||
fill: '#0f172a', 'text-anchor': 'middle',
|
||
text: b.name
|
||
}));
|
||
g.dataset = b;
|
||
svg.appendChild(g);
|
||
});
|
||
/* Термометр (draggable group) */
|
||
let thermoX = 50, thermoY = 70;
|
||
const thermoG = P8Helpers.svg.el('g', { transform: 'translate('+thermoX+','+thermoY+')', 'class': 'p8-draggable' });
|
||
/* Drop shadow rect */
|
||
thermoG.appendChild(P8Helpers.svg.el('rect', { x: -22, y: -10, width: 44, height: 130, fill: 'transparent' }));
|
||
/* Tube */
|
||
thermoG.appendChild(P8Helpers.svg.el('rect', {
|
||
x: -5, y: 0, width: 10, height: 100, rx: 5,
|
||
fill: '#f3f4f6', stroke: '#475569', 'stroke-width': 1.5
|
||
}));
|
||
const fill = P8Helpers.svg.el('rect', {
|
||
x: -3, y: 70, width: 6, height: 30, rx: 2, fill: '#f97316'
|
||
});
|
||
thermoG.appendChild(fill);
|
||
/* Bulb */
|
||
const bulb = P8Helpers.svg.el('circle', { cx: 0, cy: 110, r: 12, fill: '#f97316', stroke: '#475569', 'stroke-width': 1.5 });
|
||
thermoG.appendChild(bulb);
|
||
thermoG.appendChild(P8Helpers.svg.el('text', {
|
||
x: 0, y: -2,
|
||
'font-family': "'Inter', sans-serif", 'font-size': 10, 'font-weight': 700,
|
||
fill: '#0f172a', 'text-anchor': 'middle', text: 'Drag'
|
||
}));
|
||
svg.appendChild(thermoG);
|
||
/* Show current readout */
|
||
const tEl = document.getElementById('p1-iv6-T');
|
||
const uEl = document.getElementById('p1-iv6-U');
|
||
function checkHit(cx, cy){
|
||
for (const b of bodies){
|
||
if (Math.abs(cx - b.cx) < 50 && Math.abs(cy - b.cy) < 55){
|
||
const tColor = P8Helpers.thermal.tempColor((b.T + 20) / 130);
|
||
fill.setAttribute('fill', tColor);
|
||
bulb.setAttribute('fill', tColor);
|
||
if (tEl) tEl.textContent = b.T;
|
||
if (uEl) uEl.textContent = b.U;
|
||
return b;
|
||
}
|
||
}
|
||
fill.setAttribute('fill', '#94a3b8');
|
||
bulb.setAttribute('fill', '#94a3b8');
|
||
if (tEl) tEl.textContent = '—';
|
||
if (uEl) uEl.textContent = '—';
|
||
return null;
|
||
}
|
||
P8Drag.attach(thermoG, {
|
||
container: svg,
|
||
onMove: (ev, pos) => {
|
||
thermoX = Math.max(20, Math.min(540, pos.x));
|
||
thermoY = Math.max(10, Math.min(200, pos.y));
|
||
thermoG.setAttribute('transform', 'translate('+thermoX+','+thermoY+')');
|
||
checkHit(thermoX, thermoY + 110);
|
||
}
|
||
});
|
||
if (window.addXp) {
|
||
setTimeout(() => addXp(5, 'p1-iv6-explore'), 12000);
|
||
}
|
||
}
|
||
`;
|
||
|
||
const insertMarker = `box.innerHTML = h + secNavFor('p1') + readButton('p1');`;
|
||
if (!h.includes('p1-iv6-sandbox') && h.includes(insertMarker)) {
|
||
h = h.replace(insertMarker, IV6_P1_WIDGET.trim() + '\n\n ' + insertMarker);
|
||
// Add init call after wireReadBtn
|
||
h = h.replace(`wireReadBtn('p1');`, `wireReadBtn('p1');\n _initP1_iv6();`);
|
||
// Append init function after build_p1
|
||
const p1Start = h.indexOf('function build_p1()');
|
||
const p1End = h.indexOf('\n}\n', p1Start);
|
||
h = h.slice(0, p1End + 3) + '\n' + IV6_P1_INIT.trim() + '\n' + h.slice(p1End + 3);
|
||
console.log('IV-6 §1 drag-thermometer injected');
|
||
}
|
||
|
||
// === 5. Stub IV-6 placeholders для §2-11 ===
|
||
for (let n = 2; n <= 11; n++) {
|
||
const pid = 'p' + n;
|
||
const stubHtml = `
|
||
/* 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>';
|
||
`;
|
||
const marker = `box.innerHTML = h + secNavFor('${pid}') + readButton('${pid}');`;
|
||
if (!h.includes(`p8-iv6-${pid}`) && !h.includes(`Новый интерактив §${n}`) && h.includes(marker)) {
|
||
h = h.replace(marker, stubHtml.trim() + '\n\n ' + marker);
|
||
console.log(` ${pid}: IV-6 stub added`);
|
||
}
|
||
}
|
||
|
||
fs.writeFileSync(DST, h);
|
||
console.log('ch1 final size:', h.length);
|
||
|
||
// Sanity parse
|
||
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, 100)); process.exit(1); }
|
||
}
|
||
console.log('inline JS parses OK');
|