Files
Learn_System/frontend/js/phys-fx.js
T
Maxim Dolgolyov fb01e5aafb feat(phys11 W2): Глава 1 §4-§6 + Финал + ResonanceCurve/TransverseWave/LongitudinalWave
phys-fx.js (+3 компонента):
- PHYS.ResonanceCurve: график A(ω) при разных γ затухания, маркер ω₀ и текущей ω
- PHYS.TransverseWave: бегущая поперечная волна (струна) с красным маркером колеблющейся точки + скобка λ
- PHYS.LongitudinalWave: зоны сжатия/разрежения через 60 точек-молекул

physics_11_ch1.html (63→89 КБ):

§4 Резонанс:
- 2 теор. карточки (свобод./вынужд., резонанс ω≈ω₀, формула A(ω))
- Инт. 1: ResonanceCurve с ползунками γ и ω — видно как пик уменьшается с ростом затухания
- Инт. 2: верно/неверно (5)
- Инт. 3: что произойдёт (5, качели/мост Tacoma/солдатский шаг)
- Босс §4: 5 этапов, +70 XP

§5 Волны:
- 2 теор. карточки (определение, поперечные/продольные, λ=vT)
- Инт. 1: TransverseWave с 3 ползунками (A, λ, v) — красная точка показывает что частица колеблется на месте
- Инт. 2: LongitudinalWave (звук-аналог) с 2 ползунками
- Инт. 3: расчёт λ,v,T (5 input)
- Инт. 4: тип волны и свойства (5 MC)
- Босс §5: 5 этапов, +70 XP

§6 Звук:
- 2 теор. карточки (звук как продол. упруг. волна, диапазоны, громкость/высота/тембр)
- Инт. 1: LongitudinalWave (звуковая) с ползунками A, λ
- Инт. 2: расчёт λ звука в воздухе (5 input)
- Инт. 3: свойства звука (5 MC)
- Босс §6: 5 этапов, +65 XP

Финал главы 1:
- 4 интегральных босса (колебания, маятники+энергия, резонанс, волны+звук)
- Celebration: ачивка phys11_ch1_master + 100 XP бонус
- Сохранение в localStorage.physics11_achievements
2026-05-29 18:02:53 +03:00

599 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* phys-fx.js — библиотека анимированных физических симуляций для Физики 11.
*
* Архитектура:
* - Один глобальный requestAnimationFrame-цикл (Ticker).
* - Каждая симуляция — класс с методами update(dt, t), render().
* - IntersectionObserver: симуляция приостанавливается, когда уходит из viewport.
* - Чистый SVG (без Canvas, без WebGL, без зависимостей).
*
* Публичный API: window.PHYS = { util, Oscillogram, SpringMass, Pendulum, ... }.
*
* W0 — базовая инфраструктура + 3 компонента (Oscillogram, SpringMass, Pendulum).
* Расширяется в W3 (электротехника), W5-W7 (оптика), W9-W14 (кванты, ядро).
*/
(function(){
'use strict';
if (window.PHYS && window.PHYS.__installed) return;
const P = window.PHYS = window.PHYS || {};
P.__installed = true;
/* ============================================================ */
/* ГЛОБАЛЬНЫЙ ТАЙМЕР (один RAF на всю страницу) */
/* ============================================================ */
const Ticker = {
t: 0,
last: 0,
subs: new Set(),
running: false
};
function tick(ts){
if (!Ticker.running) return;
if (!Ticker.last) Ticker.last = ts;
const dt = Math.min((ts - Ticker.last) / 1000, 0.1); // защита от лагов
Ticker.last = ts;
Ticker.t += dt;
Ticker.subs.forEach(s => {
if (!s.paused) {
try { s.update(dt, Ticker.t); s.render && s.render(); }
catch(e) {}
}
});
requestAnimationFrame(tick);
}
function startTicker(){
if (Ticker.running) return;
Ticker.running = true;
Ticker.last = 0;
requestAnimationFrame(tick);
}
function stopTicker(){ Ticker.running = false; }
/* ============================================================ */
/* УТИЛИТЫ */
/* ============================================================ */
const util = P.util = {
subscribe(sim){ Ticker.subs.add(sim); startTicker(); },
unsubscribe(sim){ Ticker.subs.delete(sim); if (Ticker.subs.size === 0) stopTicker(); },
/* Создаёт IntersectionObserver, который ставит/снимает sim.paused */
observe(sim){
if (!sim.el || !window.IntersectionObserver) return;
const io = new IntersectionObserver(entries => {
entries.forEach(e => { sim.paused = !e.isIntersecting; });
}, { threshold: 0.05 });
io.observe(sim.el);
sim._io = io;
},
/* Безопасное удаление симуляции */
destroy(sim){
util.unsubscribe(sim);
if (sim._io) { try { sim._io.disconnect(); } catch(e){} sim._io = null; }
if (sim.el) sim.el.innerHTML = '';
},
/* Хелпер: создать SVG-обёртку с осями для графика */
svgFrame(w, h, opts){
opts = opts || {};
const bg = opts.bg || '#fafafa';
const border = opts.border || '1px solid #e2e8f0';
return '<svg viewBox="0 0 '+w+' '+h+'" preserveAspectRatio="xMidYMid meet" '
+ 'style="width:100%;height:auto;display:block;background:'+bg
+ ';border:'+border+';border-radius:10px">';
},
/* Двухмерные оси t (горизонтально) и y (вертикально). Возвращает функции toX/toY */
axes(W, H, pad, tMax, yRange){
const left = pad, right = W - pad, top = pad, bot = H - pad;
const ux = (right - left) / tMax;
const uy = (bot - top) / (yRange[1] - yRange[0]);
function toX(t){ return left + t * ux; }
function toY(y){ return bot - (y - yRange[0]) * uy; }
/* SVG сетки + рамки */
let svg = '<g stroke="#e2e8f0" stroke-width="0.8">';
/* Вертикальные линии каждую секунду */
for (let s = 0; s <= tMax; s++) svg += '<line x1="'+toX(s)+'" y1="'+top+'" x2="'+toX(s)+'" y2="'+bot+'"/>';
/* Горизонтальные линии */
const yStep = (yRange[1] - yRange[0]) / 4;
for (let i = 0; i <= 4; i++){
const y = yRange[0] + i * yStep;
svg += '<line x1="'+left+'" y1="'+toY(y)+'" x2="'+right+'" y2="'+toY(y)+'"/>';
}
svg += '</g>';
/* Ось t */
svg += '<line x1="'+left+'" y1="'+toY(0)+'" x2="'+right+'" y2="'+toY(0)+'" stroke="#0f172a" stroke-width="1.4"/>';
/* Ось y */
svg += '<line x1="'+toX(0)+'" y1="'+top+'" x2="'+toX(0)+'" y2="'+bot+'" stroke="#0f172a" stroke-width="1.4"/>';
return { svg: svg, toX, toY, left, right, top, bot };
},
/* Создать ползунок-control под симуляцией.
opts: { label, min, max, step, value, onChange } */
slider(opts){
const id = 'sl-' + Math.random().toString(36).slice(2,7);
const html = '<label style="display:flex;align-items:center;gap:8px;font-size:.82rem;color:#475569;font-weight:600;margin:4px 8px">'
+ '<span style="min-width:90px">' + opts.label + '</span>'
+ '<input type="range" id="' + id + '" min="' + opts.min + '" max="' + opts.max + '" step="' + opts.step + '" value="' + opts.value + '" style="flex:1;min-width:80px">'
+ '<b id="' + id + '-v" style="font-family:JetBrains Mono,monospace;color:#0891b2;font-size:.86rem;min-width:50px;text-align:right">' + opts.value + (opts.unit || '') + '</b>'
+ '</label>';
return { html, id, wire(root){
const inp = root.querySelector('#' + id);
const v = root.querySelector('#' + id + '-v');
if (!inp || !v) return;
inp.addEventListener('input', () => {
const val = parseFloat(inp.value);
v.textContent = (opts.fmt ? opts.fmt(val) : val) + (opts.unit || '');
if (opts.onChange) opts.onChange(val);
});
} };
}
};
/* ============================================================ */
/* Oscillogram — гармонические колебания */
/* ============================================================ */
class Oscillogram {
constructor(container, opts){
opts = opts || {};
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
this.W = opts.width || 560;
this.H = opts.height || 200;
this.pad = opts.pad || 32;
this.tWindow = opts.tWindow || 4; // секунд видно
this.A = opts.A !== undefined ? opts.A : 1.0;
this.omega = opts.omega !== undefined ? opts.omega : 2 * Math.PI;
this.phi0 = opts.phi0 !== undefined ? opts.phi0 : 0;
this.damping = opts.damping || 0;
this.color = opts.color || '#dc2626';
this.label = opts.label || 'x(t)';
this.paused = false;
this.t = 0;
this.history = []; // [t, y] точки за последние tWindow секунд
this._render();
util.subscribe(this);
util.observe(this);
}
setA(v){ this.A = v; }
setOmega(v){ this.omega = v; }
setPhi(v){ this.phi0 = v; }
setDamping(v){ this.damping = v; }
reset(){ this.history = []; this.t = 0; }
update(dt){
this.t += dt;
const y = this.A * Math.exp(-this.damping * this.t) * Math.cos(this.omega * this.t + this.phi0);
this.history.push([this.t, y]);
while (this.history.length && this.history[0][0] < this.t - this.tWindow) this.history.shift();
}
render(){
if (!this.el) return;
const W = this.W, H = this.H, pad = this.pad;
const tMin = Math.max(0, this.t - this.tWindow);
const yRange = [-Math.max(1.05, this.A * 1.1), Math.max(1.05, this.A * 1.1)];
const ax = util.axes(W, H, pad, this.tWindow, yRange);
let polyline = '';
if (this.history.length > 1){
const pts = this.history.map(([t, y]) => (ax.left + (t - tMin) * (ax.right - ax.left) / this.tWindow).toFixed(1) + ',' + ax.toY(y).toFixed(1));
polyline = '<polyline points="' + pts.join(' ') + '" fill="none" stroke="' + this.color + '" stroke-width="2.4" stroke-linejoin="round"/>';
}
/* Подпись y(t) */
const titleSvg = '<text x="' + (W - pad) + '" y="' + (pad - 8) + '" text-anchor="end" font-size="12" font-family="JetBrains Mono,monospace" fill="' + this.color + '" font-weight="700">' + this.label + '</text>';
const svg = util.svgFrame(W, H) + ax.svg + polyline + titleSvg + '</svg>';
this.el.innerHTML = svg;
}
_render(){ this.render(); }
}
P.Oscillogram = Oscillogram;
/* ============================================================ */
/* SpringMass — пружинный маятник (вертикальный) */
/* ============================================================ */
class SpringMass {
constructor(container, opts){
opts = opts || {};
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
this.W = opts.width || 240;
this.H = opts.height || 280;
this.m = opts.m !== undefined ? opts.m : 0.5; // кг
this.k = opts.k !== undefined ? opts.k : 20; // Н
this.A = opts.A !== undefined ? opts.A : 0.06; // м (амплитуда)
this.color = opts.color || '#0891b2';
this.paused = false;
this.t = 0;
this._render();
util.subscribe(this);
util.observe(this);
}
setMass(m){ this.m = Math.max(0.05, m); }
setStiffness(k){ this.k = Math.max(1, k); }
setAmplitude(A){ this.A = Math.max(0.005, A); }
period(){ return 2 * Math.PI * Math.sqrt(this.m / this.k); }
freq(){ return 1 / this.period(); }
update(dt){ this.t += dt; }
render(){
if (!this.el) return;
const W = this.W, H = this.H;
const T = this.period();
const omega = 2 * Math.PI / T;
const A_px = 60; /* визуальная амплитуда в px */
const y0 = 90; /* y-координата равновесия груза в px */
const yCur = y0 + A_px * Math.cos(omega * this.t);
/* Пружина: гармошка-зигзаг от крюка (y=20) до груза (y=yCur-18) */
const cx = W / 2, hookY = 20, massY = yCur, massR = 22;
const coils = 10;
const springTop = hookY;
const springBot = massY - massR;
const segH = (springBot - springTop) / (coils * 2);
let path = 'M ' + cx + ' ' + springTop;
for (let i = 0; i < coils; i++){
path += ' L ' + (cx - 14) + ' ' + (springTop + segH * (2 * i + 1));
path += ' L ' + (cx + 14) + ' ' + (springTop + segH * (2 * i + 2));
}
path += ' L ' + cx + ' ' + springBot;
/* Линейка справа */
const ruler = '<g stroke="#cbd5e1" stroke-width="1" font-family="JetBrains Mono,monospace" font-size="10" fill="#64748b">'
+ '<line x1="' + (W - 38) + '" y1="' + (y0 - A_px) + '" x2="' + (W - 38) + '" y2="' + (y0 + A_px) + '" stroke-width="1.6"/>'
+ '<line x1="' + (W - 44) + '" y1="' + (y0 - A_px) + '" x2="' + (W - 32) + '" y2="' + (y0 - A_px) + '"/>'
+ '<text x="' + (W - 26) + '" y="' + (y0 - A_px + 4) + '">+A</text>'
+ '<line x1="' + (W - 44) + '" y1="' + y0 + '" x2="' + (W - 32) + '" y2="' + y0 + '" stroke="#0f172a" stroke-width="1.4"/>'
+ '<text x="' + (W - 26) + '" y="' + (y0 + 4) + '">0</text>'
+ '<line x1="' + (W - 44) + '" y1="' + (y0 + A_px) + '" x2="' + (W - 32) + '" y2="' + (y0 + A_px) + '"/>'
+ '<text x="' + (W - 26) + '" y="' + (y0 + A_px + 4) + '">-A</text>'
+ '</g>';
/* Период справа сверху */
const Tlabel = '<text x="12" y="20" font-family="JetBrains Mono,monospace" font-size="12" fill="' + this.color + '" font-weight="700">T = ' + T.toFixed(2) + ' с</text>';
const svg = util.svgFrame(W, H, {bg:'#f8fafc'})
+ '<line x1="0" y1="' + (hookY - 6) + '" x2="' + W + '" y2="' + (hookY - 6) + '" stroke="#334155" stroke-width="3"/>'
+ '<g stroke="#334155" stroke-width="3" fill="none" stroke-linejoin="round" stroke-linecap="round">'
+ '<path d="' + path + '"/>'
+ '</g>'
+ '<rect x="' + (cx - massR) + '" y="' + (massY - massR) + '" width="' + (2 * massR) + '" height="' + (2 * massR) + '" rx="6" fill="' + this.color + '" stroke="#0f172a" stroke-width="1.6"/>'
+ '<text x="' + cx + '" y="' + (massY + 5) + '" text-anchor="middle" font-family="Outfit,sans-serif" font-size="14" font-weight="800" fill="#fff">m</text>'
+ ruler + Tlabel
+ '</svg>';
this.el.innerHTML = svg;
}
_render(){ this.render(); }
}
P.SpringMass = SpringMass;
/* ============================================================ */
/* Pendulum — математический маятник */
/* ============================================================ */
class Pendulum {
constructor(container, opts){
opts = opts || {};
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
this.W = opts.width || 240;
this.H = opts.height || 260;
this.l = opts.l !== undefined ? opts.l : 1.0; // м
this.g = opts.g !== undefined ? opts.g : 9.81;
this.theta0 = opts.theta0 !== undefined ? opts.theta0 : Math.PI / 12; // начальный угол (рад)
this.color = opts.color || '#0891b2';
this.paused = false;
this.t = 0;
this._render();
util.subscribe(this);
util.observe(this);
}
setLength(l){ this.l = Math.max(0.1, l); }
setG(g){ this.g = Math.max(0.5, g); }
setTheta0(theta){ this.theta0 = Math.max(0.02, Math.min(Math.PI/4, theta)); }
period(){ return 2 * Math.PI * Math.sqrt(this.l / this.g); }
update(dt){ this.t += dt; }
render(){
if (!this.el) return;
const W = this.W, H = this.H;
const T = this.period();
const omega = 2 * Math.PI / T;
const theta = this.theta0 * Math.cos(omega * this.t);
const cx = W / 2, hookY = 20;
const Lpx = Math.min(160, H - 70);
const bobR = 18;
const bx = cx + Lpx * Math.sin(theta);
const by = hookY + Lpx * Math.cos(theta);
/* Дуга-траектория */
const arcR = Lpx;
const arcStart = -this.theta0;
const arcEnd = this.theta0;
const aS = { x: cx + arcR * Math.sin(arcStart), y: hookY + arcR * Math.cos(arcStart) };
const aE = { x: cx + arcR * Math.sin(arcEnd), y: hookY + arcR * Math.cos(arcEnd) };
const largeArc = (arcEnd - arcStart) > Math.PI ? 1 : 0;
const sweep = 1;
const arc = '<path d="M ' + aS.x.toFixed(1) + ' ' + aS.y.toFixed(1) + ' A ' + arcR + ' ' + arcR + ' 0 ' + largeArc + ' ' + sweep + ' ' + aE.x.toFixed(1) + ' ' + aE.y.toFixed(1) + '" fill="none" stroke="#cbd5e1" stroke-width="1.4" stroke-dasharray="4 4"/>';
/* Вертикальная пунктирная ось */
const vert = '<line x1="' + cx + '" y1="' + hookY + '" x2="' + cx + '" y2="' + (hookY + Lpx + 5) + '" stroke="#cbd5e1" stroke-width="1" stroke-dasharray="3 3"/>';
/* Подвес */
const string = '<line x1="' + cx + '" y1="' + hookY + '" x2="' + bx.toFixed(1) + '" y2="' + by.toFixed(1) + '" stroke="#0f172a" stroke-width="2"/>';
const bob = '<circle cx="' + bx.toFixed(1) + '" cy="' + by.toFixed(1) + '" r="' + bobR + '" fill="' + this.color + '" stroke="#0f172a" stroke-width="1.6"/>';
/* Период */
const Tlabel = '<text x="12" y="20" font-family="JetBrains Mono,monospace" font-size="12" fill="' + this.color + '" font-weight="700">T = ' + T.toFixed(2) + ' с</text>';
/* Подвес-крепление */
const hook = '<line x1="' + (cx - 30) + '" y1="' + (hookY - 6) + '" x2="' + (cx + 30) + '" y2="' + (hookY - 6) + '" stroke="#334155" stroke-width="3"/>';
const svg = util.svgFrame(W, H, {bg:'#f8fafc'}) + hook + vert + arc + string + bob + Tlabel + '</svg>';
this.el.innerHTML = svg;
}
_render(){ this.render(); }
}
P.Pendulum = Pendulum;
/* ============================================================ */
/* EnergyView — превращения энергии при гарм. колебаниях */
/* Показывает W_к, W_п, W_мех=const на одном графике */
/* ============================================================ */
class EnergyView {
constructor(container, opts){
opts = opts || {};
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
this.W = opts.width || 560;
this.H = opts.height || 240;
this.pad = opts.pad || 36;
this.A = opts.A !== undefined ? opts.A : 1.0;
this.omega = opts.omega !== undefined ? opts.omega : 2 * Math.PI;
this.tWindow = opts.tWindow || 4;
this.paused = false;
this.t = 0;
this.history = []; // [t, Wk, Wp]
util.subscribe(this);
util.observe(this);
this.render();
}
setA(v){ this.A = v; this.history = []; }
setOmega(v){ this.omega = v; this.history = []; }
update(dt){
this.t += dt;
/* Для x = A cos(ωt): v = -Aω sin(ωt)
W_к = m v² / 2 = (1/2) m A² ω² sin²(ωt)
W_п = k x² / 2 = (1/2) m ω² · A² cos²(ωt) (k = m ω²)
В безразмерных: положим (1/2)mω²A² = 1 — тогда обе варьируются 0..1, сумма = 1 */
const c = Math.cos(this.omega * this.t);
const s = Math.sin(this.omega * this.t);
const Wp = c * c;
const Wk = s * s;
this.history.push([this.t, Wk, Wp]);
while (this.history.length && this.history[0][0] < this.t - this.tWindow) this.history.shift();
}
render(){
if (!this.el) return;
const W = this.W, H = this.H, pad = this.pad;
const tMin = Math.max(0, this.t - this.tWindow);
const yRange = [0, 1.1];
const ax = util.axes(W, H, pad, this.tWindow, yRange);
function path(idx, color, label){
if (this.history.length < 2) return '';
const pts = this.history.map(p => (ax.left + (p[0] - tMin) * (ax.right - ax.left) / this.tWindow).toFixed(1) + ',' + ax.toY(p[idx]).toFixed(1));
return '<polyline points="' + pts.join(' ') + '" fill="none" stroke="' + color + '" stroke-width="2.2" stroke-linejoin="round"/>';
}
const pK = path.call(this, 1, '#dc2626');
const pP = path.call(this, 2, '#16a34a');
/* W_мех = const = 1 (горизонтальная линия) */
const pM = '<line x1="' + ax.left + '" y1="' + ax.toY(1) + '" x2="' + ax.right + '" y2="' + ax.toY(1) + '" stroke="#7c3aed" stroke-width="2.4" stroke-dasharray="6 4"/>';
/* Легенда */
const legend = '<g font-family="JetBrains Mono,monospace" font-size="11" font-weight="700">'
+ '<rect x="' + (W - 130) + '" y="' + (pad - 28) + '" width="118" height="74" fill="#fff" stroke="#e2e8f0" rx="6"/>'
+ '<line x1="' + (W - 122) + '" y1="' + (pad - 14) + '" x2="' + (W - 102) + '" y2="' + (pad - 14) + '" stroke="#dc2626" stroke-width="2.4"/>'
+ '<text x="' + (W - 96) + '" y="' + (pad - 10) + '" fill="#dc2626">W кинет.</text>'
+ '<line x1="' + (W - 122) + '" y1="' + (pad + 6) + '" x2="' + (W - 102) + '" y2="' + (pad + 6) + '" stroke="#16a34a" stroke-width="2.4"/>'
+ '<text x="' + (W - 96) + '" y="' + (pad + 10) + '" fill="#16a34a">W потенц.</text>'
+ '<line x1="' + (W - 122) + '" y1="' + (pad + 26) + '" x2="' + (W - 102) + '" y2="' + (pad + 26) + '" stroke="#7c3aed" stroke-width="2.4" stroke-dasharray="4 3"/>'
+ '<text x="' + (W - 96) + '" y="' + (pad + 30) + '" fill="#7c3aed">W мех = const</text>'
+ '</g>';
const svg = util.svgFrame(W, H) + ax.svg + pM + pK + pP + legend + '</svg>';
this.el.innerHTML = svg;
}
}
P.EnergyView = EnergyView;
/* ============================================================ */
/* ResonanceCurve — резонансная кривая A(ω) при разных γ */
/* ============================================================ */
class ResonanceCurve {
constructor(container, opts){
opts = opts || {};
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
this.W = opts.width || 540;
this.H = opts.height || 240;
this.pad = opts.pad || 40;
this.omega0 = opts.omega0 || 1.0; /* собственная частота (норм.) */
this.gamma = opts.gamma !== undefined ? opts.gamma : 0.15;
this.omegaCur = opts.omegaCur !== undefined ? opts.omegaCur : 0.6;
this.color = opts.color || '#7c3aed';
this.paused = false;
this.render();
}
setGamma(g){ this.gamma = Math.max(0.02, g); this.render(); }
setOmegaCur(w){ this.omegaCur = Math.max(0.02, w); this.render(); }
/* update не нужен — статический график, обновляется по setter */
update(){}
render(){
if (!this.el) return;
const W = this.W, H = this.H, pad = this.pad;
const wMin = 0, wMax = 2 * this.omega0;
/* Подсчитаем все амплитуды чтобы знать max */
function amp(w, g, w0){
const dw2 = (w0 * w0 - w * w);
const denom = Math.sqrt(dw2 * dw2 + (2 * g * w) * (2 * g * w));
return 1 / Math.max(denom, 1e-6);
}
const gMin = 0.05;
const ampMax = amp(this.omega0, gMin, this.omega0) * 1.1;
/* Сетка */
const left = pad, right = W - pad, top = pad, bot = H - pad;
const ux = (right - left) / (wMax - wMin);
const uy = (bot - top) / ampMax;
function toX(w){ return left + (w - wMin) * ux; }
function toY(a){ return bot - a * uy; }
let svg = util.svgFrame(W, H);
/* Линии сетки */
svg += '<g stroke="#e2e8f0" stroke-width="0.8">';
for (let i = 0; i <= 4; i++){
const w = wMin + (wMax - wMin) * i / 4;
svg += '<line x1="' + toX(w) + '" y1="' + top + '" x2="' + toX(w) + '" y2="' + bot + '"/>';
}
for (let i = 0; i <= 4; i++){
svg += '<line x1="' + left + '" y1="' + (top + (bot - top) * i / 4) + '" x2="' + right + '" y2="' + (top + (bot - top) * i / 4) + '"/>';
}
svg += '</g>';
/* Оси */
svg += '<line x1="' + left + '" y1="' + bot + '" x2="' + right + '" y2="' + bot + '" stroke="#0f172a" stroke-width="1.4"/>';
svg += '<line x1="' + left + '" y1="' + top + '" x2="' + left + '" y2="' + bot + '" stroke="#0f172a" stroke-width="1.4"/>';
/* Подписи осей */
svg += '<text x="' + (right - 4) + '" y="' + (bot - 6) + '" font-family="JetBrains Mono,monospace" font-size="11" fill="#64748b" text-anchor="end">ω</text>';
svg += '<text x="' + (left + 4) + '" y="' + (top + 12) + '" font-family="JetBrains Mono,monospace" font-size="11" fill="#64748b">A</text>';
/* Линия ω₀ — собственная частота */
svg += '<line x1="' + toX(this.omega0) + '" y1="' + top + '" x2="' + toX(this.omega0) + '" y2="' + bot + '" stroke="#94a3b8" stroke-width="1.2" stroke-dasharray="4 4"/>';
svg += '<text x="' + toX(this.omega0) + '" y="' + (top - 4) + '" font-family="JetBrains Mono,monospace" font-size="10" fill="#64748b" text-anchor="middle">ω₀</text>';
/* Кривая A(ω) */
let path = 'M ';
const N = 200;
for (let i = 0; i <= N; i++){
const w = wMin + (wMax - wMin) * i / N;
const a = amp(w, this.gamma, this.omega0);
path += toX(w).toFixed(1) + ',' + toY(Math.min(a, ampMax)).toFixed(1);
if (i < N) path += ' L ';
}
svg += '<path d="' + path + '" fill="none" stroke="' + this.color + '" stroke-width="2.6" stroke-linejoin="round"/>';
/* Точка-маркер на текущей ω */
const aCur = Math.min(amp(this.omegaCur, this.gamma, this.omega0), ampMax);
svg += '<line x1="' + toX(this.omegaCur) + '" y1="' + toY(0) + '" x2="' + toX(this.omegaCur) + '" y2="' + toY(aCur) + '" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="3 3"/>';
svg += '<circle cx="' + toX(this.omegaCur) + '" cy="' + toY(aCur) + '" r="6" fill="#dc2626" stroke="#fff" stroke-width="2"/>';
/* Подпись γ */
svg += '<text x="' + (left + 10) + '" y="' + (top + 16) + '" font-family="JetBrains Mono,monospace" font-size="12" fill="' + this.color + '" font-weight="700">γ = ' + this.gamma.toFixed(2) + '</text>';
svg += '<text x="' + (left + 10) + '" y="' + (top + 32) + '" font-family="JetBrains Mono,monospace" font-size="12" fill="#dc2626" font-weight="700">ω = ' + this.omegaCur.toFixed(2) + ' · A = ' + aCur.toFixed(2) + '</text>';
svg += '</svg>';
this.el.innerHTML = svg;
}
}
P.ResonanceCurve = ResonanceCurve;
/* ============================================================ */
/* TransverseWave — поперечная волна на струне */
/* y(x, t) = A sin(kx - ωt + φ) */
/* ============================================================ */
class TransverseWave {
constructor(container, opts){
opts = opts || {};
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
this.W = opts.width || 560;
this.H = opts.height || 180;
this.A = opts.A !== undefined ? opts.A : 0.4; /* отн. амплитуда (0..1) */
this.lambda = opts.lambda !== undefined ? opts.lambda : 1.0; /* отн. длина волны */
this.v = opts.v !== undefined ? opts.v : 0.8; /* скорость распространения (отн./с) */
this.color = opts.color || '#0891b2';
this.markerX = opts.markerX !== undefined ? opts.markerX : 0.4; /* пол. красной точки (0..1 от ширины) */
this.paused = false;
this.t = 0;
util.subscribe(this);
util.observe(this);
this.render();
}
setA(v){ this.A = v; }
setLambda(v){ this.lambda = Math.max(0.1, v); }
setV(v){ this.v = v; }
update(dt){ this.t += dt; }
render(){
if (!this.el) return;
const W = this.W, H = this.H;
const yCenter = H / 2;
const amp = this.A * (H / 2 - 18);
const k = 2 * Math.PI / this.lambda;
const omega = k * this.v;
/* SVG: горизонтальная ось + волна как polyline */
let svg = util.svgFrame(W, H, {bg:'#f8fafc'});
/* Ось */
svg += '<line x1="0" y1="' + yCenter + '" x2="' + W + '" y2="' + yCenter + '" stroke="#cbd5e1" stroke-width="1" stroke-dasharray="4 3"/>';
/* Кривая */
const N = 180;
let path = 'M ';
for (let i = 0; i <= N; i++){
const px = (W * i / N);
/* Реальное x в относительных единицах (1 длина волны на ~120px) */
const x = px / 120;
const y = yCenter - amp * Math.sin(k * x - omega * this.t);
path += px.toFixed(1) + ',' + y.toFixed(1);
if (i < N) path += ' L ';
}
svg += '<path d="' + path + '" fill="none" stroke="' + this.color + '" stroke-width="2.6" stroke-linejoin="round"/>';
/* Красный маркер — колеблющаяся точка */
const mPx = this.markerX * W;
const mX = mPx / 120;
const mY = yCenter - amp * Math.sin(k * mX - omega * this.t);
svg += '<line x1="' + mPx + '" y1="' + (yCenter - amp) + '" x2="' + mPx + '" y2="' + (yCenter + amp) + '" stroke="#fca5a5" stroke-width="1" stroke-dasharray="3 3"/>';
svg += '<circle cx="' + mPx + '" cy="' + mY.toFixed(1) + '" r="7" fill="#dc2626" stroke="#fff" stroke-width="2"/>';
/* Метка λ — горизонтальная скобка над волной */
const lambdaPx = 120 * this.lambda;
if (lambdaPx < W - 60){
const lxStart = 20, lxEnd = lxStart + lambdaPx;
svg += '<line x1="' + lxStart + '" y1="20" x2="' + lxEnd + '" y2="20" stroke="#7c3aed" stroke-width="1.6"/>';
svg += '<line x1="' + lxStart + '" y1="14" x2="' + lxStart + '" y2="26" stroke="#7c3aed" stroke-width="1.6"/>';
svg += '<line x1="' + lxEnd + '" y1="14" x2="' + lxEnd + '" y2="26" stroke="#7c3aed" stroke-width="1.6"/>';
svg += '<text x="' + ((lxStart + lxEnd) / 2) + '" y="14" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" font-weight="700" fill="#7c3aed">λ</text>';
}
svg += '</svg>';
this.el.innerHTML = svg;
}
}
P.TransverseWave = TransverseWave;
/* ============================================================ */
/* LongitudinalWave — продольная волна (сжатия/разрежения) */
/* ============================================================ */
class LongitudinalWave {
constructor(container, opts){
opts = opts || {};
this.el = (typeof container === 'string') ? document.querySelector(container) : container;
this.W = opts.width || 560;
this.H = opts.height || 130;
this.A = opts.A !== undefined ? opts.A : 0.5; /* амплитуда (0..1) */
this.lambda = opts.lambda !== undefined ? opts.lambda : 1.0;
this.v = opts.v !== undefined ? opts.v : 0.8;
this.color = opts.color || '#0891b2';
this.nDots = opts.nDots || 60;
this.paused = false;
this.t = 0;
util.subscribe(this);
util.observe(this);
this.render();
}
setA(v){ this.A = v; }
setLambda(v){ this.lambda = Math.max(0.1, v); }
setV(v){ this.v = v; }
update(dt){ this.t += dt; }
render(){
if (!this.el) return;
const W = this.W, H = this.H;
const yC = H / 2;
const k = 2 * Math.PI / this.lambda;
const omega = k * this.v;
const xScale = 120; /* px на 1 ед. */
const amp = this.A * 10; /* px смещения */
const margin = 20;
let svg = util.svgFrame(W, H, {bg:'#f8fafc'});
/* Точки-молекулы */
let dots = '';
for (let i = 0; i < this.nDots; i++){
const x0 = margin + (W - 2 * margin) * i / (this.nDots - 1);
const xRel = x0 / xScale;
const disp = amp * Math.sin(k * xRel - omega * this.t);
const x = x0 + disp;
dots += '<circle cx="' + x.toFixed(1) + '" cy="' + yC + '" r="3" fill="' + this.color + '"/>';
}
svg += dots;
/* Подписи зон сжатия / разрежения */
svg += '<text x="' + (W / 2) + '" y="' + (H - 6) + '" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="10" fill="#94a3b8">сжатие &harr; разрежение</text>';
svg += '</svg>';
this.el.innerHTML = svg;
}
}
P.LongitudinalWave = LongitudinalWave;
})();