
Manimgl Best Practices
- 1.6k installs
- 1k repo stars
- Updated January 23, 2026
- adithya-s-k/manim_skill
manimGL animation best practices.
About
The manimgl-best-practices skill provides manimlib animation code patterns for attention flow visualizations and 3Blue1Brown-style transformer visuals. Includes random_bright_color hue_range helper and value_to_color mapping positive negative value gradients for BLUE and RED scales. Example AttentionArcsAnimation runnable via manimgl attention_arcs_animation.py AttentionArcsAnimation -o. Use building mathematical animations attention arc diagrams and manimGL scenes with consistent color mapping utilities for educational motion graphics content. manimlib attention arc animation patterns. value_to_color gradient mapping helper. random_bright_color hue utility. 3Blue1Brown transformer visualization style. Run via manimgl module Scene -o output. manimGL animation best practices. User asks manimgl animation best practices.
- manimlib attention arc animation patterns.
- value_to_color gradient mapping helper.
- random_bright_color hue utility.
- 3Blue1Brown transformer visualization style.
- Run via manimgl module Scene -o output.
Manimgl Best Practices by the numbers
- 1,595 all-time installs (skills.sh)
- +33 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #182 of 1,340 Generative Media skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
manimgl-best-practices capabilities & compatibility
- Capabilities
- scene patterns · color mapping
- Use cases
- video generation
npx skills add https://github.com/adithya-s-k/manim_skill --skill manimgl-best-practicesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.6k |
|---|---|
| repo stars | ★ 1k |
| Security audit | 2 / 3 scanners passed |
| Last updated | January 23, 2026 |
| Repository | adithya-s-k/manim_skill ↗ |
manimGL attention arc animation?
manimGL animation patterns including attention arcs and color utilities for mathematical visualizations.
Who is it for?
Educational animation authors.
Skip if: Non-manim video editing.
When should I use this skill?
User asks manimgl animation best practices.
What you get
Runnable manim scene with color helpers.
- ManimGL scene Python files
- rendered animation output
Files
""" 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), )
de
"""
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),
)
"""
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)
"""
Attention Mechanism Scenes - ManimGL Examples
A collection of scenes from 3Blue1Brown's Attention video,
adapted to be self-contained without external image dependencies.
Run with: manimgl attention_scenes.py <SceneName> -w -l
Available scenes:
- ShowMasking
- ScalingAPattern
- LowRankTransformation
- ThinkAboutOverallMap
- CrossAttention
- TwoHarrysExample
- QueryMap
- MultiHeadedAttention
"""
from manimlib import *
import numpy as np
import re
import itertools as it
import random
import warnings
# ============================================================
# Helper Functions
# ============================================================
def softmax(logits, temperature=1.0):
"""Compute softmax of logits array."""
logits = np.array(logits)
with warnings.catch_warnings():
warnings.filterwarnings('ignore')
logits = logits - np.max(logits)
exps = np.exp(np.divide(logits, temperature, where=temperature != 0))
if np.isinf(exps).any() or np.isnan(exps).any() or temperature == 0:
result = np.zeros_like(logits)
result[np.argmax(logits)] = 1
return result
return exps / np.sum(exps)
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 using interpolation."""
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)
def break_into_pieces(phrase_mob, offsets):
"""Break a Text mobject into pieces at given 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 break_into_words(phrase_mob):
"""Break a Text mobject into individual words."""
offsets = [m.start() for m in re.finditer(" ", phrase_mob.get_string())]
return break_into_pieces(phrase_mob, [0, *offsets])
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 behind phrase 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 WeightMatrix(DecimalMatrix):
"""A matrix display for neural network weights."""
def __init__(
self,
values=None,
shape=(6, 8),
value_range=(-9.9, 9.9),
ellipses_row=-2,
ellipses_col=-2,
num_decimal_places=1,
bracket_h_buff=0.1,
decimal_config=dict(include_sign=True),
low_positive_color=BLUE_E,
high_positive_color=BLUE_B,
low_negative_color=RED_E,
high_negative_color=RED_B,
):
if values is not None:
shape = values.shape
self.shape = shape
self.value_range = value_range
self.low_positive_color = low_positive_color
self.high_positive_color = high_positive_color
self.low_negative_color = low_negative_color
self.high_negative_color = high_negative_color
self.ellipses_row = ellipses_row
self.ellipses_col = ellipses_col
if values is None:
values = np.random.uniform(*self.value_range, size=shape)
super().__init__(
values,
num_decimal_places=num_decimal_places,
bracket_h_buff=bracket_h_buff,
decimal_config=decimal_config,
ellipses_row=ellipses_row,
ellipses_col=ellipses_col,
)
self.set_entry_colors()
def set_entry_colors(self):
for entry in self.get_entries():
if isinstance(entry, DecimalNumber):
entry.set_color(value_to_color(
entry.get_value(),
self.low_positive_color,
self.high_positive_color,
self.low_negative_color,
self.high_negative_color,
min_value=0.0,
max_value=self.value_range[1],
))
return self
class NumericEmbedding(DecimalMatrix):
"""A vector display for embeddings."""
def __init__(
self,
values=None,
length=8,
value_range=(-9.9, 9.9),
num_decimal_places=1,
bracket_h_buff=0.1,
decimal_config=dict(include_sign=True),
ellipses_row=-2,
):
if values is None:
values = np.random.uniform(*value_range, (length, 1))
super().__init__(
values,
num_decimal_places=num_decimal_places,
bracket_h_buff=bracket_h_buff,
decimal_config=decimal_config,
ellipses_row=ellipses_row,
)
class ContextAnimation(LaggedStart):
"""Animation showing context flowing between tokens."""
def __init__(
self,
target,
sources,
direction=UP,
hue_range=(0.1, 0.3),
time_width=2,
min_stroke_width=0,
max_stroke_width=5,
lag_ratio=None,
strengths=None,
run_time=3,
fix_in_frame=False,
path_arc=PI / 2,
**kwargs,
):
arcs = VGroup()
if strengths is None:
strengths = np.random.random(len(sources))**2
for source, strength in zip(sources, strengths):
sign = direction[1] * (-1)**int(source.get_x() < target.get_x())
arcs.add(Line(
source.get_edge_center(direction),
target.get_edge_center(direction),
path_arc=sign * path_arc,
stroke_color=random_bright_color(hue_range=hue_range),
stroke_width=interpolate(
min_stroke_width,
max_stroke_width,
strength,
)
))
if fix_in_frame:
arcs.fix_in_frame()
arcs.shuffle()
lag_ratio = 0.5 / len(arcs) if lag_ratio is None else lag_ratio
super().__init__(
*(
VShowPassingFlash(arc, time_width=time_width)
for arc in arcs
),
lag_ratio=lag_ratio,
run_time=run_time,
**kwargs,
)
class RandomizeMatrixEntries(Animation):
"""Animation that randomizes matrix entries."""
def __init__(self, matrix, **kwargs):
self.matrix = matrix
super().__init__(matrix, **kwargs)
def interpolate_mobject(self, alpha):
if random.random() < 0.1:
for entry in self.matrix.get_entries():
if isinstance(entry, DecimalNumber):
new_val = random.uniform(-9.9, 9.9)
entry.set_value(new_val)
# ============================================================
# Scene Definitions
# ============================================================
class ShowMasking(Scene):
"""Demonstrates causal masking in attention."""
def construct(self):
# Set up two patterns
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)
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)
# Highlight lower lefts (masking)
changers = VGroup()
for n, dec in enumerate(raw_values):
i = n // shape[1]
j = n % shape[1]
if i > j:
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()
# 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())
)
for n, value in enumerate(normalized_values):
value.set_fill(opacity=interpolate(0.5, 1, rush_from(value.get_value())))
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()
class ScalingAPattern(Scene):
"""Shows a large attention pattern scaling up."""
def construct(self):
# Position grid
N = 50
grid = Square(side_length=1.0).get_grid(N, N, buff=0)
grid.set_stroke(GREY_A, 1)
grid.stretch(0.89, 0)
grid.stretch(0.70, 1)
grid.move_to(5.0 * LEFT + 2.5 * UP, UL)
self.add(grid)
# Dots representing attention weights
values = np.random.normal(0, 1, (N, N))
dots = VGroup()
for n, row in enumerate(values):
row[:n] = -np.inf
for k, col in enumerate(values.T):
for n, value in enumerate(softmax(col)):
dot = Dot(radius=0.3 * value**0.75)
dot.move_to(grid[n * N + k])
dots.add(dot)
dots.set_fill(GREY_C, 1)
self.add(dots)
# Add Q and K symbols
q_template = Tex(R"\vec{\textbf{Q}}_0").set_color(YELLOW)
k_template = Tex(R"\vec{\textbf{K}}_0").set_color(TEAL)
for template in [q_template, k_template]:
template.scale(0.75)
template.substr = template.make_number_changeable("0")
qs = VGroup()
ks = VGroup()
for n, square in enumerate(grid[:N], start=1):
q_template.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_template.substr.set_value(k)
k_template.next_to(square, LEFT, buff=2 * SMALL_BUFF)
ks.add(k_template.copy())
self.add(qs, ks)
# Slowly zoom out
self.play(
self.frame.animate.reorient(0, 0, 0, (14.72, -14.71, 0.0), 38.06),
grid.animate.set_stroke(width=1, opacity=0.25),
dots.animate.set_fill(GREY_B, 1).set_stroke(GREY_B, 1),
run_time=20,
)
self.wait()
class LowRankTransformation(Scene):
"""Visualizes low-rank transformation in attention."""
def construct(self):
frame = self.frame
frame.set_field_of_view(10 * DEGREES)
all_axes = VGroup(
self.get_3d_axes(),
self.get_2d_axes(),
self.get_3d_axes(),
)
all_axes.arrange(RIGHT, buff=2.0)
all_axes.set_width(FRAME_WIDTH - 2)
all_axes.move_to(0.5 * DOWN)
dim_labels = VGroup(
Text("12,288 dims"),
Text("128 dims"),
Text("12,288 dims"),
)
dim_labels.scale(0.75)
dim_labels.set_fill(GREY_A)
for label, axes in zip(dim_labels, all_axes):
label.next_to(axes, UP, buff=MED_LARGE_BUFF)
map_arrows = Tex(R"\rightarrow", font_size=96).replicate(2)
map_arrows.set_color(YELLOW)
for arrow, vect in zip(map_arrows, [LEFT, RIGHT]):
arrow.next_to(all_axes[1], vect, buff=0.5)
axes_group = VGroup(all_axes, dim_labels)
self.add(axes_group)
self.add(map_arrows)
# Add vectors
all_coords = [
(4, 2, 1),
(2, 3),
(-3, 3, -2),
]
colors = [BLUE, RED_B, RED_C]
vects = VGroup(
Arrow(axes.get_origin(), axes.c2p(*coords), buff=0, stroke_color=color)
for axes, coords, color in zip(all_axes, all_coords, colors)
)
self.add(vects[0])
for v1, v2 in zip(vects, vects[1:]):
self.play(TransformFromCopy(v1, v2))
for axes, vect in zip(all_axes, vects):
axes.add(vect)
for axes in all_axes[0::2]:
axes.add_updater(lambda m, dt: m.rotate(2 * dt * DEGREES, axis=m.y_axis.get_vector()))
self.wait(3)
# Add title
big_rect = SurroundingRectangle(axes_group, buff=0.5)
big_rect.round_corners(radius=0.5)
big_rect.set_stroke(RED_B, 2)
title = Text("Low-rank transformation", font_size=72)
title.next_to(big_rect, UP, buff=MED_LARGE_BUFF)
self.play(
ShowCreation(big_rect),
FadeIn(title, shift=0.25 * UP)
)
self.wait(5)
def get_3d_axes(self, height=3):
result = ThreeDAxes((-4, 4), (-4, 4), (-4, 4))
result.set_height(height)
result.rotate(20 * DEGREES, DOWN)
result.rotate(5 * DEGREES, RIGHT)
return result
def get_2d_axes(self, height=2):
plane = NumberPlane(
(-4, 4), (-4, 4),
faded_line_ratio=0,
background_line_style=dict(
stroke_color=GREY_B,
stroke_width=1,
stroke_opacity=0.5
)
)
plane.set_height(height)
return plane
class ThinkAboutOverallMap(Scene):
"""Simple scene showing a reminder about overall maps."""
def construct(self):
rect = Rectangle(6.5, 2.75)
rect.round_corners(radius=0.5)
rect.set_stroke(RED_B, 2)
label = Text("Think about the\noverall map")
label.next_to(rect, UP, aligned_edge=LEFT)
label.shift(0.5 * RIGHT)
self.play(
ShowCreation(rect),
FadeIn(label, UP),
)
self.wait()
class CrossAttention(Scene):
"""Shows cross-attention between two languages."""
def construct(self):
# Show both language phrases
en_tokens = self.get_words("I do not want to pet it")
fr_tokens = self.get_words("Je ne veux pas le caresser", hue_range=(0.2, 0.3))
phrases = VGroup(en_tokens, fr_tokens)
phrases.arrange(DOWN, buff=2.0)
self.play(LaggedStartMap(FadeIn, en_tokens, scale=2, lag_ratio=0.25))
self.wait()
self.play(LaggedStartMap(FadeIn, fr_tokens, scale=2, lag_ratio=0.25))
self.wait()
# Create attention pattern
unnormalized_pattern = [
[3, 0, 0, 0, 0, 0],
[0, 1, 1.3, 1, 0, 0],
[0, 3, 0, 3, 0, 0],
[0, 0, 3, 0, 0, 0],
[0, 0, 0, 0, 0, 3],
[0, 0, 0, 0, 0, 3],
[0, 0, 0, 0, 3, 0],
]
attention_pattern = np.array([
softmax(col) for col in unnormalized_pattern
]).T
# Show connections
lines = VGroup()
for n, row in enumerate(attention_pattern.T):
for k, value in enumerate(row):
line = Line(en_tokens[n].get_bottom(), fr_tokens[k].get_top(), buff=0)
line.set_stroke(
color=[
en_tokens[n][0].get_color(),
fr_tokens[k][0].get_color(),
],
width=3,
opacity=value,
)
lines.add(line)
self.play(ShowCreation(lines, lag_ratio=0.01, run_time=2))
self.wait(2)
self.play(FadeOut(lines))
# Create grid
grid = Square().get_grid(len(fr_tokens), len(en_tokens), buff=0)
grid.stretch(1.2, 0)
grid.set_stroke(GREY_B, 1)
grid.set_height(5.0)
grid.to_edge(DOWN, buff=SMALL_BUFF)
grid.set_x(1)
# Create qk symbols
q_sym_generator = self.get_symbol_generator(R"\vec{\textbf{Q}}_0", color=YELLOW)
k_sym_generator = self.get_symbol_generator(R"\vec{\textbf{K}}_0", color=TEAL)
e_sym_generator = self.get_symbol_generator(R"\vec{\textbf{E}}_0", color=GREY_B)
f_sym_generator = self.get_symbol_generator(R"\vec{\textbf{F}}_0", color=BLUE)
q_syms = VGroup(q_sym_generator(n + 1) for n in range(len(en_tokens)))
k_syms = VGroup(k_sym_generator(n + 1) for n in range(len(fr_tokens)))
e_syms = VGroup(e_sym_generator(n + 1) for n in range(len(en_tokens)))
f_syms = VGroup(f_sym_generator(n + 1) for n in range(len(fr_tokens)))
VGroup(q_syms, k_syms, e_syms, f_syms).scale(0.65)
for q_sym, e_sym, square in zip(q_syms, e_syms, grid):
q_sym.next_to(square, UP, SMALL_BUFF)
e_sym.next_to(q_sym, UP, buff=0.65)
for k_sym, f_sym, square in zip(k_syms, f_syms, grid[::len(en_tokens)]):
k_sym.next_to(square, LEFT, SMALL_BUFF)
f_sym.next_to(k_sym, LEFT, buff=0.75)
q_arrows = VGroup(Arrow(*pair, buff=0.1) for pair in zip(e_syms, q_syms))
k_arrows = VGroup(Arrow(*pair, buff=0.1) for pair in zip(f_syms, k_syms))
e_arrows = VGroup(Vector(0.4 * DOWN).next_to(e_sym, UP, SMALL_BUFF) for e_sym in e_syms)
f_arrows = VGroup(Vector(0.5 * RIGHT).next_to(f_sym, LEFT, SMALL_BUFF) for f_sym in f_syms)
arrows = VGroup(q_arrows, k_arrows, e_arrows, f_arrows)
arrows.set_color(GREY_B)
wq_syms = VGroup(
Tex("W_Q", font_size=20, fill_color=YELLOW).next_to(arrow, RIGHT, buff=0.1)
for arrow in q_arrows
)
wk_syms = VGroup(
Tex("W_K", font_size=20, fill_color=TEAL).next_to(arrow, UP, buff=0.1)
for arrow in k_arrows
)
# Move tokens into place
en_tokens.target = en_tokens.generate_target()
fr_tokens.target = fr_tokens.generate_target()
for token, arrow in zip(en_tokens.target, e_arrows):
token.next_to(arrow, UP, SMALL_BUFF)
for token, arrow in zip(fr_tokens.target, f_arrows):
token.next_to(arrow, LEFT, SMALL_BUFF)
self.play(
MoveToTarget(en_tokens),
MoveToTarget(fr_tokens),
)
self.play(
LaggedStartMap(GrowArrow, e_arrows),
LaggedStartMap(GrowArrow, f_arrows),
LaggedStartMap(FadeIn, e_syms, shift=0.25 * DOWN),
LaggedStartMap(FadeIn, f_syms, shift=0.25 * RIGHT),
lag_ratio=0.25,
run_time=1.5,
)
self.play(
LaggedStartMap(GrowArrow, q_arrows),
LaggedStartMap(GrowArrow, k_arrows),
LaggedStartMap(FadeIn, wq_syms, shift=0.25 * DOWN),
LaggedStartMap(FadeIn, wk_syms, shift=0.25 * RIGHT),
LaggedStartMap(FadeIn, q_syms, shift=0.5 * DOWN),
LaggedStartMap(FadeIn, k_syms, shift=0.5 * RIGHT),
lag_ratio=0.25,
run_time=1.5,
)
self.play(FadeIn(grid, lag_ratio=1e-2), run_time=2)
self.wait()
# Show dot products
dot_prods = VGroup()
for q_sym in q_syms:
for k_sym in k_syms:
dot = Tex(".")
dot.match_x(q_sym)
dot.match_y(k_sym)
dot_prod = VGroup(q_sym.copy(), dot, k_sym.copy())
dot_prod.target = dot_prod.generate_target()
dot_prod.target.arrange(RIGHT, buff=SMALL_BUFF)
dot_prod.target.scale(0.7)
dot_prod.target.move_to(dot)
dot.set_opacity(0)
dot_prods.add(dot_prod)
self.play(
LaggedStartMap(MoveToTarget, dot_prods, lag_ratio=0.01),
run_time=3
)
self.wait()
# Show dots
dots = VGroup()
for square, value in zip(grid, attention_pattern.flatten()):
dot = Dot(radius=value * 0.4)
dot.set_fill(GREY_B, 1)
dot.move_to(square)
dots.add(dot)
self.play(
LaggedStartMap(GrowFromCenter, dots, lag_ratio=1e-2),
dot_prods.animate.set_fill(opacity=0.2).set_anim_args(lag_ratio=1e-3),
run_time=4
)
self.wait()
def get_words(self, text, hue_range=(0.5, 0.6)):
sent = Text(text)
tokens = break_into_words(sent)
rects = get_piece_rectangles(tokens, hue_range=hue_range)
return VGroup(VGroup(*pair) for pair in zip(rects, tokens))
def get_symbol_generator(self, raw_tex, subsrc="0", color=WHITE):
template = Tex(raw_tex)
template.set_color(color)
subscr = template.make_number_changeable(subsrc)
def get_sym(number):
subscr.set_value(number)
return template.copy()
return get_sym
class TwoHarrysExample(Scene):
"""Shows how context disambiguates 'Harry'."""
def construct(self):
s1, s2 = sentences = VGroup(
break_into_words(Text("... " + " ... ".join(words)))
for words in [
("wizard", "Hogwarts", "Hermione", "Harry"),
("Queen", "Sussex", "William", "Harry"),
]
)
sentences.arrange(DOWN, buff=2.0, aligned_edge=RIGHT)
sentences.to_edge(LEFT)
def context_anim(group):
self.play(
ContextAnimation(
group[-1],
VGroup(*it.chain(*group[1:-1:2])),
direction=DOWN,
path_arc=PI / 4,
run_time=5,
lag_ratio=0.025,
)
)
self.add(s1)
context_anim(s1)
self.wait()
self.play(FadeTransformPieces(s1.copy(), s2))
context_anim(s2)
class QueryMap(Scene):
"""Shows how embedding space maps to query/key space."""
map_tex = "W_Q"
map_color = YELLOW
src_name = "Creature"
pos_word = "position 4"
trg_name = "Any adjectives\nbefore position 4?"
in_vect_color = BLUE_B
in_vect_coords = (3, 2, -2)
out_vect_coords = (-2, -1)
def construct(self):
# Setup 3d axes
axes_3d = ThreeDAxes((-4, 4), (-3, 3), (-4, 4))
xz_plane = NumberPlane(
(-4, 4), (-4, 4),
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.0)
self.set_floor_plane("xz")
frame = self.frame
frame.set_field_of_view(30 * DEGREES)
frame.reorient(-32, 0, 0, (2.13, 1.11, 0.27), 4.50)
frame.add_ambient_rotation(1 * DEGREES)
self.add(axes_3d)
# Set up target plane
plane = NumberPlane(
(-3, 3), (-3, 3),
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.set_height(3.5)
plane.to_corner(DR)
arrow = Tex(R"\longrightarrow")
arrow.set_width(2)
arrow.stretch(0.75, 1)
arrow.next_to(plane, LEFT, buff=1.0)
arrow.set_color(self.map_color)
map_name = Tex(self.map_tex, font_size=72)
map_name.set_color(self.map_color)
map_name.next_to(arrow.get_left(), UR, SMALL_BUFF).shift(0.25 * RIGHT)
for mob in [plane, arrow, map_name]:
mob.fix_in_frame()
self.add(plane)
self.add(arrow)
self.add(map_name)
# Add titles
titles = VGroup(
Text("Embedding space"),
Text("Query/Key space"),
)
subtitles = VGroup(
Text("12,288-dimensional"),
Text("128-dimensional"),
)
subtitles.scale(0.75)
subtitles.set_fill(GREY_B)
x_values = [-frame.get_x() * FRAME_HEIGHT / frame.get_height(), plane.get_x()]
for title, subtitle, x_value in zip(titles, subtitles, x_values):
subtitle.next_to(title, DOWN, SMALL_BUFF)
title.add(subtitle)
title.next_to(plane, UP, MED_LARGE_BUFF)
title.set_x(x_value)
title.fix_in_frame()
self.add(titles)
# Show vector transformation
in_vect = Arrow(axes_3d.get_origin(), axes_3d.c2p(*self.in_vect_coords), buff=0)
in_vect.set_stroke(self.in_vect_color)
in_vect_label = TexText("``" + self.src_name + "''", font_size=24)
pos_label = Text(self.pos_word, font_size=16)
pos_label.next_to(in_vect_label, DOWN, SMALL_BUFF)
pos_label.set_opacity(0.75)
in_vect_label.add(pos_label)
in_vect_label.set_color(self.in_vect_color)
in_vect_label.next_to(in_vect.get_end(), UP, SMALL_BUFF)
out_vect = Arrow(plane.get_origin(), plane.c2p(*self.out_vect_coords), buff=0)
out_vect.set_stroke(self.map_color)
out_vect_label = Text(self.trg_name, font_size=30)
out_vect_label.next_to(out_vect.get_end(), DOWN, buff=0.2)
out_vect_label.set_backstroke(BLACK, 5)
VGroup(out_vect, out_vect_label).fix_in_frame()
self.play(
GrowArrow(in_vect),
FadeInFromPoint(in_vect_label, axes_3d.get_origin()),
)
self.wait(2)
self.play(
TransformFromCopy(in_vect, out_vect),
FadeTransform(in_vect_label.copy(), out_vect_label),
run_time=2,
)
self.wait(10)
self.play(FadeOut(out_vect_label))
self.wait(3)
class MultiHeadedAttention(Scene):
"""Demonstrates multi-headed attention with procedural patterns."""
def construct(self):
# Mention head
background_rect = FullScreenRectangle()
single_title = Text("Single head of attention")
multiple_title = Text("Multi-headed attention")
titles = VGroup(single_title, multiple_title)
for title in titles:
title.scale(1.25)
title.to_edge(UP)
# Create attention pattern instead of loading image
screen_rect = ScreenRectangle(height=6)
screen_rect.set_fill(BLACK, 1)
screen_rect.set_stroke(WHITE, 3)
screen_rect.next_to(titles, DOWN, buff=0.5)
head = single_title["head"][0]
self.add(background_rect)
self.add(single_title)
self.add(screen_rect)
self.wait()
self.play(
FlashAround(head, run_time=2),
head.animate.set_color(YELLOW),
)
self.wait()
# Change 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)
# Set up procedural attention pattern heads
n_heads = 15
heads = Group()
for n in range(n_heads):
# Create procedural attention pattern
pattern_grid = self.create_attention_pattern(seed=n * 7)
pattern_grid.set_opacity(1)
pattern_grid.shift(0.01 * OUT)
rect = SurroundingRectangle(pattern_grid, buff=0)
rect.set_fill(BLACK, 0.75)
rect.set_stroke(WHITE, 1, 1)
heads.add(Group(rect, pattern_grid))
# Show many parallel layers
self.set_floor_plane("xz")
frame = self.frame
multiple_title.fix_in_frame()
background_rect.fix_in_frame()
heads.set_height(4)
heads.arrange(OUT, buff=1.0)
heads.move_to(DOWN)
pre_head = self.create_attention_pattern(seed=0)
pre_head.replace(screen_rect)
pre_head_rect = SurroundingRectangle(pre_head, buff=0)
pre_head_rect.set_fill(BLACK, 0.75)
pre_head_rect.set_stroke(WHITE, 1, 1)
pre_head = Group(pre_head_rect, pre_head)
self.add(pre_head)
self.wait()
self.play(
frame.animate.reorient(41, -12, 0, (-1.0, -1.42, 1.09), 12.90).set_anim_args(run_time=2),
background_rect.animate.set_fill(opacity=0.75),
FadeTransform(pre_head, heads[-1], time_span=(1, 2)),
)
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()
# Show matrices
colors = [YELLOW, TEAL, RED, PINK]
texs = ["W_Q", "W_K", R"\downarrow W_V", R"\uparrow W_V"]
n_shown = 9
wq_syms, wk_syms, wv_down_syms, wv_up_syms = sym_groups = VGroup(
VGroup(
Tex(tex + f"^{{({n})}}", font_size=36).next_to(image, UP, MED_SMALL_BUFF)
for n, image in enumerate(heads[:-n_shown - 1:-1], start=1)
).set_color(color).set_backstroke(BLACK, 5)
for tex, color in zip(texs, colors)
)
for group in wv_down_syms, wv_up_syms:
for sym in group:
sym[0].next_to(sym[1], LEFT, buff=0.025)
dots = Tex(R"\dots", font_size=90)
dots.rotate(PI / 2, UP)
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)
dots.next_to(syms, IN, buff=0.5)
dots.match_style(syms[0])
syms.add(dots.copy())
up_shift = 0.75 * UP
self.play(
LaggedStartMap(FadeIn, wq_syms, 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,
)
for n in range(1, len(sym_groups)):
self.play(
LaggedStartMap(FadeIn, sym_groups[n], shift=0.2 * UP, lag_ratio=0.1),
sym_groups[:n].animate.shift(up_shift),
run_time=1,
)
self.wait()
# Count up 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(2)
def create_attention_pattern(self, n_rows=8, seed=0):
"""Create a procedural attention pattern grid."""
np.random.seed(seed)
grid = Square().get_grid(n_rows, 1, buff=0).get_grid(1, n_rows, buff=0)
grid.set_stroke(WHITE, 1, 0.5)
grid.set_height(3.0)
pattern = np.random.normal(0, 1, (n_rows, n_rows))
for n in range(len(pattern[0])):
pattern[:, n][n + 1:] = -np.inf
pattern[:, n] = softmax(pattern[:, n])
pattern = pattern.T
dots = VGroup()
for col, values in zip(grid, pattern):
for square, value in zip(col, values):
if value < 1e-3:
continue
dot = Dot(radius=0.4 * square.get_height() * value)
dot.move_to(square)
dots.add(dot)
dots.set_fill(GREY_B, 1)
grid.add(dots)
return grid
"""
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)
"""
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)
"""
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()
"""
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
"""
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]
"""
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()
"""
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
"""
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)
"""
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)
"""
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
"""
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)
"""
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
Related skills
FAQ
Run example?
manimgl attention_arcs_animation.py AttentionArcsAnimation -o.
Color helper?
value_to_color maps values to blue red gradients.
Library?
manimlib imported from manimGL.
Is Manimgl Best Practices safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.