/* 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 '';
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 = ''
+ ''
+ ''
+ '+A'
+ ''
+ '0'
+ ''
+ '-A'
+ '';
/* Период справа сверху */
const Tlabel = 'T = ' + T.toFixed(2) + ' с';
const svg = util.svgFrame(W, H, {bg:'#f8fafc'})
+ ''
+ ''
+ ''
+ ''
+ ''
+ 'm'
+ ruler + Tlabel
+ '';
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 = '';
/* Вертикальная пунктирная ось */
const vert = '';
/* Подвес */
const string = '';
const bob = '';
/* Период */
const Tlabel = 'T = ' + T.toFixed(2) + ' с';
/* Подвес-крепление */
const hook = '';
const svg = util.svgFrame(W, H, {bg:'#f8fafc'}) + hook + vert + arc + string + bob + Tlabel + '';
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 '';
}
const pK = path.call(this, 1, '#dc2626');
const pP = path.call(this, 2, '#16a34a');
/* W_мех = const = 1 (горизонтальная линия) */
const pM = '';
/* Легенда */
const legend = ''
+ ''
+ ''
+ 'W кинет.'
+ ''
+ 'W потенц.'
+ ''
+ 'W мех = const'
+ '';
const svg = util.svgFrame(W, H) + ax.svg + pM + pK + pP + legend + '';
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 += '';
for (let i = 0; i <= 4; i++){
const w = wMin + (wMax - wMin) * i / 4;
svg += '';
}
for (let i = 0; i <= 4; i++){
svg += '';
}
svg += '';
/* Оси */
svg += '';
svg += '';
/* Подписи осей */
svg += 'ω';
svg += 'A';
/* Линия ω₀ — собственная частота */
svg += '';
svg += 'ω₀';
/* Кривая 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 += '';
/* Точка-маркер на текущей ω */
const aCur = Math.min(amp(this.omegaCur, this.gamma, this.omega0), ampMax);
svg += '';
svg += '';
/* Подпись γ */
svg += 'γ = ' + this.gamma.toFixed(2) + '';
svg += 'ω = ' + this.omegaCur.toFixed(2) + ' · A = ' + aCur.toFixed(2) + '';
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 += '';
/* Кривая */
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 += '';
/* Красный маркер — колеблющаяся точка */
const mPx = this.markerX * W;
const mX = mPx / 120;
const mY = yCenter - amp * Math.sin(k * mX - omega * this.t);
svg += '';
svg += '';
/* Метка λ — горизонтальная скобка над волной */
const lambdaPx = 120 * this.lambda;
if (lambdaPx < W - 60){
const lxStart = 20, lxEnd = lxStart + lambdaPx;
svg += '';
svg += '';
svg += '';
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 += '';
}
svg += dots;
/* Подписи зон сжатия / разрежения */
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 += '';
svg += '';
/* Конденсатор: две параллельные пластины */
const plateW = 36;
svg += '';
svg += '';
svg += '';
svg += '';
/* Заряды на пластинах */
const sign = q > 0 ? 1 : -1;
const topCh = sign > 0 ? '+' : '−';
const botCh = sign > 0 ? '−' : '+';
const qAbsNorm = Math.abs(q) / this.Q0;
svg += '' + topCh + '';
svg += '' + botCh + '';
svg += 'C';
/* Индуктор: петли */
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 += '';
svg += 'L';
/* Стрелка тока */
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 += '';
svg += '';
svg += 'i';
}
/* Энергетические столбцы */
const eY = H - 36, eH = 24;
svg += '';
svg += '';
svg += 'W_C';
svg += '';
svg += '';
svg += 'W_L';
/* Подпись периода */
const Tdisp = T < 1e-3 ? (T * 1e6).toFixed(1) + ' мкс' : (T * 1e3).toFixed(2) + ' мс';
svg += 'T = 2π√(LC) = ' + Tdisp + '';
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 += '';
for (let i = 0; i < 5; i++){
const x = 20 + i * 40;
svg += '';
}
svg += '';
svg += '';
svg += 'B';
/* Вращающаяся рамка: эллипс, отображающий проекцию прямоугольника */
const fx = 110, fy = H / 2;
const phi = this.omega * this.t;
const rx = 40 * Math.abs(Math.cos(phi));
const ry = 30;
svg += '';
/* Ось вращения */
svg += '';
/* Контакт скользящих колец (схема) */
svg += '';
svg += '';
/* График 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 += '' + ax.svg + '';
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 += '';
}
/* Подпись */
svg += 'U(t) = U₀ sin(ωt)';
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 += '';
svg += '';
/* Первичная обмотка слева */
const coil1X = coreL + 24;
for (let i = 0; i < 6; i++){
const y = cy - 50 + i * 16;
svg += '';
}
/* Вторичная обмотка справа */
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 += '';
}
/* Провода-выходы первичной */
svg += '';
svg += '';
/* Провода вторичной */
svg += '';
svg += '';
/* Подписи N1, N2, U1, U2 */
svg += 'N₁ = ' + this.N1 + '';
svg += 'N₂ = ' + this.N2 + '';
svg += 'N₂ = ' + this.N2 + '';
svg += 'U₁ = ' + this.U1.toFixed(0) + ' В';
svg += 'U₂ = ' + U2.toFixed(1) + ' В';
/* Подпись коэф. */
svg += 'k = N₁/N₂ = ' + k.toFixed(2) + '';
svg += '' + (k > 1 ? 'понижающий' : 'повышающий') + '';
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 += '';
svg += 'laser';
/* Щели */
const slitX = 130;
const dPx = Math.min(60, this.d * 60);
const s1y = H/2 - dPx/2, s2y = H/2 + dPx/2;
svg += '';
svg += '';
svg += '';
/* Лучи от лазера к щелям */
svg += '';
svg += '';
/* Экран */
const screenX = W - 50;
svg += '';
/* Интерференционная картина: интенсивность ~ 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 += '';
}
/* Подпись формулы */
svg += 'd·sin φ = k·λ (max), d=' + this.d.toFixed(2) + ', λ=' + this.lambda.toFixed(3) + '';
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 += '';
/* «штрихи» решётки */
for (let i = 0; i < 10; i++){
const y = 40 + i * (H - 80) / 10;
svg += '';
}
svg += 'решётка';
/* Падающий луч */
svg += '';
/* Линии порядков 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 += '';
svg += 'k=' + k + '';
}
/* Подпись формулы и параметров */
svg += 'd sin φ = kλ · d=' + (this.d * 1e6).toFixed(2) + ' мкм · λ=' + (this.lambda * 1e9).toFixed(0) + ' нм';
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 += '';
for (let i = 0; i < 12; i++){
const y = 30 + i * (H - 60) / 12;
svg += '';
}
svg += 'зеркало';
/* Объект — стрелка */
const objX = mirrorX - this.objX;
const objBaseY = cy;
const objTopY = cy - this.objH;
svg += '';
svg += '';
svg += 'объект';
/* Мнимое изображение справа от зеркала, симметрично */
const imgX = mirrorX + this.objX;
svg += '';
svg += '';
svg += 'изображение';
/* Лучи: 1) от верха объекта горизонтально на зеркало, отражается под тем же углом
2) от верха объекта в зеркало по диагонали, отражается симметрично */
/* Луч 1 */
svg += '';
svg += '';
/* Продолжение в зазеркалье (пунктир) */
svg += '';
/* Луч 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 += '';
svg += '';
/* Продолжение от точки отражения в зазеркалье */
svg += '';
/* Глаз */
svg += '';
svg += '';
svg += 'наблюдатель';
/* Подпись закона */
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 += '';
/* Зеркало (дуга) */
const R = 2 * Math.abs(F);
if (this.mode === 'concave'){
svg += '';
} else {
svg += '';
}
/* Штриховка зеркала */
for (let i = 0; i < 8; i++){
const y = cy - 90 + i * 22;
const dx = this.mode === 'concave' ? 8 : -8;
svg += '';
}
/* Точки F и C */
svg += '';
svg += 'F';
if (this.mode === 'concave'){
svg += '';
svg += '2F';
}
/* Объект — красная стрелка вверх */
const objTopY = cy - this.objH;
svg += '';
svg += '';
/* Лучи (3 канонических) */
/* Луч 1: параллельный оптической оси → отражается через F */
svg += '';
if (this.mode === 'concave'){
svg += '';
/* Продолжение до изображения */
const slope1 = (cy - objTopY) / (focusX - mirrorX);
const x2 = imgX, y2 = cy + slope1 * (x2 - focusX);
svg += '';
} else {
/* Выпуклое: отражается так, словно вышел из мнимого F справа */
const slope = (objTopY - cy) / (mirrorX - focusX);
svg += '';
svg += '';
}
/* Луч 2: через F → отражается параллельно оси (только вогнутое) */
if (this.mode === 'concave' && objX !== focusX){
const slope2 = (cy - objTopY) / (focusX - objX);
const hitY = objTopY + slope2 * (mirrorX - objX);
svg += '';
svg += '';
}
/* Изображение */
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 += '';
svg += '';
}
/* Подпись параметров */
const Glabel = isFinite(G) ? G.toFixed(2) : '—';
svg += '1/d + 1/f = 1/F · Γ = -f/d = ' + Glabel + '';
svg += '' + (this.mode==='concave'?'вогнутое':'выпуклое') + ' · F=' + Math.abs(F).toFixed(0) + 'px · d=' + d.toFixed(0) + 'px';
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 += '';
/* Среда 2 (низ) */
svg += '';
/* Граница */
svg += '';
/* Нормаль (пунктир) */
svg += '';
svg += 'нормаль';
/* Падающий луч (из верхней-левой области) */
const L = 130;
const ix = cx - L * Math.sin(a), iy = cy - L * Math.cos(a);
svg += '';
/* Стрелка падающего */
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 += '';
/* Дуга угла падения */
svg += '';
svg += 'α';
/* Отражённый луч */
const rx = cx + L * Math.sin(a), ry = cy - L * Math.cos(a);
svg += '';
svg += 'отраж.';
/* Преломлённый луч или полное отражение */
if (totalInternal){
svg += 'полное внутреннее отражение';
} else {
const tx = cx + L * Math.sin(b), ty = cy + L * Math.cos(b);
svg += '';
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 += '';
svg += '';
svg += 'β';
}
/* Подписи сред */
svg += 'n₁ = ' + this.n1.toFixed(2) + '';
svg += 'n₂ = ' + this.n2.toFixed(2) + '';
/* Формула + углы */
const bDeg = totalInternal ? '—' : (b * 180 / Math.PI).toFixed(1);
svg += 'n₁ sin α = n₂ sin β';
svg += 'α=' + this.alpha.toFixed(0) + '° · β=' + bDeg + '°' + (critDeg!==null?' · αкр=' + critDeg.toFixed(1) + '°':'') + '';
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 += '';
/* Падающий белый луч на грань 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 += '';
svg += 'белый';
/* 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 += '';
/* Преломление при выходе: угол к нормали 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 += '';
}
/* Подпись */
svg += 'дисперсия: n(λ) растёт при уменьшении λ → фиолетовый отклоняется сильнее красного';
svg += 'α = ' + this.alpha.toFixed(0) + '° · стекло (модель Коши)';
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 += '';
/* Линза */
if (this.mode === 'converging'){
svg += '';
svg += '';
svg += '';
} else {
svg += '';
svg += '';
svg += '';
}
/* Фокусы F и 2F */
const Fleft = lensX - this.F, Fright = lensX + this.F;
svg += 'F';
svg += 'F';
if (this.mode === 'converging'){
const F2L = lensX - 2*this.F, F2R = lensX + 2*this.F;
if (F2L > 20) svg += '2F';
if (F2R < W - 10) svg += '2F';
}
/* Объект */
const objX = lensX - d;
const objTopY = cy - this.objH;
svg += '';
svg += '';
/* Лучи */
/* Луч 1: параллельный → через F справа (для собирающей); для рассеивающей — как-будто из F слева */
svg += '';
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 += '';
} else {
/* Рассеивающая: продолжение в сторону F слева */
const slope = (objTopY - cy) / (lensX - Fleft);
svg += '';
svg += '';
}
/* Луч 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 += '';
/* Изображение */
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 += '';
svg += '';
}
}
/* Подписи */
const Glabel = isFinite(G) ? G.toFixed(2) : '—';
const fLabel = (Math.abs(f) < 1e7) ? f.toFixed(0) : '∞';
svg += '1/d + 1/f = 1/F · Γ = -f/d = ' + Glabel + ' · f = ' + fLabel + 'px';
svg += '' + (this.mode==='converging'?'собирающая':'рассеивающая') + ' линза · F=' + this.F + 'px · d=' + d + 'px';
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 += '';
s += '';
s += '';
s += '' + label + '';
s += '';
s += '';
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 += '';
/* Линзы */
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 += '';
/* После объектива собирается в фокальной плоскости (x1 + F1) */
const focusX = x1 + this.F1;
const focusY = cy; /* для параллельного пучка вдоль оси — на оси */
svg += '';
/* От фокуса (F2 справа от окуляра у телескопа: совмещён с F1 объектива) дальше параллельно */
/* Лучи проходят через окуляр */
const yAtEye = focusY + (cy + i * 12 - focusY); /* выходные углы немного шире */
svg += '';
/* После окуляра — параллельно (изображение в бесконечности) */
svg += '';
}
svg += 'пучок от удалённого объекта';
svg += 'в глаз наблюдателя';
svg += 'телескоп Кеплера · Γ = F₁/F₂ = ' + (this.F1/this.F2).toFixed(2) + '';
} 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 += '';
svg += '';
/* Промежуточное изображение */
if (img1X > x1 && img1X < x2){
svg += '';
svg += '';
svg += 'A₁B₁';
}
/* Лучи от верха объекта через объектив (2 канонических) */
svg += '';
svg += '';
/* Через оптический центр объектива */
svg += '';
/* Окуляр работает как лупа — даёт мнимое увеличенное (за окуляром слева) */
/* Показ лучей от верха промежуточного изображения через окуляр параллельно (в глаз) */
svg += '';
svg += '';
svg += 'в глаз';
const Gtotal = G1 * (25 / this.F2); /* приближённо: |Γ_мкс| ≈ |Γ_объ| · 25 см/F_ок */
svg += 'микроскоп · Γ ≈ Γ_объ · 25 см / F_ок ≈ ' + Gtotal.toFixed(1) + '';
}
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 += '';
for (let i = 0; i <= 10; i++){
const x = left + i * (right - left) / 10;
svg += '';
}
for (let i = 0; i <= 6; i++){
const y = top + i * (bot - top) / 6;
svg += '';
}
svg += '';
/* Оси */
svg += '';
svg += '';
/* Метки осей */
svg += 'β = v/c';
svg += 'γ';
/* γ — растёт; ограничим до 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 += '';
/* γ = 1 базовая линия */
svg += '';
svg += '1';
/* Текущая точка */
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 += '';
svg += '';
/* Подпись значения */
const panelX = right + 12;
svg += 'β = ' + this.beta.toFixed(3) + '';
svg += 'γ = ' + g.toFixed(3) + '';
svg += 'τ = γτ₀';
svg += 'L = L₀/γ';
svg += 'τ/τ₀ = ' + g.toFixed(2) + '';
svg += 'L/L₀ = ' + (1 / g).toFixed(2) + '';
svg += 'γ = 1/√(1 - β²)';
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 += '';
/* Метки часов 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 += '';
}
/* Стрелка секундная (один оборот = 6 «времени») */
const a = (time / 6) * 2 * Math.PI;
const hx = cx + (r - 12) * Math.sin(a), hy = cy - (r - 12) * Math.cos(a);
s += '';
s += '';
s += '' + label + '';
s += 't = ' + time.toFixed(2) + ' с';
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 += '';
svg += '';
svg += 'v → · β = ' + this.beta.toFixed(2) + ' · γ = ' + g.toFixed(2) + '';
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 += 'L = L₀ · √(1 - β²) = L₀/γ';
/* Стержень в покое */
const cx = W / 2;
const y1 = 60;
svg += 'покой:';
svg += '';
/* Деления */
for (let i = 0; i <= 10; i++){
const x = cx - this.L0/2 + i * this.L0 / 10;
svg += '';
}
svg += 'L₀ = ' + this.L0 + ' (собственная длина)';
/* Стержень в движении */
const y2 = 140;
svg += 'движется:';
svg += '';
for (let i = 0; i <= 10; i++){
const x = cx - L/2 + i * L / 10;
svg += '';
}
/* Стрелка скорости */
svg += '';
svg += '';
svg += 'v';
svg += 'L = ' + L.toFixed(1) + ' · L/L₀ = ' + (1/g).toFixed(3) + '';
/* Параметры */
svg += 'β = ' + this.beta.toFixed(2) + ' · γ = ' + g.toFixed(2) + '';
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 += '';
svg += 'hν';
/* Луч света — цвет зависит от ν: 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 += '';
}
/* Катод */
const cx = 180, cyy = 140;
svg += '';
svg += 'катод (-)';
/* Анод */
const ax = 380;
svg += '';
svg += 'анод (' + (this.U >= 0 ? '+' : '-') + ')';
/* Стеклянный баллон */
svg += '';
/* Электроны летят */
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 += '';
svg += 'e';
}
}
/* Цепь — снизу */
svg += '';
svg += '';
svg += '';
/* Амперметр */
const amx = (cx + ax) / 2 + 7, amy = H - 30;
svg += '';
svg += 'A';
/* Стрелка тока */
if (this.flying()){
svg += 'I > 0';
} else {
svg += 'I = 0';
}
/* Подписи */
const h = 6.63e-34, e_ch = 1.6e-19;
const Em = this.ekMax();
const Aevh = h * this.nu0 / e_ch;
svg += 'hν = A + Eк';
svg += 'ν = ' + (this.nu / 1e14).toFixed(2) + '·10¹⁴ Гц';
svg += 'ν₀ = ' + (this.nu0 / 1e14).toFixed(2) + '·10¹⁴ Гц';
svg += 'A = ' + Aevh.toFixed(2) + ' эВ';
svg += 'Eк,max = ' + (Em/e_ch).toFixed(2) + ' эВ';
svg += 'U = ' + this.U.toFixed(2) + ' В';
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 += '';
for (let i = 0; i <= 12; i++){
const x = left + i * (right - left) / 12;
svg += '';
}
for (let i = 0; i <= 6; i++){
const y = top + i * (bot - top) / 6;
svg += '';
}
svg += '';
/* Оси */
svg += '';
svg += '';
svg += 'ν (·10¹⁴ Гц)';
svg += 'Eк,max (эВ)';
/* Прямая 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 += '';
/* До ν₀ — горизонтальная линия Eк = 0 */
svg += '';
/* Отметка ν₀ */
svg += '';
svg += 'ν₀';
/* Текущая точка ν */
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 += '';
svg += '';
/* Метки на осях X */
for (let v = 0; v <= 12; v += 2){
const x = left + (v / 12) * (right - left);
svg += '' + v + '';
}
/* Подпись */
svg += 'Eк,max = h(ν − ν₀) — угловой коэф. = h';
/* Панель */
const px = right + 12;
svg += 'ν = ' + (this.nu/1e14).toFixed(1) + '·10¹⁴';
svg += 'ν₀ = ' + (this.nu0/1e14).toFixed(1) + '·10¹⁴';
svg += 'Eк = ' + eVcur.toFixed(2) + ' эВ';
svg += 'A = ' + (h * this.nu0 / e_ch).toFixed(2) + ' эВ';
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 += '';
svg += '+';
/* Орбиты 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 += '';
svg += 'n=' + (n + 1) + '';
}
/* Электрон на текущей орбите */
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 += '';
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 += '';
svg += 'hν';
}
} 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 += '';
curN = this.n_to;
}
/* Подписи */
const En = -13.6 / (curN * curN);
svg += 'Боровская модель атома H';
svg += 'n = ' + curN + ' · E = -13,6/n² = ' + En.toFixed(2) + ' эВ';
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 += 'переход ' + this.n_from + ' → ' + this.n_to + '';
svg += 'hν = |E' + this.n_from + ' − E' + this.n_to + '| = ' + dE.toFixed(2) + ' эВ';
}
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 = '';
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 += '';
for (let i = 0; i <= 10; i++){
const x = left + i * (right - left) / 10;
svg += '';
}
for (let i = 0; i <= 4; i++){
const y = top + i * (bot - top) / 4;
svg += '';
}
svg += '';
svg += '';
svg += '';
/* Кривая */
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 += '';
/* Точки полураспадов: 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 += '';
svg += '';
svg += '' + k + 'T';
}
/* Текущая точка 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 += '';
/* Подписи */
svg += 'N₀';
svg += 'N₀/2';
/* Панель справа */
const px = right + 12;
svg += 'N(t) = N₀ · 2^(-t/T)';
svg += 'T = ' + this.T.toFixed(2) + '';
svg += 't = ' + this.t.toFixed(2) + '';
svg += 'N/N₀ = ' + Nt.toFixed(3) + '';
svg += '';
this.el.innerHTML = svg;
}
_render(){ this.render(); }
}
P.RadioactiveDecay = RadioactiveDecay;
})();