diff --git a/backend/scripts/redesign_p8_lab.cjs b/backend/scripts/redesign_p8_lab.cjs new file mode 100644 index 0000000..6e49d70 --- /dev/null +++ b/backend/scripts/redesign_p8_lab.cjs @@ -0,0 +1,384 @@ +// Phase 4 — Lab redesign: hero + section watermarks + 7 IV-6 sandboxes. +'use strict'; +const fs = require('fs'); +const path = require('path'); + +const DST = path.join(__dirname, '..', '..', 'frontend', 'textbooks', 'physics_8_lab.html'); +let h = fs.readFileSync(DST, 'utf8'); + +// === 1. Hero replacement === +const FLASK_WM = ` + + + + +`; + +const NEW_HERO = ` + ${FLASK_WM} + 7/7 ЛР + + Лабораторный практикум · 7 ЛР + Виртуальная лаборатория + Перетаскивайте термометры, нагреватели, динамометры и измерительные приборы. Собирайте установки и записывайте результаты. + + К физике 8 + Поиск + Шпаргалка + Тёмная + + +`; + +const oldHdrRegex = /[\s\S]*?<\/header>/; +if (h.match(oldHdrRegex)) { + h = h.replace(oldHdrRegex, NEW_HERO); + console.log('Hero replaced'); +} + +// === 2. Section watermarks === +const SEC_SYMBOLS = { + lr1: '', + lr2: '', + lr3: '', + lr4: '', + lr5: '', + lr6: 'P', + lr7: '' +}; + +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 = `${symbol}`; + h = h.replace(secOpenRegex, '$1\n ' + wmDiv); + secWmInjected++; + } +} +console.log('Section watermarks:', secWmInjected); + +// === 3. Inject IV-6 widgets into each lr-builder === +function injectIV6(lrid, title, helpHtml, body, init) { + const widgetHtml = ` + /* IV6 — ${title} (Phase 4) */ + h += '' + +'IV-6${title}' + +'${helpHtml}' + +'' + ${body} + +''; +`; + const initFn = ` +function _init${lrid.toUpperCase()}_iv6(){ + const sb = document.getElementById('${lrid}-iv6-sandbox'); + if (!sb || !window.P8Helpers) return; + ${init} +} +`; + const marker = `box.innerHTML = h + secNavFor('${lrid}') + readButton('${lrid}');`; + if (!h.includes(marker)) { console.warn(lrid+': no marker'); return; } + if (h.includes(`${lrid}-iv6-sandbox`)) { console.log(lrid+': already injected'); return; } + const eol = (h.indexOf('\r\n') >= 0) ? '\r\n' : '\n'; + const indented = widgetHtml.trim().replace(/\n/g, eol); + h = h.replace(marker, indented + eol + eol + ' ' + marker); + h = h.replace(`wireReadBtn('${lrid}');`, `wireReadBtn('${lrid}');${eol} _init${lrid.toUpperCase()}_iv6();`); + const fnStart = h.indexOf(`function build_${lrid}()`); + const fnEnd = h.indexOf('\n}\n', fnStart); + h = h.slice(0, fnEnd + 3) + eol + initFn.trim() + eol + h.slice(fnEnd + 3); + console.log(lrid+': injected IV-6'); +} + +// ============================================================ +// ЛР1 — Теплообмен (drag термометр + смешать жидкости) +// ============================================================ +injectIV6('lr1', 'Теплообмен — смешивание жидкостей', + 'Задавай начальные T₁ и T₂ скрубберами. Кнопка «Смешать» — итоговая T рассчитывается через тепловой баланс.', + '+\'T₁ (0.5 кг)80°CT₂ (1 кг)20°C\'+\'СмешатьT_итог—°C\'', + ` + const svg = P8Helpers.svg.create(560, 260); + svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block'; + sb.appendChild(svg); + let T1=80, T2=20, mixed=false, Tf=50; + function vessel(x, y, T, m){ + const g = P8Helpers.svg.el('g', { transform: 'translate('+x+','+y+')' }); + const ht = 40 + m*50; + g.appendChild(P8Helpers.svg.el('rect', { x:-32, y:-ht, width:64, height:ht, rx:5, fill:'rgba(255,255,255,.7)', stroke:'#0f172a', 'stroke-width':2 })); + g.appendChild(P8Helpers.svg.el('rect', { x:-29, y:-ht+3, width:58, height:ht-5, rx:3, fill: P8Helpers.thermal.tempColor(T/100) })); + g.appendChild(P8Helpers.svg.el('text', { x:0, y:18, 'font-family':"'JetBrains Mono',monospace", 'font-size':12, 'font-weight':700, fill:'#0f172a', 'text-anchor':'middle', text: 'T='+Math.round(T)+'°C' })); + return g; + } + function render(){ + svg.innerHTML=''; + if (!mixed){ + svg.appendChild(vessel(160, 180, T1, 0.5)); + svg.appendChild(vessel(400, 180, T2, 1)); + svg.appendChild(P8Helpers.svg.el('text', { x:160, y:235, 'font-family':"'Inter',sans-serif", 'font-size':11, fill:'var(--p8-muted,#64748b)', 'text-anchor':'middle', text:'m₁=0.5 кг' })); + svg.appendChild(P8Helpers.svg.el('text', { x:400, y:235, 'font-family':"'Inter',sans-serif", 'font-size':11, fill:'var(--p8-muted,#64748b)', 'text-anchor':'middle', text:'m₂=1 кг' })); + } else { + svg.appendChild(vessel(280, 180, Tf, 1.5)); + svg.appendChild(P8Helpers.svg.el('text', { x:280, y:70, 'font-family':"'Unbounded',sans-serif", 'font-size':18, 'font-weight':900, fill:'#10b981', 'text-anchor':'middle', text: 'T_итог = '+Math.round(Tf)+' °C' })); + } + } + document.getElementById('lr1-t1').oninput = ev => { T1 = +ev.target.value; document.getElementById('lr1-t1-val').textContent = T1; mixed = false; document.getElementById('lr1-tf').textContent='—'; render(); }; + document.getElementById('lr1-t2').oninput = ev => { T2 = +ev.target.value; document.getElementById('lr1-t2-val').textContent = T2; mixed = false; document.getElementById('lr1-tf').textContent='—'; render(); }; + document.getElementById('lr1-mix').onclick = () => { + Tf = (0.5*T1 + 1*T2)/(0.5+1); + mixed = true; + if (window.P8Anim) P8Anim.tween({ from: T1, to: Tf, duration: 1200, easing: 'cubicInOut', onUpdate: t => { Tf = t; render(); document.getElementById('lr1-tf').textContent = Math.round(t); } }); + else { render(); document.getElementById('lr1-tf').textContent = Math.round(Tf); } + if (window.addXp) addXp(15, 'lr1-iv6'); + }; + render(); + `); + +// ============================================================ +// ЛР2 — Удельная теплоёмкость (нагреватель + ёмкость) +// ============================================================ +injectIV6('lr2', 'Измерение удельной теплоёмкости', + 'Нагреватель мощности P подаёт Q=Pt в массу m. Из ΔT находим $c = Q/(m\\\\Delta T)$.', + '+\'P500Втm0.5кгt120с\'+\'Q60кДжΔT29Кc4200Дж/(кг·К)\'', + ` + const svg = P8Helpers.svg.create(560, 260); + svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block'; + sb.appendChild(svg); + let P=500, m=0.5, t=120; + const c_const = 4200; /* предположим вода */ + function render(){ + svg.innerHTML=''; + const Q = P*t; + const dT = Q/(c_const*m); + /* Vessel */ + const ht = 50+m*60; + svg.appendChild(P8Helpers.svg.el('rect', { x: 200, y: 200-ht, width: 160, height: ht, rx: 5, fill: P8Helpers.thermal.tempColor(Math.min(1, (20+dT)/120)), stroke: '#0f172a', 'stroke-width': 2, opacity: 0.85 })); + svg.appendChild(P8Helpers.svg.el('text', { x: 280, y: 200-ht+22, 'font-family':"'JetBrains Mono',monospace", 'font-size':12, 'font-weight':700, fill:'#fff', 'text-anchor':'middle', text: 'm='+m+' кг воды' })); + /* Heater */ + svg.appendChild(P8Helpers.svg.el('rect', { x: 240, y: 205, width: 80, height: 14, fill: '#dc2626', rx: 3 })); + svg.appendChild(P8Helpers.svg.el('text', { x: 280, y: 235, 'font-family':"'Inter',sans-serif", 'font-size':11, 'font-weight':700, fill:'#dc2626', 'text-anchor':'middle', text: 'нагреватель P='+P+' Вт' })); + /* Thermometer */ + svg.appendChild(P8Helpers.thermal.thermometerSVG(120, 60, 110, Math.min(1, (20+dT)/120))); + svg.appendChild(P8Helpers.svg.el('text', { x: 120, y: 50, 'font-family':"'JetBrains Mono',monospace", 'font-size':11, 'font-weight':800, fill:'#0f172a', 'text-anchor':'middle', text: 'T='+Math.round(20+dT)+'°C' })); + /* Updates */ + document.getElementById('lr2-q').textContent = (Q/1000).toFixed(1); + document.getElementById('lr2-dt').textContent = Math.round(dT); + document.getElementById('lr2-c').textContent = c_const; + } + document.getElementById('lr2-p').oninput = ev => { P = +ev.target.value; document.getElementById('lr2-p-val').textContent = P; render(); }; + document.getElementById('lr2-m').oninput = ev => { m = +ev.target.value; document.getElementById('lr2-m-val').textContent = m.toFixed(1); render(); }; + document.getElementById('lr2-t').oninput = ev => { t = +ev.target.value; document.getElementById('lr2-t-val').textContent = t; render(); }; + render(); + `); + +// ============================================================ +// ЛР3 — Простейшая цепь (батарея + лампа + А + V) +// ============================================================ +injectIV6('lr3', 'Сборка простейшей цепи', + 'Цепь: батарея → амперметр → лампа → вольтметр (параллельно). Двигай U — показания приборов обновляются.', + '+\'U батареи6.0В\'+\'A показывает0.50АV показывает6.0В\'', + ` + const svg = P8Helpers.svg.create(560, 260); + svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block'; + sb.appendChild(svg); + let U=6; + const R=12; + function render(){ + svg.innerHTML=''; + const I = U/R; + /* Battery left */ + svg.appendChild(P8Helpers.em.circuitComponent('battery', 80, 120, 'h', U.toFixed(1)+' В')); + /* Ammeter */ + svg.appendChild(P8Helpers.em.circuitComponent('ammeter', 220, 120, 'h')); + svg.appendChild(P8Helpers.svg.el('text', { x: 220, y: 100, 'font-family':"'JetBrains Mono',monospace", 'font-size':10, 'font-weight':800, fill:'#dc2626', 'text-anchor':'middle', text: I.toFixed(2)+' А' })); + /* Lamp */ + const lampG = P8Helpers.svg.el('g', { transform: 'translate(380, 120)' }); + const br = Math.min(1, I/1.2); + lampG.appendChild(P8Helpers.svg.el('circle', { cx: 0, cy: 0, r: 26, fill: '#fef3c7', opacity: br*0.6+0.1 })); + lampG.appendChild(P8Helpers.svg.el('circle', { cx: 0, cy: 0, r: 16, fill: '#fef3c7', stroke: '#0f172a', 'stroke-width': 2 })); + svg.appendChild(lampG); + /* Voltmeter (parallel above lamp) */ + svg.appendChild(P8Helpers.svg.el('line', { x1: 380, y1: 90, x2: 380, y2: 50, stroke: '#0f172a', 'stroke-width': 2 })); + svg.appendChild(P8Helpers.svg.el('line', { x1: 380, y1: 50, x2: 480, y2: 50, stroke: '#0f172a', 'stroke-width': 2 })); + svg.appendChild(P8Helpers.em.circuitComponent('voltmeter', 480, 50, 'h')); + svg.appendChild(P8Helpers.svg.el('line', { x1: 480, y1: 50, x2: 480, y2: 120, stroke: '#0f172a', 'stroke-width': 2 })); + svg.appendChild(P8Helpers.svg.el('text', { x: 480, y: 30, 'font-family':"'JetBrains Mono',monospace", 'font-size':10, 'font-weight':800, fill:'#2563eb', 'text-anchor':'middle', text: U.toFixed(1)+' В' })); + /* Wires */ + svg.appendChild(P8Helpers.svg.el('line', { x1: 110, y1: 120, x2: 190, y2: 120, stroke: '#0f172a', 'stroke-width': 2 })); + svg.appendChild(P8Helpers.svg.el('line', { x1: 250, y1: 120, x2: 354, y2: 120, stroke: '#0f172a', 'stroke-width': 2 })); + svg.appendChild(P8Helpers.svg.el('line', { x1: 380, y1: 136, x2: 510, y2: 136, stroke: '#0f172a', 'stroke-width': 2 })); + svg.appendChild(P8Helpers.svg.el('line', { x1: 510, y1: 136, x2: 510, y2: 190, stroke: '#0f172a', 'stroke-width': 2 })); + svg.appendChild(P8Helpers.svg.el('line', { x1: 50, y1: 120, x2: 50, y2: 190, stroke: '#0f172a', 'stroke-width': 2 })); + svg.appendChild(P8Helpers.svg.el('line', { x1: 50, y1: 190, x2: 510, y2: 190, stroke: '#0f172a', 'stroke-width': 2 })); + /* Updates */ + document.getElementById('lr3-a').textContent = I.toFixed(2); + document.getElementById('lr3-v').textContent = U.toFixed(1); + } + document.getElementById('lr3-u').oninput = ev => { U = +ev.target.value; document.getElementById('lr3-u-val').textContent = U.toFixed(1); render(); }; + render(); + `); + +// ============================================================ +// ЛР4 — Последовательное соединение +// ============================================================ +injectIV6('lr4', 'Последовательное соединение проводников', + 'Двигай R₁, R₂. Проверь: ток одинаков везде; напряжения складываются U = U₁ + U₂.', + '+\'R₁10ОмR₂20Ом\'+\'U₁4ВU₂8ВI0.4А\'', + ` + const svg = P8Helpers.svg.create(560, 260); + svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block'; + sb.appendChild(svg); + const U = 12; + let R1=10, R2=20; + function render(){ + svg.innerHTML=''; + const R = R1+R2, I = U/R, U1 = I*R1, U2 = I*R2; + /* Battery */ + svg.appendChild(P8Helpers.em.circuitComponent('battery', 80, 130, 'h', U+' В')); + /* R1 */ + svg.appendChild(P8Helpers.em.circuitComponent('resistor', 230, 130, 'h', R1+' Ом')); + svg.appendChild(P8Helpers.svg.el('text', { x: 230, y: 110, 'font-family':"'JetBrains Mono',monospace", 'font-size':10, 'font-weight':800, fill:'#dc2626', 'text-anchor':'middle', text: 'U₁='+U1.toFixed(1)+' В' })); + /* R2 */ + svg.appendChild(P8Helpers.em.circuitComponent('resistor', 400, 130, 'h', R2+' Ом')); + svg.appendChild(P8Helpers.svg.el('text', { x: 400, y: 110, 'font-family':"'JetBrains Mono',monospace", 'font-size':10, 'font-weight':800, fill:'#dc2626', 'text-anchor':'middle', text: 'U₂='+U2.toFixed(1)+' В' })); + /* Wires */ + svg.appendChild(P8Helpers.svg.el('line', { x1: 110, y1: 130, x2: 200, y2: 130, stroke: '#0f172a', 'stroke-width': 2 })); + svg.appendChild(P8Helpers.svg.el('line', { x1: 260, y1: 130, x2: 370, y2: 130, stroke: '#0f172a', 'stroke-width': 2 })); + svg.appendChild(P8Helpers.svg.el('line', { x1: 430, y1: 130, x2: 510, y2: 130, stroke: '#0f172a', 'stroke-width': 2 })); + svg.appendChild(P8Helpers.svg.el('line', { x1: 510, y1: 130, x2: 510, y2: 200, stroke: '#0f172a', 'stroke-width': 2 })); + svg.appendChild(P8Helpers.svg.el('line', { x1: 50, y1: 130, x2: 50, y2: 200, stroke: '#0f172a', 'stroke-width': 2 })); + svg.appendChild(P8Helpers.svg.el('line', { x1: 50, y1: 200, x2: 510, y2: 200, stroke: '#0f172a', 'stroke-width': 2 })); + /* I label */ + svg.appendChild(P8Helpers.svg.el('text', { x: 280, y: 220, 'font-family':"'JetBrains Mono',monospace", 'font-size':12, 'font-weight':800, fill:'#10b981', 'text-anchor':'middle', text: 'I = '+I.toFixed(3)+' А (одинаков везде)' })); + /* Updates */ + document.getElementById('lr4-u1').textContent = U1.toFixed(1); + document.getElementById('lr4-u2').textContent = U2.toFixed(1); + document.getElementById('lr4-i').textContent = I.toFixed(3); + } + document.getElementById('lr4-r1').oninput = ev => { R1 = +ev.target.value; document.getElementById('lr4-r1-val').textContent = R1; render(); }; + document.getElementById('lr4-r2').oninput = ev => { R2 = +ev.target.value; document.getElementById('lr4-r2-val').textContent = R2; render(); }; + render(); + `); + +// ============================================================ +// ЛР5 — Параллельное соединение +// ============================================================ +injectIV6('lr5', 'Параллельное соединение проводников', + 'Двигай R₁, R₂. Проверь: напряжение одинаково на обоих, токи складываются I = I₁ + I₂.', + '+\'R₁20ОмR₂30Ом\'+\'I₁0.6АI₂0.4АI1.0А\'', + ` + const svg = P8Helpers.svg.create(560, 260); + svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block'; + sb.appendChild(svg); + const U = 12; + let R1=20, R2=30; + function render(){ + svg.innerHTML=''; + const I1=U/R1, I2=U/R2, I=I1+I2; + svg.appendChild(P8Helpers.em.circuitComponent('battery', 80, 130, 'h', U+' В')); + svg.appendChild(P8Helpers.svg.el('line', { x1: 110, y1: 130, x2: 200, y2: 130, stroke: '#0f172a', 'stroke-width': 2 })); + svg.appendChild(P8Helpers.svg.el('line', { x1: 200, y1: 80, x2: 200, y2: 180, stroke: '#0f172a', 'stroke-width': 2 })); + svg.appendChild(P8Helpers.svg.el('line', { x1: 380, y1: 80, x2: 380, y2: 180, stroke: '#0f172a', 'stroke-width': 2 })); + svg.appendChild(P8Helpers.svg.el('line', { x1: 200, y1: 80, x2: 290, y2: 80, stroke: '#0f172a', 'stroke-width': 2 })); + svg.appendChild(P8Helpers.svg.el('line', { x1: 320, y1: 80, x2: 380, y2: 80, stroke: '#0f172a', 'stroke-width': 2 })); + svg.appendChild(P8Helpers.svg.el('line', { x1: 200, y1: 180, x2: 290, y2: 180, stroke: '#0f172a', 'stroke-width': 2 })); + svg.appendChild(P8Helpers.svg.el('line', { x1: 320, y1: 180, x2: 380, y2: 180, stroke: '#0f172a', 'stroke-width': 2 })); + svg.appendChild(P8Helpers.em.circuitComponent('resistor', 305, 80, 'h', R1+' Ом')); + svg.appendChild(P8Helpers.em.circuitComponent('resistor', 305, 180, 'h', R2+' Ом')); + svg.appendChild(P8Helpers.svg.el('text', { x: 290, y: 68, 'font-family':"'JetBrains Mono',monospace", 'font-size':10, 'font-weight':800, fill:'#dc2626', 'text-anchor':'middle', text: 'I₁='+I1.toFixed(2)+' А' })); + svg.appendChild(P8Helpers.svg.el('text', { x: 290, y: 200, 'font-family':"'JetBrains Mono',monospace", 'font-size':10, 'font-weight':800, fill:'#dc2626', 'text-anchor':'middle', text: 'I₂='+I2.toFixed(2)+' А' })); + svg.appendChild(P8Helpers.svg.el('line', { x1: 380, y1: 130, x2: 510, y2: 130, stroke: '#0f172a', 'stroke-width': 2 })); + svg.appendChild(P8Helpers.svg.el('line', { x1: 510, y1: 130, x2: 510, y2: 220, stroke: '#0f172a', 'stroke-width': 2 })); + svg.appendChild(P8Helpers.svg.el('line', { x1: 50, y1: 130, x2: 50, y2: 220, stroke: '#0f172a', 'stroke-width': 2 })); + svg.appendChild(P8Helpers.svg.el('line', { x1: 50, y1: 220, x2: 510, y2: 220, stroke: '#0f172a', 'stroke-width': 2 })); + svg.appendChild(P8Helpers.svg.el('text', { x: 150, y: 250, 'font-family':"'JetBrains Mono',monospace", 'font-size':12, 'font-weight':800, fill:'#10b981', 'text-anchor':'middle', text: 'I='+I.toFixed(2)+' А' })); + document.getElementById('lr5-i1').textContent = I1.toFixed(2); + document.getElementById('lr5-i2').textContent = I2.toFixed(2); + document.getElementById('lr5-i').textContent = I.toFixed(2); + } + document.getElementById('lr5-r1').oninput = ev => { R1 = +ev.target.value; document.getElementById('lr5-r1-val').textContent = R1; render(); }; + document.getElementById('lr5-r2').oninput = ev => { R2 = +ev.target.value; document.getElementById('lr5-r2-val').textContent = R2; render(); }; + render(); + `); + +// ============================================================ +// ЛР6 — Работа и мощность (P=UI, A=Pt) +// ============================================================ +injectIV6('lr6', 'Работа и мощность тока', + 'Задавай U, I и время t — рассчитаются P=UI, A=Pt. Лампа светится ярче с ростом P.', + '+\'U220ВI0.50Аt60с\'+\'P110ВтA6.6кДж\'', + ` + const svg = P8Helpers.svg.create(560, 260); + svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block'; + sb.appendChild(svg); + let U=220, I=0.5, t=60; + function render(){ + svg.innerHTML=''; + const P = U*I, A = P*t; + const br = Math.min(1, P/200); + /* Lamp */ + svg.appendChild(P8Helpers.svg.el('circle', { cx: 280, cy: 110, r: 55, fill: '#fef3c7', opacity: br*0.5+0.15 })); + svg.appendChild(P8Helpers.svg.el('circle', { cx: 280, cy: 110, r: 35, fill: '#fde047', stroke: '#0f172a', 'stroke-width': 2 })); + svg.appendChild(P8Helpers.svg.el('text', { x: 280, y: 113, 'font-family':"'Unbounded',sans-serif", 'font-size':18, 'font-weight':900, fill:'#0f172a', 'text-anchor':'middle', text: P.toFixed(0)+' Вт' })); + if (br > 0.5) { + for (let i = 0; i < 8; i++) { + const a = i*Math.PI/4; + svg.appendChild(P8Helpers.svg.el('line', { x1: 280+45*Math.cos(a), y1: 110+45*Math.sin(a), x2: 280+68*Math.cos(a), y2: 110+68*Math.sin(a), stroke: '#facc15', 'stroke-width': 3 })); + } + } + svg.appendChild(P8Helpers.svg.el('text', { x: 280, y: 220, 'font-family':"'JetBrains Mono',monospace", 'font-size':13, 'font-weight':800, fill:'#0f172a', 'text-anchor':'middle', text: 'P = UI = '+P.toFixed(1)+' Вт, A = Pt = '+(A/1000).toFixed(1)+' кДж' })); + document.getElementById('lr6-p').textContent = P.toFixed(1); + document.getElementById('lr6-a').textContent = (A/1000).toFixed(2); + } + document.getElementById('lr6-u').oninput = ev => { U = +ev.target.value; document.getElementById('lr6-u-val').textContent = U; render(); }; + document.getElementById('lr6-i').oninput = ev => { I = +ev.target.value; document.getElementById('lr6-i-val').textContent = I.toFixed(2); render(); }; + document.getElementById('lr6-t').oninput = ev => { t = +ev.target.value; document.getElementById('lr6-t-val').textContent = t; render(); }; + render(); + `); + +// ============================================================ +// ЛР7 — Отражение (закон отражения с протрактором) +// ============================================================ +injectIV6('lr7', 'Закон отражения света', + 'Двигай угол падения α — угол отражения β равен ему. Луч идёт по правилу: «угол падения = углу отражения».', + '+\'Угол падения40°\'+\'α40°β40°\'', + ` + const svg = P8Helpers.svg.create(560, 260); + svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block'; + sb.appendChild(svg); + let alpha = 40; + function render(){ + svg.innerHTML=''; + const cx = 280, cy = 210; + svg.appendChild(P8Helpers.optics.mirrorPlane(60, 210, 500, 210)); + svg.appendChild(P8Helpers.svg.el('line', { x1: cx, y1: 210, x2: cx, y2: 30, stroke: '#475569', 'stroke-width': 1.5, 'stroke-dasharray': '5 3' })); + const aRad = alpha*Math.PI/180; + const len = 170; + /* Incident */ + const inX = cx - len*Math.sin(aRad), inY = cy - len*Math.cos(aRad); + svg.appendChild(P8Helpers.optics.rayLine(inX, inY, cx, cy, { color: '#facc15', width: 3.5, glow: true })); + /* Reflected */ + const rX = cx + len*Math.sin(aRad), rY = cy - len*Math.cos(aRad); + svg.appendChild(P8Helpers.optics.rayLine(cx, cy, rX, rY, { color: '#facc15', width: 3.5, glow: true })); + /* Angle arcs */ + svg.appendChild(P8Helpers.svg.el('path', { d: 'M '+(cx-25*Math.sin(aRad/2))+' '+(cy-25*Math.cos(aRad/2))+' A 25 25 0 0 1 '+cx+' '+(cy-25)+' ', fill: 'none', stroke: '#dc2626', 'stroke-width': 2 })); + svg.appendChild(P8Helpers.svg.el('text', { x: cx-22, y: cy-40, 'font-family':"'JetBrains Mono',monospace", 'font-size':13, 'font-weight':800, fill:'#dc2626', 'text-anchor':'middle', text: 'α='+alpha+'°' })); + svg.appendChild(P8Helpers.svg.el('path', { d: 'M '+cx+' '+(cy-25)+' A 25 25 0 0 1 '+(cx+25*Math.sin(aRad/2))+' '+(cy-25*Math.cos(aRad/2))+' ', fill: 'none', stroke: '#16a34a', 'stroke-width': 2 })); + svg.appendChild(P8Helpers.svg.el('text', { x: cx+22, y: cy-40, 'font-family':"'JetBrains Mono',monospace", 'font-size':13, 'font-weight':800, fill:'#16a34a', 'text-anchor':'middle', text: 'β='+alpha+'°' })); + /* Verdict */ + svg.appendChild(P8Helpers.svg.el('text', { x: 280, y: 250, 'font-family':"'JetBrains Mono',monospace", 'font-size':12, 'font-weight':700, fill:'#10b981', 'text-anchor':'middle', text: '✓ α = β — закон отражения' })); + document.getElementById('lr7-a-out').textContent = alpha; + document.getElementById('lr7-b').textContent = alpha; + } + document.getElementById('lr7-a').oninput = ev => { alpha = +ev.target.value; document.getElementById('lr7-a-val').textContent = alpha; render(); }; + render(); + `); + +fs.writeFileSync(DST, h); +console.log('lab final size:', h.length); + +const scripts = [...h.matchAll(/