
# tautochrone.py
# Andrew Davison, ad@coe.psu.ac.th, Dec. 2025

'''
Tautochrone simulation - beads sliding on a cycloid curve demonstrate that they all reach the bottom at the same time regardless of starting height.


animate() uses cos(step * pi/2) to model the position parameter theta decreasing from its starting value to 0. It uses the cosine function to parameterize how far along the oscillation cycle each bead has progressed—ensuring they all reach the bottom simultaneously despite different starting heights.
'''

import math
import matplotlib.pyplot as plt
from matplotlib import animation  


WIDTH = 600
HEIGHT = 400

RAMP_OFFSET_X = 14
RAMP_OFFSET_Y = 16
RAMP_HEIGHT_OFFSET = 36

nFrames = 80
nPts = 200
a = 1

def cycloid(t, a):
  x = a * (t + math.sin(t))
  y = a * (1 - math.cos(t))
  return (x, y)


def createBeads():
  beadPts = []
  for b in beads:
    point, = ax.plot([], [], 'o', markersize=12, 
                     color=b['color'], markeredgecolor='black', 
                     markeredgewidth=1.5, zorder=3)
    beadPts.append(point)
  return beadPts



def buildRamp():
  xs = []
  ys = []
  for i in range(nPts):
    phi = i/(nPts-1)*math.pi - math.pi
    xc, yc = cycloid(phi, a) 
    xs.append(h/2*xc + x0)
    ys.append(h/2*yc + y0)
  return xs, ys


def animate(fno):
  step = fno/(nFrames-1)  # 0 to 1
  
  # Update each bead position
  for i, bead in enumerate(beads):
    theta = -bead['theta'] * math.pi * math.cos(step * math.pi/2)  
       # neg theta --> 0
    ''' a bead moves through its theta range following 
      cos(t), which is characteristic of SHM
    '''
    xc, yc = cycloid(theta, a) 
    x = h/2*xc + x0
    y = h/2*yc + y0
    beadPts[i].set_data([x], [y])
  return beadPts


# --------------------------------------

x0 = WIDTH - RAMP_OFFSET_X
y0 = RAMP_OFFSET_Y
h = HEIGHT - RAMP_HEIGHT_OFFSET

# Each bead starts at a different negative theta value
beads = [
  {'theta':1.0, 'color':'#0000c0'},
  {'theta':0.8, 'color':'#c00000'},
  {'theta':0.6, 'color':'#00c000'},
  {'theta':0.4, 'color':'#c0c000'}
]

fig = plt.figure(figsize=(WIDTH/100, HEIGHT/100))

ax = plt.gca()
ax.set_xlim(0, WIDTH)
ax.set_ylim(0, HEIGHT)
ax.axis('off')
ax.set_title('Tautochrone: All beads reach bottom at same time')
  
rampXs, rampYs = buildRamp()
ramp, = ax.plot(rampXs, rampYs, 'r-', lw=2.5)
beadPts = createBeads()

anim = animation.FuncAnimation(fig, animate, 
             frames=nFrames, interval=50, blit=True)

plt.show()
