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:
@@ -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 |
|
||||
|
||||
@@ -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.
|
||||
202
skills/creative/manim-video/references/decorations.md
Normal file
202
skills/creative/manim-video/references/decorations.md
Normal 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.
|
||||
255
skills/creative/manim-video/references/paper-explainer.md
Normal file
255
skills/creative/manim-video/references/paper-explainer.md
Normal 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).
|
||||
190
skills/creative/manim-video/references/production-quality.md
Normal file
190
skills/creative/manim-video/references/production-quality.md
Normal 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
|
||||
260
skills/creative/manim-video/references/updaters-and-trackers.md
Normal file
260
skills/creative/manim-video/references/updaters-and-trackers.md
Normal 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.
|
||||
Reference in New Issue
Block a user