Initial release — OpenMontage: the first open-source agentic video production system

11 production pipelines, 47 tools, 124 agent skills.
Supports cloud APIs (fal.ai, OpenAI, ElevenLabs, Suno, HeyGen, Runway) and
free local providers (diffusers, Piper TTS, WAN 2.1, Hunyuan, CogVideo).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
calesthio
2026-03-29 08:25:17 -07:00
commit a3e735cc7a
1147 changed files with 240221 additions and 0 deletions
@@ -0,0 +1,37 @@
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)
Copyright (c) 2025 3Blue1Brown (Grant Sanderson) - Original video code
Copyright (c) 2026 Adithya S Kolavi - Adapted examples and documentation
This work is adapted from and inspired by the 3Blue1Brown video repository:
https://github.com/3b1b/videos
You are free to:
- Share: copy and redistribute the material in any medium or format
- Adapt: remix, transform, and build upon the material
Under the following terms:
- Attribution: You must give appropriate credit to both 3Blue1Brown and the
adapter, provide a link to the license, and indicate if changes were made.
- NonCommercial: You may not use the material for commercial purposes.
- ShareAlike: If you remix, transform, or build upon the material, you must
distribute your contributions under the same license as the original.
No additional restrictions: You may not apply legal terms or technological
measures that legally restrict others from doing anything the license permits.
Full license text: https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode
---
ATTRIBUTION NOTICE:
The example code in this skill is adapted from 3Blue1Brown's video code
repository (https://github.com/3b1b/videos), which is licensed under
CC BY-NC-SA 4.0.
Original author: Grant Sanderson (3Blue1Brown)
Adapted by: Adithya S Kolavi
The reference documentation and skill structure are original work but
describe techniques from the adapted code.
@@ -0,0 +1,215 @@
---
name: manimgl-best-practices
description: |
Trigger when: (1) User mentions "manimgl" or "ManimGL" or "3b1b manim", (2) Code contains `from manimlib import *`, (3) User runs `manimgl` CLI commands, (4) Working with InteractiveScene, self.frame, self.embed(), ShowCreation(), or ManimGL-specific patterns.
Best practices for ManimGL (Grant Sanderson's 3Blue1Brown version) - OpenGL-based animation engine with interactive development. Covers InteractiveScene, Tex with t2c, camera frame control, interactive mode (-se flag), 3D rendering, and checkpoint_paste() workflow.
NOT for Manim Community Edition (which uses `manim` imports and `manim` CLI).
---
## How to use
Read individual rule files for detailed explanations and code examples:
### Core Concepts
- [rules/scenes.md](rules/scenes.md) - InteractiveScene, Scene types, and construct method
- [rules/mobjects.md](rules/mobjects.md) - Mobject types, VMobject, Groups, and positioning
- [rules/animations.md](rules/animations.md) - Animation classes, playing animations, and timing
### Creation & Transformation
- [rules/creation-animations.md](rules/creation-animations.md) - ShowCreation, Write, FadeIn, DrawBorderThenFill
- [rules/transform-animations.md](rules/transform-animations.md) - Transform, ReplacementTransform, TransformMatchingTex
- [rules/animation-groups.md](rules/animation-groups.md) - LaggedStart, Succession, AnimationGroup
### Text & Math
- [rules/tex.md](rules/tex.md) - Tex class, raw strings R"...", and LaTeX rendering
- [rules/text.md](rules/text.md) - Text mobjects, fonts, and styling
- [rules/t2c.md](rules/t2c.md) - tex_to_color_map (t2c) for coloring math expressions
### Styling & Appearance
- [rules/colors.md](rules/colors.md) - Color constants, gradients, RGB, hex, GLSL coloring
- [rules/styling.md](rules/styling.md) - Fill, stroke, opacity, backstroke, gloss, shadow
### 3D & Camera
- [rules/3d.md](rules/3d.md) - 3D objects, surfaces, Sphere, Torus, parametric surfaces, lighting
- [rules/camera.md](rules/camera.md) - frame.reorient(), Euler angles, fix_in_frame(), camera animations
### Interactive Development
- [rules/interactive.md](rules/interactive.md) - Interactive mode with `-se` flag, checkpoint_paste()
- [rules/frame.md](rules/frame.md) - self.frame, camera control, reorient, and zooming
- [rules/embedding.md](rules/embedding.md) - self.embed() for IPython debugging, touch() mode
### Configuration & CLI
- [rules/cli.md](rules/cli.md) - manimgl command, flags (-w, -o, -se, -l, -h), rendering options
- [rules/config.md](rules/config.md) - custom_config.yml, directories, camera settings, quality presets
## Working Examples
Complete, tested example files demonstrating common patterns:
- [examples/basic_animations.py](examples/basic_animations.py) - Basic shapes, text, and animations
- [examples/math_visualization.py](examples/math_visualization.py) - LaTeX equations and mathematical content
- [examples/graph_plotting.py](examples/graph_plotting.py) - Axes, functions, and graphing
- [examples/3d_visualization.py](examples/3d_visualization.py) - 3D scenes with camera control and surfaces
- [examples/updater_patterns.py](examples/updater_patterns.py) - Dynamic animations with updaters
## Scene Templates
Copy and modify these templates to start new projects:
- [templates/basic_scene.py](templates/basic_scene.py) - Standard 2D scene template
- [templates/interactive_scene.py](templates/interactive_scene.py) - InteractiveScene with self.embed()
- [templates/3d_scene.py](templates/3d_scene.py) - 3D scene with frame.reorient()
- [templates/math_scene.py](templates/math_scene.py) - Mathematical derivations and equations
## Quick Reference
### Basic Scene Structure
```python
from manimlib import *
class MyScene(InteractiveScene):
def construct(self):
# Create mobjects
circle = Circle()
# Add to scene (static)
self.add(circle)
# Or animate
self.play(ShowCreation(circle)) # Note: ShowCreation, not Create
# Wait
self.wait(1)
```
### Render Command
```bash
# Render and preview
manimgl scene.py MyScene
# Interactive mode - drop into shell at line 15
manimgl scene.py MyScene -se 15
# Write to file
manimgl scene.py MyScene -w
# Low quality for testing
manimgl scene.py MyScene -l
```
### Key Differences from ManimCE
| Feature | ManimGL (3b1b) | Manim Community |
|---------|----------------|-----------------|
| Import | `from manimlib import *` | `from manim import *` |
| CLI | `manimgl` | `manim` |
| Math text | `Tex(R"\pi")` | `MathTex(r"\pi")` |
| Scene | `InteractiveScene` | `Scene` |
| Create anim | `ShowCreation` | `Create` |
| Camera | `self.frame` | `self.camera.frame` |
| Fix in frame | `mob.fix_in_frame()` | `self.add_fixed_in_frame_mobjects(mob)` |
| Package | `manimgl` (PyPI) | `manim` (PyPI) |
### Interactive Development Workflow
ManimGL's killer feature is interactive development:
```bash
# Start at line 20 with state preserved
manimgl scene.py MyScene -se 20
```
In interactive mode:
```python
# Copy code to clipboard, then run:
checkpoint_paste() # Run with animations
checkpoint_paste(skip=True) # Run instantly (no animations)
checkpoint_paste(record=True) # Record while running
```
### Camera Control (self.frame)
```python
# Get the camera frame
frame = self.frame
# Reorient in 3D (phi, theta, gamma, center, height)
frame.reorient(45, -30, 0, ORIGIN, 8)
# Animate camera movement
self.play(frame.animate.reorient(60, -45, 0))
# Fix mobjects to stay in screen space during 3D movement
title.fix_in_frame()
```
### LaTeX with Tex class
```python
# Use raw strings with capital R
formula = Tex(R"\int_0^1 x^2 \, dx = \frac{1}{3}")
# Color mapping with t2c
equation = Tex(
R"E = mc^2",
t2c={"E": BLUE, "m": GREEN, "c": YELLOW}
)
# Isolate substrings for animation
formula = Tex(R"\sum_{n=1}^{\infty} \frac{1}{n^2} = \frac{\pi^2}{6}")
formula.set_color_by_tex("n", BLUE)
```
### Common Patterns
#### Embedding for debugging
```python
def construct(self):
circle = Circle()
self.play(ShowCreation(circle))
self.embed() # Drops into IPython shell here
```
#### Set floor plane for 3D
```python
self.set_floor_plane("xz") # Makes xy the viewing plane
```
#### Backstroke for text readability
```python
text = Text("Label")
text.set_backstroke(BLACK, 5) # Black outline behind text
```
### Installation
```bash
# Install ManimGL
pip install manimgl
# Check installation
manimgl --version
```
### Common Pitfalls to Avoid
1. **Version confusion** - Ensure you're using `manimgl`, not `manim` (community version)
2. **ShowCreation vs Create** - ManimGL uses `ShowCreation`, not `Create`
3. **Tex vs MathTex** - ManimGL uses `Tex` with capital R raw strings
4. **self.frame vs self.camera.frame** - ManimGL uses `self.frame` directly
5. **fix_in_frame()** - Call on the mobject, not the scene
6. **Interactive mode** - Use `-se` flag for interactive development
## License & Attribution
This skill contains example code adapted from [3Blue1Brown's video repository](https://github.com/3b1b/videos) by Grant Sanderson.
**License:** [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/)
- **Attribution required** - Credit both 3Blue1Brown and the adapter
- **NonCommercial** - Not for commercial use
- **ShareAlike** - Derivatives must use the same license
See [LICENSE.txt](LICENSE.txt) for full details.
@@ -0,0 +1,262 @@
"""
Attention Arcs Animation - Simple attention flow visualization
Shows how attention connects different positions with animated arcs.
Based on 3Blue1Brown's transformer visualizations.
Run: manimgl attention_arcs_animation.py AttentionArcsAnimation -o
"""
from manimlib import *
import numpy as np
import random
def random_bright_color(hue_range=(0.0, 1.0)):
"""Generate a random bright color within a hue range."""
hue = random.uniform(*hue_range)
return Color(hsl=(hue, 0.7, 0.6))
def value_to_color(
value,
low_positive_color=BLUE_E,
high_positive_color=BLUE_B,
low_negative_color=RED_E,
high_negative_color=RED_B,
min_value=0.0,
max_value=10.0
):
"""Map a value to a color based on its sign and magnitude."""
alpha = np.clip(float((abs(value) - min_value) / (max_value - min_value)), 0, 1)
if value >= 0:
return interpolate_color(low_positive_color, high_positive_color, alpha)
else:
return interpolate_color(low_negative_color, high_negative_color, alpha)
class SimpleEmbedding(VGroup):
"""A simple numeric embedding visualization."""
def __init__(self, length=7, height=2.0, **kwargs):
super().__init__(**kwargs)
# Create rectangles for entries
entries = VGroup()
for i in range(length):
value = random.uniform(-9.9, 9.9)
rect = Rectangle(width=0.3, height=height / length * 0.8)
color = value_to_color(value)
rect.set_fill(color, opacity=0.8)
rect.set_stroke(WHITE, 1)
entries.add(rect)
entries.arrange(DOWN, buff=0.05)
entries.set_height(height)
# Add brackets
lb = Tex(r"\left[", font_size=72)
rb = Tex(r"\right]", font_size=72)
lb.stretch_to_fit_height(height * 1.1)
rb.stretch_to_fit_height(height * 1.1)
lb.next_to(entries, LEFT, buff=0.05)
rb.next_to(entries, RIGHT, buff=0.05)
self.add(lb, entries, rb)
self.entries = entries
self.brackets = VGroup(lb, rb)
class AttentionArcsAnimation(Scene):
"""
Demonstrates attention mechanism through animated arcs connecting positions.
This visualization shows how each position attends to other positions,
with arc colors and widths representing attention weights.
"""
def construct(self):
# Create a row of embeddings
n_embeddings = 6
embeddings = VGroup(*(
SimpleEmbedding(length=8, height=3.0)
for _ in range(n_embeddings)
))
embeddings.arrange(RIGHT, buff=0.8)
embeddings.set_width(FRAME_WIDTH - 2)
embeddings.to_edge(DOWN, buff=1.5)
# Add position labels
labels = VGroup(*(
Text(f"Pos {i}", font_size=24)
for i in range(n_embeddings)
))
for label, emb in zip(labels, embeddings):
label.next_to(emb, DOWN, buff=0.2)
# Title
title = Text("Attention: How positions communicate", font_size=48)
title.to_edge(UP)
# Show initial setup
self.play(
Write(title),
LaggedStartMap(FadeIn, embeddings, shift=0.5 * UP, lag_ratio=0.1),
run_time=2
)
self.play(LaggedStartMap(FadeIn, labels, shift=0.2 * DOWN, lag_ratio=0.1))
self.wait()
# Create attention arcs for each position
self.play_attention_animation(embeddings, run_time=4)
self.wait()
# Show focused attention on one position
focus_label = Text("Each position gathers context from others", font_size=36)
focus_label.next_to(title, DOWN, buff=0.5)
self.play(FadeIn(focus_label, shift=DOWN))
self.play_focused_attention(embeddings, focus_index=3, run_time=3)
self.wait()
# Cleanup
self.play(
FadeOut(focus_label),
FadeOut(title),
FadeOut(labels),
FadeOut(embeddings),
)
def play_attention_animation(self, embeddings, run_time=5):
"""Play attention arcs between all positions."""
arc_groups = VGroup()
for _ in range(2): # Multiple rounds
for n, e1 in enumerate(embeddings):
arc_group = VGroup()
for e2 in embeddings[n + 1:]:
sign = (-1) ** int(e2.get_x() > e1.get_x())
arc = Line(
e1.get_top(), e2.get_top(),
path_arc=sign * PI / 3,
)
arc.set_stroke(
color=random_bright_color(hue_range=(0.1, 0.3)),
width=5 * random.random() ** 3,
)
arc_group.add(arc)
arc_group.shuffle()
if len(arc_group) > 0:
arc_groups.add(arc_group)
self.play(
LaggedStart(*(
AnimationGroup(
LaggedStartMap(VShowPassingFlash, arc_group.copy(), time_width=2, lag_ratio=0.15),
LaggedStartMap(ShowCreationThenFadeOut, arc_group, lag_ratio=0.15),
)
for arc_group in arc_groups
), lag_ratio=0.0),
run_time=run_time
)
def play_focused_attention(self, embeddings, focus_index=3, run_time=3):
"""Show attention arcs focused on one position."""
target = embeddings[focus_index]
# Highlight target
rect = SurroundingRectangle(target, buff=0.1)
rect.set_stroke(YELLOW, 3)
arcs = VGroup()
for i, emb in enumerate(embeddings):
if i == focus_index:
continue
sign = 1 if i < focus_index else -1
arc = Line(
emb.get_top(), target.get_top(),
path_arc=sign * PI / 3,
)
weight = random.random() ** 2
arc.set_stroke(
color=interpolate_color(BLUE_E, YELLOW, weight),
width=2 + 4 * weight,
)
arcs.add(arc)
self.play(ShowCreation(rect))
self.play(
LaggedStart(*(
ShowCreationThenFadeOut(arc, run_time=1.5)
for arc in arcs
), lag_ratio=0.2),
run_time=run_time
)
self.play(FadeOut(rect))
class AttentionArcs3D(Scene):
"""
3D version of attention arcs with camera movement.
"""
def construct(self):
frame = self.camera.frame
# Create 3D embeddings as colored columns
n_embeddings = 5
columns = Group()
for i in range(n_embeddings):
column = Group()
for j in range(8):
box = Cube(side_length=0.3)
box.set_color(value_to_color(random.uniform(-10, 10)))
box.set_opacity(0.8)
column.add(box)
column.arrange(OUT, buff=0.05)
columns.add(column)
columns.arrange(RIGHT, buff=1.0)
columns.center()
# Set up 3D camera
frame.set_euler_angles(phi=60 * DEGREES, theta=-30 * DEGREES)
self.add(columns)
# Create arcs in 3D
arcs = VGroup()
for i, c1 in enumerate(columns):
for c2 in columns[i + 1:]:
start = c1.get_top() + 0.2 * UP
end = c2.get_top() + 0.2 * UP
mid = (start + end) / 2 + UP
arc = VMobject()
arc.set_points_smoothly([start, mid, end])
arc.set_stroke(
random_bright_color(hue_range=(0.1, 0.4)),
width=2 + 3 * random.random()
)
arcs.add(arc)
# Animate
self.play(
frame.animate.set_euler_angles(phi=70 * DEGREES, theta=-45 * DEGREES),
run_time=2
)
self.play(
LaggedStartMap(ShowCreation, arcs, lag_ratio=0.1),
run_time=3
)
self.play(
frame.animate.increment_theta(60 * DEGREES),
LaggedStartMap(VShowPassingFlash, arcs, time_width=1.5, lag_ratio=0.05),
run_time=4
)
self.play(
FadeOut(arcs),
FadeOut(columns),
)
@@ -0,0 +1,143 @@
"""
Attention Pattern Dots Visualization
Shows the attention pattern as a grid of varying-sized dots,
where dot size represents attention weight.
"""
from manimlib import *
import numpy as np
def softmax(logits, temperature=1.0):
"""Compute softmax of logits array."""
logits = np.array(logits, dtype=float)
# Mask future tokens (causal attention)
logits = logits - np.max(logits)
exps = np.exp(logits / temperature)
return exps / np.sum(exps)
class AttentionPatternDots(InteractiveScene):
def construct(self):
# Parameters
N = 8
np.random.seed(42)
# Create grid
grid = Square(side_length=0.8).get_grid(N, N, buff=0)
grid.set_stroke(GREY_A, 1)
grid.stretch(0.95, 0)
grid.stretch(0.85, 1)
grid.move_to(0.5 * DOWN)
self.add(grid)
# Create query/key labels
q_template = Tex(R"\vec{\textbf{Q}}_0", font_size=36).set_color(YELLOW)
k_template = Tex(R"\vec{\textbf{K}}_0", font_size=36).set_color(TEAL)
q_substr = q_template.make_number_changeable("0")
k_substr = k_template.make_number_changeable("0")
qs = VGroup()
ks = VGroup()
for n, square in enumerate(grid[:N], start=1):
q_substr.set_value(n)
q_template.next_to(square, UP, buff=SMALL_BUFF)
qs.add(q_template.copy())
for k, square in enumerate(grid[::N], start=1):
k_substr.set_value(k)
k_template.next_to(square, LEFT, buff=SMALL_BUFF)
ks.add(k_template.copy())
self.play(
LaggedStartMap(FadeIn, qs, shift=0.2 * DOWN, lag_ratio=0.05),
LaggedStartMap(FadeIn, ks, shift=0.2 * RIGHT, lag_ratio=0.05),
)
# Generate attention pattern (causal masking)
values = np.random.normal(0, 1, (N, N))
# Apply causal mask
for n, row in enumerate(values):
row[:n] = -np.inf
# Softmax each column
attention_pattern = np.zeros_like(values)
for k in range(N):
attention_pattern[:, k] = softmax(values[:, k])
# Create dots based on attention weights
dots = VGroup()
for n in range(N): # row (key)
row_dots = VGroup()
for k in range(N): # column (query)
weight = attention_pattern[n, k]
dot = Dot(radius=0.35 * weight**0.5)
dot.move_to(grid[n * N + k])
# Color based on whether it's diagonal or not
if n == k:
dot.set_fill(YELLOW, 0.9)
elif n < k:
dot.set_fill(GREY_C, 0.8)
else: # Masked (should be zero)
dot.set_fill(RED, 0.2)
row_dots.add(dot)
dots.add(row_dots)
flat_dots = VGroup(*it.chain(*dots))
self.play(
LaggedStartMap(GrowFromCenter, flat_dots, lag_ratio=0.01),
run_time=2
)
self.wait()
# Add title
title = Text("Attention Pattern", font_size=60)
title.to_edge(UP)
self.play(Write(title))
self.wait()
# Highlight causal structure - masked region
mask_label = Text("Masked\n(future tokens)", font_size=30)
mask_label.set_color(RED)
mask_label.to_corner(DL)
masked_region = VGroup()
for n in range(N):
for k in range(n):
square = grid[n * N + k].copy()
square.set_fill(RED, 0.15)
square.set_stroke(RED, 1)
masked_region.add(square)
self.play(
FadeIn(masked_region, lag_ratio=0.02),
FadeIn(mask_label),
)
self.wait()
# Highlight self-attention (diagonal)
diag_label = Text("Self-attention\n(diagonal)", font_size=30)
diag_label.set_color(YELLOW)
diag_label.to_corner(DR)
diag_dots = VGroup(dots[i][i] for i in range(N))
self.play(
FadeIn(diag_label),
LaggedStart(
(dot.animate.scale(1.3).set_fill(YELLOW) for dot in diag_dots),
lag_ratio=0.1,
),
)
self.play(
LaggedStart(
(dot.animate.scale(1/1.3) for dot in diag_dots),
lag_ratio=0.1,
),
)
self.wait(2)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,118 @@
"""
Attention Softmax with Masking Visualization
Shows how masking works in transformer attention - lower triangle gets -infinity
before softmax, producing zeros in the attention pattern.
"""
from manimlib import *
import numpy as np
def softmax(logits, temperature=1.0):
"""Compute softmax of logits array."""
logits = np.array(logits)
logits = logits - np.max(logits) # For numerical stability
exps = np.exp(logits / temperature)
if np.isinf(exps).any() or np.isnan(exps).any():
result = np.zeros_like(logits)
result[np.argmax(logits)] = 1
return result
return exps / np.sum(exps)
class AttentionSoftmaxMasking(InteractiveScene):
def construct(self):
# Set up two grids: raw scores and normalized
shape = (6, 6)
left_grid = Square().get_grid(*shape, buff=0)
left_grid.set_shape(5.5, 5)
left_grid.to_edge(LEFT)
left_grid.set_y(-0.5)
left_grid.set_stroke(GREY_B, 1)
right_grid = left_grid.copy()
right_grid.to_edge(RIGHT)
grids = VGroup(left_grid, right_grid)
arrow = Arrow(left_grid, right_grid)
sm_label = Text("softmax")
sm_label.next_to(arrow, UP)
titles = VGroup(
Text("Unnormalized\nAttention Pattern"),
Text("Normalized\nAttention Pattern"),
)
for title, grid in zip(titles, grids):
title.next_to(grid, UP, buff=MED_LARGE_BUFF)
# Create random values for attention scores
values_array = np.random.normal(0, 2, shape)
font_size = 30
raw_values = VGroup(
DecimalNumber(
value,
include_sign=True,
font_size=font_size,
).move_to(square)
for square, value in zip(left_grid, values_array.flatten())
)
self.add(left_grid)
self.add(right_grid)
self.add(titles)
self.add(arrow)
self.add(sm_label)
self.add(raw_values)
self.wait()
# Highlight lower triangle (future tokens - to be masked)
changers = VGroup()
for n, dec in enumerate(raw_values):
i = n // shape[1]
j = n % shape[1]
if i > j: # Below diagonal - future tokens
changers.add(dec)
neg_inf = Tex(R"-\infty", font_size=36)
neg_inf.move_to(dec)
neg_inf.set_fill(RED, border_width=1.5)
dec.target = neg_inf
values_array[i, j] = -np.inf
rects = VGroup(map(SurroundingRectangle, changers))
rects.set_stroke(RED, 3)
self.play(LaggedStartMap(ShowCreation, rects))
self.play(
LaggedStartMap(FadeOut, rects),
LaggedStartMap(MoveToTarget, changers)
)
self.wait()
# Compute and show normalized values
normalized_array = np.array([
softmax(col)
for col in values_array.T
]).T
normalized_values = VGroup(
DecimalNumber(value, font_size=font_size).move_to(square)
for square, value in zip(right_grid, normalized_array.flatten())
)
# Color by value and mark zeros
for n, value in enumerate(normalized_values):
val = value.get_value()
value.set_fill(opacity=interpolate(0.5, 1, min(val * 3, 1)))
if (n // shape[1]) > (n % shape[1]):
value.set_fill(RED, 0.75)
self.play(
LaggedStart(
(FadeTransform(v1.copy(), v2)
for v1, v2 in zip(raw_values, normalized_values)),
lag_ratio=0.05,
group_type=Group
)
)
self.wait(2)
@@ -0,0 +1,220 @@
"""
Autoregressive Flow Visualization
Demonstrates the flow of text through a transformer model,
showing how text enters and probability distributions emerge.
Run with: manimgl autoregressive_flow.py AutoregressiveFlow
"""
from manimlib import *
import numpy as np
def get_paragraph(words, line_len=40, font_size=48):
"""Handle word wrapping for text display."""
words = list(map(str.strip, words))
word_lens = list(map(len, words))
lines = []
lh, rh = 0, 0
while rh < len(words):
rh += 1
if sum(word_lens[lh:rh]) > line_len:
rh -= 1
lines.append(words[lh:rh])
lh = rh
lines.append(words[lh:])
text = "\n".join([" ".join(line).strip() for line in lines])
return Text(text, alignment="LEFT", font_size=font_size)
class AutoregressiveFlow(InteractiveScene):
"""
Shows how text flows through a transformer-like machine,
demonstrating the autoregressive generation process.
"""
def construct(self):
# Create the "machine" visualization
machine = self.get_transformer_drawing()
machine.set_height(3.5)
machine.to_edge(LEFT, buff=0.5)
# Input text
input_text = "The quick brown fox"
text_mob = Text(input_text, font_size=32)
text_mob.to_edge(UP, buff=1.0)
text_mob.set_color(BLUE_B)
# Sample predictions
predictions = [" jumps", " ran", " leaped", " went", " moved"]
probs = np.array([0.42, 0.28, 0.15, 0.10, 0.05])
# Build distribution
bar_groups = self.build_distribution(predictions, probs)
bar_groups.next_to(machine, RIGHT, buff=1.5)
bar_groups.align_to(machine, UP)
# Arrows
in_arrow = Arrow(text_mob.get_bottom(), machine[0][0].get_top(), buff=0.2)
in_arrow.set_color(BLUE)
out_arrow = Arrow(machine[0][-1].get_right(), bar_groups.get_left(), buff=0.3)
out_arrow.set_color(TEAL)
# Labels
input_label = Text("Input Context", font_size=24)
input_label.next_to(text_mob, LEFT)
output_label = Text("Output\nProbabilities", font_size=24, alignment="CENTER")
output_label.next_to(bar_groups, RIGHT)
# Animate
self.play(FadeIn(machine))
self.wait(0.5)
self.play(Write(text_mob), FadeIn(input_label))
self.play(GrowArrow(in_arrow))
# Animate text flowing into machine
text_copy = text_mob.copy()
self.play(
text_copy.animate.scale(0.5).move_to(machine[0][0].get_top()),
run_time=0.5
)
self.play(
FadeOut(text_copy, shift=DOWN),
self.animate_machine_processing(machine),
run_time=1.5
)
# Output emerges
self.play(GrowArrow(out_arrow))
self.play(
LaggedStart(
*(FadeIn(bg, shift=RIGHT) for bg in bar_groups),
lag_ratio=0.1,
run_time=1.5
),
FadeIn(output_label)
)
self.wait(2)
def get_transformer_drawing(self):
"""Create a 3D-like stack of blocks representing the transformer."""
blocks = VGroup(*(
VGroup(
Rectangle(2.5, 0.3).set_fill(GREY_D, 1).set_stroke(WHITE, 1),
)
for n in range(8)
))
blocks.arrange(DOWN, buff=0.05)
# Add "Transformer" label
label = Text("Transformer", font_size=28)
label.next_to(blocks, UP, buff=0.3)
return VGroup(blocks, label)
def animate_machine_processing(self, machine):
"""Animate the blocks lighting up in sequence."""
blocks = machine[0]
return LaggedStart(
*(
block[0].animate.set_fill(TEAL, 0.8).set_anim_args(
rate_func=there_and_back
)
for block in blocks
),
lag_ratio=0.15,
run_time=1.5
)
def build_distribution(self, words, probs, font_size=24, width_100p=2.0, bar_height=0.25):
"""Build bar chart visualization of token probabilities."""
labels = VGroup(*(Text(word, font_size=font_size) for word in words))
bars = VGroup(*(
Rectangle(prob * width_100p, bar_height)
for prob in probs
))
bars.arrange(DOWN, aligned_edge=LEFT, buff=0.4 * bar_height)
bars.set_fill(opacity=1)
bars.set_submobject_colors_by_gradient(TEAL, YELLOW)
bars.set_stroke(WHITE, 1)
bar_groups = VGroup()
for label, bar, prob in zip(labels, bars, probs):
prob_label = Integer(int(100 * prob), unit="%", font_size=0.75 * font_size)
prob_label.next_to(bar, RIGHT, buff=SMALL_BUFF)
label.next_to(bar, LEFT)
bar_groups.add(VGroup(label, bar, prob_label))
return bar_groups
class TextToMachineFlow(InteractiveScene):
"""
Simpler version showing text entering a machine block.
"""
def construct(self):
# Machine box
machine = Rectangle(3, 2)
machine.set_fill(GREY_D, 0.8)
machine.set_stroke(WHITE, 2)
machine_label = Text("LLM", font_size=36)
machine_label.move_to(machine)
machine_group = VGroup(machine, machine_label)
machine_group.center()
# Input text
input_words = ["The", "weather", "today", "is"]
word_mobs = VGroup(*(Text(w, font_size=28) for w in input_words))
word_mobs.arrange(RIGHT, buff=0.3)
word_mobs.next_to(machine, UP, buff=1.5)
word_mobs.set_color(BLUE_B)
# Output predictions
output_words = ["sunny", "rainy", "cloudy", "warm"]
output_probs = [0.45, 0.25, 0.20, 0.10]
output_mobs = VGroup()
for word, prob in zip(output_words, output_probs):
text = Text(f"{word}: {int(prob*100)}%", font_size=24)
output_mobs.add(text)
output_mobs.arrange(DOWN, aligned_edge=LEFT, buff=0.2)
output_mobs.next_to(machine, DOWN, buff=1.0)
output_mobs.set_color(TEAL)
# Arrows
in_arrow = Arrow(word_mobs.get_bottom(), machine.get_top(), buff=0.1)
out_arrow = Arrow(machine.get_bottom(), output_mobs.get_top(), buff=0.1)
# Animate
self.play(FadeIn(machine_group))
self.play(
LaggedStart(
*(FadeIn(w, shift=DOWN) for w in word_mobs),
lag_ratio=0.2
)
)
self.play(GrowArrow(in_arrow))
# Words flow in
self.play(
LaggedStart(
*(
w.animate.scale(0.3).move_to(machine.get_center())
for w in word_mobs.copy()
),
lag_ratio=0.1
),
machine.animate.set_fill(TEAL, 0.3).set_anim_args(rate_func=there_and_back),
run_time=1.5
)
# Output emerges
self.play(GrowArrow(out_arrow))
self.play(
LaggedStart(
*(FadeIn(o, shift=DOWN) for o in output_mobs),
lag_ratio=0.15
)
)
self.wait(2)
@@ -0,0 +1,181 @@
"""
Basic Multi-Head Attention - ManimGL (using Scene, not InteractiveScene)
Run with: manimgl basic_multihead.py MultiHeadBasic -w -l
"""
from manimlib import *
import numpy as np
def softmax(logits):
logits = np.array(logits)
logits = logits - np.max(logits)
exps = np.exp(logits)
return exps / np.sum(exps)
class AttentionGrid(VGroup):
"""Attention pattern grid."""
def __init__(self, n=6, seed=0, **kwargs):
super().__init__(**kwargs)
np.random.seed(seed)
cell = 0.35
grid = VGroup()
for i in range(n):
for j in range(n):
sq = Square(side_length=cell)
sq.set_stroke(WHITE, 0.5, 0.3)
sq.move_to([j * cell, -i * cell, 0])
grid.add(sq)
grid.center()
# Causal pattern
pattern = np.random.randn(n, n)
for col in range(n):
pattern[:, col][col + 1:] = -np.inf
valid = pattern[:, col][:col + 1]
pattern[:, col][:col + 1] = softmax(valid)
pattern[:, col][col + 1:] = 0
dots = VGroup()
for i in range(n):
for j in range(n):
v = pattern[i, j]
if v > 0.05:
d = Dot(radius=cell * 0.4 * v)
d.set_fill(GREY_B)
d.move_to(grid[i * n + j])
dots.add(d)
border = SurroundingRectangle(grid, buff=0.03)
border.set_stroke(WHITE, 2)
border.set_fill(BLACK, 0.9)
self.add(border, grid, dots)
class MultiHeadBasic(Scene):
"""Basic multi-head attention visualization."""
def construct(self):
# Title
title = Text("Multi-Head Attention")
title.to_edge(UP)
self.play(Write(title))
self.wait()
# Create multiple attention heads
heads = VGroup()
for i in range(6):
head = AttentionGrid(n=5, seed=i * 10)
head.set_height(1.5)
heads.add(head)
heads.arrange_in_grid(n_rows=2, n_cols=3, buff=0.5)
heads.next_to(title, DOWN, buff=0.5)
# Labels (using Text to avoid LaTeX dependency issues)
labels = VGroup()
for i, head in enumerate(heads):
label = Text(f"Head {i+1}", font_size=18)
label.set_color(YELLOW)
label.next_to(head, UP, buff=0.1)
labels.add(label)
# Show heads one by one
self.play(
LaggedStart(
*[FadeIn(h, scale=0.8) for h in heads],
lag_ratio=0.2
),
run_time=3
)
self.play(
LaggedStart(*[FadeIn(l) for l in labels], lag_ratio=0.1)
)
self.wait()
# Explanation
explanation = VGroup(
Text("Each head learns different patterns:", font_size=24),
Text("• Subject-verb relationships", font_size=20, color=BLUE),
Text("• Adjective-noun connections", font_size=20, color=GREEN),
Text("• Positional patterns", font_size=20, color=YELLOW),
)
explanation.arrange(DOWN, aligned_edge=LEFT, buff=0.15)
explanation.to_edge(DOWN, buff=0.5)
self.play(
LaggedStart(*[Write(e) for e in explanation], lag_ratio=0.3)
)
self.wait(2)
class MultiHead3D(Scene):
"""3D multi-head visualization using Scene (simpler)."""
def construct(self):
frame = self.camera.frame
# Title (fixed in frame)
title = Text("Multi-Head Attention in 3D")
title.to_edge(UP)
title.fix_in_frame()
self.add(title)
# Create heads
heads = Group()
for i in range(8):
head = AttentionGrid(n=5, seed=i * 7)
head.set_height(2)
heads.add(head)
# Arrange in depth
heads.arrange(OUT, buff=0.7)
heads.center()
# Start with one head
self.add(heads[-1])
self.wait()
# Rotate camera
self.play(
frame.animate.set_euler_angles(
phi=70 * DEGREES,
theta=-45 * DEGREES
),
run_time=2
)
# Show all heads
self.play(
LaggedStart(
*[FadeIn(h, shift=OUT * 0.3) for h in heads[:-1]],
lag_ratio=0.15
),
run_time=3
)
self.wait()
# Add labels (using Text to avoid LaTeX dependency)
wq_labels = VGroup()
for i, head in enumerate(list(heads)[::-1][:4]):
label = Text(f"H{i+1}", font_size=24, color=YELLOW)
label.next_to(head, UP, buff=0.2)
label.rotate(70 * DEGREES, RIGHT)
label.rotate(-45 * DEGREES, OUT)
wq_labels.add(label)
self.play(
LaggedStart(*[FadeIn(l, shift=UP * 0.2) for l in wq_labels], lag_ratio=0.2)
)
self.wait()
# Rotate around
self.play(
frame.animate.increment_theta(60 * DEGREES),
run_time=4
)
self.wait()
@@ -0,0 +1,246 @@
"""
Bloch Sphere 3D Visualization
=============================
Displays a quantum state vector in 3D space with a surrounding Bloch sphere.
The vector rotates and can be observed from different angles with ambient
camera rotation.
Key concepts demonstrated:
- ThreeDAxes for 3D coordinate system
- Sphere and SurfaceMesh for Bloch sphere visualization
- frame.add_ambient_rotation for continuous camera movement
- Vector with set_perpendicular_to_camera for billboard effect
"""
from manimlib import *
class BlochSphere3D(InteractiveScene):
"""Visualize a quantum state as a vector on the Bloch sphere."""
def construct(self):
frame = self.frame
# Set up 3D axes
axes = ThreeDAxes((-1, 1), (-1, 1), (-1, 1))
axes.scale(2.0)
# Add a subtle reference plane
plane = NumberPlane(
(-1, 1 - 1e-5),
(-1, 1 - 1e-5),
faded_line_ratio=5
)
plane.scale(2.0)
plane.background_lines.set_stroke(opacity=0.5)
plane.faded_lines.set_stroke(opacity=0.25)
plane.axes.set_stroke(opacity=0.25)
# Set up camera orientation and ambient rotation
frame.reorient(14, 76, 0)
frame.add_ambient_rotation(3 * DEG)
self.add(plane, axes)
# Create the state vector
vector = Vector(
2 * normalize([1, 1, 2]),
thickness=5,
fill_color=TEAL
)
vector.set_fill(border_width=2)
vector.always.set_perpendicular_to_camera(frame)
self.play(GrowArrow(vector))
self.wait(6)
# Rotate the vector randomly
for _ in range(3):
axis = normalize(np.random.uniform(-1, 1, 3))
angle = np.random.uniform(PI / 4, PI)
self.play(
Rotate(vector, angle, axis=axis, about_point=ORIGIN),
run_time=2
)
self.wait()
# Show the Bloch sphere
sphere = Sphere(radius=2)
sphere.always_sort_to_camera(self.camera)
sphere.set_color(BLUE, 0.25)
sphere_mesh = SurfaceMesh(sphere, resolution=(41, 21))
sphere_mesh.set_stroke(WHITE, 0.5, 0.5)
self.play(
ShowCreation(sphere),
Write(sphere_mesh, lag_ratio=1e-3),
run_time=3
)
# Add axis labels
labels = VGroup(
Tex(R"|0\rangle"),
Tex(R"|1\rangle"),
Tex(R"|+\rangle"),
)
labels.scale(0.6)
labels.set_backstroke(BLACK, 3)
# Position labels at key points
labels[0].rotate(90 * DEG, RIGHT)
labels[0].next_to(axes.c2p(0, 0, 1), OUT + RIGHT, buff=0.1)
labels[1].rotate(90 * DEG, RIGHT)
labels[1].next_to(axes.c2p(0, 0, -1), OUT + RIGHT, buff=0.1)
labels[2].rotate(90 * DEG, RIGHT)
labels[2].next_to(axes.c2p(1, 0, 0), RIGHT, buff=0.1)
self.play(LaggedStartMap(FadeIn, labels, lag_ratio=0.3))
# Let it rotate for observation
self.wait(10)
class StateVectorEvolution(InteractiveScene):
"""Shows a state vector evolving on the Bloch sphere with a tracing tail."""
def construct(self):
frame = self.frame
# Set up 3D environment
axes = ThreeDAxes((-1, 1), (-1, 1), (-1, 1))
axes.scale(2.0)
sphere = Sphere(radius=2)
sphere.always_sort_to_camera(self.camera)
sphere.set_color(BLUE, 0.15)
sphere_mesh = SurfaceMesh(sphere, resolution=(21, 11))
sphere_mesh.set_stroke(WHITE, 0.25, 0.25)
frame.reorient(20, 70, 0)
frame.add_ambient_rotation(2 * DEG)
self.add(axes, sphere, sphere_mesh)
# Create evolving vector
theta_tracker = ValueTracker(0)
phi_tracker = ValueTracker(PI / 4)
def get_vector_end():
theta = theta_tracker.get_value()
phi = phi_tracker.get_value()
return 2 * np.array([
np.sin(phi) * np.cos(theta),
np.sin(phi) * np.sin(theta),
np.cos(phi)
])
vector = Vector(get_vector_end(), thickness=5, fill_color=YELLOW)
vector.always.set_perpendicular_to_camera(frame)
vector.add_updater(
lambda m: m.put_start_and_end_on(ORIGIN, get_vector_end())
)
# Add tracing tail
tail = TracingTail(
lambda: vector.get_end(),
stroke_color=YELLOW,
stroke_width=2,
time_traced=5
)
self.add(vector, tail)
self.wait()
# Evolve the state
self.play(
theta_tracker.animate.set_value(2 * TAU),
phi_tracker.animate.set_value(3 * PI / 4),
run_time=10,
rate_func=linear
)
self.wait(3)
class QuantumStateCollapse(InteractiveScene):
"""Demonstrates the concept of quantum state collapse upon measurement."""
def construct(self):
frame = self.frame
# Simple 2D representation for clarity
plane = NumberPlane((-2, 2), (-2, 2), faded_line_ratio=5)
plane.scale(1.5)
# Basis state labels
zero_label = Tex(R"|0\rangle").scale(0.8)
zero_label.next_to(plane.c2p(1, 0), DR, SMALL_BUFF)
one_label = Tex(R"|1\rangle").scale(0.8)
one_label.next_to(plane.c2p(0, 1), UL, SMALL_BUFF)
# Unit circle
circle = Circle(radius=plane.c2p(1, 0)[0])
circle.set_stroke(GREY, 1, 0.5)
self.add(plane, circle, zero_label, one_label)
# Superposition state vector
theta = 45 * DEG
vector = Arrow(
plane.c2p(0, 0),
plane.c2p(np.cos(theta), np.sin(theta)),
buff=0,
thickness=5,
fill_color=TEAL
)
state_label = Tex(
R"\frac{1}{\sqrt{2}}(|0\rangle + |1\rangle)",
font_size=36
)
state_label.next_to(vector.get_end(), UR, SMALL_BUFF)
state_label.set_backstroke(BLACK, 3)
self.play(GrowArrow(vector), FadeIn(state_label))
self.wait()
# Measurement indicator
measurement_text = Text("Measurement", font_size=36, color=RED)
measurement_text.to_edge(UP)
self.play(Write(measurement_text))
# Flash effect
self.play(
Flash(vector.get_end(), color=WHITE, flash_radius=0.5),
run_time=0.5
)
# Collapse to |0> (50% case)
collapsed_vector = Arrow(
plane.c2p(0, 0),
plane.c2p(1, 0),
buff=0,
thickness=5,
fill_color=BLUE
)
result_label = Tex(R"|0\rangle", font_size=48, color=BLUE)
result_label.next_to(collapsed_vector.get_end(), RIGHT, MED_SMALL_BUFF)
self.play(
Transform(vector, collapsed_vector),
FadeOut(state_label),
FadeIn(result_label),
run_time=0.3
)
self.wait(2)
if __name__ == "__main__":
# To run: manimgl bloch_sphere_3d.py BlochSphere3D
pass
@@ -0,0 +1,173 @@
"""
Basic block collision simulation demonstrating elastic collisions.
Based on the famous 3b1b pi-computing collision video.
"""
from manimlib import *
import math
LITTLE_BLOCK_COLOR = "#51463E"
class StateTracker(ValueTracker):
"""
Tracks the state of the block collision process as a 4d vector
[
x1 * sqrt(m1),
x2 * sqrt(m2),
v1 * sqrt(m1),
v2 * sqrt(m2),
]
"""
def __init__(self, blocks, initial_positions=[8, 5], initial_velocities=[-1, 0]):
sqrt_m1, sqrt_m2 = self.sqrt_mass_vect = np.sqrt([b.mass for b in blocks])
self.theta = math.atan2(sqrt_m2, sqrt_m1)
self.state0 = np.array([
*np.array(initial_positions) * self.sqrt_mass_vect,
*np.array(initial_velocities) * self.sqrt_mass_vect,
])
super().__init__(self.state0.copy())
def set_time(self, t):
pos0 = self.state0[0:2]
vel0 = self.state0[2:4]
self.set_value([*(pos0 + t * vel0), *vel0])
def rotate_2d(self, vect, angle):
"""Simple 2D rotation helper"""
c, s = math.cos(angle), math.sin(angle)
return np.array([c * vect[0] - s * vect[1], s * vect[0] + c * vect[1]])
def reflect_vect(self, vect):
n_reflections = self.get_n_collisions()
rot_angle = -2 * self.theta * ((n_reflections + 1) // 2)
result = self.rotate_2d(vect, rot_angle)
result[1] *= (-1)**(n_reflections % 2)
return result
def get_block_positions(self):
scaled_pos = self.get_value()[0:2]
rot_scaled_pos = self.reflect_vect(scaled_pos)
return rot_scaled_pos / self.sqrt_mass_vect
def get_scaled_block_velocities(self):
return self.reflect_vect(self.get_value()[2:4])
def get_block_velocities(self):
return self.get_scaled_block_velocities() / self.sqrt_mass_vect
def get_n_collisions(self):
state = self.get_value()
angle = math.atan2(state[1], state[0])
return int(angle / self.theta)
class BlockCollisionBasic(Scene):
"""
A simplified block collision demonstration.
Shows two blocks colliding elastically.
"""
initial_positions = [10, 7]
initial_velocities = [-2, 0]
masses = [100, 1]
widths = [1.0, 0.5]
colors = [BLUE_E, LITTLE_BLOCK_COLOR]
def construct(self):
# Create floor and wall
floor, wall = self.get_floor_and_wall()
self.add(floor, wall)
# Create blocks
blocks = self.get_blocks(floor)
self.add(blocks)
# Set up state tracking
state_tracker = StateTracker(blocks, self.initial_positions, self.initial_velocities)
time_tracker = ValueTracker(0)
state_tracker.add_updater(lambda m: m.set_time(time_tracker.get_value()))
# Bind blocks to state
min_x = floor.get_x(LEFT) + blocks[1].get_width()
def update_blocks(blocks):
pos = state_tracker.get_block_positions()
blocks[0].set_x(min_x + pos[0], LEFT)
blocks[1].set_x(min_x + pos[1], RIGHT)
blocks.add_updater(update_blocks)
self.add(state_tracker, time_tracker)
# Add collision counter
count_label = Tex(R"\# \text{Collisions} = 0")
count = count_label.make_number_changeable("0")
count.add_updater(lambda m: m.set_value(state_tracker.get_n_collisions()))
count_label.to_corner(UL)
self.add(count_label)
# Run the simulation
self.play(
time_tracker.animate.set_value(30),
run_time=15,
rate_func=linear,
)
self.wait()
def get_floor_and_wall(self, width=13, height=2, stroke_width=2, buff_to_bottom=0.75):
floor = Line(LEFT, RIGHT)
floor.set_width(width)
floor.to_edge(DOWN, buff=buff_to_bottom)
dl_point = floor.get_left()
wall = Line(ORIGIN, UP)
wall.set_height(height)
wall.move_to(dl_point, DOWN)
# Add tick marks to wall
ticks = VGroup()
tick_spacing = 0.5
tick_vect = 0.25 * DL
for y in np.arange(tick_spacing, height + tick_spacing, tick_spacing):
start = dl_point + y * UP
ticks.add(Line(start, start + tick_vect))
result = VGroup(floor, VGroup(wall, ticks))
result.set_stroke(WHITE, stroke_width)
return result
def get_blocks(self, floor):
blocks = Group()
for mass, color, width in zip(self.masses, self.colors, self.widths):
block = Square()
block.set_stroke(WHITE, 2)
block.set_fill(color, 1)
block.set_width(width)
block.next_to(floor, UP, buff=0.01)
block.mass = mass
mass_label = Tex(R"10 \, \text{kg}", font_size=24)
mass_label.make_number_changeable("10", edge_to_fix=RIGHT).set_value(mass)
mass_label.next_to(block, UP, buff=SMALL_BUFF)
block.add(mass_label)
block.mass_label = mass_label
blocks.add(block)
return blocks
# Alternative mass ratios for counting pi digits
class BlockCollision1e4(BlockCollisionBasic):
"""Mass ratio 10000:1 gives 314 collisions"""
masses = [10000, 1]
widths = [1.5, 0.5]
colors = [interpolate_color(BLUE_E, BLACK, 0.5), LITTLE_BLOCK_COLOR]
class BlockCollision1e6(BlockCollisionBasic):
"""Mass ratio 1000000:1 gives 3141 collisions"""
masses = [1000000, 1]
widths = [2.0, 0.5]
colors = [interpolate_color(BLUE_E, BLACK, 0.8), LITTLE_BLOCK_COLOR]
@@ -0,0 +1,229 @@
"""
3D block collision simulation with floor and wall.
Demonstrates 3D scene setup with physics simulation.
Based on the famous 3b1b pi-computing collision video.
"""
from manimlib import *
import math
LITTLE_BLOCK_COLOR = "#51463E"
class StateTracker(ValueTracker):
"""
Tracks the state of the block collision process.
"""
def __init__(self, blocks, initial_positions=[8, 5], initial_velocities=[-1, 0]):
sqrt_m1, sqrt_m2 = self.sqrt_mass_vect = np.sqrt([b.mass for b in blocks])
self.theta = math.atan2(sqrt_m2, sqrt_m1)
self.state0 = np.array([
*np.array(initial_positions) * self.sqrt_mass_vect,
*np.array(initial_velocities) * self.sqrt_mass_vect,
])
super().__init__(self.state0.copy())
def set_time(self, t):
pos0 = self.state0[0:2]
vel0 = self.state0[2:4]
self.set_value([*(pos0 + t * vel0), *vel0])
def rotate_2d(self, vect, angle):
c, s = math.cos(angle), math.sin(angle)
return np.array([c * vect[0] - s * vect[1], s * vect[0] + c * vect[1]])
def reflect_vect(self, vect):
n_reflections = self.get_n_collisions()
rot_angle = -2 * self.theta * ((n_reflections + 1) // 2)
result = self.rotate_2d(vect, rot_angle)
result[1] *= (-1)**(n_reflections % 2)
return result
def get_block_positions(self):
scaled_pos = self.get_value()[0:2]
rot_scaled_pos = self.reflect_vect(scaled_pos)
return rot_scaled_pos / self.sqrt_mass_vect
def get_block_velocities(self):
return self.reflect_vect(self.get_value()[2:4]) / self.sqrt_mass_vect
def get_n_collisions(self):
state = self.get_value()
angle = math.atan2(state[1], state[0])
return int(angle / self.theta)
class Blocks3D(Scene):
"""
3D visualization of colliding blocks with floor and wall.
"""
initial_positions = [10, 7]
initial_velocities = [-2, 0]
masses = [100, 1]
widths = [1.0, 0.5]
colors = [BLUE_E, LITTLE_BLOCK_COLOR]
floor_width = 15
floor_depth = 6
wall_height = 5
block_shading = (0.5, 0.5, 0)
def construct(self):
# Set up 3D camera
frame = self.frame
frame.set_field_of_view(10 * DEGREES)
frame.reorient(-10, 5, 0)
# Create 3D floor and wall
floor, wall = self.get_floor_and_wall_3d()
self.add(floor, wall)
# Create 3D blocks
blocks = self.get_blocks_3d(floor)
self.add(blocks)
# Set up state tracking
state_tracker = StateTracker(blocks, self.initial_positions, self.initial_velocities)
time_tracker = ValueTracker(0)
state_tracker.add_updater(lambda m: m.set_time(time_tracker.get_value()))
# Bind blocks to state
min_x = floor.get_x(LEFT) + blocks[1].get_width()
def update_blocks(blocks):
pos = state_tracker.get_block_positions()
blocks[0].set_x(min_x + pos[0], LEFT)
blocks[1].set_x(min_x + pos[1], RIGHT)
blocks.add_updater(update_blocks)
self.add(state_tracker, time_tracker)
# Add collision counter (fixed to frame)
count_label = Tex(R"\# \text{Collisions} = 0")
count = count_label.make_number_changeable("0")
count.add_updater(lambda m: m.set_value(state_tracker.get_n_collisions()))
count_label.to_corner(UL)
count_label.fix_in_frame()
self.add(count_label)
# Run simulation with camera movement
self.play(
time_tracker.animate.set_value(30),
frame.animate.reorient(-5, 3, 0),
run_time=15,
rate_func=linear,
)
self.wait()
def get_floor_and_wall_3d(self, buff_to_bottom=0.75, color=GREY_D, shading=(0.2, 0.2, 0.2)):
floor = Square3D(resolution=(20, 20))
floor.rotate(90 * DEGREES, LEFT)
floor.set_shape(self.floor_width, 0, self.floor_depth)
floor.to_edge(DOWN, buff=buff_to_bottom)
wall = Square3D()
wall.rotate(90 * DEGREES, UP)
wall.set_shape(0, self.wall_height, self.floor_depth)
wall.move_to(floor.get_left(), DOWN)
result = Group(floor, wall)
result.set_color(color)
result.set_shading(*shading)
result.to_corner(DL)
return result
def get_blocks_3d(self, floor, floor_buff=0.01):
blocks = Group()
for mass, color, width in zip(self.masses, self.colors, self.widths):
# Create 3D cube body
body = Cube()
body.set_color(color)
body.set_shading(*self.block_shading)
# Add wireframe shell
shell = VCube()
shell.set_fill(opacity=0)
shell.set_stroke(WHITE, width=1)
shell.replace(body)
shell.apply_depth_test()
block = Group(body, shell)
block.set_width(width)
block.next_to(floor, UP, buff=floor_buff)
block.mass = mass
# Mass label
mass_label = Tex(R"10 \, \text{kg}", font_size=24)
mass_label.make_number_changeable("10", edge_to_fix=RIGHT).set_value(mass)
mass_label.next_to(block, UP, buff=SMALL_BUFF)
mass_label.set_backstroke(BLACK, 1)
block.add(mass_label)
block.mass_label = mass_label
blocks.add(block)
return blocks
class PreviewClip3D(Blocks3D):
"""
Cinematic preview shot with camera movement.
"""
initial_velocities = [-0.75, 0]
masses = [100, 1]
widths = [2.0, 0.5]
initial_positions = [10, 7]
floor_depth = 2
wall_height = 2
def construct(self):
frame = self.frame
frame.set_field_of_view(15 * DEGREES)
# Create scene
floor, wall = self.get_floor_and_wall_3d()
self.add(floor, wall)
blocks = self.get_blocks_3d(floor)
self.add(blocks)
state_tracker = StateTracker(blocks, self.initial_positions, self.initial_velocities)
time_tracker = ValueTracker(0)
state_tracker.add_updater(lambda m: m.set_time(time_tracker.get_value()))
min_x = floor.get_x(LEFT) + blocks[1].get_width()
def update_blocks(blocks):
pos = state_tracker.get_block_positions()
blocks[0].set_x(min_x + pos[0], LEFT)
blocks[1].set_x(min_x + pos[1], RIGHT)
blocks.add_updater(update_blocks)
self.add(state_tracker, time_tracker)
# Counter
count_label = Tex(R"\# \text{Collisions} = 0")
count = count_label.make_number_changeable("0")
count.add_updater(lambda m: m.set_value(state_tracker.get_n_collisions()))
count_label.to_corner(UL)
count_label.fix_in_frame()
self.add(count_label)
# Start with dramatic angle
frame.reorient(-46, -6, 0, (0.41, -2.47, 1.07), 3.59)
# Automatic time update
time_tracker.add_updater(lambda m, dt: m.increment_value(dt))
# Cinematic camera movements
self.play(
frame.animate.reorient(-46, -4, 0, (-0.78, -2.2, -0.17), 5.41),
run_time=8
)
self.play(
frame.animate.reorient(-4, -4, 0, (-2.38, -1.95, -0.99), 6.58),
run_time=12,
)
self.wait()
@@ -0,0 +1,291 @@
"""
Phase space visualization of elastic block collisions.
Shows how conservation laws constrain the state to a circle.
Based on the famous 3b1b pi-computing collision video.
"""
from manimlib import *
import math
LITTLE_BLOCK_COLOR = "#51463E"
class StateTracker(ValueTracker):
"""
Tracks the state of the block collision process.
"""
def __init__(self, blocks, initial_positions=[8, 5], initial_velocities=[-1, 0]):
sqrt_m1, sqrt_m2 = self.sqrt_mass_vect = np.sqrt([b.mass for b in blocks])
self.theta = math.atan2(sqrt_m2, sqrt_m1)
self.state0 = np.array([
*np.array(initial_positions) * self.sqrt_mass_vect,
*np.array(initial_velocities) * self.sqrt_mass_vect,
])
super().__init__(self.state0.copy())
def set_time(self, t):
pos0 = self.state0[0:2]
vel0 = self.state0[2:4]
self.set_value([*(pos0 + t * vel0), *vel0])
def rotate_2d(self, vect, angle):
c, s = math.cos(angle), math.sin(angle)
return np.array([c * vect[0] - s * vect[1], s * vect[0] + c * vect[1]])
def reflect_vect(self, vect):
n_reflections = self.get_n_collisions()
rot_angle = -2 * self.theta * ((n_reflections + 1) // 2)
result = self.rotate_2d(vect, rot_angle)
result[1] *= (-1)**(n_reflections % 2)
return result
def get_block_positions(self):
scaled_pos = self.get_value()[0:2]
rot_scaled_pos = self.reflect_vect(scaled_pos)
return rot_scaled_pos / self.sqrt_mass_vect
def get_scaled_block_velocities(self):
return self.reflect_vect(self.get_value()[2:4])
def get_block_velocities(self):
return self.get_scaled_block_velocities() / self.sqrt_mass_vect
def get_n_collisions(self):
state = self.get_value()
angle = math.atan2(state[1], state[0])
return int(angle / self.theta)
class CollisionPhaseSpace(Scene):
"""
Shows block collisions with a phase space diagram.
The state point traces a path on a circle as collisions occur.
"""
initial_positions = [9.5, 8]
initial_velocities = [-1, 0]
masses = [10, 1]
widths = [1.0, 0.5]
colors = [BLUE_E, LITTLE_BLOCK_COLOR]
def construct(self):
# Create floor and blocks (simplified)
floor = Line(13 * LEFT / 2, 13 * RIGHT / 2)
floor.to_edge(DOWN, buff=0.75)
floor.set_stroke(WHITE, 2)
blocks = self.get_blocks(floor)
self.add(floor, blocks)
# Set up state tracking
state_tracker = StateTracker(blocks, self.initial_positions, self.initial_velocities)
time_tracker = ValueTracker(0)
state_tracker.add_updater(lambda m: m.set_time(time_tracker.get_value()))
# Bind blocks to state
min_x = floor.get_x(LEFT) + blocks[1].get_width()
def update_blocks(blocks):
pos = state_tracker.get_block_positions()
blocks[0].set_x(min_x + pos[0], LEFT)
blocks[1].set_x(min_x + pos[1], RIGHT)
blocks.add_updater(update_blocks)
self.add(state_tracker, time_tracker)
# Create phase space plane
plane = NumberPlane((-4, 4, 1), (-4, 4, 1), faded_line_ratio=1)
plane.set_height(4.5)
plane.to_corner(UR, buff=0.5)
plane.axes.set_stroke(WHITE, 1)
plane.background_lines.set_stroke(BLUE, 1, 0.5)
plane.faded_lines.set_stroke(BLUE, 0.5, 0.25)
self.add(plane)
# Add axis labels
kw = dict(t2c={"v_1": RED, "v_2": RED}, font_size=24)
x_label = Tex("x = v_1", **kw)
y_label = Tex("y = v_2", **kw)
x_label.next_to(plane.x_axis.get_right(), UR, SMALL_BUFF)
y_label.next_to(plane.y_axis.get_top(), DR, SMALL_BUFF)
self.add(x_label, y_label)
# Create state point tracking velocity
marked_velocity = ValueTracker(state_tracker.get_block_velocities())
marked_velocity.add_updater(lambda m: m.set_value(state_tracker.get_block_velocities()))
self.add(marked_velocity)
state_point = Group(
TrueDot(radius=0.05).make_3d(),
GlowDot(radius=0.2),
)
state_point.set_color(RED)
state_point.add_updater(lambda m: m.move_to(plane.c2p(*marked_velocity.get_value())))
self.add(state_point)
# Add energy circle (ellipse before scaling)
ellipse = Circle(radius=plane.x_axis.get_unit_size())
ellipse.set_stroke(YELLOW, 2)
ellipse.stretch(math.sqrt(10), 1) # sqrt(m1/m2)
ellipse.move_to(plane.c2p(0, 0))
self.add(ellipse)
# Add traced path
traced_path = TracedPath(state_point.get_center, stroke_color=RED, stroke_width=1)
self.add(traced_path)
# Add collision counter
count_label = Tex(R"\# \text{Collisions} = 0", font_size=30)
count = count_label.make_number_changeable("0")
count.add_updater(lambda m: m.set_value(state_tracker.get_n_collisions()))
count_label.to_corner(UL)
self.add(count_label)
# Add energy equation
ke_equation = Tex(
R"\frac{1}{2} m_1 (v_1)^2 + \frac{1}{2}m_2 (v_2)^2 = E",
t2c={"m_1": BLUE, "m_2": BLUE, "v_1": RED, "v_2": RED},
font_size=28
)
ke_equation.next_to(count_label, DOWN, buff=0.5, aligned_edge=LEFT)
self.add(ke_equation)
# Run simulation
self.play(
time_tracker.animate.set_value(25),
run_time=15,
rate_func=linear,
)
self.wait()
def get_blocks(self, floor):
blocks = Group()
for mass, color, width in zip(self.masses, self.colors, self.widths):
block = Square()
block.set_stroke(WHITE, 2)
block.set_fill(color, 1)
block.set_width(width)
block.next_to(floor, UP, buff=0.01)
block.mass = mass
mass_label = Tex(R"10 \, \text{kg}", font_size=20)
mass_label.make_number_changeable("10", edge_to_fix=RIGHT).set_value(mass)
mass_label.next_to(block, UP, buff=SMALL_BUFF)
block.add(mass_label)
blocks.add(block)
return blocks
class CirclePuzzle(Scene):
"""
Shows the geometric puzzle: counting lines bouncing between a circle and a line.
This is the geometric interpretation of the collision counting.
"""
def construct(self):
# Add axes
axes = VGroup(Line(1.5 * LEFT, 1.5 * RIGHT), Line(UP, DOWN))
axes.set_stroke(WHITE, 2, 0.33)
axes.set_height(6)
self.add(axes)
# Add circle
circle = Circle(radius=2.5)
circle.set_stroke(YELLOW, 2)
self.play(ShowCreation(circle))
self.wait()
# Add state point
state_point = Group(
TrueDot(radius=0.05).make_3d(),
GlowDot(radius=0.2),
)
state_point.set_color(RED)
state_point.move_to(circle.get_left())
self.play(FadeIn(state_point, shift=0.5 * DR, scale=0.5))
self.wait()
# Add bouncing lines with slope = -sqrt(m1/m2)
slope = -math.sqrt(10) # For mass ratio 10:1
lines = self.get_bounce_lines(circle, slope)
# Animate each bounce
count_label = Tex(R"\# \text{Bounces} = 0", font_size=36)
count = count_label.make_number_changeable("0")
count_label.to_corner(UL)
self.add(count_label)
for i, line in enumerate(lines):
self.play(
ShowCreation(line),
state_point.animate.move_to(line.get_end()),
ChangeDecimalToValue(count, i + 1),
run_time=0.5
)
self.wait()
# Show end zone
theta = math.atan(1 / abs(slope))
endzone_line = Line(ORIGIN, 4 * np.array([math.cos(theta), math.sin(theta), 0]))
endzone_line.set_stroke(WHITE, 2)
endzone = Polygon(
endzone_line.get_end(),
ORIGIN,
4 * RIGHT,
)
endzone.set_fill(GREEN, 0.25)
endzone.set_stroke(width=0)
self.play(FadeIn(endzone), ShowCreation(endzone_line))
self.wait(2)
def get_bounce_lines(self, circle, slope, max_bounces=10):
"""Generate lines bouncing between circle and x-axis reflection"""
lines = VGroup()
point = circle.get_left()
direction = np.array([1, slope, 0])
direction = direction / np.linalg.norm(direction)
for i in range(max_bounces):
# Find intersection with circle or x-axis
if i % 2 == 0:
# Bounce off x-axis (reflect y)
t = -point[1] / direction[1] if abs(direction[1]) > 1e-6 else 1e6
next_point = point + t * direction
# Check if still inside circle
if np.linalg.norm(next_point[:2]) > circle.get_width() / 2:
break
else:
# Find circle intersection
# Solve |point + t*direction|^2 = r^2
r = circle.get_width() / 2
a = direction[0]**2 + direction[1]**2
b = 2 * (point[0] * direction[0] + point[1] * direction[1])
c = point[0]**2 + point[1]**2 - r**2
disc = b**2 - 4 * a * c
if disc < 0:
break
t = (-b + math.sqrt(disc)) / (2 * a)
next_point = point + t * direction
# Check end condition (first quadrant)
if next_point[0] > 0 and next_point[1] > 0:
lines.add(Line(point, next_point).set_stroke(WHITE, 2))
break
lines.add(Line(point, next_point).set_stroke(WHITE, 2))
point = next_point
# Reflect direction
if i % 2 == 0:
direction[1] = -direction[1] # Bounce off x-axis
else:
# Reflect off circle (tangent)
normal = point[:2] / np.linalg.norm(point[:2])
normal = np.array([*normal, 0])
direction = direction - 2 * np.dot(direction, normal) * normal
return lines
@@ -0,0 +1,291 @@
"""
Complex S-Plane Visualization
Interactive visualization of exponential functions in the complex plane.
Shows how the parameter s affects growth, decay, and oscillation.
Run: manimgl complex_s_plane.py SPlaneVisualization -w
Preview: manimgl complex_s_plane.py SPlaneVisualization -p
Source: Adapted from 3b1b's Laplace transform video (2025)
"""
from manimlib import *
class SPlaneVisualization(InteractiveScene):
"""
Comprehensive s-plane visualization with:
- Complex s parameter with dot and label
- Output e^{st} on complex plane
- Real part graph over time
Key techniques:
- ComplexValueTracker for complex numbers
- Multiple synchronized planes
- Dynamic graph updating with bind_graph_to_func
- GlowDot for emphasis
"""
def construct(self):
# Trackers for s and t
s_tracker = ComplexValueTracker(-1)
t_tracker = ValueTracker(0)
get_s = s_tracker.get_value
get_t = t_tracker.get_value
# S-plane (input)
s_plane = self.create_s_plane()
s_dot, s_label = self.create_s_indicator(s_plane, get_s)
# Output plane (e^{st})
exp_plane = self.create_output_plane()
exp_label = self.create_output_label(exp_plane)
output_dot, output_label = self.create_output_indicator(exp_plane, get_s, get_t)
output_path = self.create_output_path(exp_plane, get_t, get_s)
# Graph of Re[e^{st}]
axes = self.create_graph_axes()
graph = self.create_dynamic_graph(axes, get_s)
v_line = self.create_graph_indicator(axes, get_t, get_s)
# Add everything
self.add(s_plane, s_dot, s_label)
self.add(exp_plane, exp_label, output_path, output_dot, output_label)
self.add(axes, graph, v_line)
# Store for later use
self.s_tracker = s_tracker
self.t_tracker = t_tracker
self.s_plane = s_plane
# Animate s exploration
self.explore_s_values()
def create_s_plane(self):
"""Create the s-plane (input plane)."""
plane = ComplexPlane((-2, 2), (-2, 2))
plane.set_width(7)
plane.to_edge(LEFT, buff=SMALL_BUFF)
plane.add_coordinate_labels(font_size=16)
return plane
def create_s_indicator(self, s_plane, get_s):
"""Create dot and label tracking s value."""
s_dot = Group(
Dot(radius=0.05, fill_color=YELLOW),
GlowDot(color=YELLOW),
)
s_dot.add_updater(lambda m: m.move_to(s_plane.n2p(get_s())))
s_label = Tex(R"s = +0.5", font_size=36)
s_rhs = s_label.make_number_changeable("+0.5")
s_rhs.f_always.set_value(get_s)
s_label.set_color(YELLOW)
s_label.set_backstroke(BLACK, 5)
s_label.always.next_to(s_dot[0], UR, SMALL_BUFF)
return Group(s_dot, s_label)
def create_output_plane(self):
"""Create the output plane showing e^{st}."""
plane = ComplexPlane((-2, 2), (-2, 2))
plane.background_lines.set_stroke(width=1)
plane.faded_lines.set_stroke(opacity=0.25)
plane.set_width(4)
plane.to_corner(DR).shift(0.5 * LEFT)
return plane
def create_output_label(self, exp_plane, font_size=60):
"""Label for output plane."""
label = Tex(R"e^{st}", font_size=font_size, t2c={"s": YELLOW, "t": BLUE})
label.set_backstroke(BLACK, 5)
label.next_to(exp_plane.get_corner(UL), DL, 0.2)
return label
def create_output_indicator(self, exp_plane, get_s, get_t):
"""Moving dot showing e^{st} value."""
output_dot = Group(
TrueDot(color=GREEN),
GlowDot(color=GREEN)
)
output_dot.add_updater(lambda m: m.move_to(
exp_plane.n2p(np.exp(get_s() * get_t()))
))
output_label = Tex(R"e^{s \cdot 0.00}", font_size=36, t2c={"s": YELLOW})
t_label = output_label.make_number_changeable("0.00")
t_label.set_color(BLUE)
t_label.f_always.set_value(get_t)
output_label.always.next_to(output_dot, UR, buff=SMALL_BUFF, aligned_edge=LEFT, index_of_submobject_to_align=0)
output_label.set_backstroke(BLACK, 3)
return Group(output_dot, output_label)
def create_output_path(self, exp_plane, get_t, get_s, delta_t=1/30, color=TEAL, stroke_width=2):
"""Traced path of e^{st} as t increases."""
path = VMobject()
path.set_points([ORIGIN])
path.set_stroke(color, stroke_width)
def get_path_points():
t_range = np.arange(0, get_t(), delta_t)
if len(t_range) == 0:
t_range = np.array([0])
values = np.exp(t_range * get_s())
return np.array([exp_plane.n2p(z) for z in values])
path.f_always.set_points_smoothly(get_path_points)
return path
def create_graph_axes(self):
"""Axes for plotting Re[e^{st}] over time."""
axes = Axes(
x_range=(0, 24),
y_range=(-2, 2),
width=15,
height=2
)
t_label = Tex(R"t", font_size=36, t2c={"t": BLUE})
y_label = Tex(R"\text{Re}\left[e^{st}\right]", font_size=36, t2c={"s": YELLOW, "t": BLUE})
t_label.next_to(axes.x_axis.get_right(), UP, buff=0.15)
y_label.next_to(axes.y_axis.get_top(), UP, SMALL_BUFF)
axes.add(t_label, y_label)
axes.next_to(ORIGIN, RIGHT, MED_LARGE_BUFF)
axes.to_edge(UP, buff=0.5)
return axes
def create_dynamic_graph(self, axes, get_s, stroke_color=TEAL, stroke_width=3):
"""Graph that updates based on current s value."""
graph = Line().set_stroke(stroke_color, stroke_width)
t_samples = np.arange(*axes.x_range[:2], 0.1)
def update_graph(graph):
s = get_s()
values = np.exp(s * t_samples)
xs = values.astype(np.complex128).real
graph.set_points_smoothly(axes.c2p(t_samples, xs))
graph.add_updater(update_graph)
return graph
def create_graph_indicator(self, axes, get_t, get_s):
"""Vertical line indicator on the graph."""
v_line = Line(DOWN, UP)
v_line.set_stroke(WHITE, 2)
v_line.f_always.put_start_and_end_on(
lambda: axes.c2p(get_t(), 0),
lambda: axes.c2p(get_t(), np.exp(get_s() * get_t()).real),
)
return v_line
def play_time_forward(self, duration, added_anims=[]):
"""Utility to animate time passing."""
self.t_tracker.set_value(0)
self.play(
self.t_tracker.animate.set_value(duration).set_anim_args(rate_func=linear),
*added_anims,
run_time=duration,
)
def explore_s_values(self):
"""Explore different s values and their effects."""
s_tracker = self.s_tracker
# Start with negative real (decay)
s_tracker.set_value(-1)
self.play(s_tracker.animate.set_value(0.2), run_time=4)
# Pure real = 0 (constant)
self.play(s_tracker.animate.set_value(0), run_time=2)
# Pure imaginary (oscillation)
self.play(s_tracker.animate.set_value(1j), run_time=3)
self.wait()
# Let time run
self.play_time_forward(3 * TAU)
self.wait()
# Reset time
self.play(self.t_tracker.animate.set_value(0), run_time=2)
# Complex with negative real (decaying oscillation)
self.play(s_tracker.animate.set_value(-0.2 + 1j), run_time=3)
self.play_time_forward(2 * TAU)
# Complex with positive real (growing oscillation)
self.t_tracker.set_value(0)
self.play(s_tracker.animate.set_value(0.1 + 1j), run_time=3)
self.play_time_forward(TAU)
class SPlaneRegions(InteractiveScene):
"""
Highlight different regions of the s-plane and their meaning:
- Right half: exponential growth
- Left half: exponential decay
- Imaginary axis: pure oscillation
"""
def construct(self):
# S-plane
plane = ComplexPlane((-3, 3), (-3, 3))
plane.set_height(6)
plane.add_coordinate_labels(font_size=20)
self.add(plane)
# Right half (growth)
right_half = Rectangle(width=plane.get_width()/2, height=plane.get_height())
right_half.set_fill(RED, 0.3)
right_half.set_stroke(width=0)
right_half.move_to(plane.n2p(1.5))
# Left half (decay)
left_half = Rectangle(width=plane.get_width()/2, height=plane.get_height())
left_half.set_fill(GREEN, 0.3)
left_half.set_stroke(width=0)
left_half.move_to(plane.n2p(-1.5))
# Imaginary axis highlight
imag_axis = Line(plane.n2p(-3j), plane.n2p(3j))
imag_axis.set_stroke(YELLOW, 4)
# Labels
growth_label = Text("Growth", color=RED)
growth_label.move_to(plane.n2p(1.5 + 2j))
decay_label = Text("Decay", color=GREEN)
decay_label.move_to(plane.n2p(-1.5 + 2j))
osc_label = Text("Oscillation", color=YELLOW)
osc_label.next_to(imag_axis, RIGHT)
osc_label.shift(UP)
# Animate
self.play(FadeIn(right_half), Write(growth_label))
self.wait()
self.play(FadeIn(left_half), Write(decay_label))
self.wait()
self.play(ShowCreation(imag_axis), Write(osc_label))
self.wait(2)
# Add sample points
sample_points = [
(1, RED, "Grows"),
(-1, GREEN, "Decays"),
(1j, YELLOW, "Oscillates"),
(-0.5 + 1j, TEAL, "Decays + Oscillates"),
]
dots = VGroup()
for s, color, label_text in sample_points:
dot = GlowDot(plane.n2p(s), color=color)
label = Text(label_text, font_size=24, color=color)
label.next_to(dot, UR, buff=0.1)
dots.add(VGroup(dot, label))
self.play(LaggedStartMap(FadeIn, dots, lag_ratio=0.5))
self.wait(2)
@@ -0,0 +1,128 @@
"""
Negative Log Loss (Cross-Entropy) cost function visualization.
Demonstrates: Graph plotting, labeled axes, mathematical expressions
"""
from manimlib import *
import numpy as np
class CostFunction(Scene):
def construct(self):
# Create axes
axes = Axes(
(0, 1, 0.1),
(0, 5, 1),
width=10,
height=6
)
axes.center().to_edge(LEFT)
axes.x_axis.add_numbers(num_decimal_places=1)
axes.y_axis.add_numbers(num_decimal_places=0, direction=LEFT)
# Add axis label
x_label = Tex("p")
x_label.next_to(axes.x_axis.get_right(), UR)
axes.add(x_label)
y_label = Text("Cost", font_size=36)
y_label.next_to(axes.y_axis.get_top(), RIGHT)
axes.add(y_label)
# Create the -log(p) graph
graph = axes.get_graph(
lambda x: -np.log(x) if x > 0.001 else 5,
x_range=(0.001, 1, 0.01)
)
graph.set_color(RED)
# Expression
expr = Tex(R"\text{Cost} = -\log(p)", font_size=60)
expr.to_edge(UP)
# Animate
self.play(FadeIn(axes))
self.wait(0.5)
self.play(
ShowCreation(graph, run_time=3),
Write(expr, run_time=2),
)
self.wait()
# Explanation labels
low_p_label = Text("Low probability\n= High cost", font_size=30, color=RED)
low_p_label.next_to(axes.i2gp(0.1, graph), RIGHT, buff=0.5)
high_p_label = Text("High probability\n= Low cost", font_size=30, color=GREEN)
high_p_label.next_to(axes.i2gp(0.8, graph), UP, buff=0.5)
self.play(FadeIn(low_p_label, shift=LEFT))
self.wait()
self.play(FadeIn(high_p_label, shift=DOWN))
self.wait()
# Show a moving dot on the curve
p_tracker = ValueTracker(0.5)
dot = Dot(color=YELLOW)
dot.f_always.move_to(lambda: axes.i2gp(p_tracker.get_value(), graph))
# Vertical line from x-axis to point
v_line = always_redraw(lambda: axes.get_line_from_axis_to_point(
0, axes.i2gp(p_tracker.get_value(), graph),
line_func=DashedLine
).set_stroke(YELLOW, 2))
# Horizontal line from y-axis to point
h_line = always_redraw(lambda: axes.get_line_from_axis_to_point(
1, axes.i2gp(p_tracker.get_value(), graph),
line_func=DashedLine
).set_stroke(YELLOW, 2))
# Value labels
p_label = VGroup(
Text("p = ", font_size=36),
DecimalNumber(p_tracker.get_value(), num_decimal_places=2, font_size=36)
)
p_label.arrange(RIGHT)
p_label.to_corner(UR)
p_label[1].f_always.set_value(p_tracker.get_value)
cost_label = VGroup(
Text("Cost = ", font_size=36),
DecimalNumber(-np.log(0.5), num_decimal_places=2, font_size=36)
)
cost_label.arrange(RIGHT)
cost_label.next_to(p_label, DOWN, aligned_edge=LEFT)
cost_label[1].f_always.set_value(lambda: -np.log(max(p_tracker.get_value(), 0.001)))
self.play(
FadeOut(low_p_label),
FadeOut(high_p_label),
FadeIn(dot),
FadeIn(v_line),
FadeIn(h_line),
FadeIn(p_label),
FadeIn(cost_label),
)
self.wait()
# Animate the dot moving
self.play(p_tracker.animate.set_value(0.1), run_time=2)
self.wait()
self.play(p_tracker.animate.set_value(0.9), run_time=3)
self.wait()
self.play(p_tracker.animate.set_value(0.05), run_time=2)
self.wait()
self.play(p_tracker.animate.set_value(0.5), run_time=2)
self.wait()
# Final message
message = Text(
"Goal: Maximize probability of correct answer",
font_size=36,
color=BLUE
)
message.to_edge(DOWN)
self.play(FadeIn(message, shift=UP))
self.wait(2)
@@ -0,0 +1,198 @@
"""
Visualization of 3D cube projection along the diagonal.
Shows how projecting a cube along the [1,1,1] direction creates a hexagonal pattern.
"""
from manimlib import *
import itertools as it
class CubeProjection3D(InteractiveScene):
"""
Demonstrates projecting a 3D cube along its main diagonal [1,1,1].
Shows:
1. Building the cube from vertices
2. Showing coordinates
3. Looking down the diagonal
4. The projected hexagonal pattern
5. Face projections
"""
def construct(self):
# Set axes
frame = self.frame
light_source = self.camera.light_source
frame.reorient(28, 68, 0, (0.99, 0.63, 0.66), 2.89)
light_source.move_to([3, 5, 7])
axes = ThreeDAxes(
(-3, 3), (-3, 3), (-3, 3),
axis_config=dict(tick_size=0.05)
)
axes.set_stroke(GREY_A, 1)
plane = NumberPlane((-3, 3), (-3, 3))
plane.axes.set_stroke(GREY_A, 1)
plane.background_lines.set_stroke(BLUE_E, 0.5)
plane.faded_lines.set_stroke(BLUE_E, 0.5, 0.25)
self.add(plane, axes)
# Add cube
vertices = np.array(list(it.product(*3 * [[0, 1]])))
vert_dots = DotCloud(vertices)
vert_dots.make_3d()
vert_dots.set_radius(0.025)
vert_dots.set_color(TEAL)
cube_shell = VGroup(
Line(vertices[i], vertices[j])
for i, p1 in enumerate(vertices)
for j, p2 in enumerate(vertices[i + 1:], start=i + 1)
if get_norm(p2 - p1) == 1
)
cube_shell.set_stroke(YELLOW, 1)
cube_shell.set_anti_alias_width(1)
cube_shell.set_width(1)
cube_shell.move_to(ORIGIN, [-1, -1, -1])
self.play(Write(cube_shell, lag_ratio=0.1, run_time=2))
self.wait()
# Show the coordinates
labels = VGroup()
for vert in vertices:
coords = vert.astype(int)
label = Tex(str(tuple(coords)), font_size=12)
label.next_to(vert, DR, buff=0.05)
label.rotate(45 * DEGREES, RIGHT, about_point=vert)
label.set_backstroke(BLACK, 2)
labels.add(label)
self.play(
LaggedStartMap(FadeIn, labels),
FadeIn(vert_dots),
frame.animate.reorient(10, 61, 0, (0.9, 0.51, 0.48), 2.44),
run_time=3,
)
self.wait()
# Show base and top square
edges = VGroup(*cube_shell)
edges.sort(lambda p: p[2])
self.play(
edges[4:].animate.set_stroke(width=0.5, opacity=0.25),
labels[1::2].animate.set_opacity(0.1)
)
self.wait()
self.play(
edges[8:].animate.set_stroke(width=2, opacity=1),
labels[1::2].animate.set_opacity(1),
edges[:4].animate.set_stroke(width=0.5, opacity=0.25),
labels[0::2].animate.set_opacity(0.1)
)
self.wait()
self.play(
edges.animate.set_stroke(width=1, opacity=1),
labels.animate.set_opacity(1)
)
self.play(FadeOut(labels))
# Orient to look down the corner
self.play(frame.animate.reorient(135.795, 55.795, 0, (-0.02, -0.08, 0.05), 3.61), run_time=4)
self.wait(2)
self.play(frame.animate.reorient(50, 68, 0, (-0.46, 0.29, 0.23), 3.45), run_time=4)
# Show the flat projection
diag_vect = Vector([1, 1, 1], thickness=2)
diag_vect.set_perpendicular_to_camera(frame)
diag_label = labels[-1].copy()
proj_mat = self.construct_proj_matrix()
proj_cube_shell = cube_shell.copy().apply_matrix(proj_mat)
proj_vert_dots = vert_dots.copy().apply_matrix(proj_mat)
self.play(
GrowArrow(diag_vect),
FadeIn(diag_label, shift=np.ones(3)),
cube_shell.animate.set_stroke(opacity=0.25),
)
self.wait()
self.play(
TransformFromCopy(cube_shell, proj_cube_shell),
TransformFromCopy(vert_dots, proj_vert_dots),
)
self.wait(3)
frame.save_state()
self.play(
frame.animate.reorient(134.75, 54.47, 0, (-0.46, 0.29, 0.23), 3.45).set_field_of_view(1 * DEGREES),
run_time=4
)
self.wait()
self.play(Restore(frame, run_time=3))
self.wait()
# Project more cubes down
cube_grid = VGroup(
cube_shell.copy().shift(vect)
for vect in it.product(*3 * [[0, 1, 2]])
)
cube_grid.remove(cube_grid[0])
proj_cube_grid = cube_grid.copy().apply_matrix(proj_mat)
proj_cube_grid.set_stroke(YELLOW, 2, 0.5)
ghost_cube = cube_shell.copy().set_opacity(0)
self.play(
LaggedStart(
(TransformFromCopy(ghost_cube, new_cube)
for new_cube in cube_grid),
lag_ratio=0.05,
),
frame.animate.reorient(40, 72, 0, (1.25, 1.69, 0.99), 5.10),
run_time=5
)
self.wait()
self.play(
TransformFromCopy(cube_grid, proj_cube_grid),
frame.animate.reorient(60, 68, 0, (0.81, 1.09, 0.94), 5.36),
run_time=3
)
self.wait()
self.play(
FadeOut(cube_grid),
FadeOut(proj_cube_grid),
FadeOut(diag_label),
FadeOut(diag_vect),
FadeOut(vert_dots),
FadeOut(proj_vert_dots),
frame.animate.reorient(42, 62, 0, (0.68, 0.48, 0.41), 2.34),
run_time=2,
)
# Show cube faces
cube = Cube()
cube.set_color(BLUE_E, 1)
cube.set_shading(0.75, 0.25, 0.5)
cube.replace(cube_shell)
cube.sort(lambda p: np.dot(p, np.ones(3)))
inner_faces = cube[:3]
for mob in [cube_shell, proj_cube_shell, plane]:
mob.apply_depth_test()
self.add(axes, cube, cube_shell, plane, proj_cube_shell)
self.play(
FadeIn(cube),
proj_cube_shell.animate.set_stroke(width=1, opacity=0.2),
)
self.wait(3)
def construct_proj_matrix(self):
diag = normalize(np.ones(3))
id3 = np.identity(3)
return np.array([self.project(basis, diag) for basis in id3]).T
def project(self, vect, unit_norm):
"""Project v1 onto the orthogonal subspace of norm"""
return vect - np.dot(unit_norm, vect) * unit_norm
@@ -0,0 +1,278 @@
"""
Damped Spring Solutions on S-Plane
Visualization of how the damped harmonic oscillator solutions
move in the complex s-plane as parameters change.
Run: manimgl damped_solutions_splane.py DampedSolutionsDemo -w
Preview: manimgl damped_solutions_splane.py DampedSolutionsDemo -p
Source: Adapted from 3b1b's Laplace transform video (2025)
"""
from manimlib import *
class DampedSolutionsDemo(InteractiveScene):
"""
Interactive visualization of damped spring solutions on the s-plane.
The characteristic equation ms^2 + μs + k = 0 has roots that:
- Stay on imaginary axis when μ=0 (undamped oscillation)
- Move into left half-plane as μ increases (damped oscillation)
- Become real when μ^2 > 4mk (overdamped)
Key techniques:
- Custom slider creation
- GlowDot for interactive points
- Dynamic function binding for graphs
- Real-time root calculation
"""
def construct(self):
# Add the complex plane
plane = ComplexPlane((-3, 2), (-2, 2))
plane.set_height(5)
plane.background_lines.set_stroke(BLUE, 1)
plane.faded_lines.set_stroke(BLUE, 0.5, 0.25)
plane.add_coordinate_labels(font_size=24)
plane.move_to(DOWN)
plane.to_edge(RIGHT, buff=1.0)
self.add(plane)
# Parameter sliders
colors = [interpolate_color_by_hsl(RED, TEAL, a) for a in np.linspace(0, 1, 3)]
chars = ["m", R"\mu", "k"]
m_slider, mu_slider, k_slider = sliders = VGroup(
self.create_slider(char, color)
for char, color in zip(chars, colors)
)
m_tracker, mu_tracker, k_tracker = trackers = Group(
slider.value_tracker for slider in sliders
)
sliders.arrange(RIGHT, buff=MED_LARGE_BUFF)
sliders.next_to(plane, UP, aligned_edge=LEFT)
# Initial values: m=1, μ=0, k=3
m_tracker.set_value(1)
mu_tracker.set_value(0)
k_tracker.set_value(3)
self.add(trackers)
self.add(sliders[0], sliders[2]) # Start without damping slider
# Root calculation
def get_roots():
a = m_tracker.get_value()
b = mu_tracker.get_value()
c = k_tracker.get_value()
# Characteristic equation: as^2 + bs + c = 0
# s = (-b ± sqrt(b^2 - 4ac)) / 2a
discriminant = b**2 - 4*a*c
if discriminant >= 0:
radical = math.sqrt(discriminant)
else:
radical = 1j * math.sqrt(-discriminant)
m = -b / (2*a)
return (m + radical / (2*a), m - radical / (2*a))
# Dots showing the roots
root_dots = GlowDot().replicate(2)
root_dots.set_color(YELLOW)
def update_dots(dots):
roots = get_roots()
for dot, root in zip(dots, roots):
dot.move_to(plane.n2p(root))
root_dots.add_updater(update_dots)
self.add(root_dots)
# Lines from a reference point
s_rhs_point = Point((-4.09, -1.0, 0.0))
def update_lines(lines):
for line, dot in zip(lines, root_dots):
line.put_start_and_end_on(s_rhs_point.get_center(), dot.get_center())
lines = Line().replicate(2)
lines.set_stroke(YELLOW, 2, 0.35)
lines.add_updater(update_lines)
# Show the roots moving as k changes (undamped case)
self.play(ShowCreation(lines, lag_ratio=0, suspend_mobject_updating=True))
self.play(k_tracker.animate.set_value(1), run_time=2)
self.play(m_tracker.animate.set_value(4), run_time=2)
self.wait()
self.play(k_tracker.animate.set_value(3), run_time=2)
self.play(m_tracker.animate.set_value(1), run_time=2)
self.wait()
# Now add damping
self.play(
VFadeOut(lines),
VFadeIn(sliders[1])
)
self.wait()
# Increase damping - roots move left
self.play(mu_tracker.animate.set_value(3), run_time=5)
self.wait()
# Decrease damping - roots approach imaginary axis
self.play(mu_tracker.animate.set_value(0.5), run_time=3)
self.play(ShowCreation(lines, lag_ratio=0, suspend_mobject_updating=True))
self.wait()
# Add solution graph
frame = self.frame
axes = Axes((0, 10, 1), (-1, 1, 1), width=10, height=3.5)
axes.next_to(plane, DOWN, MED_LARGE_BUFF, aligned_edge=LEFT)
def solution_func(t):
roots = get_roots()
# Real part of e^{s1*t} + e^{s2*t} (divided by 2 for normalization)
return 0.5 * (np.exp(roots[0] * t) + np.exp(roots[1] * t)).real
graph = axes.get_graph(solution_func)
graph.set_stroke(TEAL, 3)
axes.bind_graph_to_func(graph, solution_func)
graph_label = Tex(R"\text{Re}[e^{st}]", t2c={"s": YELLOW}, font_size=72)
graph_label.next_to(axes.get_corner(UL), DL)
self.play(
frame.animate.set_height(12, about_point=4 * UP + 2 * LEFT),
FadeIn(axes, time_span=(1.5, 3)),
ShowCreation(graph, suspend_mobject_updating=True, time_span=(1.5, 3)),
Write(graph_label),
run_time=3
)
self.wait()
# More parameter exploration
self.play(k_tracker.animate.set_value(1), run_time=2)
self.play(k_tracker.animate.set_value(4), run_time=2)
self.wait()
self.play(mu_tracker.animate.set_value(2), run_time=3)
self.play(k_tracker.animate.set_value(2), run_time=2)
self.wait()
# Show overdamped case
self.play(mu_tracker.animate.set_value(3.5), run_time=3)
self.play(k_tracker.animate.set_value(5), run_time=2)
self.wait()
# Return to underdamped
self.play(
mu_tracker.animate.set_value(0.5),
m_tracker.animate.set_value(3),
run_time=3
)
self.wait(2)
def create_slider(self, char_name, color=WHITE, x_range=(0, 5), height=1.5, font_size=36):
"""Create a vertical slider for a parameter."""
tracker = ValueTracker(0)
number_line = NumberLine(x_range, width=height, tick_size=0.05)
number_line.rotate(90 * DEG)
indicator = ArrowTip(width=0.1, length=0.2)
indicator.rotate(PI)
indicator.add_updater(lambda m: m.move_to(number_line.n2p(tracker.get_value()), LEFT))
indicator.set_color(color)
label = Tex(Rf"{char_name} = 0.00", font_size=font_size)
label[char_name].set_color(color)
label.rhs = label.make_number_changeable("0.00")
label.always.next_to(indicator, RIGHT, SMALL_BUFF)
label.rhs.f_always.set_value(tracker.get_value)
slider = VGroup(number_line, indicator, label)
slider.value_tracker = tracker
return slider
class OverdampedVsUnderdamped(InteractiveScene):
"""
Side-by-side comparison of overdamped and underdamped behavior.
"""
def construct(self):
# Two planes side by side
plane_underdamped = ComplexPlane((-2, 1), (-2, 2))
plane_overdamped = ComplexPlane((-2, 1), (-2, 2))
for plane in [plane_underdamped, plane_overdamped]:
plane.set_width(5)
plane.add_coordinate_labels(font_size=16)
planes = VGroup(plane_underdamped, plane_overdamped)
planes.arrange(RIGHT, buff=1)
planes.to_edge(UP)
# Labels
underdamped_label = Text("Underdamped", font_size=36, color=BLUE)
underdamped_label.next_to(plane_underdamped, DOWN)
overdamped_label = Text("Overdamped", font_size=36, color=RED)
overdamped_label.next_to(plane_overdamped, DOWN)
self.add(planes, underdamped_label, overdamped_label)
# Roots for underdamped: complex conjugates
underdamped_roots = [-0.5 + 1.5j, -0.5 - 1.5j]
underdamped_dots = VGroup(
GlowDot(plane_underdamped.n2p(r), color=BLUE)
for r in underdamped_roots
)
# Roots for overdamped: both real
overdamped_roots = [-0.3, -1.7]
overdamped_dots = VGroup(
GlowDot(plane_overdamped.n2p(r), color=RED)
for r in overdamped_roots
)
self.play(
LaggedStartMap(FadeIn, underdamped_dots),
LaggedStartMap(FadeIn, overdamped_dots),
)
self.wait()
# Graphs below
axes_underdamped = Axes((0, 8), (-1, 1), width=5, height=2)
axes_overdamped = Axes((0, 8), (-1, 1), width=5, height=2)
axes_underdamped.next_to(underdamped_label, DOWN)
axes_overdamped.next_to(overdamped_label, DOWN)
# Underdamped solution: decaying oscillation
def underdamped_func(t):
s = underdamped_roots[0]
return (np.exp(s * t)).real
# Overdamped solution: pure decay
def overdamped_func(t):
s1, s2 = overdamped_roots
return 0.5 * (np.exp(s1 * t) + np.exp(s2 * t))
graph_under = axes_underdamped.get_graph(underdamped_func)
graph_under.set_stroke(BLUE, 3)
graph_over = axes_overdamped.get_graph(overdamped_func)
graph_over.set_stroke(RED, 3)
self.add(axes_underdamped, axes_overdamped)
self.play(
ShowCreation(graph_under),
ShowCreation(graph_over),
run_time=3
)
self.wait(2)
@@ -0,0 +1,177 @@
"""
Dot Product Visualization
Interactive demonstration of how dot products work with two vectors.
Based on: videos/_2024/transformers/embedding.py - DotProducts
"""
from manimlib import *
class DotProductVisualization(InteractiveScene):
"""
Shows dot product calculation between two vectors in 2D.
The result updates dynamically as vectors are rotated.
"""
def construct(self):
# Set up coordinate plane
plane = NumberPlane(
(-4, 4), (-4, 4),
background_line_style=dict(
stroke_width=2,
stroke_opacity=0.5,
stroke_color=BLUE,
),
faded_line_ratio=1
)
plane.set_height(6)
plane.to_edge(LEFT, buff=0)
# Create two vectors
vects = VGroup(
Vector(0.5 * RIGHT + 2 * UP).set_stroke(MAROON_B, 6),
Vector(1.0 * RIGHT + 0.5 * UP).set_stroke(YELLOW, 6),
)
vects.shift(plane.get_center())
def get_dot_product():
coords = np.array([plane.p2c(v.get_end()) for v in vects])
return np.dot(coords[0], coords[1])
self.add(plane)
self.add(vects)
# Vector labels
vect_labels = VGroup(*(
Tex(Rf"\vec{{\textbf{{ {char} }} }}")
for char in "vw"
))
for label, vect in zip(vect_labels, vects):
label.vect = vect
label.match_color(vect)
label.add_updater(lambda m: m.move_to(
m.vect.get_end() + 0.25 * normalize(m.vect.get_vector())
))
self.add(vect_labels)
# Coordinate expressions
vect_coords = VGroup(*(
TexMatrix(
[
[char + f"_{{{str(n)}}}"]
for n in [1, 2, 3, 4, "n"]
],
bracket_h_buff=0.1,
ellipses_row=-2,
)
for char in "vw"
))
vect_coords.arrange(RIGHT, buff=0.75)
vect_coords.next_to(plane, RIGHT, buff=1)
vect_coords.set_y(1)
for coords, vect in zip(vect_coords, vects):
coords.get_entries().match_color(vect)
dot = Tex(R"\cdot", font_size=72)
dot.move_to(vect_coords)
self.add(vect_coords, dot)
# Result display
rhs = Tex("= +0.00", font_size=60)
rhs.next_to(vect_coords, RIGHT)
result = rhs.make_number_changeable("+0.00", include_sign=True)
result.add_updater(lambda m: m.set_value(get_dot_product()))
self.add(rhs)
# Label
brace = Brace(vect_coords, DOWN, buff=0.25)
dp_label = brace.get_text("Dot product", buff=0.25)
self.add(brace, dp_label)
# Helper function for dual rotation
def dual_rotate(angle1, angle2, run_time=2):
self.play(
Rotate(vects[0], angle1 * DEGREES, about_point=plane.get_origin()),
Rotate(vects[1], angle2 * DEGREES, about_point=plane.get_origin()),
run_time=run_time
)
# Demonstrate various configurations
dual_rotate(-20, 20)
dual_rotate(50, -60)
dual_rotate(0, 80)
dual_rotate(20, -80)
# Show computation breakdown
equals = rhs[0].copy()
entry_pairs = VGroup(*(
VGroup(*pair)
for pair in zip(*[vc.get_columns()[0] for vc in vect_coords])
))
prod_terms = entry_pairs.copy()
for src_pair, trg_pair in zip(entry_pairs, prod_terms):
trg_pair.arrange(RIGHT, buff=0.1)
trg_pair.next_to(equals, RIGHT, buff=0.5)
trg_pair.match_y(src_pair)
prod_terms[-2].space_out_submobjects(1e-3)
prod_terms[-2].match_x(prod_terms)
prod_terms.target = prod_terms.generate_target()
prod_terms.target.space_out_submobjects(1.5).match_y(vect_coords)
plusses = VGroup(*(
Tex("+", font_size=48).move_to(midpoint(m1.get_bottom(), m2.get_top()))
for m1, m2 in zip(prod_terms.target, prod_terms.target[1:])
))
rhs.target = rhs.generate_target()
rhs.target[0].rotate(PI / 2)
rhs.target.arrange(DOWN)
rhs.target.next_to(prod_terms, DOWN)
self.add(equals)
self.play(
LaggedStart(*(
TransformFromCopy(m1, m2)
for m1, m2 in zip(entry_pairs, prod_terms)
), lag_ratio=0.1, run_time=2),
MoveToTarget(rhs)
)
self.wait()
self.play(
MoveToTarget(prod_terms),
rhs.animate.next_to(prod_terms.target, DOWN),
LaggedStartMap(Write, plusses),
)
self.wait()
# Show positive dot product
dual_rotate(-65, 65)
self.play(FlashAround(result, time_width=1.5, run_time=3))
self.wait()
# Show orthogonal (zero dot product)
elbow = Elbow(width=0.25, angle=vects[0].get_angle())
elbow.shift(plane.get_origin())
zero = DecimalNumber(0)
zero.replace(result, 1)
dual_rotate(
(vects[1].get_angle() + PI / 2 - vects[0].get_angle()) / DEGREES,
0,
)
self.remove(result)
self.add(zero)
self.play(ShowCreation(elbow))
self.wait()
self.remove(elbow, zero)
self.add(result)
# Show negative dot product
dual_rotate(20, -60)
self.play(FlashAround(result, time_width=1.5, run_time=3))
self.wait()
# Final animation
dual_rotate(75, -95, run_time=5)
@@ -0,0 +1,327 @@
"""
Double Slit Interference Visualization
Demonstrates the classic double-slit experiment, showing how waves from two
slits interfere to create an interference pattern on a screen.
Based on 3Blue1Brown's diffraction visualizations.
Run: manimgl double_slit_interference.py DoubleSlitExperiment -w
"""
from manimlib import *
import numpy as np
class DoubleSlitExperiment(Scene):
"""
Visualizes the double-slit experiment with wave interference.
Shows plane wave hitting two slits and producing interference.
"""
def construct(self):
frame = self.camera.frame
# Create barrier with two slits
barrier_color = GREY_D
slit_separation = 2.0
slit_width = 0.15
# Create barrier pieces
barrier_y = -2
barrier_left = Rectangle(width=6, height=0.3, fill_color=barrier_color, fill_opacity=1)
barrier_left.set_stroke(WHITE, 1)
barrier_left.move_to([-(slit_separation/2 + 3 + slit_width), barrier_y, 0])
barrier_middle = Rectangle(width=slit_separation - 2*slit_width, height=0.3,
fill_color=barrier_color, fill_opacity=1)
barrier_middle.set_stroke(WHITE, 1)
barrier_middle.move_to([0, barrier_y, 0])
barrier_right = Rectangle(width=6, height=0.3, fill_color=barrier_color, fill_opacity=1)
barrier_right.set_stroke(WHITE, 1)
barrier_right.move_to([slit_separation/2 + 3 + slit_width, barrier_y, 0])
barrier = VGroup(barrier_left, barrier_middle, barrier_right)
# Slit positions
slit1_pos = np.array([-slit_separation/2, barrier_y, 0])
slit2_pos = np.array([slit_separation/2, barrier_y, 0])
# Mark the slits
slit1_marker = Dot(slit1_pos, color=RED, radius=0.1)
slit2_marker = Dot(slit2_pos, color=BLUE, radius=0.1)
# Screen to observe pattern
screen = Rectangle(width=0.2, height=6)
screen.set_fill(GREY_E, opacity=0.8)
screen.set_stroke(WHITE, 1)
screen.move_to([0, 4, 0])
# Wave parameters
wave_number = 2.0
frequency = 0.4
# Create incoming plane wave (simplified as horizontal lines)
def get_incoming_wave(time):
waves = VGroup()
for offset in np.arange(-10, 0, 0.5 / wave_number):
y = barrier_y - 1 + (time * frequency / wave_number + offset) % 3
if y < barrier_y - 0.2:
line = Line([-7, y, 0], [7, y, 0])
alpha = 1 - (barrier_y - y) / 3
line.set_stroke(TEAL, width=2, opacity=0.5 * alpha)
waves.add(line)
return waves
# Create outgoing waves from slits
def get_outgoing_waves(time):
rings = VGroup()
colors = [RED_B, BLUE_B]
positions = [slit1_pos, slit2_pos]
for pos, color in zip(positions, colors):
for phase_offset in np.arange(0, 12, 0.5 / wave_number):
radius = (time * frequency / wave_number + phase_offset)
if 0.1 < radius < 8:
# Only show upper semicircle
arc = Arc(
start_angle=0,
angle=PI,
radius=radius
)
arc.move_arc_center_to(pos)
amplitude = np.exp(-0.15 * radius)
arc.set_stroke(color, width=1.5 + 2 * amplitude, opacity=0.6 * amplitude)
rings.add(arc)
return rings
# Create intensity pattern on screen
def get_intensity_pattern(time):
dots = VGroup()
screen_y = 4
for x in np.linspace(-3, 3, 120):
point = np.array([x, screen_y, 0])
# Calculate path difference
r1 = np.linalg.norm(point - slit1_pos)
r2 = np.linalg.norm(point - slit2_pos)
# Interference
phase1 = TAU * (wave_number * r1 - frequency * time)
phase2 = TAU * (wave_number * r2 - frequency * time)
amp1 = np.cos(phase1) / np.sqrt(1 + 0.1 * r1)
amp2 = np.cos(phase2) / np.sqrt(1 + 0.1 * r2)
total_intensity = ((amp1 + amp2) / 2) ** 2
# Create dot
dot = Dot([x, screen_y - 0.1 + 0.2 * total_intensity, 0], radius=0.03)
brightness = 0.2 + 0.8 * total_intensity
dot.set_fill(interpolate_color(BLACK, WHITE, brightness), opacity=1)
dots.add(dot)
return dots
time_tracker = ValueTracker(0)
incoming = always_redraw(lambda: get_incoming_wave(time_tracker.get_value()))
outgoing = always_redraw(lambda: get_outgoing_waves(time_tracker.get_value()))
intensity = always_redraw(lambda: get_intensity_pattern(time_tracker.get_value()))
# Title
title = Text("Double Slit Interference", font_size=48)
title.to_corner(UL)
title.set_backstroke(BLACK, 5)
# Labels
incoming_label = Text("Incoming Wave", font_size=24)
incoming_label.next_to(barrier, DOWN, buff=0.5)
incoming_label.set_backstroke(BLACK, 3)
screen_label = Text("Detection Screen", font_size=24)
screen_label.next_to(screen, RIGHT)
screen_label.set_backstroke(BLACK, 3)
# Add elements
self.add(title)
self.add(barrier)
self.add(slit1_marker, slit2_marker)
self.add(screen)
self.add(incoming)
self.add(outgoing)
self.add(intensity)
self.add(incoming_label, screen_label)
# Animate
self.play(
time_tracker.animate.set_value(30),
run_time=15,
rate_func=linear
)
self.wait()
class PathDifferenceExplanation(Scene):
"""
Explains the path difference concept in interference.
Shows how different path lengths lead to phase differences.
"""
def construct(self):
# Two source points
source1 = Dot(2 * LEFT + 2 * DOWN, color=RED, radius=0.15)
source2 = Dot(2 * RIGHT + 2 * DOWN, color=BLUE, radius=0.15)
source1_label = Text("S1", font_size=24, color=RED).next_to(source1, DOWN)
source2_label = Text("S2", font_size=24, color=BLUE).next_to(source2, DOWN)
# Target point
target = Dot(UP, color=YELLOW, radius=0.15)
target_label = Text("P", font_size=24, color=YELLOW).next_to(target, UP)
# Path lines
path1 = Line(source1.get_center(), target.get_center(), color=RED)
path2 = Line(source2.get_center(), target.get_center(), color=BLUE)
# Distance labels
d1 = path1.get_length()
d2 = path2.get_length()
d1_label = Tex(f"d_1", color=RED, font_size=36)
d1_label.move_to(path1.get_center() + 0.5 * LEFT)
d2_label = Tex(f"d_2", color=BLUE, font_size=36)
d2_label.move_to(path2.get_center() + 0.5 * RIGHT)
# Title
title = Text("Path Difference and Interference", font_size=42)
title.to_edge(UP)
# Add elements
self.add(title)
self.play(
FadeIn(source1), FadeIn(source2),
Write(source1_label), Write(source2_label)
)
self.play(FadeIn(target), Write(target_label))
self.play(
ShowCreation(path1), ShowCreation(path2),
Write(d1_label), Write(d2_label)
)
self.wait()
# Path difference formula
formula = Tex(
R"\Delta d = d_2 - d_1",
font_size=48
)
formula.to_edge(DOWN)
formula.shift(UP)
self.play(Write(formula))
self.wait()
# Show constructive case
constructive_text = Text("Constructive: path diff = n * wavelength", font_size=32)
constructive_text.next_to(formula, DOWN)
constructive_text.set_color(GREEN)
self.play(Write(constructive_text))
self.wait(2)
# Show destructive case
destructive_text = Text("Destructive: path diff = (n + 1/2) * wavelength", font_size=32)
destructive_text.next_to(constructive_text, DOWN)
destructive_text.set_color(PINK)
self.play(Write(destructive_text))
self.wait(2)
class DiffractionGratingSimple(Scene):
"""
Simplified diffraction grating visualization showing multiple slits.
"""
def construct(self):
frame = self.camera.frame
# Parameters
n_slits = 8
slit_spacing = 0.8
barrier_y = -2
wave_number = 3.0
frequency = 0.3
# Create barrier with multiple slits
barrier_pieces = VGroup()
slit_positions = []
total_width = n_slits * slit_spacing
for i in range(n_slits + 1):
x_pos = -total_width / 2 + i * slit_spacing - slit_spacing / 4
piece = Rectangle(width=slit_spacing / 2, height=0.3)
piece.set_fill(GREY_D, opacity=1)
piece.set_stroke(WHITE, 1)
piece.move_to([x_pos, barrier_y, 0])
barrier_pieces.add(piece)
# Track slit positions (between pieces)
if i < n_slits:
slit_x = -total_width / 2 + i * slit_spacing + slit_spacing / 4
slit_positions.append(np.array([slit_x, barrier_y, 0]))
# Slit markers
slit_markers = VGroup(
Dot(pos, color=YELLOW, radius=0.05)
for pos in slit_positions
)
# Create outgoing waves from all slits
def get_grating_waves(time):
rings = VGroup()
for pos in slit_positions:
for phase_offset in np.arange(0, 8, 0.4 / wave_number):
radius = (time * frequency / wave_number + phase_offset)
if 0.1 < radius < 6:
arc = Arc(
start_angle=0,
angle=PI,
radius=radius
)
arc.move_arc_center_to(pos)
amplitude = np.exp(-0.2 * radius)
arc.set_stroke(BLUE, width=1 + amplitude, opacity=0.3 * amplitude)
rings.add(arc)
return rings
time_tracker = ValueTracker(0)
waves = always_redraw(lambda: get_grating_waves(time_tracker.get_value()))
# Title
title = Text("Diffraction Grating", font_size=48)
title.to_edge(UP)
title.set_backstroke(BLACK, 5)
# Spacing label
spacing_arrow = DoubleArrow(
slit_positions[0] + 0.5 * DOWN,
slit_positions[1] + 0.5 * DOWN,
buff=0
)
spacing_arrow.set_color(WHITE)
d_label = Tex("d", font_size=36)
d_label.next_to(spacing_arrow, DOWN, buff=0.1)
self.add(title)
self.add(barrier_pieces)
self.add(slit_markers)
self.add(waves)
self.add(spacing_arrow, d_label)
# Animate
self.play(
time_tracker.animate.set_value(25),
run_time=15,
rate_func=linear
)
self.wait()
@@ -0,0 +1,234 @@
"""
Eigenvalue Equations
====================
Shows the key mathematical equations for eigenvalues and eigenvectors.
Demonstrates LaTeX typesetting with color coding for mathematical concepts.
Key concepts:
- Eigenvalue equation: Av = lambda * v
- Diagonalization: A = S * D * S^(-1)
- Change of basis transformation
"""
from manimlib import *
class EigenvalueEquations(Scene):
"""
Displays the fundamental eigenvalue/eigenvector equations
with proper color coding to highlight mathematical relationships.
"""
def construct(self):
# Title
title = Text("Eigenvalue Equations", font_size=48)
title.to_edge(UP)
self.play(Write(title))
# Main eigenvalue equation
eigen_eq = Tex(
R"A \vec{\mathbf{v}} = \lambda \vec{\mathbf{v}}",
font_size=60
)
eigen_eq.set_color_by_tex(R"\lambda", TEAL)
eigen_eq.set_color_by_tex(R"\vec{\mathbf{v}}", YELLOW)
# Description
eigen_desc = Text(
"Eigenvector is scaled by eigenvalue",
font_size=28
)
eigen_desc.set_color(GREY_B)
eigen_group = VGroup(eigen_eq, eigen_desc)
eigen_group.arrange(DOWN, buff=0.3)
eigen_group.next_to(title, DOWN, buff=0.8)
self.play(Write(eigen_eq))
self.play(FadeIn(eigen_desc, shift=UP * 0.3))
self.wait()
# Move up and show diagonalization
self.play(
eigen_group.animate.shift(UP * 0.5).scale(0.8)
)
# Diagonalization equation
diag_eq = Tex(
R"A = S \Lambda S^{-1}",
font_size=48
)
diag_eq.set_color_by_tex(R"\Lambda", TEAL)
diag_eq.set_color_by_tex("S", YELLOW)
# Where clause
where_clause = Tex(
R"\text{where } \Lambda = "
R"\begin{bmatrix} \lambda_1 & 0 \\ 0 & \lambda_2 \end{bmatrix}",
font_size=36
)
where_clause.set_color_by_tex(R"\lambda_1", TEAL)
where_clause.set_color_by_tex(R"\lambda_2", YELLOW)
# S matrix explanation
s_clause = Tex(
R"S = \begin{bmatrix} \vert & \vert \\ "
R"\vec{\mathbf{v}}_1 & \vec{\mathbf{v}}_2 \\ "
R"\vert & \vert \end{bmatrix}",
font_size=36
)
s_clause.set_color_by_tex(R"\vec{\mathbf{v}}_1", TEAL)
s_clause.set_color_by_tex(R"\vec{\mathbf{v}}_2", YELLOW)
diag_group = VGroup(diag_eq, where_clause, s_clause)
diag_group.arrange(DOWN, buff=0.4, aligned_edge=LEFT)
diag_group.next_to(eigen_group, DOWN, buff=0.6)
self.play(Write(diag_eq))
self.wait(0.5)
self.play(FadeIn(where_clause, shift=UP * 0.2))
self.wait(0.5)
self.play(FadeIn(s_clause, shift=UP * 0.2))
self.wait(2)
class DiagonalMatrixPowers(Scene):
"""
Shows the key insight: diagonal matrices are easy to raise to powers.
This makes computing A^n efficient when A is diagonalizable.
"""
def construct(self):
# Title
title = Text("Power of Diagonal Matrices", font_size=42)
title.to_edge(UP)
self.add(title)
# Show diagonal matrix power
diag_power = Tex(
R"\begin{bmatrix} \lambda_1 & 0 \\ 0 & \lambda_2 \end{bmatrix}^n = "
R"\begin{bmatrix} \lambda_1^n & 0 \\ 0 & \lambda_2^n \end{bmatrix}",
font_size=44,
t2c={R"\lambda_1": TEAL, R"\lambda_2": YELLOW}
)
diag_power.next_to(title, DOWN, buff=0.8)
self.play(Write(diag_power))
self.wait()
# Therefore A^n equation
therefore = Tex(
R"\therefore \quad A^n = S \Lambda^n S^{-1}",
font_size=40
)
therefore.next_to(diag_power, DOWN, buff=0.6)
self.play(Write(therefore))
self.wait()
# Example with Fibonacci matrix
fib_title = Text("Example: Fibonacci Matrix", font_size=32)
fib_title.next_to(therefore, DOWN, buff=0.8)
fib_matrix = Tex(
R"A = \begin{bmatrix} 0 & 1 \\ 1 & 1 \end{bmatrix}",
font_size=36
)
fib_matrix.next_to(fib_title, DOWN, buff=0.3)
fib_result = Tex(
R"A^n \begin{bmatrix} 0 \\ 1 \end{bmatrix} = "
R"\begin{bmatrix} F_n \\ F_{n+1} \end{bmatrix}",
font_size=36
)
fib_result.next_to(fib_matrix, DOWN, buff=0.3)
self.play(Write(fib_title))
self.play(Write(fib_matrix))
self.wait(0.5)
self.play(Write(fib_result))
self.wait(2)
class ChangeOfBasisVisualization(Scene):
"""
Shows how the change of basis matrix S transforms coordinates
between standard basis and eigenbasis.
"""
def construct(self):
# Title
title = Text("Change of Basis", font_size=42)
title.to_edge(UP)
# Main equation
cob_eq = Tex(
R"x \hat{\mathbf{i}} + y \hat{\mathbf{j}} = "
R"\tilde{x} \vec{\mathbf{v}}_1 + \tilde{y} \vec{\mathbf{v}}_2",
font_size=40,
t2c={
R"\hat{\mathbf{i}}": GREEN,
R"\hat{\mathbf{j}}": RED,
R"\vec{\mathbf{v}}_1": TEAL,
R"\vec{\mathbf{v}}_2": YELLOW,
}
)
cob_eq.next_to(title, DOWN, buff=0.6)
# Standard basis label
std_label = Text("Standard Basis", font_size=24, color=GREY_B)
std_label.next_to(cob_eq[:6], DOWN, buff=0.3)
# Eigenbasis label
eigen_label = Text("Eigenbasis", font_size=24, color=GREY_B)
eigen_label.next_to(cob_eq[7:], DOWN, buff=0.3)
# Show transformation
self.play(Write(title))
self.play(Write(cob_eq))
self.play(
FadeIn(std_label, shift=UP * 0.2),
FadeIn(eigen_label, shift=UP * 0.2),
)
self.wait()
# Simplified ODE in eigenbasis
ode_title = Text("ODE becomes simple in eigenbasis:", font_size=28)
ode_title.next_to(eigen_label, DOWN, buff=0.8)
ode_original = Tex(
R"\frac{d}{dt}\begin{bmatrix} x \\ y \end{bmatrix} = "
R"A \begin{bmatrix} x \\ y \end{bmatrix}",
font_size=32
)
ode_original.next_to(ode_title, DOWN, buff=0.3)
arrow = Tex(R"\Downarrow", font_size=40)
arrow.next_to(ode_original, DOWN, buff=0.3)
ode_simple = Tex(
R"\frac{d}{dt}\begin{bmatrix} \tilde{x} \\ \tilde{y} \end{bmatrix} = "
R"\begin{bmatrix} \lambda_1 & 0 \\ 0 & \lambda_2 \end{bmatrix}"
R"\begin{bmatrix} \tilde{x} \\ \tilde{y} \end{bmatrix}",
font_size=32,
t2c={R"\lambda_1": TEAL, R"\lambda_2": YELLOW}
)
ode_simple.next_to(arrow, DOWN, buff=0.3)
self.play(Write(ode_title))
self.play(Write(ode_original))
self.play(Write(arrow))
self.play(Write(ode_simple))
self.wait()
# Solution
solution = Tex(
R"\tilde{x}(t) = \tilde{x}_0 e^{\lambda_1 t}, \quad "
R"\tilde{y}(t) = \tilde{y}_0 e^{\lambda_2 t}",
font_size=32,
t2c={R"\lambda_1": TEAL, R"\lambda_2": YELLOW}
)
solution.next_to(ode_simple, DOWN, buff=0.5)
self.play(Write(solution))
self.wait(2)
@@ -0,0 +1,189 @@
"""
Eigenvector Flow Field
======================
Visualizes the flow of a linear dynamical system dx/dt = Ax.
The eigenvectors appear as special directions where flow stays on a line.
This demonstrates:
- VectorField for showing derivative directions
- StreamLines for animated flow
- Computing eigenvalues/eigenvectors with numpy
- Linear algebra visualization
"""
from manimlib import *
class EigenvectorFlowField(Scene):
"""
Shows the vector field for a linear ODE system dx/dt = Ax.
The eigenvectors are the special directions where trajectories
move straight outward or inward.
"""
def construct(self):
# Define the matrix for our linear system
mat = np.array([[1, 2], [3, 1]])
# Create coordinate plane
plane = NumberPlane((-4, 4), (-4, 4), faded_line_ratio=1)
plane.set_height(FRAME_HEIGHT)
plane.background_lines.set_stroke(BLUE, 1)
plane.faded_lines.set_stroke(BLUE, 0.5, 0.5)
plane.add_coordinate_labels(font_size=36)
self.add(plane)
# Define the derivative function for the linear system
def deriv_func(x, y):
"""Returns the derivative at a point: f(v) = Av"""
v = np.array([x, y])
result = 0.5 * np.dot(mat, v)
return result[0], result[1]
# Create vector field manually using arrows
vector_field = VGroup()
for x in np.linspace(-3.5, 3.5, 12):
for y in np.linspace(-3.5, 3.5, 12):
if abs(x) < 0.4 and abs(y) < 0.4:
continue # Skip origin area
dx, dy = deriv_func(x, y)
start = plane.c2p(x, y)
direction = np.array([dx, dy, 0])
norm = np.linalg.norm(direction)
if norm > 0.1:
# Normalize and scale for visibility
direction = direction / norm * min(0.5, norm * 0.3)
end = start + direction
arrow = Arrow(
start, end, buff=0,
stroke_width=2,
max_tip_length_to_length_ratio=0.3
)
# Color based on magnitude
alpha = min(1, norm / 3)
arrow.set_color(interpolate_color(BLUE, RED, alpha))
vector_field.add(arrow)
# Show vector field
self.play(
LaggedStartMap(GrowArrow, vector_field, lag_ratio=0.01),
run_time=2
)
self.wait(2)
# Calculate eigenvectors
eigenvalues, eigenvectors = np.linalg.eig(mat)
# Create eigenvalue lines (extended versions of eigenvectors)
eigenlines = VGroup()
eigen_labels = VGroup()
for i, (ev, eigval) in enumerate(zip(eigenvectors.T, eigenvalues)):
# Create the line
line = Line(-ev, ev)
line.set_length(15)
color = [TEAL, YELLOW][i]
line.set_stroke(color, 5)
eigenlines.add(line)
# Create label
label = Tex(
R"\lambda_" + str(i + 1) + f" = {eigval:.2f}",
font_size=30
)
label.set_color(color)
label.set_backstroke(width=5)
# Position label at end of eigenvector
label.next_to(plane.c2p(*(ev * 2)), RIGHT if ev[0] > 0 else LEFT)
eigen_labels.add(label)
# Show eigenvector lines with labels
self.play(
LaggedStartMap(ShowCreation, eigenlines, lag_ratio=0.3),
run_time=2
)
self.play(LaggedStartMap(FadeIn, eigen_labels, lag_ratio=0.3))
# Let it run for a while to see the flow
self.wait(8)
class LinearSystemPhasePortrait(Scene):
"""
Shows different types of equilibria based on eigenvalues:
- Both positive: unstable node (expanding)
- Both negative: stable node (contracting)
- Mixed signs: saddle point
"""
def construct(self):
# Create three small phase portraits
matrices = [
np.array([[2, 0], [0, 1]]), # Unstable node (both positive)
np.array([[-2, 0], [0, -1]]), # Stable node (both negative)
np.array([[2, 0], [0, -1]]), # Saddle point (mixed)
]
titles = [
"Unstable Node",
"Stable Node",
"Saddle Point",
]
subtitle_data = [
(r"\lambda_1 > 0, \lambda_2 > 0", GREEN),
(r"\lambda_1 < 0, \lambda_2 < 0", RED),
(r"\lambda_1 > 0, \lambda_2 < 0", YELLOW),
]
portraits = VGroup()
for mat, title, (subtitle, color) in zip(matrices, titles, subtitle_data):
portrait = self.create_phase_portrait(mat)
label = Text(title, font_size=24)
label.next_to(portrait, UP)
eigen_label = Tex(subtitle, font_size=20)
eigen_label.set_color(color)
eigen_label.next_to(portrait, DOWN)
group = VGroup(portrait, label, eigen_label)
portraits.add(group)
portraits.arrange(RIGHT, buff=0.5)
portraits.set_width(FRAME_WIDTH - 1)
main_title = Text("Phase Portraits by Eigenvalue Type", font_size=36)
main_title.to_edge(UP)
self.add(main_title)
self.play(LaggedStartMap(FadeIn, portraits, lag_ratio=0.3))
self.wait(3)
def create_phase_portrait(self, mat):
"""Create a small phase portrait for a given matrix."""
plane = NumberPlane(
(-2, 2), (-2, 2),
background_line_style={"stroke_width": 1, "stroke_opacity": 0.5}
)
plane.set_height(3)
def func(point):
v = np.array([point[0], point[1]])
result = mat @ v
return np.array([result[0], result[1], 0]) * 0.3
# Just show arrows, no animation for static display
arrows = VGroup()
for x in np.linspace(-1.5, 1.5, 5):
for y in np.linspace(-1.5, 1.5, 5):
if abs(x) < 0.3 and abs(y) < 0.3:
continue
start = plane.c2p(x, y)
deriv = func(np.array([x, y, 0]))
if np.linalg.norm(deriv) > 0.1:
deriv = deriv / np.linalg.norm(deriv) * 0.3
end = start + deriv
arrow = Arrow(start, end, buff=0, stroke_width=2, max_tip_length_to_length_ratio=0.3)
arrow.set_color(interpolate_color(BLUE, RED, (np.linalg.norm(deriv) / 0.5)))
arrows.add(arrow)
return VGroup(plane, arrows)
@@ -0,0 +1,185 @@
"""
Eigenvector Matrix Transformation
=================================
Demonstrates how a matrix transformation looks in standard basis vs eigenbasis.
In the eigenbasis, the transformation becomes a simple scaling along each axis.
Key concepts demonstrated:
- Matrix transformation of a number plane
- Eigenvector computation with numpy
- Change of basis visualization
- Updated vectors that follow coordinate system changes
"""
from manimlib import *
class EigenvectorMatrixTransformation(Scene):
"""
Shows a matrix transformation in two perspectives:
1. Standard basis (i-hat, j-hat) - complex shearing transformation
2. Eigenbasis - simple scaling along eigenvector directions
"""
def construct(self):
# Define the transformation matrix
# This matrix has eigenvalues -1 and 3
mat = np.array([[1, 2], [3, 1]])
# Create ghost plane to show original grid
ghost_plane = NumberPlane(faded_line_ratio=0)
ghost_plane.set_stroke(GREY, 1)
# Create main plane that will be transformed
plane = self.get_plane()
# Create basis vectors that update with the plane
basis = VGroup(
self.get_updated_vector((1, 0), plane, GREEN),
self.get_updated_vector((0, 1), plane, RED),
)
# Add label
title = Text("Standard Basis Transformation", font_size=36)
title.to_corner(UL)
title.set_backstroke(width=5)
self.add(ghost_plane, plane, basis, title)
# Animate the transformation in standard basis
self.play(
plane.animate.apply_matrix(mat),
run_time=4
)
self.wait()
# Fade out standard basis view
self.play(FadeOut(VGroup(ghost_plane, plane, basis, title)))
# Now show the same transformation in eigenbasis
# Calculate eigenvectors
eigenvalues, eigenvectors = np.linalg.eig(mat)
# Create a plane already in the eigenbasis
eigenplane = self.get_plane()
eigenplane.apply_matrix(eigenvectors, about_point=ORIGIN)
# Create eigenbasis vectors
eigenbasis = VGroup(
self.get_updated_vector((1, 0), eigenplane, TEAL),
self.get_updated_vector((0, 1), eigenplane, YELLOW),
)
# Add new title
eigen_title = Text("Eigenbasis Transformation", font_size=36)
eigen_title.to_corner(UL)
eigen_title.set_backstroke(width=5)
# Show eigenvalue labels
eigen_labels = VGroup(
Tex(R"\lambda_1 = " + f"{eigenvalues[0]:.1f}", font_size=30).set_color(TEAL),
Tex(R"\lambda_2 = " + f"{eigenvalues[1]:.1f}", font_size=30).set_color(YELLOW),
)
eigen_labels.arrange(DOWN, aligned_edge=LEFT)
eigen_labels.to_corner(UR)
eigen_labels.set_backstroke(width=5)
self.add(eigenplane, eigenbasis, eigen_title, eigen_labels)
# In eigenbasis, transformation is just scaling by eigenvalues!
self.play(
eigenplane.animate.apply_matrix(mat),
run_time=4
)
self.wait()
def get_plane(self, x_range=(-16, 16), y_range=(-8, 8)):
"""Create a number plane for visualization."""
return NumberPlane(x_range, y_range, faded_line_ratio=1)
def get_updated_vector(self, coords, coord_system, color=YELLOW, thickness=4, **kwargs):
"""
Create a vector that automatically updates its position based on
the coordinate system it's attached to. This is useful for showing
how basis vectors transform with the plane.
"""
vect = Vector(RIGHT, fill_color=color, thickness=thickness, **kwargs)
vect.add_updater(lambda m: m.put_start_and_end_on(
coord_system.get_origin(),
coord_system.c2p(*coords),
))
return vect
class EigenvectorScaling(Scene):
"""
Shows that eigenvectors only get scaled by their eigenvalue.
Multiple vectors are shown - eigenvectors stay on their line,
other vectors rotate.
"""
def construct(self):
# Matrix with eigenvalues 4 and -1
mat = np.array([[1, 2], [3, 1]])
eigenvalues, eigenvectors = np.linalg.eig(mat)
# Create coordinate plane
plane = NumberPlane((-4, 4), (-4, 4))
plane.set_height(6)
plane.add_coordinate_labels(font_size=24)
# Create eigenvector lines (extended to infinity)
eigenlines = VGroup()
for i, ev in enumerate(eigenvectors.T):
line = Line(-ev * 5, ev * 5)
line.set_stroke([TEAL, YELLOW][i], 3, 0.5)
eigenlines.add(line)
# Create test vectors - some along eigenvectors, some not
test_vectors = VGroup()
colors = [TEAL, YELLOW, BLUE, RED, PURPLE]
directions = [
eigenvectors.T[0], # First eigenvector direction
eigenvectors.T[1], # Second eigenvector direction
np.array([1, 0]), # Standard basis i
np.array([0, 1]), # Standard basis j
np.array([1, 1]) / np.sqrt(2), # Diagonal
]
for direction, color in zip(directions, colors):
vect = Arrow(
plane.c2p(0, 0),
plane.c2p(*direction),
buff=0,
fill_color=color,
stroke_width=3,
)
test_vectors.add(vect)
# Labels
title = Text("Eigenvectors Stay on Their Line", font_size=36)
title.to_corner(UL)
title.set_backstroke(width=5)
self.add(plane, eigenlines, title)
self.play(LaggedStartMap(GrowArrow, test_vectors, lag_ratio=0.2))
self.wait()
# Transform all vectors
transformed_vectors = VGroup()
for i, (direction, color) in enumerate(zip(directions, colors)):
new_dir = mat @ direction
new_vect = Arrow(
plane.c2p(0, 0),
plane.c2p(*new_dir),
buff=0,
fill_color=color,
stroke_width=3,
)
transformed_vectors.add(new_vect)
self.play(
Transform(test_vectors, transformed_vectors),
run_time=3
)
self.wait(2)
@@ -0,0 +1,306 @@
"""
Elastic collision visualization with velocity vectors and conservation equations.
Shows how kinetic energy and momentum are conserved during collisions.
Based on the famous 3b1b pi-computing collision video.
"""
from manimlib import *
import math
LITTLE_BLOCK_COLOR = "#51463E"
class StateTracker(ValueTracker):
"""
Tracks the state of the block collision process.
"""
def __init__(self, blocks, initial_positions=[8, 5], initial_velocities=[-1, 0]):
sqrt_m1, sqrt_m2 = self.sqrt_mass_vect = np.sqrt([b.mass for b in blocks])
self.theta = math.atan2(sqrt_m2, sqrt_m1)
self.state0 = np.array([
*np.array(initial_positions) * self.sqrt_mass_vect,
*np.array(initial_velocities) * self.sqrt_mass_vect,
])
super().__init__(self.state0.copy())
def set_time(self, t):
pos0 = self.state0[0:2]
vel0 = self.state0[2:4]
self.set_value([*(pos0 + t * vel0), *vel0])
def rotate_2d(self, vect, angle):
c, s = math.cos(angle), math.sin(angle)
return np.array([c * vect[0] - s * vect[1], s * vect[0] + c * vect[1]])
def reflect_vect(self, vect):
n_reflections = self.get_n_collisions()
rot_angle = -2 * self.theta * ((n_reflections + 1) // 2)
result = self.rotate_2d(vect, rot_angle)
result[1] *= (-1)**(n_reflections % 2)
return result
def get_block_positions(self):
scaled_pos = self.get_value()[0:2]
rot_scaled_pos = self.reflect_vect(scaled_pos)
return rot_scaled_pos / self.sqrt_mass_vect
def get_block_velocities(self):
return self.reflect_vect(self.get_value()[2:4]) / self.sqrt_mass_vect
def get_kinetic_energy(self):
v1, v2 = self.get_value()[2:4]
return v1**2 + v2**2
def get_momentum(self):
v1, v2 = self.get_block_velocities()
m1, m2 = self.sqrt_mass_vect**2
return m1 * v1 + m2 * v2
def get_n_collisions(self):
state = self.get_value()
angle = math.atan2(state[1], state[0])
return int(angle / self.theta)
class ElasticCollisionVectors(Scene):
"""
Visualization of elastic collision with velocity vectors.
Shows conservation of kinetic energy and momentum.
"""
initial_positions = [10.5, 8]
initial_velocities = [-0.975, 0]
masses = [10, 1]
widths = [1.0, 0.5]
colors = [BLUE_E, LITTLE_BLOCK_COLOR]
def construct(self):
# Create floor and wall
floor, wall = self.get_floor_and_wall()
self.add(floor, wall)
# Create blocks
blocks = self.get_blocks(floor)
self.add(blocks)
# Set up state tracking
state_tracker = StateTracker(blocks, self.initial_positions, self.initial_velocities)
time_tracker = ValueTracker(0)
state_tracker.add_updater(lambda m: m.set_time(time_tracker.get_value()))
# Bind blocks to state
min_x = floor.get_x(LEFT) + blocks[1].get_width()
def update_blocks(blocks):
pos = state_tracker.get_block_positions()
blocks[0].set_x(min_x + pos[0], LEFT)
blocks[1].set_x(min_x + pos[1], RIGHT)
blocks.add_updater(update_blocks)
self.add(state_tracker, time_tracker)
# Set up equations
kw = dict(t2c={
"m_1": BLUE,
"m_2": BLUE,
"v_1": RED,
"v_2": RED,
})
ke_equation = Tex(R"\frac{1}{2} m_1 (v_1)^2 + \frac{1}{2}m_2 (v_2)^2 = E", **kw)
p_equation = Tex(R"m_1 v_1 + m_2 v_2 = P", **kw)
equations = VGroup(ke_equation, p_equation)
equations.arrange(DOWN, buff=0.5)
equations.to_corner(UL, buff=0.5)
self.add(equations)
# Create velocity vectors
velocity_vectors = VGroup(
self.get_velocity_vector(blocks[0], lambda: state_tracker.get_block_velocities()[0]),
self.get_velocity_vector(blocks[1], lambda: state_tracker.get_block_velocities()[1]),
)
self.add(velocity_vectors)
# Add collision counter
count_label = Tex(R"\# \text{Collisions} = 0", font_size=36)
count = count_label.make_number_changeable("0")
count.add_updater(lambda m: m.set_value(state_tracker.get_n_collisions()))
count_label.next_to(equations, DOWN, buff=0.5, aligned_edge=LEFT)
self.add(count_label)
# Run simulation
self.play(
time_tracker.animate.set_value(12),
run_time=12,
rate_func=linear,
)
self.wait()
# Show changing velocities
dec_equation = Tex(R"\frac{1}{2}(10)(+0.00)^2 + \frac{1}{2}(1)(+0.00)^2 = +0.00", font_size=36)
terms = dec_equation.make_number_changeable("+0.00", replace_all=True, include_sign=True)
dec_equation.next_to(ke_equation, DOWN, LARGE_BUFF)
dec_equation["(1)"].set_color(BLUE)
dec_equation["(10)"].set_color(BLUE)
terms[:2].set_color(RED)
terms[0].add_updater(lambda m: m.set_value(state_tracker.get_block_velocities()[0]))
terms[1].add_updater(lambda m: m.set_value(state_tracker.get_block_velocities()[1]))
terms[2].set_value(state_tracker.get_kinetic_energy())
self.add(dec_equation)
self.play(
time_tracker.animate.increment_value(10),
run_time=10,
rate_func=linear,
)
self.wait()
def get_floor_and_wall(self, width=13, height=2, stroke_width=2, buff_to_bottom=0.75):
floor = Line(LEFT, RIGHT)
floor.set_width(width)
floor.to_edge(DOWN, buff=buff_to_bottom)
dl_point = floor.get_left()
wall = Line(ORIGIN, UP)
wall.set_height(height)
wall.move_to(dl_point, DOWN)
ticks = VGroup()
tick_spacing = 0.5
tick_vect = 0.25 * DL
for y in np.arange(tick_spacing, height + tick_spacing, tick_spacing):
start = dl_point + y * UP
ticks.add(Line(start, start + tick_vect))
result = VGroup(floor, VGroup(wall, ticks))
result.set_stroke(WHITE, stroke_width)
return result
def get_blocks(self, floor):
blocks = Group()
for mass, color, width in zip(self.masses, self.colors, self.widths):
block = Square()
block.set_stroke(WHITE, 2)
block.set_fill(color, 1)
block.set_width(width)
block.next_to(floor, UP, buff=0.01)
block.mass = mass
mass_label = Tex(R"10 \, \text{kg}", font_size=24)
mass_label.make_number_changeable("10", edge_to_fix=RIGHT).set_value(mass)
mass_label.next_to(block, UP, buff=SMALL_BUFF)
block.add(mass_label)
block.mass_label = mass_label
blocks.add(block)
return blocks
def get_velocity_vector(
self,
block,
vel_function,
scale_factor=0.5,
max_width=1.0,
):
"""Create a velocity vector that follows a block."""
vector = Vector(RIGHT, thickness=2)
vector.set_fill(RED)
vector.set_backstroke(BLACK, 1)
def update_vector(vector):
start = block.get_top() + 0.1 * UP
vel = vel_function()
width = max_width * math.tanh(scale_factor * abs(vel))
if width > 0.05:
direction = RIGHT if vel > 0 else LEFT
vector.put_start_and_end_on(start, start + width * direction)
vector.set_opacity(1)
else:
vector.set_opacity(0)
return vector
vector.add_updater(update_vector)
label = DecimalNumber(0, num_decimal_places=2, font_size=18)
label.set_fill(RED)
label.set_backstroke(BLACK, 1)
label.add_updater(lambda m: m.set_value(vel_function()).next_to(
vector.get_start(), UP, buff=0.1
))
return VGroup(vector, label)
class MomentumConservation(ElasticCollisionVectors):
"""
Focuses on momentum conservation visualization.
"""
def construct(self):
# Create floor and wall
floor, wall = self.get_floor_and_wall()
self.add(floor, wall)
# Create blocks
blocks = self.get_blocks(floor)
self.add(blocks)
# Set up state tracking
state_tracker = StateTracker(blocks, self.initial_positions, self.initial_velocities)
time_tracker = ValueTracker(0)
state_tracker.add_updater(lambda m: m.set_time(time_tracker.get_value()))
# Bind blocks to state
min_x = floor.get_x(LEFT) + blocks[1].get_width()
def update_blocks(blocks):
pos = state_tracker.get_block_positions()
blocks[0].set_x(min_x + pos[0], LEFT)
blocks[1].set_x(min_x + pos[1], RIGHT)
blocks.add_updater(update_blocks)
self.add(state_tracker, time_tracker)
# Momentum equation
kw = dict(t2c={"m_1": BLUE, "m_2": BLUE, "v_1": RED, "v_2": RED})
p_equation = Tex(R"m_1 v_1 + m_2 v_2 = P", **kw)
p_equation.to_corner(UL)
self.add(p_equation)
# Numerical momentum
p_dec_equation = Tex(R"(10)(+0.00) + (1)(+0.00) = +0.00", font_size=42)
p_terms = p_dec_equation.make_number_changeable("+0.00", replace_all=True, include_sign=True)
p_terms[:2].set_color(RED)
p_dec_equation["(1)"].set_color(BLUE)
p_dec_equation["(10)"].set_color(BLUE)
p_dec_equation.next_to(p_equation, DOWN, buff=0.75)
p_terms[0].add_updater(lambda m: m.set_value(state_tracker.get_block_velocities()[0]))
p_terms[1].add_updater(lambda m: m.set_value(state_tracker.get_block_velocities()[1]))
p_terms[2].add_updater(lambda m: m.set_value(state_tracker.get_momentum()))
# Velocity vectors
velocity_vectors = VGroup(
self.get_velocity_vector(blocks[0], lambda: state_tracker.get_block_velocities()[0]),
self.get_velocity_vector(blocks[1], lambda: state_tracker.get_block_velocities()[1]),
)
self.add(velocity_vectors)
# Counter
count_label = Tex(R"\# \text{Collisions} = 0", font_size=36)
count = count_label.make_number_changeable("0")
count.add_updater(lambda m: m.set_value(state_tracker.get_n_collisions()))
count_label.next_to(p_dec_equation, DOWN, buff=0.5, aligned_edge=LEFT)
self.add(count_label)
self.add(p_dec_equation)
# Run simulation
self.play(
time_tracker.animate.set_value(20),
run_time=15,
rate_func=linear,
)
self.wait()
@@ -0,0 +1,170 @@
"""
Embedding Matrix Visualization
Shows how words map to vectors via an embedding matrix lookup.
Based on: videos/_2024/transformers/embedding.py - IntroduceEmbeddingMatrix
"""
from manimlib import *
class EmbeddingMatrixScene(InteractiveScene):
"""
Visualizes the embedding matrix concept:
- Words as columns
- Each column is a word's vector representation
"""
def construct(self):
# Sample vocabulary
words = [
'aah', 'aardvark', 'aardwolf', 'aargh', 'ab',
'aback', 'abacterial', 'abacus', 'abalone', 'abandon',
'zygoid', 'zygomatic', 'zygomorphic', 'zygosis', 'zygote',
'zygotic', 'zyme', 'zymogen', 'zymosis', 'zzz'
]
# Create word list
dots = Tex(R"\vdots")
shown_words = VGroup(
*map(Text, words[:10]),
dots,
*map(Text, words[-10:]),
)
shown_words.arrange(DOWN, aligned_edge=LEFT)
dots.match_x(shown_words[:5])
shown_words.set_height(FRAME_HEIGHT - 1)
shown_words.move_to(LEFT)
shown_words.set_fill(border_width=0)
brace = Brace(shown_words, RIGHT)
brace_text = brace.get_tex(R"\text{All words}")
# Animate words appearing
self.play(
LaggedStartMap(FadeIn, shown_words, shift=0.5 * LEFT, lag_ratio=0.1, run_time=2),
GrowFromCenter(brace, time_span=(0.5, 2.0)),
FadeIn(brace_text, time_span=(0.5, 1.5)),
)
self.wait()
# Create embedding matrix
dots_index = shown_words.submobjects.index(dots)
matrix = WeightMatrix(
shape=(8, len(shown_words)),
ellipses_col=dots_index
)
matrix.set_width(13)
matrix.center()
columns = matrix.get_columns()
matrix_name = Text("Embedding Matrix", font_size=72)
matrix_name.next_to(matrix, DOWN, buff=0.5)
# Transform words to matrix columns
shown_words.target = shown_words.generate_target()
shown_words.target.rotate(PI / 2)
shown_words.target.next_to(matrix, UP)
for word, column in zip(shown_words.target, columns):
word.match_x(column)
word.rotate(-45 * DEGREES, about_edge=DOWN)
shown_words.target[dots_index].rotate(45 * DEGREES).move_to(
shown_words.target[dots_index - 1:dots_index + 2]
)
new_brace = Brace(shown_words.target, UP, buff=0.0)
# Create column highlight rectangles
column_rects = VGroup(*(
SurroundingRectangle(column, buff=0.05)
for column in columns
))
column_rects.set_stroke(WHITE, 1)
# Animate matrix formation
self.play(
MoveToTarget(shown_words),
brace.animate.become(new_brace),
brace_text.animate.next_to(new_brace, UP, buff=0.1),
LaggedStart(*(
Write(column, lag_ratio=0.01, stroke_width=1)
for column in columns
), lag_ratio=0.2, run_time=2),
LaggedStartMap(FadeIn, matrix.get_brackets(), scale=0.5, lag_ratio=0)
)
self.play(Write(matrix_name, run_time=1))
self.wait()
# Highlight columns one by one
last_rect = VMobject()
for index in range(min(8, len(columns))):
for group in shown_words, columns:
group.target = group.generate_target()
group.target.set_opacity(0.2)
group.target[index].set_opacity(1)
rect = column_rects[index]
self.play(
*map(MoveToTarget, [shown_words, columns]),
FadeIn(rect),
FadeOut(last_rect),
run_time=0.5
)
last_rect = rect
self.wait(0.25)
# Reset opacity
self.play(
FadeOut(last_rect),
shown_words.animate.set_opacity(1),
columns.animate.set_opacity(1),
)
# Add matrix label W_E
frame = self.frame
lhs = Tex("W_E = ", font_size=72)
lhs.next_to(matrix, LEFT)
self.play(
frame.animate.set_width(FRAME_WIDTH + 3, about_edge=RIGHT),
Write(lhs)
)
self.wait()
# Highlight a single word lookup
index = words.index("aardvark")
word = shown_words[index].copy()
vector = VGroup(
matrix.get_brackets()[0],
matrix.get_columns()[index],
matrix.get_brackets()[1],
).copy()
# Animate pulling out the vector
vector.target = vector.generate_target()
vector.target.arrange(RIGHT, buff=0.1)
vector.target.set_height(4)
vector.target.move_to(3 * RIGHT + DOWN)
word.target = word.generate_target()
word.target.rotate(-45 * DEGREES)
word.target.scale(2)
word.target.next_to(vector.target, LEFT, buff=1.5)
arrow = Arrow(word.target, vector.target)
# Scale down matrix and show lookup
matrix_group = VGroup(lhs, matrix, shown_words, matrix_name)
self.play(
matrix_group.animate.scale(0.5).to_corner(UL),
FadeOut(brace, UP),
FadeOut(brace_text, 0.5 * UP),
MoveToTarget(word),
MoveToTarget(vector),
GrowFromPoint(arrow, word.get_center()),
run_time=2
)
self.wait()
# Add lookup label
lookup_label = Text("Embedding Lookup", font_size=48)
lookup_label.to_edge(DOWN)
self.play(Write(lookup_label))
self.wait(2)
@@ -0,0 +1,383 @@
"""
Equation Transforms and Mathematical Derivations
Shows step-by-step equation manipulation with highlighting,
the hallmark of 3b1b's mathematical explanations.
Run: manimgl equation_transforms.py QuadraticFormula -w
Preview: manimgl equation_transforms.py QuadraticFormula -p
Source: Inspired by 3b1b's equation transformation style
"""
from manimlib import *
class QuadraticFormula(InteractiveScene):
"""
Derives the quadratic formula step by step with
color-coded terms and smooth transformations.
"""
def construct(self):
# Color scheme for terms
colors = {
"a": RED,
"b": GREEN,
"c": BLUE,
"x": YELLOW,
}
# Step 1: Start with general quadratic
eq1 = Tex(
r"ax^2 + bx + c = 0",
t2c={"a": RED, "b": GREEN, "c": BLUE, "x": YELLOW}
)
eq1.to_edge(UP, buff=1)
self.play(Write(eq1))
self.wait()
# Step 2: Divide by a
eq2 = Tex(
r"x^2 + \frac{b}{a}x + \frac{c}{a} = 0",
t2c={"a": RED, "b": GREEN, "c": BLUE, "x": YELLOW}
)
eq2.next_to(eq1, DOWN, buff=0.8)
step1_label = Text("Divide by a", font_size=24, color=GREY)
step1_label.next_to(eq2, LEFT, buff=0.5)
self.play(
TransformMatchingTex(eq1.copy(), eq2),
FadeIn(step1_label, LEFT),
)
self.wait()
# Step 3: Complete the square
eq3 = Tex(
r"\left(x + \frac{b}{2a}\right)^2 - \frac{b^2}{4a^2} + \frac{c}{a} = 0",
t2c={"a": RED, "b": GREEN, "c": BLUE, "x": YELLOW}
)
eq3.next_to(eq2, DOWN, buff=0.8)
step2_label = Text("Complete the square", font_size=24, color=GREY)
step2_label.next_to(eq3, LEFT, buff=0.5)
self.play(
TransformMatchingTex(eq2.copy(), eq3),
FadeIn(step2_label, LEFT),
)
self.wait()
# Step 4: Isolate the squared term
eq4 = Tex(
r"\left(x + \frac{b}{2a}\right)^2 = \frac{b^2 - 4ac}{4a^2}",
t2c={"a": RED, "b": GREEN, "c": BLUE, "x": YELLOW}
)
eq4.next_to(eq3, DOWN, buff=0.8)
step3_label = Text("Rearrange", font_size=24, color=GREY)
step3_label.next_to(eq4, LEFT, buff=0.5)
self.play(
TransformMatchingTex(eq3.copy(), eq4),
FadeIn(step3_label, LEFT),
)
self.wait()
# Step 5: Take square root
eq5 = Tex(
r"x + \frac{b}{2a} = \pm\frac{\sqrt{b^2 - 4ac}}{2a}",
t2c={"a": RED, "b": GREEN, "c": BLUE, "x": YELLOW}
)
eq5.next_to(eq4, DOWN, buff=0.8)
step4_label = Text("Square root", font_size=24, color=GREY)
step4_label.next_to(eq5, LEFT, buff=0.5)
self.play(
TransformMatchingTex(eq4.copy(), eq5),
FadeIn(step4_label, LEFT),
)
self.wait()
# Final formula with box
final = Tex(
r"x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}",
t2c={"a": RED, "b": GREEN, "c": BLUE, "x": YELLOW},
font_size=60
)
final.next_to(eq5, DOWN, buff=1)
box = SurroundingRectangle(final, color=GOLD, buff=0.2)
self.play(
TransformMatchingTex(eq5.copy(), final),
)
self.play(ShowCreation(box))
self.wait(2)
class HighlightAndTransform(InteractiveScene):
"""
Demonstrates the technique of highlighting parts of equations
before transforming them. A core 3b1b pattern.
"""
def construct(self):
# Start with an equation
eq = Tex(r"(a + b)^2 = a^2 + 2ab + b^2", font_size=48)
eq.center()
self.play(Write(eq))
self.wait()
# Highlight LHS
lhs = eq[r"(a + b)^2"]
lhs_rect = SurroundingRectangle(lhs, color=YELLOW, buff=0.1)
self.play(ShowCreation(lhs_rect))
self.wait()
# Highlight RHS parts one by one
parts = [
(r"a^2", RED),
(r"2ab", GREEN),
(r"b^2", BLUE),
]
rects = []
for tex, color in parts:
part = eq[tex]
rect = SurroundingRectangle(part, color=color, buff=0.05)
self.play(ShowCreation(rect))
rects.append(rect)
self.wait(0.5)
# Fade out rectangles
self.play(
FadeOut(lhs_rect),
*[FadeOut(r) for r in rects]
)
# Show visual proof
self.play(eq.animate.to_edge(UP))
# Create squares
side = 2
a_frac = 0.6
a_side = side * a_frac
b_side = side * (1 - a_frac)
# The big square (a+b)^2
big_square = Square(side)
big_square.set_stroke(WHITE, 2)
big_square.center()
# Subdivisions
a_sq = Square(a_side)
a_sq.set_fill(RED, 0.5)
a_sq.set_stroke(WHITE, 1)
a_sq.align_to(big_square, UL)
b_sq = Square(b_side)
b_sq.set_fill(BLUE, 0.5)
b_sq.set_stroke(WHITE, 1)
b_sq.align_to(big_square, DR)
ab_rect1 = Rectangle(width=a_side, height=b_side)
ab_rect1.set_fill(GREEN, 0.5)
ab_rect1.set_stroke(WHITE, 1)
ab_rect1.next_to(a_sq, RIGHT, buff=0)
ab_rect2 = Rectangle(width=b_side, height=a_side)
ab_rect2.set_fill(GREEN, 0.5)
ab_rect2.set_stroke(WHITE, 1)
ab_rect2.next_to(a_sq, DOWN, buff=0)
squares = VGroup(a_sq, b_sq, ab_rect1, ab_rect2)
# Labels
a_label = Tex("a^2", color=RED, font_size=24)
a_label.move_to(a_sq)
b_label = Tex("b^2", color=BLUE, font_size=24)
b_label.move_to(b_sq)
ab_label1 = Tex("ab", color=GREEN, font_size=20)
ab_label1.move_to(ab_rect1)
ab_label2 = Tex("ab", color=GREEN, font_size=20)
ab_label2.move_to(ab_rect2)
self.play(ShowCreation(big_square))
self.play(
FadeIn(a_sq), Write(a_label),
FadeIn(ab_rect1), Write(ab_label1),
FadeIn(ab_rect2), Write(ab_label2),
FadeIn(b_sq), Write(b_label),
run_time=2
)
self.wait(2)
class BraceAnnotations(InteractiveScene):
"""
Uses braces to annotate and explain equation parts.
Another signature 3b1b technique.
"""
def construct(self):
# Main equation
eq = Tex(
r"F = ma",
font_size=96
)
eq.center()
self.play(Write(eq))
self.wait()
# Add braces with labels
F_brace = Brace(eq["F"], UP, color=BLUE)
F_label = F_brace.get_text("Force", font_size=30)
F_label.set_color(BLUE)
m_brace = Brace(eq["m"], DOWN, color=RED)
m_label = m_brace.get_text("Mass", font_size=30)
m_label.set_color(RED)
a_brace = Brace(eq["a"], DOWN, color=GREEN)
a_label = a_brace.get_text("Acceleration", font_size=30)
a_label.set_color(GREEN)
self.play(
GrowFromCenter(F_brace),
FadeIn(F_label, UP),
)
self.wait()
self.play(
GrowFromCenter(m_brace),
FadeIn(m_label, DOWN),
)
self.wait()
self.play(
GrowFromCenter(a_brace),
FadeIn(a_label, DOWN),
)
self.wait()
# Fade all and show rearrangement
all_braces = VGroup(F_brace, F_label, m_brace, m_label, a_brace, a_label)
eq2 = Tex(r"a = \frac{F}{m}", font_size=96)
eq2.center()
self.play(FadeOut(all_braces))
self.play(TransformMatchingTex(eq, eq2))
self.wait()
# New annotation
new_brace = Brace(eq2[r"\frac{F}{m}"], DOWN, color=YELLOW)
new_label = new_brace.get_text("Force per unit mass", font_size=24)
new_label.set_color(YELLOW)
self.play(
GrowFromCenter(new_brace),
FadeIn(new_label, DOWN),
)
self.wait(2)
class ColorCodedSubstitution(InteractiveScene):
"""
Shows variable substitution with color tracking.
Makes complex substitutions easy to follow.
"""
def construct(self):
# Define substitution
sub_def = Tex(
r"u = x^2 + 1",
t2c={"u": RED, "x": BLUE}
)
sub_def.to_edge(UP)
self.play(Write(sub_def))
self.wait()
# Original integral
integral1 = Tex(
r"\int 2x(x^2 + 1)^3 \, dx",
t2c={"x": BLUE},
font_size=48
)
integral1.center()
self.play(Write(integral1))
self.wait()
# Highlight the u part
u_part = integral1[r"(x^2 + 1)"]
u_rect = SurroundingRectangle(u_part, color=RED, buff=0.05)
self.play(ShowCreation(u_rect))
self.wait()
# Show du
du_def = Tex(
r"du = 2x \, dx",
t2c={"u": RED, "x": BLUE}
)
du_def.next_to(sub_def, DOWN)
# Highlight the 2x dx part
dx_part = integral1[r"2x"]
dx_rect = SurroundingRectangle(dx_part, color=GREEN, buff=0.05)
self.play(
Write(du_def),
ShowCreation(dx_rect),
)
self.wait()
# Transform to u integral
integral2 = Tex(
r"\int u^3 \, du",
t2c={"u": RED},
font_size=48
)
integral2.center()
self.play(
FadeOut(u_rect),
FadeOut(dx_rect),
TransformMatchingTex(integral1, integral2),
)
self.wait()
# Solve
solution = Tex(
r"= \frac{u^4}{4} + C",
t2c={"u": RED},
font_size=48
)
solution.next_to(integral2, DOWN, buff=0.5)
self.play(Write(solution))
self.wait()
# Substitute back
final = Tex(
r"= \frac{(x^2+1)^4}{4} + C",
t2c={"x": BLUE},
font_size=48
)
final.next_to(solution, DOWN, buff=0.5)
self.play(
TransformMatchingTex(solution.copy(), final),
)
self.wait(2)
@@ -0,0 +1,274 @@
"""
Exponential Function and Its Derivative
Demonstrates the fundamental property that d/dt e^t = e^t
with tangent line visualization and moving point.
Run: manimgl exponential_derivative.py ExpDerivative -w
Preview: manimgl exponential_derivative.py ExpDerivative -p
Source: Adapted from 3b1b's Laplace transform video (2025)
"""
from manimlib import *
class ExpDerivative(InteractiveScene):
"""
Visual demonstration of the exponential function's defining property.
Key techniques:
- get_v_line_to_graph for vertical lines
- get_tangent_line for derivative visualization
- always updaters for dynamic positioning
- make_number_changeable for live value displays
"""
def construct(self):
# Set up graph
axes = Axes(
x_range=(-1, 4),
y_range=(0, 20),
width=10,
height=6
)
axes.to_edge(RIGHT)
# Axis label
t_label = Tex("t")
t_label.next_to(axes.x_axis.get_right(), UL, MED_SMALL_BUFF)
axes.add(t_label)
# The exponential graph
graph = axes.get_graph(np.exp)
graph.set_stroke(BLUE, 3)
# Title showing the defining property
title = Tex(
R"\frac{d}{dt} e^t = e^t",
t2c={"t": GREY_B},
font_size=60
)
title.to_edge(UP)
title.match_x(axes.c2p(1.5, 0))
self.add(axes, graph, title)
# Tracker for the point on the graph
t_tracker = ValueTracker(1)
get_t = t_tracker.get_value
# Vertical line showing height e^t
v_line = always_redraw(
lambda: axes.get_v_line_to_graph(get_t(), graph, line_func=Line)
.set_stroke(RED, 3)
)
# Height label
height_label = Tex(R"e^t", font_size=42)
height_label.always.next_to(v_line, RIGHT, SMALL_BUFF)
# Constrain label size when line is short
height_label_height = height_label.get_height()
height_label.add_updater(lambda m: m.set_height(
min(height_label_height, 0.7 * v_line.get_height())
))
# Animate the height visualization
self.play(
ShowCreation(v_line, suspend_mobject_updating=True),
FadeIn(height_label, UP, suspend_mobject_updating=True),
)
self.wait()
# Add tangent line showing the derivative
tangent_line = always_redraw(
lambda: axes.get_tangent_line(get_t(), graph, length=10)
.set_stroke(BLUE_A, 1)
)
# Show "1" run on the tangent line
unit_size = axes.x_axis.get_unit_size()
unit_line = Line(axes.c2p(0, 0), axes.c2p(1, 0))
unit_line.add_updater(lambda m: m.move_to(v_line.get_end(), LEFT))
unit_line.set_stroke(WHITE, 2)
unit_label = Integer(1, font_size=24)
unit_label.add_updater(lambda m: m.next_to(unit_line.pfp(0.6), UP, 0.5 * SMALL_BUFF))
# Vertical rise = slope * 1 = derivative value
tan_v_line = always_redraw(
lambda: v_line.copy().shift(v_line.get_vector() + unit_size * RIGHT)
)
# Label for the derivative (rise of tangent)
deriv_label = Tex(R"\frac{d}{dt} e^t = e^t", font_size=42)
deriv_label[R"\frac{d}{dt}"].scale(0.75, about_edge=RIGHT)
deriv_label_height = deriv_label.get_height()
deriv_label.add_updater(lambda m: m.set_height(
min(deriv_label_height, 0.8 * v_line.get_height())
))
deriv_label.always.next_to(tan_v_line, RIGHT, SMALL_BUFF)
# Show the tangent line
self.play(ShowCreation(tangent_line, suspend_mobject_updating=True))
# Show unit run and derivative rise
self.play(
VFadeIn(unit_line),
VFadeIn(unit_label),
VFadeIn(tan_v_line, suspend_mobject_updating=True),
TransformFromCopy(title, deriv_label),
)
# Animate the height = derivative correspondence
self.play(
ReplacementTransform(
v_line.copy().clear_updaters(),
tan_v_line,
path_arc=45 * DEG
),
FadeTransform(height_label.copy(), deriv_label["e^t"][1], path_arc=45 * DEG, remover=True),
)
self.wait()
# Move the point around to show consistency
for t in [2.35, 0, 1, 2]:
self.play(t_tracker.animate.set_value(t), run_time=4)
self.wait()
class ExpFamilyGraph(InteractiveScene):
"""
Show family of exponentials e^{st} for different values of s.
When s > 0: growth, s < 0: decay, s = 0: constant.
"""
def construct(self):
# Axes
axes = Axes(
x_range=(-1, 8),
y_range=(-1, 5),
width=FRAME_WIDTH - 2,
height=FRAME_HEIGHT - 1.5
)
axes.to_edge(DOWN)
# Parameter tracker
s_tracker = ValueTracker(0.5)
get_s = s_tracker.get_value
# Dynamic graph
graph = axes.get_graph(lambda t: np.exp(t))
graph.set_stroke(BLUE, 3)
axes.bind_graph_to_func(graph, lambda t: np.exp(get_s() * t))
# Label
label = Tex(R"e^{st}", font_size=90)
label.move_to(UP)
label["s"].set_color(YELLOW)
# s value display
s_label = Tex(R"s = 0.50", font_size=48)
s_label["s"].set_color(YELLOW)
s_value = s_label.make_number_changeable("0.50")
s_value.add_updater(lambda m: m.set_value(get_s()))
s_label.to_corner(UR)
self.add(axes, label, s_label)
# Draw initial graph
self.play(ShowCreation(graph, suspend_mobject_updating=True))
self.wait()
# Vary s through different regimes
self.play(
s_tracker.animate.set_value(-1),
graph.animate.set_color(YELLOW),
run_time=4
)
self.wait()
self.play(s_tracker.animate.set_value(0), run_time=2)
self.wait()
self.play(
s_tracker.animate.set_value(0.3),
graph.animate.set_color(GREEN),
run_time=2
)
self.wait()
self.play(s_tracker.animate.set_value(0.5), run_time=2)
self.wait(2)
class ComplexExpSpiral(InteractiveScene):
"""
Visualize e^{(a+bi)t} as a spiral in the complex plane.
Shows how real part controls growth/decay, imaginary controls rotation.
"""
def construct(self):
# Complex plane
plane = ComplexPlane(
x_range=(-3, 3),
y_range=(-3, 3),
background_line_style=dict(stroke_color=BLUE, stroke_width=1),
)
plane.set_height(6)
plane.to_edge(LEFT)
plane.add_coordinate_labels(font_size=20)
self.add(plane)
# s = a + bi tracker
s_tracker = ComplexValueTracker(-0.1 + 1j)
get_s = s_tracker.get_value
# Time tracker
t_tracker = ValueTracker(0)
get_t = t_tracker.get_value
# Moving point
dot = GlowDot(color=TEAL)
dot.add_updater(lambda m: m.move_to(plane.n2p(np.exp(get_s() * get_t()))))
# Traced path
path = TracedPath(dot.get_center, stroke_color=TEAL, stroke_width=2)
# Vector from origin
vector = Vector(fill_color=YELLOW)
vector.add_updater(lambda m: m.put_start_and_end_on(
plane.n2p(0),
plane.n2p(np.exp(get_s() * get_t()))
))
# s value display
s_label = Tex(R"s = -0.10 + 1.00i", font_size=36)
s_label.to_corner(UR)
# Expression
exp_label = Tex(R"e^{st}", font_size=60)
exp_label["s"].set_color(YELLOW)
exp_label.next_to(plane, UP)
self.add(exp_label, s_label)
self.add(vector, path, dot)
# Run the animation
t_tracker.add_updater(lambda m, dt: m.increment_value(dt))
self.add(t_tracker)
self.wait(8)
# Change s to show different spirals
t_tracker.clear_updaters()
path.clear_updaters()
path = TracedPath(dot.get_center, stroke_color=GREEN, stroke_width=2)
self.add(path)
t_tracker.set_value(0)
s_tracker.set_value(0.1 + 1.5j)
t_tracker.add_updater(lambda m, dt: m.increment_value(dt))
self.wait(8)
@@ -0,0 +1,207 @@
"""
Fibonacci Eigenvalues
=====================
Shows how eigenvalues/eigenvectors lead to the closed-form Fibonacci formula.
This is a classic application of diagonalization in linear algebra.
Key concepts:
- Fibonacci recurrence as matrix multiplication
- Golden ratio as eigenvalue
- Binet's formula derivation
"""
from manimlib import *
class FibonacciEigenvalues(Scene):
"""
Derives the closed-form Fibonacci formula using eigenvalues.
F_n = (phi^n - psi^n) / sqrt(5)
"""
def construct(self):
# Title
title = Text("Fibonacci via Eigenvalues", font_size=48)
title.to_edge(UP)
self.play(Write(title))
# Fibonacci recurrence
recurrence = Tex(
R"F_{n+1} = F_n + F_{n-1}",
font_size=40
)
recurrence.next_to(title, DOWN, buff=0.6)
self.play(Write(recurrence))
self.wait()
# Matrix form
matrix_form = Tex(
R"\begin{bmatrix} F_{n+1} \\ F_n \end{bmatrix} = "
R"\begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix}"
R"\begin{bmatrix} F_n \\ F_{n-1} \end{bmatrix}",
font_size=36
)
matrix_form.next_to(recurrence, DOWN, buff=0.5)
self.play(Write(matrix_form))
self.wait()
# Label the matrix
a_label = Tex(R"A", font_size=36, color=BLUE)
a_label.next_to(matrix_form[10:16], UP, buff=0.1)
self.play(FadeIn(a_label, shift=DOWN * 0.2))
self.wait()
# Clear and show eigenvalue calculation
self.play(
FadeOut(recurrence),
FadeOut(matrix_form),
FadeOut(a_label),
)
# Characteristic equation
char_title = Text("Find eigenvalues:", font_size=32)
char_title.next_to(title, DOWN, buff=0.5)
char_eq = Tex(
R"\det(A - \lambda I) = 0",
font_size=36
)
char_eq.next_to(char_title, DOWN, buff=0.3)
expanded = Tex(
R"\det\begin{bmatrix} 1-\lambda & 1 \\ 1 & -\lambda \end{bmatrix} = 0",
font_size=36
)
expanded.next_to(char_eq, DOWN, buff=0.3)
polynomial = Tex(
R"\lambda^2 - \lambda - 1 = 0",
font_size=36
)
polynomial.next_to(expanded, DOWN, buff=0.3)
self.play(Write(char_title))
self.play(Write(char_eq))
self.wait(0.5)
self.play(Write(expanded))
self.wait(0.5)
self.play(Write(polynomial))
self.wait()
# Show eigenvalues (golden ratio!)
eigenvalues = Tex(
R"\lambda_1 = \phi = \frac{1 + \sqrt{5}}{2}, \quad "
R"\lambda_2 = \psi = \frac{1 - \sqrt{5}}{2}",
font_size=32,
t2c={R"\phi": TEAL, R"\psi": YELLOW, R"\lambda_1": TEAL, R"\lambda_2": YELLOW}
)
eigenvalues.next_to(polynomial, DOWN, buff=0.5)
golden_note = Text("(Golden Ratio!)", font_size=24, color=TEAL)
golden_note.next_to(eigenvalues, DOWN, buff=0.2)
self.play(Write(eigenvalues))
self.play(FadeIn(golden_note, shift=UP * 0.2))
self.wait()
# Clear and show final formula
self.play(
FadeOut(char_title),
FadeOut(char_eq),
FadeOut(expanded),
FadeOut(polynomial),
FadeOut(golden_note),
eigenvalues.animate.next_to(title, DOWN, buff=0.5)
)
# Binet's formula
binet_title = Text("Binet's Formula:", font_size=32)
binet_title.next_to(eigenvalues, DOWN, buff=0.5)
binet = Tex(
R"F_n = \frac{\phi^n - \psi^n}{\sqrt{5}}",
font_size=48,
t2c={R"\phi": TEAL, R"\psi": YELLOW}
)
binet.next_to(binet_title, DOWN, buff=0.3)
# Box around final formula
box = SurroundingRectangle(binet, buff=0.2, color=BLUE)
self.play(Write(binet_title))
self.play(Write(binet))
self.play(ShowCreation(box))
self.wait()
# Note about psi
note = Tex(
R"\text{Since } |\psi| < 1, \text{ for large } n: \quad "
R"F_n \approx \frac{\phi^n}{\sqrt{5}}",
font_size=28
)
note.next_to(box, DOWN, buff=0.5)
self.play(Write(note))
self.wait(2)
class FibonacciVisualization(Scene):
"""
Visual representation of Fibonacci spiral with golden ratio.
"""
def construct(self):
# Create Fibonacci squares
fibs = [1, 1, 2, 3, 5, 8, 13]
scale = 0.15
squares = VGroup()
current_pos = ORIGIN
directions = [RIGHT, UP, LEFT, DOWN] # Spiral pattern
for i, f in enumerate(fibs):
sq = Square(side_length=f * scale)
sq.set_stroke(BLUE, 2)
sq.set_fill(BLUE, 0.2)
if i == 0:
sq.move_to(current_pos)
else:
direction = directions[(i - 1) % 4]
prev_sq = squares[-1]
sq.next_to(prev_sq, direction, buff=0)
# Adjust position based on size difference
if direction == RIGHT or direction == LEFT:
sq.align_to(prev_sq, DOWN if i % 2 == 1 else UP)
else:
sq.align_to(prev_sq, LEFT if (i - 1) % 4 < 2 else RIGHT)
# Add number label
label = Tex(str(f), font_size=max(12, f * 3))
label.move_to(sq)
sq.add(label)
squares.add(sq)
squares.center()
squares.set_height(5)
title = Text("Fibonacci Spiral", font_size=42)
title.to_edge(UP)
golden_ratio = Tex(
R"\phi = \frac{1+\sqrt{5}}{2} \approx 1.618",
font_size=32
)
golden_ratio.to_edge(DOWN)
self.play(Write(title))
self.play(
LaggedStartMap(FadeIn, squares, lag_ratio=0.3),
run_time=3
)
self.play(Write(golden_ratio))
self.wait(2)
@@ -0,0 +1,232 @@
"""
Basic gradient descent visualization on a 2D loss landscape.
Demonstrates: Surface plots, 3D camera, path animation, optimization concepts
"""
from manimlib import *
import numpy as np
class GradientDescentBasic(Scene):
def construct(self):
# Create a simple 2D loss landscape (contour view)
axes = Axes(
x_range=(-3, 3, 1),
y_range=(-3, 3, 1),
width=8,
height=8
)
axes.to_edge(LEFT)
# Loss function: simple quadratic bowl
def loss_func(x, y):
return 0.5 * x**2 + 0.8 * y**2 + 0.3 * x * y
# Create contour lines
contours = VGroup()
for level in np.linspace(0.5, 8, 8):
# Approximate contour as ellipse
a = np.sqrt(2 * level / 0.5) # x scale
b = np.sqrt(2 * level / 0.8) # y scale
ellipse = Ellipse(width=a, height=b)
ellipse.move_to(axes.get_origin())
ellipse.set_stroke(
color=interpolate_color(BLUE, RED, level / 8),
width=2,
opacity=0.7
)
contours.add(ellipse)
# Title
title = Text("Gradient Descent", font_size=60)
title.to_edge(UP)
# Labels
w1_label = Tex("w_1")
w1_label.next_to(axes.x_axis.get_right(), DOWN)
w2_label = Tex("w_2")
w2_label.next_to(axes.y_axis.get_top(), LEFT)
self.play(FadeIn(title))
self.play(FadeIn(axes), FadeIn(w1_label), FadeIn(w2_label))
self.play(LaggedStartMap(FadeIn, contours, lag_ratio=0.1))
self.wait()
# Add minimum marker
min_dot = Dot(axes.get_origin(), color=GREEN)
min_label = Text("Minimum", font_size=24, color=GREEN)
min_label.next_to(min_dot, DOWN)
self.play(FadeIn(min_dot, scale=2), FadeIn(min_label))
self.wait()
# Starting point
start_point = axes.c2p(2.5, -2)
current_dot = Dot(start_point, color=YELLOW)
current_dot.set_z_index(1)
start_label = Text("Start", font_size=24)
start_label.next_to(current_dot, UR, buff=0.1)
self.play(FadeIn(current_dot, scale=2), FadeIn(start_label))
self.wait()
# Gradient descent path
learning_rate = 0.2
path_points = [np.array([2.5, -2.0])]
current = path_points[0].copy()
for _ in range(20):
# Gradient of loss: [x + 0.15*y, 1.6*y + 0.15*x]
grad = np.array([
current[0] + 0.15 * current[1],
1.6 * current[1] + 0.15 * current[0]
])
current = current - learning_rate * grad
path_points.append(current.copy())
if np.linalg.norm(current) < 0.01:
break
# Create path
path = VMobject()
path.set_points_smoothly([axes.c2p(p[0], p[1]) for p in path_points])
path.set_stroke(YELLOW, 3)
# Animation info panel
info_panel = VGroup()
iter_text = Text("Iteration: 0", font_size=30)
loss_text = Text("Loss: {:.3f}".format(loss_func(*path_points[0])), font_size=30)
info_panel.add(iter_text, loss_text)
info_panel.arrange(DOWN, aligned_edge=LEFT)
info_panel.to_corner(UR)
self.play(FadeIn(info_panel), FadeOut(start_label))
# Animate gradient descent
path_so_far = VMobject()
path_so_far.set_stroke(YELLOW, 3)
for i, (p1, p2) in enumerate(zip(path_points[:-1], path_points[1:])):
# Draw gradient arrow
p1_screen = axes.c2p(p1[0], p1[1])
p2_screen = axes.c2p(p2[0], p2[1])
arrow = Arrow(
p1_screen, p2_screen,
buff=0,
stroke_width=3,
color=RED
)
# Update info
new_iter = Text(f"Iteration: {i + 1}", font_size=30)
new_loss = Text(f"Loss: {loss_func(*p2):.3f}", font_size=30)
new_info = VGroup(new_iter, new_loss)
new_info.arrange(DOWN, aligned_edge=LEFT)
new_info.move_to(info_panel)
self.play(
GrowArrow(arrow),
current_dot.animate.move_to(p2_screen),
Transform(info_panel, new_info),
run_time=0.5
)
self.play(FadeOut(arrow), run_time=0.2)
if i > 15:
break
self.wait()
# Final message
converged = Text("Converged!", font_size=48, color=GREEN)
converged.next_to(title, DOWN)
self.play(FadeIn(converged, scale=1.5))
self.wait()
# Show the full path
self.play(
ShowCreation(path),
run_time=2
)
self.wait(2)
class GradientDescent3D(ThreeDScene):
"""3D visualization of gradient descent on a loss surface."""
def construct(self):
# Set up 3D view
frame = self.camera.frame
frame.set_euler_angles(theta=30 * DEGREES, phi=70 * DEGREES)
# Create 3D axes
axes = ThreeDAxes(
x_range=(-3, 3, 1),
y_range=(-3, 3, 1),
z_range=(0, 5, 1),
width=8,
height=8,
depth=4
)
# Loss surface
def loss_func(x, y):
return 0.3 * x**2 + 0.4 * y**2
surface = axes.get_graph(
loss_func,
u_range=(-3, 3),
v_range=(-3, 3),
)
surface.set_color_by_gradient(BLUE, GREEN, YELLOW, RED)
surface.set_opacity(0.7)
# Labels
title = Text("Loss Landscape", font_size=48)
title.to_corner(UL)
title.fix_in_frame()
self.play(
FadeIn(axes),
FadeIn(surface),
FadeIn(title),
)
self.wait()
# Rotate view
self.play(
frame.animate.set_euler_angles(theta=-30 * DEGREES),
run_time=3
)
self.wait()
# Gradient descent ball
start = np.array([2.5, 2.0])
ball = Sphere(radius=0.15, color=YELLOW)
ball.move_to(axes.c2p(start[0], start[1], loss_func(*start)))
self.play(FadeIn(ball, scale=2))
# Animate descent
learning_rate = 0.15
current = start.copy()
for _ in range(15):
grad = np.array([0.6 * current[0], 0.8 * current[1]])
new_pos = current - learning_rate * grad
new_point = axes.c2p(new_pos[0], new_pos[1], loss_func(*new_pos))
self.play(
ball.animate.move_to(new_point),
run_time=0.4
)
current = new_pos
self.wait()
# Final rotation
self.play(
frame.animate.set_euler_angles(theta=60 * DEGREES, phi=60 * DEGREES),
run_time=3
)
self.wait(2)
@@ -0,0 +1,86 @@
"""
Visualization showing the correspondence between hexagonal tilings
and 3D cube stacking patterns.
"""
from manimlib import *
import math
class HexagonCubeCorrespondence(InteractiveScene):
"""
Shows how a hexagonal tiling corresponds to viewing 3D cube stacks from above.
Demonstrates:
1. Creating half-cube faces in 3D
2. Viewing them from the [1,1,1] direction
3. How rotation in 2D corresponds to adding/removing cubes in 3D
"""
n = 4
colors = [BLUE_B, BLUE_D, BLUE_E]
def construct(self):
# Set up axes and camera angle
self.frame.set_field_of_view(1 * DEGREES)
self.frame.reorient(135, 55, 0)
axes = ThreeDAxes((-5, 5), (-5, 5), (-5, 5))
# Add base half-cube
base_cube = self.get_half_cube(
side_length=self.n,
shared_corner=[-1, -1, -1],
grid=True
)
self.add(base_cube)
# Add cubes to build a stack
cubes = VGroup()
block_pattern = np.zeros((self.n, self.n, self.n))
# Build a pyramid-like structure
for x in range(self.n):
for y in range(self.n - x):
for z in range(self.n - x - y):
cube = self.get_half_cube((x, y, z))
cubes.add(cube)
block_pattern[x, y, z] = 1
self.play(
LaggedStart(
(FadeIn(cube, shift=0.25 * IN) for cube in cubes),
lag_ratio=0.02,
),
run_time=3
)
self.wait()
# Remove the base and color the cubes
self.play(FadeOut(base_cube))
cubes.set_fill(BLUE_D)
self.wait()
# Rotate to show hexagonal view
self.play(
self.frame.animate.reorient(135, 55, 0, ORIGIN, 8).set_field_of_view(1 * DEGREES),
run_time=2
)
self.wait(2)
def get_half_cube(self, coords=(0, 0, 0), side_length=1, colors=None, shared_corner=[1, 1, 1], grid=False):
"""Create three visible faces of a cube (half-cube) that would be seen from the [1,1,1] direction."""
if colors is None:
colors = self.colors
squares = Square(side_length).replicate(3)
if grid:
for square in squares:
grid_lines = Square(side_length=1).get_grid(side_length, side_length, buff=0)
grid_lines.move_to(square)
square.add(grid_lines)
axes = [OUT, DOWN, LEFT]
for square, color, axis in zip(squares, colors, axes):
square.set_fill(color, 1)
square.set_stroke(color, 0)
square.rotate(90.1 * DEGREES, axis)
square.move_to(ORIGIN, shared_corner)
squares.move_to(coords, np.array([-1, -1, -1]))
squares.set_stroke(WHITE, 2)
return squares
@@ -0,0 +1,250 @@
"""
Integration Visualization
Shows integration as accumulating area under a curve,
with animated filling and Riemann sum approximations.
Run: manimgl integration_visualization.py AreaUnderCurve -w
Preview: manimgl integration_visualization.py AreaUnderCurve -p
Source: Adapted from 3b1b's Laplace transform video (2025)
"""
from manimlib import *
import numpy as np
class AreaUnderCurve(InteractiveScene):
"""
Basic visualization of definite integral as area under curve.
Shows smooth accumulation of area from left to right.
"""
def construct(self):
# Create axes
axes = Axes(
x_range=(0, 5, 1),
y_range=(0, 3, 1),
width=10,
height=5,
axis_config={"include_tip": True}
)
axes.to_edge(DOWN, buff=1)
x_label = Tex("x", font_size=30)
x_label.next_to(axes.x_axis, RIGHT)
y_label = Tex("f(x)", font_size=30)
y_label.next_to(axes.y_axis, UP)
self.play(
ShowCreation(axes),
Write(x_label),
Write(y_label),
)
# Define a nice function
def f(x):
return 0.3 * x**2 - 0.5 * x + 1.5
# Draw the curve
curve = axes.get_graph(f, x_range=[0, 4.5], color=BLUE, stroke_width=3)
curve_label = Tex("f(x) = 0.3x^2 - 0.5x + 1.5", font_size=24)
curve_label.next_to(curve.get_end(), UR, buff=0.1)
self.play(ShowCreation(curve, run_time=2))
self.play(Write(curve_label))
self.wait()
# Show area accumulating
t_tracker = ValueTracker(0.1)
# Filled area using Polygon
def get_area_polygon():
t = max(0.1, t_tracker.get_value())
xs = np.linspace(0, t, 50)
points = [axes.c2p(x, f(x)) for x in xs]
points.append(axes.c2p(t, 0))
points.append(axes.c2p(0, 0))
poly = Polygon(*points)
poly.set_fill(BLUE_E, opacity=0.5)
poly.set_stroke(width=0)
return poly
area = always_redraw(get_area_polygon)
# Vertical line at current x
def get_v_line():
t = t_tracker.get_value()
return Line(
axes.c2p(t, 0),
axes.c2p(t, f(t)),
color=YELLOW,
stroke_width=2
)
v_line = always_redraw(get_v_line)
# Integral notation
integral = Tex(
r"\int_0^{x} f(t) \, dt",
font_size=48
)
integral.to_corner(UL)
self.play(
FadeIn(area),
FadeIn(v_line),
Write(integral),
)
# Animate accumulation
self.play(
t_tracker.animate.set_value(4),
run_time=5,
rate_func=linear
)
self.wait()
class RiemannSums(InteractiveScene):
"""
Shows Riemann sum approximation converging to true integral.
Rectangles get thinner and better approximate the area.
"""
def construct(self):
# Create axes
axes = Axes(
x_range=(0, 4, 1),
y_range=(0, 3, 1),
width=8,
height=4,
)
axes.center()
def f(x):
return 0.5 * np.sin(x) + 1.5
curve = axes.get_graph(f, x_range=[0.5, 3.5], color=BLUE, stroke_width=3)
self.play(ShowCreation(axes), ShowCreation(curve))
# Create rectangles for different n values
n_values = [4, 8, 16, 32]
current_rects = None
current_label = None
for n in n_values:
dx = 3 / n
rects = VGroup()
for i in range(n):
x = 0.5 + i * dx
height = f(x)
rect = Rectangle(
width=dx * axes.x_axis.get_unit_size(),
height=height * axes.y_axis.get_unit_size(),
stroke_color=WHITE,
stroke_width=1,
fill_color=BLUE_E,
fill_opacity=0.6,
)
rect.move_to(axes.c2p(x + dx/2, height/2))
rects.add(rect)
label = Tex(f"n = {n}", font_size=36)
label.to_corner(UR)
if current_rects is None:
self.play(
LaggedStartMap(FadeIn, rects, lag_ratio=0.05),
Write(label),
)
else:
self.play(
ReplacementTransform(current_rects, rects),
ReplacementTransform(current_label, label),
)
current_rects = rects
current_label = label
self.wait(0.5)
# Final message
converge_text = Tex(r"\text{As } n \to \infty, \text{ sum } \to \int", font_size=36)
converge_text.to_corner(UL)
self.play(Write(converge_text))
self.wait()
class ExponentialDecay(InteractiveScene):
"""
Visualize the integral of e^(-x) from 0 to infinity.
Shows that the total area is exactly 1.
"""
def construct(self):
# Create axes
axes = Axes(
x_range=(0, 6, 1),
y_range=(0, 1.2, 0.5),
width=10,
height=4,
)
axes.to_edge(DOWN, buff=1.5)
x_label = Tex("x", font_size=30).next_to(axes.x_axis, RIGHT)
self.play(ShowCreation(axes), Write(x_label))
# e^(-x) curve
curve = axes.get_graph(
lambda x: np.exp(-x),
x_range=[0, 5.5],
color=BLUE,
stroke_width=3
)
curve_label = Tex(r"e^{-x}", font_size=36, color=BLUE)
curve_label.next_to(curve.get_start(), UR)
self.play(ShowCreation(curve), Write(curve_label))
# Fill area progressively
t_tracker = ValueTracker(0.1)
def get_area():
t = max(0.1, t_tracker.get_value())
xs = np.linspace(0, t, 50)
points = [axes.c2p(x, np.exp(-x)) for x in xs]
points.append(axes.c2p(t, 0))
points.append(axes.c2p(0, 0))
poly = Polygon(*points)
poly.set_fill(BLUE_E, opacity=0.5)
poly.set_stroke(width=0)
return poly
area = always_redraw(get_area)
# Show integral formula
integral = Tex(
r"\int_0^{\infty} e^{-x} \, dx = 1",
font_size=48
)
integral.to_corner(UL)
# Current value tracker
value_label = Tex(r"\text{Area} \approx 0.00", font_size=30)
value_num = value_label.make_number_changeable("0.00")
value_num.add_updater(lambda m: m.set_value(1 - np.exp(-t_tracker.get_value())))
value_label.to_corner(UR)
self.play(
FadeIn(area),
Write(integral),
Write(value_label),
)
# Animate the fill
self.play(
t_tracker.animate.set_value(5.5),
run_time=6,
rate_func=linear
)
self.wait(2)
@@ -0,0 +1,331 @@
"""
Laplace Transform Integration Visualization
Demonstrates the integral of e^{-st} as area under the curve,
showing how squishing by 1/s preserves the area relationship.
Run: manimgl laplace_integral.py LaplaceIntegral -w
Preview: manimgl laplace_integral.py LaplaceIntegral -p
Source: Adapted from 3b1b's Laplace transform video (2025)
"""
from manimlib import *
class LaplaceIntegral(InteractiveScene):
"""
Visualize the integral ∫₀^∞ e^{-st} dt = 1/s
Key techniques:
- get_area_under_graph for shaded regions
- ValueTracker for parameter animation
- Dynamic function binding
- make_number_changeable for live updates
"""
def construct(self):
# Set up axes
max_x = 15
unit_size = 4
axes = Axes(
x_range=(0, max_x, 0.25),
y_range=(0, 1, 0.25),
unit_size=unit_size
)
axes.to_edge(DL, buff=1.0)
axes.add_coordinate_labels(num_decimal_places=2, font_size=20)
# Parameter s
s_tracker = ValueTracker(1)
get_s = s_tracker.get_value
# The exponential function
def exp_func(t):
return np.exp(-get_s() * t)
# Dynamic graph
graph = axes.get_graph(np.exp)
graph.set_stroke(BLUE, 3)
axes.bind_graph_to_func(graph, exp_func)
# Label
t2c = {"s": YELLOW}
graph_label = Tex(R"e^{-st}", t2c=t2c, font_size=72)
graph_label.next_to(axes.y_axis.get_top(), UR).shift(0.5 * RIGHT)
# Integral expression
integral = Tex(R"\int^\infty_0 e^{-st} dt", t2c=t2c)
integral.set_x(1)
integral.to_edge(UP)
self.add(axes, graph, graph_label, integral)
# Add a slider for s
s_slider = self.create_slider(s_tracker)
s_slider.to_edge(UP, buff=MED_LARGE_BUFF)
s_slider.align_to(axes.c2p(0, 0), LEFT)
self.add(s_slider)
# Vary s to show different decay rates
for value in [5, 0.25, 1]:
self.play(s_tracker.animate.set_value(value), run_time=4)
self.wait()
# Show integral as area
equals = Tex(R"=", font_size=72).rotate(90 * DEG)
equals.next_to(integral, DOWN)
area_word = Text("Area", font_size=60)
area_word.next_to(equals, DOWN)
area = axes.get_area_under_graph(graph)
def update_area(area):
area.become(axes.get_area_under_graph(graph))
arrow = Arrow(area_word.get_corner(DL), axes.c2p(0.75, 0.5), thickness=4)
self.play(
LaggedStart(
Animation(graph.copy(), remover=True),
Write(equals),
FadeIn(area_word, DOWN),
GrowArrow(arrow),
UpdateFromFunc(area, update_area),
lag_ratio=0.25
),
ShowCreation(graph, suspend_mobject_updating=True, run_time=3),
)
self.wait()
# Show that area = 1 when s = 1
simple_integral = Tex(R"\int^\infty_0 e^{-t} dt")
simple_integral.move_to(integral)
equals_one = Tex(R"= 1", font_size=60)
equals_one.next_to(area_word)
area_one_label = Tex(R"1", font_size=60)
area_one_label.move_to(axes.c2p(0.35, 0.35))
area_one_label.set_z_index(1)
self.play(
TransformMatchingTex(integral, simple_integral),
FadeOut(graph_label),
)
self.wait()
self.play(Write(equals_one))
self.play(TransformFromCopy(equals_one["1"], area_one_label))
self.wait()
# Show area squishing with s
area.clear_updaters()
area.add_updater(update_area)
rhs = Tex(R"= \frac{1}{s}", t2c=t2c, font_size=60)
rhs.next_to(area_word, RIGHT)
self.play(LaggedStart(
FadeOut(equals_one),
FadeOut(area_one_label),
FadeOut(simple_integral),
FadeIn(integral),
FadeIn(graph_label),
FadeOut(arrow),
lag_ratio=0.1
))
self.play(
s_tracker.animate.set_value(5).set_anim_args(run_time=8),
)
area_word.save_state()
self.play(
area_word.animate.move_to(axes.c2p(0.6, 0.33)),
Write(rhs),
FadeOut(equals),
)
self.wait()
# Show decimal approximation
dec_rhs = Tex(R"= 1.00", font_size=60)
dec_rhs.make_number_changeable("1.00").add_updater(lambda m: m.set_value(1 / get_s()))
dec_rhs.always.next_to(rhs, RIGHT)
self.play(
VFadeIn(dec_rhs),
s_tracker.animate.set_value(0.5).set_anim_args(run_time=8),
)
self.wait()
self.play(
s_tracker.animate.set_value(2),
run_time=4,
)
self.wait(2)
def create_slider(self, tracker, x_range=(0, 5), height=1.5, font_size=36):
"""Create a visual slider for the s parameter."""
number_line = NumberLine(x_range, width=height, tick_size=0.05)
number_line.rotate(90 * DEG)
indicator = ArrowTip(width=0.1, length=0.2)
indicator.rotate(PI)
indicator.add_updater(lambda m: m.move_to(number_line.n2p(tracker.get_value()), LEFT))
indicator.set_color(YELLOW)
label = Tex(R"s = 0.00", font_size=font_size)
label["s"].set_color(YELLOW)
label.rhs = label.make_number_changeable("0.00")
label.always.next_to(indicator, RIGHT, SMALL_BUFF)
label.rhs.f_always.set_value(tracker.get_value)
slider = VGroup(number_line, indicator, label)
return slider
class AverageValueInterpretation(InteractiveScene):
"""
Show that unit integrals equal the average value over that interval.
Helps build intuition for the Laplace transform.
"""
def construct(self):
# Set up axes
axes = Axes(
x_range=(0, 6),
y_range=(0, 1.2),
width=10,
height=4
)
axes.to_edge(DOWN, buff=1)
# Fixed s value
s = 0.5
def exp_func(t):
return np.exp(-s * t)
# Graph
graph = axes.get_graph(exp_func)
graph.set_stroke(BLUE, 3)
self.add(axes, graph)
# Unit interval [0, 1]
v_lines = VGroup(
DashedLine(axes.c2p(0, 0), axes.c2p(0, 1.2)),
DashedLine(axes.c2p(1, 0), axes.c2p(1, 1.2)),
)
v_lines.set_stroke(WHITE, 1)
# Area under [0, 1]
area = axes.get_area_under_graph(graph, x_range=(0, 1))
# Integral label
int_tex = Tex(R"\int^1_0 e^{-st} dt", t2c={"s": YELLOW}, font_size=48)
int_tex.move_to(v_lines, UP).shift(0.5 * UP)
self.play(
ShowCreation(v_lines),
FadeIn(area),
Write(int_tex),
)
self.wait()
# Show average value interpretation
avg_value = np.mean([exp_func(t) for t in np.linspace(0, 1, 1000)])
avg_rect = Rectangle(
width=axes.x_axis.get_unit_size(),
height=avg_value * axes.y_axis.get_unit_size()
)
avg_rect.set_fill(GREEN, 0.5)
avg_rect.set_stroke(GREEN, 2)
avg_rect.move_to(axes.c2p(0.5, avg_value/2))
avg_label = Text("Average height", font_size=24)
avg_label.next_to(avg_rect, RIGHT)
self.play(
area.animate.set_fill(opacity=0.3),
FadeIn(avg_rect),
Write(avg_label),
)
self.wait()
# Explanation
explanation = Tex(
R"\text{Area} = \text{Width} \times \text{Height}_{avg}",
font_size=36
)
explanation.to_edge(UP)
self.play(Write(explanation))
self.wait(2)
class IntegralAsSum(InteractiveScene):
"""
Show the full integral as a sum of unit interval averages.
"""
def construct(self):
# Axes
axes = Axes(
x_range=(0, 8),
y_range=(0, 1.2),
width=12,
height=4
)
axes.to_edge(DOWN, buff=1)
s = 0.75
def exp_func(t):
return np.exp(-s * t)
graph = axes.get_graph(exp_func)
graph.set_stroke(BLUE, 3)
self.add(axes, graph)
# Create stacked areas for each unit interval
areas = VGroup()
colors = color_gradient([BLUE_E, TEAL_E], 6)
for n, color in enumerate(colors):
area = axes.get_area_under_graph(graph, x_range=(n, n+1))
area.set_fill(color, 0.7)
areas.add(area)
# Labels for each interval
labels = VGroup()
for n in range(6):
label = Tex(f"[{n}, {n+1}]", font_size=24)
label.move_to(areas[n])
labels.add(label)
# Animate adding areas
self.play(LaggedStartMap(FadeIn, areas, lag_ratio=0.3))
self.play(LaggedStartMap(FadeIn, labels, lag_ratio=0.2))
self.wait()
# Show total integral
total = Tex(
R"\int^\infty_0 e^{-st} dt = \sum_{n=0}^{\infty} \int_n^{n+1} e^{-st} dt",
t2c={"s": YELLOW},
font_size=36
)
total.to_edge(UP)
self.play(Write(total))
self.wait()
# Highlight that it converges
result = Tex(R"= \frac{1}{s}", t2c={"s": YELLOW}, font_size=48)
result.next_to(total, DOWN)
self.play(Write(result))
self.wait(2)
@@ -0,0 +1,389 @@
"""
Light Polarization and Quantum States
=====================================
Visualizes polarized light as a 3D electromagnetic wave, showing
how polarization states map to quantum states on a 2D plane.
Key concepts demonstrated:
- TimeVaryingVectorField for oscillating wave visualization
- 3D camera control with reorient
- Prism and ParametricSurface for 3D objects
- ValueTracker for controlling wave polarization angle
"""
from manimlib import *
class PolarizedLightWave(InteractiveScene):
"""Visualizes polarized light as an electromagnetic wave."""
def construct(self):
frame = self.frame
# Set up 3D view
frame.reorient(-60, 75, 0)
frame.add_ambient_rotation(DEG)
# Create axes
axes = ThreeDAxes((-1, 10), (-1, 1), (-1, 1))
axes.set_stroke(WHITE, 1, 0.5)
self.add(axes)
# Polarization angle tracker
theta_tracker = ValueTracker(45 * DEG)
# Wave parameters
wave_number = 1.5
frequency = 0.5
amplitude = 0.5
# Create the wave as a VGroup of vectors that update over time
# Note: TimeVaryingVectorField doesn't work directly with ThreeDAxes
sample_x = np.arange(0, 8, 0.15)
def get_wave_vectors(time=0):
"""Generate wave vectors at given time."""
vectors = VGroup()
theta = theta_tracker.get_value()
for x in sample_x:
phase = wave_number * x - TAU * frequency * time
magnitude = amplitude * np.cos(phase)
y_comp = np.cos(theta) * magnitude
z_comp = np.sin(theta) * magnitude
start = axes.c2p(x, 0, 0)
end = axes.c2p(x, y_comp, z_comp)
vec = Arrow(start, end, buff=0, thickness=2)
vec.set_color(BLUE)
vec.set_stroke(opacity=0.7)
vectors.add(vec)
return vectors
wave = get_wave_vectors()
# Add an updater to animate the wave
time_tracker = ValueTracker(0)
def update_wave(w):
new_wave = get_wave_vectors(time_tracker.get_value())
w.become(new_wave)
wave.add_updater(update_wave)
# Add a beam line
beam = Line(ORIGIN, 8 * RIGHT)
beam.set_stroke(GREEN, 2)
self.add(beam)
self.play(FadeIn(wave))
# Animate the wave for a few seconds
self.play(time_tracker.animate.set_value(3), run_time=3, rate_func=linear)
wave.clear_updaters() # Stop wave animation to change polarization
# Add polarization plane indicator
plane_indicator = Square(1.5)
plane_indicator.rotate(90 * DEG, RIGHT)
plane_indicator.rotate(theta_tracker.get_value(), RIGHT)
plane_indicator.move_to(4 * RIGHT)
plane_indicator.set_fill(BLUE, 0.2)
plane_indicator.set_stroke(BLUE, 1)
def update_plane(plane):
plane.rotate(
theta_tracker.get_value() - plane.get_angle(),
axis=RIGHT,
about_point=plane.get_center()
)
self.play(FadeIn(plane_indicator))
self.wait(2)
# Change polarization angle
self.play(
theta_tracker.animate.set_value(0),
run_time=3
)
self.wait(2)
self.play(
theta_tracker.animate.set_value(90 * DEG),
run_time=3
)
self.wait(2)
self.play(
theta_tracker.animate.set_value(45 * DEG),
run_time=2
)
self.wait(3)
class PolarizationTo2DState(InteractiveScene):
"""Shows how polarization maps to a 2D state vector."""
def construct(self):
frame = self.frame
# Title
title = Text("Polarization as Quantum State", font_size=48)
title.to_edge(UP)
self.add(title)
# Left side: 3D polarization representation
axes_3d = ThreeDAxes((-1, 1), (-1, 1), (-1, 1))
axes_3d.scale(1.5)
axes_3d.shift(3 * LEFT)
# Polarization vector (in yz plane at x=0)
theta_tracker = ValueTracker(45 * DEG)
def get_pol_vector():
theta = theta_tracker.get_value()
return Arrow(
axes_3d.c2p(0, 0, 0),
axes_3d.c2p(0, np.cos(theta), np.sin(theta)),
buff=0,
thickness=5,
fill_color=BLUE
)
pol_vector = always_redraw(get_pol_vector)
# Circle showing all possible polarizations
pol_circle = Circle(radius=1.5)
pol_circle.rotate(90 * DEG, UP)
pol_circle.move_to(axes_3d.c2p(0, 0, 0))
pol_circle.set_stroke(GREY, 1, 0.5)
# Labels
h_label = Tex("H", font_size=30, color=YELLOW)
h_label.rotate(90 * DEG, RIGHT)
h_label.next_to(axes_3d.c2p(0, 1, 0), UP + OUT, SMALL_BUFF)
v_label = Tex("V", font_size=30, color=GREEN)
v_label.rotate(90 * DEG, RIGHT)
v_label.next_to(axes_3d.c2p(0, 0, 1), OUT, SMALL_BUFF)
frame.reorient(-30, 70, 0, ORIGIN, 8)
self.add(axes_3d, pol_circle, pol_vector, h_label, v_label)
# Right side: 2D qubit representation
plane = NumberPlane((-2, 2), (-2, 2), faded_line_ratio=5)
plane.set_height(4)
plane.shift(3 * RIGHT)
zero_label = Tex(R"|H\rangle", font_size=30, color=YELLOW)
zero_label.next_to(plane.c2p(1, 0), DR, SMALL_BUFF)
one_label = Tex(R"|V\rangle", font_size=30, color=GREEN)
one_label.next_to(plane.c2p(0, 1), UL, SMALL_BUFF)
def get_state_vector():
theta = theta_tracker.get_value()
return Arrow(
plane.c2p(0, 0),
plane.c2p(np.cos(theta), np.sin(theta)),
buff=0,
thickness=4,
fill_color=TEAL
)
state_vector = always_redraw(get_state_vector)
# Unit circle on 2D plane
unit_circle = Circle(radius=plane.c2p(1, 0)[0] - plane.c2p(0, 0)[0])
unit_circle.move_to(plane.c2p(0, 0))
unit_circle.set_stroke(GREY, 1, 0.5)
self.add(plane, unit_circle, state_vector, zero_label, one_label)
# Arrow connecting the two representations
connection = Tex(R"\Leftrightarrow", font_size=72)
connection.move_to(ORIGIN)
self.play(Write(connection))
self.wait()
# Animate through different polarizations
for target_angle in [0, 90 * DEG, 30 * DEG, 60 * DEG, 45 * DEG]:
self.play(theta_tracker.animate.set_value(target_angle), run_time=2)
self.wait()
self.wait(2)
class BeamSplitterSimple(InteractiveScene):
"""Simplified beam splitter demonstration."""
def construct(self):
frame = self.frame
# Set up 3D view
frame.reorient(-70, 70, 0)
# Create the beam splitter cube
splitter = Cube()
splitter.set_color(WHITE)
splitter.set_opacity(0.3)
splitter.rotate(45 * DEG)
splitter.set_height(1)
splitter.move_to(ORIGIN)
# Input beam
input_beam = Line(4 * LEFT, ORIGIN)
input_beam.set_stroke(GREEN, 3)
# Output beams
output_h = Line(ORIGIN, 4 * RIGHT)
output_h.set_stroke(YELLOW, 3)
output_v = Line(ORIGIN, 4 * UP)
output_v.set_stroke(BLUE, 3)
# Labels
input_label = Tex(R"|\psi\rangle", font_size=36)
input_label.next_to(input_beam, UP)
input_label.rotate(90 * DEG, RIGHT)
h_label = Tex(R"|H\rangle", font_size=36, color=YELLOW)
h_label.next_to(output_h.get_end(), DOWN)
h_label.rotate(90 * DEG, RIGHT)
v_label = Tex(R"|V\rangle", font_size=36, color=BLUE)
v_label.next_to(output_v.get_end(), RIGHT)
v_label.rotate(90 * DEG, RIGHT)
self.add(splitter)
self.play(ShowCreation(input_beam), FadeIn(input_label))
self.wait()
# Split the beam
self.play(
ShowCreation(output_h),
ShowCreation(output_v),
FadeIn(h_label),
FadeIn(v_label),
)
self.wait()
# Add probability labels
cos_label = Tex(R"\cos(\theta)", font_size=24, color=YELLOW)
cos_label.next_to(output_h, DOWN, SMALL_BUFF)
cos_label.rotate(90 * DEG, RIGHT)
sin_label = Tex(R"\sin(\theta)", font_size=24, color=BLUE)
sin_label.next_to(output_v, LEFT, SMALL_BUFF)
sin_label.rotate(90 * DEG, RIGHT)
self.play(
FadeIn(cos_label),
FadeIn(sin_label)
)
# Animate the camera
self.play(
frame.animate.reorient(-30, 60, 0),
run_time=4
)
self.wait(2)
class WaveVectorComponents(InteractiveScene):
"""Shows decomposition of polarization into H and V components."""
def construct(self):
# 2D plane view
plane = NumberPlane((-2, 2), (-2, 2), faded_line_ratio=5)
plane.set_height(6)
# Labels
h_label = Tex(R"|H\rangle", color=YELLOW)
h_label.next_to(plane.c2p(1.2, 0), DR, SMALL_BUFF)
v_label = Tex(R"|V\rangle", color=BLUE)
v_label.next_to(plane.c2p(0, 1.2), UL, SMALL_BUFF)
self.add(plane, h_label, v_label)
# Main polarization vector
theta = 50 * DEG
main_vec = Arrow(
plane.c2p(0, 0),
plane.c2p(np.cos(theta), np.sin(theta)),
buff=0,
thickness=5,
fill_color=TEAL
)
# Component vectors
h_component = Arrow(
plane.c2p(0, 0),
plane.c2p(np.cos(theta), 0),
buff=0,
thickness=3,
fill_color=YELLOW
)
v_component = Arrow(
plane.c2p(np.cos(theta), 0),
plane.c2p(np.cos(theta), np.sin(theta)),
buff=0,
thickness=3,
fill_color=BLUE
)
# Dashed lines for projection
h_dashed = DashedLine(
plane.c2p(np.cos(theta), np.sin(theta)),
plane.c2p(np.cos(theta), 0)
)
h_dashed.set_stroke(YELLOW, 1)
v_dashed = DashedLine(
plane.c2p(np.cos(theta), np.sin(theta)),
plane.c2p(0, np.sin(theta))
)
v_dashed.set_stroke(BLUE, 1)
self.play(GrowArrow(main_vec))
self.wait()
# Show decomposition
self.play(
ShowCreation(h_dashed),
ShowCreation(v_dashed),
)
self.play(
GrowArrow(h_component),
GrowArrow(v_component),
)
# Equation
equation = Tex(
R"|\psi\rangle = \cos(\theta)|H\rangle + \sin(\theta)|V\rangle",
font_size=36
)
equation.to_edge(DOWN, buff=1.0)
self.play(Write(equation))
self.wait()
# Show angle
arc = Arc(0, theta, radius=0.5)
arc.move_to(plane.c2p(0, 0), LEFT + DOWN)
arc.set_stroke(WHITE, 2)
theta_label = Tex(R"\theta", font_size=36)
theta_label.next_to(arc.pfp(0.5), RIGHT, SMALL_BUFF)
self.play(
ShowCreation(arc),
Write(theta_label)
)
self.wait(2)
if __name__ == "__main__":
# To run: manimgl light_polarization.py PolarizedLightWave
pass
@@ -0,0 +1,135 @@
"""
Linear Regression visualization showing data points and a fitted line.
Demonstrates: Axes, DotCloud, Line, ValueTracker, updaters
"""
from manimlib import *
import numpy as np
import random
class LinearRegression(Scene):
def construct(self):
# Set up axes
x_min, x_max = (-1, 12)
y_min, y_max = (-1, 10)
axes = Axes((x_min, x_max), (y_min, y_max), width=12, height=6)
axes.to_edge(DOWN)
self.add(axes)
# Add data points
n_data_points = 30
m = 0.75 # slope
y0 = 1 # y-intercept
np.random.seed(42)
data = np.array([
(x, y0 + m * x + 0.75 * np.random.normal(0, 1))
for x in np.random.uniform(2, x_max, n_data_points)
])
points = axes.c2p(data[:, 0], data[:, 1])
dots = DotCloud(points)
dots.set_color(YELLOW)
dots.set_glow_factor(1)
dots.set_radius(0.075)
self.add(dots)
# Title
title = Text("Linear Regression", font_size=72)
title.to_edge(UP)
# Create line with trackers for slope and y-intercept
m_tracker = ValueTracker(m)
y0_tracker = ValueTracker(y0)
line = Line()
line.set_stroke(TEAL, 2)
def update_line(line):
curr_y0 = y0_tracker.get_value()
curr_m = m_tracker.get_value()
line.put_start_and_end_on(
axes.c2p(0, curr_y0),
axes.c2p(x_max, curr_y0 + curr_m * x_max),
)
line.add_updater(update_line)
self.play(
FadeIn(title, UP),
ShowCreation(line),
)
self.wait()
# Label inputs and outputs
in_label = Text("Input")
in_label.next_to(axes.x_axis, DOWN, buff=0.1, aligned_edge=RIGHT)
out_label = Text("Output")
out_label.rotate(90 * DEGREES)
out_label.next_to(axes.y_axis, LEFT, aligned_edge=UP)
self.play(LaggedStart(
FadeIn(in_label, lag_ratio=0.1),
FadeIn(out_label, lag_ratio=0.1),
lag_ratio=0.5,
))
self.wait()
# Emphasize line
self.play(
VShowPassingFlash(
line.copy().set_stroke(BLUE, 8).scale(1.1).insert_n_curves(100),
time_width=1.5,
run_time=2
),
)
self.wait()
# Show parameter labels
m_label = VGroup(
Text("slope = "),
DecimalNumber(m_tracker.get_value()),
)
m_label.arrange(RIGHT)
m_label[1].f_always.set_value(m_tracker.get_value)
y0_label = VGroup(
Text("y-intercept = "),
DecimalNumber(y0_tracker.get_value()),
)
y0_label.arrange(RIGHT)
y0_label[1].f_always.set_value(y0_tracker.get_value)
labels = VGroup(m_label, y0_label)
labels.arrange(DOWN, aligned_edge=LEFT)
labels.next_to(axes.y_axis, RIGHT, buff=1.0)
labels.to_edge(UP)
self.play(
FadeOut(title, UP),
FadeIn(m_label, UP),
)
self.play(
m_tracker.animate.set_value(1.5),
run_time=2,
)
self.play(FadeIn(y0_label, UP))
self.play(
y0_tracker.animate.set_value(-2),
run_time=2
)
self.wait()
# Tweak line parameters to show fitting
for n in range(6):
alpha = random.random()
if alpha > 0.5:
alpha += 1
new_m = interpolate(m_tracker.get_value(), m, alpha)
new_y0 = interpolate(y0_tracker.get_value(), y0, alpha)
self.play(LaggedStart(
m_tracker.animate.set_value(new_m),
y0_tracker.animate.set_value(new_y0),
run_time=1.5,
lag_ratio=0.25,
))
self.wait(0.5)
@@ -0,0 +1,307 @@
"""
LLM Prediction Pipeline Visualization
Demonstrates the complete flow of an LLM making predictions:
input context -> model processing -> probability distribution -> sampled output.
Run with: manimgl llm_prediction_pipeline.py LLMPredictionPipeline
"""
from manimlib import *
import numpy as np
def get_paragraph(words, line_len=40, font_size=48):
"""Handle word wrapping for text display."""
words = list(map(str.strip, words))
word_lens = list(map(len, words))
lines = []
lh, rh = 0, 0
while rh < len(words):
rh += 1
if sum(word_lens[lh:rh]) > line_len:
rh -= 1
lines.append(words[lh:rh])
lh = rh
lines.append(words[lh:])
text = "\n".join([" ".join(line).strip() for line in lines])
return Text(text, alignment="LEFT", font_size=font_size)
class LLMPredictionPipeline(InteractiveScene):
"""
Full visualization of the LLM prediction pipeline:
1. Input text is shown
2. Text flows into the model
3. Model processes (blocks light up)
4. Distribution appears
5. Token is sampled and added to text
"""
def construct(self):
# Initial setup
seed_text = "Michael Jordan plays the sport of"
# Create input text
input_text = get_paragraph(seed_text.split(), line_len=30, font_size=32)
input_text.to_edge(UP, buff=0.8)
input_text.set_color(BLUE_B)
# Create model visualization
model = self.create_llm_model()
model.set_height(3.0)
model.center()
model.shift(0.5 * DOWN)
# Create prediction data
predictions = [" basketball", " baseball", " golf", " tennis", " football"]
probs = np.array([0.65, 0.15, 0.08, 0.07, 0.05])
# Distribution visualization
bar_groups = self.build_distribution(predictions, probs)
bar_groups.to_edge(RIGHT, buff=0.5)
bar_groups.align_to(model, UP)
# Input arrow
in_arrow = Arrow(
input_text.get_bottom() + 0.2 * DOWN,
model.get_top() + 0.2 * UP,
buff=0
)
in_arrow.set_color(BLUE)
# Output arrow
out_arrow = Arrow(
model.get_right() + 0.2 * RIGHT,
bar_groups.get_left() + 0.2 * LEFT,
buff=0
)
out_arrow.set_color(TEAL)
# Step 1: Show input
step1 = Text("1. Input Context", font_size=24, color=YELLOW)
step1.to_corner(UL)
self.play(Write(step1))
self.play(Write(input_text))
self.wait(0.5)
# Step 2: Feed to model
step2 = Text("2. Feed to Model", font_size=24, color=YELLOW)
step2.next_to(step1, DOWN, aligned_edge=LEFT)
self.play(Write(step2))
self.play(FadeIn(model))
self.play(GrowArrow(in_arrow))
# Animate text flowing into model
text_copy = input_text.copy()
self.play(
text_copy.animate.scale(0.3).move_to(model.get_top()),
rate_func=rush_into,
run_time=0.8
)
self.play(FadeOut(text_copy, shift=DOWN, scale=0.5))
# Step 3: Model processes
step3 = Text("3. Process", font_size=24, color=YELLOW)
step3.next_to(step2, DOWN, aligned_edge=LEFT)
self.play(Write(step3))
self.play(self.animate_model_processing(model))
self.wait(0.3)
# Step 4: Output distribution
step4 = Text("4. Output Distribution", font_size=24, color=YELLOW)
step4.next_to(step3, DOWN, aligned_edge=LEFT)
self.play(Write(step4))
self.play(GrowArrow(out_arrow))
self.play(
LaggedStart(
*(FadeIn(bg, shift=LEFT) for bg in bar_groups),
lag_ratio=0.08
)
)
self.wait(0.5)
# Step 5: Sample and add
step5 = Text("5. Sample Token", font_size=24, color=YELLOW)
step5.next_to(step4, DOWN, aligned_edge=LEFT)
# Highlight top prediction
highlight = SurroundingRectangle(bar_groups[0], buff=0.05)
highlight.set_stroke(GREEN, 3)
highlight.set_fill(GREEN, 0.2)
self.play(Write(step5))
self.play(ShowCreation(highlight))
# Add word to text
new_word = Text(" basketball", font_size=32)
new_word.set_color(GREEN)
new_word.next_to(input_text, RIGHT, buff=0.1)
self.play(
FadeIn(new_word, shift=LEFT, scale=1.2),
)
self.wait(2)
def create_llm_model(self):
"""Create a visual representation of the LLM."""
# Stack of blocks
blocks = VGroup()
for i in range(6):
block = Rectangle(3.5, 0.35)
block.set_fill(GREY_D, 0.9)
block.set_stroke(WHITE, 1)
blocks.add(block)
blocks.arrange(DOWN, buff=0.08)
# Label
label = Text("Large Language Model", font_size=24)
label.next_to(blocks, UP, buff=0.2)
# Dials/parameters hint
dots = VGroup()
for block in blocks[:3]:
row_dots = VGroup(*(
Dot(radius=0.03).set_fill(random_bright_color(), 0.7)
for _ in range(8)
))
row_dots.arrange(RIGHT, buff=0.15)
row_dots.move_to(block)
dots.add(row_dots)
return VGroup(blocks, label, dots)
def animate_model_processing(self, model):
"""Animate the model blocks lighting up."""
blocks = model[0]
return LaggedStart(
*(
block.animate.set_fill(TEAL, 0.8).set_anim_args(
rate_func=there_and_back
)
for block in blocks
),
lag_ratio=0.15,
run_time=1.2
)
def build_distribution(self, words, probs, font_size=22, width_100p=2.0, bar_height=0.25):
"""Build probability distribution bars."""
bar_groups = VGroup()
for word, prob in zip(words, probs):
label = Text(word, font_size=font_size)
bar = Rectangle(prob * width_100p, bar_height)
bar.set_fill(interpolate_color(BLUE_E, TEAL, prob / max(probs)), opacity=0.9)
bar.set_stroke(WHITE, 1)
prob_label = Integer(int(100 * prob), unit="%", font_size=font_size * 0.8)
prob_label.next_to(bar, RIGHT, buff=SMALL_BUFF)
label.next_to(bar, LEFT)
bar_groups.add(VGroup(label, bar, prob_label))
bar_groups.arrange(DOWN, aligned_edge=LEFT, buff=0.2)
return bar_groups
class IterativeGeneration(InteractiveScene):
"""
Shows multiple iterations of token generation,
demonstrating the autoregressive nature of LLMs.
"""
def construct(self):
# Starting text
tokens = ["The", " sun", " rises"]
next_tokens = [" in", " the", " east"]
# Display area
text_display = VGroup()
for token in tokens:
t = Text(token, font_size=36)
t.set_color(BLUE_B)
text_display.add(t)
text_display.arrange(RIGHT, buff=0.05)
text_display.to_edge(UP, buff=1.5)
# Model box (simplified)
model_box = Rectangle(2.5, 1.5)
model_box.set_fill(GREY_D, 0.8)
model_box.set_stroke(WHITE, 2)
model_label = Text("LLM", font_size=28)
model_label.move_to(model_box)
model = VGroup(model_box, model_label)
model.center()
self.play(FadeIn(text_display, lag_ratio=0.2))
self.play(FadeIn(model))
self.wait(0.5)
# Generate tokens one by one
for i, next_token in enumerate(next_tokens):
# Arrow from text to model
in_arrow = Arrow(
text_display.get_bottom(),
model.get_top(),
buff=0.2
)
in_arrow.set_color(BLUE)
# Show input flowing
self.play(GrowArrow(in_arrow), run_time=0.4)
# Model processes
self.play(
model_box.animate.set_fill(TEAL, 0.5).set_anim_args(
rate_func=there_and_back
),
run_time=0.5
)
# New token emerges
new_token = Text(next_token, font_size=36)
new_token.set_color(GREEN)
new_token.next_to(text_display, RIGHT, buff=0.05)
out_arrow = Arrow(
model.get_top(),
new_token.get_bottom(),
buff=0.2,
path_arc=-60 * DEGREES
)
out_arrow.set_color(GREEN)
self.play(
GrowArrow(out_arrow),
FadeIn(new_token, scale=1.3),
run_time=0.6
)
# Add to display and clean up
text_display.add(new_token)
new_token.set_color(BLUE_B)
self.play(
FadeOut(in_arrow),
FadeOut(out_arrow),
run_time=0.3
)
# Final result
self.wait()
final_text = VGroup(*text_display).copy()
final_text.generate_target()
final_text.target.center()
final_text.target.shift(UP)
final_text.target.scale(1.2)
self.play(
FadeOut(model),
MoveToTarget(final_text)
)
result_label = Text("Generated Text", font_size=28)
result_label.next_to(final_text, DOWN, buff=0.5)
self.play(Write(result_label))
self.wait(2)
@@ -0,0 +1,193 @@
"""
Lorenz Attractor Visualization
Demonstrates the classic Lorenz strange attractor with multiple trajectories
showing sensitivity to initial conditions (chaos theory).
Run: manimgl lorenz_attractor.py LorenzAttractor
"""
from manimlib import *
from scipy.integrate import solve_ivp
def lorenz_system(t, state, sigma=10, rho=28, beta=8 / 3):
"""
The Lorenz system of differential equations.
These equations model atmospheric convection and exhibit
chaotic behavior for certain parameter values.
"""
x, y, z = state
dxdt = sigma * (y - x)
dydt = x * (rho - z) - y
dzdt = x * y - beta * z
return [dxdt, dydt, dzdt]
def ode_solution_points(function, state0, time, dt=0.01):
"""
Solve an ODE system and return the trajectory points.
Args:
function: The ODE system function
state0: Initial state [x0, y0, z0]
time: Total evolution time
dt: Time step for output points
Returns:
Array of shape (n_points, 3) with trajectory points
"""
solution = solve_ivp(
function,
t_span=(0, time),
y0=state0,
t_eval=np.arange(0, time, dt)
)
return solution.y.T
class LorenzAttractor(InteractiveScene):
"""
Visualizes the Lorenz attractor with multiple trajectories.
Shows how nearby initial conditions diverge over time,
demonstrating the butterfly effect in chaotic systems.
"""
def construct(self):
# Set up 3D axes
axes = ThreeDAxes(
x_range=(-50, 50, 5),
y_range=(-50, 50, 5),
z_range=(-0, 50, 5),
width=16,
height=16,
depth=8,
)
axes.set_width(FRAME_WIDTH)
axes.center()
# Set up camera rotation for 3D viewing
self.frame.reorient(43, 76, 1, IN, 10)
self.frame.add_updater(lambda m, dt: m.increment_theta(dt * 3 * DEGREES))
self.add(axes)
# Add the Lorenz equations
equations = Tex(
R"""
\begin{aligned}
\frac{\mathrm{d} x}{\mathrm{~d} t} & =\sigma(y-x) \\
\frac{\mathrm{d} y}{\mathrm{~d} t} & =x(\rho-z)-y \\
\frac{\mathrm{d} z}{\mathrm{~d} t} & =x y-\beta z
\end{aligned}
""",
t2c={
"x": RED,
"y": GREEN,
"z": BLUE,
},
font_size=30
)
equations.fix_in_frame()
equations.to_corner(UL)
equations.set_backstroke()
self.play(Write(equations))
# Compute trajectories with slightly different initial conditions
epsilon = 1e-5
evolution_time = 30
n_points = 10
states = [
[10, 10, 10 + n * epsilon]
for n in range(n_points)
]
colors = color_gradient([BLUE_E, BLUE_A], len(states))
# Create curves from solutions
curves = VGroup()
for state, color in zip(states, colors):
points = ode_solution_points(lorenz_system, state, evolution_time)
curve = VMobject().set_points_smoothly(axes.c2p(*points.T))
curve.set_stroke(color, 1, opacity=0.25)
curves.add(curve)
curves.set_stroke(width=2, opacity=1)
# Create glowing dots that follow the trajectories
dots = Group(GlowDot(color=color, radius=0.25) for color in colors)
def update_dots(dots, curves=curves):
for dot, curve in zip(dots, curves):
dot.move_to(curve.get_end())
dots.add_updater(update_dots)
# Add tracing tails for visual effect
tail = VGroup(
TracingTail(dot, time_traced=3).match_color(dot)
for dot in dots
)
self.add(dots)
self.add(tail)
curves.set_opacity(0)
# Animate the trajectories
self.play(
*(
ShowCreation(curve, rate_func=linear)
for curve in curves
),
run_time=evolution_time,
)
class LorenzSimple(Scene):
"""
A simpler version of the Lorenz attractor without equations overlay.
Good for demonstrations focused on the attractor itself.
"""
def construct(self):
frame = self.camera.frame
# Set up 3D axes
axes = ThreeDAxes(
x_range=(-50, 50, 10),
y_range=(-50, 50, 10),
z_range=(0, 50, 10),
width=12,
height=12,
depth=6,
)
axes.center()
self.add(axes)
# Set camera angle
frame.set_euler_angles(
phi=70 * DEGREES,
theta=-45 * DEGREES
)
# Add continuous rotation
frame.add_updater(lambda m, dt: m.increment_theta(dt * 2 * DEGREES))
# Compute single trajectory
evolution_time = 40
initial_state = [10, 10, 10]
points = ode_solution_points(lorenz_system, initial_state, evolution_time)
# Create the curve
curve = VMobject()
curve.set_points_smoothly(axes.c2p(*points.T))
curve.set_stroke(
color=color_gradient([BLUE, TEAL, GREEN, YELLOW, RED], 100),
width=2
)
# Animate drawing the curve
self.play(
ShowCreation(curve, rate_func=linear),
run_time=evolution_time,
)
self.wait(2)
@@ -0,0 +1,103 @@
"""
Visualization of lozenge (rhombus) tiling patterns.
Shows how lozenges can tile the plane in a honeycomb-like pattern.
"""
from manimlib import *
import math
def get_lozenge(side_length=1):
"""Create a lozenge (rhombus) shape with 60/120 degree angles."""
verts = [math.sqrt(3) * LEFT, UP, math.sqrt(3) * RIGHT, DOWN]
result = Polygon(*verts)
result.scale(side_length / get_norm(verts[0] - verts[1]))
return result
class LozengeTiling(InteractiveScene):
"""
Demonstrates lozenge tiling of the plane.
Shows:
1. A single lozenge with angle labels
2. How it tiles to create a row
3. How rows tile to fill the plane
4. The effect of stretching on the tiling
"""
def construct(self):
# Add Lozenge
lozenge = get_lozenge()
lozenge.scale(4)
lozenge.set_stroke(TEAL)
arc1 = Arc(-30 * DEGREES, 60 * DEGREES, arc_center=lozenge.get_left(), radius=0.75)
arc2 = Arc(-150 * DEGREES, 120 * DEGREES, arc_center=lozenge.get_top(), radius=0.5)
arc1_label = Tex(R"60^\circ")
arc1_label.next_to(arc1, RIGHT, MED_SMALL_BUFF)
arc2_label = Tex(R"120^\circ")
arc2_label.next_to(arc2, DOWN, MED_SMALL_BUFF)
angle_labels = VGroup(
arc1, arc1_label,
arc2, arc2_label,
)
angle_labels.set_z_index(1)
self.play(
ShowCreation(lozenge, time_span=(1, 2.5)),
VShowPassingFlash(lozenge.copy().insert_n_curves(20).set_stroke(width=5), time_width=2),
run_time=3
)
self.play(
Write(arc1_label),
ShowCreation(arc1),
)
self.play(
Write(arc2_label),
ShowCreation(arc2),
)
self.add(angle_labels)
self.wait()
# Tile the plane
verts = lozenge.get_anchors()[:4]
v1 = verts[1] - verts[0]
v2 = verts[-1] - verts[0]
row = VGroup(lozenge.copy().shift(x * v1) for x in range(-10, 11))
rows = VGroup(row.copy().shift(y * v2) for y in range(-10, 11))
tiles = VGroup(*rows.family_members_with_points())
tiles.sort(lambda p: get_norm(p))
for mob in row, rows:
mob.set_fill(GREY, 1)
mob.set_stroke(WHITE, 2)
mob.shift(-tiles[0].get_center())
self.play(
self.frame.animate.set_height(40),
lozenge.animate.set_fill(GREY, 1),
LaggedStart(
(TransformFromCopy(lozenge, tile, path_arc=30 * DEGREES) for tile in row),
lag_ratio=1.0 / len(row),
time_span=(1, 3),
),
run_time=4
)
self.play(
LaggedStart(
(TransformFromCopy(row, row2, path_arc=30 * DEGREES) for row2 in rows),
lag_ratio=1.0 / len(rows),
run_time=3,
),
)
self.clear()
self.add(rows, angle_labels)
# Squish it
self.play(FadeOut(angle_labels))
rows.save_state()
self.play(rows.animate.stretch(2, 0), run_time=2)
self.wait()
self.play(Restore(rows), run_time=2)
self.play(Write(angle_labels))
self.wait()
@@ -0,0 +1,160 @@
"""
Visualization of max(rand(), rand()) process with animated tracking dots.
Shows how the maximum of two random uniform values behaves over time.
"""
from manimlib import *
import random
class Randomize(Animation):
"""Animation that randomizes a ValueTracker's value at a given frequency."""
def __init__(self, value_tracker, frequency=8, rand_func=random.random, final_value=None, **kwargs):
self.value_tracker = value_tracker
self.rand_func = rand_func
self.frequency = frequency
self.final_value = final_value if final_value is not None else rand_func()
self.last_alpha = 0
self.running_tally = 0
super().__init__(value_tracker, **kwargs)
def interpolate_mobject(self, alpha):
if not self.new_step(alpha):
return
value = self.rand_func() if alpha < 1 else self.final_value
self.value_tracker.set_value(value)
def new_step(self, alpha):
d_alpha = alpha - self.last_alpha
self.last_alpha = alpha
self.running_tally += self.frequency * d_alpha * self.run_time
if self.running_tally > 1:
self.running_tally = self.running_tally % 1
return True
return False
class TrackingDots(Animation):
"""Animation that leaves a trail of fading dots at specified positions."""
def __init__(self, point_func, fade_factor=0.95, radius=0.25, color=YELLOW, **kwargs):
self.point_func = point_func
self.fade_factor = fade_factor
self.dots = GlowDot(point_func(), color=color, radius=radius)
kwargs.update(remover=True)
super().__init__(self.dots, **kwargs)
def interpolate_mobject(self, alpha):
opacities = self.dots.get_opacities()
point = self.point_func()
if not np.isclose(self.dots.get_end(), point).all():
self.dots.add_point(point)
opacities = np.hstack([opacities, [1]])
opacities *= self.fade_factor
self.dots.set_opacity(opacities)
def get_random_var_label_group(axis, label_name, color=GREY, initial_value=None, font_size=36, direction=None):
"""Create a group with a tracker, arrow tip indicator, and label for a random variable on an axis."""
if initial_value is None:
initial_value = random.uniform(*axis.x_range[:2])
tracker = ValueTracker(initial_value)
tip = ArrowTip(angle=90 * DEGREES)
tip.set_height(0.15)
tip.set_fill(color)
tip.rotate(-axis.get_angle())
if direction is None:
direction = np.round(rotate_vector(UP, -axis.get_angle()), 1)
tip.add_updater(lambda m: m.move_to(axis.n2p(tracker.get_value()), direction))
label = Tex(label_name, font_size=font_size)
label.set_color(color)
label.set_backstroke(BLACK, 5)
label.always.next_to(tip, -direction, buff=0.1)
return Group(tracker, tip, label)
class MaxRandomProcess(InteractiveScene):
"""
Visualizes the max(rand(), rand()) process.
Shows three intervals:
- x1 = rand() (blue)
- x2 = rand() (yellow)
- max(x1, x2) (green)
Animated tracking dots show the distribution of values over time.
"""
def construct(self):
# Set up intervals
intervals = VGroup(UnitInterval() for _ in range(3))
intervals.set_width(3)
intervals.arrange(DOWN, buff=2.5)
intervals.shift(2 * LEFT)
intervals[1].shift(0.5 * UP)
for interval in intervals:
interval.add_numbers(np.arange(0, 1.1, 0.2), font_size=16, buff=0.1, direction=UP)
interval.numbers.set_opacity(0.75)
colors = [BLUE, YELLOW, GREEN]
x1_group, x2_group, max_group = groups = Group(
get_random_var_label_group(interval, "", color=color)
for interval, color in zip(intervals, colors)
)
x1_tracker, x1_tip, x1_label = x1_group
x2_tracker, x2_tip, x2_label = x2_group
max_tracker, max_tip, max_label = max_group
max_tracker.add_updater(lambda m: m.set_value(max(x1_tracker.get_value(), x2_tracker.get_value())))
self.add(intervals)
self.add(groups)
# Add labels
tex_to_color = {"x_1": BLUE, "x_2": YELLOW}
labels = VGroup(
Tex(tex + R"\rightarrow 0.00", t2c=tex_to_color)
for tex in [
R"x_1 = \text{rand}()",
R"x_2 = \text{rand}()",
R"\max(x_1, x_2)",
]
)
for label, group, interval in zip(labels, groups, intervals):
label.next_to(interval, RIGHT, buff=0.5)
num = label.make_number_changeable("0.00")
num.tracker = group[0]
num.add_updater(lambda m: m.set_value(m.tracker.get_value()))
self.add(labels)
# Add rectangles
top_rect = SurroundingRectangle(intervals[:2], buff=0.25)
top_rect.stretch(1.1, 1)
top_rect.set_stroke(WHITE, 2)
top_rect.set_fill(GREY_E, 1)
arrow = Vector(1.5 * DOWN, thickness=5)
arrow.next_to(top_rect, DOWN)
arrow_label = Text("max", font_size=60)
arrow_label.next_to(arrow, RIGHT)
self.add(top_rect, intervals, groups)
self.add(arrow, arrow_label)
# Line connecting max to its source
def get_line():
x1 = x1_tracker.get_value()
x2 = x2_tracker.get_value()
tip = x1_tip if x1 > x2 else x2_tip
line = DashedLine(max_tip.get_top(), tip.get_top())
line.set_stroke(GREY, 2, opacity=0.5)
return line
line = always_redraw(get_line)
self.add(line)
# Animate the random process
self.play(
Randomize(x1_tracker, frequency=4, run_time=15),
Randomize(x2_tracker, frequency=4, run_time=15),
TrackingDots(x1_tip.get_top, color=BLUE),
TrackingDots(x2_tip.get_top, color=YELLOW),
TrackingDots(max_tip.get_top, color=GREEN),
)
@@ -0,0 +1,283 @@
"""
MLP Forward Pass Visualization
Shows data flowing through Linear -> ReLU -> Linear operations.
"""
from manimlib import *
import numpy as np
def value_to_color(value, max_value=10.0):
"""Maps a value to blue (positive) or red (negative)."""
alpha = clip(abs(value) / max_value, 0, 1)
if value >= 0:
return interpolate_color_by_hsl(BLUE_E, BLUE_B, alpha)
else:
return interpolate_color_by_hsl(RED_E, RED_B, alpha)
class MLPForwardPass(InteractiveScene):
"""
Shows the three-step MLP forward pass:
1. Linear transformation (matrix multiply + bias)
2. ReLU activation
3. Linear transformation (matrix multiply + bias)
Demonstrates: Sequential animations, data transformation visualization
"""
def construct(self):
# Title
title = Text("MLP Forward Pass", font_size=48)
title.to_edge(UP)
self.play(Write(title))
# Create the three arrows showing the pipeline
arrows = VGroup(
Arrow(ORIGIN, 1.8 * RIGHT) for _ in range(3)
)
arrows.arrange(RIGHT, buff=0.8)
arrows.move_to(ORIGIN)
# Labels for each stage
labels = VGroup(
Text("Linear", font_size=28),
Text("ReLU", font_size=28),
Text("Linear", font_size=28),
)
for label, arrow in zip(labels, arrows):
label.next_to(arrow, UP, buff=0.1)
# Position for vectors at each stage
# Input vector
input_values = np.array([1.5, -0.8, 2.1, -1.4, 0.6])
input_vect = self.create_vector(input_values, YELLOW)
input_vect.next_to(arrows[0], LEFT, buff=0.5)
# After first linear (expanded to 8 neurons)
mid1_values = np.array([2.3, -1.5, 0.8, -2.1, 1.9, -0.3, 0.1, -1.8])
mid1_vect = self.create_vector(mid1_values, None) # Will color by value
# After ReLU (negative values zeroed)
relu_values = np.maximum(mid1_values, 0)
relu_vect = self.create_vector(relu_values, None, zero_color=GREY)
# After second linear (back to 5 output neurons)
output_values = np.array([1.2, 0.5, -0.3, 1.8, 0.9])
output_vect = self.create_vector(output_values, GREEN)
# Position intermediate vectors
vects = [mid1_vect, relu_vect, output_vect]
positions = [
arrows[0].get_right() + 0.5 * RIGHT,
arrows[1].get_right() + 0.5 * RIGHT,
arrows[2].get_right() + 0.5 * RIGHT,
]
for vect, pos in zip(vects, positions):
vect.move_to(pos)
# Show input
self.play(FadeIn(input_vect, shift=LEFT))
self.wait()
# Show first linear arrow
self.play(
GrowArrow(arrows[0]),
FadeIn(labels[0])
)
# Animate transformation to mid1
self.play(
TransformFromCopy(input_vect, mid1_vect, run_time=1.5)
)
self.wait()
# Show ReLU arrow
self.play(
GrowArrow(arrows[1]),
FadeIn(labels[1])
)
# Show negative values being zeroed
neg_highlights = VGroup()
for i, val in enumerate(mid1_values):
if val < 0:
rect = SurroundingRectangle(mid1_vect[i], buff=0.05)
rect.set_stroke(RED, 2)
neg_highlights.add(rect)
self.play(ShowCreation(neg_highlights, lag_ratio=0.2))
self.wait(0.5)
# Transform to ReLU output
self.play(
TransformFromCopy(mid1_vect, relu_vect),
FadeOut(neg_highlights),
run_time=1.5
)
self.wait()
# Show second linear arrow
self.play(
GrowArrow(arrows[2]),
FadeIn(labels[2])
)
# Final transformation
self.play(
TransformFromCopy(relu_vect, output_vect, run_time=1.5)
)
self.wait(2)
def create_vector(self, values, color=None, zero_color=GREY):
"""Creates a vertical vector display with colored entries."""
entries = VGroup()
for val in values:
entry = DecimalNumber(
val,
num_decimal_places=1,
include_sign=True,
font_size=24
)
if color is not None:
entry.set_color(color)
elif val == 0:
entry.set_color(zero_color)
else:
entry.set_color(value_to_color(val, max_value=3))
entries.add(entry)
entries.arrange(DOWN, buff=0.15)
# Add brackets
left_b = Tex("[").stretch_to_fit_height(entries.get_height() * 1.1)
right_b = Tex("]").stretch_to_fit_height(entries.get_height() * 1.1)
left_b.next_to(entries, LEFT, buff=0.05)
right_b.next_to(entries, RIGHT, buff=0.05)
return VGroup(*entries, left_b, right_b)
class MLPBlockDiagram(InteractiveScene):
"""
Shows a high-level block diagram of an MLP.
Input -> [Up Projection] -> [Nonlinearity] -> [Down Projection] -> Output
Demonstrates: Block diagram style, text labels, arrows
"""
def construct(self):
# Title
title = Text("MLP Block Structure", font_size=48)
title.to_edge(UP)
# Create blocks
def create_block(text, color, width=2.5, height=1.5):
rect = Rectangle(width=width, height=height)
rect.set_fill(color, 0.3)
rect.set_stroke(color, 2)
label = Text(text, font_size=24)
label.move_to(rect)
return VGroup(rect, label)
up_proj = create_block("Up\nProjection", BLUE)
nonlin = create_block("ReLU", YELLOW, width=1.5)
down_proj = create_block("Down\nProjection", GREEN)
# Arrange blocks
blocks = VGroup(up_proj, nonlin, down_proj)
blocks.arrange(RIGHT, buff=1.0)
# Arrows between blocks
arrow1 = Arrow(up_proj.get_right(), nonlin.get_left(), buff=0.1)
arrow2 = Arrow(nonlin.get_right(), down_proj.get_left(), buff=0.1)
# Input/Output arrows
input_arrow = Arrow(up_proj.get_left() + LEFT, up_proj.get_left(), buff=0.1)
output_arrow = Arrow(down_proj.get_right(), down_proj.get_right() + RIGHT, buff=0.1)
# Input/Output labels
input_label = Tex(R"\vec{E}", font_size=36)
input_label.next_to(input_arrow, LEFT)
output_label = Tex(R"\vec{E}'", font_size=36)
output_label.next_to(output_arrow, RIGHT)
# Dimension labels
dim_in = Text("d", font_size=20, color=GREY)
dim_mid = Text("4d", font_size=20, color=GREY)
dim_out = Text("d", font_size=20, color=GREY)
dim_in.next_to(input_arrow, DOWN, buff=0.1)
dim_mid.next_to(arrow1, DOWN, buff=0.1)
dim_out.next_to(output_arrow, DOWN, buff=0.1)
# Build the scene
self.play(Write(title))
self.play(
FadeIn(input_label),
GrowArrow(input_arrow)
)
self.play(FadeIn(up_proj, shift=RIGHT))
self.play(
GrowArrow(arrow1),
FadeIn(dim_in)
)
self.play(FadeIn(nonlin, shift=RIGHT))
self.play(
GrowArrow(arrow2),
FadeIn(dim_mid)
)
self.play(FadeIn(down_proj, shift=RIGHT))
self.play(
GrowArrow(output_arrow),
FadeIn(output_label),
FadeIn(dim_out)
)
self.wait()
# Show data flow animation
data_dot = Dot(radius=0.1, color=ORANGE)
data_dot.move_to(input_arrow.get_start())
path = VMobject()
path.set_points_as_corners([
input_arrow.get_start(),
input_arrow.get_end(),
up_proj.get_center(),
arrow1.get_start(),
arrow1.get_end(),
nonlin.get_center(),
arrow2.get_start(),
arrow2.get_end(),
down_proj.get_center(),
output_arrow.get_start(),
output_arrow.get_end(),
])
self.play(
MoveAlongPath(data_dot, path, run_time=4),
)
self.wait()
# Highlight the residual connection concept
residual_label = Text(
"Output = Input + MLP(Input)",
font_size=28
)
residual_label.next_to(blocks, DOWN, buff=1.0)
plus_sign = Tex("+", font_size=48)
plus_sign.next_to(down_proj, RIGHT, buff=0.5)
skip_arrow = CurvedArrow(
input_arrow.get_end() + 0.2 * UP,
plus_sign.get_left() + 0.1 * LEFT,
angle=-TAU/4
)
skip_arrow.set_color(PINK)
self.play(
FadeIn(residual_label),
ShowCreation(skip_arrow),
Write(plus_sign)
)
self.wait(2)
@@ -0,0 +1,105 @@
"""
MLP Network Icon - A simple visualization of a multilayer perceptron structure
Shows dots arranged in layers with connecting lines between neurons.
"""
from manimlib import *
import random
class MLPNetworkIcon(InteractiveScene):
"""
Creates a classic MLP icon with three layers:
- Input layer
- Hidden layer (wider)
- Output layer
Demonstrates: VGroup organization, Line connections, random styling
"""
def construct(self):
# Create the MLP icon
network = self.get_mlp_icon(layer_buff=2.5, layer0_size=5)
# Animate the network appearing
self.play(Write(network, stroke_width=0.5, lag_ratio=1e-2, run_time=3))
self.wait()
# Show data propagating through the network
lines = VGroup(network[1].family_members_with_points()).copy()
for line in lines:
line.set_stroke(width=2 * line.get_width())
line.insert_n_curves(20)
self.play(
LaggedStartMap(
VShowPassingFlash,
lines,
time_width=1.5,
lag_ratio=5e-3,
run_time=3
)
)
self.wait()
def get_mlp_icon(self, dot_buff=0.15, layer_buff=1.5, layer0_size=5):
"""
Creates an MLP icon with three layers.
Args:
dot_buff: Spacing between neurons in a layer
layer_buff: Spacing between layers
layer0_size: Number of neurons in input/output layers
"""
# Create three layers of dots
layers = VGroup(
Dot().get_grid(layer0_size, 1, buff=dot_buff),
Dot().get_grid(2 * layer0_size, 1, buff=dot_buff), # Hidden layer is wider
Dot().get_grid(layer0_size, 1, buff=dot_buff),
)
layers.set_height(4)
layers.arrange(RIGHT, buff=layer_buff)
# Set random opacities for visual interest
for layer in layers:
for dot in layer:
dot.set_fill(opacity=random.random())
layers.set_stroke(WHITE, 0.5)
# Create connection lines between layers
lines = VGroup(
Line(
dot1.get_center(),
dot2.get_center(),
buff=dot1.get_width() / 2
)
for l1, l2 in zip(layers, layers[1:])
for dot1 in l1
for dot2 in l2
)
# Color and style the lines randomly
for line in lines:
line.set_stroke(
color=self.value_to_color(random.uniform(-10, 10)),
width=3 * random.random()**3
)
return VGroup(layers, lines)
def value_to_color(
self,
value,
low_positive_color=BLUE_E,
high_positive_color=BLUE_B,
low_negative_color=RED_E,
high_negative_color=RED_B,
min_value=0.0,
max_value=10.0
):
"""Maps a numeric value to a color based on its sign and magnitude."""
alpha = clip(float(inverse_interpolate(min_value, max_value, abs(value))), 0, 1)
if value >= 0:
colors = (low_positive_color, high_positive_color)
else:
colors = (low_negative_color, high_negative_color)
return interpolate_color_by_hsl(*colors, alpha)
@@ -0,0 +1,246 @@
"""
Neuron Activation Visualization
Shows neurons firing based on input patterns, with active/inactive states.
"""
from manimlib import *
import numpy as np
class NeuronActivationStates(InteractiveScene):
"""
Visualizes neurons as dots with varying activation levels.
Active neurons glow, inactive neurons are dim.
Demonstrates: Dot animations, opacity changes, highlighting
"""
def construct(self):
# Title
title = Text("Neuron Activations", font_size=48)
title.to_edge(UP)
# Create a column of neurons (dots)
neuron_values = [0.0, 0.8, 0.0, 0.5, 0.9, 0.0, 0.3, 0.0, 0.7]
neurons = VGroup()
for val in neuron_values:
neuron = Dot(radius=0.25)
neuron.set_stroke(WHITE, 2)
# Active neurons are bright, inactive are dim
if val > 0:
neuron.set_fill(BLUE, opacity=val)
else:
neuron.set_fill(GREY_D, opacity=0.3)
neurons.add(neuron)
neurons.arrange(DOWN, buff=0.15)
neurons.set_height(5)
neurons.move_to(ORIGIN)
# Add labels showing activation values
labels = VGroup()
for i, (neuron, val) in enumerate(zip(neurons, neuron_values)):
label = DecimalNumber(val, num_decimal_places=1, font_size=24)
label.next_to(neuron, RIGHT, buff=0.5)
if val > 0:
label.set_color(BLUE)
else:
label.set_color(GREY)
labels.add(label)
# Animate appearance
self.play(Write(title))
self.play(
LaggedStartMap(GrowFromCenter, neurons, lag_ratio=0.1)
)
self.play(
LaggedStartMap(FadeIn, labels, shift=LEFT, lag_ratio=0.1)
)
self.wait()
# Highlight active vs inactive
active_rect = SurroundingRectangle(
VGroup(neurons[1], neurons[4], neurons[6], neurons[8]),
buff=0.15
)
active_rect.set_stroke(GREEN, 3)
active_label = Text("Active", font_size=30, color=GREEN)
active_label.next_to(active_rect, LEFT, buff=0.5)
inactive_rect = SurroundingRectangle(
VGroup(neurons[0], neurons[2], neurons[5], neurons[7]),
buff=0.15
)
inactive_rect.set_stroke(RED, 3)
inactive_label = Text("Inactive", font_size=30, color=RED)
inactive_label.next_to(inactive_rect, LEFT, buff=0.5)
self.play(
ShowCreation(active_rect),
FadeIn(active_label)
)
self.wait()
self.play(
ShowCreation(inactive_rect),
FadeIn(inactive_label)
)
self.wait(2)
# Show activation changing
self.play(
FadeOut(active_rect),
FadeOut(active_label),
FadeOut(inactive_rect),
FadeOut(inactive_label)
)
# Animate neurons activating/deactivating
new_values = [0.9, 0.0, 0.6, 0.0, 0.0, 0.8, 0.0, 0.4, 0.0]
anims = []
for neuron, label, old_val, new_val in zip(neurons, labels, neuron_values, new_values):
if new_val > 0:
anims.append(neuron.animate.set_fill(BLUE, opacity=new_val))
else:
anims.append(neuron.animate.set_fill(GREY_D, opacity=0.3))
anims.append(ChangeDecimalToValue(label, new_val))
if new_val > 0:
anims.append(label.animate.set_color(BLUE))
else:
anims.append(label.animate.set_color(GREY))
self.play(*anims, run_time=2)
self.wait(2)
class ClassicNeuronDiagram(InteractiveScene):
"""
Shows the classic neural network diagram with connected nodes.
Inputs feed into hidden layer neurons which connect to outputs.
Demonstrates: VGroup, Line connections, network structure
"""
def construct(self):
# Title
title = Text("Neural Network Layer", font_size=42)
title.to_edge(UP)
# Create three layers
input_layer = VGroup(
Dot(radius=0.2) for _ in range(4)
)
input_layer.arrange(DOWN, buff=0.5)
input_layer.set_fill(YELLOW, 0.8)
input_layer.set_stroke(WHITE, 2)
hidden_layer = VGroup(
Dot(radius=0.2) for _ in range(6)
)
hidden_layer.arrange(DOWN, buff=0.35)
hidden_layer.set_stroke(WHITE, 2)
output_layer = VGroup(
Dot(radius=0.2) for _ in range(3)
)
output_layer.arrange(DOWN, buff=0.6)
output_layer.set_fill(GREEN, 0.8)
output_layer.set_stroke(WHITE, 2)
# Position layers
layers = VGroup(input_layer, hidden_layer, output_layer)
layers.arrange(RIGHT, buff=2.5)
# Create connections
def create_connections(layer1, layer2):
lines = VGroup()
for n1 in layer1:
for n2 in layer2:
line = Line(n1.get_center(), n2.get_center(), buff=0.2)
line.set_stroke(GREY, 1, opacity=0.5)
lines.add(line)
return lines
connections1 = create_connections(input_layer, hidden_layer)
connections2 = create_connections(hidden_layer, output_layer)
# Layer labels
input_label = Text("Input", font_size=28)
input_label.next_to(input_layer, DOWN)
hidden_label = Text("Hidden", font_size=28)
hidden_label.next_to(hidden_layer, DOWN)
output_label = Text("Output", font_size=28)
output_label.next_to(output_layer, DOWN)
# Animate construction
self.play(Write(title))
self.play(
LaggedStartMap(GrowFromCenter, input_layer, lag_ratio=0.2),
FadeIn(input_label)
)
self.play(
ShowCreation(connections1, lag_ratio=0.01, run_time=2),
LaggedStartMap(GrowFromCenter, hidden_layer, lag_ratio=0.1),
FadeIn(hidden_label)
)
self.play(
ShowCreation(connections2, lag_ratio=0.01, run_time=2),
LaggedStartMap(GrowFromCenter, output_layer, lag_ratio=0.2),
FadeIn(output_label)
)
self.wait()
# Show activation propagating
for i, neuron in enumerate(hidden_layer):
# Random activation
activation = np.random.random()
if activation > 0.5:
neuron.set_fill(BLUE, activation)
else:
neuron.set_fill(GREY_D, 0.3)
self.play(
LaggedStart(
*(
neuron.animate.set_fill(
BLUE if np.random.random() > 0.4 else GREY_D,
np.random.random() if np.random.random() > 0.4 else 0.3
)
for neuron in hidden_layer
),
lag_ratio=0.1
)
)
self.wait()
# Highlight signal flow with VShowPassingFlash
flash_lines = connections1.copy()
for line in flash_lines:
line.set_stroke(YELLOW, 3)
line.insert_n_curves(20)
self.play(
LaggedStartMap(
VShowPassingFlash,
flash_lines,
time_width=0.5,
lag_ratio=0.02,
run_time=2
)
)
flash_lines2 = connections2.copy()
for line in flash_lines2:
line.set_stroke(GREEN, 3)
line.insert_n_curves(20)
self.play(
LaggedStartMap(
VShowPassingFlash,
flash_lines2,
time_width=0.5,
lag_ratio=0.02,
run_time=2
)
)
self.wait(2)
@@ -0,0 +1,331 @@
"""
MLP/Feedforward Neurons Flow Visualization
Shows data flowing through neurons in an MLP/Feedforward layer.
Based on 3Blue1Brown's transformer visualizations.
Run: manimgl mlp_neurons_flow.py MLPNeuronsFlow -o
"""
from manimlib import *
import numpy as np
import random
import itertools as it
def value_to_color(
value,
low_positive_color=BLUE_E,
high_positive_color=BLUE_B,
low_negative_color=RED_E,
high_negative_color=RED_B,
min_value=0.0,
max_value=10.0
):
"""Map a value to a color based on its sign and magnitude."""
alpha = np.clip(float((abs(value) - min_value) / (max_value - min_value)), 0, 1)
if value >= 0:
return interpolate_color(low_positive_color, high_positive_color, alpha)
else:
return interpolate_color(low_negative_color, high_negative_color, alpha)
class MLPNeuronsFlow(Scene):
"""
Visualizes data flowing through MLP/Feedforward neurons.
Shows the expansion and contraction of data through the hidden layer.
"""
def construct(self):
frame = self.camera.frame
# Title
title = Text("Feedforward Layer: Neurons in Action", font_size=48)
title.to_edge(UP)
self.play(Write(title))
self.wait()
# Create input layer (small)
input_layer = self.create_layer(8, radius=0.15, color=BLUE)
input_layer.to_edge(LEFT, buff=2)
# Create hidden layer (large - 4x expansion)
hidden_layer = self.create_layer(24, radius=0.12, color=GREEN)
hidden_layer.center()
# Create output layer (same as input)
output_layer = self.create_layer(8, radius=0.15, color=BLUE)
output_layer.to_edge(RIGHT, buff=2)
# Labels
input_label = Text("Input\n(d dims)", font_size=24)
input_label.next_to(input_layer, DOWN)
hidden_label = Text("Hidden\n(4d dims)", font_size=24)
hidden_label.next_to(hidden_layer, DOWN)
output_label = Text("Output\n(d dims)", font_size=24)
output_label.next_to(output_layer, DOWN)
# Show layers
self.play(
FadeIn(input_layer, shift=RIGHT),
FadeIn(input_label),
)
self.wait()
self.play(
FadeIn(hidden_layer, scale=0.8),
FadeIn(hidden_label),
)
self.wait()
self.play(
FadeIn(output_layer, shift=LEFT),
FadeIn(output_label),
)
self.wait()
# Create connections (sparse for visibility)
connections_in = self.create_connections(input_layer, hidden_layer, density=0.15)
connections_out = self.create_connections(hidden_layer, output_layer, density=0.15)
self.play(
Write(connections_in, stroke_width=1),
run_time=2
)
self.play(
Write(connections_out, stroke_width=1),
run_time=2
)
self.wait()
# Animate data flow
self.play_data_flow(connections_in, connections_out)
# Show "this happens per token" note
note = Text("This happens independently for each token position", font_size=30)
note.next_to(title, DOWN, buff=0.5)
self.play(FadeIn(note, shift=DOWN))
self.wait(2)
# Cleanup
self.play(FadeOut(VGroup(
title, note,
input_layer, hidden_layer, output_layer,
input_label, hidden_label, output_label,
connections_in, connections_out
)))
def create_layer(self, n_neurons, radius=0.15, color=BLUE):
"""Create a vertical layer of neurons."""
neurons = VGroup()
for _ in range(n_neurons):
dot = Dot(radius=radius)
dot.set_fill(color, opacity=random.uniform(0.5, 1.0))
dot.set_stroke(WHITE, 1)
neurons.add(dot)
neurons.arrange(DOWN, buff=0.15)
neurons.set_height(5)
return neurons
def create_connections(self, layer1, layer2, density=0.2):
"""Create sparse connections between two layers."""
lines = VGroup()
for n1 in layer1:
for n2 in layer2:
if random.random() < density:
line = Line(
n1.get_center(), n2.get_center(),
buff=n1.get_width() / 2
)
line.set_stroke(
color=value_to_color(random.uniform(-10, 10)),
width=2 * random.random(),
opacity=0.6
)
lines.add(line)
return lines
def play_data_flow(self, connections_in, connections_out):
"""Animate data flowing through the network."""
for _ in range(2):
self.play(
LaggedStart(*(
VShowPassingFlash(line.copy().set_stroke(YELLOW, 3), time_width=0.5)
for line in connections_in
), lag_ratio=0.01),
run_time=1.5
)
self.play(
LaggedStart(*(
VShowPassingFlash(line.copy().set_stroke(YELLOW, 3), time_width=0.5)
for line in connections_out
), lag_ratio=0.01),
run_time=1.5
)
class NeuralNetworkBasic(Scene):
"""
Simple neural network visualization with multiple layers.
"""
def construct(self):
# Create network
layer_sizes = [6, 12, 6]
layers = VGroup()
for n in layer_sizes:
layer = VGroup(*(
Dot(radius=0.12).set_fill(WHITE, opacity=random.uniform(0.4, 1.0))
for _ in range(n)
))
layer.arrange(DOWN, buff=0.2)
layers.add(layer)
layers.arrange(RIGHT, buff=2.5)
layers.center()
# Create connections
all_connections = VGroup()
for l1, l2 in zip(layers[:-1], layers[1:]):
connections = VGroup()
for n1 in l1:
for n2 in l2:
line = Line(n1.get_center(), n2.get_center(), buff=0.12)
line.set_stroke(
value_to_color(random.uniform(-10, 10)),
width=2 * random.random() ** 2,
opacity=0.5
)
connections.add(line)
all_connections.add(connections)
# Layer labels
labels = VGroup(
Text("Input", font_size=30),
Text("Hidden", font_size=30),
Text("Output", font_size=30),
)
for label, layer in zip(labels, layers):
label.next_to(layer, DOWN, buff=0.5)
# Title
title = Text("Simple Neural Network", font_size=48)
title.to_edge(UP)
# Animate
self.play(Write(title))
self.play(LaggedStartMap(FadeIn, layers[0], shift=RIGHT, lag_ratio=0.1))
self.play(FadeIn(labels[0]))
for i, (connections, layer, label) in enumerate(zip(all_connections, layers[1:], labels[1:])):
self.play(
Write(connections, lag_ratio=0.01),
run_time=1.5
)
self.play(
LaggedStartMap(FadeIn, layer, shift=RIGHT, lag_ratio=0.1),
FadeIn(label),
)
self.wait()
# Animate forward pass
for _ in range(2):
for connections in all_connections:
self.play(
LaggedStart(*(
VShowPassingFlash(
line.copy().set_stroke(YELLOW, 4),
time_width=0.8
)
for line in connections
), lag_ratio=0.005),
run_time=1.5
)
self.wait()
# Cleanup
self.play(FadeOut(VGroup(title, layers, all_connections, labels)))
class MLPExpansion3D(Scene):
"""
3D visualization of MLP expansion from d to 4d dimensions.
"""
def construct(self):
frame = self.camera.frame
frame.set_euler_angles(phi=70 * DEGREES, theta=-30 * DEGREES)
# Title
title = Text("MLP: Dimension Expansion", font_size=48)
title.to_edge(UP)
title.fix_in_frame()
self.play(Write(title))
# Create 3D neuron clusters
input_neurons = self.create_3d_cluster(8, spread=0.3, color=BLUE)
input_neurons.shift(3 * LEFT)
hidden_neurons = self.create_3d_cluster(32, spread=0.8, color=GREEN)
output_neurons = self.create_3d_cluster(8, spread=0.3, color=BLUE)
output_neurons.shift(3 * RIGHT)
# Labels
for neurons, text in [(input_neurons, "d"), (hidden_neurons, "4d"), (output_neurons, "d")]:
label = Text(text, font_size=36)
label.next_to(neurons, DOWN, buff=0.5)
neurons.add(label)
# Show progression
self.play(FadeIn(input_neurons, scale=0.8))
self.wait()
self.play(
frame.animate.set_euler_angles(phi=65 * DEGREES, theta=-45 * DEGREES),
TransformFromCopy(input_neurons[:-1], hidden_neurons[:-1]),
FadeIn(hidden_neurons[-1]),
run_time=2
)
self.wait()
self.play(
frame.animate.set_euler_angles(phi=60 * DEGREES, theta=-60 * DEGREES),
TransformFromCopy(hidden_neurons[:-1], output_neurons[:-1]),
FadeIn(output_neurons[-1]),
run_time=2
)
self.wait()
# Rotate view
self.play(
frame.animate.increment_theta(90 * DEGREES),
run_time=3
)
self.wait()
# Cleanup
self.play(FadeOut(VGroup(title, input_neurons, hidden_neurons, output_neurons)))
def create_3d_cluster(self, n_points, spread=0.5, color=BLUE):
"""Create a 3D cluster of points/neurons."""
points = np.random.randn(n_points, 3) * spread
dots = VGroup()
for point in points:
dot = Dot3D(radius=0.08)
dot.move_to(point)
dot.set_color(color)
dot.set_opacity(random.uniform(0.6, 1.0))
dots.add(dot)
return dots
@@ -0,0 +1,179 @@
"""
ReLU Activation Function Visualization
Shows the ReLU and GELU activation functions used in neural networks.
"""
from manimlib import *
from scipy.stats import norm
class ReLUVisualization(InteractiveScene):
"""
Visualizes the ReLU (Rectified Linear Unit) activation function
and compares it with GELU.
Demonstrates: Axes, graph plotting, labels, transitions
"""
def construct(self):
# Create axes for the activation function
axes = Axes(
x_range=(-4, 4),
y_range=(-1, 4),
axis_config=dict(include_tip=True),
)
axes.set_width(8)
axes.add_coordinate_labels(font_size=20)
# Graph ReLU: f(x) = max(0, x)
relu_graph = axes.get_graph(
lambda x: max(0, x),
discontinuities=[0]
)
relu_graph.set_stroke(YELLOW, 4)
# Labels
relu_title = Text("Rectified Linear Unit (ReLU)", font_size=36)
relu_title.to_edge(UP)
relu_label = Text("ReLU", font_size=30)
relu_label.set_color(YELLOW)
relu_label.move_to(axes.c2p(2, 3))
# Formula
relu_formula = Tex(R"f(x) = \max(0, x)", font_size=36)
relu_formula.next_to(axes, DOWN)
# Animate building the scene
self.play(Write(axes))
self.play(
Write(relu_title),
ShowCreation(relu_graph, run_time=2)
)
self.play(
FadeIn(relu_label),
Write(relu_formula)
)
self.wait(2)
# Show GELU comparison
gelu_graph = axes.get_graph(lambda x: x * norm.cdf(x))
gelu_graph.set_stroke(GREEN, 4)
gelu_label = Text("GELU", font_size=30)
gelu_label.set_color(GREEN)
gelu_label.next_to(relu_label, DOWN, buff=0.5, aligned_edge=LEFT)
gelu_title = Text("Gaussian Error Linear Unit (GELU)", font_size=36)
gelu_title.to_edge(UP)
self.play(
relu_graph.animate.set_stroke(opacity=0.3),
relu_label.animate.set_fill(opacity=0.3),
FadeTransform(relu_title, gelu_title),
ShowCreation(gelu_graph),
FadeIn(gelu_label)
)
self.wait(2)
# Back to ReLU
self.play(
gelu_graph.animate.set_stroke(opacity=0.3),
gelu_label.animate.set_fill(opacity=0.3),
relu_graph.animate.set_stroke(opacity=1),
relu_label.animate.set_fill(opacity=1),
FadeTransform(gelu_title, relu_title)
)
self.wait(2)
class ReLUNeuronBehavior(InteractiveScene):
"""
Shows how ReLU affects neuron values - negative values become 0,
positive values pass through unchanged.
Demonstrates: DecimalNumber, color coding, visual feedback
"""
def construct(self):
# Create input values
input_values = [-3.5, -1.2, 0.5, 2.8, -0.7, 1.5, 4.2, -2.1, 0.0]
output_values = [max(0, v) for v in input_values]
# Create input column
input_entries = VGroup()
output_entries = VGroup()
for val in input_values:
entry = DecimalNumber(val, num_decimal_places=1, include_sign=True)
entry.set_color(BLUE if val >= 0 else RED)
input_entries.add(entry)
for val in output_values:
entry = DecimalNumber(val, num_decimal_places=1, include_sign=True)
entry.set_color(BLUE if val > 0 else GREY)
output_entries.add(entry)
input_entries.arrange(DOWN, buff=0.3)
output_entries.arrange(DOWN, buff=0.3)
# Add brackets
input_group = VGroup(
Tex("["),
input_entries,
Tex("]")
)
input_group[0].next_to(input_entries, LEFT)
input_group[2].next_to(input_entries, RIGHT)
output_group = VGroup(
Tex("["),
output_entries,
Tex("]")
)
output_group[0].next_to(output_entries, LEFT)
output_group[2].next_to(output_entries, RIGHT)
# Position groups
input_group.move_to(2 * LEFT)
output_group.move_to(2 * RIGHT)
# Arrow with ReLU label
arrow = Arrow(input_group.get_right(), output_group.get_left(), buff=0.3)
relu_label = Text("ReLU", font_size=36)
relu_label.next_to(arrow, UP)
# Title
title = Text("ReLU: Negative values become zero", font_size=36)
title.to_edge(UP)
# Animate
self.play(Write(title))
self.play(FadeIn(input_group, shift=LEFT))
self.play(
GrowArrow(arrow),
FadeIn(relu_label)
)
self.wait()
# Transform input to output with highlighting
for i, (inp, out) in enumerate(zip(input_entries, output_entries)):
inp_copy = inp.copy()
if input_values[i] < 0:
# Highlight negative -> zero transformation
self.play(
Transform(inp_copy, out),
Flash(inp, color=RED),
run_time=0.5
)
else:
self.play(
Transform(inp_copy, out),
run_time=0.3
)
output_group.add(inp_copy)
self.play(
FadeIn(output_group[0]),
FadeIn(output_group[2])
)
self.wait(2)
@@ -0,0 +1,266 @@
"""
Vector Space and Dot Products for MLPs
Shows how dot products can be used to detect features in embeddings.
"""
from manimlib import *
import numpy as np
class VectorDotProduct(InteractiveScene):
"""
Visualizes how the dot product between a feature direction
and an embedding determines neuron activation.
Demonstrates: NumberPlane, vectors, dot product projection
"""
def construct(self):
# Create 2D plane for visualization
unit_size = 2.0
plane = NumberPlane(
x_range=(-3, 3),
y_range=(-3, 3),
axis_config=dict(stroke_width=1),
background_line_style=dict(
stroke_color=BLUE_D,
stroke_width=1,
stroke_opacity=0.5
),
faded_line_ratio=1,
unit_size=unit_size,
)
plane.shift(DOWN * 0.5)
# Title
title = Text("Dot Product as Feature Detection", font_size=36)
title.to_edge(UP)
self.play(Write(title))
self.play(FadeIn(plane))
# Feature direction vector (represents what the neuron is looking for)
feature_angle = 60 * DEGREES
feature_vect = Vector(
unit_size * np.array([np.cos(feature_angle), np.sin(feature_angle), 0])
)
feature_vect.set_color(RED)
feature_vect.shift(plane.get_origin())
feature_label = Text("Feature\nDirection", font_size=20)
feature_label.set_color(RED)
feature_label.next_to(feature_vect.get_end(), UR, buff=0.1)
self.play(
GrowArrow(feature_vect),
FadeIn(feature_label)
)
self.wait()
# Embedding vector (the input we're testing)
emb_vect = Vector(unit_size * 1.5 * RIGHT)
emb_vect.set_color(YELLOW)
emb_vect.shift(plane.get_origin())
emb_label = Tex(R"\vec{E}", font_size=36)
emb_label.set_color(YELLOW)
emb_label.next_to(emb_vect.get_end(), DR, buff=0.1)
self.play(
GrowArrow(emb_vect),
FadeIn(emb_label)
)
self.wait()
# Show the projection (dot product visualization)
feature_unit = normalize(feature_vect.get_vector())
def get_projection_point():
emb_vec = emb_vect.get_end() - plane.get_origin()
proj_length = np.dot(emb_vec, feature_unit)
return plane.get_origin() + proj_length * feature_unit
proj_line = Line(plane.get_origin(), get_projection_point())
proj_line.set_stroke(PINK, 4)
dashed_line = DashedLine(emb_vect.get_end(), get_projection_point())
dashed_line.set_stroke(GREY, 2)
proj_dot = Dot(get_projection_point(), radius=0.1)
proj_dot.set_color(PINK)
self.play(
ShowCreation(proj_line),
ShowCreation(dashed_line),
GrowFromCenter(proj_dot)
)
# Dot product value
dp_value = DecimalNumber(
np.dot(emb_vect.get_end() - plane.get_origin(), feature_unit) / unit_size,
num_decimal_places=2,
font_size=30
)
dp_value.set_color(PINK)
dp_label = Text("Dot Product: ", font_size=24)
dp_display = VGroup(dp_label, dp_value).arrange(RIGHT)
dp_display.next_to(proj_dot, RIGHT, buff=0.3)
self.play(FadeIn(dp_display))
self.wait()
# Animate the embedding vector rotating
original_angle = 0
def update_emb_vect(mob, angle):
new_end = plane.get_origin() + unit_size * 1.5 * np.array([
np.cos(angle), np.sin(angle), 0
])
mob.put_start_and_end_on(plane.get_origin(), new_end)
emb_label.next_to(new_end, normalize(new_end - plane.get_origin()), buff=0.1)
def update_projection():
proj_pt = get_projection_point()
proj_line.put_start_and_end_on(plane.get_origin(), proj_pt)
dashed_line.put_start_and_end_on(emb_vect.get_end(), proj_pt)
proj_dot.move_to(proj_pt)
dp_val = np.dot(emb_vect.get_end() - plane.get_origin(), feature_unit) / unit_size
dp_value.set_value(dp_val)
dp_display.next_to(proj_dot, RIGHT, buff=0.3)
# Rotate through different angles
for target_angle in [45 * DEGREES, 90 * DEGREES, 150 * DEGREES, 220 * DEGREES, 300 * DEGREES, 0]:
self.play(
Rotate(
emb_vect,
target_angle - original_angle,
about_point=plane.get_origin()
),
UpdateFromFunc(proj_line, lambda m: update_projection()),
run_time=1.5
)
update_projection()
original_angle = target_angle
self.wait(0.5)
self.wait()
class FeatureDirectionThreshold(InteractiveScene):
"""
Shows how a threshold on the dot product creates a decision boundary.
Positive side = "Yes", Negative side = "No".
Demonstrates: Regions, decision boundaries, classification
"""
def construct(self):
# Create plane
unit_size = 2.0
plane = NumberPlane(
x_range=(-3, 3),
y_range=(-3, 3),
axis_config=dict(stroke_width=1),
background_line_style=dict(
stroke_color=BLUE_D,
stroke_width=1,
stroke_opacity=0.5
),
faded_line_ratio=1,
unit_size=unit_size,
)
# Title
title = Text("Decision Boundary from Dot Product", font_size=32)
title.to_edge(UP)
self.play(Write(title))
self.play(FadeIn(plane))
# Feature direction
feature_angle = 45 * DEGREES
feature_dir = np.array([np.cos(feature_angle), np.sin(feature_angle), 0])
feature_vect = Vector(unit_size * feature_dir)
feature_vect.set_color(WHITE)
# Decision boundary (perpendicular to feature direction)
perp_dir = np.array([-feature_dir[1], feature_dir[0], 0])
boundary_line = Line(
-4 * perp_dir * unit_size,
4 * perp_dir * unit_size
)
boundary_line.set_stroke(WHITE, 3)
self.play(GrowArrow(feature_vect))
self.play(ShowCreation(boundary_line))
# Create "Yes" and "No" regions
yes_region = Rectangle(width=8, height=8)
yes_region.set_fill(GREEN, 0.2)
yes_region.set_stroke(width=0)
yes_region.rotate(feature_angle)
yes_region.shift(2 * feature_dir * unit_size)
no_region = Rectangle(width=8, height=8)
no_region.set_fill(RED, 0.15)
no_region.set_stroke(width=0)
no_region.rotate(feature_angle)
no_region.shift(-2 * feature_dir * unit_size)
# Clip regions to visible area
yes_region.set_clip_path(Rectangle(width=12, height=8))
no_region.set_clip_path(Rectangle(width=12, height=8))
self.play(
FadeIn(yes_region),
FadeIn(no_region)
)
# Labels
yes_label = Text("Yes", font_size=36, color=GREEN)
yes_label.move_to(2.5 * feature_dir * unit_size)
no_label = Text("No", font_size=36, color=RED)
no_label.move_to(-2.5 * feature_dir * unit_size)
self.play(
FadeIn(yes_label),
FadeIn(no_label)
)
self.wait()
# Show example points
points_data = [
(1.5 * unit_size, 1.8 * unit_size, "Match", GREEN),
(-0.5 * unit_size, -1.0 * unit_size, "No Match", RED),
(2.0 * unit_size, 0.5 * unit_size, "Match", GREEN),
(-1.5 * unit_size, 0.2 * unit_size, "No Match", RED),
]
dots = VGroup()
for x, y, label_text, color in points_data:
dot = Dot(point=np.array([x, y, 0]), radius=0.15)
dot.set_color(color)
dots.add(dot)
self.play(
LaggedStartMap(GrowFromCenter, dots, lag_ratio=0.3)
)
self.wait(2)
# Show threshold adjustment
threshold_label = Text("Threshold can be adjusted with bias", font_size=24)
threshold_label.next_to(title, DOWN)
self.play(Write(threshold_label))
# Move boundary line (simulating bias adjustment)
for offset in [0.5, -0.5, 0]:
new_line = Line(
-4 * perp_dir * unit_size + offset * feature_dir * unit_size,
4 * perp_dir * unit_size + offset * feature_dir * unit_size
)
new_line.set_stroke(WHITE, 3)
self.play(Transform(boundary_line, new_line))
self.wait(0.5)
self.wait(2)
@@ -0,0 +1,257 @@
"""
Weight Matrix Visualization for MLPs
Shows a color-coded weight matrix with values mapped to colors.
"""
from manimlib import *
import numpy as np
def value_to_color(
value,
low_positive_color=BLUE_E,
high_positive_color=BLUE_B,
low_negative_color=RED_E,
high_negative_color=RED_B,
min_value=0.0,
max_value=10.0
):
"""Maps a numeric value to a color based on sign and magnitude."""
alpha = clip(float(inverse_interpolate(min_value, max_value, abs(value))), 0, 1)
if value >= 0:
colors = (low_positive_color, high_positive_color)
else:
colors = (low_negative_color, high_negative_color)
return interpolate_color_by_hsl(*colors, alpha)
class WeightMatrixVisualization(InteractiveScene):
"""
Visualizes a weight matrix with color-coded entries.
Blue = positive values, Red = negative values.
Brighter = larger magnitude.
Demonstrates: DecimalMatrix, color mapping, matrix operations
"""
def construct(self):
# Create weight matrix with random values
np.random.seed(42)
n_rows, n_cols = 6, 8
values = np.random.uniform(-9.9, 9.9, size=(n_rows, n_cols))
# Build the matrix display
matrix = self.create_weight_matrix(values)
matrix.set_height(4)
matrix.to_edge(LEFT, buff=1)
# Title
title = Text("Weight Matrix", font_size=48)
title.to_edge(UP)
# Legend
legend = self.create_color_legend()
legend.to_edge(RIGHT, buff=1)
# Animate
self.play(Write(title))
self.play(FadeIn(matrix, lag_ratio=0.02, run_time=2))
self.play(FadeIn(legend))
self.wait(2)
# Highlight a single row
row_idx = 2
row = matrix[row_idx]
row_rect = SurroundingRectangle(row, buff=0.1)
row_rect.set_stroke(YELLOW, 3)
row_label = Text(f"Row {row_idx}: one neuron's weights", font_size=24)
row_label.next_to(row_rect, DOWN)
self.play(ShowCreation(row_rect))
self.play(Write(row_label))
self.wait(2)
# Show dot product concept
self.play(FadeOut(row_rect), FadeOut(row_label))
self.wait()
def create_weight_matrix(self, values):
"""Creates a VGroup of DecimalNumbers arranged as a matrix."""
n_rows, n_cols = values.shape
entries = VGroup()
rows = VGroup()
for i in range(n_rows):
row = VGroup()
for j in range(n_cols):
val = values[i, j]
entry = DecimalNumber(
val,
num_decimal_places=1,
include_sign=True,
font_size=24
)
entry.set_color(value_to_color(val, max_value=9.9))
row.add(entry)
entries.add(entry)
row.arrange(RIGHT, buff=0.3)
rows.add(row)
rows.arrange(DOWN, buff=0.2)
# Add brackets
left_bracket = Tex(R"\left[", font_size=72)
right_bracket = Tex(R"\right]", font_size=72)
left_bracket.stretch_to_fit_height(rows.get_height() * 1.1)
right_bracket.stretch_to_fit_height(rows.get_height() * 1.1)
left_bracket.next_to(rows, LEFT, buff=0.1)
right_bracket.next_to(rows, RIGHT, buff=0.1)
return VGroup(*rows, left_bracket, right_bracket)
def create_color_legend(self):
"""Creates a color legend showing value-to-color mapping."""
legend = VGroup()
# Title
title = Text("Color Legend", font_size=24)
legend.add(title)
# Positive values
pos_example = DecimalNumber(5.0, include_sign=True, font_size=24)
pos_example.set_color(value_to_color(5.0))
pos_label = Text("Positive", font_size=20)
pos_row = VGroup(pos_example, pos_label).arrange(RIGHT, buff=0.3)
legend.add(pos_row)
# Negative values
neg_example = DecimalNumber(-5.0, include_sign=True, font_size=24)
neg_example.set_color(value_to_color(-5.0))
neg_label = Text("Negative", font_size=20)
neg_row = VGroup(neg_example, neg_label).arrange(RIGHT, buff=0.3)
legend.add(neg_row)
# Arrange vertically
legend.arrange(DOWN, buff=0.4, aligned_edge=LEFT)
return legend
class MatrixVectorProduct(InteractiveScene):
"""
Shows how a weight matrix multiplies with an input vector.
Demonstrates: Matrix-vector multiplication visualization
"""
def construct(self):
# Create a simple 4x3 matrix and 3x1 vector
np.random.seed(123)
matrix_values = np.random.uniform(-5, 5, size=(4, 3))
vector_values = np.random.uniform(-5, 5, size=(3,))
# Build matrix display
matrix_entries = VGroup()
for i in range(4):
row = VGroup()
for j in range(3):
val = matrix_values[i, j]
entry = DecimalNumber(val, num_decimal_places=1, include_sign=True, font_size=28)
entry.set_color(value_to_color(val, max_value=5))
row.add(entry)
row.arrange(RIGHT, buff=0.4)
matrix_entries.add(row)
matrix_entries.arrange(DOWN, buff=0.3)
# Add brackets
m_left = Tex("[").stretch_to_fit_height(matrix_entries.get_height() * 1.1)
m_right = Tex("]").stretch_to_fit_height(matrix_entries.get_height() * 1.1)
m_left.next_to(matrix_entries, LEFT, buff=0.05)
m_right.next_to(matrix_entries, RIGHT, buff=0.05)
matrix = VGroup(matrix_entries, m_left, m_right)
# Build vector display
vector_entries = VGroup()
for val in vector_values:
entry = DecimalNumber(val, num_decimal_places=1, include_sign=True, font_size=28)
entry.set_color(YELLOW)
vector_entries.add(entry)
vector_entries.arrange(DOWN, buff=0.3)
v_left = Tex("[").stretch_to_fit_height(vector_entries.get_height() * 1.1)
v_right = Tex("]").stretch_to_fit_height(vector_entries.get_height() * 1.1)
v_left.next_to(vector_entries, LEFT, buff=0.05)
v_right.next_to(vector_entries, RIGHT, buff=0.05)
vector = VGroup(vector_entries, v_left, v_right)
# Position matrix and vector
matrix.move_to(2.5 * LEFT)
vector.next_to(matrix, RIGHT, buff=0.5)
# Labels
matrix_label = Tex("W", font_size=48)
matrix_label.next_to(matrix, UP)
vector_label = Tex(R"\vec{x}", font_size=48).set_color(YELLOW)
vector_label.next_to(vector, UP)
# Title
title = Text("Matrix-Vector Product", font_size=42)
title.to_edge(UP)
# Show initial setup
self.play(Write(title))
self.play(
FadeIn(matrix),
FadeIn(vector),
Write(matrix_label),
Write(vector_label)
)
self.wait()
# Equals and result placeholder
equals = Tex("=", font_size=48)
equals.next_to(vector, RIGHT, buff=0.5)
# Compute result
result_values = matrix_values @ vector_values
result_entries = VGroup()
for val in result_values:
entry = DecimalNumber(val, num_decimal_places=1, include_sign=True, font_size=28)
entry.set_color(GREEN)
result_entries.add(entry)
result_entries.arrange(DOWN, buff=0.3)
r_left = Tex("[").stretch_to_fit_height(result_entries.get_height() * 1.1)
r_right = Tex("]").stretch_to_fit_height(result_entries.get_height() * 1.1)
r_left.next_to(result_entries, LEFT, buff=0.05)
r_right.next_to(result_entries, RIGHT, buff=0.05)
result = VGroup(result_entries, r_left, r_right)
result.next_to(equals, RIGHT, buff=0.5)
self.play(Write(equals))
# Animate row-by-row computation
for row_idx in range(4):
row = matrix_entries[row_idx]
row_rect = SurroundingRectangle(row, buff=0.05)
row_rect.set_stroke(PINK, 2)
vec_rect = SurroundingRectangle(vector_entries, buff=0.05)
vec_rect.set_stroke(PINK, 2)
self.play(
ShowCreation(row_rect),
ShowCreation(vec_rect),
run_time=0.5
)
self.play(
Write(result_entries[row_idx]),
run_time=0.5
)
self.play(
FadeOut(row_rect),
FadeOut(vec_rect),
run_time=0.3
)
self.play(FadeIn(r_left), FadeIn(r_right))
self.wait(2)
@@ -0,0 +1,272 @@
"""
Multi-Head Attention Visualization - Native ManimGL
This is the proper ManimGL implementation using native 3D features.
Based on 3b1b's transformer visualization style.
Run with: manimgl multi_head_attention.py MultiHeadedAttention
Interactive: manimgl multi_head_attention.py MultiHeadedAttention -se 30
"""
from manimlib import *
import numpy as np
def softmax(logits, temperature=1.0):
"""Numerically stable softmax."""
logits = np.array(logits)
logits = logits - np.max(logits)
exps = np.exp(logits / max(temperature, 1e-10))
return exps / np.sum(exps)
class AttentionPatternGrid(VGroup):
"""A grid showing attention pattern with dots."""
def __init__(self, n_rows=8, seed=None, **kwargs):
super().__init__(**kwargs)
if seed is not None:
np.random.seed(seed)
cell_size = 0.4
# Create grid of squares
self.grid = VGroup()
for i in range(n_rows):
for j in range(n_rows):
cell = Square(side_length=cell_size)
cell.set_stroke(WHITE, 0.5, opacity=0.3)
cell.move_to(np.array([j * cell_size, -i * cell_size, 0]))
self.grid.add(cell)
self.grid.center()
# Generate causal attention pattern
pattern = np.random.normal(0, 1, (n_rows, n_rows))
for n in range(n_rows):
pattern[:, n][n + 1:] = -np.inf
valid = pattern[:, n][pattern[:, n] > -np.inf]
if len(valid) > 0:
pattern[:, n][:n + 1] = softmax(valid)
pattern[:, n][n + 1:] = 0
pattern = np.nan_to_num(pattern, nan=0.0)
# Add dots based on weights
self.dots = VGroup()
for i in range(n_rows):
for j in range(n_rows):
value = pattern[i, j]
if value > 0.05:
dot = Dot(radius=cell_size * 0.4 * value)
dot.set_fill(GREY_B, 1)
dot.move_to(self.grid[i * n_rows + j].get_center())
self.dots.add(dot)
# Border
self.border = SurroundingRectangle(self.grid, buff=0.05)
self.border.set_stroke(WHITE, 2)
self.border.set_fill(BLACK, 0.9)
self.add(self.border, self.grid, self.dots)
class MultiHeadedAttention(InteractiveScene):
"""
Multi-Head Attention visualization in native ManimGL.
Shows multiple attention heads in 3D space with camera movement.
"""
def construct(self):
# Background
background = FullScreenRectangle()
background.set_fill(GREY_E, 1)
background.fix_in_frame()
self.add(background)
# Title animation: Single head -> Multi-headed
single_title = Text("Single head of attention")
multiple_title = Text("Multi-headed attention")
for title in [single_title, multiple_title]:
title.scale(1.25)
title.to_edge(UP)
self.add(single_title)
self.wait()
# Flash around "head"
head = single_title["head"][0]
self.play(
FlashAround(head, run_time=2),
head.animate.set_color(YELLOW),
)
self.wait()
# Transform title
kw = dict(path_arc=45 * DEGREES)
self.play(
FadeTransform(single_title["Single"], multiple_title["Multi-"], **kw),
FadeTransform(single_title["head"], multiple_title["head"], **kw),
FadeIn(multiple_title["ed"], 0.25 * RIGHT),
FadeTransform(single_title["attention"], multiple_title["attention"], **kw),
FadeOut(single_title["of"])
)
self.add(multiple_title)
self.wait()
# Create attention pattern heads
n_heads = 15
heads = Group()
for n in range(n_heads):
pattern = AttentionPatternGrid(n_rows=6, seed=n * 42)
pattern.set_height(4)
heads.add(pattern)
# Arrange in 3D depth
self.set_floor_plane("xz")
frame = self.camera.frame
multiple_title.fix_in_frame()
heads.arrange(OUT, buff=1.0)
heads.move_to(DOWN)
# Show initial pattern
pre_head = heads[-1].copy()
pre_head.move_to(DOWN)
self.add(pre_head)
self.wait()
# Rotate camera to reveal 3D
self.play(
frame.animate.reorient(41, -12, 0, (-1.0, -1.42, 1.09), 12.90).set_anim_args(run_time=2),
background.animate.set_fill(opacity=0.75),
FadeTransform(pre_head, heads[-1], time_span=(1, 2)),
)
# Fan out all heads
self.play(
frame.animate.reorient(48, -11, 0, (-1.0, -1.42, 1.09), 12.90),
LaggedStart(
*(FadeTransform(heads[-1].copy(), image) for image in heads),
lag_ratio=0.1,
group_type=Group,
),
run_time=4,
)
self.add(heads)
self.wait()
# Add matrix labels W_Q, W_K, W_V for visible heads
colors = [YELLOW, TEAL, RED, PINK]
tex_labels = ["W_Q", "W_K", R"\downarrow W_V", R"\uparrow W_V"]
n_shown = 9
sym_groups = VGroup()
for tex, color in zip(tex_labels[:2], colors[:2]): # Just W_Q and W_K for now
syms = VGroup()
for n, image in enumerate(list(heads)[:-n_shown - 1:-1], start=1):
sym = Tex(tex + f"^{{({n})}}", font_size=36)
sym.next_to(image, UP, MED_SMALL_BUFF)
sym.set_color(color)
sym.set_backstroke(BLACK, 5)
syms.add(sym)
sym_groups.add(syms)
# Rotate labels to face camera
sym_rot_angle = 70 * DEGREES
for syms in sym_groups:
syms.align_to(heads, LEFT)
for sym in syms:
sym.rotate(sym_rot_angle, UP)
# Show W_Q labels
self.play(
LaggedStartMap(FadeIn, sym_groups[0], shift=0.2 * UP, lag_ratio=0.25),
frame.animate.reorient(59, -7, 0, (-1.62, 0.25, 1.29), 14.18),
run_time=2,
)
# Show W_K labels
self.play(
LaggedStartMap(FadeIn, sym_groups[1], shift=0.2 * UP, lag_ratio=0.1),
sym_groups[0].animate.shift(0.75 * UP),
run_time=1,
)
self.wait()
# Add brace showing "96 heads"
depth = heads.get_depth()
brace = Brace(Line(LEFT, RIGHT).set_width(0.5 * depth), UP).scale(2)
brace_label = brace.get_text("96", font_size=96, buff=MED_SMALL_BUFF)
brace_group = VGroup(brace, brace_label)
brace_group.rotate(PI / 2, UP)
brace_group.next_to(heads, UP, buff=MED_LARGE_BUFF)
self.add(brace, brace_label, sym_groups)
self.play(
frame.animate.reorient(62, -6, 0, (-0.92, -0.08, -0.51), 14.18).set_anim_args(run_time=5),
GrowFromCenter(brace),
sym_groups.animate.set_fill(opacity=0.5).set_stroke(width=0),
FadeIn(brace_label, 0.5 * UP, time_span=(0.5, 1.5)),
)
self.wait()
# Return to front view
self.play(
frame.animate.reorient(0, 0, 0, ORIGIN, FRAME_HEIGHT).set_anim_args(run_time=2),
FadeOut(multiple_title, UP),
FadeOut(brace_group),
FadeOut(sym_groups),
)
self.wait()
class SimpleMultiHead(InteractiveScene):
"""Simpler version for quick testing."""
def construct(self):
# Title
title = Text("Multi-Head Attention", font_size=48)
title.to_edge(UP)
title.fix_in_frame()
self.add(title)
# Create heads
heads = Group()
for i in range(8):
pattern = AttentionPatternGrid(n_rows=5, seed=i * 10)
pattern.set_height(2)
heads.add(pattern)
# Arrange in 3D
heads.arrange(OUT, buff=0.5)
heads.move_to(ORIGIN)
frame = self.camera.frame
# Show one, then fan out
self.add(heads[-1])
self.wait()
self.play(
frame.animate.reorient(50, -20, 0),
run_time=2
)
self.play(
LaggedStart(
*[FadeIn(h, shift=OUT * 0.3) for h in heads[:-1]],
lag_ratio=0.2
),
run_time=2
)
self.wait()
# Rotate around
self.play(
frame.animate.reorient(50, 60, 0),
run_time=4
)
self.wait()
@@ -0,0 +1,286 @@
"""
Network Block Flow - 3D visualization of data flowing through network blocks
Shows data moving through attention and MLP blocks as 3D cubes.
Based on 3Blue1Brown's transformer visualizations.
Run: manimgl network_block_flow.py NetworkBlockFlow3D -o
"""
from manimlib import *
import numpy as np
import random
def value_to_color(
value,
low_positive_color=BLUE_E,
high_positive_color=BLUE_B,
low_negative_color=RED_E,
high_negative_color=RED_B,
min_value=0.0,
max_value=10.0
):
"""Map a value to a color based on its sign and magnitude."""
alpha = np.clip(float((abs(value) - min_value) / (max_value - min_value)), 0, 1)
if value >= 0:
return interpolate_color(low_positive_color, high_positive_color, alpha)
else:
return interpolate_color(low_negative_color, high_negative_color, alpha)
def random_bright_color(hue_range=(0.0, 1.0)):
"""Generate a random bright color within a hue range."""
hue = random.uniform(*hue_range)
return Color(hsl=(hue, 0.7, 0.6))
class SimpleEmbeddingColumn(VGroup):
"""A simple 3D-style embedding column."""
def __init__(self, n_entries=8, height=3.0, width=0.4, **kwargs):
super().__init__(**kwargs)
entries = VGroup()
entry_height = height / n_entries * 0.85
for _ in range(n_entries):
value = random.uniform(-10, 10)
rect = Rectangle(width=width, height=entry_height)
rect.set_fill(value_to_color(value), opacity=0.9)
rect.set_stroke(WHITE, 1)
entries.add(rect)
entries.arrange(DOWN, buff=0.02)
self.add(entries)
self.entries = entries
def randomize_values(self):
for entry in self.entries:
value = random.uniform(-10, 10)
entry.set_fill(value_to_color(value), opacity=0.9)
return self
class NetworkBlockFlow3D(Scene):
"""
3D visualization of data flowing through transformer blocks.
Shows embeddings passing through attention and MLP blocks,
represented as 3D cubes that process the data.
"""
def construct(self):
frame = self.camera.frame
# Setup 3D view
frame.set_euler_angles(phi=65 * DEGREES, theta=-40 * DEGREES)
frame.set_z(2)
# Create input embeddings
n_tokens = 5
embeddings = VGroup(*(
SimpleEmbeddingColumn(n_entries=10, height=3.5, width=0.5)
for _ in range(n_tokens)
))
embeddings.arrange(RIGHT, buff=0.6)
embeddings.set_z(0)
# Title
title = Text("Data Flow Through Network Blocks", font_size=48)
title.to_edge(UP)
title.fix_in_frame()
self.play(
Write(title),
LaggedStartMap(FadeIn, embeddings, shift=0.5 * DOWN, lag_ratio=0.1),
run_time=2
)
self.wait()
# Create and show first block (Attention)
att_block = self.create_block(embeddings, "Attention", BLUE_E)
self.play(
frame.animate.reorient(-50, -15, 0).shift(2 * OUT),
FadeIn(att_block, scale=0.8),
run_time=2
)
# Flow through attention
new_embeddings = self.flow_through_block(embeddings, att_block)
# Create second block (MLP/Feedforward)
mlp_block = self.create_block(new_embeddings, "Feedforward", GREEN_E)
mlp_block.shift(3 * OUT)
self.play(
frame.animate.shift(2 * OUT),
FadeIn(mlp_block, scale=0.8),
run_time=2
)
# Flow through MLP
final_embeddings = self.flow_through_block(new_embeddings, mlp_block)
# Show "many more" indication
self.show_repetition_hint(mlp_block, frame)
# Cleanup
self.play(
FadeOut(VGroup(embeddings, new_embeddings, final_embeddings)),
FadeOut(att_block),
FadeOut(mlp_block),
FadeOut(title),
)
def create_block(self, layer, title_text, color):
"""Create a processing block (cube) next to the layer."""
body = Cube(color=color, opacity=0.7)
body.set_shading(0.5, 0.5, 0.0)
width = layer.get_width() + 1
height = layer.get_height() + 0.5
depth = 2.0
body.set_shape(width, height, depth)
body.next_to(layer, OUT, buff=1.0)
title = Text(title_text, font_size=60)
title.set_backstroke(BLACK, 3)
title.rotate(PI / 2, RIGHT)
title.next_to(body, UP, buff=0.2)
block = Group(body, title)
block.body = body
block.title = title
return block
def flow_through_block(self, embeddings, block):
"""Animate embeddings flowing through the block."""
# Create output embeddings
new_embeddings = VGroup(*(
SimpleEmbeddingColumn(n_entries=10, height=3.5, width=0.5)
for _ in range(len(embeddings))
))
new_embeddings.arrange(RIGHT, buff=0.6)
new_embeddings.move_to(block.body.get_center())
new_embeddings.set_z(block.body.get_z(OUT) + 1)
# Animate transformation
self.play(
TransformFromCopy(embeddings, new_embeddings),
run_time=2
)
return new_embeddings
def show_repetition_hint(self, last_block, frame):
"""Show indication of many more blocks."""
dots = Text("...", font_size=120)
dots.rotate(PI / 2, RIGHT)
dots.next_to(last_block, OUT, buff=1)
brace = Brace(Line(ORIGIN, 4 * OUT), RIGHT)
brace.rotate(PI / 2, RIGHT)
brace.next_to(dots, RIGHT)
label = Text("Many\nrepetitions", font_size=36)
label.rotate(PI / 2, RIGHT)
label.next_to(brace, RIGHT)
hint_group = VGroup(dots, brace, label)
self.play(
frame.animate.shift(2 * OUT),
FadeIn(dots),
GrowFromCenter(brace),
FadeIn(label),
run_time=2
)
self.wait(2)
self.play(FadeOut(hint_group))
class SimpleBlockTransition(Scene):
"""
Simpler 2D version showing block transitions.
"""
def construct(self):
# Create token representations
n_tokens = 6
tokens = VGroup()
for i in range(n_tokens):
token = VGroup()
# Colored rectangle
rect = Rectangle(width=0.8, height=2.5)
rect.set_fill(BLUE, opacity=0.3)
rect.set_stroke(BLUE, 2)
# Inner value indicators
for j in range(5):
small_rect = Rectangle(width=0.6, height=0.35)
small_rect.set_fill(value_to_color(random.uniform(-10, 10)), opacity=0.8)
small_rect.set_stroke(WHITE, 0.5)
token.add(small_rect)
token.arrange(DOWN, buff=0.05)
tokens.add(token)
tokens.arrange(RIGHT, buff=0.5)
tokens.to_edge(LEFT, buff=1)
# Block representations
att_block = self.create_2d_block("Attention", BLUE_D)
mlp_block = self.create_2d_block("Feedforward", GREEN_D)
att_block.next_to(tokens, RIGHT, buff=1.5)
mlp_block.next_to(att_block, RIGHT, buff=2)
# Arrows
arrow1 = Arrow(tokens.get_right(), att_block.get_left(), buff=0.2)
arrow2 = Arrow(att_block.get_right(), mlp_block.get_left(), buff=0.2)
# Labels
input_label = Text("Input\nEmbeddings", font_size=24)
input_label.next_to(tokens, DOWN)
# Animate
self.play(LaggedStartMap(FadeIn, tokens, shift=UP, lag_ratio=0.1))
self.play(FadeIn(input_label))
self.wait()
self.play(
GrowArrow(arrow1),
FadeIn(att_block, shift=RIGHT),
)
self.wait()
self.play(
GrowArrow(arrow2),
FadeIn(mlp_block, shift=RIGHT),
)
self.wait()
# Show data flow animation
for _ in range(2):
self.play(
VShowPassingFlash(arrow1.copy().set_stroke(YELLOW, 4), time_width=0.5),
VShowPassingFlash(arrow2.copy().set_stroke(YELLOW, 4), time_width=0.5),
run_time=1.5
)
self.wait()
def create_2d_block(self, label_text, color):
"""Create a 2D block representation."""
rect = RoundedRectangle(width=2.5, height=3, corner_radius=0.2)
rect.set_fill(color, opacity=0.5)
rect.set_stroke(color, 3)
label = Text(label_text, font_size=28)
label.move_to(rect)
return VGroup(rect, label)
@@ -0,0 +1,148 @@
"""
Basic Neural Network visualization with animated connections and layers.
Demonstrates: Custom VGroup class, randomized styling, layer-based animation
"""
from manimlib import *
import numpy as np
import random
import itertools as it
def value_to_color(
value,
low_positive_color=BLUE_E,
high_positive_color=BLUE_B,
low_negative_color=RED_E,
high_negative_color=RED_B,
min_value=0.0,
max_value=10.0
):
"""Map a numeric value to a color gradient."""
alpha = clip(float(inverse_interpolate(min_value, max_value, abs(value))), 0, 1)
if value >= 0:
colors = (low_positive_color, high_positive_color)
else:
colors = (low_negative_color, high_negative_color)
return interpolate_color_by_hsl(*colors, alpha)
class NeuralNetwork(VGroup):
"""A simple neural network visualization with layers and connections."""
def __init__(
self,
layer_sizes=[6, 12, 6],
neuron_radius=0.1,
v_buff_ratio=1.0,
h_buff_ratio=7.0,
max_stroke_width=2.0,
stroke_decay=2.0,
):
self.max_stroke_width = max_stroke_width
self.stroke_decay = stroke_decay
# Create neuron layers
layers = VGroup(*(
Dot(radius=neuron_radius).get_grid(n, 1, v_buff_ratio=v_buff_ratio)
for n in layer_sizes
))
layers.arrange(RIGHT, buff=h_buff_ratio * layers[0].get_width())
# Create connections between layers
lines = VGroup(*(
VGroup(*(
Line(
n1.get_center(),
n2.get_center(),
buff=n1.get_width() / 2,
)
for n1, n2 in it.product(l1, l2)
))
for l1, l2 in zip(layers, layers[1:])
))
super().__init__(layers, lines)
self.layers = layers
self.lines = lines
self.randomize_layer_values()
self.randomize_line_style()
def randomize_layer_values(self):
"""Randomize the fill opacity of neurons."""
for layer in self.layers:
for dot in layer:
dot.set_stroke(WHITE, 1)
dot.set_fill(WHITE, random.random())
return self
def randomize_line_style(self):
"""Randomize connection colors and widths."""
for group in self.lines:
for line in group:
line.set_stroke(
value_to_color(random.uniform(-10, 10)),
self.max_stroke_width * random.random()**self.stroke_decay,
)
return self
class NeuralNetworkBasic(Scene):
def construct(self):
# Title
title = Text("Neural Network", font_size=60)
title.to_edge(UP)
# Create neural network
network = NeuralNetwork([5, 10, 5])
network.set_height(5)
network.center()
self.play(FadeIn(title, shift=DOWN))
self.wait(0.5)
# Animate layers appearing
self.play(
FadeIn(network.layers[0]),
ShowCreation(network.lines[0], lag_ratio=0.01),
FadeIn(network.layers[1], lag_ratio=0.5),
run_time=2
)
self.play(
ShowCreation(network.lines[1], lag_ratio=0.01),
FadeIn(network.layers[2], lag_ratio=0.5),
run_time=2
)
# Ambiently change the network
for _ in range(4):
self.play(
network.animate.randomize_line_style().randomize_layer_values(),
run_time=2,
lag_ratio=1e-4
)
# Add labels for layers
input_label = Text("Input", font_size=36)
hidden_label = Text("Hidden", font_size=36)
output_label = Text("Output", font_size=36)
input_label.next_to(network.layers[0], DOWN)
hidden_label.next_to(network.layers[1], DOWN)
output_label.next_to(network.layers[2], DOWN)
self.play(LaggedStart(
FadeIn(input_label, shift=UP),
FadeIn(hidden_label, shift=UP),
FadeIn(output_label, shift=UP),
lag_ratio=0.3
))
self.wait()
# Final animation
for _ in range(2):
self.play(
network.animate.randomize_line_style().randomize_layer_values(),
run_time=2,
)
self.wait()
@@ -0,0 +1,209 @@
"""
Parallax Effect with 3D Starfield
Demonstrates the parallax effect - how nearby objects appear to move more
than distant objects when the observer moves. This is a fundamental concept
in astronomy for measuring distances to stars.
Run: manimgl parallax_starfield.py ParallaxStarfield -w
Preview: manimgl parallax_starfield.py ParallaxStarfield -p
Source: Adapted from 3b1b's cosmic_distance video (2025)
"""
from manimlib import *
import numpy as np
class ParallaxStarfield(InteractiveScene):
"""
A 3D scene showing parallax effect with stars at different distances.
Key techniques demonstrated:
- GlowDots for efficient star rendering
- 3D camera manipulation with frame.animate.reorient()
- VCube as a visual reference box
- Observer movement to demonstrate parallax
"""
def construct(self):
# Setup 3D environment
frame = self.frame
self.set_floor_plane("xz") # Set z as vertical axis
# Create a reference cube to help visualize 3D space
height = 4
cube = VCube(height)
cube.set_fill(opacity=0)
cube.set_stroke(BLUE, 2)
# Create stars as GlowDots - efficient for many point lights
n_stars = 200
# Random positions in a cube
star_positions = np.random.uniform(-1, 1, (n_stars, 3))
stars = GlowDots(star_positions)
stars.scale(height / 2) # Scale to fit within our cube
stars.set_color(WHITE)
stars.set_glow_factor(2)
# Vary star sizes for visual interest
stars.set_radii(np.random.uniform(0, 0.075, n_stars))
self.add(cube)
self.add(stars)
# Animate stars appearing
self.play(ShowCreation(stars, run_time=3))
# Add an observer (using a simple 3D sphere)
observer = Sphere(radius=0.3)
observer.set_color(BLUE_E)
observer.set_shading(0.5, 0.5, 0.5)
observer.next_to(cube, LEFT, buff=1)
# Add an arrow to show viewing direction
eye_arrow = Arrow(
observer.get_center(),
observer.get_center() + 1.5 * RIGHT,
buff=0,
stroke_color=YELLOW,
stroke_width=4,
)
eye_arrow.add_updater(lambda m: m.put_start_and_end_on(
observer.get_center(),
observer.get_center() + 1.5 * RIGHT
))
self.play(
FadeIn(observer),
ShowCreation(eye_arrow),
)
# Rotate camera for better 3D view
self.play(frame.animate.reorient(-40, -26, 0), run_time=2)
# Key demonstration: Move observer up and down
# Watch how nearby stars shift more than distant ones
for dy in [1.5, -3, 3, -3, 1.5]:
self.play(
observer.animate.shift(dy * IN), # IN = into screen = Z axis
run_time=3
)
self.wait()
class ParallaxFromObserverPOV(InteractiveScene):
"""
Same parallax demo but from the observer's point of view.
This variant shows what the observer would actually see -
the apparent motion of stars against the background.
"""
def construct(self):
frame = self.frame
self.set_floor_plane("xz")
# Create starfield
height = 4
cube = VCube(height)
cube.set_fill(opacity=0)
cube.set_stroke(BLUE, 2)
n_stars = 200
star_positions = np.random.uniform(-1, 1, (n_stars, 3))
stars = GlowDots(star_positions)
stars.scale(height / 2)
stars.set_color(WHITE)
stars.set_glow_factor(2)
stars.set_radii(np.random.uniform(0, 0.075, n_stars))
self.add(cube, stars)
self.play(ShowCreation(stars, run_time=2))
# Add observer as a tracking point
observer = Sphere(radius=0.3)
observer.set_color(BLUE_E)
observer.next_to(cube, LEFT, buff=1)
self.play(FadeIn(observer))
# Move camera to observer's perspective
self.play(
frame.animate.reorient(-89, -4, 0, (0.01, 0.21, 0.0), 3.05),
observer.animate.set_opacity(0),
cube.animate.set_stroke(width=5).set_anti_alias_width(10),
run_time=3,
)
# Camera follows observer's z position
frame.always.match_z(observer)
# Move observer - camera follows, showing parallax from their view
for dy in [1.5, -3, 3, -3, 1.5]:
self.play(observer.animate.shift(dy * IN), run_time=4)
self.wait()
class LayeredParallax(InteractiveScene):
"""
Demonstrates parallax with explicitly layered star planes.
Shows three distinct layers at different distances to make
the parallax effect more obvious and educational.
"""
def construct(self):
frame = self.frame
self.set_floor_plane("xz")
# Create three layers of stars at different distances
layers = []
colors = [RED, YELLOW, BLUE]
distances = [2, 5, 10] # Distance from origin
n_stars_per_layer = 50
for color, dist in zip(colors, distances):
# Create stars in an XY plane at distance Z
positions = np.random.uniform(-3, 3, (n_stars_per_layer, 3))
positions[:, 2] = dist # Set all Z to this layer's distance
layer = GlowDots(positions)
layer.set_color(color)
layer.set_glow_factor(1.5)
layer.set_radii(np.full(n_stars_per_layer, 0.05))
layers.append(layer)
all_stars = Group(*layers)
# Add distance labels
labels = VGroup()
for color, dist in zip(colors, distances):
label = Text(f"{dist} units away", color=color, font_size=24)
label.to_corner(UL)
label.shift(DOWN * (distances.index(dist) * 0.5))
labels.add(label)
self.add(all_stars, labels)
# Position camera to see all layers
frame.reorient(-30, -20, 0)
frame.set_height(12)
# Create observer dot
observer = Sphere(radius=0.2)
observer.set_color(GREEN)
observer.move_to(ORIGIN)
self.add(observer)
self.wait()
# Move observer laterally - watch the layers shift differently
for dx in [2, -4, 4, -2]:
self.play(
observer.animate.shift(dx * RIGHT),
run_time=3,
rate_func=smooth
)
self.wait()
@@ -0,0 +1,392 @@
"""
Probability Distribution Visualization
======================================
Visualizes how a quantum state vector maps to a probability distribution
through the Born rule (amplitude squared = probability).
Key concepts demonstrated:
- DecimalMatrix for state vector display
- Rectangle bars for probability visualization
- always_redraw for reactive updates
- LaggedStartMap for sequential animations
"""
from manimlib import *
class ProbabilityDistribution(InteractiveScene):
"""Shows how state vector amplitudes become probabilities."""
def construct(self):
# Title
title = Text("State Vector to Probability", font_size=48)
title.to_edge(UP)
self.add(title)
# Create a simple 4-state vector (2 qubit system)
state = normalize(np.array([1, 2, 0.5, 1.5]))
# State vector display
state_vector = DecimalMatrix(
state.reshape((4, 1)),
decimal_config=dict(include_sign=True, num_decimal_places=2)
)
state_vector.scale(0.8)
state_vector.shift(3 * LEFT)
vector_label = Text("State Vector", font_size=30)
vector_label.next_to(state_vector, UP)
# Bit string labels
bit_labels = VGroup(
Tex(R"|00\rangle", font_size=30),
Tex(R"|01\rangle", font_size=30),
Tex(R"|10\rangle", font_size=30),
Tex(R"|11\rangle", font_size=30),
)
bit_labels.set_color(GREY_B)
for bits, entry in zip(bit_labels, state_vector.get_entries()):
bits.next_to(state_vector, LEFT, buff=0.3)
bits.match_y(entry)
self.add(state_vector, vector_label, bit_labels)
# Arrow with transformation rule
arrow = Arrow(LEFT, RIGHT, thickness=5)
arrow.next_to(state_vector, RIGHT, buff=0.5)
rule = Tex(R"|\alpha|^2", font_size=36)
rule.next_to(arrow, UP, SMALL_BUFF)
self.play(GrowArrow(arrow), Write(rule))
# Probability bars
probs = state ** 2
max_bar_width = 3.0
bar_labels = VGroup(
Tex(R"|00\rangle", font_size=30),
Tex(R"|01\rangle", font_size=30),
Tex(R"|10\rangle", font_size=30),
Tex(R"|11\rangle", font_size=30),
)
bar_labels.arrange(DOWN, buff=0.5)
bar_labels.next_to(arrow, RIGHT, buff=1.0)
bars = VGroup()
prob_labels = VGroup()
for i, (label, prob) in enumerate(zip(bar_labels, probs)):
bar = Rectangle(
width=prob * max_bar_width,
height=0.4
)
bar.next_to(label, RIGHT, buff=0.2)
bar.set_fill(
interpolate_color(BLUE_D, GREEN, prob),
opacity=1
)
bar.set_stroke(WHITE, 1)
pct = Integer(int(100 * prob), unit=R"\%", font_size=24)
pct.next_to(bar, RIGHT, SMALL_BUFF)
bars.add(bar)
prob_labels.add(pct)
# Animate bars appearing
self.play(
FadeIn(bar_labels),
LaggedStart(
(GrowFromEdge(bar, LEFT)
for bar in bars),
lag_ratio=0.2
),
LaggedStartMap(FadeIn, prob_labels, lag_ratio=0.2),
run_time=2
)
# Add sum constraint
sum_eq = Tex(
R"\sum_i |\alpha_i|^2 = 1",
font_size=30
)
sum_eq.to_edge(DOWN, buff=1.0)
sum_eq.set_color(YELLOW)
self.play(Write(sum_eq))
self.wait(2)
class DynamicStateVector(InteractiveScene):
"""Shows state vector evolving and probabilities updating in real-time."""
def construct(self):
# Set up state tracker
n_states = 8
phase_trackers = [ValueTracker(np.random.uniform(0, TAU)) for _ in range(n_states)]
def get_state():
"""Generate a normalized state from phases."""
raw = np.array([
np.sin(tracker.get_value())
for tracker in phase_trackers
])
return normalize(raw + 0.1)
# Create layout
# Left: Quantum computer symbol
qc_symbol = VGroup(
Square(1.5).set_stroke(TEAL, 2).set_fill(GREY_E, 1),
Tex(R"|Q\rangle", color=TEAL).scale(0.8)
)
qc_symbol[1].move_to(qc_symbol[0])
qc_symbol.shift(4 * LEFT)
# Middle: State vector
state_vector = DecimalMatrix(
np.zeros((n_states, 1)),
decimal_config=dict(include_sign=True, num_decimal_places=2)
)
state_vector.scale(0.5)
state_vector.center()
def update_state_vector(matrix):
state = get_state()
for elem, val in zip(matrix.elements, state):
elem.set_value(val)
state_vector.add_updater(update_state_vector)
# Bit labels
bit_labels = VGroup(
Tex(R"|" + bin(n)[2:].zfill(3) + R"\rangle", font_size=20)
for n in range(n_states)
)
bit_labels.set_color(GREY_C)
def update_bit_labels(labels):
for bits, entry in zip(labels, state_vector.get_entries()):
bits.next_to(state_vector, LEFT, buff=0.15)
bits.match_y(entry)
bit_labels.add_updater(update_bit_labels)
# Right: Probability bars
qubit_labels = VGroup(
Tex(R"|" + bin(n)[2:].zfill(3) + R"\rangle", font_size=24)
for n in range(n_states)
)
qubit_labels.arrange(DOWN, buff=0.25)
qubit_labels.shift(2.5 * RIGHT)
def get_prob_bars():
probs = get_state() ** 2
bars = VGroup()
for qubit, prob in zip(qubit_labels, probs):
bar = Rectangle(
width=prob * 4,
height=qubit.get_height() * 0.8
)
bar.next_to(qubit, RIGHT, buff=0.15)
bar.set_fill(
interpolate_color(BLUE_D, GREEN, prob * 1.5),
opacity=1
)
bar.set_stroke(WHITE, 1)
bars.add(bar)
return bars
prob_bars = always_redraw(get_prob_bars)
# Arrow connecting state vector to probabilities
arrow = Arrow(state_vector.get_right() + 0.3 * RIGHT,
qubit_labels.get_left() + 0.3 * LEFT,
thickness=4)
arrow_label = Tex(R"|\cdot|^2", font_size=24)
arrow_label.next_to(arrow, UP, SMALL_BUFF)
# Add all elements
self.add(qc_symbol, state_vector, bit_labels)
self.add(arrow, arrow_label)
self.add(qubit_labels, prob_bars)
# Animate state evolution
animations = [
tracker.animate.set_value(tracker.get_value() + np.random.uniform(2, 5) * TAU)
for tracker in phase_trackers
]
self.play(
*animations,
run_time=10,
rate_func=linear
)
self.wait()
class BornRuleExplanation(InteractiveScene):
"""Explains the Born rule for quantum measurement."""
def construct(self):
# Title
title = Text("The Born Rule", font_size=60)
title.to_edge(UP)
self.add(title)
# The rule
rule = Tex(
R"P(i) = |\langle i | \psi \rangle|^2 = |\alpha_i|^2",
font_size=48
)
rule.next_to(title, DOWN, buff=1.0)
self.play(Write(rule))
self.wait()
# Explanation
explanation = VGroup(
Tex(R"\alpha_i \text{ = amplitude for state } |i\rangle", font_size=30),
Tex(R"|\alpha_i|^2 \text{ = probability of measuring } |i\rangle", font_size=30),
Tex(R"\sum_i |\alpha_i|^2 = 1 \text{ (normalization)}", font_size=30),
)
explanation.arrange(DOWN, aligned_edge=LEFT, buff=0.5)
explanation.next_to(rule, DOWN, buff=1.0)
for line in explanation:
self.play(FadeIn(line, shift=RIGHT))
self.wait(0.5)
self.wait()
# Visual example
example_title = Text("Example:", font_size=36)
example_title.next_to(explanation, DOWN, buff=1.0)
example_title.to_edge(LEFT, buff=1.0)
state = Tex(
R"|\psi\rangle = \frac{1}{\sqrt{2}}|0\rangle + \frac{1}{\sqrt{2}}|1\rangle",
font_size=36
)
state.next_to(example_title, RIGHT, buff=0.5)
probs = VGroup(
Tex(R"P(0) = \left|\frac{1}{\sqrt{2}}\right|^2 = \frac{1}{2}", font_size=30),
Tex(R"P(1) = \left|\frac{1}{\sqrt{2}}\right|^2 = \frac{1}{2}", font_size=30),
)
probs.arrange(DOWN, aligned_edge=LEFT, buff=0.3)
probs.next_to(state, DOWN, buff=0.5)
self.play(Write(example_title), Write(state))
self.wait()
self.play(LaggedStartMap(FadeIn, probs, lag_ratio=0.3))
self.wait(2)
class GroverAmplification(InteractiveScene):
"""Visualizes amplitude amplification in Grover's algorithm."""
def construct(self):
# Title
title = Text("Grover's Amplitude Amplification", font_size=48)
title.to_edge(UP)
self.add(title)
# Create bar chart for amplitudes
n_states = 8
target = 5 # The "marked" state
# Initial uniform state
initial_amps = np.ones(n_states) / np.sqrt(n_states)
def create_bars(amps, highlighted=None):
bars = VGroup()
labels = VGroup()
for i, amp in enumerate(amps):
bar = Rectangle(
width=0.5,
height=amp * 4
)
bar.set_fill(
YELLOW if i == highlighted else BLUE_D,
opacity=1
)
bar.set_stroke(WHITE, 1)
bars.add(bar)
label = Tex(R"|" + bin(i)[2:].zfill(3) + R"\rangle", font_size=16)
labels.add(label)
bars.arrange(RIGHT, buff=0.2, aligned_edge=DOWN)
bars.center().shift(DOWN)
for bar, label in zip(bars, labels):
label.next_to(bar, DOWN, SMALL_BUFF)
return VGroup(bars, labels)
# Show initial state
bar_chart = create_bars(initial_amps)
step_label = Text("Initial: Uniform Superposition", font_size=30)
step_label.next_to(bar_chart, UP, buff=0.5)
self.play(
LaggedStartMap(GrowFromEdge, bar_chart[0], edge=DOWN, lag_ratio=0.1),
FadeIn(bar_chart[1]),
Write(step_label)
)
self.wait()
# Grover iterations
amps = initial_amps.copy()
for iteration in range(3):
# Oracle: flip amplitude of target
amps[target] *= -1
# Show oracle step
new_bars = create_bars(np.abs(amps), target)
new_bars[0][target].set_fill(RED)
oracle_label = Text(f"Step {iteration * 2 + 1}: Oracle (flip target)", font_size=30)
oracle_label.next_to(bar_chart, UP, buff=0.5)
self.play(
Transform(bar_chart[0], new_bars[0]),
Transform(step_label, oracle_label)
)
self.wait()
# Diffusion: reflect about mean
mean = np.mean(amps)
amps = 2 * mean - amps
new_bars = create_bars(amps, target)
diffusion_label = Text(f"Step {iteration * 2 + 2}: Diffusion (amplify)", font_size=30)
diffusion_label.next_to(bar_chart, UP, buff=0.5)
self.play(
Transform(bar_chart[0], new_bars[0]),
Transform(step_label, diffusion_label)
)
self.wait()
# Final result
final_label = Text("Result: High probability for target state!", font_size=30, color=GREEN)
final_label.next_to(bar_chart, UP, buff=0.5)
self.play(Transform(step_label, final_label))
# Highlight target
rect = SurroundingRectangle(
VGroup(bar_chart[0][target], bar_chart[1][target]),
buff=0.1,
color=YELLOW
)
self.play(ShowCreation(rect))
self.wait(2)
if __name__ == "__main__":
# To run: manimgl probability_distribution.py ProbabilityDistribution
pass
@@ -0,0 +1,393 @@
"""
Probability Output Visualization
Shows how the network produces probability distributions over possible next tokens.
Based on 3Blue1Brown's transformer visualizations.
Run: manimgl probability_output.py ProbabilityOutput -o
"""
from manimlib import *
import numpy as np
import random
def value_to_color(
value,
low_positive_color=BLUE_E,
high_positive_color=BLUE_B,
low_negative_color=RED_E,
high_negative_color=RED_B,
min_value=0.0,
max_value=10.0
):
"""Map a value to a color based on its sign and magnitude."""
alpha = np.clip(float((abs(value) - min_value) / (max_value - min_value)), 0, 1)
if value >= 0:
return interpolate_color(low_positive_color, high_positive_color, alpha)
else:
return interpolate_color(low_negative_color, high_negative_color, alpha)
def softmax(logits, temperature=1.0):
"""Compute softmax of logits."""
logits = np.array(logits) / temperature
logits = logits - np.max(logits)
exps = np.exp(logits)
return exps / np.sum(exps)
class ProbabilityOutput(Scene):
"""
Demonstrates how the final layer outputs probability distributions.
Shows the transformation from embedding vector to probabilities over vocabulary.
"""
# Example predictions
possible_next_tokens = [
("the", 0.35),
("a", 0.25),
("an", 0.15),
("this", 0.10),
("that", 0.08),
("some", 0.04),
("my", 0.02),
("...", 0.01),
]
def construct(self):
# Title
title = Text("Network Output: Probability Distribution", font_size=44)
title.to_edge(UP)
self.play(Write(title))
self.wait()
# Show the prompt
prompt = Text('Input: "The cat sat on"', font_size=36)
prompt.next_to(title, DOWN, buff=0.5)
self.play(FadeIn(prompt, shift=DOWN))
self.wait()
# Create final embedding vector
vector = self.create_embedding_vector()
vector.to_edge(LEFT, buff=1.5)
vector.shift(0.5 * DOWN)
vector_label = Text("Final\nembedding", font_size=24)
vector_label.next_to(vector, DOWN)
self.play(
FadeIn(vector, shift=RIGHT),
FadeIn(vector_label),
)
self.wait()
# Arrow to probabilities
arrow = Arrow(vector.get_right(), vector.get_right() + 2 * RIGHT, buff=0.2)
arrow.set_color(YELLOW)
softmax_label = Text("softmax", font_size=28)
softmax_label.next_to(arrow, UP, buff=0.1)
self.play(
GrowArrow(arrow),
FadeIn(softmax_label),
)
# Create probability bars
prob_group = self.create_probability_bars()
prob_group.next_to(arrow, RIGHT, buff=0.5)
self.play(
LaggedStartMap(FadeIn, prob_group, shift=0.3 * RIGHT, lag_ratio=0.1),
run_time=2
)
self.wait()
# Highlight top prediction
highlight = SurroundingRectangle(prob_group[0], buff=0.1)
highlight.set_stroke(GREEN, 3)
prediction_label = Text('Prediction: "the"', font_size=36, color=GREEN)
prediction_label.next_to(prob_group, DOWN, buff=0.8)
self.play(ShowCreation(highlight))
self.play(FadeIn(prediction_label, shift=UP))
self.wait()
# Show this is a distribution
dist_note = Text("This is a probability distribution over ~50,000 tokens", font_size=28)
dist_note.next_to(prediction_label, DOWN, buff=0.5)
self.play(FadeIn(dist_note, shift=UP))
self.wait(2)
# Cleanup
self.play(FadeOut(VGroup(
title, prompt, vector, vector_label,
arrow, softmax_label, prob_group,
highlight, prediction_label, dist_note
)))
def create_embedding_vector(self, length=12, height=4.0):
"""Create a visual embedding vector."""
entries = VGroup()
entry_height = (height / length) * 0.85
for _ in range(length):
value = random.uniform(-9.9, 9.9)
rect = Rectangle(width=0.4, height=entry_height)
rect.set_fill(value_to_color(value), opacity=0.9)
rect.set_stroke(WHITE, 0.5)
entries.add(rect)
entries.arrange(DOWN, buff=0.02)
# Brackets
lb = Text("[", font_size=96)
rb = Text("]", font_size=96)
lb.stretch_to_fit_height(height * 1.1)
rb.stretch_to_fit_height(height * 1.1)
lb.set_color(GREY_B)
rb.set_color(GREY_B)
lb.next_to(entries, LEFT, buff=0.05)
rb.next_to(entries, RIGHT, buff=0.05)
return VGroup(lb, entries, rb)
def create_probability_bars(self):
"""Create probability bar chart."""
bars_group = VGroup()
for word, prob in self.possible_next_tokens:
# Bar
bar = Rectangle(
width=4 * prob,
height=0.4,
)
bar.set_fill(interpolate_color(BLUE_E, BLUE_B, prob), opacity=0.8)
bar.set_stroke(WHITE, 1)
# Word label
word_label = Text(word, font_size=24)
word_label.next_to(bar, LEFT, buff=0.2)
# Probability label
prob_label = Text(f"{prob:.0%}", font_size=20)
prob_label.next_to(bar, RIGHT, buff=0.1)
row = VGroup(word_label, bar, prob_label)
bars_group.add(row)
bars_group.arrange(DOWN, buff=0.15, aligned_edge=LEFT)
# Align bars
for row in bars_group:
row[1].align_to(bars_group[0][1], LEFT)
return bars_group
class SoftmaxVisualization(Scene):
"""
Shows the softmax transformation turning logits into probabilities.
"""
def construct(self):
# Title
title = Text("Softmax: Logits to Probabilities", font_size=48)
title.to_edge(UP)
self.play(Write(title))
# Create logits
logits = [2.5, 1.8, 1.2, 0.5, 0.1, -0.3, -1.0, -2.0]
probs = softmax(logits)
# Logits bars
logit_bars = self.create_bars(logits, max_val=3.0, color=RED)
logit_bars.to_edge(LEFT, buff=1)
logit_label = Text("Logits (raw scores)", font_size=28)
logit_label.next_to(logit_bars, DOWN)
# Probability bars
prob_bars = self.create_bars(probs * 10, max_val=10, color=BLUE)
prob_bars.to_edge(RIGHT, buff=1)
prob_label = Text("Probabilities", font_size=28)
prob_label.next_to(prob_bars, DOWN)
# Arrow with softmax
arrow = Arrow(logit_bars.get_right(), prob_bars.get_left(), buff=0.3)
softmax_text = Tex(r"\text{softmax}", font_size=36)
softmax_text.next_to(arrow, UP)
# Animate
self.play(FadeIn(logit_bars, shift=RIGHT))
self.play(FadeIn(logit_label))
self.wait()
self.play(
GrowArrow(arrow),
FadeIn(softmax_text),
)
self.play(TransformFromCopy(logit_bars, prob_bars))
self.play(FadeIn(prob_label))
self.wait()
# Show formula
formula = Tex(
r"\text{softmax}(x_i) = \frac{e^{x_i}}{\sum_j e^{x_j}}",
font_size=36
)
formula.next_to(arrow, DOWN, buff=1)
self.play(Write(formula))
self.wait(2)
# Cleanup
self.play(FadeOut(VGroup(
title, logit_bars, logit_label,
arrow, softmax_text, prob_bars, prob_label, formula
)))
def create_bars(self, values, max_val=1.0, color=BLUE):
"""Create a group of horizontal bars."""
bars = VGroup()
for val in values:
# Normalize width
width = max(0.1, abs(val) / max_val * 3)
bar = Rectangle(width=width, height=0.3)
if val >= 0:
bar.set_fill(color, opacity=0.7)
else:
bar.set_fill(RED, opacity=0.7)
bar.set_stroke(WHITE, 1)
bars.add(bar)
bars.arrange(DOWN, buff=0.1, aligned_edge=LEFT)
return bars
class VocabProjection(Scene):
"""
Shows the unembedding matrix projecting to vocabulary space.
"""
def construct(self):
# Title
title = Text("Projecting to Vocabulary Space", font_size=44)
title.to_edge(UP)
self.play(Write(title))
# Embedding vector (small)
emb_entries = VGroup(*(
Rectangle(width=0.3, height=0.25).set_fill(
value_to_color(random.uniform(-10, 10)), opacity=0.9
).set_stroke(WHITE, 0.5)
for _ in range(10)
))
emb_entries.arrange(DOWN, buff=0.02)
emb_bracket_l = Text("[", font_size=72).stretch_to_fit_height(emb_entries.get_height() * 1.1)
emb_bracket_r = Text("]", font_size=72).stretch_to_fit_height(emb_entries.get_height() * 1.1)
emb_bracket_l.next_to(emb_entries, LEFT, buff=0.05)
emb_bracket_r.next_to(emb_entries, RIGHT, buff=0.05)
embedding = VGroup(emb_bracket_l, emb_entries, emb_bracket_r)
embedding.scale(0.8)
embedding.to_edge(LEFT, buff=1)
embedding.shift(0.5 * DOWN)
emb_label = Text("Embedding\n(d dims)", font_size=24)
emb_label.next_to(embedding, DOWN)
# Matrix (wide)
matrix = self.create_matrix(rows=8, cols=10)
matrix.next_to(embedding, RIGHT, buff=1)
matrix_label = Text("Unembedding\nMatrix", font_size=24)
matrix_label.next_to(matrix, DOWN)
# Result (vocab sized)
result_entries = VGroup(*(
Rectangle(width=0.25, height=0.2).set_fill(
value_to_color(random.uniform(-10, 10)), opacity=0.9
).set_stroke(WHITE, 0.5)
for _ in range(8)
))
result_entries.arrange(DOWN, buff=0.02)
result_bracket_l = Text("[", font_size=72).stretch_to_fit_height(result_entries.get_height() * 1.1)
result_bracket_r = Text("]", font_size=72).stretch_to_fit_height(result_entries.get_height() * 1.1)
result_bracket_l.next_to(result_entries, LEFT, buff=0.05)
result_bracket_r.next_to(result_entries, RIGHT, buff=0.05)
result = VGroup(result_bracket_l, result_entries, result_bracket_r)
result.scale(0.8)
result.next_to(matrix, RIGHT, buff=0.8)
result_label = Text("Logits\n(~50k)", font_size=24)
result_label.next_to(result, DOWN)
# Multiply symbol
times = Tex(r"\times", font_size=48)
times.move_to(midpoint(embedding.get_right(), matrix.get_left()))
equals = Tex("=", font_size=48)
equals.move_to(midpoint(matrix.get_right(), result.get_left()))
# Animate
self.play(FadeIn(embedding, shift=RIGHT), FadeIn(emb_label))
self.play(FadeIn(times))
self.play(FadeIn(matrix, scale=0.9), FadeIn(matrix_label))
self.play(FadeIn(equals))
self.play(FadeIn(result, shift=LEFT), FadeIn(result_label))
self.wait()
# Formula
formula = Tex(r"W_U \cdot \text{emb} = \text{logits}", font_size=36)
formula.next_to(VGroup(embedding, matrix, result), UP, buff=0.8)
self.play(Write(formula))
self.wait(2)
# Cleanup
self.play(FadeOut(VGroup(
title, embedding, emb_label, times, matrix, matrix_label,
equals, result, result_label, formula
)))
def create_matrix(self, rows=6, cols=8):
"""Create a visual matrix."""
entries = VGroup()
for i in range(rows):
row = VGroup()
for j in range(cols):
value = random.uniform(-10, 10)
rect = Rectangle(width=0.25, height=0.25)
rect.set_fill(value_to_color(value), opacity=0.8)
rect.set_stroke(WHITE, 0.3)
row.add(rect)
row.arrange(RIGHT, buff=0.02)
entries.add(row)
entries.arrange(DOWN, buff=0.02)
# Brackets
lb = Text("[", font_size=72)
rb = Text("]", font_size=72)
lb.stretch_to_fit_height(entries.get_height() * 1.1)
rb.stretch_to_fit_height(entries.get_height() * 1.1)
lb.next_to(entries, LEFT, buff=0.05)
rb.next_to(entries, RIGHT, buff=0.05)
return VGroup(lb, entries, rb)
@@ -0,0 +1,308 @@
"""
Quantum Gates Visualization
===========================
Demonstrates quantum gate operations (H, X, Z) as reflections/rotations
of the state vector on the qubit plane.
Key concepts demonstrated:
- DashedLine for reflection axes
- Rotate animation with custom axis
- Gate labels and transitions
- Multiple gate applications
"""
from manimlib import *
class QuantumGatesVisualization(InteractiveScene):
"""Shows how quantum gates transform qubit states."""
def construct(self):
# Title
title = Text("Quantum Gates", font_size=60)
title.to_edge(UP)
self.add(title)
# Set up the qubit plane
plane = NumberPlane((-2, 2), (-2, 2), faded_line_ratio=5)
plane.set_height(5)
plane.center().shift(0.5 * DOWN)
# Qubit labels
zero_label = Tex(R"|0\rangle").scale(0.7)
zero_label.next_to(plane.c2p(1, 0), DR, SMALL_BUFF)
one_label = Tex(R"|1\rangle").scale(0.7)
one_label.next_to(plane.c2p(0, 1), UL, SMALL_BUFF)
# Unit circle
circle = Circle(radius=plane.c2p(1, 0)[0] - plane.c2p(0, 0)[0])
circle.move_to(plane.c2p(0, 0))
circle.set_stroke(GREY, 1, 0.5)
self.add(plane, circle, zero_label, one_label)
# Create the state vector
vector = Arrow(
plane.c2p(0, 0),
plane.c2p(1, 0),
buff=0,
thickness=5,
fill_color=TEAL
)
self.add(vector)
# Define gate reflection axes
# Z gate: reflection about x-axis (horizontal)
# H gate: reflection about 22.5 degree line
# X gate: reflection about 45 degree line (diagonal)
gate_info = [
("Z", 0, BLUE),
("H", PI / 8, YELLOW),
("X", PI / 4, RED),
]
gate_lines = VGroup()
gate_labels = VGroup()
for name, angle, color in gate_info:
line = DashedLine(2 * LEFT, 2 * RIGHT)
line.rotate(angle)
line.move_to(plane.c2p(0, 0))
line.set_stroke(color, 2)
label = Text(name + " gate", font_size=24, color=color)
label.next_to(plane.c2p(1, 1), DR)
gate_lines.add(line)
gate_labels.add(label)
# Apply gates in sequence
gate_sequence = [1, 0, 2, 1, 2, 1, 0, 1] # H, Z, X, H, X, H, Z, H
for i in gate_sequence:
name, angle, color = gate_info[i]
line = gate_lines[i]
label = gate_labels[i]
# Show the gate axis and label
self.play(
FadeIn(line),
FadeIn(label),
run_time=0.5
)
# Rotate vector by 180 degrees about the axis
axis = rotate_vector(RIGHT, angle)
axis_3d = np.array([axis[0], axis[1], 0])
self.play(
Rotate(
vector,
PI,
axis=axis_3d,
about_point=plane.c2p(0, 0)
),
run_time=1.5
)
# Hide the gate visualization
self.play(
FadeOut(line),
FadeOut(label),
run_time=0.3
)
self.wait()
class HadamardGateDetail(InteractiveScene):
"""Detailed visualization of the Hadamard gate transformation."""
def construct(self):
# Set up two planes: before and after
plane1 = NumberPlane((-2, 2), (-2, 2), faded_line_ratio=5)
plane1.set_height(4)
plane2 = plane1.copy()
planes = VGroup(plane1, plane2)
planes.arrange(RIGHT, buff=3)
planes.center()
# Labels
before_label = Text("Before H", font_size=36)
before_label.next_to(plane1, UP)
after_label = Text("After H", font_size=36)
after_label.next_to(plane2, UP)
# Arrow between planes
arrow = Arrow(plane1.get_right(), plane2.get_left(), thickness=5)
h_label = Text("H", font_size=48, color=YELLOW)
h_label.next_to(arrow, UP, SMALL_BUFF)
# Hadamard matrix
matrix_tex = Tex(
R"\frac{1}{\sqrt{2}} \begin{bmatrix} 1 & 1 \\ 1 & -1 \end{bmatrix}",
font_size=30
)
matrix_tex.set_fill(GREY_B)
matrix_tex.next_to(arrow, DOWN, SMALL_BUFF)
self.add(planes, before_label, after_label, arrow, h_label, matrix_tex)
# Add unit circles
for plane in planes:
circle = Circle(radius=plane.c2p(1, 0)[0] - plane.c2p(0, 0)[0])
circle.move_to(plane.c2p(0, 0))
circle.set_stroke(GREY, 1, 0.5)
self.add(circle)
# Create basis vectors
# |0> state
zero_vec = Arrow(
plane1.c2p(0, 0),
plane1.c2p(1, 0),
buff=0,
thickness=4,
fill_color=BLUE
)
zero_label = Tex(R"|0\rangle", font_size=30, color=BLUE)
zero_label.next_to(zero_vec.get_end(), UR, SMALL_BUFF)
# |1> state
one_vec = Arrow(
plane1.c2p(0, 0),
plane1.c2p(0, 1),
buff=0,
thickness=4,
fill_color=GREEN
)
one_label = Tex(R"|1\rangle", font_size=30, color=GREEN)
one_label.next_to(one_vec.get_end(), UL, SMALL_BUFF)
# H|0> = |+> = (|0> + |1>)/sqrt(2)
h_zero_vec = Arrow(
plane2.c2p(0, 0),
plane2.c2p(1, 1) / np.sqrt(2),
buff=0,
thickness=4,
fill_color=BLUE
)
h_zero_label = Tex(R"H|0\rangle = |+\rangle", font_size=24, color=BLUE)
h_zero_label.next_to(h_zero_vec.get_end(), UR, SMALL_BUFF)
# H|1> = |-> = (|0> - |1>)/sqrt(2)
h_one_vec = Arrow(
plane2.c2p(0, 0),
plane2.c2p(1, -1) / np.sqrt(2),
buff=0,
thickness=4,
fill_color=GREEN
)
h_one_label = Tex(R"H|1\rangle = |-\rangle", font_size=24, color=GREEN)
h_one_label.next_to(h_one_vec.get_end(), DR, SMALL_BUFF)
# Animate
self.play(
GrowArrow(zero_vec),
FadeIn(zero_label)
)
self.play(
TransformFromCopy(zero_vec, h_zero_vec, path_arc=-30 * DEG),
FadeIn(h_zero_label),
run_time=2
)
self.wait()
self.play(
GrowArrow(one_vec),
FadeIn(one_label)
)
self.play(
TransformFromCopy(one_vec, h_one_vec, path_arc=-30 * DEG),
FadeIn(h_one_label),
run_time=2
)
self.wait(2)
class GateComposition(InteractiveScene):
"""Shows how multiple gates compose to create quantum circuits."""
def construct(self):
# Create a simple quantum circuit visualization
wire = Line(4 * LEFT, 4 * RIGHT)
wire.set_stroke(WHITE, 2)
# Gate boxes
gates = VGroup()
gate_names = ["H", "X", "Z", "H"]
colors = [YELLOW, RED, BLUE, YELLOW]
for i, (name, color) in enumerate(zip(gate_names, colors)):
box = Square(0.8)
box.set_stroke(WHITE, 2)
box.set_fill(BLACK, 1)
box.move_to(wire.pfp((i + 1) / (len(gate_names) + 1)))
label = Text(name, font_size=36, color=color)
label.move_to(box)
gates.add(VGroup(box, label))
# Input and output labels
input_label = Tex(R"|0\rangle", font_size=48)
input_label.next_to(wire, LEFT)
output_label = Tex(R"|\psi\rangle", font_size=48)
output_label.next_to(wire, RIGHT)
circuit = VGroup(wire, gates, input_label, output_label)
circuit.center().shift(UP)
# Title
title = Text("Quantum Circuit", font_size=48)
title.to_edge(UP)
self.add(title)
self.play(
ShowCreation(wire),
FadeIn(input_label),
FadeIn(output_label)
)
# Show gates appearing one by one
for gate in gates:
self.play(FadeIn(gate, scale=1.2))
self.wait()
# Animate a "quantum state" passing through
glow = GlowDot(wire.get_start(), color=TEAL, radius=0.3)
glow.set_z_index(1)
self.play(
glow.animate.move_to(wire.get_end()),
rate_func=linear,
run_time=3
)
# Show final state
final_state = Tex(
R"|\psi\rangle = -|1\rangle",
font_size=36
)
final_state.next_to(circuit, DOWN, buff=1.0)
self.play(
FadeOut(glow),
FadeIn(final_state, shift=UP)
)
self.wait(2)
if __name__ == "__main__":
# To run: manimgl quantum_gates.py QuantumGatesVisualization
pass
@@ -0,0 +1,199 @@
"""
Qubit State Vector Visualization
================================
Shows a 2D plane representing a single qubit's state as a unit vector.
The vector rotates through different states while displaying probability
distribution for measuring |0> or |1>.
Key concepts demonstrated:
- NumberPlane for 2D visualization
- Vector with updaters tracking angle
- DecimalMatrix for live coordinate display
- Distribution bars showing measurement probabilities
"""
from manimlib import *
class QubitStateVector(InteractiveScene):
def construct(self):
# Set up the 2D plane for qubit visualization
plane = NumberPlane((-2, 2), (-2, 2), faded_line_ratio=5)
plane.set_height(6)
plane.to_edge(LEFT, buff=1.0)
# Create qubit labels |0> and |1>
zero_label = VGroup(Tex(R"|"), Integer(0), Tex(R"\rangle"))
zero_label.arrange(RIGHT, buff=0.05)
one_label = VGroup(Tex(R"|"), Integer(1), Tex(R"\rangle"))
one_label.arrange(RIGHT, buff=0.05)
qubit_labels = VGroup(zero_label, one_label)
qubit_labels.scale(0.6)
zero_label.next_to(plane.c2p(1, 0), DR, SMALL_BUFF)
one_label.next_to(plane.c2p(0, 1), DR, SMALL_BUFF)
self.add(plane, qubit_labels)
# Create the state vector
theta_tracker = ValueTracker(30 * DEG)
vector = Arrow(
plane.c2p(0, 0),
plane.c2p(1, 0),
buff=0,
thickness=6,
fill_color=TEAL
)
vector.add_updater(lambda m: m.set_angle(theta_tracker.get_value()))
vector.add_updater(lambda m: m.shift(plane.c2p(0, 0) - m.get_start()))
# Coordinate display
coord_display = DecimalMatrix(
[[1.0], [0.0]],
bracket_h_buff=0.1,
decimal_config=dict(include_sign=True, num_decimal_places=2)
)
coord_display.scale(0.6)
coord_display.add_background_rectangle()
coord_display.set_backstroke(BLACK, 5)
def get_state():
theta = theta_tracker.get_value()
return np.array([math.cos(theta), math.sin(theta)])
def update_coordinates(matrix):
for element, value in zip(matrix.elements, get_state()):
element.set_value(value)
def position_label(matrix):
x, y = get_state()
buff = SMALL_BUFF + 0.4 * interpolate(
matrix.get_width(), matrix.get_height(), x**2
)
vect = normalize(vector.get_vector())
matrix.move_to(vector.get_end() + buff * vect)
coord_display.add_updater(update_coordinates)
coord_display.add_updater(position_label)
self.add(vector, coord_display)
# Add probability display on the right
prob_title = Text("Measurement Probabilities", font_size=36)
prob_title.to_edge(RIGHT, buff=1.0)
prob_title.to_edge(UP, buff=1.0)
qubits = VGroup(
VGroup(Tex(R"|0\rangle"), Tex("")),
VGroup(Tex(R"|1\rangle"), Tex("")),
)
qubits.arrange(DOWN, buff=1.0)
qubits.next_to(prob_title, DOWN, buff=1.0)
# Probability bars
def get_prob_bars():
probs = get_state()**2
bars = VGroup()
for i, (qubit, prob) in enumerate(zip(qubits, probs)):
bar = Rectangle(
width=prob * 3,
height=0.4
)
bar.next_to(qubit[0], RIGHT, buff=0.3)
bar.set_fill(
interpolate_color(BLUE_D, GREEN, prob),
opacity=1
)
bar.set_stroke(WHITE, 1)
label = Integer(int(100 * prob), unit=R"\%", font_size=24)
label.next_to(bar, RIGHT, SMALL_BUFF)
bars.add(VGroup(bar, label))
return bars
prob_bars = always_redraw(get_prob_bars)
self.add(prob_title, qubits, prob_bars)
# Add unit circle
circle = Circle(radius=plane.c2p(1, 0)[0] - plane.c2p(0, 0)[0])
circle.move_to(plane.c2p(0, 0))
circle.set_stroke(YELLOW, 1, 0.5)
self.play(ShowCreation(circle))
# Animate the vector rotation
self.play(theta_tracker.animate.set_value(60 * DEG), run_time=2)
self.wait()
self.play(theta_tracker.animate.set_value(90 * DEG), run_time=2)
self.wait()
self.play(theta_tracker.animate.set_value(45 * DEG), run_time=2)
self.wait()
# Show the constraint x^2 + y^2 = 1
constraint = Tex(R"x^2 + y^2 = 1", font_size=48)
constraint.to_corner(UR, buff=1.0)
constraint.set_color(YELLOW)
self.play(Write(constraint))
self.wait()
# Full rotation
self.play(
theta_tracker.animate.set_value(theta_tracker.get_value() + TAU),
run_time=6
)
self.wait()
class QubitKetNotation(InteractiveScene):
"""Shows the relationship between vector coordinates and ket notation."""
def construct(self):
# Title
title = Text("Qubit State Representation", font_size=48)
title.to_edge(UP)
self.add(title)
# Vector form
vector_form = Tex(
R"\begin{bmatrix} x \\ y \end{bmatrix}",
font_size=72
)
vector_form.shift(2 * LEFT)
# Ket form
ket_form = Tex(
R"x|0\rangle + y|1\rangle",
font_size=72
)
ket_form.shift(2 * RIGHT)
# Equals sign
equals = Tex(R"\Leftrightarrow", font_size=72)
self.play(Write(vector_form))
self.wait()
self.play(Write(equals))
self.play(Write(ket_form))
self.wait()
# Constraint
constraint = Tex(
R"\text{where } x^2 + y^2 = 1",
font_size=36
)
constraint.next_to(VGroup(vector_form, equals, ket_form), DOWN, buff=1.0)
constraint.set_color(YELLOW)
self.play(FadeIn(constraint, shift=UP))
self.wait(2)
if __name__ == "__main__":
# To run: manimgl qubit_state_vector.py QubitStateVector
pass
@@ -0,0 +1,127 @@
"""
Query-Key Dot Products Grid Visualization
Shows how queries and keys produce a grid of dot products that form the attention pattern.
"""
from manimlib import *
import numpy as np
class QueryKeyDotProducts(InteractiveScene):
def construct(self):
# Create query and key symbols
n_tokens = 5
# Query template
q_template = Tex(R"\vec{\textbf{Q}}_0")
q_template[0].scale(1.5, about_edge=DOWN)
q_template.set_color(YELLOW)
q_subscript = q_template.make_number_changeable("0")
# Key template
k_template = Tex(R"\vec{\textbf{K}}_0")
k_template[0].scale(1.5, about_edge=DOWN)
k_template.set_color(TEAL)
k_subscript = k_template.make_number_changeable("0")
# Create query symbols along top
q_syms = VGroup()
for n in range(1, n_tokens + 1):
q_subscript.set_value(n)
q_syms.add(q_template.copy())
q_syms.arrange(RIGHT, buff=0.8)
q_syms.move_to(2 * UP)
# Create key symbols along left
k_syms = VGroup()
for n in range(1, n_tokens + 1):
k_subscript.set_value(n)
k_syms.add(k_template.copy())
k_syms.arrange(DOWN, buff=0.6)
k_syms.next_to(q_syms, DL, buff=0.8)
k_syms.shift(0.5 * LEFT)
self.play(
LaggedStartMap(FadeIn, q_syms, shift=0.5 * DOWN, lag_ratio=0.1),
LaggedStartMap(FadeIn, k_syms, shift=0.5 * RIGHT, lag_ratio=0.1),
)
self.wait()
# Draw grid lines
h_lines = VGroup()
for k in k_syms:
h_line = Line(LEFT, RIGHT).set_width(6)
h_line.next_to(k, DOWN, buff=0.3)
h_line.align_to(k_syms, LEFT)
h_lines.add(h_line)
v_lines = VGroup()
for q in q_syms:
v_line = Line(UP, DOWN).set_height(5)
v_line.next_to(q, DOWN, buff=0.3)
v_lines.add(v_line)
v_lines.add(v_lines[-1].copy().next_to(q_syms, RIGHT, buff=0.5))
grid_lines = VGroup(*h_lines, *v_lines)
grid_lines.set_stroke(GREY_A, 1)
self.play(
ShowCreation(h_lines, lag_ratio=0.2),
ShowCreation(v_lines, lag_ratio=0.2),
)
# Create dot products in each cell
dot_prods = VGroup()
for k_sym in k_syms:
for q_sym in q_syms:
square_center = np.array([q_sym.get_x(), k_sym.get_y(), 0])
dot = Tex(R"\cdot", font_size=48)
dot.move_to(square_center)
dot.set_fill(opacity=0)
dot_prod = VGroup(k_sym.copy(), dot, q_sym.copy())
dot_prod.target = dot_prod.generate_target()
dot_prod.target.arrange(RIGHT, buff=0.1)
dot_prod.target.scale(0.5)
dot_prod.target.move_to(square_center)
dot_prod.target.set_fill(opacity=1)
dot_prods.add(dot_prod)
self.play(
LaggedStartMap(MoveToTarget, dot_prods, lag_ratio=0.02, run_time=3)
)
self.wait()
# Show numerical values (random attention scores)
np.random.seed(42)
dots = VGroup(
VGroup(Dot().match_x(q_sym).match_y(k_sym) for q_sym in q_syms)
for k_sym in k_syms
)
# Set sizes based on "attention" - diagonal and some off-diagonal get bigger
for n, row in enumerate(dots):
for k, dot in enumerate(row):
base_size = 0.1 + 0.15 * np.random.random()
dot.set_width(base_size)
dot.set_fill(GREY_C, 0.8)
# Make diagonal stronger (self-attention)
if n == k:
dot.set_width(0.5 + 0.2 * np.random.random())
dot.set_fill(WHITE, 1)
flat_dots = VGroup(*it.chain(*dots))
self.play(
dot_prods.animate.set_fill(opacity=0.3),
LaggedStartMap(GrowFromCenter, flat_dots, lag_ratio=0.02)
)
self.wait()
# Label as attention pattern
pattern_label = Text("Attention Pattern", font_size=60)
pattern_label.to_edge(DOWN)
pattern_label.set_color(YELLOW)
self.play(Write(pattern_label))
self.wait(2)
@@ -0,0 +1,199 @@
"""
Query/Key Space Mapping Visualization
Shows how embeddings in high-dimensional space get projected to a lower-dimensional
query/key space where dot products measure relevance.
"""
from manimlib import *
import numpy as np
class QueryKeySpaceMapping(InteractiveScene):
def construct(self):
# Set up 3D view
self.set_floor_plane("xz")
frame = self.frame
frame.set_field_of_view(30 * DEGREES)
frame.reorient(-30, -5, 0, (2, 1, 0), 4.5)
frame.add_ambient_rotation(1 * DEGREES)
# Create 3D axes representing embedding space
axes_3d = ThreeDAxes((-3, 3), (-3, 3), (-3, 3))
xz_plane = NumberPlane(
(-3, 3), (-3, 3),
background_line_style=dict(
stroke_color=GREY,
stroke_width=1,
),
faded_line_ratio=0
)
xz_plane.rotate(90 * DEGREES, RIGHT)
xz_plane.move_to(axes_3d)
xz_plane.axes.set_opacity(0)
axes_3d.add(xz_plane)
axes_3d.set_height(2.5)
self.add(axes_3d)
# Create target 2D plane (Query/Key space)
plane_2d = NumberPlane(
(-2.5, 2.5), (-2.5, 2.5),
faded_line_ratio=1,
background_line_style=dict(
stroke_color=BLUE,
stroke_width=1,
stroke_opacity=0.75
),
faded_line_style=dict(
stroke_color=BLUE,
stroke_width=1,
stroke_opacity=0.25,
)
)
plane_2d.set_height(3.0)
plane_2d.to_corner(DR)
plane_2d.fix_in_frame()
# Arrow showing the mapping
arrow = Tex(R"\longrightarrow", font_size=72)
arrow.set_width(1.5)
arrow.stretch(0.7, 1)
arrow.next_to(plane_2d, LEFT, buff=0.8)
arrow.set_color(YELLOW)
arrow.fix_in_frame()
# Label for the mapping
map_label = Tex("W_Q", font_size=60)
map_label.set_color(YELLOW)
map_label.next_to(arrow.get_left(), UR, SMALL_BUFF)
map_label.shift(0.2 * RIGHT)
map_label.fix_in_frame()
# Titles
titles = VGroup(
Text("Embedding space", font_size=30),
Text("Query/Key space", font_size=30),
)
subtitles = VGroup(
Text("12,288-dimensional", font_size=22),
Text("128-dimensional", font_size=22),
)
subtitles.set_fill(GREY_B)
for title, subtitle in zip(titles, subtitles):
subtitle.next_to(title, DOWN, SMALL_BUFF)
title.add(subtitle)
titles[0].to_edge(UL, buff=0.5)
titles[0].fix_in_frame()
titles[1].next_to(plane_2d, UP, MED_LARGE_BUFF)
titles[1].fix_in_frame()
self.add(plane_2d)
self.add(arrow)
self.add(map_label)
self.add(titles)
# Create a vector in 3D space
in_coords = (2, 2.5, 1)
in_vect = Arrow(axes_3d.get_origin(), axes_3d.c2p(*in_coords), buff=0)
in_vect.set_stroke(TEAL, 5)
in_label = Text("\"Creature\"", font_size=20)
in_label.set_color(TEAL)
in_label.next_to(in_vect.get_end(), UP, SMALL_BUFF)
# Create corresponding vector in 2D space
out_coords = (-1.5, -1)
out_vect = Arrow(plane_2d.get_origin(), plane_2d.c2p(*out_coords), buff=0)
out_vect.set_stroke(YELLOW, 4)
out_vect.fix_in_frame()
out_label = Text("Query:\nAny adjectives\nbefore me?", font_size=16)
out_label.next_to(out_vect.get_end(), DOWN, buff=0.15)
out_label.set_backstroke(BLACK, 3)
out_label.fix_in_frame()
# Animate the transformation
self.play(
GrowArrow(in_vect),
FadeInFromPoint(in_label, axes_3d.get_origin()),
run_time=1.5
)
self.wait(2)
self.play(
TransformFromCopy(in_vect, out_vect),
FadeTransform(in_label.copy(), out_label),
run_time=2,
)
self.wait()
# Show second vector (Key)
in_coords_2 = (-2, 1, 2)
in_vect_2 = Arrow(axes_3d.get_origin(), axes_3d.c2p(*in_coords_2), buff=0)
in_vect_2.set_stroke(BLUE, 5)
in_label_2 = Text("\"Fluffy\"", font_size=20)
in_label_2.set_color(BLUE)
in_label_2.next_to(in_vect_2.get_end(), UP, SMALL_BUFF)
out_coords_2 = (-1.2, -0.8)
out_vect_2 = Arrow(plane_2d.get_origin(), plane_2d.c2p(*out_coords_2), buff=0)
out_vect_2.set_stroke(TEAL, 4)
out_vect_2.fix_in_frame()
out_label_2 = Text("Key:\nAdjective at\nposition 1", font_size=16)
out_label_2.next_to(out_vect_2.get_end(), LEFT, buff=0.15)
out_label_2.set_backstroke(BLACK, 3)
out_label_2.fix_in_frame()
# Change map label to W_K
map_label_k = Tex("W_K", font_size=60)
map_label_k.set_color(TEAL)
map_label_k.move_to(map_label)
map_label_k.fix_in_frame()
self.play(
GrowArrow(in_vect_2),
FadeInFromPoint(in_label_2, axes_3d.get_origin()),
FadeTransform(map_label, map_label_k),
arrow.animate.set_color(TEAL),
run_time=1.5
)
self.wait(2)
self.play(
TransformFromCopy(in_vect_2, out_vect_2),
FadeTransform(in_label_2.copy(), out_label_2),
run_time=2,
)
self.wait()
# Show dot product in 2D space
dot_product_label = Tex(R"\vec{Q} \cdot \vec{K}", font_size=36)
dot_product_label.set_color(WHITE)
dot_product_label.next_to(plane_2d, DOWN, buff=0.3)
dot_product_label.fix_in_frame()
# Highlight the angle between vectors
angle_arc = Arc(
start_angle=out_vect.get_angle(),
angle=out_vect_2.get_angle() - out_vect.get_angle(),
radius=0.4,
arc_center=plane_2d.get_origin(),
)
angle_arc.set_stroke(WHITE, 2)
angle_arc.fix_in_frame()
high_score = Text("High score = relevant!", font_size=24)
high_score.set_color(GREEN)
high_score.next_to(dot_product_label, DOWN, SMALL_BUFF)
high_score.fix_in_frame()
self.play(
Write(dot_product_label),
ShowCreation(angle_arc),
)
self.play(Write(high_score))
self.wait(5)
@@ -0,0 +1,251 @@
"""
Radial Wave Visualization
A beautiful visualization of a radial wave emanating from a point source,
demonstrating wave propagation and decay. Based on 3Blue1Brown's hologram/diffraction
visualizations.
Run: manimgl radial_wave_visualization.py RadialWaveDemo -w
"""
from manimlib import *
import numpy as np
class RadialWaveDemo(Scene):
"""
Demonstrates a radial wave visualization using procedural graphics.
Shows how waves propagate from a point source with decay.
"""
def construct(self):
# Setup
frame = self.camera.frame
frame.reorient(0, 0, 0)
# Create point source
source_point = Dot(ORIGIN, color=WHITE, radius=0.15)
source_glow = VGroup(
Circle(radius=r, stroke_color=WHITE, stroke_opacity=0.5 - 0.1 * r, stroke_width=2)
for r in [0.2, 0.3, 0.4, 0.5]
)
source = VGroup(source_point, source_glow)
# Wave parameters
wave_number = 2.0
frequency = 0.5
decay_factor = 0.3
max_radius = 8.0
# Create concentric wave rings that expand
def get_wave_rings(time):
rings = VGroup()
for phase_offset in np.arange(0, 8, 0.5):
radius = (time * frequency / wave_number + phase_offset) % (max_radius + 1)
if radius > 0.1 and radius < max_radius:
amplitude = np.exp(-decay_factor * radius)
ring = Circle(radius=radius)
ring.set_stroke(
color=BLUE,
width=2 + 3 * amplitude,
opacity=0.8 * amplitude
)
rings.add(ring)
return rings
# Initial state
time_tracker = ValueTracker(0)
wave_rings = always_redraw(lambda: get_wave_rings(time_tracker.get_value()))
# Add title
title = Text("Radial Wave Propagation", font_size=48)
title.to_edge(UP)
title.set_backstroke(BLACK, 5)
self.add(title)
self.add(source)
self.add(wave_rings)
# Animate wave propagation
self.play(
time_tracker.animate.set_value(20),
run_time=10,
rate_func=linear
)
# Show label for decay
decay_label = Text("Amplitude decays with distance", font_size=32)
decay_label.next_to(title, DOWN)
decay_label.set_backstroke(BLACK, 3)
self.play(Write(decay_label))
self.play(
time_tracker.animate.set_value(35),
run_time=8,
rate_func=linear
)
self.wait()
class WaveInterferencePattern(Scene):
"""
Shows interference pattern from two point sources.
Demonstrates constructive and destructive interference.
"""
def construct(self):
frame = self.camera.frame
# Two source points
separation = 3.0
source1_pos = separation / 2 * LEFT
source2_pos = separation / 2 * RIGHT
source1 = Dot(source1_pos, color=RED, radius=0.15)
source2 = Dot(source2_pos, color=BLUE, radius=0.15)
# Wave parameters
wave_number = 1.5
frequency = 0.5
max_radius = 10.0
# Create wave function that shows interference
def get_interference_field(time):
# Create a grid of points
x_range = np.linspace(-7, 7, 70)
y_range = np.linspace(-4, 4, 40)
dots = VGroup()
for x in x_range:
for y in y_range:
point = np.array([x, y, 0])
r1 = np.linalg.norm(point - source1_pos)
r2 = np.linalg.norm(point - source2_pos)
# Wave from source 1
phase1 = TAU * (wave_number * r1 - frequency * time)
amp1 = np.cos(phase1) / (1 + 0.3 * r1)
# Wave from source 2
phase2 = TAU * (wave_number * r2 - frequency * time)
amp2 = np.cos(phase2) / (1 + 0.3 * r2)
# Combined amplitude
total_amp = (amp1 + amp2) / 2
# Color based on amplitude
if total_amp > 0:
color = interpolate_color(BLACK, BLUE, min(total_amp, 1))
else:
color = interpolate_color(BLACK, RED, min(-total_amp, 1))
dot = Dot(point, radius=0.05, color=color)
dot.set_fill(opacity=0.3 + 0.7 * abs(total_amp))
dots.add(dot)
return dots
time_tracker = ValueTracker(0)
field = always_redraw(lambda: get_interference_field(time_tracker.get_value()))
# Title
title = Text("Two-Source Interference", font_size=48)
title.to_edge(UP)
title.set_backstroke(BLACK, 5)
# Labels for sources
label1 = Text("Source 1", font_size=24, color=RED)
label1.next_to(source1, DOWN)
label2 = Text("Source 2", font_size=24, color=BLUE)
label2.next_to(source2, DOWN)
self.add(title)
self.add(field)
self.add(source1, source2)
self.add(label1, label2)
# Animate
self.play(
time_tracker.animate.set_value(12),
run_time=12,
rate_func=linear
)
# Show constructive/destructive labels
constructive = Text("Constructive (bright)", font_size=28, color=BLUE)
destructive = Text("Destructive (dark)", font_size=28, color=RED)
labels = VGroup(constructive, destructive)
labels.arrange(DOWN, buff=0.5)
labels.to_edge(LEFT)
labels.set_backstroke(BLACK, 3)
self.play(Write(labels))
self.play(
time_tracker.animate.set_value(20),
run_time=8,
rate_func=linear
)
self.wait()
class WavePropagation3D(Scene):
"""
3D visualization of wave propagation from a point source.
Shows the wave as expanding spherical shells.
"""
def construct(self):
frame = self.camera.frame
frame.reorient(30, 70, 0)
# Parameters
wave_number = 1.0
frequency = 0.4
max_radius = 6.0
# Source point
source = Sphere(radius=0.15, color=WHITE)
source.move_to(ORIGIN)
# Create expanding wave shells
def get_wave_shells(time):
shells = Group()
for phase_offset in np.arange(0, 10, 1.0 / wave_number):
radius = (time * frequency / wave_number + phase_offset)
if 0.3 < radius < max_radius:
amplitude = np.exp(-0.2 * radius)
shell = Sphere(radius=radius)
shell.set_color(BLUE)
shell.set_opacity(0.15 * amplitude)
shells.add(shell)
return shells
time_tracker = ValueTracker(0)
shells = always_redraw(lambda: get_wave_shells(time_tracker.get_value()))
# Add axes for reference
axes = ThreeDAxes(
x_range=[-5, 5, 1],
y_range=[-5, 5, 1],
z_range=[-5, 5, 1],
)
axes.set_opacity(0.3)
self.add(axes)
self.add(shells)
self.add(source)
# Animate with camera rotation
self.play(
time_tracker.animate.set_value(15),
frame.animate.increment_theta(60 * DEGREES),
run_time=15,
rate_func=linear
)
self.play(
time_tracker.animate.set_value(25),
frame.animate.increment_theta(30 * DEGREES).set_phi(50 * DEGREES),
run_time=10,
rate_func=linear
)
self.wait()
@@ -0,0 +1,330 @@
"""
Rotating Exponentials and Complex Numbers
Visualizes e^(it) as a rotating vector in the complex plane,
showing how cosine emerges from combining two counter-rotating exponentials.
Run: manimgl rotating_exponentials.py RotatingExponential -w
Preview: manimgl rotating_exponentials.py RotatingExponential -p
Source: Adapted from 3b1b's Laplace transform video (2025)
"""
from manimlib import *
import numpy as np
class RotatingExponential(InteractiveScene):
"""
Shows e^(it) as a rotating vector on the complex plane.
The fundamental visualization of Euler's formula.
"""
def construct(self):
# Create complex plane
plane = ComplexPlane(
x_range=(-2, 2, 1),
y_range=(-2, 2, 1),
background_line_style={"stroke_opacity": 0.5}
)
plane.add_coordinate_labels(font_size=20)
# Title
title = Tex(r"e^{it}", font_size=60)
title.to_corner(UL)
self.play(FadeIn(plane), Write(title))
# Create rotating vector
omega = 1 # Angular frequency
time_tracker = ValueTracker(0)
# The vector
vector = Vector(RIGHT, color=YELLOW)
vector.add_updater(lambda v: v.put_start_and_end_on(
ORIGIN,
plane.n2p(np.exp(1j * time_tracker.get_value()))
))
# Dot at tip
tip_dot = Dot(color=YELLOW)
tip_dot.add_updater(lambda d: d.move_to(vector.get_end()))
# Traced path (the unit circle)
traced = TracedPath(
tip_dot.get_center,
stroke_color=BLUE,
stroke_width=2,
)
# Angle arc
angle_arc = always_redraw(lambda: Arc(
start_angle=0,
angle=time_tracker.get_value() % TAU,
radius=0.3,
color=GREEN
))
# Angle label
angle_label = Tex("t", font_size=30, color=GREEN)
angle_label.add_updater(lambda m: m.move_to(
0.5 * (np.cos(time_tracker.get_value() / 2) * RIGHT +
np.sin(time_tracker.get_value() / 2) * UP)
))
self.play(
GrowArrow(vector),
FadeIn(tip_dot),
FadeIn(angle_arc),
FadeIn(angle_label),
)
self.add(traced)
# Rotate through one full cycle
self.play(
time_tracker.animate.set_value(TAU),
run_time=4,
rate_func=linear
)
# Continue rotating
time_tracker.add_updater(lambda m, dt: m.increment_value(dt))
self.wait(4)
class CounterRotatingExponentials(InteractiveScene):
"""
Shows how e^(it) + e^(-it) = 2cos(t).
Two counter-rotating vectors that sum to give real cosine.
"""
def construct(self):
# Create complex plane
plane = ComplexPlane(
x_range=(-3, 3, 1),
y_range=(-2, 2, 1),
background_line_style={"stroke_opacity": 0.4}
)
plane.add_coordinate_labels(font_size=18)
self.play(FadeIn(plane))
# Time tracker
time_tracker = ValueTracker(0)
# e^(it) vector (counter-clockwise)
v1 = Vector(RIGHT, color=BLUE)
v1.add_updater(lambda v: v.put_start_and_end_on(
ORIGIN,
plane.n2p(np.exp(1j * time_tracker.get_value()))
))
# e^(-it) vector (clockwise)
v2 = Vector(RIGHT, color=RED)
v2.add_updater(lambda v: v.put_start_and_end_on(
ORIGIN,
plane.n2p(np.exp(-1j * time_tracker.get_value()))
))
# Sum vector (always real = 2cos(t))
v_sum = Vector(RIGHT, color=GREEN, stroke_width=6)
v_sum.add_updater(lambda v: v.put_start_and_end_on(
ORIGIN,
plane.n2p(2 * np.cos(time_tracker.get_value()))
))
# Labels
labels = VGroup(
Tex(r"e^{it}", color=BLUE, font_size=36),
Tex(r"e^{-it}", color=RED, font_size=36),
Tex(r"e^{it} + e^{-it} = 2\cos(t)", color=GREEN, font_size=36),
)
labels.arrange(DOWN, aligned_edge=LEFT)
labels.to_corner(UL)
# Traced paths
dot1 = Dot(color=BLUE, radius=0.05)
dot1.add_updater(lambda d: d.move_to(v1.get_end()))
trace1 = TracedPath(dot1.get_center, stroke_color=BLUE, stroke_width=1)
dot2 = Dot(color=RED, radius=0.05)
dot2.add_updater(lambda d: d.move_to(v2.get_end()))
trace2 = TracedPath(dot2.get_center, stroke_color=RED, stroke_width=1)
self.play(
GrowArrow(v1),
GrowArrow(v2),
Write(labels[0]),
Write(labels[1]),
)
self.add(dot1, dot2, trace1, trace2)
# Rotate to show counter-rotation
self.play(
time_tracker.animate.set_value(TAU),
run_time=4,
rate_func=linear
)
# Now show the sum
self.play(
GrowArrow(v_sum),
Write(labels[2]),
)
# Continue rotating to show sum is always real
time_tracker.add_updater(lambda m, dt: m.increment_value(dt))
self.wait(6)
class EulersFormula(InteractiveScene):
"""
The famous e^(i*pi) = -1 visualization.
Shows how rotating by pi radians lands at -1.
"""
def construct(self):
# Create plane
plane = ComplexPlane(
x_range=(-2, 2, 1),
y_range=(-1.5, 1.5, 1),
)
plane.add_coordinate_labels(font_size=20)
# Unit circle
circle = Circle(radius=1, color=BLUE_C, stroke_width=2)
self.play(FadeIn(plane), ShowCreation(circle))
# Start at 1
start_dot = Dot(plane.n2p(1), color=YELLOW)
start_label = Tex("1", font_size=30)
start_label.next_to(start_dot, DR, buff=0.1)
self.play(FadeIn(start_dot), Write(start_label))
# Show the formula building up
formula = Tex(r"e^{i\pi}", font_size=72)
formula.to_corner(UR)
self.play(Write(formula))
# Animate rotation from 1 to -1
rotating_dot = Dot(plane.n2p(1), color=GREEN)
rotating_vec = Vector(RIGHT, color=GREEN)
angle_tracker = ValueTracker(0)
rotating_vec.add_updater(lambda v: v.put_start_and_end_on(
ORIGIN,
plane.n2p(np.exp(1j * angle_tracker.get_value()))
))
rotating_dot.add_updater(lambda d: d.move_to(rotating_vec.get_end()))
# Arc to trace the path
traced_arc = TracedPath(rotating_dot.get_center, stroke_color=YELLOW, stroke_width=3)
self.play(GrowArrow(rotating_vec), FadeIn(rotating_dot))
self.add(traced_arc)
# Rotate to pi
self.play(
angle_tracker.animate.set_value(PI),
run_time=3,
rate_func=smooth
)
# Show = -1
end_dot = Dot(plane.n2p(-1), color=RED)
end_label = Tex("-1", font_size=30, color=RED)
end_label.next_to(end_dot, DL, buff=0.1)
equals = Tex(r"= -1", font_size=72)
equals.next_to(formula, RIGHT)
self.play(
FadeIn(end_dot),
Write(end_label),
Write(equals),
)
self.wait()
# Rearrange to famous form
famous = Tex(r"e^{i\pi} + 1 = 0", font_size=72)
famous.move_to(formula.get_center() + 0.5 * RIGHT)
self.play(
FadeOut(equals),
TransformMatchingTex(formula, famous),
)
self.wait(2)
class ComplexExponentialSpiral(InteractiveScene):
"""
Shows e^((a+bi)t) = e^(at) * e^(bit) as an exponential spiral.
When a < 0, we get a decaying spiral (damped oscillation).
"""
def construct(self):
# Create plane
plane = ComplexPlane(
x_range=(-4, 4, 1),
y_range=(-3, 3, 1),
background_line_style={"stroke_opacity": 0.3}
)
plane.scale(0.8)
self.play(FadeIn(plane))
# Parameters
a = -0.15 # Decay rate
b = 2 # Angular frequency
# Title showing the exponent
title = Tex(r"e^{(-0.15 + 2i)t}", font_size=48)
title.to_corner(UL)
self.play(Write(title))
# Time tracker
time_tracker = ValueTracker(0)
def get_position():
t = time_tracker.get_value()
return plane.n2p(np.exp((a + 1j * b) * t))
# Spiral tracer
dot = Dot(get_position(), color=YELLOW)
dot.add_updater(lambda d: d.move_to(get_position()))
spiral = TracedPath(
dot.get_center,
stroke_color=BLUE,
stroke_width=2,
)
# Vector from origin
vec = Vector(RIGHT, color=YELLOW)
vec.add_updater(lambda v: v.put_start_and_end_on(ORIGIN, get_position()))
self.play(FadeIn(dot), GrowArrow(vec))
self.add(spiral)
# Trace the spiral
self.play(
time_tracker.animate.set_value(15),
run_time=8,
rate_func=linear
)
self.wait()
# Show components
explanation = VGroup(
Tex(r"e^{at}", r"\text{ controls amplitude}", font_size=30),
Tex(r"e^{ibt}", r"\text{ controls rotation}", font_size=30),
)
explanation.arrange(DOWN, aligned_edge=LEFT)
explanation.to_corner(DR)
explanation[0][0].set_color(RED)
explanation[1][0].set_color(BLUE)
self.play(Write(explanation))
self.wait(2)
@@ -0,0 +1,191 @@
"""
Semantic Similarity Visualization
Shows how similar words cluster together in embedding space.
Based on: videos/_2024/transformers/embedding.py - ShowNearestNeighbors
"""
from manimlib import *
class SemanticSimilarity(InteractiveScene):
"""
Demonstrates semantic clustering in word embedding space.
Similar words are shown as nearby vectors.
"""
def construct(self):
# Set up 3D scene
frame = self.frame
frame.reorient(-21, 87, 0, (2.18, 0.09, 0.72), 4)
frame.add_ambient_rotation(1 * DEGREES)
# Create axes
axes = ThreeDAxes(
x_range=(-5, 5, 1),
y_range=(-5, 5, 1),
z_range=(-4, 4, 1),
width=8,
height=8,
depth=6.4,
)
axes.set_stroke(width=2)
self.add(axes)
# Add reference plane
plane = NumberPlane(
axes.x_range[:2], axes.y_range[:2],
width=axes.get_width(),
height=axes.get_height(),
background_line_style=dict(
stroke_color=GREY,
stroke_width=1,
),
faded_line_style=dict(
stroke_opacity=0.25,
stroke_width=0.5,
),
faded_line_ratio=1,
)
self.add(plane)
# Seed word and its neighbors
seed_word = "tower"
seed_color = YELLOW
neighbor_words = [
"castle", "fortress", "building", "spire",
"monument", "cathedral", "skyscraper"
]
# Create seed vector
seed_pos = np.array([2, 0, 1])
def create_word_arrow(word, pos, color):
arrow = Arrow(
axes.get_origin(),
axes.c2p(*pos),
buff=0,
stroke_color=color,
stroke_width=4,
)
arrow.set_flat_stroke(False)
label = Text(word, font_size=24)
label.set_backstroke(BLACK, 3)
label.next_to(arrow.get_end(), normalize(arrow.get_vector()), buff=0.05)
# Keep label visible by fixing in frame
label.fix_in_frame()
return VGroup(arrow, label)
seed_vect = create_word_arrow(seed_word, seed_pos, seed_color)
self.add(seed_vect)
# Create title (fixed in frame)
title = Text(f"Words similar to '{seed_word}'", font_size=42)
title.fix_in_frame()
title.to_corner(UR)
underline = Underline(title)
underline.fix_in_frame()
self.add(title, underline)
# Create neighbor positions (clustered around seed)
np.random.seed(42)
neighbor_positions = [
seed_pos + np.random.uniform(-0.8, 0.8, 3)
for _ in neighbor_words
]
# Create list display
items = VGroup(*(
Text(f" {word}", font_size=30)
for word in neighbor_words
))
items.arrange(DOWN, aligned_edge=LEFT)
items.next_to(underline, DOWN, buff=0.5)
items.align_to(title, LEFT)
items.fix_in_frame()
# Animate neighbors appearing
neighbors = []
last_neighbor = VectorizedPoint()
for i, (word, pos, item) in enumerate(zip(neighbor_words, neighbor_positions, items)):
# Create slightly different colors for variety
hue = 0.55 + 0.1 * np.random.random()
color = Color(hsl=(hue, 0.6, 0.5))
neighbor = create_word_arrow(word, pos, color)
neighbors.append(neighbor)
# Fade previous neighbor
faded_neighbor = last_neighbor.copy()
faded_neighbor.set_opacity(0.3)
self.add(faded_neighbor, seed_vect, neighbor)
self.play(
FadeIn(item),
FadeIn(neighbor),
FadeOut(last_neighbor),
FadeIn(faded_neighbor),
run_time=0.5
)
last_neighbor = neighbor
self.wait(0.3)
# Fade last neighbor
self.play(last_neighbor.animate.set_opacity(0.3))
self.wait(2)
# Show all neighbors together
all_neighbors = VGroup(*neighbors)
self.play(all_neighbors.animate.set_opacity(1))
self.wait()
# Draw circle to show clustering
cluster_center = axes.c2p(*seed_pos)
cluster_circle = Circle(radius=1.2)
cluster_circle.move_to(cluster_center)
cluster_circle.set_stroke(YELLOW, 2)
cluster_circle.set_fill(YELLOW, 0.1)
self.play(ShowCreation(cluster_circle))
self.wait()
# Add clustering label
cluster_label = Text("Semantic cluster", font_size=36, color=YELLOW)
cluster_label.fix_in_frame()
cluster_label.next_to(items, DOWN, buff=1.0)
self.play(Write(cluster_label))
self.wait(3)
# Show contrasting words far away
contrast_words = ["banana", "running", "purple"]
contrast_positions = [
np.array([-3, -2, -1]),
np.array([-2, 3, 0]),
np.array([0, -3, 2]),
]
contrast_label = Text("Unrelated words", font_size=30, color=RED)
contrast_label.fix_in_frame()
contrast_label.next_to(cluster_label, DOWN, buff=0.3)
contrast_vects = VGroup()
for word, pos in zip(contrast_words, contrast_positions):
vect = create_word_arrow(word, pos, RED)
vect.set_opacity(0.6)
contrast_vects.add(vect)
self.play(
LaggedStartMap(FadeIn, contrast_vects, lag_ratio=0.3),
Write(contrast_label),
run_time=2
)
self.wait(3)
# Rotate scene to show 3D structure
frame.clear_updaters()
self.play(
frame.animate.reorient(-100, 60, 100, (0, 0, 0), 6),
run_time=5
)
self.wait(2)
@@ -0,0 +1,83 @@
"""
Simple ManimGL test without LaTeX
Run: PATH="/Library/TeX/texbin:$PATH" manimgl simple_test.py SimpleTest -w -l
"""
from manimlib import *
import numpy as np
class SimpleTest(Scene):
"""Basic shapes test - no LaTeX required."""
def construct(self):
# Title using Text (no LaTeX needed)
title = Text("ManimGL Test")
title.to_edge(UP)
self.play(Write(title))
self.wait()
# Create simple shapes
circle = Circle(color=BLUE)
circle.set_fill(BLUE, opacity=0.5)
square = Square(color=RED)
square.set_fill(RED, opacity=0.5)
triangle = Triangle(color=GREEN)
triangle.set_fill(GREEN, opacity=0.5)
shapes = VGroup(circle, square, triangle)
shapes.arrange(RIGHT, buff=1)
self.play(
LaggedStart(
*[ShowCreation(s) for s in shapes],
lag_ratio=0.3
)
)
self.wait()
# Transform
self.play(
circle.animate.shift(UP),
square.animate.rotate(PI/4),
triangle.animate.scale(1.5),
)
self.wait()
# Fade out
self.play(FadeOut(VGroup(shapes, title)))
class Simple3D(Scene):
"""Basic 3D test."""
def construct(self):
frame = self.camera.frame
# 3D axes
axes = ThreeDAxes()
self.add(axes)
# Sphere
sphere = Sphere(radius=1)
sphere.set_color(BLUE)
self.add(sphere)
# Rotate camera
self.play(
frame.animate.set_euler_angles(
phi=70 * DEGREES,
theta=-45 * DEGREES
),
run_time=2
)
self.wait()
# Rotate around
self.play(
frame.animate.increment_theta(90 * DEGREES),
run_time=3
)
self.wait()
@@ -0,0 +1,151 @@
"""
Softmax function visualization showing probability distributions.
Demonstrates: BarChart, DecimalNumber, animations for probability concepts
"""
from manimlib import *
import numpy as np
def softmax(logits, temperature=1.0):
"""Compute softmax with optional temperature parameter."""
logits = np.array(logits)
logits = logits - np.max(logits) # For numerical stability
if temperature == 0:
result = np.zeros_like(logits, dtype=float)
result[np.argmax(logits)] = 1
return result
exps = np.exp(logits / temperature)
return exps / np.sum(exps)
class SoftmaxVisualization(Scene):
def construct(self):
# Example data - logits for different categories
categories = ['Cat', 'Dog', 'Bird', 'Fish', 'Rabbit', 'Hamster']
logits = np.array([-0.8, 2.5, 0.5, 1.5, 3.4, -2.3])
probs = softmax(logits)
# Create bar chart
chart = BarChart(probs, width=10, height=5)
chart.bars.set_stroke(width=1)
chart.to_edge(DOWN, buff=1)
# Add category labels
labels = VGroup()
for word, bar in zip(categories, chart.bars):
label = Text(word, font_size=30)
label.next_to(bar, DOWN)
labels.add(label)
# Add probability values above bars
prob_labels = VGroup()
for p, bar in zip(probs, chart.bars):
label = DecimalNumber(p, num_decimal_places=3, font_size=24)
label.next_to(bar, UP, buff=0.1)
prob_labels.add(label)
# Title
title = Text("Softmax: Converting Logits to Probabilities", font_size=48)
title.to_edge(UP)
# Animate
self.play(FadeIn(title))
self.wait(0.5)
# Show logits first
logit_text = Text("Input logits:", font_size=36)
logit_values = VGroup(*(
DecimalNumber(v, include_sign=True, font_size=30)
for v in logits
))
logit_values.arrange(RIGHT, buff=0.5)
logit_group = VGroup(logit_text, logit_values)
logit_group.arrange(RIGHT, buff=0.5)
logit_group.next_to(title, DOWN)
self.play(FadeIn(logit_group))
self.wait()
# Animate bars growing
chart.save_state()
for bar in chart.bars:
bar.stretch(0, 1, about_edge=DOWN)
chart.set_opacity(0)
self.play(
Restore(chart, lag_ratio=0.1),
LaggedStartMap(FadeIn, labels),
run_time=2
)
self.play(LaggedStartMap(FadeIn, prob_labels, shift=0.2 * UP))
self.wait()
# Show constraint: sum = 1
sum_text = Tex(R"\sum p_i = 1", font_size=48)
sum_text.next_to(chart, RIGHT, buff=1)
self.play(Write(sum_text))
self.wait()
# Show line at p=1
one_line = DashedLine(
chart.c2p(0, 1),
chart.c2p(len(categories), 1),
)
one_line.set_stroke(RED, 2)
self.play(ShowCreation(one_line))
self.wait()
# Demonstrate temperature effect
self.play(
FadeOut(one_line),
FadeOut(sum_text),
FadeOut(logit_group),
)
temp_label = VGroup(
Text("Temperature T = ", font_size=36),
DecimalNumber(1.0, font_size=36)
)
temp_label.arrange(RIGHT)
temp_label.next_to(title, DOWN)
temp_tracker = ValueTracker(1.0)
temp_label[1].f_always.set_value(temp_tracker.get_value)
self.play(FadeIn(temp_label))
self.wait()
# Update function for bars
def update_chart(chart):
t = temp_tracker.get_value()
new_probs = softmax(logits, t)
for bar, p, label in zip(chart.bars, new_probs, prob_labels):
target_height = p * chart.y_axis.get_unit_size()
bar.set_height(max(target_height, 0.01), stretch=True, about_edge=DOWN)
label.set_value(p)
label.next_to(bar, UP, buff=0.1)
chart.add_updater(update_chart)
prob_labels.add_updater(lambda m: None) # Keep visible
# Vary temperature
self.play(temp_tracker.animate.set_value(0.5), run_time=3)
self.wait()
self.play(temp_tracker.animate.set_value(2.0), run_time=3)
self.wait()
self.play(temp_tracker.animate.set_value(0.1), run_time=3)
self.wait()
# Low temperature = more confident
confident_text = Text("Low T = More confident", font_size=30, color=YELLOW)
confident_text.next_to(chart, RIGHT)
self.play(FadeIn(confident_text))
self.wait()
self.play(temp_tracker.animate.set_value(5.0), run_time=3)
self.play(FadeOut(confident_text))
uniform_text = Text("High T = More uniform", font_size=30, color=YELLOW)
uniform_text.next_to(chart, RIGHT)
self.play(FadeIn(uniform_text))
self.wait(2)
@@ -0,0 +1,296 @@
"""
Solving Damped Harmonic Oscillator ODE
Demonstrates animated equation solving for the damped spring-mass system.
Shows hypothesis substitution, algebraic manipulation, and quadratic formula.
Run: manimgl solve_damped_ode.py SolveDampedODE -w
Preview: manimgl solve_damped_ode.py SolveDampedODE -p
Source: Adapted from 3b1b's Laplace transform video (2025)
"""
from manimlib import *
def get_coef_colors(n_coefs=3):
"""Generate gradient colors for position, velocity, acceleration."""
return [
interpolate_color_by_hsl(TEAL, RED, a)
for a in np.linspace(0, 1, n_coefs)
]
class SolveDampedODE(InteractiveScene):
"""
Animated walkthrough of solving x'' + μx' + kx = 0
using the exponential hypothesis x(t) = e^{st}.
Key techniques demonstrated:
- TransformMatchingTex for equation transformations
- Animated arrows between derivatives
- SurroundingRectangle for highlighting
- Brace annotations
"""
def construct(self):
# Color scheme for derivatives
colors = get_coef_colors()
# Show x, x', x'' with labels
self.show_derivative_relationship(colors)
# Show F = ma equation
self.show_force_equation(colors)
# Hypothesis: x = e^{st}
self.show_exponential_hypothesis(colors)
# Solve for s
self.solve_for_s()
def show_derivative_relationship(self, colors):
"""Show position, velocity, acceleration and their relationships."""
pos, vel, acc = funcs = VGroup(
Tex(R"x(t)"),
Tex(R"x'(t)"),
Tex(R"x''(t)"),
)
funcs.arrange(DOWN, buff=MED_LARGE_BUFF, aligned_edge=LEFT)
labels = VGroup(
Text("Position").set_color(colors[0]),
Text("Velocity").set_color(colors[1]),
Text("Acceleration").set_color(colors[2]),
)
for line, label in zip(funcs, labels):
label.next_to(line, RIGHT, MED_LARGE_BUFF)
label.align_to(labels[0], LEFT)
VGroup(funcs, labels).to_corner(UR)
# Derivative arrows between terms
arrows = VGroup()
for l1, l2 in zip(funcs, funcs[1:]):
arrow = Line(l1.get_left(), l2.get_left(), path_arc=150 * DEG, buff=0.2)
arrow.add_tip(width=0.2, length=0.2)
arrow.set_color(GREY_B)
ddt = Tex(R"\frac{d}{dt}", font_size=30)
ddt.set_color(GREY_B)
ddt.next_to(arrow, LEFT, SMALL_BUFF)
arrow.add(ddt)
arrows.add(arrow)
# Animate
self.play(Write(funcs[0]), Write(labels[0]))
self.wait()
for func1, func2, label1, label2, arrow in zip(funcs, funcs[1:], labels, labels[1:], arrows):
self.play(LaggedStart(
GrowFromPoint(arrow, arrow.get_corner(UR), path_arc=30 * DEG),
TransformFromCopy(func1, func2, path_arc=30 * DEG),
FadeTransform(label1.copy(), label2),
lag_ratio=0.1
))
self.wait()
self.deriv_group = VGroup(funcs, labels, arrows)
self.funcs = funcs
self.colors = colors
def show_force_equation(self, colors):
"""Show F = ma formulation: mx'' = -kx - μx'"""
t2c = {
"x(t)": colors[0],
"x'(t)": colors[1],
"x''(t)": colors[2],
}
equation1 = Tex(R"{m} x''(t) = -k x(t) - \mu x'(t)", t2c=t2c)
equation1.to_corner(UL)
ma = equation1["{m} x''(t)"][0]
kx = equation1["-k x(t)"][0]
mu_v = equation1[R"- \mu x'(t)"][0]
# Braces for each term
ma_brace = Brace(ma, DOWN, buff=SMALL_BUFF)
ma_brace.add(ma_brace.get_tex(R"\textbf{F}"))
kx_brace = Brace(kx, DOWN, buff=SMALL_BUFF)
kx_brace.add(kx_brace.get_tex(R"\text{Spring force}"))
mu_v_brace = Brace(mu_v, DOWN, buff=SMALL_BUFF)
mu_v_brace.add(mu_v_brace.get_tex(R"\text{Damping}"))
pos, vel, acc = self.funcs
self.play(TransformFromCopy(acc, ma[1:], path_arc=-45 * DEG))
self.play(LaggedStart(
GrowFromCenter(ma_brace),
Write(ma[0]),
run_time=1,
lag_ratio=0.1
))
self.wait()
self.play(LaggedStart(
Write(equation1["= -k"][0]),
FadeTransformPieces(ma_brace, kx_brace),
TransformFromCopy(pos, equation1["x(t)"][0], path_arc=-45 * DEG),
))
self.wait()
self.play(LaggedStart(
FadeTransformPieces(kx_brace, mu_v_brace),
Write(equation1[R"- \mu"][0]),
TransformFromCopy(vel, equation1["x'(t)"][0], path_arc=-45 * DEG),
))
self.wait()
self.play(FadeOut(mu_v_brace))
# Rearrange to standard form
equation2 = Tex(R"{m} x''(t) + \mu x'(t) + k x(t) = 0", t2c=t2c)
equation2.move_to(equation1, UL)
self.play(TransformMatchingTex(equation1, equation2, path_arc=45 * DEG))
self.wait()
self.equation = equation2
def show_exponential_hypothesis(self, colors):
"""Show guess x(t) = e^{st} and plug it in."""
t2c = {"s": YELLOW, "x(t)": TEAL}
hyp_word, hyp_tex = hypothesis = VGroup(
Text("Hypothesis: "),
Tex("x(t) = e^{st}", t2c=t2c),
)
hypothesis.arrange(RIGHT)
hypothesis.to_corner(UR)
sub_hyp = TexText(R"(For some $s$)", t2c={"$s$": YELLOW}, font_size=36, fill_color=GREY_B)
sub_hyp.next_to(hyp_tex, DOWN)
pos = self.funcs[0]
self.play(LaggedStart(
FadeTransform(pos.copy(), hyp_tex[:4], path_arc=45 * DEG, remover=True),
FadeOut(self.deriv_group),
Write(hyp_word, run_time=1),
Write(hyp_tex[4:], time_span=(0.5, 1.5)),
))
self.add(hypothesis)
self.wait()
self.play(FadeIn(sub_hyp, 0.25 * DOWN))
self.wait()
self.hypothesis = hypothesis
self.sub_hyp = sub_hyp
def solve_for_s(self):
"""Plug in hypothesis and solve the characteristic equation."""
t2c = {"s": YELLOW}
# After substitution: m s^2 e^{st} + μ s e^{st} + k e^{st} = 0
equation3 = Tex(R"{m} s^2 e^{st} + \mu s e^{st} + k e^{st} = 0", t2c=t2c)
equation3.next_to(self.equation, DOWN, LARGE_BUFF)
self.play(FadeIn(equation3, 0.5 * DOWN))
self.wait()
# Factor out e^{st}
equation4 = Tex(R"e^{st} \left( ms^2 + \mu s + k \right) = 0", t2c=t2c)
equation4.next_to(equation3, DOWN, LARGE_BUFF)
self.play(
TransformMatchingTex(
equation3.copy(),
equation4,
matched_keys=[R"e^{st}"],
run_time=1.5,
path_arc=30 * DEG
)
)
self.wait()
# Highlight e^{st} ≠ 0
exp_rect = SurroundingRectangle(equation4[R"e^{st}"])
exp_rect.set_stroke(YELLOW, 2)
ne_0 = VGroup(Tex(R"\ne").rotate(90 * DEG), Integer(0))
ne_0.arrange(DOWN).next_to(exp_rect, DOWN)
self.play(ShowCreation(exp_rect))
self.play(Write(ne_0))
self.wait()
# Characteristic equation
equation5 = Tex(R"ms^2 + \mu s + k = 0", t2c=t2c)
equation5.next_to(equation4, DOWN, LARGE_BUFF)
self.play(
FadeOut(ne_0),
FadeOut(exp_rect),
Write(equation5),
)
self.wait()
# Quadratic formula result
equation6 = Tex(R"s = {{-\mu \pm \sqrt{\mu^2 - 4mk}} \over 2m}")
equation6["s"].set_color(YELLOW)
equation6.next_to(equation5, DOWN, LARGE_BUFF)
qf_words = Text("Quadratic Formula", font_size=30, fill_color=GREY_B)
qf_words.next_to(equation6, RIGHT, MED_LARGE_BUFF)
self.play(
FadeIn(equation6, 0.5 * DOWN),
FadeIn(qf_words),
)
self.wait(2)
class SimpleODEDemo(InteractiveScene):
"""
Simpler version showing just the undamped case: x'' + ωx = 0
Results in x = e^{±iωt}, demonstrating complex exponentials.
"""
def construct(self):
# Undamped equation
equation = Tex(R"x''(t) + \omega^2 x(t) = 0", font_size=60)
equation.to_edge(UP)
self.add(equation)
self.wait()
# Hypothesis
hypothesis = Tex(R"\text{Try } x(t) = e^{st}", font_size=48)
hypothesis["s"].set_color(YELLOW)
hypothesis.next_to(equation, DOWN, LARGE_BUFF)
self.play(Write(hypothesis))
self.wait()
# Result
result = Tex(R"s^2 + \omega^2 = 0 \implies s = \pm i\omega", font_size=48)
result["s"].set_color(YELLOW)
result.next_to(hypothesis, DOWN, LARGE_BUFF)
self.play(Write(result))
self.wait()
# Solutions
solutions = Tex(
R"x(t) = c_1 e^{i\omega t} + c_2 e^{-i\omega t}",
font_size=48
)
solutions.next_to(result, DOWN, LARGE_BUFF)
self.play(Write(solutions))
self.wait()
# Box the result
box = SurroundingRectangle(solutions, buff=0.2)
box.set_stroke(TEAL, 3)
self.play(ShowCreation(box))
self.wait(2)
@@ -0,0 +1,303 @@
"""
Spring-Mass System with Live Graph
A physics simulation showing a spring-mass oscillator with real-time
position tracking on a graph. Demonstrates damped harmonic motion.
Run: manimgl spring_mass_system.py SpringMassDemo -w
Preview: manimgl spring_mass_system.py SpringMassDemo -p
Source: Adapted from 3b1b's Laplace transform video (2025)
"""
from manimlib import *
import math
class SpringMassSystem(VGroup):
"""
A reusable spring-mass system component with physics simulation.
This is a great example of 3b1b's approach: create a self-contained
VGroup subclass that handles its own physics and rendering.
"""
def __init__(
self,
x0=0, # Initial displacement from equilibrium
v0=0, # Initial velocity
k=3, # Spring constant
mu=0.1, # Damping coefficient
equilibrium_length=5, # Rest length of spring
equilibrium_position=ORIGIN,
direction=RIGHT,
spring_stroke_color=GREY_B,
spring_stroke_width=2,
spring_radius=0.25,
n_spring_curls=8,
mass_width=1.0,
mass_color=BLUE_E,
mass_label="m",
):
super().__init__()
self.equilibrium_position = equilibrium_position
self.fixed_spring_point = equilibrium_position - (equilibrium_length - 0.5 * mass_width) * direction
self.direction = direction
self.rot_off_horizontal = angle_between_vectors(RIGHT, direction)
# Create visual components
self.mass = self._create_mass(mass_width, mass_color, mass_label)
self.spring = self._create_spring(spring_stroke_color, spring_stroke_width, n_spring_curls, spring_radius)
self.add(self.spring, self.mass)
# Physics state
self.k = k
self.mu = mu
self.velocity = v0
self._is_running = True
# Set initial position
self.set_x(x0)
# Add physics updater
self.add_updater(lambda m, dt: m.time_step(dt))
def _create_spring(self, stroke_color, stroke_width, n_curls, radius):
"""Create a 3D helix spring using parametric curve."""
spring = ParametricCurve(
lambda t: [t, -radius * math.sin(TAU * t), radius * math.cos(TAU * t)],
t_range=(0, n_curls, 0.01),
stroke_color=stroke_color,
stroke_width=stroke_width,
)
spring.rotate(self.rot_off_horizontal)
return spring
def _create_mass(self, mass_width, mass_color, mass_label):
"""Create the mass block with label."""
mass = Square(mass_width)
mass.set_fill(mass_color, 1)
mass.set_stroke(WHITE, 1)
mass.set_shading(0.1, 0.1, 0.1)
label = Tex(mass_label)
label.set_max_width(0.5 * mass.get_width())
label.move_to(mass)
mass.add(label)
mass.label = label
return mass
def set_x(self, x):
"""Set displacement from equilibrium position."""
self.mass.move_to(self.equilibrium_position + x * self.direction)
# Stretch spring to connect fixed point to mass
spring_width = SMALL_BUFF + get_norm(self.mass.get_left() - self.fixed_spring_point)
self.spring.rotate(-self.rot_off_horizontal)
self.spring.set_width(spring_width, stretch=True)
self.spring.rotate(self.rot_off_horizontal)
self.spring.move_to(self.fixed_spring_point, -self.direction)
def get_x(self):
"""Get current displacement."""
return (self.mass.get_center() - self.equilibrium_position)[0]
def time_step(self, delta_t, dt_size=0.01):
"""Integrate physics using simple Euler method."""
if not self._is_running or delta_t == 0:
return
state = [self.get_x(), self.velocity]
sub_steps = max(int(delta_t / dt_size), 1)
true_dt = delta_t / sub_steps
for _ in range(sub_steps):
x, v = state
# Damped harmonic oscillator: x'' = -kx - μv
acceleration = -self.k * x - self.mu * v
state[0] += v * true_dt
state[1] += acceleration * true_dt
self.set_x(state[0])
self.velocity = state[1]
def pause(self):
self._is_running = False
def unpause(self):
self._is_running = True
def get_velocity_vector(self, scale_factor=0.5, v_offset=-0.25, color=GREEN):
"""Get a dynamic vector showing velocity."""
vector = Vector(RIGHT, fill_color=color, stroke_color=color)
v_shift = v_offset * UP
vector.add_updater(lambda m: m.put_start_and_end_on(
self.mass.get_center() + v_shift,
self.mass.get_center() + v_shift + scale_factor * self.velocity * RIGHT
))
return vector
def get_force_vector(self, scale_factor=0.5, v_offset=0.25, color=RED):
"""Get a dynamic vector showing net force."""
vector = Vector(RIGHT, fill_color=color, stroke_color=color)
v_shift = v_offset * UP
def get_force():
return -self.k * self.get_x() - self.mu * self.velocity
vector.add_updater(lambda m: m.put_start_and_end_on(
self.mass.get_center() + v_shift,
self.mass.get_center() + v_shift + scale_factor * get_force() * RIGHT
))
return vector
class SpringMassDemo(InteractiveScene):
"""
Main demonstration scene showing spring-mass oscillation.
"""
def construct(self):
# Create spring system with initial displacement
spring = SpringMassSystem(
x0=2,
mu=0.15,
k=3,
equilibrium_position=2 * LEFT,
equilibrium_length=5,
)
self.add(spring)
# Create number line to show position
number_line = NumberLine(x_range=(-4, 4, 1))
number_line.next_to(spring.equilibrium_position, DOWN, buff=2.0)
number_line.add_numbers(font_size=24)
# Arrow tip indicator on number line
arrow_tip = ArrowTip(length=0.2, width=0.1)
arrow_tip.rotate(-90 * DEG)
arrow_tip.set_fill(TEAL)
arrow_tip.add_updater(lambda m: m.move_to(number_line.n2p(spring.get_x()), DOWN))
# Let it oscillate for a moment
self.wait(2)
# Fade in tracking elements
self.play(
FadeIn(number_line),
FadeIn(arrow_tip),
)
self.wait(5)
# Add velocity vector
v_vect = spring.get_velocity_vector(color=GREEN, scale_factor=0.25)
self.play(FadeIn(v_vect))
self.wait(5)
# Add force vector
f_vect = spring.get_force_vector(color=RED, scale_factor=0.25)
self.play(FadeIn(f_vect))
self.wait(8)
class SpringWithGraph(InteractiveScene):
"""
Spring-mass system with real-time x(t) graph plotting.
Shows how position evolves over time.
"""
def construct(self):
# Create spring
spring = SpringMassSystem(
x0=2,
mu=0.2,
k=4,
equilibrium_position=3 * LEFT + DOWN,
equilibrium_length=4,
)
# Create axes for position-time graph
axes = Axes(
x_range=(0, 15, 1),
y_range=(-2.5, 2.5, 1),
width=10,
height=3,
axis_config={"stroke_color": GREY}
)
axes.next_to(spring.equilibrium_position, UP, buff=1.5)
axes.shift(RIGHT)
# Axis labels
t_label = Text("Time (t)", font_size=24)
t_label.next_to(axes.x_axis, RIGHT, buff=0.1)
x_label = Tex("x(t)", font_size=24)
x_label.next_to(axes.y_axis.get_top(), RIGHT, buff=0.1)
# Time tracker
time_tracker = ValueTracker(0)
time_tracker.add_updater(lambda m, dt: m.increment_value(dt))
# Tracking point for graph
tracking_point = Point()
tracking_point.add_updater(lambda p: p.move_to(
axes.c2p(time_tracker.get_value(), spring.get_x())
))
# Traced path creates the graph line
position_graph = TracedPath(
tracking_point.get_center,
stroke_color=BLUE,
stroke_width=3,
)
# Start paused to set up
spring.pause()
self.add(spring)
self.play(
FadeIn(axes),
Write(t_label),
Write(x_label),
)
# Start simulation and graphing
self.add(tracking_point, position_graph, time_tracker)
spring.unpause()
# Let it run and trace
self.wait(12)
class MultipleSprings(InteractiveScene):
"""
Multiple springs with different parameters side by side.
Great for comparing effects of mass, spring constant, damping.
"""
def construct(self):
# Create three springs with different damping
springs = VGroup()
damping_values = [0.0, 0.2, 0.5]
labels_text = ["No damping", "Light damping", "Heavy damping"]
colors = [BLUE, GREEN, RED]
for i, (mu, label_text, color) in enumerate(zip(damping_values, labels_text, colors)):
spring = SpringMassSystem(
x0=1.5,
mu=mu,
k=4,
equilibrium_position=4 * LEFT + (2 - i * 2) * UP,
equilibrium_length=4,
mass_color=color,
)
label = Text(label_text, font_size=24, color=color)
label.next_to(spring.mass, RIGHT, buff=2)
label.add_updater(lambda m, s=spring, t=label_text, c=color: m.become(
Text(t, font_size=24, color=c).next_to(s.mass, RIGHT, buff=2)
))
springs.add(spring)
self.add(label)
self.add(springs)
self.wait(12)
@@ -0,0 +1,136 @@
"""
Visualization of sqrt(rand()) process showing how the square root
transforms a uniform distribution.
"""
from manimlib import *
import random
import math
class Randomize(Animation):
"""Animation that randomizes a ValueTracker's value at a given frequency."""
def __init__(self, value_tracker, frequency=8, rand_func=random.random, final_value=None, **kwargs):
self.value_tracker = value_tracker
self.rand_func = rand_func
self.frequency = frequency
self.final_value = final_value if final_value is not None else rand_func()
self.last_alpha = 0
self.running_tally = 0
super().__init__(value_tracker, **kwargs)
def interpolate_mobject(self, alpha):
if not self.new_step(alpha):
return
value = self.rand_func() if alpha < 1 else self.final_value
self.value_tracker.set_value(value)
def new_step(self, alpha):
d_alpha = alpha - self.last_alpha
self.last_alpha = alpha
self.running_tally += self.frequency * d_alpha * self.run_time
if self.running_tally > 1:
self.running_tally = self.running_tally % 1
return True
return False
class TrackingDots(Animation):
"""Animation that leaves a trail of fading dots at specified positions."""
def __init__(self, point_func, fade_factor=0.95, radius=0.25, color=YELLOW, **kwargs):
self.point_func = point_func
self.fade_factor = fade_factor
self.dots = GlowDot(point_func(), color=color, radius=radius)
kwargs.update(remover=True)
super().__init__(self.dots, **kwargs)
def interpolate_mobject(self, alpha):
opacities = self.dots.get_opacities()
point = self.point_func()
if not np.isclose(self.dots.get_end(), point).all():
self.dots.add_point(point)
opacities = np.hstack([opacities, [1]])
opacities *= self.fade_factor
self.dots.set_opacity(opacities)
def get_random_var_label_group(axis, label_name, color=GREY, initial_value=None, font_size=36, direction=None):
"""Create a group with a tracker, arrow tip indicator, and label for a random variable on an axis."""
if initial_value is None:
initial_value = random.uniform(*axis.x_range[:2])
tracker = ValueTracker(initial_value)
tip = ArrowTip(angle=90 * DEGREES)
tip.set_height(0.15)
tip.set_fill(color)
tip.rotate(-axis.get_angle())
if direction is None:
direction = np.round(rotate_vector(UP, -axis.get_angle()), 1)
tip.add_updater(lambda m: m.move_to(axis.n2p(tracker.get_value()), direction))
label = Tex(label_name, font_size=font_size)
label.set_color(color)
label.set_backstroke(BLACK, 5)
label.always.next_to(tip, -direction, buff=0.1)
return Group(tracker, tip, label)
class SqrtRandomProcess(InteractiveScene):
"""
Visualizes the sqrt(rand()) process.
Shows two intervals:
- x = rand() (blue)
- sqrt(x) (teal)
Demonstrates that sqrt(rand()) has the same distribution as max(rand(), rand()).
"""
def construct(self):
# Set up intervals
intervals = VGroup(UnitInterval() for _ in range(2))
intervals.set_width(3)
intervals.arrange(DOWN, buff=3.5)
intervals.shift(2 * LEFT)
for interval in intervals:
interval.add_numbers(np.arange(0, 1.1, 0.2), font_size=16, buff=0.1, direction=UP)
interval.numbers.set_opacity(0.75)
colors = [BLUE, TEAL]
x_group, sqrt_group = groups = Group(
get_random_var_label_group(interval, "", color=color)
for interval, color in zip(intervals, colors)
)
x_tracker, x_tip, x_label = x_group
sqrt_tracker, sqrt_tip, sqrt_label = sqrt_group
sqrt_tracker.add_updater(lambda m: m.set_value(math.sqrt(x_tracker.get_value())))
self.add(intervals)
self.add(groups)
# Add labels
tex_to_color = {"x": BLUE}
labels = VGroup(
Tex(tex + R"\rightarrow 0.00", t2c=tex_to_color)
for tex in [
R"x = \text{rand}()",
R"\sqrt{x}",
]
)
for label, group, interval in zip(labels, groups, intervals):
label.next_to(interval, RIGHT, buff=0.5)
num = label.make_number_changeable("0.00")
num.tracker = group[0]
num.add_updater(lambda m: m.set_value(m.tracker.get_value()))
self.add(labels)
# Big arrow
arrow = Arrow(*intervals, buff=0.5, thickness=5)
label = Text(R"sqrt", font_size=60)
label.next_to(arrow, RIGHT)
self.add(arrow, label)
# Animate the random process
self.play(
Randomize(x_tracker, frequency=4, run_time=15),
TrackingDots(x_tip.get_top, color=colors[0]),
TrackingDots(sqrt_tip.get_top, color=colors[1]),
)
@@ -0,0 +1,290 @@
"""
Superposition Effect Visualization
==================================
Creates a visual "superposition" effect where multiple quantum states
appear to exist simultaneously with a glowing, oscillating appearance.
Key concepts demonstrated:
- Custom Group subclass with updaters
- ValueTracker for controlling animation parameters
- Glow effects using replicated objects with varying stroke widths
- Continuous animation with add_updater
"""
from manimlib import *
class Superposition(Group):
"""
A visual effect that makes multiple pieces appear to be in superposition.
The pieces jitter/oscillate around their center positions with a glowing
effect, simulating the uncertainty of a quantum superposition.
"""
def __init__(
self,
pieces,
offset_multiple=0.2,
max_rot_vel=3,
glow_color=TEAL,
glow_stroke_range=(1, 22, 4),
glow_stroke_opacity=0.05
):
self.pieces = pieces
self.center_points = Group(
Point(piece.get_center())
for piece in pieces
)
self.offset_multiplier = ValueTracker(offset_multiple)
# Initialize each piece with random offset and rotation velocity
for piece, point_mob in zip(pieces, self.center_points):
piece.center_point = point_mob
piece.offset_vect = rotate_vector(RIGHT, np.random.uniform(0, TAU))
piece.offset_vect_rot_vel = np.random.uniform(-max_rot_vel, max_rot_vel)
# Create glow layers with varying stroke widths
glow_strokes = np.arange(*glow_stroke_range)
glows = pieces.replicate(len(glow_strokes))
glows.set_fill(opacity=0)
glows.set_joint_type('no_joint')
for glow, sw in zip(glows, glow_strokes):
glow.set_stroke(glow_color, width=float(sw), opacity=glow_stroke_opacity)
self.glows = glows
super().__init__(glows, pieces, self.center_points, self.offset_multiplier)
self.add_updater(lambda m, dt: m.update_piece_positions(dt))
def update_piece_positions(self, dt):
"""Update positions with oscillating motion."""
offset_multiple = self.offset_multiplier.get_value()
for piece in self.pieces:
piece.offset_vect = rotate_vector(
piece.offset_vect,
dt * piece.offset_vect_rot_vel
)
piece.offset_radius = offset_multiple
piece.move_to(
piece.center_point.get_center() +
piece.offset_radius * piece.offset_vect
)
# Update glow positions to match pieces
for glow in self.glows:
for sm1, sm2 in zip(
glow.family_members_with_points(),
self.pieces.family_members_with_points()
):
sm1.match_points(sm2)
def set_offset_multiple(self, value):
"""Control the amount of jitter."""
self.offset_multiplier.set_value(value)
return self
def set_glow_opacity(self, opacity=0.1):
"""Control the glow intensity."""
self.glows.set_stroke(opacity=opacity)
return self
class SuperpositionDemo(InteractiveScene):
"""Demonstrates the superposition visual effect."""
def construct(self):
# Title
title = Text("Quantum Superposition", font_size=60)
title.to_edge(UP)
self.add(title)
# Create bit strings representing possible quantum states
def create_bit_string(value, length=4):
"""Create a visual bit string like |0101>"""
bits = bin(value)[2:].zfill(length)
bit_mobs = VGroup(
Integer(int(b)) for b in bits
)
bit_mobs.arrange(RIGHT, buff=0.1)
return bit_mobs
# Create ket notation
def create_ket(value, length=4):
bits = create_bit_string(value, length)
ket = VGroup(
Tex(R"|"),
bits,
Tex(R"\rangle")
)
ket[0].next_to(bits, LEFT, buff=0.05)
ket[2].next_to(bits, RIGHT, buff=0.05)
return ket
# Create multiple states
states = VGroup(
create_ket(n, 4)
for n in range(16)
)
states.arrange(DOWN, buff=0.2)
states.set_height(5)
states.center()
# Create superposition effect
superposition = Superposition(states, offset_multiple=0, glow_stroke_opacity=0)
superposition.update()
self.add(superposition)
# Animate the superposition emerging
self.play(
superposition.animate.set_offset_multiple(0.15).set_glow_opacity(0.08),
run_time=2
)
# Let it oscillate
self.wait(5)
# Collapse to a single state (measurement)
measurement_label = Text("Measurement", font_size=36, color=RED)
measurement_label.next_to(superposition, RIGHT, buff=1.0)
self.play(Write(measurement_label))
self.play(
Flash(states[7].get_center(), color=WHITE),
run_time=0.3
)
# Collapse effect
self.play(
superposition.animate.set_offset_multiple(0).set_glow_opacity(0),
run_time=0.5
)
# Highlight the measured state
rect = SurroundingRectangle(states[7], buff=0.1, color=YELLOW)
result_label = Text("Result: |0111>", font_size=36, color=YELLOW)
result_label.next_to(superposition, DOWN, buff=0.5)
self.play(
ShowCreation(rect),
FadeIn(result_label)
)
self.wait(2)
class BitStringVisualization(InteractiveScene):
"""Shows bit strings with ket notation."""
def construct(self):
# Create a grid of possible 4-qubit states
def create_ket(value, length=4):
bits_str = bin(value)[2:].zfill(length)
tex = Tex(
R"|" + bits_str + R"\rangle",
font_size=36
)
return tex
# Create grid
states = VGroup(
create_ket(n, 4)
for n in range(16)
)
states.arrange_in_grid(4, 4, buff=0.5)
states.center()
# Title
title = Text("4-Qubit Computational Basis States", font_size=48)
title.to_edge(UP)
self.add(title)
self.play(
LaggedStartMap(FadeIn, states, lag_ratio=0.1),
run_time=3
)
self.wait()
# Highlight the pattern: powers of 2
# |0000> = 0, |0001> = 1, |0010> = 2, etc.
decimal_labels = VGroup()
for i, state in enumerate(states):
label = Integer(i, font_size=24, color=YELLOW)
label.next_to(state, DOWN, SMALL_BUFF)
decimal_labels.add(label)
self.play(
LaggedStartMap(FadeIn, decimal_labels, lag_ratio=0.05),
run_time=2
)
self.wait(2)
class QuantumParallelism(InteractiveScene):
"""Visualizes the concept of quantum parallelism."""
def construct(self):
# Classical vs Quantum comparison
classical_title = Text("Classical", font_size=36)
quantum_title = Text("Quantum", font_size=36)
titles = VGroup(classical_title, quantum_title)
titles.arrange(RIGHT, buff=4)
titles.to_edge(UP, buff=1.0)
v_line = Line(UP, DOWN).set_height(5)
v_line.set_stroke(WHITE, 1)
self.add(titles, v_line)
# Classical: one input at a time
classical_inputs = VGroup(
Tex(R"|" + bin(n)[2:].zfill(4) + R"\rangle", font_size=30)
for n in range(8)
)
classical_inputs.arrange(DOWN, buff=0.2)
classical_inputs.next_to(classical_title, DOWN, buff=0.5)
# Quantum: superposition of all inputs
quantum_pieces = VGroup(
Tex(R"|" + bin(n)[2:].zfill(4) + R"\rangle", font_size=30)
for n in range(8)
)
quantum_pieces.arrange(DOWN, buff=0.2)
quantum_pieces.next_to(quantum_title, DOWN, buff=0.5)
# Create superposition effect for quantum side
superposition = Superposition(
quantum_pieces.copy(),
offset_multiple=0.1,
glow_color=TEAL
)
superposition.move_to(quantum_pieces)
# Classical: process one at a time
self.play(FadeIn(classical_inputs[0]))
for i in range(1, 4):
self.play(
classical_inputs[i - 1].animate.set_opacity(0.3),
FadeIn(classical_inputs[i])
)
# Show dots to indicate continuation
dots = Tex(R"\vdots", font_size=48)
dots.next_to(classical_inputs[3], DOWN)
self.play(FadeIn(dots))
# Quantum: all at once
quantum_label = Text("All states\nsimultaneously!", font_size=24, color=TEAL)
quantum_label.next_to(superposition, DOWN, buff=0.3)
self.play(FadeIn(superposition))
self.play(Write(quantum_label))
self.wait(5)
if __name__ == "__main__":
# To run: manimgl superposition_effect.py SuperpositionDemo
pass
@@ -0,0 +1,259 @@
"""
3D Surfaces and Camera Movement
Demonstrates 3D surface creation, parametric surfaces,
and camera manipulation in ManimGL.
Run: manimgl three_d_surfaces.py ParametricSurface3D -w
Preview: manimgl three_d_surfaces.py ParametricSurface3D -p
Source: Inspired by 3b1b's 3D visualizations
"""
from manimlib import *
import numpy as np
class ParametricSurface3D(InteractiveScene):
"""
Creates a beautiful 3D parametric surface with camera rotation.
"""
def construct(self):
frame = self.frame
# Create 3D axes
axes = ThreeDAxes(
x_range=(-3, 3, 1),
y_range=(-3, 3, 1),
z_range=(-2, 2, 1),
)
# Parametric surface: z = sin(x) * cos(y)
surface = ParametricSurface(
lambda u, v: [u, v, np.sin(u) * np.cos(v)],
u_range=(-3, 3),
v_range=(-3, 3),
resolution=(30, 30),
)
# Color by z value
surface.set_color(BLUE)
surface.set_opacity(0.8)
self.add(axes)
# Rotate camera to good initial position
frame.reorient(-30, 70, 0)
frame.set_height(10)
# Create surface
self.play(ShowCreation(surface, run_time=3))
self.wait()
# Rotate camera around
self.play(
frame.animate.reorient(30, 60, 0),
run_time=3
)
self.play(
frame.animate.reorient(-60, 80, 0),
run_time=3
)
self.wait()
class SphereSurface(InteractiveScene):
"""
Creates a sphere and demonstrates 3D transformations.
"""
def construct(self):
frame = self.frame
frame.reorient(-20, 70, 0)
# Create sphere
sphere = Sphere(radius=2)
sphere.set_color(BLUE)
sphere.set_opacity(0.7)
# Create latitude/longitude lines
lat_lines = VGroup(*[
ParametricCurve(
lambda t, phi=phi: 2 * np.array([
np.cos(t) * np.cos(phi),
np.sin(t) * np.cos(phi),
np.sin(phi)
]),
t_range=(0, TAU, 0.1),
color=WHITE,
stroke_width=1,
stroke_opacity=0.5,
)
for phi in np.linspace(-PI/2 + 0.3, PI/2 - 0.3, 6)
])
long_lines = VGroup(*[
ParametricCurve(
lambda t, theta=theta: 2 * np.array([
np.cos(theta) * np.cos(t),
np.sin(theta) * np.cos(t),
np.sin(t)
]),
t_range=(-PI/2, PI/2, 0.1),
color=WHITE,
stroke_width=1,
stroke_opacity=0.5,
)
for theta in np.linspace(0, TAU, 12, endpoint=False)
])
self.play(ShowCreation(sphere))
self.play(
ShowCreation(lat_lines, run_time=2),
ShowCreation(long_lines, run_time=2),
)
# Rotate
self.play(
Rotate(sphere, TAU, axis=OUT, run_time=4),
Rotate(lat_lines, TAU, axis=OUT, run_time=4),
Rotate(long_lines, TAU, axis=OUT, run_time=4),
)
self.wait()
class ConeUnfolding(InteractiveScene):
"""
A cone that unfolds into a flat sector.
Demonstrates surface transformation.
"""
def construct(self):
frame = self.frame
frame.reorient(-30, 70, 0)
frame.set_height(8)
# Create cone
height = 3
radius = 2
cone = ParametricSurface(
lambda u, v: [
v * radius / height * np.cos(u),
v * radius / height * np.sin(u),
height - v
],
u_range=(0, TAU),
v_range=(0, height),
resolution=(30, 10),
)
cone.set_color(BLUE_E)
cone.set_opacity(0.8)
self.play(ShowCreation(cone, run_time=2))
self.wait()
# Animate camera
self.play(
frame.animate.reorient(0, 0, 0).set_height(10),
run_time=2
)
self.wait()
class SaddleSurface(InteractiveScene):
"""
Hyperbolic paraboloid (saddle surface).
Classic example of negative Gaussian curvature.
"""
def construct(self):
frame = self.frame
frame.reorient(-40, 70, 0)
# Create saddle: z = x^2 - y^2
surface = ParametricSurface(
lambda u, v: [u, v, 0.3 * (u**2 - v**2)],
u_range=(-2, 2),
v_range=(-2, 2),
resolution=(20, 20),
)
# Color gradient based on z
surface.set_color(BLUE)
surface.set_opacity(0.9)
# Axes
axes = ThreeDAxes(
x_range=(-3, 3, 1),
y_range=(-3, 3, 1),
z_range=(-2, 2, 1),
)
self.play(ShowCreation(axes))
self.play(ShowCreation(surface, run_time=2))
# Show cross sections
x_section = ParametricCurve(
lambda t: [t, 0, 0.3 * t**2],
t_range=(-2, 2, 0.1),
color=RED,
stroke_width=4,
)
y_section = ParametricCurve(
lambda t: [0, t, -0.3 * t**2],
t_range=(-2, 2, 0.1),
color=BLUE,
stroke_width=4,
)
self.play(ShowCreation(x_section))
self.play(ShowCreation(y_section))
# Rotate view
self.play(
frame.animate.reorient(60, 60, 0),
run_time=4
)
self.wait()
class TorusSurface(InteractiveScene):
"""
Creates a torus (donut shape).
Classic example of parametric surface.
"""
def construct(self):
frame = self.frame
frame.reorient(-30, 70, 0)
# Torus parameters
R = 2 # Major radius
r = 0.7 # Minor radius
torus = ParametricSurface(
lambda u, v: [
(R + r * np.cos(v)) * np.cos(u),
(R + r * np.cos(v)) * np.sin(u),
r * np.sin(v)
],
u_range=(0, TAU),
v_range=(0, TAU),
resolution=(40, 20),
)
torus.set_color(BLUE_D)
torus.set_opacity(0.8)
self.play(ShowCreation(torus, run_time=3))
# Rotate the torus
self.play(
Rotate(torus, TAU, axis=UP, run_time=6, rate_func=linear),
)
# Camera orbit
self.play(
frame.animate.reorient(150, 50, 0),
run_time=4
)
self.wait()
@@ -0,0 +1,99 @@
"""
3D Vector Space Example
Demonstrates how coordinates create a point in 3D space with animated construction.
Based on: videos/_2024/transformers/embedding.py - ThreeDSpaceExample
"""
from manimlib import *
class ThreeDVectorSpace(InteractiveScene):
"""
Visualizes how 3D coordinates define a point in space.
Shows step-by-step construction along x, y, z axes.
"""
def construct(self):
# Set up 3D frame and axes
frame = self.frame
frame.reorient(-15, 78, 0, (1.07, 1.71, 1.41), 6.72)
frame.add_ambient_rotation(1 * DEGREES)
axes = ThreeDAxes((-5, 5), (-5, 5), (-4, 4))
plane = NumberPlane((-5, 5), (-5, 5))
plane.set_stroke(opacity=0.5)
self.add(plane)
self.add(axes)
# Target coordinates
x, y, z = coordinates = np.array([3, 1, 2])
colors = [RED, GREEN, BLUE]
# Create coordinate display (fixed in frame)
coords = DecimalMatrix(np.zeros((3, 1)), num_decimal_places=1)
coords.fix_in_frame()
coords.to_corner(UR)
coords.shift(1.5 * LEFT)
coords.get_entries().set_submobject_colors_by_gradient(*colors)
# Create path lines for x, y, z components
lines = VGroup(
Line(axes.c2p(0, 0, 0), axes.c2p(x, 0, 0)),
Line(axes.c2p(x, 0, 0), axes.c2p(x, y, 0)),
Line(axes.c2p(x, y, 0), axes.c2p(x, y, z)),
)
lines.set_flat_stroke(False)
lines.set_submobject_colors_by_gradient(*colors)
# Create axis labels
labels = VGroup(*map(Tex, "xyz"))
labels.rotate(89 * DEGREES, RIGHT)
directions = [OUT, OUT + RIGHT, RIGHT]
for label, line, direction in zip(labels, lines, directions):
label.next_to(line, direction, buff=SMALL_BUFF)
label.match_color(line)
# Glowing dot to track position
dot = GlowDot(color=WHITE)
dot.move_to(axes.get_origin())
# Final vector arrow
vect = Arrow(axes.get_origin(), axes.c2p(x, y, z), buff=0)
vect.set_flat_stroke(False)
# Show coordinate matrix
self.add(coords)
# Animate building the vector step by step
for entry, line, label, value in zip(coords.get_entries(), lines, labels, coordinates):
rect = SurroundingRectangle(entry)
rect.set_fill(line.get_color(), opacity=0.3)
rect.set_stroke(line.get_color(), width=2, opacity=1.0)
self.play(
ShowCreation(line),
FadeInFromPoint(label, line.get_start()),
FadeIn(rect, rate_func=there_and_back),
ChangeDecimalToValue(entry, value),
dot.animate.move_to(line.get_end()),
)
self.wait(0.5)
# Show the complete vector
self.play(ShowCreation(vect))
self.wait(3)
# Show many random points
points = GlowDots(np.random.uniform(-3, 3, size=(50, 3)), radius=0.1)
frame.clear_updaters()
self.play(
FadeOut(coords),
FadeOut(dot),
FadeOut(plane),
LaggedStartMap(FadeOut, VGroup(*lines, vect, *labels)),
frame.animate.reorient(-81, 61, 0, (-0.82, 0.6, 0.36), 8.95),
ShowCreation(points),
run_time=2,
)
frame.add_ambient_rotation(5 * DEGREES)
self.wait(5)
@@ -0,0 +1,191 @@
"""
Token Embeddings Flow Visualization
Shows tokens being converted to embeddings and then updated through attention.
"""
from manimlib import *
import numpy as np
def value_to_color(
value,
low_positive_color=BLUE_E,
high_positive_color=BLUE_B,
low_negative_color=RED_E,
high_negative_color=RED_B,
min_value=0.0,
max_value=10.0
):
"""Map a value to a color based on sign and magnitude."""
alpha = clip(float(inverse_interpolate(min_value, max_value, abs(value))), 0, 1)
if value >= 0:
return interpolate_color(low_positive_color, high_positive_color, alpha)
else:
return interpolate_color(low_negative_color, high_negative_color, alpha)
class NumericEmbedding(DecimalMatrix):
"""A column vector (embedding) with color-coded entries."""
def __init__(
self,
values=None,
length=7,
value_range=(-9.9, 9.9),
ellipses_row=-2,
num_decimal_places=1,
bracket_h_buff=0.1,
**kwargs
):
if values is None:
values = np.random.uniform(*value_range, size=(length, 1))
elif len(values.shape) == 1:
values = values.reshape((values.shape[0], 1))
self.value_range = value_range
super().__init__(
values,
num_decimal_places=num_decimal_places,
bracket_h_buff=bracket_h_buff,
decimal_config=dict(include_sign=True),
ellipses_row=ellipses_row,
ellipses_col=None,
)
self.reset_entry_colors()
def reset_entry_colors(self):
for entry in self.get_entries():
entry.set_fill(color=value_to_color(
entry.get_value(),
low_positive_color=GREY_C,
high_positive_color=WHITE,
low_negative_color=GREY_C,
high_negative_color=WHITE,
min_value=0,
max_value=max(self.value_range),
))
return self
class TokenEmbeddingsFlow(InteractiveScene):
def construct(self):
# Create sentence
phrase = "a fluffy blue creature"
phrase_mob = Text(phrase, font_size=42)
phrase_mob.to_edge(UP, buff=1)
words = phrase.split()
word_mobs = VGroup()
for word in words:
word_mob = phrase_mob[word][0]
word_mobs.add(word_mob)
self.play(
LaggedStartMap(FadeIn, word_mobs, shift=0.5 * UP, lag_ratio=0.15)
)
self.wait()
# Create colored rectangles around words
colors = [GREY, TEAL, BLUE, ORANGE]
rects = VGroup()
for word_mob, color in zip(word_mobs, colors):
rect = SurroundingRectangle(word_mob, buff=0.1)
rect.set_stroke(color, 2)
rect.set_fill(color, 0.2)
rects.add(rect)
self.play(LaggedStartMap(DrawBorderThenFill, rects, lag_ratio=0.1))
self.wait()
# Create embeddings below each word
np.random.seed(42)
embeddings = VGroup(
NumericEmbedding(length=8).set_height(2.5)
for _ in word_mobs
)
embeddings.arrange(RIGHT, buff=0.6)
embeddings.next_to(rects, DOWN, buff=1.5)
# Arrows from words to embeddings
arrows = VGroup(
Arrow(rect.get_bottom(), emb.get_top(), buff=0.15)
for rect, emb in zip(rects, embeddings)
)
# Labels for embeddings
e_template = Tex(R"\vec{\textbf{E}}_0", font_size=36)
e_subscript = e_template.make_number_changeable("0")
e_labels = VGroup()
for n, emb in enumerate(embeddings, start=1):
e_subscript.set_value(n)
label = e_template.copy()
label.set_color(GREY_A)
label.next_to(emb, DOWN, buff=0.3)
e_labels.add(label)
self.play(
LaggedStartMap(GrowArrow, arrows, lag_ratio=0.1),
LaggedStartMap(FadeIn, embeddings, shift=0.5 * DOWN, lag_ratio=0.1),
LaggedStartMap(FadeIn, e_labels, shift=0.2 * DOWN, lag_ratio=0.1),
run_time=2
)
self.wait()
# Show attention arrows (adjectives -> noun)
# fluffy -> creature, blue -> creature
attention_arrows = VGroup(
Arrow(
embeddings[1].get_top() + 0.3 * UP,
embeddings[3].get_top() + 0.3 * UP,
path_arc=-120 * DEGREES,
buff=0.1
).set_stroke(TEAL, 3),
Arrow(
embeddings[2].get_top() + 0.3 * UP,
embeddings[3].get_top() + 0.3 * UP,
path_arc=-90 * DEGREES,
buff=0.1
).set_stroke(BLUE, 3),
)
attention_label = Text("Attention", font_size=30)
attention_label.next_to(attention_arrows, UP, buff=0.2)
attention_label.set_color(YELLOW)
self.play(
LaggedStartMap(ShowCreation, attention_arrows, lag_ratio=0.3),
FadeIn(attention_label, shift=0.2 * DOWN),
run_time=1.5
)
self.wait()
# Show updated embedding for "creature"
updated_emb = embeddings[3].copy()
updated_emb.set_color(YELLOW)
prime = Tex("'", font_size=48)
prime.next_to(e_labels[3], RIGHT, buff=0)
prime.shift(0.1 * UL)
prime.set_color(YELLOW)
update_label = Text("Updated with context!", font_size=28)
update_label.set_color(YELLOW)
update_label.next_to(embeddings[3], RIGHT, buff=0.5)
# Animate the update
self.play(
embeddings[3].animate.set_color(YELLOW),
FadeIn(prime),
Write(update_label),
run_time=1.5
)
self.wait()
# Final message
final_message = Text(
"Now 'creature' knows about 'fluffy' and 'blue'",
font_size=32
)
final_message.to_edge(DOWN)
self.play(Write(final_message))
self.wait(2)
@@ -0,0 +1,192 @@
"""
Token Probability Distribution Visualization
Demonstrates how to visualize next-token probability distributions
as animated bar charts - a key component of autoregressive generation.
Run with: manimgl token_probability_distribution.py TokenProbabilityDistribution
"""
from manimlib import *
import numpy as np
def get_paragraph(words, line_len=40, font_size=48):
"""Handle word wrapping for text display."""
words = list(map(str.strip, words))
word_lens = list(map(len, words))
lines = []
lh, rh = 0, 0
while rh < len(words):
rh += 1
if sum(word_lens[lh:rh]) > line_len:
rh -= 1
lines.append(words[lh:rh])
lh = rh
lines.append(words[lh:])
text = "\n".join([" ".join(line).strip() for line in lines])
return Text(text, alignment="LEFT", font_size=font_size)
class TokenProbabilityDistribution(InteractiveScene):
"""
Visualizes a probability distribution over next tokens.
Shows how language models output probabilities for each possible next word.
"""
def construct(self):
# Sample predictions with probabilities
predictions = [" habitat", " environment", " forest", " home", " land", " world", " area"]
probs = np.array([0.35, 0.25, 0.15, 0.10, 0.08, 0.05, 0.02])
# Input context
context = "Behold, a wild pi creature, foraging in its native"
context_mob = get_paragraph(context.split(" "), line_len=35, font_size=36)
context_mob.to_edge(UP, buff=0.5)
context_mob.set_color(BLUE_B)
# Next word indicator
next_word_line = Underline(context_mob[-6:])
next_word_line.set_stroke(TEAL, 2)
next_word_line.next_to(context_mob[-1], RIGHT, SMALL_BUFF, aligned_edge=DOWN)
# Build the distribution visualization
bar_groups = self.build_distribution(predictions, probs)
bar_groups.next_to(context_mob, DOWN, buff=1.0)
bar_groups.shift(RIGHT)
# Title
title = Text("Next Token Probabilities", font_size=42)
title.to_edge(UP, buff=0.1)
title.set_color(YELLOW)
# Animate
self.play(Write(title))
self.play(
FadeIn(context_mob, lag_ratio=0.02),
ShowCreation(next_word_line),
)
self.wait(0.5)
# Animate bars appearing
self.play(
LaggedStart(
*(FadeIn(bg, shift=LEFT) for bg in bar_groups),
lag_ratio=0.1,
run_time=2
)
)
self.wait()
# Highlight top prediction
highlight = SurroundingRectangle(bar_groups[0], buff=0.05)
highlight.set_stroke(YELLOW, 3)
highlight.set_fill(YELLOW, 0.2)
self.play(ShowCreation(highlight))
self.wait()
# Show that probabilities sum to 1
sum_label = Tex(R"\sum P = 1", font_size=36)
sum_label.next_to(bar_groups, RIGHT, buff=0.5)
self.play(Write(sum_label))
self.wait(2)
def build_distribution(
self,
words,
probs,
font_size=24,
width_100p=3.0,
bar_height=0.3
):
"""Build bar chart visualization of token probabilities."""
labels = VGroup(*(Text(word, font_size=font_size) for word in words))
bars = VGroup(*(
Rectangle(prob * width_100p, bar_height)
for prob in probs
))
bars.arrange(DOWN, aligned_edge=LEFT, buff=0.4 * bar_height)
bars.set_fill(opacity=1)
bars.set_submobject_colors_by_gradient(TEAL, YELLOW)
bars.set_stroke(WHITE, 1)
bar_groups = VGroup()
for label, bar, prob in zip(labels, bars, probs):
prob_label = Integer(int(100 * prob), unit="%", font_size=0.75 * font_size)
prob_label.next_to(bar, RIGHT, buff=SMALL_BUFF)
label.next_to(bar, LEFT)
bar_groups.add(VGroup(label, bar, prob_label))
# Add ellipsis to indicate more tokens
ellipses = Tex(R"\vdots", font_size=font_size)
ellipses.next_to(bar_groups[-1][0], DOWN)
bar_groups.add(ellipses)
return bar_groups
class AnimatedDistributionBars(InteractiveScene):
"""
Shows probability distribution bars animating as context changes.
Demonstrates how the distribution shifts based on input.
"""
def construct(self):
# Two different contexts
context1 = "The cat sat on the"
context2 = "The astronaut floated in"
# Different probability distributions for each context
predictions1 = [" mat", " floor", " chair", " bed", " couch"]
probs1 = np.array([0.40, 0.25, 0.15, 0.12, 0.08])
predictions2 = [" space", " air", " void", " capsule", " orbit"]
probs2 = np.array([0.45, 0.20, 0.18, 0.10, 0.07])
# Create context displays
ctx1_mob = Text(context1, font_size=32)
ctx1_mob.to_edge(UP, buff=1.0)
ctx1_mob.set_color(BLUE_B)
# Build first distribution
bar_groups1 = self.build_simple_distribution(predictions1, probs1)
bar_groups1.center()
bar_groups1.shift(0.5 * DOWN)
# Show first context and distribution
self.play(Write(ctx1_mob))
self.play(FadeIn(bar_groups1, lag_ratio=0.1))
self.wait()
# Transform to second context
ctx2_mob = Text(context2, font_size=32)
ctx2_mob.to_edge(UP, buff=1.0)
ctx2_mob.set_color(GREEN_B)
bar_groups2 = self.build_simple_distribution(predictions2, probs2)
bar_groups2.center()
bar_groups2.shift(0.5 * DOWN)
self.play(
ReplacementTransform(ctx1_mob, ctx2_mob),
ReplacementTransform(bar_groups1, bar_groups2),
run_time=2
)
self.wait(2)
def build_simple_distribution(self, words, probs, font_size=28, width_100p=4.0, bar_height=0.4):
"""Build a simple bar chart for probabilities."""
bar_groups = VGroup()
for word, prob in zip(words, probs):
label = Text(word, font_size=font_size)
bar = Rectangle(prob * width_100p, bar_height)
bar.set_fill(interpolate_color(RED, GREEN, prob), opacity=0.8)
bar.set_stroke(WHITE, 1)
prob_label = Integer(int(100 * prob), unit="%", font_size=font_size * 0.8)
prob_label.next_to(bar, RIGHT, buff=SMALL_BUFF)
label.next_to(bar, LEFT)
bar_groups.add(VGroup(label, bar, prob_label))
bar_groups.arrange(DOWN, aligned_edge=LEFT, buff=0.3)
return bar_groups
@@ -0,0 +1,177 @@
"""
Token Sampling Animation
Demonstrates the random sampling process used in autoregressive generation,
where the next token is sampled from the probability distribution.
Run with: manimgl token_sampling.py TokenSamplingAnimation
"""
from manimlib import *
import numpy as np
import random
class TokenSamplingAnimation(InteractiveScene):
"""
Shows how tokens are randomly sampled from a probability distribution.
The highlight rectangle bounces between options before settling.
"""
def construct(self):
# Title
title = Text("Sampling from Distribution", font_size=36)
title.to_edge(UP, buff=0.5)
self.play(Write(title))
# Create distribution
words = [" habitat", " environment", " forest", " home", " land"]
probs = np.array([0.35, 0.28, 0.20, 0.12, 0.05])
probs = probs / probs.sum() # Normalize
bar_groups = self.build_distribution(words, probs)
bar_groups.center()
bar_groups.shift(0.5 * DOWN)
self.play(FadeIn(bar_groups, lag_ratio=0.1))
self.wait(0.5)
# Create highlight rectangle
highlight = SurroundingRectangle(bar_groups[0], buff=0.05)
highlight.set_stroke(YELLOW, 3)
highlight.set_fill(YELLOW, 0.25)
# Animate random sampling
seed = random.randint(0, 1000)
def highlight_randomly(rect, alpha):
np.random.seed(seed + int(15 * alpha))
index = np.random.choice(len(words), p=probs)
rect.surround(bar_groups[index], buff=0.05)
rect.stretch(1.05, 0)
self.play(FadeIn(highlight))
self.play(
UpdateFromAlphaFunc(
highlight,
lambda r, a: highlight_randomly(r, a)
),
run_time=2.5,
rate_func=linear
)
# Final selection
final_index = np.random.choice(len(words), p=probs)
final_highlight = SurroundingRectangle(bar_groups[final_index], buff=0.05)
final_highlight.set_stroke(GREEN, 4)
final_highlight.set_fill(GREEN, 0.3)
self.play(Transform(highlight, final_highlight))
# Show selected word
selected_word = Text(words[final_index].strip(), font_size=48)
selected_word.set_color(GREEN)
selected_word.next_to(bar_groups, RIGHT, buff=1.0)
selected_label = Text("Selected:", font_size=28)
selected_label.next_to(selected_word, UP)
self.play(
Write(selected_label),
FadeIn(selected_word, scale=1.5)
)
self.wait(2)
def build_distribution(self, words, probs, font_size=28, width_100p=4.0, bar_height=0.35):
"""Build bar chart visualization."""
bar_groups = VGroup()
for word, prob in zip(words, probs):
label = Text(word, font_size=font_size)
bar = Rectangle(prob * width_100p, bar_height)
bar.set_fill(interpolate_color(BLUE_E, TEAL, prob / max(probs)), opacity=0.9)
bar.set_stroke(WHITE, 1)
prob_label = Integer(int(100 * prob), unit="%", font_size=font_size * 0.8)
prob_label.next_to(bar, RIGHT, buff=SMALL_BUFF)
label.next_to(bar, LEFT)
bar_groups.add(VGroup(label, bar, prob_label))
bar_groups.arrange(DOWN, aligned_edge=LEFT, buff=0.25)
return bar_groups
class TemperatureSampling(InteractiveScene):
"""
Demonstrates how temperature affects the sampling distribution.
Higher temperature = more uniform, lower = more peaked.
"""
def construct(self):
def softmax(logits, temperature=1.0):
"""Compute softmax with temperature scaling."""
logits = np.array(logits, dtype=float)
logits = logits - np.max(logits)
if temperature == 0:
result = np.zeros_like(logits)
result[np.argmax(logits)] = 1.0
return result
exps = np.exp(logits / temperature)
return exps / np.sum(exps)
# Base logits (before softmax)
logits = np.array([2.5, 2.0, 1.5, 1.0, 0.5])
words = ["word1", "word2", "word3", "word4", "word5"]
# Different temperatures
temperatures = [0.5, 1.0, 2.0]
temp_labels = ["T = 0.5 (focused)", "T = 1.0 (normal)", "T = 2.0 (creative)"]
# Create three distributions side by side
dist_groups = VGroup()
for temp, label in zip(temperatures, temp_labels):
probs = softmax(logits, temp)
bars = self.build_mini_distribution(probs)
title = Text(label, font_size=22)
title.next_to(bars, UP, buff=0.3)
dist_groups.add(VGroup(title, bars))
dist_groups.arrange(RIGHT, buff=1.0)
dist_groups.center()
# Main title
main_title = Text("Temperature Effect on Sampling", font_size=36)
main_title.to_edge(UP, buff=0.5)
# Animate
self.play(Write(main_title))
self.play(
LaggedStart(
*(FadeIn(dg, shift=UP) for dg in dist_groups),
lag_ratio=0.3
)
)
self.wait()
# Highlight differences
arrows = VGroup()
for i, (dg, temp) in enumerate(zip(dist_groups, temperatures)):
if temp == 0.5:
note = Text("More deterministic", font_size=18, color=BLUE)
elif temp == 2.0:
note = Text("More random", font_size=18, color=RED)
else:
note = Text("Balanced", font_size=18, color=GREEN)
note.next_to(dg, DOWN, buff=0.3)
arrows.add(note)
self.play(FadeIn(arrows, lag_ratio=0.2))
self.wait(2)
def build_mini_distribution(self, probs, bar_width=1.5, bar_height=0.2):
"""Build a compact bar chart."""
bars = VGroup()
for prob in probs:
bar = Rectangle(prob * bar_width, bar_height)
bar.set_fill(interpolate_color(GREY_D, TEAL, prob), opacity=0.9)
bar.set_stroke(WHITE, 1)
bars.add(bar)
bars.arrange(DOWN, aligned_edge=LEFT, buff=0.1)
return bars
@@ -0,0 +1,274 @@
"""
Token to Embedding Visualization
Shows the transformation from text tokens to vector embeddings.
Based on 3Blue1Brown's transformer visualizations.
Run: manimgl token_to_embedding.py TokenToEmbedding -o
"""
from manimlib import *
import numpy as np
import random
def value_to_color(
value,
low_positive_color=BLUE_E,
high_positive_color=BLUE_B,
low_negative_color=RED_E,
high_negative_color=RED_B,
min_value=0.0,
max_value=10.0
):
"""Map a value to a color based on its sign and magnitude."""
alpha = np.clip(float((abs(value) - min_value) / (max_value - min_value)), 0, 1)
if value >= 0:
return interpolate_color(low_positive_color, high_positive_color, alpha)
else:
return interpolate_color(low_negative_color, high_negative_color, alpha)
def random_bright_color(hue_range=(0.0, 1.0)):
"""Generate a random bright color within a hue range."""
hue = random.uniform(*hue_range)
return Color(hsl=(hue, 0.7, 0.6))
class SimpleNumericEmbedding(VGroup):
"""A simplified numeric embedding visualization."""
def __init__(self, length=8, height=2.5, width=0.5, bracket_color=GREY_B, **kwargs):
super().__init__(**kwargs)
entries = VGroup()
entry_height = (height / length) * 0.85
for _ in range(length):
value = random.uniform(-9.9, 9.9)
rect = Rectangle(width=width * 0.8, height=entry_height)
rect.set_fill(value_to_color(value), opacity=0.9)
rect.set_stroke(WHITE, 0.5)
entries.add(rect)
entries.arrange(DOWN, buff=0.02)
entries.set_height(height)
# Brackets
lb = Text("[", font_size=72)
rb = Text("]", font_size=72)
lb.stretch_to_fit_height(height * 1.1)
rb.stretch_to_fit_height(height * 1.1)
lb.set_color(bracket_color)
rb.set_color(bracket_color)
lb.next_to(entries, LEFT, buff=0.05)
rb.next_to(entries, RIGHT, buff=0.05)
self.add(lb, entries, rb)
self.entries = entries
self.brackets = VGroup(lb, rb)
class TokenToEmbedding(Scene):
"""
Demonstrates the conversion of text tokens into vector embeddings.
Shows how each word/token in a sentence gets converted into
a numerical vector representation.
"""
example_text = "The quick brown fox"
def construct(self):
# Show the phrase
phrase = Text(self.example_text, font_size=60)
phrase.to_edge(UP, buff=1)
self.play(Write(phrase))
self.wait()
# Split into words/tokens
word_strings = self.example_text.split()
colors = [BLUE, GREEN, YELLOW, RED]
word_groups = VGroup()
for i, word_str in enumerate(word_strings):
word = phrase[word_str][0]
rect = SurroundingRectangle(word, buff=0.1)
rect.set_stroke(colors[i % len(colors)], 2)
rect.set_fill(colors[i % len(colors)], 0.2)
word_groups.add(VGroup(rect, word.copy()))
# Animate word rectangles appearing
self.play(
LaggedStart(*(
DrawBorderThenFill(wg[0])
for wg in word_groups
), lag_ratio=0.2),
run_time=2
)
self.wait()
# Create embedding vectors
vectors = VGroup(*(
SimpleNumericEmbedding(length=10, height=3.0, width=0.6)
for _ in word_strings
))
vectors.arrange(RIGHT, buff=1.0)
vectors.set_width(FRAME_WIDTH - 2)
vectors.to_edge(DOWN, buff=1)
# Color code the brackets
for vec, color in zip(vectors, colors):
vec.brackets.set_color(color)
# Position token blocks above vectors
token_blocks = VGroup()
for i, (wg, vec) in enumerate(zip(word_groups, vectors)):
block = wg.copy()
block.set_width(vec.get_width() * 1.2)
block.next_to(vec, UP, buff=1.5)
token_blocks.add(block)
# Create arrows
arrows = VGroup(*(
Arrow(block.get_bottom(), vec.get_top(), stroke_width=3, buff=0.1)
for block, vec in zip(token_blocks, vectors)
))
for arrow, color in zip(arrows, colors):
arrow.set_color(color)
# Animate transformation
self.play(
ReplacementTransform(
VGroup(*(wg.copy() for wg in word_groups)),
token_blocks
),
self.frame.animate.shift(0.5 * DOWN) if hasattr(self, 'frame') else Wait(),
run_time=2
)
self.play(
LaggedStartMap(GrowArrow, arrows, lag_ratio=0.2),
LaggedStartMap(FadeIn, vectors, shift=0.5 * DOWN, lag_ratio=0.2),
run_time=2
)
self.wait()
# Add dimension label
dim_label = Text("Each vector has d dimensions", font_size=36)
dim_label.next_to(vectors, DOWN)
brace = Brace(vectors[0], RIGHT)
dim_num = brace.get_tex("d = 12288", font_size=30)
self.play(
FadeIn(dim_label),
GrowFromCenter(brace),
FadeIn(dim_num),
)
self.wait(2)
# Cleanup
self.play(
FadeOut(VGroup(
phrase, word_groups, token_blocks, arrows, vectors,
dim_label, brace, dim_num
))
)
class EmbeddingArrayVisualization(Scene):
"""
Shows multiple embeddings arranged as an array/matrix.
"""
def construct(self):
# Title
title = Text("Embedding Array", font_size=56)
title.to_edge(UP)
# Create array of embeddings
n_tokens = 7
n_dims = 10
# Create the embedding columns
columns = VGroup()
for i in range(n_tokens):
col = VGroup()
for j in range(n_dims):
value = random.uniform(-10, 10)
rect = Rectangle(width=0.5, height=0.35)
rect.set_fill(value_to_color(value), opacity=0.9)
rect.set_stroke(WHITE, 0.5)
col.add(rect)
col.arrange(DOWN, buff=0.02)
columns.add(col)
columns.arrange(RIGHT, buff=0.3)
# Add brackets
left_bracket = Tex(r"\left[", font_size=120)
right_bracket = Tex(r"\right]", font_size=120)
left_bracket.stretch_to_fit_height(columns.get_height() * 1.1)
right_bracket.stretch_to_fit_height(columns.get_height() * 1.1)
left_bracket.next_to(columns, LEFT, buff=0.1)
right_bracket.next_to(columns, RIGHT, buff=0.1)
array = VGroup(left_bracket, columns, right_bracket)
array.center()
# Token labels
token_labels = VGroup(*(
Text(f"t{i}", font_size=24)
for i in range(n_tokens)
))
for label, col in zip(token_labels, columns):
label.next_to(col, UP, buff=0.3)
# Dimension label
dim_brace = Brace(columns[0], LEFT)
dim_label = dim_brace.get_text("d", font_size=36)
# Animate
self.play(Write(title))
self.play(
LaggedStartMap(FadeIn, columns, shift=0.3 * DOWN, lag_ratio=0.1),
run_time=2
)
self.play(
FadeIn(left_bracket, shift=0.2 * LEFT),
FadeIn(right_bracket, shift=0.2 * RIGHT),
)
self.play(LaggedStartMap(FadeIn, token_labels, shift=0.2 * DOWN, lag_ratio=0.1))
self.play(
GrowFromCenter(dim_brace),
FadeIn(dim_label),
)
self.wait()
# Highlight one column
highlight_rect = SurroundingRectangle(columns[3], buff=0.1)
highlight_rect.set_stroke(YELLOW, 3)
self.play(ShowCreation(highlight_rect))
self.wait()
# Show context note
context_note = Text(
"Each column encodes one token's meaning + context",
font_size=30
)
context_note.next_to(array, DOWN, buff=1)
self.play(FadeIn(context_note, shift=UP))
self.wait(2)
# Cleanup
self.play(FadeOut(VGroup(
title, array, token_labels, dim_brace, dim_label,
highlight_rect, context_note
)))
@@ -0,0 +1,200 @@
"""
Tokenization Visualization Demo
Shows how text gets broken into tokens with colored rectangles.
Based on: videos/_2024/transformers/embedding.py - LyingAboutTokens2
"""
from manimlib import *
def break_into_words(phrase_mob):
"""Break a Text mobject into individual word submobjects."""
import re
phrase = phrase_mob.get_string()
offsets = [m.start() for m in re.finditer(" ", phrase)]
return break_into_pieces(phrase_mob, [0, *offsets])
def break_into_pieces(phrase_mob, offsets):
"""Break a Text mobject at specified character offsets."""
phrase = phrase_mob.get_string()
lhs = offsets
rhs = [*offsets[1:], len(phrase)]
result = []
for lh, rh in zip(lhs, rhs):
substr = phrase[lh:rh]
start = phrase_mob.substr_to_path_count(phrase[:lh])
end = start + phrase_mob.substr_to_path_count(substr)
result.append(phrase_mob[start:end])
return VGroup(*result)
def random_bright_color(hue_range=(0.5, 0.6)):
"""Generate a random bright color within a hue range."""
import random
hue = random.uniform(*hue_range)
return Color(hsl=(hue, 0.8, 0.6))
def get_piece_rectangles(
phrase_pieces,
h_buff=0.05,
v_buff=0.1,
fill_opacity=0.15,
fill_color=None,
stroke_width=1,
stroke_color=None,
hue_range=(0.5, 0.6),
leading_spaces=False,
):
"""Create colored rectangles around text pieces."""
rects = VGroup()
height = phrase_pieces.get_height() + 2 * v_buff
last_right_x = phrase_pieces.get_x(LEFT)
for piece in phrase_pieces:
left_x = last_right_x if leading_spaces else piece.get_x(LEFT)
right_x = piece.get_x(RIGHT)
fill = random_bright_color(hue_range) if fill_color is None else fill_color
stroke = fill if stroke_color is None else stroke_color
rect = Rectangle(
width=right_x - left_x + 2 * h_buff,
height=height,
fill_color=fill,
fill_opacity=fill_opacity,
stroke_color=stroke,
stroke_width=stroke_width
)
if leading_spaces:
rect.set_x(left_x, LEFT)
else:
rect.move_to(piece)
rect.set_y(0)
rects.add(rect)
last_right_x = right_x
rects.match_y(phrase_pieces)
return rects
class TokenizationDemo(InteractiveScene):
"""
Demonstrates how text is broken into tokens/words with visual highlighting.
"""
def construct(self):
# Title
title = Text("Tokenization", font_size=72)
title.to_edge(UP)
self.play(Write(title))
self.wait()
# Show a phrase being tokenized
phrase = Text("The goal of our model is to predict the next word")
phrase.set_width(FRAME_WIDTH - 2)
phrase.next_to(title, DOWN, buff=1.0)
self.play(Write(phrase, run_time=2))
self.wait()
# Break into words
words = break_into_words(phrase)
rects = get_piece_rectangles(words, hue_range=(0.5, 0.6))
# Animate rectangles appearing
self.add(rects, phrase)
self.play(
LaggedStartMap(FadeIn, rects, lag_ratio=0.1),
LaggedStart(*(
word.animate.set_color(rect.get_color())
for word, rect in zip(words, rects)
), lag_ratio=0.1)
)
self.wait()
# Highlight last word as prediction target
last_rect = rects[-1]
q_marks = Text("???", font_size=48)
q_marks.next_to(last_rect, DOWN)
self.play(
last_rect.animate.set_color(YELLOW),
FadeIn(q_marks)
)
self.wait()
# Show arrow from context to prediction
context_rect = Rectangle()
context_rect.replace(rects[:-1], stretch=True)
context_rect.set_stroke(WHITE, 2)
arrow = Arrow(context_rect.get_top(), last_rect.get_top(), path_arc=-90 * DEGREES)
arrow.scale(0.6, about_edge=DR)
self.play(
FadeIn(context_rect),
GrowArrow(arrow),
)
self.wait()
# Transition to showing embedding concept
self.play(
FadeOut(title),
FadeOut(context_rect),
FadeOut(arrow),
FadeOut(q_marks),
)
# Show words becoming vectors
word_labels = VGroup(*(
Text(word.get_string().strip(), font_size=36)
for word in words[:-1]
))
# Create simple vector representations
vectors = VGroup(*(
VGroup(
Tex("["),
VGroup(*(
DecimalNumber(np.random.uniform(-1, 1), num_decimal_places=2)
for _ in range(4)
)).arrange(DOWN, buff=0.1),
Tex("]"),
).arrange(RIGHT, buff=0.05)
for _ in words[:-1]
))
for vector in vectors:
vector.scale(0.6)
# Arrange word-vector pairs
pairs = VGroup()
for word, vec, rect in zip(word_labels, vectors, rects[:-1]):
vec.get_brackets = lambda v=vec: VGroup(v[0], v[-1])
vec.get_brackets().match_color(rect.get_color())
pair = VGroup(word, vec)
pair.arrange(DOWN, buff=0.5)
pairs.add(pair)
pairs.arrange(RIGHT, buff=0.8)
pairs.set_width(FRAME_WIDTH - 1)
pairs.center()
# Animate transformation
self.play(
LaggedStart(*(
AnimationGroup(
ReplacementTransform(VGroup(rect, word), label),
FadeIn(vec, shift=DOWN),
)
for word, rect, label, vec in zip(words[:-1], rects[:-1], word_labels, vectors)
), lag_ratio=0.1),
FadeOut(rects[-1]),
FadeOut(words[-1]),
run_time=3
)
self.wait()
# Add title for embedding
embed_title = Text("Word Embeddings", font_size=60)
embed_title.to_edge(UP)
self.play(Write(embed_title))
self.wait(2)
@@ -0,0 +1,248 @@
"""
Transit Animation
Simple but elegant animations showing objects crossing
in front of others. Useful for astronomical transits,
loading animations, or timing demonstrations.
Run: manimgl transit_animation.py TransitOfVenus -w
Preview: manimgl transit_animation.py TransitOfVenus -p
Source: Adapted from 3b1b's cosmic_distance video (2025)
"""
from manimlib import *
class TransitOfVenus(InteractiveScene):
"""
Venus (small dot) transiting across the Sun.
Shows how astronomers measured distances historically.
"""
def construct(self):
# Create the Sun (large yellow circle)
sun = Circle(radius=2.5)
sun.set_fill(YELLOW, opacity=0.8)
sun.set_stroke(ORANGE, width=3)
# Add some texture with a glow
sun_glow = Circle(radius=2.7)
sun_glow.set_fill(YELLOW, opacity=0.2)
sun_glow.set_stroke(width=0)
self.add(sun_glow, sun)
# Path for Venus transit
path = Line(3 * LEFT, 3 * RIGHT)
path.set_y(-0.5) # Slightly below center
# Venus as small black dot
venus = Dot(radius=0.08, color=BLACK)
venus.move_to(path.get_start())
venus.set_fill(BLACK, opacity=1)
self.add(venus)
# Show transit with periodic snapshots
velocity = 0.3
venus.add_updater(lambda m, dt: m.shift(dt * velocity * RIGHT))
# Collect snapshots
copies = VGroup()
self.add(copies)
wait_time = 0.8
n_snapshots = int(path.get_length() / velocity / wait_time)
for _ in range(n_snapshots):
self.wait(wait_time)
copy = venus.copy().clear_updaters()
copy.set_fill(BLACK, opacity=0.5)
copies.add(copy)
# Remove venus, show path
self.remove(venus)
path.set_stroke(BLACK, 2)
self.play(Transform(copies, VGroup(path)))
self.wait()
class OrbitalTransit(InteractiveScene):
"""
Shows a planet orbiting and periodically transiting
in front of its star from the viewer's perspective.
"""
def construct(self):
# Star
star = Circle(radius=1)
star.set_fill(YELLOW_E, opacity=1)
star.set_stroke(YELLOW, width=2)
# Orbit path (ellipse viewed at an angle)
orbit = Ellipse(width=5, height=1)
orbit.set_stroke(WHITE, 1, opacity=0.3)
self.add(orbit, star)
# Planet
planet = Dot(radius=0.15, color=BLUE)
planet.move_to(orbit.get_right())
# Orbit animation using angle tracker
angle = ValueTracker(0)
def update_planet(p):
a = angle.get_value()
x = 2.5 * np.cos(a)
y = 0.5 * np.sin(a)
p.move_to([x, y, 0])
# Depth effect: size changes based on y position
scale = 0.12 + 0.06 * np.sin(a)
p.set_width(2 * scale)
planet.add_updater(update_planet)
self.add(planet)
# Multiple orbits
self.play(
angle.animate.set_value(4 * TAU),
run_time=12,
rate_func=linear
)
class LoadingDots(InteractiveScene):
"""
Classic loading animation with dots.
Demonstrates phase-shifted periodic motion.
"""
def construct(self):
# Create three dots
n_dots = 3
dots = VGroup(*[
Dot(radius=0.15, color=BLUE)
for _ in range(n_dots)
])
dots.arrange(RIGHT, buff=0.5)
dots.center()
time = ValueTracker(0)
# Each dot oscillates with a phase shift
for i, dot in enumerate(dots):
phase = i * TAU / n_dots
original_y = dot.get_y()
dot.add_updater(
lambda m, o=original_y, p=phase: m.set_y(
o + 0.3 * np.sin(3 * time.get_value() + p)
)
)
self.add(dots)
# Animate
time.add_updater(lambda m, dt: m.increment_value(dt))
self.wait(5)
class WaveTransit(InteractiveScene):
"""
A wave propagating across the screen.
Good for demonstrating wave motion or signal propagation.
"""
def construct(self):
# Create axes
axes = Axes(
x_range=(-5, 5, 1),
y_range=(-2, 2, 1),
width=12,
height=4,
)
self.add(axes)
# Time tracker
t = ValueTracker(0)
# Wave function
def wave(x):
return np.sin(2 * x - 3 * t.get_value()) * np.exp(-0.1 * (x + 5 - t.get_value())**2)
# Wave curve
wave_curve = always_redraw(
lambda: axes.get_graph(wave, color=BLUE, stroke_width=3)
)
self.add(wave_curve)
# Propagate wave
self.play(
t.animate.set_value(10),
run_time=5,
rate_func=linear
)
self.wait()
class PendulumSwing(InteractiveScene):
"""
Simple pendulum animation.
Classic physics visualization.
"""
def construct(self):
# Pivot point
pivot = Dot(ORIGIN, color=WHITE)
# Pendulum parameters
length = 3
g = 10
omega = np.sqrt(g / length)
# Angle tracker (start displaced)
theta = ValueTracker(PI / 4)
# Bob
bob = Dot(radius=0.2, color=BLUE)
bob.add_updater(lambda m: m.move_to(
pivot.get_center() + length * np.array([
np.sin(theta.get_value()),
-np.cos(theta.get_value()),
0
])
))
# Rod
rod = Line(ORIGIN, DOWN)
rod.set_stroke(WHITE, 3)
rod.add_updater(lambda m: m.put_start_and_end_on(
pivot.get_center(),
bob.get_center()
))
# Trail
trail = TracedPath(
bob.get_center,
stroke_color=YELLOW,
stroke_width=1,
stroke_opacity=0.5
)
self.add(pivot, rod, bob, trail)
# Simple harmonic motion approximation
time = ValueTracker(0)
amplitude = PI / 4
def update_theta(m):
t = time.get_value()
m.set_value(amplitude * np.cos(omega * t) * np.exp(-0.05 * t))
theta.add_updater(update_theta)
time.add_updater(lambda m, dt: m.increment_value(dt))
self.wait(10)
@@ -0,0 +1,222 @@
"""
Value Matrix Transformation Visualization
Shows how the Value matrix transforms embeddings and how the weighted sum
of value vectors produces the output.
"""
from manimlib import *
import numpy as np
def value_to_color(
value,
low_positive_color=BLUE_E,
high_positive_color=BLUE_B,
low_negative_color=RED_E,
high_negative_color=RED_B,
min_value=0.0,
max_value=10.0
):
"""Map a value to a color based on sign and magnitude."""
alpha = clip(float(inverse_interpolate(min_value, max_value, abs(value))), 0, 1)
if value >= 0:
return interpolate_color(low_positive_color, high_positive_color, alpha)
else:
return interpolate_color(low_negative_color, high_negative_color, alpha)
class WeightMatrix(DecimalMatrix):
"""A matrix with color-coded entries based on value."""
def __init__(
self,
values=None,
shape=(5, 7),
value_range=(-9.9, 9.9),
ellipses_row=-2,
ellipses_col=-2,
num_decimal_places=1,
bracket_h_buff=0.1,
**kwargs
):
if values is None:
values = np.random.uniform(*value_range, size=shape)
self.shape = shape
self.value_range = value_range
self.ellipses_row = ellipses_row
super().__init__(
values,
num_decimal_places=num_decimal_places,
bracket_h_buff=bracket_h_buff,
decimal_config=dict(include_sign=True),
ellipses_row=ellipses_row,
ellipses_col=ellipses_col,
)
self.reset_entry_colors()
def reset_entry_colors(self):
for entry in self.get_entries():
entry.set_fill(color=value_to_color(
entry.get_value(),
min_value=0,
max_value=max(self.value_range),
))
return self
class ValueMatrixTransform(InteractiveScene):
def construct(self):
# Title
title = Text("Value Matrix: Creating Contextual Updates", font_size=42)
title.to_edge(UP)
self.play(Write(title))
# Create words with embeddings
words = ["fluffy", "blue", "creature"]
word_mobs = VGroup(Text(word, font_size=36) for word in words)
word_mobs.arrange(DOWN, buff=1.5)
word_mobs.shift(4 * LEFT + 0.5 * DOWN)
# Color code words
word_mobs[0].set_color(TEAL)
word_mobs[1].set_color(BLUE)
word_mobs[2].set_color(ORANGE)
# Embedding symbols
e_template = Tex(R"\vec{\textbf{E}}_0", font_size=36)
e_substr = e_template.make_number_changeable("0")
e_syms = VGroup()
e_arrows = VGroup()
for i, word in enumerate(word_mobs, start=1):
e_substr.set_value(i)
e_sym = e_template.copy()
e_sym.set_color(word.get_color())
arrow = Arrow(word.get_right(), word.get_right() + 0.8 * RIGHT, buff=0.1)
e_sym.next_to(arrow, RIGHT, buff=0.1)
e_syms.add(e_sym)
e_arrows.add(arrow)
self.play(
LaggedStartMap(FadeIn, word_mobs, shift=0.5 * RIGHT, lag_ratio=0.2),
)
self.play(
LaggedStartMap(GrowArrow, e_arrows, lag_ratio=0.2),
LaggedStartMap(FadeIn, e_syms, shift=0.5 * RIGHT, lag_ratio=0.2),
)
self.wait()
# Value matrix
np.random.seed(42)
matrix = WeightMatrix(shape=(5, 7))
matrix.set_height(2.5)
matrix.move_to(0.5 * DOWN)
mat_label = Tex("W_V", font_size=48)
mat_label.set_color(RED)
mat_label.next_to(matrix, UP)
# Value vectors
v_template = Tex(R"\vec{\textbf{V}}_0", font_size=36)
v_template.set_color(RED)
v_substr = v_template.make_number_changeable("0")
v_syms = VGroup()
v_arrows = VGroup()
for i, e_sym in enumerate(e_syms, start=1):
v_substr.set_value(i)
v_arrow = Arrow(ORIGIN, 0.8 * RIGHT, buff=0)
v_arrow.next_to(matrix, RIGHT, buff=0.3)
v_arrow.match_y(e_sym)
v_sym = v_template.copy()
v_sym.next_to(v_arrow, RIGHT, buff=0.1)
v_syms.add(v_sym)
v_arrows.add(v_arrow)
# Show transformation
self.play(
FadeIn(matrix, lag_ratio=0.01),
FadeIn(mat_label, shift=0.25 * UP),
)
self.wait()
# Transform each E to V
for e_sym, v_arrow, v_sym in zip(e_syms, v_arrows, v_syms):
self.play(
TransformFromCopy(e_sym, v_sym, path_arc=-30 * DEGREES),
GrowArrow(v_arrow),
run_time=0.7
)
self.wait()
# Show weighted sum
weighted_label = Text("Weighted Sum of Values", font_size=36)
weighted_label.to_edge(RIGHT)
weighted_label.shift(UP)
# Attention weights
weights = [0.6, 0.3, 0.1]
weight_labels = VGroup()
for w, v_sym in zip(weights, v_syms):
w_label = DecimalNumber(w, num_decimal_places=1, font_size=30)
w_label.next_to(v_sym, RIGHT, buff=0.3)
w_label.set_color(YELLOW)
weight_labels.add(w_label)
times_syms = VGroup(
Tex(R"\times", font_size=30).next_to(wl, LEFT, buff=0.1)
for wl in weight_labels
)
self.play(
Write(weighted_label),
LaggedStartMap(FadeIn, weight_labels, shift=0.2 * LEFT, lag_ratio=0.1),
LaggedStartMap(FadeIn, times_syms, lag_ratio=0.1),
)
self.wait()
# Show result
result_label = Tex(R"\Delta \vec{\textbf{E}}_3", font_size=42)
result_label.set_color(YELLOW)
result_label.next_to(weighted_label, DOWN, buff=1.0)
plus_syms = VGroup(Tex("+", font_size=30) for _ in range(2))
weighted_v = VGroup()
for i, (w, v_sym) in enumerate(zip(weight_labels, v_syms)):
term = VGroup(w.copy(), v_sym.copy())
weighted_v.add(term)
weighted_v.arrange(RIGHT, buff=0.3)
for plus, term in zip(plus_syms, weighted_v[1:]):
plus.next_to(term, LEFT, buff=0.1)
weighted_sum = VGroup(weighted_v[0], plus_syms[0], weighted_v[1], plus_syms[1], weighted_v[2])
weighted_sum.scale(0.8)
weighted_sum.next_to(result_label, UP, buff=0.5)
eq_sign = Tex("=", font_size=36)
eq_sign.next_to(result_label, LEFT, buff=0.2)
self.play(
LaggedStart(
(TransformFromCopy(VGroup(wl, vs), wv)
for wl, vs, wv in zip(weight_labels, v_syms, weighted_v)),
lag_ratio=0.2
),
LaggedStartMap(FadeIn, plus_syms, lag_ratio=0.3),
)
self.play(
FadeIn(eq_sign),
FadeIn(result_label, shift=0.2 * DOWN),
)
self.wait()
# Explanation
explanation = Text(
"This update adds context\nfrom attended tokens",
font_size=30
)
explanation.to_edge(DOWN)
self.play(Write(explanation))
self.wait(2)
@@ -0,0 +1,248 @@
"""
Vector Fields and Flow Visualization
Demonstrates vector field rendering using arrows,
streamlines, and particle flow animations.
Run: manimgl vector_fields.py SimpleVectorField -w
Preview: manimgl vector_fields.py SimpleVectorField -p
Source: Inspired by 3b1b's vector field visualizations
"""
from manimlib import *
import numpy as np
class SimpleVectorField(InteractiveScene):
"""
Basic 2D vector field visualization using arrows.
Shows rotation field around origin.
"""
def construct(self):
# Create plane
plane = NumberPlane(
x_range=(-4, 4, 1),
y_range=(-3, 3, 1),
background_line_style={"stroke_opacity": 0.3}
)
self.add(plane)
# Create arrows manually for vector field
arrows = VGroup()
for x in np.arange(-3.5, 4, 0.7):
for y in np.arange(-2.5, 3, 0.7):
# Rotation field: F = (-y, x)
vx, vy = -y * 0.15, x * 0.15
if abs(vx) < 0.01 and abs(vy) < 0.01:
continue
arrow = Arrow(
start=[x, y, 0],
end=[x + vx, y + vy, 0],
buff=0,
stroke_width=2,
max_tip_length_to_length_ratio=0.3,
)
# Color by magnitude
mag = np.sqrt(vx**2 + vy**2)
arrow.set_color(interpolate_color(BLUE, YELLOW, mag / 0.5))
arrows.add(arrow)
self.play(LaggedStartMap(GrowArrow, arrows, lag_ratio=0.02, run_time=2))
self.wait()
# Add a particle that follows the field
dot = Dot(color=RED, radius=0.1)
dot.move_to(2 * RIGHT + UP)
def follow_field(mob, dt):
x, y = mob.get_center()[:2]
vx, vy = -y * 0.5, x * 0.5
mob.shift(np.array([vx, vy, 0]) * dt)
dot.add_updater(follow_field)
trail = TracedPath(dot.get_center, stroke_color=RED, stroke_width=2)
self.add(trail, dot)
self.wait(8)
class GradientFieldDemo(InteractiveScene):
"""
Shows gradient of a scalar field.
Arrows point toward steepest ascent.
"""
def construct(self):
# Create colored background showing scalar field
plane = NumberPlane(
x_range=(-4, 4, 1),
y_range=(-3, 3, 1),
background_line_style={"stroke_opacity": 0.2}
)
self.add(plane)
# Scalar field: f(x,y) = -(x^2 + y^2) (peak at origin)
# Gradient: (-2x, -2y) pointing toward origin
# Create dots colored by height
dots = VGroup()
for x in np.arange(-3.5, 4, 0.3):
for y in np.arange(-2.5, 3, 0.3):
val = -(x**2 + y**2)
t = (val + 25) / 25 # Normalize
color = interpolate_color(BLUE_E, RED, t)
dot = Dot([x, y, 0], radius=0.08, color=color)
dots.add(dot)
self.play(FadeIn(dots))
# Gradient vectors (pointing toward origin = uphill)
arrows = VGroup()
for x in np.arange(-3, 3.5, 0.8):
for y in np.arange(-2, 2.5, 0.8):
if abs(x) < 0.3 and abs(y) < 0.3:
continue
# Gradient direction (toward origin for this function)
gx, gy = -2*x, -2*y
length = np.sqrt(gx**2 + gy**2)
# Normalize and scale
scale = 0.3
gx, gy = gx/length * scale, gy/length * scale
arrow = Arrow(
start=[x, y, 0],
end=[x + gx, y + gy, 0],
buff=0,
stroke_width=2,
stroke_color=WHITE,
)
arrows.add(arrow)
self.play(LaggedStartMap(GrowArrow, arrows, lag_ratio=0.02, run_time=2))
# Label
label = Tex(r"\nabla f = (-2x, -2y)", font_size=36)
label.to_corner(UL)
label.set_backstroke(BLACK, 3)
self.play(Write(label))
self.wait()
class ParticleFlow(InteractiveScene):
"""
Multiple particles flowing through a vector field.
Great for visualizing fluid flow.
"""
def construct(self):
# Vortex field visualization
plane = NumberPlane(
x_range=(-5, 5, 1),
y_range=(-4, 4, 1),
background_line_style={"stroke_opacity": 0.2}
)
self.add(plane)
# Create particles
n_particles = 15
particles = VGroup()
trails = VGroup()
for i in range(n_particles):
# Start in a circle
angle = i * TAU / n_particles
start_pos = 2 * np.array([np.cos(angle), np.sin(angle), 0])
dot = Dot(start_pos, radius=0.1, color=YELLOW)
def make_updater():
def update(mob, dt):
x, y = mob.get_center()[:2]
r = np.sqrt(x**2 + y**2) + 0.1
vx, vy = -y/r, x/r
mob.shift(np.array([vx, vy, 0]) * dt * 0.8)
return update
dot.add_updater(make_updater())
trail = TracedPath(
dot.get_center,
stroke_color=BLUE,
stroke_width=1.5,
stroke_opacity=0.7,
)
particles.add(dot)
trails.add(trail)
self.add(trails, particles)
self.wait(10)
class ElectricDipole(InteractiveScene):
"""
Electric field from two point charges (dipole).
"""
def construct(self):
# Charge positions
q1_pos = np.array([-2, 0, 0])
q2_pos = np.array([2, 0, 0])
# Draw charges
q_plus = Dot(q1_pos, radius=0.25, color=RED)
q_plus_label = Tex("+", font_size=36, color=WHITE)
q_plus_label.move_to(q1_pos)
q_minus = Dot(q2_pos, radius=0.25, color=BLUE)
q_minus_label = Tex("-", font_size=36, color=WHITE)
q_minus_label.move_to(q2_pos)
self.add(q_plus, q_plus_label, q_minus, q_minus_label)
# Create field arrows
arrows = VGroup()
for x in np.arange(-4, 4.5, 0.6):
for y in np.arange(-3, 3.5, 0.6):
pos = np.array([x, y, 0])
# Skip near charges
if np.linalg.norm(pos - q1_pos) < 0.5:
continue
if np.linalg.norm(pos - q2_pos) < 0.5:
continue
# Electric field from both charges
r1 = pos - q1_pos
r2 = pos - q2_pos
d1 = np.linalg.norm(r1) + 0.1
d2 = np.linalg.norm(r2) + 0.1
# E = kq/r^2 in direction of r (positive) or -r (negative)
E1 = r1 / d1**3 # From positive charge
E2 = -r2 / d2**3 # From negative charge
E = E1 + E2
mag = np.linalg.norm(E)
if mag < 0.001:
continue
# Normalize and scale
E_norm = E / mag
length = min(0.4, mag * 2)
arrow = Arrow(
start=pos,
end=pos + E_norm * length,
buff=0,
stroke_width=2,
)
# Color by magnitude
color = interpolate_color(BLUE_E, YELLOW, min(mag * 5, 1))
arrow.set_color(color)
arrows.add(arrow)
self.play(LaggedStartMap(GrowArrow, arrows, lag_ratio=0.01, run_time=3))
self.wait()
@@ -0,0 +1,410 @@
"""
Wave Amplitude and Phase Visualization
Demonstrates various ways to visualize electromagnetic waves,
including vector field representations and amplitude graphs.
Based on 3Blue1Brown's wave visualization techniques.
Run: manimgl wave_amplitude_visualization.py WaveAmplitudeDemo -w
"""
from manimlib import *
import numpy as np
class WaveAmplitudeDemo(Scene):
"""
Shows wave amplitude with oscillating vectors along a propagation line.
"""
def construct(self):
frame = self.camera.frame
# Title
title = Text("Wave Amplitude Visualization", font_size=42)
title.to_edge(UP)
title.set_backstroke(BLACK, 5)
self.add(title)
# Wave parameters
wave_number = 1.5
frequency = 0.5
amplitude = 1.0
# Create a line of points along which the wave propagates
n_points = 40
x_range = np.linspace(-6, 6, n_points)
# Wave function
def wave_value(x, time):
return amplitude * np.sin(TAU * (wave_number * x - frequency * time))
# Create oscillating vectors
def get_wave_vectors(time):
vectors = VGroup()
for x in x_range:
y_val = wave_value(x, time)
# Create vector from baseline
start = np.array([x, -2, 0])
end = np.array([x, -2 + y_val, 0])
vec = Arrow(start, end, buff=0, stroke_width=2, max_tip_length_to_length_ratio=0.15)
# Color based on displacement
if y_val > 0:
vec.set_color(interpolate_color(WHITE, BLUE, min(y_val / amplitude, 1)))
else:
vec.set_color(interpolate_color(WHITE, RED, min(-y_val / amplitude, 1)))
vectors.add(vec)
return vectors
# Create wave curve
def get_wave_curve(time):
curve = FunctionGraph(
lambda x: -2 + wave_value(x, time),
x_range=[-6, 6, 0.1],
color=TEAL
)
curve.set_stroke(width=3)
return curve
time_tracker = ValueTracker(0)
vectors = always_redraw(lambda: get_wave_vectors(time_tracker.get_value()))
curve = always_redraw(lambda: get_wave_curve(time_tracker.get_value()))
# Baseline
baseline = Line([-6, -2, 0], [6, -2, 0])
baseline.set_stroke(WHITE, 1, opacity=0.5)
# Labels
wavelength_brace = Brace(
Line([-2, -2 - 1.2, 0], [-2 + 1/wave_number, -2 - 1.2, 0]),
DOWN
)
lambda_label = Tex(R"\lambda", font_size=36)
lambda_label.next_to(wavelength_brace, DOWN)
amp_line = VGroup(
Arrow([-6.5, -2, 0], [-6.5, -2 + amplitude, 0], buff=0),
Arrow([-6.5, -2 + amplitude, 0], [-6.5, -2, 0], buff=0),
)
amp_line.set_color(YELLOW)
amp_label = Text("Amplitude", font_size=20, color=YELLOW)
amp_label.next_to(amp_line, LEFT)
self.add(baseline)
self.add(vectors)
self.add(curve)
# Animate wave motion
self.play(
time_tracker.animate.set_value(8),
run_time=8,
rate_func=linear
)
# Add labels
self.add(amp_line, amp_label)
self.play(
time_tracker.animate.set_value(12),
FadeIn(wavelength_brace),
FadeIn(lambda_label),
run_time=4,
rate_func=linear
)
self.wait()
class PhaseVisualization(Scene):
"""
Visualizes the phase of a wave using rotating phasors.
"""
def construct(self):
# Title
title = Text("Wave Phase as Rotating Phasor", font_size=42)
title.to_edge(UP)
title.set_backstroke(BLACK, 5)
self.add(title)
# Parameters
frequency = 0.3
# Phasor circle
circle = Circle(radius=1.5, color=GREY)
circle.move_to(LEFT * 3)
circle_center = circle.get_center()
# Phasor arrow
def get_phasor(time):
angle = TAU * frequency * time
end_point = circle_center + 1.5 * np.array([np.cos(angle), np.sin(angle), 0])
arrow = Arrow(circle_center, end_point, buff=0, color=BLUE)
return arrow
# Projection on vertical axis (wave value)
def get_projection_line(time):
angle = TAU * frequency * time
y_val = 1.5 * np.sin(angle)
line = DashedLine(
circle_center + 1.5 * np.array([np.cos(angle), np.sin(angle), 0]),
circle_center + np.array([0, y_val, 0]),
dash_length=0.1
)
line.set_stroke(YELLOW, 2)
return line
# Wave trace
def get_wave_trace(time, length=8):
wave = VGroup()
x_start = 0
for i in range(int(length * 30)):
x = x_start + i / 30
t = time - (x - x_start) / 2
y = 1.5 * np.sin(TAU * frequency * t)
dot = Dot([x, y, 0], radius=0.02, color=TEAL)
wave.add(dot)
return wave
time_tracker = ValueTracker(0)
phasor = always_redraw(lambda: get_phasor(time_tracker.get_value()))
projection = always_redraw(lambda: get_projection_line(time_tracker.get_value()))
wave_trace = always_redraw(lambda: get_wave_trace(time_tracker.get_value()))
# Center dot
center_dot = Dot(circle_center, color=WHITE, radius=0.08)
# Phase angle arc
def get_phase_arc(time):
angle = TAU * frequency * time % TAU
if angle > 0.1:
arc = Arc(0, angle, radius=0.5, arc_center=circle_center)
arc.set_stroke(GREEN, 2)
return arc
return VGroup()
phase_arc = always_redraw(lambda: get_phase_arc(time_tracker.get_value()))
# Labels
phasor_label = Text("Phasor", font_size=24)
phasor_label.next_to(circle, DOWN)
wave_label = Text("Wave amplitude = vertical projection", font_size=24)
wave_label.to_edge(DOWN)
self.add(circle, center_dot)
self.add(phasor)
self.add(projection)
self.add(phase_arc)
self.add(wave_trace)
self.add(phasor_label, wave_label)
# Animate
self.play(
time_tracker.animate.set_value(15),
run_time=15,
rate_func=linear
)
self.wait()
class TwoWaveSuperposition(Scene):
"""
Shows superposition of two waves with different phases.
"""
def construct(self):
# Title
title = Text("Wave Superposition", font_size=42)
title.to_edge(UP)
title.set_backstroke(BLACK, 5)
self.add(title)
# Parameters
wave_number = 1.0
frequency = 0.4
amplitude = 0.8
# Phase difference
phase_diff_tracker = ValueTracker(0)
# Wave functions
def wave1_value(x, time):
return amplitude * np.sin(TAU * (wave_number * x - frequency * time))
def wave2_value(x, time, phase_diff):
return amplitude * np.sin(TAU * (wave_number * x - frequency * time) + phase_diff)
def combined_value(x, time, phase_diff):
return wave1_value(x, time) + wave2_value(x, time, phase_diff)
# Wave curves
def get_wave1(time):
curve = FunctionGraph(
lambda x: 2 + wave1_value(x, time),
x_range=[-6, 6, 0.1],
color=RED
)
curve.set_stroke(width=2)
return curve
def get_wave2(time, phase_diff):
curve = FunctionGraph(
lambda x: wave2_value(x, time, phase_diff),
x_range=[-6, 6, 0.1],
color=BLUE
)
curve.set_stroke(width=2)
return curve
def get_combined(time, phase_diff):
curve = FunctionGraph(
lambda x: -2 + combined_value(x, time, phase_diff),
x_range=[-6, 6, 0.1],
color=GREEN
)
curve.set_stroke(width=3)
return curve
time_tracker = ValueTracker(0)
wave1 = always_redraw(lambda: get_wave1(time_tracker.get_value()))
wave2 = always_redraw(lambda: get_wave2(time_tracker.get_value(),
phase_diff_tracker.get_value()))
combined = always_redraw(lambda: get_combined(time_tracker.get_value(),
phase_diff_tracker.get_value()))
# Baselines
baseline1 = Line([-6, 2, 0], [6, 2, 0]).set_stroke(WHITE, 1, opacity=0.3)
baseline2 = Line([-6, 0, 0], [6, 0, 0]).set_stroke(WHITE, 1, opacity=0.3)
baseline3 = Line([-6, -2, 0], [6, -2, 0]).set_stroke(WHITE, 1, opacity=0.3)
# Labels
label1 = Text("Wave 1", font_size=24, color=RED).to_corner(UL).shift(DOWN)
label2 = Text("Wave 2", font_size=24, color=BLUE).next_to(label1, DOWN)
label_sum = Text("Sum", font_size=24, color=GREEN).next_to(label2, DOWN)
# Phase difference display
phase_display = always_redraw(lambda: Text(
f"Phase diff: {phase_diff_tracker.get_value() / PI:.2f}π",
font_size=28
).to_corner(DR))
self.add(baseline1, baseline2, baseline3)
self.add(wave1, wave2, combined)
self.add(label1, label2, label_sum)
self.add(phase_display)
# Show in-phase waves
self.play(
time_tracker.animate.set_value(6),
run_time=6,
rate_func=linear
)
# Transition to out-of-phase
in_phase_label = Text("In phase: Constructive", font_size=28, color=GREEN)
in_phase_label.to_edge(DOWN)
self.play(Write(in_phase_label))
self.play(
phase_diff_tracker.animate.set_value(PI),
time_tracker.animate.set_value(12),
run_time=6,
rate_func=linear
)
out_phase_label = Text("Out of phase: Destructive", font_size=28, color=PINK)
out_phase_label.next_to(in_phase_label, UP)
self.play(
Write(out_phase_label),
time_tracker.animate.set_value(18),
run_time=6,
rate_func=linear
)
self.wait()
class StandingWave(Scene):
"""
Visualization of a standing wave from two counter-propagating waves.
"""
def construct(self):
# Title
title = Text("Standing Wave", font_size=42)
title.to_edge(UP)
title.set_backstroke(BLACK, 5)
self.add(title)
# Parameters
wave_number = 2.0
frequency = 0.5
amplitude = 1.2
# Standing wave = 2A * sin(kx) * cos(wt)
def standing_wave_value(x, time):
return 2 * amplitude * np.sin(TAU * wave_number * x) * np.cos(TAU * frequency * time)
# Envelope
def envelope_upper(x):
return 2 * amplitude * abs(np.sin(TAU * wave_number * x))
def envelope_lower(x):
return -2 * amplitude * abs(np.sin(TAU * wave_number * x))
# Create wave and envelopes
def get_standing_wave(time):
curve = FunctionGraph(
lambda x: standing_wave_value(x, time),
x_range=[-5, 5, 0.1],
color=TEAL
)
curve.set_stroke(width=3)
return curve
upper_env = FunctionGraph(envelope_upper, x_range=[-5, 5, 0.1], color=YELLOW)
lower_env = FunctionGraph(envelope_lower, x_range=[-5, 5, 0.1], color=YELLOW)
upper_env.set_stroke(width=1, opacity=0.5)
lower_env.set_stroke(width=1, opacity=0.5)
time_tracker = ValueTracker(0)
wave = always_redraw(lambda: get_standing_wave(time_tracker.get_value()))
# Baseline
baseline = Line([-5, 0, 0], [5, 0, 0]).set_stroke(WHITE, 1, opacity=0.3)
# Node and antinode markers
nodes = VGroup()
antinodes = VGroup()
for i in range(-4, 5):
x = i / (2 * wave_number)
if i % 2 == 0:
node = Dot([x, 0, 0], color=RED, radius=0.08)
nodes.add(node)
else:
antinode = Dot([x, 0, 0], color=GREEN, radius=0.08)
antinodes.add(antinode)
# Labels
node_label = Text("Nodes (no motion)", font_size=24, color=RED)
node_label.to_corner(DL)
antinode_label = Text("Antinodes (max motion)", font_size=24, color=GREEN)
antinode_label.next_to(node_label, UP)
self.add(baseline)
self.add(upper_env, lower_env)
self.add(wave)
self.add(nodes, antinodes)
self.add(node_label, antinode_label)
# Animate
self.play(
time_tracker.animate.set_value(15),
run_time=15,
rate_func=linear
)
self.wait()
@@ -0,0 +1,191 @@
"""
Weight Matrix Product Visualization
Shows how a weight matrix transforms an embedding vector step by step,
demonstrating the row-by-vector dot product pattern.
"""
from manimlib import *
import numpy as np
def value_to_color(
value,
low_positive_color=BLUE_E,
high_positive_color=BLUE_B,
low_negative_color=RED_E,
high_negative_color=RED_B,
min_value=0.0,
max_value=10.0
):
"""Map a value to a color based on sign and magnitude."""
alpha = clip(float(inverse_interpolate(min_value, max_value, abs(value))), 0, 1)
if value >= 0:
return interpolate_color(low_positive_color, high_positive_color, alpha)
else:
return interpolate_color(low_negative_color, high_negative_color, alpha)
class WeightMatrix(DecimalMatrix):
"""A matrix with color-coded entries based on value."""
def __init__(
self,
values=None,
shape=(5, 7),
value_range=(-9.9, 9.9),
ellipses_row=-2,
ellipses_col=-2,
num_decimal_places=1,
bracket_h_buff=0.1,
**kwargs
):
if values is None:
values = np.random.uniform(*value_range, size=shape)
self.shape = shape
self.value_range = value_range
self.ellipses_row = ellipses_row
super().__init__(
values,
num_decimal_places=num_decimal_places,
bracket_h_buff=bracket_h_buff,
decimal_config=dict(include_sign=True),
ellipses_row=ellipses_row,
ellipses_col=ellipses_col,
)
self.reset_entry_colors()
def reset_entry_colors(self):
for entry in self.get_entries():
entry.set_fill(color=value_to_color(
entry.get_value(),
min_value=0,
max_value=max(self.value_range),
))
return self
class NumericEmbedding(WeightMatrix):
"""A column vector (embedding) with color-coded entries."""
def __init__(
self,
values=None,
length=7,
value_range=(-9.9, 9.9),
ellipses_row=-2,
**kwargs
):
if values is None:
shape = (length, 1)
else:
if len(values.shape) == 1:
values = values.reshape((values.shape[0], 1))
shape = values.shape
super().__init__(
values=values,
shape=shape,
value_range=value_range,
ellipses_row=ellipses_row,
ellipses_col=None,
**kwargs
)
class WeightMatrixProduct(InteractiveScene):
def construct(self):
# Create the weight matrix
np.random.seed(42)
matrix = WeightMatrix(shape=(5, 7))
matrix.set_height(3.5)
matrix.to_edge(LEFT, buff=1)
# Create input vector
in_vect = NumericEmbedding(length=7)
in_vect.match_height(matrix)
in_vect.next_to(matrix, RIGHT, buff=0.3)
# Labels
mat_brace = Brace(matrix, UP)
mat_label = Tex("W_Q", font_size=48)
mat_label.set_color(YELLOW)
mat_label.next_to(mat_brace, UP, SMALL_BUFF)
vect_label = Tex(R"\vec{E}", font_size=48)
vect_label.set_color(TEAL)
vect_label.next_to(in_vect, UP, buff=0.5)
self.play(
FadeIn(matrix, lag_ratio=0.01),
FadeIn(in_vect),
GrowFromCenter(mat_brace),
FadeIn(mat_label, shift=0.25 * UP),
FadeIn(vect_label, shift=0.25 * DOWN),
)
self.wait()
# Create result vector
eq = Tex("=", font_size=60)
eq.next_to(in_vect, RIGHT, buff=0.4)
result = NumericEmbedding(length=5)
result.match_height(matrix)
result.next_to(eq, RIGHT, buff=0.4)
result_label = Tex(R"\vec{Q}", font_size=48)
result_label.set_color(YELLOW)
result_label.next_to(result, UP, buff=0.5)
self.play(
FadeIn(eq),
FadeIn(result.get_brackets()),
)
# Animate row-by-vector products
rows = matrix.get_rows()
result_entries = result.get_entries()
vect_entries = in_vect.get_entries()
last_rects = VGroup()
for n, (row, entry) in enumerate(zip(rows, result_entries)):
if n == len(rows) - 2: # Skip ellipses row
self.add(entry)
continue
# Highlight current row and vector
row_rects = VGroup(SurroundingRectangle(r, buff=0.05) for r in row)
vect_rects = VGroup(SurroundingRectangle(v, buff=0.05) for v in vect_entries[:-2])
row_rects.set_stroke(YELLOW, 2)
vect_rects.set_stroke(YELLOW, 2)
# Compute actual dot product
row_vals = [r.get_value() for r in row if isinstance(r, DecimalNumber)]
vect_vals = [v.get_value() for v in vect_entries[:-2] if isinstance(v, DecimalNumber)]
dot_product = sum(a * b for a, b in zip(row_vals, vect_vals))
self.play(
ShowIncreasingSubsets(row_rects),
ShowIncreasingSubsets(vect_rects),
UpdateFromAlphaFunc(
entry,
lambda m, a, target=dot_product: m.set_value(target * a)
),
FadeOut(last_rects),
rate_func=linear,
run_time=0.8,
)
last_rects = VGroup(row_rects, vect_rects)
self.play(FadeOut(last_rects))
# Show result label
self.play(FadeIn(result_label, shift=0.25 * DOWN))
self.wait()
# Add explanation
explanation = Text(
"Each row produces one\nentry of the output",
font_size=36
)
explanation.to_edge(DOWN)
self.play(Write(explanation))
self.wait(2)
@@ -0,0 +1,223 @@
"""
Visualization distinguishing between weights (model parameters) and data.
Demonstrates: DecimalMatrix, VGroup organization, Transform animations
"""
from manimlib import *
import numpy as np
import random
def value_to_color(
value,
low_positive_color=BLUE_E,
high_positive_color=BLUE_B,
low_negative_color=RED_E,
high_negative_color=RED_B,
min_value=0.0,
max_value=10.0
):
"""Map a numeric value to a color gradient."""
alpha = clip(float(inverse_interpolate(min_value, max_value, abs(value))), 0, 1)
if value >= 0:
colors = (low_positive_color, high_positive_color)
else:
colors = (low_negative_color, high_negative_color)
return interpolate_color_by_hsl(*colors, alpha)
class WeightMatrix(DecimalMatrix):
"""A matrix displaying weight values with color coding."""
def __init__(
self,
values=None,
shape=(4, 6),
value_range=(-9.9, 9.9),
num_decimal_places=1,
**kwargs
):
if values is None:
values = np.random.uniform(*value_range, size=shape)
self.value_range = value_range
super().__init__(
values,
num_decimal_places=num_decimal_places,
**kwargs
)
self.color_entries()
def color_entries(self):
for entry in self.get_entries():
entry.set_fill(color=value_to_color(
entry.get_value(),
min_value=0,
max_value=max(abs(self.value_range[0]), abs(self.value_range[1])),
))
return self
class NumericVector(DecimalMatrix):
"""A column vector displaying numeric values."""
def __init__(
self,
values=None,
length=6,
value_range=(-9.9, 9.9),
num_decimal_places=1,
**kwargs
):
if values is None:
values = np.random.uniform(*value_range, size=(length, 1))
elif len(values.shape) == 1:
values = values.reshape((-1, 1))
super().__init__(
values,
num_decimal_places=num_decimal_places,
**kwargs
)
# Color entries from dark to light based on value
for entry in self.get_entries():
alpha = clip(inverse_interpolate(
value_range[0], value_range[1], abs(entry.get_value())
), 0, 1)
entry.set_fill(interpolate_color(GREY_C, WHITE, alpha))
class WeightsVsData(Scene):
def construct(self):
# Create titles
weights_title = Text("Weights", font_size=60, color=BLUE)
data_title = Text("Data", font_size=60, color=GREY_B)
weights_title.set_x(-FRAME_WIDTH / 4)
data_title.set_x(FRAME_WIDTH / 4)
for title in [weights_title, data_title]:
title.to_edge(UP, buff=0.5)
underline = Underline(title, stretch_factor=1.5)
underline.match_color(title)
title.add(underline)
# Create vertical divider
v_line = Line(UP, DOWN).set_height(5)
v_line.set_stroke(GREY_A, 2)
v_line.next_to(weights_title, DOWN, buff=0.5)
v_line.set_x(0)
# Create weight matrices (model parameters)
matrices = VGroup(*(
WeightMatrix(shape=(4, 5))
for _ in range(2)
))
matrices.arrange(DOWN, buff=0.5)
matrices.set_height(4)
matrices.next_to(weights_title, DOWN, buff=0.75)
# Create data vectors (what flows through the network)
vectors = VGroup(*(
NumericVector(length=5)
for _ in range(4)
))
vectors.arrange(RIGHT, buff=0.3)
vectors.set_height(3)
vectors.next_to(data_title, DOWN, buff=0.75)
# Animation: scatter numbers first, then organize
all_mat_entries = VGroup(*(
entry
for mat in matrices
for entry in mat.get_entries()
))
all_vec_entries = VGroup(*(
entry
for vec in vectors
for entry in vec.get_entries()
))
# Save final positions
for entry in [*all_mat_entries, *all_vec_entries]:
entry.final_pos = entry.get_center().copy()
# Scatter to random positions
all_entries = VGroup(*all_mat_entries, *all_vec_entries)
all_entries.shuffle()
for entry in all_entries:
entry.move_to([
random.uniform(-7, 7),
random.uniform(-3, 3),
0
])
entry.set_height(0.15)
# Start animation
self.add(all_entries)
self.wait(0.5)
# Animate gathering
self.play(
LaggedStart(*(
entry.animate.move_to(entry.final_pos).set_height(0.25)
for entry in all_mat_entries
), lag_ratio=0.02),
ShowCreation(v_line),
run_time=2
)
self.play(
Write(weights_title),
*(FadeIn(mat.get_brackets()) for mat in matrices),
)
self.play(
LaggedStart(*(
entry.animate.move_to(entry.final_pos).set_height(0.25)
for entry in all_vec_entries
), lag_ratio=0.02),
run_time=2
)
self.play(
Write(data_title),
*(FadeIn(vec.get_brackets()) for vec in vectors),
)
self.wait()
# Add subtitles
weights_sub = Text("Fixed during inference", font_size=30)
weights_sub.next_to(matrices, DOWN, buff=0.3)
data_sub = Text("Flows through network", font_size=30)
data_sub.next_to(vectors, DOWN, buff=0.3)
self.play(
FadeIn(weights_sub, shift=UP),
FadeIn(data_sub, shift=UP),
)
self.wait()
# Show data flowing (animate vectors changing)
for _ in range(3):
new_vectors = VGroup(*(
NumericVector(length=5)
for _ in range(4)
))
new_vectors.arrange(RIGHT, buff=0.3)
new_vectors.set_height(3)
new_vectors.move_to(vectors)
self.play(
Transform(vectors, new_vectors),
run_time=1.5
)
self.wait(0.5)
# Final emphasis
weights_rect = SurroundingRectangle(matrices, color=BLUE, buff=0.2)
data_rect = SurroundingRectangle(vectors, color=GREY_B, buff=0.2)
self.play(
ShowCreation(weights_rect),
ShowCreation(data_rect),
)
self.wait(2)
@@ -0,0 +1,190 @@
"""
Word Vector Analogy Visualization
Demonstrates the famous king - man + woman = queen analogy in embedding space.
Based on: videos/_2024/transformers/embedding.py - KingQueenExample
"""
from manimlib import *
class WordVectorAnalogy(InteractiveScene):
"""
Visualizes word vector arithmetic in 3D space.
Shows how semantic relationships are encoded as directions.
"""
def construct(self):
# Set up 3D scene
frame = self.frame
frame.reorient(-20, 70, 0)
frame.add_ambient_rotation(2 * DEGREES)
# Create axes
axes = ThreeDAxes(
x_range=(-4, 4, 1),
y_range=(-4, 4, 1),
z_range=(-3, 3, 1),
width=8,
height=8,
depth=6,
)
axes.set_stroke(width=2)
self.add(axes)
# Add plane for reference
plane = NumberPlane(
axes.x_range[:2], axes.y_range[:2],
width=axes.get_width(),
height=axes.get_height(),
background_line_style=dict(
stroke_color=GREY,
stroke_width=1,
),
faded_line_style=dict(
stroke_opacity=0.25,
stroke_width=0.5,
),
faded_line_ratio=1,
)
plane.rotate(90 * DEGREES, LEFT)
self.add(plane)
# Define word positions (simplified for demo)
word_data = {
"man": {"pos": np.array([1, -1, 0.5]), "color": BLUE_B},
"woman": {"pos": np.array([1, 1, 0.5]), "color": RED_B},
"king": {"pos": np.array([-2, -1, 1.5]), "color": BLUE_D},
"queen": {"pos": np.array([-2, 1, 1.5]), "color": RED_D},
}
def create_labeled_arrow(word, pos, color):
"""Create an arrow with a word label."""
arrow = Arrow(
axes.get_origin(),
axes.c2p(*pos),
buff=0,
stroke_color=color,
stroke_width=4,
)
arrow.set_flat_stroke(False)
label = Text(word, font_size=30)
label.set_backstroke(BLACK, 3)
label.next_to(arrow.get_end(), normalize(arrow.get_vector()), buff=0.1)
label.rotate(90 * DEGREES, RIGHT) # Orient for 3D
return arrow, label
# Create all word vectors
vectors = {}
labels = {}
for word, data in word_data.items():
arrow, label = create_labeled_arrow(word, data["pos"], data["color"])
vectors[word] = arrow
labels[word] = label
# Show equation (fixed in frame)
equation = Tex(
R"\text{woman} - \text{man} \approx \text{queen} - \text{king}",
font_size=42
)
equation.fix_in_frame()
equation.to_corner(UR)
equation["woman"].set_color(RED_B)
equation["man"].set_color(BLUE_B)
equation["queen"].set_color(RED_D)
equation["king"].set_color(BLUE_D)
top_rect = FullScreenFadeRectangle().set_fill(BLACK, 0.7)
top_rect.set_height(1.2, about_edge=UP, stretch=True)
top_rect.fix_in_frame()
# Animate man and woman vectors
self.play(
GrowArrow(vectors["man"]),
FadeIn(labels["man"]),
GrowArrow(vectors["woman"]),
FadeIn(labels["woman"]),
run_time=2
)
self.wait()
# Show difference vector (man -> woman)
diff = Arrow(
vectors["man"].get_end(),
vectors["woman"].get_end(),
buff=0,
stroke_color=YELLOW,
stroke_width=4,
)
diff.set_flat_stroke(False)
self.play(GrowArrow(diff))
self.wait()
# Show equation
self.add(top_rect)
self.play(Write(equation))
self.wait()
# Add king and queen
self.play(
GrowArrow(vectors["king"]),
FadeIn(labels["king"]),
run_time=1.5
)
# Show the same difference applied to king
king_to_queen = diff.copy()
king_to_queen.shift(vectors["king"].get_end() - vectors["man"].get_end())
self.play(TransformFromCopy(diff, king_to_queen))
self.wait()
# Show queen at the tip
self.play(
GrowArrow(vectors["queen"]),
FadeIn(labels["queen"]),
)
self.wait()
# Rotate to show the relationship
frame.clear_updaters()
self.play(
frame.animate.reorient(-100, 20, 100),
run_time=4
)
frame.add_ambient_rotation(2 * DEGREES)
# Flash the gender direction
gender_dir = diff.get_vector()
lines = Line(ORIGIN, 1.5 * normalize(gender_dir)).replicate(100)
lines.insert_n_curves(20)
lines.set_stroke(YELLOW, 3)
for line in lines:
line.move_to(np.random.uniform(-2, 2, 3))
self.play(
LaggedStartMap(
VShowPassingFlash, lines,
lag_ratio=1 / len(lines),
run_time=3
)
)
# Add direction label
dir_label = Text("Gender direction", font_size=36, color=YELLOW)
dir_label.fix_in_frame()
dir_label.next_to(equation, DOWN, buff=0.5)
self.play(Write(dir_label))
self.wait(3)
# Show another example
new_eq = Tex(
R"\text{uncle} - \text{aunt} \approx \text{man} - \text{woman}",
font_size=36
)
new_eq.fix_in_frame()
new_eq.next_to(dir_label, DOWN, buff=0.3)
self.play(Write(new_eq))
self.wait(5)
@@ -0,0 +1,367 @@
"""
Zone Plate / Fresnel Zone Plate Visualization
Demonstrates the creation and properties of a Fresnel zone plate,
which is the simplest form of hologram - recording interference
between a point source and a reference wave.
Based on 3Blue1Brown's hologram visualizations.
Run: manimgl zone_plate_hologram.py ZonePlateCreation -w
"""
from manimlib import *
import numpy as np
class ZonePlateCreation(Scene):
"""
Shows how a zone plate pattern emerges from interference
between a point source and a plane reference wave.
"""
def construct(self):
frame = self.camera.frame
# Title
title = Text("Fresnel Zone Plate", font_size=48)
title.to_edge(UP)
title.set_backstroke(BLACK, 5)
self.add(title)
# Parameters
source_distance = 4.0 # Distance of point source from plate
wavelength = 0.3
plate_size = 6.0
# Point source position (behind the plate plane)
source_pos = np.array([0, 0, source_distance])
# Create zone plate pattern
def get_zone_plate(resolution=200):
plate = VGroup()
# Sample grid on the plate
for i in range(resolution):
for j in range(resolution):
x = (i / resolution - 0.5) * plate_size
y = (j / resolution - 0.5) * plate_size
point = np.array([x, y, 0])
# Distance from point source
r = np.linalg.norm(point - source_pos)
# Phase from point source
phase_obj = (r / wavelength) % 1
# Phase from reference (plane wave from behind)
phase_ref = (source_distance / wavelength) % 1
# Interference pattern intensity
phase_diff = (phase_obj - phase_ref) * TAU
intensity = (1 + np.cos(phase_diff)) / 2
# Create small square
size = plate_size / resolution * 1.1
square = Square(side_length=size)
square.move_to([x, y, 0])
square.set_stroke(width=0)
square.set_fill(
interpolate_color(BLACK, WHITE, intensity),
opacity=1
)
plate.add(square)
return plate
# Create the pattern with increasing resolution
low_res = get_zone_plate(30)
self.play(FadeIn(low_res, lag_ratio=0.001))
self.wait()
# Show it's made of concentric rings
ring_explanation = Text("Concentric rings from interference", font_size=32)
ring_explanation.next_to(title, DOWN)
ring_explanation.set_backstroke(BLACK, 3)
self.play(Write(ring_explanation))
self.wait()
# Increase resolution
mid_res = get_zone_plate(60)
self.play(
ReplacementTransform(low_res, mid_res),
run_time=2
)
self.wait()
high_res = get_zone_plate(100)
self.play(
ReplacementTransform(mid_res, high_res),
run_time=2
)
self.wait(2)
class ZonePlateFromPointSource(Scene):
"""
Shows the geometry of how zone plates form from a point source.
"""
def construct(self):
frame = self.camera.frame
frame.reorient(20, 70, 0)
# 3D setup
axes = ThreeDAxes(
x_range=[-4, 4, 1],
y_range=[-4, 4, 1],
z_range=[0, 5, 1],
)
axes.set_opacity(0.3)
# Point source
source_z = 4.0
source = Sphere(radius=0.15, color=WHITE)
source.move_to([0, 0, source_z])
source_label = Text("Point Source", font_size=24)
source_label.rotate(PI/2, RIGHT)
source_label.next_to(source, OUT + UP, buff=0.3)
source_label.set_backstroke(BLACK, 3)
# Film plane
film = Square(side_length=6)
film.set_fill(GREY_E, opacity=0.5)
film.set_stroke(WHITE, 1)
film.move_to(ORIGIN)
film_label = Text("Film Plane", font_size=24)
film_label.next_to(film, DOWN)
film_label.set_backstroke(BLACK, 3)
# Wavefronts from point source (spherical shells)
def get_spherical_waves(time, n_waves=6):
waves = Group()
for i in range(n_waves):
radius = 0.5 + i * 0.8 + time * 0.2
if radius < 6:
sphere = Sphere(radius=radius)
sphere.move_to([0, 0, source_z])
sphere.set_color(BLUE)
sphere.set_opacity(0.15 * (1 - radius / 6))
waves.add(sphere)
return waves
time_tracker = ValueTracker(0)
waves = always_redraw(lambda: get_spherical_waves(time_tracker.get_value()))
# Reference wave fronts (planes)
def get_plane_waves(time, n_waves=8):
planes = Group()
for i in range(n_waves):
z = source_z - 0.5 - i * 0.8 + time * 0.2
if 0 < z < source_z:
plane = Square(side_length=8)
plane.set_fill(TEAL, opacity=0.1 * (z / source_z))
plane.set_stroke(TEAL, 1, opacity=0.3)
plane.move_to([0, 0, z])
planes.add(plane)
return planes
ref_waves = always_redraw(lambda: get_plane_waves(time_tracker.get_value()))
self.add(axes)
self.add(film, film_label)
self.add(source, source_label)
self.add(waves)
self.add(ref_waves)
# Animate waves
self.play(
time_tracker.animate.set_value(10),
frame.animate.increment_theta(30 * DEGREES),
run_time=10,
rate_func=linear
)
self.wait()
class ZonePlateAsLens(Scene):
"""
Demonstrates how a zone plate acts as a lens, focusing light.
"""
def construct(self):
# Title
title = Text("Zone Plate as Focusing Element", font_size=42)
title.to_edge(UP)
title.set_backstroke(BLACK, 5)
self.add(title)
# Zone plate representation
plate_x = -2
plate = VGroup()
n_rings = 12
for i in range(n_rings):
r_outer = 0.15 * (i + 1)
r_inner = 0.15 * i if i > 0 else 0
if i % 2 == 0:
ring = Annulus(inner_radius=r_inner, outer_radius=r_outer)
ring.set_fill(GREY_D, opacity=1)
ring.set_stroke(width=0)
plate.add(ring)
else:
ring = Annulus(inner_radius=r_inner, outer_radius=r_outer)
ring.set_fill(WHITE, opacity=0.8)
ring.set_stroke(width=0)
plate.add(ring)
plate.move_to([plate_x, 0, 0])
plate_label = Text("Zone Plate", font_size=24)
plate_label.next_to(plate, DOWN)
# Focal point
focal_x = 3
focal_point = Dot([focal_x, 0, 0], color=YELLOW, radius=0.15)
focal_label = Text("Focus", font_size=24, color=YELLOW)
focal_label.next_to(focal_point, DOWN)
# Incoming parallel rays
incoming_rays = VGroup()
ray_positions = np.linspace(-1.5, 1.5, 7)
for y in ray_positions:
ray = Arrow([-6, y, 0], [plate_x - 0.2, y, 0], buff=0, stroke_width=2)
ray.set_color(BLUE)
incoming_rays.add(ray)
# Diffracted rays converging to focus
diffracted_rays = VGroup()
for y in ray_positions:
ray = Line([plate_x + 0.2, y, 0], [focal_x, 0, 0])
ray.set_stroke(RED, 2)
diffracted_rays.add(ray)
self.add(plate, plate_label)
self.play(
LaggedStartMap(GrowArrow, incoming_rays, lag_ratio=0.1),
run_time=2
)
self.wait()
incoming_label = Text("Parallel Light", font_size=24, color=BLUE)
incoming_label.next_to(incoming_rays, UP)
self.play(Write(incoming_label))
self.wait()
# Show diffraction
self.play(
LaggedStartMap(ShowCreation, diffracted_rays, lag_ratio=0.1),
FadeIn(focal_point),
Write(focal_label),
run_time=2
)
self.wait()
# Explanation
explanation = Text(
"Zone plate diffracts light to focal point",
font_size=28
)
explanation.next_to(title, DOWN)
self.play(Write(explanation))
self.wait(2)
class InterferenceBands(Scene):
"""
Shows the intensity pattern resulting from two-wave interference.
A simplified representation of holographic recording.
"""
def construct(self):
# Title
title = Text("Interference Pattern on Film", font_size=42)
title.to_edge(UP)
title.set_backstroke(BLACK, 5)
self.add(title)
# Parameters
wavelength = 0.4
angle = 15 * DEGREES # Angle between reference and object beams
# Create interference pattern
def get_interference_bands(width=12, height=6, resolution=200):
bands = VGroup()
# The spacing of fringes depends on the angle between beams
fringe_spacing = wavelength / (2 * np.sin(angle / 2))
for i in range(resolution):
x = (i / resolution - 0.5) * width
# Intensity from interference
intensity = (1 + np.cos(TAU * x / fringe_spacing)) / 2
# Create vertical strip
strip = Rectangle(width=width / resolution * 1.05, height=height)
strip.move_to([x, 0, 0])
strip.set_stroke(width=0)
strip.set_fill(
interpolate_color(BLACK, WHITE, intensity),
opacity=1
)
bands.add(strip)
return bands
bands = get_interference_bands()
border = Rectangle(width=12, height=6)
border.set_stroke(WHITE, 2)
self.play(FadeIn(bands), ShowCreation(border))
self.wait()
# Labels
spacing_label = Text("Fringe spacing depends on beam angle", font_size=28)
spacing_label.next_to(border, DOWN, buff=0.5)
formula = Tex(
R"d = \frac{\lambda}{2\sin(\theta/2)}",
font_size=36
)
formula.next_to(spacing_label, DOWN)
self.play(Write(spacing_label))
self.play(Write(formula))
self.wait(2)
# Show changing angle effect
angle_label = Text("Decreasing angle = wider fringes", font_size=24)
angle_label.to_corner(DR)
for new_angle in [10 * DEGREES, 5 * DEGREES]:
wavelength_local = wavelength
fringe_spacing = wavelength_local / (2 * np.sin(new_angle / 2))
new_bands = VGroup()
for i in range(200):
x = (i / 200 - 0.5) * 12
intensity = (1 + np.cos(TAU * x / fringe_spacing)) / 2
strip = Rectangle(width=12 / 200 * 1.05, height=6)
strip.move_to([x, 0, 0])
strip.set_stroke(width=0)
strip.set_fill(
interpolate_color(BLACK, WHITE, intensity),
opacity=1
)
new_bands.add(strip)
self.play(
Transform(bands, new_bands),
FadeIn(angle_label) if new_angle == 10 * DEGREES else Animation(angle_label),
run_time=2
)
self.wait()
self.wait()
@@ -0,0 +1,94 @@
# Equation Transforms - Reference Guide
**Example file**: `examples/equation_transforms.py`
## User Query Scenarios
This example addresses queries like:
- "Show step-by-step equation derivation"
- "Animate the quadratic formula derivation"
- "Highlight parts of an equation"
- "Show variable substitution with color tracking"
- "Add braces to explain equation parts"
## Scene Thinking Process (3b1b Style)
### 1. Core Concept
**Mathematical Derivations**: Step-by-step equation manipulation is clearer when terms are color-coded and transformations are animated smoothly.
### 2. Technical Implementation
#### Color-Coded Terms with t2c
```python
eq = Tex(
r"ax^2 + bx + c = 0",
t2c={"a": RED, "b": GREEN, "c": BLUE, "x": YELLOW}
)
```
#### Smooth Equation Transformation
```python
eq1 = Tex(r"ax^2 + bx + c = 0", t2c=colors)
eq2 = Tex(r"x^2 + \frac{b}{a}x + \frac{c}{a} = 0", t2c=colors)
self.play(TransformMatchingTex(eq1.copy(), eq2))
```
**Key insight**: `TransformMatchingTex` matches characters between equations and morphs them smoothly.
#### Highlighting with SurroundingRectangle
```python
part = eq[r"a^2"] # Select by tex string
rect = SurroundingRectangle(part, color=RED, buff=0.05)
self.play(ShowCreation(rect))
```
#### Brace Annotations
```python
brace = Brace(eq["F"], UP, color=BLUE)
label = brace.get_text("Force", font_size=30)
self.play(GrowFromCenter(brace), FadeIn(label, UP))
```
### 3. Scene Variants
| Scene | Purpose |
|-------|---------|
| `QuadraticFormula` | Full derivation with step labels |
| `HighlightAndTransform` | Highlighting + visual proof |
| `BraceAnnotations` | F=ma with labeled parts |
| `ColorCodedSubstitution` | u-substitution with tracking |
## Key Patterns
### Pattern: Step Labels
```python
step_label = Text("Divide by a", font_size=24, color=GREY)
step_label.next_to(eq2, LEFT, buff=0.5)
self.play(FadeIn(step_label, LEFT))
```
### Pattern: Final Answer Box
```python
final = Tex(r"x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}")
box = SurroundingRectangle(final, color=GOLD, buff=0.2)
self.play(ShowCreation(box))
```
### Pattern: Selecting Equation Parts
```python
# By tex substring
eq["x^2"] # Returns submobject matching "x^2"
eq[r"\frac{b}{a}"] # LaTeX commands work too
# By index
eq[0] # First character/group
```
## Run Commands
```bash
manimgl equation_transforms.py QuadraticFormula -w
manimgl equation_transforms.py HighlightAndTransform -w
manimgl equation_transforms.py BraceAnnotations -w
manimgl equation_transforms.py ColorCodedSubstitution -w
```
@@ -0,0 +1,83 @@
# Integration Visualization - Reference Guide
**Example file**: `examples/integration_visualization.py`
## User Query Scenarios
This example addresses queries like:
- "Show the area under a curve"
- "Visualize Riemann sums converging to integral"
- "Animate definite integral accumulation"
- "Show integral of e^(-x) equals 1"
## Scene Thinking Process (3b1b Style)
### 1. Core Concept
**Definite Integral**: The integral ∫f(x)dx represents accumulated area under curve f(x). Riemann sums with shrinking rectangles converge to the true integral.
### 2. Technical Implementation
#### Animated Area Fill (Using Polygon)
```python
def get_area_polygon():
t = t_tracker.get_value()
xs = np.linspace(0, t, 50)
# Points along curve
points = [axes.c2p(x, f(x)) for x in xs]
# Close the polygon along x-axis
points.append(axes.c2p(t, 0))
points.append(axes.c2p(0, 0))
poly = Polygon(*points)
poly.set_fill(BLUE_E, opacity=0.5)
poly.set_stroke(width=0)
return poly
area = always_redraw(get_area_polygon)
```
**Key insight**: ManimGL doesn't have `axes.get_area()`, so build polygons manually from curve points.
#### Riemann Sum Rectangles
```python
for i in range(n):
x = start + i * dx
height = f(x)
rect = Rectangle(
width=dx * axes.x_axis.get_unit_size(),
height=height * axes.y_axis.get_unit_size(),
)
rect.move_to(axes.c2p(x + dx/2, height/2))
```
### 3. Scene Variants
| Scene | Purpose |
|-------|---------|
| `AreaUnderCurve` | Basic accumulating area animation |
| `RiemannSums` | Rectangles converging (n=4,8,16,32) |
| `ExponentialDecay` | ∫e^(-x)dx = 1 with live area counter |
## Key Patterns
### Pattern: Live Value Display
```python
value_label = Tex(r"\text{Area} \approx 0.00")
value_num = value_label.make_number_changeable("0.00")
value_num.add_updater(lambda m: m.set_value(computed_area))
```
### Pattern: Progressive Rectangle Refinement
```python
for n in [4, 8, 16, 32]:
new_rects = create_rectangles(n)
self.play(ReplacementTransform(current_rects, new_rects))
current_rects = new_rects
```
## Run Commands
```bash
manimgl integration_visualization.py AreaUnderCurve -w
manimgl integration_visualization.py RiemannSums -w
manimgl integration_visualization.py ExponentialDecay -w
```
@@ -0,0 +1,149 @@
# Parallax Starfield - Reference Guide
**Example file**: `examples/parallax_starfield.py`
## User Query Scenarios
This example addresses queries like:
- "Show how parallax works with stars"
- "Create a 3D scene demonstrating depth perception"
- "Animate an observer moving through a starfield"
- "Explain stellar parallax visually"
- "Show why nearby objects move more than distant ones when you move"
## Scene Thinking Process (3b1b Style)
### 1. Identify the Core Concept
**Parallax**: When an observer moves, nearby objects appear to shift more against the background than distant objects. This is how astronomers measure distances to nearby stars.
### 2. Visual Design Decisions
**Why stars/dots instead of complex objects?**
- Stars naturally exist at varying distances
- Dots are computationally efficient (GlowDots handles 200+ easily)
- The effect is clear without distraction from object shapes
**Why a reference cube?**
- Provides spatial context in 3D
- Helps viewer understand the volume where stars exist
- The wireframe doesn't obscure the stars
**Why use a Pi creature as observer?**
- Makes the scene relatable - you're watching someone observe
- Their movement is intuitive to understand
- Can show reactions with `observer.change("pondering")`
### 3. Technical Implementation
#### GlowDots for Efficient Star Rendering
```python
# Random 3D positions
star_positions = np.random.uniform(-1, 1, (n_stars, 3))
stars = GlowDots(star_positions)
stars.set_glow_factor(2) # Soft bloom effect
stars.set_radii(np.random.uniform(0, 0.075, n_stars)) # Varying sizes
```
**Key insight**: `GlowDots` is far more efficient than creating individual `Dot` objects. For 200+ points, this is essential.
#### 3D Camera Control
```python
frame = self.frame
self.set_floor_plane("xz") # Z is now vertical
# Smooth camera reorientation
self.play(frame.animate.reorient(-40, -26, 0), run_time=2)
```
**Why `set_floor_plane("xz")`?** In astronomy visualizations, we often want Z as the vertical axis. This call reconfigures the coordinate system.
#### Observer Movement Pattern
```python
for dy in [1.5, -3, 3, -3, 1.5]:
self.play(observer.animate.shift(dy * IN), run_time=3)
```
**Why this specific pattern?**
- `[1.5, -3, 3, -3, 1.5]` creates: up → down → up → down → center
- The viewer sees the full range of parallax shift
- Returns to starting position for clean looping if needed
### 4. Scene Variants
The example includes three variants showing progressive complexity:
| Scene | Purpose | When to Use |
|-------|---------|-------------|
| `ParallaxStarfield` | Basic effect, third-person view | General explanation |
| `ParallaxFromObserverPOV` | First-person perspective | "What would you see?" |
| `LayeredParallax` | Explicit distance layers | Teaching the concept clearly |
## Key Patterns Demonstrated
### Pattern: Frame Following an Object
```python
frame.always.match_z(observer)
```
The camera's Z position continuously matches the observer, creating a first-person view.
### Pattern: Layered Depth for Clarity
```python
colors = [RED, YELLOW, BLUE]
distances = [2, 5, 10]
```
Using distinct colors at specific distances makes the parallax effect unmistakably clear for educational purposes.
### Pattern: Smooth Lateral Movement
```python
self.play(
observer.animate.shift(dx * RIGHT),
run_time=3,
rate_func=smooth
)
```
Slow, smooth movement lets viewers track individual stars and observe the effect.
## Common Modifications
### Add More Stars
```python
n_stars = 500 # Increase count
stars.set_radii(np.random.uniform(0, 0.05, n_stars)) # Smaller radii for density
```
### Different Star Colors
```python
# Temperature-based star colors
colors = [RED, ORANGE, YELLOW, WHITE, BLUE_A]
for i, star in enumerate(stars):
star.set_color(random.choice(colors))
```
### Add Background Galaxy
```python
background = ImageMobject("milky_way.png")
background.set_height(20)
background.shift(50 * OUT) # Far behind stars
self.add(background)
```
## Output
When rendered, this produces:
- A 3D starfield within a blue wireframe cube
- An observer (Randolph) that moves up/down
- Stars appearing to shift differently based on distance
- Clear demonstration of the parallax principle
## Run Commands
```bash
# Full render
manimgl parallax_starfield.py ParallaxStarfield -w
# Preview (no file output)
manimgl parallax_starfield.py ParallaxStarfield -p
# All three scenes
manimgl parallax_starfield.py ParallaxStarfield ParallaxFromObserverPOV LayeredParallax -w
```
@@ -0,0 +1,96 @@
# Rotating Exponentials - Reference Guide
**Example file**: `examples/rotating_exponentials.py`
## User Query Scenarios
This example addresses queries like:
- "Visualize e^(it) on the complex plane"
- "Show Euler's formula animation"
- "Demonstrate how cosine comes from rotating exponentials"
- "Create a complex plane with rotating vector"
- "Show e^(iπ) = -1 visually"
## Scene Thinking Process (3b1b Style)
### 1. Core Concept
**Euler's Formula**: `e^(it) = cos(t) + i·sin(t)` - a rotating unit vector in the complex plane. Two counter-rotating exponentials sum to give real cosine.
### 2. Visual Design Decisions
**Why use ComplexPlane?**
- Natural coordinate system for complex numbers
- Built-in grid and labels
- `n2p()` method converts complex to point
**Why show the traced path?**
- Reveals the unit circle emerges naturally
- Shows the relationship between angle and position
### 3. Technical Implementation
#### Rotating Vector with TracedPath
```python
time_tracker = ValueTracker(0)
vector = Vector(RIGHT, color=YELLOW)
vector.add_updater(lambda v: v.put_start_and_end_on(
ORIGIN,
plane.n2p(np.exp(1j * time_tracker.get_value()))
))
tip_dot = Dot(color=YELLOW)
tip_dot.add_updater(lambda d: d.move_to(vector.get_end()))
traced = TracedPath(tip_dot.get_center, stroke_color=BLUE)
```
#### Counter-Rotating for Cosine
```python
# e^(it) rotates counter-clockwise
v1.add_updater(lambda v: v.put_start_and_end_on(
ORIGIN, plane.n2p(np.exp(1j * t))
))
# e^(-it) rotates clockwise
v2.add_updater(lambda v: v.put_start_and_end_on(
ORIGIN, plane.n2p(np.exp(-1j * t))
))
# Sum is always real: 2cos(t)
```
### 4. Scene Variants
| Scene | Purpose |
|-------|---------|
| `RotatingExponential` | Basic e^(it) visualization |
| `CounterRotatingExponentials` | Shows e^(it) + e^(-it) = 2cos(t) |
| `EulersFormula` | Famous e^(iπ) = -1 |
| `ComplexExponentialSpiral` | Decaying spiral e^((a+bi)t) |
## Key Patterns
### Pattern: always_redraw for Arcs
```python
angle_arc = always_redraw(lambda: Arc(
start_angle=0,
angle=time_tracker.get_value() % TAU,
radius=0.3,
color=GREEN
))
```
### Pattern: Complex Number to Point
```python
# Using ComplexPlane.n2p() (number to point)
point = plane.n2p(1 + 2j) # Complex number
point = plane.n2p(np.exp(1j * theta)) # Euler form
```
## Run Commands
```bash
manimgl rotating_exponentials.py RotatingExponential -w
manimgl rotating_exponentials.py CounterRotatingExponentials -w
manimgl rotating_exponentials.py EulersFormula -w
manimgl rotating_exponentials.py ComplexExponentialSpiral -w
```
@@ -0,0 +1,109 @@
# Spring-Mass System - Reference Guide
**Example file**: `examples/spring_mass_system.py`
## User Query Scenarios
This example addresses queries like:
- "Create a spring animation with oscillation"
- "Show damped harmonic motion"
- "Visualize physics simulation with a mass on a spring"
- "Animate a spring-mass system with real-time graph"
- "Compare different damping coefficients"
## Scene Thinking Process (3b1b Style)
### 1. Identify the Core Concept
**Damped Harmonic Motion**: A mass attached to a spring oscillates, with amplitude decreasing over time due to friction/damping. The equation is: `x'' = -kx - μv`
### 2. Visual Design Decisions
**Why a parametric helix for the spring?**
- Looks realistic with 3D coils
- Stretches naturally when mass moves
- Uses `ParametricCurve` for smooth rendering
**Why track position on a number line?**
- Gives quantitative feedback
- Shows exact displacement values
- Easy to understand motion direction
### 3. Technical Implementation
#### Creating a Self-Contained Physics Component
```python
class SpringMassSystem(VGroup):
def __init__(self, x0=0, v0=0, k=3, mu=0.1, ...):
# Store physics state
self.k = k
self.mu = mu
self.velocity = v0
# Add physics updater
self.add_updater(lambda m, dt: m.time_step(dt))
```
**Key insight**: Encapsulate physics + visuals in one VGroup subclass. This makes it reusable and keeps animation code clean.
#### Physics Integration (Euler Method)
```python
def time_step(self, delta_t, dt_size=0.01):
state = [self.get_x(), self.velocity]
for _ in range(sub_steps):
x, v = state
acceleration = -self.k * x - self.mu * v
state[0] += v * true_dt
state[1] += acceleration * true_dt
```
#### Dynamic Velocity/Force Vectors
```python
def get_velocity_vector(self, scale_factor=0.5, color=GREEN):
vector = Vector(RIGHT, fill_color=color)
vector.add_updater(lambda m: m.put_start_and_end_on(
self.mass.get_center(),
self.mass.get_center() + scale_factor * self.velocity * RIGHT
))
return vector
```
### 4. Scene Variants
| Scene | Purpose |
|-------|---------|
| `SpringMassDemo` | Basic oscillation with velocity/force vectors |
| `SpringWithGraph` | Real-time x(t) graph using TracedPath |
| `MultipleSprings` | Compare different damping values |
## Key Patterns Demonstrated
### Pattern: Pausable Physics
```python
def pause(self):
self._is_running = False
def unpause(self):
self._is_running = True
```
### Pattern: TracedPath for Graphs
```python
tracking_point = Point()
tracking_point.add_updater(lambda p: p.move_to(
axes.c2p(time_tracker.get_value(), spring.get_x())
))
position_graph = TracedPath(tracking_point.get_center, stroke_color=BLUE)
```
## Run Commands
```bash
# Basic demo
manimgl spring_mass_system.py SpringMassDemo -w
# With real-time graph
manimgl spring_mass_system.py SpringWithGraph -w
# Compare damping
manimgl spring_mass_system.py MultipleSprings -w
```
@@ -0,0 +1,113 @@
# 3D Surfaces - Reference Guide
**Example file**: `examples/three_d_surfaces.py`
## User Query Scenarios
This example addresses queries like:
- "Create a 3D surface visualization"
- "Show a parametric surface"
- "Animate camera rotation around object"
- "Create a torus/sphere/cone"
- "Show saddle surface"
## Scene Thinking Process (3b1b Style)
### 1. Core Concept
**Parametric Surfaces**: Define surfaces as functions (u,v) → (x,y,z). Camera movement reveals 3D structure.
### 2. Technical Implementation
#### Basic Parametric Surface
```python
surface = ParametricSurface(
lambda u, v: [u, v, np.sin(u) * np.cos(v)],
u_range=(-3, 3),
v_range=(-3, 3),
resolution=(30, 30),
)
surface.set_color(BLUE)
surface.set_opacity(0.8)
```
#### Camera Setup and Movement
```python
frame = self.frame
frame.reorient(-30, 70, 0) # phi, theta, gamma
frame.set_height(10)
# Animate camera
self.play(frame.animate.reorient(30, 60, 0), run_time=3)
```
#### Sphere with Latitude/Longitude Lines
```python
# Latitude lines
for phi in np.linspace(-PI/2 + 0.3, PI/2 - 0.3, 6):
line = ParametricCurve(
lambda t: radius * np.array([
np.cos(t) * np.cos(phi),
np.sin(t) * np.cos(phi),
np.sin(phi)
]),
t_range=(0, TAU),
)
```
#### Torus Parameterization
```python
R, r = 2, 0.7 # Major and minor radius
torus = ParametricSurface(
lambda u, v: [
(R + r * np.cos(v)) * np.cos(u),
(R + r * np.cos(v)) * np.sin(u),
r * np.sin(v)
],
u_range=(0, TAU),
v_range=(0, TAU),
)
```
### 3. Scene Variants
| Scene | Purpose |
|-------|---------|
| `ParametricSurface3D` | z = sin(x)cos(y) with camera orbit |
| `SphereSurface` | Sphere with grid lines, rotating |
| `ConeUnfolding` | 3D cone visualization |
| `SaddleSurface` | z = x² - y² with cross-sections |
| `TorusSurface` | Donut shape with rotation |
## Key Patterns
### Pattern: ThreeDAxes
```python
axes = ThreeDAxes(
x_range=(-3, 3, 1),
y_range=(-3, 3, 1),
z_range=(-2, 2, 1),
)
```
### Pattern: Rotating Objects
```python
self.play(
Rotate(surface, TAU, axis=UP, run_time=6, rate_func=linear),
)
```
### Pattern: Frame Reorientation
```python
# reorient(phi, theta, gamma, center, height)
frame.reorient(-30, 70, 0) # Just angles
frame.animate.reorient(60, 60, 0) # Animated
```
## Run Commands
```bash
manimgl three_d_surfaces.py ParametricSurface3D -w
manimgl three_d_surfaces.py SphereSurface -w
manimgl three_d_surfaces.py TorusSurface -w
manimgl three_d_surfaces.py SaddleSurface -w
```
@@ -0,0 +1,90 @@
# Transit Animations - Reference Guide
**Example file**: `examples/transit_animation.py`
## User Query Scenarios
This example addresses queries like:
- "Create a planet transit animation"
- "Show loading dots animation"
- "Animate a pendulum swing"
- "Create wave propagation"
- "Show orbital motion"
## Scene Thinking Process (3b1b Style)
### 1. Core Concept
**Transit/Periodic Motion**: Objects moving along paths, leaving traces, showing periodic behavior. Used for astronomical transits, loading indicators, physics demos.
### 2. Technical Implementation
#### Transit with Snapshots
```python
venus.add_updater(lambda m, dt: m.shift(dt * velocity * RIGHT))
copies = VGroup()
for _ in range(n_snapshots):
self.wait(wait_time)
copies.add(venus.copy().clear_updaters())
self.play(Transform(copies, VGroup(path))) # Collapse to line
```
#### Orbital Motion with Depth Effect
```python
def update_planet(p):
a = angle.get_value()
x = 2.5 * np.cos(a)
y = 0.5 * np.sin(a) # Compressed y = tilted orbit
p.move_to([x, y, 0])
# Size varies with "depth"
scale = 0.12 + 0.06 * np.sin(a)
p.set_width(2 * scale)
```
#### Phase-Shifted Oscillation (Loading Dots)
```python
for i, dot in enumerate(dots):
phase = i * TAU / n_dots
dot.add_updater(lambda m, p=phase: m.set_y(
original_y + 0.3 * np.sin(3 * time.get_value() + p)
))
```
#### Pendulum Physics
```python
omega = np.sqrt(g / length) # Natural frequency
amplitude = PI / 4
theta.add_updater(lambda m: m.set_value(
amplitude * np.cos(omega * time.get_value()) * np.exp(-0.05 * time.get_value())
))
```
### 3. Scene Variants
| Scene | Purpose |
|-------|---------|
| `TransitOfVenus` | Historical astronomical transit |
| `OrbitalTransit` | Exoplanet-style orbit with depth |
| `LoadingDots` | Classic loading animation |
| `WaveTransit` | Wave pulse propagation |
| `PendulumSwing` | Damped pendulum with trail |
## Key Patterns
### Pattern: Copy and Freeze
```python
copy = mobject.copy().clear_updaters() # Snapshot current state
```
### Pattern: Continuous Time Updater
```python
time = ValueTracker(0)
time.add_updater(lambda m, dt: m.increment_value(dt))
```
## Run Commands
```bash
manimgl transit_animation.py TransitOfVenus -w
manimgl transit_animation.py LoadingDots -w
manimgl transit_animation.py PendulumSwing -w
```
@@ -0,0 +1,102 @@
# Vector Fields - Reference Guide
**Example file**: `examples/vector_fields.py`
## User Query Scenarios
This example addresses queries like:
- "Create a vector field visualization"
- "Show particles flowing through a field"
- "Visualize electric field from charges"
- "Animate gradient descent"
- "Show fluid flow"
## Scene Thinking Process (3b1b Style)
### 1. Core Concept
**Vector Fields**: At each point in space, there's a vector showing direction and magnitude. Particles follow the field, revealing flow patterns.
### 2. Technical Implementation
#### Manual Arrow Field (Portable Approach)
```python
arrows = VGroup()
for x in np.arange(-3.5, 4, 0.7):
for y in np.arange(-2.5, 3, 0.7):
vx, vy = -y * 0.15, x * 0.15 # Rotation field
arrow = Arrow(
start=[x, y, 0],
end=[x + vx, y + vy, 0],
buff=0,
stroke_width=2,
)
# Color by magnitude
mag = np.sqrt(vx**2 + vy**2)
arrow.set_color(interpolate_color(BLUE, YELLOW, mag / 0.5))
arrows.add(arrow)
```
#### Particle Following Field
```python
def follow_field(mob, dt):
x, y = mob.get_center()[:2]
vx, vy = field_func(x, y)
mob.shift(np.array([vx, vy, 0]) * dt)
dot.add_updater(follow_field)
trail = TracedPath(dot.get_center, stroke_color=RED)
```
#### Electric Dipole Field
```python
def E_field(pos):
r1, r2 = pos - q1_pos, pos - q2_pos
d1, d2 = np.linalg.norm(r1), np.linalg.norm(r2)
E1 = r1 / d1**3 # From + charge
E2 = -r2 / d2**3 # From - charge
return E1 + E2
```
### 3. Scene Variants
| Scene | Purpose |
|-------|---------|
| `SimpleVectorField` | Rotation field with particle |
| `GradientFieldDemo` | Scalar field + gradient arrows |
| `ParticleFlow` | Multiple particles in vortex |
| `ElectricDipole` | Field from +/- charges |
## Key Patterns
### Pattern: Color by Magnitude
```python
mag = np.linalg.norm([vx, vy])
color = interpolate_color(BLUE, YELLOW, min(mag * scale, 1))
arrow.set_color(color)
```
### Pattern: LaggedStartMap for Many Arrows
```python
self.play(LaggedStartMap(GrowArrow, arrows, lag_ratio=0.02, run_time=2))
```
### Pattern: Closure for Updaters in Loops
```python
for i in range(n):
dot = Dot(...)
def make_updater(): # Closure captures current state
def update(mob, dt):
# use mob, not dot
...
return update
dot.add_updater(make_updater())
```
## Run Commands
```bash
manimgl vector_fields.py SimpleVectorField -w
manimgl vector_fields.py GradientFieldDemo -w
manimgl vector_fields.py ParticleFlow -w
manimgl vector_fields.py ElectricDipole -w
```
@@ -0,0 +1,431 @@
# 3D in ManimGL
ManimGL has powerful 3D capabilities with a flexible camera system. Unlike ManimCE, ManimGL doesn't require a special ThreeDScene class.
## Basic 3D Setup
### Creating 3D Objects
```python
from manimlib import *
class Basic3DScene(Scene):
def construct(self):
# Get camera frame
frame = self.camera.frame
# Set 3D orientation
frame.reorient(20, 70) # theta, phi in degrees
# Create 3D objects
sphere = Sphere(radius=2)
sphere.set_color(BLUE, opacity=0.7)
self.add(sphere)
```
## Camera Frame Control
### frame.reorient()
The `reorient()` method is the primary way to control 3D camera orientation.
```python
# frame.reorient(theta, phi, gamma=0, center=ORIGIN, height=8)
# Front view
frame.reorient(0, 0)
# Isometric view
frame.reorient(20, 70)
# Top-down view
frame.reorient(0, 90)
# Side view
frame.reorient(90, 90)
```
### Animating Camera Movement
```python
class AnimatedCamera(Scene):
def construct(self):
frame = self.camera.frame
# Create object
cube = Cube()
self.add(cube)
# Animate camera rotation
self.play(frame.animate.reorient(30, 60))
self.wait()
# Continuous rotation
frame.add_updater(lambda m, dt: m.increment_theta(0.2 * dt))
self.wait(10)
```
### Frame Methods
```python
# Set Euler angles
frame.set_euler_angles(theta=30*DEGREES, phi=70*DEGREES)
# Increment angles (useful for rotation)
frame.increment_theta(10*DEGREES)
frame.increment_phi(5*DEGREES)
frame.increment_gamma(2*DEGREES)
# Get current angles
theta = frame.get_theta()
phi = frame.get_phi()
```
## 3D Geometric Primitives
### Sphere
```python
sphere = Sphere(
radius=2,
resolution=(20, 20), # (u_resolution, v_resolution)
color=BLUE
)
sphere.set_opacity(0.7)
```
### Cube
```python
cube = Cube(
side_length=2,
color=GREEN,
fill_opacity=0.8
)
```
### Surface
```python
# Parametric surface
surface = Surface(
lambda u, v: np.array([u, v, u**2 + v**2]),
u_range=(-2, 2),
v_range=(-2, 2),
resolution=(20, 20)
)
```
### Torus
```python
torus = Torus(
r1=2, # Major radius
r2=0.5, # Minor radius
color=YELLOW
)
```
### Cylinder
```python
cylinder = Cylinder(
height=3,
radius=1,
color=RED
)
```
### 3D Lines and Shapes
```python
# 3D Line
line = Line3D(
start=[-2, -2, -2],
end=[2, 2, 2],
color=WHITE,
width=0.05
)
# 3D Disk
disk = Disk3D(radius=1.5, color=PURPLE)
# 3D Square
square = Square3D(side_length=2, color=ORANGE)
```
## Textured Surfaces
### Adding Textures to 3D Objects
```python
class TexturedSphere(Scene):
def construct(self):
frame = self.camera.frame
frame.reorient(20, 70)
# Create sphere with texture
sphere = Sphere(radius=2)
# Apply texture from URL or local file
textured_sphere = TexturedSurface(
surface=sphere,
# Can use URL or local path
image_file="path/to/texture.jpg"
)
self.add(textured_sphere)
# Rotate camera
self.play(frame.animate.increment_theta(360*DEGREES), run_time=10)
```
### Earth Example
```python
earth = Sphere(radius=2, resolution=(40, 40))
textured_earth = TexturedSurface(
earth,
"https://upload.wikimedia.org/wikipedia/commons/thumb/4/4d/Whole_world_-_land_and_oceans.jpg/1280px-Whole_world_-_land_and_oceans.jpg"
)
```
## 3D Axes
### ThreeDAxes
```python
axes = ThreeDAxes(
x_range=(-5, 5, 1),
y_range=(-5, 5, 1),
z_range=(-5, 5, 1),
width=10,
height=10,
depth=10
)
axes.add_coordinate_labels(font_size=20)
```
### NumberPlane in 3D
```python
# XY plane
xy_plane = NumberPlane(
x_range=(-5, 5),
y_range=(-5, 5)
)
# XZ plane (horizontal floor)
xz_plane = NumberPlane(
x_range=(-5, 5),
y_range=(-5, 5)
)
xz_plane.rotate(90*DEGREES, axis=RIGHT)
```
## Parametric Surfaces
### Creating Custom Surfaces
```python
# Paraboloid
paraboloid = Surface(
lambda u, v: np.array([
u,
v,
u**2 + v**2
]),
u_range=(-2, 2),
v_range=(-2, 2),
resolution=(30, 30),
color=BLUE
)
# Wave surface
wave = Surface(
lambda u, v: np.array([
u,
v,
np.sin(u) * np.cos(v)
]),
u_range=(-PI, PI),
v_range=(-PI, PI),
resolution=(40, 40)
)
```
### Surface Mesh
```python
# Create surface
surface = Sphere(radius=2)
# Add mesh overlay
mesh = SurfaceMesh(surface)
mesh.set_stroke(BLUE, width=1, opacity=0.5)
self.add(surface, mesh)
```
## Fixing Objects in Frame
### fix_in_frame()
Keep 2D elements (like labels) fixed while camera rotates.
```python
class FixedLabels(Scene):
def construct(self):
frame = self.camera.frame
frame.reorient(20, 70)
# 3D object that rotates with camera
cube = Cube()
self.add(cube)
# 2D title that stays fixed
title = Text("3D Cube", font_size=60)
title.to_edge(UP)
title.fix_in_frame() # Stays in screen space
self.add(title)
# Rotate camera
self.play(frame.animate.reorient(50, 80), run_time=3)
```
## Lighting
### Light Source
```python
# Access light source
light = self.camera.light_source
# Move light
light.move_to([10, 10, 10])
# Animate light movement
self.play(light.animate.move_to([0, 0, 10]), run_time=2)
```
### Gloss and Shadow
```python
# Add gloss to objects
sphere = Sphere(radius=2)
sphere.set_gloss(0.8) # 0 to 1
# Add shadow
sphere.set_shadow(0.5) # 0 to 1
```
## Complex 3D Scene Example
```python
class Complex3DScene(Scene):
def construct(self):
frame = self.camera.frame
frame.reorient(20, 75)
# Create axes
axes = ThreeDAxes(
x_range=(-3, 3),
y_range=(-3, 3),
z_range=(-3, 3)
)
# Create parametric surface
surface = Surface(
lambda u, v: np.array([
u,
v,
np.sin(np.sqrt(u**2 + v**2))
]),
u_range=(-3, 3),
v_range=(-3, 3),
resolution=(30, 30),
color=BLUE
)
surface.set_opacity(0.7)
# Add mesh
mesh = SurfaceMesh(surface)
mesh.set_stroke(WHITE, 0.5, opacity=0.3)
# Add title (fixed in frame)
title = Text("Sinc Function Surface", font_size=48)
title.to_edge(UP)
title.fix_in_frame()
title.set_backstroke(BLACK, 5)
# Build scene
self.add(axes, surface, mesh, title)
self.wait()
# Rotate camera smoothly
self.play(
frame.animate.reorient(45, 70),
run_time=3
)
self.wait()
# Add continuous rotation
frame.add_updater(lambda m, dt: m.increment_theta(20 * dt))
self.wait(10)
```
## Interactive 3D Controls
When running with `manimgl`, you can interact with 3D scenes:
- Press `d` + move mouse: Rotate camera
- Press `z` + scroll: Zoom in/out
- Press `r`: Reset camera to default position
- Press `q`: Exit interaction mode
## Best Practices
1. **Use reorient()**: Cleaner than setting Euler angles manually
2. **fix_in_frame() for labels**: Keep UI elements readable
3. **Appropriate resolution**: Higher resolution for final renders, lower for development
4. **Opacity for depth**: Set opacity < 1 to see through surfaces
5. **Lighting matters**: Adjust light source position for better visualization
6. **Continuous rotation**: Use updaters for smooth camera rotation
7. **Frame rate considerations**: 3D scenes may need lower frame rates during development
## Common Patterns
### Rotating camera around object
```python
# 360-degree rotation
self.play(
frame.animate.increment_theta(360*DEGREES),
run_time=10,
rate_func=linear
)
```
### Creating a floor plane
```python
floor = NumberPlane(
x_range=(-10, 10),
y_range=(-10, 10),
background_line_style={"stroke_color": GREY, "stroke_width": 1}
)
floor.rotate(90*DEGREES, RIGHT)
floor.shift(2*DOWN)
```
### Multiple viewpoints
```python
# Show from different angles
angles = [(0, 0), (20, 70), (45, 45), (90, 90)]
for theta, phi in angles:
self.play(frame.animate.reorient(theta, phi))
self.wait()
```
@@ -0,0 +1,462 @@
# Animation Groups in ManimGL
Animation groups allow you to coordinate multiple animations, running them simultaneously, sequentially, or with staggered timing.
## AnimationGroup
Runs multiple animations together.
### Basic Usage
```python
from manimlib import *
class GroupExample(Scene):
def construct(self):
circle = Circle()
square = Square()
circle.shift(LEFT * 2)
square.shift(RIGHT * 2)
# Run both animations simultaneously
self.play(AnimationGroup(
ShowCreation(circle),
ShowCreation(square)
))
self.wait()
```
### Shorthand Syntax
```python
# Equivalent to AnimationGroup
self.play(
ShowCreation(circle),
ShowCreation(square)
)
```
## LaggedStart
Starts animations with a staggered delay.
### Basic LaggedStart
```python
class LaggedStartExample(Scene):
def construct(self):
circles = VGroup(*[
Circle(radius=0.5).shift(i * RIGHT)
for i in range(-3, 4)
])
# Staggered creation
self.play(LaggedStart(
*[ShowCreation(c) for c in circles],
lag_ratio=0.2, # Delay ratio between animations
run_time=3
))
self.wait()
```
### lag_ratio Parameter
```python
# lag_ratio controls the delay
# 0 = all at once (like AnimationGroup)
# 1 = completely sequential (like Succession)
# 0.5 = overlapping animations
# Subtle overlap
self.play(LaggedStart(*animations, lag_ratio=0.1))
# More pronounced stagger
self.play(LaggedStart(*animations, lag_ratio=0.5))
# Nearly sequential
self.play(LaggedStart(*animations, lag_ratio=0.9))
```
## Succession
Runs animations one after another.
```python
class SuccessionExample(Scene):
def construct(self):
shapes = VGroup(
Circle().shift(LEFT * 2),
Square(),
Triangle().shift(RIGHT * 2)
)
# One after another (no overlap)
self.play(Succession(
ShowCreation(shapes[0]),
ShowCreation(shapes[1]),
ShowCreation(shapes[2])
))
self.wait()
```
### Succession vs Sequential play() Calls
```python
# Using Succession (all in one play call)
self.play(Succession(
animation1,
animation2,
animation3
))
# Equivalent to separate play calls
self.play(animation1)
self.play(animation2)
self.play(animation3)
```
## Combining Animation Groups
### Nested Groups
```python
class NestedGroups(Scene):
def construct(self):
# Top row
top = VGroup(*[Circle().shift(i*RIGHT) for i in range(-2, 3)])
# Bottom row
bottom = VGroup(*[Square().shift(i*RIGHT + 2*DOWN) for i in range(-2, 3)])
# Stagger within each row, but rows appear simultaneously
self.play(
LaggedStart(*[ShowCreation(c) for c in top], lag_ratio=0.2),
LaggedStart(*[ShowCreation(s) for s in bottom], lag_ratio=0.2),
)
self.wait()
```
### Sequential Groups
```python
# First group, then second group
self.play(Succession(
LaggedStart(*[ShowCreation(t) for t in top], lag_ratio=0.2),
LaggedStart(*[ShowCreation(b) for b in bottom], lag_ratio=0.2)
))
```
## LaggedStartMap
Applies an animation constructor to mobjects with lag.
```python
class LaggedStartMapExample(Scene):
def construct(self):
dots = VGroup(*[
Dot().shift(i * RIGHT + j * UP)
for i in range(-3, 4)
for j in range(-2, 3)
])
# Apply FadeIn to all dots with lag
self.play(LaggedStartMap(
FadeIn, dots,
lag_ratio=0.05
))
self.wait()
```
## Timing Control
### run_time for Groups
```python
# Total time for all animations
self.play(LaggedStart(
*animations,
lag_ratio=0.2,
run_time=5 # Total duration
))
# Each animation's individual timing
self.play(LaggedStart(
ShowCreation(circle, run_time=2),
ShowCreation(square, run_time=1),
lag_ratio=0.3
))
```
### rate_func with Groups
```python
# Apply rate function to entire group
self.play(
LaggedStart(*animations, lag_ratio=0.2),
rate_func=smooth
)
# Different rate functions for each
self.play(
ShowCreation(circle, rate_func=linear),
ShowCreation(square, rate_func=rush_into),
ShowCreation(triangle, rate_func=rush_from)
)
```
## Practical Examples
### Text Appearance
```python
class TextReveal(Scene):
def construct(self):
title = Text("Animated Title", font_size=72)
subtitle = Text("With smooth appearance", font_size=40)
subtitle.next_to(title, DOWN)
# Title letters appear one by one
self.play(LaggedStart(
*[FadeIn(char, shift=UP) for char in title],
lag_ratio=0.05
))
self.wait(0.3)
# Subtitle fades in
self.play(FadeIn(subtitle, shift=DOWN))
self.wait()
```
### Grid Animation
```python
class GridAnimation(Scene):
def construct(self):
grid = VGroup(*[
Square(side_length=0.8).shift([i, j, 0])
for i in range(-3, 4)
for j in range(-2, 3)
])
# Ripple effect
self.play(LaggedStart(
*[ShowCreation(square) for square in grid],
lag_ratio=0.02,
run_time=4
))
self.wait()
```
### Wave Effect
```python
class WaveEffect(Scene):
def construct(self):
dots = VGroup(*[
Dot().shift(i * 0.5 * RIGHT)
for i in range(-10, 11)
])
# Wave up and down
def wave_animation(dot, delay):
return Succession(
Wait(delay),
dot.animate.shift(UP),
dot.animate.shift(DOWN)
)
self.add(dots)
self.play(*[
wave_animation(dot, i * 0.1)
for i, dot in enumerate(dots)
])
self.wait()
```
### Cascade Effect
```python
class CascadeEffect(Scene):
def construct(self):
squares = VGroup(*[
Square(side_length=1).shift(i * 1.5 * DOWN)
for i in range(-2, 3)
])
# Cascade from top to bottom
self.play(LaggedStart(
*[
AnimationGroup(
square.animate.shift(RIGHT * 3),
square.animate.set_color(random_color())
)
for square in squares
],
lag_ratio=0.3
))
self.wait()
```
## Simultaneous Transformations
### Multiple Object Transformations
```python
class SimultaneousTransforms(Scene):
def construct(self):
shapes = VGroup(
Circle().shift(LEFT * 3),
Square().shift(LEFT),
Triangle().shift(RIGHT),
Star().shift(RIGHT * 3)
)
self.play(LaggedStart(
*[ShowCreation(s) for s in shapes],
lag_ratio=0.2
))
self.wait()
# Transform all simultaneously with different targets
targets = [
Square().shift(LEFT * 3),
Circle().shift(LEFT),
Star().shift(RIGHT),
Triangle().shift(RIGHT * 3)
]
self.play(*[
Transform(s, t)
for s, t in zip(shapes, targets)
])
self.wait()
```
## Best Practices
1. **Use LaggedStart for visual rhythm**: Creates more dynamic animations
2. **lag_ratio tuning**:
- 0.1-0.3 for subtle effects
- 0.5 for balanced overlap
- 0.8-1.0 for nearly sequential
3. **Nested groups**: Combine for complex choreography
4. **Total run_time**: Set on the group for consistent timing
5. **Don't overuse**: Too many lagged animations can be distracting
## Common Patterns
### Fade out everything
```python
# Fade out all objects with lag
self.play(LaggedStart(
*[FadeOut(mob) for mob in self.mobjects],
lag_ratio=0.1
))
```
### Build complex figure
```python
# Build parts sequentially
self.play(Succession(
ShowCreation(axes),
ShowCreation(graph),
Write(labels),
FadeIn(legend)
))
```
### Reveal diagram
```python
# Reveal components with rhythm
components = [background, main_shape, decorations, labels]
self.play(LaggedStart(
*[FadeIn(c, scale=0.8) for c in components],
lag_ratio=0.4
))
```
### Synchronized movement
```python
# Move multiple objects together
objects = VGroup(circle, square, triangle)
self.play(*[
obj.animate.shift(RIGHT * 2)
for obj in objects
])
```
## Full Example
```python
class ComprehensiveGrouping(Scene):
def construct(self):
# Title
title = Text("Animation Groups", font_size=60)
title.to_edge(UP)
self.play(Write(title))
self.wait()
# Create grid of dots
dots = VGroup(*[
Dot(color=interpolate_color(BLUE, RED, i/20))
.shift([
(i % 7 - 3) * 0.8,
(i // 7 - 1.5) * 0.8,
0
])
for i in range(21)
])
# Lagged appearance
self.play(LaggedStart(
*[FadeIn(dot, scale=0.5) for dot in dots],
lag_ratio=0.05,
run_time=3
))
self.wait()
# Synchronized color change
self.play(*[
dot.animate.set_color(YELLOW)
for dot in dots
])
self.wait()
# Cascade disappearance
self.play(LaggedStart(
*[FadeOut(dot, shift=DOWN) for dot in dots],
lag_ratio=0.05,
run_time=2
))
# Clean up
self.play(FadeOut(title))
self.wait()
```
## Debugging Groups
### Print timing information
```python
# Check total duration
group = LaggedStart(*animations, lag_ratio=0.2)
print(f"Group duration: {group.get_run_time()}")
# Visualize timing
for i, anim in enumerate(animations):
print(f"Animation {i}: starts at {i * 0.2 * group.get_run_time()}")
```
### Test lag_ratio values
```python
# Try different values to find the right feel
for lag in [0.1, 0.3, 0.5, 0.7]:
self.play(LaggedStart(*animations, lag_ratio=lag))
self.wait()
```
@@ -0,0 +1,166 @@
# ManimGL Animations
## Animation System Overview
ManimGL's animation system is built around the `Animation` base class. Specialized subclasses handle creation, transformation, and indication effects.
## Playing Animations
```python
# Single animation
self.play(ShowCreation(circle))
# Multiple animations simultaneously
self.play(
ShowCreation(circle),
Write(text),
)
# With run_time
self.play(ShowCreation(circle), run_time=2)
# With rate function
self.play(ShowCreation(circle), rate_func=smooth)
```
## Creation Animations
| Animation | Description |
|-----------|-------------|
| `ShowCreation` | Draw a VMobject's path (NOT `Create` like in ManimCE) |
| `Write` | Write text or LaTeX |
| `DrawBorderThenFill` | Draw outline then fill |
| `FadeIn` | Fade in with optional direction |
| `FadeOut` | Fade out with optional direction |
| `GrowFromCenter` | Scale up from center |
| `GrowFromPoint` | Scale up from a point |
| `GrowArrow` | Specialized for arrows |
```python
# ShowCreation for paths
self.play(ShowCreation(circle))
# Write for text
self.play(Write(Tex(R"\pi")))
# FadeIn with direction
self.play(FadeIn(square, shift=UP))
```
## Transform Animations
| Animation | Description |
|-----------|-------------|
| `Transform` | Morph one mobject into another (modifies original) |
| `ReplacementTransform` | Replace source with target |
| `TransformMatchingShapes` | Match similar shapes |
| `TransformMatchingTex` | Match LaTeX parts |
| `FadeTransform` | Fade while transforming |
| `MoveToTarget` | Move to mobject's `.target` |
```python
# Transform (modifies circle, becomes square)
self.play(Transform(circle, square))
# ReplacementTransform (removes circle, adds square)
self.play(ReplacementTransform(circle, square))
# Using .target
circle.generate_target()
circle.target.shift(RIGHT * 2)
circle.target.set_color(RED)
self.play(MoveToTarget(circle))
```
## Indication Animations
| Animation | Description |
|-----------|-------------|
| `Indicate` | Flash/pulse to draw attention |
| `ShowPassingFlash` | Flash along a path |
| `Flash` | Burst of light |
| `Circumscribe` | Draw circle/rect around |
| `Wiggle` | Wiggle the mobject |
| `FlashAround` | Flash effect around object |
```python
self.play(Indicate(important_text))
self.play(FlashAround(equation, run_time=2))
```
## Movement Animations
```python
# Using .animate syntax
self.play(circle.animate.shift(RIGHT * 2))
self.play(circle.animate.scale(2).set_color(RED))
# Rotate
self.play(Rotate(square, PI/2))
self.play(Rotate(square, 90 * DEGREES)) # Same thing
# MoveAlongPath
path = Line(LEFT, RIGHT)
self.play(MoveAlongPath(dot, path))
```
## LaggedStart and Groups
```python
# Staggered animations
self.play(LaggedStart(
*[ShowCreation(mob) for mob in mobjects],
lag_ratio=0.2
))
# AnimationGroup for simultaneous
self.play(AnimationGroup(
ShowCreation(circle),
Write(text),
lag_ratio=0 # Simultaneous
))
# Succession for sequential
self.play(Succession(
ShowCreation(circle),
Write(text),
))
```
## Animation Parameters
Common parameters for all animations:
| Parameter | Description |
|-----------|-------------|
| `run_time` | Duration in seconds |
| `rate_func` | Easing function (smooth, linear, etc.) |
| `lag_ratio` | Stagger ratio for grouped animations |
| `remover` | Remove mobject after animation |
| `introducer` | Add mobject at animation start |
```python
self.play(
ShowCreation(circle),
run_time=3,
rate_func=there_and_back,
)
```
## Rate Functions
Common rate functions:
- `smooth` - Default smooth easing
- `linear` - Constant speed
- `rush_into` - Fast start, slow end
- `rush_from` - Slow start, fast end
- `there_and_back` - Go and return
- `double_smooth` - Extra smooth
## Waiting
```python
self.wait() # Default pause
self.wait(2) # 2 second pause
self.wait(0.5) # Half second
```
@@ -0,0 +1,448 @@
# Camera and Frame in ManimGL
ManimGL's camera system is centered around the `CameraFrame`, accessible via `self.camera.frame`. This provides powerful control over both 2D and 3D perspectives.
## Accessing the Camera Frame
```python
from manimlib import *
class CameraExample(Scene):
def construct(self):
# Get the camera frame
frame = self.camera.frame
# frame is a Mobject, so it has all Mobject methods
# move_to, shift, scale, rotate, etc.
```
## 2D Camera Movement
### Basic Movement
```python
# Shift the camera
self.play(frame.animate.shift(RIGHT * 2))
# Move to a specific position
self.play(frame.animate.move_to([3, 2, 0]))
# Scale (zoom)
self.play(frame.animate.scale(0.5)) # Zoom in
self.play(frame.animate.scale(2)) # Zoom out
```
### Following Objects
```python
class FollowObject(Scene):
def construct(self):
frame = self.camera.frame
dot = Dot(color=RED)
# Camera follows dot
frame.add_updater(lambda m: m.move_to(dot))
# Move dot around
self.add(dot)
self.play(dot.animate.shift(RIGHT * 5), run_time=3)
self.play(dot.animate.shift(UP * 3), run_time=2)
self.wait()
```
### Frame Dimensions
```python
# Set frame width/height
frame.set_width(10)
frame.set_height(6)
# Animate frame size
self.play(frame.animate.set_width(20), run_time=2)
```
## 3D Camera Orientation
### reorient() Method
The `reorient()` method is the primary way to set 3D camera orientation in ManimGL.
```python
# Signature:
# frame.reorient(theta, phi, gamma=0, center=ORIGIN, height=8)
# Parameters:
# - theta: Rotation around z-axis (azimuthal angle) in degrees
# - phi: Angle from z-axis (polar angle) in degrees
# - gamma: Roll angle in degrees (optional)
# - center: Point the camera looks at (optional)
# - height: Frame height (optional)
# Common views:
frame.reorient(0, 0) # Front view (XY plane)
frame.reorient(20, 70) # Isometric-like view
frame.reorient(0, 90) # Top-down view (XY plane from above)
frame.reorient(90, 90) # Side view (YZ plane)
frame.reorient(45, 45) # Diagonal view
```
### Euler Angles
```python
# Set angles individually
frame.set_theta(30 * DEGREES)
frame.set_phi(70 * DEGREES)
frame.set_gamma(0 * DEGREES)
# Set all at once
frame.set_euler_angles(
theta=30 * DEGREES,
phi=70 * DEGREES,
gamma=0 * DEGREES
)
# Get current angles
theta = frame.get_theta()
phi = frame.get_phi()
gamma = frame.get_gamma()
```
### Incremental Rotation
```python
# Increment angles (useful for animations)
frame.increment_theta(10 * DEGREES)
frame.increment_phi(5 * DEGREES)
frame.increment_gamma(2 * DEGREES)
# Animated increments
self.play(frame.animate.increment_theta(90 * DEGREES))
```
## Animating Camera
### Simple Camera Animations
```python
class AnimateCamera(Scene):
def construct(self):
frame = self.camera.frame
cube = Cube()
self.add(cube)
# Reorient to isometric view
self.play(frame.animate.reorient(20, 70), run_time=2)
self.wait()
# Rotate around object
self.play(frame.animate.increment_theta(360 * DEGREES), run_time=8)
self.wait()
```
### Continuous Camera Motion
```python
class ContinuousRotation(Scene):
def construct(self):
frame = self.camera.frame
frame.reorient(20, 70)
sphere = Sphere(radius=2, color=BLUE)
self.add(sphere)
# Add continuous rotation updater
frame.add_updater(lambda m, dt: m.increment_theta(20 * dt))
# Let it rotate for 10 seconds
self.wait(10)
# Stop rotation
frame.clear_updaters()
self.wait()
```
### Camera Zoom In/Out
```python
class ZoomEffect(Scene):
def construct(self):
frame = self.camera.frame
objects = VGroup(*[Square() for _ in range(5)])
objects.arrange(RIGHT, buff=1)
self.add(objects)
# Zoom out to see all objects
self.play(frame.animate.set_width(20), run_time=2)
self.wait()
# Zoom in on first object
self.play(
frame.animate.set_width(2).move_to(objects[0]),
run_time=2
)
self.wait()
```
## Fixing Mobjects in Frame
### fix_in_frame() Method
Keep 2D elements fixed in screen space while the camera moves.
```python
class FixedInFrame(Scene):
def construct(self):
frame = self.camera.frame
frame.reorient(20, 70)
# 3D object that moves with camera
cube = Cube(color=BLUE)
self.add(cube)
# 2D label that stays fixed
title = Text("Rotating Cube", font_size=60)
title.to_edge(UP)
title.fix_in_frame() # Fixes it to screen space
self.add(title)
# Rotate camera - cube rotates, title stays fixed
self.play(frame.animate.reorient(60, 80), run_time=3)
self.wait()
```
### Multiple Fixed Elements
```python
class MultipleFixed(Scene):
def construct(self):
frame = self.camera.frame
frame.reorient(30, 70)
# 3D content
surface = Sphere(radius=2, color=BLUE, opacity=0.7)
self.add(surface)
# Fixed UI elements
title = Text("3D Visualization", font_size=48)
title.to_edge(UP)
title.fix_in_frame()
subtitle = Text("Interactive Camera", font_size=30, color=GREY)
subtitle.next_to(title, DOWN)
subtitle.fix_in_frame()
controls = Text("Press 'd' to rotate", font_size=24)
controls.to_corner(DL)
controls.fix_in_frame()
self.add(title, subtitle, controls)
# Rotate camera
self.play(frame.animate.increment_theta(180 * DEGREES), run_time=6)
```
## Reset Camera
```python
# Reset to default state
frame.to_default_state()
# Animate reset
self.play(frame.animate.to_default_state())
```
## Camera Center
```python
# Set what the camera looks at
frame.set_center([2, 3, 0])
# Animate center change
self.play(frame.animate.set_center([0, 0, 2]))
# Get current center
center = frame.get_center()
```
## Advanced Camera Patterns
### Orbit Camera Around Object
```python
class OrbitCamera(Scene):
def construct(self):
frame = self.camera.frame
frame.reorient(30, 70)
# Central object
torus = Torus(r1=2, r2=0.5, color=YELLOW)
self.add(torus)
# Orbit 360 degrees
self.play(
frame.animate.increment_theta(360 * DEGREES),
run_time=10,
rate_func=linear
)
```
### Camera Following Path
```python
class CameraPath(Scene):
def construct(self):
frame = self.camera.frame
# Create path
path = Circle(radius=5)
self.add(path)
# Dot to follow
dot = Dot(color=RED)
dot.move_to(path.point_from_proportion(0))
# Camera follows dot
frame.add_updater(lambda m: m.move_to(dot))
# Move dot along path
self.play(
MoveAlongPath(dot, path),
run_time=8,
rate_func=linear
)
```
### Multiple Camera Positions
```python
class CameraTour(Scene):
def construct(self):
frame = self.camera.frame
# Create scene
objects = VGroup(
Square(side_length=2, color=RED).shift(LEFT * 3),
Circle(radius=1, color=BLUE),
Triangle(color=GREEN).shift(RIGHT * 3)
)
self.add(objects)
# Tour each object
for obj in objects:
self.play(
frame.animate.set_width(3).move_to(obj),
run_time=2
)
self.wait()
# Return to overview
self.play(
frame.animate.set_width(14).move_to(ORIGIN),
run_time=2
)
```
### Dynamic Camera with Updater
```python
class DynamicCamera(Scene):
def construct(self):
frame = self.camera.frame
# Moving object
dot = Dot(color=RED)
# Camera tracks and zooms based on distance from origin
def update_frame(frame):
frame.move_to(dot)
dist = np.linalg.norm(dot.get_center())
frame.set_width(max(8, dist * 2))
frame.add_updater(update_frame)
# Move dot around
self.add(dot)
self.play(dot.animate.shift(RIGHT * 5 + UP * 3), run_time=4)
self.play(dot.animate.shift(LEFT * 8 + DOWN * 2), run_time=4)
self.wait()
```
## Light Source
### Accessing and Moving Light
```python
class LightControl(Scene):
def construct(self):
frame = self.camera.frame
frame.reorient(20, 70)
# Get light source
light = self.camera.light_source
# Create 3D object
sphere = Sphere(radius=2, color=BLUE)
sphere.set_gloss(0.8)
self.add(sphere)
# Show light position (for debugging)
light_indicator = Dot(color=YELLOW)
light_indicator.add_updater(lambda m: m.move_to(light.get_center()))
self.add(light_indicator)
# Move light around
self.play(light.animate.move_to([5, 5, 5]), run_time=2)
self.wait()
self.play(light.animate.move_to([-5, -5, 5]), run_time=2)
self.wait()
```
## Best Practices
1. **Store frame reference**: `frame = self.camera.frame` at the start
2. **Use reorient() for 3D**: Cleaner than setting angles individually
3. **fix_in_frame() for UI**: Keep labels and titles readable
4. **Smooth transitions**: Use appropriate run_time for camera movements
5. **rate_func=linear**: For continuous rotations
6. **to_default_state()**: Reset camera when needed
7. **Updaters for following**: Use updaters to track moving objects
## Common Patterns
### Zoom and pan
```python
def zoom_to(self, mobject, scale_factor=1.5):
frame = self.camera.frame
self.play(
frame.animate
.set_width(mobject.get_width() * scale_factor)
.move_to(mobject),
run_time=2
)
```
### 360-degree showcase
```python
def showcase_3d(self, mobject):
frame = self.camera.frame
frame.reorient(20, 70)
self.play(
frame.animate.increment_theta(360 * DEGREES),
run_time=8,
rate_func=linear
)
```
### Picture-in-picture effect
```python
# Small inset camera view
small_frame = self.camera.frame.copy()
small_frame.set_width(4)
small_frame.to_corner(UR, buff=0.5)
small_frame.fix_in_frame()
```
@@ -0,0 +1,407 @@
# Command Line Interface in ManimGL
ManimGL uses the `manimgl` command for rendering scenes. It offers powerful flags for different workflows.
## Basic Usage
### Running a Scene
```bash
# Basic syntax
manimgl scene_file.py SceneName
# Example
manimgl my_animation.py SquareToCircle
```
### Auto-Select Scene
```bash
# If only one scene in file, it runs automatically
manimgl my_animation.py
# If multiple scenes, presents a menu to choose from
manimgl my_animations.py
```
## Common Flags
### Writing to File
```bash
# Write to file (no preview)
manimgl scene.py MyScene -w
# Write and open the file
manimgl scene.py MyScene -o
# Show final frame only
manimgl scene.py MyScene -s
# Save final frame as image and show
manimgl scene.py MyScene -so
```
### Interactive Mode
```bash
# Skip to line 15 and enter interactive mode
manimgl scene.py MyScene -se 15
# Interactive mode at specific line
manimgl scene.py MyScene --skip_animations --embed 20
```
### Display Options
```bash
# Fullscreen window
manimgl scene.py MyScene -f
# Custom window size
manimgl scene.py MyScene --resolution 1920,1080
# Hide progress bar
manimgl scene.py MyScene --quiet
```
## Quality and Resolution
### Resolution Presets
```bash
# Low quality (for testing)
manimgl scene.py MyScene -l
# Medium quality
manimgl scene.py MyScene -m
# High quality (1080p)
manimgl scene.py MyScene -h
# 4K quality
manimgl scene.py MyScene --uhd
# Custom resolution
manimgl scene.py MyScene --resolution 2560,1440
```
### Frame Rate
```bash
# Set frame rate (default is 60)
manimgl scene.py MyScene --frame_rate 30
# Lower frame rate for faster renders
manimgl scene.py MyScene --frame_rate 15
```
## Advanced Flags
### Skip to Specific Animation
```bash
# Skip to nth animation
manimgl scene.py MyScene -n 5
# Skip animations (instant mode)
manimgl scene.py MyScene --skip_animations
```
### Output Options
```bash
# Specify output file
manimgl scene.py MyScene -o output.mp4
# Save as GIF
manimgl scene.py MyScene --format gif
# Transparent background
manimgl scene.py MyScene --transparent
```
### Configuration
```bash
# Use custom config file
manimgl scene.py MyScene --config_file custom_config.yml
# Set specific config values
manimgl scene.py MyScene --config camera_config.frame_rate=30
```
## Interactive Development
### The -se Flag
The `-se` (skip and embed) flag is ManimGL's killer feature:
```bash
# Drop into interactive shell at line 15
manimgl scene.py MyScene -se 15
```
In the interactive shell:
```python
# Use abbreviated commands (no self.)
play(circle.animate.shift(RIGHT))
add(Square())
remove(circle)
wait(2)
# Copy code to clipboard, then:
checkpoint_paste() # Run with animations
checkpoint_paste(skip=True) # Run instantly
checkpoint_paste(record=True) # Record while running
# Interactive camera control
touch() # Press 'd' + mouse to rotate, 'z' + scroll to zoom
# Exit
exit()
```
## File Organization
### Running from Different Directories
```bash
# From same directory as manimlib/
manimgl project/scene.py MyScene
# With absolute path
manimgl /full/path/to/scene.py MyScene
# With relative path
manimgl ../other_project/scene.py MyScene
```
## Combining Flags
### Common Combinations
```bash
# High quality, write and open
manimgl scene.py MyScene -h -o
# Low quality, fullscreen, for testing
manimgl scene.py MyScene -l -f
# Skip animations, final frame only
manimgl scene.py MyScene -s --skip_animations
# Interactive at line 20, low quality
manimgl scene.py MyScene -l -se 20
# Save as GIF, high quality
manimgl scene.py MyScene -h --format gif -o
```
## Workflow Examples
### Development Workflow
```bash
# 1. Initial testing (low quality, fast)
manimgl scene.py MyScene -l
# 2. Interactive debugging at specific point
manimgl scene.py MyScene -l -se 25
# 3. Check final frame
manimgl scene.py MyScene -s
# 4. Final render (high quality, save and open)
manimgl scene.py MyScene -h -o
```
### Quick Preview Workflow
```bash
# Show final frame immediately
manimgl scene.py MyScene -s
# If it looks good, render full animation
manimgl scene.py MyScene -o
```
### Batch Rendering
```bash
# Render multiple scenes
for scene in Scene1 Scene2 Scene3; do
manimgl scenes.py $scene -h -w
done
```
## Debugging Flags
### Verbose Output
```bash
# Show detailed output
manimgl scene.py MyScene --verbose
# Show all debug info
manimgl scene.py MyScene --debug
```
### Profiling
```bash
# Show performance stats
manimgl scene.py MyScene --profile
# Detailed timing information
manimgl scene.py MyScene --timing
```
## Configuration Override
### Temporary Config Changes
```bash
# Override window size
manimgl scene.py MyScene --config window_config.size=fullscreen
# Override output directory
manimgl scene.py MyScene --config directories.output=/tmp/manim
# Multiple overrides
manimgl scene.py MyScene \
--config camera_config.frame_rate=30 \
--config camera_config.pixel_width=1280
```
## Help and Information
### Getting Help
```bash
# Show all available flags
manimgl --help
# Show version
manimgl --version
# List scenes in file without running
manimgl scene.py --list_scenes
```
## Full CLI Reference
### All Major Flags
```bash
# Quality/Resolution
-l, --low_quality # 480p, 15fps
-m, --medium_quality # 720p, 30fps
-h, --high_quality # 1080p, 60fps
--uhd # 4K, 60fps
--resolution WIDTHxHEIGHT # Custom resolution
# Output
-w, --write_file # Write to file
-o, --open # Write and open
-s, --show_last_frame # Show final frame
--format FORMAT # Output format (mp4, gif, png)
--transparent # Transparent background
# Playback
-f, --fullscreen # Fullscreen window
-n NUM, --skip_to NUM # Skip to animation number
--skip_animations # Skip all animations
# Interactive
-e, --embed # Drop into IPython shell
--skip_animations --embed # Interactive at end (skip animations)
-se LINE, --skip_and_embed # Interactive at line number
# Configuration
--config_file FILE # Custom config file
--config KEY=VALUE # Override config value
# Debugging
--verbose # Verbose output
--debug # Debug mode
--quiet # Minimize output
--profile # Performance profiling
# Other
--version # Show version
--help # Show help
--list_scenes # List scenes in file
```
## Best Practices
1. **Use -l for development**: Fast iteration with low quality
2. **Use -se for debugging**: Interactive mode at problem points
3. **Use -s for quick checks**: Verify final frame before full render
4. **Use -h -o for final**: High quality output when ready
5. **Combine flags wisely**: `-l -f` for fullscreen testing
6. **Custom configs**: Use different configs for different projects
7. **Script common commands**: Create shell aliases for frequent tasks
## Common Aliases
Add to `.bashrc` or `.zshrc`:
```bash
# Quick preview
alias mgl='manimgl -l'
# Final render
alias mgf='manimgl -h -o'
# Interactive debug
alias mgd='manimgl -l -se'
# Show final frame
alias mgs='manimgl -s'
```
## Troubleshooting
### Common Issues
```bash
# Scene not found
manimgl scene.py # Lists all scenes if you don't specify
# Can't find manimlib
# Ensure you're in the directory with manimlib/ or use full paths
# Window not showing
# Check window_config in custom_config.yml
# Poor performance
# Use -l flag, reduce frame_rate, or lower resolution
```
## Example Commands
```bash
# Simple preview
manimgl examples/basic_animations.py SquareToCircle
# High quality render
manimgl examples/basic_animations.py SquareToCircle -h -o
# Interactive debugging at line 30
manimgl examples/basic_animations.py SquareToCircle -se 30
# Save as GIF
manimgl examples/basic_animations.py SquareToCircle --format gif -o
# Custom resolution
manimgl examples/basic_animations.py SquareToCircle --resolution 2560,1440
# Skip to 5th animation and show
manimgl examples/basic_animations.py SquareToCircle -n 5
# Fullscreen, low quality for testing
manimgl examples/basic_animations.py SquareToCircle -l -f
```
@@ -0,0 +1,403 @@
# Colors in ManimGL
ManimGL provides extensive color support with built-in color constants, gradients, and color manipulation utilities.
## Color Constants
### Basic Colors
```python
# Primary colors
RED, GREEN, BLUE
YELLOW, CYAN, MAGENTA
# Grayscale
WHITE, GREY, GRAY, BLACK
# Common colors
ORANGE, PURPLE, PINK, BROWN
MAROON, TEAL, GOLD
```
### Color Variations
ManimGL provides color gradients with letter suffixes:
```python
# Blue variations (darkest to lightest)
BLUE_E # Darkest blue
BLUE_D
BLUE_C
BLUE_B
BLUE_A # Lightest blue
# Similarly for other colors:
RED_E, RED_D, RED_C, RED_B, RED_A
GREEN_E, GREEN_D, GREEN_C, GREEN_B, GREEN_A
YELLOW_E, YELLOW_D, YELLOW_C, YELLOW_B, YELLOW_A
```
### Usage Example
```python
from manimlib import *
class ColorExample(Scene):
def construct(self):
# Create circles with different color variations
circles = VGroup(*[
Circle(radius=0.5, color=color)
for color in [BLUE_E, BLUE_D, BLUE_C, BLUE_B, BLUE_A]
])
circles.arrange(RIGHT, buff=0.5)
self.add(circles)
```
## Setting Colors
### Basic Color Setting
```python
# At creation
circle = Circle(color=BLUE)
# After creation
square = Square()
square.set_color(RED)
# Multiple mobjects
group = VGroup(Circle(), Square(), Triangle())
group.set_color(GREEN)
```
### Animated Color Changes
```python
class ColorAnimation(Scene):
def construct(self):
circle = Circle(color=BLUE)
self.add(circle)
# Animate color change
self.play(circle.animate.set_color(RED))
self.wait()
# Another change
self.play(circle.animate.set_color(YELLOW))
self.wait()
```
## Gradients
### set_submobject_colors_by_gradient
```python
# Apply gradient to submobjects
text = Text("Gradient Text")
text.set_submobject_colors_by_gradient(BLUE, GREEN, YELLOW)
# Multiple objects with gradient
squares = VGroup(*[Square() for _ in range(10)])
squares.arrange(RIGHT)
squares.set_submobject_colors_by_gradient(RED, BLUE)
```
### Color Interpolation
```python
from manimlib.utils.color import interpolate_color
# Create color between two colors
mid_color = interpolate_color(RED, BLUE, 0.5) # Purple
# Create gradient programmatically
n_colors = 10
gradient = [
interpolate_color(RED, BLUE, alpha)
for alpha in np.linspace(0, 1, n_colors)
]
```
## Advanced Color Techniques
### set_color_by_code (GLSL)
ManimGL allows dynamic coloring using GLSL code:
```python
# Color based on position
square = Square()
square.set_color_by_code("""
color.r = x;
color.g = y;
color.b = 1.0;
""")
```
### set_color_by_xyz_func
```python
# Color based on 3D position
surface = Sphere(radius=2)
surface.set_color_by_xyz_func(
glsl_snippet="float value = sqrt(x*x + y*y + z*z); return value;",
min_value=0,
max_value=5,
colormap='viridis'
)
```
## Color for Text and LaTeX
### Coloring Text Parts
```python
# Color specific words
text = Text(
"Red, Green, and Blue",
t2c={"Red": RED, "Green": GREEN, "Blue": BLUE}
)
```
### Coloring LaTeX
```python
# Color math symbols
equation = Tex(
R"E = mc^2",
t2c={"E": BLUE, "m": GREEN, "c": YELLOW}
)
# Color by tex substring
formula = Tex(R"\int_0^1 x^2 dx")
formula.set_color_by_tex("x", BLUE)
formula.set_color_by_tex(R"\int", RED)
```
## RGB and Hex Colors
### Using RGB Values
```python
from manimlib.utils.color import rgb_to_color
# RGB values (0-1 range)
custom_color = rgb_to_color([0.5, 0.3, 0.8])
circle = Circle(color=custom_color)
# RGB from 0-255 range (convert to 0-1)
custom_color = rgb_to_color([128/255, 77/255, 204/255])
```
### Using Hex Colors
```python
from manimlib.utils.color import hex_to_rgb, rgb_to_color
# Hex color
hex_color = "#FF5733"
rgb = hex_to_rgb(hex_color)
color = rgb_to_color(rgb)
circle = Circle(color=color)
```
## Opacity and Transparency
### Setting Opacity
```python
# Transparent circle
circle = Circle(color=BLUE, fill_opacity=0.5)
# Change opacity
circle.set_opacity(0.7)
# Fill vs Stroke opacity
square = Square()
square.set_fill(BLUE, opacity=0.5)
square.set_stroke(WHITE, width=4, opacity=1.0)
```
## Color Utilities
### Getting Color from Mobject
```python
circle = Circle(color=BLUE)
# Get color
color = circle.get_color()
# Get fill color
fill_color = circle.get_fill_color()
# Get stroke color
stroke_color = circle.get_stroke_color()
```
### Color Matching
```python
# Match color from another mobject
circle = Circle(color=BLUE)
square = Square()
square.match_color(circle)
# Match fill color
square.match_fill(circle)
# Match stroke
square.match_stroke(circle)
```
## Color Schemes
### Creating Consistent Color Palettes
```python
# Define color scheme
COLOR_SCHEME = {
"background": "#1e1e1e",
"primary": BLUE_C,
"secondary": GREEN_C,
"accent": YELLOW_C,
"text": WHITE,
"highlight": RED_C
}
# Use in scene
class StyledScene(Scene):
def construct(self):
title = Text("Title", color=COLOR_SCHEME["primary"])
subtitle = Text("Subtitle", color=COLOR_SCHEME["secondary"])
highlight = Circle(color=COLOR_SCHEME["accent"])
self.add(title, subtitle, highlight)
```
### 3Blue1Brown Color Scheme
```python
# Grant's typical colors
BLUE_3B1B = BLUE_C
GREEN_3B1B = GREEN_C
YELLOW_3B1B = YELLOW_C
RED_3B1B = RED_C
# Background
BACKGROUND_COLOR = "#0a0a0a"
```
## Gloss and Visual Properties
### Adding Gloss (for 3D)
```python
# Add glossy appearance
sphere = Sphere(radius=2, color=BLUE)
sphere.set_gloss(0.8) # 0 to 1
# Get gloss value
gloss = sphere.get_gloss()
```
### Shadow
```python
# Add shadow (for 3D)
cube = Cube(color=RED)
cube.set_shadow(0.5) # 0 to 1
# Get shadow value
shadow = cube.get_shadow()
```
## Full Color Example
```python
class ComprehensiveColorExample(Scene):
def construct(self):
# Color variations showcase
blue_shades = VGroup(*[
Circle(radius=0.4, color=color)
for color in [BLUE_E, BLUE_D, BLUE_C, BLUE_B, BLUE_A]
])
blue_shades.arrange(RIGHT, buff=0.3)
blue_shades.to_edge(UP, buff=1)
# Gradient
squares = VGroup(*[Square(side_length=0.6) for _ in range(8)])
squares.arrange(RIGHT, buff=0.2)
squares.set_submobject_colors_by_gradient(RED, YELLOW, GREEN, BLUE)
# Custom RGB color
custom_circle = Circle(
radius=1,
color=rgb_to_color([0.8, 0.2, 0.6]),
fill_opacity=0.7
)
custom_circle.shift(DOWN * 2)
# Colored text
text = Text(
"Colorful Text",
font_size=48,
t2c={"Colorful": BLUE, "Text": GREEN}
)
text.next_to(custom_circle, UP, buff=0.5)
# Add everything
self.play(
FadeIn(blue_shades, lag_ratio=0.1),
FadeIn(squares, lag_ratio=0.1),
ShowCreation(custom_circle),
Write(text)
)
self.wait()
# Animate color changes
self.play(
squares.animate.set_submobject_colors_by_gradient(PURPLE, ORANGE),
custom_circle.animate.set_color(TEAL)
)
self.wait()
```
## Best Practices
1. **Use named constants**: Prefer `BLUE` over RGB values for readability
2. **Consistent color schemes**: Define color palettes for coherent visuals
3. **Gradients for emphasis**: Use gradients to show progression or relationships
4. **Opacity for layering**: Use transparency to show overlapping elements
5. **Color accessibility**: Ensure sufficient contrast for visibility
6. **t2c for LaTeX**: Color math expressions to highlight important parts
7. **Don't overdo it**: Too many colors can be distracting
## Common Patterns
### Rainbow gradient
```python
def rainbow_gradient(mobjects):
colors = [RED, ORANGE, YELLOW, GREEN, BLUE, PURPLE]
VGroup(*mobjects).set_submobject_colors_by_gradient(*colors)
```
### Fade to color animation
```python
self.play(
circle.animate.set_color(RED),
run_time=2
)
```
### Color cycling
```python
colors = [RED, ORANGE, YELLOW, GREEN, BLUE, PURPLE]
for color in colors:
self.play(circle.animate.set_color(color), run_time=0.5)
self.wait(0.2)
```
@@ -0,0 +1,442 @@
# Configuration in ManimGL
ManimGL uses `custom_config.yml` files for configuration. These files control directories, camera settings, window properties, and more.
## Configuration File Location
### Default Locations
ManimGL looks for `custom_config.yml` in this order:
1. Current directory
2. Parent directories (recursively up to project root)
3. ManimGL installation directory
```
my_project/
├── custom_config.yml # Project-specific config
├── scenes/
│ ├── custom_config.yml # Scenes-specific config (overrides project config)
│ └── scene.py
└── manimlib/ # ManimGL installation
```
### Multiple Configs
```bash
# Use specific config file
manimgl scene.py MyScene --config_file /path/to/config.yml
# Project structure with multiple configs
project/
├── custom_config.yml # Default for project
├── experiments/
│ ├── custom_config.yml # Overrides for experiments
│ └── test_scene.py
└── final/
├── custom_config.yml # High quality settings
└── final_scene.py
```
## Basic Configuration
### Minimal custom_config.yml
```yaml
# Directories
directories:
output: "./media/videos"
raster_images: "./media/images"
vector_images: "./media/svg"
sounds: "./media/sounds"
data: "./media/data"
# Window configuration
window_config:
size: "default" # or "fullscreen"
# Camera settings
camera_config:
pixel_height: 1080
pixel_width: 1920
frame_rate: 60
```
## Detailed Configuration Options
### Directory Configuration
```yaml
directories:
# Where rendered videos are saved
output: "/path/to/output/videos"
# Where temporary files go
temporary_storage: "/tmp/manim"
# Image resources
raster_images: "./assets/images"
vector_images: "./assets/svg"
# Audio resources
sounds: "./assets/audio"
# Data files
data: "./assets/data"
# LaTeX templates
tex_templates: "./assets/tex_templates"
# Font directory
fonts: "./assets/fonts"
```
### Camera Configuration
```yaml
camera_config:
# Resolution
pixel_width: 1920
pixel_height: 1080
# Frame rate
frame_rate: 60
# Background color
background_color: "#000000"
# Frame settings
frame_height: 8.0
frame_width: 14.222222222222221 # 16:9 aspect ratio
# Quality presets
# These override pixel_width, pixel_height, frame_rate
quality:
low:
pixel_width: 854
pixel_height: 480
frame_rate: 15
medium:
pixel_width: 1280
pixel_height: 720
frame_rate: 30
high:
pixel_width: 1920
pixel_height: 1080
frame_rate: 60
ultra_high:
pixel_width: 3840
pixel_height: 2160
frame_rate: 60
```
### Window Configuration
```yaml
window_config:
# Window size: "default", "fullscreen", or [width, height]
size: "default"
# size: "fullscreen"
# size: [1280, 720]
# Window position on screen
position: "UR" # Upper right
# Options: UL, UR, DL, DR, TOP, BOTTOM, LEFT, RIGHT, CENTER
# Monitor to display on (for multi-monitor setups)
monitor: 0
# Window title
window_title: "ManimGL Preview"
# Show file name in title
show_file_name_in_title: true
```
### Style Configuration
```yaml
style:
# Default color constants
background_color: "#000000"
# Font settings
font: "Consolas"
tex_font: "Latin Modern Math"
# Default stroke width
stroke_width: 4
# Default animation run time
default_animation_run_time: 1.0
```
### Universal Import Configuration
```yaml
# Auto-import common modules
universal_import_line: |
from manimlib import *
import numpy as np
import itertools as it
```
## Quality Presets
### Command Line Override
```bash
# Use low quality preset
manimgl scene.py MyScene -l
# Use medium quality
manimgl scene.py MyScene -m
# Use high quality
manimgl scene.py MyScene -h
# Use 4K quality
manimgl scene.py MyScene --uhd
```
### Custom Quality Preset
```yaml
camera_config:
quality:
custom:
pixel_width: 2560
pixel_height: 1440
frame_rate: 120
```
## LaTeX Configuration
### TeX Configuration
```yaml
tex_config:
# TeX compiler
tex_compiler: "latex" # or "xelatex", "lualatex"
# TeX template
tex_template: "tex_template.tex"
# Additional packages
tex_packages:
- "amsmath"
- "amssymb"
- "mathtools"
# Text to LaTeX map
text_to_replace: {
# Replacements for common symbols
"pi": "\\pi",
"alpha": "\\alpha"
}
```
## Project-Specific Configuration
### Development Config (fast iteration)
```yaml
# dev_config.yml
directories:
output: "./output/dev"
camera_config:
pixel_height: 480
pixel_width: 854
frame_rate: 15
window_config:
size: [1280, 720]
position: "UR"
```
Usage:
```bash
manimgl scene.py MyScene --config_file dev_config.yml
```
### Production Config (high quality)
```yaml
# prod_config.yml
directories:
output: "./output/final"
camera_config:
pixel_height: 2160
pixel_width: 3840
frame_rate: 60
style:
default_animation_run_time: 1.5
```
## Runtime Configuration Override
### Command Line Override
```bash
# Override single value
manimgl scene.py MyScene --config camera_config.frame_rate=30
# Override multiple values
manimgl scene.py MyScene \
--config camera_config.frame_rate=30 \
--config camera_config.pixel_width=1280 \
--config camera_config.pixel_height=720
# Override output directory
manimgl scene.py MyScene --config directories.output=/tmp/manim_output
```
## Complete Example Configuration
### Full custom_config.yml
```yaml
# Directory Configuration
directories:
output: "./media/videos"
temporary_storage: "/tmp/manim"
raster_images: "./assets/images"
vector_images: "./assets/svg"
sounds: "./assets/audio"
data: "./assets/data"
tex_templates: "./assets/tex"
fonts: "./assets/fonts"
# Camera Configuration
camera_config:
pixel_width: 1920
pixel_height: 1080
frame_rate: 60
background_color: "#0a0a0a"
frame_height: 8.0
frame_width: 14.222222222222221
# Window Configuration
window_config:
size: "default"
position: "UR"
monitor: 0
window_title: "ManimGL Preview"
show_file_name_in_title: true
# Style Configuration
style:
background_color: "#0a0a0a"
font: "Consolas"
tex_font: "Latin Modern Math"
stroke_width: 4
default_animation_run_time: 1.0
# TeX Configuration
tex_config:
tex_compiler: "latex"
tex_template: "tex_template.tex"
tex_packages:
- "amsmath"
- "amssymb"
- "mathtools"
- "physics"
# Universal Imports
universal_import_line: |
from manimlib import *
import numpy as np
import itertools as it
import random
# Logging
log_level: "INFO" # DEBUG, INFO, WARNING, ERROR
```
## Best Practices
1. **Separate dev and prod configs**: Use different configs for development and final renders
2. **Project-level configs**: Keep `custom_config.yml` in project root
3. **Override for testing**: Use `--config` flag for temporary changes
4. **Version control**: Commit `custom_config.yml` to git
5. **Document custom settings**: Add comments to explain non-standard values
6. **Consistent paths**: Use relative paths for portability
7. **Quality presets**: Use built-in quality flags (-l, -m, -h) instead of manual resolution changes
## Common Configurations
### For YouTube Videos (1080p)
```yaml
camera_config:
pixel_width: 1920
pixel_height: 1080
frame_rate: 60
background_color: "#000000"
```
### For Quick Testing
```yaml
camera_config:
pixel_width: 854
pixel_height: 480
frame_rate: 15
```
### For 4K Production
```yaml
camera_config:
pixel_width: 3840
pixel_height: 2160
frame_rate: 60
```
### For Vertical Video (TikTok/Shorts)
```yaml
camera_config:
pixel_width: 1080
pixel_height: 1920
frame_rate: 60
frame_height: 14.222222222222221
frame_width: 8.0
```
## Troubleshooting
### Config Not Loading
```bash
# Check which config is being used
manimgl scene.py MyScene --verbose
# Specify config explicitly
manimgl scene.py MyScene --config_file ./custom_config.yml
```
### Invalid Configuration
- Ensure YAML syntax is correct (indentation, colons, etc.)
- Check for typos in configuration keys
- Verify paths exist and are accessible
- Use quotes around paths with spaces
### Performance Issues
```yaml
# Reduce quality for testing
camera_config:
pixel_width: 854
pixel_height: 480
frame_rate: 15
# Use temporary storage on SSD
directories:
temporary_storage: "/path/to/fast/storage"
```
@@ -0,0 +1,399 @@
# Creation Animations in ManimGL
Creation animations bring mobjects into existence. ManimGL provides several animation classes for different creation effects.
## ShowCreation
**Note**: ManimGL uses `ShowCreation`, not `Create` (which is used in ManimCE).
### Basic Usage
```python
from manimlib import *
class CreationExample(Scene):
def construct(self):
circle = Circle()
# ShowCreation draws the object
self.play(ShowCreation(circle))
self.wait()
```
### Different Mobjects
```python
# Works with any VMobject
self.play(ShowCreation(Circle()))
self.play(ShowCreation(Square()))
self.play(ShowCreation(Line(LEFT, RIGHT)))
self.play(ShowCreation(Text("Hello")))
```
### Reverse Creation
```python
# Uncreate (reverse of ShowCreation)
circle = Circle()
self.add(circle)
self.play(ShowCreation(circle, reverse=True)) # Uncreates
```
## Write
The `Write` animation is specifically for text and LaTeX.
### Writing Text
```python
# Write text letter by letter
text = Text("Hello World", font_size=60)
self.play(Write(text))
# Write LaTeX
formula = Tex(R"\int_0^1 x^2 dx = \frac{1}{3}")
self.play(Write(formula))
```
### Write Speed
```python
# Control writing speed with run_time
text = Text("Fast", font_size=72)
self.play(Write(text), run_time=0.5)
text2 = Text("Slow", font_size=72)
self.play(Write(text2), run_time=3)
```
## FadeIn
Fade objects into view.
### Basic FadeIn
```python
circle = Circle()
self.play(FadeIn(circle))
```
### FadeIn with Shift
```python
# Fade in while shifting
text = Text("Appearing", font_size=60)
self.play(FadeIn(text, shift=UP))
# From different directions
self.play(FadeIn(circle, shift=DOWN))
self.play(FadeIn(square, shift=LEFT))
self.play(FadeIn(triangle, shift=RIGHT))
```
### FadeIn with Scale
```python
# Fade in while scaling
circle = Circle()
self.play(FadeIn(circle, scale=0.5)) # Starts at half size
# Shrink while fading in
square = Square()
self.play(FadeIn(square, scale=2)) # Starts at double size
```
## DrawBorderThenFill
Draws the border first, then fills the shape.
```python
class DrawBorderExample(Scene):
def construct(self):
square = Square()
square.set_fill(BLUE, opacity=0.7)
square.set_stroke(WHITE, width=4)
self.play(DrawBorderThenFill(square))
self.wait()
```
## GrowFromCenter
Grows object from its center.
```python
circle = Circle()
self.play(GrowFromCenter(circle))
# Control growth speed
square = Square()
self.play(GrowFromCenter(square), run_time=2)
```
## GrowFromEdge
Grows object from a specific edge.
```python
square = Square()
# Grow from different edges
self.play(GrowFromEdge(square, DOWN))
# or: UP, DOWN, LEFT, RIGHT
```
## GrowFromPoint
Grows object from a specific point.
```python
circle = Circle()
point = np.array([2, 2, 0])
self.play(GrowFromPoint(circle, point))
```
## SpinInFromNothing
Spins object into view while growing.
```python
star = Star()
self.play(SpinInFromNothing(star))
```
## AnimationGroup for Multiple Creations
### Simultaneous Creation
```python
class MultipleCreations(Scene):
def construct(self):
shapes = VGroup(
Circle().shift(LEFT * 2),
Square(),
Triangle().shift(RIGHT * 2)
)
# Create all simultaneously
self.play(*[ShowCreation(shape) for shape in shapes])
self.wait()
```
### Sequential Creation
```python
# One after another
for shape in shapes:
self.play(ShowCreation(shape))
self.wait(0.2)
```
## LaggedStart
Creates objects with a staggered delay.
```python
class LaggedCreation(Scene):
def construct(self):
circles = VGroup(*[
Circle(radius=0.5).shift(i * RIGHT)
for i in range(-3, 4)
])
# Staggered creation
self.play(LaggedStart(
*[ShowCreation(circle) for circle in circles],
lag_ratio=0.2 # Delay between each
))
self.wait()
```
## Comparison: Creation Animations
```python
class CreationComparison(Scene):
def construct(self):
methods = [
("ShowCreation", ShowCreation),
("FadeIn", FadeIn),
("GrowFromCenter", GrowFromCenter),
("DrawBorderThenFill", DrawBorderThenFill),
]
for name, AnimClass in methods:
# Create label
label = Text(name, font_size=30)
label.to_edge(UP)
# Create shape
square = Square()
square.set_fill(BLUE, opacity=0.7)
square.set_stroke(WHITE, width=3)
# Show animation
self.play(Write(label))
self.play(AnimClass(square))
self.wait()
self.play(FadeOut(VGroup(label, square)))
```
## Advanced Creation Patterns
### Partial Creation
```python
# Show only part of the creation
line = Line(LEFT * 3, RIGHT * 3)
self.play(
ShowCreation(line),
rate_func=lambda t: smooth(t * 0.5) # Only 50% created
)
```
### Reversed Rate Function
```python
# Create backwards
circle = Circle()
self.play(
ShowCreation(circle),
rate_func=lambda t: 1 - smooth(t) # Reverse
)
```
### Creation with Color Change
```python
class ColoredCreation(Scene):
def construct(self):
line = Line(LEFT * 3, RIGHT * 3)
line.set_color_by_gradient(BLUE, RED)
self.play(ShowCreation(line), run_time=2)
self.wait()
```
## Writing Mathematical Content
### Writing Equations
```python
class WriteEquation(Scene):
def construct(self):
equation = Tex(R"E = mc^2")
equation.scale(2)
self.play(Write(equation))
self.wait()
# Color parts
equation.set_color_by_tex("E", BLUE)
equation.set_color_by_tex("m", GREEN)
equation.set_color_by_tex("c", YELLOW)
self.wait()
```
### Writing Multi-line Content
```python
class MultiLineWrite(Scene):
def construct(self):
lines = VGroup(
Tex(R"a^2 + b^2 = c^2"),
Tex(R"e^{i\pi} + 1 = 0"),
Tex(R"\int_0^\infty e^{-x^2} dx = \frac{\sqrt{\pi}}{2}")
)
lines.arrange(DOWN, buff=0.5)
# Write line by line
for line in lines:
self.play(Write(line))
self.wait(0.5)
```
## Best Practices
1. **ShowCreation for shapes**: Use for geometric objects and paths
2. **Write for text**: Use for Text and Tex objects
3. **FadeIn for groups**: Good for bringing in multiple objects
4. **LaggedStart for sequences**: Creates visual rhythm
5. **Consistent timing**: Keep run_time similar for related objects
6. **Match animation to content**: Use appropriate animation for the context
## Common Patterns
### Create and highlight
```python
shape = Circle()
self.play(ShowCreation(shape))
self.play(shape.animate.set_color(YELLOW))
self.play(shape.animate.scale(1.5))
```
### Sequential text appearance
```python
title = Text("Title", font_size=72)
subtitle = Text("Subtitle", font_size=48)
self.play(Write(title))
self.wait(0.3)
self.play(FadeIn(subtitle, shift=UP))
```
### Grid creation
```python
grid = VGroup(*[
Square(side_length=0.5).shift([i, j, 0])
for i in range(-3, 4)
for j in range(-2, 3)
])
self.play(LaggedStart(
*[ShowCreation(square) for square in grid],
lag_ratio=0.01
))
```
## Full Example
```python
class ComprehensiveCreation(Scene):
def construct(self):
# Title
title = Text("Creation Animations", font_size=60)
title.to_edge(UP)
self.play(Write(title))
self.wait()
# Create shapes with different animations
circle = Circle(radius=1, color=BLUE)
circle.shift(LEFT * 3)
square = Square(side_length=2, color=GREEN)
square.set_fill(GREEN, opacity=0.5)
triangle = Triangle(color=YELLOW)
triangle.shift(RIGHT * 3)
# Staggered creation
self.play(
ShowCreation(circle),
FadeIn(square, scale=0.5),
GrowFromCenter(triangle),
run_time=2
)
self.wait()
# Add formula
formula = Tex(R"\sum_{n=1}^{\infty} \frac{1}{n^2} = \frac{\pi^2}{6}")
formula.next_to(title, DOWN, buff=1)
self.play(Write(formula))
self.wait(2)
# Clear scene
self.play(FadeOut(VGroup(title, circle, square, triangle, formula)))
```
@@ -0,0 +1,438 @@
# Interactive Embedding in ManimGL
ManimGL's `self.embed()` feature drops you into an interactive IPython shell during scene execution, making debugging and experimentation incredibly powerful.
## Basic Usage
### Adding embed() to Your Scene
```python
from manimlib import *
class MyScene(Scene):
def construct(self):
circle = Circle()
self.play(ShowCreation(circle))
# Drop into interactive shell here
self.embed()
# Code continues after you exit the shell
self.play(circle.animate.shift(RIGHT))
self.wait()
```
### Running with Embed
```bash
# Run scene - will pause at embed() point
manimgl scene.py MyScene
```
## Interactive Commands
### Available in Shell
When `self.embed()` opens the IPython shell, you have access to:
```python
# Scene methods (abbreviated - no 'self.' needed)
play(animation) # Play animation
add(mobject) # Add mobject to scene
remove(mobject) # Remove mobject
wait(duration) # Wait for duration
clear() # Clear scene
# Camera/frame control
frame # Access camera frame
play(frame.animate.shift(RIGHT))
# All local variables from construct()
circle, square, text, etc. # Your mobjects
# Interactive camera
touch() # Enter touch mode (press 'q' to exit)
# Press 'd' + mouse to rotate
# Press 'z' + scroll to zoom
# Press 'r' to reset
# Exit shell and continue
exit() # Continue scene execution
```
## Practical Examples
### Debugging Animation
```python
class DebugScene(Scene):
def construct(self):
circle = Circle()
square = Square()
self.add(circle, square)
# Problem with this animation?
self.play(circle.animate.move_to(square))
# Debug it interactively
self.embed()
# In the shell:
# >>> play(circle.animate.set_color(RED))
# >>> circle.get_center()
# >>> square.get_center()
```
### Experimenting with Positioning
```python
class PositioningExperiment(Scene):
def construct(self):
shapes = VGroup(*[
Circle(radius=0.5) for _ in range(5)
])
# Try different arrangements interactively
self.add(shapes)
self.embed()
# In the shell, try:
# >>> play(shapes.animate.arrange(RIGHT, buff=1))
# >>> play(shapes.animate.arrange(DOWN, buff=0.5))
# >>> play(shapes.animate.arrange_in_grid(rows=2))
```
### Color and Style Exploration
```python
class StyleExploration(Scene):
def construct(self):
text = Text("Experiment", font_size=72)
self.add(text)
self.embed()
# In the shell:
# >>> play(text.animate.set_color(BLUE))
# >>> text.set_backstroke(BLACK, width=10)
# >>> play(text.animate.scale(2))
```
## Advanced embed() Usage
### Multiple Embed Points
```python
class MultipleEmbeds(Scene):
def construct(self):
# First checkpoint
circle = Circle()
self.play(ShowCreation(circle))
self.embed() # First pause
# Second checkpoint
square = Square()
self.play(ShowCreation(square))
self.embed() # Second pause
# Third checkpoint
self.play(FadeOut(VGroup(circle, square)))
self.embed() # Third pause
```
### Conditional Embedding
```python
class ConditionalEmbed(Scene):
def construct(self):
DEBUG = True
circle = Circle()
self.play(ShowCreation(circle))
if DEBUG:
self.embed() # Only embed in debug mode
self.play(circle.animate.shift(RIGHT))
```
## Using with -se Flag
### Skip and Embed
The `-se` flag skips to a specific line and embeds:
```python
class LargeScene(Scene):
def construct(self):
# Line 5
circle = Circle()
self.play(ShowCreation(circle))
# Line 10
square = Square()
self.play(ShowCreation(square))
# Line 15
text = Text("Hello")
self.play(Write(text))
# Line 20
self.play(FadeOut(VGroup(circle, square, text)))
```
```bash
# Skip directly to line 15 and embed
manimgl scene.py LargeScene -se 15
```
## checkpoint_paste()
### Interactive Code Execution
`checkpoint_paste()` runs code from your clipboard:
```python
class CheckpointScene(Scene):
def construct(self):
circle = Circle()
self.add(circle)
self.embed()
```
```bash
# Run the scene
manimgl scene.py CheckpointScene
```
In the shell:
```python
# Copy this code to clipboard first:
"""
square = Square()
play(ShowCreation(square))
play(square.animate.next_to(circle, RIGHT))
"""
# Then in the shell:
>>> checkpoint_paste() # Runs with animations
>>> checkpoint_paste(skip=True) # Runs instantly
>>> checkpoint_paste(record=True) # Records while running
```
## Saving and Restoring State
### save_state() and restore()
```python
class StateManagement(Scene):
def construct(self):
circle = Circle()
square = Square()
self.add(circle, square)
# Save current state
self.save_state()
# Make changes
self.play(circle.animate.shift(RIGHT * 3))
self.play(square.animate.shift(LEFT * 3))
self.embed()
# In the shell:
# >>> restore() # Revert to saved state
```
## Interactive 3D Exploration
### touch() Mode
```python
class Interactive3D(Scene):
def construct(self):
frame = self.camera.frame
frame.reorient(20, 70)
# Create 3D object
sphere = Sphere(radius=2, color=BLUE)
self.add(sphere)
self.embed()
# In the shell:
# >>> touch()
# Now you can:
# - Press 'd' and move mouse to rotate
# - Press 'z' and scroll to zoom
# - Press 'r' to reset camera
# - Press 'q' to exit touch mode
```
## Debugging Patterns
### Inspect Mobject Properties
```python
class InspectProperties(Scene):
def construct(self):
circle = Circle(radius=2, color=BLUE)
circle.shift(RIGHT * 3)
self.add(circle)
self.embed()
# In the shell:
# >>> circle.get_center()
# >>> circle.get_color()
# >>> circle.get_width()
# >>> circle.get_height()
# >>> circle.get_all_points()
```
### Test Animation Timing
```python
class TimingTest(Scene):
def construct(self):
circle = Circle()
self.add(circle)
self.embed()
# In the shell, test different timings:
# >>> play(circle.animate.shift(RIGHT), run_time=0.5)
# >>> play(circle.animate.shift(LEFT), run_time=2)
# >>> play(circle.animate.shift(UP), run_time=1, rate_func=smooth)
```
### Build Complex Scenes Iteratively
```python
class IterativeBuilding(Scene):
def construct(self):
self.embed()
# Build entire scene in the shell:
# >>> title = Text("My Animation")
# >>> title.to_edge(UP)
# >>> add(title)
#
# >>> circles = VGroup(*[Circle(radius=0.5) for _ in range(5)])
# >>> circles.arrange(RIGHT, buff=0.5)
# >>> play(LaggedStart(*[ShowCreation(c) for c in circles], lag_ratio=0.2))
#
# >>> formula = Tex(R"E = mc^2")
# >>> formula.next_to(circles, DOWN, buff=1)
# >>> play(Write(formula))
```
## Best Practices
1. **Use for debugging**: Add `self.embed()` when animations don't work as expected
2. **Experiment freely**: Try different approaches in the shell before adding to code
3. **save_state() before experimenting**: Easy to revert if something goes wrong
4. **Use -se for large scenes**: Jump to problem area instead of watching entire animation
5. **checkpoint_paste() for iteration**: Quickly test code snippets
6. **touch() for 3D**: Essential for finding the right camera angle
7. **Remove embed() for final render**: Don't forget to remove debugging embeds
## Common Patterns
### Quick experiment pattern
```python
# Add at problem point
self.embed()
# In shell, test fix
play(mobject.animate.scale(2)) # Test different values
# If it works, add to code
# exit()
```
### Interactive development pattern
```python
# Start with minimal setup
class Scene(Scene):
def construct(self):
self.embed()
# Build everything in the shell
# Copy successful commands back to code
```
### 3D camera setup pattern
```python
# Get to 3D scene
frame.reorient(20, 70)
add(sphere)
self.embed()
# Find perfect angle
touch() # Rotate with mouse
# Press 'q' when done
# Check frame.get_theta(), frame.get_phi()
# Add those values to code
```
## Troubleshooting
### embed() Not Working
- Ensure you're running with `manimgl` command
- Check that IPython is installed
- Verify no syntax errors before embed() point
### Can't Access Variables
- Variables must be defined before `self.embed()`
- Use `locals()` or `globals()` to inspect available variables
### Shell Exits Immediately
- Don't call `exit()` unless you want to continue
- Press Ctrl+D to exit and continue
- Use `quit()` or `exit()` to close shell
## Example: Full Interactive Development
```python
class InteractiveDevelopment(Scene):
def construct(self):
# Start with embed
self.embed()
# In the shell, build everything:
"""
# Create title
title = Text("Interactive Development", font_size=60)
title.to_edge(UP)
play(Write(title))
# Create content
circle = Circle(radius=1.5, color=BLUE)
circle.set_fill(BLUE, opacity=0.5)
circle.set_stroke(WHITE, width=3)
play(ShowCreation(circle))
# Add label
label = Text("Circle", font_size=36)
label.next_to(circle, DOWN)
play(FadeIn(label, shift=UP))
# Animate
play(
circle.animate.shift(RIGHT * 2),
label.animate.shift(RIGHT * 2)
)
wait(2)
# When happy, copy all this code to your construct method
exit()
"""
```
This makes ManimGL incredibly powerful for rapid prototyping and debugging!
@@ -0,0 +1,160 @@
# ManimGL Frame (Camera) Control
## CameraFrame
In ManimGL, camera control is done through `self.camera.frame` (CameraFrame is a Mobject):
```python
frame = self.camera.frame
# Set euler angles for 3D orientation
frame.set_euler_angles(
theta=-30 * DEGREES,
phi=70 * DEGREES,
)
```
**Note:** In InteractiveScene, you can also use `self.frame` as a shortcut.
## CameraFrame Methods (from official docs)
The CameraFrame inherits standard Mobject methods plus these specific ones:
- `.to_default_state()` - Reset camera
- `.set_euler_angles(theta, phi, gamma)` - Set all angles
- `.set_theta(theta)` - Horizontal rotation
- `.set_phi(phi)` - Vertical rotation
- `.set_gamma(gamma)` - Roll
- `.increment_theta(dtheta)` - Add to theta
- `.increment_phi(dphi)` - Add to phi
- `.increment_gamma(dgamma)` - Add to gamma
Also inherits: `.shift()`, `.scale()`, `.move_to()`
```python
# Look down at 45 degrees, rotated 30 degrees
self.frame.reorient(45, -30, 0, ORIGIN, 8)
# Animate the reorientation
self.play(
self.frame.animate.reorient(60, -45, 0, (1, 0, 0), 10),
run_time=3
)
```
## Common Camera Operations
### Zoom
```python
# Zoom in (smaller height = closer)
self.play(self.frame.animate.set_height(4))
# Zoom out
self.play(self.frame.animate.set_height(12))
```
### Pan
```python
# Move camera center
self.play(self.frame.animate.move_to(RIGHT * 3))
# Shift camera
self.play(self.frame.animate.shift(UP * 2))
```
### Combined Movement
```python
self.play(
self.frame.animate.reorient(50, -40, 0, (2, 1, 0), 6).set_anim_args(run_time=3)
)
```
## fix_in_frame()
Keep mobjects fixed in screen space during 3D camera movement:
```python
title = Text("My Title")
title.to_edge(UP)
title.fix_in_frame() # Call on the mobject, not the scene!
self.add(title)
# Title stays fixed while camera moves
self.play(self.frame.animate.reorient(60, -45, 0))
```
**Key difference from ManimCE:** In ManimCE you call `self.add_fixed_in_frame_mobjects(title)`. In ManimGL you call `title.fix_in_frame()`.
## set_floor_plane()
Set the floor plane orientation for 3D scenes:
```python
self.set_floor_plane("xz") # y is up, xz is floor
self.set_floor_plane("xy") # z is up, xy is floor (default)
```
## Frame Animation Syntax
```python
# Chain with set_anim_args for run_time
self.play(
self.frame.animate.reorient(45, -30, 0, ORIGIN, 8).set_anim_args(run_time=2)
)
# Multiple frame operations
self.play(
self.frame.animate.shift(RIGHT * 2).set_height(6),
run_time=1.5
)
```
## Background Rectangle
For scenes with 3D camera movement, add a background:
```python
background = FullScreenRectangle()
background.set_fill(BLACK, 1)
background.fix_in_frame()
self.add(background)
```
## Complete 3D Example
```python
class Camera3DDemo(InteractiveScene):
def construct(self):
# Background
bg = FullScreenRectangle()
bg.set_fill(GREY_E, 1)
bg.fix_in_frame()
self.add(bg)
# Title fixed in frame
title = Text("3D Demo")
title.to_edge(UP)
title.fix_in_frame()
self.add(title)
# 3D content
cube = Cube(side_length=2)
cube.set_color(BLUE)
self.add(cube)
# Animate camera
self.play(
self.frame.animate.reorient(60, -45, 0, ORIGIN, 8),
run_time=3
)
# Rotate around
self.play(
self.frame.animate.reorient(60, 45, 0),
run_time=4
)
```
@@ -0,0 +1,159 @@
# ManimGL Interactive Development
ManimGL's killer feature is interactive development mode, allowing you to iterate rapidly without re-rendering the entire scene.
## Starting Interactive Mode
Use the `-se` (skip and embed) flag with a line number:
```bash
# Enter interactive mode at line 20
manimgl scene.py MyScene -se 20
# Enter at the beginning
manimgl scene.py MyScene -se 1
```
The scene runs up to that line, then drops into an IPython shell.
## checkpoint_paste()
The core workflow function. Copy code to your clipboard, then:
```python
# Run code from clipboard with full animations
checkpoint_paste()
# Run instantly without animations (for quick iteration)
checkpoint_paste(skip=True)
# Record animations while running
checkpoint_paste(record=True)
```
### Typical Workflow
1. Write your scene with placeholder line
2. Run with `-se` at that line
3. Copy animation code to clipboard
4. Call `checkpoint_paste()` to test
5. Iterate until satisfied
6. Move code into the actual file
## self.embed()
Drop into IPython shell programmatically:
```python
class MyScene(InteractiveScene):
def construct(self):
circle = Circle()
self.play(ShowCreation(circle))
self.embed() # Pause here, enter shell
# Code below runs after you exit the shell
self.play(FadeOut(circle))
```
In the shell, you have full access to:
- `self` - the scene
- All mobjects in scope
- All ManimGL functions
## Interactive Shell Commands
Once in the shell:
```python
# Inspect current mobjects
self.mobjects
# Add something new
square = Square()
self.play(ShowCreation(square))
# Clear and try again
self.clear()
# Exit shell and continue scene
exit()
# or Ctrl+D
```
## Quick Iteration Pattern
```python
class DevelopScene(InteractiveScene):
def construct(self):
# Setup that doesn't change often
axes = Axes()
self.add(axes)
# Breakpoint for development
self.embed()
# Code you're iterating on goes here
# (Or use checkpoint_paste() in the shell)
```
## Recording Mode
When you want to capture what you're doing interactively:
```python
# Start recording
checkpoint_paste(record=True)
# All animations are now recorded
# When done, video is saved
```
## Useful Shell Variables
```python
# Current frame (camera)
self.frame
# All mobjects
self.mobjects
# Specific mobjects by type
[m for m in self.mobjects if isinstance(m, Circle)]
# Frame center
self.frame.get_center()
```
## Debugging Tips
```python
# Print mobject info
print(circle.get_center())
print(circle.get_height())
print(circle.get_color())
# Highlight a mobject
circle.set_color(YELLOW)
self.wait(0.1)
# Check what's in the scene
print(len(self.mobjects))
```
## Exit and Continue
```python
# After interactive session, continue scene
exit() # or Ctrl+D
# The scene continues from where it left off
```
## Best Practices
1. **Use `-se` during development** - Much faster than re-rendering
2. **Keep setup code before the embed** - Reuse state
3. **Use `checkpoint_paste(skip=True)`** - For quick tests
4. **Use `checkpoint_paste(record=True)`** - When you've got it right
5. **Organize code into functions** - Easier to paste and test
@@ -0,0 +1,186 @@
# ManimGL Mobjects
## Mobject Hierarchy
```
Mobject (base class)
├── VMobject (vectorized - most common)
│ ├── VGroup
│ ├── Circle, Square, Rectangle, Line, Arrow
│ ├── Tex, Text, TexText
│ └── Axes, NumberPlane
├── Group (non-vectorized container)
├── ImageMobject
├── Point
└── 3D objects (Surface, ParametricSurface, etc.)
```
## Creating Mobjects
```python
# Geometric shapes
circle = Circle(radius=1, color=BLUE)
square = Square(side_length=2)
rect = Rectangle(width=3, height=2)
line = Line(LEFT, RIGHT)
arrow = Arrow(ORIGIN, UP)
# Text
text = Text("Hello")
math = Tex(R"\pi r^2")
# Groups
group = VGroup(circle, square)
```
## Positioning
```python
# Absolute position
circle.move_to(ORIGIN)
circle.move_to(RIGHT * 2 + UP)
# Relative to screen edges
circle.to_edge(UP)
circle.to_edge(LEFT, buff=1)
circle.to_corner(UL)
# Relative to other mobjects
square.next_to(circle, RIGHT)
square.next_to(circle, DOWN, buff=0.5)
# Alignment
group.align_to(other, UP)
group.align_to(other, LEFT)
# Shifting
circle.shift(RIGHT * 2)
circle.shift(UP + RIGHT)
```
## Styling
```python
# Fill
circle.set_fill(BLUE, opacity=0.5)
# Stroke (outline)
circle.set_stroke(WHITE, width=2)
circle.set_stroke(color=RED, width=4, opacity=0.8)
# Both
circle.set_style(
fill_color=BLUE,
fill_opacity=0.5,
stroke_color=WHITE,
stroke_width=2
)
# Color (affects both fill and stroke)
circle.set_color(RED)
# Backstroke (outline behind for readability)
text.set_backstroke(BLACK, 5)
```
## VGroup
Container for vectorized mobjects:
```python
# Create group
shapes = VGroup(circle, square, triangle)
# Arrange
shapes.arrange(RIGHT, buff=0.5)
shapes.arrange(DOWN, aligned_edge=LEFT)
shapes.arrange_in_grid(rows=2, cols=3)
# Apply to all
shapes.set_color(BLUE)
shapes.scale(0.5)
shapes.shift(UP)
# Access elements
shapes[0] # First element
shapes[-1] # Last element
shapes[1:3] # Slice
```
## Group vs VGroup
```python
# VGroup - for vectorized mobjects (VMobject subclasses)
vgroup = VGroup(Circle(), Square())
# Group - for any mobjects including images, 3D, etc.
group = Group(ImageMobject("photo.png"), Circle())
```
## Common Methods
| Method | Description |
|--------|-------------|
| `.move_to(point)` | Move center to point |
| `.shift(vector)` | Move by vector |
| `.scale(factor)` | Scale by factor |
| `.rotate(angle)` | Rotate by angle (radians) |
| `.next_to(mob, dir)` | Position next to another |
| `.align_to(mob, dir)` | Align edge with another |
| `.to_edge(dir)` | Move to screen edge |
| `.to_corner(corner)` | Move to screen corner |
| `.get_center()` | Get center point |
| `.get_width()` | Get width |
| `.get_height()` | Get height |
| `.copy()` | Create a copy |
## Generating Targets
For animating to a modified version:
```python
circle.generate_target()
circle.target.shift(RIGHT * 2)
circle.target.scale(2)
circle.target.set_color(RED)
self.play(MoveToTarget(circle))
```
## Saving and Restoring State
```python
circle.save_state()
self.play(circle.animate.shift(RIGHT).scale(2))
# Later...
self.play(Restore(circle))
```
## Updaters
Dynamic behavior:
```python
# Always follow another mobject
label.add_updater(lambda m: m.next_to(dot, UP))
# Time-based
circle.add_updater(lambda m, dt: m.rotate(dt))
# Value-based with ValueTracker
tracker = ValueTracker(0)
circle.add_updater(
lambda m: m.set_fill(opacity=tracker.get_value())
)
self.play(tracker.animate.set_value(1))
```
## Useful Shortcuts
```python
# f_always for common updater patterns
label.f_always.next_to(dot, UP)
# always (function form)
always(label.next_to, dot, UP)
```
@@ -0,0 +1,115 @@
# ManimGL Scenes
## Scene Types
ManimGL provides several scene types:
### InteractiveScene (Recommended)
The default for most development. Supports interactive mode with `-se` flag.
```python
from manimlib import *
class MyScene(InteractiveScene):
def construct(self):
circle = Circle()
self.play(ShowCreation(circle))
self.wait()
```
### Scene (Base Class)
Basic scene without interactive features:
```python
class BasicScene(Scene):
def construct(self):
self.play(Write(Text("Hello")))
```
### ThreeDScene
For 3D animations with proper camera setup:
```python
from manimlib import *
class My3DScene(ThreeDScene):
def construct(self):
axes = ThreeDAxes()
self.add(axes)
self.camera.frame.reorient(-45*DEGREES, 75*DEGREES)
```
## The construct Method
All scene logic goes in `construct()`:
```python
class MyScene(InteractiveScene):
def construct(self):
# 1. Create mobjects
circle = Circle(color=BLUE)
square = Square(color=RED)
# 2. Position them
circle.shift(LEFT * 2)
square.shift(RIGHT * 2)
# 3. Animate
self.play(ShowCreation(circle), ShowCreation(square))
# 4. Wait for viewer
self.wait(2)
```
## Adding vs Playing
```python
# Static add (instant, no animation)
self.add(circle)
# Animated add
self.play(ShowCreation(circle))
self.play(FadeIn(square))
```
## Scene Methods
| Method | Description |
|--------|-------------|
| `self.play(*anims)` | Play animations |
| `self.wait(t)` | Wait t seconds |
| `self.add(*mobs)` | Add mobjects instantly |
| `self.remove(*mobs)` | Remove mobjects |
| `self.clear()` | Clear all mobjects |
| `self.embed()` | Drop into IPython shell |
## Interactive Mode
Run with `-se` flag to enter at a specific line:
```bash
manimgl scene.py MyScene -se 15
```
In the shell:
```python
checkpoint_paste() # Run clipboard code with animations
checkpoint_paste(skip=True) # Run instantly
checkpoint_paste(record=True) # Record while running
```
## Class Attributes
Define scene configuration as class attributes:
```python
class MyScene(InteractiveScene):
camera_class = ThreeDCamera # Use 3D camera
random_seed = 42 # For reproducibility
def construct(self):
...
```
@@ -0,0 +1,406 @@
# Styling in ManimGL
ManimGL provides comprehensive styling options for mobjects including fill, stroke, opacity, and special effects.
## Fill Properties
### Basic Fill
```python
from manimlib import *
# Set fill at creation
circle = Circle(fill_color=BLUE, fill_opacity=0.7)
# Set fill after creation
square = Square()
square.set_fill(RED, opacity=0.5)
```
### Fill Examples
```python
class FillExample(Scene):
def construct(self):
# Solid fill
solid = Circle(radius=1)
solid.set_fill(BLUE, opacity=1.0)
# Transparent fill
transparent = Circle(radius=1)
transparent.set_fill(GREEN, opacity=0.3)
# No fill (just outline)
outline = Circle(radius=1)
outline.set_fill(opacity=0)
outline.set_stroke(YELLOW, width=4)
VGroup(solid, transparent, outline).arrange(RIGHT, buff=1)
self.add(solid, transparent, outline)
```
## Stroke Properties
### Basic Stroke
```python
# Set stroke at creation
line = Line(stroke_color=WHITE, stroke_width=4)
# Set stroke after creation
circle = Circle()
circle.set_stroke(BLUE, width=3, opacity=0.8)
```
### Stroke Width
```python
# Different stroke widths
thin = Circle().set_stroke(width=1)
medium = Circle().set_stroke(width=4)
thick = Circle().set_stroke(width=10)
VGroup(thin, medium, thick).arrange(RIGHT, buff=0.5)
```
### Stroke Behind Fill
```python
# Draw stroke behind fill (useful for borders)
shape = Circle(fill_color=BLUE, fill_opacity=0.8)
shape.set_stroke(WHITE, width=6, opacity=1, background=True)
```
## Backstroke
The `backstroke` feature adds an outline behind text or shapes for better visibility.
```python
# Text with backstroke (black outline)
text = Text("Readable Text", font_size=60)
text.set_backstroke(BLACK, width=5)
# Works great over complex backgrounds
text.set_backstroke(BLACK, width=8, opacity=1.0)
```
### Backstroke Example
```python
class BackstrokeExample(Scene):
def construct(self):
# Create complex background
background = VGroup(*[
Circle(radius=2 * np.random.random(), color=random_color())
for _ in range(20)
])
background.set_opacity(0.3)
self.add(background)
# Text with backstroke stands out
text = Text("Clear and Readable", font_size=72, color=WHITE)
text.set_backstroke(BLACK, width=10)
self.add(text)
```
## Opacity Control
### Fill Opacity
```python
# Control fill transparency
circle = Circle()
circle.set_fill_opacity(0.5)
# Animate opacity
self.play(circle.animate.set_fill_opacity(1.0))
```
### Stroke Opacity
```python
# Control stroke transparency
square = Square()
square.set_stroke_opacity(0.7)
```
### Overall Opacity
```python
# Set both fill and stroke opacity
mobject = Circle()
mobject.set_opacity(0.5) # Affects both fill and stroke
```
## Gloss (3D)
### Adding Gloss to 3D Objects
```python
# Make objects glossy/shiny
sphere = Sphere(radius=2, color=BLUE)
sphere.set_gloss(0.8) # 0 (matte) to 1 (very glossy)
# Get gloss value
gloss_value = sphere.get_gloss()
```
## Shadow (3D)
### Adding Shadows
```python
# Add shadow to 3D objects
cube = Cube(color=RED)
cube.set_shadow(0.6) # 0 (no shadow) to 1 (strong shadow)
# Get shadow value
shadow_value = cube.get_shadow()
```
## Combined Styling
### Complete Styling Control
```python
class CompleteStyling(Scene):
def construct(self):
shape = Circle(radius=2)
# Set all properties
shape.set_fill(BLUE, opacity=0.7)
shape.set_stroke(WHITE, width=4, opacity=1.0)
shape.set_backstroke(BLACK, width=6)
self.add(shape)
```
## Style Matching
### Match Style from Another Mobject
```python
# Create styled source
source = Circle()
source.set_fill(BLUE, opacity=0.7)
source.set_stroke(WHITE, width=3)
# Match style
target = Square()
target.match_style(source) # Copies all styling
# Match specific properties
target2 = Triangle()
target2.match_fill(source) # Copy fill only
target2.match_stroke(source) # Copy stroke only
target2.match_color(source) # Copy color only
```
## Gradients and Color Transitions
### Gradient Fills
```python
# Gradient across submobjects
text = Text("Gradient")
text.set_submobject_colors_by_gradient(BLUE, GREEN, YELLOW)
# For shapes with submobjects
squares = VGroup(*[Square() for _ in range(10)])
squares.arrange(RIGHT)
squares.set_submobject_colors_by_gradient(RED, PURPLE)
```
## Visual Effects
### Glow Effect
```python
# Create glow effect with multiple strokes
def add_glow(mobject, color=YELLOW, radius=0.5):
glow_layers = VGroup(*[
mobject.copy().set_stroke(
color,
width=width,
opacity=0.3 / (i + 1)
)
for i, width in enumerate(range(2, 20, 2))
])
return VGroup(glow_layers, mobject)
# Usage
circle = Circle(color=BLUE)
glowing_circle = add_glow(circle)
```
### Neon Effect
```python
def neon_style(mobject, color=BLUE):
mobject.set_fill(color, opacity=0.2)
mobject.set_stroke(color, width=3)
mobject.set_backstroke(color, width=10, opacity=0.5)
return mobject
# Usage
neon_text = neon_style(Text("NEON", font_size=90), BLUE)
```
## Style Presets
### Creating Reusable Styles
```python
# Define style functions
def outline_style(mobject):
mobject.set_fill(opacity=0)
mobject.set_stroke(WHITE, width=3)
return mobject
def solid_style(mobject, color=BLUE):
mobject.set_fill(color, opacity=1.0)
mobject.set_stroke(color, width=0)
return mobject
def glass_style(mobject, color=BLUE):
mobject.set_fill(color, opacity=0.3)
mobject.set_stroke(WHITE, width=2, opacity=0.8)
mobject.set_gloss(0.9)
return mobject
# Usage
circle1 = outline_style(Circle())
circle2 = solid_style(Circle(), RED)
circle3 = glass_style(Circle(), GREEN)
```
## Animating Styles
### Style Transitions
```python
class StyleAnimation(Scene):
def construct(self):
square = Square()
square.set_fill(BLUE, opacity=0)
square.set_stroke(WHITE, width=1)
self.add(square)
self.wait()
# Animate style changes
self.play(
square.animate.set_fill(BLUE, opacity=0.7),
square.animate.set_stroke(WHITE, width=5)
)
self.wait()
# Change colors
self.play(
square.animate.set_fill(RED, opacity=0.9),
square.animate.set_stroke(YELLOW, width=3)
)
self.wait()
```
## Full Styling Example
```python
class ComprehensiveStyleExample(Scene):
def construct(self):
# Different styling approaches
shapes = VGroup()
# Filled shape
filled = Circle(radius=0.8)
filled.set_fill(BLUE, opacity=0.8)
filled.set_stroke(width=0)
shapes.add(filled)
# Outlined shape
outlined = Circle(radius=0.8)
outlined.set_fill(opacity=0)
outlined.set_stroke(WHITE, width=4)
shapes.add(outlined)
# Transparent with border
transparent = Circle(radius=0.8)
transparent.set_fill(GREEN, opacity=0.3)
transparent.set_stroke(GREEN, width=3)
shapes.add(transparent)
# With backstroke
backstroke = Circle(radius=0.8)
backstroke.set_fill(YELLOW, opacity=0.6)
backstroke.set_stroke(WHITE, width=2)
backstroke.set_backstroke(BLACK, width=5)
shapes.add(backstroke)
# Gradient (multiple submobjects)
gradient_circles = VGroup(*[
Circle(radius=0.15).shift(i * 0.3 * RIGHT)
for i in range(-2, 3)
])
gradient_circles.set_submobject_colors_by_gradient(RED, YELLOW)
shapes.add(gradient_circles)
# Arrange and display
shapes.arrange(RIGHT, buff=1)
self.play(LaggedStart(*[
FadeIn(shape)
for shape in shapes
], lag_ratio=0.2))
self.wait()
# Animate style transitions
self.play(
filled.animate.set_opacity(0.3),
outlined.animate.set_stroke(YELLOW, width=8),
transparent.animate.set_fill(RED, opacity=0.8)
)
self.wait()
```
## Best Practices
1. **Opacity for layering**: Use transparency to show overlapping elements
2. **Backstroke for readability**: Add backstroke to text over complex backgrounds
3. **Consistent stroke width**: Maintain visual hierarchy with consistent widths
4. **Fill vs stroke**: Use fill for areas, stroke for borders
5. **Gloss for realism**: Add gloss to 3D objects for more realistic appearance
6. **Match style for consistency**: Use style matching for consistent appearance
7. **Gradients for flow**: Use gradients to show transitions or relationships
## Common Patterns
### Outline style for emphasis
```python
def emphasize(mobject):
return mobject.set_stroke(YELLOW, width=8, opacity=1.0)
```
### Transparent overlay
```python
def overlay(mobject, color=BLUE):
return mobject.set_fill(color, opacity=0.2)
```
### Clean UI style
```python
def ui_style(mobject):
mobject.set_fill(BLUE_C, opacity=0.9)
mobject.set_stroke(WHITE, width=2)
return mobject
```
### Highlighted text
```python
text = Text("Important", font_size=60)
text.set_fill(YELLOW, opacity=1.0)
text.set_backstroke(BLACK, width=8)
text.set_stroke(WHITE, width=1)
```
@@ -0,0 +1,268 @@
# Tex to Color Map (t2c) in ManimGL
The `t2c` parameter (tex_to_color_map) is a powerful feature for coloring specific parts of LaTeX expressions.
## Basic t2c Usage
### Coloring Math Symbols
```python
from manimlib import *
class T2CExample(Scene):
def construct(self):
# Color specific variables
equation = Tex(
R"E = mc^2",
t2c={"E": BLUE, "m": GREEN, "c": YELLOW}
)
self.add(equation)
```
### Coloring Substrings
```python
# Color parts of the formula
formula = Tex(
R"\int_0^1 x^2 \, dx = \frac{1}{3}",
t2c={
R"\int": BLUE,
"x": GREEN,
R"\frac{1}{3}": YELLOW
}
)
```
## Advanced t2c Patterns
### Coloring Multiple Instances
```python
# All instances of a variable get colored
series = Tex(
R"\sum_{n=1}^{\infty} \frac{1}{n^2} = \frac{\pi^2}{6}",
t2c={
"n": BLUE, # Colors all 'n's
R"\pi": RED,
R"\sum": GREEN
}
)
```
### Using isolate with t2c
```python
# Isolate specific parts for individual control
equation = Tex(
R"a^2 + b^2 = c^2",
isolate=["a", "b", "c", "^2"],
t2c={
"a": RED,
"b": GREEN,
"c": BLUE,
"^2": YELLOW
}
)
```
## Dynamic Coloring
### set_color_by_tex
```python
# Color after creation
formula = Tex(R"f(x) = x^2 + 2x + 1")
formula.set_color_by_tex("x", BLUE)
formula.set_color_by_tex("f", GREEN)
formula.set_color_by_tex("1", YELLOW)
```
### Gradient Coloring
```python
# Apply gradient to entire formula
formula = Tex(R"\nabla \times \vec{E} = -\frac{\partial \vec{B}}{\partial t}")
formula.set_submobject_colors_by_gradient(BLUE, GREEN, YELLOW)
```
## Text Coloring (Text class)
### t2c for Text Objects
```python
# Color words in Text
text = Text(
"The quick brown fox jumps",
t2c={
"quick": BLUE,
"brown": ORANGE,
"fox": GREEN
}
)
```
### Multiple Styling Options
```python
# Combine t2c, t2f, t2s, t2w
text = Text(
"Different styles and colors",
t2c={"Different": RED, "colors": BLUE},
t2f={"styles": "Consolas"},
t2s={"styles": ITALIC},
t2w={"Different": BOLD}
)
```
## Complex Examples
### Physics Equation with Color Coding
```python
class ColoredPhysicsEquation(Scene):
def construct(self):
# Maxwell's equation with color-coded components
maxwell = Tex(
R"\nabla \times \vec{E} = -\frac{\partial \vec{B}}{\partial t}",
t2c={
R"\nabla": BLUE,
R"\vec{E}": RED,
R"\vec{B}": GREEN,
"t": YELLOW
}
)
self.play(Write(maxwell))
self.wait()
```
### Step-by-Step Derivation
```python
class ColoredDerivation(Scene):
def construct(self):
# Initial equation
eq1 = Tex(
R"(a + b)^2 = a^2 + 2ab + b^2",
t2c={"a": BLUE, "b": GREEN}
)
# Expanded form
eq2 = Tex(
R"(a + b)^2 = (a + b)(a + b)",
t2c={"a": BLUE, "b": GREEN}
)
# Show transformation
self.play(Write(eq1))
self.wait()
self.play(TransformMatchingTex(eq1, eq2))
self.wait()
```
### Highlighting Specific Terms
```python
class HighlightTerms(Scene):
def construct(self):
# Quadratic formula with highlighted discriminant
formula = Tex(
R"x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}",
t2c={
"x": WHITE,
"b": BLUE,
"a": GREEN,
"c": YELLOW,
R"b^2 - 4ac": RED # Discriminant in red
}
)
# Add label for discriminant
discriminant_label = Text("Discriminant", color=RED, font_size=30)
discriminant_label.next_to(formula, DOWN)
self.play(Write(formula))
self.play(FadeIn(discriminant_label, shift=UP))
self.wait()
```
## Coloring LaTeX Operators
```python
# Color different operator types
expression = Tex(
R"\int_0^\infty e^{-x^2} dx = \frac{\sqrt{\pi}}{2}",
t2c={
R"\int": BLUE, # Integral
"e": GREEN, # Exponential
R"\pi": RED, # Pi
"x": YELLOW, # Variable
"2": ORANGE # Exponent
}
)
```
## Best Practices
1. **Use raw strings with R**: Always use `R"..."` for LaTeX strings in ManimGL
2. **Test isolate first**: Use `isolate=` to verify what can be colored independently
3. **Consistent color scheme**: Use meaningful colors (e.g., variables in blue, constants in green)
4. **Don't over-color**: Too many colors can be distracting
5. **Color for emphasis**: Highlight the important parts you want viewers to focus on
## Common Patterns
### Creating a color scheme for math
```python
MATH_COLORS = {
"variables": BLUE,
"constants": GREEN,
"operators": YELLOW,
"results": RED
}
equation = Tex(
R"x^2 + y^2 = r^2",
t2c={
"x": MATH_COLORS["variables"],
"y": MATH_COLORS["variables"],
"r": MATH_COLORS["constants"]
}
)
```
### Animating color changes
```python
class AnimateColorChange(Scene):
def construct(self):
formula = Tex(R"f(x) = x^2")
# Start with one color
formula.set_color(BLUE)
self.add(formula)
self.wait()
# Animate to different colors
self.play(formula.animate.set_color_by_tex("x", RED))
self.wait()
```
## Troubleshooting
### If t2c doesn't work:
1. Check if the substring exists exactly in the LaTeX string
2. Use `isolate=` to separate the part you want to color
3. Remember that spacing matters in LaTeX
4. Use raw strings `R"..."` not regular strings
### Example of common issue:
```python
# This might not work if spaces don't match:
wrong = Tex(R"a+b", t2c={"a + b": RED}) # Won't match "a+b"
# This will work:
right = Tex(R"a + b", t2c={"a": RED, "b": BLUE})
```

Some files were not shown because too many files have changed in this diff Show More