// 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(/