diff --git a/skills/creative/manim-video/SKILL.md b/skills/creative/manim-video/SKILL.md index 15bc3d3860..5c82526fc9 100644 --- a/skills/creative/manim-video/SKILL.md +++ b/skills/creative/manim-video/SKILL.md @@ -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 | diff --git a/skills/creative/manim-video/references/animation-design-thinking.md b/skills/creative/manim-video/references/animation-design-thinking.md new file mode 100644 index 0000000000..2ef3739aa0 --- /dev/null +++ b/skills/creative/manim-video/references/animation-design-thinking.md @@ -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. diff --git a/skills/creative/manim-video/references/decorations.md b/skills/creative/manim-video/references/decorations.md new file mode 100644 index 0000000000..4c89fe7d83 --- /dev/null +++ b/skills/creative/manim-video/references/decorations.md @@ -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. diff --git a/skills/creative/manim-video/references/paper-explainer.md b/skills/creative/manim-video/references/paper-explainer.md new file mode 100644 index 0000000000..9088ffcae3 --- /dev/null +++ b/skills/creative/manim-video/references/paper-explainer.md @@ -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). diff --git a/skills/creative/manim-video/references/production-quality.md b/skills/creative/manim-video/references/production-quality.md new file mode 100644 index 0000000000..1b371f89b0 --- /dev/null +++ b/skills/creative/manim-video/references/production-quality.md @@ -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 diff --git a/skills/creative/manim-video/references/updaters-and-trackers.md b/skills/creative/manim-video/references/updaters-and-trackers.md new file mode 100644 index 0000000000..ae39463966 --- /dev/null +++ b/skills/creative/manim-video/references/updaters-and-trackers.md @@ -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.