Skip to content

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.

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

1import { BaseExperiment } from "../core/base-experiment";
2import 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 */
9export 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.