Creating interactive physics simulations for the web can be a rewarding way to visualize complex concepts. This article walks through the development of a versatile platform for simulating various types of pendulums, built using HTML, CSS, and modern JavaScript. We’ll explore the core architecture, how different simulations are managed, and how user controls are dynamically generated.
The index.html
file sets up the user interface:
col-lg-8
for canvas, col-lg-4
for controls).<canvas id="simulationCanvas">
element where the magic happens. It’s wrapped in a div.canvas-container
to manage aspect ratio and responsiveness.id="simulationDropdown"
) allows users to choose which pendulum to simulate.div#colorPalette
with div.color-swatch
elements for selecting the primary color of the simulation elements.#startButton
), Pause (#pauseButton
), Stop/Reset (#stopButton
), and Step Forward (#stepButton
).div#simulationControlsContainer
where JavaScript will inject sliders and inputs specific to the selected simulation.The canvas container uses a common CSS trick for maintaining aspect ratio:
/* css/pendulum-simul.css */
.canvas-container {
position: relative;
width: 100%;
padding-bottom: 75%; /* Default to 4:3, adjust if needed */
/* ... other styles */
}
#simulationCanvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: block;
}
A base Simulation
class provides common functionality for all pendulum types. This promotes code reuse and a consistent API.
Key features of Simulation.js
:
selectedColor
), and state flags (isPaused
, isStopped
).init()
: Sets up the simulation.reset()
: Resets to initial conditions.update(deltaTime)
: Contains the physics logic to advance the simulation by one time step.draw()
: Renders the current state to the canvas.loop(timestamp)
):
requestAnimationFrame
for smooth animations.deltaTime
.
// Part of loop() method in Simulation.js
this.timeAccumulator += deltaTime;
while (this.timeAccumulator >= this.fixedDeltaTime) {
this.update(this.fixedDeltaTime); // Update with fixed step
this.timeAccumulator -= this.fixedDeltaTime;
}
start()
, pause()
, stop()
, step()
manage the simulation state and animation loop.resize(width, height)
): Updates canvas dimensions and redraws.getControlsHTML()
: Returns an HTML string for simulation-specific controls.bindSpecificControls()
: Binds event listeners to the dynamically added controls.updateConfig(newConfig)
): Allows updating common configurations like selectedColor
.destroy()
): Cancels animation frames and can be extended to remove event listeners.This script is the central hub that connects the UI elements to the simulation logic.
simulationMap
: A crucial object mapping dropdown data-value
attributes to their corresponding simulation classes.
// js/pendulum-simul.js
const simulationMap = {
doublePendulum: DoublePendulum,
DrivenDampedPendulum: DrivenDampedPendulum, // Assuming "DrivenDamped"
simplePendulum: SimplePendulum,
CycloidalPendulum: CycloidalPendulum,
ElasticPendulum: ElasticPendulum,
};
setupCanvas()
): Manages responsive canvas sizing. Ensures the canvas rendering resolution matches its display size and calls the current simulation’s resize
method.loadSimulation(simName)
):
destroy()
method is called for cleanup.simulationMap
.currentSim = new SimClass(simulationCanvas, { selectedColor });
.setupCanvas()
is called for the new simulation.init()
method is called.
controlsContainer.innerHTML = currentSim.getControlsHTML();
currentSim.bindSpecificControls();
currentSim
methods.selectedColor
and call currentSim.updateConfig()
.setupCanvas()
.loadSimulation()
with the new simulation name.Many physical systems, including pendulums, are described by ordinary differential equations (ODEs). The rk4.js
file provides a standard 4th order Runge-Kutta (RK4) method for numerically solving these ODEs.
// js/rk4.js
export function rk4(f, y0, t, dt, ...params) {
// k1, k2, k3, k4 calculations...
// ...
// const y_next = y0.map((val, i) => val + (k1[i] + 2 * k2[i] + 2 * k3[i] + k4[i]) / 6);
// return y_next;
}
The f
parameter is a function that defines the derivatives of the system (e.g., d(angle)/dt
, d(angularVelocity)/dt
). y0
is the current state vector, t
is current time, dt
is the time step, and ...params
are any additional parameters needed by the derivative function (like gravity, length, mass).
Each pendulum simulation (e.g., simple-pendulum.js
, double-pendulum.js
) extends the base Simulation
class.
Let’s look at SimplePendulum.js
as an example:
super(canvas, config)
.length
, initialAngle
, initialAngularVelocity
, gravity
).angle
, angularVelocity
).this
for the _derivatives
method: this._derivatives = this._derivatives.bind(this);
.reset()
:
angle
and angularVelocity
to their configured initial values.origin
based on canvas size.super.reset()
._derivatives(t_unused, y, effective_g, length)
:
y
is the state vector [angle, angularVelocity]
.[angularVelocity, angularAcceleration]
.
// js/simple-pendulum.js
_derivatives(t_unused, y, effective_g, length) {
const angle_val = y[0];
const angularAcceleration = (-effective_g / length) * Math.sin(angle_val);
return [y[1] /* angularVelocity */, angularAcceleration];
}
update(deltaTime)
:
currentState = [this.angle, this.angularVelocity]
.effective_g
.rk4()
.this.angle
and this.angularVelocity
from nextState
.DoublePendulum
or DrivenDampedPendulum
, it also updates a trace
array for drawing paths).
const nextState = rk4(
this._derivatives,
currentState,
0, // time (not directly used by this autonomous system's derivatives)
deltaTime,
g_scaled_for_physics, // effective_g
this.length
);
draw()
:
this.angle
and this.length
.moveTo
, lineTo
, arc
, stroke
, fill
).this.config.selectedColor
for the bob and darkenColor
(from simul-utils.js
) for borders or traces.getControlsHTML()
:
<label>
and <div>
elements for each slider (e.g., Length, Initial Angle, Gravity). The div
s will be targeted by noUiSlider
.
<!-- Example output for SimplePendulum -->
<div>
<label for="pendulumLengthSlider" class="slider-label">Length (px): <span id="pendulumLengthValue">${this.length}</span></label>
<div id="pendulumLengthSlider" class="slider slider-primary"></div>
</div>
<!-- ... other sliders ... -->
bindSpecificControls()
:
noUiSlider.create(element, options)
to initialize each slider defined in getControlsHTML()
.'update'
event listeners to each slider. When a slider value changes:
this.length
, this.initialAngle
) is updated.<span id="pendulumLengthValue">
) is updated.this.draw()
is called to reflect the change immediately. For initial condition changes, this.reset()
or parts of it might be called.destroy()
:
noUiSlider
instances were created, their destroy()
method should be called here (as done in cycloidal-pendulum.js
, driven-dumped-pendulum.js
, etc.).super.destroy()
.The other pendulum files (double-pendulum.js
, elastic-pendulum.js
, cycloidal-pendulum.js
, driven-dumped-pendulum.js
) follow a similar pattern, differing mainly in: * Their specific parameters and state variables. * The equations within their _derivatives
method. * The controls generated by getControlsHTML()
and bound in bindSpecificControls()
. * The CycloidalPendulum
is unique as it simulates two pendulums and has more complex drawing logic for the cycloid paths.
css/pendulum-simul.css
: Provides custom styles for the canvas container, color swatches (making them circular, adding hover effects, and a selected state), and slider labels.noUiSlider
: This library is essential for providing user-friendly sliders. Its CSS files (nouislider.min.css
and nouislider-theme.min.css
) are included for styling.js/simul-utils.js
: The darkenColor
function is a nice touch for generating border or trace colors programmatically based on the user’s selected color.This project demonstrates a robust and extensible architecture for creating interactive physics simulations on the web. Key takeaways from the codebase:
update()
method handles physics, while draw()
handles rendering, driven by a fixed-timestep loop.This platform can be further extended by adding more complex simulations, data visualization (like phase space plots), or more sophisticated user interaction features.
The simulations are available at the following link here.