Cycloidal Curves: Theory, Parametric Equations, and Animation

Category
Cycloid
Back to top

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_fixed_visible_800px.gif
Cardioid ($r = R$), a special case of the epicycloid.

 

Back to top

Cycloid

Parametric equations:
$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

ss_cycloid_0.png

$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

cycloid_matplotlib.png

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

Parametric equations:
$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

ss_epicycloid_0.png

$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

epicycloid_matplotlib.png

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

Parametric equations of 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

ss_hypocycloid_0.png

$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

hypocycloid_matplotlib.png

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.
 

Back to top