---
title: "Brachistochrone"
description: |
  Lets create a double pendulum in Observable JS!
date: 2025-05-09
categories:
  - Observable JS
  - Code
  - Math
---

This is a bunch of text This is a bunch of text This is a bunch of text This is a bunch of text This is a bunch of text This is a bunch of text This is a bunch of text This is a bunch of text This is a bunch of text This is a bunch of text This is a bunch of text 

:::{.column-screen}
```{ojs}
//| echo: false


viewof canvas = {
  const height = 500;
  const canvas = html`<canvas width=${width} height="${height}" style="border: 1px solid #ccc; cursor: crosshair;"></canvas>`;
  const ctx = canvas.getContext('2d');

  let isDrawing = false;
  let lastX = 0;
  let lastY = 0;
  let pathLength = 0;
  let pathHistory = [{time: Date.now(), length: 0}];

  // Drawing settings
  ctx.strokeStyle = '#000';
  ctx.lineWidth = 2;
  ctx.lineCap = 'round';
  ctx.lineJoin = 'round';

  canvas.addEventListener('mousedown', (e) => {
    if (!isDrawing) {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      ctx.arc(300, 100, 5, 0, 2 * Math.PI);
      ctx.fillStyle = 'red';
      ctx.fill();
      ctx.arc(600, 300, 5, 0, 2 * Math.PI);
      ctx.fillStyle = 'red';
      ctx.fill();
      pathLength = 0;
      pathHistory = [{time: Date.now(), length: 0}];
      canvas.value = {length: pathLength, history: pathHistory};
      canvas.dispatchEvent(new CustomEvent("input"));
    }
    isDrawing = true;
    [lastX, lastY] = [e.offsetX, e.offsetY];
  });

  canvas.addEventListener('mousemove', (e) => {
    if (!isDrawing) return;

    // Calculate distance
    const distance = Math.sqrt(
      Math.pow(e.offsetX - lastX, 2) + 
      Math.pow(e.offsetY - lastY, 2)
    );
    const angle = Math.atan2(e.offsetY - lastY, e.offsetX - lastX)
    pathLength += distance;

    // Draw
    ctx.beginPath();
    ctx.moveTo(lastX, lastY);
    ctx.lineTo(e.offsetX, e.offsetY);
    ctx.stroke();
    [lastX, lastY] = [e.offsetX, e.offsetY];

    // Update value and emit event
    pathHistory.push({time: Date.now(), length: pathLength});
    canvas.value = {length: pathLength, history: pathHistory};
    canvas.dispatchEvent(new CustomEvent("input"));
  });

  canvas.addEventListener('mouseup', () => isDrawing = false);
  canvas.addEventListener('mouseout', () => isDrawing = false);

  // Initial value
  canvas.value = {length: 0, history: pathHistory};
  return canvas;
}

```


```{ojs}
//| echo: false
md`Current path length: **${canvas.length?.toFixed(0) || 0} pixels**`
```

```{ojs}
//| echo: false
import {Plot} from "@observablehq/plot"

pathChart = Plot.plot({
  width: width,
  height: 200,
  y: {label: "Path Length (pixels)"},
  x: {label: "Time", type: "time"},
  marks: [
    Plot.line(canvas.history || [], {
      x: "time", 
      y: "length",
      stroke: "steelblue",
      strokeWidth: 2
    })
  ]
})
```

This is a bunch of text This is a bunch of text This is a bunch of text This is a bunch of text This is a bunch of text This is a bunch of text This is a bunch of text This is a bunch of text This is a bunch of text This is a bunch of text This is a bunch of text 