Skip to content

Instantly share code, notes, and snippets.

@Soul-Master
Created February 3, 2025 17:01
Show Gist options
  • Select an option

  • Save Soul-Master/39cfe5adb4bd3295e672723810f162d1 to your computer and use it in GitHub Desktop.

Select an option

Save Soul-Master/39cfe5adb4bd3295e672723810f162d1 to your computer and use it in GitHub Desktop.
Brachistochrone - Realtime Gravity Simulation
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Brachistochrone – Real Gravity Realtime Simulation</title>
<style>
body { font-family: sans-serif; }
canvas { border: 1px solid #ccc; }
</style>
</head>
<body>
<h2>Brachistochrone – Real Gravity Realtime Simulation</h2>
<p>
Each path shows a bead sliding under gravity (without friction). A separate timer for each path stops when the bead reaches the goal (point B). All curves share the same endpoints (P₁ and P₂) even though their paths differ. The blue path is the cycloid (the brachistochrone—the fastest descent).<br>
<em>Note:</em> For the cycloid we initialize the simulation parameter at a tiny value (instead of exactly zero) to avoid numerical issues at the cusp. Until the simulation starts (elapsed time zero) the cycloid bead is drawn at P₁.
</p>
<canvas id="canvas" width="500" height="300"></canvas>
<div id="info"></div>
<script>
// --- Global settings & physics constants ---
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
// Gravitational acceleration (units/s^2)
const g = 9.81;
// Physics coordinates: we use
// P₁ = (0,0) and P₂ = (314,200)
const P1 = { x: 0, y: 0 };
const P2 = { x: 314, y: 200 };
// An offset so the drawing fits nicely on the canvas:
const offsetX = 50, offsetY = 50;
// --- Animation timing ---
let lastTimestamp = null;
// --- Define curve functions and initialize state ---
// Each curve object has:
// - name and color (for drawing)
// - f(u): parameterization of the curve (with u in [0,1]) that returns {x,y} in physics coords.
// - state: { u (curve parameter), v (speed along the curve), t (elapsed time), finished (bool) }
//
// Note: For the cycloid, we initialize u to a small epsilon (1e-4) to avoid the degenerate derivative at u=0.
const curves = [
{
name: "Straight Line",
color: "red",
f: function(u) {
return {
x: P1.x + (P2.x - P1.x) * u,
y: P1.y + (P2.y - P1.y) * u
};
},
state: { u: 0, v: 0, t: 0, finished: false }
},
{
name: "Cycloid",
color: "blue",
// Standard cycloid for the brachistochrone:
// x = R*(θ - sinθ), y = R*(1 - cosθ)
// We choose R = 100 so that when θ = π we get:
// x = 100*(π) ≈ 314 and y = 100*(1 - (-1)) = 200.
f: function(u) {
const R = 100;
const theta = Math.PI * u;
return {
x: R * (theta - Math.sin(theta)),
y: R * (1 - Math.cos(theta))
};
},
// Initialize at a tiny positive u (instead of 0) to avoid a degenerate derivative.
state: { u: 1e-4, v: 0, t: 0, finished: false }
},
{
name: "Circular Arc",
color: "green",
// We compute the circle that goes exactly through P₁ and P₂ with a desired sagitta.
// Given chord endpoints A and B, chord midpoint M, chord length L, and desired sagitta s:
// R = L^2/(8*s) + s/2, d = R - s,
// and the center is M - d·(unit perpendicular vector) (chosen so the arc “dips” downward).
f: function(u) {
const A = P1, B = P2;
const M = { x: (A.x + B.x) / 2, y: (A.y + B.y) / 2 };
const vx = B.x - A.x, vy = B.y - A.y;
const L = Math.hypot(vx, vy);
const s = 50; // desired sagitta
const R = (L * L) / (8 * s) + s / 2;
const d = R - s;
// Two possible perpendicular directions: choose one pointing downward.
let perpX = -vy, perpY = vx;
const L_perp = Math.hypot(perpX, perpY);
perpX /= L_perp;
perpY /= L_perp;
// The circle’s center:
const center = { x: M.x - d * perpX, y: M.y - d * perpY };
// Determine the angles corresponding to endpoints A and B.
const angleA = Math.atan2(A.y - center.y, A.x - center.x);
const angleB = Math.atan2(B.y - center.y, B.x - center.x);
// Adjust the difference to interpolate along the shorter arc.
let deltaAngle = angleB - angleA;
if (deltaAngle > Math.PI) deltaAngle -= 2 * Math.PI;
if (deltaAngle < -Math.PI) deltaAngle += 2 * Math.PI;
const angle = angleA + u * deltaAngle;
return { x: center.x + R * Math.cos(angle), y: center.y + R * Math.sin(angle) };
},
state: { u: 0, v: 0, t: 0, finished: false }
},
{
name: "Parabola",
color: "orange",
// A quadratic dip from the straight line.
f: function(u) {
return {
x: P2.x * u,
y: P2.y * u - 100 * u * (1 - u)
};
},
state: { u: 0, v: 0, t: 0, finished: false }
},
{
name: "Sinusoid",
color: "purple",
f: function(u) {
return {
x: P2.x * u,
y: P2.y * u - 50 * Math.sin(Math.PI * u)
};
},
state: { u: 0, v: 0, t: 0, finished: false }
}
];
// --- Helper: Numerical derivative of f(u) ---
// Uses central differences (or forward/backward near the boundaries).
function derivative(u, f) {
const h = 1e-5;
let u1, u2;
if (u < h) {
u1 = u;
u2 = u + h;
} else if (u > 1 - h) {
u1 = u - h;
u2 = u;
} else {
u1 = u - h;
u2 = u + h;
}
const p1 = f(u1);
const p2 = f(u2);
const dx = (p2.x - p1.x) / (u2 - u1);
const dy = (p2.y - p1.y) / (u2 - u1);
return { dx: dx, dy: dy };
}
// --- Helper: Draw the full path for a given curve (by sampling points) ---
function drawCurvePath(curve) {
ctx.beginPath();
const steps = 100;
let p = curve.f(0);
ctx.moveTo(p.x + offsetX, p.y + offsetY);
for (let i = 1; i <= steps; i++) {
const u = i / steps;
p = curve.f(u);
ctx.lineTo(p.x + offsetX, p.y + offsetY);
}
ctx.strokeStyle = curve.color;
ctx.lineWidth = 2;
ctx.stroke();
}
// --- Animation Loop ---
function animate(timestamp) {
if (!lastTimestamp) lastTimestamp = timestamp;
// dt in seconds.
const dt = (timestamp - lastTimestamp) / 1000;
lastTimestamp = timestamp;
// Clear canvas.
ctx.clearRect(0, 0, canvas.width, canvas.height);
// For each curve, update its bead (if not finished) and draw its path and bead.
curves.forEach(curve => {
// Draw the full path.
drawCurvePath(curve);
// Update bead state if not finished.
if (!curve.state.finished) {
let u = curve.state.u;
let deriv = derivative(u, curve.f);
// If the tangent is nearly zero (as for the cycloid near u=0), use a fallback.
if (Math.hypot(deriv.dx, deriv.dy) < 1e-8) {
deriv = derivative(1e-5, curve.f);
}
const dsdu = Math.hypot(deriv.dx, deriv.dy);
const du_dt = curve.state.v / dsdu;
// Tangential acceleration: projection of gravity along the curve.
const a = g * (deriv.dy) / dsdu;
// Euler integration update.
let newU = curve.state.u + du_dt * dt;
let newV = curve.state.v + a * dt;
let newT = curve.state.t + dt;
if (newU >= 1) {
newU = 1;
newV = 0;
curve.state.finished = true;
}
curve.state.u = newU;
curve.state.v = newV;
curve.state.t = newT;
}
// Determine the bead position.
// For the cycloid, if the elapsed time is still zero, force the position to be P₁.
let pos;
if (curve.name === "Cycloid" && curve.state.t === 0) {
pos = P1;
} else {
pos = curve.f(curve.state.u);
}
// Draw the bead.
ctx.beginPath();
ctx.arc(pos.x + offsetX, pos.y + offsetY, 5, 0, 2 * Math.PI);
ctx.fillStyle = curve.color;
ctx.fill();
});
// Draw the start (P₁) and end (P₂) points.
// P₁:
ctx.beginPath();
ctx.arc(P1.x + offsetX, P1.y + offsetY, 6, 0, 2 * Math.PI);
ctx.fillStyle = "black";
ctx.fill();
ctx.font = "12px sans-serif";
ctx.fillText("P₁", P1.x + offsetX - 10, P1.y + offsetY - 10);
// P₂:
ctx.beginPath();
ctx.arc(P2.x + offsetX, P2.y + offsetY, 6, 0, 2 * Math.PI);
ctx.fillStyle = "black";
ctx.fill();
ctx.fillText("P₂", P2.x + offsetX - 10, P2.y + offsetY - 10);
// Update the info div with elapsed time for each path.
let infoHTML = "<h3>Elapsed Time (seconds):</h3><ul>";
curves.forEach(curve => {
infoHTML += `<li style="color:${curve.color}">${curve.name}: ${curve.state.t.toFixed(2)} s ${curve.state.finished ? "(Finished)" : ""}</li>`;
});
infoHTML += "</ul>";
document.getElementById("info").innerHTML = infoHTML;
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
</script>
<p>
<strong>References:</strong>
<ul>
<li><a href="https://en.wikipedia.org/wiki/Brachistochrone_problem" target="_blank">Brachistochrone Problem – Wikipedia</a></li>
<li><a href="https://en.wikipedia.org/wiki/Cycloid" target="_blank">Cycloid – Wikipedia</a></li>
</ul>
</p>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment