feat(phys11 ch3): Wave 6 — §18-§20 + phys-fx SphericalMirror/RefractionLab/PrismSpectrum
- phys-fx.js: PHYS.SphericalMirror (вогнутые/выпуклые с формулой и 3 лучами), PHYS.RefractionLab (закон Снелла + ПВО), PHYS.PrismSpectrum (дисперсия, 7 цветов через модель Коши) - §18: сферические зеркала, формула 1/d + 1/f = 1/F, увеличение Γ = -f/d, построение - §19: показатель преломления n=c/v, закон Снелла, полное внутр. отражение - §20: призма + дисперсия, плоскопараллельная пластинка, оптоволокно - 6 квизов (I5-I7 CALC/TH) + 3 босса (b5-b7) - §21-§23 + Final остаются заглушками для W7
This commit is contained in:
@@ -1033,4 +1033,300 @@ class FlatMirror {
|
||||
}
|
||||
P.FlatMirror = FlatMirror;
|
||||
|
||||
/* ============================================================ */
|
||||
/* SphericalMirror — вогнутое / выпуклое зеркало */
|
||||
/* ============================================================ */
|
||||
|
||||
class SphericalMirror {
|
||||
constructor(container, opts){
|
||||
opts = opts || {};
|
||||
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
|
||||
this.W = opts.width || 620;
|
||||
this.H = opts.height || 280;
|
||||
this.F = opts.F !== undefined ? opts.F : 80; /* фокусное расстояние в px */
|
||||
this.d = opts.d !== undefined ? opts.d : 180; /* расстояние до объекта в px */
|
||||
this.objH = opts.objH !== undefined ? opts.objH : 50; /* высота объекта в px */
|
||||
this.mode = opts.mode || 'concave'; /* 'concave' | 'convex' */
|
||||
this.color = opts.color || '#d97706';
|
||||
this.paused = true;
|
||||
this.render();
|
||||
}
|
||||
setF(v){ this.F = Math.max(20, v); this.render(); }
|
||||
setD(v){ this.d = Math.max(20, v); this.render(); }
|
||||
setMode(m){ this.mode = m; this.render(); }
|
||||
update(){}
|
||||
render(){
|
||||
if (!this.el) return;
|
||||
const W = this.W, H = this.H;
|
||||
const cy = H / 2;
|
||||
const mirrorX = W - 80; /* зеркало справа */
|
||||
const F = (this.mode === 'concave') ? this.F : -this.F;
|
||||
const focusX = mirrorX - F;
|
||||
const centerX = mirrorX - 2 * F;
|
||||
const d = this.d;
|
||||
const objX = mirrorX - d;
|
||||
/* Формула 1/d + 1/f = 1/F → f = 1/(1/F - 1/d) */
|
||||
let f;
|
||||
if (Math.abs(1/F - 1/d) < 1e-6) f = 1e9;
|
||||
else f = 1 / (1/F - 1/d);
|
||||
const imgX = mirrorX - f;
|
||||
/* Линейное увеличение Γ = -f/d (мнимое: f<0 → прямое, действ.: f>0 → перевёрнутое) */
|
||||
const G = -f / d;
|
||||
const imgH = this.objH * G;
|
||||
let svg = util.svgFrame(W, H, {bg:'#f8fafc'});
|
||||
/* Главная оптическая ось */
|
||||
svg += '<line x1="20" y1="' + cy + '" x2="' + (W - 10) + '" y2="' + cy + '" stroke="#94a3b8" stroke-width="1" stroke-dasharray="4 3"/>';
|
||||
/* Зеркало (дуга) */
|
||||
const R = 2 * Math.abs(F);
|
||||
if (this.mode === 'concave'){
|
||||
svg += '<path d="M ' + mirrorX + ' ' + (cy - 100) + ' Q ' + (mirrorX - 30) + ' ' + cy + ' ' + mirrorX + ' ' + (cy + 100) + '" stroke="#0f172a" stroke-width="3" fill="none"/>';
|
||||
} else {
|
||||
svg += '<path d="M ' + mirrorX + ' ' + (cy - 100) + ' Q ' + (mirrorX + 30) + ' ' + cy + ' ' + mirrorX + ' ' + (cy + 100) + '" stroke="#0f172a" stroke-width="3" fill="none"/>';
|
||||
}
|
||||
/* Штриховка зеркала */
|
||||
for (let i = 0; i < 8; i++){
|
||||
const y = cy - 90 + i * 22;
|
||||
const dx = this.mode === 'concave' ? 8 : -8;
|
||||
svg += '<line x1="' + (mirrorX + (this.mode==='concave'?1:-1)) + '" y1="' + y + '" x2="' + (mirrorX + dx) + '" y2="' + (y + 6) + '" stroke="#0f172a" stroke-width="1.1"/>';
|
||||
}
|
||||
/* Точки F и C */
|
||||
svg += '<circle cx="' + focusX + '" cy="' + cy + '" r="3.5" fill="#dc2626"/>';
|
||||
svg += '<text x="' + focusX + '" y="' + (cy + 18) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" fill="#dc2626" font-weight="700">F</text>';
|
||||
if (this.mode === 'concave'){
|
||||
svg += '<circle cx="' + centerX + '" cy="' + cy + '" r="3.5" fill="#1d4ed8"/>';
|
||||
svg += '<text x="' + centerX + '" y="' + (cy + 18) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" fill="#1d4ed8" font-weight="700">2F</text>';
|
||||
}
|
||||
/* Объект — красная стрелка вверх */
|
||||
const objTopY = cy - this.objH;
|
||||
svg += '<line x1="' + objX + '" y1="' + cy + '" x2="' + objX + '" y2="' + objTopY + '" stroke="' + this.color + '" stroke-width="3"/>';
|
||||
svg += '<polygon points="' + objX + ',' + objTopY + ' ' + (objX - 6) + ',' + (objTopY + 10) + ' ' + (objX + 6) + ',' + (objTopY + 10) + '" fill="' + this.color + '"/>';
|
||||
/* Лучи (3 канонических) */
|
||||
/* Луч 1: параллельный оптической оси → отражается через F */
|
||||
svg += '<line x1="' + objX + '" y1="' + objTopY + '" x2="' + mirrorX + '" y2="' + objTopY + '" stroke="#16a34a" stroke-width="1.6"/>';
|
||||
if (this.mode === 'concave'){
|
||||
svg += '<line x1="' + mirrorX + '" y1="' + objTopY + '" x2="' + focusX + '" y2="' + cy + '" stroke="#16a34a" stroke-width="1.6"/>';
|
||||
/* Продолжение до изображения */
|
||||
const slope1 = (cy - objTopY) / (focusX - mirrorX);
|
||||
const x2 = imgX, y2 = cy + slope1 * (x2 - focusX);
|
||||
svg += '<line x1="' + focusX + '" y1="' + cy + '" x2="' + x2 + '" y2="' + y2.toFixed(1) + '" stroke="#16a34a" stroke-width="1" stroke-dasharray="3 3"/>';
|
||||
} else {
|
||||
/* Выпуклое: отражается так, словно вышел из мнимого F справа */
|
||||
const slope = (objTopY - cy) / (mirrorX - focusX);
|
||||
svg += '<line x1="' + mirrorX + '" y1="' + objTopY + '" x2="20" y2="' + (objTopY + slope * (20 - mirrorX)).toFixed(1) + '" stroke="#16a34a" stroke-width="1.6"/>';
|
||||
svg += '<line x1="' + mirrorX + '" y1="' + objTopY + '" x2="' + (mirrorX + 50) + '" y2="' + (objTopY + slope * 50).toFixed(1) + '" stroke="#16a34a" stroke-width="1" stroke-dasharray="3 3"/>';
|
||||
}
|
||||
/* Луч 2: через F → отражается параллельно оси (только вогнутое) */
|
||||
if (this.mode === 'concave' && objX !== focusX){
|
||||
const slope2 = (cy - objTopY) / (focusX - objX);
|
||||
const hitY = objTopY + slope2 * (mirrorX - objX);
|
||||
svg += '<line x1="' + objX + '" y1="' + objTopY + '" x2="' + mirrorX + '" y2="' + hitY.toFixed(1) + '" stroke="#1d4ed8" stroke-width="1.6"/>';
|
||||
svg += '<line x1="' + mirrorX + '" y1="' + hitY.toFixed(1) + '" x2="' + Math.max(20, imgX) + '" y2="' + hitY.toFixed(1) + '" stroke="#1d4ed8" stroke-width="1.6"/>';
|
||||
}
|
||||
/* Изображение */
|
||||
if (Math.abs(imgX) < 1e8 && imgX > 20 && imgX < mirrorX){
|
||||
const imgTopY = cy - imgH;
|
||||
const dashed = (this.mode === 'convex' || imgH > 0); /* мнимое = пунктир */
|
||||
const isVirtual = (this.mode === 'convex') || (f < 0);
|
||||
const sd = isVirtual ? ' stroke-dasharray="4 3"' : '';
|
||||
const op = isVirtual ? 0.7 : 1.0;
|
||||
svg += '<line x1="' + imgX + '" y1="' + cy + '" x2="' + imgX + '" y2="' + imgTopY + '" stroke="#7c2d12" stroke-width="2.6"' + sd + ' opacity="' + op + '"/>';
|
||||
svg += '<polygon points="' + imgX + ',' + imgTopY + ' ' + (imgX - 5) + ',' + (imgTopY + (imgH > 0 ? 10 : -10)) + ' ' + (imgX + 5) + ',' + (imgTopY + (imgH > 0 ? 10 : -10)) + '" fill="#7c2d12" opacity="' + op + '"/>';
|
||||
}
|
||||
/* Подпись параметров */
|
||||
const Glabel = isFinite(G) ? G.toFixed(2) : '—';
|
||||
svg += '<text x="' + (W/2) + '" y="22" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" fill="#475569" font-weight="700">1/d + 1/f = 1/F · Γ = -f/d = ' + Glabel + '</text>';
|
||||
svg += '<text x="' + (W/2) + '" y="' + (H - 8) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="10" fill="#64748b">' + (this.mode==='concave'?'вогнутое':'выпуклое') + ' · F=' + Math.abs(F).toFixed(0) + 'px · d=' + d.toFixed(0) + 'px</text>';
|
||||
svg += '</svg>';
|
||||
this.el.innerHTML = svg;
|
||||
}
|
||||
}
|
||||
P.SphericalMirror = SphericalMirror;
|
||||
|
||||
/* ============================================================ */
|
||||
/* RefractionLab — преломление на границе двух сред (Снелл) */
|
||||
/* ============================================================ */
|
||||
|
||||
class RefractionLab {
|
||||
constructor(container, opts){
|
||||
opts = opts || {};
|
||||
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
|
||||
this.W = opts.width || 540;
|
||||
this.H = opts.height || 320;
|
||||
this.n1 = opts.n1 !== undefined ? opts.n1 : 1.0; /* воздух */
|
||||
this.n2 = opts.n2 !== undefined ? opts.n2 : 1.5; /* стекло */
|
||||
this.alpha = opts.alpha !== undefined ? opts.alpha : 35; /* градусов */
|
||||
this.color = opts.color || '#d97706';
|
||||
this.paused = true;
|
||||
this.render();
|
||||
}
|
||||
setN1(v){ this.n1 = Math.max(1, v); this.render(); }
|
||||
setN2(v){ this.n2 = Math.max(1, v); this.render(); }
|
||||
setAlpha(v){ this.alpha = Math.max(0, Math.min(89, v)); this.render(); }
|
||||
update(){}
|
||||
render(){
|
||||
if (!this.el) return;
|
||||
const W = this.W, H = this.H;
|
||||
const cx = W / 2, cy = H / 2;
|
||||
/* Углы (рад) */
|
||||
const a = this.alpha * Math.PI / 180;
|
||||
const sinB = this.n1 / this.n2 * Math.sin(a);
|
||||
const totalInternal = Math.abs(sinB) > 1;
|
||||
const b = totalInternal ? null : Math.asin(sinB);
|
||||
/* Критический угол n1>n2 */
|
||||
let critDeg = null;
|
||||
if (this.n1 > this.n2) critDeg = Math.asin(this.n2 / this.n1) * 180 / Math.PI;
|
||||
let svg = util.svgFrame(W, H, {bg:'#f8fafc'});
|
||||
/* Среда 1 (верх) */
|
||||
svg += '<rect x="0" y="0" width="' + W + '" height="' + cy + '" fill="#e0f2fe" opacity="0.5"/>';
|
||||
/* Среда 2 (низ) */
|
||||
svg += '<rect x="0" y="' + cy + '" width="' + W + '" height="' + cy + '" fill="#fef3c7" opacity="0.6"/>';
|
||||
/* Граница */
|
||||
svg += '<line x1="0" y1="' + cy + '" x2="' + W + '" y2="' + cy + '" stroke="#0f172a" stroke-width="2"/>';
|
||||
/* Нормаль (пунктир) */
|
||||
svg += '<line x1="' + cx + '" y1="40" x2="' + cx + '" y2="' + (H - 40) + '" stroke="#94a3b8" stroke-width="1" stroke-dasharray="5 4"/>';
|
||||
svg += '<text x="' + (cx + 8) + '" y="55" font-family="JetBrains Mono,monospace" font-size="10" fill="#64748b">нормаль</text>';
|
||||
/* Падающий луч (из верхней-левой области) */
|
||||
const L = 130;
|
||||
const ix = cx - L * Math.sin(a), iy = cy - L * Math.cos(a);
|
||||
svg += '<line x1="' + ix.toFixed(1) + '" y1="' + iy.toFixed(1) + '" x2="' + cx + '" y2="' + cy + '" stroke="#dc2626" stroke-width="2.4"/>';
|
||||
/* Стрелка падающего */
|
||||
const ang_i = Math.atan2(cy - iy, cx - ix);
|
||||
const arrPx = cx - 30 * Math.cos(ang_i), arrPy = cy - 30 * Math.sin(ang_i);
|
||||
svg += '<polygon points="' + (arrPx + 7 * Math.cos(ang_i - 0.4)).toFixed(1) + ',' + (arrPy + 7 * Math.sin(ang_i - 0.4)).toFixed(1) + ' ' + arrPx.toFixed(1) + ',' + arrPy.toFixed(1) + ' ' + (arrPx + 7 * Math.cos(ang_i + 0.4)).toFixed(1) + ',' + (arrPy + 7 * Math.sin(ang_i + 0.4)).toFixed(1) + '" fill="#dc2626"/>';
|
||||
/* Дуга угла падения */
|
||||
svg += '<path d="M ' + (cx - 30 * Math.sin(a/2)) + ' ' + (cy - 30 * Math.cos(a/2)) + ' A 30 30 0 0 1 ' + cx + ' ' + (cy - 30) + '" stroke="#dc2626" stroke-width="1.2" fill="none"/>';
|
||||
svg += '<text x="' + (cx - 16 * Math.sin(a/2) - 12).toFixed(1) + '" y="' + (cy - 16 * Math.cos(a/2)).toFixed(1) + '" font-family="JetBrains Mono,monospace" font-size="11" fill="#dc2626" font-weight="700">α</text>';
|
||||
/* Отражённый луч */
|
||||
const rx = cx + L * Math.sin(a), ry = cy - L * Math.cos(a);
|
||||
svg += '<line x1="' + cx + '" y1="' + cy + '" x2="' + rx.toFixed(1) + '" y2="' + ry.toFixed(1) + '" stroke="#7c2d12" stroke-width="1.6" stroke-dasharray="4 2"/>';
|
||||
svg += '<text x="' + (rx + 4).toFixed(1) + '" y="' + (ry - 4).toFixed(1) + '" font-family="JetBrains Mono,monospace" font-size="10" fill="#7c2d12">отраж.</text>';
|
||||
/* Преломлённый луч или полное отражение */
|
||||
if (totalInternal){
|
||||
svg += '<text x="' + (cx + 10) + '" y="' + (cy + 30) + '" font-family="Outfit,sans-serif" font-size="13" fill="#dc2626" font-weight="800">полное внутреннее отражение</text>';
|
||||
} else {
|
||||
const tx = cx + L * Math.sin(b), ty = cy + L * Math.cos(b);
|
||||
svg += '<line x1="' + cx + '" y1="' + cy + '" x2="' + tx.toFixed(1) + '" y2="' + ty.toFixed(1) + '" stroke="#16a34a" stroke-width="2.4"/>';
|
||||
const ang_t = Math.atan2(ty - cy, tx - cx);
|
||||
const apx = cx + 30 * Math.cos(ang_t), apy = cy + 30 * Math.sin(ang_t);
|
||||
svg += '<polygon points="' + (apx - 7 * Math.cos(ang_t - 0.4)).toFixed(1) + ',' + (apy - 7 * Math.sin(ang_t - 0.4)).toFixed(1) + ' ' + apx.toFixed(1) + ',' + apy.toFixed(1) + ' ' + (apx - 7 * Math.cos(ang_t + 0.4)).toFixed(1) + ',' + (apy - 7 * Math.sin(ang_t + 0.4)).toFixed(1) + '" fill="#16a34a"/>';
|
||||
svg += '<path d="M ' + cx + ' ' + (cy + 30) + ' A 30 30 0 0 0 ' + (cx + 30 * Math.sin(b/2)) + ' ' + (cy + 30 * Math.cos(b/2)) + '" stroke="#16a34a" stroke-width="1.2" fill="none"/>';
|
||||
svg += '<text x="' + (cx + 18 * Math.sin(b/2) + 4).toFixed(1) + '" y="' + (cy + 18 * Math.cos(b/2) + 4).toFixed(1) + '" font-family="JetBrains Mono,monospace" font-size="11" fill="#16a34a" font-weight="700">β</text>';
|
||||
}
|
||||
/* Подписи сред */
|
||||
svg += '<text x="20" y="22" font-family="JetBrains Mono,monospace" font-size="11" fill="#0369a1" font-weight="700">n₁ = ' + this.n1.toFixed(2) + '</text>';
|
||||
svg += '<text x="20" y="' + (H - 12) + '" font-family="JetBrains Mono,monospace" font-size="11" fill="#a16207" font-weight="700">n₂ = ' + this.n2.toFixed(2) + '</text>';
|
||||
/* Формула + углы */
|
||||
const bDeg = totalInternal ? '—' : (b * 180 / Math.PI).toFixed(1);
|
||||
svg += '<text x="' + (W - 12) + '" y="22" text-anchor="end" font-family="JetBrains Mono,monospace" font-size="11" fill="#475569" font-weight="700">n₁ sin α = n₂ sin β</text>';
|
||||
svg += '<text x="' + (W - 12) + '" y="' + (H - 12) + '" text-anchor="end" font-family="JetBrains Mono,monospace" font-size="10" fill="#475569">α=' + this.alpha.toFixed(0) + '° · β=' + bDeg + '°' + (critDeg!==null?' · αкр=' + critDeg.toFixed(1) + '°':'') + '</text>';
|
||||
svg += '</svg>';
|
||||
this.el.innerHTML = svg;
|
||||
}
|
||||
}
|
||||
P.RefractionLab = RefractionLab;
|
||||
|
||||
/* ============================================================ */
|
||||
/* PrismSpectrum — призма, дисперсия белого света */
|
||||
/* ============================================================ */
|
||||
|
||||
class PrismSpectrum {
|
||||
constructor(container, opts){
|
||||
opts = opts || {};
|
||||
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
|
||||
this.W = opts.width || 580;
|
||||
this.H = opts.height || 260;
|
||||
this.alpha = opts.alpha !== undefined ? opts.alpha : 50; /* угол падения, градусы */
|
||||
this.paused = true;
|
||||
this.render();
|
||||
}
|
||||
setAlpha(v){ this.alpha = Math.max(20, Math.min(75, v)); this.render(); }
|
||||
update(){}
|
||||
/* Показатели преломления стекла для разных длин волн (упрощённая модель Коши) */
|
||||
nForColor(lamNm){
|
||||
return 1.5 + 6500 / (lamNm * lamNm); /* красный ~1.518, фиолет ~1.530 */
|
||||
}
|
||||
render(){
|
||||
if (!this.el) return;
|
||||
const W = this.W, H = this.H;
|
||||
/* Геометрия равностороннего треугольника-призмы */
|
||||
const cx = W / 2, cy = H / 2 + 10;
|
||||
const side = 140;
|
||||
const h = side * Math.sqrt(3) / 2;
|
||||
const A = { x: cx, y: cy - h * 2/3 }; /* верхняя вершина */
|
||||
const B = { x: cx - side/2, y: cy + h/3 }; /* нижняя левая */
|
||||
const C = { x: cx + side/2, y: cy + h/3 }; /* нижняя правая */
|
||||
let svg = util.svgFrame(W, H, {bg:'#0f172a'});
|
||||
/* Призма (полупрозрачная) */
|
||||
svg += '<polygon points="' + A.x + ',' + A.y.toFixed(1) + ' ' + B.x + ',' + B.y.toFixed(1) + ' ' + C.x + ',' + C.y.toFixed(1) + '" fill="rgba(255,255,255,0.08)" stroke="#cbd5e1" stroke-width="1.6"/>';
|
||||
/* Падающий белый луч на грань AB */
|
||||
const a = this.alpha * Math.PI / 180;
|
||||
/* Точка входа — середина грани AB */
|
||||
const Pin = { x: (A.x + B.x) / 2, y: (A.y + B.y) / 2 };
|
||||
/* Нормаль к AB (наружу, влево-вверх) */
|
||||
const ABx = B.x - A.x, ABy = B.y - A.y;
|
||||
const Lab = Math.hypot(ABx, ABy);
|
||||
const nABx = -ABy / Lab, nABy = ABx / Lab; /* перпендикуляр */
|
||||
/* Источник падающего луча */
|
||||
const inLen = 160;
|
||||
const inx = Pin.x + inLen * (nABx * Math.cos(a) - (ABx/Lab) * Math.sin(a));
|
||||
const iny = Pin.y + inLen * (nABy * Math.cos(a) - (ABy/Lab) * Math.sin(a));
|
||||
svg += '<line x1="' + inx.toFixed(1) + '" y1="' + iny.toFixed(1) + '" x2="' + Pin.x.toFixed(1) + '" y2="' + Pin.y.toFixed(1) + '" stroke="#fff" stroke-width="3"/>';
|
||||
svg += '<text x="' + (inx - 8).toFixed(1) + '" y="' + (iny - 6).toFixed(1) + '" text-anchor="end" font-family="JetBrains Mono,monospace" font-size="11" fill="#fff">белый</text>';
|
||||
/* 7 цветов спектра — каждый со своим n и углом преломления */
|
||||
const colors = [
|
||||
{ lam: 700, c: '#dc2626' }, /* красный */
|
||||
{ lam: 620, c: '#ea580c' }, /* оранжевый */
|
||||
{ lam: 580, c: '#facc15' }, /* жёлтый */
|
||||
{ lam: 520, c: '#16a34a' }, /* зелёный */
|
||||
{ lam: 480, c: '#06b6d4' }, /* голубой */
|
||||
{ lam: 450, c: '#1d4ed8' }, /* синий */
|
||||
{ lam: 420, c: '#7c3aed' } /* фиолетовый */
|
||||
];
|
||||
/* Нормаль к BC (наружу, вправо-вниз) */
|
||||
const BCx = C.x - B.x, BCy = C.y - B.y;
|
||||
const Lbc = Math.hypot(BCx, BCy);
|
||||
const nBCx = BCy / Lbc, nBCy = -BCx / Lbc;
|
||||
/* Точки выхода — равномерно по грани BC */
|
||||
const outLen = 180;
|
||||
for (let i = 0; i < colors.length; i++){
|
||||
const co = colors[i];
|
||||
const n = this.nForColor(co.lam);
|
||||
/* Преломление при входе: sin β = sin α / n */
|
||||
const sinB = Math.sin(a) / n;
|
||||
if (Math.abs(sinB) > 1) continue;
|
||||
const beta = Math.asin(sinB);
|
||||
/* Внутри призмы луч идёт от Pin под углом beta от нормали к AB, в сторону BC */
|
||||
/* Направление внутри: поворот нормали-в-стекло (-nAB) на beta */
|
||||
const dirX = -nABx * Math.cos(beta) + (ABx/Lab) * Math.sin(beta);
|
||||
const dirY = -nABy * Math.cos(beta) + (ABy/Lab) * Math.sin(beta);
|
||||
/* Найти точку выхода на BC (параметрический луч / линия BC) */
|
||||
const denom = dirX * (-(C.y - B.y)) + dirY * (C.x - B.x);
|
||||
if (Math.abs(denom) < 1e-6) continue;
|
||||
const t = ((B.x - Pin.x) * (-(C.y - B.y)) + (B.y - Pin.y) * (C.x - B.x)) / denom;
|
||||
const Pout = { x: Pin.x + t * dirX, y: Pin.y + t * dirY };
|
||||
/* Луч внутри */
|
||||
svg += '<line x1="' + Pin.x.toFixed(1) + '" y1="' + Pin.y.toFixed(1) + '" x2="' + Pout.x.toFixed(1) + '" y2="' + Pout.y.toFixed(1) + '" stroke="' + co.c + '" stroke-width="1.6" opacity="0.85"/>';
|
||||
/* Преломление при выходе: угол к нормали BC внутри */
|
||||
const cosIn = -(dirX * nBCx + dirY * nBCy); /* направление к внешней нормали */
|
||||
const sinIn = Math.sqrt(Math.max(0, 1 - cosIn * cosIn));
|
||||
const sinOut = sinIn * n;
|
||||
if (sinOut > 1) continue;
|
||||
const cosOut = Math.sqrt(1 - sinOut * sinOut);
|
||||
/* Тангенциальная составляющая (вдоль BC) */
|
||||
const tBCx = BCx / Lbc, tBCy = BCy / Lbc;
|
||||
const tanSign = (dirX * tBCx + dirY * tBCy) >= 0 ? 1 : -1;
|
||||
const outX = nBCx * cosOut + tBCx * sinOut * tanSign;
|
||||
const outY = nBCy * cosOut + tBCy * sinOut * tanSign;
|
||||
const Pend = { x: Pout.x + outLen * outX, y: Pout.y + outLen * outY };
|
||||
svg += '<line x1="' + Pout.x.toFixed(1) + '" y1="' + Pout.y.toFixed(1) + '" x2="' + Pend.x.toFixed(1) + '" y2="' + Pend.y.toFixed(1) + '" stroke="' + co.c + '" stroke-width="2.4"/>';
|
||||
}
|
||||
/* Подпись */
|
||||
svg += '<text x="' + (W/2) + '" y="20" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" fill="#cbd5e1" font-weight="700">дисперсия: n(λ) растёт при уменьшении λ → фиолетовый отклоняется сильнее красного</text>';
|
||||
svg += '<text x="' + (W/2) + '" y="' + (H - 8) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="10" fill="#94a3b8">α = ' + this.alpha.toFixed(0) + '° · стекло (модель Коши)</text>';
|
||||
svg += '</svg>';
|
||||
this.el.innerHTML = svg;
|
||||
}
|
||||
}
|
||||
P.PrismSpectrum = PrismSpectrum;
|
||||
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user