Screenplay-driven stick-figure animation for Manim, with Fountain+ conversion, graph-based characters, prop systems, cartoon faces, wardrobe overlays, camera blocking, and optional Blender export.
PAM is a Python/Manim toolkit for building fast, scriptable blocking animations from JSON or annotated Fountain screenplays. Characters are represented as graph-theoretic skeletons: joints are nodes, bones are edges, and every pose is a dictionary of joint coordinates. Actions interpolate those graphs through named poses, programmatic targets, prop interactions, speech bubbles, and camera beats.
PAM was developed for the Too Nice to Die animated sci-fi production pipeline, but the engine is general enough for storyboards, animatics, classroom demonstrations, character blocking tests, prop-layout tests, and graph-based animation experiments.
Current reference basis: PAM v0.9.18
Fountain converter basis:fountain2pamv0.9.13
Primary renderer: Manim Community
Primary authoring formats: PAM JSON and Fountain+
Canonical cast registry:tntd_characters.json
- What PAM does
- Pipeline
- Feature highlights
- Installation
- Quick start: render a JSON scene
- Quick start: use Fountain+
- Core concepts
- PAM JSON format
- Characters
- Faces and appearance
- Wardrobe overlays and accessories
- Props and scene objects
- Actions
- Camera system
- Environment variables
- Project layout
- Development guide
- Troubleshooting
- Version highlights
- Documentation
- Contributing
- License
PAM turns screenplay-like action descriptions into Manim animations.
It can:
- register a cast of humans, aliens, dogs, and dodecahedral “Governor” figures;
- animate walking, running, turning, waving, nodding, shrugging, reacting, grabbing, placing, punching buttons, dodging, falling, sitting, exiting, and other blocking actions;
- render speech bubbles for characters and props;
- persist or clear bubbles, captions, sound cues, overlays, and focus states;
- build and attach props such as desks, doors, phones, folders, laptops, backpacks, flowers, backdrops, buildings, pods, elevators, TV monitors, and dodecahedra;
- distinguish interactive props from large camera-addressable scene objects;
- attach flat-cartoon faces generated from JSON rather than bitmap assets;
- swap face expressions at runtime;
- attach wardrobe overlays such as name tags, gloves, shoes, torso icons, dog harnesses, and service labels;
- drive camera framing from screenplay annotations;
- convert Fountain+ scripts into PAM JSON plus prompt metadata;
- optionally export layout information to Blender.
PAM’s design priority is screenplay-driven blocking. It answers production questions quickly:
- Does the scene read?
- Are the characters in the right positions?
- Does the dialogue timing work?
- Are prop interactions clear?
- Does the camera cut or drift at the right beat?
- Can this be used as a reliable reference for downstream rendering?
The blocking render is intentionally cheap to regenerate. It is a choreography and timing artifact, not a final character-animation pass.
A typical PAM-centered production path looks like this:
Fountain screenplay (.fountain)
|
v
+------------------+
| fountain2pam.py |
| Fountain+ parser|
+------------------+
|
+--> screenplay.json PAM action sequence
+--> prompts.json per-subscene visual prompts
+--> characters.txt optional synced character list
|
v
+------------------+
| pam_player.py |
| Manim renderer |
+------------------+
|
v
blocking MP4
|
v
+------------------+
| pam2blender.py |
| optional export |
+------------------+
|
v
Blender layout / lights / camera / props
|
v
AI video generation or manual refinement
|
v
Final edit
The blocking MP4 is the main iteration artifact. It is normally rendered many times while tuning positions, line timing, reactions, captions, camera drift, and prop choreography.
PAM represents characters as graph figures. A character has:
- a dictionary of joints;
- a set of edges connecting those joints;
- a named pose;
- a scale;
- an offset in Manim world coordinates;
- a style dictionary;
- optional face, wardrobe, bubble, and tic-profile metadata.
Supported figure families include:
| Figure family | Typical class | Notes |
|---|---|---|
| Human | HumanGraph |
Humanoid actors with male, female, and child presets. |
| Alien | AlienGraph |
Humanoid/split-torso alien actors with male/female variants. |
| Dog | DogGraph |
Quadruped figures, often dual-registered as cast members and props. |
| Governor | GovernorGraph |
Dodecahedral autonomous prop-character, useful for AI/governmental displays. |
PAM includes roughly thirty registered prop builders organized across furniture, carried props, flora, environmental props, and accessories.
Examples include:
chairdeskdoorpocket_doorelevatoravatar_podtv_monitorphonefolderbriefcasebackpacklaptopflowerfloral_arrangementbuildingsunmoondodecahedronbackdropsolar_panelsilver_hair
Several props are stateful and expose animation methods, for example doors opening/closing, avatar pods opening/closing, laptops opening/closing, and TV monitors showing anchor content.
PAM works directly from JSON, but the intended authoring layer is often Fountain+:
INT. POLYMERCORP OFFICE - DAY
[[ CAMERA: FRAMING=medium | SUBJECT=ensemble | MOVE=static | TRANSITION=cut ]]
[[ MOOD: corporate, fluorescent, slightly absurd ]]
[[ CAPTION: Office of Mrs Tatiana Bosch | lower-third | 5s | italic ]]
ALICE
Hi, Bob.
BOB
Hi, Alice.
fountain2pam.py converts these annotations into JSON actions, shot metadata, prompts, and optional character-registry merges.
PAM v0.9.x includes a JSON-driven face system implemented in face_builder.py.
Faces are built from Manim vector objects, not image files. A face can define:
- skin color;
- hair color and style;
- eyes;
- brows;
- mouth;
- clothing panel;
- hats;
- buns;
- earrings;
- expression variants;
- front and side views.
The face system is separate from skeleton physics. The cast block defines the body; the faces block defines the cartoon head overlay.
PAM can attach small updater-driven mobjects to live figures:
- torso icons;
- rectangular name tags;
- gloves;
- shoes;
- text-only badge labels;
- dog harnesses and service labels.
These overlays follow the character through walking, running, morphing, turning, scale changes, and fade-out cleanup.
PAM’s player is a Manim MovingCameraScene. It can consume _subscene_marker and _shot_meta data generated by Fountain+ CAMERA annotations.
Common camera vocabulary includes:
- framing:
wide,medium,close,insert; - movement:
static,push,pull,pan-follow,pan-up,pan-down,drift; - subject:
ensemble, a character key, or a prop/insert target; - transition: usually
cut, with animated transitions handled through movement duration.
Camera mode must be explicitly enabled with PAM_CAMERA_MODE=1.
Recommended baseline:
- Python 3.10+
- Manim Community, tested in the reference manual with Manim 0.20.1 on the Cairo backend
screenplain, used byfountain2pam.py- NumPy and SciPy, typically installed through Manim
- FFmpeg and system libraries required by Manim
- optional: LaTeX, if scenes use TeX-rendered labels
- optional: Blender 4.x, if using
pam2blender.py
git clone https://github.com/wdjoyner/pam.git
cd pampython3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pippip install manim screenplain
pip install -e .manim --versionpython - <<'PY'
import pam
print("PAM import OK")
PYDepending on how the repository is packaged at a given commit, you may also be able to work directly from the repository root without an editable install, as long as Python can import the local pam modules.
Create scene_quick_start.json:
[
{
"action": "title",
"text": "Quick Start",
"subtitle": "PAM v0.9.18"
},
{
"action": "cast",
"characters": {
"alice": {
"figure_type": "human",
"gender": "female",
"pose": "standing_front",
"offset": [-2.0, 0, 0],
"style": {
"head_label": "A",
"edge_color": "#cc4477",
"node_color": "#cc4477",
"head_color": "#ffffff",
"head_stroke": "#cc4477"
}
},
"bob": {
"figure_type": "human",
"gender": "male",
"pose": "standing_front",
"offset": [2.0, 0, 0],
"style": {
"head_label": "B",
"edge_color": "#4477cc",
"node_color": "#4477cc",
"head_color": "#ffffff",
"head_stroke": "#4477cc"
}
}
}
},
{
"action": "props",
"items": {
"table": {
"type": "desk",
"x": 0.0,
"label": "T"
}
}
},
{ "action": "fade_in", "who": "alice" },
{ "action": "fade_in", "who": "bob" },
{ "action": "say", "who": "alice", "text": "Hi, Bob!" },
{ "action": "wave", "who": "bob", "cycles": 2 },
{ "action": "say", "who": "bob", "text": "Hi, Alice!" },
{ "action": "walk_to", "who": "alice", "to_x": -0.5 },
{ "action": "walk_to", "who": "bob", "to_x": 0.5 },
{ "action": "say", "who": "alice", "text": "Nice desk." }
]Render it:
PAM_SCRIPT=scene_quick_start.json manim -pql pam_player.py PAMPlayerHigh-quality render:
PAM_SCRIPT=scene_quick_start.json manim -pqh pam_player.py PAMPlayer4K render:
PAM_SCRIPT=scene_quick_start.json manim -qk pam_player.py PAMPlayerSome older PAM notes used command-line arguments such as:
manim -pql pam_player.py PAMScene --json scene_quick_start.jsonThe current pam_player.py reference describes PAMPlayer, which reads the screenplay path from the PAM_SCRIPT environment variable. Prefer the PAM_SCRIPT=... manim ... PAMPlayer form unless you are intentionally running an older branch.
Create scene.fountain:
Title: Quick PAM Scene
Author: David Joyner
INT. POLYMERCORP OFFICE - DAY
[[ CAMERA: FRAMING=medium | SUBJECT=ensemble | MOVE=static | TRANSITION=cut ]]
[[ MOOD: corporate, fluorescent, slightly absurd ]]
[[ CAPTION: Office of Mrs Tatiana Bosch | lower-third | 5s | italic ]]
ALICE
Hi, Bob.
BOB
Hi, Alice.
Convert it to PAM JSON:
python fountain2pam.py scene.fountain \
-o scene.json \
--prompts scene_prompts.jsonRender it with camera mode enabled:
PAM_SCRIPT=scene.json \
PAM_PROMPTS=scene_prompts.json \
PAM_CAMERA_MODE=1 \
manim -pql pam_player.py PAMPlayerIf the scene uses a project character registry:
python fountain2pam.py scene.fountain \
-o scene.json \
--prompts scene_prompts.json \
--characters tntd_characters.json| Key | Purpose | Typical output |
|---|---|---|
CAMERA |
Framing, subject, move, transition | _shot_meta on a _subscene_marker |
MOOD |
Lighting or atmosphere | Prompt metadata |
KIND |
Scene type | Prompt metadata |
SCENE POPULATION |
Extras/background density | Prompt metadata |
NEGATIVE |
“Avoid this” prompt terms | Prompt metadata |
CAPTION |
On-screen text | caption action |
SOUND |
Diegetic sound flash | sound_cue action |
PHONE |
Phone/intercut speech mode | say with O.S. bubble metadata |
CHARACTER |
Per-scene character override | Cast-block entry/update |
PRODUCTION NOTE |
Human-only metadata | No executable action |
When using [[ ... ]] annotation blocks before dialogue, keep a blank line before the character cue.
Good:
[[ CAMERA: FRAMING=close | SUBJECT=Sidel | MOVE=static | TRANSITION=cut ]]
SIDEL
The Governor is waiting.
Risky:
[[ CAMERA: FRAMING=close | SUBJECT=Sidel | MOVE=static | TRANSITION=cut ]]
SIDEL
The Governor is waiting.
A PAM screenplay is a JSON array. Each executable step has an "action" key:
[
{ "action": "cast", "characters": {} },
{ "action": "props", "items": {} },
{ "action": "fade_in", "who": "alice" },
{ "action": "say", "who": "alice", "text": "Hello." }
]The cast action registers character specs. Later actions refer to them by who.
{
"action": "cast",
"characters": {
"sidel": {
"figure_type": "alien",
"gender": "female",
"pose": "standing_front",
"offset": [-3, 0, 0],
"scale": { "sx": 0.7, "sy": 0.7, "anchor": "lankle" },
"style": {
"head_label": "Sidel",
"edge_color": "#4f77aa"
}
}
}
}The props action registers initial props. Later prop actions refer to them by prop.
{
"action": "props",
"items": {
"phone": {
"type": "phone",
"style": "cellphone",
"x": 1.5,
"y": -1.2
},
"desk": {
"type": "desk",
"x": 0.0
}
}
}Large background or camera-addressable objects may be stored in a separate scene_objects registry. Use scene-object teardown actions for those, not prop teardown actions.
[
{
"action": "scene_objects",
"items": {
"skyline": { "type": "backdrop", "x": 0, "y": 1.5 }
}
},
{ "action": "remove_scene_object", "prop": "skyline", "rt": 0.4 }
]Use remove_prop for interactive props. Use remove_scene_object or clear_all_scene_objects for non-interactive stage dressing registered under scene_objects.
Fountain+ CAMERA annotations generate _subscene_marker steps. The player only uses them when:
PAM_CAMERA_MODE=1Without camera mode, _subscene_marker steps are skipped.
A common JSON screenplay order is:
- optional
title; cast;- optional
faces; - initial
props; - optional
scene_objects; - executable actions.
Example skeleton:
[
{ "action": "title", "text": "Scene 25", "subtitle": "Hospital corridor" },
{
"action": "cast",
"characters": {
"freydoon": {
"figure_type": "human",
"gender": "male",
"pose": "standing_front",
"offset": [-4.5, 0, 0],
"style": { "head_label": "Freydoon" }
}
}
},
{
"action": "faces",
"characters": {
"freydoon": {
"skin": "#c07848",
"hair": "#181818",
"hair_style": "short_male",
"eye_type": "human",
"eye_color": "#6a3808",
"mouth": "neutral"
}
}
},
{
"action": "props",
"items": {
"phone": { "type": "phone", "x": 1.5 }
}
},
{ "action": "fade_in", "who": "freydoon" },
{ "action": "attach_face", "who": "freydoon", "image": "freydoon", "scale": 0.35 },
{ "action": "walk_to_prop", "who": "freydoon", "prop": "phone" },
{ "action": "pick_up_phone", "who": "freydoon", "prop": "phone" },
{ "action": "say", "who": "freydoon", "text": "This had better be important." }
]PAM uses underscore-prefixed keys for comments and generated metadata.
| Key | Purpose |
|---|---|
_comment |
Human-readable comment; skipped by the player |
_hint |
Authoring/debugging hint; skipped by the player |
_subscene_marker |
Generated subscene boundary used for camera and prompt chunking |
_shot_meta |
Camera metadata generated from Fountain+ CAMERA tags |
Use _comment entries instead of trying to add JSON comments:
{ "_comment": "Sidel crosses to the phone." }| Field | Meaning |
|---|---|
figure_type |
human, alien, dog, or dodecahedral/Governor-style prop-character type |
gender |
Preset used by human/alien builds |
build |
Body-proportion preset |
pose |
Initial named pose |
scale |
sx, sy, and anchor information |
offset |
Initial world-space position |
color |
Canonical base color, often expanded into style colors |
torso_color |
Torso/uniform color |
style |
Head label, edge color, node color, bubble colors, clothing fields, overlays |
uniforms |
Named uniform variants for change_uniform |
tic_profile |
Optional triggered micro-motion profile |
hidden |
Register without initially showing |
scale may be a float or a dictionary. The dictionary form is clearer:
"scale": {
"sx": 0.7,
"sy": 0.7,
"anchor": "lankle"
}Several scale actions use target scale, not multiplier semantics. If a character is already at 0.7 and you set sy to 0.5, the new scale is 0.5, not 0.35.
A tic_profile attaches small motion fragments to a character and fires them on triggers such as react or sentence_end.
{
"figure_type": "alien",
"tic_profile": [
{ "fragment": "hand_twitch_r", "triggers": ["react"] },
{ "fragment": "foot_tap_l", "triggers": ["sentence_end"] }
]
}Relevant fragments include:
hand_twitch_rhand_twitch_lfoot_tap_rfoot_tap_l- dog-specific fragments such as
tail_wag
A say step may fire sentence_end tics automatically. Use suppress_tics: true on the speech step to skip them.
The flat-cartoon face system is implemented in face_builder.py.
A face spec lives in an "action": "faces" block and is independent of the cast skeleton. The cast defines the body. The face spec defines the cartoon overlay attached to the head dot.
{
"action": "faces",
"characters": {
"sidel": {
"skin": "#8cc47c",
"hair": "#202020",
"hair_style": "short_male",
"eye_type": "alien",
"eye_color": "#c87820",
"brow_thick": true,
"mouth": "neutral",
"cloth_color": "#202840",
"cloth_style": "uniform"
}
}
}Only skin and hair are strictly required by the builder. Other fields have defaults.
{ "action": "attach_face", "who": "sidel", "image": "sidel", "scale": 0.35 }The image value can be:
- a
FACE_DATAkey such assidel; - an expression variant key such as
sidel_worried; - a legacy
.pngor.svgfile inpam/assets/.
{ "action": "attach_face", "who": "sidel", "image": "sidel", "view": "lside", "scale": 0.35 }Valid views:
| View | Meaning |
|---|---|
front |
Default front-facing portrait |
lside |
Profile facing left |
rside |
Profile facing right |
Use side-view faces when the skeleton is in a side pose or walking direction.
Calling attach_face again removes the previous face and attaches the new one:
[
{ "action": "attach_face", "who": "sidel", "image": "sidel", "scale": 0.35 },
{ "action": "say", "who": "sidel", "text": "The Council's decision is final." },
{ "action": "attach_face", "who": "sidel", "image": "sidel_worried", "scale": 0.35 },
{ "action": "say", "who": "sidel", "text": "This will not go well for us." }
]Keep the same scale for all expression swaps on the same character unless you deliberately want a visible size jump.
PAM v0.9.18 adds per-character geometry overrides for hair and triangle hats.
Hair overrides:
| Field | Meaning |
|---|---|
hair_width |
Override the front-view hair-cap width |
hair_height |
Override the hair-cap height |
hair_offset_y |
Raise or lower the hair cap center |
Triangle hat overrides:
| Field | Meaning |
|---|---|
hat_width |
Override triangle or inverted-triangle base width |
hat_height |
Override triangle height/span |
Example:
{
"factor": {
"skin": "#8cc47c",
"hair": "#1f1f1f",
"hair_style": "short_male",
"hat_style": "triangle",
"hat_color": "#2a6830",
"hat_width": 0.72,
"hat_height": 0.03
}
}These overrides are optional and default to previous hardcoded geometry, so existing characters should render unchanged.
Wardrobe overlays are decorative Manim mobjects attached to live characters. They are not pose data, skeleton joints, or props.
General lifecycle:
- attach once after
fade_in; - the updater tracks the character through motion;
- re-attaching the same overlay type first removes the old one;
- a matching detach action is available;
fade_outcleans up attached faces, icons, gloves, shoes, tags, and harnesses.
{
"action": "attach_torso_icon",
"who": "yannos",
"preset": "name_tag",
"text": "Y. YANNOS",
"fill": "#f0e4b8",
"stroke": "#3a2a14",
"scale": 1.0
}Detach:
{ "action": "detach_torso_icon", "who": "yannos" }{ "action": "attach_gloves", "who": "bevers", "color": "#cc3333" }{ "action": "detach_gloves", "who": "bevers" }{ "action": "attach_shoes", "who": "bevers", "color": "#1a1a1a" }{ "action": "detach_shoes", "who": "bevers" }Shoe updaters track the figure’s facing direction, so shoes mirror when a character changes walking direction.
{ "action": "attach_name_tag", "who": "chekov", "text": "CHEKOV" }For human/alien panel-clothed faces, attach the face first so the badge anchor exists:
[
{ "action": "attach_face", "who": "sidel", "image": "sidel", "scale": 0.35 },
{
"action": "attach_name_tag",
"who": "sidel",
"text": "CMDR\nSIDEL",
"font_size": 14,
"color": "#c8a020",
"dy": 0.02
}
]Dog harnesses are controlled through the dog’s style dictionary:
{
"action": "cast",
"characters": {
"chekov": {
"figure_type": "dog",
"offset": [-2, 0, 0],
"style": {
"harness_style": "standard",
"harness_color": "#2d6a4f"
}
}
}
}{
"action": "props",
"items": {
"main_desk": {
"type": "desk",
"x": 0.0,
"label": "D"
},
"exit_door": {
"type": "pocket_door",
"x": 5.5
}
}
}{
"action": "spawn_prop",
"prop": "folder",
"type": "folder",
"x": 1.0,
"y": -1.0
}Always provide type. In some older/fallback paths, a missing type may default to hat, which is rarely what was intended.
{ "action": "remove_prop", "prop": "folder", "rt": 0.3 }Examples:
{ "action": "open_doors", "prop": "elevator" }{ "action": "close_doors", "prop": "elevator" }{ "action": "open_lid", "prop": "avatar_pod" }{ "action": "close_lid", "prop": "avatar_pod" }{
"action": "prop_say",
"prop": "governor",
"text": "Processing.",
"bubble_color": "#382050",
"text_color": "#ffffff",
"border_color": "#c8a020"
}For GovernorGraph, v0.9.18 adds geometry-aware bubble placement and a y_offset key for per-call vertical adjustment:
{
"action": "prop_say",
"prop": "governor",
"text": "Please wait.",
"y_offset": 0.2
}Dog-like figures may be registered in both the cast registry and the prop registry. This allows either character-like or prop-like addressing:
[
{ "action": "fade_in", "who": "chekov" },
{ "action": "trot_to", "who": "chekov", "x": 2.0 },
{ "action": "prop_say", "prop": "chekov", "text": "Woof." },
{ "action": "react", "who": "chekov" }
]This is a practical overview. The full action reference belongs in the detailed manual.
{ "action": "say", "who": "sidel", "text": "The Governor is waiting." }{ "action": "prop_say", "prop": "phone", "text": "RING!" }{ "action": "clear_bubble", "who": "sidel" }{ "action": "clear_prop_bubble", "prop": "phone" }{ "action": "clear_all_bubbles", "rt": 0.2 }{ "action": "walk_to", "who": "sidel", "to_x": 2.0 }{ "action": "run_to", "who": "sidel", "to_x": -4.0 }{ "action": "rush_to", "who": "sidel", "to_x": 3.0 }{ "action": "walk_to_prop", "who": "sidel", "prop": "desk" }{ "action": "exit_through_doors", "who": "sidel", "door": "exit_door" }Common locomotion actions:
walk_torun_torush_torush_outsqueeze_throughwalk_to_proprun_to_propexit_throughexit_through_doorsjump_upfall_downdodgetrot_to
{ "action": "turn", "who": "sidel", "facing": "left" }{ "action": "face", "who": "sidel", "target": "governor" }{ "action": "sit_down", "who": "sidel", "target": "chair" }{ "action": "stand_up", "who": "sidel" }{ "action": "wave", "who": "bob", "cycles": 2 }{ "action": "nod", "who": "bob" }{ "action": "shake_head", "who": "bob" }{ "action": "shrug", "who": "bob" }{ "action": "react", "who": "bob", "expression": "surprised" }Common expressive actions:
wavewave_leftwave_rightnodshake_headshrugpoint_atexpressreact
{ "action": "reach_for", "who": "chava", "target": "folder" }{ "action": "grab", "who": "chava", "prop": "folder" }{ "action": "pick_up", "who": "sidel", "prop": "briefcase" }{ "action": "place_on", "who": "sidel", "prop": "briefcase", "target": "desk" }{ "action": "punch_button", "who": "bevers", "target": "elevator_button" }Common prop-interaction actions:
reach_forgrabpick_uppick_up_phonehang_upplace_onput_downpeel_from_handpunch_buttonsearch_drawerssnap_photostick_tomove_asidecarry
{ "action": "hold_hands", "who": "alice", "target": "bob" }{ "action": "pat_head", "who": "alice", "target": "bob" }Common two-figure actions:
kisshold_handspatpat_headhand_tograb_armtwist_arm_behindrelease_armreach_character
{ "action": "caption", "text": "Three hours later...", "position": "center", "duration": 3.0 }{ "action": "persistent_caption", "text": "LIVE FEED", "position": "upper-left" }{ "action": "sound_cue", "label": "KNOCK!", "display": true }{ "action": "on_screen_text", "text": "SYSTEM ERROR", "duration": 2.0 }{ "action": "wait", "rt": 1.0 }{ "action": "focus", "who": "sidel", "rt": 0.4 }{ "action": "focus_reset", "rt": 0.4 }{ "action": "flash", "who": "sidel", "color": "#ffff00", "rt": 0.2 }PAM v0.9.18 improves focus/focus-reset behavior around face-attached figures by keeping head/face overlays in front during the animation, reducing transient head-dot or neck/shoulder flicker.
parallel wraps actions that should happen together:
{
"action": "parallel",
"rt": 1.0,
"steps": [
{ "action": "walk_to", "who": "alice", "to_x": -0.5 },
{ "action": "walk_to", "who": "bob", "to_x": 0.5 }
]
}PAM uses a two-pass approach internally:
- collect/coordinate locomotion-like animations;
- collect simple parallel-safe animations;
- apply required post-pass state updates.
Not every action is parallel-safe. Actions that mutate shared scene state, speech-bubble state, prop ownership, or camera state are usually better authored sequentially.
Known caution: group_translate is documented as potentially unsafe in parallel blocks in the v0.9.x line.
pam_player.py is a Manim MovingCameraScene. The camera frame has:
- a width, controlling zoom/framing;
- a center, controlling pan/tilt;
- optional animated transitions.
PAM_SCRIPT=scene.json PAM_CAMERA_MODE=1 manim -pql pam_player.py PAMPlayerPAM_SCRIPT=scene.json \
PAM_PROMPTS=scene_prompts.json \
PAM_CAMERA_MODE=1 \
manim -pql pam_player.py PAMPlayer| Framing | Typical use |
|---|---|
wide |
Whole-stage or ensemble coverage |
medium |
Two-shot or character-medium framing |
close |
Character close-up |
insert |
Prop close-up |
| Move | Typical behavior |
|---|---|
static |
Instant cut/reframe |
push |
Animated move inward |
pull |
Animated move outward |
pan-follow |
Follow subject horizontally |
pan-up |
Vertical upward pan |
pan-down |
Vertical downward pan |
drift |
Slow atmospheric move |
At the default Manim frame:
- visible x-range is roughly
[-7.1, 7.1]; - visible y-range is roughly
[-4, 4]; - the practical stage is roughly
x in [-6, 6]; - a default standing character’s feet are around
y = -2.0; - a default head is around
y = 1.2; - speech bubbles extend about 2.5 units from the speaker.
Safe speech-bubble placement:
- right-side bubble: speaker should usually be at
x <= 4.0; - left-side bubble: speaker should usually be at
x >= -4.0.
PAM-specific configuration is passed through environment variables because Manim owns the command line.
| Variable | Required? | Meaning |
|---|---|---|
PAM_SCRIPT |
yes | Path to the PAM JSON screenplay |
PAM_CAMERA_MODE |
no | 1 enables camera mode; unset or 0 skips _subscene_marker camera handling |
PAM_PROMPTS |
no | Path to prompts JSON; default is normally based on the PAM_SCRIPT stem |
PAM_SHOW_CLOCK |
no | 1 displays an elapsed-time overlay for preview renders |
PAM_SCRIPT=scene.json PAM_SHOW_CLOCK=1 manim -pql pam_player.py PAMPlayerThe clock displays elapsed scene time as M:SS.ss. It is intended for low-quality timing previews, not final renders.
| Flag | Quality | Resolution | FPS |
|---|---|---|---|
-ql |
low | 480p | 15 |
-qm |
medium | 720p | 30 |
-qh |
high | 1080p | 60 |
-qk |
4K | 2160p | 60 |
-p |
preview after render | — | — |
Most authoring iteration uses:
PAM_SCRIPT=scene.json manim -pql pam_player.py PAMPlayerReference layout:
pam/
├── __init__.py public API exports
├── pam_player.py main Manim scene, camera, bubbles, registries, dispatcher
├── actions.py character-targeting action handlers and ACTION_REGISTRY
├── characters.py HumanGraph, AlienGraph, DogGraph, GovernorGraph
├── figure.py shared figure infrastructure, bubbles, scale anchors
├── poses.py named poses, cycles, expression glyphs
├── paired_poses.py multi-character interaction templates
├── builds.py body proportions and build presets
├── face_builder.py JSON-driven flat-cartoon face generation
├── tics.py tic fragments and triggered micro-motion helpers
├── props.py thin prop dispatcher / re-export layer
├── props_core.py shared prop infrastructure
├── props_furniture.py chair, desk, pocket door, elevator, avatar pod, TV monitor
├── props_carried.py hat, briefcase, folder, phone, backpack, laptop, bug
├── props_flora.py flowers and floral arrangements
├── props_environment.py buildings, sun, moon, dodecahedron, backdrop, panels
├── props_accessories.py accessory builders such as silver hair
├── character_gallery.py visual regression / documentation utility
├── fountain2pam.py Fountain+ to PAM JSON converter
├── pam2blender.py PAM JSON to Blender scene exporter
└── tntd_characters.json canonical TNTD character registry
| Problem area | Start here |
|---|---|
| Character-only action | actions.py |
| Action dispatch / main loop | pam_player.py |
| Cast registration | pam_player.py |
| Prop registration | pam_player.py, props_*.py |
| Camera behavior | pam_player.py |
| Speech bubbles | pam_player.py, figure.py, characters.py |
| Face attachment | face_builder.py, pam_player.py |
| Wardrobe overlays | actions.py, figure.py, characters.py |
| Pose shape | poses.py |
| Body proportions | builds.py |
| Prop geometry | props_*.py |
| Fountain parsing | fountain2pam.py |
| Blender export | pam2blender.py |
Most character-targeting actions belong in actions.py.
General pattern:
- implement a handler function;
- follow the existing handler signature convention;
- register it in
ACTION_REGISTRY; - decide whether it belongs in
_CANNOT_PARALLEL; - add a minimal JSON example;
- document the action.
Sketch:
def do_salute(fig, step, scene, name, *, props=None, cast=None):
rt = float(step.get("rt", 0.4))
# Build Manim animations.
# Update figure state if needed.
# Return animations if step requests _collect_anims.
...
ACTION_REGISTRY["salute"] = do_saluteAsk whether the action mutates:
- the cast registry;
- the prop registry;
- the bubble registry;
- camera state;
- prop ownership;
- z-order;
- attached updaters.
If it does, it may need special handling in pam_player.py or may not be parallel-safe.
Most prop builders belong in a props_*.py module.
General pattern:
- choose the correct prop module;
- implement a
build_*function returning a ManimVGroup; - assign stable metadata such as
pam_name,pam_type, and surface information; - define attachment points if relevant;
- register the builder;
- add a JSON example;
- document creation-time fields and runtime behavior.
Build presets belong in builds.py or the relevant figure-construction module.
Checklist:
- define body proportions;
- verify named pose compatibility;
- test front and side orientations;
- test speech bubble placement;
- test face attachment if the build uses faces;
- add a character-gallery sample;
- document the build.
Parser changes belong in fountain2pam.py.
Checklist:
- define syntax;
- decide whether the key emits executable actions, shot metadata, prompt metadata, cast metadata, or human-only notes;
- add parser support;
- update generated JSON examples;
- add a small screenplay test case;
- document the key.
Set PAM_SCRIPT explicitly:
PAM_SCRIPT=scene.json manim -pql pam_player.py PAMPlayerRun from the repository root or use an absolute path.
Enable camera mode:
PAM_SCRIPT=scene.json PAM_CAMERA_MODE=1 manim -pql pam_player.py PAMPlayerAlso verify that the JSON contains _subscene_marker entries with _shot_meta.
Check:
- Was the Fountain file converted with a recent
fountain2pam.py? - Does the JSON contain
_subscene_markerentries? - Did you render with
PAM_CAMERA_MODE=1? - Is there a blank line between the annotation block and the next character cue?
Keep speakers inside the safe region:
- right-side bubble: speaker around
x <= 4; - left-side bubble: speaker around
x >= -4.
Use camera framing or character repositioning to keep bubbles visible.
Clear bubbles before focus reset:
[
{ "action": "clear_bubble", "who": "bevers" },
{ "action": "focus_reset", "rt": 0.4 }
]Or clear all bubbles:
[
{ "action": "clear_all_bubbles", "rt": 0.2 },
{ "action": "focus_reset", "rt": 0.4 }
]Check:
- the
"faces"block appears beforeattach_face; - the
imagekey matches a loaded face key or expression variant; skinandhairare present in the face spec;- the character exists in the cast registry;
fade_inhas occurred beforeattach_face.
For JSON-built faces, PAM uses a pam_head_ref anchor so tall hair and hats align by the head oval rather than the full bounding box. If a bitmap or SVG face mis-anchors, crop or compose the source image so its center matches the intended head center; bitmap/SVG assets do not have the pam_head_ref metadata.
For a human or alien panel-badge name tag, attach the face first:
[
{ "action": "attach_face", "who": "sidel", "image": "sidel", "scale": 0.35 },
{ "action": "attach_name_tag", "who": "sidel", "text": "CMDR\nSIDEL" }
]For a dog name tag, ensure the dog has a harness style before fade-in.
Check whether the prop was declared with hidden: true. Hidden props are registered but initially transparent; reveal them with spawn_prop or the appropriate prop action.
If it was created under scene_objects, use:
{ "action": "remove_scene_object", "prop": "skyline" }Use remove_prop only for interactive props in the prop registry.
Dog-like figures may be dual-registered as both cast members and props. Use:
whofor character-style actions such astrot_toandreact;propfor prop-style speech or prop-targeting actions such asprop_say.
The canonical registry is authoritative when using fountain2pam.py --characters. Edit tntd_characters.json or your project registry rather than relying on per-scene color overrides that may be overwritten during conversion.
Specify the prop type. A missing type may fall through to a fallback builder.
{ "action": "spawn_prop", "prop": "briefcase_1", "type": "briefcase", "x": 1.0 }Verify that Manim works:
manim --versionVerify that PAM imports in the same environment:
python - <<'PY'
import pam
print(pam)
PYIf using a virtual environment, make sure both Manim and PAM are installed in that environment.
Headline changes:
- per-character hair geometry overrides:
hair_width,hair_height,hair_offset_y; - triangle and inverted-triangle hats now honor
hat_widthandhat_height; - alien ear and earring anchoring now uses species-correct head radius;
pam/tics.pyaddshand_twitch_l,foot_tap_r, andfoot_tap_l;GovernorGraph.saybubble placement is geometry-aware;prop_saysupportsy_offsetfor Governor vertical bubble nudging;- focus/focus-reset behavior improves face-overlay handling and reduces flicker.
Notable changes:
rotateaction for characters and props;- fixed
wavedirection-key behavior; lying_flat_rightandlying_flat_leftposes;closedparameter onbuild_laptop;paired_poses.pyfor multi-character interaction templates.
nodshake_headshrug
clear_bubbleclear_prop_bubbleclear_all_bubbles- documented focus-reset and bubble lifecycle gotchas.
change_uniform- cast-block
uniformsdictionary for named variants.
- clarified color authority chain;
fountain2pam.py --charactersapplies canonical palettes.
Major action expansion:
grabplace_onmove_asidereach_forpunch_buttonstick_tosnap_photopeel_from_handjump_updodgereactexpresspatsearch_drawerscaptionpersistent_captionsound_cuezone_shiftrush_torush_outexit_through_doors
Recommended documentation set for the repository:
| Document | Scope |
|---|---|
README.md |
Repository front door, quick start, architecture overview |
pam-reference-book_v0918.tex or compiled PDF |
Full reference manual |
pam_player-manual.md |
Player internals, camera, action loop, bubble registry, environment variables |
CHARACTER_PROPERTIES.md |
Character creation fields, actions, faces, wardrobe, styles, tics |
PROP_PROPERTIES.md |
Prop creation fields, metadata, registry behavior, prop actions, prop catalog |
fountain2pam-manual.md |
Fountain+ syntax and conversion workflow |
examples/ |
Working JSON and Fountain+ scenes |
tests/ |
Smoke tests and regression checks |
The README should stay short enough to be useful on GitHub. Detailed tables belong in the reference manual and companion docs.
A useful examples/ directory might contain:
examples/
├── scene_quick_start.json
├── scene_quick_start.fountain
├── camera_demo.fountain
├── face_attachment_demo.json
├── wardrobe_overlay_demo.json
├── prop_interaction_demo.json
├── scene_object_teardown_demo.json
├── parallel_walk_demo.json
├── phone_call_demo.fountain
└── blender_export_demo.json
Each example should ideally include:
- source
.fountain, if applicable; - generated
.json; - generated
_prompts.json, if applicable; - render command;
- short description of the behavior demonstrated.
Animation libraries need both code tests and visual checks.
Useful tests include:
- import tests;
- JSON schema/smoke tests;
- action-dispatch tests;
- prop-builder construction tests;
- Fountain+ parser tests;
- camera-marker generation tests;
- short low-quality render tests;
- character-gallery snapshots;
- face-gallery snapshots;
- prop-gallery snapshots.
Minimal smoke render:
PAM_SCRIPT=examples/scene_quick_start.json manim -ql pam_player.py PAMPlayerPossible next steps:
- normalize action parameter names such as
xvs.to_x; - add a formal JSON schema;
- expand automated tests for
parallel; - improve diagnostics for unknown face keys and missing face specs;
- add visual regression snapshots for face variants and wardrobe overlays;
- add a prop-gallery generator;
- strengthen Fountain+ annotation diagnostics;
- package
pam-renderandfountain2pamas console scripts; - continue the bubble-lifecycle migration so bubbles, focus, overlays, and face attachments have simpler z-order semantics.
Useful contributions include:
- a minimal JSON or Fountain+ file that reproduces a bug;
- a before/after render clip;
- a new documented action;
- a new prop builder with attachment metadata;
- a new face style or wardrobe overlay;
- a new character build or pose;
- tests for parser or dispatcher behavior;
- documentation improvements.
When reporting a bug, include:
- PAM version or commit;
- Python version;
- Manim version;
- operating system;
- exact command used;
- relevant JSON or Fountain+ excerpt;
- traceback, if any;
- whether
PAM_CAMERA_MODE=1was enabled; - whether the scene uses faces, wardrobe overlays, or persistent bubbles.
Copyright © David Joyner.
See LICENSE for terms.
PAM is a working artifact of the Too Nice to Die production pipeline and builds on the Manim ecosystem for rendering. The reference manual and companion documents were developed alongside iterative production needs for screenplay-driven animation.