feat(phys11 ch3): Wave 7 — §21-§23 + Финал главы 3 (ThinLens + TwoLensSystem)
- phys-fx.js: PHYS.ThinLens (собирающая/рассеивающая, 2 канон. луча, формула, мнимое=пунктир), PHYS.TwoLensSystem (телескоп Кеплера + микроскоп) - §21: тонкая линза, формула 1/d + 1/f = 1/F, оптическая сила D = 1/F (дптр), 3 характерных луча - §22: фотоаппарат (d > 2F) и проектор (F < d < 2F) на одном интерактиве - §23: лупа Γ = 25/F, микроскоп Γ ≈ Γ_об·25/F_ок, телескоп Кеплера Γ = F_об/F_ок - 6 квизов (I8-I10 CALC/TH) + 3 босса (b8-b10) - Финал главы 3: 5 интегрированных боссов (fb1-fb5), +200 XP бонус, ачивка ch3_master - checkFinalDone() — авто-проверка победы над всеми 5 боссами - Глава 3 полностью завершена (10 параграфов + финал)
This commit is contained in:
@@ -1329,4 +1329,215 @@ class PrismSpectrum {
|
||||
}
|
||||
P.PrismSpectrum = PrismSpectrum;
|
||||
|
||||
/* ============================================================ */
|
||||
/* ThinLens — тонкая линза (собирающая / рассеивающая) */
|
||||
/* ============================================================ */
|
||||
|
||||
class ThinLens {
|
||||
constructor(container, opts){
|
||||
opts = opts || {};
|
||||
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
|
||||
this.W = opts.width || 640;
|
||||
this.H = opts.height || 300;
|
||||
this.F = opts.F !== undefined ? opts.F : 70; /* фокусное (px) */
|
||||
this.d = opts.d !== undefined ? opts.d : 160; /* расст. до объекта (px) */
|
||||
this.objH = opts.objH !== undefined ? opts.objH : 50;
|
||||
this.mode = opts.mode || 'converging'; /* 'converging' | 'diverging' */
|
||||
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 + 10;
|
||||
const lensX = W / 2;
|
||||
const Fsign = (this.mode === 'converging') ? this.F : -this.F;
|
||||
const d = this.d;
|
||||
/* 1/d + 1/f = 1/F → f = 1/(1/F - 1/d) */
|
||||
let f;
|
||||
if (Math.abs(1/Fsign - 1/d) < 1e-6) f = 1e9;
|
||||
else f = 1 / (1/Fsign - 1/d);
|
||||
/* По соглашению: f > 0 справа (действительное), f < 0 слева (мнимое) */
|
||||
const imgX = lensX + f;
|
||||
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"/>';
|
||||
/* Линза */
|
||||
if (this.mode === 'converging'){
|
||||
svg += '<line x1="' + lensX + '" y1="40" x2="' + lensX + '" y2="' + (H - 40) + '" stroke="#0f172a" stroke-width="3"/>';
|
||||
svg += '<polygon points="' + lensX + ',32 ' + (lensX - 7) + ',46 ' + (lensX + 7) + ',46" fill="#0f172a"/>';
|
||||
svg += '<polygon points="' + lensX + ',' + (H - 32) + ' ' + (lensX - 7) + ',' + (H - 46) + ' ' + (lensX + 7) + ',' + (H - 46) + '" fill="#0f172a"/>';
|
||||
} else {
|
||||
svg += '<line x1="' + lensX + '" y1="40" x2="' + lensX + '" y2="' + (H - 40) + '" stroke="#0f172a" stroke-width="3" stroke-dasharray="2 2"/>';
|
||||
svg += '<polygon points="' + (lensX - 8) + ',32 ' + lensX + ',46 ' + (lensX + 8) + ',32" fill="#0f172a"/>';
|
||||
svg += '<polygon points="' + (lensX - 8) + ',' + (H - 32) + ' ' + lensX + ',' + (H - 46) + ' ' + (lensX + 8) + ',' + (H - 32) + '" fill="#0f172a"/>';
|
||||
}
|
||||
/* Фокусы F и 2F */
|
||||
const Fleft = lensX - this.F, Fright = lensX + this.F;
|
||||
svg += '<circle cx="' + Fleft + '" cy="' + cy + '" r="3.5" fill="#dc2626"/><text x="' + Fleft + '" y="' + (cy + 18) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" fill="#dc2626" font-weight="700">F</text>';
|
||||
svg += '<circle cx="' + Fright + '" cy="' + cy + '" r="3.5" fill="#dc2626"/><text x="' + Fright + '" y="' + (cy + 18) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" fill="#dc2626" font-weight="700">F</text>';
|
||||
if (this.mode === 'converging'){
|
||||
const F2L = lensX - 2*this.F, F2R = lensX + 2*this.F;
|
||||
if (F2L > 20) svg += '<circle cx="' + F2L + '" cy="' + cy + '" r="3" fill="#1d4ed8"/><text x="' + F2L + '" y="' + (cy + 18) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="10" fill="#1d4ed8">2F</text>';
|
||||
if (F2R < W - 10) svg += '<circle cx="' + F2R + '" cy="' + cy + '" r="3" fill="#1d4ed8"/><text x="' + F2R + '" y="' + (cy + 18) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="10" fill="#1d4ed8">2F</text>';
|
||||
}
|
||||
/* Объект */
|
||||
const objX = lensX - d;
|
||||
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 + '"/>';
|
||||
/* Лучи */
|
||||
/* Луч 1: параллельный → через F справа (для собирающей); для рассеивающей — как-будто из F слева */
|
||||
svg += '<line x1="' + objX + '" y1="' + objTopY + '" x2="' + lensX + '" y2="' + objTopY + '" stroke="#16a34a" stroke-width="1.6"/>';
|
||||
if (this.mode === 'converging'){
|
||||
const slope = (cy - objTopY) / (Fright - lensX);
|
||||
const x2 = Math.min(W - 10, imgX > lensX ? imgX : W - 10);
|
||||
const y2 = objTopY + slope * (x2 - lensX);
|
||||
svg += '<line x1="' + lensX + '" y1="' + objTopY + '" x2="' + x2.toFixed(1) + '" y2="' + y2.toFixed(1) + '" stroke="#16a34a" stroke-width="1.6"/>';
|
||||
} else {
|
||||
/* Рассеивающая: продолжение в сторону F слева */
|
||||
const slope = (objTopY - cy) / (lensX - Fleft);
|
||||
svg += '<line x1="' + lensX + '" y1="' + objTopY + '" x2="' + (W - 10) + '" y2="' + (objTopY + slope * (W - 10 - lensX)).toFixed(1) + '" stroke="#16a34a" stroke-width="1.6"/>';
|
||||
svg += '<line x1="' + lensX + '" y1="' + objTopY + '" x2="' + Fleft + '" y2="' + cy + '" stroke="#16a34a" stroke-width="1" stroke-dasharray="3 3"/>';
|
||||
}
|
||||
/* Луч 2: через оптический центр O (без преломления) */
|
||||
const slope2 = (cy - objTopY) / (lensX - objX);
|
||||
const ex = Math.min(W - 10, imgX > lensX ? imgX : W - 10);
|
||||
const ey = objTopY + slope2 * (ex - objX);
|
||||
svg += '<line x1="' + objX + '" y1="' + objTopY + '" x2="' + ex.toFixed(1) + '" y2="' + ey.toFixed(1) + '" stroke="#1d4ed8" stroke-width="1.6"/>';
|
||||
/* Изображение */
|
||||
if (isFinite(imgH) && Math.abs(imgX) < 1e7){
|
||||
const imgTopY = cy - imgH;
|
||||
const isVirtual = (this.mode === 'diverging') || (f < 0);
|
||||
const sd = isVirtual ? ' stroke-dasharray="4 3"' : '';
|
||||
const op = isVirtual ? 0.7 : 1.0;
|
||||
const fillCol = '#7c2d12';
|
||||
if (imgX > 20 && imgX < W - 10){
|
||||
svg += '<line x1="' + imgX.toFixed(1) + '" y1="' + cy + '" x2="' + imgX.toFixed(1) + '" y2="' + imgTopY.toFixed(1) + '" stroke="' + fillCol + '" stroke-width="2.6"' + sd + ' opacity="' + op + '"/>';
|
||||
svg += '<polygon points="' + imgX.toFixed(1) + ',' + imgTopY.toFixed(1) + ' ' + (imgX - 5).toFixed(1) + ',' + (imgTopY + (imgH > 0 ? 10 : -10)).toFixed(1) + ' ' + (imgX + 5).toFixed(1) + ',' + (imgTopY + (imgH > 0 ? 10 : -10)).toFixed(1) + '" fill="' + fillCol + '" opacity="' + op + '"/>';
|
||||
}
|
||||
}
|
||||
/* Подписи */
|
||||
const Glabel = isFinite(G) ? G.toFixed(2) : '—';
|
||||
const fLabel = (Math.abs(f) < 1e7) ? f.toFixed(0) : '∞';
|
||||
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 + ' · f = ' + fLabel + 'px</text>';
|
||||
svg += '<text x="' + (W/2) + '" y="' + (H - 8) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="10" fill="#64748b">' + (this.mode==='converging'?'собирающая':'рассеивающая') + ' линза · F=' + this.F + 'px · d=' + d + 'px</text>';
|
||||
svg += '</svg>';
|
||||
this.el.innerHTML = svg;
|
||||
}
|
||||
}
|
||||
P.ThinLens = ThinLens;
|
||||
|
||||
/* ============================================================ */
|
||||
/* TwoLensSystem — две линзы (микроскоп / телескоп) */
|
||||
/* ============================================================ */
|
||||
|
||||
class TwoLensSystem {
|
||||
constructor(container, opts){
|
||||
opts = opts || {};
|
||||
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
|
||||
this.W = opts.width || 700;
|
||||
this.H = opts.height || 280;
|
||||
this.F1 = opts.F1 !== undefined ? opts.F1 : 50; /* объектив */
|
||||
this.F2 = opts.F2 !== undefined ? opts.F2 : 90; /* окуляр */
|
||||
this.L = opts.L !== undefined ? opts.L : 280; /* расстояние между линзами */
|
||||
this.mode = opts.mode || 'telescope'; /* 'telescope' | 'microscope' */
|
||||
this.paused = true;
|
||||
this.render();
|
||||
}
|
||||
setMode(m){ this.mode = m; this.render(); }
|
||||
setF1(v){ this.F1 = Math.max(20, v); this.render(); }
|
||||
setF2(v){ this.F2 = Math.max(20, v); this.render(); }
|
||||
setL(v){ this.L = Math.max(this.F1 + this.F2 + 20, v); this.render(); }
|
||||
update(){}
|
||||
drawLens(svg, x, cy, label, F){
|
||||
let s = svg;
|
||||
s += '<line x1="' + x + '" y1="' + (cy - 80) + '" x2="' + x + '" y2="' + (cy + 80) + '" stroke="#0f172a" stroke-width="2.6"/>';
|
||||
s += '<polygon points="' + x + ',' + (cy - 88) + ' ' + (x - 6) + ',' + (cy - 76) + ' ' + (x + 6) + ',' + (cy - 76) + '" fill="#0f172a"/>';
|
||||
s += '<polygon points="' + x + ',' + (cy + 88) + ' ' + (x - 6) + ',' + (cy + 76) + ' ' + (x + 6) + ',' + (cy + 76) + '" fill="#0f172a"/>';
|
||||
s += '<text x="' + x + '" y="' + (cy + 102) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" fill="#0f172a" font-weight="700">' + label + '</text>';
|
||||
s += '<circle cx="' + (x - F) + '" cy="' + cy + '" r="2.5" fill="#dc2626"/>';
|
||||
s += '<circle cx="' + (x + F) + '" cy="' + cy + '" r="2.5" fill="#dc2626"/>';
|
||||
return s;
|
||||
}
|
||||
render(){
|
||||
if (!this.el) return;
|
||||
const W = this.W, H = this.H;
|
||||
const cy = H / 2 + 10;
|
||||
const x1 = 130; /* объектив */
|
||||
const x2 = x1 + this.L; /* окуляр */
|
||||
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"/>';
|
||||
/* Линзы */
|
||||
svg = this.drawLens(svg, x1, cy, 'объектив (F₁=' + this.F1 + ')', this.F1);
|
||||
svg = this.drawLens(svg, x2, cy, 'окуляр (F₂=' + this.F2 + ')', this.F2);
|
||||
/* Сценарий */
|
||||
if (this.mode === 'telescope'){
|
||||
/* Объект «на бесконечности» — пучок параллельных лучей входит слева */
|
||||
const angle = -0.06; /* небольшой угол */
|
||||
for (let i = -2; i <= 2; i++){
|
||||
const yIn = cy + i * 18;
|
||||
/* Луч входит горизонтально (или под малым углом α₁) */
|
||||
const xIn = 25;
|
||||
svg += '<line x1="' + xIn + '" y1="' + yIn + '" x2="' + x1 + '" y2="' + yIn + '" stroke="#16a34a" stroke-width="1.4"/>';
|
||||
/* После объектива собирается в фокальной плоскости (x1 + F1) */
|
||||
const focusX = x1 + this.F1;
|
||||
const focusY = cy; /* для параллельного пучка вдоль оси — на оси */
|
||||
svg += '<line x1="' + x1 + '" y1="' + yIn + '" x2="' + focusX + '" y2="' + focusY + '" stroke="#16a34a" stroke-width="1.4"/>';
|
||||
/* От фокуса (F2 справа от окуляра у телескопа: совмещён с F1 объектива) дальше параллельно */
|
||||
/* Лучи проходят через окуляр */
|
||||
const yAtEye = focusY + (cy + i * 12 - focusY); /* выходные углы немного шире */
|
||||
svg += '<line x1="' + focusX + '" y1="' + focusY + '" x2="' + x2 + '" y2="' + (cy + i * 8) + '" stroke="#16a34a" stroke-width="1.2" stroke-dasharray="3 3"/>';
|
||||
/* После окуляра — параллельно (изображение в бесконечности) */
|
||||
svg += '<line x1="' + x2 + '" y1="' + (cy + i * 8) + '" x2="' + (W - 20) + '" y2="' + (cy + i * 14) + '" stroke="#16a34a" stroke-width="1.4"/>';
|
||||
}
|
||||
svg += '<text x="60" y="40" font-family="JetBrains Mono,monospace" font-size="11" fill="#1d4ed8" font-weight="700">пучок от удалённого объекта</text>';
|
||||
svg += '<text x="' + (W - 20) + '" y="40" text-anchor="end" font-family="JetBrains Mono,monospace" font-size="11" fill="#1d4ed8" font-weight="700">в глаз наблюдателя</text>';
|
||||
svg += '<text x="' + (W/2) + '" y="' + (H - 10) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" fill="#475569">телескоп Кеплера · Γ = F₁/F₂ = ' + (this.F1/this.F2).toFixed(2) + '</text>';
|
||||
} else {
|
||||
/* Микроскоп: объект чуть дальше F1 от объектива */
|
||||
const d1 = this.F1 + 12;
|
||||
const objX = x1 - d1;
|
||||
const objH = 40;
|
||||
/* 1/d + 1/f = 1/F */
|
||||
const f1 = 1 / (1/this.F1 - 1/d1);
|
||||
const G1 = -f1 / d1;
|
||||
const h1 = objH * G1;
|
||||
const img1X = x1 + f1;
|
||||
/* Изображение объектива должно лежать чуть ближе F2 от окуляра, чтобы окуляр работал как лупа */
|
||||
/* Объект */
|
||||
svg += '<line x1="' + objX + '" y1="' + cy + '" x2="' + objX + '" y2="' + (cy - objH) + '" stroke="#d97706" stroke-width="3"/>';
|
||||
svg += '<polygon points="' + objX + ',' + (cy - objH) + ' ' + (objX - 5) + ',' + (cy - objH + 9) + ' ' + (objX + 5) + ',' + (cy - objH + 9) + '" fill="#d97706"/>';
|
||||
/* Промежуточное изображение */
|
||||
if (img1X > x1 && img1X < x2){
|
||||
svg += '<line x1="' + img1X.toFixed(1) + '" y1="' + cy + '" x2="' + img1X.toFixed(1) + '" y2="' + (cy - h1).toFixed(1) + '" stroke="#7c2d12" stroke-width="2.4"/>';
|
||||
svg += '<polygon points="' + img1X.toFixed(1) + ',' + (cy - h1).toFixed(1) + ' ' + (img1X - 4).toFixed(1) + ',' + (cy - h1 + (h1>0?9:-9)).toFixed(1) + ' ' + (img1X + 4).toFixed(1) + ',' + (cy - h1 + (h1>0?9:-9)).toFixed(1) + '" fill="#7c2d12"/>';
|
||||
svg += '<text x="' + img1X.toFixed(1) + '" y="' + (cy + 14) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="9" fill="#7c2d12">A₁B₁</text>';
|
||||
}
|
||||
/* Лучи от верха объекта через объектив (2 канонических) */
|
||||
svg += '<line x1="' + objX + '" y1="' + (cy - objH) + '" x2="' + x1 + '" y2="' + (cy - objH) + '" stroke="#16a34a" stroke-width="1.4"/>';
|
||||
svg += '<line x1="' + x1 + '" y1="' + (cy - objH) + '" x2="' + img1X.toFixed(1) + '" y2="' + (cy - h1).toFixed(1) + '" stroke="#16a34a" stroke-width="1.4"/>';
|
||||
/* Через оптический центр объектива */
|
||||
svg += '<line x1="' + objX + '" y1="' + (cy - objH) + '" x2="' + img1X.toFixed(1) + '" y2="' + (cy - h1).toFixed(1) + '" stroke="#1d4ed8" stroke-width="1.4"/>';
|
||||
/* Окуляр работает как лупа — даёт мнимое увеличенное (за окуляром слева) */
|
||||
/* Показ лучей от верха промежуточного изображения через окуляр параллельно (в глаз) */
|
||||
svg += '<line x1="' + img1X.toFixed(1) + '" y1="' + (cy - h1).toFixed(1) + '" x2="' + x2 + '" y2="' + (cy - h1).toFixed(1) + '" stroke="#16a34a" stroke-width="1.4"/>';
|
||||
svg += '<line x1="' + x2 + '" y1="' + (cy - h1).toFixed(1) + '" x2="' + (W - 20) + '" y2="' + (cy - h1 * 1.6).toFixed(1) + '" stroke="#16a34a" stroke-width="1.4"/>';
|
||||
svg += '<text x="' + (W - 20) + '" y="40" text-anchor="end" font-family="JetBrains Mono,monospace" font-size="11" fill="#1d4ed8" font-weight="700">в глаз</text>';
|
||||
const Gtotal = G1 * (25 / this.F2); /* приближённо: |Γ_мкс| ≈ |Γ_объ| · 25 см/F_ок */
|
||||
svg += '<text x="' + (W/2) + '" y="' + (H - 10) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" fill="#475569">микроскоп · Γ ≈ Γ_объ · 25 см / F_ок ≈ ' + Gtotal.toFixed(1) + '</text>';
|
||||
}
|
||||
this.el.innerHTML = svg;
|
||||
}
|
||||
}
|
||||
P.TwoLensSystem = TwoLensSystem;
|
||||
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user