From test-manim
Create teaching-quality STEM animations using ManimGL. Generates narrative-driven animations as 15-second chunks merged into a final video. Configurable topic, duration, and model name. Works with Claude Code and opencode across any model. Invoke: /test-manim "topic" [--duration 30|60|90|120] [--model name] Default duration: 30s. Primary goal: great storytelling + visual clarity.
How this skill is triggered — by the user, by Claude, or both
Slash command
/test-manim:test-manimThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Produce pedagogically-effective STEM teaching animations: narrative-first, chunk-based
Produce pedagogically-effective STEM teaching animations: narrative-first, chunk-based (15s segments), rendered in ManimGL (manimgl v1.7+), merged into one final video.
/test-manim "Newton's Laws of Motion"
/test-manim "Fourier Series" --duration 60
/test-manim "Neural Networks" --duration 90 --model minimax
| Arg | Default | Values |
|---|---|---|
| topic | required | any STEM subject string |
| --duration | 30 | 30, 60, 90, 120 (seconds) |
| --model | session model | identifier used in folder name |
Extract from invocation:
TOPIC — subject string (required)DURATION — total seconds (default: 30)MODEL_NAME — folder identifier (default: derive from session context or use "claude")CHUNK_COUNT = ceil(DURATION / 15)TOPIC_SLUG = TOPIC lowercased, spaces→underscores, strip special charsCreate project at current working directory:
manim-{MODEL_NAME}/
{TOPIC_SLUG}/
scenes/
chunk_01_{name}.py
chunk_02_{name}.py
...
output/
chunks/
final/
assets/
SCENE_PLAN.md
render.sh
merge.sh
Create all directories with mkdir -p before writing any files.
Make render.sh and merge.sh executable with chmod +x.
Write SCENE_PLAN.md completely before writing any .py files.
Every animation must have:
# {TOPIC} — Animation Plan
**Duration**: {DURATION}s | **Chunks**: {CHUNK_COUNT} × 15s | **Model**: {MODEL_NAME}
## Story Arc
- Hook: [striking opening visual — what question does it pose?]
- Build: [ordered list of concepts, one per chunk]
- Resolution: [final synthesis — what understanding has the viewer gained?]
## Visual Language
- Background: BLACK
- Primary concept color: BLUE
- Secondary concept color: YELLOW
- Accent / highlight: RED or GREEN
- Text / labels: WHITE
- Rule: one color = one concept, consistent across ALL chunks
## Chunks
### Chunk 1 — {name} (0s–15s)
- Role: Hook
- Concept: [what is demonstrated]
- Opening: [first visual the viewer sees]
- Key moment: [the reveal or insight at ~10s]
- Closing: [held frame at 15s — clean fade out]
- ManimGL objects: [list specific classes]
- Equations: [LaTeX strings if any]
### Chunk 2 — {name} (15s–30s)
- Role: Build
- Concept: ...
[repeat for each chunk]
### Chunk {CHUNK_COUNT} — {name} ({DURATION-15}s–{DURATION}s)
- Role: Resolution
- Concept: ...
- Callback: [reference to visuals from chunk 1]
## Issues Log
[fill during render phase]
Load references/manim-gl-api.md before writing any scene code.
Load references/storytelling.md during planning to shape narrative.
Write ALL chunk files before rendering any of them.
chunk_{N:02d}_{slug}.py — slug is 1–3 words from chunk name, underscored, lowercase.
from manimlib import *
class Chunk{N}{PascalName}(Scene):
def construct(self):
# Target: ~15 seconds total
# Color contract from SCENE_PLAN.md — do not deviate
pass
Timing — target exactly 15s per chunk
total = sum(all run_time values) + sum(all self.wait() values) ≈ 15
run_time=0.8–1.5run_time=1.5–2.5self.wait(1.5) minimumself.wait(3)self.wait(2)Layout — stay within camera bounds
obj.to_edge(UP) (y≈3.0)obj.move_to(ORIGIN)obj.to_edge(DOWN) (y≈-3.0)Typography
Tex(r"F = ma")TexText("Newton's First Law")TexText("An object at rest...", font_size=36)Chunk cleanup — end of every construct()
# Safe fadeout — handles empty scene
to_fade = list(self.mobjects)
if to_fade:
self.play(*[FadeOut(m) for m in to_fade], run_time=0.5)
Quality checks — verify before moving to render
self.play() call (prevents black frame)self.play()self.add(obj) if object needs to appear instantly without animationr"..."Write render.sh into the project root, then run it:
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
mkdir -p "$SCRIPT_DIR/output/chunks"
for scene_file in $(ls "$SCRIPT_DIR/scenes"/chunk_*.py | sort); do
base="${scene_file##*/}"
out_name="${base%.py}"
class_name=$(python3 -c "
import ast
with open('$scene_file') as f:
tree = ast.parse(f.read())
for node in ast.walk(tree):
if isinstance(node, ast.ClassDef):
print(node.name)
break
")
echo "Rendering: $class_name → output/chunks/$out_name.mp4"
manimgl "$scene_file" "$class_name" \
-w -q --hd \
--video_dir "$SCRIPT_DIR/output/chunks" \
--file_name "$out_name"
done
echo "All chunks rendered."
If a chunk fails:
references/manim-gl-api.md error table for known fixesmanimgl scenes/chunk_NN_name.py ClassName -w -q --hd --video_dir output/chunks --file_name chunk_NN_nameSCENE_PLAN.md under ## Issues LogDo NOT merge until ALL chunks render without error.
Write merge.sh into the project root, then run it:
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CHUNKS_DIR="$SCRIPT_DIR/output/chunks"
FINAL_DIR="$SCRIPT_DIR/output/final"
mkdir -p "$FINAL_DIR"
CONCAT_FILE=$(mktemp /tmp/manim_concat_XXXXXX.txt)
for f in $(ls "$CHUNKS_DIR"/chunk_*.mp4 | sort); do
echo "file '$f'" >> "$CONCAT_FILE"
done
OUTPUT_FILE="$FINAL_DIR/TOPIC_SLUG_final.mp4"
ffmpeg -f concat -safe 0 -i "$CONCAT_FILE" \
-c:v libx264 -crf 18 -preset slow \
-pix_fmt yuv420p \
"$OUTPUT_FILE"
rm "$CONCAT_FILE"
echo "Final video: $OUTPUT_FILE"
Replace TOPIC_SLUG_final.mp4 with the actual topic slug when generating this file.
Verify output:
ffprobe -v error -show_entries format=duration -of csv=p=0 "output/final/TOPIC_SLUG_final.mp4"
## Animation Report: {TOPIC}
| Field | Value |
|-------|-------|
| Model | {MODEL_NAME} |
| Duration | {actual}s ({CHUNK_COUNT} chunks × ~15s) |
| Output | manim-{MODEL_NAME}/{TOPIC_SLUG}/output/final/{TOPIC_SLUG}_final.mp4 |
### What Was Created
[2–3 sentences: topic, visual approach, key animations used]
### Chunk Summary
| Chunk | Concept | Render | Notes |
|-------|---------|--------|-------|
| 1 | ... | ✓ | |
| 2 | ... | ✓ | |
### Code Quality
- ManimGL API: [correct / N workarounds needed]
- Timing: [within 2s of target per chunk / drifted by Xs]
- Color contract: [maintained / broken at chunk N]
- Cleanup: [all chunks fade cleanly / issues at chunk N]
### Issues Encountered
[list render errors and fixes — or "None"]
### Rating: X/10
[1-line justification]
### Recommendations
- [concrete improvement 1]
- [concrete improvement 2]
.py chunk files in one batch before running any rendermanim-gl-api.md at Phase 2 start, storytelling.md at Phase 1 startrender.sh once for all chunks — only re-render individual chunks when fixing errorsApplied always; critical for non-Claude models:
from manimlib import * at top of every file — no partial importsr"\frac{d}{dx}" not "\\frac{d}{dx}"construct() under 80 lines — extract _helper() methods if neededBLUE, YELLOW, RED, GREEN, WHITEself.play(self.camera.frame.animate.shift(RIGHT * 2))| Feature | ManimGL (correct) | ManimCE (wrong — will error) |
|---|---|---|
| Import | from manimlib import * | from manim import * |
| Regular text | TexText("hello") | Text("hello") |
| Math | Tex(r"x^2") | MathTex(r"x^2") |
| Create anim | ShowCreation(obj) | Create(obj) |
| Write anim | Write(obj) | same ✓ |
| Camera | self.camera.frame | self.camera |
| Axes / NumberPlane | same ✓ | same ✓ |
Provides a checklist for code reviews covering functionality, security, performance, maintainability, tests, and quality. Use for pull requests, audits, team standards, and developer training.
npx claudepluginhub shihabshahrier/manim-coding-skill --plugin test-manim