/* 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 ''; }, /* Двухмерные оси 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 = ''; /* Вертикальные линии каждую секунду */ for (let s = 0; s <= tMax; s++) svg += ''; /* Горизонтальные линии */ const yStep = (yRange[1] - yRange[0]) / 4; for (let i = 0; i <= 4; i++){ const y = yRange[0] + i * yStep; svg += ''; } svg += ''; /* Ось t */ svg += ''; /* Ось y */ svg += ''; 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 = '' + opts.label + '' + '' + '' + opts.value + (opts.unit || '') + '' + ''; 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 = ''; } /* Подпись y(t) */ const titleSvg = '' + this.label + ''; const svg = util.svgFrame(W, H) + ax.svg + polyline + titleSvg + ''; 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 += ''; /* Луч света — цвет зависит от ν: 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 += ''; } } 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 = ''; svg += ''; /* Заголовок */ svg += 'Eₙ = −13,6 / n² эВ (атом водорода)'; /* Цветные полосы серий */ 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 += ''; const ym = ((b.yTop + b.yBot) / 2).toFixed(1); const bx2 = bandLeft + bandW / 2; svg += '' + b.label + ''; } /* Вертикальная ось — верхняя часть */ const axisX = leftLine - 3; svg += ''; /* Ось нижней части */ svg += ''; /* Маркер разрыва оси */ const bMid = (splitY + splitY + 22) / 2; for (const dy of [-5, 5]){ const my = bMid + dy; svg += ''; } /* Подпись разрыва */ svg += '∿ масштаб'; /* Линия ионизации */ const y0 = yE(0); svg += ''; svg += 'E = 0 (ионизация)'; /* Уровни 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 += ''; } svg += ''; /* Метка n= */ const lx = axisX - 4; const textC = isFrom ? '#4f46e5' : isTo ? '#0284c7' : '#475569'; if (active){ svg += ''; } svg += 'n=' + n + ''; /* Значение энергии */ const ec = active ? '#334155' : '#94a3b8'; svg += '' + En.toFixed(2) + ' эВ'; } /* Стрелка перехода */ 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 += ''; const dir = yt < yf ? -1 : 1; svg += ''; /* 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 += ''; svg += 'hν = ' + dE.toFixed(3) + ' эВ'; svg += 'λ ≈ ' + lam.toFixed(0) + ' нм'; if (lam >= 380 && lam <= 700){ const hue = Math.round(270 - (lam - 380) / 320 * 270); const cy = yMid - bxH / 2 + 40; svg += ''; 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 += ''; 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; })();