Spring Simulation
Build a Hooke's Law spring-mass system with force vectors, energy bars, and resonance driving.
Paste each step into main.js in the editor, then click Restart.
Step 1: Basic Spring-Mass
A mass attached to a spring, oscillating horizontally.
// 1. State Setup
if (!state.init) {
state.x = 150; // displacement from equilibrium
state.v = 0; // velocity
state.k = 25; // spring constant (N/m)
state.m = 1.0; // mass (kg)
state.damping = 0; // damping coefficient
state.init = true;
}
// 2. Physics Step — Hooke's Law: F = -kx
const F_spring = -state.k * state.x;
const F_damp = -state.damping * state.v;
const F_total = F_spring + F_damp;
const a = F_total / state.m;
state.v += a * dt;
state.x += state.v * dt;
// Visual positions
const anchorX = w * 0.3;
const centerY = h * 0.5;
const massX = anchorX + state.x;
// 3. Render Return — one lines array (duplicate keys would overwrite)
return {
lines: [
{ x1: anchorX - 30, y1: centerY - 30, x2: anchorX - 30, y2: centerY + 30, color: '#64748b', width: 3 },
{ x1: anchorX - 30, y1: centerY, x2: massX, y2: centerY, color: '#F59E0B', width: 2 },
{ x1: anchorX, y1: centerY - 25, x2: anchorX, y2: centerY + 25, color: 'rgba(255,255,255,0.15)', width: 1, dashed: true },
],
particles: [
{ x: massX, y: centerY, r: 14, color: '#8B5CF6', label: 'm' },
],
vectors: [
{ x: F_spring * 0.05, y: 0, ox: massX, oy: centerY - 20, color: '#EF4444', label: 'F' },
{ x: state.v * 0.3, y: 0, ox: massX, oy: centerY + 20, color: '#22C55E', label: 'v' },
],
text: {
x: state.x.toFixed(2) + ' px',
v: state.v.toFixed(2),
a: a.toFixed(2),
F: F_spring.toFixed(1),
},
};📐 MATH CHECK Hooke's Law: F = -kx Natural frequency: ω₀ = sqrt(k/m) = sqrt(25/1) = 5 rad/s Period: T = 2π/ω₀ ≈ 1.26s The mass oscillates between
x = +150andx = -150(amplitude preserved since there's no damping).
Step 2: Add Energy Bars
Visualize kinetic and potential energy in real-time.
// Add after the physics step:
const KE = 0.5 * state.m * state.v * state.v;
const PE = 0.5 * state.k * state.x * state.x;
const totalE = KE + PE;
const maxE = 0.5 * state.k * 150 * 150; // max amplitude energy
const barMaxH = 120;
// Add to the return object:
bars: [
{ x: w - 80, y: h - 30 - (KE / maxE) * barMaxH, w: 30, h: (KE / maxE) * barMaxH, color: '#EF4444', label: 'KE' },
{ x: w - 40, y: h - 30 - (PE / maxE) * barMaxH, w: 30, h: (PE / maxE) * barMaxH, color: '#3B82F6', label: 'PE' },
],💡 PRO TIP In an undamped spring, KE + PE is constant. Watch the bars trade height as the mass moves — when KE is max (at equilibrium), PE is zero, and vice versa.
Step 3: Driven Oscillation with Resonance
Add a periodic driving force: F_drive = A * sin(ω_drive * t)
if (!state.init) {
state.x = 0;
state.v = 0;
state.k = 25;
state.m = 1.0;
state.damping = 2; // light damping
state.driveAmplitude = 30; // driving force amplitude
state.driveFreq = 5; // driving frequency (rad/s) — try ω₀ = 5 for resonance
state.init = true;
}
// Physics
const F_spring = -state.k * state.x;
const F_damp = -state.damping * state.v;
const F_drive = state.driveAmplitude * Math.sin(state.driveFreq * time);
const a = (F_spring + F_damp + F_drive) / state.m;
state.v += a * dt;
state.x += state.v * dt;
// Track max amplitude for bar scaling
state.maxAmp = Math.max(state.maxAmp ?? 0, Math.abs(state.x));
// ... rendering code same as Step 1, with updated text:
text: {
'x': state.x.toFixed(1),
'F_drive': F_drive.toFixed(1),
'ω_drive': state.driveFreq.toFixed(1),
'ω₀': Math.sqrt(state.k / state.m).toFixed(1),
},📐 MATH CHECK Resonance occurs when
ω_drive ≈ ω₀. At resonance, the driving force is always in phase with the velocity, pumping energy into the system. The amplitude grows until damping balances the input.Try
driveFreq = 5(= ω₀) to see resonance. Then trydriveFreq = 2ordriveFreq = 10to see off-resonance behavior with much smaller amplitude.
Parameter Guide
| Parameter | Effect | Try These Values |
|---|---|---|
k | Spring stiffness (higher = faster oscillation) | 10, 25, 100 |
m | Mass (higher = slower oscillation) | 0.5, 1, 3 |
damping | Energy loss (0 = perpetual, >5 = overdamped) | 0, 2, 10 |
driveAmplitude | Force input strength | 10, 30, 100 |
driveFreq | Forcing frequency (match ω₀ for resonance) | 2, 5, 10 |
Extending This Example
- Add a frequency sweep: gradually change
driveFreqover time and plot amplitude vs. frequency - Implement a phase portrait: plot
(x, v)points each frame to show the attractor - Add multiple coupled springs (two masses connected by springs)