Files
Learn_System/frontend/js/phys-fx.js
T

2240 lines
133 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* phys-fx.js — библиотека анимированных физических симуляций для Физики 11.
*
* Архитектура:
* - Один глобальный requestAnimationFrame-цикл (Ticker).
* - Каждая симуляция — класс с методами update(dt, t), render().
* - IntersectionObserver: симуляция приостанавливается, когда уходит из viewport.
* - Чистый SVG (без Canvas, без WebGL, без зависимостей).
*
* Публичный API: window.PHYS = { util, Oscillogram, SpringMass, Pendulum, ... }.
*
* W0 — базовая инфраструктура + 3 компонента (Oscillogram, SpringMass, Pendulum).
* Расширяется в W3 (электротехника), W5-W7 (оптика), W9-W14 (кванты, ядро).
*/
(function(){
'use strict';
if (window.PHYS && window.PHYS.__installed) return;
const P = window.PHYS = window.PHYS || {};
P.__installed = true;
/* ============================================================ */
/* ГЛОБАЛЬНЫЙ ТАЙМЕР (один RAF на всю страницу) */
/* ============================================================ */
const Ticker = {
t: 0,
last: 0,
subs: new Set(),
running: false
};
function tick(ts){
if (!Ticker.running) return;
if (!Ticker.last) Ticker.last = ts;
const dt = Math.min((ts - Ticker.last) / 1000, 0.1); // защита от лагов
Ticker.last = ts;
Ticker.t += dt;
Ticker.subs.forEach(s => {
if (!s.paused) {
try { s.update(dt, Ticker.t); s.render && s.render(); }
catch(e) {}
}
});
requestAnimationFrame(tick);
}
function startTicker(){
if (Ticker.running) return;
Ticker.running = true;
Ticker.last = 0;
requestAnimationFrame(tick);
}
function stopTicker(){ Ticker.running = false; }
/* ============================================================ */
/* УТИЛИТЫ */
/* ============================================================ */
const util = P.util = {
subscribe(sim){ Ticker.subs.add(sim); startTicker(); },
unsubscribe(sim){ Ticker.subs.delete(sim); if (Ticker.subs.size === 0) stopTicker(); },
/* Создаёт IntersectionObserver, который ставит/снимает sim.paused */
observe(sim){
if (!sim.el || !window.IntersectionObserver) return;
const io = new IntersectionObserver(entries => {
entries.forEach(e => { sim.paused = !e.isIntersecting; });
}, { threshold: 0.05 });
io.observe(sim.el);
sim._io = io;
},
/* Безопасное удаление симуляции */
destroy(sim){
util.unsubscribe(sim);
if (sim._io) { try { sim._io.disconnect(); } catch(e){} sim._io = null; }
if (sim.el) sim.el.innerHTML = '';
},
/* Хелпер: создать SVG-обёртку с осями для графика */
svgFrame(w, h, opts){
opts = opts || {};
const bg = opts.bg || '#fafafa';
const border = opts.border || '1px solid #e2e8f0';
return '<svg viewBox="0 0 '+w+' '+h+'" preserveAspectRatio="xMidYMid meet" '
+ 'style="width:100%;height:auto;display:block;background:'+bg
+ ';border:'+border+';border-radius:10px">';
},
/* Двухмерные оси t (горизонтально) и y (вертикально). Возвращает функции toX/toY */
axes(W, H, pad, tMax, yRange){
const left = pad, right = W - pad, top = pad, bot = H - pad;
const ux = (right - left) / tMax;
const uy = (bot - top) / (yRange[1] - yRange[0]);
function toX(t){ return left + t * ux; }
function toY(y){ return bot - (y - yRange[0]) * uy; }
/* SVG сетки + рамки */
let svg = '<g stroke="#e2e8f0" stroke-width="0.8">';
/* Вертикальные линии каждую секунду */
for (let s = 0; s <= tMax; s++) svg += '<line x1="'+toX(s)+'" y1="'+top+'" x2="'+toX(s)+'" y2="'+bot+'"/>';
/* Горизонтальные линии */
const yStep = (yRange[1] - yRange[0]) / 4;
for (let i = 0; i <= 4; i++){
const y = yRange[0] + i * yStep;
svg += '<line x1="'+left+'" y1="'+toY(y)+'" x2="'+right+'" y2="'+toY(y)+'"/>';
}
svg += '</g>';
/* Ось t */
svg += '<line x1="'+left+'" y1="'+toY(0)+'" x2="'+right+'" y2="'+toY(0)+'" stroke="#0f172a" stroke-width="1.4"/>';
/* Ось y */
svg += '<line x1="'+toX(0)+'" y1="'+top+'" x2="'+toX(0)+'" y2="'+bot+'" stroke="#0f172a" stroke-width="1.4"/>';
return { svg: svg, toX, toY, left, right, top, bot };
},
/* Создать ползунок-control под симуляцией.
opts: { label, min, max, step, value, onChange } */
slider(opts){
const id = 'sl-' + Math.random().toString(36).slice(2,7);
const html = '<label style="display:flex;align-items:center;gap:8px;font-size:.82rem;color:#475569;font-weight:600;margin:4px 8px">'
+ '<span style="min-width:90px">' + opts.label + '</span>'
+ '<input type="range" id="' + id + '" min="' + opts.min + '" max="' + opts.max + '" step="' + opts.step + '" value="' + opts.value + '" style="flex:1;min-width:80px">'
+ '<b id="' + id + '-v" style="font-family:JetBrains Mono,monospace;color:#0891b2;font-size:.86rem;min-width:50px;text-align:right">' + opts.value + (opts.unit || '') + '</b>'
+ '</label>';
return { html, id, wire(root){
const inp = root.querySelector('#' + id);
const v = root.querySelector('#' + id + '-v');
if (!inp || !v) return;
inp.addEventListener('input', () => {
const val = parseFloat(inp.value);
v.textContent = (opts.fmt ? opts.fmt(val) : val) + (opts.unit || '');
if (opts.onChange) opts.onChange(val);
});
} };
}
};
/* ============================================================ */
/* Oscillogram — гармонические колебания */
/* ============================================================ */
class Oscillogram {
constructor(container, opts){
opts = opts || {};
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
this.W = opts.width || 560;
this.H = opts.height || 200;
this.pad = opts.pad || 32;
this.tWindow = opts.tWindow || 4; // секунд видно
this.A = opts.A !== undefined ? opts.A : 1.0;
this.omega = opts.omega !== undefined ? opts.omega : 2 * Math.PI;
this.phi0 = opts.phi0 !== undefined ? opts.phi0 : 0;
this.damping = opts.damping || 0;
this.color = opts.color || '#dc2626';
this.label = opts.label || 'x(t)';
this.paused = false;
this.t = 0;
this.history = []; // [t, y] точки за последние tWindow секунд
this._render();
util.subscribe(this);
util.observe(this);
}
setA(v){ this.A = v; }
setOmega(v){ this.omega = v; }
setPhi(v){ this.phi0 = v; }
setDamping(v){ this.damping = v; }
reset(){ this.history = []; this.t = 0; }
update(dt){
this.t += dt;
const y = this.A * Math.exp(-this.damping * this.t) * Math.cos(this.omega * this.t + this.phi0);
this.history.push([this.t, y]);
while (this.history.length && this.history[0][0] < this.t - this.tWindow) this.history.shift();
}
render(){
if (!this.el) return;
const W = this.W, H = this.H, pad = this.pad;
const tMin = Math.max(0, this.t - this.tWindow);
const yRange = [-Math.max(1.05, this.A * 1.1), Math.max(1.05, this.A * 1.1)];
const ax = util.axes(W, H, pad, this.tWindow, yRange);
let polyline = '';
if (this.history.length > 1){
const pts = this.history.map(([t, y]) => (ax.left + (t - tMin) * (ax.right - ax.left) / this.tWindow).toFixed(1) + ',' + ax.toY(y).toFixed(1));
polyline = '<polyline points="' + pts.join(' ') + '" fill="none" stroke="' + this.color + '" stroke-width="2.4" stroke-linejoin="round"/>';
}
/* Подпись y(t) */
const titleSvg = '<text x="' + (W - pad) + '" y="' + (pad - 8) + '" text-anchor="end" font-size="12" font-family="JetBrains Mono,monospace" fill="' + this.color + '" font-weight="700">' + this.label + '</text>';
const svg = util.svgFrame(W, H) + ax.svg + polyline + titleSvg + '</svg>';
this.el.innerHTML = svg;
}
_render(){ this.render(); }
}
P.Oscillogram = Oscillogram;
/* ============================================================ */
/* SpringMass — пружинный маятник (вертикальный) */
/* ============================================================ */
class SpringMass {
constructor(container, opts){
opts = opts || {};
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
this.W = opts.width || 240;
this.H = opts.height || 280;
this.m = opts.m !== undefined ? opts.m : 0.5; // кг
this.k = opts.k !== undefined ? opts.k : 20; // Н
this.A = opts.A !== undefined ? opts.A : 0.06; // м (амплитуда)
this.color = opts.color || '#0891b2';
this.paused = false;
this.t = 0;
this._render();
util.subscribe(this);
util.observe(this);
}
setMass(m){ this.m = Math.max(0.05, m); }
setStiffness(k){ this.k = Math.max(1, k); }
setAmplitude(A){ this.A = Math.max(0.005, A); }
period(){ return 2 * Math.PI * Math.sqrt(this.m / this.k); }
freq(){ return 1 / this.period(); }
update(dt){ this.t += dt; }
render(){
if (!this.el) return;
const W = this.W, H = this.H;
const T = this.period();
const omega = 2 * Math.PI / T;
const A_px = 60; /* визуальная амплитуда в px */
const y0 = 90; /* y-координата равновесия груза в px */
const yCur = y0 + A_px * Math.cos(omega * this.t);
/* Пружина: гармошка-зигзаг от крюка (y=20) до груза (y=yCur-18) */
const cx = W / 2, hookY = 20, massY = yCur, massR = 22;
const coils = 10;
const springTop = hookY;
const springBot = massY - massR;
const segH = (springBot - springTop) / (coils * 2);
let path = 'M ' + cx + ' ' + springTop;
for (let i = 0; i < coils; i++){
path += ' L ' + (cx - 14) + ' ' + (springTop + segH * (2 * i + 1));
path += ' L ' + (cx + 14) + ' ' + (springTop + segH * (2 * i + 2));
}
path += ' L ' + cx + ' ' + springBot;
/* Линейка справа */
const ruler = '<g stroke="#cbd5e1" stroke-width="1" font-family="JetBrains Mono,monospace" font-size="10" fill="#64748b">'
+ '<line x1="' + (W - 38) + '" y1="' + (y0 - A_px) + '" x2="' + (W - 38) + '" y2="' + (y0 + A_px) + '" stroke-width="1.6"/>'
+ '<line x1="' + (W - 44) + '" y1="' + (y0 - A_px) + '" x2="' + (W - 32) + '" y2="' + (y0 - A_px) + '"/>'
+ '<text x="' + (W - 26) + '" y="' + (y0 - A_px + 4) + '">+A</text>'
+ '<line x1="' + (W - 44) + '" y1="' + y0 + '" x2="' + (W - 32) + '" y2="' + y0 + '" stroke="#0f172a" stroke-width="1.4"/>'
+ '<text x="' + (W - 26) + '" y="' + (y0 + 4) + '">0</text>'
+ '<line x1="' + (W - 44) + '" y1="' + (y0 + A_px) + '" x2="' + (W - 32) + '" y2="' + (y0 + A_px) + '"/>'
+ '<text x="' + (W - 26) + '" y="' + (y0 + A_px + 4) + '">-A</text>'
+ '</g>';
/* Период справа сверху */
const Tlabel = '<text x="12" y="20" font-family="JetBrains Mono,monospace" font-size="12" fill="' + this.color + '" font-weight="700">T = ' + T.toFixed(2) + ' с</text>';
const svg = util.svgFrame(W, H, {bg:'#f8fafc'})
+ '<line x1="0" y1="' + (hookY - 6) + '" x2="' + W + '" y2="' + (hookY - 6) + '" stroke="#334155" stroke-width="3"/>'
+ '<g stroke="#334155" stroke-width="3" fill="none" stroke-linejoin="round" stroke-linecap="round">'
+ '<path d="' + path + '"/>'
+ '</g>'
+ '<rect x="' + (cx - massR) + '" y="' + (massY - massR) + '" width="' + (2 * massR) + '" height="' + (2 * massR) + '" rx="6" fill="' + this.color + '" stroke="#0f172a" stroke-width="1.6"/>'
+ '<text x="' + cx + '" y="' + (massY + 5) + '" text-anchor="middle" font-family="Outfit,sans-serif" font-size="14" font-weight="800" fill="#fff">m</text>'
+ ruler + Tlabel
+ '</svg>';
this.el.innerHTML = svg;
}
_render(){ this.render(); }
}
P.SpringMass = SpringMass;
/* ============================================================ */
/* Pendulum — математический маятник */
/* ============================================================ */
class Pendulum {
constructor(container, opts){
opts = opts || {};
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
this.W = opts.width || 240;
this.H = opts.height || 260;
this.l = opts.l !== undefined ? opts.l : 1.0; // м
this.g = opts.g !== undefined ? opts.g : 9.81;
this.theta0 = opts.theta0 !== undefined ? opts.theta0 : Math.PI / 12; // начальный угол (рад)
this.color = opts.color || '#0891b2';
this.paused = false;
this.t = 0;
this._render();
util.subscribe(this);
util.observe(this);
}
setLength(l){ this.l = Math.max(0.1, l); }
setG(g){ this.g = Math.max(0.5, g); }
setTheta0(theta){ this.theta0 = Math.max(0.02, Math.min(Math.PI/4, theta)); }
period(){ return 2 * Math.PI * Math.sqrt(this.l / this.g); }
update(dt){ this.t += dt; }
render(){
if (!this.el) return;
const W = this.W, H = this.H;
const T = this.period();
const omega = 2 * Math.PI / T;
const theta = this.theta0 * Math.cos(omega * this.t);
const cx = W / 2, hookY = 20;
const Lpx = Math.min(160, H - 70);
const bobR = 18;
const bx = cx + Lpx * Math.sin(theta);
const by = hookY + Lpx * Math.cos(theta);
/* Дуга-траектория */
const arcR = Lpx;
const arcStart = -this.theta0;
const arcEnd = this.theta0;
const aS = { x: cx + arcR * Math.sin(arcStart), y: hookY + arcR * Math.cos(arcStart) };
const aE = { x: cx + arcR * Math.sin(arcEnd), y: hookY + arcR * Math.cos(arcEnd) };
const largeArc = (arcEnd - arcStart) > Math.PI ? 1 : 0;
const sweep = 1;
const arc = '<path d="M ' + aS.x.toFixed(1) + ' ' + aS.y.toFixed(1) + ' A ' + arcR + ' ' + arcR + ' 0 ' + largeArc + ' ' + sweep + ' ' + aE.x.toFixed(1) + ' ' + aE.y.toFixed(1) + '" fill="none" stroke="#cbd5e1" stroke-width="1.4" stroke-dasharray="4 4"/>';
/* Вертикальная пунктирная ось */
const vert = '<line x1="' + cx + '" y1="' + hookY + '" x2="' + cx + '" y2="' + (hookY + Lpx + 5) + '" stroke="#cbd5e1" stroke-width="1" stroke-dasharray="3 3"/>';
/* Подвес */
const string = '<line x1="' + cx + '" y1="' + hookY + '" x2="' + bx.toFixed(1) + '" y2="' + by.toFixed(1) + '" stroke="#0f172a" stroke-width="2"/>';
const bob = '<circle cx="' + bx.toFixed(1) + '" cy="' + by.toFixed(1) + '" r="' + bobR + '" fill="' + this.color + '" stroke="#0f172a" stroke-width="1.6"/>';
/* Период */
const Tlabel = '<text x="12" y="20" font-family="JetBrains Mono,monospace" font-size="12" fill="' + this.color + '" font-weight="700">T = ' + T.toFixed(2) + ' с</text>';
/* Подвес-крепление */
const hook = '<line x1="' + (cx - 30) + '" y1="' + (hookY - 6) + '" x2="' + (cx + 30) + '" y2="' + (hookY - 6) + '" stroke="#334155" stroke-width="3"/>';
const svg = util.svgFrame(W, H, {bg:'#f8fafc'}) + hook + vert + arc + string + bob + Tlabel + '</svg>';
this.el.innerHTML = svg;
}
_render(){ this.render(); }
}
P.Pendulum = Pendulum;
/* ============================================================ */
/* EnergyView — превращения энергии при гарм. колебаниях */
/* Показывает W_к, W_п, W_мех=const на одном графике */
/* ============================================================ */
class EnergyView {
constructor(container, opts){
opts = opts || {};
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
this.W = opts.width || 560;
this.H = opts.height || 240;
this.pad = opts.pad || 36;
this.A = opts.A !== undefined ? opts.A : 1.0;
this.omega = opts.omega !== undefined ? opts.omega : 2 * Math.PI;
this.tWindow = opts.tWindow || 4;
this.paused = false;
this.t = 0;
this.history = []; // [t, Wk, Wp]
util.subscribe(this);
util.observe(this);
this.render();
}
setA(v){ this.A = v; this.history = []; }
setOmega(v){ this.omega = v; this.history = []; }
update(dt){
this.t += dt;
/* Для x = A cos(ωt): v = -Aω sin(ωt)
W_к = m v² / 2 = (1/2) m A² ω² sin²(ωt)
W_п = k x² / 2 = (1/2) m ω² · A² cos²(ωt) (k = m ω²)
В безразмерных: положим (1/2)mω²A² = 1 — тогда обе варьируются 0..1, сумма = 1 */
const c = Math.cos(this.omega * this.t);
const s = Math.sin(this.omega * this.t);
const Wp = c * c;
const Wk = s * s;
this.history.push([this.t, Wk, Wp]);
while (this.history.length && this.history[0][0] < this.t - this.tWindow) this.history.shift();
}
render(){
if (!this.el) return;
const W = this.W, H = this.H, pad = this.pad;
const tMin = Math.max(0, this.t - this.tWindow);
const yRange = [0, 1.1];
const ax = util.axes(W, H, pad, this.tWindow, yRange);
function path(idx, color, label){
if (this.history.length < 2) return '';
const pts = this.history.map(p => (ax.left + (p[0] - tMin) * (ax.right - ax.left) / this.tWindow).toFixed(1) + ',' + ax.toY(p[idx]).toFixed(1));
return '<polyline points="' + pts.join(' ') + '" fill="none" stroke="' + color + '" stroke-width="2.2" stroke-linejoin="round"/>';
}
const pK = path.call(this, 1, '#dc2626');
const pP = path.call(this, 2, '#16a34a');
/* W_мех = const = 1 (горизонтальная линия) */
const pM = '<line x1="' + ax.left + '" y1="' + ax.toY(1) + '" x2="' + ax.right + '" y2="' + ax.toY(1) + '" stroke="#7c3aed" stroke-width="2.4" stroke-dasharray="6 4"/>';
/* Легенда */
const legend = '<g font-family="JetBrains Mono,monospace" font-size="11" font-weight="700">'
+ '<rect x="' + (W - 130) + '" y="' + (pad - 28) + '" width="118" height="74" fill="#fff" stroke="#e2e8f0" rx="6"/>'
+ '<line x1="' + (W - 122) + '" y1="' + (pad - 14) + '" x2="' + (W - 102) + '" y2="' + (pad - 14) + '" stroke="#dc2626" stroke-width="2.4"/>'
+ '<text x="' + (W - 96) + '" y="' + (pad - 10) + '" fill="#dc2626">W кинет.</text>'
+ '<line x1="' + (W - 122) + '" y1="' + (pad + 6) + '" x2="' + (W - 102) + '" y2="' + (pad + 6) + '" stroke="#16a34a" stroke-width="2.4"/>'
+ '<text x="' + (W - 96) + '" y="' + (pad + 10) + '" fill="#16a34a">W потенц.</text>'
+ '<line x1="' + (W - 122) + '" y1="' + (pad + 26) + '" x2="' + (W - 102) + '" y2="' + (pad + 26) + '" stroke="#7c3aed" stroke-width="2.4" stroke-dasharray="4 3"/>'
+ '<text x="' + (W - 96) + '" y="' + (pad + 30) + '" fill="#7c3aed">W мех = const</text>'
+ '</g>';
const svg = util.svgFrame(W, H) + ax.svg + pM + pK + pP + legend + '</svg>';
this.el.innerHTML = svg;
}
}
P.EnergyView = EnergyView;
/* ============================================================ */
/* ResonanceCurve — резонансная кривая A(ω) при разных γ */
/* ============================================================ */
class ResonanceCurve {
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.pad = opts.pad || 40;
this.omega0 = opts.omega0 || 1.0; /* собственная частота (норм.) */
this.gamma = opts.gamma !== undefined ? opts.gamma : 0.15;
this.omegaCur = opts.omegaCur !== undefined ? opts.omegaCur : 0.6;
this.color = opts.color || '#7c3aed';
this.paused = false;
this.render();
}
setGamma(g){ this.gamma = Math.max(0.02, g); this.render(); }
setOmegaCur(w){ this.omegaCur = Math.max(0.02, w); this.render(); }
/* update не нужен — статический график, обновляется по setter */
update(){}
render(){
if (!this.el) return;
const W = this.W, H = this.H, pad = this.pad;
const wMin = 0, wMax = 2 * this.omega0;
/* Подсчитаем все амплитуды чтобы знать max */
function amp(w, g, w0){
const dw2 = (w0 * w0 - w * w);
const denom = Math.sqrt(dw2 * dw2 + (2 * g * w) * (2 * g * w));
return 1 / Math.max(denom, 1e-6);
}
const gMin = 0.05;
const ampMax = amp(this.omega0, gMin, this.omega0) * 1.1;
/* Сетка */
const left = pad, right = W - pad, top = pad, bot = H - pad;
const ux = (right - left) / (wMax - wMin);
const uy = (bot - top) / ampMax;
function toX(w){ return left + (w - wMin) * ux; }
function toY(a){ return bot - a * uy; }
let svg = util.svgFrame(W, H);
/* Линии сетки */
svg += '<g stroke="#e2e8f0" stroke-width="0.8">';
for (let i = 0; i <= 4; i++){
const w = wMin + (wMax - wMin) * i / 4;
svg += '<line x1="' + toX(w) + '" y1="' + top + '" x2="' + toX(w) + '" y2="' + bot + '"/>';
}
for (let i = 0; i <= 4; i++){
svg += '<line x1="' + left + '" y1="' + (top + (bot - top) * i / 4) + '" x2="' + right + '" y2="' + (top + (bot - top) * i / 4) + '"/>';
}
svg += '</g>';
/* Оси */
svg += '<line x1="' + left + '" y1="' + bot + '" x2="' + right + '" y2="' + bot + '" stroke="#0f172a" stroke-width="1.4"/>';
svg += '<line x1="' + left + '" y1="' + top + '" x2="' + left + '" y2="' + bot + '" stroke="#0f172a" stroke-width="1.4"/>';
/* Подписи осей */
svg += '<text x="' + (right - 4) + '" y="' + (bot - 6) + '" font-family="JetBrains Mono,monospace" font-size="11" fill="#64748b" text-anchor="end">ω</text>';
svg += '<text x="' + (left + 4) + '" y="' + (top + 12) + '" font-family="JetBrains Mono,monospace" font-size="11" fill="#64748b">A</text>';
/* Линия ω₀ — собственная частота */
svg += '<line x1="' + toX(this.omega0) + '" y1="' + top + '" x2="' + toX(this.omega0) + '" y2="' + bot + '" stroke="#94a3b8" stroke-width="1.2" stroke-dasharray="4 4"/>';
svg += '<text x="' + toX(this.omega0) + '" y="' + (top - 4) + '" font-family="JetBrains Mono,monospace" font-size="10" fill="#64748b" text-anchor="middle">ω₀</text>';
/* Кривая A(ω) */
let path = 'M ';
const N = 200;
for (let i = 0; i <= N; i++){
const w = wMin + (wMax - wMin) * i / N;
const a = amp(w, this.gamma, this.omega0);
path += toX(w).toFixed(1) + ',' + toY(Math.min(a, ampMax)).toFixed(1);
if (i < N) path += ' L ';
}
svg += '<path d="' + path + '" fill="none" stroke="' + this.color + '" stroke-width="2.6" stroke-linejoin="round"/>';
/* Точка-маркер на текущей ω */
const aCur = Math.min(amp(this.omegaCur, this.gamma, this.omega0), ampMax);
svg += '<line x1="' + toX(this.omegaCur) + '" y1="' + toY(0) + '" x2="' + toX(this.omegaCur) + '" y2="' + toY(aCur) + '" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="3 3"/>';
svg += '<circle cx="' + toX(this.omegaCur) + '" cy="' + toY(aCur) + '" r="6" fill="#dc2626" stroke="#fff" stroke-width="2"/>';
/* Подпись γ */
svg += '<text x="' + (left + 10) + '" y="' + (top + 16) + '" font-family="JetBrains Mono,monospace" font-size="12" fill="' + this.color + '" font-weight="700">γ = ' + this.gamma.toFixed(2) + '</text>';
svg += '<text x="' + (left + 10) + '" y="' + (top + 32) + '" font-family="JetBrains Mono,monospace" font-size="12" fill="#dc2626" font-weight="700">ω = ' + this.omegaCur.toFixed(2) + ' · A = ' + aCur.toFixed(2) + '</text>';
svg += '</svg>';
this.el.innerHTML = svg;
}
}
P.ResonanceCurve = ResonanceCurve;
/* ============================================================ */
/* TransverseWave — поперечная волна на струне */
/* y(x, t) = A sin(kx - ωt + φ) */
/* ============================================================ */
class TransverseWave {
constructor(container, opts){
opts = opts || {};
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
this.W = opts.width || 560;
this.H = opts.height || 180;
this.A = opts.A !== undefined ? opts.A : 0.4; /* отн. амплитуда (0..1) */
this.lambda = opts.lambda !== undefined ? opts.lambda : 1.0; /* отн. длина волны */
this.v = opts.v !== undefined ? opts.v : 0.8; /* скорость распространения (отн./с) */
this.color = opts.color || '#0891b2';
this.markerX = opts.markerX !== undefined ? opts.markerX : 0.4; /* пол. красной точки (0..1 от ширины) */
this.paused = false;
this.t = 0;
util.subscribe(this);
util.observe(this);
this.render();
}
setA(v){ this.A = v; }
setLambda(v){ this.lambda = Math.max(0.1, v); }
setV(v){ this.v = v; }
update(dt){ this.t += dt; }
render(){
if (!this.el) return;
const W = this.W, H = this.H;
const yCenter = H / 2;
const amp = this.A * (H / 2 - 18);
const k = 2 * Math.PI / this.lambda;
const omega = k * this.v;
/* SVG: горизонтальная ось + волна как polyline */
let svg = util.svgFrame(W, H, {bg:'#f8fafc'});
/* Ось */
svg += '<line x1="0" y1="' + yCenter + '" x2="' + W + '" y2="' + yCenter + '" stroke="#cbd5e1" stroke-width="1" stroke-dasharray="4 3"/>';
/* Кривая */
const N = 180;
let path = 'M ';
for (let i = 0; i <= N; i++){
const px = (W * i / N);
/* Реальное x в относительных единицах (1 длина волны на ~120px) */
const x = px / 120;
const y = yCenter - amp * Math.sin(k * x - omega * this.t);
path += px.toFixed(1) + ',' + y.toFixed(1);
if (i < N) path += ' L ';
}
svg += '<path d="' + path + '" fill="none" stroke="' + this.color + '" stroke-width="2.6" stroke-linejoin="round"/>';
/* Красный маркер — колеблющаяся точка */
const mPx = this.markerX * W;
const mX = mPx / 120;
const mY = yCenter - amp * Math.sin(k * mX - omega * this.t);
svg += '<line x1="' + mPx + '" y1="' + (yCenter - amp) + '" x2="' + mPx + '" y2="' + (yCenter + amp) + '" stroke="#fca5a5" stroke-width="1" stroke-dasharray="3 3"/>';
svg += '<circle cx="' + mPx + '" cy="' + mY.toFixed(1) + '" r="7" fill="#dc2626" stroke="#fff" stroke-width="2"/>';
/* Метка λ — горизонтальная скобка над волной */
const lambdaPx = 120 * this.lambda;
if (lambdaPx < W - 60){
const lxStart = 20, lxEnd = lxStart + lambdaPx;
svg += '<line x1="' + lxStart + '" y1="20" x2="' + lxEnd + '" y2="20" stroke="#7c3aed" stroke-width="1.6"/>';
svg += '<line x1="' + lxStart + '" y1="14" x2="' + lxStart + '" y2="26" stroke="#7c3aed" stroke-width="1.6"/>';
svg += '<line x1="' + lxEnd + '" y1="14" x2="' + lxEnd + '" y2="26" stroke="#7c3aed" stroke-width="1.6"/>';
svg += '<text x="' + ((lxStart + lxEnd) / 2) + '" y="14" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" font-weight="700" fill="#7c3aed">λ</text>';
}
svg += '</svg>';
this.el.innerHTML = svg;
}
}
P.TransverseWave = TransverseWave;
/* ============================================================ */
/* LongitudinalWave — продольная волна (сжатия/разрежения) */
/* ============================================================ */
class LongitudinalWave {
constructor(container, opts){
opts = opts || {};
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
this.W = opts.width || 560;
this.H = opts.height || 130;
this.A = opts.A !== undefined ? opts.A : 0.5; /* амплитуда (0..1) */
this.lambda = opts.lambda !== undefined ? opts.lambda : 1.0;
this.v = opts.v !== undefined ? opts.v : 0.8;
this.color = opts.color || '#0891b2';
this.nDots = opts.nDots || 60;
this.paused = false;
this.t = 0;
util.subscribe(this);
util.observe(this);
this.render();
}
setA(v){ this.A = v; }
setLambda(v){ this.lambda = Math.max(0.1, v); }
setV(v){ this.v = v; }
update(dt){ this.t += dt; }
render(){
if (!this.el) return;
const W = this.W, H = this.H;
const yC = H / 2;
const k = 2 * Math.PI / this.lambda;
const omega = k * this.v;
const xScale = 120; /* px на 1 ед. */
const amp = this.A * 10; /* px смещения */
const margin = 20;
let svg = util.svgFrame(W, H, {bg:'#f8fafc'});
/* Точки-молекулы */
let dots = '';
for (let i = 0; i < this.nDots; i++){
const x0 = margin + (W - 2 * margin) * i / (this.nDots - 1);
const xRel = x0 / xScale;
const disp = amp * Math.sin(k * xRel - omega * this.t);
const x = x0 + disp;
dots += '<circle cx="' + x.toFixed(1) + '" cy="' + yC + '" r="3" fill="' + this.color + '"/>';
}
svg += dots;
/* Подписи зон сжатия / разрежения */
svg += '<text x="' + (W / 2) + '" y="' + (H - 6) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="10" fill="#94a3b8">сжатие &harr; разрежение</text>';
svg += '</svg>';
this.el.innerHTML = svg;
}
}
P.LongitudinalWave = LongitudinalWave;
/* ============================================================ */
/* LCcircuit — колебательный контур */
/* q(t) = Q0 cos(ωt), i(t) = -Q0 ω sin(ωt), ω = 1/√(LC) */
/* ============================================================ */
class LCcircuit {
constructor(container, opts){
opts = opts || {};
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
this.W = opts.width || 480;
this.H = opts.height || 280;
this.L = opts.L !== undefined ? opts.L : 0.01; /* Гн */
this.C = opts.C !== undefined ? opts.C : 1e-6; /* Ф */
this.Q0 = opts.Q0 !== undefined ? opts.Q0 : 1.0; /* нормированный заряд */
this.color = opts.color || '#7c3aed';
this.paused = false;
this.t = 0;
util.subscribe(this);
util.observe(this);
this.render();
}
setL(L){ this.L = Math.max(1e-4, L); }
setC(C){ this.C = Math.max(1e-9, C); }
period(){ return 2 * Math.PI * Math.sqrt(this.L * this.C); }
freq(){ return 1 / this.period(); }
update(dt){ this.t += dt; }
render(){
if (!this.el) return;
const W = this.W, H = this.H;
const T = this.period();
const omega = 2 * Math.PI / T;
const phase = omega * this.t;
const q = this.Q0 * Math.cos(phase);
const i = -this.Q0 * omega * Math.sin(phase);
/* Геометрия: C слева вверху, L справа вверху, соединены проводами */
const cx = W / 2, cy = H / 2 - 20;
const cap = {x: cx - 100, y: cy};
const ind = {x: cx + 100, y: cy};
/* Energies (для подсветки): W_C ~ q², W_L ~ i² */
const WC = q * q;
const WL = (i / omega) * (i / omega); /* в норм. единицах */
const total = WC + WL;
const cOpacity = 0.3 + 0.7 * WC / total;
const lOpacity = 0.3 + 0.7 * WL / total;
let svg = util.svgFrame(W, H, {bg:'#f8fafc'});
/* Провода */
const wireY1 = cy - 50, wireY2 = cy + 50;
svg += '<path d="M ' + cap.x + ' ' + (cy - 18) + ' L ' + cap.x + ' ' + wireY1 + ' L ' + ind.x + ' ' + wireY1 + ' L ' + ind.x + ' ' + (cy - 18) + '" fill="none" stroke="#0f172a" stroke-width="2"/>';
svg += '<path d="M ' + cap.x + ' ' + (cy + 18) + ' L ' + cap.x + ' ' + wireY2 + ' L ' + ind.x + ' ' + wireY2 + ' L ' + ind.x + ' ' + (cy + 18) + '" fill="none" stroke="#0f172a" stroke-width="2"/>';
/* Конденсатор: две параллельные пластины */
const plateW = 36;
svg += '<g stroke="#0f172a" stroke-width="3">';
svg += '<line x1="' + (cap.x - plateW/2) + '" y1="' + (cap.y - 18) + '" x2="' + (cap.x + plateW/2) + '" y2="' + (cap.y - 18) + '"/>';
svg += '<line x1="' + (cap.x - plateW/2) + '" y1="' + (cap.y + 18) + '" x2="' + (cap.x + plateW/2) + '" y2="' + (cap.y + 18) + '"/>';
svg += '</g>';
/* Заряды на пластинах */
const sign = q > 0 ? 1 : -1;
const topCh = sign > 0 ? '+' : '';
const botCh = sign > 0 ? '' : '+';
const qAbsNorm = Math.abs(q) / this.Q0;
svg += '<text x="' + cap.x + '" y="' + (cap.y - 24) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="14" font-weight="800" fill="#dc2626" opacity="' + (qAbsNorm).toFixed(2) + '">' + topCh + '</text>';
svg += '<text x="' + cap.x + '" y="' + (cap.y + 36) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="14" font-weight="800" fill="#2563eb" opacity="' + (qAbsNorm).toFixed(2) + '">' + botCh + '</text>';
svg += '<text x="' + (cap.x - 30) + '" y="' + (cap.y + 5) + '" text-anchor="end" font-family="JetBrains Mono,monospace" font-size="13" fill="#dc2626" font-weight="700">C</text>';
/* Индуктор: петли */
const coils = 4, coilR = 8, coilW = 64;
let coilPath = 'M ' + (ind.x - coilW/2) + ' ' + ind.y;
for (let k = 0; k < coils; k++){
const x0 = ind.x - coilW/2 + (coilW / coils) * k;
coilPath += ' a ' + coilR + ' ' + coilR + ' 0 0 1 ' + (coilW / coils) + ' 0';
}
svg += '<path d="' + coilPath + '" fill="none" stroke="#0f172a" stroke-width="2.4"/>';
svg += '<text x="' + (ind.x + 40) + '" y="' + (ind.y + 5) + '" font-family="JetBrains Mono,monospace" font-size="13" fill="#2563eb" font-weight="700">L</text>';
/* Стрелка тока */
const iDir = i > 0 ? 1 : -1;
const iAbs = Math.abs(i) / (this.Q0 * omega);
if (iAbs > 0.05){
const aY = wireY1 - 14;
const aX1 = cx - 30 * iDir;
const aX2 = cx + 30 * iDir;
svg += '<line x1="' + aX1 + '" y1="' + aY + '" x2="' + aX2 + '" y2="' + aY + '" stroke="#16a34a" stroke-width="2.4" opacity="' + iAbs.toFixed(2) + '"/>';
svg += '<polygon points="' + aX2 + ',' + aY + ' ' + (aX2 - 8 * iDir) + ',' + (aY - 4) + ' ' + (aX2 - 8 * iDir) + ',' + (aY + 4) + '" fill="#16a34a" opacity="' + iAbs.toFixed(2) + '"/>';
svg += '<text x="' + cx + '" y="' + (aY - 6) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="12" fill="#16a34a" font-weight="700">i</text>';
}
/* Энергетические столбцы */
const eY = H - 36, eH = 24;
svg += '<rect x="40" y="' + eY + '" width="120" height="' + eH + '" fill="#fef3c7" stroke="#d97706" stroke-width="1"/>';
svg += '<rect x="40" y="' + eY + '" width="' + (120 * WC / total).toFixed(1) + '" height="' + eH + '" fill="#dc2626" opacity="0.7"/>';
svg += '<text x="' + (40 + 60) + '" y="' + (eY + eH + 12) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="10" fill="#475569">W_C</text>';
svg += '<rect x="' + (W - 160) + '" y="' + eY + '" width="120" height="' + eH + '" fill="#fef3c7" stroke="#d97706" stroke-width="1"/>';
svg += '<rect x="' + (W - 160) + '" y="' + eY + '" width="' + (120 * WL / total).toFixed(1) + '" height="' + eH + '" fill="#2563eb" opacity="0.7"/>';
svg += '<text x="' + (W - 100) + '" y="' + (eY + eH + 12) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="10" fill="#475569">W_L</text>';
/* Подпись периода */
const Tdisp = T < 1e-3 ? (T * 1e6).toFixed(1) + ' мкс' : (T * 1e3).toFixed(2) + ' мс';
svg += '<text x="' + (W / 2) + '" y="22" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="13" fill="' + this.color + '" font-weight="800">T = 2π√(LC) = ' + Tdisp + '</text>';
svg += '</svg>';
this.el.innerHTML = svg;
}
}
P.LCcircuit = LCcircuit;
/* ============================================================ */
/* ACgen — генератор переменного тока (вращающаяся рамка в B) */
/* ============================================================ */
class ACgen {
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.omega = opts.omega !== undefined ? opts.omega : 2 * Math.PI; /* рад/с */
this.U0 = opts.U0 !== undefined ? opts.U0 : 1.0;
this.color = opts.color || '#7c3aed';
this.tWindow = opts.tWindow || 4;
this.paused = false;
this.t = 0;
this.history = [];
util.subscribe(this);
util.observe(this);
this.render();
}
setOmega(w){ this.omega = w; this.history = []; }
update(dt){
this.t += dt;
this.history.push([this.t, this.U0 * Math.sin(this.omega * this.t)]);
while (this.history.length && this.history[0][0] < this.t - this.tWindow) this.history.shift();
}
render(){
if (!this.el) return;
const W = this.W, H = this.H;
/* Левая часть: рамка в магнитном поле; правая: график U(t) */
const leftW = 200;
const rightLeft = leftW + 10;
let svg = util.svgFrame(W, H, {bg:'#f8fafc'});
/* Магнитное поле (стрелки B) */
svg += '<g stroke="#94a3b8" stroke-width="1.2">';
for (let i = 0; i < 5; i++){
const x = 20 + i * 40;
svg += '<line x1="' + x + '" y1="20" x2="' + x + '" y2="' + (H - 20) + '" marker-end="url(#acgen-arr)"/>';
}
svg += '</g>';
svg += '<defs><marker id="acgen-arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" fill="#94a3b8"/></marker></defs>';
svg += '<text x="14" y="14" font-family="JetBrains Mono,monospace" font-size="11" fill="#475569" font-weight="700">B</text>';
/* Вращающаяся рамка: эллипс, отображающий проекцию прямоугольника */
const fx = 110, fy = H / 2;
const phi = this.omega * this.t;
const rx = 40 * Math.abs(Math.cos(phi));
const ry = 30;
svg += '<ellipse cx="' + fx + '" cy="' + fy + '" rx="' + rx.toFixed(1) + '" ry="' + ry + '" fill="none" stroke="#dc2626" stroke-width="2.4"/>';
/* Ось вращения */
svg += '<line x1="' + fx + '" y1="' + (fy - 50) + '" x2="' + fx + '" y2="' + (fy + 50) + '" stroke="#0f172a" stroke-width="2" stroke-dasharray="4 3"/>';
/* Контакт скользящих колец (схема) */
svg += '<line x1="' + fx + '" y1="' + (fy + 50) + '" x2="' + fx + '" y2="' + (fy + 70) + '" stroke="#0f172a" stroke-width="2"/>';
svg += '<line x1="' + fx + '" y1="' + (fy + 70) + '" x2="' + rightLeft + '" y2="' + (fy + 70) + '" stroke="#0f172a" stroke-width="2"/>';
/* График U(t) — справа */
const gPad = 26;
const tMin = Math.max(0, this.t - this.tWindow);
const ax = util.axes(W - rightLeft, H, gPad, this.tWindow, [-this.U0 * 1.2, this.U0 * 1.2]);
svg += '<g transform="translate(' + rightLeft + ' 0)">' + ax.svg + '</g>';
if (this.history.length > 1){
const pts = this.history.map(([t, y]) => (rightLeft + ax.left + (t - tMin) * (ax.right - ax.left) / this.tWindow).toFixed(1) + ',' + ax.toY(y).toFixed(1));
svg += '<polyline points="' + pts.join(' ') + '" fill="none" stroke="' + this.color + '" stroke-width="2.4"/>';
}
/* Подпись */
svg += '<text x="' + (rightLeft + 20) + '" y="20" font-family="JetBrains Mono,monospace" font-size="12" fill="' + this.color + '" font-weight="700">U(t) = U₀ sin(ωt)</text>';
svg += '</svg>';
this.el.innerHTML = svg;
}
}
P.ACgen = ACgen;
/* ============================================================ */
/* Transformer — схема трансформатора с расчётом */
/* ============================================================ */
class Transformer {
constructor(container, opts){
opts = opts || {};
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
this.W = opts.width || 520;
this.H = opts.height || 240;
this.N1 = opts.N1 || 200;
this.N2 = opts.N2 || 50;
this.U1 = opts.U1 || 220;
this.color = opts.color || '#7c3aed';
this.render();
}
setN1(n){ this.N1 = Math.max(1, n|0); this.render(); }
setN2(n){ this.N2 = Math.max(1, n|0); this.render(); }
setU1(u){ this.U1 = u; this.render(); }
update(){ /* статика */ }
render(){
if (!this.el) return;
const W = this.W, H = this.H;
const k = this.N1 / this.N2;
const U2 = this.U1 / k;
const cy = H / 2;
let svg = util.svgFrame(W, H, {bg:'#f8fafc'});
/* Сердечник трансформатора (прямоугольник с вырезом) */
const coreL = 110, coreR = W - 110;
svg += '<rect x="' + coreL + '" y="' + (cy - 70) + '" width="' + (coreR - coreL) + '" height="140" fill="none" stroke="#64748b" stroke-width="6"/>';
svg += '<rect x="' + (coreL + 16) + '" y="' + (cy - 54) + '" width="' + (coreR - coreL - 32) + '" height="108" fill="#f8fafc" stroke="none"/>';
/* Первичная обмотка слева */
const coil1X = coreL + 24;
for (let i = 0; i < 6; i++){
const y = cy - 50 + i * 16;
svg += '<path d="M ' + (coil1X - 12) + ' ' + y + ' a 8 8 0 0 1 8 -8 a 8 8 0 0 1 8 8 a 8 8 0 0 1 -8 8 a 8 8 0 0 1 -8 -8 z" fill="none" stroke="#dc2626" stroke-width="2.4"/>';
}
/* Вторичная обмотка справа */
const coil2X = coreR - 24;
/* Адаптируем число витков визуально (max 8 для удобства) */
const visTurns2 = Math.max(2, Math.min(10, Math.round(6 * this.N2 / this.N1)));
for (let i = 0; i < visTurns2; i++){
const y = cy - 50 + i * (100 / visTurns2);
svg += '<path d="M ' + (coil2X - 12) + ' ' + y + ' a 6 6 0 0 1 8 -6 a 6 6 0 0 1 8 6 a 6 6 0 0 1 -8 6 a 6 6 0 0 1 -8 -6 z" fill="none" stroke="#2563eb" stroke-width="2.4"/>';
}
/* Провода-выходы первичной */
svg += '<line x1="' + (coil1X - 12) + '" y1="' + (cy - 50) + '" x2="20" y2="' + (cy - 50) + '" stroke="#dc2626" stroke-width="2"/>';
svg += '<line x1="' + (coil1X - 12) + '" y1="' + (cy + 50) + '" x2="20" y2="' + (cy + 50) + '" stroke="#dc2626" stroke-width="2"/>';
/* Провода вторичной */
svg += '<line x1="' + (coil2X + 4) + '" y1="' + (cy - 50) + '" x2="' + (W - 20) + '" y2="' + (cy - 50) + '" stroke="#2563eb" stroke-width="2"/>';
svg += '<line x1="' + (coil2X + 4) + '" y1="' + (cy + 50) + '" x2="' + (W - 20) + '" y2="' + (cy + 50) + '" stroke="#2563eb" stroke-width="2"/>';
/* Подписи N1, N2, U1, U2 */
svg += '<text x="' + (coil1X - 16) + '" y="' + (cy - 60) + '" font-family="JetBrains Mono,monospace" font-size="13" fill="#dc2626" font-weight="800">N₁ = ' + this.N1 + '</text>';
svg += '<text x="' + (coil2X + 16) + '" y="' + (cy - 60) + '" font-family="JetBrains Mono,monospace" font-size="13" fill="#2563eb" font-weight="800" text-anchor="end">N₂ = ' + this.N2 + '</text>';
svg += '<text x="' + (coil2X + 6) + '" y="' + (cy - 60) + '" font-family="JetBrains Mono,monospace" font-size="13" fill="#2563eb" font-weight="800">N₂ = ' + this.N2 + '</text>';
svg += '<text x="20" y="' + (cy - 56) + '" font-family="JetBrains Mono,monospace" font-size="12" fill="#dc2626">U₁ = ' + this.U1.toFixed(0) + ' В</text>';
svg += '<text x="' + (W - 20) + '" y="' + (cy - 56) + '" font-family="JetBrains Mono,monospace" font-size="12" fill="#2563eb" text-anchor="end">U₂ = ' + U2.toFixed(1) + ' В</text>';
/* Подпись коэф. */
svg += '<text x="' + (W/2) + '" y="22" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="13" fill="' + this.color + '" font-weight="800">k = N₁/N₂ = ' + k.toFixed(2) + '</text>';
svg += '<text x="' + (W/2) + '" y="' + (H - 12) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" fill="#64748b">' + (k > 1 ? 'понижающий' : 'повышающий') + '</text>';
svg += '</svg>';
this.el.innerHTML = svg;
}
}
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;
/* ============================================================ */
/* 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;
/* ============================================================ */
/* 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;
/* ============================================================ */
/* GammaPlot — график γ(β) и τ/τ₀, L/L₀ */
/* ============================================================ */
class GammaPlot {
constructor(container, opts){
opts = opts || {};
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
this.W = opts.width || 580;
this.H = opts.height || 280;
this.beta = opts.beta !== undefined ? opts.beta : 0.5; /* v/c */
this.color = opts.color || '#2563eb';
this.paused = true;
this.render();
}
setBeta(v){ this.beta = Math.max(0, Math.min(0.999, v)); this.render(); }
update(){}
render(){
if (!this.el) return;
const W = this.W, H = this.H;
const pad = 40;
const left = pad, right = W - pad - 100, top = 30, bot = H - 40;
let svg = util.svgFrame(W, H, {bg:'#f8fafc'});
/* Сетка */
svg += '<g stroke="#e2e8f0" stroke-width="0.8">';
for (let i = 0; i <= 10; i++){
const x = left + i * (right - left) / 10;
svg += '<line x1="' + x + '" y1="' + top + '" x2="' + x + '" y2="' + bot + '"/>';
}
for (let i = 0; i <= 6; i++){
const y = top + i * (bot - top) / 6;
svg += '<line x1="' + left + '" y1="' + y + '" x2="' + right + '" y2="' + y + '"/>';
}
svg += '</g>';
/* Оси */
svg += '<line x1="' + left + '" y1="' + bot + '" x2="' + right + '" y2="' + bot + '" stroke="#0f172a" stroke-width="1.4"/>';
svg += '<line x1="' + left + '" y1="' + top + '" x2="' + left + '" y2="' + bot + '" stroke="#0f172a" stroke-width="1.4"/>';
/* Метки осей */
svg += '<text x="' + ((left + right) / 2) + '" y="' + (bot + 26) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" fill="#475569">β = v/c</text>';
svg += '<text x="' + (left - 24) + '" y="' + ((top + bot) / 2) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" fill="#475569" transform="rotate(-90 ' + (left - 24) + ' ' + ((top + bot) / 2) + ')">γ</text>';
/* γ — растёт; ограничим до 6 */
let pts = '';
for (let i = 0; i <= 100; i++){
const b = i / 100 * 0.99;
const g = 1 / Math.sqrt(1 - b * b);
const x = left + b * (right - left);
const y = bot - Math.min(6, g) * (bot - top) / 6;
pts += x.toFixed(1) + ',' + y.toFixed(1) + ' ';
}
svg += '<polyline points="' + pts + '" fill="none" stroke="' + this.color + '" stroke-width="2.4"/>';
/* γ = 1 базовая линия */
svg += '<line x1="' + left + '" y1="' + (bot - (bot - top) / 6) + '" x2="' + right + '" y2="' + (bot - (bot - top) / 6) + '" stroke="#94a3b8" stroke-width="0.8" stroke-dasharray="3 3"/>';
svg += '<text x="' + (left - 6) + '" y="' + (bot - (bot - top) / 6 + 4) + '" text-anchor="end" font-family="JetBrains Mono,monospace" font-size="10" fill="#94a3b8">1</text>';
/* Текущая точка */
const g = 1 / Math.sqrt(1 - this.beta * this.beta);
const cx = left + this.beta * (right - left);
const cy = bot - Math.min(6, g) * (bot - top) / 6;
svg += '<line x1="' + cx + '" y1="' + bot + '" x2="' + cx + '" y2="' + cy.toFixed(1) + '" stroke="#dc2626" stroke-width="1" stroke-dasharray="3 3"/>';
svg += '<circle cx="' + cx + '" cy="' + cy.toFixed(1) + '" r="5" fill="#dc2626"/>';
/* Подпись значения */
const panelX = right + 12;
svg += '<text x="' + panelX + '" y="' + (top + 14) + '" font-family="JetBrains Mono,monospace" font-size="11" fill="#0f172a" font-weight="700">β = ' + this.beta.toFixed(3) + '</text>';
svg += '<text x="' + panelX + '" y="' + (top + 32) + '" font-family="JetBrains Mono,monospace" font-size="11" fill="#2563eb" font-weight="700">γ = ' + g.toFixed(3) + '</text>';
svg += '<text x="' + panelX + '" y="' + (top + 50) + '" font-family="JetBrains Mono,monospace" font-size="10" fill="#475569">τ = γτ₀</text>';
svg += '<text x="' + panelX + '" y="' + (top + 64) + '" font-family="JetBrains Mono,monospace" font-size="10" fill="#475569">L = L₀/γ</text>';
svg += '<text x="' + panelX + '" y="' + (top + 82) + '" font-family="JetBrains Mono,monospace" font-size="10" fill="#475569">τ/τ₀ = ' + g.toFixed(2) + '</text>';
svg += '<text x="' + panelX + '" y="' + (top + 96) + '" font-family="JetBrains Mono,monospace" font-size="10" fill="#475569">L/L₀ = ' + (1 / g).toFixed(2) + '</text>';
svg += '<text x="' + (W/2) + '" y="20" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" fill="#475569" font-weight="700">γ = 1/√(1 - β²)</text>';
svg += '</svg>';
this.el.innerHTML = svg;
}
}
P.GammaPlot = GammaPlot;
/* ============================================================ */
/* TimeDilation — двое часов: «покоящиеся» и «движущиеся» */
/* ============================================================ */
class TimeDilation {
constructor(container, opts){
opts = opts || {};
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
this.W = opts.width || 600;
this.H = opts.height || 240;
this.beta = opts.beta !== undefined ? opts.beta : 0.5;
this.t = 0;
this.paused = false;
this._render();
util.subscribe(this);
util.observe(this);
}
setBeta(v){ this.beta = Math.max(0, Math.min(0.99, v)); }
update(dt){ this.t += dt; }
drawClock(svg, cx, cy, r, time, label, color){
let s = svg;
s += '<circle cx="' + cx + '" cy="' + cy + '" r="' + r + '" fill="#fff" stroke="' + color + '" stroke-width="2.4"/>';
/* Метки часов 12, 3, 6, 9 */
for (let i = 0; i < 12; i++){
const a = i * Math.PI / 6;
const x1 = cx + (r - 6) * Math.sin(a), y1 = cy - (r - 6) * Math.cos(a);
const x2 = cx + (r - 2) * Math.sin(a), y2 = cy - (r - 2) * Math.cos(a);
s += '<line x1="' + x1.toFixed(1) + '" y1="' + y1.toFixed(1) + '" x2="' + x2.toFixed(1) + '" y2="' + y2.toFixed(1) + '" stroke="#475569" stroke-width="' + (i % 3 === 0 ? 2 : 1) + '"/>';
}
/* Стрелка секундная (один оборот = 6 «времени») */
const a = (time / 6) * 2 * Math.PI;
const hx = cx + (r - 12) * Math.sin(a), hy = cy - (r - 12) * Math.cos(a);
s += '<line x1="' + cx + '" y1="' + cy + '" x2="' + hx.toFixed(1) + '" y2="' + hy.toFixed(1) + '" stroke="' + color + '" stroke-width="2.6" stroke-linecap="round"/>';
s += '<circle cx="' + cx + '" cy="' + cy + '" r="3" fill="' + color + '"/>';
s += '<text x="' + cx + '" y="' + (cy + r + 22) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" fill="' + color + '" font-weight="700">' + label + '</text>';
s += '<text x="' + cx + '" y="' + (cy + r + 38) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="10" fill="#475569">t = ' + time.toFixed(2) + ' с</text>';
return s;
}
render(){
if (!this.el) return;
const W = this.W, H = this.H;
const g = 1 / Math.sqrt(1 - this.beta * this.beta);
/* Часы покоя — реальное время */
const t0 = this.t;
/* Движущиеся — идут медленнее в γ раз для наблюдателя */
const tmov = this.t / g;
let svg = util.svgFrame(W, H, {bg:'#f8fafc'});
svg = this.drawClock(svg, 140, 100, 55, t0, 'часы наблюдателя', '#0f172a');
svg = this.drawClock(svg, W - 140, 100, 55, tmov, 'часы в движ. системе', '#dc2626');
/* Стрелка движения */
svg += '<line x1="220" y1="' + (H - 30) + '" x2="' + (W - 220) + '" y2="' + (H - 30) + '" stroke="#94a3b8" stroke-width="1.4"/>';
svg += '<polygon points="' + (W - 220) + ',' + (H - 30) + ' ' + (W - 232) + ',' + (H - 36) + ' ' + (W - 232) + ',' + (H - 24) + '" fill="#94a3b8"/>';
svg += '<text x="' + (W/2) + '" y="' + (H - 36) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" fill="#475569">v → · β = ' + this.beta.toFixed(2) + ' · γ = ' + g.toFixed(2) + '</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;
}
_render(){ this.render(); }
}
P.TimeDilation = TimeDilation;
/* ============================================================ */
/* LengthContraction — стержень в покое и в движении */
/* ============================================================ */
class LengthContraction {
constructor(container, opts){
opts = opts || {};
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
this.W = opts.width || 600;
this.H = opts.height || 220;
this.beta = opts.beta !== undefined ? opts.beta : 0.5;
this.L0 = opts.L0 !== undefined ? opts.L0 : 320;
this.paused = true;
this.render();
}
setBeta(v){ this.beta = Math.max(0, Math.min(0.99, v)); this.render(); }
update(){}
render(){
if (!this.el) return;
const W = this.W, H = this.H;
const g = 1 / Math.sqrt(1 - this.beta * this.beta);
const L = this.L0 / g;
let svg = util.svgFrame(W, H, {bg:'#f8fafc'});
/* Подпись */
svg += '<text x="' + (W/2) + '" y="20" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" fill="#475569" font-weight="700">L = L₀ · √(1 - β²) = L₀/γ</text>';
/* Стержень в покое */
const cx = W / 2;
const y1 = 60;
svg += '<text x="' + (cx - this.L0/2 - 10) + '" y="' + (y1 + 5) + '" text-anchor="end" font-family="JetBrains Mono,monospace" font-size="11" fill="#0f172a" font-weight="700">покой:</text>';
svg += '<rect x="' + (cx - this.L0/2) + '" y="' + (y1 - 12) + '" width="' + this.L0 + '" height="24" fill="#dbeafe" stroke="#0f172a" stroke-width="1.6"/>';
/* Деления */
for (let i = 0; i <= 10; i++){
const x = cx - this.L0/2 + i * this.L0 / 10;
svg += '<line x1="' + x + '" y1="' + (y1 - 12) + '" x2="' + x + '" y2="' + (y1 + 12) + '" stroke="#0f172a" stroke-width="0.8"/>';
}
svg += '<text x="' + cx + '" y="' + (y1 + 32) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" fill="#0f172a">L₀ = ' + this.L0 + ' (собственная длина)</text>';
/* Стержень в движении */
const y2 = 140;
svg += '<text x="' + (cx - L/2 - 10) + '" y="' + (y2 + 5) + '" text-anchor="end" font-family="JetBrains Mono,monospace" font-size="11" fill="#dc2626" font-weight="700">движется:</text>';
svg += '<rect x="' + (cx - L/2) + '" y="' + (y2 - 12) + '" width="' + L.toFixed(1) + '" height="24" fill="#fee2e2" stroke="#dc2626" stroke-width="1.6"/>';
for (let i = 0; i <= 10; i++){
const x = cx - L/2 + i * L / 10;
svg += '<line x1="' + x.toFixed(1) + '" y1="' + (y2 - 12) + '" x2="' + x.toFixed(1) + '" y2="' + (y2 + 12) + '" stroke="#dc2626" stroke-width="0.8"/>';
}
/* Стрелка скорости */
svg += '<line x1="' + (cx + L/2 + 10) + '" y1="' + y2 + '" x2="' + (cx + L/2 + 60) + '" y2="' + y2 + '" stroke="#dc2626" stroke-width="2"/>';
svg += '<polygon points="' + (cx + L/2 + 60) + ',' + y2 + ' ' + (cx + L/2 + 50) + ',' + (y2 - 6) + ' ' + (cx + L/2 + 50) + ',' + (y2 + 6) + '" fill="#dc2626"/>';
svg += '<text x="' + (cx + L/2 + 30) + '" y="' + (y2 - 10) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="10" fill="#dc2626" font-weight="700">v</text>';
svg += '<text x="' + cx + '" y="' + (y2 + 32) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" fill="#dc2626">L = ' + L.toFixed(1) + ' · L/L₀ = ' + (1/g).toFixed(3) + '</text>';
/* Параметры */
svg += '<text x="' + (W - 12) + '" y="' + (H - 10) + '" text-anchor="end" font-family="JetBrains Mono,monospace" font-size="10" fill="#475569">β = ' + this.beta.toFixed(2) + ' · γ = ' + g.toFixed(2) + '</text>';
svg += '</svg>';
this.el.innerHTML = svg;
}
}
P.LengthContraction = LengthContraction;
/* ============================================================ */
/* PhotoeffectLab — катод, свет, цепь, амперметр */
/* ============================================================ */
class PhotoeffectLab {
constructor(container, opts){
opts = opts || {};
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
this.W = opts.width || 580;
this.H = opts.height || 280;
this.nu = opts.nu !== undefined ? opts.nu : 8e14; /* Гц */
this.nu0 = opts.nu0 !== undefined ? opts.nu0 : 5.5e14; /* красная граница */
this.U = opts.U !== undefined ? opts.U : 0; /* В, тормозящее < 0 */
this.phase = 0;
this.paused = false;
util.subscribe(this);
util.observe(this);
this._render();
}
setNu(v){ this.nu = v; }
setNu0(v){ this.nu0 = v; }
setU(v){ this.U = v; }
/* Кинетическая энергия фотоэлектронов: hν - A (А = hν₀) */
ekMax(){
const h = 6.63e-34;
return Math.max(0, h * (this.nu - this.nu0));
}
/* Электроны летят, если их Eк больше тормозящего eU */
flying(){
const e = 1.6e-19;
return this.nu > this.nu0 && this.ekMax() > e * Math.max(0, -this.U);
}
update(dt){ this.phase += dt * 4; }
render(){
if (!this.el) return;
const W = this.W, H = this.H;
let svg = util.svgFrame(W, H, {bg:'#fef3c7'});
/* Лампа-источник */
const lx = 50, ly = 60;
svg += '<circle cx="' + lx + '" cy="' + ly + '" r="22" fill="#fde68a" stroke="#f59e0b" stroke-width="2"/>';
svg += '<text x="' + lx + '" y="' + (ly + 5) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" fill="#92400e" font-weight="700">hν</text>';
/* Луч света — цвет зависит от ν: 4e14 (красн) до 1e15 (фиолет) */
const nuRel = Math.max(0, Math.min(1, (this.nu - 4e14) / 6e14));
const lightColor = `rgb(${Math.round(255*(1-nuRel))},${Math.round(80+120*(1-Math.abs(nuRel-0.5)*2))},${Math.round(255*nuRel)})`;
for (let i = 0; i < 6; i++){
const yo = (this.phase * 30 + i * 22) % 120;
svg += '<line x1="' + (lx + 22) + '" y1="' + (ly - 30 + yo) + '" x2="' + (lx + 100) + '" y2="' + (ly + 50 + yo) + '" stroke="' + lightColor + '" stroke-width="2" opacity="0.7"/>';
}
/* Катод */
const cx = 180, cyy = 140;
svg += '<rect x="' + cx + '" y="' + (cyy - 40) + '" width="14" height="80" fill="#475569" stroke="#0f172a" stroke-width="1.6"/>';
svg += '<text x="' + (cx + 7) + '" y="' + (cyy + 60) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" fill="#0f172a" font-weight="700">катод (-)</text>';
/* Анод */
const ax = 380;
svg += '<rect x="' + ax + '" y="' + (cyy - 40) + '" width="14" height="80" fill="#94a3b8" stroke="#0f172a" stroke-width="1.6"/>';
svg += '<text x="' + (ax + 7) + '" y="' + (cyy + 60) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" fill="#0f172a" font-weight="700">анод (' + (this.U >= 0 ? '+' : '-') + ')</text>';
/* Стеклянный баллон */
svg += '<ellipse cx="' + ((cx + ax + 14) / 2) + '" cy="' + cyy + '" rx="160" ry="70" fill="none" stroke="#94a3b8" stroke-width="1.5" stroke-dasharray="4 3"/>';
/* Электроны летят */
if (this.flying()){
for (let i = 0; i < 5; i++){
const t = ((this.phase * 0.7) + i * 0.2) % 1;
const ex = cx + 14 + t * (ax - cx - 14);
const ey = cyy + Math.sin(t * 6 + i) * 8;
svg += '<circle cx="' + ex.toFixed(1) + '" cy="' + ey.toFixed(1) + '" r="4" fill="#16a34a" stroke="#065f46" stroke-width="1"/>';
svg += '<text x="' + ex.toFixed(1) + '" y="' + (ey + 3).toFixed(1) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="8" fill="#fff">e</text>';
}
}
/* Цепь — снизу */
svg += '<line x1="' + (cx + 7) + '" y1="' + (cyy + 40) + '" x2="' + (cx + 7) + '" y2="' + (H - 30) + '" stroke="#0f172a" stroke-width="1.6"/>';
svg += '<line x1="' + (cx + 7) + '" y1="' + (H - 30) + '" x2="' + (ax + 7) + '" y2="' + (H - 30) + '" stroke="#0f172a" stroke-width="1.6"/>';
svg += '<line x1="' + (ax + 7) + '" y1="' + (cyy + 40) + '" x2="' + (ax + 7) + '" y2="' + (H - 30) + '" stroke="#0f172a" stroke-width="1.6"/>';
/* Амперметр */
const amx = (cx + ax) / 2 + 7, amy = H - 30;
svg += '<circle cx="' + amx + '" cy="' + amy + '" r="14" fill="#fff" stroke="#0f172a" stroke-width="1.6"/>';
svg += '<text x="' + amx + '" y="' + (amy + 4) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" fill="#0f172a" font-weight="700">A</text>';
/* Стрелка тока */
if (this.flying()){
svg += '<text x="' + (amx - 28) + '" y="' + (amy - 18) + '" font-family="JetBrains Mono,monospace" font-size="10" fill="#16a34a" font-weight="700">I &gt; 0</text>';
} else {
svg += '<text x="' + (amx - 28) + '" y="' + (amy - 18) + '" font-family="JetBrains Mono,monospace" font-size="10" fill="#dc2626" font-weight="700">I = 0</text>';
}
/* Подписи */
const h = 6.63e-34, e_ch = 1.6e-19;
const Em = this.ekMax();
const Aevh = h * this.nu0 / e_ch;
svg += '<text x="' + (W - 12) + '" y="22" text-anchor="end" font-family="JetBrains Mono,monospace" font-size="11" fill="#0f172a" font-weight="700">hν = A + Eк</text>';
svg += '<text x="' + (W - 12) + '" y="40" text-anchor="end" font-family="JetBrains Mono,monospace" font-size="10" fill="#475569">ν = ' + (this.nu / 1e14).toFixed(2) + '·10¹⁴ Гц</text>';
svg += '<text x="' + (W - 12) + '" y="56" text-anchor="end" font-family="JetBrains Mono,monospace" font-size="10" fill="#475569">ν₀ = ' + (this.nu0 / 1e14).toFixed(2) + '·10¹⁴ Гц</text>';
svg += '<text x="' + (W - 12) + '" y="72" text-anchor="end" font-family="JetBrains Mono,monospace" font-size="10" fill="#475569">A = ' + Aevh.toFixed(2) + ' эВ</text>';
svg += '<text x="' + (W - 12) + '" y="88" text-anchor="end" font-family="JetBrains Mono,monospace" font-size="10" fill="#475569">Eк,max = ' + (Em/e_ch).toFixed(2) + ' эВ</text>';
svg += '<text x="' + (W - 12) + '" y="104" text-anchor="end" font-family="JetBrains Mono,monospace" font-size="10" fill="#475569">U = ' + this.U.toFixed(2) + ' В</text>';
svg += '</svg>';
this.el.innerHTML = svg;
}
_render(){ this.render(); }
}
P.PhotoeffectLab = PhotoeffectLab;
/* ============================================================ */
/* PlanckLinear — график Eк,max от ν */
/* ============================================================ */
class PlanckLinear {
constructor(container, opts){
opts = opts || {};
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
this.W = opts.width || 560;
this.H = opts.height || 280;
this.nu0 = opts.nu0 !== undefined ? opts.nu0 : 5e14;
this.nu = opts.nu !== undefined ? opts.nu : 8e14;
this.paused = true;
this.render();
}
setNu0(v){ this.nu0 = v; this.render(); }
setNu(v){ this.nu = v; this.render(); }
update(){}
render(){
if (!this.el) return;
const W = this.W, H = this.H;
const pad = 50;
const left = pad, right = W - pad - 80, top = 30, bot = H - 50;
const numax = 12e14;
const h = 6.63e-34, e_ch = 1.6e-19;
const eMaxEV = h * (numax - this.nu0) / e_ch;
let svg = util.svgFrame(W, H, {bg:'#fdf2f8'});
/* Сетка */
svg += '<g stroke="#fbcfe8" stroke-width="0.8">';
for (let i = 0; i <= 12; i++){
const x = left + i * (right - left) / 12;
svg += '<line x1="' + x + '" y1="' + top + '" x2="' + x + '" y2="' + bot + '"/>';
}
for (let i = 0; i <= 6; i++){
const y = top + i * (bot - top) / 6;
svg += '<line x1="' + left + '" y1="' + y + '" x2="' + right + '" y2="' + y + '"/>';
}
svg += '</g>';
/* Оси */
svg += '<line x1="' + left + '" y1="' + bot + '" x2="' + right + '" y2="' + bot + '" stroke="#0f172a" stroke-width="1.4"/>';
svg += '<line x1="' + left + '" y1="' + top + '" x2="' + left + '" y2="' + bot + '" stroke="#0f172a" stroke-width="1.4"/>';
svg += '<text x="' + ((left+right)/2) + '" y="' + (bot + 30) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" fill="#475569">ν (·10¹⁴ Гц)</text>';
svg += '<text x="' + (left - 30) + '" y="' + ((top+bot)/2) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" fill="#475569" transform="rotate(-90 ' + (left - 30) + ' ' + ((top+bot)/2) + ')">Eк,max (эВ)</text>';
/* Прямая Eк = h(ν - ν₀) */
const nu0x = left + (this.nu0 / numax) * (right - left);
let pts = '';
for (let i = 0; i <= 50; i++){
const v = this.nu0 + i * (numax - this.nu0) / 50;
const eV = h * (v - this.nu0) / e_ch;
const x = left + (v / numax) * (right - left);
const y = bot - (eV / Math.max(0.1, eMaxEV)) * (bot - top);
pts += x.toFixed(1) + ',' + y.toFixed(1) + ' ';
}
svg += '<polyline points="' + pts + '" fill="none" stroke="#ec4899" stroke-width="2.4"/>';
/* До ν₀ — горизонтальная линия Eк = 0 */
svg += '<line x1="' + left + '" y1="' + bot + '" x2="' + nu0x + '" y2="' + bot + '" stroke="#ec4899" stroke-width="2.4" stroke-dasharray="4 3"/>';
/* Отметка ν₀ */
svg += '<line x1="' + nu0x + '" y1="' + bot + '" x2="' + nu0x + '" y2="' + (bot + 6) + '" stroke="#dc2626" stroke-width="1.4"/>';
svg += '<text x="' + nu0x + '" y="' + (bot + 18) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="10" fill="#dc2626" font-weight="700">ν₀</text>';
/* Текущая точка ν */
const cx2 = left + (this.nu / numax) * (right - left);
const eVcur = Math.max(0, h * (this.nu - this.nu0) / e_ch);
const cy2 = bot - (eVcur / Math.max(0.1, eMaxEV)) * (bot - top);
svg += '<line x1="' + cx2.toFixed(1) + '" y1="' + bot + '" x2="' + cx2.toFixed(1) + '" y2="' + cy2.toFixed(1) + '" stroke="#0f172a" stroke-width="1" stroke-dasharray="3 3"/>';
svg += '<circle cx="' + cx2.toFixed(1) + '" cy="' + cy2.toFixed(1) + '" r="5" fill="#0f172a"/>';
/* Метки на осях X */
for (let v = 0; v <= 12; v += 2){
const x = left + (v / 12) * (right - left);
svg += '<text x="' + x + '" y="' + (bot + 14) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="9" fill="#64748b">' + v + '</text>';
}
/* Подпись */
svg += '<text x="' + (W/2) + '" y="20" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" fill="#475569" font-weight="700">Eк,max = h(ν − ν₀) — угловой коэф. = h</text>';
/* Панель */
const px = right + 12;
svg += '<text x="' + px + '" y="' + (top + 14) + '" font-family="JetBrains Mono,monospace" font-size="11" fill="#ec4899" font-weight="700">ν = ' + (this.nu/1e14).toFixed(1) + '·10¹⁴</text>';
svg += '<text x="' + px + '" y="' + (top + 32) + '" font-family="JetBrains Mono,monospace" font-size="10" fill="#475569">ν₀ = ' + (this.nu0/1e14).toFixed(1) + '·10¹⁴</text>';
svg += '<text x="' + px + '" y="' + (top + 50) + '" font-family="JetBrains Mono,monospace" font-size="10" fill="#0f172a">Eк = ' + eVcur.toFixed(2) + ' эВ</text>';
svg += '<text x="' + px + '" y="' + (top + 68) + '" font-family="JetBrains Mono,monospace" font-size="10" fill="#475569">A = ' + (h * this.nu0 / e_ch).toFixed(2) + ' эВ</text>';
svg += '</svg>';
this.el.innerHTML = svg;
}
}
P.PlanckLinear = PlanckLinear;
/* ============================================================ */
/* BohrAtom — атом водорода с орбитами и переходом */
/* ============================================================ */
class BohrAtom {
constructor(container, opts){
opts = opts || {};
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
this.W = opts.width || 540;
this.H = opts.height || 380;
this.n_from = opts.n_from !== undefined ? opts.n_from : 3;
this.n_to = opts.n_to !== undefined ? opts.n_to : 2;
this.phase = 0;
this.transitioning = false;
this.t_trans = 0;
this.paused = false;
util.subscribe(this);
util.observe(this);
this._render();
}
setFrom(n){ this.n_from = n; this.startTransition(); }
setTo(n){ this.n_to = n; this.startTransition(); }
startTransition(){ this.transitioning = true; this.t_trans = 0; }
update(dt){
this.phase += dt * 2;
if (this.transitioning){
this.t_trans += dt;
if (this.t_trans > 1.5){ this.transitioning = false; this.t_trans = 0; }
}
}
render(){
if (!this.el) return;
const W = this.W, H = this.H;
const cx = W / 2, cy = H / 2;
let svg = util.svgFrame(W, H, {bg:'#0f172a'});
/* Ядро */
svg += '<circle cx="' + cx + '" cy="' + cy + '" r="14" fill="#dc2626" stroke="#fff" stroke-width="2"/>';
svg += '<text x="' + cx + '" y="' + (cy + 5) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="13" fill="#fff" font-weight="700">+</text>';
/* Орбиты n = 1..5 */
const radii = [30, 55, 85, 120, 160];
for (let n = 0; n < 5; n++){
const r = radii[n];
const active = (n + 1 === this.n_from || n + 1 === this.n_to);
svg += '<circle cx="' + cx + '" cy="' + cy + '" r="' + r + '" fill="none" stroke="' + (active ? '#fcd34d' : '#475569') + '" stroke-width="' + (active ? 1.5 : 0.8) + '" opacity="' + (active ? 0.9 : 0.5) + '"/>';
svg += '<text x="' + (cx + r + 4) + '" y="' + (cy + 4) + '" font-family="JetBrains Mono,monospace" font-size="10" fill="#94a3b8">n=' + (n + 1) + '</text>';
}
/* Электрон на текущей орбите */
let curN;
if (this.transitioning){
const t = Math.min(1, this.t_trans / 0.8);
const r1 = radii[this.n_from - 1] || 30;
const r2 = radii[this.n_to - 1] || 30;
const r = r1 + (r2 - r1) * t;
const a = this.phase;
const ex = cx + r * Math.cos(a), ey = cy + r * Math.sin(a);
svg += '<circle cx="' + ex.toFixed(1) + '" cy="' + ey.toFixed(1) + '" r="6" fill="#06b6d4" stroke="#fff" stroke-width="1.4"/>';
curN = this.n_to;
/* Фотон при переходе на нижний уровень */
if (this.n_from > this.n_to && t > 0.4){
const pt = (t - 0.4) / 0.6;
const fx = ex + 80 * pt;
const fy = ey - 40 * pt;
svg += '<g><path d="M ' + ex.toFixed(1) + ' ' + ey.toFixed(1) + ' Q ' + ((ex + fx) / 2).toFixed(1) + ' ' + ((ey + fy) / 2 - 10).toFixed(1) + ' ' + fx.toFixed(1) + ' ' + fy.toFixed(1) + '" stroke="#facc15" stroke-width="2" fill="none" opacity="' + (1 - pt) + '"/>';
svg += '<text x="' + (fx + 12).toFixed(1) + '" y="' + fy.toFixed(1) + '" font-family="JetBrains Mono,monospace" font-size="11" fill="#facc15" font-weight="700" opacity="' + (1 - pt) + '">hν</text></g>';
}
} else {
const r = radii[this.n_to - 1] || 30;
const a = this.phase;
const ex = cx + r * Math.cos(a), ey = cy + r * Math.sin(a);
svg += '<circle cx="' + ex.toFixed(1) + '" cy="' + ey.toFixed(1) + '" r="6" fill="#06b6d4" stroke="#fff" stroke-width="1.4"/>';
curN = this.n_to;
}
/* Подписи */
const En = -13.6 / (curN * curN);
svg += '<text x="14" y="22" font-family="JetBrains Mono,monospace" font-size="11" fill="#cbd5e1" font-weight="700">Боровская модель атома H</text>';
svg += '<text x="14" y="38" font-family="JetBrains Mono,monospace" font-size="10" fill="#94a3b8">n = ' + curN + ' · E = -13,6/n² = ' + En.toFixed(2) + ' эВ</text>';
if (this.n_from !== this.n_to){
const E_f = -13.6 / (this.n_from * this.n_from);
const E_t = -13.6 / (this.n_to * this.n_to);
const dE = Math.abs(E_t - E_f);
svg += '<text x="14" y="' + (H - 26) + '" font-family="JetBrains Mono,monospace" font-size="11" fill="#fcd34d" font-weight="700">переход ' + this.n_from + ' → ' + this.n_to + '</text>';
svg += '<text x="14" y="' + (H - 12) + '" font-family="JetBrains Mono,monospace" font-size="10" fill="#cbd5e1">hν = |E' + this.n_from + ' E' + this.n_to + '| = ' + dE.toFixed(2) + ' эВ</text>';
}
svg += '</svg>';
this.el.innerHTML = svg;
}
_render(){ this.render(); }
}
P.BohrAtom = BohrAtom;
/* ============================================================ */
/* EnergyLevels — диаграмма E_n + переход */
/* ============================================================ */
class EnergyLevels {
constructor(container, opts){
opts = opts || {};
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
this.W = opts.width || 540;
this.H = opts.height || 360;
this.n_from = opts.n_from !== undefined ? opts.n_from : 4;
this.n_to = opts.n_to !== undefined ? opts.n_to : 2;
this.paused = true;
this.render();
}
setFrom(n){ this.n_from = n; this.render(); }
setTo(n){ this.n_to = n; this.render(); }
update(){}
render(){
if (!this.el) return;
const W = this.W;
/* Внутренняя высота увеличена для читаемости */
const H = Math.max(this.H, 440);
const padTop = 36, padBot = 32;
const bandW = 16, bandLeft = 4;
const leftLine = 170, rightLine = W - 82;
const nMax = 6;
const nFrom = this.n_from, nTo = this.n_to;
function E(n){ return -13.6 / (n * n); }
/* Разрывная шкала:
— верхняя зона (70% высоты): n=2..6 + ионизация, линейная по E
— нижняя зона (30% высоты): только n=1, сжата с маркером разрыва */
const upperH = Math.round((H - padTop - padBot) * 0.72);
const splitY = padTop + upperH; /* граница зон в пикселях */
const y1fixed = H - padBot - 16; /* фиксированная y для n=1 */
const E2 = E(2), maxE = 0.4;
function yE(En){
if (En <= E2 - 0.05){
/* Нижняя зона: линейная E(1)→E(2) → y1fixed→splitY */
const t = (En - E(1)) / (E2 - E(1));
return y1fixed - t * (y1fixed - splitY);
}
/* Верхняя зона: линейная E(2)→maxE → splitY→padTop */
return padTop + (maxE - En) / (maxE - E2) * upperH;
}
/* SVG */
let svg = '<svg width="' + W + '" height="' + H + '" viewBox="0 0 ' + W + ' ' + H + '" xmlns="http://www.w3.org/2000/svg">';
svg += '<rect x="0" y="0" width="' + W + '" height="' + H + '" rx="14" fill="#fff" stroke="#e2e8f0" stroke-width="1.2"/>';
/* Заголовок */
svg += '<text x="' + (W / 2) + '" y="23" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11.5" fill="#64748b" font-weight="600">E&#8345; = 13,6 / n² эВ (атом водорода)</text>';
/* Цветные полосы серий */
const seriesData = [
{ yTop: yE(E2), yBot: y1fixed, fill: '#ede9fe', tc: '#6d28d9', label: 'Лайман' },
{ yTop: yE(E(3)), yBot: yE(E2), fill: '#dbeafe', tc: '#1d4ed8', label: 'Бальмер' },
{ yTop: padTop, yBot: yE(E(3)),fill: '#d1fae5', tc: '#065f46', label: 'Пашен' },
];
for (const b of seriesData){
const bh = b.yBot - b.yTop;
if (bh < 6) continue;
svg += '<rect x="' + bandLeft + '" y="' + b.yTop.toFixed(1) + '" width="' + bandW + '" height="' + bh.toFixed(1) + '" rx="3" fill="' + b.fill + '"/>';
const ym = ((b.yTop + b.yBot) / 2).toFixed(1);
const bx2 = bandLeft + bandW / 2;
svg += '<text x="' + bx2 + '" y="' + ym + '" text-anchor="middle" dominant-baseline="middle" font-family="JetBrains Mono,monospace" font-size="8.5" fill="' + b.tc + '" font-weight="700" transform="rotate(-90,' + bx2 + ',' + ym + ')">' + b.label + '</text>';
}
/* Вертикальная ось — верхняя часть */
const axisX = leftLine - 3;
svg += '<line x1="' + axisX + '" y1="' + padTop + '" x2="' + axisX + '" y2="' + (splitY - 6) + '" stroke="#cbd5e1" stroke-width="1"/>';
/* Ось нижней части */
svg += '<line x1="' + axisX + '" y1="' + (splitY + 22) + '" x2="' + axisX + '" y2="' + (H - padBot) + '" stroke="#cbd5e1" stroke-width="1"/>';
/* Маркер разрыва оси */
const bMid = (splitY + splitY + 22) / 2;
for (const dy of [-5, 5]){
const my = bMid + dy;
svg += '<line x1="' + (axisX - 5) + '" y1="' + (my - 5) + '" x2="' + (axisX + 5) + '" y2="' + (my + 5) + '" stroke="#94a3b8" stroke-width="1.4" stroke-linecap="round"/>';
}
/* Подпись разрыва */
svg += '<text x="' + (axisX + 8) + '" y="' + (bMid + 4) + '" font-family="JetBrains Mono,monospace" font-size="8" fill="#94a3b8">∿ масштаб</text>';
/* Линия ионизации */
const y0 = yE(0);
svg += '<line x1="' + leftLine + '" y1="' + y0.toFixed(1) + '" x2="' + rightLine + '" y2="' + y0.toFixed(1) + '" stroke="#ef4444" stroke-width="1.4" stroke-dasharray="6 3"/>';
svg += '<text x="' + (leftLine + 5) + '" y="' + (y0 - 4).toFixed(1) + '" font-family="JetBrains Mono,monospace" font-size="9" fill="#ef4444" font-weight="600">E = 0 (ионизация)</text>';
/* Уровни n=1..6 */
for (let n = 1; n <= nMax; n++){
const En = E(n);
const yL = yE(En);
const isFrom = (n === nFrom), isTo = (n === nTo);
const active = isFrom || isTo;
const lc = isFrom ? '#4f46e5' : isTo ? '#0284c7' : '#94a3b8';
const sw = active ? 2.4 : 1;
if (active){
svg += '<line x1="' + leftLine + '" y1="' + yL.toFixed(1) + '" x2="' + rightLine + '" y2="' + yL.toFixed(1) + '" stroke="' + lc + '" stroke-width="8" opacity="0.07"/>';
}
svg += '<line x1="' + leftLine + '" y1="' + yL.toFixed(1) + '" x2="' + rightLine + '" y2="' + yL.toFixed(1) + '" stroke="' + lc + '" stroke-width="' + sw + '"/>';
/* Метка n= */
const lx = axisX - 4;
const textC = isFrom ? '#4f46e5' : isTo ? '#0284c7' : '#475569';
if (active){
svg += '<rect x="' + (lx - 24) + '" y="' + (yL - 8).toFixed(1) + '" width="26" height="15" rx="3" fill="' + lc + '" opacity="0.12"/>';
}
svg += '<text x="' + lx + '" y="' + (yL + 4).toFixed(1) + '" text-anchor="end" font-family="JetBrains Mono,monospace" font-size="11" fill="' + textC + '" font-weight="' + (active ? '800' : '500') + '">n=' + n + '</text>';
/* Значение энергии */
const ec = active ? '#334155' : '#94a3b8';
svg += '<text x="' + (rightLine + 5) + '" y="' + (yL + 4).toFixed(1) + '" font-family="JetBrains Mono,monospace" font-size="9.5" fill="' + ec + '" font-weight="' + (active ? '600' : '400') + '">' + En.toFixed(2) + ' эВ</text>';
}
/* Стрелка перехода */
if (nFrom !== nTo){
const Ef = E(nFrom), Et = E(nTo);
const yf = yE(Ef), yt = yE(Et);
const emission = Ef > Et;
const ac = emission ? '#ef4444' : '#16a34a';
const xT = leftLine + (rightLine - leftLine) * 0.42;
svg += '<line x1="' + xT.toFixed(1) + '" y1="' + yf.toFixed(1) + '" x2="' + xT.toFixed(1) + '" y2="' + yt.toFixed(1) + '" stroke="' + ac + '" stroke-width="2.4" stroke-linecap="round"/>';
const dir = yt < yf ? -1 : 1;
svg += '<polygon points="' + xT + ',' + yt.toFixed(1) + ' ' + (xT - 7) + ',' + (yt + 11 * dir).toFixed(1) + ' ' + (xT + 7) + ',' + (yt + 11 * dir).toFixed(1) + '" fill="' + ac + '"/>';
/* Info-box */
const dE = Math.abs(Ef - Et);
const lam = 1240 / dE;
const bxW = 94, bxH = lam >= 380 && lam <= 700 ? 50 : 42;
const yMid = (yf + yt) / 2;
let bxLeft = xT + 12;
if (bxLeft + bxW > W - 4) bxLeft = xT - bxW - 12;
const bf = emission ? '#fff5f5' : '#f0fdf4';
const bs = emission ? '#fca5a5' : '#86efac';
svg += '<rect x="' + bxLeft.toFixed(1) + '" y="' + (yMid - bxH / 2).toFixed(1) + '" width="' + bxW + '" height="' + bxH + '" rx="7" fill="' + bf + '" stroke="' + bs + '" stroke-width="1.2"/>';
svg += '<text x="' + (bxLeft + 8).toFixed(1) + '" y="' + (yMid - bxH / 2 + 16).toFixed(1) + '" font-family="JetBrains Mono,monospace" font-size="11.5" fill="' + ac + '" font-weight="800">hν = ' + dE.toFixed(3) + ' эВ</text>';
svg += '<text x="' + (bxLeft + 8).toFixed(1) + '" y="' + (yMid - bxH / 2 + 31).toFixed(1) + '" font-family="JetBrains Mono,monospace" font-size="11" fill="' + ac + '">λ ≈ ' + lam.toFixed(0) + ' нм</text>';
if (lam >= 380 && lam <= 700){
const hue = Math.round(270 - (lam - 380) / 320 * 270);
const cy = yMid - bxH / 2 + 40;
svg += '<defs><linearGradient id="vg"><stop offset="0%" stop-color="hsl(' + hue + ',88%,54%)"/><stop offset="100%" stop-color="hsl(' + (hue - 18) + ',88%,58%)"/></linearGradient></defs>';
svg += '<rect x="' + (bxLeft + 8).toFixed(1) + '" y="' + cy.toFixed(1) + '" width="' + (bxW - 16) + '" height="6" rx="3" fill="url(#vg)"/>';
}
}
svg += '</svg>';
this.el.innerHTML = svg;
}
}
P.EnergyLevels = EnergyLevels;
/* ============================================================ */
/* RadioactiveDecay — N(t) = N0 * 2^(-t/T) */
/* ============================================================ */
class RadioactiveDecay {
constructor(container, opts){
opts = opts || {};
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
this.W = opts.width || 600;
this.H = opts.height || 300;
this.T = opts.T !== undefined ? opts.T : 2.0; /* период полураспада */
this.tMax = opts.tMax !== undefined ? opts.tMax : 10;
this.t = 0;
this.paused = false;
util.subscribe(this);
util.observe(this);
this._render();
}
setT(v){ this.T = Math.max(0.2, v); this.t = 0; }
reset(){ this.t = 0; }
update(dt){ this.t = (this.t + dt * 0.5) % this.tMax; }
render(){
if (!this.el) return;
const W = this.W, H = this.H;
const pad = 40, left = pad, right = W - pad - 100, top = 30, bot = H - 40;
let svg = util.svgFrame(W, H, {bg:'#fef9c3'});
/* Сетка */
svg += '<g stroke="#facc15" stroke-width="0.6" opacity="0.5">';
for (let i = 0; i <= 10; i++){
const x = left + i * (right - left) / 10;
svg += '<line x1="' + x + '" y1="' + top + '" x2="' + x + '" y2="' + bot + '"/>';
}
for (let i = 0; i <= 4; i++){
const y = top + i * (bot - top) / 4;
svg += '<line x1="' + left + '" y1="' + y + '" x2="' + right + '" y2="' + y + '"/>';
}
svg += '</g>';
svg += '<line x1="' + left + '" y1="' + bot + '" x2="' + right + '" y2="' + bot + '" stroke="#0f172a" stroke-width="1.4"/>';
svg += '<line x1="' + left + '" y1="' + top + '" x2="' + left + '" y2="' + bot + '" stroke="#0f172a" stroke-width="1.4"/>';
/* Кривая */
let pts = '';
for (let i = 0; i <= 100; i++){
const tau = i * this.tMax / 100;
const N = Math.pow(2, -tau / this.T);
const x = left + tau / this.tMax * (right - left);
const y = bot - N * (bot - top);
pts += x.toFixed(1) + ',' + y.toFixed(1) + ' ';
}
svg += '<polyline points="' + pts + '" fill="none" stroke="#dc2626" stroke-width="2.6"/>';
/* Точки полураспадов: 1T, 2T, 3T... */
for (let k = 1; k * this.T < this.tMax; k++){
const tau = k * this.T;
const N = Math.pow(2, -k);
const x = left + tau / this.tMax * (right - left);
const y = bot - N * (bot - top);
svg += '<circle cx="' + x.toFixed(1) + '" cy="' + y.toFixed(1) + '" r="3.5" fill="#0f172a"/>';
svg += '<line x1="' + x.toFixed(1) + '" y1="' + bot + '" x2="' + x.toFixed(1) + '" y2="' + y.toFixed(1) + '" stroke="#0f172a" stroke-width="0.8" stroke-dasharray="2 2"/>';
svg += '<text x="' + x.toFixed(1) + '" y="' + (bot + 12) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="9" fill="#475569">' + k + 'T</text>';
}
/* Текущая точка t */
const Nt = Math.pow(2, -this.t / this.T);
const cx = left + this.t / this.tMax * (right - left);
const cy = bot - Nt * (bot - top);
svg += '<circle cx="' + cx.toFixed(1) + '" cy="' + cy.toFixed(1) + '" r="6" fill="#facc15" stroke="#92400e" stroke-width="1.8"/>';
/* Подписи */
svg += '<text x="' + (left - 4) + '" y="' + top + '" text-anchor="end" font-family="JetBrains Mono,monospace" font-size="10" fill="#475569">N₀</text>';
svg += '<text x="' + (left - 4) + '" y="' + (top + (bot - top) / 2 + 4) + '" text-anchor="end" font-family="JetBrains Mono,monospace" font-size="10" fill="#475569">N₀/2</text>';
/* Панель справа */
const px = right + 12;
svg += '<text x="' + px + '" y="' + (top + 14) + '" font-family="JetBrains Mono,monospace" font-size="11" fill="#dc2626" font-weight="700">N(t) = N₀ · 2^(-t/T)</text>';
svg += '<text x="' + px + '" y="' + (top + 34) + '" font-family="JetBrains Mono,monospace" font-size="11" fill="#0f172a">T = ' + this.T.toFixed(2) + '</text>';
svg += '<text x="' + px + '" y="' + (top + 50) + '" font-family="JetBrains Mono,monospace" font-size="11" fill="#0f172a">t = ' + this.t.toFixed(2) + '</text>';
svg += '<text x="' + px + '" y="' + (top + 66) + '" font-family="JetBrains Mono,monospace" font-size="11" fill="#92400e" font-weight="700">N/N₀ = ' + Nt.toFixed(3) + '</text>';
svg += '</svg>';
this.el.innerHTML = svg;
}
_render(){ this.render(); }
}
P.RadioactiveDecay = RadioactiveDecay;
})();