Pendulum Simulation Platform

Pendulum Simulation
Platform Simulating Various Types of Pendulums

Pendulum Simulation Platform

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.

HTML file

The index.html file sets up the user interface:

  • Layout: A two-column layout using Bootstrap (col-lg-8 for canvas, col-lg-4 for controls).
  • Simulation Canvas: A <canvas id="simulationCanvas"> element where the magic happens. It’s wrapped in a div.canvas-container to manage aspect ratio and responsiveness.
  • Simulation Selector: A Bootstrap dropdown (id="simulationDropdown") allows users to choose which pendulum to simulate.
  • Color Palette: A div#colorPalette with div.color-swatch elements for selecting the primary color of the simulation elements.
  • Global Controls: Buttons for Start (#startButton), Pause (#pauseButton), Stop/Reset (#stopButton), and Step Forward (#stepButton).
  • Dynamic Controls Area: A div#simulationControlsContainer where JavaScript will inject sliders and inputs specific to the selected simulation.
  • Library Imports: CSS and JS for Bootstrap, Font Awesome, noUiSlider, and our custom scripts.

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;
}

Core simulation engine

A base Simulation class provides common functionality for all pendulum types. This promotes code reuse and a consistent API.

Key features of Simulation.js:

  • Constructor: Initializes canvas context, default configuration (like selectedColor), and state flags (isPaused, isStopped).
  • Lifecycle Methods (intended for override):
    • 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.
  • Animation Loop (loop(timestamp)):
    • Uses requestAnimationFrame for smooth animations.
    • Calculates deltaTime.
    • Implements a fixed timestep update loop. This ensures that physics calculations are stable and consistent, regardless of frame rate fluctuations.

// 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;
}
  • Control Methods: start(), pause(), stop(), step() manage the simulation state and animation loop.
  • Canvas Resizing (resize(width, height)): Updates canvas dimensions and redraws.
  • Dynamic UI Hooks (for override):
    • getControlsHTML(): Returns an HTML string for simulation-specific controls.
    • bindSpecificControls(): Binds event listeners to the dynamically added controls.
  • Configuration (updateConfig(newConfig)): Allows updating common configurations like selectedColor.
  • Cleanup (destroy()): Cancels animation frames and can be extended to remove event listeners.

Main Orchestrator

This script is the central hub that connects the UI elements to the simulation logic.

  • Imports: Imports all specific pendulum simulation classes.
  • 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,
};
  • DOM Element References: Gets references to the canvas, control buttons, color palette, and controls container.
  • Canvas Setup (setupCanvas()): Manages responsive canvas sizing. Ensures the canvas rendering resolution matches its display size and calls the current simulation’s resize method.
  • Loading Simulations (loadSimulation(simName)):
    1. If a simulation is already running, its destroy() method is called for cleanup.
    2. The appropriate simulation class is retrieved from simulationMap.
    3. A new instance of the simulation is created: currentSim = new SimClass(simulationCanvas, { selectedColor });.
    4. setupCanvas() is called for the new simulation.
    5. The simulation’s init() method is called.
    6. The simulation-specific controls are injected:

controlsContainer.innerHTML = currentSim.getControlsHTML();
currentSim.bindSpecificControls();
  • Event Listeners:
    • Global controls (Start, Pause, Stop, Step) are wired to currentSim methods.
    • Color palette clicks update selectedColor and call currentSim.updateConfig().
    • Window resize calls setupCanvas().
    • Dropdown item clicks trigger loadSimulation() with the new simulation name.
  • Initial Load: The script automatically loads the first simulation listed in the dropdown.

Numerical integration: RK4

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).

Implementing a specific pendulum

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:

  • Constructor:
    • Calls super(canvas, config).
    • Sets initial parameters specific to the simple pendulum (e.g., length, initialAngle, initialAngularVelocity, gravity).
    • Initializes state variables (angle, angularVelocity).
    • Binds this for the _derivatives method: this._derivatives = this._derivatives.bind(this);.
  • reset():
    • Resets angle and angularVelocity to their configured initial values.
    • Updates origin based on canvas size.
    • Calls super.reset().
  • _derivatives(t_unused, y, effective_g, length):
    • This is the heart of the physics for this specific pendulum.
    • y is the state vector [angle, angularVelocity].
    • It returns an array of derivatives: [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):
    • Gets the currentState = [this.angle, this.angularVelocity].
    • Calculates effective_g.
    • Calls rk4().
    • Updates this.angle and this.angularVelocity from nextState.
    • (For other pendulums like 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():
    • Clears the canvas.
    • Calculates bob position based on this.angle and this.length.
    • Draws the pendulum rod and bob using Canvas API methods (moveTo, lineTo, arc, stroke, fill).
    • Uses this.config.selectedColor for the bob and darkenColor (from simul-utils.js) for borders or traces.
  • getControlsHTML():
    • Returns an HTML string containing <label> and <div> elements for each slider (e.g., Length, Initial Angle, Gravity). The divs 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():
    • Uses noUiSlider.create(element, options) to initialize each slider defined in getControlsHTML().
    • Attaches 'update' event listeners to each slider. When a slider value changes:
      • The corresponding simulation parameter (e.g., this.length, this.initialAngle) is updated.
      • The display value (e.g., <span id="pendulumLengthValue">) is updated.
      • If the simulation is stopped or paused, this.draw() is called to reflect the change immediately. For initial condition changes, this.reset() or parts of it might be called.
  • destroy():
    • If noUiSlider instances were created, their destroy() method should be called here (as done in cycloidal-pendulum.js, driven-dumped-pendulum.js, etc.).
    • Calls 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.

Styling and UI enhancements

  • 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.
  • Bootstrap: Simplifies responsive design and provides default styling for buttons and dropdowns.
  • 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.

Conclusion

This project demonstrates a robust and extensible architecture for creating interactive physics simulations on the web. Key takeaways from the codebase:

  • Modular Design: Separating concerns into a base simulation class, individual simulation modules, a main orchestrator, and utility functions makes the code manageable and easier to extend with new simulations.
  • Object-Oriented Approach: Using classes for simulations encapsulates state and behavior effectively.
  • Dynamic UI: Generating simulation-specific controls dynamically provides a tailored user experience for each pendulum type.
  • Clear Separation of Physics and Rendering: The update() method handles physics, while draw() handles rendering, driven by a fixed-timestep loop.
  • Use of External Libraries: Leveraging libraries like Bootstrap and noUiSlider speeds up UI development and enhances user interaction.

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.

Go to the top of the page