feat(phys11 ch3): Wave 5 — Глава 3 «Оптика» §14-§17 + phys-fx TwoSlit/DiffractionGrating/FlatMirror

- phys-fx.js: PHYS.TwoSlit (опыт Юнга), PHYS.DiffractionGrating (с радужным спектром), PHYS.FlatMirror
- ch3 §14: Электромагнитная природа света + скорость света
- ch3 §15: Интерференция (опыт Юнга)
- ch3 §16: Дифракция света + дифракционная решётка
- ch3 §17: Отражение света + плоское зеркало
- 8 квизов (I1_CALC/NAT, I2_CALC/TH, I3_CALC/TH, I4_CALC/IMG)
- 4 босса (b1-b4) для §14-§17
- §18-§23 + Final — заглушки для W6/W7
This commit is contained in:
Maxim Dolgolyov
2026-05-29 18:35:08 +03:00
parent bed085ac98
commit e4801dcc2f
2 changed files with 1139 additions and 190 deletions
+201
View File
@@ -832,4 +832,205 @@ class Transformer {
}
P.Transformer = Transformer;
/* ============================================================ */
/* TwoSlit — интерференция от двух щелей (опыт Юнга) */
/* ============================================================ */
class TwoSlit {
constructor(container, opts){
opts = opts || {};
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
this.W = opts.width || 540;
this.H = opts.height || 200;
this.d = opts.d !== undefined ? opts.d : 0.5; /* расстояние между щелями (отн. ед.) */
this.L = opts.L !== undefined ? opts.L : 5; /* расстояние до экрана */
this.lambda = opts.lambda !== undefined ? opts.lambda : 0.05; /* длина волны */
this.color = opts.color || '#f59e0b';
this.paused = true; /* статика */
this.render();
}
setD(v){ this.d = Math.max(0.05, v); this.render(); }
setLambda(v){ this.lambda = Math.max(0.005, v); this.render(); }
update(){}
render(){
if (!this.el) return;
const W = this.W, H = this.H;
/* Слева: щели (две точки), справа: экран с полосами */
let svg = util.svgFrame(W, H, {bg:'#f8fafc'});
/* Лазер слева */
svg += '<rect x="6" y="' + (H/2 - 14) + '" width="40" height="28" fill="#fca5a5" stroke="#0f172a" stroke-width="1.4" rx="3"/>';
svg += '<text x="26" y="' + (H/2 + 4) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="10" font-weight="700">laser</text>';
/* Щели */
const slitX = 130;
const dPx = Math.min(60, this.d * 60);
const s1y = H/2 - dPx/2, s2y = H/2 + dPx/2;
svg += '<line x1="' + slitX + '" y1="20" x2="' + slitX + '" y2="' + (s1y - 4) + '" stroke="#0f172a" stroke-width="3"/>';
svg += '<line x1="' + slitX + '" y1="' + (s1y + 4) + '" x2="' + slitX + '" y2="' + (s2y - 4) + '" stroke="#0f172a" stroke-width="3"/>';
svg += '<line x1="' + slitX + '" y1="' + (s2y + 4) + '" x2="' + slitX + '" y2="' + (H - 20) + '" stroke="#0f172a" stroke-width="3"/>';
/* Лучи от лазера к щелям */
svg += '<line x1="46" y1="' + (H/2) + '" x2="' + slitX + '" y2="' + s1y + '" stroke="#fca5a5" stroke-width="1.4" opacity="0.6"/>';
svg += '<line x1="46" y1="' + (H/2) + '" x2="' + slitX + '" y2="' + s2y + '" stroke="#fca5a5" stroke-width="1.4" opacity="0.6"/>';
/* Экран */
const screenX = W - 50;
svg += '<line x1="' + screenX + '" y1="20" x2="' + screenX + '" y2="' + (H - 20) + '" stroke="#0f172a" stroke-width="3"/>';
/* Интерференционная картина: интенсивность ~ cos²(πdy/(λL)) */
const lp = this.lambda * this.L / this.d; /* шаг полос */
const lpPx = Math.max(3, Math.min(80, lp * 200));
const halfH = (H - 40) / 2;
const N = 240;
for (let i = 0; i < N; i++){
const y = 20 + (H - 40) * i / N;
const ry = y - H/2;
const intens = Math.pow(Math.cos(Math.PI * ry / lpPx), 2);
const op = intens.toFixed(3);
svg += '<rect x="' + screenX + '" y="' + y.toFixed(1) + '" width="22" height="' + ((H - 40) / N + 1).toFixed(1) + '" fill="' + this.color + '" opacity="' + op + '"/>';
}
/* Подпись формулы */
svg += '<text x="' + (W/2) + '" y="' + (H - 4) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" fill="#475569">d·sin φ = k·λ (max), d=' + this.d.toFixed(2) + ', λ=' + this.lambda.toFixed(3) + '</text>';
svg += '</svg>';
this.el.innerHTML = svg;
}
}
P.TwoSlit = TwoSlit;
/* ============================================================ */
/* DiffractionGrating — дифракционная решётка + спектр */
/* ============================================================ */
class DiffractionGrating {
constructor(container, opts){
opts = opts || {};
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
this.W = opts.width || 540;
this.H = opts.height || 220;
this.d = opts.d !== undefined ? opts.d : 2e-6; /* период решётки, м */
this.lambda = opts.lambda !== undefined ? opts.lambda : 550e-9; /* λ, м */
this.color = opts.color || '#22c55e';
this.paused = true;
this.render();
}
setD(v){ this.d = Math.max(0.3e-6, v); this.render(); }
setLambda(v){ this.lambda = Math.max(380e-9, Math.min(760e-9, v)); this.render(); }
update(){}
/* Возвращает цвет HEX для данной длины волны (для видимого света) */
wavelengthToColor(lamNm){
const ranges = [
[380, 440, '#7c3aed'], [440, 490, '#3b82f6'], [490, 520, '#06b6d4'],
[520, 570, '#22c55e'], [570, 590, '#facc15'], [590, 630, '#f97316'],
[630, 760, '#dc2626']
];
for (const [lo, hi, c] of ranges) if (lamNm >= lo && lamNm < hi) return c;
return '#94a3b8';
}
render(){
if (!this.el) return;
const W = this.W, H = this.H;
let svg = util.svgFrame(W, H, {bg:'#0f172a'}); /* тёмный фон для спектра */
/* Решётка слева */
const gx = 50;
svg += '<rect x="' + (gx - 6) + '" y="40" width="12" height="' + (H - 80) + '" fill="#475569"/>';
/* «штрихи» решётки */
for (let i = 0; i < 10; i++){
const y = 40 + i * (H - 80) / 10;
svg += '<line x1="' + gx + '" y1="' + y + '" x2="' + (gx + 6) + '" y2="' + y + '" stroke="#fff" stroke-width="0.8"/>';
}
svg += '<text x="' + gx + '" y="30" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" fill="#fff">решётка</text>';
/* Падающий луч */
svg += '<line x1="10" y1="' + (H/2) + '" x2="' + (gx - 6) + '" y2="' + (H/2) + '" stroke="' + this.wavelengthToColor(this.lambda * 1e9) + '" stroke-width="2.2"/>';
/* Линии порядков k = -3..3 */
const cx = gx + 6, cy = H/2;
const lamNm = this.lambda * 1e9;
const color = this.wavelengthToColor(lamNm);
for (let k = -3; k <= 3; k++){
const sinPhi = k * this.lambda / this.d;
if (Math.abs(sinPhi) > 1) continue;
const phi = Math.asin(sinPhi);
const dx = 400, dy = dx * Math.tan(phi);
const x2 = cx + dx, y2 = cy - dy;
const op = k === 0 ? 1.0 : (1 - Math.abs(k) * 0.18);
svg += '<line x1="' + cx + '" y1="' + cy + '" x2="' + x2 + '" y2="' + y2.toFixed(1) + '" stroke="' + color + '" stroke-width="' + (2.4 - Math.abs(k) * 0.3) + '" opacity="' + op.toFixed(2) + '"/>';
svg += '<text x="' + (x2 - 24) + '" y="' + (y2 - 6).toFixed(1) + '" font-family="JetBrains Mono,monospace" font-size="10" fill="' + color + '" font-weight="700">k=' + k + '</text>';
}
/* Подпись формулы и параметров */
svg += '<text x="' + (W/2) + '" y="' + (H - 8) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" fill="#cbd5e1">d sin φ = kλ · d=' + (this.d * 1e6).toFixed(2) + ' мкм · λ=' + (this.lambda * 1e9).toFixed(0) + ' нм</text>';
svg += '</svg>';
this.el.innerHTML = svg;
}
}
P.DiffractionGrating = DiffractionGrating;
/* ============================================================ */
/* FlatMirror — плоское зеркало, объект и мнимое изображение */
/* ============================================================ */
class FlatMirror {
constructor(container, opts){
opts = opts || {};
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
this.W = opts.width || 540;
this.H = opts.height || 240;
this.objX = opts.objX !== undefined ? opts.objX : 100; /* px от зеркала */
this.objY = opts.objY !== undefined ? opts.objY : 50;
this.objH = opts.objH !== undefined ? opts.objH : 50;
this.color = opts.color || '#f59e0b';
this.paused = true;
this.render();
}
setObjX(v){ this.objX = Math.max(20, v); this.render(); }
setObjY(v){ this.objY = v; this.render(); }
update(){}
render(){
if (!this.el) return;
const W = this.W, H = this.H;
const cy = H / 2 + 30;
const mirrorX = W / 2;
let svg = util.svgFrame(W, H, {bg:'#f8fafc'});
/* Зеркало с штриховкой */
svg += '<line x1="' + mirrorX + '" y1="30" x2="' + mirrorX + '" y2="' + (H - 30) + '" stroke="#0f172a" stroke-width="3"/>';
for (let i = 0; i < 12; i++){
const y = 30 + i * (H - 60) / 12;
svg += '<line x1="' + (mirrorX + 1) + '" y1="' + y + '" x2="' + (mirrorX + 10) + '" y2="' + (y - 8) + '" stroke="#0f172a" stroke-width="1.2"/>';
}
svg += '<text x="' + (mirrorX + 14) + '" y="' + (H - 16) + '" font-family="JetBrains Mono,monospace" font-size="11" fill="#0f172a">зеркало</text>';
/* Объект — стрелка */
const objX = mirrorX - this.objX;
const objBaseY = cy;
const objTopY = cy - this.objH;
svg += '<line x1="' + objX + '" y1="' + objBaseY + '" 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 + '"/>';
svg += '<text x="' + (objX - 5) + '" y="' + (objBaseY + 16) + '" text-anchor="end" font-family="JetBrains Mono,monospace" font-size="11" fill="' + this.color + '" font-weight="700">объект</text>';
/* Мнимое изображение справа от зеркала, симметрично */
const imgX = mirrorX + this.objX;
svg += '<line x1="' + imgX + '" y1="' + objBaseY + '" x2="' + imgX + '" y2="' + objTopY + '" stroke="' + this.color + '" stroke-width="2" stroke-dasharray="4 3" opacity="0.65"/>';
svg += '<polygon points="' + imgX + ',' + objTopY + ' ' + (imgX - 6) + ',' + (objTopY + 10) + ' ' + (imgX + 6) + ',' + (objTopY + 10) + '" fill="' + this.color + '" opacity="0.5"/>';
svg += '<text x="' + (imgX + 5) + '" y="' + (objBaseY + 16) + '" font-family="JetBrains Mono,monospace" font-size="11" fill="' + this.color + '" opacity="0.7">изображение</text>';
/* Лучи: 1) от верха объекта горизонтально на зеркало, отражается под тем же углом
2) от верха объекта в зеркало по диагонали, отражается симметрично */
/* Луч 1 */
svg += '<line x1="' + objX + '" y1="' + objTopY + '" x2="' + mirrorX + '" y2="' + objTopY + '" stroke="#dc2626" stroke-width="1.6"/>';
svg += '<line x1="' + mirrorX + '" y1="' + objTopY + '" x2="' + objX + '" y2="' + objTopY + '" stroke="#dc2626" stroke-width="1.6"/>';
/* Продолжение в зазеркалье (пунктир) */
svg += '<line x1="' + mirrorX + '" y1="' + objTopY + '" x2="' + imgX + '" y2="' + objTopY + '" stroke="#dc2626" stroke-width="1" stroke-dasharray="3 3"/>';
/* Луч 2: от верха к точке наблюдения слева внизу */
const eyeX = 40, eyeY = H - 50;
const hitY = objTopY + (cy - objTopY) * (mirrorX - objX) / (eyeX - objX + 2 * (mirrorX - objX));
/* Упрощённо: луч от верха объекта к зеркалу и затем к глазу */
const hitX = mirrorX;
const hitYsimple = objTopY + (cy + 30 - objTopY) * 0.3;
svg += '<line x1="' + objX + '" y1="' + objTopY + '" x2="' + hitX + '" y2="' + hitYsimple.toFixed(1) + '" stroke="#16a34a" stroke-width="1.6"/>';
svg += '<line x1="' + hitX + '" y1="' + hitYsimple.toFixed(1) + '" x2="' + eyeX + '" y2="' + eyeY + '" stroke="#16a34a" stroke-width="1.6"/>';
/* Продолжение от точки отражения в зазеркалье */
svg += '<line x1="' + hitX + '" y1="' + hitYsimple.toFixed(1) + '" x2="' + imgX + '" y2="' + objTopY + '" stroke="#16a34a" stroke-width="1" stroke-dasharray="3 3"/>';
/* Глаз */
svg += '<circle cx="' + eyeX + '" cy="' + eyeY + '" r="10" fill="#fff" stroke="#0f172a" stroke-width="1.4"/>';
svg += '<circle cx="' + eyeX + '" cy="' + eyeY + '" r="4" fill="#0f172a"/>';
svg += '<text x="' + eyeX + '" y="' + (eyeY + 20) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="10" fill="#475569">наблюдатель</text>';
/* Подпись закона */
svg += '<text x="' + (W/2) + '" y="22" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" fill="#475569" font-weight="700">∠ пад = ∠ отр · изображение мнимое, прямое, равное</text>';
svg += '</svg>';
this.el.innerHTML = svg;
}
}
P.FlatMirror = FlatMirror;
})();
File diff suppressed because it is too large Load Diff