Display Real-Time Energy Charts in Matter.js
This article explains how to build a real-time mechanical energy visualization alongside a Matter.js pendulum simulation. By tracking the pendulum bob's mass, velocity, and vertical position relative to gravity, you can compute kinetic energy, gravitational potential energy, and total mechanical energy frame by frame. Using a secondary HTML5 canvas, these values can be rendered instantly as animated bar charts that visually demonstrate the law of conservation of energy.
Understanding the Energy Equations
To visualize mechanical energy accurately, you need to calculate two components at every simulation tick:
Kinetic Energy (\(KE\)): For a rigid body in 2D space, kinetic energy is the sum of translational and rotational components: \[KE = \frac{1}{2} m (v_x^2 + v_y^2) + \frac{1}{2} I \omega^2\] Matter.js tracks translational velocity (
body.velocity), angular velocity (body.angularVelocity), mass (body.mass), and inertia (body.inertia).Gravitational Potential Energy (\(PE\)): Potential energy depends on the body's vertical displacement relative to a chosen reference height (datum): \[PE = m \cdot g \cdot (y_{\text{datum}} - y)\] In Matter.js, the vertical y-axis increases downward. Therefore, higher positions have lower \(y\) values. Choosing the lowest possible point of the pendulum's swing as \(y_{\text{datum}}\) ensures that potential energy remains non-negative.
Total Mechanical Energy (\(E\)): \[E = KE + PE\]
Step 1: Set Up the Pendulum in Matter.js
Create an engine, a render context for the pendulum, and the physical bodies comprising the pendulum (a fixed anchor, a swinging bob, and a constraint representing the rod).
const { Engine, Render, Runner, Bodies, Composite, Constraint, Events } = Matter;
const engine = Engine.create();
const world = engine.world;
// Gravity settings
world.gravity.y = 1;
world.gravity.scale = 0.001;
const render = Render.create({
element: document.getElementById('simulation-container'),
engine: engine,
options: { width: 400, height: 500, wireframes: false }
});
Render.run(render);
const runner = Runner.create();
Runner.run(runner, engine);
// Pendulum components
const anchor = { x: 200, y: 100 };
const bob = Bodies.circle(320, 100, 20, {
mass: 2,
restitution: 1, // Elastic collision
frictionAir: 0 // Zero drag for ideal energy conservation
});
const rod = Constraint.create({
pointA: anchor,
bodyB: bob,
length: 150,
stiffness: 1
});
Composite.add(world, [bob, rod]);Step 2: Configure the Chart Canvas
Create a secondary HTML5 <canvas> element in your
markup positioned next to the Matter.js simulation canvas:
<div style="display: flex; gap: 20px;">
<div id="simulation-container"></div>
<canvas id="chart-canvas" width="300" height="500"></canvas>
</div>Step 3: Compute Energies and Render the Bar Chart
Hook into the Matter.js afterUpdate event. This event
fires every time the physics engine advances a frame, providing the most
up-to-date velocity and position values.
const chartCanvas = document.getElementById('chart-canvas');
const ctx = chartCanvas.getContext('2d');
// Lowest point the bob can reach along the y-axis
const datumY = anchor.y + rod.length;
const g = world.gravity.y * world.gravity.scale * 1000; // Normalized gravity factor
Events.on(engine, 'afterUpdate', () => {
// 1. Calculate Kinetic Energy
const speedSq = Math.pow(bob.velocity.x, 2) + Math.pow(bob.velocity.y, 2);
const translationalKE = 0.5 * bob.mass * speedSq;
const rotationalKE = 0.5 * bob.inertia * Math.pow(bob.angularVelocity, 2);
const kineticEnergy = translationalKE + rotationalKE;
// 2. Calculate Potential Energy
const height = Math.max(0, datumY - bob.position.y);
const potentialEnergy = bob.mass * g * height;
// 3. Calculate Total Energy
const totalEnergy = kineticEnergy + potentialEnergy;
// 4. Render Bars
renderEnergyBars(kineticEnergy, potentialEnergy, totalEnergy);
});
function renderEnergyBars(ke, pe, total) {
ctx.clearRect(0, 0, chartCanvas.width, chartCanvas.height);
const maxEnergyScale = 15; // Calibration factor to fit bar height to canvas
const barWidth = 50;
const spacing = 30;
const startX = 40;
const baseY = 420;
const data = [
{ label: 'KE', value: ke, color: '#e74c3c' },
{ label: 'PE', value: pe, color: '#3498db' },
{ label: 'Total', value: total, color: '#2ecc71' }
];
data.forEach((item, index) => {
const x = startX + index * (barWidth + spacing);
const barHeight = item.value * maxEnergyScale;
// Draw Bar
ctx.fillStyle = item.color;
ctx.fillRect(x, baseY - barHeight, barWidth, barHeight);
// Draw Labels and Values
ctx.fillStyle = '#333333';
ctx.font = '14px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(item.label, x + barWidth / 2, baseY + 20);
ctx.fillText(item.value.toFixed(1), x + barWidth / 2, baseY - barHeight - 8);
});
// Draw Ground/Base Line
ctx.strokeStyle = '#888888';
ctx.beginPath();
ctx.moveTo(20, baseY);
ctx.lineTo(280, baseY);
ctx.stroke();
}Key Considerations for Realistic Energy Tracking
- Air Resistance: Setting
frictionAir: 0produces an undamped system where the total energy bar remains completely steady. IncreasingfrictionAirwill cause both \(KE\) and \(PE\) to decay over time, visually demonstrating thermal energy dissipation. - Numerical Drift: Physics engines use discrete
numerical integration (Euler or Verlet). Over extended runtimes, small
numerical integration errors can cause slight variations in the total
energy. You can minimize this by reducing the engine time step using
engine.timing.timeScaleor using smaller delta values.