diff --git a/.gitignore b/.gitignore index c74b015..9010c3f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ posts/*/\.jupyter_cache/ !/.quarto/_freeze/ !/.quarto/_freeze/* /.quarto/ +**/.DS_Store diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5ff6d5f..8ead562 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,7 +1,7 @@ build: stage: build image: - name: gcr.io/kaniko-project/executor:v1.21.0-debug + name: gcr.io/kaniko-project/executor:v1.23.2-debug entrypoint: [""] script: - /kaniko/executor @@ -9,6 +9,7 @@ build: --dockerfile "${CI_PROJECT_DIR}/Dockerfile" --destination "${CI_REGISTRY_IMAGE}:${CI_COMMIT_BRANCH}" --destination "${CI_REGISTRY_IMAGE}:latest" + --cleanup staging: cache: diff --git a/posts/2025-05-10-double-pendulum-redux/FeistyCompetentGarpike-mobile.mp4 b/posts/2025-05-10-double-pendulum-redux/FeistyCompetentGarpike-mobile.mp4 new file mode 100644 index 0000000..b98e3ff Binary files /dev/null and b/posts/2025-05-10-double-pendulum-redux/FeistyCompetentGarpike-mobile.mp4 differ diff --git a/posts/2025-05-10-double-pendulum-redux/index.qmd b/posts/2025-05-10-double-pendulum-redux/index.qmd new file mode 100644 index 0000000..e728abb --- /dev/null +++ b/posts/2025-05-10-double-pendulum-redux/index.qmd @@ -0,0 +1,155 @@ +--- +title: "Double Pendulum" +description: | + Lets create a double pendulum in Observable JS! +date: 2024-05-09 +categories: + - Observable JS + - Code + - Math +draft: false +freeze: true +image: FeistyCompetentGarpike-mobile.mp4 +image-alt: "My original Double Pendulum done in Python and Processing.js" +--- + +Quarto (which this blog is built on) recently added support for [Observable JS](https://observablehq.com/@observablehq/observable-javascript), 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. + +![My original Double Pendulum done in Python and Processing.js](FeistyCompetentGarpike-mobile.mp4){fig-alt="My original Double Pendulum done in Python and Processing.js"} + +Many years ago I hit the front page the [/r/python](https://www.reddit.com/r/Python/comments/ci1cg4/double_pendulum_made_with_processingpy/) with a double pendulum I made after watching the wonderful [Daniel Shiffman](https://thecodingtrain.com/showcase/author/anson-biggs) 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](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. + + +```{ojs} +//| echo: false + +// Interactive controls +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"}) + +``` + + + +```{ojs} +//| code-fold: true +//| column: page + +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](https://gitlab.com/MisterBiggs/double_pendulum/blob/master/double_pendulum.pyde), 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. \ No newline at end of file