e4801dcc2f
- phys-fx.js: PHYS.TwoSlit (опыт Юнга), PHYS.DiffractionGrating (с радужным спектром), PHYS.FlatMirror - ch3 §14: Электромагнитная природа света + скорость света - ch3 §15: Интерференция (опыт Юнга) - ch3 §16: Дифракция света + дифракционная решётка - ch3 §17: Отражение света + плоское зеркало - 8 квизов (I1_CALC/NAT, I2_CALC/TH, I3_CALC/TH, I4_CALC/IMG) - 4 босса (b1-b4) для §14-§17 - §18-§23 + Final — заглушки для W6/W7
1037 lines
56 KiB
JavaScript
1037 lines
56 KiB
JavaScript
/* phys-fx.js — библиотека анимированных физических симуляций для Физики 11.
|
||
*
|
||
* Архитектура:
|
||
* - Один глобальный requestAnimationFrame-цикл (Ticker).
|
||
* - Каждая симуляция — класс с методами update(dt, t), render().
|
||
* - IntersectionObserver: симуляция приостанавливается, когда уходит из viewport.
|
||
* - Чистый SVG (без Canvas, без WebGL, без зависимостей).
|
||
*
|
||
* Публичный API: window.PHYS = { util, Oscillogram, SpringMass, Pendulum, ... }.
|
||
*
|
||
* W0 — базовая инфраструктура + 3 компонента (Oscillogram, SpringMass, Pendulum).
|
||
* Расширяется в W3 (электротехника), W5-W7 (оптика), W9-W14 (кванты, ядро).
|
||
*/
|
||
(function(){
|
||
'use strict';
|
||
|
||
if (window.PHYS && window.PHYS.__installed) return;
|
||
const P = window.PHYS = window.PHYS || {};
|
||
P.__installed = true;
|
||
|
||
/* ============================================================ */
|
||
/* ГЛОБАЛЬНЫЙ ТАЙМЕР (один RAF на всю страницу) */
|
||
/* ============================================================ */
|
||
const Ticker = {
|
||
t: 0,
|
||
last: 0,
|
||
subs: new Set(),
|
||
running: false
|
||
};
|
||
function tick(ts){
|
||
if (!Ticker.running) return;
|
||
if (!Ticker.last) Ticker.last = ts;
|
||
const dt = Math.min((ts - Ticker.last) / 1000, 0.1); // защита от лагов
|
||
Ticker.last = ts;
|
||
Ticker.t += dt;
|
||
Ticker.subs.forEach(s => {
|
||
if (!s.paused) {
|
||
try { s.update(dt, Ticker.t); s.render && s.render(); }
|
||
catch(e) {}
|
||
}
|
||
});
|
||
requestAnimationFrame(tick);
|
||
}
|
||
function startTicker(){
|
||
if (Ticker.running) return;
|
||
Ticker.running = true;
|
||
Ticker.last = 0;
|
||
requestAnimationFrame(tick);
|
||
}
|
||
function stopTicker(){ Ticker.running = false; }
|
||
|
||
/* ============================================================ */
|
||
/* УТИЛИТЫ */
|
||
/* ============================================================ */
|
||
|
||
const util = P.util = {
|
||
subscribe(sim){ Ticker.subs.add(sim); startTicker(); },
|
||
unsubscribe(sim){ Ticker.subs.delete(sim); if (Ticker.subs.size === 0) stopTicker(); },
|
||
|
||
/* Создаёт IntersectionObserver, который ставит/снимает sim.paused */
|
||
observe(sim){
|
||
if (!sim.el || !window.IntersectionObserver) return;
|
||
const io = new IntersectionObserver(entries => {
|
||
entries.forEach(e => { sim.paused = !e.isIntersecting; });
|
||
}, { threshold: 0.05 });
|
||
io.observe(sim.el);
|
||
sim._io = io;
|
||
},
|
||
|
||
/* Безопасное удаление симуляции */
|
||
destroy(sim){
|
||
util.unsubscribe(sim);
|
||
if (sim._io) { try { sim._io.disconnect(); } catch(e){} sim._io = null; }
|
||
if (sim.el) sim.el.innerHTML = '';
|
||
},
|
||
|
||
/* Хелпер: создать SVG-обёртку с осями для графика */
|
||
svgFrame(w, h, opts){
|
||
opts = opts || {};
|
||
const bg = opts.bg || '#fafafa';
|
||
const border = opts.border || '1px solid #e2e8f0';
|
||
return '<svg viewBox="0 0 '+w+' '+h+'" preserveAspectRatio="xMidYMid meet" '
|
||
+ 'style="width:100%;height:auto;display:block;background:'+bg
|
||
+ ';border:'+border+';border-radius:10px">';
|
||
},
|
||
|
||
/* Двухмерные оси t (горизонтально) и y (вертикально). Возвращает функции toX/toY */
|
||
axes(W, H, pad, tMax, yRange){
|
||
const left = pad, right = W - pad, top = pad, bot = H - pad;
|
||
const ux = (right - left) / tMax;
|
||
const uy = (bot - top) / (yRange[1] - yRange[0]);
|
||
function toX(t){ return left + t * ux; }
|
||
function toY(y){ return bot - (y - yRange[0]) * uy; }
|
||
/* SVG сетки + рамки */
|
||
let svg = '<g stroke="#e2e8f0" stroke-width="0.8">';
|
||
/* Вертикальные линии каждую секунду */
|
||
for (let s = 0; s <= tMax; s++) svg += '<line x1="'+toX(s)+'" y1="'+top+'" x2="'+toX(s)+'" y2="'+bot+'"/>';
|
||
/* Горизонтальные линии */
|
||
const yStep = (yRange[1] - yRange[0]) / 4;
|
||
for (let i = 0; i <= 4; i++){
|
||
const y = yRange[0] + i * yStep;
|
||
svg += '<line x1="'+left+'" y1="'+toY(y)+'" x2="'+right+'" y2="'+toY(y)+'"/>';
|
||
}
|
||
svg += '</g>';
|
||
/* Ось t */
|
||
svg += '<line x1="'+left+'" y1="'+toY(0)+'" x2="'+right+'" y2="'+toY(0)+'" stroke="#0f172a" stroke-width="1.4"/>';
|
||
/* Ось y */
|
||
svg += '<line x1="'+toX(0)+'" y1="'+top+'" x2="'+toX(0)+'" y2="'+bot+'" stroke="#0f172a" stroke-width="1.4"/>';
|
||
return { svg: svg, toX, toY, left, right, top, bot };
|
||
},
|
||
|
||
/* Создать ползунок-control под симуляцией.
|
||
opts: { label, min, max, step, value, onChange } */
|
||
slider(opts){
|
||
const id = 'sl-' + Math.random().toString(36).slice(2,7);
|
||
const html = '<label style="display:flex;align-items:center;gap:8px;font-size:.82rem;color:#475569;font-weight:600;margin:4px 8px">'
|
||
+ '<span style="min-width:90px">' + opts.label + '</span>'
|
||
+ '<input type="range" id="' + id + '" min="' + opts.min + '" max="' + opts.max + '" step="' + opts.step + '" value="' + opts.value + '" style="flex:1;min-width:80px">'
|
||
+ '<b id="' + id + '-v" style="font-family:JetBrains Mono,monospace;color:#0891b2;font-size:.86rem;min-width:50px;text-align:right">' + opts.value + (opts.unit || '') + '</b>'
|
||
+ '</label>';
|
||
return { html, id, wire(root){
|
||
const inp = root.querySelector('#' + id);
|
||
const v = root.querySelector('#' + id + '-v');
|
||
if (!inp || !v) return;
|
||
inp.addEventListener('input', () => {
|
||
const val = parseFloat(inp.value);
|
||
v.textContent = (opts.fmt ? opts.fmt(val) : val) + (opts.unit || '');
|
||
if (opts.onChange) opts.onChange(val);
|
||
});
|
||
} };
|
||
}
|
||
};
|
||
|
||
/* ============================================================ */
|
||
/* Oscillogram — гармонические колебания */
|
||
/* ============================================================ */
|
||
|
||
class Oscillogram {
|
||
constructor(container, opts){
|
||
opts = opts || {};
|
||
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
|
||
this.W = opts.width || 560;
|
||
this.H = opts.height || 200;
|
||
this.pad = opts.pad || 32;
|
||
this.tWindow = opts.tWindow || 4; // секунд видно
|
||
this.A = opts.A !== undefined ? opts.A : 1.0;
|
||
this.omega = opts.omega !== undefined ? opts.omega : 2 * Math.PI;
|
||
this.phi0 = opts.phi0 !== undefined ? opts.phi0 : 0;
|
||
this.damping = opts.damping || 0;
|
||
this.color = opts.color || '#dc2626';
|
||
this.label = opts.label || 'x(t)';
|
||
this.paused = false;
|
||
this.t = 0;
|
||
this.history = []; // [t, y] точки за последние tWindow секунд
|
||
this._render();
|
||
util.subscribe(this);
|
||
util.observe(this);
|
||
}
|
||
setA(v){ this.A = v; }
|
||
setOmega(v){ this.omega = v; }
|
||
setPhi(v){ this.phi0 = v; }
|
||
setDamping(v){ this.damping = v; }
|
||
reset(){ this.history = []; this.t = 0; }
|
||
update(dt){
|
||
this.t += dt;
|
||
const y = this.A * Math.exp(-this.damping * this.t) * Math.cos(this.omega * this.t + this.phi0);
|
||
this.history.push([this.t, y]);
|
||
while (this.history.length && this.history[0][0] < this.t - this.tWindow) this.history.shift();
|
||
}
|
||
render(){
|
||
if (!this.el) return;
|
||
const W = this.W, H = this.H, pad = this.pad;
|
||
const tMin = Math.max(0, this.t - this.tWindow);
|
||
const yRange = [-Math.max(1.05, this.A * 1.1), Math.max(1.05, this.A * 1.1)];
|
||
const ax = util.axes(W, H, pad, this.tWindow, yRange);
|
||
let polyline = '';
|
||
if (this.history.length > 1){
|
||
const pts = this.history.map(([t, y]) => (ax.left + (t - tMin) * (ax.right - ax.left) / this.tWindow).toFixed(1) + ',' + ax.toY(y).toFixed(1));
|
||
polyline = '<polyline points="' + pts.join(' ') + '" fill="none" stroke="' + this.color + '" stroke-width="2.4" stroke-linejoin="round"/>';
|
||
}
|
||
/* Подпись y(t) */
|
||
const titleSvg = '<text x="' + (W - pad) + '" y="' + (pad - 8) + '" text-anchor="end" font-size="12" font-family="JetBrains Mono,monospace" fill="' + this.color + '" font-weight="700">' + this.label + '</text>';
|
||
const svg = util.svgFrame(W, H) + ax.svg + polyline + titleSvg + '</svg>';
|
||
this.el.innerHTML = svg;
|
||
}
|
||
_render(){ this.render(); }
|
||
}
|
||
P.Oscillogram = Oscillogram;
|
||
|
||
/* ============================================================ */
|
||
/* SpringMass — пружинный маятник (вертикальный) */
|
||
/* ============================================================ */
|
||
|
||
class SpringMass {
|
||
constructor(container, opts){
|
||
opts = opts || {};
|
||
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
|
||
this.W = opts.width || 240;
|
||
this.H = opts.height || 280;
|
||
this.m = opts.m !== undefined ? opts.m : 0.5; // кг
|
||
this.k = opts.k !== undefined ? opts.k : 20; // Н/м
|
||
this.A = opts.A !== undefined ? opts.A : 0.06; // м (амплитуда)
|
||
this.color = opts.color || '#0891b2';
|
||
this.paused = false;
|
||
this.t = 0;
|
||
this._render();
|
||
util.subscribe(this);
|
||
util.observe(this);
|
||
}
|
||
setMass(m){ this.m = Math.max(0.05, m); }
|
||
setStiffness(k){ this.k = Math.max(1, k); }
|
||
setAmplitude(A){ this.A = Math.max(0.005, A); }
|
||
period(){ return 2 * Math.PI * Math.sqrt(this.m / this.k); }
|
||
freq(){ return 1 / this.period(); }
|
||
update(dt){ this.t += dt; }
|
||
render(){
|
||
if (!this.el) return;
|
||
const W = this.W, H = this.H;
|
||
const T = this.period();
|
||
const omega = 2 * Math.PI / T;
|
||
const A_px = 60; /* визуальная амплитуда в px */
|
||
const y0 = 90; /* y-координата равновесия груза в px */
|
||
const yCur = y0 + A_px * Math.cos(omega * this.t);
|
||
/* Пружина: гармошка-зигзаг от крюка (y=20) до груза (y=yCur-18) */
|
||
const cx = W / 2, hookY = 20, massY = yCur, massR = 22;
|
||
const coils = 10;
|
||
const springTop = hookY;
|
||
const springBot = massY - massR;
|
||
const segH = (springBot - springTop) / (coils * 2);
|
||
let path = 'M ' + cx + ' ' + springTop;
|
||
for (let i = 0; i < coils; i++){
|
||
path += ' L ' + (cx - 14) + ' ' + (springTop + segH * (2 * i + 1));
|
||
path += ' L ' + (cx + 14) + ' ' + (springTop + segH * (2 * i + 2));
|
||
}
|
||
path += ' L ' + cx + ' ' + springBot;
|
||
/* Линейка справа */
|
||
const ruler = '<g stroke="#cbd5e1" stroke-width="1" font-family="JetBrains Mono,monospace" font-size="10" fill="#64748b">'
|
||
+ '<line x1="' + (W - 38) + '" y1="' + (y0 - A_px) + '" x2="' + (W - 38) + '" y2="' + (y0 + A_px) + '" stroke-width="1.6"/>'
|
||
+ '<line x1="' + (W - 44) + '" y1="' + (y0 - A_px) + '" x2="' + (W - 32) + '" y2="' + (y0 - A_px) + '"/>'
|
||
+ '<text x="' + (W - 26) + '" y="' + (y0 - A_px + 4) + '">+A</text>'
|
||
+ '<line x1="' + (W - 44) + '" y1="' + y0 + '" x2="' + (W - 32) + '" y2="' + y0 + '" stroke="#0f172a" stroke-width="1.4"/>'
|
||
+ '<text x="' + (W - 26) + '" y="' + (y0 + 4) + '">0</text>'
|
||
+ '<line x1="' + (W - 44) + '" y1="' + (y0 + A_px) + '" x2="' + (W - 32) + '" y2="' + (y0 + A_px) + '"/>'
|
||
+ '<text x="' + (W - 26) + '" y="' + (y0 + A_px + 4) + '">-A</text>'
|
||
+ '</g>';
|
||
/* Период справа сверху */
|
||
const Tlabel = '<text x="12" y="20" font-family="JetBrains Mono,monospace" font-size="12" fill="' + this.color + '" font-weight="700">T = ' + T.toFixed(2) + ' с</text>';
|
||
const svg = util.svgFrame(W, H, {bg:'#f8fafc'})
|
||
+ '<line x1="0" y1="' + (hookY - 6) + '" x2="' + W + '" y2="' + (hookY - 6) + '" stroke="#334155" stroke-width="3"/>'
|
||
+ '<g stroke="#334155" stroke-width="3" fill="none" stroke-linejoin="round" stroke-linecap="round">'
|
||
+ '<path d="' + path + '"/>'
|
||
+ '</g>'
|
||
+ '<rect x="' + (cx - massR) + '" y="' + (massY - massR) + '" width="' + (2 * massR) + '" height="' + (2 * massR) + '" rx="6" fill="' + this.color + '" stroke="#0f172a" stroke-width="1.6"/>'
|
||
+ '<text x="' + cx + '" y="' + (massY + 5) + '" text-anchor="middle" font-family="Outfit,sans-serif" font-size="14" font-weight="800" fill="#fff">m</text>'
|
||
+ ruler + Tlabel
|
||
+ '</svg>';
|
||
this.el.innerHTML = svg;
|
||
}
|
||
_render(){ this.render(); }
|
||
}
|
||
P.SpringMass = SpringMass;
|
||
|
||
/* ============================================================ */
|
||
/* Pendulum — математический маятник */
|
||
/* ============================================================ */
|
||
|
||
class Pendulum {
|
||
constructor(container, opts){
|
||
opts = opts || {};
|
||
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
|
||
this.W = opts.width || 240;
|
||
this.H = opts.height || 260;
|
||
this.l = opts.l !== undefined ? opts.l : 1.0; // м
|
||
this.g = opts.g !== undefined ? opts.g : 9.81;
|
||
this.theta0 = opts.theta0 !== undefined ? opts.theta0 : Math.PI / 12; // начальный угол (рад)
|
||
this.color = opts.color || '#0891b2';
|
||
this.paused = false;
|
||
this.t = 0;
|
||
this._render();
|
||
util.subscribe(this);
|
||
util.observe(this);
|
||
}
|
||
setLength(l){ this.l = Math.max(0.1, l); }
|
||
setG(g){ this.g = Math.max(0.5, g); }
|
||
setTheta0(theta){ this.theta0 = Math.max(0.02, Math.min(Math.PI/4, theta)); }
|
||
period(){ return 2 * Math.PI * Math.sqrt(this.l / this.g); }
|
||
update(dt){ this.t += dt; }
|
||
render(){
|
||
if (!this.el) return;
|
||
const W = this.W, H = this.H;
|
||
const T = this.period();
|
||
const omega = 2 * Math.PI / T;
|
||
const theta = this.theta0 * Math.cos(omega * this.t);
|
||
const cx = W / 2, hookY = 20;
|
||
const Lpx = Math.min(160, H - 70);
|
||
const bobR = 18;
|
||
const bx = cx + Lpx * Math.sin(theta);
|
||
const by = hookY + Lpx * Math.cos(theta);
|
||
/* Дуга-траектория */
|
||
const arcR = Lpx;
|
||
const arcStart = -this.theta0;
|
||
const arcEnd = this.theta0;
|
||
const aS = { x: cx + arcR * Math.sin(arcStart), y: hookY + arcR * Math.cos(arcStart) };
|
||
const aE = { x: cx + arcR * Math.sin(arcEnd), y: hookY + arcR * Math.cos(arcEnd) };
|
||
const largeArc = (arcEnd - arcStart) > Math.PI ? 1 : 0;
|
||
const sweep = 1;
|
||
const arc = '<path d="M ' + aS.x.toFixed(1) + ' ' + aS.y.toFixed(1) + ' A ' + arcR + ' ' + arcR + ' 0 ' + largeArc + ' ' + sweep + ' ' + aE.x.toFixed(1) + ' ' + aE.y.toFixed(1) + '" fill="none" stroke="#cbd5e1" stroke-width="1.4" stroke-dasharray="4 4"/>';
|
||
/* Вертикальная пунктирная ось */
|
||
const vert = '<line x1="' + cx + '" y1="' + hookY + '" x2="' + cx + '" y2="' + (hookY + Lpx + 5) + '" stroke="#cbd5e1" stroke-width="1" stroke-dasharray="3 3"/>';
|
||
/* Подвес */
|
||
const string = '<line x1="' + cx + '" y1="' + hookY + '" x2="' + bx.toFixed(1) + '" y2="' + by.toFixed(1) + '" stroke="#0f172a" stroke-width="2"/>';
|
||
const bob = '<circle cx="' + bx.toFixed(1) + '" cy="' + by.toFixed(1) + '" r="' + bobR + '" fill="' + this.color + '" stroke="#0f172a" stroke-width="1.6"/>';
|
||
/* Период */
|
||
const Tlabel = '<text x="12" y="20" font-family="JetBrains Mono,monospace" font-size="12" fill="' + this.color + '" font-weight="700">T = ' + T.toFixed(2) + ' с</text>';
|
||
/* Подвес-крепление */
|
||
const hook = '<line x1="' + (cx - 30) + '" y1="' + (hookY - 6) + '" x2="' + (cx + 30) + '" y2="' + (hookY - 6) + '" stroke="#334155" stroke-width="3"/>';
|
||
const svg = util.svgFrame(W, H, {bg:'#f8fafc'}) + hook + vert + arc + string + bob + Tlabel + '</svg>';
|
||
this.el.innerHTML = svg;
|
||
}
|
||
_render(){ this.render(); }
|
||
}
|
||
P.Pendulum = Pendulum;
|
||
|
||
/* ============================================================ */
|
||
/* EnergyView — превращения энергии при гарм. колебаниях */
|
||
/* Показывает W_к, W_п, W_мех=const на одном графике */
|
||
/* ============================================================ */
|
||
|
||
class EnergyView {
|
||
constructor(container, opts){
|
||
opts = opts || {};
|
||
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
|
||
this.W = opts.width || 560;
|
||
this.H = opts.height || 240;
|
||
this.pad = opts.pad || 36;
|
||
this.A = opts.A !== undefined ? opts.A : 1.0;
|
||
this.omega = opts.omega !== undefined ? opts.omega : 2 * Math.PI;
|
||
this.tWindow = opts.tWindow || 4;
|
||
this.paused = false;
|
||
this.t = 0;
|
||
this.history = []; // [t, Wk, Wp]
|
||
util.subscribe(this);
|
||
util.observe(this);
|
||
this.render();
|
||
}
|
||
setA(v){ this.A = v; this.history = []; }
|
||
setOmega(v){ this.omega = v; this.history = []; }
|
||
update(dt){
|
||
this.t += dt;
|
||
/* Для x = A cos(ωt): v = -Aω sin(ωt)
|
||
W_к = m v² / 2 = (1/2) m A² ω² sin²(ωt)
|
||
W_п = k x² / 2 = (1/2) m ω² · A² cos²(ωt) (k = m ω²)
|
||
В безразмерных: положим (1/2)mω²A² = 1 — тогда обе варьируются 0..1, сумма = 1 */
|
||
const c = Math.cos(this.omega * this.t);
|
||
const s = Math.sin(this.omega * this.t);
|
||
const Wp = c * c;
|
||
const Wk = s * s;
|
||
this.history.push([this.t, Wk, Wp]);
|
||
while (this.history.length && this.history[0][0] < this.t - this.tWindow) this.history.shift();
|
||
}
|
||
render(){
|
||
if (!this.el) return;
|
||
const W = this.W, H = this.H, pad = this.pad;
|
||
const tMin = Math.max(0, this.t - this.tWindow);
|
||
const yRange = [0, 1.1];
|
||
const ax = util.axes(W, H, pad, this.tWindow, yRange);
|
||
function path(idx, color, label){
|
||
if (this.history.length < 2) return '';
|
||
const pts = this.history.map(p => (ax.left + (p[0] - tMin) * (ax.right - ax.left) / this.tWindow).toFixed(1) + ',' + ax.toY(p[idx]).toFixed(1));
|
||
return '<polyline points="' + pts.join(' ') + '" fill="none" stroke="' + color + '" stroke-width="2.2" stroke-linejoin="round"/>';
|
||
}
|
||
const pK = path.call(this, 1, '#dc2626');
|
||
const pP = path.call(this, 2, '#16a34a');
|
||
/* W_мех = const = 1 (горизонтальная линия) */
|
||
const pM = '<line x1="' + ax.left + '" y1="' + ax.toY(1) + '" x2="' + ax.right + '" y2="' + ax.toY(1) + '" stroke="#7c3aed" stroke-width="2.4" stroke-dasharray="6 4"/>';
|
||
/* Легенда */
|
||
const legend = '<g font-family="JetBrains Mono,monospace" font-size="11" font-weight="700">'
|
||
+ '<rect x="' + (W - 130) + '" y="' + (pad - 28) + '" width="118" height="74" fill="#fff" stroke="#e2e8f0" rx="6"/>'
|
||
+ '<line x1="' + (W - 122) + '" y1="' + (pad - 14) + '" x2="' + (W - 102) + '" y2="' + (pad - 14) + '" stroke="#dc2626" stroke-width="2.4"/>'
|
||
+ '<text x="' + (W - 96) + '" y="' + (pad - 10) + '" fill="#dc2626">W кинет.</text>'
|
||
+ '<line x1="' + (W - 122) + '" y1="' + (pad + 6) + '" x2="' + (W - 102) + '" y2="' + (pad + 6) + '" stroke="#16a34a" stroke-width="2.4"/>'
|
||
+ '<text x="' + (W - 96) + '" y="' + (pad + 10) + '" fill="#16a34a">W потенц.</text>'
|
||
+ '<line x1="' + (W - 122) + '" y1="' + (pad + 26) + '" x2="' + (W - 102) + '" y2="' + (pad + 26) + '" stroke="#7c3aed" stroke-width="2.4" stroke-dasharray="4 3"/>'
|
||
+ '<text x="' + (W - 96) + '" y="' + (pad + 30) + '" fill="#7c3aed">W мех = const</text>'
|
||
+ '</g>';
|
||
const svg = util.svgFrame(W, H) + ax.svg + pM + pK + pP + legend + '</svg>';
|
||
this.el.innerHTML = svg;
|
||
}
|
||
}
|
||
P.EnergyView = EnergyView;
|
||
|
||
/* ============================================================ */
|
||
/* ResonanceCurve — резонансная кривая A(ω) при разных γ */
|
||
/* ============================================================ */
|
||
|
||
class ResonanceCurve {
|
||
constructor(container, opts){
|
||
opts = opts || {};
|
||
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
|
||
this.W = opts.width || 540;
|
||
this.H = opts.height || 240;
|
||
this.pad = opts.pad || 40;
|
||
this.omega0 = opts.omega0 || 1.0; /* собственная частота (норм.) */
|
||
this.gamma = opts.gamma !== undefined ? opts.gamma : 0.15;
|
||
this.omegaCur = opts.omegaCur !== undefined ? opts.omegaCur : 0.6;
|
||
this.color = opts.color || '#7c3aed';
|
||
this.paused = false;
|
||
this.render();
|
||
}
|
||
setGamma(g){ this.gamma = Math.max(0.02, g); this.render(); }
|
||
setOmegaCur(w){ this.omegaCur = Math.max(0.02, w); this.render(); }
|
||
/* update не нужен — статический график, обновляется по setter */
|
||
update(){}
|
||
render(){
|
||
if (!this.el) return;
|
||
const W = this.W, H = this.H, pad = this.pad;
|
||
const wMin = 0, wMax = 2 * this.omega0;
|
||
/* Подсчитаем все амплитуды чтобы знать max */
|
||
function amp(w, g, w0){
|
||
const dw2 = (w0 * w0 - w * w);
|
||
const denom = Math.sqrt(dw2 * dw2 + (2 * g * w) * (2 * g * w));
|
||
return 1 / Math.max(denom, 1e-6);
|
||
}
|
||
const gMin = 0.05;
|
||
const ampMax = amp(this.omega0, gMin, this.omega0) * 1.1;
|
||
/* Сетка */
|
||
const left = pad, right = W - pad, top = pad, bot = H - pad;
|
||
const ux = (right - left) / (wMax - wMin);
|
||
const uy = (bot - top) / ampMax;
|
||
function toX(w){ return left + (w - wMin) * ux; }
|
||
function toY(a){ return bot - a * uy; }
|
||
let svg = util.svgFrame(W, H);
|
||
/* Линии сетки */
|
||
svg += '<g stroke="#e2e8f0" stroke-width="0.8">';
|
||
for (let i = 0; i <= 4; i++){
|
||
const w = wMin + (wMax - wMin) * i / 4;
|
||
svg += '<line x1="' + toX(w) + '" y1="' + top + '" x2="' + toX(w) + '" y2="' + bot + '"/>';
|
||
}
|
||
for (let i = 0; i <= 4; i++){
|
||
svg += '<line x1="' + left + '" y1="' + (top + (bot - top) * i / 4) + '" x2="' + right + '" y2="' + (top + (bot - top) * i / 4) + '"/>';
|
||
}
|
||
svg += '</g>';
|
||
/* Оси */
|
||
svg += '<line x1="' + left + '" y1="' + bot + '" x2="' + right + '" y2="' + bot + '" stroke="#0f172a" stroke-width="1.4"/>';
|
||
svg += '<line x1="' + left + '" y1="' + top + '" x2="' + left + '" y2="' + bot + '" stroke="#0f172a" stroke-width="1.4"/>';
|
||
/* Подписи осей */
|
||
svg += '<text x="' + (right - 4) + '" y="' + (bot - 6) + '" font-family="JetBrains Mono,monospace" font-size="11" fill="#64748b" text-anchor="end">ω</text>';
|
||
svg += '<text x="' + (left + 4) + '" y="' + (top + 12) + '" font-family="JetBrains Mono,monospace" font-size="11" fill="#64748b">A</text>';
|
||
/* Линия ω₀ — собственная частота */
|
||
svg += '<line x1="' + toX(this.omega0) + '" y1="' + top + '" x2="' + toX(this.omega0) + '" y2="' + bot + '" stroke="#94a3b8" stroke-width="1.2" stroke-dasharray="4 4"/>';
|
||
svg += '<text x="' + toX(this.omega0) + '" y="' + (top - 4) + '" font-family="JetBrains Mono,monospace" font-size="10" fill="#64748b" text-anchor="middle">ω₀</text>';
|
||
/* Кривая A(ω) */
|
||
let path = 'M ';
|
||
const N = 200;
|
||
for (let i = 0; i <= N; i++){
|
||
const w = wMin + (wMax - wMin) * i / N;
|
||
const a = amp(w, this.gamma, this.omega0);
|
||
path += toX(w).toFixed(1) + ',' + toY(Math.min(a, ampMax)).toFixed(1);
|
||
if (i < N) path += ' L ';
|
||
}
|
||
svg += '<path d="' + path + '" fill="none" stroke="' + this.color + '" stroke-width="2.6" stroke-linejoin="round"/>';
|
||
/* Точка-маркер на текущей ω */
|
||
const aCur = Math.min(amp(this.omegaCur, this.gamma, this.omega0), ampMax);
|
||
svg += '<line x1="' + toX(this.omegaCur) + '" y1="' + toY(0) + '" x2="' + toX(this.omegaCur) + '" y2="' + toY(aCur) + '" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="3 3"/>';
|
||
svg += '<circle cx="' + toX(this.omegaCur) + '" cy="' + toY(aCur) + '" r="6" fill="#dc2626" stroke="#fff" stroke-width="2"/>';
|
||
/* Подпись γ */
|
||
svg += '<text x="' + (left + 10) + '" y="' + (top + 16) + '" font-family="JetBrains Mono,monospace" font-size="12" fill="' + this.color + '" font-weight="700">γ = ' + this.gamma.toFixed(2) + '</text>';
|
||
svg += '<text x="' + (left + 10) + '" y="' + (top + 32) + '" font-family="JetBrains Mono,monospace" font-size="12" fill="#dc2626" font-weight="700">ω = ' + this.omegaCur.toFixed(2) + ' · A = ' + aCur.toFixed(2) + '</text>';
|
||
svg += '</svg>';
|
||
this.el.innerHTML = svg;
|
||
}
|
||
}
|
||
P.ResonanceCurve = ResonanceCurve;
|
||
|
||
/* ============================================================ */
|
||
/* TransverseWave — поперечная волна на струне */
|
||
/* y(x, t) = A sin(kx - ωt + φ) */
|
||
/* ============================================================ */
|
||
|
||
class TransverseWave {
|
||
constructor(container, opts){
|
||
opts = opts || {};
|
||
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
|
||
this.W = opts.width || 560;
|
||
this.H = opts.height || 180;
|
||
this.A = opts.A !== undefined ? opts.A : 0.4; /* отн. амплитуда (0..1) */
|
||
this.lambda = opts.lambda !== undefined ? opts.lambda : 1.0; /* отн. длина волны */
|
||
this.v = opts.v !== undefined ? opts.v : 0.8; /* скорость распространения (отн./с) */
|
||
this.color = opts.color || '#0891b2';
|
||
this.markerX = opts.markerX !== undefined ? opts.markerX : 0.4; /* пол. красной точки (0..1 от ширины) */
|
||
this.paused = false;
|
||
this.t = 0;
|
||
util.subscribe(this);
|
||
util.observe(this);
|
||
this.render();
|
||
}
|
||
setA(v){ this.A = v; }
|
||
setLambda(v){ this.lambda = Math.max(0.1, v); }
|
||
setV(v){ this.v = v; }
|
||
update(dt){ this.t += dt; }
|
||
render(){
|
||
if (!this.el) return;
|
||
const W = this.W, H = this.H;
|
||
const yCenter = H / 2;
|
||
const amp = this.A * (H / 2 - 18);
|
||
const k = 2 * Math.PI / this.lambda;
|
||
const omega = k * this.v;
|
||
/* SVG: горизонтальная ось + волна как polyline */
|
||
let svg = util.svgFrame(W, H, {bg:'#f8fafc'});
|
||
/* Ось */
|
||
svg += '<line x1="0" y1="' + yCenter + '" x2="' + W + '" y2="' + yCenter + '" stroke="#cbd5e1" stroke-width="1" stroke-dasharray="4 3"/>';
|
||
/* Кривая */
|
||
const N = 180;
|
||
let path = 'M ';
|
||
for (let i = 0; i <= N; i++){
|
||
const px = (W * i / N);
|
||
/* Реальное x в относительных единицах (1 длина волны на ~120px) */
|
||
const x = px / 120;
|
||
const y = yCenter - amp * Math.sin(k * x - omega * this.t);
|
||
path += px.toFixed(1) + ',' + y.toFixed(1);
|
||
if (i < N) path += ' L ';
|
||
}
|
||
svg += '<path d="' + path + '" fill="none" stroke="' + this.color + '" stroke-width="2.6" stroke-linejoin="round"/>';
|
||
/* Красный маркер — колеблющаяся точка */
|
||
const mPx = this.markerX * W;
|
||
const mX = mPx / 120;
|
||
const mY = yCenter - amp * Math.sin(k * mX - omega * this.t);
|
||
svg += '<line x1="' + mPx + '" y1="' + (yCenter - amp) + '" x2="' + mPx + '" y2="' + (yCenter + amp) + '" stroke="#fca5a5" stroke-width="1" stroke-dasharray="3 3"/>';
|
||
svg += '<circle cx="' + mPx + '" cy="' + mY.toFixed(1) + '" r="7" fill="#dc2626" stroke="#fff" stroke-width="2"/>';
|
||
/* Метка λ — горизонтальная скобка над волной */
|
||
const lambdaPx = 120 * this.lambda;
|
||
if (lambdaPx < W - 60){
|
||
const lxStart = 20, lxEnd = lxStart + lambdaPx;
|
||
svg += '<line x1="' + lxStart + '" y1="20" x2="' + lxEnd + '" y2="20" stroke="#7c3aed" stroke-width="1.6"/>';
|
||
svg += '<line x1="' + lxStart + '" y1="14" x2="' + lxStart + '" y2="26" stroke="#7c3aed" stroke-width="1.6"/>';
|
||
svg += '<line x1="' + lxEnd + '" y1="14" x2="' + lxEnd + '" y2="26" stroke="#7c3aed" stroke-width="1.6"/>';
|
||
svg += '<text x="' + ((lxStart + lxEnd) / 2) + '" y="14" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" font-weight="700" fill="#7c3aed">λ</text>';
|
||
}
|
||
svg += '</svg>';
|
||
this.el.innerHTML = svg;
|
||
}
|
||
}
|
||
P.TransverseWave = TransverseWave;
|
||
|
||
/* ============================================================ */
|
||
/* LongitudinalWave — продольная волна (сжатия/разрежения) */
|
||
/* ============================================================ */
|
||
|
||
class LongitudinalWave {
|
||
constructor(container, opts){
|
||
opts = opts || {};
|
||
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
|
||
this.W = opts.width || 560;
|
||
this.H = opts.height || 130;
|
||
this.A = opts.A !== undefined ? opts.A : 0.5; /* амплитуда (0..1) */
|
||
this.lambda = opts.lambda !== undefined ? opts.lambda : 1.0;
|
||
this.v = opts.v !== undefined ? opts.v : 0.8;
|
||
this.color = opts.color || '#0891b2';
|
||
this.nDots = opts.nDots || 60;
|
||
this.paused = false;
|
||
this.t = 0;
|
||
util.subscribe(this);
|
||
util.observe(this);
|
||
this.render();
|
||
}
|
||
setA(v){ this.A = v; }
|
||
setLambda(v){ this.lambda = Math.max(0.1, v); }
|
||
setV(v){ this.v = v; }
|
||
update(dt){ this.t += dt; }
|
||
render(){
|
||
if (!this.el) return;
|
||
const W = this.W, H = this.H;
|
||
const yC = H / 2;
|
||
const k = 2 * Math.PI / this.lambda;
|
||
const omega = k * this.v;
|
||
const xScale = 120; /* px на 1 ед. */
|
||
const amp = this.A * 10; /* px смещения */
|
||
const margin = 20;
|
||
let svg = util.svgFrame(W, H, {bg:'#f8fafc'});
|
||
/* Точки-молекулы */
|
||
let dots = '';
|
||
for (let i = 0; i < this.nDots; i++){
|
||
const x0 = margin + (W - 2 * margin) * i / (this.nDots - 1);
|
||
const xRel = x0 / xScale;
|
||
const disp = amp * Math.sin(k * xRel - omega * this.t);
|
||
const x = x0 + disp;
|
||
dots += '<circle cx="' + x.toFixed(1) + '" cy="' + yC + '" r="3" fill="' + this.color + '"/>';
|
||
}
|
||
svg += dots;
|
||
/* Подписи зон сжатия / разрежения */
|
||
svg += '<text x="' + (W / 2) + '" y="' + (H - 6) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="10" fill="#94a3b8">сжатие ↔ разрежение</text>';
|
||
svg += '</svg>';
|
||
this.el.innerHTML = svg;
|
||
}
|
||
}
|
||
P.LongitudinalWave = LongitudinalWave;
|
||
|
||
/* ============================================================ */
|
||
/* LCcircuit — колебательный контур */
|
||
/* q(t) = Q0 cos(ωt), i(t) = -Q0 ω sin(ωt), ω = 1/√(LC) */
|
||
/* ============================================================ */
|
||
|
||
class LCcircuit {
|
||
constructor(container, opts){
|
||
opts = opts || {};
|
||
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
|
||
this.W = opts.width || 480;
|
||
this.H = opts.height || 280;
|
||
this.L = opts.L !== undefined ? opts.L : 0.01; /* Гн */
|
||
this.C = opts.C !== undefined ? opts.C : 1e-6; /* Ф */
|
||
this.Q0 = opts.Q0 !== undefined ? opts.Q0 : 1.0; /* нормированный заряд */
|
||
this.color = opts.color || '#7c3aed';
|
||
this.paused = false;
|
||
this.t = 0;
|
||
util.subscribe(this);
|
||
util.observe(this);
|
||
this.render();
|
||
}
|
||
setL(L){ this.L = Math.max(1e-4, L); }
|
||
setC(C){ this.C = Math.max(1e-9, C); }
|
||
period(){ return 2 * Math.PI * Math.sqrt(this.L * this.C); }
|
||
freq(){ return 1 / this.period(); }
|
||
update(dt){ this.t += dt; }
|
||
render(){
|
||
if (!this.el) return;
|
||
const W = this.W, H = this.H;
|
||
const T = this.period();
|
||
const omega = 2 * Math.PI / T;
|
||
const phase = omega * this.t;
|
||
const q = this.Q0 * Math.cos(phase);
|
||
const i = -this.Q0 * omega * Math.sin(phase);
|
||
/* Геометрия: C слева вверху, L справа вверху, соединены проводами */
|
||
const cx = W / 2, cy = H / 2 - 20;
|
||
const cap = {x: cx - 100, y: cy};
|
||
const ind = {x: cx + 100, y: cy};
|
||
/* Energies (для подсветки): W_C ~ q², W_L ~ i² */
|
||
const WC = q * q;
|
||
const WL = (i / omega) * (i / omega); /* в норм. единицах */
|
||
const total = WC + WL;
|
||
const cOpacity = 0.3 + 0.7 * WC / total;
|
||
const lOpacity = 0.3 + 0.7 * WL / total;
|
||
let svg = util.svgFrame(W, H, {bg:'#f8fafc'});
|
||
/* Провода */
|
||
const wireY1 = cy - 50, wireY2 = cy + 50;
|
||
svg += '<path d="M ' + cap.x + ' ' + (cy - 18) + ' L ' + cap.x + ' ' + wireY1 + ' L ' + ind.x + ' ' + wireY1 + ' L ' + ind.x + ' ' + (cy - 18) + '" fill="none" stroke="#0f172a" stroke-width="2"/>';
|
||
svg += '<path d="M ' + cap.x + ' ' + (cy + 18) + ' L ' + cap.x + ' ' + wireY2 + ' L ' + ind.x + ' ' + wireY2 + ' L ' + ind.x + ' ' + (cy + 18) + '" fill="none" stroke="#0f172a" stroke-width="2"/>';
|
||
/* Конденсатор: две параллельные пластины */
|
||
const plateW = 36;
|
||
svg += '<g stroke="#0f172a" stroke-width="3">';
|
||
svg += '<line x1="' + (cap.x - plateW/2) + '" y1="' + (cap.y - 18) + '" x2="' + (cap.x + plateW/2) + '" y2="' + (cap.y - 18) + '"/>';
|
||
svg += '<line x1="' + (cap.x - plateW/2) + '" y1="' + (cap.y + 18) + '" x2="' + (cap.x + plateW/2) + '" y2="' + (cap.y + 18) + '"/>';
|
||
svg += '</g>';
|
||
/* Заряды на пластинах */
|
||
const sign = q > 0 ? 1 : -1;
|
||
const topCh = sign > 0 ? '+' : '−';
|
||
const botCh = sign > 0 ? '−' : '+';
|
||
const qAbsNorm = Math.abs(q) / this.Q0;
|
||
svg += '<text x="' + cap.x + '" y="' + (cap.y - 24) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="14" font-weight="800" fill="#dc2626" opacity="' + (qAbsNorm).toFixed(2) + '">' + topCh + '</text>';
|
||
svg += '<text x="' + cap.x + '" y="' + (cap.y + 36) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="14" font-weight="800" fill="#2563eb" opacity="' + (qAbsNorm).toFixed(2) + '">' + botCh + '</text>';
|
||
svg += '<text x="' + (cap.x - 30) + '" y="' + (cap.y + 5) + '" text-anchor="end" font-family="JetBrains Mono,monospace" font-size="13" fill="#dc2626" font-weight="700">C</text>';
|
||
/* Индуктор: петли */
|
||
const coils = 4, coilR = 8, coilW = 64;
|
||
let coilPath = 'M ' + (ind.x - coilW/2) + ' ' + ind.y;
|
||
for (let k = 0; k < coils; k++){
|
||
const x0 = ind.x - coilW/2 + (coilW / coils) * k;
|
||
coilPath += ' a ' + coilR + ' ' + coilR + ' 0 0 1 ' + (coilW / coils) + ' 0';
|
||
}
|
||
svg += '<path d="' + coilPath + '" fill="none" stroke="#0f172a" stroke-width="2.4"/>';
|
||
svg += '<text x="' + (ind.x + 40) + '" y="' + (ind.y + 5) + '" font-family="JetBrains Mono,monospace" font-size="13" fill="#2563eb" font-weight="700">L</text>';
|
||
/* Стрелка тока */
|
||
const iDir = i > 0 ? 1 : -1;
|
||
const iAbs = Math.abs(i) / (this.Q0 * omega);
|
||
if (iAbs > 0.05){
|
||
const aY = wireY1 - 14;
|
||
const aX1 = cx - 30 * iDir;
|
||
const aX2 = cx + 30 * iDir;
|
||
svg += '<line x1="' + aX1 + '" y1="' + aY + '" x2="' + aX2 + '" y2="' + aY + '" stroke="#16a34a" stroke-width="2.4" opacity="' + iAbs.toFixed(2) + '"/>';
|
||
svg += '<polygon points="' + aX2 + ',' + aY + ' ' + (aX2 - 8 * iDir) + ',' + (aY - 4) + ' ' + (aX2 - 8 * iDir) + ',' + (aY + 4) + '" fill="#16a34a" opacity="' + iAbs.toFixed(2) + '"/>';
|
||
svg += '<text x="' + cx + '" y="' + (aY - 6) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="12" fill="#16a34a" font-weight="700">i</text>';
|
||
}
|
||
/* Энергетические столбцы */
|
||
const eY = H - 36, eH = 24;
|
||
svg += '<rect x="40" y="' + eY + '" width="120" height="' + eH + '" fill="#fef3c7" stroke="#d97706" stroke-width="1"/>';
|
||
svg += '<rect x="40" y="' + eY + '" width="' + (120 * WC / total).toFixed(1) + '" height="' + eH + '" fill="#dc2626" opacity="0.7"/>';
|
||
svg += '<text x="' + (40 + 60) + '" y="' + (eY + eH + 12) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="10" fill="#475569">W_C</text>';
|
||
svg += '<rect x="' + (W - 160) + '" y="' + eY + '" width="120" height="' + eH + '" fill="#fef3c7" stroke="#d97706" stroke-width="1"/>';
|
||
svg += '<rect x="' + (W - 160) + '" y="' + eY + '" width="' + (120 * WL / total).toFixed(1) + '" height="' + eH + '" fill="#2563eb" opacity="0.7"/>';
|
||
svg += '<text x="' + (W - 100) + '" y="' + (eY + eH + 12) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="10" fill="#475569">W_L</text>';
|
||
/* Подпись периода */
|
||
const Tdisp = T < 1e-3 ? (T * 1e6).toFixed(1) + ' мкс' : (T * 1e3).toFixed(2) + ' мс';
|
||
svg += '<text x="' + (W / 2) + '" y="22" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="13" fill="' + this.color + '" font-weight="800">T = 2π√(LC) = ' + Tdisp + '</text>';
|
||
svg += '</svg>';
|
||
this.el.innerHTML = svg;
|
||
}
|
||
}
|
||
P.LCcircuit = LCcircuit;
|
||
|
||
/* ============================================================ */
|
||
/* ACgen — генератор переменного тока (вращающаяся рамка в B) */
|
||
/* ============================================================ */
|
||
|
||
class ACgen {
|
||
constructor(container, opts){
|
||
opts = opts || {};
|
||
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
|
||
this.W = opts.width || 540;
|
||
this.H = opts.height || 240;
|
||
this.omega = opts.omega !== undefined ? opts.omega : 2 * Math.PI; /* рад/с */
|
||
this.U0 = opts.U0 !== undefined ? opts.U0 : 1.0;
|
||
this.color = opts.color || '#7c3aed';
|
||
this.tWindow = opts.tWindow || 4;
|
||
this.paused = false;
|
||
this.t = 0;
|
||
this.history = [];
|
||
util.subscribe(this);
|
||
util.observe(this);
|
||
this.render();
|
||
}
|
||
setOmega(w){ this.omega = w; this.history = []; }
|
||
update(dt){
|
||
this.t += dt;
|
||
this.history.push([this.t, this.U0 * Math.sin(this.omega * this.t)]);
|
||
while (this.history.length && this.history[0][0] < this.t - this.tWindow) this.history.shift();
|
||
}
|
||
render(){
|
||
if (!this.el) return;
|
||
const W = this.W, H = this.H;
|
||
/* Левая часть: рамка в магнитном поле; правая: график U(t) */
|
||
const leftW = 200;
|
||
const rightLeft = leftW + 10;
|
||
let svg = util.svgFrame(W, H, {bg:'#f8fafc'});
|
||
/* Магнитное поле (стрелки B) */
|
||
svg += '<g stroke="#94a3b8" stroke-width="1.2">';
|
||
for (let i = 0; i < 5; i++){
|
||
const x = 20 + i * 40;
|
||
svg += '<line x1="' + x + '" y1="20" x2="' + x + '" y2="' + (H - 20) + '" marker-end="url(#acgen-arr)"/>';
|
||
}
|
||
svg += '</g>';
|
||
svg += '<defs><marker id="acgen-arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" fill="#94a3b8"/></marker></defs>';
|
||
svg += '<text x="14" y="14" font-family="JetBrains Mono,monospace" font-size="11" fill="#475569" font-weight="700">B</text>';
|
||
/* Вращающаяся рамка: эллипс, отображающий проекцию прямоугольника */
|
||
const fx = 110, fy = H / 2;
|
||
const phi = this.omega * this.t;
|
||
const rx = 40 * Math.abs(Math.cos(phi));
|
||
const ry = 30;
|
||
svg += '<ellipse cx="' + fx + '" cy="' + fy + '" rx="' + rx.toFixed(1) + '" ry="' + ry + '" fill="none" stroke="#dc2626" stroke-width="2.4"/>';
|
||
/* Ось вращения */
|
||
svg += '<line x1="' + fx + '" y1="' + (fy - 50) + '" x2="' + fx + '" y2="' + (fy + 50) + '" stroke="#0f172a" stroke-width="2" stroke-dasharray="4 3"/>';
|
||
/* Контакт скользящих колец (схема) */
|
||
svg += '<line x1="' + fx + '" y1="' + (fy + 50) + '" x2="' + fx + '" y2="' + (fy + 70) + '" stroke="#0f172a" stroke-width="2"/>';
|
||
svg += '<line x1="' + fx + '" y1="' + (fy + 70) + '" x2="' + rightLeft + '" y2="' + (fy + 70) + '" stroke="#0f172a" stroke-width="2"/>';
|
||
/* График U(t) — справа */
|
||
const gPad = 26;
|
||
const tMin = Math.max(0, this.t - this.tWindow);
|
||
const ax = util.axes(W - rightLeft, H, gPad, this.tWindow, [-this.U0 * 1.2, this.U0 * 1.2]);
|
||
svg += '<g transform="translate(' + rightLeft + ' 0)">' + ax.svg + '</g>';
|
||
if (this.history.length > 1){
|
||
const pts = this.history.map(([t, y]) => (rightLeft + ax.left + (t - tMin) * (ax.right - ax.left) / this.tWindow).toFixed(1) + ',' + ax.toY(y).toFixed(1));
|
||
svg += '<polyline points="' + pts.join(' ') + '" fill="none" stroke="' + this.color + '" stroke-width="2.4"/>';
|
||
}
|
||
/* Подпись */
|
||
svg += '<text x="' + (rightLeft + 20) + '" y="20" font-family="JetBrains Mono,monospace" font-size="12" fill="' + this.color + '" font-weight="700">U(t) = U₀ sin(ωt)</text>';
|
||
svg += '</svg>';
|
||
this.el.innerHTML = svg;
|
||
}
|
||
}
|
||
P.ACgen = ACgen;
|
||
|
||
/* ============================================================ */
|
||
/* Transformer — схема трансформатора с расчётом */
|
||
/* ============================================================ */
|
||
|
||
class Transformer {
|
||
constructor(container, opts){
|
||
opts = opts || {};
|
||
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
|
||
this.W = opts.width || 520;
|
||
this.H = opts.height || 240;
|
||
this.N1 = opts.N1 || 200;
|
||
this.N2 = opts.N2 || 50;
|
||
this.U1 = opts.U1 || 220;
|
||
this.color = opts.color || '#7c3aed';
|
||
this.render();
|
||
}
|
||
setN1(n){ this.N1 = Math.max(1, n|0); this.render(); }
|
||
setN2(n){ this.N2 = Math.max(1, n|0); this.render(); }
|
||
setU1(u){ this.U1 = u; this.render(); }
|
||
update(){ /* статика */ }
|
||
render(){
|
||
if (!this.el) return;
|
||
const W = this.W, H = this.H;
|
||
const k = this.N1 / this.N2;
|
||
const U2 = this.U1 / k;
|
||
const cy = H / 2;
|
||
let svg = util.svgFrame(W, H, {bg:'#f8fafc'});
|
||
/* Сердечник трансформатора (прямоугольник с вырезом) */
|
||
const coreL = 110, coreR = W - 110;
|
||
svg += '<rect x="' + coreL + '" y="' + (cy - 70) + '" width="' + (coreR - coreL) + '" height="140" fill="none" stroke="#64748b" stroke-width="6"/>';
|
||
svg += '<rect x="' + (coreL + 16) + '" y="' + (cy - 54) + '" width="' + (coreR - coreL - 32) + '" height="108" fill="#f8fafc" stroke="none"/>';
|
||
/* Первичная обмотка слева */
|
||
const coil1X = coreL + 24;
|
||
for (let i = 0; i < 6; i++){
|
||
const y = cy - 50 + i * 16;
|
||
svg += '<path d="M ' + (coil1X - 12) + ' ' + y + ' a 8 8 0 0 1 8 -8 a 8 8 0 0 1 8 8 a 8 8 0 0 1 -8 8 a 8 8 0 0 1 -8 -8 z" fill="none" stroke="#dc2626" stroke-width="2.4"/>';
|
||
}
|
||
/* Вторичная обмотка справа */
|
||
const coil2X = coreR - 24;
|
||
/* Адаптируем число витков визуально (max 8 для удобства) */
|
||
const visTurns2 = Math.max(2, Math.min(10, Math.round(6 * this.N2 / this.N1)));
|
||
for (let i = 0; i < visTurns2; i++){
|
||
const y = cy - 50 + i * (100 / visTurns2);
|
||
svg += '<path d="M ' + (coil2X - 12) + ' ' + y + ' a 6 6 0 0 1 8 -6 a 6 6 0 0 1 8 6 a 6 6 0 0 1 -8 6 a 6 6 0 0 1 -8 -6 z" fill="none" stroke="#2563eb" stroke-width="2.4"/>';
|
||
}
|
||
/* Провода-выходы первичной */
|
||
svg += '<line x1="' + (coil1X - 12) + '" y1="' + (cy - 50) + '" x2="20" y2="' + (cy - 50) + '" stroke="#dc2626" stroke-width="2"/>';
|
||
svg += '<line x1="' + (coil1X - 12) + '" y1="' + (cy + 50) + '" x2="20" y2="' + (cy + 50) + '" stroke="#dc2626" stroke-width="2"/>';
|
||
/* Провода вторичной */
|
||
svg += '<line x1="' + (coil2X + 4) + '" y1="' + (cy - 50) + '" x2="' + (W - 20) + '" y2="' + (cy - 50) + '" stroke="#2563eb" stroke-width="2"/>';
|
||
svg += '<line x1="' + (coil2X + 4) + '" y1="' + (cy + 50) + '" x2="' + (W - 20) + '" y2="' + (cy + 50) + '" stroke="#2563eb" stroke-width="2"/>';
|
||
/* Подписи N1, N2, U1, U2 */
|
||
svg += '<text x="' + (coil1X - 16) + '" y="' + (cy - 60) + '" font-family="JetBrains Mono,monospace" font-size="13" fill="#dc2626" font-weight="800">N₁ = ' + this.N1 + '</text>';
|
||
svg += '<text x="' + (coil2X + 16) + '" y="' + (cy - 60) + '" font-family="JetBrains Mono,monospace" font-size="13" fill="#2563eb" font-weight="800" text-anchor="end">N₂ = ' + this.N2 + '</text>';
|
||
svg += '<text x="' + (coil2X + 6) + '" y="' + (cy - 60) + '" font-family="JetBrains Mono,monospace" font-size="13" fill="#2563eb" font-weight="800">N₂ = ' + this.N2 + '</text>';
|
||
svg += '<text x="20" y="' + (cy - 56) + '" font-family="JetBrains Mono,monospace" font-size="12" fill="#dc2626">U₁ = ' + this.U1.toFixed(0) + ' В</text>';
|
||
svg += '<text x="' + (W - 20) + '" y="' + (cy - 56) + '" font-family="JetBrains Mono,monospace" font-size="12" fill="#2563eb" text-anchor="end">U₂ = ' + U2.toFixed(1) + ' В</text>';
|
||
/* Подпись коэф. */
|
||
svg += '<text x="' + (W/2) + '" y="22" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="13" fill="' + this.color + '" font-weight="800">k = N₁/N₂ = ' + k.toFixed(2) + '</text>';
|
||
svg += '<text x="' + (W/2) + '" y="' + (H - 12) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" fill="#64748b">' + (k > 1 ? 'понижающий' : 'повышающий') + '</text>';
|
||
svg += '</svg>';
|
||
this.el.innerHTML = svg;
|
||
}
|
||
}
|
||
P.Transformer = Transformer;
|
||
|
||
/* ============================================================ */
|
||
/* TwoSlit — интерференция от двух щелей (опыт Юнга) */
|
||
/* ============================================================ */
|
||
|
||
class TwoSlit {
|
||
constructor(container, opts){
|
||
opts = opts || {};
|
||
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
|
||
this.W = opts.width || 540;
|
||
this.H = opts.height || 200;
|
||
this.d = opts.d !== undefined ? opts.d : 0.5; /* расстояние между щелями (отн. ед.) */
|
||
this.L = opts.L !== undefined ? opts.L : 5; /* расстояние до экрана */
|
||
this.lambda = opts.lambda !== undefined ? opts.lambda : 0.05; /* длина волны */
|
||
this.color = opts.color || '#f59e0b';
|
||
this.paused = true; /* статика */
|
||
this.render();
|
||
}
|
||
setD(v){ this.d = Math.max(0.05, v); this.render(); }
|
||
setLambda(v){ this.lambda = Math.max(0.005, v); this.render(); }
|
||
update(){}
|
||
render(){
|
||
if (!this.el) return;
|
||
const W = this.W, H = this.H;
|
||
/* Слева: щели (две точки), справа: экран с полосами */
|
||
let svg = util.svgFrame(W, H, {bg:'#f8fafc'});
|
||
/* Лазер слева */
|
||
svg += '<rect x="6" y="' + (H/2 - 14) + '" width="40" height="28" fill="#fca5a5" stroke="#0f172a" stroke-width="1.4" rx="3"/>';
|
||
svg += '<text x="26" y="' + (H/2 + 4) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="10" font-weight="700">laser</text>';
|
||
/* Щели */
|
||
const slitX = 130;
|
||
const dPx = Math.min(60, this.d * 60);
|
||
const s1y = H/2 - dPx/2, s2y = H/2 + dPx/2;
|
||
svg += '<line x1="' + slitX + '" y1="20" x2="' + slitX + '" y2="' + (s1y - 4) + '" stroke="#0f172a" stroke-width="3"/>';
|
||
svg += '<line x1="' + slitX + '" y1="' + (s1y + 4) + '" x2="' + slitX + '" y2="' + (s2y - 4) + '" stroke="#0f172a" stroke-width="3"/>';
|
||
svg += '<line x1="' + slitX + '" y1="' + (s2y + 4) + '" x2="' + slitX + '" y2="' + (H - 20) + '" stroke="#0f172a" stroke-width="3"/>';
|
||
/* Лучи от лазера к щелям */
|
||
svg += '<line x1="46" y1="' + (H/2) + '" x2="' + slitX + '" y2="' + s1y + '" stroke="#fca5a5" stroke-width="1.4" opacity="0.6"/>';
|
||
svg += '<line x1="46" y1="' + (H/2) + '" x2="' + slitX + '" y2="' + s2y + '" stroke="#fca5a5" stroke-width="1.4" opacity="0.6"/>';
|
||
/* Экран */
|
||
const screenX = W - 50;
|
||
svg += '<line x1="' + screenX + '" y1="20" x2="' + screenX + '" y2="' + (H - 20) + '" stroke="#0f172a" stroke-width="3"/>';
|
||
/* Интерференционная картина: интенсивность ~ cos²(πdy/(λL)) */
|
||
const lp = this.lambda * this.L / this.d; /* шаг полос */
|
||
const lpPx = Math.max(3, Math.min(80, lp * 200));
|
||
const halfH = (H - 40) / 2;
|
||
const N = 240;
|
||
for (let i = 0; i < N; i++){
|
||
const y = 20 + (H - 40) * i / N;
|
||
const ry = y - H/2;
|
||
const intens = Math.pow(Math.cos(Math.PI * ry / lpPx), 2);
|
||
const op = intens.toFixed(3);
|
||
svg += '<rect x="' + screenX + '" y="' + y.toFixed(1) + '" width="22" height="' + ((H - 40) / N + 1).toFixed(1) + '" fill="' + this.color + '" opacity="' + op + '"/>';
|
||
}
|
||
/* Подпись формулы */
|
||
svg += '<text x="' + (W/2) + '" y="' + (H - 4) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" fill="#475569">d·sin φ = k·λ (max), d=' + this.d.toFixed(2) + ', λ=' + this.lambda.toFixed(3) + '</text>';
|
||
svg += '</svg>';
|
||
this.el.innerHTML = svg;
|
||
}
|
||
}
|
||
P.TwoSlit = TwoSlit;
|
||
|
||
/* ============================================================ */
|
||
/* DiffractionGrating — дифракционная решётка + спектр */
|
||
/* ============================================================ */
|
||
|
||
class DiffractionGrating {
|
||
constructor(container, opts){
|
||
opts = opts || {};
|
||
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
|
||
this.W = opts.width || 540;
|
||
this.H = opts.height || 220;
|
||
this.d = opts.d !== undefined ? opts.d : 2e-6; /* период решётки, м */
|
||
this.lambda = opts.lambda !== undefined ? opts.lambda : 550e-9; /* λ, м */
|
||
this.color = opts.color || '#22c55e';
|
||
this.paused = true;
|
||
this.render();
|
||
}
|
||
setD(v){ this.d = Math.max(0.3e-6, v); this.render(); }
|
||
setLambda(v){ this.lambda = Math.max(380e-9, Math.min(760e-9, v)); this.render(); }
|
||
update(){}
|
||
/* Возвращает цвет HEX для данной длины волны (для видимого света) */
|
||
wavelengthToColor(lamNm){
|
||
const ranges = [
|
||
[380, 440, '#7c3aed'], [440, 490, '#3b82f6'], [490, 520, '#06b6d4'],
|
||
[520, 570, '#22c55e'], [570, 590, '#facc15'], [590, 630, '#f97316'],
|
||
[630, 760, '#dc2626']
|
||
];
|
||
for (const [lo, hi, c] of ranges) if (lamNm >= lo && lamNm < hi) return c;
|
||
return '#94a3b8';
|
||
}
|
||
render(){
|
||
if (!this.el) return;
|
||
const W = this.W, H = this.H;
|
||
let svg = util.svgFrame(W, H, {bg:'#0f172a'}); /* тёмный фон для спектра */
|
||
/* Решётка слева */
|
||
const gx = 50;
|
||
svg += '<rect x="' + (gx - 6) + '" y="40" width="12" height="' + (H - 80) + '" fill="#475569"/>';
|
||
/* «штрихи» решётки */
|
||
for (let i = 0; i < 10; i++){
|
||
const y = 40 + i * (H - 80) / 10;
|
||
svg += '<line x1="' + gx + '" y1="' + y + '" x2="' + (gx + 6) + '" y2="' + y + '" stroke="#fff" stroke-width="0.8"/>';
|
||
}
|
||
svg += '<text x="' + gx + '" y="30" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" fill="#fff">решётка</text>';
|
||
/* Падающий луч */
|
||
svg += '<line x1="10" y1="' + (H/2) + '" x2="' + (gx - 6) + '" y2="' + (H/2) + '" stroke="' + this.wavelengthToColor(this.lambda * 1e9) + '" stroke-width="2.2"/>';
|
||
/* Линии порядков k = -3..3 */
|
||
const cx = gx + 6, cy = H/2;
|
||
const lamNm = this.lambda * 1e9;
|
||
const color = this.wavelengthToColor(lamNm);
|
||
for (let k = -3; k <= 3; k++){
|
||
const sinPhi = k * this.lambda / this.d;
|
||
if (Math.abs(sinPhi) > 1) continue;
|
||
const phi = Math.asin(sinPhi);
|
||
const dx = 400, dy = dx * Math.tan(phi);
|
||
const x2 = cx + dx, y2 = cy - dy;
|
||
const op = k === 0 ? 1.0 : (1 - Math.abs(k) * 0.18);
|
||
svg += '<line x1="' + cx + '" y1="' + cy + '" x2="' + x2 + '" y2="' + y2.toFixed(1) + '" stroke="' + color + '" stroke-width="' + (2.4 - Math.abs(k) * 0.3) + '" opacity="' + op.toFixed(2) + '"/>';
|
||
svg += '<text x="' + (x2 - 24) + '" y="' + (y2 - 6).toFixed(1) + '" font-family="JetBrains Mono,monospace" font-size="10" fill="' + color + '" font-weight="700">k=' + k + '</text>';
|
||
}
|
||
/* Подпись формулы и параметров */
|
||
svg += '<text x="' + (W/2) + '" y="' + (H - 8) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" fill="#cbd5e1">d sin φ = kλ · d=' + (this.d * 1e6).toFixed(2) + ' мкм · λ=' + (this.lambda * 1e9).toFixed(0) + ' нм</text>';
|
||
svg += '</svg>';
|
||
this.el.innerHTML = svg;
|
||
}
|
||
}
|
||
P.DiffractionGrating = DiffractionGrating;
|
||
|
||
/* ============================================================ */
|
||
/* FlatMirror — плоское зеркало, объект и мнимое изображение */
|
||
/* ============================================================ */
|
||
|
||
class FlatMirror {
|
||
constructor(container, opts){
|
||
opts = opts || {};
|
||
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
|
||
this.W = opts.width || 540;
|
||
this.H = opts.height || 240;
|
||
this.objX = opts.objX !== undefined ? opts.objX : 100; /* px от зеркала */
|
||
this.objY = opts.objY !== undefined ? opts.objY : 50;
|
||
this.objH = opts.objH !== undefined ? opts.objH : 50;
|
||
this.color = opts.color || '#f59e0b';
|
||
this.paused = true;
|
||
this.render();
|
||
}
|
||
setObjX(v){ this.objX = Math.max(20, v); this.render(); }
|
||
setObjY(v){ this.objY = v; this.render(); }
|
||
update(){}
|
||
render(){
|
||
if (!this.el) return;
|
||
const W = this.W, H = this.H;
|
||
const cy = H / 2 + 30;
|
||
const mirrorX = W / 2;
|
||
let svg = util.svgFrame(W, H, {bg:'#f8fafc'});
|
||
/* Зеркало с штриховкой */
|
||
svg += '<line x1="' + mirrorX + '" y1="30" x2="' + mirrorX + '" y2="' + (H - 30) + '" stroke="#0f172a" stroke-width="3"/>';
|
||
for (let i = 0; i < 12; i++){
|
||
const y = 30 + i * (H - 60) / 12;
|
||
svg += '<line x1="' + (mirrorX + 1) + '" y1="' + y + '" x2="' + (mirrorX + 10) + '" y2="' + (y - 8) + '" stroke="#0f172a" stroke-width="1.2"/>';
|
||
}
|
||
svg += '<text x="' + (mirrorX + 14) + '" y="' + (H - 16) + '" font-family="JetBrains Mono,monospace" font-size="11" fill="#0f172a">зеркало</text>';
|
||
/* Объект — стрелка */
|
||
const objX = mirrorX - this.objX;
|
||
const objBaseY = cy;
|
||
const objTopY = cy - this.objH;
|
||
svg += '<line x1="' + objX + '" y1="' + objBaseY + '" x2="' + objX + '" y2="' + objTopY + '" stroke="' + this.color + '" stroke-width="3"/>';
|
||
svg += '<polygon points="' + objX + ',' + objTopY + ' ' + (objX - 6) + ',' + (objTopY + 10) + ' ' + (objX + 6) + ',' + (objTopY + 10) + '" fill="' + this.color + '"/>';
|
||
svg += '<text x="' + (objX - 5) + '" y="' + (objBaseY + 16) + '" text-anchor="end" font-family="JetBrains Mono,monospace" font-size="11" fill="' + this.color + '" font-weight="700">объект</text>';
|
||
/* Мнимое изображение справа от зеркала, симметрично */
|
||
const imgX = mirrorX + this.objX;
|
||
svg += '<line x1="' + imgX + '" y1="' + objBaseY + '" x2="' + imgX + '" y2="' + objTopY + '" stroke="' + this.color + '" stroke-width="2" stroke-dasharray="4 3" opacity="0.65"/>';
|
||
svg += '<polygon points="' + imgX + ',' + objTopY + ' ' + (imgX - 6) + ',' + (objTopY + 10) + ' ' + (imgX + 6) + ',' + (objTopY + 10) + '" fill="' + this.color + '" opacity="0.5"/>';
|
||
svg += '<text x="' + (imgX + 5) + '" y="' + (objBaseY + 16) + '" font-family="JetBrains Mono,monospace" font-size="11" fill="' + this.color + '" opacity="0.7">изображение</text>';
|
||
/* Лучи: 1) от верха объекта горизонтально на зеркало, отражается под тем же углом
|
||
2) от верха объекта в зеркало по диагонали, отражается симметрично */
|
||
/* Луч 1 */
|
||
svg += '<line x1="' + objX + '" y1="' + objTopY + '" x2="' + mirrorX + '" y2="' + objTopY + '" stroke="#dc2626" stroke-width="1.6"/>';
|
||
svg += '<line x1="' + mirrorX + '" y1="' + objTopY + '" x2="' + objX + '" y2="' + objTopY + '" stroke="#dc2626" stroke-width="1.6"/>';
|
||
/* Продолжение в зазеркалье (пунктир) */
|
||
svg += '<line x1="' + mirrorX + '" y1="' + objTopY + '" x2="' + imgX + '" y2="' + objTopY + '" stroke="#dc2626" stroke-width="1" stroke-dasharray="3 3"/>';
|
||
/* Луч 2: от верха к точке наблюдения слева внизу */
|
||
const eyeX = 40, eyeY = H - 50;
|
||
const hitY = objTopY + (cy - objTopY) * (mirrorX - objX) / (eyeX - objX + 2 * (mirrorX - objX));
|
||
/* Упрощённо: луч от верха объекта к зеркалу и затем к глазу */
|
||
const hitX = mirrorX;
|
||
const hitYsimple = objTopY + (cy + 30 - objTopY) * 0.3;
|
||
svg += '<line x1="' + objX + '" y1="' + objTopY + '" x2="' + hitX + '" y2="' + hitYsimple.toFixed(1) + '" stroke="#16a34a" stroke-width="1.6"/>';
|
||
svg += '<line x1="' + hitX + '" y1="' + hitYsimple.toFixed(1) + '" x2="' + eyeX + '" y2="' + eyeY + '" stroke="#16a34a" stroke-width="1.6"/>';
|
||
/* Продолжение от точки отражения в зазеркалье */
|
||
svg += '<line x1="' + hitX + '" y1="' + hitYsimple.toFixed(1) + '" x2="' + imgX + '" y2="' + objTopY + '" stroke="#16a34a" stroke-width="1" stroke-dasharray="3 3"/>';
|
||
/* Глаз */
|
||
svg += '<circle cx="' + eyeX + '" cy="' + eyeY + '" r="10" fill="#fff" stroke="#0f172a" stroke-width="1.4"/>';
|
||
svg += '<circle cx="' + eyeX + '" cy="' + eyeY + '" r="4" fill="#0f172a"/>';
|
||
svg += '<text x="' + eyeX + '" y="' + (eyeY + 20) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="10" fill="#475569">наблюдатель</text>';
|
||
/* Подпись закона */
|
||
svg += '<text x="' + (W/2) + '" y="22" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" fill="#475569" font-weight="700">∠ пад = ∠ отр · изображение мнимое, прямое, равное</text>';
|
||
svg += '</svg>';
|
||
this.el.innerHTML = svg;
|
||
}
|
||
}
|
||
P.FlatMirror = FlatMirror;
|
||
|
||
})();
|