feat(phys8 ch2): Phase 2.1 — визуальный hero + 20 IV-6 stubs

Hero: новый p8-hero с electric-pulse градиентом (5s), молнией
SVG-watermark (flicker анимация 3.2s), live meter тока в углу
(0.5 → 2.0 → 1.2 → 0.8 → 1.5 А, плавная tween).

20 section watermarks: тематические SVG символы по теме § —
2 заряда, проводник, индукция, атом, силовые линии, U-стрелка,
батарея, цепь, омега Ω, зигзаг ρl/S, посл/парал соединения,
P-мощность, магнит N/S, компас, электромагнит.

20 IV-6 stubs: 'Новый интерактив §N · coming soon' (заглушки
для Phase 2. bulk content). Все 20 builders на месте,
JS парсится.
This commit is contained in:
Maxim Dolgolyov
2026-05-30 10:14:21 +03:00
parent d190fd2de9
commit 1de2aed05d
2 changed files with 415 additions and 27 deletions
+140
View File
@@ -0,0 +1,140 @@
// Phase 2.1 — визуальный редизайн ch2 (Электромагнитные явления):
// 1. Hero: новый p8-hero с electric theme, lightning SVG watermark.
// 2. Section watermarks: тематические SVG в каждой <section sec-pN>.
// 3. IV-6 stubs для §12-31 (20 параграфов).
'use strict';
const fs = require('fs');
const path = require('path');
const DST = path.join(__dirname, '..', '..', 'frontend', 'textbooks', 'physics_8_ch2.html');
let h = fs.readFileSync(DST, 'utf8');
// === 1. Replace .hdr block with p8-hero ===
const LIGHTNING_WM = `<svg viewBox="0 0 100 100" aria-hidden="true">
<path d="M55 8 L25 56 L46 56 L40 92 L75 38 L52 38 L60 8 Z"/>
</svg>`;
const NEW_HERO = `<header class="p8-hero">
<div class="p8-hero-wm">${LIGHTNING_WM}</div>
<div class="p8-hero-meter" id="p8-meter-ch2"><span id="p8-meter-val">0.5</span> А</div>
<div class="p8-hero-inner">
<div class="p8-hero-eyebrow">Глава 2 · 20 параграфов</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. Live meter скрипт (ток 0.5 → 2 → 1.2 → 0.8 → 1.5 А) ===
const METER_SCRIPT = `
<script>
/* P8 hero meter — анимированный ток (Phase 2 electric) */
(function(){
function init(){
const el = document.getElementById('p8-meter-val');
if (!el || !window.P8Anim) return;
const targets = [0.5, 2.0, 1.2, 0.8, 1.5];
let i = 0;
function step(){
const from = parseFloat(el.textContent) || 0;
const to = targets[i % targets.length];
P8Anim.tween({
from, to, duration: 1200, easing: 'cubicInOut',
onUpdate: v => { el.textContent = v.toFixed(1); },
onComplete: () => { i++; setTimeout(step, 1500); }
});
}
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 added');
}
// === 3. Section watermarks ===
const SEC_SYMBOLS = {
p12: '<svg viewBox="0 0 100 100"><circle cx="35" cy="50" r="14" fill="currentColor"/><circle cx="65" cy="50" r="14" fill="currentColor"/><path d="M40 50 L60 50 M50 40 L50 60" stroke="currentColor" stroke-width="3" fill="none"/></svg>', // 2 заряда
p13: '<svg viewBox="0 0 100 100"><rect x="20" y="40" width="60" height="20" fill="none" stroke="currentColor" stroke-width="4"/><circle cx="35" cy="50" r="3" fill="currentColor"/><circle cx="50" cy="50" r="3" fill="currentColor"/><circle cx="65" cy="50" r="3" fill="currentColor"/></svg>', // проводник
p14: '<svg viewBox="0 0 100 100"><circle cx="50" cy="50" r="24" fill="none" stroke="currentColor" stroke-width="4"/><path d="M50 26 L50 14 M50 86 L50 74 M26 50 L14 50 M86 50 L74 50" stroke="currentColor" stroke-width="4"/></svg>', // индукция
p15: '<svg viewBox="0 0 100 100"><circle cx="50" cy="50" r="16" fill="currentColor"/><path d="M50 30 L50 18 M50 82 L50 70 M30 50 L18 50 M82 50 L70 50" stroke="currentColor" stroke-width="3"/></svg>', // заряд центр
p16: '<svg viewBox="0 0 100 100"><circle cx="50" cy="50" r="6" fill="currentColor"/><ellipse cx="50" cy="50" rx="32" ry="14" fill="none" stroke="currentColor" stroke-width="2.5"/><ellipse cx="50" cy="50" rx="14" ry="32" fill="none" stroke="currentColor" stroke-width="2.5"/></svg>', // атом
p17: '<svg viewBox="0 0 100 100"><line x1="20" y1="50" x2="80" y2="50" stroke="currentColor" stroke-width="3"/><line x1="20" y1="35" x2="80" y2="65" stroke="currentColor" stroke-width="3"/><line x1="20" y1="65" x2="80" y2="35" stroke="currentColor" stroke-width="3"/></svg>', // силовые линии
p18: '<svg viewBox="0 0 100 100"><path d="M30 50 L70 50" stroke="currentColor" stroke-width="5" fill="none"/><path d="M65 45 L70 50 L65 55" stroke="currentColor" stroke-width="3" fill="none"/><text x="30" y="40" font-family="Inter" font-size="14" font-weight="700" fill="currentColor">U</text></svg>', // напряжение
p19: '<svg viewBox="0 0 100 100"><rect x="20" y="40" width="14" height="20" fill="currentColor"/><rect x="38" y="35" width="6" height="30" fill="currentColor"/><line x1="50" y1="50" x2="80" y2="50" stroke="currentColor" stroke-width="3"/></svg>', // батарея
p20: '<svg viewBox="0 0 100 100"><path d="M20 70 L40 30 L60 70 L80 30" stroke="currentColor" stroke-width="4" fill="none"/></svg>', // I=q/t
p21: '<svg viewBox="0 0 100 100"><rect x="20" y="40" width="60" height="20" fill="none" stroke="currentColor" stroke-width="3"/><path d="M30 50 L50 50 M55 45 L60 50 L55 55" stroke="currentColor" stroke-width="2" fill="none"/></svg>', // цепь
p22: '<svg viewBox="0 0 100 100"><text x="50" y="60" font-family="Unbounded" font-size="36" font-weight="900" fill="currentColor" text-anchor="middle">Ω</text></svg>', // Ом
p23: '<svg viewBox="0 0 100 100"><path d="M20 50 Q30 20, 40 50 T60 50 T80 50" stroke="currentColor" stroke-width="4" fill="none"/></svg>', // ρl/S зигзаг
p24: '<svg viewBox="0 0 100 100"><path d="M15 50 L25 50 L30 40 L40 60 L50 40 L60 60 L70 40 L75 50 L85 50" stroke="currentColor" stroke-width="3" fill="none"/></svg>', // последовательно
p25: '<svg viewBox="0 0 100 100"><path d="M20 30 L80 30 M20 70 L80 70 M50 30 L50 70" stroke="currentColor" stroke-width="3" fill="none"/></svg>', // параллельно
p26: '<svg viewBox="0 0 100 100"><text x="50" y="60" font-family="Unbounded" font-size="32" font-weight="900" fill="currentColor" text-anchor="middle">P</text></svg>', // мощность
p27: '<svg viewBox="0 0 100 100"><circle cx="50" cy="50" r="28" fill="none" stroke="currentColor" stroke-width="4"/><path d="M28 50 L72 50 M50 28 L50 72" stroke="currentColor" stroke-width="3"/></svg>', // энергия
p28: '<svg viewBox="0 0 100 100"><rect x="20" y="42" width="60" height="16" fill="currentColor"/><text x="32" y="56" font-family="Unbounded" font-size="14" font-weight="900" fill="#fff" text-anchor="middle">N</text><text x="68" y="56" font-family="Unbounded" font-size="14" font-weight="900" fill="#fff" text-anchor="middle">S</text></svg>', // магнит
p29: '<svg viewBox="0 0 100 100"><circle cx="50" cy="50" r="6" fill="currentColor"/><ellipse cx="50" cy="50" rx="36" ry="18" fill="none" stroke="currentColor" stroke-width="3"/><path d="M82 38 L86 48 L78 46" stroke="currentColor" stroke-width="3" fill="none"/></svg>', // силовые линии магн
p30: '<svg viewBox="0 0 100 100"><circle cx="50" cy="50" r="6" fill="currentColor"/><line x1="50" y1="20" x2="50" y2="80" stroke="currentColor" stroke-width="3"/><line x1="20" y1="50" x2="80" y2="50" stroke="currentColor" stroke-width="3"/></svg>', // компас
p31: '<svg viewBox="0 0 100 100"><path d="M30 30 Q40 20, 50 30 T70 30 M30 50 Q40 40, 50 50 T70 50 M30 70 Q40 60, 50 70 T70 70" stroke="currentColor" stroke-width="3" fill="none"/><rect x="48" y="20" width="4" height="60" 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);
// === 4. IV-6 stubs для §12-31 ===
let stubsAdded = 0;
for (let n = 12; n <= 31; n++) {
const pid = 'p' + n;
const stubHtml = `
/* IV6 — flagship интерактив (заглушка Phase 2, наполнение в Phase 2.${n}) */
h += '<div class="wg p8-iv6">'
+'<div class="wg-header"><span class="wg-badge p8-badge p8-badge-electric">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 2.${n} — coming soon</div>'
+'</div>'
+'</div>';
`;
const marker = `box.innerHTML = h + secNavFor('${pid}') + readButton('${pid}');`;
if (!h.includes(`Новый интерактив §${n}`) && h.includes(marker)) {
h = h.replace(marker, stubHtml.trim() + '\n\n ' + marker);
stubsAdded++;
}
}
console.log('IV-6 stubs added:', stubsAdded);
fs.writeFileSync(DST, h);
console.log('ch2 final size:', h.length);
const scripts = [...h.matchAll(/<script>([\s\S]*?)<\/script>/g)];
for (const m of scripts) {
try { new Function(m[1]); }
catch (e) { console.error('JS PARSE FAIL:', e.message.slice(0, 150)); process.exit(1); }
}
console.log('inline JS parses OK');
const fns = [...h.matchAll(/function build_p(\d+)\(\)/g)].map(m => parseInt(m[1]));
console.log('Builders after:', fns.length, fns);