Cycloidal Curves: Theory, Parametric Equations, and Animation
Contents
Objective
The objective of this post is to systematically derive the corresponding parametric equations and implement them in Python for visualization using Manim and Matplotlib. The aim is not only to develop a clear analytical understanding of these curves, but also to bring them to life through computational animation.
AI-Assisted Content Notice:
Computational visualizations and Python scripts provided here were developed with AI assistance and carefully reviewed to ensure alignment with the mathematical principles discussed.
Back to top
Definition
Cycloidal curves are the loci of a fixed point on the circumference of a circle as it rolls without slipping along a given path. Depending on whether the circle rolls along a straight line, on the outside of another circle, or on the inside of another circle, the resulting curve is classified as a cycloid, an epicycloid, or a hypocycloid.

Cardioid ($r = R$), a special case of the epicycloid.
Back to top
Cycloid
$x(\theta) = r(\theta - \sin \theta)$
$y(\theta) = r(1 - \cos \theta)$
A cycloid is the curve traced by a point on the circumference of a circle as it rolls along a straight line (the directrix) without slipping.
In engineering, the cycloid is notable for its brachistochrone property—representing the path of fastest descent under gravity—and its tautochrone property, where the time taken by a particle to reach the lowest point is independent of its starting position.
Derivation of Parametric Equations of Cycloid

$s = r\theta$
$a = r \cos \theta$
$b = r \sin \theta$
$x = s - b = r\theta - r \sin \theta$
$x = r(\theta - \sin \theta)$
$y = r - a = r - r \cos \theta)$
$y = r(1 - \cos \theta)$
Python Code for Matplotlib: Cycloid
import numpy as np
import matplotlib.pyplot as plt
# Parameters
r = 1.0 # Radius of the rolling circle
theta_val = np.pi / 3 # Highlighted angle (60 degrees)
theta = np.linspace(0, 2 * np.pi, 1000)
# 1. Calculate Cycloid Coordinates
# Equations: x = r(theta - sin(theta)), y = r(1 - cos(theta))
x_cyc = r * (theta - np.sin(theta))
y_cyc = r * (1 - np.cos(theta))
# 2. Calculate Rolling Circle at theta_val
# Center of the circle is at (r*theta_val, r)
xc = r * theta_val
yc = r
circle_angles = np.linspace(0, 2 * np.pi, 200)
x_circle = xc + r * np.cos(circle_angles)
y_circle = yc + r * np.sin(circle_angles)
# 3. Calculate the Tracing Point (the point on the circle edge)
px = r * (theta_val - np.sin(theta_val))
py = r * (1 - np.cos(theta_val))
# 4. Calculate the Arc of the Circle
# The circle starts rolling from angle -pi/2 (the bottom contact point)
# It rotates clockwise, so the arc spans from -pi/2 to (-pi/2 - theta_val)
arc_angles = np.linspace(-np.pi/2, -np.pi/2 - theta_val, 100)
x_arc = xc + r * np.cos(arc_angles)
y_arc = yc + r * np.sin(arc_angles)
# --- Plotting ---
plt.figure(figsize=(12, 6))
# Plot the full cycloid (Blue)
plt.plot(x_cyc, y_cyc, label='Cycloid Path', color='blue', linewidth=2, zorder=1)
# Plot the rolling circle (Black)
plt.plot(x_circle, y_circle, label='Rolling Circle', color='black', linestyle='--', linewidth=1.5)
# Plot the horizontal distance traveled (Red)
plt.plot([0, xc], [0, 0], color='red', linewidth=3, label='Linear Distance Traveled', solid_capstyle='round')
# Plot the arc of the circle (Red)
plt.plot(x_arc, y_arc, color='red', linewidth=3, label='Arc Length Rolled')
# Additional visual markers
plt.plot(xc, yc, 'ko', markersize=4) # Circle Center
plt.plot(px, py, 'ro', markersize=6) # Tracing Point P
plt.plot([xc, px], [yc, py], color='black', alpha=0.3) # Radius to P
plt.axhline(0, color='black', linewidth=1) # Ground line
# Formatting
plt.title(r'Cycloid: $\text{Arc Length} = \text{Horizontal Distance}$', fontsize=14)
plt.xlabel('$x = r(\theta - \sin\\theta)$')
plt.ylabel('$y = r(1 - \cos\\theta)$')
plt.axis('equal')
plt.grid(True, linestyle=':', alpha=0.6)
plt.legend(loc='upper right')
plt.tight_layout()
plt.show()Output of Matplotlib: Cycloid

Python Code for Manim: Cycloid
from manim import *
import numpy as np
class Cycloid(Scene):
def construct(self):
# Parameters
r = 1.5
# Ground shifted downward by 0.5 * r
ground_y = -0.5 * r # -0.75
# Center y-level so circle touches the ground: ground_y + r
center_y_level = ground_y + r
# Horizontal shift to center the cycloid peak at x=0
shift_x = -PI * r
start_theta = -60 * DEGREES
end_theta = TAU + 60 * DEGREES
# 1. Directrix (Ground) - Red
# Note: Ground is placed at ground_y
ground = Line(
start=[-6.75, ground_y, 0],
end=[6.75, ground_y, 0],
color=RED,
stroke_width=2
)
# 2. ValueTracker for the rolling angle theta
t_tracker = ValueTracker(start_theta)
# 3. The Rolling Group (Circle + Rotating Radius + Tracing Point)
def get_rolling_group():
theta = t_tracker.get_value()
# Center moves linearly: x = r * theta + shift, y = center_y_level
center_pos = np.array([r * theta + shift_x, center_y_level, 0])
# The Tracing Point (Yellow)
# Calculated relative to center
dot_pos = center_pos + np.array([
-r * np.sin(theta),
-r * np.cos(theta),
0
])
circle = Circle(radius=r, color=BLUE, stroke_width=4).move_to(center_pos)
radius_line = Line(center_pos, dot_pos, color=BLUE, stroke_width=4)
dot = Dot(dot_pos, color=YELLOW, radius=0.08).set_z_index(10)
return VGroup(circle, radius_line, dot)
rolling_circle_group = always_redraw(get_rolling_group)
# 4. The Cycloid Trace (Yellow)
# y-coordinate is (r * (1 - cos(t))) + ground_y
cycloid_path = always_redraw(lambda: ParametricFunction(
lambda t: np.array([
r * (t - np.sin(t)) + shift_x,
r * (1 - np.cos(t)) + ground_y,
0
]),
t_range=[start_theta, max(start_theta + 0.001, t_tracker.get_value())],
color=YELLOW,
stroke_width=6
))
title = Tex(r"\textbf{Cycloid}", font_size=52).next_to(ground, DOWN, buff=0.5)
# --- Animation Sequence ---
self.play(Write(title))
self.play(Create(ground))
self.play(Create(cycloid_path), Create(rolling_circle_group))
self.play(
t_tracker.animate.set_value(end_theta),
run_time=12, # Slightly slower for the larger scale
rate_func=linear
)
self.wait(3)Output of Manim: Cycloid
Note: The visual branding shown in the videos (e.g. MATHalino logo) is not included in the shared Manim code to keep the scripts clean and reusable.
Back to top
Epicycloid
$x(\theta) = (R + r) \cos \theta - r \cos \left( \dfrac{R + r}{r} \theta \right)$
$y(\theta) = (R + r) \sin \theta - r \sin \left( \dfrac{R + r}{r} \theta \right)$
An epicycloid is a plane curve generated by a point on the circumference of a circle (the generating circle) that rolls along the exterior of a fixed circle (the base circle). The shape of the resulting path is determined by the ratio of the radii of the two circles. A common application of the epicycloid is in the profile of planetary gears and certain types of rotary pumps.
Derivation of Parametric Equations of Epicycloid

$s_2 = s_1$
$r \alpha = R \theta$
$\alpha = \dfrac{R \theta}{r}$
$x_1 = (R + r) \cos \theta$
$\begin{align}x_2 & = r \cos (\alpha + \theta) = r \cos \left(\dfrac{R \theta}{r} + \theta \right) \\
& = r \cos \left(\dfrac{R + r}{r} \theta \right)\end{align}$
$x = x_1 - x_2$
$x = (R + r) \cos \theta - r \cos \left(\dfrac{R + r}{r} \theta \right)$
$y_1 = (R + r) \sin \theta$
$\begin{align}y_2 & = r \sin (\alpha + \theta) = r \sin \left(\dfrac{R \theta}{r} + \theta \right) \\
& = r \sin \left(\dfrac{R + r}{r} \theta \right)\end{align}$
$y = y_1 - y_2$
$y = (R + r) \sin \theta - r \sin \left(\dfrac{R + r}{r} \theta \right)$
Python Code for Matplotlib: Epicycloid
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Circle, Arc
# -----------------------------
# Parameters
# -----------------------------
R = 3.0 # fixed circle radius
r = 1.0 # rolling circle radius
theta = np.linspace(0, 2*np.pi, 2000)
# Epicycloid curve
k = (R + r) / r
x = (R + r) * np.cos(theta) - r * np.cos(k * theta)
y = (R + r) * np.sin(theta) - r * np.sin(k * theta)
# -----------------------------
# Snapshot angle (30 degrees)
# -----------------------------
theta0_deg = 30.0
theta0 = np.deg2rad(theta0_deg)
# Rolling circle center at theta0
C = np.array([(R + r) * np.cos(theta0), (R + r) * np.sin(theta0)])
# Tracing point at theta0 (BLUE dot)
P = np.array([
(R + r) * np.cos(theta0) - r * np.cos(k * theta0),
(R + r) * np.sin(theta0) - r * np.sin(k * theta0),
])
# Fixed-circle contact point (on radius R, direction theta0)
u = np.array([np.cos(theta0), np.sin(theta0)])
Q_fixed = R * u
# Rolling-circle contact point (point of tangency on rolling circle)
# (from rolling center C toward origin, i.e., opposite u)
Q_roll = C - r * u
# -----------------------------
# Arcs: fixed arc and equal-length rolling arc
# -----------------------------
# Fixed arc length = R * theta0
# Rolling arc must satisfy: r * phi = R * theta0 => phi = (R/r)*theta0
phi = (R / r) * theta0
phi_deg = np.rad2deg(phi)
# Fixed-circle arc: from 0 to theta0_deg
fixed_arc = Arc(
(0, 0), 2*R, 2*R,
angle=0, theta1=0, theta2=theta0_deg,
color="red", lw=3
)
# Rolling-circle arc: start at the CURRENT contact point direction (from C to Q_roll)
# That direction (in degrees) is theta0 + 180°
start_deg = theta0_deg + 180.0
rolling_arc = Arc(
(C[0], C[1]), 2*r, 2*r,
angle=0, theta1=start_deg, theta2=start_deg + phi_deg,
color="red", lw=3
)
# -----------------------------
# Plot
# -----------------------------
fig, ax = plt.subplots(figsize=(8, 8))
# Epicycloid curve (keep as your original style)
ax.plot(x, y, color="royalblue", lw=2, label=f"Epicycloid (R={R}, r={r})")
# Fixed circle (context)
ax.add_patch(Circle((0, 0), R, fill=False, ec="gray", lw=2, ls="--", alpha=0.6))
# Rolling circle at 30° (BLACK)
ax.add_patch(Circle((C[0], C[1]), r, fill=False, ec="black", lw=2.5))
# Tracing point dot (BLUE)
ax.plot(P[0], P[1], "o", color="blue", markersize=7, zorder=5)
# Radius line from rolling center to tracing point (BLACK)
ax.plot([C[0], P[0]], [C[1], P[1]], color="black", lw=2)
# (Optional but helpful) show tangency/contact points lightly
ax.plot(Q_fixed[0], Q_fixed[1], "o", color="red", markersize=5, alpha=0.8)
ax.plot(Q_roll[0], Q_roll[1], "o", color="red", markersize=5, alpha=0.8)
# Arcs (RED)
ax.add_patch(fixed_arc)
ax.add_patch(rolling_arc)
# Labels
ax.set_title("Epicycloid Snapshot at 30° with Equal Arc Lengths", fontsize=14)
ax.set_aspect("equal", adjustable="box")
ax.grid(True, linestyle=":", alpha=0.6)
ax.legend(loc="upper right")
# Nice framing
pad = R + 2.5*r
ax.set_xlim(-(R+pad), (R+pad))
ax.set_ylim(-(R+pad), (R+pad))
plt.show()Output of Matplotlib: Epicycloid

Python Code for Manim: Epicycloid
from manim import *
import numpy as np
class Epicycloid(Scene):
def construct(self):
# --- Parameters ---
R = 1.75
r = R / 3
# --- Title ---
title = Tex(r"\textbf{Epicycloid}", font_size=52).to_edge(UP)
self.play(Write(title))
# --- Tracker ---
t = ValueTracker(0.0)
# --- Fixed circle (RED) ---
fixed_circle = Circle(radius=R, color=RED, stroke_width=4)
# --- Rolling center position ---
def rolling_center_point():
ang = t.get_value()
return np.array([(R + r) * np.cos(ang), (R + r) * np.sin(ang), 0.0])
# --- Epicycloid tracing point position ---
def epicycloid_point():
ang = t.get_value()
k = (R + r) / r
return np.array([
(R + r) * np.cos(ang) - r * np.cos(k * ang),
(R + r) * np.sin(ang) - r * np.sin(k * ang),
0.0
])
# --- Rolling circle (BLUE) ---
rolling_circle = Circle(radius=r, color=BLUE, stroke_width=3)
# Move + roll (rotate) updater
def roll_updater(mob):
mob.move_to(rolling_center_point())
# desired physical rotation angle about its center
# theta = -((R+r)/r) * t
target = -((R + r) / r) * t.get_value()
# set absolute angle (not incremental): rotate by (target - current_angle)
mob.rotate(target - mob.get_angle(), about_point=mob.get_center())
rolling_circle.add_updater(roll_updater)
# --- Tracing dot (YELLOW) ---
gen_dot = always_redraw(lambda: Dot(epicycloid_point(), color=YELLOW, radius=0.06))
# --- Radius line inside rolling circle: center -> tracing point ---
radius_line = always_redraw(
lambda: Line(
rolling_center_point(),
epicycloid_point(),
color=BLUE,
stroke_width=4
)
)
# --- Trace (YELLOW epicycloid) ---
trace = TracedPath(
gen_dot.get_center,
stroke_color=YELLOW,
stroke_width=5,
dissipating_time=0.0
)
label = Tex(r"$r=\frac{R}{3}$", font_size=38).to_edge(DOWN)
# --- Add objects ---
self.play(Create(fixed_circle), Create(rolling_circle), Create(radius_line), Create(trace), Create(gen_dot))
self.play(Write(label))
# --- Animate (closes at 2π) ---
self.play(t.animate.set_value(2 * PI), run_time=10, rate_func=linear)
self.wait(2)Output of Manim: Epicycloid
Back to top
Hypocycloid
$x(\theta) = (R - r) \cos \theta + r \cos \left( \dfrac{R - r}{r} \theta \right)$
$y(\theta) = (R - r) \sin \theta + r \sin \left( \dfrac{R - r}{r} \theta \right)$
A hypocycloid is the path traced by a point on the circumference of a circle as it rolls along the interior of a larger fixed circle. If the radius of the rolling circle is exactly one-fourth that of the fixed circle, the resulting four-cusped curve is known as an astroid. These curves are frequently analyzed in kinematics to describe the relative motion of machine parts within a circular housing.
Derivation of Parametric Equations of Hypocycloid

$s_2 = s_1$
$r(\alpha + \theta) = R \theta$
$\alpha = \dfrac{R \theta}{r} - \theta$
$\alpha = \dfrac{R - r}{r}\theta$
$x_1 = (R - r) \cos \theta$
$x_2 = r \cos \alpha = r \cos \left( \dfrac{R - r}{r}\theta \right)$
$x = x_1 + x_2$
$x = (R - r) \cos \theta + r \cos \left( \dfrac{R - r}{r}\theta \right)$
$y_1 = (R - r) \sin \theta$
$y_2 = r \sin \alpha = r \sin \left( \dfrac{R - r}{r}\theta \right)$
$y = y_1 + y_2$
$y = (R - r) \sin \theta + r \sin \left( \dfrac{R - r}{r}\theta \right)$
Python Code for Matplotlib: Hypocycloid
import numpy as np
import matplotlib.pyplot as plt
# -----------------------------
# Parameters
# -----------------------------
R = 4.0
r = R / 4
t_pos = np.pi / 6 # 30 degrees
theta = np.linspace(0, 2 * np.pi, 1000)
# -----------------------------
# 1) Hypocycloid curve
# -----------------------------
x_hypo = (R - r) * np.cos(theta) + r * np.cos(((R - r) / r) * theta)
y_hypo = (R - r) * np.sin(theta) - r * np.sin(((R - r) / r) * theta)
# -----------------------------
# 2) Fixed circle (reference)
# -----------------------------
x_fixed = R * np.cos(theta)
y_fixed = R * np.sin(theta)
# -----------------------------
# 3) Rolling circle at position t_pos
# -----------------------------
center = np.array([(R - r) * np.cos(t_pos), (R - r) * np.sin(t_pos)])
x_roll = center[0] + r * np.cos(theta)
y_roll = center[1] + r * np.sin(theta)
# Contact point on the fixed circle
contact = np.array([R * np.cos(t_pos), R * np.sin(t_pos)])
# -----------------------------
# Tracing point (blue dot) at angle t_pos
# -----------------------------
trace = np.array([
(R - r) * np.cos(t_pos) + r * np.cos(((R - r) / r) * t_pos),
(R - r) * np.sin(t_pos) - r * np.sin(((R - r) / r) * t_pos)
])
# -----------------------------
# Equal-length arcs:
# s = R*t_pos, phi = (R/r)*t_pos
# Fixed arc: 0 -> t_pos (OK as before)
# Rolling arc: CLOCKWISE from contact direction: t_pos -> (t_pos - phi)
# -----------------------------
phi = (R / r) * t_pos
# Fixed arc (solid red)
t_arc_fixed = np.linspace(0, t_pos, 200)
x_arc_fixed = R * np.cos(t_arc_fixed)
y_arc_fixed = R * np.sin(t_arc_fixed)
# Rolling arc (solid red) - CLOCKWISE
t_arc_roll = np.linspace(t_pos, t_pos - phi, 300)
x_arc_roll = center[0] + r * np.cos(t_arc_roll)
y_arc_roll = center[1] + r * np.sin(t_arc_roll)
# -----------------------------
# Plotting
# -----------------------------
plt.figure(figsize=(8, 8))
# Fixed circle (light reference)
plt.plot(x_fixed, y_fixed, 'k--', alpha=0.25, label='Fixed Circle (R)')
# Hypocycloid curve (BLUE)
plt.plot(x_hypo, y_hypo, color='blue', linewidth=2.5, label='Hypocycloid (r=R/4)')
# Rolling circle
plt.plot(x_roll, y_roll, 'black', linewidth=1.5, label='Rolling Circle (r)')
# Rolling center
plt.plot(center[0], center[1], 'ro', markersize=4)
# Contact point (optional)
plt.plot(contact[0], contact[1], 'k.', markersize=6)
# Fixed arc (solid red)
plt.plot(x_arc_fixed, y_arc_fixed, 'red', linewidth=3, solid_capstyle='round') #, label='Fixed Arc (length s)')
# Rolling arc (solid red)
plt.plot(x_arc_roll, y_arc_roll, 'red', linewidth=3, solid_capstyle='round') #, label='Rolling Arc (length s)')
# Tracing point (smaller, same blue)
plt.plot(trace[0], trace[1], marker='o', color='blue', markersize=6) #, label='Tracing Point')
# Final polish
ax = plt.gca()
ax.set_aspect('equal', adjustable='box')
plt.axis('off')
plt.legend(loc='upper right')
plt.title(rf"Hypocycloid Geometry ($r = R/4$), $t_{{pos}}={t_pos:.3f}$ rad")
plt.show()Output of Matplotlib: Hypocycloid

Python Code for Manim: Hypocycloid
from manim import *
import numpy as np
class Hypocycloid(Scene):
def construct(self):
# --- Background + defaults ---
self.camera.background_color = GRAY_E
Tex.set_default(color=GRAY_A)
# Add MATHalino logo watermark
logo = ImageMobject("logo.png")
logo.scale(0.8)
logo.set_opacity(0.03)
self.add(logo)
# --- Geometry parameters ---
R = 3.5
r = R / 4
# Colors (as requested)
FIXED_COLOR = RED
ROLLING_COLOR = BLUE
TRACE_COLOR = YELLOW
# --- Trackers ---
t = ValueTracker(0.0)
# --- Helper functions ---
def rolling_center():
ang = t.get_value()
return (R - r) * np.array([np.cos(ang), np.sin(ang), 0.0])
def tracing_point():
ang = t.get_value()
k = (R - r) / r # for R=3.5, r=0.875 => k=3
return np.array([
(R - r) * np.cos(ang) + r * np.cos(k * ang),
(R - r) * np.sin(ang) - r * np.sin(k * ang),
0.0
])
# --- Fixed circle (RED) ---
fixed_circle = Circle(radius=R, color=FIXED_COLOR).move_to(ORIGIN)
# --- Rolling circle (BLUE), updated as it rolls ---
rolling_circle = Circle(radius=r, color=ROLLING_COLOR)
rolling_circle.add_updater(lambda m: m.move_to(rolling_center()))
# No-slip rotation of rolling circle about its own center
# (set absolute angle each frame to avoid drift)
def roll_spin_updater(mob):
ang = t.get_value()
target = -((R - r) / r) * ang
mob.rotate(target - mob.get_angle(), about_point=mob.get_center())
rolling_circle.add_updater(roll_spin_updater)
# --- Radius of rolling circle (BLUE) ---
radius_line = always_redraw(
lambda: Line(
rolling_center(),
tracing_point(),
color=ROLLING_COLOR
)
)
# --- Tracing dot (YELLOW) ---
dot = always_redraw(lambda: Dot(tracing_point(), radius=0.06, color=TRACE_COLOR))
# --- Hypocycloid path (YELLOW) ---
path = TracedPath(dot.get_center, stroke_color=TRACE_COLOR, stroke_width=6)
# --- Animate ---
self.play(Create(fixed_circle), Create(rolling_circle))
self.add(path, radius_line, dot)
# One closed curve for r = R/4 (k = 3) completes at 2π
self.play(t.animate.set_value(TAU), run_time=8, rate_func=linear)
# Keep everything on screen at the end
self.wait(1)Output of Manim: Hypocycloid
If you are looking for the calculations of length of arc and area of a cycloid by integration, you can find it in node 2353.
