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
This commit is contained in:
@@ -389,4 +389,210 @@ class EnergyView {
|
||||
}
|
||||
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;
|
||||
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user