Wave Superposition
Add two sines, get beats or silence.
Beginner / waves / superposition
Two traveling waves with independent frequency, amplitude, and phase are added point by point - nothing more. Close frequencies produce beats at their difference; matched amplitudes at opposite phases cancel to a flat line. The whole of wave interference is in that one addition.
Tagged: beats, interference, phase, standing waves
Live simulation
Frequencies close together produce beats at their difference. Keyboard: Space runs or pauses, . steps, r resets.
Speed
Frequencies close together produce beats at their difference.
Mathematics
Waves add by superposition: the displacement of the sum is the sum of the displacements, point by point. Two traveling waves y_i = A_i sin(2 pi (f_i t - f_i x / v)) sum to beats when frequencies are close, to a standing pattern when they match, and to cancellation when equal and opposite.
y(x, t) = A1 sin(2 pi f1 (t - x/v)) + A2 sin(2 pi f2 (t - x/v))
beat frequency: |f1 - f2|
envelope bounds: |A1 - A2| <= |y| <= A1 + A2
Nothing is simulated here beyond addition - which is the point. The beat envelope visible when the frequencies differ by a fraction of a hertz is not a modeled effect; it falls out of the identity sin A + sin B = 2 cos((A-B)/2) sin((A+B)/2). The metrics show the predicted beat frequency and the envelope bounds from the amplitudes.
How it works
Each frame, three curves are evaluated sample-by-sample: the two component waves in their own colors and their sum in accent, all from the same function of (x, t). The time slider advances the waves' phase; zero speed freezes the pattern for reading it.
Set amplitudes equal and frequencies equal with one phase flipped conceptually by sliding... actually simply equalize frequencies to see a pure standing wave shape sliding.
Implementation
src/engine/experiments/wave-superposition.ts · 99 lines · the module running above
TypeScript · fixed-timestep engine
| 1 | import { BaseExperiment } from "../core/base-experiment"; |
| 2 | import type { ParameterDef, ThemeColors } from "../core/types"; |
| 3 | |
| 4 | /** |
| 5 | * Superposition of two traveling waves with independent frequency, |
| 6 | * amplitude, and phase: sum them and see beats, standing-wave patterns, or |
| 7 | * near-cancellation - pure addition, drawn point by point. |
| 8 | */ |
| 9 | export class WaveSuperpositionExperiment extends BaseExperiment { |
| 10 | readonly id = "wave-superposition"; |
| 11 | |
| 12 | protected params(): ParameterDef[] { |
| 13 | return [ |
| 14 | { key: "f1", label: "Wave 1 frequency", min: 0.5, max: 8, step: 0.1, defaultValue: 2, unit: "Hz" }, |
| 15 | { key: "a1", label: "Wave 1 amplitude", min: 0, max: 1, step: 0.05, defaultValue: 0.6 }, |
| 16 | { key: "f2", label: "Wave 2 frequency", min: 0.5, max: 8, step: 0.1, defaultValue: 2.4, unit: "Hz" }, |
| 17 | { key: "a2", label: "Wave 2 amplitude", min: 0, max: 1, step: 0.05, defaultValue: 0.5 }, |
| 18 | { key: "phase2", label: "Wave 2 phase", min: 0, max: 6.28, step: 0.1, defaultValue: 0.8, unit: "rad" }, |
| 19 | { key: "speed", label: "Time speed", min: 0, max: 3, step: 0.1, defaultValue: 1 }, |
| 20 | ]; |
| 21 | } |
| 22 | |
| 23 | private time = 0; |
| 24 | |
| 25 | protected onReset(): void { |
| 26 | this.time = 0; |
| 27 | } |
| 28 | |
| 29 | protected onUpdate(dt: number): void { |
| 30 | this.time += dt * this.num("speed"); |
| 31 | } |
| 32 | |
| 33 | private wave(frequency: number, amplitude: number, phase: number, x: number, t: number): number { |
| 34 | return amplitude * Math.sin(2 * Math.PI * (frequency * t - frequency * 0.22 * x) + phase); |
| 35 | } |
| 36 | |
| 37 | render(ctx: CanvasRenderingContext2D, theme: ThemeColors): void { |
| 38 | ctx.fillStyle = theme.bg; |
| 39 | ctx.fillRect(0, 0, this.width, this.height); |
| 40 | const mid = this.height / 2; |
| 41 | const amp = this.height / 4.4; |
| 42 | ctx.strokeStyle = theme.line; |
| 43 | ctx.beginPath(); |
| 44 | ctx.moveTo(0, mid); |
| 45 | ctx.lineTo(this.width, mid); |
| 46 | ctx.stroke(); |
| 47 | const draw = (fn: (x: number) => number, color: string, width: number) => { |
| 48 | ctx.strokeStyle = color; |
| 49 | ctx.lineWidth = width; |
| 50 | ctx.beginPath(); |
| 51 | for (let px = 0; px <= this.width; px += 1) { |
| 52 | const x = px / this.width; |
| 53 | const y = mid - fn(x) * amp; |
| 54 | if (px === 0) ctx.moveTo(px, y); |
| 55 | else ctx.lineTo(px, y); |
| 56 | } |
| 57 | ctx.stroke(); |
| 58 | }; |
| 59 | const f1 = this.num("f1"); |
| 60 | const f2 = this.num("f2"); |
| 61 | const a1 = this.num("a1"); |
| 62 | const a2 = this.num("a2"); |
| 63 | const phase2 = this.num("phase2"); |
| 64 | draw((x) => this.wave(f1, a1, 0, x, this.time), theme.viz[1], 1.2); |
| 65 | draw((x) => this.wave(f2, a2, phase2, x, this.time), theme.viz[2], 1.2); |
| 66 | draw((x) => this.wave(f1, a1, 0, x, this.time) + this.wave(f2, a2, phase2, x, this.time), theme.accent, 2.2); |
| 67 | ctx.font = "11px ui-monospace, monospace"; |
| 68 | ctx.fillStyle = theme.viz[1]; |
| 69 | ctx.fillText(`WAVE 1 ${f1.toFixed(1)} Hz`, 10, 14); |
| 70 | ctx.fillStyle = theme.viz[2]; |
| 71 | ctx.fillText(`WAVE 2 ${f2.toFixed(1)} Hz`, 120, 14); |
| 72 | ctx.fillStyle = theme.accent; |
| 73 | ctx.fillText("SUM", 232, 14); |
| 74 | ctx.lineWidth = 1; |
| 75 | } |
| 76 | |
| 77 | getMetrics() { |
| 78 | const f1 = this.num("f1"); |
| 79 | const f2 = this.num("f2"); |
| 80 | return { |
| 81 | beatFrequency: Math.abs(f1 - f2).toFixed(2), |
| 82 | phaseOffset: `${(this.time * (f2 - f1) * 360 % 360).toFixed(0)} deg`, |
| 83 | maxAmplitude: (this.num("a1") + this.num("a2")).toFixed(2), |
| 84 | minAmplitude: Math.abs(this.num("a1") - this.num("a2")).toFixed(2), |
| 85 | }; |
| 86 | } |
| 87 | |
| 88 | describe(): string { |
| 89 | const beat = Math.abs(this.num("f1") - this.num("f2")); |
| 90 | return beat < 0.3 |
| 91 | ? `Two close frequencies (${this.num("f1").toFixed(1)} and ${this.num("f2").toFixed(1)} Hz) sum into a beat pattern with envelope ${beat.toFixed(2)} Hz.` |
| 92 | : `Waves at ${this.num("f1").toFixed(1)} and ${this.num("f2").toFixed(1)} Hz superpose; where they cancel, the sum touches zero.`; |
| 93 | } |
| 94 | |
| 95 | entities(): number { |
| 96 | return 2; |
| 97 | } |
| 98 | } |
| 99 | |
Performance
- Time per tick
- O(width) evaluations per frame
- Space
- None
- Notes
- Interference is arithmetic. Every interference pattern in physics is this addition wearing different constants.
Live numbers (FPS, frame time, steps per second, microseconds per step, entity count) are in the performance overlay on the simulation above: the pulse icon in its top-right corner.