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:
@@ -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
Reference in New Issue
Block a user