
Motion Graphics
- 56 installs
- 292 repo stars
- Updated January 29, 2026
- rohitg00/manim-video-generator
Helps with ai & agent building tasks during AI-assisted development.
About
motion-graphics is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- motion-graphics
- AI & Agent Building
- AI-coding skill
Motion Graphics by the numbers
- 56 all-time installs (skills.sh)
- +2 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #6,750 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/rohitg00/manim-video-generator --skill motion-graphicsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 56 |
|---|---|
| repo stars | ★ 292 |
| Last updated | January 29, 2026 |
| Repository | rohitg00/manim-video-generator ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Motion Graphics Skill
The Motion Graphics skill creates visually striking animations focused on aesthetic impact, perfect for titles, intros, and attention-grabbing content.
Design Principles
The 4 S's of Motion Graphics
- Style: Consistent visual language throughout
- Surprise: Unexpected movements that delight
- Smoothness: Fluid transitions and easing
- Simplicity: Clean design, no visual clutter
Motion Design Fundamentals
- Anticipation: Small movement before main action
- Follow-through: Elements continue after stopping
- Overshoot: Go past, then settle into place
- Squash & Stretch: Emphasize weight and impact
Rules
rules/kinetic-typography.md
Creating impactful text animations that convey emotion.
rules/timing-and-easing.md
Professional easing curves and timing patterns.
rules/visual-hierarchy.md
Directing attention through size, color, and motion.
rules/brand-consistency.md
Maintaining consistent style across animations.
Templates
Kinetic Typography - Word by Word
from manim import *
class KineticTypography(Scene):
def construct(self):
self.camera.background_color = "#1a1a2e"
words = ["The", "Future", "Is", "NOW"]
colors = [WHITE, BLUE, WHITE, YELLOW]
for word, color in zip(words, colors):
text = Text(word, font_size=96, color=color, weight=BOLD)
# Dramatic entrance
text.scale(0)
self.add(text)
self.play(
text.animate.scale(1),
rate_func=rate_functions.ease_out_back,
run_time=0.4
)
self.wait(0.3)
# Exit with style
self.play(
text.animate.shift(UP * 2).set_opacity(0),
rate_func=rate_functions.ease_in_cubic,
run_time=0.3
)
self.remove(text)
# Final combination
final = Text("THE FUTURE IS NOW", font_size=48, color=GOLD)
self.play(Write(final), run_time=1)
self.play(Indicate(final, scale_factor=1.1, color=WHITE))Logo Reveal - Particle Assemble
from manim import *
import random
class LogoReveal(Scene):
def construct(self):
self.camera.background_color = "#0a0a0a"
# Create logo text
logo = Text("BRAND", font_size=120, color=WHITE, weight=BOLD)
# Create particles that will form the logo
particles = VGroup()
for _ in range(100):
dot = Dot(
point=[random.uniform(-7, 7), random.uniform(-4, 4), 0],
radius=0.05,
color=random_color()
)
particles.add(dot)
self.add(particles)
# Swirl particles
self.play(
*[Rotating(p, radians=PI * 2, about_point=ORIGIN)
for p in particles],
run_time=1.5,
rate_func=rate_functions.ease_in_out_sine
)
# Converge to logo position
self.play(
FadeOut(particles),
FadeIn(logo, scale=0.5),
run_time=0.8
)
# Glow effect
glow = logo.copy().set_color(BLUE).set_opacity(0.5)
self.play(
glow.animate.scale(1.2).set_opacity(0),
run_time=0.5
)
self.remove(glow)
self.wait()Title Sequence - Slide & Stack
from manim import *
class TitleSequence(Scene):
def construct(self):
self.camera.background_color = "#16213e"
# Main title
title = Text("EPISODE ONE", font_size=72, color=WHITE)
subtitle = Text("The Beginning", font_size=36, color=BLUE_C)
# Slide in from sides
title.shift(LEFT * 10)
subtitle.shift(RIGHT * 10)
self.add(title, subtitle)
self.play(
title.animate.move_to(UP * 0.5),
subtitle.animate.move_to(DOWN * 0.5),
run_time=0.8,
rate_func=rate_functions.ease_out_cubic
)
# Add decorative lines
line_left = Line(LEFT * 3, LEFT * 0.5, color=GOLD, stroke_width=2)
line_right = Line(RIGHT * 0.5, RIGHT * 3, color=GOLD, stroke_width=2)
lines = VGroup(line_left, line_right).next_to(title, UP, buff=0.3)
self.play(
Create(line_left),
Create(line_right),
run_time=0.5
)
# Fade all together
self.wait(2)
self.play(
FadeOut(VGroup(title, subtitle, lines), shift=UP),
run_time=0.6
)Text Glitch Effect
from manim import *
class GlitchText(Scene):
def construct(self):
self.camera.background_color = "#000000"
text = Text("GLITCH", font_size=96, color=WHITE, weight=BOLD)
# Create glitch copies
red_copy = text.copy().set_color(RED).shift(LEFT * 0.05 + UP * 0.02)
blue_copy = text.copy().set_color(BLUE).shift(RIGHT * 0.05 + DOWN * 0.02)
self.add(red_copy, blue_copy, text)
# Glitch animation
for _ in range(5):
# Random offset
self.play(
red_copy.animate.shift(RIGHT * 0.1),
blue_copy.animate.shift(LEFT * 0.1),
run_time=0.05
)
self.play(
red_copy.animate.shift(LEFT * 0.1),
blue_copy.animate.shift(RIGHT * 0.1),
run_time=0.05
)
# Settle
self.play(
red_copy.animate.move_to(text.get_center()),
blue_copy.animate.move_to(text.get_center()),
run_time=0.2
)
self.remove(red_copy, blue_copy)
self.wait()Counter Animation
from manim import *
class CounterAnimation(Scene):
def construct(self):
self.camera.background_color = "#1a1a2e"
# Value tracker
counter = ValueTracker(0)
# Dynamic text
number = always_redraw(lambda: Text(
f"{int(counter.get_value()):,}",
font_size=120,
color=WHITE,
weight=BOLD
))
label = Text("SUBSCRIBERS", font_size=32, color=BLUE_C)
label.next_to(number, DOWN, buff=0.5)
self.add(number, label)
# Animate count
self.play(
counter.animate.set_value(1000000),
run_time=3,
rate_func=rate_functions.ease_out_cubic
)
# Celebration effect
self.play(
Indicate(number, scale_factor=1.2, color=GOLD),
run_time=0.5
)Easing Functions Reference
| Effect | Rate Function | Use Case |
|---|---|---|
| Smooth | smooth | General purpose |
| Dramatic entrance | ease_out_back | Pop-in effects |
| Gentle exit | ease_in_cubic | Fade outs |
| Bouncy | ease_out_bounce | Playful motion |
| Snappy | ease_out_expo | Quick, impactful |
| Natural | ease_in_out_sine | Organic movement |
| Linear | linear | Constant speed |
Color Palettes
Neon Cyberpunk
NEON_PINK = "#ff00ff"
NEON_BLUE = "#00ffff"
NEON_GREEN = "#00ff00"
DARK_BG = "#0a0a0a"Corporate Professional
CORP_BLUE = "#0066cc"
CORP_GRAY = "#333333"
CORP_WHITE = "#ffffff"
CORP_ACCENT = "#ff6600"Minimal Modern
MIN_BLACK = "#1a1a1a"
MIN_WHITE = "#f5f5f5"
MIN_GRAY = "#888888"
MIN_ACCENT = "#3366ff"Best Practices
1. Less is more - Restraint makes key moments impactful 2. Timing is everything - Wrong timing kills good animation 3. Consistency builds trust - Same style throughout 4. Sound design matters - Design with audio in mind 5. Test on target platform - Different devices, different experience
"""
Motion Graphics Animation Presets
Ready-to-use animation functions for common effects
"""
from manim import *
import numpy as np
# =============================================================================
# ENTRANCE ANIMATIONS
# =============================================================================
def pop_in(scene, mobject, scale_factor=0.5, duration=0.4):
"""Element pops in with overshoot"""
mobject.scale(0)
scene.play(
mobject.animate.scale(1/scale_factor if scale_factor != 0 else 1),
rate_func=rate_functions.ease_out_back,
run_time=duration
)
def slide_in_left(scene, mobject, duration=0.5):
"""Slide in from left side"""
original_pos = mobject.get_center()
mobject.shift(LEFT * 15)
scene.play(
mobject.animate.move_to(original_pos),
rate_func=rate_functions.ease_out_cubic,
run_time=duration
)
def slide_in_right(scene, mobject, duration=0.5):
"""Slide in from right side"""
original_pos = mobject.get_center()
mobject.shift(RIGHT * 15)
scene.play(
mobject.animate.move_to(original_pos),
rate_func=rate_functions.ease_out_cubic,
run_time=duration
)
def fade_in_up(scene, mobject, distance=0.5, duration=0.6):
"""Fade in while moving up"""
mobject.shift(DOWN * distance).set_opacity(0)
scene.play(
mobject.animate.shift(UP * distance).set_opacity(1),
rate_func=rate_functions.ease_out_cubic,
run_time=duration
)
def zoom_in(scene, mobject, duration=0.5):
"""Zoom in from small to normal"""
mobject.scale(0.1).set_opacity(0)
scene.play(
mobject.animate.scale(10).set_opacity(1),
rate_func=rate_functions.ease_out_expo,
run_time=duration
)
# =============================================================================
# EXIT ANIMATIONS
# =============================================================================
def pop_out(scene, mobject, duration=0.3):
"""Element shrinks and disappears"""
scene.play(
mobject.animate.scale(0),
rate_func=rate_functions.ease_in_back,
run_time=duration
)
scene.remove(mobject)
def fade_out_up(scene, mobject, distance=0.5, duration=0.4):
"""Fade out while moving up"""
scene.play(
mobject.animate.shift(UP * distance).set_opacity(0),
rate_func=rate_functions.ease_in_cubic,
run_time=duration
)
scene.remove(mobject)
def slide_out_left(scene, mobject, duration=0.4):
"""Slide out to left"""
scene.play(
mobject.animate.shift(LEFT * 15),
rate_func=rate_functions.ease_in_cubic,
run_time=duration
)
scene.remove(mobject)
def dissolve(scene, mobject, duration=0.5):
"""Gentle dissolve/fade"""
scene.play(
mobject.animate.set_opacity(0),
rate_func=rate_functions.smooth,
run_time=duration
)
scene.remove(mobject)
# =============================================================================
# EMPHASIS ANIMATIONS
# =============================================================================
def pulse(scene, mobject, scale=1.2, duration=0.4):
"""Scale up then back down"""
scene.play(
mobject.animate.scale(scale),
rate_func=rate_functions.there_and_back,
run_time=duration
)
def shake(scene, mobject, amplitude=0.1, duration=0.4):
"""Quick shake effect"""
original_pos = mobject.get_center()
for _ in range(3):
scene.play(
mobject.animate.shift(LEFT * amplitude),
run_time=duration/6
)
scene.play(
mobject.animate.shift(RIGHT * amplitude * 2),
run_time=duration/6
)
scene.play(
mobject.animate.move_to(original_pos),
run_time=duration/6
)
def glow(scene, mobject, color=YELLOW, duration=0.5):
"""Add glow effect"""
glow_copy = mobject.copy()
glow_copy.set_color(color).set_opacity(0.5)
scene.play(
glow_copy.animate.scale(1.3).set_opacity(0),
run_time=duration
)
scene.remove(glow_copy)
def color_flash(scene, mobject, color=YELLOW, duration=0.3):
"""Quick color change and back"""
original_color = mobject.get_color()
scene.play(
mobject.animate.set_color(color),
run_time=duration/2
)
scene.play(
mobject.animate.set_color(original_color),
run_time=duration/2
)
# =============================================================================
# TEXT ANIMATIONS
# =============================================================================
def typewriter(scene, text_mobject, duration=None):
"""Typewriter text effect"""
if duration is None:
duration = len(text_mobject.text) * 0.05
scene.play(AddTextLetterByLetter(text_mobject), run_time=duration)
def word_by_word(scene, text, font_size=48, color=WHITE, wait=0.2):
"""Display text word by word"""
words = text.split()
mobjects = []
for word in words:
t = Text(word, font_size=font_size, color=color)
mobjects.append(t)
group = VGroup(*mobjects).arrange(RIGHT, buff=0.3)
for m in mobjects:
m.set_opacity(0)
scene.add(group)
for m in mobjects:
scene.play(m.animate.set_opacity(1), run_time=0.2)
scene.wait(wait)
return group
def text_replace(scene, old_text, new_text, duration=0.5):
"""Replace one text with another"""
new_text.move_to(old_text)
scene.play(
FadeOut(old_text, shift=UP * 0.3),
FadeIn(new_text, shift=UP * 0.3),
run_time=duration
)
return new_text
# =============================================================================
# TRANSITION ANIMATIONS
# =============================================================================
def wipe_left(scene, duration=0.8):
"""Wipe transition to the left"""
wipe = Rectangle(
width=16, height=10,
fill_opacity=1,
fill_color=BLACK,
stroke_width=0
)
wipe.to_edge(RIGHT, buff=0).shift(RIGHT * 16)
scene.play(
wipe.animate.shift(LEFT * 32),
run_time=duration
)
scene.clear()
scene.remove(wipe)
def circle_wipe(scene, duration=1.0):
"""Circle expanding wipe"""
circle = Circle(radius=0.1, fill_opacity=1, fill_color=BLACK, stroke_width=0)
scene.play(
circle.animate.scale(100),
rate_func=rate_functions.ease_in_cubic,
run_time=duration
)
scene.clear()
def zoom_transition(scene, duration=0.5):
"""Zoom out transition"""
all_objects = VGroup(*scene.mobjects)
scene.play(
all_objects.animate.scale(0.01).set_opacity(0),
run_time=duration
)
scene.clear()
# =============================================================================
# SPECIAL EFFECTS
# =============================================================================
def confetti(scene, num_pieces=30, duration=2):
"""Confetti celebration effect"""
colors = [RED, BLUE, GREEN, YELLOW, PURPLE, ORANGE]
pieces = VGroup()
for _ in range(num_pieces):
piece = Square(side_length=0.1, fill_opacity=1, stroke_width=0)
piece.set_fill(np.random.choice(colors))
piece.move_to(UP * 4 + np.random.uniform(-6, 6) * RIGHT)
pieces.add(piece)
scene.add(pieces)
animations = []
for piece in pieces:
end_pos = DOWN * 5 + np.random.uniform(-7, 7) * RIGHT
animations.append(
piece.animate.move_to(end_pos).rotate(np.random.uniform(0, 4*PI))
)
scene.play(*animations, run_time=duration, rate_func=rate_functions.linear)
scene.remove(pieces)
def spotlight(scene, mobject, others, duration=0.5):
"""Spotlight on one element, dim others"""
scene.play(
mobject.animate.set_opacity(1),
*[o.animate.set_opacity(0.2) for o in others],
run_time=duration
)
def restore_from_spotlight(scene, mobject, others, duration=0.5):
"""Restore from spotlight effect"""
scene.play(
*[o.animate.set_opacity(1) for o in [mobject] + list(others)],
run_time=duration
)
# =============================================================================
# COUNTER ANIMATIONS
# =============================================================================
def count_up(scene, start, end, duration=2, font_size=72, color=WHITE):
"""Animated counting number"""
counter = ValueTracker(start)
number = always_redraw(lambda: Text(
f"{int(counter.get_value()):,}",
font_size=font_size,
color=color
))
scene.add(number)
scene.play(
counter.animate.set_value(end),
run_time=duration,
rate_func=rate_functions.ease_out_cubic
)
return number
"""
Motion Graphics Color Palettes
Pre-defined color schemes for different visual styles
"""
# =============================================================================
# NEON CYBERPUNK
# Dark backgrounds with vibrant neon accents
# =============================================================================
class NeonCyberpunk:
BACKGROUND = "#0a0a0a"
PRIMARY = "#ff00ff" # Magenta
SECONDARY = "#00ffff" # Cyan
ACCENT = "#00ff00" # Neon green
TEXT = "#ffffff"
TEXT_DIM = "#888888"
# Gradient pairs
GRADIENT_1 = ("#ff00ff", "#00ffff") # Magenta to Cyan
GRADIENT_2 = ("#ff0066", "#6600ff") # Pink to Purple
# =============================================================================
# CORPORATE PROFESSIONAL
# Clean, trustworthy business aesthetic
# =============================================================================
class CorporatePro:
BACKGROUND = "#ffffff"
PRIMARY = "#0066cc" # Corporate blue
SECONDARY = "#333333" # Dark gray
ACCENT = "#ff6600" # Orange highlight
TEXT = "#1a1a1a"
TEXT_DIM = "#666666"
# Gradient pairs
GRADIENT_1 = ("#0066cc", "#003366") # Blue depth
GRADIENT_2 = ("#333333", "#666666") # Gray gradient
# =============================================================================
# MINIMAL MODERN
# Clean, sophisticated minimalism
# =============================================================================
class MinimalModern:
BACKGROUND = "#f5f5f5"
PRIMARY = "#1a1a1a" # Near black
SECONDARY = "#888888" # Medium gray
ACCENT = "#3366ff" # Accent blue
TEXT = "#1a1a1a"
TEXT_DIM = "#aaaaaa"
# Gradient pairs
GRADIENT_1 = ("#f5f5f5", "#e0e0e0") # Subtle gray
GRADIENT_2 = ("#1a1a1a", "#333333") # Dark subtle
# =============================================================================
# 3BLUE1BROWN STYLE
# Educational, warm mathematical aesthetic
# =============================================================================
class ThreeBlue1Brown:
BACKGROUND = "#1a1a2e" # Dark blue-gray
PRIMARY = "#58c4dd" # Signature blue
SECONDARY = "#83c167" # Green
ACCENT = "#ffff00" # Yellow highlight
TEXT = "#ffffff"
TEXT_DIM = "#888888"
# Additional math colors
VARIABLE = "#58c4dd" # Blue for variables
CONSTANT = "#ffff00" # Yellow for constants
FUNCTION = "#83c167" # Green for functions
RESULT = "#ffd700" # Gold for results
# =============================================================================
# PLAYFUL VIBRANT
# Fun, energetic, youth-oriented
# =============================================================================
class PlayfulVibrant:
BACKGROUND = "#ffeaa7" # Warm yellow
PRIMARY = "#ff6b6b" # Coral red
SECONDARY = "#4ecdc4" # Teal
ACCENT = "#9b59b6" # Purple
TEXT = "#2d3436"
TEXT_DIM = "#636e72"
# Fun colors
PINK = "#fd79a8"
ORANGE = "#e17055"
BLUE = "#0984e3"
GREEN = "#00b894"
# =============================================================================
# DARK ELEGANT
# Sophisticated dark theme with gold accents
# =============================================================================
class DarkElegant:
BACKGROUND = "#0d0d0d"
PRIMARY = "#d4af37" # Gold
SECONDARY = "#c0c0c0" # Silver
ACCENT = "#b87333" # Copper
TEXT = "#f5f5f5"
TEXT_DIM = "#777777"
# Metallic gradient
GRADIENT_GOLD = ("#d4af37", "#aa8c2c")
GRADIENT_SILVER = ("#c0c0c0", "#a0a0a0")
# =============================================================================
# NATURE ORGANIC
# Earthy, natural color palette
# =============================================================================
class NatureOrganic:
BACKGROUND = "#f4f1de" # Cream
PRIMARY = "#3d405b" # Dark slate
SECONDARY = "#81b29a" # Sage green
ACCENT = "#e07a5f" # Terra cotta
TEXT = "#3d405b"
TEXT_DIM = "#6c757d"
# Nature tones
FOREST = "#2d6a4f"
EARTH = "#8b5e3c"
SKY = "#89c2d9"
SUNSET = "#f4a261"
# =============================================================================
# RETRO SYNTHWAVE
# 80s inspired neon retro aesthetic
# =============================================================================
class RetroSynthwave:
BACKGROUND = "#1a0a2e" # Deep purple
PRIMARY = "#ff00ff" # Hot pink
SECONDARY = "#00fff7" # Cyan
ACCENT = "#ffff00" # Yellow
TEXT = "#ffffff"
TEXT_DIM = "#9966cc"
# Retro gradient (sunset)
GRADIENT_SUNSET = ("#ff6b6b", "#feca57", "#ff9ff3")
# Grid color
GRID = "#4a0080"
# =============================================================================
# UTILITY FUNCTIONS
# =============================================================================
def get_palette(style_name):
"""Get palette by name"""
palettes = {
'neon': NeonCyberpunk,
'corporate': CorporatePro,
'minimal': MinimalModern,
'3blue1brown': ThreeBlue1Brown,
'playful': PlayfulVibrant,
'elegant': DarkElegant,
'nature': NatureOrganic,
'retro': RetroSynthwave,
}
return palettes.get(style_name.lower(), ThreeBlue1Brown)
def apply_palette(scene, palette):
"""Apply palette to scene"""
scene.camera.background_color = palette.BACKGROUND
"""
Thumbnail Generation Presets
Ready-to-use functions for creating video thumbnails and title cards
"""
from manim import *
import numpy as np
# =============================================================================
# ASPECT RATIO CONFIGURATIONS
# =============================================================================
ASPECT_RATIOS = {
"youtube": {"width": 1920, "height": 1080, "name": "16:9"},
"youtube_short": {"width": 1080, "height": 1920, "name": "9:16"},
"instagram": {"width": 1080, "height": 1080, "name": "1:1"},
"twitter": {"width": 1200, "height": 675, "name": "16:9"},
"linkedin": {"width": 1200, "height": 627, "name": "1.91:1"},
}
# =============================================================================
# THUMBNAIL SCENE BASE
# =============================================================================
class ThumbnailScene(Scene):
"""Base class for thumbnail generation."""
# Override in subclass
TITLE = "Your Title Here"
SUBTITLE = ""
ASPECT_RATIO = "youtube"
def construct(self):
# Set up frame for aspect ratio
config = ASPECT_RATIOS.get(self.ASPECT_RATIO, ASPECT_RATIOS["youtube"])
# Build thumbnail
self.create_background()
self.create_main_visual()
self.create_text_overlay()
# Hold frame (for image export)
self.wait(0.1)
def create_background(self):
"""Override to customize background."""
# Default gradient background
bg = Rectangle(
width=config.frame_width + 1,
height=config.frame_height + 1,
fill_opacity=1,
stroke_width=0
)
bg.set_color(color=[BLUE_E, PURPLE_E])
self.add(bg)
def create_main_visual(self):
"""Override to add main visual element."""
pass
def create_text_overlay(self):
"""Override to customize text."""
title = Text(
self.TITLE,
font_size=72,
weight=BOLD,
color=WHITE
)
title.move_to(ORIGIN)
if self.SUBTITLE:
subtitle = Text(
self.SUBTITLE,
font_size=36,
color=GREY_A
)
subtitle.next_to(title, DOWN, buff=0.5)
self.add(subtitle)
self.add(title)
# =============================================================================
# PRESET THUMBNAIL STYLES
# =============================================================================
def create_math_thumbnail(scene, title, equation, colors=None):
"""
Create a math-focused thumbnail.
Args:
scene: The Scene instance
title: Main title text
equation: LaTeX equation string
colors: Optional dict of {substring: color}
"""
if colors is None:
colors = {}
# Dark gradient background
bg = Rectangle(
width=20, height=12,
fill_opacity=1, stroke_width=0
)
bg.set_color(color=[BLUE_E, BLACK])
scene.add(bg)
# Equation (main focus)
eq = MathTex(equation, font_size=96)
for substr, color in colors.items():
eq.set_color_by_tex(substr, color)
eq.move_to(ORIGIN)
# Title above
title_text = Text(title, font_size=56, weight=BOLD, color=WHITE)
title_text.to_edge(UP, buff=0.8)
# Decorative elements
line_left = Line(LEFT * 6, LEFT * 2, color=BLUE, stroke_width=4)
line_right = Line(RIGHT * 2, RIGHT * 6, color=BLUE, stroke_width=4)
line_left.next_to(title_text, DOWN, buff=0.3)
line_right.next_to(title_text, DOWN, buff=0.3)
scene.add(bg, eq, title_text, line_left, line_right)
def create_concept_thumbnail(scene, title, icon_mobject, subtitle=""):
"""
Create a concept-focused thumbnail with icon.
Args:
scene: The Scene instance
title: Main title text
icon_mobject: A Mobject to use as the central icon
subtitle: Optional subtitle
"""
# Gradient background
bg = Rectangle(width=20, height=12, fill_opacity=1, stroke_width=0)
bg.set_color(color=[PURPLE_E, BLUE_E])
scene.add(bg)
# Icon (scaled and centered)
icon = icon_mobject.copy()
icon.scale_to_fit_height(4)
icon.move_to(ORIGIN)
# Add glow effect
glow = icon.copy()
glow.set_color(WHITE)
glow.set_opacity(0.3)
glow.scale(1.2)
scene.add(glow)
scene.add(icon)
# Title
title_text = Text(title, font_size=64, weight=BOLD, color=WHITE)
title_text.to_edge(UP, buff=0.6)
scene.add(title_text)
# Subtitle
if subtitle:
sub = Text(subtitle, font_size=32, color=GREY_A)
sub.to_edge(DOWN, buff=0.8)
scene.add(sub)
def create_comparison_thumbnail(scene, left_text, right_text, vs_text="VS"):
"""
Create a comparison/versus thumbnail.
Args:
scene: The Scene instance
left_text: Text for left side
right_text: Text for right side
vs_text: Center divider text
"""
# Split background
left_bg = Rectangle(width=8, height=12, fill_opacity=1, stroke_width=0)
left_bg.set_color(BLUE_E)
left_bg.shift(LEFT * 4)
right_bg = Rectangle(width=8, height=12, fill_opacity=1, stroke_width=0)
right_bg.set_color(RED_E)
right_bg.shift(RIGHT * 4)
scene.add(left_bg, right_bg)
# VS circle
vs_circle = Circle(radius=1, fill_opacity=1, fill_color=WHITE, stroke_width=0)
vs_label = Text(vs_text, font_size=48, weight=BOLD, color=BLACK)
vs_group = VGroup(vs_circle, vs_label)
scene.add(vs_group)
# Left text
left = Text(left_text, font_size=56, weight=BOLD, color=WHITE)
left.move_to(LEFT * 4)
scene.add(left)
# Right text
right = Text(right_text, font_size=56, weight=BOLD, color=WHITE)
right.move_to(RIGHT * 4)
scene.add(right)
def create_numbered_thumbnail(scene, number, title, subtitle=""):
"""
Create a numbered episode/part thumbnail.
Args:
scene: The Scene instance
number: Episode/part number
title: Main title
subtitle: Optional subtitle
"""
# Background
bg = Rectangle(width=20, height=12, fill_opacity=1, stroke_width=0)
bg.set_color(color=[GREY_E, BLACK])
scene.add(bg)
# Large number
num = Text(str(number), font_size=200, weight=BOLD, color=BLUE)
num.set_opacity(0.3)
num.move_to(LEFT * 3)
scene.add(num)
# Title
title_text = Text(title, font_size=64, weight=BOLD, color=WHITE)
title_text.move_to(RIGHT * 1 + UP * 0.5)
scene.add(title_text)
# Part label
part_label = Text(f"PART {number}", font_size=28, color=BLUE)
part_label.next_to(title_text, UP, buff=0.5)
scene.add(part_label)
# Subtitle
if subtitle:
sub = Text(subtitle, font_size=32, color=GREY_A)
sub.next_to(title_text, DOWN, buff=0.5)
scene.add(sub)
def create_quote_thumbnail(scene, quote, author=""):
"""
Create a quote-style thumbnail.
Args:
scene: The Scene instance
quote: The quote text
author: Optional author attribution
"""
# Dark background
bg = Rectangle(width=20, height=12, fill_opacity=1, stroke_width=0)
bg.set_color(BLACK)
scene.add(bg)
# Quote marks
open_quote = Text('"', font_size=200, color=BLUE)
open_quote.set_opacity(0.5)
open_quote.to_corner(UL, buff=0.5)
scene.add(open_quote)
close_quote = Text('"', font_size=200, color=BLUE)
close_quote.set_opacity(0.5)
close_quote.to_corner(DR, buff=0.5)
scene.add(close_quote)
# Quote text
quote_text = Text(
quote,
font_size=48,
color=WHITE,
line_spacing=1.5
)
quote_text.move_to(ORIGIN)
# Wrap if too wide
if quote_text.width > 12:
quote_text.scale_to_fit_width(12)
scene.add(quote_text)
# Author
if author:
author_text = Text(f"— {author}", font_size=32, color=GREY_A)
author_text.next_to(quote_text, DOWN, buff=0.8)
scene.add(author_text)
# =============================================================================
# TITLE CARD PRESETS
# =============================================================================
class IntroTitleCard(Scene):
"""Animated intro title card."""
TITLE = "Your Title"
SUBTITLE = "Your Subtitle"
DURATION = 3
def construct(self):
# Background
bg = Rectangle(width=20, height=12, fill_opacity=1, stroke_width=0)
bg.set_color(color=[BLUE_E, BLACK])
self.add(bg)
# Title
title = Text(self.TITLE, font_size=72, weight=BOLD, color=WHITE)
title.move_to(UP * 0.5)
# Subtitle
subtitle = Text(self.SUBTITLE, font_size=36, color=GREY_A)
subtitle.next_to(title, DOWN, buff=0.5)
# Underline
underline = Line(LEFT * 4, RIGHT * 4, color=BLUE, stroke_width=4)
underline.next_to(subtitle, DOWN, buff=0.5)
# Animate
self.play(Write(title), run_time=1)
self.play(FadeIn(subtitle, shift=UP * 0.2), run_time=0.5)
self.play(Create(underline), run_time=0.5)
self.wait(self.DURATION - 2)
class OutroTitleCard(Scene):
"""Animated outro/end card."""
THANKS_TEXT = "Thanks for watching!"
CTA_TEXT = "Subscribe for more"
DURATION = 4
def construct(self):
# Background
bg = Rectangle(width=20, height=12, fill_opacity=1, stroke_width=0)
bg.set_color(BLACK)
self.add(bg)
# Thanks
thanks = Text(self.THANKS_TEXT, font_size=64, weight=BOLD, color=WHITE)
thanks.move_to(UP * 1)
# CTA
cta = Text(self.CTA_TEXT, font_size=36, color=BLUE)
cta.move_to(DOWN * 0.5)
# Animate
self.play(FadeIn(thanks, scale=1.2), run_time=1)
self.wait(0.5)
self.play(FadeIn(cta, shift=UP * 0.3), run_time=0.5)
self.wait(self.DURATION - 2)
self.play(FadeOut(thanks), FadeOut(cta), run_time=0.5)
# =============================================================================
# UTILITY FUNCTIONS
# =============================================================================
def export_thumbnail(scene_class, output_path, **scene_kwargs):
"""
Export a thumbnail scene as an image.
Usage:
export_thumbnail(MyThumbnail, "thumbnail.png", TITLE="Custom Title")
"""
# Set scene attributes
for key, value in scene_kwargs.items():
setattr(scene_class, key, value)
# Configure for image export
config.frame_rate = 1
config.pixel_width = 1920
config.pixel_height = 1080
# Render
scene = scene_class()
scene.render()
# The output will be in media/images/
def batch_export_thumbnails(scenes, output_dir):
"""
Export multiple thumbnails.
Args:
scenes: List of (scene_class, filename, kwargs) tuples
output_dir: Directory for output files
"""
import os
os.makedirs(output_dir, exist_ok=True)
for scene_class, filename, kwargs in scenes:
output_path = os.path.join(output_dir, filename)
export_thumbnail(scene_class, output_path, **kwargs)
print(f"Exported: {output_path}")
Audio Synchronization
Guidelines for integrating audio (voiceovers, music, sound effects) with Manim animations.
Overview
Audio adds a powerful dimension to animations. Proper synchronization ensures the visual and audio elements work together seamlessly.
Audio Types
| Type | Use Case | Sync Precision |
|---|---|---|
| Voiceover | Narration, explanations | High (word-level) |
| Background Music | Mood, pacing | Low (beat-level) |
| Sound Effects | Emphasis, feedback | High (frame-level) |
TTS (Text-to-Speech) Integration
TTS Options
| Service | Quality | Cost | Speed |
|---|---|---|---|
| Edge TTS | Good | Free | Fast |
| gTTS | Basic | Free | Fast |
| ElevenLabs | Excellent | Paid | Medium |
| OpenAI TTS | Excellent | Paid | Fast |
| Azure TTS | Excellent | Paid | Fast |
Edge TTS Example
import edge_tts
import asyncio
async def generate_voiceover(text, output_file, voice="en-US-AriaNeural"):
"""Generate voiceover using Edge TTS."""
communicate = edge_tts.Communicate(text, voice)
await communicate.save(output_file)
# Usage
asyncio.run(generate_voiceover(
"Welcome to this animation about calculus.",
"voiceover_intro.mp3"
))Available Voices (Edge TTS)
# English voices
VOICES = {
"en-US-AriaNeural": "Female, conversational",
"en-US-GuyNeural": "Male, narration",
"en-US-JennyNeural": "Female, friendly",
"en-GB-SoniaNeural": "Female, British",
"en-AU-NatashaNeural": "Female, Australian",
}Audio Timing Workflow
1. Generate Audio First
# Generate all voiceover segments
segments = [
("intro", "Welcome to our tutorial on derivatives."),
("definition", "A derivative measures the rate of change."),
("example", "Let's look at a simple example."),
]
for name, text in segments:
asyncio.run(generate_voiceover(text, f"audio/{name}.mp3"))2. Get Audio Durations
from pydub import AudioSegment
def get_audio_duration(file_path):
"""Get duration of audio file in seconds."""
audio = AudioSegment.from_mp3(file_path)
return len(audio) / 1000.0 # Convert ms to seconds
# Example
intro_duration = get_audio_duration("audio/intro.mp3")
print(f"Intro duration: {intro_duration:.2f}s")3. Sync Animation to Audio
class SyncedScene(Scene):
def construct(self):
# Pre-calculated durations from audio
AUDIO_DURATIONS = {
"intro": 3.2,
"definition": 4.5,
"example": 2.8,
}
# Intro segment
title = Text("Derivatives")
self.play(Write(title), run_time=min(1.5, AUDIO_DURATIONS["intro"]))
self.wait(AUDIO_DURATIONS["intro"] - 1.5) # Fill remaining time
# Definition segment
definition = MathTex(r"f'(x) = \lim_{h \to 0} \frac{f(x+h) - f(x)}{h}")
self.play(
FadeOut(title),
Write(definition),
run_time=min(2.0, AUDIO_DURATIONS["definition"])
)
self.wait(AUDIO_DURATIONS["definition"] - 2.0)Voiceover Markers
Marker Format in Scene Spec
## Scene 1: Introduction
**Duration:** 5.2s (from audio)
### Voiceover
"Welcome to this tutorial. [PAUSE 0.5] Today we'll learn about derivatives."
### Timing Markers
| Marker | Time | Action |
|--------|------|--------|
| 0.0s | Start | Show title |
| 1.2s | "tutorial" | Title fully written |
| 1.7s | Pause start | Wait |
| 2.2s | "Today" | Start equation fade-in |
| 5.2s | End | Transition |Implementing Markers
class MarkedScene(Scene):
MARKERS = [
(0.0, "start"),
(1.2, "title_complete"),
(1.7, "pause_start"),
(2.2, "equation_start"),
(5.2, "end"),
]
def construct(self):
title = Text("Derivatives")
equation = MathTex("f'(x)")
# Start to title_complete (1.2s)
self.play(Write(title), run_time=1.2)
# Pause (0.5s)
self.wait(0.5)
# Equation start to end
self.play(
FadeOut(title),
Write(equation),
run_time=3.0 # 5.2 - 2.2
)Audio/Video Muxing
Using FFmpeg
# Combine video with single audio track
ffmpeg -i video.mp4 -i audio.mp3 -c:v copy -c:a aac -shortest output.mp4
# Combine video with multiple audio segments
ffmpeg -i video.mp4 \
-i audio/intro.mp3 \
-i audio/main.mp3 \
-i audio/outro.mp3 \
-filter_complex "[1:a][2:a][3:a]concat=n=3:v=0:a=1[out]" \
-map 0:v -map "[out]" \
-c:v copy -c:a aac \
output.mp4Python Wrapper
import subprocess
def mux_audio_video(video_path, audio_path, output_path):
"""Combine video and audio into single file."""
cmd = [
"ffmpeg", "-y",
"-i", video_path,
"-i", audio_path,
"-c:v", "copy",
"-c:a", "aac",
"-shortest",
output_path
]
subprocess.run(cmd, check=True)
def concatenate_audio(audio_files, output_path):
"""Concatenate multiple audio files."""
# Create file list
with open("audio_list.txt", "w") as f:
for audio_file in audio_files:
f.write(f"file '{audio_file}'\n")
cmd = [
"ffmpeg", "-y",
"-f", "concat",
"-safe", "0",
"-i", "audio_list.txt",
"-c", "copy",
output_path
]
subprocess.run(cmd, check=True)Background Music
Volume Balancing
# Lower music volume when voiceover plays (ducking)
ffmpeg -i video.mp4 -i voiceover.mp3 -i music.mp3 \
-filter_complex "[2:a]volume=0.2[music];[1:a][music]amix=inputs=2:duration=longest[out]" \
-map 0:v -map "[out]" \
-c:v copy -c:a aac \
output.mp4Music Timing
# Music should align with scene changes
MUSIC_MARKERS = {
"intro": {"start": 0, "end": 10, "fade_in": 2},
"main": {"start": 10, "end": 120, "volume": 0.3},
"outro": {"start": 120, "end": 140, "fade_out": 5},
}Sound Effects
Common Effects
| Effect | Use Case | Duration |
|---|---|---|
| Whoosh | Transitions | 0.3-0.5s |
| Pop | Element appears | 0.1-0.2s |
| Click | Button/selection | 0.05-0.1s |
| Ding | Completion | 0.3-0.5s |
| Swoosh | Movement | 0.2-0.4s |
Triggering Effects
class SFXScene(Scene):
# Map animations to sound effects
SFX_MAP = {
"appear": "sfx/pop.mp3",
"transform": "sfx/whoosh.mp3",
"complete": "sfx/ding.mp3",
}
def construct(self):
circle = Circle()
# Log timing for post-processing
self.sfx_cues = []
# Pop sound at 0s
self.sfx_cues.append((0, "appear"))
self.play(GrowFromCenter(circle))
# Whoosh at current time
self.sfx_cues.append((self.renderer.time, "transform"))
self.play(circle.animate.shift(RIGHT * 2))Complete Pipeline
Workflow
1. Write script text
↓
2. Generate TTS audio
↓
3. Get audio durations
↓
4. Create scene spec with timings
↓
5. Implement Manim animation
↓
6. Render video (no audio)
↓
7. Mux audio + video
↓
8. Add music/SFX (optional)
↓
9. Final exportPipeline Script
import asyncio
import subprocess
from pathlib import Path
class AudioPipeline:
def __init__(self, project_dir):
self.project_dir = Path(project_dir)
self.audio_dir = self.project_dir / "audio"
self.audio_dir.mkdir(exist_ok=True)
async def generate_all_audio(self, script_segments):
"""Generate TTS for all segments."""
for name, text in script_segments.items():
output = self.audio_dir / f"{name}.mp3"
await generate_voiceover(text, str(output))
def get_all_durations(self):
"""Get durations for all audio files."""
durations = {}
for audio_file in self.audio_dir.glob("*.mp3"):
durations[audio_file.stem] = get_audio_duration(str(audio_file))
return durations
def mux_final(self, video_path, output_path):
"""Combine all audio with video."""
audio_files = sorted(self.audio_dir.glob("*.mp3"))
# Concatenate audio
combined_audio = self.audio_dir / "combined.mp3"
concatenate_audio(
[str(f) for f in audio_files],
str(combined_audio)
)
# Mux with video
mux_audio_video(video_path, str(combined_audio), output_path)Best Practices
Do
- Generate audio before animating
- Use timing markers in specs
- Leave buffer time between segments
- Test audio sync at multiple points
- Use consistent voice throughout
Avoid
- Animating faster than narration
- Abrupt audio cuts
- Inconsistent volume levels
- Missing audio for long sequences
- Over-compressed audio
Brand Consistency Rules
Establishing a Visual Identity
Core Brand Elements
1. Color Palette - Primary, secondary, accent colors 2. Typography - Font families and weights 3. Motion Style - Animation timing and easing 4. Visual Elements - Shapes, icons, patterns
Color Palette Management
Define Once, Use Everywhere
# Brand colors (define at top of file)
class BrandColors:
PRIMARY = "#3498DB" # Main brand color
SECONDARY = "#2ECC71" # Supporting color
ACCENT = "#F39C12" # Highlight/CTA
BACKGROUND = "#1A1A2E" # Dark background
TEXT_LIGHT = "#FFFFFF" # Light text
TEXT_DARK = "#333333" # Dark text
ERROR = "#E74C3C" # Error/warning
SUCCESS = "#27AE60" # Success/positive
# Usage
title = Text("Brand Title", color=BrandColors.PRIMARY)
background = Rectangle(color=BrandColors.BACKGROUND, fill_opacity=1)Color Usage Rules
# Primary: Main messages, key elements
# Max: 60% of visual space
main_title.set_color(BrandColors.PRIMARY)
# Secondary: Supporting elements, backgrounds
# Max: 30% of visual space
supporting_box.set_fill(BrandColors.SECONDARY, opacity=0.3)
# Accent: CTAs, highlights, emphasis
# Max: 10% of visual space
highlight.set_color(BrandColors.ACCENT)Typography Standards
Font Stack Definition
class BrandFonts:
HEADING = "Montserrat"
BODY = "Open Sans"
MONO = "Fira Code"
class BrandSizes:
HERO = 96
TITLE = 72
HEADING = 48
SUBHEAD = 36
BODY = 28
CAPTION = 20
# Usage
title = Text(
"Main Title",
font=BrandFonts.HEADING,
font_size=BrandSizes.TITLE,
weight=BOLD
)Font Weight Guidelines
# Heroes and titles: Bold/Black
# Subheadings: SemiBold
# Body text: Regular
# Captions: Light
hero = Text("HERO", weight=ULTRABOLD)
heading = Text("Heading", weight=BOLD)
subhead = Text("Subheading", weight=SEMIBOLD)
body = Text("Body text", weight=NORMAL)
caption = Text("Caption", weight=LIGHT)Motion Standards
Animation Presets
class BrandMotion:
# Timing
FAST = 0.3
NORMAL = 0.5
SLOW = 0.8
DRAMATIC = 1.2
# Easing
ENTRANCE = rate_functions.ease_out_cubic
EXIT = rate_functions.ease_in_cubic
EMPHASIS = rate_functions.ease_out_back
SMOOTH = rate_functions.smooth
# Usage
self.play(
logo.animate.move_to(ORIGIN),
run_time=BrandMotion.NORMAL,
rate_func=BrandMotion.ENTRANCE
)Transition Standards
def brand_fade_in(self, mobject):
"""Standard brand entrance"""
mobject.set_opacity(0).shift(UP * 0.3)
self.play(
mobject.animate.set_opacity(1).shift(DOWN * 0.3),
run_time=BrandMotion.NORMAL,
rate_func=BrandMotion.ENTRANCE
)
def brand_fade_out(self, mobject):
"""Standard brand exit"""
self.play(
mobject.animate.set_opacity(0).shift(UP * 0.3),
run_time=BrandMotion.FAST,
rate_func=BrandMotion.EXIT
)Visual Element Standards
Shape Language
class BrandShapes:
CORNER_RADIUS = 0.2 # Rounded corners
STROKE_WIDTH = 3 # Line thickness
DOT_SIZE = 0.1 # Standard dot radius
@staticmethod
def rounded_rect(width, height, **kwargs):
return RoundedRectangle(
width=width,
height=height,
corner_radius=BrandShapes.CORNER_RADIUS,
stroke_width=BrandShapes.STROKE_WIDTH,
**kwargs
)Icon Style
# Consistent icon treatment
def brand_icon(svg_path, size=1):
icon = SVGMobject(svg_path)
icon.set_color(BrandColors.PRIMARY)
icon.scale_to_fit_height(size)
return iconSpacing Standards
Margins and Padding
class BrandSpacing:
MARGIN_LARGE = 1.0 # Edge margins
MARGIN_MEDIUM = 0.5 # Section margins
MARGIN_SMALL = 0.25 # Element margins
PADDING = 0.3 # Internal padding
# Usage
title.to_edge(UP, buff=BrandSpacing.MARGIN_LARGE)
elements.arrange(DOWN, buff=BrandSpacing.MARGIN_MEDIUM)Grid System
# 12-column grid
COLUMN_WIDTH = 14 / 12 # Screen width / 12
def grid_position(column, span=1):
"""Position element on grid"""
center = (column + span/2 - 6.5) * COLUMN_WIDTH
return center * RIGHTCreating a Brand Template
Scene Base Class
class BrandedScene(Scene):
def setup(self):
# Set background
self.camera.background_color = BrandColors.BACKGROUND
def add_brand_header(self, title_text):
title = Text(
title_text,
font=BrandFonts.HEADING,
font_size=BrandSizes.TITLE,
color=BrandColors.TEXT_LIGHT
)
title.to_edge(UP, buff=BrandSpacing.MARGIN_LARGE)
self.play(Write(title))
return title
def add_brand_footer(self):
logo = SVGMobject("logo.svg")
logo.scale(0.3)
logo.to_corner(DR, buff=BrandSpacing.MARGIN_SMALL)
self.add(logo)Quality Checklist
Before finalizing any animation:
- [ ] Colors match brand palette exactly
- [ ] Fonts are correct family and weight
- [ ] Animation timing follows brand standards
- [ ] Spacing is consistent
- [ ] Logo is present and correctly sized
- [ ] No off-brand colors or fonts
- [ ] Motion style matches established patterns
Common Mistakes
DON'T: Mix brand colors with arbitrary colors
# BAD
text.set_color("#FF69B4") # Random pink not in paletteDO: Use defined palette
# GOOD
text.set_color(BrandColors.ACCENT)DON'T: Inconsistent animation styles
# BAD - Different easing everywhere
self.play(obj1.animate.shift(UP), rate_func=ease_out_bounce)
self.play(obj2.animate.shift(UP), rate_func=linear)
self.play(obj3.animate.shift(UP), rate_func=rush_into)DO: Use brand motion standards
# GOOD - Consistent brand motion
for obj in [obj1, obj2, obj3]:
self.play(
obj.animate.shift(UP),
rate_func=BrandMotion.ENTRANCE,
run_time=BrandMotion.NORMAL
)Community Edition Implementation Patterns
Best practices for implementing animations with the Manim Community Edition renderer.
Import Patterns
# Standard import (recommended)
from manim import *
# Selective imports for clarity
from manim import Scene, Circle, Square, Write, FadeIn, MathTexScene Types
Standard Scene
class MyAnimation(Scene):
def construct(self):
circle = Circle()
self.play(Create(circle))
self.wait()3D Scene
class My3DAnimation(ThreeDScene):
def construct(self):
self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)
sphere = Sphere()
self.play(Create(sphere))Moving Camera Scene
class CameraAnimation(MovingCameraScene):
def construct(self):
circle = Circle()
self.play(Create(circle))
self.play(self.camera.frame.animate.scale(0.5).move_to(circle))Core Animations
Creation Animations
| Animation | Use Case |
|---|---|
Create(mobject) | Draw shapes stroke-first |
Write(text) | Write text/equations |
FadeIn(mobject) | Gentle appearance |
GrowFromCenter(mobject) | Expand from center |
DrawBorderThenFill(mobject) | Outline then fill |
Transformation Animations
| Animation | Use Case |
|---|---|
Transform(a, b) | Morph a into b |
ReplacementTransform(a, b) | Replace a with b |
TransformMatchingTex(a, b) | Match LaTeX parts |
TransformMatchingShapes(a, b) | Match shape components |
Exit Animations
| Animation | Use Case |
|---|---|
FadeOut(mobject) | Gentle disappearance |
Uncreate(mobject) | Reverse of Create |
ShrinkToCenter(mobject) | Collapse to center |
LaTeX Rendering
Basic Math
# Simple equation
eq = MathTex("E = mc^2")
# With color isolation
eq = MathTex("E", "=", "m", "c^2")
eq[0].set_color(YELLOW) # E
eq[3].set_color(BLUE) # c^2Using substrings_to_isolate
eq = MathTex(
"x^2 + 2x + 1 = 0",
substrings_to_isolate=["x", "1", "0"]
)
eq.set_color_by_tex("x", BLUE)
eq.set_color_by_tex("1", GREEN)Text with Math
text = Tex("The area is ", "$A = \\pi r^2$")CLI Usage
# Preview with low quality (fast)
manim -pql script.py SceneName
# Preview with medium quality
manim -pqm script.py SceneName
# High quality render
manim -pqh script.py SceneName
# 4K render
manim -pqk script.py SceneName
# Save last frame as image
manim -pql -s script.py SceneName
# Transparent background
manim -pql -t script.py SceneNameQuality Presets
| Flag | Resolution | FPS | Use Case |
|---|---|---|---|
-ql | 480p | 15 | Quick preview |
-qm | 720p | 30 | Development |
-qh | 1080p | 60 | Production |
-qk | 4K | 60 | High-end production |
Debugging Techniques
Visual Debugging
# Add without animation (instant)
self.add(circle)
# Show bounding box
circle.add(SurroundingRectangle(circle))
# Add coordinate labels
self.add(NumberPlane())Print Debugging
def construct(self):
circle = Circle()
print(f"Circle center: {circle.get_center()}")
print(f"Circle radius: {circle.radius}")
print(f"Circle color: {circle.get_color()}")Interactive Mode
# Pause and inspect
self.wait()
self.interactive_embed() # Opens interactive shellAnimation Timing
Run Time Control
self.play(Create(circle), run_time=2) # 2 seconds
self.play(FadeIn(square), run_time=0.5) # FastSimultaneous Animations
# All at once
self.play(Create(circle), Write(text), FadeIn(square))
# With AnimationGroup
self.play(AnimationGroup(
Create(circle),
Write(text),
lag_ratio=0.5 # Stagger start times
))Sequential with Succession
self.play(Succession(
Create(circle),
circle.animate.shift(RIGHT),
FadeOut(circle)
))Common Patterns
Animate Method
# Fluent animation syntax
self.play(circle.animate.shift(RIGHT).scale(2).set_color(RED))Value Tracking
tracker = ValueTracker(0)
number = always_redraw(lambda: DecimalNumber(tracker.get_value()))
self.play(tracker.animate.set_value(100), run_time=3)Updaters
dot = Dot()
label = always_redraw(lambda: Text(f"({dot.get_x():.1f}, {dot.get_y():.1f})").next_to(dot, UP))
self.add(dot, label)
self.play(dot.animate.shift(RIGHT * 3))Anti-Patterns
Avoid
# Don't modify mobjects during play
self.play(Create(circle))
circle.shift(RIGHT) # Wrong - not animated
# Don't forget to add mobjects
text = Text("Hello")
self.play(text.animate.shift(UP)) # Error - text not addedPrefer
# Animate changes
self.play(Create(circle))
self.play(circle.animate.shift(RIGHT)) # Correct
# Add before animating
text = Text("Hello")
self.add(text)
self.play(text.animate.shift(UP)) # Or use FadeInKinetic Typography Rules
Core Principles
1. Text as Character
Each word or phrase should have personality through motion:
- Bold statements → Strong, impactful entrances
- Questions → Floating, uncertain movement
- Emphasis words → Scale and color changes
- Whispers → Small, subtle animations
2. Timing is Meaning
The speed of text animations conveys emotion:
- Fast → Urgency, excitement, energy
- Slow → Importance, drama, contemplation
- Staggered → Building, listing, progression
Entry Animations
Pop In (Impact)
text = Text("BOOM", font_size=96, weight=BOLD)
text.scale(0)
self.play(
text.animate.scale(1),
rate_func=ease_out_back,
run_time=0.3
)Slide In (Smooth)
text.shift(LEFT * 10) # Start off-screen
self.play(
text.animate.move_to(ORIGIN),
rate_func=ease_out_cubic,
run_time=0.5
)Fade Up (Gentle)
text.shift(DOWN * 0.5).set_opacity(0)
self.play(
text.animate.shift(UP * 0.5).set_opacity(1),
run_time=0.7
)Type On (Typewriter)
self.play(AddTextLetterByLetter(text), run_time=1)Scale from Point
text.scale(0).move_to(start_point)
self.play(
text.animate.scale(1).move_to(ORIGIN),
rate_func=ease_out_expo,
run_time=0.4
)Exit Animations
Fade Out Up
self.play(
text.animate.shift(UP * 0.5).set_opacity(0),
run_time=0.3
)Scale to Nothing
self.play(
text.animate.scale(0),
rate_func=ease_in_cubic,
run_time=0.3
)Slide Out
self.play(
text.animate.shift(RIGHT * 15),
rate_func=ease_in_cubic,
run_time=0.4
)Shatter (Advanced)
# Split into letters and scatter
letters = VGroup(*[Text(c) for c in text.text])
letters.arrange(RIGHT, buff=0.1)
self.play(
*[l.animate.shift(
np.random.uniform(-3, 3) * RIGHT +
np.random.uniform(-2, 2) * UP
).set_opacity(0) for l in letters],
run_time=0.5
)Word-by-Word Techniques
Sequential Words
words = ["The", "Future", "Is", "Now"]
positions = [LEFT * 3, LEFT * 1, RIGHT * 1, RIGHT * 3]
for word, pos in zip(words, positions):
text = Text(word, font_size=72)
text.move_to(pos)
self.play(FadeIn(text, scale=0.5), run_time=0.3)
self.wait(0.2)Replace Words
words = ["Think", "Create", "Innovate"]
current = Text(words[0], font_size=96)
self.play(FadeIn(current))
for word in words[1:]:
new = Text(word, font_size=96)
self.play(
FadeOut(current, shift=UP),
FadeIn(new, shift=UP),
run_time=0.5
)
current = newStacking Words
words = ["We", "Make", "Magic", "Happen"]
stack = VGroup()
for word in words:
text = Text(word, font_size=48)
stack.add(text)
stack.arrange(DOWN, buff=0.3)
self.play(FadeIn(text, shift=LEFT), run_time=0.3)Text Effects
Glitch Effect
# Create color-shifted copies
red = text.copy().set_color(RED).shift(LEFT * 0.03 + UP * 0.02)
blue = text.copy().set_color(BLUE).shift(RIGHT * 0.03 + DOWN * 0.02)
self.add(red, blue, text)
for _ in range(5):
self.play(
red.animate.shift(RIGHT * 0.05),
blue.animate.shift(LEFT * 0.05),
run_time=0.05
)
self.play(
red.animate.shift(LEFT * 0.05),
blue.animate.shift(RIGHT * 0.05),
run_time=0.05
)Wave Effect
letters = VGroup(*text)
for i, letter in enumerate(letters):
letter.add_updater(
lambda m, i=i: m.shift(UP * 0.1 * np.sin(self.time * 5 + i))
)Color Sweep
# Gradient color change across text
self.play(
text.animate.set_color_by_gradient(BLUE, PURPLE, RED),
run_time=1
)Font and Style Guidelines
Font Choices by Mood
| Mood | Font Style | Weight |
|---|---|---|
| Professional | Sans-serif | Regular |
| Bold statement | Sans-serif | Bold/Black |
| Elegant | Serif | Light |
| Playful | Rounded | Medium |
| Technical | Monospace | Regular |
Size Guidelines
# Hierarchy through size
TITLE = 96
SUBTITLE = 48
BODY = 36
CAPTION = 24Best Practices
1. One focal point - Don't animate multiple text elements competing for attention 2. Consistent style - Same animation style for same content type 3. Readable duration - Text must be on screen long enough to read 4. Purposeful motion - Every animation should support the message 5. Contrast matters - Ensure text is readable against background
Common Mistakes
DON'T: Too many simultaneous animations
# BAD - Chaotic
self.play(
word1.animate.rotate(PI),
word2.animate.scale(2),
word3.animate.shift(UP * 3),
word4.animate.set_color(RED)
)DO: Focused, sequential animations
# GOOD - Clear
self.play(FadeIn(word1))
self.play(Indicate(word2))
self.play(word3.animate.set_color(YELLOW))OpenGL Renderer Implementation Patterns
Best practices for implementing animations with the OpenGL-based renderer (alternative to Cairo).
Key Differences from Community Edition
| Feature | Community Edition | OpenGL Renderer |
|---|---|---|
| Creation animation | Create() | ShowCreation() |
| Color in LaTeX | set_color_by_tex() | t2c parameter |
| Camera control | self.camera.frame | self.frame |
| 3D Scene class | ThreeDScene | Standard Scene |
| Interactive mode | Limited | Full with -se flag |
Import Patterns
# Standard import for OpenGL renderer
from manimlib import *
# Note: Different package nameInteractive Development
Starting Interactive Mode
# Run with interactive embed
manimgl script.py SceneName -se
# The -se flag enables:
# - Pausing at self.embed() calls
# - Live code execution
# - Real-time mobject inspectionUsing embed()
class InteractiveScene(Scene):
def construct(self):
circle = Circle()
self.play(ShowCreation(circle))
# Pause here for interactive exploration
self.embed()
# Continue after exiting embed
self.play(circle.animate.shift(RIGHT))Checkpoint Workflow
def construct(self):
# Create initial state
circle = Circle()
self.play(ShowCreation(circle))
# Save checkpoint
self.checkpoint_paste() # Copy current state
# Experiment from here
self.play(circle.animate.scale(2))LaTeX with Color Coding
Using t2c Parameter
# t2c = "tex to color" dictionary
equation = Tex(
"E = mc^2",
t2c={
"E": YELLOW,
"m": GREEN,
"c": BLUE
}
)Complex Color Mapping
# Multiple characters with same color
formula = Tex(
"\\frac{d}{dx}[x^n] = nx^{n-1}",
t2c={
"x": BLUE,
"n": RED,
"d": GREEN,
"dx": GREEN
}
)Isolating Substrings
# For transformation matching
eq1 = Tex("x^2", " + ", "2x", " + ", "1")
eq2 = Tex("(x + 1)^2")
# Color individual parts
eq1[0].set_color(BLUE) # x^2
eq1[2].set_color(GREEN) # 2xCamera Control
Frame-Based Control
class CameraScene(Scene):
def construct(self):
circle = Circle()
self.play(ShowCreation(circle))
# Access frame directly (not self.camera.frame)
self.play(self.frame.animate.scale(0.5))
self.play(self.frame.animate.move_to(circle))3D Camera Orientation
class My3DScene(Scene):
def construct(self):
# No special 3D scene class needed
surface = Surface(lambda u, v: [u, v, u*v])
self.play(ShowCreation(surface))
# Reorient camera
self.play(self.frame.animate.reorient(45, 60))
# Camera parameters: theta (rotation), phi (elevation)Camera Movements
# Pan
self.play(self.frame.animate.shift(RIGHT * 2))
# Zoom
self.play(self.frame.animate.scale(0.5)) # Zoom in
self.play(self.frame.animate.scale(2)) # Zoom out
# Combined
self.play(
self.frame.animate
.move_to(target)
.scale(0.3)
.reorient(30, 45)
)Animation Syntax
ShowCreation (not Create)
# Correct for OpenGL renderer
self.play(ShowCreation(circle))
# NOT Create(circle) - that's Community EditionOther Animation Differences
# These work the same
self.play(Write(text))
self.play(FadeIn(mobject))
self.play(Transform(a, b))
# Animate method works the same
self.play(circle.animate.shift(RIGHT).scale(2))3D Without Special Classes
class Simple3D(Scene):
def construct(self):
# 3D objects work in regular Scene
axes = ThreeDAxes()
sphere = Sphere(radius=1)
self.play(ShowCreation(axes))
self.play(ShowCreation(sphere))
# Camera reorientation
self.play(self.frame.animate.reorient(60, 70))
# Continuous rotation
self.frame.add_updater(lambda m, dt: m.increment_theta(0.1 * dt))
self.wait(5)Debugging in Interactive Mode
Inspecting Mobjects
def construct(self):
circle = Circle()
self.add(circle)
self.embed()
# In interactive mode, you can:
# >>> circle.get_center()
# >>> circle.set_color(RED)
# >>> self.play(circle.animate.scale(2))Real-Time Modifications
# While in embed(), you can:
# - Create new mobjects
# - Run animations
# - Test transformations
# - Adjust colors and positionsPerformance Optimizations
GPU Acceleration
# OpenGL renderer leverages GPU for:
# - Shader-based rendering
# - Real-time preview
# - Smooth 3D rotationsEfficient Updates
# Use always_redraw for dynamic content
graph = always_redraw(lambda: FunctionGraph(
lambda x: np.sin(x + tracker.get_value()),
x_range=[-PI, PI]
))Common Patterns
Interactive Workflow
# 1. Start with interactive flag
manimgl script.py MyScene -se
# 2. Scene runs until embed()
# 3. Test changes in Python shell
# 4. Type 'exit()' to continue
# 5. Iterate on codeRapid Prototyping
class Prototype(Scene):
def construct(self):
# Quick setup
self.play(ShowCreation(Circle()))
# Pause to experiment
self.embed()
# Continue with refined versionAnti-Patterns
Avoid
# Don't use Create() - use ShowCreation()
self.play(Create(circle)) # Wrong for OpenGL
# Don't use self.camera.frame
self.play(self.camera.frame.animate.scale(0.5)) # WrongPrefer
# Use ShowCreation()
self.play(ShowCreation(circle)) # Correct
# Use self.frame directly
self.play(self.frame.animate.scale(0.5)) # CorrectTiming and Easing Rules
The 12 Principles Applied to Motion Graphics
1. Timing
The number of frames for an action determines perception:
- Fewer frames = faster, snappier
- More frames = slower, heavier
2. Ease In / Ease Out (Slow In / Slow Out)
Objects accelerate and decelerate naturally:
# Natural motion
rate_func=smooth # Default, good for most
# Dramatic entrance (fast start, slow end)
rate_func=ease_out_cubic
# Dramatic exit (slow start, fast end)
rate_func=ease_in_cubicManim Rate Functions Reference
Standard Easing
| Rate Function | Feel | Best For |
|---|---|---|
linear | Mechanical | Loading bars, clocks |
smooth | Natural | General purpose |
rush_into | Aggressive | Exits, impacts |
rush_from | Dramatic | Entrances |
Cubic Family
| Rate Function | Curve | Use Case |
|---|---|---|
ease_in_cubic | Slow→Fast | Exits |
ease_out_cubic | Fast→Slow | Entrances |
ease_in_out_cubic | Slow→Fast→Slow | Smooth transitions |
Expo Family (More Dramatic)
| Rate Function | Effect | Use Case |
|---|---|---|
ease_in_expo | Very slow start | Suspense |
ease_out_expo | Snappy stop | Impact |
Bounce and Back
| Rate Function | Effect | Use Case |
|---|---|---|
ease_out_bounce | Bouncy landing | Playful |
ease_out_back | Overshoot | Pop-in effects |
ease_in_back | Wind-up | Anticipation |
Special Functions
| Rate Function | Effect | Use Case |
|---|---|---|
there_and_back | Go and return | Pulses |
double_smooth | Extra smooth | Gentle loops |
lingering | Slow at end | Emphasis |
Timing by Animation Type
Entrances
# Pop in (impactful)
run_time=0.3, rate_func=ease_out_back
# Slide in (smooth)
run_time=0.5, rate_func=ease_out_cubic
# Fade in (gentle)
run_time=0.7, rate_func=smoothExits
# Quick exit
run_time=0.3, rate_func=ease_in_cubic
# Dramatic exit
run_time=0.5, rate_func=ease_in_expo
# Gentle exit
run_time=0.7, rate_func=smoothEmphasis
# Scale pulse
run_time=0.4, rate_func=there_and_back
# Color flash
run_time=0.3, rate_func=ease_out_cubic
# Shake/wiggle
run_time=0.5, rate_func=wiggleTransforms
# Shape morph
run_time=1.0, rate_func=smooth
# Position change
run_time=0.5, rate_func=ease_in_out_cubic
# Dramatic transformation
run_time=1.5, rate_func=ease_in_out_expoDuration Guidelines
By Content Type
| Animation | Duration |
|---|---|
| Logo reveal | 1-2s |
| Title entrance | 0.5-1s |
| Text word | 0.3-0.5s |
| Icon pop | 0.2-0.4s |
| Scene transition | 0.5-1s |
| Background change | 0.3-0.5s |
By Emotional Tone
| Tone | Speed Multiplier |
|---|---|
| Energetic | 0.7x |
| Normal | 1.0x |
| Calm | 1.3x |
| Dramatic | 1.5x |
| Suspenseful | 2.0x |
Custom Rate Functions
Creating Custom Easing
def custom_ease(t):
"""
t goes from 0 to 1
Return value should also be 0 to 1
"""
# Example: slow middle, fast ends
return t ** 2 if t < 0.5 else 1 - (1 - t) ** 2
self.play(obj.animate.move_to(RIGHT), rate_func=custom_ease)Bezier Curve Easing
from manim import bezier
# Custom bezier curve
custom = bezier([0, 0, 0.2, 1]) # Control points
self.play(obj.animate.shift(RIGHT), rate_func=custom)Timing Patterns
The "Snap" Pattern
Quick movement with overshoot:
self.play(
obj.animate.move_to(target),
rate_func=ease_out_back,
run_time=0.3
)The "Slide" Pattern
Smooth, professional movement:
self.play(
obj.animate.move_to(target),
rate_func=ease_in_out_cubic,
run_time=0.7
)The "Bounce" Pattern
Playful, energetic:
self.play(
obj.animate.move_to(target),
rate_func=ease_out_bounce,
run_time=0.8
)The "Anticipation" Pattern
Wind-up before action:
# Anticipation
self.play(
obj.animate.shift(LEFT * 0.2),
rate_func=ease_out_cubic,
run_time=0.2
)
# Main action
self.play(
obj.animate.shift(RIGHT * 5),
rate_func=ease_in_cubic,
run_time=0.3
)Common Mistakes
DON'T: Same timing for everything
# BAD - Monotonous
self.play(obj1.animate.shift(UP), run_time=1)
self.play(obj2.animate.shift(UP), run_time=1)
self.play(obj3.animate.shift(UP), run_time=1)DO: Vary timing for interest
# GOOD - Dynamic
self.play(obj1.animate.shift(UP), run_time=0.5)
self.play(obj2.animate.shift(UP), run_time=0.3)
self.play(obj3.animate.shift(UP), run_time=0.7)DON'T: Linear easing for organic motion
# BAD - Robotic
self.play(logo.animate.move_to(ORIGIN), rate_func=linear)DO: Use appropriate easing
# GOOD - Natural
self.play(logo.animate.move_to(ORIGIN), rate_func=ease_out_cubic)Visual Hierarchy Rules
The Hierarchy Stack
From most to least attention-grabbing: 1. Movement - Moving elements catch the eye first 2. Size - Larger elements dominate 3. Color - Bright/contrasting colors stand out 4. Position - Center > edges 5. Complexity - Detailed elements attract attention
Size Hierarchy
Scale Ratios
# Primary element (hero)
primary_scale = 1.5
# Secondary elements
secondary_scale = 1.0
# Tertiary/supporting
tertiary_scale = 0.7
# Background/ambient
background_scale = 0.5Application
# Hero title
title = Text("MAIN MESSAGE", font_size=96)
# Supporting info
subtitle = Text("Supporting details", font_size=48)
# Fine print
caption = Text("Additional context", font_size=24)Color Hierarchy
Attention Levels
# Maximum attention (use sparingly)
ATTENTION_HIGH = "#FF0000" # Pure red
ATTENTION_HIGH = "#FFD700" # Gold
# Medium attention
ATTENTION_MED = "#3498DB" # Blue
ATTENTION_MED = "#2ECC71" # Green
# Low attention
ATTENTION_LOW = "#888888" # Gray
ATTENTION_LOW = "#CCCCCC" # Light gray
# Background
BACKGROUND = "#1A1A1A" # Near blackColor Contrast Rules
# High contrast = high visibility
bright_on_dark = Text("Important", color=WHITE) # on dark bg
bright_on_dark.set_stroke(BLACK, width=2) # outline for extra pop
# Low contrast = supporting role
muted = Text("Background info", color=GRAY)Position Hierarchy
Screen Zones by Importance
┌─────────────────────────────────┐
│ HIGH ATTENTION │
│ (TOP CENTER) │
├─────────────────────────────────┤
│ MED │ HIGHEST │ MED │
│ (LEFT) │ (CENTER) │ (RIGHT) │
├─────────────────────────────────┤
│ LOW ATTENTION │
│ (BOTTOM CENTER) │
└─────────────────────────────────┘Positioning Code
# Highest priority
main_element.move_to(ORIGIN)
# High priority
title.to_edge(UP, buff=1)
# Medium priority
side_element.to_edge(LEFT, buff=1)
# Low priority
caption.to_edge(DOWN, buff=0.5)Movement Hierarchy
Speed = Importance
# Important = deliberate motion
self.play(
important.animate.move_to(ORIGIN),
run_time=1.0 # Slower = more weight
)
# Less important = quick motion
self.play(
background_elem.animate.shift(UP),
run_time=0.3 # Faster = less attention
)Isolation Through Stillness
# Make everything still except focus
for elem in background_elements:
elem.clear_updaters() # Stop any motion
# Only focus element moves
self.play(focus.animate.pulse())Layering Hierarchy
Z-Index Control
# Foreground (most important)
hero.set_z_index(10)
# Middle layer
content.set_z_index(5)
# Background
ambient.set_z_index(0)
# Far background
texture.set_z_index(-5)Depth Through Opacity
# Foreground: full opacity
hero.set_opacity(1)
# Middle: slightly faded
content.set_opacity(0.9)
# Background: significantly faded
ambient.set_opacity(0.4)Typography Hierarchy
Font Weight Ladder
# Level 1: Hero text
Text("HERO", weight=ULTRABOLD, font_size=96)
# Level 2: Section header
Text("Section", weight=BOLD, font_size=64)
# Level 3: Subheader
Text("Subheader", weight=SEMIBOLD, font_size=48)
# Level 4: Body
Text("Body text", weight=NORMAL, font_size=32)
# Level 5: Caption
Text("Caption", weight=LIGHT, font_size=24)Creating Visual Flow
Leading the Eye
# Use lines/arrows to guide attention
flow_line = CurvedArrow(
start_point=elem1.get_right(),
end_point=elem2.get_left()
)
# Numbered sequence
for i, elem in enumerate(elements):
number = Text(str(i + 1)).scale(0.5).next_to(elem, LEFT)Reading Order (Western)
1 → 2 → 3
↓ ↓
4 → 5 → 6
↓ ↓
7 → 8 → 9Dynamic Hierarchy
Shifting Focus
# Initial state: elem1 is focus
elem1.set_opacity(1).scale(1.2)
elem2.set_opacity(0.5).scale(1)
# Shift focus to elem2
self.play(
elem1.animate.set_opacity(0.5).scale(1/1.2),
elem2.animate.set_opacity(1).scale(1.2)
)Temporal Hierarchy
# First = most important (primacy effect)
self.play(FadeIn(most_important))
self.wait(2)
# Middle = supporting
for elem in supporting:
self.play(FadeIn(elem), run_time=0.3)
# Last = second most important (recency effect)
self.play(FadeIn(conclusion))
self.wait(2)Common Mistakes
DON'T: Everything same size
# BAD - No hierarchy
elements = [Text(w, font_size=48) for w in words]DO: Clear size difference
# GOOD - Obvious hierarchy
title = Text("TITLE", font_size=72)
subtitle = Text("Subtitle", font_size=36)DON'T: Competing focal points
# BAD - Where should I look?
self.play(
elem1.animate.set_color(RED).scale(1.5),
elem2.animate.set_color(YELLOW).scale(1.5),
elem3.animate.set_color(GREEN).scale(1.5)
)DO: Single clear focal point
# GOOD - Clear focus
self.play(
focus.animate.set_color(YELLOW).scale(1.5),
others.animate.set_opacity(0.3)
)Web Export Patterns
Guidelines for exporting Manim animations for web deployment, including React/Next.js integration.
Export Formats for Web
| Format | Use Case | File Size | Browser Support |
|---|---|---|---|
| MP4 (H.264) | General video | Medium | Universal |
| WebM (VP9) | Modern browsers | Small | Chrome, Firefox |
| GIF | Short loops | Large | Universal |
| PNG Sequence | Programmatic control | Very Large | Universal |
| SVG | Vector graphics | Tiny | Universal |
| Lottie/JSON | Interactive | Small | With library |
Transparent Background Export
Manim CLI
# Export with transparent background
manim -pql -t script.py SceneName
# Transparent + high quality
manim -pqh -t script.py SceneName --format webmIn Code
class TransparentScene(Scene):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.camera.background_color = None # Transparent
def construct(self):
circle = Circle(color=BLUE, fill_opacity=0.8)
self.play(Create(circle))Optimized Video Export
Web-Optimized MP4
# Two-pass encoding for best quality/size ratio
ffmpeg -i input.mp4 -c:v libx264 -preset slow -crf 22 \
-c:a aac -b:a 128k \
-movflags +faststart \ # Enable streaming
-vf scale=1920:1080 \
output_web.mp4WebM for Modern Browsers
ffmpeg -i input.mp4 -c:v libvpx-vp9 -crf 30 -b:v 0 \
-c:a libopus -b:a 128k \
output.webmResponsive Sizes
EXPORT_SIZES = {
"mobile": {"width": 640, "height": 360, "suffix": "_mobile"},
"tablet": {"width": 1280, "height": 720, "suffix": "_tablet"},
"desktop": {"width": 1920, "height": 1080, "suffix": "_desktop"},
"4k": {"width": 3840, "height": 2160, "suffix": "_4k"},
}
def export_all_sizes(input_file, output_base):
"""Export video in multiple sizes for responsive loading."""
for name, config in EXPORT_SIZES.items():
output = f"{output_base}{config['suffix']}.mp4"
cmd = [
"ffmpeg", "-i", input_file,
"-vf", f"scale={config['width']}:{config['height']}",
"-c:v", "libx264", "-preset", "slow", "-crf", "22",
output
]
subprocess.run(cmd)GIF Export
High-Quality GIF Pipeline
# Step 1: Generate palette
ffmpeg -i input.mp4 -vf "fps=15,scale=480:-1:flags=lanczos,palettegen" palette.png
# Step 2: Use palette for GIF
ffmpeg -i input.mp4 -i palette.png \
-filter_complex "fps=15,scale=480:-1:flags=lanczos[x];[x][1:v]paletteuse" \
output.gifPython Wrapper
def create_optimized_gif(input_video, output_gif, fps=15, width=480):
"""Create optimized GIF from video."""
palette = "palette_temp.png"
# Generate palette
subprocess.run([
"ffmpeg", "-y", "-i", input_video,
"-vf", f"fps={fps},scale={width}:-1:flags=lanczos,palettegen",
palette
])
# Create GIF
subprocess.run([
"ffmpeg", "-y", "-i", input_video, "-i", palette,
"-filter_complex",
f"fps={fps},scale={width}:-1:flags=lanczos[x];[x][1:v]paletteuse",
output_gif
])
# Cleanup
os.remove(palette)React Integration
Basic Video Component
// components/ManimAnimation.jsx
import React, { useRef, useEffect } from 'react';
export function ManimAnimation({ src, autoplay = true, loop = false }) {
const videoRef = useRef(null);
useEffect(() => {
if (autoplay && videoRef.current) {
videoRef.current.play();
}
}, [autoplay]);
return (
<video
ref={videoRef}
src={src}
loop={loop}
muted
playsInline
style={{ maxWidth: '100%', height: 'auto' }}
/>
);
}Responsive Video with Sources
// components/ResponsiveAnimation.jsx
export function ResponsiveAnimation({ basePath, alt }) {
return (
<video
autoPlay
muted
loop
playsInline
style={{ maxWidth: '100%' }}
>
<source
src={`${basePath}_desktop.webm`}
type="video/webm"
media="(min-width: 1024px)"
/>
<source
src={`${basePath}_tablet.mp4`}
type="video/mp4"
media="(min-width: 768px)"
/>
<source
src={`${basePath}_mobile.mp4`}
type="video/mp4"
/>
{alt}
</video>
);
}Scroll-Driven Animations
Scroll Progress Hook
// hooks/useScrollProgress.js
import { useState, useEffect } from 'react';
export function useScrollProgress(ref) {
const [progress, setProgress] = useState(0);
useEffect(() => {
const handleScroll = () => {
if (!ref.current) return;
const rect = ref.current.getBoundingClientRect();
const windowHeight = window.innerHeight;
// Calculate progress (0 to 1)
const start = windowHeight; // Element enters viewport
const end = -rect.height; // Element leaves viewport
const current = rect.top;
const progress = Math.max(0, Math.min(1,
(start - current) / (start - end)
));
setProgress(progress);
};
window.addEventListener('scroll', handleScroll, { passive: true });
handleScroll(); // Initial calculation
return () => window.removeEventListener('scroll', handleScroll);
}, [ref]);
return progress;
}Scroll-Synced Video
// components/ScrollVideo.jsx
import { useRef, useEffect } from 'react';
import { useScrollProgress } from '../hooks/useScrollProgress';
export function ScrollVideo({ src }) {
const containerRef = useRef(null);
const videoRef = useRef(null);
const progress = useScrollProgress(containerRef);
useEffect(() => {
if (videoRef.current && videoRef.current.duration) {
videoRef.current.currentTime = progress * videoRef.current.duration;
}
}, [progress]);
return (
<div ref={containerRef} style={{ height: '300vh' }}>
<video
ref={videoRef}
src={src}
muted
playsInline
style={{
position: 'sticky',
top: '50%',
transform: 'translateY(-50%)',
maxWidth: '100%'
}}
/>
</div>
);
}Next.js Integration
Static Asset Placement
public/
├── animations/
│ ├── intro.mp4
│ ├── intro.webm
│ ├── derivative.mp4
│ └── derivative.webmDynamic Import for Heavy Components
// pages/tutorial.jsx
import dynamic from 'next/dynamic';
const HeavyAnimation = dynamic(
() => import('../components/HeavyAnimation'),
{
loading: () => <div>Loading animation...</div>,
ssr: false // Disable server-side rendering
}
);
export default function TutorialPage() {
return (
<main>
<h1>Calculus Tutorial</h1>
<HeavyAnimation src="/animations/derivative.mp4" />
</main>
);
}Pre-rendering at Build Time
// next.config.js
module.exports = {
// Pre-render animations during build
async headers() {
return [
{
source: '/animations/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
],
},
];
},
};SVG Export for Text
Export Text as SVG
class SVGTextScene(Scene):
def construct(self):
text = Text("Mathematical Beauty")
self.add(text)
# Export to SVG (instead of video)
# Use: manim -pql --format svg script.pyAnimate SVG in Browser
// components/AnimatedSVG.jsx
import { motion } from 'framer-motion';
export function AnimatedText({ svgPath }) {
return (
<motion.svg
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 2, ease: "easeInOut" }}
>
{/* SVG paths from Manim export */}
</motion.svg>
);
}Pre-rendering Workflow
Build-Time Rendering
// scripts/prerender-animations.js
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const SCENES = [
{ file: 'scenes/intro.py', scene: 'IntroScene' },
{ file: 'scenes/main.py', scene: 'MainContent' },
{ file: 'scenes/outro.py', scene: 'OutroScene' },
];
const OUTPUT_DIR = 'public/animations';
// Ensure output directory exists
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
// Render each scene
for (const { file, scene } of SCENES) {
console.log(`Rendering ${scene}...`);
execSync(
`manim -qh --format mp4 ${file} ${scene} -o ${OUTPUT_DIR}/${scene}.mp4`,
{ stdio: 'inherit' }
);
// Also create WebM version
execSync(
`ffmpeg -i ${OUTPUT_DIR}/${scene}.mp4 -c:v libvpx-vp9 -crf 30 ${OUTPUT_DIR}/${scene}.webm`,
{ stdio: 'inherit' }
);
}
console.log('Pre-rendering complete!');Add to Build Pipeline
// package.json
{
"scripts": {
"prerender": "node scripts/prerender-animations.js",
"build": "npm run prerender && next build"
}
}Performance Optimization
Lazy Loading
// Only load video when in viewport
import { useInView } from 'react-intersection-observer';
function LazyVideo({ src }) {
const { ref, inView } = useInView({
triggerOnce: true,
rootMargin: '200px', // Start loading before visible
});
return (
<div ref={ref}>
{inView && (
<video src={src} autoPlay muted loop playsInline />
)}
</div>
);
}Poster Images
// Show poster while video loads
<video
src="/animations/intro.mp4"
poster="/animations/intro-poster.jpg"
autoPlay
muted
/>Generate Poster
# Extract first frame as poster
ffmpeg -i input.mp4 -vframes 1 -q:v 2 poster.jpgBest Practices
Do
- Use WebM for modern browsers, MP4 as fallback
- Implement lazy loading for below-fold content
- Pre-render at build time when possible
- Use appropriate quality for device
- Add loading states
Avoid
- Autoplay with sound (blocked by browsers)
- Very large files without compression
- Rendering client-side (slow)
- Missing fallbacks for older browsers