From a6a9fb858c1bf80b5123303026125786e350380f Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sat, 30 May 2026 09:58:11 +0300 Subject: [PATCH] =?UTF-8?q?feat(phys8=20ch1):=20Phase=201=20visual=20hero?= =?UTF-8?q?=20+=20IV-6=20=C2=A71=20drag-thermometer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Визуальный редизайн 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: в каждой
добавлены тематические 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. --- backend/scripts/redesign_p8_ch1.cjs | 257 ++++++++++++++++++++++++ frontend/textbooks/physics_8_ch1.html | 275 ++++++++++++++++++++++++-- 2 files changed, 514 insertions(+), 18 deletions(-) create mode 100644 backend/scripts/redesign_p8_ch1.cjs diff --git a/backend/scripts/redesign_p8_ch1.cjs b/backend/scripts/redesign_p8_ch1.cjs new file mode 100644 index 0000000..fd81ace --- /dev/null +++ b/backend/scripts/redesign_p8_ch1.cjs @@ -0,0 +1,257 @@ +// Phase 1 — визуальный редизайн ch1 (Тепловые явления): +// 1. Hero: заменяет старый
на p8-hero с +// eyebrow, title, sub, live meter, watermark (огонь SVG). +// 2. Section watermarks: в каждом
добавляет +// тематический 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 = ``; + +const NEW_HERO = `
+
${FIRE_WM}
+
37°C
+
+
Глава 1 · 11 параграфов
+

Тепловые явления

+
Внутренняя энергия, способы теплопередачи, плавление и кипение. Перетаскивайте термометры, нагреватели и материалы — наблюдайте поведение тепла в реальном времени.
+
+ К физике 8 + + + +
+
+
`; + +const oldHdrRegex = /
[\s\S]*?<\/header>/; +if (h.match(oldHdrRegex)) { + h = h.replace(oldHdrRegex, NEW_HERO); + console.log('Hero replaced'); +} + +// === 2. Update meter live: добавим скрипт, который анимирует значение в углу === +const METER_SCRIPT = ` + +`; +if (!h.includes('P8 hero meter')) { + h = h.replace('', METER_SCRIPT + '\n'); + console.log('Meter animation script added'); +} + +// === 3. Section watermarks — добавить data-attribute, CSS подхватит === +// Используем pseudo-element через inline стиль не получится; вместо этого +// инжектим
в каждую секцию. + +// Watermark SVG-символы по § +const SEC_SYMBOLS = { + p1: '', // атом + p2: '', // вверх/вниз + p3: '', // правая стрелка + p4: '', // спирали (конвекция) + p5: '', // солнце + p6: '', // сосуд + p7: '', // пламя/огонь + p8: '', // фазовый переход + p9: '', // ромб + p10: '', // капля + пар + p11: '', // пузыри +}; + +let secWmInjected = 0; +for (const pid of Object.keys(SEC_SYMBOLS)) { + const symbol = SEC_SYMBOLS[pid]; + const secOpenRegex = new RegExp(`(]+id="sec-${pid}"[^>]*>)`); + if (h.match(secOpenRegex) && !h.includes(`p8-sec-wm-${pid}`)) { + const wmDiv = ``; + 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 += '
' + +'
IV-6
Перетащи термометр
' + +'
Перетащи термометр на одно из четырёх тел и наблюдай, как меняется его температурный отсчёт. Тела разной массы и при разных условиях — оцени, в каком из них больше внутренней энергии.
' + +'
' + +'
' + +'
T°C
' + +'
U отн.
' + +'
' + +'
'; +`; + +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 += '
' + +'
IV-6
Новый интерактив §${n}
' + +'
Готовится: интерактивная визуализация с drag-and-drop для углубления темы. Скоро будет доступна.
' + +'
' + +'' + +'
Phase 1.${n} — coming soon
' + +'
' + +'
'; +`; + 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(/ + + +