Mathematical Animation Engine
I recently discovered an interesting Python library called Manim. It’s a powerful tool that allows us to create beautiful animations of mathematical concepts and geometric shapes such as polygons, graphs, and functions.
What makes Manim even more exciting is that it can be used directly in Jupyter Notebook. This means I can combine code, text, and animation all in one place — perfect for explaining mathematical ideas in a more dynamic and visual way.
To try it out, I created a simple example using Manim to illustrate the Fourier Transform, one of my favourite topics from university. With just a few lines of code, Manim can generate smooth, professional-looking animations that show how complex waveforms are built from simple sine components.
I’m excited to explore more possibilities with Manim and use it to present other mathematical concepts in a creative and engaging way.
1. Installation
!sudo apt update
!sudo apt install libcairo2-dev \
texlive texlive-latex-extra texlive-fonts-extra \
texlive-latex-recommended texlive-science \
tipa libpango1.0-dev
!pip install manim
!pip install IPython==8.21.0
2. Configuration
import manim as mn
from manim import *
config.media_width = "75%"
config.verbosity = "WARNING"
print(mn.__version__)
3. Fourier Transform Animation
%%manim -qm FourierSineMixed
# Fourier Transform animation: sin(2x) + 2*sin(x + π/4)
from manim import *
import numpy as np
class FourierSineMixed(Scene):
def construct(self):
# -------------------------
# Title
# -------------------------
title = Text("Fourier Transform of sin(2x) + 2·sin(x + π/4)", font_size=32).to_edge(UP)
self.play(Write(title))
self.wait(0.8)
# -------------------------
# Time-domain signal
# -------------------------
t = np.linspace(0, 2 * np.pi, 400)
signal = np.sin(2 * t) + 2 * np.sin(t + np.pi / 4)
# Time-domain axes (shifted left)
time_axes = Axes(
x_range=[0, 2 * np.pi, np.pi / 2],
y_range=[-3.5, 3.5, 1],
x_length=6,
y_length=3.5,
axis_config={"color": BLUE},
tips=False,
).shift(LEFT * 3.3)
time_label = time_axes.get_axis_labels(Text("t"), Text("x(t)"))
time_wave = time_axes.plot(lambda x: np.sin(2 * x) + 2 * np.sin(x + np.pi / 4), color=YELLOW)
# y-axis numerical labels
y_min, y_max = -3.0, 3.0
y_min_label = Text(f{"{y_min:.1f}"}, font_size=22).next_to(time_axes.c2p(0, y_min), LEFT, buff=0.15)
y_max_label = Text(f{"{y_max:.1f}"}, font_size=22).next_to(time_axes.c2p(0, y_max), LEFT, buff=0.15)
self.play(Create(time_axes), Write(time_label))
self.play(Create(time_wave))
self.play(Write(y_min_label), Write(y_max_label))
self.wait(0.8)
# -------------------------
# Fourier Transform computation
# -------------------------
freqs = np.fft.fftfreq(len(t), d=(t[1] - t[0]))
fft_vals = np.fft.fft(signal)
magnitude = np.abs(fft_vals) / len(t)
mask = freqs >= 0
freqs_pos = freqs[mask]
mag_pos = magnitude[mask]
# -------------------------
# Frequency-domain axes (0–0.4)
# -------------------------
freq_axes = Axes(
x_range=[0, 0.4, 0.05],
y_range=[0, max(1.0, np.max(mag_pos) * 1.2), 0.2],
x_length=3.5,
y_length=3.5,
axis_config={"color": GREEN},
tips=False,
).shift(RIGHT * 3.0)
# Axis labels (explicitly positioned)
x_label = Text("f", font_size=24).next_to(freq_axes.x_axis, DOWN, buff=0.15)
y_label = Text("|X(f)|", font_size=24).rotate(PI / 2).next_to(freq_axes.y_axis, LEFT, buff=0.35)
self.play(Create(freq_axes), Write(x_label), Write(y_label))
self.wait(0.6)
# -------------------------
# Bars and labels
# -------------------------
bars = VGroup()
bar_labels = VGroup()
amp_labels = VGroup()
bar_width = 0.35
for f, m in zip(freqs_pos, mag_pos):
if m < 0.005 or f > 0.4:
continue
x = freq_axes.c2p(f, 0)[0]
y_bottom = freq_axes.c2p(0, 0)[1]
y_top = freq_axes.c2p(0, m)[1]
height = y_top - y_bottom
# Bar itself
bar = Rectangle(
width=bar_width,
height=height,
color=ORANGE,
fill_color=ORANGE,
fill_opacity=0.9
).move_to([x, y_bottom + height / 2, 0])
bars.add(bar)
# Frequency label (below bar)
f_label = Text(f"f = {f:.2f}", font_size=20).next_to(bar, DOWN, buff=0.1)
bar_labels.add(f_label)
# Magnitude label (above bar)
m_label = Text(f"{m:.2f}", font_size=20).next_to(bar, UP, buff=0.05)
amp_labels.add(m_label)
# -------------------------
# Transition from time → frequency
# -------------------------
self.play(
Transform(time_wave, bars),
FadeOut(time_axes),
FadeOut(time_label),
FadeOut(y_min_label),
FadeOut(y_max_label),
run_time=3
)
self.wait(0.6)
self.play(Create(bars), Write(bar_labels), Write(amp_labels))
self.wait(1.2)
# -------------------------
# Highlight peaks
# -------------------------
peaks = []
for f, m in zip(freqs_pos, mag_pos):
if 0 < f <= 0.4 and m > 0.05:
peaks.append(freq_axes.get_vertical_line(freq_axes.c2p(f, m), color=RED))
if peaks:
self.play(*[Create(p) for p in peaks])
self.wait(1.5)
# -------------------------
# Cleanup & final text
# -------------------------
self.play(
FadeOut(VGroup(title, bars, freq_axes, x_label, y_label, bar_labels, amp_labels, *peaks, time_wave)),
run_time=1.2
)
end_text = Text(
"Fourier Transform Reveals Frequency Components",
font_size=28
).to_edge(DOWN).shift(DOWN * 0.5)
self.play(Write(end_text))
self.wait(2.5)
Comments