This project is powered by p5.js and originates from this Double Pendulum tutorial. This is a p5 version that includes drag+drop, styling, and sliders to modify realtime variables.
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" />
<title>p5.js - Double Penulum</title>
<style type="text/css"> body { padding: 0; margin: 0; } </style>
<script src="lib/p5.min.js"></script>
<script src="lib/p5.dom.min.js"></script>
<!-- <script src="lib/p5.sound.min.js"></script> -->
<script src="sketch.js"></script>
</head>
<body>
</body>
</html>
// screen size
let screenW = 512;
let screenH = 512;
// colors
let bgcolor = "#1B1B1E";
let textcolor = "#A9BCD0";
let linecolor = "#58A4B0";
// pendulum sizes/physics
let r1 = (screenW / 6);
let r2 = (screenW / 6);
let m1 = 10.0;
let m2 = 10.0;
let a1 = 0;
let a2 = 0;
let a1_v = 0.0;
let a2_v = 0.0;
let a1_a = 0.0;
let a2_a = 0.0;
let g = 1.0;
let ballDiameter = 6;
let dampening = 0.998;
// ball points
let x1;
let y1;
let x2;
let y2;
// sliders
let r1_slider;
let r2_slider;
let m1_slider;
let m2_slider;
let dampening_slider;
// state
let dragging = false;
function setup() {
// set bg color
document.body.style.background = bgcolor;
// create canvas
createCanvas(screenW, screenH);
// set default state
a1 = PI / 4;
a2 = -PI / 8;
// create sliders
r1_slider = createSlider(10, (width / 3), r1);
r1_slider.position(15, 15);
r2_slider = createSlider(10, (width / 3), r2);
r2_slider.position(r1_slider.x, (r1_slider.y + r1_slider.height + 10));
m1_slider = createSlider(0.2, 20, m1, 0.1);
m1_slider.position(r2_slider.x, (r2_slider.y + r2_slider.height + 10));
m2_slider = createSlider(0.2, 20, m2, 0.1);
m2_slider.position(m1_slider.x, (m1_slider.y + m1_slider.height + 10));
gravity_slider = createSlider(0, 20, g, 0.1);
gravity_slider.position(m2_slider.x, (m2_slider.y + m2_slider.height + 10));
dampening_slider = createSlider(975, 999, (dampening * 1000));
dampening_slider.position(gravity_slider.x, (gravity_slider.y + gravity_slider.height + 10));
}
function draw() {
// pull realtime values from sliders
r1 = r1_slider.value();
r2 = r2_slider.value();
m1 = m1_slider.value();
m2 = m2_slider.value();
g = gravity_slider.value();
dampening = (dampening_slider.value() / 1000);
// draw bg
background(bgcolor);
// draw text for sliders
let text_size = 12;
strokeWeight(0);
fill(textcolor);
textSize(text_size);
text("rod-1 length: "+ r1, (r1_slider.x + r1_slider.width + 15), (r1_slider.y + (r1_slider.height / 2) + (text_size / 2)));
text("rod-2 length: "+ r2, (r2_slider.x + r2_slider.width + 15), (r2_slider.y + (r2_slider.height / 2) + (text_size / 2)));
text("mass 1: "+ m1, (m1_slider.x + m1_slider.width + 15), (m1_slider.y + (m1_slider.height / 2) + (text_size / 2)));
text("mass 2: "+ m2, (m2_slider.x + m2_slider.width + 15), (m2_slider.y + (m2_slider.height / 2) + (text_size / 2)));
text("gravity: "+ g, (gravity_slider.x + gravity_slider.width + 15), (gravity_slider.y + (gravity_slider.height / 2) + (text_size / 2)));
text("dampening: "+ dampening, (dampening_slider.x + dampening_slider.width + 15), (dampening_slider.y + (dampening_slider.height / 2) + (text_size / 2)));
// line styling
stroke(linecolor);
strokeWeight(3);
fill(linecolor);
// translate to center
translate(width/2, height/2);
calcPolarPoints();
// draw pendulum
line(0, 0, x1, y1);
ellipse(x1, y1, ballDiameter, ballDiameter);
line(x1, y1, x2, y2);
ellipse(x2, y2, ballDiameter, ballDiameter);
// calculate angles
calcAngles();
// apply forces
applyForces();
// soften velocities over time
dampenVelocities();
}
function calcPolarPoints() {
// ball 1
x1 = r1 * sin(a1);
y1 = r1 * cos(a1);
// ball 2
x2 = (x1 + (r2 * sin(a2)));
y2 = (y1 + (r2 * cos(a2)));
}
function calcAngles() {
if(dragging !== false) {
a1_v = 0;
a1_a = 0;
a2_v = 0;
a2_a = 0;
return;
}
let num1, num2, num3, num4, den;
// −g (2 m1 + m2) sin θ1
num1 = (-g * (2 * m1 + m2) * sin(a1));
// −m2 g sin(θ1 − 2 θ2)
num2 = (-m2 * g * sin(a1 - 2 * a2));
// −2 sin(θ1 − θ2) m2
num3 = (-2 * sin(a1 - a2) * m2);
// θ2'2 L2 + θ1'2 L1 cos(θ1 − θ2)
num4 = (sq(a2_v) * r2 + sq(a1_v) * r1 * cos(a1 - a2));
// L1 (2 m1 + m2 − m2 cos(2 θ1 − 2 θ2))
den = (r1 * (2 * m1 + m2 - m2 * cos(2 * a1 - 2 * a2)));
a1_a = ((num1 + num2 + (num3 * num4)) / den);
// 2 sin(θ1 − θ2)
num1 = (2 * sin(a1 - a2));
// θ1'2 L1 (m1 + m2)
num2 = (sq(a1_v) * r1 * (m1 + m2));
// g (m1 + m2) cos θ1
num3 = (g * (m1 + m2) * cos(a1));
// θ2'2 L2 m2 cos(θ1 − θ2))
num4 = (sq(a2_v) * r2 * m2 * cos(a1 - a2));
// L2 (2 m1 + m2 − m2 cos(2 θ1 − 2 θ2))
den = (r2 * (2 * m1 + m2 - m2 * cos(2 * a1 - 2 * a2)));
a2_a = ((num1 * (num2 + num3 + num4)) / den);
// wrap acceleration to prevent drawing from breaking due to high speed
a1_a %= (PI * 2);
a2_a %= (PI * 2);
}
function applyForces() {
// add acceleration to velocity
a1_v += a1_a;
a2_v += a2_a;
// add velocity to pendulum angles
a1 += a1_v;
a2 += a2_v;
}
function dampenVelocities() {
// soften velocity
a1_v *= dampening;
a2_v *= dampening;
}
function calcDraggedAngle() {
if(false === dragging) {
return;
}
if(dragging === 1) {
// set angle1 to be from ball 1 starting point (center) to mouse position
let delta_x = (mouseX - (width / 2));
let delta_y = ((height / 2) - mouseY);
a1 = atan2(delta_y, delta_x) + PI/2;
} else if(dragging === 2) {
// set angle1 to be from ball 2 starting point (center + (x1, x2)) to mouse position
let delta_x = (mouseX - ((width / 2) + x1));
let delta_y = (((height / 2) + y1) - mouseY);
a2 = atan2(delta_y, delta_x) + PI/2;
}
}
function touchStarted() {
// distance is from center since we translate to center of screen in draw()
let mouseDeltaX = (mouseX - (width / 2));
let mouseDeltaY = (mouseY - (height / 2));
// check if we clicked on ball 1
let dist1 = dist(x1, y1, mouseDeltaX, mouseDeltaY);
// max distance is half of ball's line distance from ball
let maxDist1 = (r1 / 2);
if(dist1 <= maxDist1) {
// dragging ball 1
dragging = 1;
calcDraggedAngle();
return;
}
// check if we clicked on ball 2
let dist2 = dist(x2, y2, mouseDeltaX, mouseDeltaY);
let maxDist2 = (r2 / 2);
if(dist2 <= maxDist2) {
// dragging ball 2
dragging = 2;
// stop acceleration on a1 to prevent movement when dragging child ball
a1_a = 0;
calcDraggedAngle();
return;
}
dragging = false;
return;
}
function touchMoved() {
if(false === dragging) {
// not dragging either ball
return;
}
calcDraggedAngle();
}
function touchEnded() {
calcDraggedAngle();
// reset dragging
dragging = false;
}