From ca67ae6e0d20fa00b13ff2533d7174a831c7f227 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sat, 30 May 2026 10:26:17 +0300 Subject: [PATCH] =?UTF-8?q?feat(phys8=20ch3):=20Phase=203=20=E2=80=94=20?= =?UTF-8?q?=D0=A1=D0=B2=D0=B5=D1=82=D0=BE=D0=B2=D1=8B=D0=B5=20=D1=8F=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20(=D0=B2=D0=B8=D0=B7=D1=83?= =?UTF-8?q?=D0=B0=D0=BB=20+=209=20IV-6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hero: spectrum-drift градиент (18s), солнце SVG-watermark (rotate-анимация 40s), live-meter длины волны (400/470/550/600/700 нм с цветами цикла). 9 section watermarks: лампа, тень, угол, зеркало, парабола, рефракция, линза, призма, глаз. 9 IV-6 интерактивов: §32 Источники — кнопки 'Точечный/Протяжённый' с динамической тенью (точечный — чёткая, протяжённый — с полутенью). §33 Тени — drag-источника по X, размер тени пересчитывается проективно. §34 Закон отражения — scrubber угла, лучи + нормаль. §35 Плоское зеркало — drag-d объекта, мнимое изображение за зеркалом на том же расстоянии (штриховая стрелка). §36 Сферическое зеркало — drag-d, формула 1/v+1/d=1/F, изображение с правильным знаком/размером. §37 Преломление — scrubber угла, закон Снеллиуса (n₁=1, n₂=1.33). §38 Линза — 3 главных луча от объекта, формула v=dF/(d-F), изображение по принципу геометрической оптики. §39 Дисперсия — призма с разложением белого света на 7 цветов видимого спектра. §40 Глаз — кнопки 'Норма/Близорукость/Дальнозоркость' с fokus-точкой и корректирующей линзой (рассеив/собир). --- backend/scripts/redesign_p8_ch3.cjs | 544 ++++++++++++++++++++++++++ frontend/textbooks/physics_8_ch3.html | 506 +++++++++++++++++++++++- 2 files changed, 1034 insertions(+), 16 deletions(-) create mode 100644 backend/scripts/redesign_p8_ch3.cjs diff --git a/backend/scripts/redesign_p8_ch3.cjs b/backend/scripts/redesign_p8_ch3.cjs new file mode 100644 index 0000000..37409fb --- /dev/null +++ b/backend/scripts/redesign_p8_ch3.cjs @@ -0,0 +1,544 @@ +// Phase 3 — Ch3 Световые явления: hero + 9 section watermarks + 9 IV-6. +'use strict'; +const fs = require('fs'); +const path = require('path'); + +const DST = path.join(__dirname, '..', '..', 'frontend', 'textbooks', 'physics_8_ch3.html'); +let h = fs.readFileSync(DST, 'utf8'); + +// === 1. Hero replacement === +const SUN_WM = ``; + +const NEW_HERO = `
+
${SUN_WM}
+
λ=550 нм
+
+
Глава 3 · 9 параграфов
+

Световые явления

+
Лучи, тени, отражение, преломление, линзы, дисперсия, глаз. Перетаскивайте источники света и зеркала, наблюдайте за лучами и спектром.
+
+ К физике 8 + + + +
+
+
`; + +const oldHdrRegex = /
[\s\S]*?<\/header>/; +if (h.match(oldHdrRegex)) { + h = h.replace(oldHdrRegex, NEW_HERO); + console.log('Hero replaced'); +} + +// === 2. Live meter (wavelength cycles through visible spectrum) === +const METER_SCRIPT = ` + +`; +if (!h.includes('P8 hero meter')) { + h = h.replace('', METER_SCRIPT + '\n'); + console.log('Meter added'); +} + +// === 3. Section watermarks === +const SEC_SYMBOLS = { + p32: '', + p33: '', + p34: '', + p35: '', + p36: '', + p37: '', + p38: '', + p39: '', + p40: '' +}; + +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:', secWmInjected); + +// === 4. Stub function === +function makeStubText(n) { + return `/* IV6 — flagship интерактив (заглушка Phase 3, наполнение в Phase 3.${n}) */ + h += '
' + +'
IV-6
Новый интерактив §${n}
' + +'
Готовится: интерактивная визуализация с drag-and-drop для углубления темы. Скоро будет доступна.
' + +'
' + +'' + +'
Phase 3.${n} — coming soon
' + +'
' + +'
';`; +} + +function replaceWithReal(pid, n, widgetHtml, initFn) { + // Two paths: stub already present (need to replace) OR no stub (just inject before box.innerHTML). + 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; + const eol = (h.indexOf('\r\n') >= 0) ? '\r\n' : '\n'; + const widget = widgetHtml.trim().replace(/\n/g, eol); + if (stubText) { + h = h.replace(stubText, widget); + } else { + // Inject before box.innerHTML + const marker = `box.innerHTML = h + secNavFor('${pid}') + readButton('${pid}');`; + if (!h.includes(marker)) { console.warn(`${pid}: no marker`); return false; } + h = h.replace(marker, widget + eol + eol + ' ' + marker); + } + // Add init call + h = h.replace(`wireReadBtn('${pid}');`, `wireReadBtn('${pid}');${eol} _init${pid.toUpperCase()}_iv6();`); + // Append init function after build_pN + const fnStart = h.indexOf(`function build_${pid}()`); + const fnEnd = h.indexOf('\n}\n', fnStart); + h = h.slice(0, fnEnd + 3) + '\n' + initFn.trim() + '\n' + h.slice(fnEnd + 3); + console.log(`${pid}: injected real IV-6`); + return true; +} + +// === Compact widget builder === +function widget(pid, n, title, help, height, body, init) { + const html = `/* IV6 — ${title} (Phase 3) */ + h += '
' + +'
IV-6
${title}
' + +'
${help}
' + +'
' + ${body} + +'
';`; + const initFn = ` +function _init${pid.toUpperCase()}_iv6(){ + const sb = document.getElementById('${pid}-iv6-sandbox'); + if (!sb || !window.P8Helpers) return; + ${init} +} +`; + replaceWithReal(pid, n, html, initFn); +} + +// ============================================================ +// §32 — Источники света (типы) +// ============================================================ +widget('p32', 32, 'Точечные и протяжённые источники', + 'Точечный источник (свеча издалека) даёт чёткие тени. Протяжённый (Солнце) — размытые с полутенью.', + 240, + '+\'
\'', + ` + const svg = P8Helpers.svg.create(560, 240); + svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block'; + sb.appendChild(svg); + let mode = 'point'; + function render(){ + svg.innerHTML = ''; + /* Object */ + svg.appendChild(P8Helpers.svg.el('rect', { x: 230, y: 90, width: 30, height: 60, fill: '#475569' })); + if (mode === 'point') { + svg.appendChild(P8Helpers.svg.el('circle', { cx: 80, cy: 120, r: 12, fill: '#facc15', stroke: '#0f172a', 'stroke-width': 2 })); + svg.appendChild(P8Helpers.svg.el('line', { x1: 92, y1: 110, x2: 230, y2: 90, stroke: '#facc15', 'stroke-width': 2, 'stroke-dasharray': '3 3' })); + svg.appendChild(P8Helpers.svg.el('line', { x1: 92, y1: 130, x2: 230, y2: 150, stroke: '#facc15', 'stroke-width': 2, 'stroke-dasharray': '3 3' })); + /* Sharp shadow */ + svg.appendChild(P8Helpers.svg.el('polygon', { points: '260,90 460,30 460,210 260,150', fill: '#0f172a', opacity: 0.7 })); + svg.appendChild(P8Helpers.svg.el('text', { x: 380, y: 220, 'font-family':"'Inter',sans-serif", 'font-size':12, 'font-weight':700, fill: '#fff', 'text-anchor':'middle', text: 'Чёткая тень' })); + } else { + /* Sun */ + svg.appendChild(P8Helpers.svg.el('circle', { cx: 80, cy: 120, r: 30, fill: '#facc15', stroke: '#0f172a', 'stroke-width': 2 })); + svg.appendChild(P8Helpers.svg.el('line', { x1: 110, y1: 90, x2: 230, y2: 90, stroke: '#facc15', 'stroke-width': 1.5, 'stroke-dasharray': '3 3' })); + svg.appendChild(P8Helpers.svg.el('line', { x1: 110, y1: 150, x2: 230, y2: 150, stroke: '#facc15', 'stroke-width': 1.5, 'stroke-dasharray': '3 3' })); + /* Sharp inner shadow (umbra) */ + svg.appendChild(P8Helpers.svg.el('polygon', { points: '260,100 380,80 380,160 260,140', fill: '#0f172a', opacity: 0.7 })); + /* Penumbra */ + svg.appendChild(P8Helpers.svg.el('polygon', { points: '260,90 460,30 380,80 260,100', fill: '#0f172a', opacity: 0.3 })); + svg.appendChild(P8Helpers.svg.el('polygon', { points: '260,150 380,160 460,210 260,150', fill: '#0f172a', opacity: 0.3 })); + svg.appendChild(P8Helpers.svg.el('text', { x: 320, y: 220, 'font-family':"'Inter',sans-serif", 'font-size':12, 'font-weight':700, fill: '#0f172a', 'text-anchor':'middle', text: 'Тень + полутень' })); + } + } + document.getElementById('p32-iv6-point').onclick = () => { mode = 'point'; render(); }; + document.getElementById('p32-iv6-ext').onclick = () => { mode = 'ext'; render(); }; + render(); + `); + +// ============================================================ +// §33 — Тени (расстояние источник-объект) +// ============================================================ +widget('p33', 33, 'Тень и её размер', + 'Двигай источник света — наблюдай, как меняется размер тени. Чем ближе источник — тем больше тень.', + 240, + '+\'
Источник X80px
\'', + ` + const svg = P8Helpers.svg.create(560, 240); + svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block'; + sb.appendChild(svg); + let lampX = 80; + function render(){ + svg.innerHTML = ''; + /* Light */ + svg.appendChild(P8Helpers.svg.el('circle', { cx: lampX, cy: 120, r: 12, fill: '#facc15', stroke: '#0f172a', 'stroke-width': 2 })); + /* Object */ + const objX = 300; + svg.appendChild(P8Helpers.svg.el('rect', { x: objX - 12, y: 90, width: 24, height: 60, fill: '#475569' })); + /* Rays + shadow */ + const wallX = 510; + const t = (wallX - lampX) / (objX - lampX); + const yTop = 120 + (90 - 120) * t; + const yBot = 120 + (150 - 120) * t; + svg.appendChild(P8Helpers.svg.el('line', { x1: lampX + 12, y1: 110, x2: wallX, y2: yTop, stroke: '#facc15', 'stroke-width': 1.5, 'stroke-dasharray': '3 3' })); + svg.appendChild(P8Helpers.svg.el('line', { x1: lampX + 12, y1: 130, x2: wallX, y2: yBot, stroke: '#facc15', 'stroke-width': 1.5, 'stroke-dasharray': '3 3' })); + /* Wall */ + svg.appendChild(P8Helpers.svg.el('line', { x1: wallX, y1: 20, x2: wallX, y2: 220, stroke: '#0f172a', 'stroke-width': 3 })); + /* Shadow on wall */ + svg.appendChild(P8Helpers.svg.el('rect', { x: wallX, y: yTop, width: 28, height: yBot - yTop, fill: '#0f172a', opacity: 0.7 })); + svg.appendChild(P8Helpers.svg.el('text', { x: wallX + 14, y: yTop - 5, 'font-family':"'JetBrains Mono',monospace", 'font-size':10, 'font-weight':700, fill:'#0f172a', 'text-anchor':'middle', text: 'h='+(yBot-yTop).toFixed(0) })); + } + document.getElementById('p33-iv6-x').oninput = ev => { lampX = +ev.target.value; document.getElementById('p33-iv6-x-val').textContent = lampX; render(); }; + render(); + `); + +// ============================================================ +// §34 — Отражение (угол падения = угол отражения) +// ============================================================ +widget('p34', 34, 'Закон отражения', + 'Двигай угол падения — угол отражения равен ему.', + 240, + '+\'
Угол α35°
\'', + ` + const svg = P8Helpers.svg.create(560, 240); + svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block'; + sb.appendChild(svg); + let alpha = 35; + function render(){ + svg.innerHTML = ''; + const cx = 280, cy = 200; + /* Mirror */ + svg.appendChild(P8Helpers.optics.mirrorPlane(80, 200, 480, 200)); + /* Normal */ + svg.appendChild(P8Helpers.svg.el('line', { x1: cx, y1: 200, x2: cx, y2: 30, stroke: '#475569', 'stroke-width': 1.5, 'stroke-dasharray': '5 3' })); + svg.appendChild(P8Helpers.svg.el('text', { x: cx + 8, y: 40, 'font-family':"'Inter',sans-serif", 'font-size':11, 'font-weight':700, fill:'#475569', text: 'нормаль' })); + /* Incident ray */ + const rad = alpha * Math.PI / 180; + const len = 150; + const inX = cx - len * Math.sin(rad); + const inY = cy - len * Math.cos(rad); + svg.appendChild(P8Helpers.optics.rayLine(inX, inY, cx, cy, { color: '#facc15', width: 3, glow: true })); + /* Reflected ray */ + const rX = cx + len * Math.sin(rad); + const rY = cy - len * Math.cos(rad); + svg.appendChild(P8Helpers.optics.rayLine(cx, cy, rX, rY, { color: '#facc15', width: 3, glow: true })); + /* Angle labels */ + svg.appendChild(P8Helpers.svg.el('text', { x: cx - 25, y: 130, 'font-family':"'JetBrains Mono',monospace", 'font-size':13, 'font-weight':800, fill:'#dc2626', text: 'α='+alpha+'°' })); + svg.appendChild(P8Helpers.svg.el('text', { x: cx + 8, y: 130, 'font-family':"'JetBrains Mono',monospace", 'font-size':13, 'font-weight':800, fill:'#16a34a', text: 'β='+alpha+'°' })); + } + document.getElementById('p34-iv6-a').oninput = ev => { alpha = +ev.target.value; document.getElementById('p34-iv6-a-val').textContent = alpha; render(); }; + render(); + `); + +// ============================================================ +// §35 — Плоское зеркало (объект → мнимое изображение) +// ============================================================ +widget('p35', 35, 'Плоское зеркало', + 'Двигай объект — мнимое изображение появляется за зеркалом на том же расстоянии.', + 240, + '+\'
Дистанция d100px
\'', + ` + const svg = P8Helpers.svg.create(560, 240); + svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block'; + sb.appendChild(svg); + let d = 100; + function render(){ + svg.innerHTML = ''; + const mirX = 280; + /* Mirror */ + svg.appendChild(P8Helpers.svg.el('line', { x1: mirX, y1: 40, x2: mirX, y2: 200, stroke: '#0f172a', 'stroke-width': 4 })); + /* Hatch */ + for (let i = 0; i < 12; i++) { + svg.appendChild(P8Helpers.svg.el('line', { x1: mirX, y1: 45 + i * 14, x2: mirX + 8, y2: 49 + i * 14, stroke: '#475569', 'stroke-width': 1.5 })); + } + /* Object (arrow) */ + const objX = mirX - d; + svg.appendChild(P8Helpers.svg.el('line', { x1: objX, y1: 180, x2: objX, y2: 90, stroke: '#dc2626', 'stroke-width': 3 })); + svg.appendChild(P8Helpers.svg.el('polygon', { points: objX+',85 '+(objX-6)+',95 '+(objX+6)+',95', fill: '#dc2626' })); + svg.appendChild(P8Helpers.svg.el('text', { x: objX, y: 220, 'font-family':"'Inter',sans-serif", 'font-size':11, 'font-weight':700, fill:'#dc2626', 'text-anchor':'middle', text: 'объект' })); + /* Virtual image */ + const imgX = mirX + d; + svg.appendChild(P8Helpers.svg.el('line', { x1: imgX, y1: 180, x2: imgX, y2: 90, stroke: '#94a3b8', 'stroke-width': 3, 'stroke-dasharray': '4 3' })); + svg.appendChild(P8Helpers.svg.el('polygon', { points: imgX+',85 '+(imgX-6)+',95 '+(imgX+6)+',95', fill: '#94a3b8' })); + svg.appendChild(P8Helpers.svg.el('text', { x: imgX, y: 220, 'font-family':"'Inter',sans-serif", 'font-size':11, 'font-weight':700, fill:'#94a3b8', 'text-anchor':'middle', text: 'мнимое изображение' })); + /* Distance arrows */ + svg.appendChild(P8Helpers.svg.el('line', { x1: objX, y1: 60, x2: mirX, y2: 60, stroke: '#475569', 'stroke-width': 1.5 })); + svg.appendChild(P8Helpers.svg.el('line', { x1: mirX, y1: 60, x2: imgX, y2: 60, stroke: '#475569', 'stroke-width': 1.5 })); + svg.appendChild(P8Helpers.svg.el('text', { x: (objX + mirX) / 2, y: 52, 'font-family':"'JetBrains Mono',monospace", 'font-size':11, 'font-weight':700, fill:'#475569', 'text-anchor':'middle', text: 'd='+d })); + svg.appendChild(P8Helpers.svg.el('text', { x: (mirX + imgX) / 2, y: 52, 'font-family':"'JetBrains Mono',monospace", 'font-size':11, 'font-weight':700, fill:'#475569', 'text-anchor':'middle', text: 'd='+d })); + } + document.getElementById('p35-iv6-d').oninput = ev => { d = +ev.target.value; document.getElementById('p35-iv6-d-val').textContent = d; render(); }; + render(); + `); + +// ============================================================ +// §36 — Сферическое зеркало (фокус) +// ============================================================ +widget('p36', 36, 'Сферическое зеркало', + 'Двигай расстояние объекта до фокуса — изображение меняет тип (увеличенное/уменьшенное, прямое/перевёрнутое).', + 240, + '+\'
Объект → зеркало180мм
\'', + ` + const svg = P8Helpers.svg.create(560, 240); + svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block'; + sb.appendChild(svg); + const F = 100, mirX = 480; + let d = 180; + function render(){ + svg.innerHTML = ''; + /* Mirror curve */ + svg.appendChild(P8Helpers.svg.el('path', { d: 'M '+mirX+' 60 Q '+(mirX-30)+' 120, '+mirX+' 180', stroke: '#0f172a', 'stroke-width': 4, fill: 'none' })); + /* Axis */ + svg.appendChild(P8Helpers.svg.el('line', { x1: 50, y1: 120, x2: mirX, y2: 120, stroke: '#94a3b8', 'stroke-width': 1, 'stroke-dasharray': '4 3' })); + /* Focus */ + svg.appendChild(P8Helpers.svg.el('circle', { cx: mirX - F, cy: 120, r: 3, fill: '#16a34a' })); + svg.appendChild(P8Helpers.svg.el('text', { x: mirX - F, y: 138, 'font-family':"'JetBrains Mono',monospace", 'font-size':10, 'font-weight':700, fill:'#16a34a', 'text-anchor':'middle', text: 'F' })); + /* Object */ + const objX = mirX - d; + svg.appendChild(P8Helpers.svg.el('line', { x1: objX, y1: 120, x2: objX, y2: 80, stroke: '#dc2626', 'stroke-width': 2.5 })); + svg.appendChild(P8Helpers.svg.el('polygon', { points: objX+',75 '+(objX-4)+',82 '+(objX+4)+',82', fill: '#dc2626' })); + /* Lens formula: 1/v - 1/d = 1/F, here mirror equation: 1/v + 1/d = 1/F (using d positive in front) */ + const v = 1 / (1 / F - 1 / d); + const imgX = mirX - v; + const h_img = 40 * v / d * -1; + svg.appendChild(P8Helpers.svg.el('line', { x1: imgX, y1: 120, x2: imgX, y2: 120 + h_img, stroke: '#2563eb', 'stroke-width': 2.5, 'stroke-dasharray': v < 0 ? '4 3' : '0' })); + svg.appendChild(P8Helpers.svg.el('polygon', { points: imgX+','+(120 + h_img - Math.sign(h_img) * 4)+' '+(imgX-4)+','+(120 + h_img + 1)+' '+(imgX+4)+','+(120 + h_img + 1), fill: '#2563eb' })); + svg.appendChild(P8Helpers.svg.el('text', { x: 280, y: 220, 'font-family':"'JetBrains Mono',monospace", 'font-size':11, 'font-weight':700, fill:'#0f172a', 'text-anchor':'middle', text: 'd='+d+' мм, F='+F+', v='+v.toFixed(0)+' мм' })); + } + document.getElementById('p36-iv6-d').oninput = ev => { d = +ev.target.value; document.getElementById('p36-iv6-d-val').textContent = d; render(); }; + render(); + `); + +// ============================================================ +// §37 — Преломление (углы) +// ============================================================ +widget('p37', 37, 'Преломление света', + 'Двигай угол падения. На границе двух сред (воздух/вода n=1.33) угол преломления меньше.', + 240, + '+\'
Угол α40°
\'', + ` + const svg = P8Helpers.svg.create(560, 240); + svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block'; + sb.appendChild(svg); + let alpha = 40; + const n1 = 1, n2 = 1.33; + function render(){ + svg.innerHTML = ''; + const cx = 280, cy = 120; + /* Water region */ + svg.appendChild(P8Helpers.svg.el('rect', { x: 0, y: 120, width: 560, height: 120, fill: '#7dd3fc', opacity: 0.35 })); + /* Interface */ + svg.appendChild(P8Helpers.svg.el('line', { x1: 30, y1: cy, x2: 530, y2: cy, stroke: '#0f172a', 'stroke-width': 2 })); + /* Normal */ + svg.appendChild(P8Helpers.svg.el('line', { x1: cx, y1: 20, x2: cx, y2: 220, stroke: '#475569', 'stroke-width': 1.5, 'stroke-dasharray': '4 3' })); + /* Incident */ + const aRad = alpha * Math.PI / 180; + const len = 120; + const inX = cx - len * Math.sin(aRad); + const inY = cy - len * Math.cos(aRad); + svg.appendChild(P8Helpers.optics.rayLine(inX, inY, cx, cy, { color: '#facc15', width: 3, glow: true })); + /* Snell: n1 sin α = n2 sin β */ + const beta = Math.asin(Math.min(1, n1 / n2 * Math.sin(aRad))); + const rX = cx + len * Math.sin(beta); + const rY = cy + len * Math.cos(beta); + svg.appendChild(P8Helpers.optics.rayLine(cx, cy, rX, rY, { color: '#facc15', width: 3, glow: true })); + /* Labels */ + svg.appendChild(P8Helpers.svg.el('text', { x: cx - 25, y: 70, 'font-family':"'JetBrains Mono',monospace", 'font-size':13, 'font-weight':800, fill:'#dc2626', text: 'α='+alpha+'°' })); + svg.appendChild(P8Helpers.svg.el('text', { x: cx + 8, y: 175, 'font-family':"'JetBrains Mono',monospace", 'font-size':13, 'font-weight':800, fill:'#16a34a', text: 'β='+(beta * 180 / Math.PI).toFixed(1)+'°' })); + svg.appendChild(P8Helpers.svg.el('text', { x: 50, y: 50, 'font-family':"'Inter',sans-serif", 'font-size':11, 'font-weight':700, fill:'#475569', text: 'n₁=1 (воздух)' })); + svg.appendChild(P8Helpers.svg.el('text', { x: 50, y: 215, 'font-family':"'Inter',sans-serif", 'font-size':11, 'font-weight':700, fill:'#0f172a', text: 'n₂=1.33 (вода)' })); + } + document.getElementById('p37-iv6-a').oninput = ev => { alpha = +ev.target.value; document.getElementById('p37-iv6-a-val').textContent = alpha; render(); }; + render(); + `); + +// ============================================================ +// §38 — Линзы (3 главных луча) +// ============================================================ +widget('p38', 38, 'Собирающая линза — построение изображения', + 'Двигай объект. Три главных луча: через центр (прямо), параллельно главной оси (через F), через F (параллельно оси).', + 280, + '+\'
Дистанция d180мм
\'', + ` + const svg = P8Helpers.svg.create(560, 280); + svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block'; + sb.appendChild(svg); + let d = 180; + const F = 100, lensX = 320, axisY = 150; + function render(){ + svg.innerHTML = ''; + /* Axis */ + svg.appendChild(P8Helpers.svg.el('line', { x1: 30, y1: axisY, x2: 530, y2: axisY, stroke: '#94a3b8', 'stroke-width': 1, 'stroke-dasharray': '4 3' })); + /* Lens */ + svg.appendChild(P8Helpers.optics.lensSVG(lensX, axisY, 140, 'converging')); + /* F marks */ + svg.appendChild(P8Helpers.svg.el('circle', { cx: lensX - F, cy: axisY, r: 3, fill: '#16a34a' })); + svg.appendChild(P8Helpers.svg.el('circle', { cx: lensX + F, cy: axisY, r: 3, fill: '#16a34a' })); + svg.appendChild(P8Helpers.svg.el('text', { x: lensX - F - 8, y: axisY + 18, 'font-family':"'JetBrains Mono',monospace", 'font-size':10, 'font-weight':700, fill:'#16a34a', text: 'F' })); + svg.appendChild(P8Helpers.svg.el('text', { x: lensX + F + 8, y: axisY + 18, 'font-family':"'JetBrains Mono',monospace", 'font-size':10, 'font-weight':700, fill:'#16a34a', text: 'F' })); + /* Object */ + const objX = lensX - d; + const objH = 50; + svg.appendChild(P8Helpers.svg.el('line', { x1: objX, y1: axisY, x2: objX, y2: axisY - objH, stroke: '#dc2626', 'stroke-width': 3 })); + svg.appendChild(P8Helpers.svg.el('polygon', { points: objX+','+(axisY-objH-6)+' '+(objX-5)+','+(axisY-objH+2)+' '+(objX+5)+','+(axisY-objH+2), fill: '#dc2626' })); + /* Thin lens equation: 1/v - 1/(-d) = 1/F → v = dF/(d-F) (object on left, d>0) */ + const v = (d * F) / (d - F); + const imgX = lensX + v; + const imgH = objH * v / d; + /* Three principal rays */ + /* Ray 1: parallel to axis, refracts through far F */ + svg.appendChild(P8Helpers.optics.rayLine(objX, axisY - objH, lensX, axisY - objH, { color: '#facc15', width: 1.5, arrow: false })); + svg.appendChild(P8Helpers.optics.rayLine(lensX, axisY - objH, imgX, axisY + imgH, { color: '#facc15', width: 1.5, arrow: false })); + /* Ray 2: through optic center */ + svg.appendChild(P8Helpers.optics.rayLine(objX, axisY - objH, imgX, axisY + imgH, { color: '#16a34a', width: 1.5, arrow: false })); + /* Ray 3: through near F, refracts parallel */ + svg.appendChild(P8Helpers.optics.rayLine(objX, axisY - objH, lensX, axisY + ((lensX - objX) / (lensX - F - objX)) * (-objH) - (-objH) * ((lensX - F - objX) / (lensX - F - objX) - 1), { color: '#2563eb', width: 1.5, arrow: false })); + /* Image */ + svg.appendChild(P8Helpers.svg.el('line', { x1: imgX, y1: axisY, x2: imgX, y2: axisY + imgH, stroke: '#2563eb', 'stroke-width': 3 })); + svg.appendChild(P8Helpers.svg.el('polygon', { points: imgX+','+(axisY+imgH+6)+' '+(imgX-5)+','+(axisY+imgH-2)+' '+(imgX+5)+','+(axisY+imgH-2), fill: '#2563eb' })); + svg.appendChild(P8Helpers.svg.el('text', { x: 280, y: 260, 'font-family':"'JetBrains Mono',monospace", 'font-size':11, 'font-weight':700, fill:'#0f172a', 'text-anchor':'middle', text: 'd='+d+' мм, F='+F+', v='+v.toFixed(0)+' мм' })); + } + document.getElementById('p38-iv6-d').oninput = ev => { d = +ev.target.value; document.getElementById('p38-iv6-d-val').textContent = d; render(); }; + render(); + `); + +// ============================================================ +// §39 — Дисперсия (призма + спектр) +// ============================================================ +widget('p39', 39, 'Дисперсия — разложение белого света', + 'Через призму белый свет разлагается на спектр. Угол отклонения зависит от длины волны: красный отклоняется меньше, фиолетовый больше.', + 240, + '', + ` + const svg = P8Helpers.svg.create(560, 240); + svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block'; + sb.appendChild(svg); + function render(){ + svg.innerHTML = ''; + /* Incident white */ + svg.appendChild(P8Helpers.svg.el('line', { x1: 30, y1: 120, x2: 200, y2: 120, stroke: '#fff', 'stroke-width': 5 })); + svg.appendChild(P8Helpers.svg.el('line', { x1: 30, y1: 120, x2: 200, y2: 120, stroke: '#facc15', 'stroke-width': 2 })); + svg.appendChild(P8Helpers.svg.el('text', { x: 30, y: 105, 'font-family':"'Inter',sans-serif", 'font-size':11, 'font-weight':700, fill:'#0f172a', text: 'Белый свет' })); + /* Prism */ + svg.appendChild(P8Helpers.svg.el('polygon', { points: '200,180 280,40 360,180', fill: 'rgba(125,211,252,.35)', stroke: '#0284c7', 'stroke-width': 2 })); + /* Spectrum out */ + const colors = [ + { c: '#dc2626', off: 0, label: 'красный' }, + { c: '#f97316', off: 8, label: 'оранжевый' }, + { c: '#facc15', off: 16, label: 'жёлтый' }, + { c: '#16a34a', off: 24, label: 'зелёный' }, + { c: '#0ea5e9', off: 32, label: 'голубой' }, + { c: '#2563eb', off: 40, label: 'синий' }, + { c: '#7c3aed', off: 48, label: 'фиолетовый' } + ]; + colors.forEach((cl, i) => { + svg.appendChild(P8Helpers.svg.el('line', { x1: 290, y1: 120, x2: 530, y2: 100 + cl.off, stroke: cl.c, 'stroke-width': 2.5, 'stroke-linecap': 'round' })); + svg.appendChild(P8Helpers.svg.el('text', { x: 535, y: 104 + cl.off, 'font-family':"'Inter',sans-serif", 'font-size':9, 'font-weight':700, fill: cl.c, text: cl.label })); + }); + } + render(); + `); + +// ============================================================ +// §40 — Глаз / коррекция (близорукость / дальнозоркость) +// ============================================================ +widget('p40', 40, 'Глаз: аккомодация и очки', + 'Нормальный глаз: лучи фокусируются на сетчатке. Близорукий — перед сетчаткой (нужна рассеивающая). Дальнозоркий — за сетчаткой (нужна собирающая).', + 240, + '+\'
\'', + ` + const svg = P8Helpers.svg.create(560, 240); + svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block'; + sb.appendChild(svg); + let mode = 'normal'; + function render(){ + svg.innerHTML = ''; + /* Eye outline */ + svg.appendChild(P8Helpers.svg.el('ellipse', { cx: 380, cy: 120, rx: 90, ry: 70, fill: '#fff', stroke: '#0f172a', 'stroke-width': 2 })); + /* Cornea */ + svg.appendChild(P8Helpers.svg.el('path', { d: 'M 290 105 Q 270 120, 290 135', fill: '#bae6fd', stroke: '#0284c7', 'stroke-width': 2 })); + /* Lens */ + svg.appendChild(P8Helpers.svg.el('ellipse', { cx: 310, cy: 120, rx: 8, ry: 26, fill: 'rgba(125,211,252,.55)', stroke: '#0284c7', 'stroke-width': 1.5 })); + /* Retina */ + svg.appendChild(P8Helpers.svg.el('path', { d: 'M 440 80 Q 470 120, 440 160', stroke: '#dc2626', 'stroke-width': 3, fill: 'none' })); + svg.appendChild(P8Helpers.svg.el('text', { x: 465, y: 85, 'font-family':"'Inter',sans-serif", 'font-size':10, 'font-weight':700, fill:'#dc2626', text: 'сетчатка' })); + /* Rays */ + const focusX = mode === 'normal' ? 440 : (mode === 'myop' ? 420 : 480); + const colorFocus = mode === 'normal' ? '#16a34a' : '#dc2626'; + /* 3 incoming rays */ + [80, 120, 160].forEach(y => { + svg.appendChild(P8Helpers.svg.el('line', { x1: 50, y1: y, x2: 305, y2: y, stroke: '#facc15', 'stroke-width': 2 })); + svg.appendChild(P8Helpers.svg.el('line', { x1: 315, y1: y, x2: focusX, y2: 120, stroke: '#facc15', 'stroke-width': 2 })); + }); + /* Focus point */ + svg.appendChild(P8Helpers.svg.el('circle', { cx: focusX, cy: 120, r: 5, fill: colorFocus })); + /* Correction lens if needed */ + if (mode === 'myop') { + svg.appendChild(P8Helpers.optics.lensSVG(180, 120, 70, 'diverging')); + svg.appendChild(P8Helpers.svg.el('text', { x: 180, y: 200, 'font-family':"'Inter',sans-serif", 'font-size':10, 'font-weight':700, fill:'#2563eb', 'text-anchor':'middle', text: '−дптр (рассеивающая)' })); + } else if (mode === 'hyper') { + svg.appendChild(P8Helpers.optics.lensSVG(180, 120, 70, 'converging')); + svg.appendChild(P8Helpers.svg.el('text', { x: 180, y: 200, 'font-family':"'Inter',sans-serif", 'font-size':10, 'font-weight':700, fill:'#dc2626', 'text-anchor':'middle', text: '+дптр (собирающая)' })); + } + /* Label */ + const labels = { normal: 'Норма: фокус на сетчатке', myop: 'Близорукость: фокус перед сетчаткой', hyper: 'Дальнозоркость: фокус за сетчаткой' }; + svg.appendChild(P8Helpers.svg.el('text', { x: 280, y: 222, 'font-family':"'JetBrains Mono',monospace", 'font-size':11, 'font-weight':700, fill:'#0f172a', 'text-anchor':'middle', text: labels[mode] })); + } + document.getElementById('p40-iv6-normal').onclick = () => { mode = 'normal'; render(); }; + document.getElementById('p40-iv6-myop').onclick = () => { mode = 'myop'; render(); }; + document.getElementById('p40-iv6-hyper').onclick = () => { mode = 'hyper'; render(); }; + render(); + `); + +fs.writeFileSync(DST, h); +console.log('ch3 final size:', h.length); + +const scripts = [...h.matchAll(/ + + +