Merge pull request #5588 from SHL0MS/feat/manim-skill-deep-expansion

docs(manim-video): add 5 new reference files — design thinking, updaters, paper explainer, decorations, production quality
This commit is contained in:
SHL0MS
2026-04-06 13:58:00 -04:00
committed by GitHub
6 changed files with 1073 additions and 0 deletions

View File

@@ -234,3 +234,8 @@ Always iterate at `-ql`. Only render `-qh` for final output.
| `references/scene-planning.md` | Narrative arcs, layout templates, scene transitions, planning template |
| `references/rendering.md` | CLI reference, quality presets, ffmpeg, voiceover workflow, GIF export |
| `references/troubleshooting.md` | LaTeX errors, animation errors, common mistakes, debugging |
| `references/animation-design-thinking.md` | When to animate vs show static, decomposition, pacing, narration sync |
| `references/updaters-and-trackers.md` | ValueTracker, add_updater, always_redraw, time-based updaters, patterns |
| `references/paper-explainer.md` | Turning research papers into animations — workflow, templates, domain patterns |
| `references/decorations.md` | SurroundingRectangle, Brace, arrows, DashedLine, Angle, annotation lifecycle |
| `references/production-quality.md` | Pre-code, pre-render, post-render checklists, spatial layout, color, tempo |

View File

@@ -0,0 +1,161 @@
# Animation Design Thinking
How to decide WHAT to animate and HOW to structure it — before writing any code.
## Should I animate this?
Not everything benefits from animation. Motion adds cognitive load. Bad animation is worse than a good static diagram.
**Animate when:**
- A sequence unfolds over time (algorithm steps, derivation, pipeline stages)
- Spatial relationships change (transformation, deformation, rotation)
- Something is built from parts (construction, assembly, accumulation)
- You're comparing states (before/after, method A vs method B)
- Temporal evolution is the point (training curves, wave propagation, gradient descent)
**Show static when:**
- The concept is a single labeled diagram (circuit, anatomy, architecture overview)
- Motion would distract from spatial layout
- The viewer needs to study it carefully (dense table, reference chart)
- The concept is already intuitive from a well-labeled figure
**Rule of thumb:** If you'd explain it with "first X, then Y, then Z" — animate it. If you'd explain it by pointing at parts of one picture — show it static.
## Decomposing a concept into animation
### Step 1: Write the narration first
Before any code, write what the narrator would say. This determines:
- **Order** — what concept comes first
- **Duration** — how long each idea gets
- **Visuals** — what the viewer must SEE when they HEAR each sentence
A scene where the narration says "the gradient points uphill" must show a gradient arrow at that moment. If the visual doesn't match the audio, the viewer's brain splits attention and both tracks are lost.
### Step 2: Identify visual beats
A "beat" is a moment where something changes on screen. Mark each beat in your narration:
```
"Consider a function f of x." → [BEAT: axes + curve appear]
"At this point..." → [BEAT: dot appears on curve]
"...the slope is positive." → [BEAT: tangent line drawn]
"So the gradient tells us to go left." → [BEAT: arrow points left, dot moves]
```
Each beat is one `self.play()` call or a small group of simultaneous animations.
### Step 3: Choose the right tool per beat
| Visual need | Manim approach |
|-------------|----------------|
| Object appears for first time | `Create`, `Write`, `FadeIn`, `GrowFromCenter` |
| Object transforms into another | `Transform`, `ReplacementTransform`, `FadeTransform` |
| Attention drawn to existing object | `Indicate`, `Circumscribe`, `Flash`, `ShowPassingFlash` |
| Continuous relationship maintained | `add_updater`, `always_redraw`, `ValueTracker` |
| Object leaves the scene | `FadeOut`, `Uncreate`, `ShrinkToCenter` |
| Static context that stays visible | `self.add()` (no animation) |
## Pacing: the universal mistake is too fast
### Timing rules
| Content type | Minimum on-screen time |
|-------------|----------------------|
| New equation appearing | 2.0s animation + 2.0s pause |
| New concept label | 1.0s animation + 1.0s pause |
| Key insight ("aha moment") | 2.5s animation + 3.0s pause |
| Supporting annotation | 0.8s animation + 0.5s pause |
| Scene transition (FadeOut all) | 0.5s animation + 0.3s pause |
### Breathing room
After every reveal, add `self.wait()`. The viewer needs time to:
1. Read the new text
2. Connect it to what's already on screen
3. Form an expectation about what comes next
**No wait = the viewer is always behind you.** They're still reading the equation when you've already started transforming it.
### Tempo variation
Monotonous pacing feels like a lecture. Vary the tempo:
- **Slow build** for core concepts (long run_time, long pauses)
- **Quick succession** for supporting details (short run_time, minimal pauses)
- **Dramatic pause** before the key reveal (extra `self.wait(2.0)` before the "aha")
- **Rapid montage** for "and this applies to X, Y, Z..." sequences (`LaggedStart` with tight lag_ratio)
## Narration synchronization
### The "see then hear" principle
The visual should appear slightly BEFORE the narration describes it. When the viewer sees a circle appear and THEN hears "consider a circle," the visual primes their brain for the concept. The reverse — hearing first, seeing second — creates confusion because they're searching the screen for something that isn't there yet.
### Practical timing
```python
# Scene duration should match narration duration.
# If narration for this scene is 8 seconds:
# Total animation run_times + total self.wait() times = ~8 seconds.
# Use manim-voiceover for automatic sync:
with self.voiceover(text="The gradient points downhill") as tracker:
self.play(GrowArrow(gradient_arrow), run_time=tracker.duration)
```
## Equation decomposition strategy
### The "dim and reveal" pattern
When building a complex equation step by step:
1. Show the full equation dimmed at `opacity=0.2` (sets expectation for where you're going)
2. Highlight the first term at full opacity
3. Explain it
4. Highlight the next term, dim the first to `0.5` (it's now context)
5. Repeat until the full equation is bright
This is better than building left-to-right because the viewer always sees the destination.
### Term ordering
Animate terms in the order the viewer needs to understand them, not in the order they appear in the equation. For `E = mc²`:
- Show `E` (the thing we want to know)
- Then `m` (the input)
- Then `c²` (the constant that makes it work)
- Then the `=` (connecting them)
## Architecture and pipeline diagrams
### Box granularity
The most common mistake: too many boxes. Each box is a concept the viewer must track. Five boxes with clear labels beats twelve boxes with abbreviations.
**Rule:** If two consecutive boxes could be labeled "X" and "process X output," merge them into one box.
### Animation strategy
Build pipelines left-to-right (or top-to-bottom) with arrows connecting them:
1. First box appears alone → explain it
2. Arrow grows from first to second → "the output feeds into..."
3. Second box appears → explain it
4. Repeat
Then show data flowing through: `ShowPassingFlash` along the arrows, or a colored dot traversing the path.
### The zoom-and-return pattern
For complex systems:
1. Show the full overview (all boxes, small)
2. Zoom into one box (`MovingCameraScene.camera.frame.animate`)
3. Expand that box into its internal components
4. Zoom back out to the overview
5. Zoom into the next box
## Common design mistakes
1. **Animating everything at once.** The viewer can track 1-2 simultaneous animations. More than that and nothing registers.
2. **No visual hierarchy.** Everything at the same opacity/size/color means nothing stands out. Use opacity layering.
3. **Equations without context.** An equation appearing alone means nothing. Always show the geometric/visual interpretation first or simultaneously.
4. **Skipping the "why."** Showing HOW a transformation works without WHY it matters. Add a sentence/label explaining the purpose.
5. **Identical pacing throughout.** Every animation at run_time=1.5, every wait at 1.0. Vary it.
6. **Forgetting the audience.** A video for high schoolers needs different pacing and complexity than one for PhD students. Decide the audience in the planning phase.

View File

@@ -0,0 +1,202 @@
# Decorations and Visual Polish
Decorations are mobjects that annotate, highlight, or frame other mobjects. They turn a technically correct animation into a visually polished one.
## SurroundingRectangle
Draws a rectangle around any mobject. The go-to for highlighting:
```python
highlight = SurroundingRectangle(
equation[2], # the term to highlight
color=YELLOW,
buff=0.15, # padding between content and border
corner_radius=0.1, # rounded corners
stroke_width=2
)
self.play(Create(highlight))
self.wait(1)
self.play(FadeOut(highlight))
```
### Around part of an equation
```python
eq = MathTex(r"E", r"=", r"m", r"c^2")
box = SurroundingRectangle(eq[2:], color=YELLOW, buff=0.1) # highlight "mc²"
label = Text("mass-energy", font_size=18, font="Menlo", color=YELLOW)
label.next_to(box, DOWN, buff=0.2)
self.play(Create(box), FadeIn(label))
```
## BackgroundRectangle
Semi-transparent background behind text for readability over complex scenes:
```python
bg = BackgroundRectangle(equation, fill_opacity=0.7, buff=0.2, color=BLACK)
self.play(FadeIn(bg), Write(equation))
# Or using set_stroke for a "backdrop" effect on the text itself:
label.set_stroke(BLACK, width=5, background=True)
```
The `set_stroke(background=True)` approach is cleaner for text labels over graphs/diagrams.
## Brace and BraceLabel
Curly braces that annotate sections of a diagram or equation:
```python
brace = Brace(equation[2:4], DOWN, color=YELLOW)
brace_label = brace.get_text("these terms", font_size=20)
self.play(GrowFromCenter(brace), FadeIn(brace_label))
# Between two specific points
brace = BraceBetweenPoints(point_a, point_b, direction=UP)
```
### Brace placement
```python
# Below a group
Brace(group, DOWN)
# Above a group
Brace(group, UP)
# Left of a group
Brace(group, LEFT)
# Right of a group
Brace(group, RIGHT)
```
## Arrows for Annotation
### Straight arrows pointing to mobjects
```python
arrow = Arrow(
start=label.get_bottom(),
end=target.get_top(),
color=YELLOW,
stroke_width=2,
buff=0.1, # gap between arrow tip and target
max_tip_length_to_length_ratio=0.15 # small arrowhead
)
self.play(GrowArrow(arrow), FadeIn(label))
```
### Curved arrows
```python
arrow = CurvedArrow(
start_point=source.get_right(),
end_point=target.get_left(),
angle=PI/4, # curve angle
color=PRIMARY
)
```
### Labeling with arrows
```python
# LabeledArrow: arrow with built-in text label
arr = LabeledArrow(
Text("gradient", font_size=16, font="Menlo"),
start=point_a, end=point_b, color=RED
)
```
## DashedLine and DashedVMobject
```python
# Dashed line (for asymptotes, construction lines, implied connections)
asymptote = DashedLine(
axes.c2p(2, -3), axes.c2p(2, 3),
color=YELLOW, dash_length=0.15
)
# Make any VMobject dashed
dashed_circle = DashedVMobject(Circle(radius=2, color=BLUE), num_dashes=30)
```
## Angle and RightAngle Markers
```python
line1 = Line(ORIGIN, RIGHT * 2)
line2 = Line(ORIGIN, UP * 2 + RIGHT)
# Angle arc between two lines
angle = Angle(line1, line2, radius=0.5, color=YELLOW)
angle_value = angle.get_value() # radians
# Right angle marker (the small square)
right_angle = RightAngle(line1, Line(ORIGIN, UP * 2), length=0.3, color=WHITE)
```
## Cross (strikethrough)
Mark something as wrong or deprecated:
```python
cross = Cross(old_equation, color=RED, stroke_width=4)
self.play(Create(cross))
# Then show the correct version
```
## Underline
```python
underline = Underline(important_text, color=ACCENT, stroke_width=3)
self.play(Create(underline))
```
## Color Highlighting Workflow
### Method 1: At creation with t2c
```python
text = Text("The gradient is negative here", t2c={"gradient": BLUE, "negative": RED})
```
### Method 2: set_color_by_tex after creation
```python
eq = MathTex(r"\nabla L = -\frac{\partial L}{\partial w}")
eq.set_color_by_tex(r"\nabla", BLUE)
eq.set_color_by_tex(r"\partial", RED)
```
### Method 3: Index into submobjects
```python
eq = MathTex(r"a", r"+", r"b", r"=", r"c")
eq[0].set_color(RED) # "a"
eq[2].set_color(BLUE) # "b"
eq[4].set_color(GREEN) # "c"
```
## Combining Annotations
Layer multiple annotations for emphasis:
```python
# Highlight a term, add a brace, and an arrow — in sequence
box = SurroundingRectangle(eq[2], color=YELLOW, buff=0.1)
brace = Brace(eq[2], DOWN, color=YELLOW)
label = brace.get_text("learning rate", font_size=18)
self.play(Create(box))
self.wait(0.5)
self.play(FadeOut(box), GrowFromCenter(brace), FadeIn(label))
self.wait(1.5)
self.play(FadeOut(brace), FadeOut(label))
```
### The annotation lifecycle
Annotations should follow a rhythm:
1. **Appear** — draw attention (Create, GrowFromCenter)
2. **Hold** — viewer reads and understands (self.wait)
3. **Disappear** — clear the stage for the next thing (FadeOut)
Never leave annotations on screen indefinitely — they become visual noise once their purpose is served.

View File

@@ -0,0 +1,255 @@
# Paper Explainer Workflow
How to turn a research paper into an animated explainer video.
## Why animate a paper?
A research paper is optimized for precision and completeness. A video is optimized for understanding and retention. The translation is NOT "read the paper aloud with pictures" — it's "extract the core insight and make it feel obvious through visual storytelling."
The paper has one job: prove the claim is true. The video has a different job: make the viewer understand WHY the claim is true, and WHY it matters.
## Who is watching?
Before anything, decide the audience:
| Audience | Prerequisites | Pacing | Depth |
|----------|--------------|--------|-------|
| General public | None | Slow, many analogies | Intuition only, skip proofs |
| Undergrad students | Basic math/CS | Medium, some formalism | Key equations, skip derivations |
| Grad students / researchers | Domain knowledge | Faster, more notation | Full equations, sketch proofs |
This determines everything: vocabulary, pacing, which sections to animate, how much math to show.
## The 5-minute template
Most paper explainers fit this structure (scale times proportionally for longer videos):
| Section | Duration | Purpose |
|---------|----------|---------|
| **Hook** | 0:00-0:30 | Surprising result or provocative question |
| **Problem** | 0:30-1:30 | What was broken/missing before this paper |
| **Key insight** | 1:30-3:00 | The core idea, explained visually |
| **How it works** | 3:00-4:00 | Method/algorithm, simplified |
| **Evidence** | 4:00-4:30 | Key result that proves it works |
| **Implications** | 4:30-5:00 | Why it matters, what it enables |
### What to skip
- Related work survey → one sentence: "Previous approaches did X, which had problem Y"
- Implementation details → skip unless they're the contribution
- Ablation studies → show one chart at most
- Proofs → show the key step, not the full proof
- Hyperparameter tuning → skip entirely
### What to expand
- The core insight → this gets the most screen time
- Geometric/visual intuition → if the paper has math, show what it MEANS
- Before/after comparison → the most compelling evidence
## Pre-code workflow
### Gate 1: Narration script
Write the full narration before any code. Every sentence maps to a visual beat. If you can't write the narration, you don't understand the paper well enough to animate it.
```markdown
## Hook (30s)
"What if I told you that a model with 7 billion parameters can outperform
one with 70 billion — if you train it on the right data?"
## Problem (60s)
"The standard approach is to scale up. More parameters, more compute.
[VISUAL: bar chart showing model sizes growing exponentially]
But Chinchilla showed us that most models are undertrained..."
```
### Gate 2: Scene list
After the narration, break it into scenes. Each scene is one Manim class.
```markdown
Scene 1: Hook — surprising stat with animated counter
Scene 2: Problem — model size bar chart growing
Scene 3: Key insight — training data vs parameters, animated 2D plot
Scene 4: Method — pipeline diagram building left to right
Scene 5: Results — before/after comparison with animated bars
Scene 6: Closing — implications text
```
### Gate 3: Style constants
Before coding scenes, define the visual language:
```python
# style.py — import in every scene file
BG = "#0D1117"
PRIMARY = "#58C4DD"
SECONDARY = "#83C167"
ACCENT = "#FFFF00"
HIGHLIGHT = "#FF6B6B"
MONO = "Menlo"
# Color meanings for THIS paper
MODEL_COLOR = PRIMARY # "the model"
DATA_COLOR = SECONDARY # "training data"
BASELINE_COLOR = HIGHLIGHT # "previous approach"
RESULT_COLOR = ACCENT # "our result"
```
## First-principles equation explanation
When the paper has a key equation, don't just show it — build it from intuition:
### The "what would you do?" pattern
1. Pose the problem in plain language
2. Ask what the simplest solution would be
3. Show why it doesn't work (animate the failure)
4. Introduce the paper's solution as the fix
5. THEN show the equation — it now feels earned
```python
# Scene: Why we need attention (for a Transformer paper)
# Step 1: "How do we let each word look at every other word?"
# Step 2: Show naive approach (fully connected = O(n²) everything)
# Step 3: Show it breaks (information overload, no selectivity)
# Step 4: "What if each word could CHOOSE which words to attend to?"
# Step 5: Show attention equation — Q, K, V now mean something
```
### Equation reveal strategy
```python
# Show equation dimmed first (full destination)
eq = MathTex(r"Attention(Q,K,V) = softmax\left(\frac{QK^T}{\sqrt{d_k}}\right)V")
eq.set_opacity(0.15)
self.play(FadeIn(eq))
# Highlight Q, K, V one at a time with color + label
for part, color, label_text in [
(r"Q", PRIMARY, "Query: what am I looking for?"),
(r"K", SECONDARY, "Key: what do I contain?"),
(r"V", ACCENT, "Value: what do I output?"),
]:
eq.set_color_by_tex(part, color)
label = Text(label_text, font_size=18, color=color, font=MONO)
# position label, animate it, wait, then dim it
```
## Building architecture diagrams
### The progressive build pattern
Don't show the full architecture at once. Build it:
1. First component appears alone → explain
2. Arrow grows → "this feeds into..."
3. Second component appears → explain
4. Repeat until complete
```python
# Component factory
def make_box(label, color, width=2.0, height=0.8):
box = RoundedRectangle(corner_radius=0.1, width=width, height=height,
color=color, fill_opacity=0.1, stroke_width=1.5)
text = Text(label, font_size=18, font=MONO, color=color).move_to(box)
return Group(box, text)
encoder = make_box("Encoder", PRIMARY)
decoder = make_box("Decoder", SECONDARY).next_to(encoder, RIGHT, buff=1.5)
arrow = Arrow(encoder.get_right(), decoder.get_left(), color=DIM, stroke_width=1.5)
self.play(FadeIn(encoder))
self.wait(1) # explain encoder
self.play(GrowArrow(arrow))
self.play(FadeIn(decoder))
self.wait(1) # explain decoder
```
### Data flow animation
After building the diagram, show data moving through it:
```python
# Dot traveling along the pipeline
data_dot = Dot(color=ACCENT, radius=0.1).move_to(encoder)
self.play(FadeIn(data_dot))
self.play(MoveAlongPath(data_dot, arrow), run_time=1)
self.play(data_dot.animate.move_to(decoder), run_time=0.5)
self.play(Flash(data_dot.get_center(), color=ACCENT), run_time=0.3)
```
## Animating results
### Bar chart comparison (most common)
```python
# Before/after bars
before_data = [45, 52, 38, 61]
after_data = [78, 85, 72, 91]
labels = ["Task A", "Task B", "Task C", "Task D"]
before_chart = BarChart(before_data, bar_names=labels,
y_range=[0, 100, 20], bar_colors=[HIGHLIGHT]*4).scale(0.6).shift(LEFT*3)
after_chart = BarChart(after_data, bar_names=labels,
y_range=[0, 100, 20], bar_colors=[SECONDARY]*4).scale(0.6).shift(RIGHT*3)
before_label = Text("Baseline", font_size=20, color=HIGHLIGHT, font=MONO)
after_label = Text("Ours", font_size=20, color=SECONDARY, font=MONO)
# Reveal baseline first, then ours (dramatic comparison)
self.play(Create(before_chart), FadeIn(before_label))
self.wait(1.5)
self.play(Create(after_chart), FadeIn(after_label))
self.wait(0.5)
# Highlight the improvement
improvement = Text("+35% avg", font_size=24, color=ACCENT, font=MONO)
self.play(FadeIn(improvement))
```
### Training curve (for ML papers)
```python
tracker = ValueTracker(0)
curve = always_redraw(lambda: axes.plot(
lambda x: 1 - 0.8 * np.exp(-x / 3),
x_range=[0, tracker.get_value()], color=PRIMARY
))
epoch_label = always_redraw(lambda: Text(
f"Epoch {int(tracker.get_value())}", font_size=18, font=MONO
).to_corner(UR))
self.add(curve, epoch_label)
self.play(tracker.animate.set_value(10), run_time=5, rate_func=linear)
```
## Domain-specific patterns
### ML papers
- Show data flow through the model (animated pipeline)
- Training curves with `ValueTracker`
- Attention heatmaps as colored grids
- Embedding space as 2D scatter (PCA/t-SNE visualization)
- Loss landscape as 3D surface with gradient descent dot
### Physics/math papers
- Use `LinearTransformationScene` for linear algebra
- Vector fields with `ArrowVectorField` / `StreamLines`
- Phase spaces with `NumberPlane` + trajectories
- Wave equations with time-parameterized plots
### Systems/architecture papers
- Pipeline diagrams built progressively
- `ShowPassingFlash` for data flow along arrows
- `ZoomedScene` for zooming into components
- Before/after latency/throughput comparisons
## Common mistakes
1. **Trying to cover the whole paper.** A 5-minute video can explain ONE core insight well. Covering everything means explaining nothing.
2. **Reading the abstract as narration.** Academic writing is designed for readers, not listeners. Rewrite in conversational language.
3. **Showing notation without meaning.** Never show a symbol without first showing what it represents visually.
4. **Skipping the motivation.** Jumping straight to "here's our method" without showing why the problem matters. The Problem section is what makes the viewer care.
5. **Identical pacing throughout.** The hook and key insight need the most visual energy. The method section can be faster. Evidence should land with impact (pause after showing the big number).

View File

@@ -0,0 +1,190 @@
# Production Quality Checklist
Standards and checks for ensuring animation output is publication-ready.
## Pre-Code Checklist
Before writing any Manim code:
- [ ] Narration script written with visual beats marked
- [ ] Scene list with purpose, duration, and layout for each
- [ ] Color palette defined with meaning assignments (`PRIMARY` = main concept, etc.)
- [ ] `MONO = "Menlo"` set as the font constant
- [ ] Target resolution and aspect ratio decided
## Text Quality
### Overlap prevention
```python
# RULE: buff >= 0.5 for edge text
label.to_edge(DOWN, buff=0.5) # GOOD
label.to_edge(DOWN, buff=0.3) # BAD — may clip
# RULE: FadeOut previous before adding new at same position
self.play(ReplacementTransform(note1, note2)) # GOOD
self.play(Write(note2)) # BAD — overlaps note1
# RULE: Reduce font size for dense scenes
# When > 4 text elements visible, use font_size=20 not 28
```
### Width enforcement
Long text strings overflow the frame:
```python
# RULE: Set max width for any text that might be long
text = Text("This is a potentially long description", font_size=22, font=MONO)
if text.width > config.frame_width - 1.0:
text.set_width(config.frame_width - 1.0)
```
### Font consistency
```python
# RULE: Define MONO once, use everywhere
MONO = "Menlo"
# WRONG: mixing fonts
Text("Title", font="Helvetica")
Text("Label", font="Arial")
Text("Code", font="Courier")
# RIGHT: one font
Text("Title", font=MONO, weight=BOLD, font_size=48)
Text("Label", font=MONO, font_size=20)
Text("Code", font=MONO, font_size=18)
```
## Spatial Layout
### The coordinate budget
The visible frame is approximately 14.2 wide × 8.0 tall (default 16:9). With mandatory margins:
```
Usable area: x ∈ [-6.5, 6.5], y ∈ [-3.5, 3.5]
Top title zone: y ∈ [2.5, 3.5]
Bottom note zone: y ∈ [-3.5, -2.5]
Main content: y ∈ [-2.5, 2.5], x ∈ [-6.0, 6.0]
```
### Fill the frame
Empty scenes look unfinished. If the main content is small, add context:
- A dimmed grid/axes behind the content
- A title/subtitle at the top
- A source citation at the bottom
- Decorative geometry at low opacity
### Maximum simultaneous elements
**Hard limit: 6 actively visible elements.** Beyond that, the viewer can't track everything. If you need more:
- Dim old elements to opacity 0.3
- Remove elements that have served their purpose
- Split into two scenes
## Animation Quality
### Variety audit
Check that no two consecutive scenes use the exact same:
- Animation type (if Scene 3 uses Write for everything, Scene 4 should use FadeIn or Create)
- Color emphasis (rotate through palette colors)
- Layout (center, left-right, grid — alternate)
- Pacing (if Scene 2 was slow and deliberate, Scene 3 can be faster)
### Tempo curve
A good video follows a tempo curve:
```
Slow ──→ Medium ──→ FAST (climax) ──→ Slow (conclusion)
Scene 1: Slow (introduction, setup)
Scene 2: Medium (building understanding)
Scene 3: Medium-Fast (core content, lots of animation)
Scene 4: FAST (montage of applications/results)
Scene 5: Slow (conclusion, key takeaway)
```
### Transition quality
Between scenes:
- **Clean exit**: `self.play(FadeOut(Group(*self.mobjects)), run_time=0.5)`
- **Brief pause**: `self.wait(0.3)` after fadeout, before next scene's first animation
- **Never hard-cut**: always animate the transition
## Color Quality
### Dimming on dark backgrounds
Colors that look vibrant on white look muddy on dark backgrounds (#0D1117, #1C1C1C). Test your palette:
```python
# Colors that work well on dark backgrounds:
# Bright and saturated: #58C4DD, #83C167, #FFFF00, #FF6B6B
# Colors that DON'T work: #666666 (invisible), #2244AA (too dark)
# RULE: Structural elements (axes, grids) at opacity 0.15
# Context elements at 0.3-0.4
# Primary elements at 1.0
```
### Color meaning consistency
Once a color is assigned a meaning, it keeps that meaning for the entire video:
```python
# If PRIMARY (#58C4DD) means "the model" in Scene 1,
# it means "the model" in every scene.
# Never reuse PRIMARY for a different concept later.
```
## Data Visualization Quality
### Minimum requirements for charts
- Axis labels on every axis
- Y-axis range starts at 0 (or has a clear break indicator)
- Bar/line colors match the legend
- Numbers on notable data points (at least the maximum and the comparison point)
### Animated counters
When showing a number changing:
```python
# GOOD: DecimalNumber with smooth animation
counter = DecimalNumber(0, font_size=48, num_decimal_places=0, font="Menlo")
self.play(counter.animate.set_value(1000), run_time=3, rate_func=rush_from)
# BAD: Text that jumps between values
```
## Pre-Render Checklist
Before running `manim -qh`:
- [ ] All scenes render without errors at `-ql`
- [ ] Preview stills at `-qm` for text-heavy scenes (check kerning)
- [ ] Background color set in every scene (`self.camera.background_color = BG`)
- [ ] `add_subcaption()` or `subcaption=` on every significant animation
- [ ] No text smaller than font_size=18
- [ ] No text using proportional fonts (use monospace)
- [ ] buff >= 0.5 on all `.to_edge()` calls
- [ ] Clean exit (FadeOut all) at end of every scene
- [ ] `self.wait()` after every reveal
- [ ] Color constants used (no hardcoded hex strings in scene code)
- [ ] All scenes use the same quality flag (don't mix `-ql` and `-qh`)
## Post-Render Checklist
After stitching the final video:
- [ ] Watch the complete video at 1x speed — does it feel rushed anywhere?
- [ ] Is there a moment where two things animate simultaneously and it's confusing?
- [ ] Does every text label have enough time to be read?
- [ ] Are transitions between scenes smooth (no black frames, no jarring cuts)?
- [ ] Is the audio in sync with the visuals (if using voiceover)?
- [ ] Is the Gibbs-like "first impression" good? The first 5 seconds determine if someone keeps watching

View File

@@ -0,0 +1,260 @@
# Updaters and Value Trackers
## The problem updaters solve
Normal animations are discrete: `self.play()` goes from state A to state B. But what if you need continuous relationships — a label that always hovers above a moving dot, or a line that always connects two points?
Without updaters, you'd manually reposition every dependent object before every `self.play()`. Five animations that move a dot means five manual repositioning calls for the label. Miss one and it freezes in the wrong spot.
Updaters let you declare a relationship ONCE. Manim calls the updater function EVERY FRAME (15-60 fps depending on quality) to enforce that relationship, no matter what else is happening.
## ValueTracker: an invisible steering wheel
A ValueTracker is an invisible Mobject that holds a single float. It never appears on screen. It exists so you can ANIMATE it while other objects REACT to its value.
Think of it as a slider: drag the slider from 0 to 5, and every object wired to it responds in real time.
```python
tracker = ValueTracker(0) # invisible, stores 0.0
tracker.get_value() # read: 0.0
tracker.set_value(5) # write: jump to 5.0 instantly
tracker.animate.set_value(5) # animate: smoothly interpolate to 5.0
```
### The three-step pattern
Every ValueTracker usage follows this:
1. **Create the tracker** (the invisible slider)
2. **Create visible objects that READ the tracker** via updaters
3. **Animate the tracker** — all dependents update automatically
```python
# Step 1: Create tracker
x_tracker = ValueTracker(1)
# Step 2: Create dependent objects
dot = always_redraw(lambda: Dot(axes.c2p(x_tracker.get_value(), 0), color=YELLOW))
v_line = always_redraw(lambda: axes.get_vertical_line(
axes.c2p(x_tracker.get_value(), func(x_tracker.get_value())), color=BLUE
))
label = always_redraw(lambda: DecimalNumber(x_tracker.get_value(), font_size=24)
.next_to(dot, UP))
self.add(dot, v_line, label)
# Step 3: Animate the tracker — everything follows
self.play(x_tracker.animate.set_value(5), run_time=3)
```
## Types of updaters
### Lambda updater (most common)
Runs a function every frame, passing the mobject itself:
```python
# Label always stays above the dot
label.add_updater(lambda m: m.next_to(dot, UP, buff=0.2))
# Line always connects two points
line.add_updater(lambda m: m.put_start_and_end_on(
point_a.get_center(), point_b.get_center()
))
```
### Time-based updater (with dt)
The second argument `dt` is the time since the last frame (~0.017s at 60fps):
```python
# Continuous rotation
square.add_updater(lambda m, dt: m.rotate(0.5 * dt))
# Continuous rightward drift
dot.add_updater(lambda m, dt: m.shift(RIGHT * 0.3 * dt))
# Oscillation
dot.add_updater(lambda m, dt: m.move_to(
axes.c2p(m.get_center()[0], np.sin(self.time))
))
```
Use `dt` updaters for physics simulations, continuous motion, and time-dependent effects.
### always_redraw: full rebuild every frame
Creates a new mobject from scratch each frame. More expensive than `add_updater` but handles cases where the mobject's structure changes (not just position/color):
```python
# Brace that follows a resizing square
brace = always_redraw(Brace, square, UP)
# Area under curve that updates as function changes
area = always_redraw(lambda: axes.get_area(
graph, x_range=[0, x_tracker.get_value()], color=BLUE, opacity=0.3
))
# Label that reconstructs its text
counter = always_redraw(lambda: Text(
f"n = {int(x_tracker.get_value())}", font_size=24, font="Menlo"
).to_corner(UR))
```
**When to use which:**
- `add_updater` — position, color, opacity changes (cheap, preferred)
- `always_redraw` — when the shape/structure itself changes (expensive, use sparingly)
## DecimalNumber: showing live values
```python
# Counter that tracks a ValueTracker
tracker = ValueTracker(0)
number = DecimalNumber(0, font_size=48, num_decimal_places=1, color=PRIMARY)
number.add_updater(lambda m: m.set_value(tracker.get_value()))
number.add_updater(lambda m: m.next_to(dot, RIGHT, buff=0.3))
self.add(number)
self.play(tracker.animate.set_value(100), run_time=3)
```
### Variable: the labeled version
```python
var = Variable(0, Text("x", font_size=24, font="Menlo"), num_decimal_places=2)
self.add(var)
self.play(var.tracker.animate.set_value(PI), run_time=2)
# Displays: x = 3.14
```
## Removing updaters
```python
# Remove all updaters
mobject.clear_updaters()
# Suspend temporarily (during an animation that would fight the updater)
mobject.suspend_updating()
self.play(mobject.animate.shift(RIGHT))
mobject.resume_updating()
# Remove specific updater (if you stored a reference)
def my_updater(m):
m.next_to(dot, UP)
label.add_updater(my_updater)
# ... later ...
label.remove_updater(my_updater)
```
## Animation-based updaters
### UpdateFromFunc / UpdateFromAlphaFunc
These are ANIMATIONS (passed to `self.play`), not persistent updaters:
```python
# Call a function on each frame of the animation
self.play(UpdateFromFunc(mobject, lambda m: m.next_to(moving_target, UP)), run_time=3)
# With alpha (0 to 1) — useful for custom interpolation
self.play(UpdateFromAlphaFunc(circle, lambda m, a: m.set_fill(opacity=a)), run_time=2)
```
### turn_animation_into_updater
Convert a one-shot animation into a continuous updater:
```python
from manim import turn_animation_into_updater
# This would normally play once — now it loops forever
turn_animation_into_updater(Rotating(gear, rate=PI/4))
self.add(gear)
self.wait(5) # gear rotates for 5 seconds
```
## Practical patterns
### Pattern 1: Dot tracing a function
```python
tracker = ValueTracker(0)
graph = axes.plot(np.sin, x_range=[0, 2*PI], color=PRIMARY)
dot = always_redraw(lambda: Dot(
axes.c2p(tracker.get_value(), np.sin(tracker.get_value())),
color=YELLOW
))
tangent = always_redraw(lambda: axes.get_secant_slope_group(
x=tracker.get_value(), graph=graph, dx=0.01,
secant_line_color=HIGHLIGHT, secant_line_length=3
))
self.add(graph, dot, tangent)
self.play(tracker.animate.set_value(2*PI), run_time=6, rate_func=linear)
```
### Pattern 2: Live area under curve
```python
tracker = ValueTracker(0.5)
area = always_redraw(lambda: axes.get_area(
graph, x_range=[0, tracker.get_value()],
color=PRIMARY, opacity=0.3
))
area_label = always_redraw(lambda: DecimalNumber(
# Numerical integration
sum(func(x) * 0.01 for x in np.arange(0, tracker.get_value(), 0.01)),
font_size=24
).next_to(axes, RIGHT))
self.add(area, area_label)
self.play(tracker.animate.set_value(4), run_time=5)
```
### Pattern 3: Connected diagram
```python
# Nodes that can be moved, with edges that auto-follow
node_a = Dot(LEFT * 2, color=PRIMARY)
node_b = Dot(RIGHT * 2, color=SECONDARY)
edge = Line().add_updater(lambda m: m.put_start_and_end_on(
node_a.get_center(), node_b.get_center()
))
label = Text("edge", font_size=18, font="Menlo").add_updater(
lambda m: m.move_to(edge.get_center() + UP * 0.3)
)
self.add(node_a, node_b, edge, label)
self.play(node_a.animate.shift(UP * 2), run_time=2)
self.play(node_b.animate.shift(DOWN + RIGHT), run_time=2)
# Edge and label follow automatically
```
### Pattern 4: Parameter exploration
```python
# Explore how a parameter changes a curve
a_tracker = ValueTracker(1)
curve = always_redraw(lambda: axes.plot(
lambda x: a_tracker.get_value() * np.sin(x),
x_range=[0, 2*PI], color=PRIMARY
))
param_label = always_redraw(lambda: Text(
f"a = {a_tracker.get_value():.1f}", font_size=24, font="Menlo"
).to_corner(UR))
self.add(curve, param_label)
self.play(a_tracker.animate.set_value(3), run_time=3)
self.play(a_tracker.animate.set_value(0.5), run_time=2)
self.play(a_tracker.animate.set_value(1), run_time=1)
```
## Common mistakes
1. **Updater fights animation:** If a mobject has an updater that sets its position, and you try to animate it elsewhere, the updater wins every frame. Suspend updating first.
2. **always_redraw for simple moves:** If you only need to reposition, use `add_updater`. `always_redraw` reconstructs the entire mobject every frame — expensive and unnecessary for position tracking.
3. **Forgetting to add to scene:** Updaters only run on mobjects that are in the scene. `always_redraw` creates the mobject but you still need `self.add()`.
4. **Updater creates new mobjects without cleanup:** If your updater creates Text objects every frame, they accumulate. Use `always_redraw` (which handles cleanup) or update properties in-place.