ml-content
Generate ML explainer content that looks like 3Blue1Brown, not like AI slop.
⚠️ PRIME DIRECTIVE — treat every video as nuclear. ZERO errors ship.
The content goes public to an audience that will fact-check it. A single wrong number, mislabeled quantity, or overstated claim destroys trust in everything else and gets screenshotted. So: nothing — not even slightly — may be wrong. Every spoken line, every on-screen number and label, every caption, and the thumbnail must be verified against the primary source before it is rendered, and audited again on the rendered video before it ships (§10, the non-negotiable gate). If you cannot cite the exact source line for a claim, you do not say it, write it, or put it on screen. Soften it or cut it. No "dramatic license" on numbers. When in doubt, it is wrong until proven right.
This skill was rebuilt from a full read of Grant Sanderson's actual production code (github.com/3b1b/videos, 503K LOC, 2015→2026) and the real manimGL engine (github.com/3b1b/manim). Every rule below is grounded in that source with file:line citations. Where this skill once guessed, it now measures.
The video engine is manimGL (the 3b1b version), driven by manimgl. Manim Community Edition (CE) is a different library with a different, incompatible API — code written for one crashes on the other. Static IG carousels use HTML/matplotlib (see the Carousel section); everything animated is manimGL.
0. Why the old output was slop (read this once)
The previous version of this skill produced overlapping elements, weak animation, no consistency, and infographic-feeling stills. The root causes, now fixed:
- It shipped Manim CE code while preaching manimGL. The old template used
from manim import *, MathTex, ThreeDScene, set_camera_orientation, set_fill_by_value, Create, begin_ambient_camera_rotation — none of which exist in manimGL (grep over the entire engine = 0 hits). It would not even run.
- 100% of real output was actually built in matplotlib, hand-placing ~57 text calls + ~20 boxes per scene with absolute coordinates. That is the worst possible tool for animation: no relative layout, no transform system, no camera. Overlap is guaranteed.
- It treated overlap as a validation problem (bbox asserts, frame validators, ffmpeg caption-pads) instead of a construction problem. 3b1b never validates overlap — it makes overlap structurally impossible by building self-arranging objects.
- It treated motion as decoration. Elements faded in from nowhere as disconnected islands. In real 3b1b, objects are born from the thing they abstract (
TransformFromCopy), so every motion teaches.
- Planning was marketing copy with word-count targets. Real 3b1b planning is an ordered list of named teaching beats that reads top-to-bottom as the narration.
The fix is a different mental model, encoded as the Six Laws below.
1. The engine reality (the single most important section)
Start every manim file with:
from manim_imports_ext import * # in the 3b1b/videos repo
# or, standalone: from manimlib import *
Scene base class is InteractiveScene (interactive_scene.py:66) — for 2D and 3D. In the entire 2025 corpus: InteractiveScene subclassed 385 times, ThreeDScene (scene.py:930) 0 times — it exists but 3b1b never uses it. 3D is achieved on an InteractiveScene by moving self.frame.
The camera is self.frame (a CameraFrame mobject; scene.py:112). Local alias frame = self.frame appears 114× in 2025 code. self.camera.frame appears 0×.
Render / iterate:
manimgl file.py SceneName # render
manimgl file.py SceneName -se 120 # drop into IPython at line 120 (the dev loop)
manimgl file.py SceneName -w # write to file
# inside the embed: checkpoint_paste() runs clipboard code with checkpoint rewind
CE landmines — Table A: these genuinely CRASH on manimGL (absent symbols)
| You must NOT emit (CE) |
Use instead (manimGL) |
Evidence |
from manim import * |
from manim_imports_ext import * / from manimlib import * |
CLAUDE.md:59 (loads the wrong library) |
MathTex(...) |
Tex(...) |
grep MathTex over repo = 0; CLAUDE.md:82 |
self.set_camera_orientation(phi=,theta=,zoom=) |
self.frame.reorient(theta, phi, gamma, center, height) |
absent; camera_frame.py:172 |
self.move_camera(...) |
self.play(self.frame.animate.reorient(...)) |
absent |
self.begin_ambient_camera_rotation() |
self.frame.add_ambient_rotation(1 * DEG) |
absent; camera_frame.py:212 |
self.add_fixed_in_frame_mobjects(m) |
m.fix_in_frame() |
absent (no scene-level helper) |
Create(m) |
ShowCreation(m) |
absent; creation.py:48 |
Unwrite(m) |
Uncreate(m) or FadeOut(m) |
absent |
surface.set_fill_by_value(...) |
surface.set_color(c, opacity) / set_color_by_xyz_func(...) |
absent |
Circumscribe(m) |
FlashAround(m) |
absent in engine (the one truly-missing indicator) |
Wiggle(m) (class) |
WiggleOutThenIn(m) or rate_func=wiggle |
no Wiggle class; indication.py:355 |
ease_in_out_*, smoothstep, easeOutCubic |
the 15 real rate funcs (§7) |
not in rate_functions.py |
FadeInFrom, FadeOutAndShift, SpinInFromNothing, AddTextLetterByLetter |
FadeIn(m, shift=, scale=), AddTextWordByWord |
absent |
Self-check (must return nothing):
grep -nE 'MathTex|from manim import \*|set_camera_orientation|begin_ambient_camera_rotation|move_camera|add_fixed_in_frame_mobjects|set_fill_by_value|\bCreate\(|\bCircumscribe\b|\bWiggle\(|\bUnwrite\(|FadeInFrom|SpinInFromNothing|AddTextLetterByLetter' your_file.py
Table B: these RUN on manimGL but are wrong/stale style — don't emit anyway
| Avoid (valid but off-style) |
Prefer |
Why |
class S(Scene) / class S(ThreeDScene) |
class S(InteractiveScene) |
both exist & run, but 2025 corpus is 385× InteractiveScene, 0× ThreeDScene; 3D uses self.frame |
TransformMatchingTex(a, b) |
TransformMatchingStrings(a, b) |
TransformMatchingTex exists (subclasses Strings) but 3b1b uses Strings |
eq.set_color_by_tex(tok, c) |
Tex(R"...", t2c={tok: c}) or inline eq[tok].set_color(c) |
set_color_by_tex is a real Tex method (tex_mobject.py:207) but ~unused; inline set_color dominates |
Indicate(m) / CircleIndicate(m) |
FlashAround(m) / Flash / FlashUnder |
both exist & are used (indication.py:73,142); modern corpus just reaches for Flash* far more |
DEGREES |
DEG |
DEGREES is a live alias of DEG (won't crash); 2025 uses DEG ~520× vs DEGREES 3× |
The split matters: don't let a self-check grep reject valid 3b1b code. Indicate, TransformMatchingTex, set_color_by_tex, and DEGREES are style calls, not crashers — only Table A NameErrors.
2. The mental model (what makes it 3b1b, not slop)
You are not laying out a frame. You are building a small cast of self-arranging objects and transforming them through a sequence of beats where each motion is the explanation.
Two layers, every video:
helpers.py — the domain DSL. 4–10 Mobject subclasses that build and arrange themselves (WeightMatrix, NumericEmbedding, EmbeddingArray, Dial, NeuralNetwork, ContextAnimation — _2024/transformers/helpers.py), plus 2–5 show_*(scene, ...) choreography verbs. Define the cast before writing a single construct().
- Scene files — thin
InteractiveScene subclasses whose construct() is a flat list of # Beat name comments, each assembling DSL objects and animating them.
Everything below serves this model.
3. The Six Laws (non-negotiable)
Law 1 — Relative layout only. Overlap is structural, not policed.
Real 3b1b layout is overwhelmingly relative: next_to used ~10,500×, arrange ~2,460× across the repo, versus only a handful of absolute move_to([x,y,z]) content placements — and nearly all of those absolutes are the camera, never content. Forbid absolute content coordinates. move_to([x,y,z]) and frame.animate.move_to(...) are for the camera/light source only.
Allowed positioning primitives — content position ALWAYS comes from one of these:
a.next_to(b, DOWN, buff=MED_LARGE_BUFF) # relative to another object
a.next_to(b, RIGHT).match_y(c) # compound: x from b, y aligned to c
a.align_to(b, LEFT) # share an edge
a.to_edge(UP, buff=LARGE_BUFF) # to a frame edge (chrome only)
a.to_corner(UL) # to a corner (chrome only)
VGroup(*items).arrange(DOWN, buff=MED_SMALL_BUFF, aligned_edge=LEFT)
VGroup(*items).arrange_in_grid(rows, cols, buff=...)
dots = Dot().get_grid(n_rows, n_cols, buff_ratio=0.5)
a.match_x(b) / a.match_y(b) / a.match_width(b) / a.match_height(b)
Boxes and pills are content-sized, never hand-sized:
rect = SurroundingRectangle(label, buff=SMALL_BUFF) # measures real glyph bounds
brace = Brace(group, DOWN); brace.get_text("12,288") # fits the span, anchors at tip
under = Underline(word) # width derived from content
SurroundingRectangle is used ~1,400× in the repo and cannot clip or mis-center because its size = target.get_shape() + 2*buff (shape_matchers.py:22). Delete every text_width() character-count heuristic — it has no analog in real code.
The buff ladder is the ONLY source of gaps (default_config.yml:109):
SMALL_BUFF=0.1 MED_SMALL_BUFF=0.25 MED_LARGE_BUFF=0.5 LARGE_BUFF=1.0
Never write buff=0.37. Consistent margins across scenes come for free from this ladder.
Build the group, then place the group. Assemble a VGroup declaratively, .arrange() its internals once, then position the whole thing. This is the dominant pattern across the repo's thousands of VGroups. Never position leaf elements against screen coordinates.
Labels on moving targets follow live: label.always.next_to(target, UP, buff=SMALL_BUFF) (or add_updater). A single-frame bbox assert can't catch a mid-animation collision; a re-running next_to never collides.
Because of Law 1, the old "Five-Layer Defense" (bbox asserts + frame validator + ffmpeg caption pad) is retired. Overlap is prevented at construction. Keep at most a light visual probe-frame check for taste and timing, not collision.
Law 2 — Motion carries meaning. Born-from, never spawn.
The highest-frequency, highest-leverage technique in the corpus: a key object enters by transforming from the concrete thing it abstracts, so the causal link is visible.
# the DALL·E image literally dissolves INTO the numeric vector entries (attention.py:128)
self.play(LaggedStart(*(bake_mobject_into_vector_entries(img, vec) for img, vec in ...)))
# 12,288 numbers collapse into a single symbol E_n (attention.py:200)
self.play(FadeTransform(entry, sym))
# a formula term is delivered by morphing the data column it denotes (ml_basics.py:642)
self.play(TransformFromCopy(data_column, x_symbols))
Transform-family hard counts (attention.py): TransformFromCopy 46 > FadeTransform 31 > ReplacementTransform 6 > plain Transform ~5 ; TransformMatchingTex 0.
TransformFromCopy(src, dst) is THE workhorse — source persists, a copy morphs to the destination, so the viewer sees "this becomes that" while "this" is still there.
- For equation retitles,
TransformMatchingStrings (not ...Tex).
- Never
FadeIn a load-bearing object from nothing. Decorative scaffolding can fade in; the thing the lesson is about must be born from its referent.
time_span=(start, end) choreographs a reveal inside ONE self.play (used 638× in the repo) so a camera move and a multi-part reveal cascade together instead of firing simultaneously:
# the softmax aha — one play, run_time=3, cascaded (attention.py:921)
self.play(
self.frame.animate.reorient(...),
GrowArrow(arrow, time_span=(1, 2)),
FadeIn(label, time_span=(1, 2)),
TransformFromCopy(ndp_col, softmax_col, time_span=(1.5, 3)),
run_time=3,
)
generate_target / MoveToTarget (127 refs) is how dozens of objects snap into a new arrangement with zero hand-keyed coordinates:
grp.target = grp.generate_target()
grp.target.arrange(RIGHT, buff=0.15) # mutate the target with normal layout calls
grp.target.scale(0.65).next_to(anchor, DOWN)
self.play(MoveToTarget(grp))
Law 3 — A tiny domain DSL gives consistency.
Every video defines ~6 custom Mobjects/Animations in a helpers.py plus ~6 scene-local get_*/show_* factories. A vocabulary, not a framework. This is the mechanism for both consistency and no-overlap (the objects arrange themselves).
For ML content, don't start from scratch — vendor 3b1b's transformer DSL (NumericEmbedding, WeightMatrix, EmbeddingArray, ContextAnimation, value_to_color). It is the single fastest path to the authentic "columns of real numbers" look. See §13.
Mobject-subclass recipe (model: Dial, helpers.py:655; MachineWithDials:761):
class WidgetThing(VGroup):
def __init__(self, value=0, ...):
# 1. build sub-parts
body = Rectangle(...); needle = Line(...)
# 2. lay them out RELATIVELY (ratio buffers, never move_to([x,y,0]))
ticks = Line(...).get_grid(1, n, buff_ratio=0.5)
ticks.set_width(body.get_width() - SMALL_BUFF); ticks.move_to(body)
# 3. assemble, 4. name every meaningful part
super().__init__(body, ticks, needle)
self.body, self.needle = body, needle
# 5. a state mutator that recomputes geometry+style from the logical value
self.set_value(value)
def set_value(self, v):
self.needle.put_start_and_end_on(self.get_center(), self._value_to_point(v))
self.needle.set_color(value_to_color(v)) # color is a pure function of value
def animate_set_value(self, v, **kw): # 6. methods that RETURN animations
return AnimationGroup(self.animate.set_value(v), ...)
show_*(scene, ...) choreography-verb recipe (model: show_matrix_vector_product, helpers.py:97):
- first arg is
scene; the function calls scene.play/scene.wait internally,
- owns its transient highlights via a
last_rects/to_fade accumulator so exactly one highlight is ever on screen,
- returns the persistent mobjects it created.
Variants are 2-line subclasses of a shared base overriding one class attribute — never a copy-pasted construct() (model HighlightEarthOrbit(NearestPlanets) with highlighted_orbit = 2, and its sibling HighlightMarsOrbit(NearestPlanets) with = 3; planets.py:2202). Domain numbers live in ALL-CAPS module constants with one conversion_factor (planets.py:4), so sizes can't disagree across scenes.
Large data uses honest ellipsis: render a finite set, swap one element for Tex(R"\dots") (or ellipses_row=-2, default in WeightMatrix), so big arrays read as big without overflowing (helpers.py:478, 620).
Law 4 — Earned 3D only, and 3D is never static.
3D is earned only when a quantity's dimensionality is the payload (a vector space, a surface f(x,y)=z, a volumetric field). Roughly half of even the attention video is intentionally flat. A flat data series in 3D is the #1 faux-3D tell — and the old matplotlib "isometric stack of parallelograms" cube is exactly the slop to never produce.
Production lesson (§13): 3D actively HURTS a stack of thin layers (interleaved attention layers, a residual tower) — viewed at an angle they collapse into one solid block and the per-layer colors vanish. Render those flat, face-on. Reserve 3D for clouds, surfaces, and collapsing volumes.
Counter-lesson — over-flattening is its own slop (learned shipping the DeepSeek/Qwen attention series). Apply "flat by default" too zealously and every scene becomes the same colored square grid. A viewer's note, verbatim: "it just seems like a single block matrix throughout the whole video, we're lacking elements." Flatness is honest, but monotony kills retention, and the cure is variety + rigor, not decoration:
- Rotate the visual primitive across the reel. Don't reuse the row-of-cells / n×n grid in more than ~half the scenes. Across ~10 beats budget at least: one numeric-matrix scene, one on-screen equation, one earned-3D scene — plus vectors / a plot / a code view.
- Earned-3D is earned more often than "half flat" implies — use it. Three patterns that shipped clean (and read as not faux-3D because dimensionality IS the payload):
- A memory/field as a
z=f(x,y) surface that DEFORMS under its update. e.g. the gated-delta-rule state: forget = scale heights down, erase = remove a bump, write = raise a new bump. srf = ThreeDAxes((-3,3),(-3,3),(0,3)).get_graph(lambda x,y: sum_of_gaussians(x,y,bumps)); srf.set_opacity(0.7); srf.always_sort_to_camera(self.camera) then Transform(srf, make_surf(new_bumps)) between states; frame.add_ambient_rotation(0.5*DEG) to orbit.
- A multi-dimensional tensor as a
Cube volume that grows or collapses. The KV cache is tokens×layers×heads (grow it: cube.animate.set_width(w, stretch=True, about_edge=LEFT)); attention compute is L×L×d collapsing to L×k×d (set_width(0.8, stretch=True)). box3d(w,h,d)=Cube(opacity=0.3).set_width/height/depth(.., stretch=True).
- An embedding space as 3D vectors / a
GlowDots point cloud for relevance-as-alignment or nearest-neighbour selection.
- A feature/representation geometry as a polytope of unit vectors (the superposition reel):
Line(ORIGIN, v) rays + GlowDot tips + faint hull Lines, set_floor_plane('xz'), and escalate the dimension — show the 2-D cases face-on (antipodal pair, then a triangle in-plane), then reorient into 3-D for the tetrahedron with add_ambient_rotation. The directions in space ARE the payload (a genuine vector space), so it never reads as faux-3D. Sequence the corner readout swaps (D=\frac{2}{3} → D=\frac{3}{4}) with non-overlapping time_span or the speed-fit stretch garbles them.
- Show the REAL math, not abstract blocks — the single biggest density win. Replace a colored grid with the actual numbers + the actual equation:
- Numeric matrices/vectors with exact, consistent values:
WeightMatrix(values=...), NumericEmbedding(values=...) (compute the numbers so Δ = v − Sk literally checks out on a pause), then show_matrix_vector_product(scene, M, v) to animate o = S q row-by-row.
- Animate a dot product accumulating pair-by-pair:
for i: FlashAround(q[i]); FlashAround(k[i]); ChangeDecimalToValue(running_sum, partials[i]).
- Pin the actual update/score equation as
Tex (S_t = S_{t-1}\alpha_t(I-\beta_t k_t k_t^\top)+\beta_t k_t v_t^\top, I_{t,s}=\sum_h w_h\,\mathrm{ReLU}(q_h\cdot k_s)) with fix_in_frame() — but above CAP_FLOOR: a to_edge(DOWN) HUD equation lands inside the burned-caption band, so place it at y ≈ −2.4, not −3.4.
There is one camera verb. All five DOF specified together:
self.frame.reorient(theta_deg, phi_deg, gamma_deg, center_tuple, height)
# azimuth polar roll look-at zoom(=visible height)
# static set, or animated inside a play that ALSO moves content:
self.play(self.frame.animate.reorient(-21, 79, 0, (1.13, 0.35, 0.88), 3.81), run_time=5)
- Angles in degrees.
reorient(0, 0, 0, center, height) is a flat pan/zoom used constantly even in 2D — the camera is the pointer.
- Camera args must look hand-tuned, not round.
reorient(-178, 9, 178, (2.15, 1.12, 0.56), 6.84), never reorient(45, 60, 0). (Grant copies exact numbers out of interactive mode.)
- A 3D scene is never static. Either chain multiple reorients with multi-second
run_times, or self.frame.add_ambient_rotation(0.5 * DEG) during holds. Call self.frame.clear_updaters() before a new scripted reorient so ambient + scripted motion don't compound.
self.set_floor_plane('xz') when Y is up and Z is depth (makes hand-tuned angles compose intuitively).
3D legibility kit (from the holograms video, the 3D gold standard):
- Vectors/arrows in 3D get
vector.always.set_perpendicular_to_camera(self.frame) so they don't foreshorten edge-on (geometry.py:798 — it's a Line method; every real call site applies it to a vector, never a Text). Text labels in 3D are kept readable a different way: either lay them into the plane (label.rotate(PI/2, RIGHT) + set_backstroke(BLACK, 3)) as spatial labels that move with the camera, or pin them as a HUD with fix_in_frame() (titles/equations/readouts). That two-layer split — spatial vectors face-camera, text labels either in-plane or fixed-in-frame — is the legibility rule.
- Light sources/sparks/dots are
GlowDot/GlowDots/TrueDot/DotCloud (additive glow), never a shaded Sphere (a Lambert ball reads as a solid ball — faux-3D tell). A beam = ~50 stacked GlowDots of rising radius, ~3/n opacity.
- Coplanar stacks separate by tiny graded offsets along
IN/OUT (1e-3…2e-2) to kill z-fighting.
surface.always_sort_to_camera(self.camera) on any translucent surface that will be orbited.
- Real 3D solids use real depth (
Cube().set_shape(w,h,depth)), never an extruded flat rect — so a side view doesn't collapse.
- The 3D should not be able to lie: drive the render from a real point array / real projected data (
basis @ model[word]), so the picture is a computed consequence of the geometry.
Law 5 — The metronome (measured pacing).
These numbers are counted from the source, not guessed.
- play : wait ≈ 1.6 : 1. Stable across the corpus (attention 216/148, 2025 aggregate 2287/1414). ~3 animated actions per 2 holds. Flag scenes below 1.2 or above 2.0.
- The default hold is
self.wait() with no argument = 1.0s, and it is 80% of all waits. Write the bare call. Don't emit wait(1) or wait(1.7).
- Timed waits come from a small rounded set:
{0.5, 2, 3, 4, 5} for content; {8,10,12,15,20,30} only for ambient/3D holds. (One wait(1.5) and one wait(0.25) exist in the entire transformers corpus.)
- 95% of plays use the default
run_time=1.0. Emit a bare self.play(...). Override only with purpose: run_time=2 is the deliberate-reveal workhorse (~2× the next value), 3–5 for important/complex reveals, 0.5 for a snappy punch.
run_time ≥ 8 is exclusively camera moves / axis Rotate / slow enumerations, almost always rate_func=linear. Never on a text FadeIn.
lag_ratio is an intent dial: 0.1 = standard gentle stagger (the default, ~2.3× the next value, usually with FadeIn); 0.25–0.5 = readable "enumerate, read each" cascade; 0.01–0.05 = near-simultaneous bulk reveal (matrix entries, dot fields); 1.0 = strict succession; 0 = simultaneous. Scale to count with lag_ratio=1.0/len(items) to hold total duration constant.
rate_func is left default (smooth ease) ~97% of the time. The only overrides that earn their place: there_and_back (emphasis pulse returning to rest), linear (continuous camera/rotation), there_and_back_with_pause, rush_from (snappy entrance, often run_time=0.5). Never set rate_func unless the motion's meaning requires it.
- The authoring unit is the labeled beat: one
# Capitalized beat comment ≈ one self.wait() (attention: 149 comments / 148 waits). Each beat = 1–2 plays then a wait. Avoid firing 5+ animations before a hold.
self.add() is the silent reveal (1 instant add per ~3.5 plays): drop in scaffolding/re-layered elements with zero runtime; animate only the element the eye should follow.
- Ensembles always stagger. Many similar objects enter via
LaggedStartMap(FadeIn, group, shift=0.25*DOWN, lag_ratio=0.1), never a simultaneous FadeIn(a), FadeIn(b) (that stacks — the #1 slop tell). Nest LaggedStart + .shuffle() for a field of many things so timing isn't mechanical.
Law 6 — One color per concept; figure/ground discipline.
- Default everything WHITE on BLACK. Greys (
GREY_A..E) for chrome/de-emphasis. Spend a saturated hue only on a load-bearing role, bound to a named variable reused everywhere (value_color = RED; Q=YELLOW, K=TEAL, V=RED, input=BLUE_B). Never flood the frame with color.
- Color is assigned inline per part (
part.set_color(NAMED) — 226× in transformers) and bound to a concept; t2c= is fine at construction but used far less. Do not color by hand-counted glyph slices — use substring indexing eq["\\omega"].set_color(PINK) or t2c.
- Signed numeric grids use
value_to_color (helpers.py:51): positive→blue (BLUE_E→BLUE_B), negative→red (RED_E→RED_B), magnitude→lightness, blended in HSL. Unsigned data (raw embeddings) uses magnitude-only GREY_C→WHITE — never a good/bad diverging scale on neutral data. Forbid hand-setting per-element colors on number grids.
- Real palette is manimGL's
default_config.yml (see §8 for the full A–E ladder + correct hexes). YELLOW is pure #FFFF00, not the old skill's invented #F7D96F.
4. Workflow
User has... → Start at...
─────────────────────────────────────────────────────────
Topic/paper, no plan → Recon → Beat-sheet → DSL → Build → Ground
A beat-sheet → DSL → Build → Ground
A helpers.py + beat-sheet → Build → Ground
A finished video → Grounding pass
Always ask what they have and what they need; don't assume the full pipeline.
- AHA + Recon. One sentence: the shift in perspective the viewer leaves with. If it's a fact, not a shift, stop. Then a lean recon (see §5) — primary source + 2 independent writeups for anything recent; flag every claim DIRECT/INFERRED/UNCERTAIN.
- Beat-sheet (§6) — the ordered named beats that read as the narration. This replaces the old word-count
brainstorm.md.
- Gate A — claims ledger (§10). Before writing scene code, verify every number/label/claim the beats will assert against the primary source. Fix or cut anything not CONFIRMED. Cheaper to fix on paper than in a rendered scene.
- DSL — write
helpers.py (and vendor tb_helpers.py, §13): the self-arranging Mobjects + show_* verbs the beat-sheet needs.
- Build — thin
InteractiveScene files, beats as # comments. The continuous-VO + speed-fit + captions pipeline is §13.
- Render → read frames → fix loop (§13): every scene, every overlap/garble. 3–8 cycles.
- Gate B — audit the rendered video (§10). Sample frames across the whole cut, read each, confirm on-screen numbers/labels/captions/thumbnail are all correct and clean. Nothing ships until this passes.
- Caption + thumbnail + ship.
The render→critique→re-render loop still applies: render, sample 5–10 key frames (manimgl -w then ffmpeg -ss T -frames:v 1, or manim -ql -s --format=png for a scene's last frame), read each frame, fix, re-render. 3–8 cycles. But you are now checking taste, timing, and motion, not hunting collisions — Law 1 already prevents those.
5. Recon (lean)
For the paper/topic, produce concise working docs (no word-count targets — quality is whether they're usable, not long):
paper-summary.md — title + arXiv ID + authors; per-claim DIRECT/INFERRED/UNCERTAIN; the thing studied; method; the numbers that matter; lineage; limitations; why-now; hook angles in tension.
related-work.md — lineage tree, predecessor/competitor deltas, what it does and does NOT contribute.
discussions.md — the social reception: camps, surfaces to cite, which hook will latch.
moodboard.md — references mined from the audience's visual world (Distill.pub, Anthropic Circuits, 3b1b itself), each with one line on what to steal. For manim, the strongest moodboard is the relevant _2024/transformers/*.py scene itself — point at the exact scene that already solves a similar visual.
Discipline: never quote hard citation counts ("widely cited", not "348 citations"); avoid generic adjectives; flag every recent (≤6 mo) claim for re-verification at publish time. Use BrightData (mcp__brightdata__scrape_as_markdown, search_engine) for primary + 2 secondary sources; fall back to WebFetch/WebSearch. Large scrapes (a full paper/blog) overflow the tool result and save to a file, returning the path — don't try to read the whole thing inline; spawn a fact-extraction subagent with the path + a precise brief ("read all N lines in chunks; return verbatim quotes + exact numbers for these claims"), so the raw text stays out of your context. WebFetch's small model silently drops detail on long pages — prefer scrape-to-file + subagent when you need exact figures. Gate B tip: run a final adversarial-reviewer subagent — hand it the rendered video + the claims ledger and tell it to refute every on-screen number and recompute the arithmetic; it catches what your own frame-sweep rationalizes away.
6. The beat-sheet (replaces the old brainstorm.md)
The real 3b1b planning artifact is an ordered, numbered list of named beats that mirrors SCENES_IN_ORDER and the in-construct() section comments — it reads top-to-bottom as the spoken script. Model: ml_basics.py, attention.py section comments, supplements.py chapter banners.
beat-sheet.md format — one line per beat:
NN. BeatName — <imperative narration verb: Show/Ask/Label/Contrast/Highlight> + the single idea
· operates-on: <which persistent object this beat moves>
· [REVEAL] (if this is THE aha: tag reorient + run_time≥2-6 + wait≥2 after)
· objection: <predicted viewer confusion here> → <the interjection beat that resolves it>
· caveat: <if this beat simplifies, name the honesty beat that flags it>
Mandatory structure:
- ESTABLISH first: nest the topic in known categories (Transformer → Deep Learning → ML → "learn from data"), zooming down to the one concrete object the video manipulates. Never open on a labelled diagram or a definition. Open on the phenomenon (the crime scene).
- GAME-PLAN beat: an on-screen ordered checklist of the chapter's atomic teaching units.
- One persistent worked example, locked (the "fluffy blue creature" / "Michael Jordan → Basketball"), carried by the same mobjects from setup to payoff. If any beat introduces a NEW concrete example mid-thread, flag it.
- Every formula is grounded: the formula beat sits after its motivating concrete beats, and is immediately dismantled back to concrete arrays with color-coded symbol→object mapping (
DescribeAttentionEquation, attention.py:1850).
- Motivate-by-violation for any normalizing op (softmax/normalization/masking): show the raw output failing the desired property first, then introduce the op as relief.
- Tag exactly one beat
[REVEAL] and give it the reveal rhythm. Setup beats stay at default time.
- Objection + caveat columns: in-lesson, per-beat objection handling and scheduled honesty beats wherever you simplify (translate the recon's DIRECT/INFERRED/UNCERTAIN flags into on-screen caveats).
- RECAP beat mirrors the opening game-plan with units checked off; mark PREVIEW→PAYOFF pairs.
Self-check before building: read the beats top-to-bottom. Do they form a coherent spoken narration where (a) phenomenon precedes formalism, (b) one example carries throughout, (c) the AHA is tagged and paced, (d) every formula is grounded, (e) objections/caveats sit at their trigger points, (f) a recap mirrors the open? If it reads as a feature list, it fails.
7. manimGL grammar reference (the real catalog)
Animations that exist (use these by exact name)
- Reveal:
FadeIn(m, shift=0.25*DOWN, scale=2) (most common), Write(m) (text/equations; default rate_func=linear, auto run_time), ShowCreation(m) (strokes/paths), DrawBorderThenFill(m), GrowFromCenter/GrowFromPoint/GrowArrow, FadeInFromPoint(m, point).
- Morph:
TransformFromCopy(src, dst) ⟵ workhorse, FadeTransform(a, b), Transform(a, b), ReplacementTransform(a, b), TransformMatchingStrings(eq1, eq2, key_map={...}), FadeTransformPieces. Use path_arc=PI/2 for an arced morph (~1,200× in repo).
- Compose:
LaggedStart(*anims, lag_ratio=0.1), LaggedStartMap(FadeIn, group, shift=, lag_ratio=), AnimationGroup(*anims), Succession. Schedule sub-anims with time_span=(a,b) and/or per-target .set_anim_args(run_time=, rate_func=).
- Emphasis (the real set):
FlashAround(m, time_width=1.5, run_time=2), Flash(point), FlashUnder(m), VShowPassingFlash(path, time_width=1.5, run_time=4) (glowing pulse along a connection), FocusOn, WiggleOutThenIn.
- Numbers:
ChangeDecimalToValue(dec, target), CountInFrom(dec, start) — dec must be a DecimalNumber/Integer. For bespoke motion, UpdateFromAlphaFunc(m, lambda m,a: ...) (the escape hatch Grant actually uses) over inventing Animation subclasses.
- Move/rotate:
MoveToTarget(m) (+ m.generate_target()), Restore(m) (+ m.save_state()), MoveAlongPath, Rotate(m, PI) (discrete) vs Rotating(m, TAU, run_time=5, rate_func=linear) (continuous), m.animate.method(...).
The 15 rate functions (the ONLY ones that exist)
linear, smooth (default), rush_into, rush_from, slow_into, double_smooth, there_and_back, there_and_back_with_pause, running_start, overshoot, not_quite_there, wiggle, squish_rate_func(f,a,b), lingering, exponential_decay. smooth is a quintic smoothstep. squish_rate_func is a factory returning a rate func (windows an ease into part of an animation).
Text & math
Tex(R"...") — pure math (raw strings always). TexText("words $math$ words") — prose with inline math. Text("plain", alignment="LEFT") — non-LaTeX (Pango).
- Substring access:
eq["Q"] (all occurrences, a VGroup), eq["Q"][0] (first), eq[R"\sqrt{d_k}"][0][1:3] (specific glyphs). Mark selectable substrings with isolate=[...] or t2c keys (which auto-isolate). Substrings can't partially overlap.
- Color:
Tex(R"e^{i\omega t}", t2c={R"\omega": PINK, "t": BLUE}) at construction, or inline eq["\\omega"].set_color(PINK).
- Animate a number inside an equation:
dec = eq.make_number_changeable("0.00"), then drive dec with an updater/ValueTracker — don't rebuild the Tex.
- Braces:
Brace(mob, DOWN, buff=0.2) then brace.get_text("A.U.") / brace.get_tex(R"2R_E"). There is no brace_text() free function. BraceLabel/BraceText when brace+label move as a unit.
- Legibility:
m.set_backstroke(BLACK, 5) (width 3 small / 5 labels / 8 headline) on any text over busy content — 42× in transformers, preferred over background rectangles.
Building blocks
- Matrices/vectors:
DecimalMatrix, IntegerMatrix, TexMatrix, MobjectMatrix; address via .get_rows()/.get_columns()/.get_entries()/.get_brackets()/.get_column(i). Entries auto-tile; brackets auto-fit. (Prefer the video DSL WeightMatrix/NumericEmbedding.)
- Graphs: place data-space objects with
axes.c2p(x,y) / number_line.n2p(x); labels via add_coordinate_labels()/get_axis_labels(). Never compute pixel positions.
- Bars are rectangles:
Rectangle(prob * width_100p, bar_height) (width is positional, first arg = value); VGroup(*bars).arrange(DOWN, aligned_edge=LEFT); set_submobject_colors_by_gradient(TEAL, YELLOW). No BarChart/Axes for distributions — length-encoding is the point.
- Connectors: pass node mobjects as endpoints —
Arrow(boxA, boxB, buff=0.1) — so endpoints resolve to boundaries. For data flow, transform the data object between stages rather than drawing arrow glyphs.
- Animated scalar: one
ValueTracker + always_redraw/updaters + DecimalNumber.set_value, so one number drives all dependents and nothing drifts. (455 ValueTracker, 253 always_redraw in repo.) Two-tier sugar: m.f_always.method(getter) and m.always.method(args) over raw add_updater where it reads cleaner — pass bound getters (orbit.get_start), not calls.
Camera / 3D
self.frame.reorient(theta, phi, gamma, center, height) (degrees) · frame.animate.reorient(...) inside a play · frame.add_ambient_rotation(1*DEG) · frame.to_default_state() (snap front-on) · frame.set_height(h) (zoom) · self.set_floor_plane('xz'). Surfaces: ParametricSurface(lambda u,v: [...], u_range=, v_range=, resolution=(nu,nv)) or subclass Surface; built-ins Sphere, Torus, Cylinder, Cube, Prism, Square3D, ThreeDAxes, TexturedSurface. Color a surface with set_color(c, opacity) + set_shading(refl, gloss, shadow) (flat 2D mobjects use set_shading(0,0,0)); always_sort_to_camera(self.camera) for translucent. Light: self.camera.light_source.move_to(...).
8. Brand baseline (manimGL default_config.yml)
Canvas is pure black #000000; default mobject color WHITE. manimGL's engine default bg is #333333 — so to get the brand-black canvas, drop a custom_config.yml in the project dir (the way the videos repo does). Verified working (corner pixel renders (0,0,0)):
# custom_config.yml — manimgl reads it from the working directory
camera:
background_color: "#000000"
Core hues (each has a full A=light → E=dark ladder; C is the base alias):
BLUE_A #C7E9F1 B #9CDCEB C/BLUE #58C4DD D #29ABCA E #1C758A
RED_A #F7A1A3 B #FF8080 C/RED #FC6255 D #E65A4C E #CF5044
GREEN_A ... ... C/GREEN #83C167 ... E ...
TEAL_C #5CD0B3 PURPLE_C #9A72AC GOLD_C #F0AC5F MAROON ladder
YELLOW_C/YELLOW #FFFF00 (pure!) YELLOW_D #F4D345 YELLOW_E #E8C11C
ORANGE #FF862F (no ladder)
GREY_A #DDDDDD B #BBBBBB C/GREY #888888 D #444444 E #222222
GREY_BROWN #736357 PINK #D147BD WHITE #FFFFFF BLACK #000000
COLORMAP_3B1B = [BLUE_E, GREEN, YELLOW, RED] # the canonical 4-stop heatmap
Use named constants and their A–E steps — never invent hexes. value_to_color (signed blue/red) and color_gradient(colors, n) / interpolate_color_by_hsl for ramps.
Color roles (bind each to a named variable, reuse everywhere): orange = hero/innovation · yellow = single aha pulse (one per video) · blue = data/process · green = correct/converged · red = error/caveat · purple = secondary · grey = chrome/de-emphasis.
Typography (manim scenes): math Tex renders in mathastext (3b1b's look; \minus macro available). Text/TexText default to CMU Serif, alignment="CENTER" in the videos repo. Inter Tight / JetBrains Mono are NOT used in manim — they belong only to HTML/poster assets. font_size ladder (4K canvas, frame_height=8): 120/96/90 hero · 72/60 headline · 48 (default) /42/36 body · 30/24 caption/inline · 16 fine print. Pass integers from this ladder.
Chrome (paper title, episode number, brand handle, scene dots) is pinned with fix_in_frame(), not an ffmpeg pad.
9. Audio sync (manimGL has no voiceover plugin — do it yourself)
For the proven, shipped pipeline → see §13. It supersedes the sketch below: one continuous ElevenLabs VO with character timestamps, scenes speed-fit to narration spans, and burned .ass captions. Use scripts/pipeline/. The notes below explain why.
manim-voiceover's VoiceoverScene is a Manim CE plugin and does NOT subclass manimGL's InteractiveScene — don't use it here. The entire videos corpus contains zero voiceover code (grep voiceover = 0 hits), so 3b1b records VO out-of-band and times animations to it. For an automated reels pipeline, do the DIY equivalent that works on manimGL:
- Generate one ElevenLabs clip per narration line, cache to
audio/lines/NN.mp3 (skip if exists → zero re-cost).
- Measure each clip's dur
…(truncated)
1---2name: ml-content3description: ml-content4---56# ml-content78Generate ML explainer content that looks like 3Blue1Brown, not like AI slop.910> ## ⚠️ PRIME DIRECTIVE — treat every video as nuclear. ZERO errors ship.11> The content goes public to an audience that **will** fact-check it. A single wrong number, mislabeled quantity, or overstated claim destroys trust in everything else and gets screenshotted. So: **nothing — not even slightly — may be wrong.** Every spoken line, every on-screen number and label, every caption, and the thumbnail must be **verified against the primary source before it is rendered, and audited again on the rendered video before it ships** (§10, the non-negotiable gate). If you cannot cite the exact source line for a claim, you do not say it, write it, or put it on screen. Soften it or cut it. **No "dramatic license" on numbers.** When in doubt, it is wrong until proven right.1213This skill was rebuilt from a full read of **Grant Sanderson's actual production code** (`github.com/3b1b/videos`, 503K LOC, 2015→2026) and **the real manimGL engine** (`github.com/3b1b/manim`). Every rule below is grounded in that source with `file:line` citations. Where this skill once guessed, it now measures.1415**The video engine is manimGL** (the 3b1b version), driven by `manimgl`. Manim Community Edition (CE) is a *different library with a different, incompatible API* — code written for one crashes on the other. Static IG carousels use HTML/matplotlib (see the Carousel section); everything animated is manimGL.1617---1819## 0. Why the old output was slop (read this once)2021The previous version of this skill produced overlapping elements, weak animation, no consistency, and infographic-feeling stills. The root causes, now fixed:22231. **It shipped Manim CE code while preaching manimGL.** The old template used `from manim import *`, `MathTex`, `ThreeDScene`, `set_camera_orientation`, `set_fill_by_value`, `Create`, `begin_ambient_camera_rotation` — **none of which exist in manimGL** (`grep` over the entire engine = 0 hits). It would not even run.242. **100% of real output was actually built in matplotlib**, hand-placing ~57 text calls + ~20 boxes per scene with absolute coordinates. That is the worst possible tool for animation: no relative layout, no transform system, no camera. Overlap is guaranteed.253. **It treated overlap as a validation problem** (bbox asserts, frame validators, ffmpeg caption-pads) instead of a *construction* problem. 3b1b never validates overlap — it makes overlap structurally impossible by building self-arranging objects.264. **It treated motion as decoration.** Elements faded in from nowhere as disconnected islands. In real 3b1b, objects are *born from the thing they abstract* (`TransformFromCopy`), so every motion teaches.275. **Planning was marketing copy with word-count targets.** Real 3b1b planning is an ordered list of named teaching beats that reads top-to-bottom as the narration.2829The fix is a different mental model, encoded as the Six Laws below.3031---3233## 1. The engine reality (the single most important section)3435**Start every manim file with:**36```python37from manim_imports_ext import * # in the 3b1b/videos repo38# or, standalone: from manimlib import *39```4041**Scene base class is `InteractiveScene`** (`interactive_scene.py:66`) — for 2D *and* 3D. In the entire 2025 corpus: `InteractiveScene` subclassed 385 times, `ThreeDScene` (`scene.py:930`) 0 times — it exists but 3b1b never uses it. 3D is achieved on an `InteractiveScene` by moving `self.frame`.4243**The camera is `self.frame`** (a `CameraFrame` mobject; `scene.py:112`). Local alias `frame = self.frame` appears 114× in 2025 code. `self.camera.frame` appears 0×.4445**Render / iterate:**46```bash47manimgl file.py SceneName # render48manimgl file.py SceneName -se 120 # drop into IPython at line 120 (the dev loop)49manimgl file.py SceneName -w # write to file50# inside the embed: checkpoint_paste() runs clipboard code with checkpoint rewind51```5253### CE landmines — Table A: these genuinely CRASH on manimGL (absent symbols)5455| You must NOT emit (CE) | Use instead (manimGL) | Evidence |56|---|---|---|57| `from manim import *` | `from manim_imports_ext import *` / `from manimlib import *` | CLAUDE.md:59 (loads the wrong library) |58| `MathTex(...)` | `Tex(...)` | grep MathTex over repo = 0; CLAUDE.md:82 |59| `self.set_camera_orientation(phi=,theta=,zoom=)` | `self.frame.reorient(theta, phi, gamma, center, height)` | absent; `camera_frame.py:172` |60| `self.move_camera(...)` | `self.play(self.frame.animate.reorient(...))` | absent |61| `self.begin_ambient_camera_rotation()` | `self.frame.add_ambient_rotation(1 * DEG)` | absent; `camera_frame.py:212` |62| `self.add_fixed_in_frame_mobjects(m)` | `m.fix_in_frame()` | absent (no scene-level helper) |63| `Create(m)` | `ShowCreation(m)` | absent; `creation.py:48` |64| `Unwrite(m)` | `Uncreate(m)` or `FadeOut(m)` | absent |65| `surface.set_fill_by_value(...)` | `surface.set_color(c, opacity)` / `set_color_by_xyz_func(...)` | absent |66| `Circumscribe(m)` | `FlashAround(m)` | absent in engine (the one truly-missing indicator) |67| `Wiggle(m)` (class) | `WiggleOutThenIn(m)` or `rate_func=wiggle` | no `Wiggle` class; `indication.py:355` |68| `ease_in_out_*`, `smoothstep`, `easeOutCubic` | the 15 real rate funcs (§7) | not in `rate_functions.py` |69| `FadeInFrom`, `FadeOutAndShift`, `SpinInFromNothing`, `AddTextLetterByLetter` | `FadeIn(m, shift=, scale=)`, `AddTextWordByWord` | absent |7071**Self-check (must return nothing):**72```73grep -nE 'MathTex|from manim import \*|set_camera_orientation|begin_ambient_camera_rotation|move_camera|add_fixed_in_frame_mobjects|set_fill_by_value|\bCreate\(|\bCircumscribe\b|\bWiggle\(|\bUnwrite\(|FadeInFrom|SpinInFromNothing|AddTextLetterByLetter' your_file.py74```7576### Table B: these RUN on manimGL but are wrong/stale style — don't emit anyway7778| Avoid (valid but off-style) | Prefer | Why |79|---|---|---|80| `class S(Scene)` / `class S(ThreeDScene)` | `class S(InteractiveScene)` | both exist & run, but 2025 corpus is 385× InteractiveScene, 0× ThreeDScene; 3D uses `self.frame` |81| `TransformMatchingTex(a, b)` | `TransformMatchingStrings(a, b)` | `TransformMatchingTex` exists (subclasses Strings) but 3b1b uses Strings |82| `eq.set_color_by_tex(tok, c)` | `Tex(R"...", t2c={tok: c})` or inline `eq[tok].set_color(c)` | `set_color_by_tex` is a real Tex method (`tex_mobject.py:207`) but ~unused; inline `set_color` dominates |83| `Indicate(m)` / `CircleIndicate(m)` | `FlashAround(m)` / `Flash` / `FlashUnder` | both exist & are used (`indication.py:73,142`); modern corpus just reaches for Flash* far more |84| `DEGREES` | `DEG` | `DEGREES` is a live alias of `DEG` (won't crash); 2025 uses `DEG` ~520× vs `DEGREES` 3× |8586> The split matters: don't let a self-check grep reject valid 3b1b code. `Indicate`, `TransformMatchingTex`, `set_color_by_tex`, and `DEGREES` are *style* calls, not crashers — only Table A NameErrors.8788---8990## 2. The mental model (what makes it 3b1b, not slop)9192> You are not laying out a frame. You are building a **small cast of self-arranging objects** and **transforming them through a sequence of beats** where each motion is the explanation.9394Two layers, every video:95961. **`helpers.py` — the domain DSL.** 4–10 `Mobject` subclasses that build and arrange *themselves* (`WeightMatrix`, `NumericEmbedding`, `EmbeddingArray`, `Dial`, `NeuralNetwork`, `ContextAnimation` — `_2024/transformers/helpers.py`), plus 2–5 `show_*(scene, ...)` choreography verbs. **Define the cast before writing a single `construct()`.**972. **Scene files** — thin `InteractiveScene` subclasses whose `construct()` is a flat list of `# Beat name` comments, each assembling DSL objects and animating them.9899Everything below serves this model.100101---102103## 3. The Six Laws (non-negotiable)104105### Law 1 — Relative layout only. Overlap is structural, not policed.106107Real 3b1b layout is overwhelmingly relative: `next_to` used ~10,500×, `arrange` ~2,460× across the repo, versus only a handful of absolute `move_to([x,y,z])` content placements — and nearly all of those absolutes are the *camera*, never content. **Forbid absolute content coordinates.** `move_to([x,y,z])` and `frame.animate.move_to(...)` are for the camera/light source only.108109Allowed positioning primitives — content position ALWAYS comes from one of these:110```python111a.next_to(b, DOWN, buff=MED_LARGE_BUFF) # relative to another object112a.next_to(b, RIGHT).match_y(c) # compound: x from b, y aligned to c113a.align_to(b, LEFT) # share an edge114a.to_edge(UP, buff=LARGE_BUFF) # to a frame edge (chrome only)115a.to_corner(UL) # to a corner (chrome only)116VGroup(*items).arrange(DOWN, buff=MED_SMALL_BUFF, aligned_edge=LEFT)117VGroup(*items).arrange_in_grid(rows, cols, buff=...)118dots = Dot().get_grid(n_rows, n_cols, buff_ratio=0.5)119a.match_x(b) / a.match_y(b) / a.match_width(b) / a.match_height(b)120```121122**Boxes and pills are content-sized, never hand-sized:**123```python124rect = SurroundingRectangle(label, buff=SMALL_BUFF) # measures real glyph bounds125brace = Brace(group, DOWN); brace.get_text("12,288") # fits the span, anchors at tip126under = Underline(word) # width derived from content127```128`SurroundingRectangle` is used ~1,400× in the repo and **cannot clip or mis-center** because its size = `target.get_shape() + 2*buff` (`shape_matchers.py:22`). Delete every `text_width()` character-count heuristic — it has no analog in real code.129130**The buff ladder is the ONLY source of gaps** (`default_config.yml:109`):131```132SMALL_BUFF=0.1 MED_SMALL_BUFF=0.25 MED_LARGE_BUFF=0.5 LARGE_BUFF=1.0133```134Never write `buff=0.37`. Consistent margins across scenes come for free from this ladder.135136**Build the group, then place the group.** Assemble a `VGroup` declaratively, `.arrange()` its internals once, then position the whole thing. This is the dominant pattern across the repo's thousands of `VGroup`s. Never position leaf elements against screen coordinates.137138**Labels on moving targets follow live:** `label.always.next_to(target, UP, buff=SMALL_BUFF)` (or `add_updater`). A single-frame bbox assert can't catch a mid-animation collision; a re-running `next_to` never collides.139140> Because of Law 1, the old "Five-Layer Defense" (bbox asserts + frame validator + ffmpeg caption pad) is **retired**. Overlap is prevented at construction. Keep at most a light visual probe-frame check for *taste and timing*, not collision.141142### Law 2 — Motion carries meaning. Born-from, never spawn.143144The highest-frequency, highest-leverage technique in the corpus: **a key object enters by transforming from the concrete thing it abstracts**, so the causal link is visible.145146```python147# the DALL·E image literally dissolves INTO the numeric vector entries (attention.py:128)148self.play(LaggedStart(*(bake_mobject_into_vector_entries(img, vec) for img, vec in ...)))149# 12,288 numbers collapse into a single symbol E_n (attention.py:200)150self.play(FadeTransform(entry, sym))151# a formula term is delivered by morphing the data column it denotes (ml_basics.py:642)152self.play(TransformFromCopy(data_column, x_symbols))153```154155Transform-family hard counts (attention.py): **`TransformFromCopy` 46 > `FadeTransform` 31 > `ReplacementTransform` 6 > plain `Transform` ~5 ; `TransformMatchingTex` 0.**156- **`TransformFromCopy(src, dst)` is THE workhorse** — source persists, a copy morphs to the destination, so the viewer sees "this *becomes* that" while "this" is still there.157- For equation retitles, `TransformMatchingStrings` (not `...Tex`).158- **Never `FadeIn` a load-bearing object from nothing.** Decorative scaffolding can fade in; the thing the lesson is about must be born from its referent.159160**`time_span=(start, end)` choreographs a reveal inside ONE `self.play`** (used 638× in the repo) so a camera move and a multi-part reveal cascade together instead of firing simultaneously:161```python162# the softmax aha — one play, run_time=3, cascaded (attention.py:921)163self.play(164 self.frame.animate.reorient(...),165 GrowArrow(arrow, time_span=(1, 2)),166 FadeIn(label, time_span=(1, 2)),167 TransformFromCopy(ndp_col, softmax_col, time_span=(1.5, 3)),168 run_time=3,169)170```171172**`generate_target` / `MoveToTarget`** (127 refs) is how dozens of objects snap into a new arrangement with zero hand-keyed coordinates:173```python174grp.target = grp.generate_target()175grp.target.arrange(RIGHT, buff=0.15) # mutate the target with normal layout calls176grp.target.scale(0.65).next_to(anchor, DOWN)177self.play(MoveToTarget(grp))178```179180### Law 3 — A tiny domain DSL gives consistency.181182Every video defines ~6 custom Mobjects/Animations in a `helpers.py` plus ~6 scene-local `get_*`/`show_*` factories. A *vocabulary, not a framework*. This is the mechanism for both consistency and no-overlap (the objects arrange themselves).183184> **For ML content, don't start from scratch — vendor 3b1b's transformer DSL** (`NumericEmbedding`, `WeightMatrix`, `EmbeddingArray`, `ContextAnimation`, `value_to_color`). It is the single fastest path to the authentic "columns of real numbers" look. See §13.185186**Mobject-subclass recipe** (model: `Dial`, `helpers.py:655`; `MachineWithDials:761`):187```python188class WidgetThing(VGroup):189 def __init__(self, value=0, ...):190 # 1. build sub-parts191 body = Rectangle(...); needle = Line(...)192 # 2. lay them out RELATIVELY (ratio buffers, never move_to([x,y,0]))193 ticks = Line(...).get_grid(1, n, buff_ratio=0.5)194 ticks.set_width(body.get_width() - SMALL_BUFF); ticks.move_to(body)195 # 3. assemble, 4. name every meaningful part196 super().__init__(body, ticks, needle)197 self.body, self.needle = body, needle198 # 5. a state mutator that recomputes geometry+style from the logical value199 self.set_value(value)200 def set_value(self, v):201 self.needle.put_start_and_end_on(self.get_center(), self._value_to_point(v))202 self.needle.set_color(value_to_color(v)) # color is a pure function of value203 def animate_set_value(self, v, **kw): # 6. methods that RETURN animations204 return AnimationGroup(self.animate.set_value(v), ...)205```206207**`show_*(scene, ...)` choreography-verb recipe** (model: `show_matrix_vector_product`, `helpers.py:97`):208- first arg is `scene`; the function calls `scene.play/scene.wait` internally,209- owns its transient highlights via a `last_rects`/`to_fade` accumulator so **exactly one highlight is ever on screen**,210- **returns** the persistent mobjects it created.211212**Variants are 2-line subclasses of a shared base overriding one class attribute** — never a copy-pasted `construct()` (model `HighlightEarthOrbit(NearestPlanets)` with `highlighted_orbit = 2`, and its sibling `HighlightMarsOrbit(NearestPlanets)` with `= 3`; `planets.py:2202`). Domain numbers live in ALL-CAPS module constants with one `conversion_factor` (`planets.py:4`), so sizes can't disagree across scenes.213214**Large data uses honest ellipsis:** render a finite set, swap one element for `Tex(R"\dots")` (or `ellipses_row=-2`, default in `WeightMatrix`), so big arrays read as big without overflowing (`helpers.py:478, 620`).215216### Law 4 — Earned 3D only, and 3D is never static.2172183D is earned **only when a quantity's dimensionality is the payload** (a vector space, a surface `f(x,y)=z`, a volumetric field). Roughly half of even the attention video is intentionally flat. A flat data series in 3D is the #1 faux-3D tell — and the old matplotlib "isometric stack of parallelograms" cube is exactly the slop to never produce.219220> **Production lesson (§13): 3D actively HURTS a *stack of thin layers*** (interleaved attention layers, a residual tower) — viewed at an angle they collapse into one solid block and the per-layer colors vanish. Render those **flat, face-on**. Reserve 3D for clouds, surfaces, and collapsing volumes.221222> **Counter-lesson — over-flattening is its own slop (learned shipping the DeepSeek/Qwen attention series).** Apply "flat by default" too zealously and every scene becomes the same colored square grid. A viewer's note, verbatim: *"it just seems like a single block matrix throughout the whole video, we're lacking elements."* Flatness is honest, but monotony kills retention, and the cure is **variety + rigor**, not decoration:223> - **Rotate the visual primitive across the reel.** Don't reuse the row-of-cells / n×n grid in more than ~half the scenes. Across ~10 beats budget at least: one **numeric-matrix** scene, one **on-screen equation**, one **earned-3D** scene — plus vectors / a plot / a code view.224> - **Earned-3D is earned more often than "half flat" implies — use it.** Three patterns that shipped clean (and read as *not* faux-3D because dimensionality IS the payload):225> - **A memory/field as a `z=f(x,y)` surface that DEFORMS under its update.** e.g. the gated-delta-rule state: forget = scale heights down, erase = remove a bump, write = raise a new bump. `srf = ThreeDAxes((-3,3),(-3,3),(0,3)).get_graph(lambda x,y: sum_of_gaussians(x,y,bumps)); srf.set_opacity(0.7); srf.always_sort_to_camera(self.camera)` then `Transform(srf, make_surf(new_bumps))` between states; `frame.add_ambient_rotation(0.5*DEG)` to orbit.226> - **A multi-dimensional tensor as a `Cube` volume that grows or collapses.** The KV cache is *tokens×layers×heads* (grow it: `cube.animate.set_width(w, stretch=True, about_edge=LEFT)`); attention compute is *L×L×d* collapsing to *L×k×d* (`set_width(0.8, stretch=True)`). `box3d(w,h,d)=Cube(opacity=0.3).set_width/height/depth(.., stretch=True)`.227> - **An embedding space as 3D vectors / a `GlowDots` point cloud** for relevance-as-alignment or nearest-neighbour selection.228> - **A feature/representation geometry as a polytope of unit vectors** (the superposition reel): `Line(ORIGIN, v)` rays + `GlowDot` tips + faint hull `Line`s, `set_floor_plane('xz')`, and *escalate the dimension* — show the 2-D cases face-on (antipodal pair, then a triangle in-plane), then `reorient` into 3-D for the tetrahedron with `add_ambient_rotation`. The directions in space ARE the payload (a genuine vector space), so it never reads as faux-3D. Sequence the corner readout swaps (`D=\frac{2}{3}` → `D=\frac{3}{4}`) with non-overlapping `time_span` or the speed-fit stretch garbles them.229> - **Show the REAL math, not abstract blocks — the single biggest density win.** Replace a colored grid with the actual numbers + the actual equation:230> - Numeric matrices/vectors with **exact, consistent** values: `WeightMatrix(values=...)`, `NumericEmbedding(values=...)` (compute the numbers so `Δ = v − Sk` literally checks out on a pause), then `show_matrix_vector_product(scene, M, v)` to animate `o = S q` row-by-row.231> - Animate a **dot product accumulating** pair-by-pair: `for i: FlashAround(q[i]); FlashAround(k[i]); ChangeDecimalToValue(running_sum, partials[i])`.232> - Pin the **actual update/score equation** as `Tex` (`S_t = S_{t-1}\alpha_t(I-\beta_t k_t k_t^\top)+\beta_t k_t v_t^\top`, `I_{t,s}=\sum_h w_h\,\mathrm{ReLU}(q_h\cdot k_s)`) with `fix_in_frame()` — **but above `CAP_FLOOR`**: a `to_edge(DOWN)` HUD equation lands *inside* the burned-caption band, so place it at y ≈ −2.4, not −3.4.233234There is one camera verb. All five DOF specified together:235```python236self.frame.reorient(theta_deg, phi_deg, gamma_deg, center_tuple, height)237# azimuth polar roll look-at zoom(=visible height)238# static set, or animated inside a play that ALSO moves content:239self.play(self.frame.animate.reorient(-21, 79, 0, (1.13, 0.35, 0.88), 3.81), run_time=5)240```241- Angles in **degrees**. `reorient(0, 0, 0, center, height)` is a flat pan/zoom used constantly even in 2D — the camera is the pointer.242- **Camera args must look hand-tuned, not round.** `reorient(-178, 9, 178, (2.15, 1.12, 0.56), 6.84)`, never `reorient(45, 60, 0)`. (Grant copies exact numbers out of interactive mode.)243- **A 3D scene is never static.** Either chain multiple reorients with multi-second `run_time`s, or `self.frame.add_ambient_rotation(0.5 * DEG)` during holds. Call `self.frame.clear_updaters()` before a new scripted reorient so ambient + scripted motion don't compound.244- `self.set_floor_plane('xz')` when Y is up and Z is depth (makes hand-tuned angles compose intuitively).2452463D legibility kit (from the holograms video, the 3D gold standard):247- **Vectors/arrows in 3D** get `vector.always.set_perpendicular_to_camera(self.frame)` so they don't foreshorten edge-on (`geometry.py:798` — it's a `Line` method; every real call site applies it to a vector, never a `Text`). **Text labels** in 3D are kept readable a different way: either lay them into the plane (`label.rotate(PI/2, RIGHT)` + `set_backstroke(BLACK, 3)`) as spatial labels that move with the camera, **or** pin them as a HUD with `fix_in_frame()` (titles/equations/readouts). That two-layer split — spatial vectors face-camera, text labels either in-plane or fixed-in-frame — is the legibility rule.248- Light sources/sparks/dots are **`GlowDot`/`GlowDots`/`TrueDot`/`DotCloud`** (additive glow), **never a shaded `Sphere`** (a Lambert ball reads as a solid ball — faux-3D tell). A beam = ~50 stacked `GlowDots` of rising radius, ~3/n opacity.249- Coplanar stacks separate by tiny graded offsets along `IN`/`OUT` (1e-3…2e-2) to kill z-fighting.250- `surface.always_sort_to_camera(self.camera)` on any translucent surface that will be orbited.251- Real 3D solids use real depth (`Cube().set_shape(w,h,depth)`), never an extruded flat rect — so a side view doesn't collapse.252- **The 3D should not be able to lie:** drive the render from a real point array / real projected data (`basis @ model[word]`), so the picture is a computed consequence of the geometry.253254### Law 5 — The metronome (measured pacing).255256These numbers are counted from the source, not guessed.257258- **play : wait ≈ 1.6 : 1.** Stable across the corpus (attention 216/148, 2025 aggregate 2287/1414). ~3 animated actions per 2 holds. Flag scenes below 1.2 or above 2.0.259- **The default hold is `self.wait()` with no argument = 1.0s, and it is 80% of all waits.** Write the bare call. Don't emit `wait(1)` or `wait(1.7)`.260- **Timed waits come from a small rounded set:** `{0.5, 2, 3, 4, 5}` for content; `{8,10,12,15,20,30}` only for ambient/3D holds. (One `wait(1.5)` and one `wait(0.25)` exist in the *entire* transformers corpus.)261- **95% of plays use the default `run_time=1.0`.** Emit a bare `self.play(...)`. Override only with purpose: `run_time=2` is the deliberate-reveal workhorse (~2× the next value), `3–5` for important/complex reveals, `0.5` for a snappy punch.262- **`run_time ≥ 8` is exclusively camera moves / axis `Rotate` / slow enumerations, almost always `rate_func=linear`.** Never on a text `FadeIn`.263- **`lag_ratio` is an intent dial:** `0.1` = standard gentle stagger (the default, ~2.3× the next value, usually with `FadeIn`); `0.25–0.5` = readable "enumerate, read each" cascade; `0.01–0.05` = near-simultaneous bulk reveal (matrix entries, dot fields); `1.0` = strict succession; `0` = simultaneous. Scale to count with `lag_ratio=1.0/len(items)` to hold total duration constant.264- **`rate_func` is left default (smooth ease) ~97% of the time.** The only overrides that earn their place: `there_and_back` (emphasis pulse returning to rest), `linear` (continuous camera/rotation), `there_and_back_with_pause`, `rush_from` (snappy entrance, often `run_time=0.5`). Never set `rate_func` unless the motion's *meaning* requires it.265- **The authoring unit is the labeled beat:** one `# Capitalized beat` comment ≈ one `self.wait()` (attention: 149 comments / 148 waits). Each beat = 1–2 plays then a wait. Avoid firing 5+ animations before a hold.266- **`self.add()` is the silent reveal** (1 instant add per ~3.5 plays): drop in scaffolding/re-layered elements with zero runtime; animate only the element the eye should follow.267- **Ensembles always stagger.** Many similar objects enter via `LaggedStartMap(FadeIn, group, shift=0.25*DOWN, lag_ratio=0.1)`, never a simultaneous `FadeIn(a), FadeIn(b)` (that stacks — the #1 slop tell). Nest `LaggedStart` + `.shuffle()` for a field of many things so timing isn't mechanical.268269### Law 6 — One color per concept; figure/ground discipline.270271- **Default everything WHITE on BLACK.** Greys (`GREY_A..E`) for chrome/de-emphasis. Spend a saturated hue only on a load-bearing role, **bound to a named variable reused everywhere** (`value_color = RED`; Q=`YELLOW`, K=`TEAL`, V=`RED`, input=`BLUE_B`). Never flood the frame with color.272- **Color is assigned inline per part** (`part.set_color(NAMED)` — 226× in transformers) and bound to a concept; `t2c=` is fine at construction but used far less. Do **not** color by hand-counted glyph slices — use substring indexing `eq["\\omega"].set_color(PINK)` or `t2c`.273- **Signed numeric grids use `value_to_color`** (`helpers.py:51`): positive→blue (`BLUE_E→BLUE_B`), negative→red (`RED_E→RED_B`), magnitude→lightness, blended in HSL. **Unsigned** data (raw embeddings) uses magnitude-only `GREY_C→WHITE` — never a good/bad diverging scale on neutral data. Forbid hand-setting per-element colors on number grids.274- Real palette is manimGL's `default_config.yml` (see §8 for the full A–E ladder + correct hexes). **`YELLOW` is pure `#FFFF00`**, not the old skill's invented `#F7D96F`.275276---277278## 4. Workflow279280```281User has... → Start at...282─────────────────────────────────────────────────────────283Topic/paper, no plan → Recon → Beat-sheet → DSL → Build → Ground284A beat-sheet → DSL → Build → Ground285A helpers.py + beat-sheet → Build → Ground286A finished video → Grounding pass287```288289Always ask what they have and what they need; don't assume the full pipeline.2902911. **AHA + Recon.** One sentence: the shift in perspective the viewer leaves with. If it's a fact, not a shift, stop. Then a *lean* recon (see §5) — primary source + 2 independent writeups for anything recent; flag every claim DIRECT/INFERRED/UNCERTAIN.2922. **Beat-sheet** (§6) — the ordered named beats that read as the narration. This replaces the old word-count `brainstorm.md`.2933. **Gate A — claims ledger (§10).** Before writing scene code, verify *every* number/label/claim the beats will assert against the primary source. Fix or cut anything not CONFIRMED. Cheaper to fix on paper than in a rendered scene.2944. **DSL** — write `helpers.py` (and vendor `tb_helpers.py`, §13): the self-arranging Mobjects + `show_*` verbs the beat-sheet needs.2955. **Build** — thin `InteractiveScene` files, beats as `# comments`. The continuous-VO + speed-fit + captions pipeline is §13.2966. **Render → read frames → fix** loop (§13): every scene, every overlap/garble. 3–8 cycles.2977. **Gate B — audit the rendered video (§10).** Sample frames across the whole cut, read each, confirm on-screen numbers/labels/captions/thumbnail are all correct and clean. **Nothing ships until this passes.**2988. **Caption + thumbnail + ship.**299300The render→critique→re-render loop still applies: render, sample 5–10 key frames (`manimgl -w` then `ffmpeg -ss T -frames:v 1`, or `manim -ql -s --format=png` for a scene's last frame), read each frame, fix, re-render. 3–8 cycles. But you are now checking *taste, timing, and motion*, not hunting collisions — Law 1 already prevents those.301302---303304## 5. Recon (lean)305306For the paper/topic, produce concise working docs (no word-count targets — quality is whether they're *usable*, not long):307308- **`paper-summary.md`** — title + arXiv ID + authors; per-claim DIRECT/INFERRED/UNCERTAIN; the thing studied; method; the numbers that matter; lineage; limitations; why-now; hook angles in tension.309- **`related-work.md`** — lineage tree, predecessor/competitor deltas, what it does and does NOT contribute.310- **`discussions.md`** — the social reception: camps, surfaces to cite, which hook will latch.311- **`moodboard.md`** — references mined from the audience's visual world (Distill.pub, Anthropic Circuits, 3b1b itself), each with one line on what to steal. **For manim, the strongest moodboard is the relevant `_2024/transformers/*.py` scene itself** — point at the exact scene that already solves a similar visual.312313Discipline: never quote hard citation counts ("widely cited", not "348 citations"); avoid generic adjectives; flag every recent (≤6 mo) claim for re-verification at publish time. Use BrightData (`mcp__brightdata__scrape_as_markdown`, `search_engine`) for primary + 2 secondary sources; fall back to `WebFetch`/`WebSearch`. **Large scrapes (a full paper/blog) overflow the tool result and save to a file, returning the path** — don't try to read the whole thing inline; spawn a fact-extraction **subagent** with the path + a precise brief ("read all N lines in chunks; return verbatim quotes + exact numbers for these claims"), so the raw text stays out of your context. `WebFetch`'s small model silently drops detail on long pages — prefer scrape-to-file + subagent when you need exact figures. **Gate B tip:** run a final **adversarial-reviewer subagent** — hand it the rendered video + the claims ledger and tell it to *refute* every on-screen number and recompute the arithmetic; it catches what your own frame-sweep rationalizes away.314315---316317## 6. The beat-sheet (replaces the old brainstorm.md)318319The real 3b1b planning artifact is an **ordered, numbered list of named beats** that mirrors `SCENES_IN_ORDER` and the in-`construct()` section comments — it reads top-to-bottom as the spoken script. Model: `ml_basics.py`, `attention.py` section comments, `supplements.py` chapter banners.320321`beat-sheet.md` format — one line per beat:322```323NN. BeatName — <imperative narration verb: Show/Ask/Label/Contrast/Highlight> + the single idea324 · operates-on: <which persistent object this beat moves>325 · [REVEAL] (if this is THE aha: tag reorient + run_time≥2-6 + wait≥2 after)326 · objection: <predicted viewer confusion here> → <the interjection beat that resolves it>327 · caveat: <if this beat simplifies, name the honesty beat that flags it>328```329330Mandatory structure:331- **ESTABLISH** first: nest the topic in known categories (Transformer → Deep Learning → ML → "learn from data"), zooming down to the one concrete object the video manipulates. Never open on a labelled diagram or a definition. Open on the *phenomenon* (the crime scene).332- **GAME-PLAN** beat: an on-screen ordered checklist of the chapter's atomic teaching units.333- **One persistent worked example**, locked (the "fluffy blue creature" / "Michael Jordan → Basketball"), carried by the *same mobjects* from setup to payoff. If any beat introduces a NEW concrete example mid-thread, flag it.334- **Every formula is grounded:** the formula beat sits *after* its motivating concrete beats, and is immediately dismantled back to concrete arrays with color-coded symbol→object mapping (`DescribeAttentionEquation`, `attention.py:1850`).335- **Motivate-by-violation** for any normalizing op (softmax/normalization/masking): show the raw output *failing* the desired property first, then introduce the op as relief.336- **Tag exactly one beat `[REVEAL]`** and give it the reveal rhythm. Setup beats stay at default time.337- **Objection + caveat columns:** in-lesson, per-beat objection handling and scheduled honesty beats wherever you simplify (translate the recon's DIRECT/INFERRED/UNCERTAIN flags into on-screen caveats).338- **RECAP** beat mirrors the opening game-plan with units checked off; mark PREVIEW→PAYOFF pairs.339340**Self-check before building:** read the beats top-to-bottom. Do they form a coherent spoken narration where (a) phenomenon precedes formalism, (b) one example carries throughout, (c) the AHA is tagged and paced, (d) every formula is grounded, (e) objections/caveats sit at their trigger points, (f) a recap mirrors the open? If it reads as a feature list, it fails.341342---343344## 7. manimGL grammar reference (the real catalog)345346### Animations that exist (use these by exact name)347- **Reveal:** `FadeIn(m, shift=0.25*DOWN, scale=2)` (most common), `Write(m)` (text/equations; default `rate_func=linear`, auto run_time), `ShowCreation(m)` (strokes/paths), `DrawBorderThenFill(m)`, `GrowFromCenter/GrowFromPoint/GrowArrow`, `FadeInFromPoint(m, point)`.348- **Morph:** `TransformFromCopy(src, dst)` ⟵ workhorse, `FadeTransform(a, b)`, `Transform(a, b)`, `ReplacementTransform(a, b)`, `TransformMatchingStrings(eq1, eq2, key_map={...})`, `FadeTransformPieces`. Use `path_arc=PI/2` for an arced morph (~1,200× in repo).349- **Compose:** `LaggedStart(*anims, lag_ratio=0.1)`, `LaggedStartMap(FadeIn, group, shift=, lag_ratio=)`, `AnimationGroup(*anims)`, `Succession`. Schedule sub-anims with `time_span=(a,b)` and/or per-target `.set_anim_args(run_time=, rate_func=)`.350- **Emphasis (the real set):** `FlashAround(m, time_width=1.5, run_time=2)`, `Flash(point)`, `FlashUnder(m)`, `VShowPassingFlash(path, time_width=1.5, run_time=4)` (glowing pulse along a connection), `FocusOn`, `WiggleOutThenIn`.351- **Numbers:** `ChangeDecimalToValue(dec, target)`, `CountInFrom(dec, start)` — `dec` must be a `DecimalNumber`/`Integer`. For bespoke motion, `UpdateFromAlphaFunc(m, lambda m,a: ...)` (the escape hatch Grant actually uses) over inventing `Animation` subclasses.352- **Move/rotate:** `MoveToTarget(m)` (+ `m.generate_target()`), `Restore(m)` (+ `m.save_state()`), `MoveAlongPath`, `Rotate(m, PI)` (discrete) vs `Rotating(m, TAU, run_time=5, rate_func=linear)` (continuous), `m.animate.method(...)`.353354### The 15 rate functions (the ONLY ones that exist)355`linear, smooth (default), rush_into, rush_from, slow_into, double_smooth, there_and_back, there_and_back_with_pause, running_start, overshoot, not_quite_there, wiggle, squish_rate_func(f,a,b), lingering, exponential_decay`. `smooth` is a quintic smoothstep. `squish_rate_func` is a *factory* returning a rate func (windows an ease into part of an animation).356357### Text & math358- `Tex(R"...")` — pure math (raw strings always). `TexText("words $math$ words")` — prose with inline math. `Text("plain", alignment="LEFT")` — non-LaTeX (Pango).359- Substring access: `eq["Q"]` (all occurrences, a VGroup), `eq["Q"][0]` (first), `eq[R"\sqrt{d_k}"][0][1:3]` (specific glyphs). Mark selectable substrings with `isolate=[...]` or `t2c` keys (which auto-isolate). Substrings can't partially overlap.360- Color: `Tex(R"e^{i\omega t}", t2c={R"\omega": PINK, "t": BLUE})` at construction, or inline `eq["\\omega"].set_color(PINK)`.361- Animate a number inside an equation: `dec = eq.make_number_changeable("0.00")`, then drive `dec` with an updater/`ValueTracker` — don't rebuild the `Tex`.362- Braces: `Brace(mob, DOWN, buff=0.2)` then `brace.get_text("A.U.")` / `brace.get_tex(R"2R_E")`. There is no `brace_text()` free function. `BraceLabel`/`BraceText` when brace+label move as a unit.363- Legibility: `m.set_backstroke(BLACK, 5)` (width 3 small / 5 labels / 8 headline) on any text over busy content — 42× in transformers, preferred over background rectangles.364365### Building blocks366- Matrices/vectors: `DecimalMatrix`, `IntegerMatrix`, `TexMatrix`, `MobjectMatrix`; address via `.get_rows()/.get_columns()/.get_entries()/.get_brackets()/.get_column(i)`. Entries auto-tile; brackets auto-fit. (Prefer the video DSL `WeightMatrix`/`NumericEmbedding`.)367- Graphs: place data-space objects with `axes.c2p(x,y)` / `number_line.n2p(x)`; labels via `add_coordinate_labels()/get_axis_labels()`. Never compute pixel positions.368- Bars are rectangles: `Rectangle(prob * width_100p, bar_height)` (width is positional, first arg = value); `VGroup(*bars).arrange(DOWN, aligned_edge=LEFT)`; `set_submobject_colors_by_gradient(TEAL, YELLOW)`. No `BarChart`/`Axes` for distributions — length-encoding is the point.369- Connectors: pass node *mobjects* as endpoints — `Arrow(boxA, boxB, buff=0.1)` — so endpoints resolve to boundaries. For data flow, *transform the data object* between stages rather than drawing arrow glyphs.370- Animated scalar: one `ValueTracker` + `always_redraw`/updaters + `DecimalNumber.set_value`, so one number drives all dependents and nothing drifts. (455 `ValueTracker`, 253 `always_redraw` in repo.) Two-tier sugar: `m.f_always.method(getter)` and `m.always.method(args)` over raw `add_updater` where it reads cleaner — pass *bound* getters (`orbit.get_start`), not calls.371372### Camera / 3D373`self.frame.reorient(theta, phi, gamma, center, height)` (degrees) · `frame.animate.reorient(...)` inside a play · `frame.add_ambient_rotation(1*DEG)` · `frame.to_default_state()` (snap front-on) · `frame.set_height(h)` (zoom) · `self.set_floor_plane('xz')`. Surfaces: `ParametricSurface(lambda u,v: [...], u_range=, v_range=, resolution=(nu,nv))` or subclass `Surface`; built-ins `Sphere, Torus, Cylinder, Cube, Prism, Square3D, ThreeDAxes, TexturedSurface`. Color a surface with `set_color(c, opacity)` + `set_shading(refl, gloss, shadow)` (flat 2D mobjects use `set_shading(0,0,0)`); `always_sort_to_camera(self.camera)` for translucent. Light: `self.camera.light_source.move_to(...)`.374375---376377## 8. Brand baseline (manimGL `default_config.yml`)378379Canvas is **pure black `#000000`**; default mobject color **WHITE**. manimGL's engine default bg is `#333333` — so to get the brand-black canvas, drop a `custom_config.yml` in the project dir (the way the videos repo does). **Verified working** (corner pixel renders `(0,0,0)`):380```yaml381# custom_config.yml — manimgl reads it from the working directory382camera:383 background_color: "#000000"384```385386**Core hues** (each has a full A=light → E=dark ladder; `C` is the base alias):387```388BLUE_A #C7E9F1 B #9CDCEB C/BLUE #58C4DD D #29ABCA E #1C758A389RED_A #F7A1A3 B #FF8080 C/RED #FC6255 D #E65A4C E #CF5044390GREEN_A ... ... C/GREEN #83C167 ... E ...391TEAL_C #5CD0B3 PURPLE_C #9A72AC GOLD_C #F0AC5F MAROON ladder392YELLOW_C/YELLOW #FFFF00 (pure!) YELLOW_D #F4D345 YELLOW_E #E8C11C393ORANGE #FF862F (no ladder)394GREY_A #DDDDDD B #BBBBBB C/GREY #888888 D #444444 E #222222395GREY_BROWN #736357 PINK #D147BD WHITE #FFFFFF BLACK #000000396COLORMAP_3B1B = [BLUE_E, GREEN, YELLOW, RED] # the canonical 4-stop heatmap397```398Use named constants and their A–E steps — never invent hexes. `value_to_color` (signed blue/red) and `color_gradient(colors, n)` / `interpolate_color_by_hsl` for ramps.399400**Color roles** (bind each to a named variable, reuse everywhere): orange = hero/innovation · yellow = single aha pulse (one per video) · blue = data/process · green = correct/converged · red = error/caveat · purple = secondary · grey = chrome/de-emphasis.401402**Typography (manim scenes):** math `Tex` renders in `mathastext` (3b1b's look; `\minus` macro available). `Text`/`TexText` default to `CMU Serif`, `alignment="CENTER"` in the videos repo. **Inter Tight / JetBrains Mono are NOT used in manim** — they belong only to HTML/poster assets. `font_size` ladder (4K canvas, `frame_height=8`): `120/96/90` hero · `72/60` headline · `48` (default) `/42/36` body · `30/24` caption/inline · `16` fine print. Pass integers from this ladder.403404Chrome (paper title, episode number, brand handle, scene dots) is pinned with `fix_in_frame()`, not an ffmpeg pad.405406---407408## 9. Audio sync (manimGL has no voiceover plugin — do it yourself)409410> **For the proven, shipped pipeline → see §13.** It supersedes the sketch below: one *continuous* ElevenLabs VO with character timestamps, scenes **speed-fit** to narration spans, and burned `.ass` captions. Use `scripts/pipeline/`. The notes below explain why.411412`manim-voiceover`'s `VoiceoverScene` is a **Manim CE plugin** and does NOT subclass manimGL's `InteractiveScene` — don't use it here. The entire `videos` corpus contains zero voiceover code (`grep voiceover` = 0 hits), so 3b1b records VO out-of-band and times animations to it. For an automated reels pipeline, do the DIY equivalent that *works on manimGL*:4134141. Generate one ElevenLabs clip per narration line, cache to `audio/lines/NN.mp3` (skip if exists → zero re-cost).4152. Measure each clip's dur416417…(truncated)