// --- Configuration ---
const g = 9.81; // Gravity
// Pendulum Properties
const l1 = 160, l2 = 160, l3 = 160; // Lengths of arms
const m1 = 15, m2 = 15, m3 = 15; // Masses of bobs
// Initial State (angles in radians, velocities in rad/s)
let theta1 = Math.PI / 1.5;
let theta2 = Math.PI / 1.2;
let theta3 = Math.PI;
let omega1 = 0, omega2 = 0, omega3 = 0; // Angular velocities
// Animation settings
const substeps = 8; // Number of physics updates per frame for stability
const maxTrailLength = 800;
const trailFade = 0.04; // Lower is longer lasting trails
// Trail arrays
const trail1 = [], trail2 = [], trail3 = [];
let pivotX, pivotY;
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
pivotX = canvas.width / 2;
pivotY = canvas.height / 2 - Math.min(canvas.width, canvas.height) * 0.2;
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
function updatePhysics(timeStep) {
// Shorthands for sin/cos and other terms
const c1 = Math.cos(theta1), s1 = Math.sin(theta1);
const c2 = Math.cos(theta2), s2 = Math.sin(theta2);
const c3 = Math.cos(theta3), s3 = Math.sin(theta3);
const c12 = Math.cos(theta1 - theta2), s12 = Math.sin(theta1 - theta2);
const c13 = Math.cos(theta1 - theta3), s13 = Math.sin(theta1 - theta3);
const c23 = Math.cos(theta2 - theta3), s23 = Math.sin(theta2 - theta3);
const l1_2 = l1 * l1, l2_2 = l2 * l2, l3_2 = l3 * l3;
const w1_2 = omega1 * omega1, w2_2 = omega2 * omega2, w3_2 = omega3 * omega3;
// Define the M matrix and F vector for the system M * alpha = F
const M11 = (m1 + m2 + m3) * l1_2;
const M12 = (m2 + m3) * l1 * l2 * c12;
const M13 = m3 * l1 * l3 * c13;
const M21 = M12;
const M22 = (m2 + m3) * l2_2;
const M23 = m3 * l2 * l3 * c23;
const M31 = M13;
const M32 = M23;
const M33 = m3 * l3_2;
const F1 = -(m2 + m3) * l1 * l2 * w2_2 * s12 - m3 * l1 * l3 * w3_2 * s13 - (m1 + m2 + m3) * g * l1 * s1;
const F2 = (m2 + m3) * l1 * l2 * w1_2 * s12 - m3 * l2 * l3 * w3_2 * s23 - (m2 + m3) * g * l2 * s2;
const F3 = m3 * l1 * l3 * w1_2 * s13 + m3 * l2 * l3 * w2_2 * s23 - m3 * g * l3 * s3;
// Solve for angular accelerations (alpha) using Cramer's rule
const detM = M11 * (M22 * M33 - M23 * M32) - M12 * (M21 * M33 - M23 * M31) + M13 * (M21 * M32 - M22 * M31);
if (Math.abs(detM) < 1e-12) return; // Avoid division by near-zero determinant
const detM1 = F1 * (M22 * M33 - M23 * M32) - M12 * (F2 * M33 - M23 * F3) + M13 * (F2 * M32 - M22 * F3);
const detM2 = M11 * (F2 * M33 - M23 * F3) - F1 * (M21 * M33 - M23 * M31) + M13 * (M21 * F3 - F2 * M31);
const detM3 = M11 * (M22 * F3 - F2 * M32) - M12 * (M21 * F3 - F2 * M31) + F1 * (M21 * M32 - M22 * M31);
const alpha1 = detM1 / detM;
const alpha2 = detM2 / detM;
const alpha3 = detM3 / detM;
// Update velocities and positions using Euler integration
omega1 += alpha1 * timeStep;
omega2 += alpha2 * timeStep;
omega3 += alpha3 * timeStep;
theta1 += omega1 * timeStep;
theta2 += omega2 * timeStep;
theta3 += omega3 * timeStep;
}
function draw() {
// Apply a semi-transparent overlay to create fading trails
ctx.fillStyle = `rgba(13, 13, 13, ${trailFade})`;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Convert polar coordinates (angle, length) to Cartesian (x, y)
const x1 = pivotX + l1 * Math.sin(theta1);
const y1 = pivotY + l1 * Math.cos(theta1);
const x2 = x1 + l2 * Math.sin(theta2);
const y2 = y1 + l2 * Math.cos(theta2);
const x3 = x2 + l3 * Math.sin(theta3);
const y3 = y2 + l3 * Math.cos(theta3);
// Add new points to the trail arrays
trail1.push({ x: x1, y: y1 });
trail2.push({ x: x2, y: y2 });
trail3.push({ x: x3, y: y3 });
if (trail1.length > maxTrailLength) trail1.shift();
if (trail2.length > maxTrailLength) trail2.shift();
if (trail3.length > maxTrailLength) trail3.shift();
// Draw trails
drawTrail(trail1, '#ff4545');
drawTrail(trail2, '#45ff75');
drawTrail(trail3, '#45a5ff');
// Draw pendulum arms
ctx.strokeStyle = '#cccccc';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(pivotX, pivotY);
ctx.lineTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.lineTo(x3, y3);
ctx.stroke();
// Draw bobs
drawBob(x1, y1, m1, '#ff4545');
drawBob(x2, y2, m2, '#45ff75');
drawBob(x3, y3, m3, '#45a5ff');
// Draw pivot point
drawBob(pivotX, pivotY, 5, '#ffffff');
}
function drawTrail(trail, color) {
ctx.beginPath();
if (trail.length > 0) {
ctx.moveTo(trail[0].x, trail[0].y);
for (let i = 1; i < trail.length; i++) {
ctx.lineTo(trail[i].x, trail[i].y);
}
}
ctx.strokeStyle = color;
ctx.lineWidth = 1.5;
ctx.stroke();
}
function drawBob(x, y, mass, color) {
const radius = Math.cbrt(mass) * 3;
ctx.beginPath();
ctx.arc(x, y, radius, 0, 2 * Math.PI);
ctx.fillStyle = color;
ctx.fill();
}
let lastTime = 0;
function animate(currentTime) {
const deltaTime = (currentTime - lastTime) / 1000; // time in seconds
lastTime = currentTime;
// Use a fixed time step for physics stability, running multiple steps if needed
const timeStep = 1 / 60; // Simulate at a consistent 60 FPS
const steps = Math.max(1, Math.round(deltaTime / timeStep));
for (let i = 0; i < steps; i++) {
for (let j = 0; j < substeps; j++) {
updatePhysics(timeStep / substeps);
}
}
draw();
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
</script>
// --- Configuration ---
const g = 9.81; // Gravity
// Pendulum Properties
const l1 = 160, l2 = 160, l3 = 160; // Lengths of arms
const m1 = 15, m2 = 15, m3 = 15; // Masses of bobs
// Initial State (angles in radians, velocities in rad/s)
let theta1 = Math.PI / 1.5;
let theta2 = Math.PI / 1.2;
let theta3 = Math.PI;
let omega1 = 0, omega2 = 0, omega3 = 0; // Angular velocities
// Animation settings
const substeps = 8; // Number of physics updates per frame for stability
const maxTrailLength = 800;
const trailFade = 0.04; // Lower is longer lasting trails
// Trail arrays
const trail1 = [], trail2 = [], trail3 = [];
let pivotX, pivotY;
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
pivotX = canvas.width / 2;
pivotY = canvas.height / 2 - Math.min(canvas.width, canvas.height) * 0.2;
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
function updatePhysics(timeStep) {
// Shorthands for sin/cos and other terms
const c1 = Math.cos(theta1), s1 = Math.sin(theta1);
const c2 = Math.cos(theta2), s2 = Math.sin(theta2);
const c3 = Math.cos(theta3), s3 = Math.sin(theta3);
const c12 = Math.cos(theta1 - theta2), s12 = Math.sin(theta1 - theta2);
const c13 = Math.cos(theta1 - theta3), s13 = Math.sin(theta1 - theta3);
const c23 = Math.cos(theta2 - theta3), s23 = Math.sin(theta2 - theta3);
const l1_2 = l1 * l1, l2_2 = l2 * l2, l3_2 = l3 * l3;
const w1_2 = omega1 * omega1, w2_2 = omega2 * omega2, w3_2 = omega3 * omega3;
// Define the M matrix and F vector for the system M * alpha = F
const M11 = (m1 + m2 + m3) * l1_2;
const M12 = (m2 + m3) * l1 * l2 * c12;
const M13 = m3 * l1 * l3 * c13;
const M21 = M12;
const M22 = (m2 + m3) * l2_2;
const M23 = m3 * l2 * l3 * c23;
const M31 = M13;
const M32 = M23;
const M33 = m3 * l3_2;
const F1 = -(m2 + m3) * l1 * l2 * w2_2 * s12 - m3 * l1 * l3 * w3_2 * s13 - (m1 + m2 + m3) * g * l1 * s1;
const F2 = (m2 + m3) * l1 * l2 * w1_2 * s12 - m3 * l2 * l3 * w3_2 * s23 - (m2 + m3) * g * l2 * s2;
const F3 = m3 * l1 * l3 * w1_2 * s13 + m3 * l2 * l3 * w2_2 * s23 - m3 * g * l3 * s3;
// Solve for angular accelerations (alpha) using Cramer's rule
const detM = M11 * (M22 * M33 - M23 * M32) - M12 * (M21 * M33 - M23 * M31) + M13 * (M21 * M32 - M22 * M31);
if (Math.abs(detM) < 1e-12) return; // Avoid division by near-zero determinant
const detM1 = F1 * (M22 * M33 - M23 * M32) - M12 * (F2 * M33 - M23 * F3) + M13 * (F2 * M32 - M22 * F3);
const detM2 = M11 * (F2 * M33 - M23 * F3) - F1 * (M21 * M33 - M23 * M31) + M13 * (M21 * F3 - F2 * M31);
const detM3 = M11 * (M22 * F3 - F2 * M32) - M12 * (M21 * F3 - F2 * M31) + F1 * (M21 * M32 - M22 * M31);
const alpha1 = detM1 / detM;
const alpha2 = detM2 / detM;
const alpha3 = detM3 / detM;
// Update velocities and positions using Euler integration
omega1 += alpha1 * timeStep;
omega2 += alpha2 * timeStep;
omega3 += alpha3 * timeStep;
theta1 += omega1 * timeStep;
theta2 += omega2 * timeStep;
theta3 += omega3 * timeStep;
}
function draw() {
// Apply a semi-transparent overlay to create fading trails
ctx.fillStyle = `rgba(13, 13, 13, ${trailFade})`;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Convert polar coordinates (angle, length) to Cartesian (x, y)
const x1 = pivotX + l1 * Math.sin(theta1);
const y1 = pivotY + l1 * Math.cos(theta1);
const x2 = x1 + l2 * Math.sin(theta2);
const y2 = y1 + l2 * Math.cos(theta2);
const x3 = x2 + l3 * Math.sin(theta3);
const y3 = y2 + l3 * Math.cos(theta3);
// Add new points to the trail arrays
trail1.push({ x: x1, y: y1 });
trail2.push({ x: x2, y: y2 });
trail3.push({ x: x3, y: y3 });
if (trail1.length > maxTrailLength) trail1.shift();
if (trail2.length > maxTrailLength) trail2.shift();
if (trail3.length > maxTrailLength) trail3.shift();
// Draw trails
drawTrail(trail1, '#ff4545');
drawTrail(trail2, '#45ff75');
drawTrail(trail3, '#45a5ff');
// Draw pendulum arms
ctx.strokeStyle = '#cccccc';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(pivotX, pivotY);
ctx.lineTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.lineTo(x3, y3);
ctx.stroke();
// Draw bobs
drawBob(x1, y1, m1, '#ff4545');
drawBob(x2, y2, m2, '#45ff75');
drawBob(x3, y3, m3, '#45a5ff');
// Draw pivot point
drawBob(pivotX, pivotY, 5, '#ffffff');
}
function drawTrail(trail, color) {
ctx.beginPath();
if (trail.length > 0) {
ctx.moveTo(trail[0].x, trail[0].y);
for (let i = 1; i < trail.length; i++) {
ctx.lineTo(trail[i].x, trail[i].y);
}
}
ctx.strokeStyle = color;
ctx.lineWidth = 1.5;
ctx.stroke();
}
function drawBob(x, y, mass, color) {
const radius = Math.cbrt(mass) * 3;
ctx.beginPath();
ctx.arc(x, y, radius, 0, 2 * Math.PI);
ctx.fillStyle = color;
ctx.fill();
}
let lastTime = 0;
function animate(currentTime) {
const deltaTime = (currentTime - lastTime) / 1000; // time in seconds
lastTime = currentTime;
// Use a fixed time step for physics stability, running multiple steps if needed
const timeStep = 1 / 60; // Simulate at a consistent 60 FPS
const steps = Math.max(1, Math.round(deltaTime / timeStep));
for (let i = 0; i < steps; i++) {
for (let j = 0; j < substeps; j++) {
updatePhysics(timeStep / substeps);
}
}
draw();
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
</script>