Created
February 3, 2025 17:01
-
-
Save Soul-Master/39cfe5adb4bd3295e672723810f162d1 to your computer and use it in GitHub Desktop.
Brachistochrone - Realtime Gravity Simulation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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