feat(labs): wave 2 — depth features across 6 sims

Электрические цепи (circuit):
- Индуктивность L как новый компонент (1–1000 мГн, шорт в DC, jωL в AC)
- RLC preset для демонстрации резонанса
- Осциллограф: U(t)/I(t) для выбранного компонента, 100 sample, dual-axis
- Heatmap мощности: радиальный градиент halo от blue→red пропорционально P=UI

Стереометрия 3D (stereo):
- Сечение через 3 произвольные точки: pick на гранях/рёбрах/вершинах
- Плоскость + полигон пересечения с авто-определением типа (3–6-угольник) и площадью
- Step-by-step режим: визуализация P1→линия→P2→линия→P3→плоскость→сечение
- Поддержка всех solids (включая cylinder/cone через sampling fallback)

Планиметрия (geometry):
- Задачник framework: CHALLENGES[] с setup/check функциями
- 5 стартовых задач: серединный перпендикуляр, биссектриса, описанная окружность, ГМТ, касательная
- Авто-checker: толерантности ±0.5° для углов, ±1–5% для расстояний
- UI: collapsible панель с статус-иконками, конфетти + «Молодец!» на success

Электромагнитные поля (emfield):
- Preset «Тороид»: 16+16 проводов в концентрических кольцах
- Поверхность Гаусса: draggable круг, считает Φ = q_enc/ε₀, подсвечивает охваченные заряды
- Motional EMF: draggable rod, arrow-keys управление, считает ε = ∫(v×B)·dl

Химическая песочница (chemsandbox):
- Live-overlay с уравнением реакции: молекулярное / полное ионное / сокращённое ионное
- Coverage: 49/49 молекулярных, 34/49 ионных, 36/49 сокращённых
- Auto-hide через 5 сек, fade-in animation, цветовая кодировка типов

Волны и звук (waves):
- Doppler: source+observer drag, expanding wavefronts, f_obs формула, Mach cone при v>c
- Beats: f1+f2, sum waveform с envelope, индикация f_beat=|f1-f2|
- Spectrum (DFT): N=256 samples pure JS, bar-chart с пиками и labels, «Добавить гармонику»

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-23 12:48:14 +03:00
parent 7f75c96acd
commit 8f30a8cef6
8 changed files with 2367 additions and 36 deletions
+476 -17
View File
@@ -1,7 +1,8 @@
'use strict';
/* ═══════════════════════════════════════════
WavesSim v2 — Волны и звук
WavesSim v3 — Волны и звук
Modes: transverse | longitudinal | superposition | standing
doppler | beats | spectrum
─────────────────────────────────────────── */
class WavesSim {
static BG = '#0D0D1A';
@@ -29,6 +30,25 @@ class WavesSim {
this._n = 1;
this._speed = 2.0;
/* doppler state */
this._dopSrcX = 0; this._dopSrcY = 0;
this._dopObsX = 0; this._dopObsY = 0;
this._dopRings = []; /* [{x,y,r,age}] */
this._dopDrag = null; /* 'src'|'obs'|null */
this._dopVs = 0.35; /* source speed, px/s as fraction of c_px */
this._dopDir = 1; /* +1 right, -1 left */
this._dopSrcVelX = 0;
this._dopSrcVelY = 0;
this._dopLastEmit = 0;
/* beats state */
this._beatsF1 = 440;
this._beatsF2 = 444;
/* spectrum state */
this._specComponents = []; /* [{f, A}] */
this._specNewF = 5; /* Hz of component to add (slider) */
this._resizeObs = null;
this.onUpdate = null;
}
@@ -55,6 +75,9 @@ class WavesSim {
this._mode = mode;
this._t = 0;
this._last = null;
if (mode === 'doppler') this._dopInit();
if (mode === 'spectrum' && !this._specComponents.length)
this._specComponents = [{ f: 5, A: 60 }, { f: 10, A: 30 }];
this.draw();
this._emit();
}
@@ -63,15 +86,20 @@ class WavesSim {
return { A1: this._A1, f1: this._f1, phi1: this._phi1, A2: this._A2, f2: this._f2, phi2: this._phi2,
n: this._n, speed: this._speed, mode: this._mode };
}
setParams({ A1, f1, phi1, A2, f2, phi2, n, speed } = {}) {
if (A1 !== undefined) this._A1 = Math.max(5, Math.min(90, +A1));
if (f1 !== undefined) this._f1 = Math.max(0.3, Math.min(4, +f1));
if (phi1 !== undefined) this._phi1 = +phi1;
if (A2 !== undefined) this._A2 = Math.max(5, Math.min(90, +A2));
if (f2 !== undefined) this._f2 = Math.max(0.3, Math.min(4, +f2));
if (phi2 !== undefined) this._phi2 = +phi2;
if (n !== undefined) this._n = Math.max(1, Math.min(5, Math.round(+n)));
if (speed !== undefined) this._speed = Math.max(0.3, Math.min(5, +speed));
setParams({ A1, f1, phi1, A2, f2, phi2, n, speed,
dopVs, beatsF1, beatsF2, specNewF } = {}) {
if (A1 !== undefined) this._A1 = Math.max(5, Math.min(90, +A1));
if (f1 !== undefined) this._f1 = Math.max(0.3, Math.min(4, +f1));
if (phi1 !== undefined) this._phi1 = +phi1;
if (A2 !== undefined) this._A2 = Math.max(5, Math.min(90, +A2));
if (f2 !== undefined) this._f2 = Math.max(0.3, Math.min(4, +f2));
if (phi2 !== undefined) this._phi2 = +phi2;
if (n !== undefined) this._n = Math.max(1, Math.min(5, Math.round(+n)));
if (speed !== undefined) this._speed = Math.max(0.3, Math.min(5, +speed));
if (dopVs !== undefined) this._dopVs = Math.max(0, Math.min(1.8, +dopVs));
if (beatsF1 !== undefined) this._beatsF1 = Math.max(1, Math.min(1000, +beatsF1));
if (beatsF2 !== undefined) this._beatsF2 = Math.max(1, Math.min(1000, +beatsF2));
if (specNewF !== undefined) this._specNewF = Math.max(1, Math.min(50, +specNewF));
this.draw();
this._emit();
}
@@ -103,8 +131,11 @@ class WavesSim {
_tick(ts) {
if (!this._paused) {
if (this._last !== null)
this._t += Math.min((ts - this._last) / 1000, 0.05) * this._speed;
if (this._last !== null) {
const dt = Math.min((ts - this._last) / 1000, 0.05) * this._speed;
this._t += dt;
if (this._mode === 'doppler') this._dopStep(dt);
}
this._last = ts;
this._raf = requestAnimationFrame(t => this._tick(t));
} else {
@@ -126,7 +157,10 @@ class WavesSim {
if (this._mode === 'transverse') this._transvDraw(ctx, W, H);
else if (this._mode === 'longitudinal') this._longDraw(ctx, W, H);
else if (this._mode === 'superposition') this._superDraw(ctx, W, H);
else this._standDraw(ctx, W, H);
else if (this._mode === 'standing') this._standDraw(ctx, W, H);
else if (this._mode === 'doppler') this._dopplerDraw(ctx, W, H);
else if (this._mode === 'beats') this._beatsDraw(ctx, W, H);
else if (this._mode === 'spectrum') this._spectrumDraw(ctx, W, H);
}
/* ══════════════════════════════════════
@@ -410,6 +444,363 @@ class WavesSim {
ctx.fillText('n = ' + n + ' \u03bb = 2L/' + n, PL, PT - 14);
}
/* ══════════════════════════════════════
ЭФФЕКТ ДОПЛЕРА
══════════════════════════════════════ */
_dopInit() {
const W = this._W || 600, H = this._H || 400;
this._dopSrcX = W * 0.3; this._dopSrcY = H * 0.5;
this._dopObsX = W * 0.75; this._dopObsY = H * 0.5;
this._dopRings = [];
this._dopLastEmit = 0;
this._dopDir = 1;
}
_dopStep(dt) {
const W = this._W || 600, H = this._H || 400;
/* speed in px/s: c_px ~= W*0.55 so Mach 1 is full screen width */
const c_px = W * 0.55;
const vsPx = this._dopVs * c_px;
/* move source horizontally, bounce at margins */
if (!this._dopDrag || this._dopDrag !== 'src') {
this._dopSrcX += this._dopDir * vsPx * dt;
if (this._dopSrcX > W - 30) { this._dopSrcX = W - 30; this._dopDir = -1; }
if (this._dopSrcX < 30) { this._dopSrcX = 30; this._dopDir = 1; }
}
this._dopSrcVelX = this._dopDir * vsPx;
this._dopSrcVelY = 0;
/* emit rings at source frequency f0 = _f1 */
const f0 = Math.max(0.5, this._f1);
this._dopLastEmit += dt;
const emitInterval = 1 / f0;
while (this._dopLastEmit >= emitInterval) {
this._dopLastEmit -= emitInterval;
this._dopRings.push({ x: this._dopSrcX, y: this._dopSrcY, r: 0, age: 0 });
}
/* expand rings at c_px */
const maxR = Math.sqrt(W * W + H * H);
this._dopRings = this._dopRings.filter(ring => {
ring.r += c_px * dt;
ring.age += dt;
return ring.r < maxR;
});
}
_dopplerDraw(ctx, W, H) {
if (!this._dopSrcX) this._dopInit();
const c_px = W * 0.55;
const vs = this._dopVs * c_px; /* px/s */
const f0 = Math.max(0.5, this._f1);
/* observed frequency (source moving toward/away observer) */
const dx = this._dopObsX - this._dopSrcX;
const dy = this._dopObsY - this._dopSrcY;
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
const cosAngle = dx / dist;
/* projection of source velocity onto source→observer */
const vsProj = this._dopDir * vs * cosAngle; /* +: toward obs */
const fObs = f0 * c_px / Math.max(1, c_px - vsProj);
const mach = vs / c_px;
/* draw rings */
const ringAlpha = 0.55;
ctx.save();
ctx.strokeStyle = WavesSim.C;
ctx.lineWidth = 1.5;
this._dopRings.forEach(ring => {
const a = Math.max(0, ringAlpha * (1 - ring.age * f0 * 0.5));
if (a < 0.02) return;
ctx.globalAlpha = a;
ctx.beginPath();
ctx.arc(ring.x, ring.y, ring.r, 0, Math.PI * 2);
ctx.stroke();
});
ctx.restore();
/* Mach cone if vs >= c_px */
if (mach >= 1.0) {
const sinTheta = Math.min(1, c_px / vs);
const theta = Math.asin(sinTheta);
const coneLen = W * 0.9;
const sx = this._dopSrcX, sy = this._dopSrcY;
const dir = this._dopDir;
ctx.save();
ctx.globalAlpha = 0.35;
ctx.fillStyle = WavesSim.P;
ctx.beginPath();
ctx.moveTo(sx, sy);
ctx.lineTo(sx - dir * coneLen * Math.cos(theta),
sy - coneLen * Math.sin(theta));
ctx.lineTo(sx - dir * coneLen * Math.cos(theta),
sy + coneLen * Math.sin(theta));
ctx.closePath(); ctx.fill();
ctx.restore();
ctx.save();
ctx.strokeStyle = WavesSim.P; ctx.lineWidth = 1.5; ctx.globalAlpha = 0.7;
ctx.setLineDash([6, 4]);
ctx.beginPath();
ctx.moveTo(sx, sy);
ctx.lineTo(sx - dir * coneLen * Math.cos(theta),
sy - coneLen * Math.sin(theta));
ctx.moveTo(sx, sy);
ctx.lineTo(sx - dir * coneLen * Math.cos(theta),
sy + coneLen * Math.sin(theta));
ctx.stroke();
ctx.restore();
}
/* source dot */
ctx.save();
ctx.shadowColor = WavesSim.G; ctx.shadowBlur = 18;
ctx.fillStyle = WavesSim.G;
ctx.beginPath(); ctx.arc(this._dopSrcX, this._dopSrcY, 9, 0, Math.PI * 2); ctx.fill();
ctx.restore();
ctx.fillStyle = WavesSim.BG;
ctx.font = "700 9px 'Manrope',sans-serif"; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText('S', this._dopSrcX, this._dopSrcY);
ctx.textBaseline = 'alphabetic';
/* observer dot */
ctx.save();
ctx.shadowColor = WavesSim.P; ctx.shadowBlur = 14;
ctx.fillStyle = WavesSim.P;
ctx.beginPath(); ctx.arc(this._dopObsX, this._dopObsY, 7, 0, Math.PI * 2); ctx.fill();
ctx.restore();
ctx.fillStyle = WavesSim.BG;
ctx.font = "700 9px 'Manrope',sans-serif"; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText('O', this._dopObsX, this._dopObsY);
ctx.textBaseline = 'alphabetic';
/* HUD */
const hudX = 14, hudY = 20;
ctx.fillStyle = 'rgba(13,13,26,0.72)';
ctx.beginPath(); ctx.roundRect(hudX, hudY, 178, 72, 8); ctx.fill();
ctx.font = "600 10px 'Manrope',sans-serif"; ctx.textAlign = 'left';
const rows = [
{ c: WavesSim.G, t: 'f₀ = ' + f0.toFixed(1) + ' Гц' },
{ c: WavesSim.C, t: 'fᵒᵇˢ = ' + fObs.toFixed(1) + ' Гц' },
{ c: WavesSim.P, t: 'Mach = ' + mach.toFixed(2) + (mach >= 1 ? ' [удар. волна]' : '') },
];
rows.forEach((r, i) => {
ctx.fillStyle = r.c;
ctx.fillText(r.t, hudX + 10, hudY + 18 + i * 18);
});
/* drag hint */
ctx.fillStyle = 'rgba(255,255,255,0.22)';
ctx.font = "500 9px 'Manrope',sans-serif"; ctx.textAlign = 'center';
ctx.fillText('Перетащи S (источник) или O (наблюдатель)', W / 2, H - 10);
}
/* ══════════════════════════════════════
БИЕНИЯ
══════════════════════════════════════ */
_beatsDraw(ctx, W, H) {
const PL = 48, PR = 20, PT = 60, PB = 40;
const cw = W - PL - PR;
const ch = H - PT - PB;
const cy = PT + ch / 2;
this._grid(ctx, PL, PR, PT, PB, W, H);
this._axisLine(ctx, PL, PR, PT, PB, W, H, cy);
const f1 = this._beatsF1;
const f2 = this._beatsF2;
const fBeat = Math.abs(f1 - f2);
const fAvg = (f1 + f2) / 2;
const TBeat = fBeat > 0 ? 1 / fBeat : Infinity;
/* draw time window that spans ~3 beat periods (or 0.1s if no beat) */
const winS = TBeat < Infinity ? Math.min(TBeat * 3, 2) : 0.12;
const tOff = this._t % (winS > 0 ? winS : 1); /* scroll slowly */
const A = Math.max(4, Math.min(ch / 2 - 4, 60));
/* sum waveform */
ctx.save();
ctx.shadowColor = WavesSim.P; ctx.shadowBlur = 12;
ctx.strokeStyle = WavesSim.P; ctx.lineWidth = 2;
ctx.beginPath();
for (let px = 0; px <= cw; px++) {
const t_s = (px / cw) * winS + tOff;
const y = A * Math.cos(2 * Math.PI * f1 * t_s) +
A * Math.cos(2 * Math.PI * f2 * t_s);
const py = cy - y / 2; /* /2 because sum can reach 2A */
px === 0 ? ctx.moveTo(PL + px, py) : ctx.lineTo(PL + px, py);
}
ctx.stroke(); ctx.restore();
/* envelope */
ctx.save();
ctx.strokeStyle = WavesSim.G; ctx.lineWidth = 1.4; ctx.globalAlpha = 0.6;
ctx.setLineDash([6, 4]);
for (const sign of [1, -1]) {
ctx.beginPath();
for (let px = 0; px <= cw; px++) {
const t_s = (px / cw) * winS + tOff;
const env = 2 * A * Math.abs(Math.cos(Math.PI * fBeat * t_s));
const py = cy - sign * env / 2;
px === 0 ? ctx.moveTo(PL + px, py) : ctx.lineTo(PL + px, py);
}
ctx.stroke();
}
ctx.restore();
/* individual waves (dimmed) */
const drawSingle = (f, color) => {
ctx.save();
ctx.globalAlpha = 0.3; ctx.strokeStyle = color; ctx.lineWidth = 1;
ctx.beginPath();
for (let px = 0; px <= cw; px++) {
const t_s = (px / cw) * winS + tOff;
const py = cy - A * Math.cos(2 * Math.PI * f * t_s);
px === 0 ? ctx.moveTo(PL + px, py) : ctx.lineTo(PL + px, py);
}
ctx.stroke(); ctx.restore();
};
drawSingle(f1, WavesSim.V);
drawSingle(f2, WavesSim.C);
/* HUD */
ctx.fillStyle = 'rgba(13,13,26,0.72)';
ctx.beginPath(); ctx.roundRect(PL + 6, PT - 52, 220, 48, 8); ctx.fill();
ctx.font = "600 10px 'Manrope',sans-serif"; ctx.textAlign = 'left';
const hudRows = [
{ c: WavesSim.V, t: 'f₁ = ' + f1.toFixed(1) + ' Гц' },
{ c: WavesSim.C, t: 'f₂ = ' + f2.toFixed(1) + ' Гц' },
{ c: WavesSim.G, t: 'fбиет = ' + fBeat.toFixed(2) + ' Гц Tбиет = ' + (TBeat < Infinity ? TBeat.toFixed(3) : '∞') + ' с' },
];
hudRows.forEach((r, i) => {
ctx.fillStyle = r.c;
ctx.fillText(r.t, PL + 16, PT - 36 + i * 16);
});
ctx.fillStyle = 'rgba(255,255,255,0.28)';
ctx.font = WavesSim.FONT; ctx.textAlign = 'left';
ctx.fillText('Биения: fбиет = |f₁ − f₂|', PL, H - 8);
}
/* ══════════════════════════════════════
СПЕКТР (ДПФ)
══════════════════════════════════════ */
_dft(signal) {
/* Real-valued DFT, returns magnitude array of length N/2 */
const N = signal.length;
const half = Math.floor(N / 2);
const mag = new Float32Array(half);
for (let k = 0; k < half; k++) {
let re = 0, im = 0;
const angle = (2 * Math.PI * k) / N;
for (let n = 0; n < N; n++) {
re += signal[n] * Math.cos(angle * n);
im -= signal[n] * Math.sin(angle * n);
}
mag[k] = Math.sqrt(re * re + im * im) / N;
}
return mag;
}
_spectrumDraw(ctx, W, H) {
const PL = 48, PR = 20, PT = 40, PB = 60;
const cw = W - PL - PR;
const ch = H - PT - PB;
/* build signal from components */
const N = 256;
const fs = 100; /* sample rate Hz */
const signal = new Float32Array(N);
const comps = this._specComponents;
for (let n = 0; n < N; n++) {
let val = 0;
for (const c of comps) val += (c.A / 90) * Math.cos(2 * Math.PI * c.f * n / fs + this._t);
signal[n] = val;
}
/* DFT */
const mag = this._dft(signal);
const half = mag.length;
const df = fs / N; /* Hz per bin */
const maxF = fs / 2; /* Nyquist */
/* find max for normalisation */
let maxMag = 0;
for (let k = 0; k < half; k++) if (mag[k] > maxMag) maxMag = mag[k];
if (maxMag < 1e-9) maxMag = 1;
/* axes */
this._axisLine(ctx, PL, PR, PT, PB, W, H, PT + ch);
ctx.strokeStyle = 'rgba(255,255,255,0.04)'; ctx.lineWidth = 1;
ctx.beginPath();
for (let gx = PL; gx <= PL + cw; gx += 40) { ctx.moveTo(gx, PT); ctx.lineTo(gx, PT + ch); }
for (let gy = PT; gy <= PT + ch; gy += 28) { ctx.moveTo(PL, gy); ctx.lineTo(PL + cw, gy); }
ctx.stroke();
/* frequency axis labels */
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.font = "500 9px 'Manrope',sans-serif"; ctx.textAlign = 'center';
const nLabels = Math.min(10, Math.floor(cw / 40));
for (let i = 0; i <= nLabels; i++) {
const f = (maxF * i) / nLabels;
const x = PL + cw * i / nLabels;
ctx.fillText(f.toFixed(0) + 'Hz', x, PT + ch + 14);
}
ctx.fillText('Частота', PL + cw / 2, H - 4);
ctx.save(); ctx.translate(PL - 32, PT + ch / 2); ctx.rotate(-Math.PI / 2);
ctx.fillText('Амплитуда', 0, 0); ctx.restore();
/* bars */
const barW = Math.max(1, cw / half - 1);
for (let k = 0; k < half; k++) {
const norm = mag[k] / maxMag;
const bH = norm * ch;
const bx = PL + k * (cw / half);
const color = norm > 0.5 ? WavesSim.G : WavesSim.V;
ctx.save();
if (norm > 0.5) { ctx.shadowColor = WavesSim.G; ctx.shadowBlur = 10; }
ctx.fillStyle = color;
ctx.globalAlpha = 0.3 + norm * 0.7;
ctx.fillRect(bx, PT + ch - bH, barW, bH);
ctx.restore();
}
/* label peaks — bins where this bin's magnitude > neighbors & > 5% */
const fmtF = f => f.toFixed(1) + 'Hz';
ctx.font = "700 9px 'Manrope',sans-serif"; ctx.textAlign = 'center';
for (let k = 1; k < half - 1; k++) {
if (mag[k] > mag[k - 1] && mag[k] > mag[k + 1] && mag[k] / maxMag > 0.05) {
const bx = PL + k * (cw / half) + barW / 2;
const bH = (mag[k] / maxMag) * ch;
ctx.save();
ctx.shadowColor = WavesSim.G; ctx.shadowBlur = 6;
ctx.fillStyle = WavesSim.G;
ctx.fillText(fmtF(k * df), bx, PT + ch - bH - 5);
ctx.restore();
}
}
/* components list */
const listX = PL + 6, listY = PT + 6;
ctx.fillStyle = 'rgba(13,13,26,0.7)';
ctx.beginPath();
ctx.roundRect(listX, listY, 140, Math.min(comps.length * 16 + 8, ch - 12), 6);
ctx.fill();
ctx.font = "600 9px 'Manrope',sans-serif"; ctx.textAlign = 'left';
comps.forEach((c, i) => {
ctx.fillStyle = i % 2 === 0 ? WavesSim.V : WavesSim.C;
ctx.fillText('f=' + c.f.toFixed(0) + 'Hz A=' + c.A, listX + 8, listY + 14 + i * 16);
});
ctx.fillStyle = 'rgba(255,255,255,0.28)';
ctx.font = WavesSim.FONT; ctx.textAlign = 'left';
ctx.fillText('ДПФ: N=' + N + ', fs=' + fs + 'Hz', PL, PT - 12);
}
/* ══════════════════════════════════════
ВСПОМОГАТЕЛЬНЫЕ
══════════════════════════════════════ */
@@ -452,6 +843,56 @@ class WavesSim {
ctx.textAlign = 'left'; ctx.fillText('x', W - PR + 8, H - PB + 4);
}
/* Spectrum: add a component at current _specNewF */
specAddComponent() {
const f = this._specNewF;
const A = 60;
if (this._specComponents.length < 12)
this._specComponents.push({ f, A });
this.draw();
}
/* Spectrum: clear all components */
specClear() {
this._specComponents = [];
this.draw();
}
/* Doppler: attach mouse/touch drag for source and observer */
dopAttachDrag(canvas) {
const pos = e => {
const r = canvas.getBoundingClientRect();
const src = e.touches ? e.touches[0] : e;
return { x: (src.clientX - r.left) * (this._W / r.width),
y: (src.clientY - r.top) * (this._H / r.height) };
};
const hitTest = p => {
const dS = Math.hypot(p.x - this._dopSrcX, p.y - this._dopSrcY);
const dO = Math.hypot(p.x - this._dopObsX, p.y - this._dopObsY);
if (dS < 18) return 'src';
if (dO < 18) return 'obs';
return null;
};
const start = e => {
const p = pos(e); this._dopDrag = hitTest(p);
if (this._dopDrag) e.preventDefault();
};
const move = e => {
if (!this._dopDrag) return;
e.preventDefault();
const p = pos(e);
if (this._dopDrag === 'src') { this._dopSrcX = p.x; this._dopSrcY = p.y; }
else { this._dopObsX = p.x; this._dopObsY = p.y; }
};
const end = () => { this._dopDrag = null; };
canvas.addEventListener('mousedown', start);
canvas.addEventListener('mousemove', move);
canvas.addEventListener('mouseup', end);
canvas.addEventListener('touchstart', start, { passive: false });
canvas.addEventListener('touchmove', move, { passive: false });
canvas.addEventListener('touchend', end);
}
_emit() { if (this.onUpdate) this.onUpdate(this.info()); }
}
@@ -478,9 +919,15 @@ class WavesSim {
function wavesMode(mode, btn) {
document.querySelectorAll('.wave-mode-btn').forEach(b => b.classList.remove('active'));
if (btn) btn.classList.add('active');
document.getElementById('waves-w2-section').style.display = mode === 'superposition' ? '' : 'none';
document.getElementById('waves-n-section').style.display = mode === 'standing' ? '' : 'none';
if (wavesSim) wavesSim.setMode(mode);
document.getElementById('waves-w2-section').style.display = mode === 'superposition' ? '' : 'none';
document.getElementById('waves-n-section').style.display = mode === 'standing' ? '' : 'none';
document.getElementById('waves-doppler-section').style.display = mode === 'doppler' ? '' : 'none';
document.getElementById('waves-beats-section').style.display = mode === 'beats' ? '' : 'none';
document.getElementById('waves-spectrum-section').style.display = mode === 'spectrum' ? '' : 'none';
if (wavesSim) {
wavesSim.setMode(mode);
if (mode === 'doppler') wavesSim.dopAttachDrag(document.getElementById('waves-canvas'));
}
}
function wavesParam(name, val) {
@@ -492,10 +939,22 @@ class WavesSim {
if (name === 'A2') el('waves-A2-val', v);
if (name === 'f2') el('waves-f2-val', v.toFixed(1) + ' Гц');
if (name === 'phi2') el('waves-phi2-val', v.toFixed(1));
if (name === 'speed') el('waves-speed-val', '\u00d7' + v.toFixed(1));
if (name === 'speed') el('waves-speed-val', '\u00d7' + v.toFixed(1));
if (name === 'dopVs') el('waves-dopVs-val', v.toFixed(2) + 'c');
if (name === 'beatsF1') el('waves-beatsF1-val', v.toFixed(0) + ' \u0413\u0446');
if (name === 'beatsF2') el('waves-beatsF2-val', v.toFixed(0) + ' \u0413\u0446');
if (name === 'specNewF') el('waves-specNewF-val', v.toFixed(0) + ' \u0413\u0446');
if (wavesSim) wavesSim.setParams({ [name]: v });
}
function wavesSpecAdd() {
if (wavesSim) wavesSim.specAddComponent();
}
function wavesSpecClear() {
if (wavesSim) wavesSim.specClear();
}
function wavesN(n, btn) {
document.querySelectorAll('.wave-n-btn').forEach(b => b.classList.remove('active'));
if (btn) btn.classList.add('active');