Physics Simulation
Double Pendulum
Interactive chaos simulation using Lagrangian mechanics and RK4 integration. Visualize how small differences in initial conditions lead to dramatically different trajectories.
Status
Running
●
Time (s)
0.00
θ₁ (°)
0.00
θ₂ (°)
0.00
Energy (J)
0.00
Pendulum Params
Initial Angles
Visual Markers & Vectors
Trail Settings
Multiple pendulums show chaos—identical except for tiny initial angle differences.
Simulation
Equations of Motion
θ₁'' = [-g(2m₁+m₂)sin(θ₁) - m₂g·sin(θ₁-2θ₂) - 2sin(θ₁-θ₂)m₂(θ₂'²L₂+θ₁'²L₁cos(θ₁-θ₂))] / [L₁(2m₁+m₂-m₂cos(2θ₁-2θ₂))]
θ₂'' = [2sin(θ₁-θ₂)(θ₁'²L₁(m₁+m₂)+g(m₁+m₂)cos(θ₁)+θ₂'²L₂m₂cos(θ₁-θ₂))] / [L₂(2m₁+m₂-m₂cos(2θ₁-2θ₂))]
Integrated using RK4 (4th-order Runge-Kutta) with dt=0.02. Trails cycle through a rainbow gradient showing temporal evolution.
Developer Reference
Core Algorithm & Standalone Script
Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// Responsive canvas sizing
function resizeCanvas() {
const rect = canvas.parentElement.getBoundingClientRect();
canvas.width = rect.width - 32; // account for padding
canvas.height = Math.max(400, window.innerHeight * 0.6);
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
// UI Elements
const l1Slider = document.getElementById('l1-slider');
const l2Slider = document.getElementById('l2-slider');
const m1Slider = document.getElementById('m1-slider');
const m2Slider = document.getElementById('m2-slider');
const gSlider = document.getElementById('g-slider');
const angle1Slider = document.getElementById('angle1-slider');
const angle2Slider = document.getElementById('angle2-slider');
const trailSlider = document.getElementById('trail-slider');
const countSlider = document.getElementById('count-slider');
// Checkbox elements for visual overlays
const showLabelsCheck = document.getElementById('show-labels-check');
const showVectorsCheck = document.getElementById('show-vectors-check');
const showForcesCheck = document.getElementById('show-forces-check');
const showAnglesCheck = document.getElementById('show-angles-check');
const showTrail1Check = document.getElementById('show-trail1-check');
const showPathMarksCheck = document.getElementById('show-pathmarks-check');
const pauseBtn = document.getElementById('pause-btn');
const resetBtn = document.getElementById('reset-btn');
const clearTrailBtn = document.getElementById('clear-trail-btn');
const randomBtn = document.getElementById('random-btn');
// Update display values
function updateSliderDisplays() {
document.getElementById('l1-val').textContent = l1Slider.value;
document.getElementById('l2-val').textContent = l2Slider.value;
document.getElementById('m1-val').textContent = m1Slider.value;
document.getElementById('m2-val').textContent = m2Slider.value;
document.getElementById('g-val').textContent = parseFloat(gSlider.value).toFixed(2);
document.getElementById('angle1-val').textContent = angle1Slider.value;
document.getElementById('angle2-val').textContent = angle2Slider.value;
document.getElementById('trail-val').textContent = trailSlider.value;
document.getElementById('count-val').textContent = countSlider.value;
}
[l1Slider, l2Slider, m1Slider, m2Slider, gSlider, angle1Slider, angle2Slider, trailSlider, countSlider].forEach(el => {
el.addEventListener('input', updateSliderDisplays);
});
// Pendulum state
let paused = false;
let time = 0;
let pendulums = [];
const dt = 0.02; // RK4 time step
const colors = [
'#ff2200', // red
'#ff8800', // orange
'#ffcc00' // yellow
];
// Helper functions for drawing anchored marks, vectors, and arcs
function drawArrow(ctx, fromX, fromY, toX, toY, color, label, lineWidth = 2) {
const dx = toX - fromX;
const dy = toY - fromY;
const len = Math.hypot(dx, dy);
if (len < 3) return;
const angle = Math.atan2(dy, dx);
const headLen = Math.min(8, len * 0.4);
ctx.save();
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.lineWidth = lineWidth;
// Shaft
ctx.beginPath();
ctx.moveTo(fromX, fromY);
ctx.lineTo(toX, toY);
ctx.stroke();
// Arrowhead
ctx.beginPath();
ctx.moveTo(toX, toY);
ctx.lineTo(toX - headLen * Math.cos(angle - Math.PI / 6), toY - headLen * Math.sin(angle - Math.PI / 6));
ctx.lineTo(toX - headLen * Math.cos(angle + Math.PI / 6), toY - headLen * Math.sin(angle + Math.PI / 6));
ctx.closePath();
ctx.fill();
// Label at arrowhead
if (label) {
ctx.font = '500 10px "DM Mono", monospace';
const textWidth = ctx.measureText(label).width;
const lx = toX + Math.cos(angle) * 10;
const ly = toY + Math.sin(angle) * 10;
ctx.fillStyle = 'rgba(10, 10, 15, 0.85)';
ctx.fillRect(lx - 4, ly - 9, textWidth + 8, 14);
ctx.strokeStyle = color;
ctx.lineWidth = 1;
ctx.strokeRect(lx - 4, ly - 9, textWidth + 8, 14);
ctx.fillStyle = color;
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
ctx.fillText(label, lx, ly - 2);
}
ctx.restore();
}
function drawMassBadge(ctx, x, y, labelText, color, offsetX = 16, offsetY = -16) {
ctx.save();
ctx.font = '500 11px "DM Mono", monospace';
const textWidth = ctx.measureText(labelText).width;
const badgeW = textWidth + 12;
const badgeH = 18;
const bx = x + offsetX;
const by = y + offsetY;
// Leader line connecting center of mass to floating badge
ctx.strokeStyle = color;
ctx.lineWidth = 1;
ctx.setLineDash([2, 2]);
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(bx < x ? bx + badgeW : bx, by + badgeH / 2);
ctx.stroke();
ctx.setLineDash([]);
// Badge background pill
ctx.fillStyle = 'rgba(12, 12, 18, 0.88)';
ctx.beginPath();
ctx.roundRect(bx, by, badgeW, badgeH, 4);
ctx.fill();
// Border
ctx.strokeStyle = color;
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.roundRect(bx, by, badgeW, badgeH, 4);
ctx.stroke();
// Text inside badge
ctx.fillStyle = '#e8e0d5';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
ctx.fillText(labelText, bx + 6, by + badgeH / 2 + 1);
ctx.restore();
}
function drawAngleArc(ctx, cx, cy, thetaRad, radius, labelText, color) {
ctx.save();
// Downward vertical reference line
ctx.strokeStyle = 'rgba(255, 255, 255, 0.25)';
ctx.setLineDash([3, 3]);
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(cx, cy + radius + 15);
ctx.stroke();
// Arc from downward vertical (Math.PI / 2) to rod angle
const startA = Math.PI / 2;
const rodA = Math.atan2(Math.cos(thetaRad), Math.sin(thetaRad));
ctx.setLineDash([]);
ctx.strokeStyle = color;
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.arc(cx, cy, radius, Math.min(startA, rodA), Math.max(startA, rodA));
ctx.stroke();
if (labelText) {
const midA = (startA + rodA) / 2;
const lx = cx + Math.cos(midA) * (radius + 16);
const ly = cy + Math.sin(midA) * (radius + 16);
ctx.font = '500 10px "DM Mono", monospace';
ctx.fillStyle = color;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(labelText, lx, ly);
}
ctx.restore();
}
function drawRodLabel(ctx, x1, y1, x2, y2, labelText, color) {
ctx.save();
const mx = (x1 + x2) / 2;
const my = (y1 + y2) / 2;
const dx = x2 - x1;
const dy = y2 - y1;
const angle = Math.atan2(dy, dx);
// Perpendicular offset
const nx = -Math.sin(angle) * 14;
const ny = Math.cos(angle) * 14;
ctx.font = '500 10px "DM Mono", monospace';
const textWidth = ctx.measureText(labelText).width;
ctx.fillStyle = 'rgba(10, 10, 15, 0.75)';
ctx.fillRect(mx + nx - textWidth / 2 - 4, my + ny - 7, textWidth + 8, 14);
ctx.fillStyle = color;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(labelText, mx + nx, my + ny);
ctx.restore();
}
// Pendulum class
class Pendulum {
constructor(theta1, theta2, offsetAngle = 0) {
this.theta1 = (theta1 + offsetAngle) * Math.PI / 180;
this.theta2 = (theta2 + offsetAngle) * Math.PI / 180;
this.theta1_vel = 0;
this.theta2_vel = 0;
this.trail = []; // trail for m2
this.trail1 = []; // trail for m1
this.colorIdx = Math.floor(Math.random() * colors.length);
}
getAccelerations(theta1, theta2, theta1_vel, theta2_vel, L1, L2, m1, m2, g) {
const dtheta = theta1 - theta2;
const c = Math.cos(dtheta);
const s = Math.sin(dtheta);
const denom = L1 * (2 * m1 + m2 - m2 * Math.cos(2 * (theta1 - theta2)));
if (Math.abs(denom) < 1e-6) return [0, 0];
const num1 = -g * (2 * m1 + m2) * Math.sin(theta1)
- m2 * g * Math.sin(theta1 - 2 * theta2)
- 2 * s * m2 * (theta2_vel * theta2_vel * L2 + theta1_vel * theta1_vel * L1 * c);
const theta1_acc = num1 / denom;
const denom2 = L2 * (2 * m1 + m2 - m2 * Math.cos(2 * (theta1 - theta2)));
const num2 = 2 * s * (theta1_vel * theta1_vel * L1 * (m1 + m2) + g * (m1 + m2) * Math.cos(theta1)
+ theta2_vel * theta2_vel * L2 * m2 * c);
const theta2_acc = num2 / denom2;
return [theta1_acc, theta2_acc];
}
step(L1, L2, m1, m2, g) {
const rk4Step = (theta, theta_vel, accel_fn) => {
const k1 = theta_vel;
const ak1 = accel_fn(theta, theta_vel);
const k2 = theta_vel + ak1 * dt / 2;
const ak2 = accel_fn(theta + k1 * dt / 2, k2);
const k3 = theta_vel + ak2 * dt / 2;
const ak3 = accel_fn(theta + k2 * dt / 2, k3);
const k4 = theta_vel + ak3 * dt;
const ak4 = accel_fn(theta + k3 * dt, k4);
return {
pos: theta + (k1 + 2 * k2 + 2 * k3 + k4) * dt / 6,
vel: theta_vel + (ak1 + 2 * ak2 + 2 * ak3 + ak4) * dt / 6
};
};
const theta1_accel_fn = (t1, tv1) => {
const [a1] = this.getAccelerations(t1, this.theta2, tv1, this.theta2_vel, L1, L2, m1, m2, g);
return a1;
};
const theta2_accel_fn = (t2, tv2) => {
const [, a2] = this.getAccelerations(this.theta1, t2, this.theta1_vel, tv2, L1, L2, m1, m2, g);
return a2;
};
const res1 = rk4Step(this.theta1, this.theta1_vel, theta1_accel_fn);
const res2 = rk4Step(this.theta2, this.theta2_vel, theta2_accel_fn);
this.theta1 = res1.pos;
this.theta1_vel = res1.vel;
this.theta2 = res2.pos;
this.theta2_vel = res2.vel;
}
addTrailPoint(x1, y1, x2, y2) {
this.trail.push({ x: x2, y: y2, age: 0 });
this.trail1.push({ x: x1, y: y1, age: 0 });
const maxLen = parseInt(trailSlider.value);
if (this.trail.length > maxLen) {
this.trail.shift();
}
if (this.trail1.length > maxLen) {
this.trail1.shift();
}
}
ageDtrail() {
this.trail.forEach(pt => pt.age++);
this.trail1.forEach(pt => pt.age++);
}
getPosition(L1, L2, pivotX, pivotY) {
const x1 = pivotX + L1 * Math.sin(this.theta1);
const y1 = pivotY + L1 * Math.cos(this.theta1);
const x2 = x1 + L2 * Math.sin(this.theta2);
const y2 = y1 + L2 * Math.cos(this.theta2);
return { x1, y1, x2, y2 };
}
getEnergy(L1, L2, m1, m2, g, pivotY) {
const { x1, y1, x2, y2 } = this.getPosition(L1, L2, canvas.width / 2, pivotY);
const h1 = pivotY - y1;
const h2 = pivotY - y2;
const pe = m1 * g * h1 + m2 * g * h2;
const ke = 0.5 * m1 * (L1 * this.theta1_vel) ** 2 + 0.5 * m2 * ((L1 * this.theta1_vel) ** 2 + (L2 * this.theta2_vel) ** 2 + 2 * L1 * L2 * this.theta1_vel * this.theta2_vel * Math.cos(this.theta1 - this.theta2));
return pe + ke;
}
}
function initPendulums() {
pendulums = [];
const count = parseInt(countSlider.value);
const angle1 = parseInt(angle1Slider.value);
const angle2 = parseInt(angle2Slider.value);
for (let i = 0; i < count; i++) {
const offsetAngle = i === 0 ? 0 : (0.5 + i * 0.2); // chaos: tiny angle offsets
pendulums.push(new Pendulum(angle1, angle2, offsetAngle));
}
time = 0;
}
function updateStats() {
if (pendulums.length === 0) return;
const p = pendulums[0]; // display first pendulum
const L1 = parseInt(l1Slider.value);
const L2 = parseInt(l2Slider.value);
const m1 = parseFloat(m1Slider.value);
const m2 = parseFloat(m2Slider.value);
const g = parseFloat(gSlider.value);
document.getElementById('time-display').textContent = time.toFixed(2);
document.getElementById('theta1-display').textContent = (p.theta1 * 180 / Math.PI % 360).toFixed(2);
document.getElementById('theta2-display').textContent = (p.theta2 * 180 / Math.PI % 360).toFixed(2);
const energy = p.getEnergy(L1, L2, m1, m2, g, 80);
document.getElementById('energy-display').textContent = energy.toFixed(2);
}
function drawPendulum(pend, idx) {
const L1 = parseInt(l1Slider.value);
const L2 = parseInt(l2Slider.value);
const m1 = parseFloat(m1Slider.value);
const m2 = parseFloat(m2Slider.value);
const g = parseFloat(gSlider.value);
const pivotX = canvas.width / 2;
const pivotY = 80;
const { x1, y1, x2, y2 } = pend.getPosition(L1, L2, pivotX, pivotY);
// Update trail
pend.addTrailPoint(x1, y1, x2, y2);
pend.ageDtrail();
// Draw m2 trail (rainbow)
if (pend.trail.length > 1) {
const maxTrailLen = parseInt(trailSlider.value);
const hueOffset = (time * 0.5) % 360;
for (let i = 0; i < pend.trail.length - 1; i++) {
const pt = pend.trail[i];
const ptNext = pend.trail[i + 1];
const ratio = i / pend.trail.length;
const hue = (hueOffset + ratio * 120) % 360;
const color = `hsl(${hue}, 100%, 50%)`;
const ageFactor = 1 - (pt.age / maxTrailLen);
ctx.globalAlpha = Math.max(0, ageFactor * 0.6);
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(pt.x, pt.y);
ctx.lineTo(ptNext.x, ptNext.y);
ctx.stroke();
}
}
// Draw m1 trail (if enabled)
if (showTrail1Check && showTrail1Check.checked && pend.trail1.length > 1) {
const maxTrailLen = parseInt(trailSlider.value);
for (let i = 0; i < pend.trail1.length - 1; i++) {
const pt = pend.trail1[i];
const ptNext = pend.trail1[i + 1];
const ageFactor = 1 - (pt.age / maxTrailLen);
ctx.globalAlpha = Math.max(0, ageFactor * 0.4);
ctx.strokeStyle = '#ff00aa';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(pt.x, pt.y);
ctx.lineTo(ptNext.x, ptNext.y);
ctx.stroke();
}
}
ctx.globalAlpha = 1;
// Trajectory Path Markers (milestone dots along path)
if (showPathMarksCheck && showPathMarksCheck.checked && pend.trail.length > 15) {
const step = Math.max(12, Math.floor(pend.trail.length / 8));
for (let i = step; i < pend.trail.length; i += step) {
const pt = pend.trail[i];
ctx.save();
ctx.fillStyle = idx === 0 ? '#00c896' : '#ffcc00';
ctx.beginPath();
ctx.arc(pt.x, pt.y, 2.5, 0, Math.PI * 2);
ctx.fill();
ctx.font = '9px "DM Mono", monospace';
ctx.fillStyle = 'rgba(255, 255, 255, 0.45)';
ctx.fillText(`p${i}`, pt.x + 4, pt.y - 3);
ctx.restore();
}
}
// Draw angle arcs
if (showAnglesCheck && showAnglesCheck.checked && idx === 0) {
const deg1 = (pend.theta1 * 180 / Math.PI % 360).toFixed(1);
const deg2 = (pend.theta2 * 180 / Math.PI % 360).toFixed(1);
drawAngleArc(ctx, pivotX, pivotY, pend.theta1, 35, `θ₁=${deg1}°`, '#ff6600');
drawAngleArc(ctx, x1, y1, pend.theta2, 30, `θ₂=${deg2}°`, '#00d2ff');
}
// Draw rods
ctx.strokeStyle = idx === 0 ? '#ff2200' : '#ff8800';
ctx.lineWidth = 6;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.beginPath();
ctx.moveTo(pivotX, pivotY);
ctx.lineTo(x1, y1);
ctx.stroke();
ctx.strokeStyle = idx === 0 ? '#e8e0d5' : '#ffcc00';
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
// Rod labels
if (showLabelsCheck && showLabelsCheck.checked && idx === 0) {
drawRodLabel(ctx, pivotX, pivotY, x1, y1, `L₁=${L1}px`, 'rgba(255, 255, 255, 0.7)');
drawRodLabel(ctx, x1, y1, x2, y2, `L₂=${L2}px`, 'rgba(255, 255, 255, 0.7)');
}
// Draw masses
const radius1 = 4 + m1 * 2;
const radius2 = 4 + m2 * 2;
// Mass 1
ctx.fillStyle = '#ff2200';
ctx.shadowColor = 'rgba(255, 34, 0, 0.6)';
ctx.shadowBlur = 12;
ctx.beginPath();
ctx.arc(x1, y1, radius1, 0, Math.PI * 2);
ctx.fill();
// Mass 2
ctx.fillStyle = idx === 0 ? '#00c896' : '#ffff00';
ctx.shadowColor = idx === 0 ? 'rgba(0, 200, 150, 0.6)' : 'rgba(255, 255, 0, 0.6)';
ctx.beginPath();
ctx.arc(x2, y2, radius2, 0, Math.PI * 2);
ctx.fill();
ctx.shadowColor = 'transparent';
ctx.shadowBlur = 0;
// Object Marks / Badges (anchored to moving objects)
if (showLabelsCheck && showLabelsCheck.checked) {
const pPrefix = pendulums.length > 1 ? `P${idx + 1}:` : '';
if (idx === 0) {
drawMassBadge(ctx, pivotX, pivotY, 'O (0,0)', '#888888', -45, -25);
}
drawMassBadge(ctx, x1, y1, `${pPrefix}m₁ (${m1.toFixed(1)}kg)`, '#ff4400', 16, -20);
drawMassBadge(ctx, x2, y2, `${pPrefix}m₂ (${m2.toFixed(1)}kg)`, idx === 0 ? '#00c896' : '#ffff00', 16, -20);
}
// Velocities and velocity vector arrows
const vx1 = L1 * pend.theta1_vel * Math.cos(pend.theta1);
const vy1 = -L1 * pend.theta1_vel * Math.sin(pend.theta1);
const vx2 = vx1 + L2 * pend.theta2_vel * Math.cos(pend.theta2);
const vy2 = vy1 - L2 * pend.theta2_vel * Math.sin(pend.theta2);
const speed1 = Math.hypot(vx1, vy1);
const speed2 = Math.hypot(vx2, vy2);
if (showVectorsCheck && showVectorsCheck.checked && idx === 0) {
const vScale = 0.15;
drawArrow(ctx, x1, y1, x1 + vx1 * vScale, y1 + vy1 * vScale, '#00d2ff', `v₁ (${speed1.toFixed(1)})`);
drawArrow(ctx, x2, y2, x2 + vx2 * vScale, y2 + vy2 * vScale, '#00ffaa', `v₂ (${speed2.toFixed(1)})`);
}
// Forces (Gravity & Tension)
if (showForcesCheck && showForcesCheck.checked && idx === 0) {
const gScale = 3.5;
drawArrow(ctx, x1, y1, x1, y1 + m1 * g * gScale, '#ff4444', `m₁g`);
drawArrow(ctx, x2, y2, x2, y2 + m2 * g * gScale, '#ff4444', `m₂g`);
const dx1 = pivotX - x1, dy1 = pivotY - y1;
const d1 = Math.hypot(dx1, dy1) || 1;
drawArrow(ctx, x1, y1, x1 + (dx1 / d1) * 35, y1 + (dy1 / d1) * 35, '#ffbb00', `T₁`);
const dx2 = x1 - x2, dy2 = y1 - y2;
const d2 = Math.hypot(dx2, dy2) || 1;
drawArrow(ctx, x2, y2, x2 + (dx2 / d2) * 35, y2 + (dy2 / d2) * 35, '#ffbb00', `T₂`);
}
}
function animate() {
// Clear canvas
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Update physics
if (!paused) {
const L1 = parseInt(l1Slider.value);
const L2 = parseInt(l2Slider.value);
const m1 = parseFloat(m1Slider.value);
const m2 = parseFloat(m2Slider.value);
const g = parseFloat(gSlider.value);
pendulums.forEach(pend => {
pend.step(L1, L2, m1, m2, g);
});
time += dt;
}
// Draw pivot point
ctx.fillStyle = '#555555';
ctx.beginPath();
ctx.arc(canvas.width / 2, 80, 6, 0, Math.PI * 2);
ctx.fill();
// Draw all pendulums
pendulums.forEach((pend, idx) => {
drawPendulum(pend, idx);
});
updateStats();
requestAnimationFrame(animate);
}
// Event listeners
pauseBtn.addEventListener('click', () => {
paused = !paused;
pauseBtn.textContent = paused ? 'Resume' : 'Pause';
const badge = document.getElementById('status-badge');
const text = document.getElementById('status-text');
if (paused) {
badge.classList.remove('running');
badge.classList.add('paused');
text.textContent = 'Paused';
} else {
badge.classList.remove('paused');
badge.classList.add('running');
text.textContent = 'Running';
}
});
resetBtn.addEventListener('click', () => {
initPendulums();
paused = false;
pauseBtn.textContent = 'Pause';
const badge = document.getElementById('status-badge');
const text = document.getElementById('status-text');
badge.classList.remove('paused');
badge.classList.add('running');
text.textContent = 'Running';
});
clearTrailBtn.addEventListener('click', () => {
pendulums.forEach(pend => {
pend.trail = [];
pend.trail1 = [];
});
});
randomBtn.addEventListener('click', () => {
angle1Slider.value = Math.random() * 360;
angle2Slider.value = Math.random() * 360;
updateSliderDisplays();
initPendulums();
});
// Parameter change listeners
[l1Slider, l2Slider, m1Slider, m2Slider, gSlider, angle1Slider, angle2Slider].forEach(el => {
el.addEventListener('input', () => {
initPendulums();
});
});
countSlider.addEventListener('input', () => {
initPendulums();
});
// Start
updateSliderDisplays();
initPendulums();
animate();