Double Pendulum

Summary

The web page discusses the creation of a double pendulum animation using Observable JS, a tool recently supported by Quarto for interactive and animated visualizations. The author, inspired by Daniel Shiffman's work, initially created a double pendulum in Python and Processing.js, which gained popularity. The author has now ported this animation to Observable JS, sharing the code and explaining the physics calculations involved. The animation involves complex mathematical equations to simulate the pendulum's motion and draw it on a canvas. The author acknowledges that the implementation is not perfect and considers it a beginner's attempt. Despite some energy loss in the system, the author appreciates the real-time, interactive nature of the Observable JS implementation, making it superior to other languages for sharing data visualizations. The author also provides a link to the original Python implementation and encourages further exploration of the code.

Quarto (which this blog is built on) recently added support for Observable JS, which lets you make really cool interactive and animated visualizations. I have an odd fixation with finding new tools to visualize data, and while JS is far from the first tool I want to grab I figure I should give OJS a shot. Web browsers have been the best way to distribute and share applications for a long time now so I think its time that I invest some time to learn something better than a plotly diagram or jupyter notebook saved as a pdf to share data.

Video

My original Double Pendulum done in Python and Processing.js

Many years ago I hit the front page the /r/python with a double pendulum I made after watching the wonderful Daniel Shiffman of the Coding Train. The video was posted on gfycat which is now defunct but the internet archive has saved it: https://web.archive.org/web/20201108021323/https://gfycat.com/feistycompetentgarpike-daniel-shiffman-double-pendulum-coding-train

I originally used Processing’s Python bindings to make the animation. So, a lot of the hard work was done (mostly by Daniel), and this animation seems to be a crowd pleaser so I went ahead and ported it over. Keeping the code hidden since its not the focus here, but feel free to expand it and peruse.

viewof length1 = Inputs.range([50, 300], {step: 10, value: 200, label: "Length of pendulum 1"})
viewof length2 = Inputs.range([50, 300], {step: 10, value: 200, label: "Length of pendulum 2"})
viewof mass1 = Inputs.range([10, 100], {step: 5, value: 40, label: "Mass of pendulum 1"})
viewof mass2 = Inputs.range([10, 100], {step: 5, value: 40, label: "Mass of pendulum 2"})

Code

pendulum = {
  const width = 900;
  const height = 600;
  const canvas = DOM.canvas(width, height);
  const ctx = canvas.getContext("2d");
  const gravity = .1;
  const traceCanvas = DOM.canvas(width, height);
  const traceCtx = traceCanvas.getContext("2d");
  traceCtx.fillStyle = "white";
  traceCtx.fillRect(0, 0, width, height);

  const centerX = width / 2;
  const centerY = 200;

  // State variables
  let angle1 = Math.PI / 2;
  let angle2 = Math.PI / 2;
  let angularVelocity1 = 0;
  let angularVelocity2 = 0;
  let previousPosition2X = -1;
  let previousPosition2Y = -1;


  function animate() {
    // Physics calculations (same equations as Python)
    let numerator1Part1 = -gravity * (2 * mass1 + mass2) * Math.sin(angle1);
    let numerator1Part2 = -mass2 * gravity * Math.sin(angle1 - 2 * angle2);
    let numerator1Part3 = -2 * Math.sin(angle1 - angle2) * mass2;
    let numerator1Part4 = angularVelocity2 * angularVelocity2 * length2 + 
                          angularVelocity1 * angularVelocity1 * length1 * Math.cos(angle1 - angle2);
    let denominator1 = length1 * (2 * mass1 + mass2 - mass2 * Math.cos(2 * angle1 - 2 * angle2));
    let angularAcceleration1 = (numerator1Part1 + numerator1Part2 + numerator1Part3 * numerator1Part4) / denominator1;

    let numerator2Part1 = 2 * Math.sin(angle1 - angle2);
    let numerator2Part2 = angularVelocity1 * angularVelocity1 * length1 * (mass1 + mass2);
    let numerator2Part3 = gravity * (mass1 + mass2) * Math.cos(angle1);
    let numerator2Part4 = angularVelocity2 * angularVelocity2 * length2 * mass2 * Math.cos(angle1 - angle2);
    let denominator2 = length2 * (2 * mass1 + mass2 - mass2 * Math.cos(2 * angle1 - 2 * angle2));
    let angularAcceleration2 = (numerator2Part1 * (numerator2Part2 + numerator2Part3 + numerator2Part4)) / denominator2;

    // Update velocities and angles
    angularVelocity1 += angularAcceleration1;
    angularVelocity2 += angularAcceleration2;
    angle1 += angularVelocity1;
    angle2 += angularVelocity2;

    // Calculate positions
    let position1X = length1 * Math.sin(angle1);
    let position1Y = length1 * Math.cos(angle1);
    let position2X = position1X + length2 * Math.sin(angle2);
    let position2Y = position1Y + length2 * Math.cos(angle2);

    // Clear and draw to canvas
    ctx.fillStyle = "white";
    ctx.fillRect(0, 0, width, height);
    ctx.drawImage(traceCanvas, 0, 0);

    // Draw pendulum
    ctx.save();
    ctx.translate(centerX, centerY);

    // First arm and mass
    ctx.beginPath();
    ctx.moveTo(0, 0);
    ctx.lineTo(position1X, position1Y);
    ctx.strokeStyle = "black";
    ctx.lineWidth = 2;
    ctx.stroke();

    ctx.beginPath();
    ctx.arc(position1X, position1Y, mass1/2, 0, 2 * Math.PI);
    ctx.fillStyle = "black";
    ctx.fill();

    // Second arm and mass
    ctx.beginPath();
    ctx.moveTo(position1X, position1Y);
    ctx.lineTo(position2X, position2Y);
    ctx.stroke();

    ctx.beginPath();
    ctx.arc(position2X, position2Y, mass2/2, 0, 2 * Math.PI);
    ctx.fill();

    ctx.restore();

    // Draw trace line
    if (previousPosition2X !== -1 && previousPosition2Y !== -1) {
      traceCtx.save();
      traceCtx.translate(centerX, centerY);
      traceCtx.beginPath();
      traceCtx.moveTo(previousPosition2X, previousPosition2Y);
      traceCtx.lineTo(position2X, position2Y);
      traceCtx.strokeStyle = "black";
      traceCtx.stroke();
      traceCtx.restore();
    }

    previousPosition2X = position2X;
    previousPosition2Y = position2Y;

    requestAnimationFrame(animate);
  }

  animate();
  return canvas;
}

Conclusion

I think this is far from an idiomatic implementation so I’ll keep this brief. I don’t think I used JS or Observable as well as I could have so treat this as a beginner stabbing into the dark because thats essentially what the code is.

This was quite a bit more work than the original Python implementation, but running real time, having beaufitul defaults, and being interactive without a backend make this leagues better than anything offered by any other language. There is definitely a loss of energy in the system over time that I attribute to Javascript being a mess, but I doubt that I would ever move all of my analysis to JS anyways so I don’t think it matters. Its also very likely I’m doing something bad with my timesteps.

Reuse

CC BY 4.0


This content was originally posted on my projects website here. The above summary was generated by the Kagi Summarizer.