Chapter 2 · Manim
Every example below shows the exact code on the left and the real video it rendered on the right — click any video to play it, click again to replay. Eleven topics from first circle to 3D camera work — about 6 hours total; each topic stands alone, so stop whenever you like.
Guess: in an animation library, what might a 'Scene' be?
construct() — Topic 1.How do you think you'd make two animations happen at once?
play() call. Sequential = separate calls — Topic 2.What could make a label FOLLOW a moving dot?
1Scenes & Mobjects — the two words that explain everything
A Scene is your canvas + timeline. A Mobject ("mathematical object") is anything drawable: circles, text, formulas, graphs. You subclass Scene, override construct(), and inside it you create mobjects and play() animations on them. That's the whole framework.
from manim import *
class FirstCircle(Scene):
def construct(self):
circle = Circle(radius=1.5, color=BLUE)
self.play(Create(circle))
self.wait(0.5)▶ click the video to play — click again to replay
Render it: manim render -ql -p first.py FirstCircle
class SquareAndLabel(Scene):
def construct(self):
square = Square(side_length=2, color=GREEN)
label = Text("A square", font_size=36).next_to(square, DOWN)
self.play(Create(square))
self.play(Write(label))
self.wait(0.5)▶ click the video to play — click again to replay
Each self.play(...) is one beat of the timeline — they run in order.
class ShapeFamily(Scene):
def construct(self):
shapes = VGroup(
Circle(color=BLUE), Square(color=GREEN),
Triangle(color=YELLOW), Star(color=RED),
).arrange(RIGHT, buff=0.8).scale(0.7)
self.play(LaggedStart(*[Create(s) for s in shapes],
lag_ratio=0.3))▶ click the video to play — click again to replay
arrange lays members out; LaggedStart staggers their animations.
class MorphingShapes(Scene):
def construct(self):
shape = Circle(radius=1.5, color=BLUE)
self.play(Create(shape))
for target in [Square(side_length=2.5, color=GREEN),
Triangle(color=YELLOW).scale(1.5),
RegularPolygon(6, color=PURPLE).scale(1.5)]:
self.play(Transform(shape, target))▶ click the video to play — click again to replay
Transform(a, b) smoothly morphs a into b's shape.
MyFirst that creates a blue Circle and plays Create on it.2The animation vocabulary
Manim ships dozens of animation classes, but you'll use about eight constantly: Create, Write, FadeIn/FadeOut, Transform, GrowFromCenter, LaggedStart, and the special .animate syntax. Multiple animations passed to one play() run simultaneously.
class HelloWrite(Scene):
def construct(self):
text = Text("Hello, Manim!", font_size=60,
gradient=(BLUE, TEAL))
self.play(Write(text))▶ click the video to play — click again to replay
class FadeAndGrow(Scene):
def construct(self):
circle = Circle(color=BLUE, fill_opacity=0.5).shift(LEFT * 2.5)
square = Square(color=GREEN, fill_opacity=0.5).shift(RIGHT * 2.5)
self.play(FadeIn(circle), GrowFromCenter(square)) # together!
self.wait(0.3)
self.play(FadeOut(circle, shift=UP),
FadeOut(square, shift=DOWN))▶ click the video to play — click again to replay
Two animations in one play() = simultaneous.
class WordMorph(Scene):
def construct(self):
a = Text("mathematics", font_size=56)
b = Text("animations", font_size=56, color=TEAL)
self.play(Write(a))
self.play(TransformMatchingShapes(a, b))▶ click the video to play — click again to replay
Letters that exist in both words fly to their new positions.
class RainDots(Scene):
def construct(self):
dots = VGroup(*[Dot(color=random_bright_color())
.move_to([x, 3.5, 0])
for x in np.linspace(-6, 6, 25)])
self.play(LaggedStart(
*[d.animate.shift(DOWN * 7) for d in dots],
lag_ratio=0.05, run_time=2.5,
rate_func=rate_functions.ease_in_quad))▶ click the video to play — click again to replay
rate_func shapes the speed curve — ease_in_quad = accelerating, like gravity.
Create, transforms into a Circle, then fades out. Three plays.3Positioning: shift, next_to, arrange, paths
The screen is a coordinate grid: origin at center, ~7 units left-right, ~4 up-down. Constants UP, DOWN, LEFT, RIGHT are unit vectors you can scale and add: UP * 2 + RIGHT * 3 is just a point.
class ShiftAround(Scene):
def construct(self):
dot = Dot(color=YELLOW).scale(2)
self.play(FadeIn(dot))
for direction in [UP * 2, RIGHT * 3, DOWN * 4,
LEFT * 6, UP * 2 + RIGHT * 3]:
self.play(dot.animate.shift(direction), run_time=0.5)▶ click the video to play — click again to replay
.animate.shift() animates the move; plain .shift() teleports before rendering.
class NeighborLayout(Scene):
def construct(self):
center = Square(color=BLUE)
up = Text("above", font_size=30).next_to(center, UP)
down = Text("below", font_size=30).next_to(center, DOWN)
left = Text("left", font_size=30).next_to(center, LEFT)
right = Text("right", font_size=30).next_to(center, RIGHT)
self.play(Create(center))
self.play(FadeIn(up), FadeIn(down),
FadeIn(left), FadeIn(right))▶ click the video to play — click again to replay
Relative positioning survives refactors — move the square, labels follow (at creation time).
class GridOfShapes(Scene):
def construct(self):
grid = VGroup(*[
Circle(radius=0.3, color=c, fill_opacity=0.8)
for c in [RED, ORANGE, YELLOW, GREEN, TEAL,
BLUE, PURPLE, PINK, WHITE]
]).arrange_in_grid(rows=3, cols=3, buff=0.6)
self.play(LaggedStart(*[GrowFromCenter(s) for s in grid],
lag_ratio=0.1))
self.play(grid.animate.arrange(RIGHT, buff=0.25).scale(0.8))▶ click the video to play — click again to replay
A VGroup can be re-arranged as an animation — the layout itself animates.
class OrbitingMoon(Scene):
def construct(self):
planet = Circle(radius=0.6, color=BLUE, fill_opacity=1)
orbit = Circle(radius=2.2, color=GREY).set_stroke(width=2)
moon = Dot(color=WHITE).scale(1.5)
moon.move_to(orbit.point_from_proportion(0))
self.play(FadeIn(planet), Create(orbit), FadeIn(moon))
self.play(MoveAlongPath(moon, orbit),
run_time=3, rate_func=linear)▶ click the video to play — click again to replay
Any VMobject can be a path — circles, arcs, even hand-drawn Bezier curves.
Text label below it with next_to, then group both in a VGroup and shift the group 2 units LEFT.4Color & styling
Every mobject has a stroke (outline) and a fill. color= sets both; fill_opacity= reveals the fill (default 0!). Gradients work on both text and shapes.
class FillAndStroke(Scene):
def construct(self):
s1 = Square(color=BLUE).shift(LEFT * 3) # stroke only
s2 = Square(color=BLUE, fill_opacity=1) # filled
s3 = Square(fill_color=YELLOW, fill_opacity=1,
stroke_color=RED, stroke_width=8).shift(RIGHT * 3)
self.play(Create(s1), Create(s2), Create(s3))▶ click the video to play — click again to replay
The #1 beginner surprise: shapes are hollow until you set fill_opacity.
class GradientTitle(Scene):
def construct(self):
title = Text("Gradients!", font_size=72,
gradient=(RED, YELLOW, GREEN))
underline = Line(LEFT * 3, RIGHT * 3).next_to(title, DOWN)
underline.set_color_by_gradient(RED, YELLOW, GREEN)
self.play(Write(title), Create(underline))▶ click the video to play — click again to replay
class DashAndOpacity(Scene):
def construct(self):
solid = Circle(radius=1.2, color=TEAL).shift(LEFT * 3)
dashed = DashedVMobject(Circle(radius=1.2, color=TEAL))
ghost = Circle(radius=1.2, color=TEAL, fill_opacity=0.25,
stroke_opacity=0.4).shift(RIGHT * 3)
self.play(Create(solid), Create(dashed), FadeIn(ghost))▶ click the video to play — click again to replay
Low opacity = 'this is context, not the point' — a key visual-communication trick.
class StyleWave(Scene):
def construct(self):
squares = VGroup(*[Square(side_length=0.7, fill_opacity=0.9)
for _ in range(10)]).arrange(RIGHT, buff=0.15)
squares.set_color_by_gradient(PURPLE, TEAL, YELLOW)
self.play(LaggedStart(*[GrowFromCenter(s) for s in squares],
lag_ratio=0.08))
self.play(LaggedStart(
*[s.animate.shift(UP * 0.8).set_fill(WHITE)
for s in squares],
lag_ratio=0.1, rate_func=there_and_back, run_time=2))▶ click the video to play — click again to replay
there_and_back plays the animation forward then in reverse — great for waves and pulses.
.animate syntax.5Updaters & ValueTracker — animations that react
So far every animation was pre-scripted. Updaters are little functions that run every frame: "keep this line attached to that dot", "keep this number equal to that value". ValueTracker holds a number you can animate; always_redraw rebuilds a mobject each frame. This combination is Manim's superpower.
class LiveCounter(Scene):
def construct(self):
value = ValueTracker(0)
number = DecimalNumber(0, num_decimal_places=1,
font_size=96)
number.add_updater(
lambda m: m.set_value(value.get_value()))
self.add(number)
self.play(value.animate.set_value(100),
run_time=3, rate_func=linear)▶ click the video to play — click again to replay
You animate the tracker; the updater drags the number along every frame.
class DotChaser(Scene):
def construct(self):
anchor = Dot(LEFT * 4, color=BLUE).scale(1.5)
runner = Dot(RIGHT * 4 + UP * 2, color=YELLOW).scale(1.5)
rope = always_redraw(lambda: Line(
anchor.get_center(), runner.get_center(), color=GREY))
self.add(anchor, runner, rope)
self.play(runner.animate.move_to(RIGHT * 4 + DOWN * 2))
self.play(runner.animate.move_to(UP * 2.5))▶ click the video to play — click again to replay
always_redraw rebuilds the line every frame from live positions.
class TickingClock(Scene):
def construct(self):
face = Circle(radius=2, color=WHITE)
hand = Line(ORIGIN, UP * 1.6, color=YELLOW,
stroke_width=6)
hand.add_updater(lambda m, dt:
m.rotate(-dt * PI / 2, about_point=ORIGIN))
self.play(Create(face))
self.add(hand)
self.wait(4) # the hand keeps turning by itself!▶ click the video to play — click again to replay
An updater taking (mobject, dt) runs on wall-clock time — even during wait().
class GrowingBar(Scene):
def construct(self):
progress = ValueTracker(0)
track = Rectangle(width=8, height=0.6, color=GREY)
bar = always_redraw(lambda: Rectangle(
width=max(progress.get_value() * 8, 0.001), height=0.6,
fill_color=TEAL, fill_opacity=1, stroke_width=0,
).align_to(track, LEFT))
pct = always_redraw(lambda: Integer(
int(progress.get_value() * 100), unit=r"\%",
font_size=40).next_to(track, UP))
self.add(track, bar, pct)
self.play(progress.animate.set_value(1), run_time=3,
rate_func=rate_functions.ease_in_out_sine)▶ click the video to play — click again to replay
One tracker drives both the bar's width and the percentage — single source of truth.
ValueTracker starting at 0, a DecimalNumber that follows it with an updater, and animate the tracker to 100.6Graphs & MathTex — where LaTeX pays off
Axes gives you a coordinate system; .plot() draws functions on it; MathTex renders any LaTeX from Chapter 1 as an animatable mobject. This is the toolkit of every math explainer video you've ever watched.
class SinePlot(Scene):
def construct(self):
axes = Axes(x_range=[-4, 4], y_range=[-2, 2],
x_length=10, y_length=5)
curve = axes.plot(lambda x: np.sin(x), color=YELLOW)
self.play(Create(axes))
self.play(Create(curve), run_time=2)▶ click the video to play — click again to replay
plot() takes any Python function of x.
class EulerFormula(Scene):
def construct(self):
formula = MathTex(r"e^{i\pi} + 1 = 0", font_size=96)
name = Text("Euler's identity", font_size=32,
color=GREY).next_to(formula, DOWN)
self.play(Write(formula))
self.play(FadeIn(name))▶ click the video to play — click again to replay
Everything from Chapter 1 works inside MathTex — always with the r prefix.
class RiemannIntro(Scene):
def construct(self):
axes = Axes(x_range=[0, 4], y_range=[0, 9],
x_length=9, y_length=5)
curve = axes.plot(lambda x: x**2 * 0.55 + 0.5, color=TEAL)
rects = axes.get_riemann_rectangles(
curve, x_range=[0, 4], dx=0.5, fill_opacity=0.7)
fine = axes.get_riemann_rectangles(
curve, x_range=[0, 4], dx=0.125, fill_opacity=0.7)
self.play(Create(axes), Create(curve))
self.play(FadeIn(rects))
self.play(Transform(rects, fine))▶ click the video to play — click again to replay
Transforming coarse rectangles into fine ones IS the idea of integration, visually.
class TangentSlide(Scene):
def construct(self):
axes = Axes(x_range=[-3, 3], y_range=[-1, 8],
x_length=10, y_length=5.5)
curve = axes.plot(lambda x: 0.6 * x**2 + 0.4, color=YELLOW)
x = ValueTracker(-2.2)
tangent = always_redraw(lambda: TangentLine(
curve, alpha=(x.get_value() + 3) / 6,
length=4, color=RED))
dot = always_redraw(lambda: Dot(color=RED).move_to(
axes.c2p(x.get_value(),
0.6 * x.get_value()**2 + 0.4)))
self.play(Create(axes), Create(curve))
self.add(tangent, dot)
self.play(x.animate.set_value(2.2), run_time=3,
rate_func=linear)▶ click the video to play — click again to replay
Updaters (topic 5) + graphs (topic 6) = the derivative, animated. This exact scene becomes a slide deck in Chapter 3.
Axes, plot sin(x) on them with a lambda, and play Create on both.7MathTex mastery: transforms, braces & spotlights
The killer feature for math talks: split a formula into parts, then morph one equation into another while matching terms fly to their new places. This is why you learned LaTeX first.
class TexTransform(Scene):
def construct(self):
eq1 = MathTex("a^2", "+", "b^2", "=", "c^2",
font_size=72)
eq2 = MathTex("c^2", "=", "a^2", "+", "b^2",
font_size=72)
self.play(Write(eq1))
self.wait(0.6)
self.play(TransformMatchingTex(eq1, eq2),
run_time=1.5)
self.wait(0.6)▶ click the video to play — click again to replay
Each string argument becomes a separately-animatable part. TransformMatchingTex moves identical parts to their new positions.
class BraceAnnotate(Scene):
def construct(self):
eq = MathTex("(", "x+1", ")", "^2", "=",
"x^2+2x+1", font_size=60)
self.play(Write(eq))
brace = Brace(eq[1], DOWN, color=YELLOW)
note = brace.get_text("this part gets squared")
note.set_color(YELLOW)
box = SurroundingRectangle(eq[5], color=TEAL,
buff=0.15)
self.play(GrowFromCenter(brace), FadeIn(note))
self.play(Create(box), Indicate(eq[5]))▶ click the video to play — click again to replay
eq[1] indexes the parts you split. Brace points at anything; SurroundingRectangle + Indicate = instant spotlight.
MathTex for a²+b²=c² split into separate substrings (so terms can move), then morph it into another MathTex with TransformMatchingTex.8Timing & choreography: LaggedStart, rate functions
Amateur animations play everything at once at constant speed. Professional ones stagger entrances and ease movements. Two tools give you 90% of that polish.
class LaggedShapes(Scene):
def construct(self):
dots = VGroup(*[Dot(radius=0.14, color=TEAL)
for _ in range(12)])
dots.arrange(RIGHT, buff=0.35)
self.play(LaggedStart(
*[GrowFromCenter(d) for d in dots],
lag_ratio=0.15))
self.play(dots.animate.set_color(YELLOW),
run_time=1.5)▶ click the video to play — click again to replay
lag_ratio=0.15: each dot starts when the previous one is 15% done — a wave instead of a blob.
class RateFuncs(Scene):
def construct(self):
labels = ["linear", "smooth",
"there_and_back", "rush_into"]
funcs = [linear, smooth,
there_and_back, rush_into]
rows = VGroup(*[
VGroup(Text(n, font_size=24), Dot(color=ORANGE))
.arrange(RIGHT, buff=0.5)
for n in labels])
rows.arrange(DOWN, aligned_edge=LEFT,
buff=0.5).to_edge(LEFT)
self.add(rows)
self.play(*[row[1].animate(rate_func=fn,
run_time=2.5)
.shift(RIGHT * 8)
for row, fn in zip(rows, funcs)])▶ click the video to play — click again to replay
Same shift, four personalities. smooth is the default; there_and_back returns home — great for 'pulse' effects.
LaggedStart, a lag_ratio of 0.3, a total run_time of 2, and a rate_func of your choice.9Camera work: zoom, pan, follow
Switch Scene → MovingCameraScene and the viewport itself becomes an animatable object. Zooming into detail and following motion are the two moves you'll actually use.
class CameraZoom(MovingCameraScene):
def construct(self):
dots = VGroup(*[Dot(color=BLUE)
for _ in range(9)])
dots.arrange_in_grid(3, 3, buff=1.2)
target = dots[4].set_color(YELLOW)
self.play(Create(dots))
self.camera.frame.save_state()
self.play(self.camera.frame.animate
.scale(0.35).move_to(target))
self.wait(0.4)
self.play(Restore(self.camera.frame))▶ click the video to play — click again to replay
self.camera.frame is a rectangle mobject: scale it (zoom), move_to it (pan), Restore brings it back.
class CameraFollow(MovingCameraScene):
def construct(self):
path = Line(LEFT * 5, RIGHT * 5).shift(DOWN)
car = Triangle(color=RED, fill_opacity=1)
car.scale(0.3).rotate(-PI / 2)
car.move_to(path.get_start())
self.add(path, car)
self.camera.frame.scale(0.6).move_to(car)
self.camera.frame.add_updater(
lambda f: f.move_to(car.get_center()))
self.play(car.animate.move_to(path.get_end()),
run_time=3, rate_func=linear)▶ click the video to play — click again to replay
An updater on the camera frame = a follow-cam. Same updater idea from Topic 5, applied to the camera.
MovingCameraScene, animate self.camera.frame to scale to half size and move to a dot.103D scenes: axes, spheres, surfaces
ThreeDScene unlocks the third axis. You position the camera with two angles — phi (tilt down from vertical) and theta (spin around) — and can set it slowly orbiting while you present.
class First3D(ThreeDScene):
def construct(self):
axes = ThreeDAxes(x_range=[-4, 4],
y_range=[-4, 4],
z_range=[-3, 3])
sphere = Sphere(radius=1,
resolution=(18, 18))
sphere.set_color(BLUE)
self.set_camera_orientation(
phi=70 * DEGREES, theta=-45 * DEGREES)
self.play(Create(axes))
self.play(Create(sphere))
self.begin_ambient_camera_rotation(rate=0.4)
self.wait(2.5)▶ click the video to play — click again to replay
phi=70° tilts you above the plane; ambient rotation keeps the scene alive while you talk over it.
class Surface3D(ThreeDScene):
def construct(self):
axes = ThreeDAxes(x_range=[-3, 3],
y_range=[-3, 3],
z_range=[-2, 2])
surface = Surface(
lambda u, v: axes.c2p(
u, v, np.sin(u) * np.cos(v)),
u_range=[-3, 3], v_range=[-3, 3],
resolution=(24, 24), fill_opacity=0.8)
surface.set_fill_by_value(axes=axes,
colorscale=[(BLUE, -1), (GREEN, 0),
(YELLOW, 1)])
self.set_camera_orientation(
phi=65 * DEGREES, theta=-50 * DEGREES)
self.play(Create(axes), Create(surface),
run_time=2)
self.begin_ambient_camera_rotation(rate=0.3)
self.wait(2)▶ click the video to play — click again to replay
Surface takes a function (u,v) → 3D point; set_fill_by_value colors by height like a heat map.
ThreeDScene that sets a camera orientation (phi and theta), creates ThreeDAxes and a Sphere.11Production settings: quality, format, partial renders
The last mile: rendering the same scene for a quick check, a slide deck, or a final video are just different flags. These are the ones that matter in real work.
# fast draft while iterating (480p @ 15fps)
manim render -ql --fps 15 scene.py MyScene
# full quality for the final export (1080p60)
manim render -qh scene.py MyScene
# render a transparent-background overlay (for OBS / video editors)
manim render -qh -t --format=mov scene.py MyScene
# just the LAST play() call — lifesaver when polishing an ending
manim render -ql -n -1 scene.py MyScene
# save the final frame as PNG (thumbnails!)
manim render -qh -s scene.py MyScene# per-project defaults: put a manim.cfg next to your scene file
[CLI]
quality = medium_quality
preview = True
background_color = #101418MyScene from talk.py at high quality and opens the result when done.Self-examination
show solution
class Ex1(Scene):
def construct(self):
sq = Square(color=RED, fill_opacity=0.8)
self.play(GrowFromCenter(sq))
self.play(sq.animate.shift(RIGHT * 3))
self.play(FadeOut(sq, shift=UP))show solution
class Countdown(Scene):
def construct(self):
t = ValueTracker(10)
num = DecimalNumber(10, num_decimal_places=1, font_size=96)
num.add_updater(lambda m: m.set_value(t.get_value()))
self.add(num)
self.play(t.animate.set_value(0),
num.animate.set_color(RED),
run_time=5, rate_func=linear)show solution
class CubicDot(Scene):
def construct(self):
axes = Axes(x_range=[-3, 3], y_range=[-4, 4],
x_length=10, y_length=5.5)
f = lambda x: x**3 - 3 * x
curve = axes.plot(f, color=TEAL)
x = ValueTracker(-2.2)
dot = always_redraw(lambda: Dot(color=YELLOW).move_to(
axes.c2p(x.get_value(), f(x.get_value()))))
self.play(Create(axes), Create(curve))
self.add(dot)
self.play(x.animate.set_value(2.2), run_time=4)