
Stix
- 14 installs
- 2 repo stars
- Updated March 1, 2026
- bassimeledath/stix
Stix is a skill that generates stick-figure animations from natural-language descriptions, outputting GIF and MP4.
About
Stix generates stick-figure animations from a natural-language description. It decomposes the prompt into scenes, builds an asset manifest of reusable SVG characters, backgrounds, and props, and captures frames to render GIF and MP4 output. A developer uses it to turn a short scene description, like a cat chasing a mouse, into an animated clip.
- Generates stick-figure animations from a natural-language description
- Builds an asset manifest of SVG characters, backgrounds, and props with position contracts
- Outputs GIF and MP4 with per-scene coordinate and continuity contracts
Stix by the numbers
- 14 all-time installs (skills.sh)
- Ranked #1,029 of 1,335 Generative Media skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
stix capabilities & compatibility
- Capabilities
- video generation · animation
- Use cases
- video generation
- Pricing
- Free
What stix says it does
Generate stick figure animations from natural language descriptions. Outputs GIF and MP4.
Each scene is always **8 seconds** (the animation system is built around this fixed duration).
npx skills add https://github.com/bassimeledath/stix --skill stixAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 14 |
|---|---|
| repo stars | ★ 2 |
| Last updated | March 1, 2026 |
| Repository | bassimeledath/stix ↗ |
What it does
Turn a natural-language scene description into a stick-figure animation (GIF and MP4).
Who is it for?
Producing short animated stick-figure clips from a described scene
Skip if: Photorealistic video, live-action footage, or non-animation visual tasks
When should I use this skill?
The user asks to animate a described scene as stick figures, or invokes /stix
What you get
A GIF and MP4 animation where characters follow position contracts and continuity across scenes
- GIF animation
- MP4 animation
- reusable SVG assets
By the numbers
- 8-second fixed scene duration
- 1-6 scenes
- 900x400 viewBox
Files
/stix — Stick Figure Animation
Generate stick figure animations from natural language descriptions. Outputs GIF and MP4 files.
Usage
/stix "a cat chasing a mouse across a park"Pipeline
When the user invokes /stix, execute these steps in order:
---
Step 1: Parameter Inference
Parse the user's prompt and infer these parameters. The user never sees or sets these directly — infer them from the natural language description:
| Parameter | Range | Default | How to infer |
|---|---|---|---|
scenes | 1–6 | 2 | Count distinct moments/locations/emotional beats in the prompt |
fps | 8–24 | 8 | Default for most; use 12 for fast action |
format | gif/mp4/both | both | Default both unless user specifies |
mood | playful/melancholy/energetic/calm | playful | Infer from the emotion in the prompt |
Each scene is always 8 seconds (the animation system is built around this fixed duration).
Write these to .stix/params.json:
{
"prompt": "a cat chasing a mouse across a park",
"scenes": 3,
"fps": 8,
"format": "both",
"mood": "playful"
}---
Step 2: Preflight Checks
Verify dependencies are installed:
command -v agent-browser >/dev/null 2>&1 || { echo "agent-browser not found"; exit 1; }
command -v ffmpeg >/dev/null 2>&1 || { echo "ffmpeg not found"; exit 1; }
# Detect agent CLI (configurable via STIX_AGENT_CLI env var)
STIX_AGENT_CLI="${STIX_AGENT_CLI:-}"
if [ -z "$STIX_AGENT_CLI" ]; then
if command -v claude >/dev/null 2>&1; then
STIX_AGENT_CLI="claude -p --dangerously-skip-permissions"
STIX_AGENT_ENV_UNSET="CLAUDE_CODE_ENTRYPOINT,CLAUDECODE"
elif command -v codex >/dev/null 2>&1; then
STIX_AGENT_CLI="codex --quiet --full-auto"
STIX_AGENT_ENV_UNSET=""
else
echo "No supported agent CLI found. Set STIX_AGENT_CLI env var."
echo " Supported: claude, codex. Or set STIX_AGENT_CLI to your agent's pipe command."
exit 1
fi
fi
AGENT_BIN=$(echo "$STIX_AGENT_CLI" | awk '{print $1}')
command -v "$AGENT_BIN" >/dev/null 2>&1 || { echo "Agent CLI '$AGENT_BIN' not found"; exit 1; }Create the working directory:
rm -rf .stix
mkdir -p .stix/scenes .stix/assets/characters .stix/assets/props .stix/qc .stix/frames .stix/output---
Step 3: Story Decomposition
Break the prompt into scenes. The story includes an asset manifest that lists every character, background element, and prop needed across all scenes, plus per-scene position contracts with exact coordinates.
Write .stix/story.json:
{
"title": "Cat Chases Mouse",
"assets": {
"characters": [
{ "id": "cat", "description": "Orange tabby cat (#e8a040), pointy ears, whiskers, curved tail", "expressions_needed": ["determined", "happy"] },
{ "id": "mouse", "description": "Small grey mouse (#999), round ears, thin tail", "expressions_needed": ["neutral", "surprised"] }
],
"background": {
"description": "Park setting: green grass, 2 trees, bench on the right, blue sky gradient, sun"
},
"props": ["tree", "bench"]
},
"scenes": [
{
"id": 1,
"description": "A mouse scurries across a park path, unaware.",
"characters": {
"mouse": { "start": [50, 330], "end": [500, 330], "start_expr": "neutral", "end_expr": "neutral" }
},
"animation_notes": "Mouse runs left to center, scurry animation cycle",
"props_used": ["tree", "bench"],
"timing": { "run": "0-100%" },
"mood": "playful"
},
{
"id": 2,
"description": "A cat spots the mouse and launches into a chase.",
"characters": {
"cat": { "start": [30, 310], "end": [600, 310], "start_expr": "determined", "end_expr": "determined" },
"mouse": { "start": [500, 330], "end": [850, 330], "start_expr": "neutral", "end_expr": "surprised" }
},
"animation_notes": "Cat enters from left chasing mouse, both running right. Cat gains on mouse.",
"props_used": ["tree"],
"timing": { "chase": "0-100%" },
"mood": "energetic",
"continuity": "Mouse position matches end of scene 1"
}
]
}Asset manifest rules:
- Every character that appears in ANY scene must be listed in
assets.characters - One shared background for ALL scenes
- All unique props listed in
assets.props - Character descriptions include specific colors for consistency
Per-scene position contracts:
start/endare[x, y]pixel coordinates within the 900x400 viewBoxstart_expr/end_exprspecify which expression variant to showtimingmaps animation phases to percentages of the 8s durationcontinuitynotes link scenes together
---
Step 4: Asset Generation (Parallel Workers)
Generate reusable SVG assets that will be shared across all scenes. Each asset is a self-contained SVG file.
Output structure:
.stix/assets/
├── characters/
│ ├── cat.svg
│ └── mouse.svg
├── background.svg
└── props/
├── tree.svg
└── bench.svgThe dispatcher must read and inject library file contents into each worker prompt. Workers run as independent agent subprocesses and cannot access the skill directory.
SKILL_DIR="$(dirname "$(readlink -f "$0")" 2>/dev/null || cd "$(dirname "$0")" && pwd)"
# If SKILL_DIR detection fails, try common install locations:
# ~/.claude/skills/stix, ~/.codex/skills/stix, or ~/.agents/skills/stix
# Read library files
STYLE_GUIDE="$(cat "$SKILL_DIR/library/style-guide.md")"
POSES="$(cat "$SKILL_DIR/library/poses.md")"
EXPRESSIONS="$(cat "$SKILL_DIR/library/expressions.md")"
PROPS_LIB="$(cat "$SKILL_DIR/library/props.md")"
ANIMALS="$(cat "$SKILL_DIR/library/animals.md")"
# Read reference templates
CHAR_REF="$(cat "$SKILL_DIR/references/asset-character.md")"
BG_REF="$(cat "$SKILL_DIR/references/asset-background.md")"
PROPS_REF="$(cat "$SKILL_DIR/references/asset-props.md")"
# Read asset manifest from story.json
ASSET_MANIFEST=$(python3 -c "
import json
story = json.load(open('.stix/story.json'))
print(json.dumps(story['assets'], indent=2))
")Spawn parallel workers:
4a. Character workers (1 per character)
For each character in the asset manifest:
CHARACTER_DESC=$(python3 -c "
import json
story = json.load(open('.stix/story.json'))
chars = story['assets']['characters']
char = [c for c in chars if c['id'] == '$CHAR_ID'][0]
print(json.dumps(char, indent=2))
")
cat > /tmp/stix-asset-char-${CHAR_ID}-prompt.txt << PROMPT_EOF
You are generating a reusable SVG character asset for a stick figure animation system.
## Character Specification
$CHARACTER_DESC
## Asset Template & Instructions
$CHAR_REF
## Component Library
### Style Guide
$STYLE_GUIDE
### Poses
$POSES
### Expressions
$EXPRESSIONS
### Animals (if this is an animal character)
$ANIMALS
## Output
Write ONLY the SVG content to: .stix/assets/characters/${CHAR_ID}.svg
No explanation, no markdown — just the SVG file.
PROMPT_EOF
# Build env unset command
ENV_CMD="env"
if [ -n "$STIX_AGENT_ENV_UNSET" ]; then
IFS=',' read -ra UNSET_VARS <<< "$STIX_AGENT_ENV_UNSET"
for var in "${UNSET_VARS[@]}"; do
[ -n "$var" ] && ENV_CMD="$ENV_CMD -u $var"
done
fi
$ENV_CMD $STIX_AGENT_CLI \
"$(cat /tmp/stix-asset-char-${CHAR_ID}-prompt.txt)" &4b. Background worker (1 total)
BG_DESC=$(python3 -c "
import json
story = json.load(open('.stix/story.json'))
print(json.dumps(story['assets']['background'], indent=2))
")
cat > /tmp/stix-asset-bg-prompt.txt << PROMPT_EOF
You are generating a reusable SVG background for a stick figure animation system.
## Background Specification
$BG_DESC
## Asset Template & Instructions
$BG_REF
## Component Library
### Style Guide
$STYLE_GUIDE
### Props (for background elements like trees, benches)
$PROPS_LIB
## Output
Write ONLY the SVG content to: .stix/assets/background.svg
No explanation, no markdown — just the SVG file.
PROMPT_EOF
$ENV_CMD $STIX_AGENT_CLI \
"$(cat /tmp/stix-asset-bg-prompt.txt)" &4c. Props worker (1 total for all props)
PROPS_LIST=$(python3 -c "
import json
story = json.load(open('.stix/story.json'))
print(json.dumps(story['assets']['props'], indent=2))
")
cat > /tmp/stix-asset-props-prompt.txt << PROMPT_EOF
You are generating reusable SVG props for a stick figure animation system.
## Props Needed
$PROPS_LIST
## Asset Template & Instructions
$PROPS_REF
## Component Library
### Style Guide
$STYLE_GUIDE
### Props Reference
$PROPS_LIB
## Output
Write each prop as a separate SVG file to: .stix/assets/props/<prop_id>.svg
No explanation, no markdown — just the SVG files.
PROMPT_EOF
$ENV_CMD $STIX_AGENT_CLI \
"$(cat /tmp/stix-asset-props-prompt.txt)" &Wait for all asset workers:
waitVerify all assets exist:
# Check characters
for CHAR_ID in $(python3 -c "
import json
story = json.load(open('.stix/story.json'))
for c in story['assets']['characters']:
print(c['id'])
"); do
FILE=".stix/assets/characters/${CHAR_ID}.svg"
if [ ! -f "$FILE" ]; then
echo "Missing character asset: $FILE"
exit 1
fi
echo "character: $FILE"
done
# Check background
if [ ! -f ".stix/assets/background.svg" ]; then
echo "Missing background asset"
exit 1
fi
echo "background: .stix/assets/background.svg"
# Check props
for PROP_ID in $(python3 -c "
import json
story = json.load(open('.stix/story.json'))
for p in story['assets']['props']:
print(p)
"); do
FILE=".stix/assets/props/${PROP_ID}.svg"
if [ ! -f "$FILE" ]; then
echo "Missing prop asset: $FILE"
exit 1
fi
echo "prop: $FILE"
done---
Step 5: Asset Validation
Render each asset in a minimal HTML wrapper and screenshot it. The orchestrating agent visually inspects the screenshots and regenerates bad assets (max 2 retries).
# For each asset file, create a wrapper HTML
for ASSET in .stix/assets/characters/*.svg .stix/assets/props/*.svg .stix/assets/background.svg; do
[ -e "$ASSET" ] || continue
ASSET_NAME=$(basename "$ASSET" .svg)
ASSET_CONTENT=$(cat "$ASSET")
cat > "/tmp/stix-validate-${ASSET_NAME}.html" << WRAP_EOF
<!DOCTYPE html>
<html><head>
<meta charset="UTF-8">
<style>
body { background: #faf9f6; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; }
svg { width: 400px; height: 400px; border: 1px solid #eee; }
</style>
</head><body>
<svg viewBox="-100 -100 300 300" xmlns="http://www.w3.org/2000/svg">
$ASSET_CONTENT
</svg>
</body></html>
WRAP_EOF
ABS_WRAPPER="$(realpath /tmp/stix-validate-${ASSET_NAME}.html)"
agent-browser open "file://$ABS_WRAPPER"
sleep 0.5
agent-browser screenshot "$(realpath .stix/qc)/asset-${ASSET_NAME}.png"
agent-browser close
doneVisually inspect each screenshot. If an asset is malformed (missing body parts, wrong proportions, empty SVG), regenerate it by re-running the relevant worker from Step 4 with additional fix instructions. Max 2 retries per asset.
---
Step 6: Scene Composition (Parallel Workers)
Scene composers read the pre-built assets and compose them into animated HTML scenes. Their job is ONLY positioning + keyframes + timing — not character/prop design.
Read the scene count:
TOTAL_SCENES=$(python3 -c "import json; print(len(json.load(open('.stix/story.json'))['scenes']))")Read reference template:
COMPOSE_REF="$(cat "$SKILL_DIR/references/scene-compose.md")"
BASE_TEMPLATE="$(cat "$SKILL_DIR/references/scene-base.html")"
TRANSITIONS="$(cat "$SKILL_DIR/library/transitions.md")"For each scene N, build the composer prompt:
SCENE_DESC=$(python3 -c "
import json
story = json.load(open('.stix/story.json'))
scene = story['scenes'][$((N-1))]
print(json.dumps(scene, indent=2))
")
# List asset files this scene needs
ASSET_FILES=""
for CHAR_ID in $(python3 -c "
import json
story = json.load(open('.stix/story.json'))
scene = story['scenes'][$((N-1))]
for cid in scene['characters']:
print(cid)
"); do
ASSET_FILES="$ASSET_FILES
- .stix/assets/characters/${CHAR_ID}.svg"
done
ASSET_FILES="$ASSET_FILES
- .stix/assets/background.svg"
for PROP_ID in $(python3 -c "
import json
story = json.load(open('.stix/story.json'))
scene = story['scenes'][$((N-1))]
for p in scene.get('props_used', []):
print(p)
"); do
ASSET_FILES="$ASSET_FILES
- .stix/assets/props/${PROP_ID}.svg"
done
cat > /tmp/stix-compose-$(printf "%02d" $N)-prompt.txt << PROMPT_EOF
You are composing an animation scene by positioning pre-built SVG assets and adding CSS keyframes.
## Scene Specification
$SCENE_DESC
## Composition Instructions
$COMPOSE_REF
## Base HTML Template
$BASE_TEMPLATE
## Transition Library
$TRANSITIONS
## Asset Files to Read
Read these SVG files and embed them in the scene:
$ASSET_FILES
## Rules
1. COPY asset SVG groups exactly as-is — do NOT modify SVG paths or shapes
2. Position each asset via \`<g transform="translate(x, y)">\` wrappers
3. Add CSS @keyframes for movement, limb animation, expression toggling
4. All scene animations: 8s duration, CSS-only, infinite loop
5. Sub-loops (walk cycle 0.5s, run cycle 0.4s) nest inside the 8s scene loop
6. Background is placed once, identical across scenes
7. Use start/end coordinates from the scene spec for translate animations
8. Toggle expressions via display:none/block at keyframe percentages
## Output
Write ONLY the complete HTML file to: .stix/scenes/scene-$(printf "%02d" $N).html
No explanation, no markdown — just the HTML file.
PROMPT_EOF
# Build env unset command
ENV_CMD="env"
if [ -n "$STIX_AGENT_ENV_UNSET" ]; then
IFS=',' read -ra UNSET_VARS <<< "$STIX_AGENT_ENV_UNSET"
for var in "${UNSET_VARS[@]}"; do
[ -n "$var" ] && ENV_CMD="$ENV_CMD -u $var"
done
fi
$ENV_CMD $STIX_AGENT_CLI \
"$(cat /tmp/stix-compose-$(printf "%02d" $N)-prompt.txt)" &After spawning all workers, wait for completion:
waitVerify all scene files exist:
for N in $(seq 1 $TOTAL_SCENES); do
FILE=".stix/scenes/scene-$(printf "%02d" $N).html"
if [ ! -f "$FILE" ]; then
echo "Missing: $FILE"
exit 1
fi
echo "scene: $FILE"
done---
Step 7: Visual QC
For each scene, capture keyframes using agent-browser eval for frame-accurate positioning and run a review worker.
Per scene:
1. Capture 4 keyframes using eval-based seeking:
SCENE_FILE=".stix/scenes/scene-$(printf "%02d" $N).html"
ABS_PATH="$(realpath "$SCENE_FILE")"
agent-browser open "file://$ABS_PATH"
sleep 0.5
# Pause all CSS animations
agent-browser eval "document.getAnimations().forEach(a => a.pause())"
# Seek to 0%
agent-browser eval "document.getAnimations().forEach(a => { a.currentTime = 0 })"
agent-browser screenshot "$(realpath .stix/qc)/scene-${N}-kf0.png"
# Seek to 33% (2640ms of 8000ms)
agent-browser eval "document.getAnimations().forEach(a => { a.currentTime = 2640 })"
agent-browser screenshot "$(realpath .stix/qc)/scene-${N}-kf33.png"
# Seek to 66% (5280ms)
agent-browser eval "document.getAnimations().forEach(a => { a.currentTime = 5280 })"
agent-browser screenshot "$(realpath .stix/qc)/scene-${N}-kf66.png"
# Seek to 100% (8000ms)
agent-browser eval "document.getAnimations().forEach(a => { a.currentTime = 8000 })"
agent-browser screenshot "$(realpath .stix/qc)/scene-${N}-kf100.png"
agent-browser close2. Spawn a QC reviewer (use a fast model — review-tier is sufficient):
QC_RUBRIC="$(cat "$SKILL_DIR/references/qc-rubric.md")"
SCENE_DESC=$(python3 -c "
import json
story = json.load(open('.stix/story.json'))
scene = story['scenes'][$((N-1))]
print(json.dumps(scene, indent=2))
")
cat > /tmp/stix-qc-$(printf "%02d" $N)-prompt.txt << QC_EOF
Review these 4 keyframe screenshots of a stick figure animation scene against the QC rubric.
Screenshots:
- .stix/qc/scene-${N}-kf0.png (0% - start)
- .stix/qc/scene-${N}-kf33.png (33%)
- .stix/qc/scene-${N}-kf66.png (66%)
- .stix/qc/scene-${N}-kf100.png (100% - end)
QC Rubric:
$QC_RUBRIC
Scene description:
$SCENE_DESC
Evaluate each category: Animation Correctness, Scene Composition, Visual Quality.
Note: Character and prop integrity was already validated at the asset step. Focus on
whether the COMPOSITION and ANIMATION are correct — positioning, movement, timing,
expression changes, and visual quality.
Output a JSON object:
{
"scene": $N,
"overall": "PASS" or "FAIL",
"categories": {
"animation_correctness": { "result": "PASS/FAIL", "notes": "..." },
"scene_composition": { "result": "PASS/FAIL", "notes": "..." },
"visual_quality": { "result": "PASS/FAIL", "notes": "..." }
},
"fix_instructions": ["list of specific fixes needed"]
}
Write the result to: .stix/qc/scene-${N}-result.json
QC_EOF
ENV_CMD="env"
if [ -n "$STIX_AGENT_ENV_UNSET" ]; then
IFS=',' read -ra UNSET_VARS <<< "$STIX_AGENT_ENV_UNSET"
for var in "${UNSET_VARS[@]}"; do
[ -n "$var" ] && ENV_CMD="$ENV_CMD -u $var"
done
fi
$ENV_CMD $STIX_AGENT_CLI \
"$(cat /tmp/stix-qc-$(printf "%02d" $N)-prompt.txt)"3. If FAIL, fix and re-review (max 3 iterations):
ITERATION=0
MAX_ITERATIONS=3
while [ $ITERATION -lt $MAX_ITERATIONS ]; do
QC_RESULT=$(cat ".stix/qc/scene-${N}-result.json" 2>/dev/null)
if echo "$QC_RESULT" | grep -q '"overall": "PASS"'; then
echo "Scene $N passed QC"
break
fi
ITERATION=$((ITERATION + 1))
echo "Scene $N failed QC (attempt $ITERATION/$MAX_ITERATIONS). Fixing..."
CURRENT_HTML="$(cat ".stix/scenes/scene-$(printf "%02d" $N).html")"
cat > /tmp/stix-fix-$(printf "%02d" $N)-prompt.txt << FIX_EOF
Fix this stick figure animation scene based on QC feedback.
IMPORTANT: Do NOT modify the SVG asset shapes — only fix positioning, keyframes, and timing.
Current HTML:
$CURRENT_HTML
QC Result:
$QC_RESULT
## Transition Library
$TRANSITIONS
Fix the issues listed in fix_instructions. Write the corrected HTML to:
.stix/scenes/scene-$(printf "%02d" $N).html
FIX_EOF
ENV_CMD="env"
if [ -n "$STIX_AGENT_ENV_UNSET" ]; then
IFS=',' read -ra UNSET_VARS <<< "$STIX_AGENT_ENV_UNSET"
for var in "${UNSET_VARS[@]}"; do
[ -n "$var" ] && ENV_CMD="$ENV_CMD -u $var"
done
fi
$ENV_CMD $STIX_AGENT_CLI \
"$(cat /tmp/stix-fix-$(printf "%02d" $N)-prompt.txt)"
# Re-capture keyframes and re-review (repeat eval-based capture + QC from above)
done4. Continuity check (multi-scene only):
For scenes 2+, compare the last frame of the previous scene with the first frame of the current scene:
PREV=$((N - 1))
cat > /tmp/stix-continuity-prompt.txt << CONT_EOF
Compare these two screenshots for continuity:
1. .stix/qc/scene-${PREV}-kf100.png (end of scene $PREV)
2. .stix/qc/scene-${N}-kf0.png (start of scene $N)
Check:
- Character position consistency
- Pose continuity
- Prop consistency
- Emotional progression
Output: PASS or FAIL with specific fix notes.
Write to: .stix/qc/continuity-${PREV}-${N}.json
CONT_EOF
ENV_CMD="env"
if [ -n "$STIX_AGENT_ENV_UNSET" ]; then
IFS=',' read -ra UNSET_VARS <<< "$STIX_AGENT_ENV_UNSET"
for var in "${UNSET_VARS[@]}"; do
[ -n "$var" ] && ENV_CMD="$ENV_CMD -u $var"
done
fi
$ENV_CMD $STIX_AGENT_CLI \
"$(cat /tmp/stix-continuity-prompt.txt)"---
Step 8: Frame Capture & Stitch
Run the capture script with inferred parameters. The capture script uses agent-browser eval to pause CSS animations and seek to exact millisecond positions — no timing drift.
SKILL_DIR="$(dirname "$(readlink -f "$0")" 2>/dev/null || cd "$(dirname "$0")" && pwd)"
bash "$SKILL_DIR/scripts/capture.sh" \
--scenes-dir .stix/scenes \
--frames-dir .stix/frames \
--output-dir .stix/output \
--fps $FPS \
--format $FORMAT---
Step 9: Report
After the pipeline completes, report to the user:
Animation complete!
Prompt: "a cat chasing a mouse across a park"
Scenes: 3
Duration: 24s (3 x 8s)
FPS: 8
Assets: 2 characters, 1 background, 2 props
QC: All scenes passed (1 required fix)
Output:
GIF: .stix/output/animation.gif (270KB)
MP4: .stix/output/animation.mp4 (90KB)---
Component Library Reference
When generating assets, the dispatcher must read these files and inject their full contents into worker prompts:
| File | Used by |
|---|---|
library/style-guide.md | All asset workers + scene composers |
library/poses.md | Character asset workers |
library/transitions.md | Scene composition workers |
library/expressions.md | Character asset workers |
library/props.md | Background + prop asset workers |
library/animals.md | Character asset workers (for animal characters) |
Asset Reference Templates
| File | Purpose |
|---|---|
references/asset-character.md | Template + instructions for character asset workers |
references/asset-background.md | Template + instructions for background asset workers |
references/asset-props.md | Template + instructions for prop asset workers |
references/scene-compose.md | Instructions for scene composition workers |
references/scene-base.html | Base HTML template for composed scenes |
references/qc-rubric.md | QC evaluation rubric |
Key Conventions
1. Asset-first pipeline — characters, background, and props are generated as reusable SVG assets before scene composition. This guarantees visual consistency across scenes. 2. CSS-only animation — no JavaScript in scene HTML. Deterministic timing, no race conditions. 3. 8s scene duration — all animation-duration: 8s with infinite loop. 4. Relative coordinates — characters defined at origin, positioned via translate(). 5. Phase-based scenes — for multi-moment scenes, use opacity toggling (see transitions.md → phase_transition). 6. Sub-loops — walk cycle (0.5s), run cycle (0.4s) nest inside the 8s scene loop. 7. Eval-based capture — agent-browser eval pauses animations and seeks to exact millisecond positions. No sleep-based timing drift. 8. Generation tier for creation, review tier for QC — use your agent's most capable model for asset/scene generation, the fastest capable model for QC review. 9. Max 3 QC fix iterations — if still failing after 3, report the remaining issues to the user.
node_modules/
.doodle/
*.tgz
.git
.github
.dispatch
*.test.*
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
SKILL_NAME="stix"
echo "🎨 Installing stix skill..."
# Detect available agents and their skill directories
INSTALLED=0
# Claude Code
if [ -d "$HOME/.claude" ] || command -v claude >/dev/null 2>&1; then
CLAUDE_DIR="$HOME/.claude/skills/$SKILL_NAME"
mkdir -p "$(dirname "$CLAUDE_DIR")"
ln -sfn "$SCRIPT_DIR" "$CLAUDE_DIR"
echo " ✓ Linked to Claude Code: $CLAUDE_DIR"
INSTALLED=$((INSTALLED + 1))
fi
# Codex / OpenAI
if [ -d "$HOME/.codex" ] || command -v codex >/dev/null 2>&1; then
CODEX_DIR="$HOME/.codex/skills/$SKILL_NAME"
mkdir -p "$(dirname "$CODEX_DIR")"
ln -sfn "$SCRIPT_DIR" "$CODEX_DIR"
echo " ✓ Linked to Codex: $CODEX_DIR"
INSTALLED=$((INSTALLED + 1))
fi
# Generic agents (.agents/skills/)
AGENTS_DIR="$HOME/.agents/skills/$SKILL_NAME"
mkdir -p "$(dirname "$AGENTS_DIR")"
ln -sfn "$SCRIPT_DIR" "$AGENTS_DIR"
echo " ✓ Linked to generic agents: $AGENTS_DIR"
INSTALLED=$((INSTALLED + 1))
echo ""
echo "✅ stix installed! ($INSTALLED agent location(s))"
echo ""
echo "Usage: /stix \"a cat chasing a mouse across a park\""
echo ""
echo "Dependencies: agent-browser, ffmpeg"
echo "Run 'stix-skill --check' to verify."
#!/usr/bin/env node
const { execSync } = require('child_process');
const path = require('path');
const installScript = path.join(__dirname, 'install.sh');
try {
execSync(`bash "${installScript}"`, { stdio: 'inherit' });
} catch (e) {
console.warn('⚠ Skill linking skipped (this is normal during npx one-shot installs)');
}
Stick Figure Animals Library
All animals defined relative to hip/body center at (0, 0). Position on stage via transform="translate(X, Y)".
Animals use the same line style as characters (stroke: #2a2a2a, stroke-linecap: round, stroke-linejoin: round, fill: none) with scaled stroke widths per animal size (cat/dog 2, bird 1.8, mouse 1.5).
---
cat
Scale: 60% of human height (~76px tall). Pointed ears, whiskers, curved tail.
<g class="cat" stroke="#2a2a2a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none">
<!-- Body (horizontal, quadruped) -->
<line x1="-20" y1="0" x2="20" y2="0" />
<!-- Head -->
<circle cx="-28" cy="-8" r="10" />
<!-- Pointed ears -->
<path d="M -35 -15 L -33 -25 L -28 -16" />
<path d="M -23 -15 L -21 -25 L -26 -16" />
<!-- Eyes -->
<circle cx="-31" cy="-10" r="1.5" fill="#2a2a2a" />
<circle cx="-25" cy="-10" r="1.5" fill="#2a2a2a" />
<!-- Whiskers -->
<line x1="-38" y1="-8" x2="-48" y2="-12" stroke-width="1" />
<line x1="-38" y1="-6" x2="-48" y2="-6" stroke-width="1" />
<line x1="-38" y1="-4" x2="-48" y2="0" stroke-width="1" />
<!-- Nose -->
<circle cx="-38" cy="-7" r="1" fill="#2a2a2a" />
<!-- Front legs -->
<line x1="-15" y1="0" x2="-15" y2="20" class="cat-front-l" />
<line x1="-10" y1="0" x2="-10" y2="20" class="cat-front-r" />
<!-- Back legs -->
<line x1="15" y1="0" x2="15" y2="20" class="cat-back-l" />
<line x1="20" y1="0" x2="20" y2="20" class="cat-back-r" />
<!-- Tail (curved upward) -->
<path d="M 20 0 Q 35 -5 30 -20" class="cat-tail" />
</g>Cat Walk Cycle
.cat-front-l { animation: catFrontL 0.4s ease-in-out infinite; }
@keyframes catFrontL {
0%, 100% { d: path("M -15 0 L -18 20"); }
50% { d: path("M -15 0 L -12 20"); }
}
.cat-front-r { animation: catFrontR 0.4s ease-in-out infinite; }
@keyframes catFrontR {
0%, 100% { d: path("M -10 0 L -7 20"); }
50% { d: path("M -10 0 L -13 20"); }
}
.cat-back-l { animation: catBackL 0.4s ease-in-out infinite; }
@keyframes catBackL {
0%, 100% { d: path("M 15 0 L 18 20"); }
50% { d: path("M 15 0 L 12 20"); }
}
.cat-back-r { animation: catBackR 0.4s ease-in-out infinite; }
@keyframes catBackR {
0%, 100% { d: path("M 20 0 L 17 20"); }
50% { d: path("M 20 0 L 23 20"); }
}Tail Wag
.cat-tail { animation: catTailWag 1s ease-in-out infinite; }
@keyframes catTailWag {
0%, 100% { d: path("M 20 0 Q 35 -5 30 -20"); }
50% { d: path("M 20 0 Q 35 -10 28 -22"); }
}---
dog
Scale: 70% of human height (~88px tall). Floppy ears, wagging tail, longer body.
<g class="dog" stroke="#2a2a2a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none">
<!-- Body -->
<line x1="-25" y1="0" x2="25" y2="0" />
<!-- Head -->
<circle cx="-33" cy="-8" r="12" />
<!-- Floppy ears -->
<path d="M -42 -14 Q -48 -8 -44 0" />
<path d="M -26 -14 Q -20 -8 -24 0" />
<!-- Eyes -->
<circle cx="-37" cy="-10" r="1.8" fill="#2a2a2a" />
<circle cx="-29" cy="-10" r="1.8" fill="#2a2a2a" />
<!-- Nose -->
<circle cx="-43" cy="-6" r="2" fill="#2a2a2a" />
<!-- Tongue (optional, happy dog) -->
<path d="M -43 -3 L -44 4" stroke="#e85c5c" stroke-width="1.5" />
<!-- Front legs -->
<line x1="-18" y1="0" x2="-18" y2="25" />
<line x1="-12" y1="0" x2="-12" y2="25" />
<!-- Back legs -->
<path d="M 18 0 L 15 12 L 18 25" />
<path d="M 23 0 L 20 12 L 23 25" />
<!-- Tail (wagging) -->
<path d="M 25 0 Q 38 -15 32 -25" class="dog-tail" />
</g>Tail Wag (fast, happy)
.dog-tail { animation: dogTailWag 0.3s ease-in-out infinite; }
@keyframes dogTailWag {
0%, 100% { d: path("M 25 0 Q 38 -15 32 -25"); }
50% { d: path("M 25 0 Q 40 -8 38 -20"); }
}---
bird
Scale: 30% of human height (~38px). Wing arcs, small beak.
<g class="bird" stroke="#2a2a2a" stroke-width="1.8" stroke-linecap="round" fill="none">
<!-- Body -->
<ellipse cx="0" cy="0" rx="10" ry="6" />
<!-- Head -->
<circle cx="-12" cy="-4" r="5" />
<!-- Eye -->
<circle cx="-14" cy="-5" r="1" fill="#2a2a2a" />
<!-- Beak -->
<path d="M -17 -4 L -22 -3 L -17 -2" fill="#e8a85c" stroke="#e8a85c" stroke-width="1" />
<!-- Wing -->
<path d="M -2 -4 Q 8 -18 18 -8" stroke-width="2" class="bird-wing" />
<!-- Tail feathers -->
<path d="M 10 0 L 18 -4" />
<path d="M 10 0 L 18 2" />
<!-- Legs -->
<line x1="-3" y1="6" x2="-3" y2="12" />
<line x1="3" y1="6" x2="3" y2="12" />
<!-- Feet -->
<path d="M -5 12 L -1 12" stroke-width="1" />
<path d="M 1 12 L 5 12" stroke-width="1" />
</g>Wing Flap
.bird-wing { animation: wingFlap 0.3s ease-in-out infinite; }
@keyframes wingFlap {
0%, 100% { d: path("M -2 -4 Q 8 -18 18 -8"); }
50% { d: path("M -2 -4 Q 8 -2 18 -6"); }
}Flying (translate + flap)
.bird-fly {
animation: birdFly 8s linear infinite;
}
@keyframes birdFly {
0% { transform: translate(-100px, 50px); }
100% { transform: translate(900px, -30px); }
}---
mouse
Scale: 25% of human height (~32px). Round ears, long thin tail.
<g class="mouse" stroke="#2a2a2a" stroke-width="1.5" stroke-linecap="round" fill="none">
<!-- Body -->
<ellipse cx="0" cy="0" rx="8" ry="5" />
<!-- Head -->
<circle cx="-10" cy="-2" r="4.5" />
<!-- Round ears -->
<circle cx="-14" cy="-7" r="3" />
<circle cx="-7" cy="-7" r="3" />
<!-- Eye -->
<circle cx="-12" cy="-3" r="0.8" fill="#2a2a2a" />
<!-- Nose -->
<circle cx="-14.5" cy="-1" r="0.6" fill="#2a2a2a" />
<!-- Whiskers -->
<line x1="-15" y1="-2" x2="-22" y2="-4" stroke-width="0.8" />
<line x1="-15" y1="-1" x2="-22" y2="-1" stroke-width="0.8" />
<line x1="-15" y1="0" x2="-22" y2="2" stroke-width="0.8" />
<!-- Legs (tiny) -->
<line x1="-4" y1="5" x2="-4" y2="9" />
<line x1="0" y1="5" x2="0" y2="9" />
<line x1="4" y1="5" x2="4" y2="9" />
<!-- Tail (long, curving) -->
<path d="M 8 0 Q 20 -5 25 5 Q 30 12 35 8" />
</g>Mouse Scurry
.mouse-scurry {
animation: mouseScurry 0.2s linear infinite;
}
@keyframes mouseScurry {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-2px); }
}
/* Combine with horizontal movement */
.mouse-run {
animation: mouseRun 8s linear infinite;
}
@keyframes mouseRun {
0% { transform: translateX(-50px); }
100% { transform: translateX(850px); }
}Facial Expressions Library
All expressions defined relative to head center at (0, 0). Wrap in <g transform="translate(HEAD_CX, HEAD_CY)"> to position on stage. Default head center is at (0, -60) relative to hip.
The head circle is at (0, 0) with radius 18 (male) or 16 (female).
Eyes are at (-6, -3) and (6, -3). Mouth baseline is at y=5.
---
happy
Upward-curving smile arc. Eyes at normal size.
<!-- Eyes -->
<circle cx="-6" cy="-3" r="2" class="char-eyes" />
<circle cx="6" cy="-3" r="2" class="char-eyes" />
<!-- Mouth: smile arc -->
<path d="M -8 5 Q 0 15 8 5" class="char-mouth" />sad
Downward-curving frown. Eyebrows angled inward-up.
<!-- Eyes -->
<circle cx="-6" cy="-3" r="2" class="char-eyes" />
<circle cx="6" cy="-3" r="2" class="char-eyes" />
<!-- Sad eyebrows (angled inward-up) -->
<line x1="-9" y1="-7" x2="-3" y2="-6" stroke="#2a2a2a" stroke-width="1.8" stroke-linecap="round" />
<line x1="9" y1="-7" x2="3" y2="-6" stroke="#2a2a2a" stroke-width="1.8" stroke-linecap="round" />
<!-- Mouth: frown arc -->
<path d="M -8 7 Q 0 0 8 7" class="char-mouth" />neutral
Flat horizontal line mouth. Default resting face.
<!-- Eyes -->
<circle cx="-6" cy="-3" r="2" class="char-eyes" />
<circle cx="6" cy="-3" r="2" class="char-eyes" />
<!-- Mouth: flat line -->
<line x1="-5" y1="5" x2="5" y2="5" class="char-mouth" />surprised
Mouth is a small open circle. Eyes wider (larger radius).
<!-- Eyes (wider) -->
<circle cx="-6" cy="-3" r="2.8" class="char-eyes" />
<circle cx="6" cy="-3" r="2.8" class="char-eyes" />
<!-- Raised eyebrows -->
<line x1="-9" y1="-9" x2="-3" y2="-8" stroke="#2a2a2a" stroke-width="1.8" stroke-linecap="round" />
<line x1="9" y1="-9" x2="3" y2="-8" stroke="#2a2a2a" stroke-width="1.8" stroke-linecap="round" />
<!-- Mouth: open O -->
<circle cx="0" cy="7" r="4" class="char-mouth" />determined
Flat pressed mouth, slight furrowed brows. Resolute face.
<!-- Eyes -->
<circle cx="-6" cy="-3" r="2" class="char-eyes" />
<circle cx="6" cy="-3" r="2" class="char-eyes" />
<!-- Furrowed brows (angled inward-down) -->
<line x1="-9" y1="-6" x2="-3" y2="-7" stroke="#2a2a2a" stroke-width="1.8" stroke-linecap="round" />
<line x1="9" y1="-6" x2="3" y2="-7" stroke="#2a2a2a" stroke-width="1.8" stroke-linecap="round" />
<!-- Mouth: pressed flat line -->
<line x1="-5" y1="5" x2="5" y2="5" stroke="#2a2a2a" stroke-width="2.4" stroke-linecap="round" />angry
V-shaped brows angled down, tight frown.
<!-- Eyes -->
<circle cx="-6" cy="-3" r="2" class="char-eyes" />
<circle cx="6" cy="-3" r="2" class="char-eyes" />
<!-- Angry brows (steep V, angled inward-down) -->
<line x1="-10" y1="-5" x2="-3" y2="-8" stroke="#2a2a2a" stroke-width="2" stroke-linecap="round" />
<line x1="10" y1="-5" x2="3" y2="-8" stroke="#2a2a2a" stroke-width="2" stroke-linecap="round" />
<!-- Mouth: tight frown -->
<path d="M -6 6 Q 0 2 6 6" class="char-mouth" />laughing
Wide open mouth arc, eyes squinted (reduced to arcs).
<!-- Eyes (squinted — arcs instead of circles) -->
<path d="M -8 -3 Q -6 -5 -4 -3" stroke="#2a2a2a" stroke-width="2" stroke-linecap="round" fill="none" />
<path d="M 4 -3 Q 6 -5 8 -3" stroke="#2a2a2a" stroke-width="2" stroke-linecap="round" fill="none" />
<!-- Mouth: wide open smile -->
<path d="M -10 4 Q 0 18 10 4" class="char-mouth" />crying
Tears streaming, sad mouth, eyebrows up.
<!-- Eyes -->
<circle cx="-6" cy="-3" r="2" class="char-eyes" />
<circle cx="6" cy="-3" r="2" class="char-eyes" />
<!-- Sad eyebrows -->
<line x1="-9" y1="-7" x2="-3" y2="-6" stroke="#2a2a2a" stroke-width="1.8" stroke-linecap="round" />
<line x1="9" y1="-7" x2="3" y2="-6" stroke="#2a2a2a" stroke-width="1.8" stroke-linecap="round" />
<!-- Mouth: wobbly frown -->
<path d="M -8 7 Q 0 0 8 7" class="char-mouth" />
<!-- Tear drops -->
<path d="M -6 0 L -7 8" stroke="#8bb8d0" stroke-width="1.5" stroke-linecap="round" />
<path d="M 6 0 L 7 8" stroke="#8bb8d0" stroke-width="1.5" stroke-linecap="round" />smirk
Asymmetric half-smile.
<!-- Eyes -->
<circle cx="-6" cy="-3" r="2" class="char-eyes" />
<circle cx="6" cy="-3" r="2" class="char-eyes" />
<!-- One raised eyebrow -->
<line x1="3" y1="-8" x2="9" y2="-7" stroke="#2a2a2a" stroke-width="1.8" stroke-linecap="round" />
<!-- Mouth: asymmetric smile (one side up) -->
<path d="M -6 5 Q 0 6 6 2" class="char-mouth" />Poses Library
All poses defined with hip at origin (0, 0), shoulder at (0, -37), head center at (0, -60). Wrap in <g transform="translate(HIP_X, HIP_Y)"> to position on stage.
For absolute coordinates, add the hip position. Default hip position: (450, 295).
---
standing
Neutral upright stance, arms relaxed at sides.
<!-- Head -->
<circle cx="0" cy="-60" r="18" class="char-head" />
<circle cx="-6" cy="-63" r="2" class="char-eyes" />
<circle cx="6" cy="-63" r="2" class="char-eyes" />
<!-- Torso -->
<line x1="0" y1="-42" x2="0" y2="0" class="char-body" />
<!-- Arms -->
<path d="M 0 -37 L -18 -17 L -20 0" class="char-body" />
<path d="M 0 -37 L 18 -17 L 20 0" class="char-body" />
<!-- Legs -->
<path d="M 0 0 L -15 35 L -20 53" class="char-body" />
<path d="M 0 0 L 15 35 L 20 53" class="char-body" />walking_a
Left foot forward, right arm forward (first frame of walk cycle). Forward leg reaches ahead with bent knee; back leg pushes off straighter.
<!-- Torso -->
<line x1="0" y1="-42" x2="0" y2="0" class="char-body" />
<!-- Arms -->
<path d="M 0 -37 L -12 -18 L -5 -5" class="char-body" /> <!-- left arm trailing back -->
<path d="M 0 -37 L 15 -22 L 22 -12" class="char-body" /> <!-- right arm swinging forward -->
<!-- Legs -->
<path d="M 0 0 L -18 28 L -22 50" class="char-body" /> <!-- left leg forward (knee bent ahead) -->
<path d="M 0 0 L 12 32 L 8 53" class="char-body" /> <!-- right leg back (pushing off) -->walking_b
Right foot forward, left arm forward (second frame of walk cycle). Mirror stride of walking_a.
<!-- Torso -->
<line x1="0" y1="-42" x2="0" y2="0" class="char-body" />
<!-- Arms -->
<path d="M 0 -37 L 12 -18 L 5 -5" class="char-body" /> <!-- right arm trailing back -->
<path d="M 0 -37 L -15 -22 L -22 -12" class="char-body" /> <!-- left arm swinging forward -->
<!-- Legs -->
<path d="M 0 0 L 18 28 L 22 50" class="char-body" /> <!-- right leg forward (knee bent ahead) -->
<path d="M 0 0 L -12 32 L -8 53" class="char-body" /> <!-- left leg back (pushing off) -->running_a
Extended stride, left foot forward. Arms pumping with tight elbows. Forward leg reaches far out; back leg trails behind pushing off.
<!-- Torso (forward lean) -->
<line x1="0" y1="-42" x2="3" y2="0" class="char-body" />
<!-- Arms (pumping, elbows bent tight) -->
<path d="M 0 -37 L -15 -32 L -10 -22" class="char-body" /> <!-- left arm back -->
<path d="M 0 -37 L 22 -30 L 18 -20" class="char-body" /> <!-- right arm forward -->
<!-- Legs (wide stride) -->
<path d="M 0 0 L -22 22 L -28 45" class="char-body" /> <!-- left leg extended forward -->
<path d="M 0 0 L 15 25 L 22 48" class="char-body" /> <!-- right leg pushing off behind -->running_b
Extended stride, right foot forward. Mirror of running_a.
<!-- Torso (forward lean) -->
<line x1="0" y1="-42" x2="3" y2="0" class="char-body" />
<!-- Arms (opposite pump) -->
<path d="M 0 -37 L 15 -32 L 10 -22" class="char-body" /> <!-- right arm back -->
<path d="M 0 -37 L -22 -30 L -18 -20" class="char-body" /> <!-- left arm forward -->
<!-- Legs (wide stride) -->
<path d="M 0 0 L 22 22 L 28 45" class="char-body" /> <!-- right leg extended forward -->
<path d="M 0 0 L -15 25 L -22 48" class="char-body" /> <!-- left leg pushing off behind -->sitting
Seated on a surface (surface at ~hip level). Legs bent forward.
<!-- Torso -->
<line x1="0" y1="-42" x2="0" y2="0" class="char-body" />
<!-- Arms (resting on lap) -->
<path d="M 0 -37 L -20 -17 L -15 0" class="char-body" />
<path d="M 0 -37 L 20 -17 L 15 0" class="char-body" />
<!-- Legs (bent at knee, feet on ground) -->
<path d="M 0 0 L -13 23 L -20 53" class="char-body" />
<path d="M 0 0 L 13 23 L 20 53" class="char-body" />sitting_slouched
Slouched sitting — torso rotated forward, head drooped. Use transform-origin at shoulder for the slump rotation.
<!-- Torso (angled forward via parent rotation 18deg) -->
<line x1="0" y1="-42" x2="0" y2="0" class="char-body" />
<!-- Arms (dangling) -->
<path d="M 0 -37 L -20 -17 L -15 0" class="char-body" />
<path d="M 0 -37 L 20 -17 L 15 0" class="char-body" />
<!-- Legs -->
<path d="M 0 0 L -13 23 L -20 53" class="char-body" />
<path d="M 0 0 L 13 23 L 20 53" class="char-body" />CSS to animate slump (example using default hip at 450, 295 — shoulder-Y is 258):
.slump-group {
animation: slump 8s ease-in-out infinite;
transform-origin: 450px 258px; /* adjust per character position */
}
@keyframes slump {
0%, 72% { transform: rotate(0deg); }
85%, 100% { transform: rotate(18deg); }
}jumping
Mid-air, legs tucked, arms raised.
<!-- Torso -->
<line x1="0" y1="-42" x2="0" y2="0" class="char-body" />
<!-- Arms (raised up) -->
<path d="M 0 -37 L -18 -55 L -22 -65" class="char-body" />
<path d="M 0 -37 L 18 -55 L 22 -65" class="char-body" />
<!-- Legs (tucked) -->
<path d="M 0 0 L -12 15 L -18 10" class="char-body" />
<path d="M 0 0 L 12 15 L 18 10" class="char-body" />crouching
Low stance, knees deeply bent.
<!-- Torso (shortened — crouch) -->
<line x1="0" y1="-30" x2="0" y2="0" class="char-body" />
<!-- Head (lower) -->
<circle cx="0" cy="-48" r="18" class="char-head" />
<!-- Arms (reaching down) -->
<path d="M 0 -25 L -18 -10 L -22 5" class="char-body" />
<path d="M 0 -25 L 18 -10 L 22 5" class="char-body" />
<!-- Legs (deeply bent) -->
<path d="M 0 0 L -20 10 L -25 30" class="char-body" />
<path d="M 0 0 L 20 10 L 25 30" class="char-body" />waving
Standing with right arm raised in a wave.
<!-- Torso -->
<line x1="0" y1="-42" x2="0" y2="0" class="char-body" />
<!-- Left arm (relaxed) -->
<path d="M 0 -37 L -18 -17 L -20 0" class="char-body" />
<!-- Right arm (waving — raised, hand oscillates) -->
<path d="M 0 -37 L 18 -55 L 28 -60" class="char-body" />
<!-- Legs -->
<path d="M 0 0 L -15 35 L -20 53" class="char-body" />
<path d="M 0 0 L 15 35 L 20 53" class="char-body" />arms_raised
Both arms straight up (celebration / victory).
<!-- Torso -->
<line x1="0" y1="-42" x2="0" y2="0" class="char-body" />
<!-- Arms (straight up) -->
<path d="M 0 -37 L -10 -65 L -8 -80" class="char-body" />
<path d="M 0 -37 L 10 -65 L 8 -80" class="char-body" />
<!-- Legs -->
<path d="M 0 0 L -15 35 L -20 53" class="char-body" />
<path d="M 0 0 L 15 35 L 20 53" class="char-body" />reaching
One arm extended forward to grab something.
<!-- Torso -->
<line x1="0" y1="-42" x2="0" y2="0" class="char-body" />
<!-- Left arm (at side) -->
<path d="M 0 -37 L -18 -17 L -20 0" class="char-body" />
<!-- Right arm (reaching forward) -->
<path d="M 0 -37 L 30 -25 L 45 -20" class="char-body" />
<!-- Legs -->
<path d="M 0 0 L -15 35 L -20 53" class="char-body" />
<path d="M 0 0 L 15 35 L 20 53" class="char-body" />lying_down
Horizontal on the ground. Hip at ground level, body extends left.
<!-- Torso (horizontal) -->
<line x1="0" y1="0" x2="-37" y2="0" class="char-body" />
<!-- Head (at left end) -->
<circle cx="-55" cy="0" r="18" class="char-head" />
<!-- Arms (resting alongside) -->
<path d="M -37 0 L -40 15 L -35 25" class="char-body" />
<path d="M -5 0 L -2 15 L 3 25" class="char-body" />
<!-- Legs (extending right) -->
<path d="M 0 0 L 30 2 L 53 0" class="char-body" />
<path d="M 0 0 L 30 -2 L 53 0" class="char-body" />dancing_a
Arms up and angled, one leg lifted. Party pose.
<!-- Torso -->
<line x1="0" y1="-42" x2="0" y2="0" class="char-body" />
<!-- Arms (angled up, disco style) -->
<path d="M 0 -37 L -25 -55 L -30 -45" class="char-body" />
<path d="M 0 -37 L 25 -50 L 35 -55" class="char-body" />
<!-- Left leg (planted) -->
<path d="M 0 0 L -10 35 L -15 53" class="char-body" />
<!-- Right leg (lifted/kicked) -->
<path d="M 0 0 L 20 20 L 30 15" class="char-body" />dancing_b
Mirror of dancing_a.
<!-- Torso -->
<line x1="0" y1="-42" x2="0" y2="0" class="char-body" />
<!-- Arms (opposite angles) -->
<path d="M 0 -37 L 25 -55 L 30 -45" class="char-body" />
<path d="M 0 -37 L -25 -50 L -35 -55" class="char-body" />
<!-- Right leg (planted) -->
<path d="M 0 0 L 10 35 L 15 53" class="char-body" />
<!-- Left leg (lifted/kicked) -->
<path d="M 0 0 L -20 20 L -30 15" class="char-body" />lifting
Arms extended down gripping a barbell, body slightly bent.
<!-- Torso -->
<line x1="0" y1="-42" x2="0" y2="0" class="char-body" />
<!-- Arms (reaching down to barbell) -->
<path d="M 0 -37 L -25 -23 L -40 -15" class="char-body" />
<path d="M 0 -37 L 25 -23 L 40 -15" class="char-body" />
<!-- Legs (standing firm) -->
<path d="M 0 0 L -15 35 L -20 53" class="char-body" />
<path d="M 0 0 L 15 35 L 20 53" class="char-body" />
<!-- Barbell (attached to hands) -->
<line x1="-45" y1="-13" x2="45" y2="-13" stroke="#888888" stroke-width="3" stroke-linecap="round" />
<circle cx="-45" cy="-13" r="8" fill="none" stroke="#888888" stroke-width="2.4" />
<circle cx="45" cy="-13" r="8" fill="none" stroke="#888888" stroke-width="2.4" />Props Library
Reusable SVG snippets for scene props. All positioned at a reference origin — use transform="translate(X, Y)" to place on stage.
---
bench
Park bench. Width ~160px, seat at y=0 reference.
<g class="bench" stroke="#2a2a2a" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" fill="none">
<!-- Seat -->
<line x1="-80" y1="0" x2="80" y2="0" />
<!-- Back rest -->
<line x1="-75" y1="-35" x2="-75" y2="0" />
<line x1="-75" y1="-35" x2="75" y2="-35" />
<line x1="75" y1="-35" x2="75" y2="0" />
<!-- Back slats -->
<line x1="-20" y1="-33" x2="-20" y2="-2" />
<line x1="10" y1="-33" x2="10" y2="-2" />
<line x1="40" y1="-33" x2="40" y2="-2" />
<!-- Legs -->
<line x1="-65" y1="0" x2="-70" y2="40" />
<line x1="65" y1="0" x2="70" y2="40" />
<!-- Arm rests -->
<line x1="-80" y1="-15" x2="-85" y2="0" />
<line x1="80" y1="-15" x2="85" y2="0" />
</g>table
Simple table. Width ~160px, surface at y=0.
<g class="table">
<!-- Surface -->
<line x1="-80" y1="0" x2="80" y2="0" stroke="#d4d0c8" stroke-width="2" stroke-linecap="round" />
<!-- Legs -->
<line x1="-70" y1="0" x2="-70" y2="60" stroke="#d4d0c8" stroke-width="2" stroke-linecap="round" />
<line x1="70" y1="0" x2="70" y2="60" stroke="#d4d0c8" stroke-width="2" stroke-linecap="round" />
</g>heart
Whole heart. Center at (0, 0), ~32px wide.
<path d="M 0 -12 C -8 -20, -16 -12, -16 -4 C -16 4, 0 12, 0 12 C 0 12, 16 4, 16 -4 C 16 -12, 8 -20, 0 -12 Z" fill="#e85c5c" />broken_heart
Heart split in two halves with crack.
<g class="broken-heart">
<!-- Left half -->
<path d="M 0 -12 C -8 -20, -16 -12, -16 -4 C -16 4, 0 12, 0 12 L 0 -12 Z" fill="#e85c5c" />
<!-- Right half (offset slightly) -->
<path d="M 3 -12 C 11 -20, 19 -12, 19 -4 C 19 4, 3 12, 3 12 L 3 -12 Z" fill="#e85c5c" />
<!-- Crack line -->
<path d="M 0 -10 L 2 -5 L -1 0 L 2 5 L 0 10" stroke="#faf9f6" stroke-width="1.5" fill="none" stroke-linecap="round" />
</g>barbell
Horizontal bar with weights. Width ~90px.
<g class="barbell">
<line x1="-45" y1="0" x2="45" y2="0" stroke="#888888" stroke-width="3" stroke-linecap="round" />
<circle cx="-45" cy="0" r="8" fill="none" stroke="#888888" stroke-width="2.4" />
<circle cx="45" cy="0" r="8" fill="none" stroke="#888888" stroke-width="2.4" />
</g>sun
Sun with rays. Center at (0, 0).
<g class="sun">
<circle cx="0" cy="0" r="26" fill="#f0c040" />
<g stroke="#f0c040" stroke-width="2.5" stroke-linecap="round">
<line x1="0" y1="-38" x2="0" y2="-50" />
<line x1="0" y1="38" x2="0" y2="50" />
<line x1="-38" y1="0" x2="-50" y2="0" />
<line x1="38" y1="0" x2="50" y2="0" />
<line x1="-27" y1="-27" x2="-35" y2="-35" />
<line x1="27" y1="-27" x2="35" y2="-35" />
<line x1="-27" y1="27" x2="-35" y2="35" />
<line x1="27" y1="27" x2="35" y2="35" />
</g>
</g>Optional glow layer:
<circle cx="0" cy="0" r="45" fill="#f0c040" opacity="0.15" />cloud
Fluffy cloud. Width ~60px.
<g class="cloud" opacity="0.5">
<circle cx="-12" cy="0" r="12" fill="#8bb8d0" />
<circle cx="2" cy="-4" r="14" fill="#8bb8d0" />
<circle cx="16" cy="1" r="11" fill="#8bb8d0" />
<circle cx="-5" cy="6" r="10" fill="#8bb8d0" />
<circle cx="10" cy="6" r="10" fill="#8bb8d0" />
</g>rain_cloud
Cloud with rain streaks.
<g class="rain-cloud">
<!-- Cloud body -->
<circle cx="-12" cy="0" r="12" fill="#8bb8d0" opacity="0.5" />
<circle cx="2" cy="-4" r="14" fill="#8bb8d0" opacity="0.5" />
<circle cx="16" cy="1" r="11" fill="#8bb8d0" opacity="0.5" />
<circle cx="-5" cy="6" r="10" fill="#8bb8d0" opacity="0.45" />
<circle cx="10" cy="6" r="10" fill="#8bb8d0" opacity="0.45" />
<!-- Rain drops -->
<line x1="-10" y1="14" x2="-12" y2="22" stroke="#8bb8d0" stroke-width="1.8" stroke-linecap="round" />
<line x1="0" y1="16" x2="-2" y2="24" stroke="#8bb8d0" stroke-width="1.8" stroke-linecap="round" />
<line x1="10" y1="14" x2="8" y2="22" stroke="#8bb8d0" stroke-width="1.8" stroke-linecap="round" />
<line x1="-5" y1="18" x2="-7" y2="26" stroke="#8bb8d0" stroke-width="1.8" stroke-linecap="round" />
<line x1="5" y1="17" x2="3" y2="25" stroke="#8bb8d0" stroke-width="1.8" stroke-linecap="round" />
</g>tree
Simple tree. Trunk from ground, canopy of overlapping circles.
<g class="tree" stroke="#2a2a2a" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.25">
<line x1="0" y1="0" x2="0" y2="-65" />
<circle cx="0" cy="-80" r="20" />
<circle cx="-15" cy="-70" r="14" />
<circle cx="15" cy="-70" r="14" />
</g>Place with transform="translate(X, GROUND_Y)" where GROUND_Y is 355.
pizza_slice
Single pizza slice. Triangle with toppings.
<g class="pizza-slice">
<path d="M 0 -10 L 7 0 L -7 0 Z" fill="#e8a85c" stroke="#2a2a2a" stroke-width="1.2" stroke-linejoin="round" />
<circle cx="1" cy="-3" r="1.2" fill="#e85c5c" />
<circle cx="-2" cy="-2" r="1" fill="#7a9a3a" />
</g>pizza_box
Closed pizza box.
<g class="pizza-box">
<rect x="-17" y="-3" width="35" height="6" rx="1" fill="none" stroke="#2a2a2a" stroke-width="1.8" />
<line x1="-17" y1="0" x2="18" y2="0" stroke="#e8a85c" stroke-width="0.8" opacity="0.5" />
</g>salad_bowl
Bowl with leafy greens.
<g class="salad-bowl">
<path d="M -15 0 Q -15 -12 0 -15 Q 15 -12 15 0 Z" fill="#7ec87e" fill-opacity="0.3" stroke="#7ec87e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<path d="M -7 -6 Q -3 -12 1 -6" fill="none" stroke="#5aa55a" stroke-width="1.5" stroke-linecap="round" />
<path d="M -2 -4 Q 3 -10 7 -4" fill="none" stroke="#5aa55a" stroke-width="1.5" stroke-linecap="round" />
<path d="M -10 -3 Q -6 -8 -2 -2" fill="none" stroke="#5aa55a" stroke-width="1.5" stroke-linecap="round" />
<path d="M -17 0 L 17 0" stroke="#7ec87e" stroke-width="2" stroke-linecap="round" />
</g>coffee_mug
Simple mug with steam.
<g class="coffee-mug">
<rect x="-8" y="-12" width="16" height="16" rx="2" fill="none" stroke="#2a2a2a" stroke-width="2" />
<!-- Handle -->
<path d="M 8 -8 Q 16 -4 8 0" fill="none" stroke="#2a2a2a" stroke-width="2" stroke-linecap="round" />
<!-- Steam wisps -->
<path d="M -3 -14 Q -5 -22 -2 -28" fill="none" stroke="#d4d0c8" stroke-width="1.2" stroke-linecap="round" />
<path d="M 3 -15 Q 5 -23 2 -30" fill="none" stroke="#d4d0c8" stroke-width="1.2" stroke-linecap="round" />
</g>bicycle
Simple stick bicycle.
<g class="bicycle" stroke="#2a2a2a" stroke-width="2" stroke-linecap="round" fill="none">
<!-- Wheels -->
<circle cx="-20" cy="0" r="15" />
<circle cx="20" cy="0" r="15" />
<!-- Frame -->
<line x1="-20" y1="0" x2="0" y2="-20" />
<line x1="0" y1="-20" x2="20" y2="0" />
<line x1="0" y1="-20" x2="-10" y2="-25" />
<line x1="0" y1="-20" x2="5" y2="-30" />
<!-- Handlebars -->
<line x1="2" y1="-32" x2="10" y2="-30" />
<!-- Seat -->
<line x1="-12" y1="-27" x2="-6" y2="-25" />
</g>speech_bubble
Speech bubble with tail pointing down-left.
<g class="speech-bubble">
<ellipse cx="0" cy="-20" rx="38" ry="18" fill="#faf9f6" stroke="#2a2a2a" stroke-width="1.8" />
<!-- Tail -->
<path d="M -10 -4 L -18 10 L 0 -6" fill="#faf9f6" stroke="#2a2a2a" stroke-width="1.5" />
<!-- Fill overlap to clean interior -->
<ellipse cx="0" cy="-20" rx="30" ry="12" fill="#faf9f6" stroke="none" />
</g>Add text inside: <text x="0" y="-18" text-anchor="middle" font-size="10" fill="#2a2a2a">Hello!</text>
thought_bubble
Thought bubble with small leading circles.
<g class="thought-bubble">
<!-- Leading dots -->
<circle cx="-20" cy="10" r="3" fill="none" stroke="#2a2a2a" stroke-width="1.2" />
<circle cx="-28" cy="0" r="4.5" fill="none" stroke="#2a2a2a" stroke-width="1.2" />
<!-- Main cloud -->
<ellipse cx="0" cy="-25" rx="38" ry="22" fill="#faf9f6" stroke="#2a2a2a" stroke-width="1.8" />
<circle cx="-20" cy="-30" r="12" fill="#faf9f6" stroke="#2a2a2a" stroke-width="1.5" />
<circle cx="20" cy="-33" r="13" fill="#faf9f6" stroke="#2a2a2a" stroke-width="1.5" />
<circle cx="0" cy="-38" r="11" fill="#faf9f6" stroke="#2a2a2a" stroke-width="1.5" />
<!-- Fill interior -->
<ellipse cx="0" cy="-27" rx="30" ry="16" fill="#faf9f6" stroke="none" />
</g>musical_note
Single eighth note.
<text font-size="20" fill="#b8a080">♪</text>Double note: ♫
sparkle
4-point sparkle star (cross pattern).
<g class="sparkle">
<path d="M 0 -5 L 0 5 M -5 0 L 5 0" stroke="#f0c040" stroke-width="2" stroke-linecap="round" />
</g>tv
Television with antenna and flickering screen.
<g class="tv">
<!-- Stand -->
<line x1="-10" y1="25" x2="-10" y2="40" stroke="#2a2a2a" stroke-width="2" />
<line x1="10" y1="25" x2="10" y2="40" stroke="#2a2a2a" stroke-width="2" />
<line x1="-20" y1="40" x2="20" y2="40" stroke="#2a2a2a" stroke-width="2" />
<!-- TV body -->
<rect x="-35" y="-25" width="70" height="50" rx="3" fill="none" stroke="#2a2a2a" stroke-width="2.4" />
<!-- Screen -->
<rect x="-30" y="-20" width="60" height="40" rx="1" fill="#1a2a3a" opacity="0.15" />
<!-- Antenna -->
<line x1="-8" y1="-25" x2="-15" y2="-40" stroke="#2a2a2a" stroke-width="1.8" stroke-linecap="round" />
<line x1="8" y1="-25" x2="15" y2="-40" stroke="#2a2a2a" stroke-width="1.8" stroke-linecap="round" />
</g>couch
Couch/sofa. Width ~140px.
<g class="couch">
<rect x="-70" y="0" width="140" height="22" rx="4" fill="none" stroke="#2a2a2a" stroke-width="2.4" />
<!-- Back -->
<rect x="-70" y="-50" width="12" height="72" rx="4" fill="none" stroke="#2a2a2a" stroke-width="2.4" />
<!-- Armrest -->
<rect x="58" y="-25" width="12" height="47" rx="4" fill="none" stroke="#2a2a2a" stroke-width="2.4" />
<!-- Legs -->
<line x1="-60" y1="22" x2="-60" y2="40" stroke="#2a2a2a" stroke-width="2" stroke-linecap="round" />
<line x1="60" y1="22" x2="60" y2="40" stroke="#2a2a2a" stroke-width="2" stroke-linecap="round" />
<!-- Cushion line -->
<line x1="0" y1="0" x2="0" y2="20" stroke="#2a2a2a" stroke-width="1.2" opacity="0.4" />
</g>window
Window with cross panes and sill.
<g class="window">
<rect x="-35" y="-40" width="70" height="80" rx="2" fill="none" stroke="#d4d0c8" stroke-width="2" />
<line x1="0" y1="-40" x2="0" y2="40" stroke="#d4d0c8" stroke-width="1.2" />
<line x1="-35" y1="0" x2="35" y2="0" stroke="#d4d0c8" stroke-width="1.2" />
<!-- Sill -->
<line x1="-40" y1="40" x2="40" y2="40" stroke="#d4d0c8" stroke-width="2" />
</g>Stick Figure Animation Style Guide
Coordinate System
All characters are defined relative to hip origin at (0, 0). To position a character on the stage, wrap it in a <g transform="translate(X, Y)"> where X,Y is the desired hip position.
Relative offsets from hip (0, 0):
| Part | Offset |
|---|---|
| Head center | (0, -60) |
| Neck | (0, -42) |
| Shoulder | (0, -37) |
| Hip (origin) | (0, 0) |
| Knee | (±15, +35) |
| Feet | (±20, +53) |
Absolute coordinates (when hip is at typical position 450, 295):
| Part | Y position |
|---|---|
| Head center Y | 235 |
| Neck Y | 253 |
| Shoulder Y | 258 |
| Hip Y | 295 |
| Feet Y | 348 |
| Ground Y | 355 |
Character Proportions
- Head: circle, radius 18px (male) / 16px (female)
- Body/Torso: vertical line, 37px long (shoulder to hips)
- Arms: 2-segment paths from shoulder, ~35px total length
- Legs: 2-segment paths from hip, ~53px total length
- Total height: ~126px (head top to feet)
Line Style
stroke: #2a2a2a;
stroke-width: 2.4;
stroke-linecap: round;
stroke-linejoin: round;
fill: none;CSS Class Conventions
/* Always define these base classes in every scene */
.char-head { fill: none; stroke: #2a2a2a; stroke-width: 2.4; }
.char-body { stroke: #2a2a2a; stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; fill: none; }
.char-eyes { fill: #2a2a2a; }
.char-mouth { stroke: #2a2a2a; stroke-width: 1.8; stroke-linecap: round; fill: none; }
.ground { stroke: #d4d0c8; stroke-width: 1.5; stroke-dasharray: 6 4; }Eye Style
- Two circles,
r="2",fill="#2a2a2a" - Positioned at
head_cx ± 6,head_cy - 3
Female Character Variant
- Head radius: 16px (vs 18px male)
- Triangular skirt shape at hips:
<path d="M hip_x hip_y-5 L hip_x-12 hip_y+15 L hip_x+12 hip_y+15 Z" /> - Hair: 3 curved paths flowing down from head
- Eyelashes: tiny lines above eyes (2 per eye)
- Scale: 0.9 of male proportions
Color Palette
| Element | Color |
|---|---|
| Background | #faf9f6 (warm off-white) |
| Character lines | #2a2a2a |
| Heart | #e85c5c |
| Broken heart crack | #faf9f6 on #e85c5c |
| Pizza/food | #e8a85c |
| Salad/healthy | #7ec87e |
| Gym equipment | #888888 |
| Sun | #f0c040 |
| Rain/sweat | #8bb8d0 |
| Musical notes | #b8a080 |
| TV glow | #a0c8e8 |
| Room walls | #e8e4dc |
| Furniture outline | #d4d0c8 |
| Tree canopy | #2a2a2a at 0.25 opacity |
SVG ViewBox
All scenes use viewBox="0 0 900 400". Ground dashed line at y=355.
Animation Timing
- Each scene: 8 seconds total,
animation-duration: 8s - All animations loop with
infinite - Use
ease-in-outfor natural movement (default for most motions) - Use
linearfor constant-speed locomotion (walk-across, run cycles, character_translate) where acceleration would look unnatural - Sub-loops (walk cycles, etc.) use their own shorter duration (0.4-0.5s)
- Phase-based scenes split 8s into equal segments (e.g., 4 phases = 2s each)
Character Positioning via translate()
To move a character across the stage, animate the parent <g> element's transform:
@keyframes walk-across {
0% { transform: translateX(-120px); }
100% { transform: translateX(850px); }
}Never rewrite individual path coordinates to move a character — always use translate() on the containing group.
Animation Transitions Library
CSS keyframe patterns for common motions. All timings assume animation-duration: 8s for the scene and infinite looping.
Example values below use the default hip position (450, 295), giving shoulder at (450, 258), head center at (450, 235), knee-Y at 330, and feet-Y at 348. Adjust these values per character position.
---
walk_cycle
Duration: 0.5s sub-loop Use: Character walking in place (combine with translateX on parent group to move across stage).
/* Leg oscillation */
.walk-leg-l {
animation: walkLegL 0.5s ease-in-out infinite;
}
@keyframes walkLegL {
0%, 100% { d: path("M 450 295 L 435 330 L 435 348"); }
50% { d: path("M 450 295 L 465 330 L 465 348"); }
}
.walk-leg-r {
animation: walkLegR 0.5s ease-in-out infinite;
}
@keyframes walkLegR {
0%, 100% { d: path("M 450 295 L 465 330 L 465 348"); }
50% { d: path("M 450 295 L 435 330 L 435 348"); }
}
/* Arm swing (opposite phase to legs) */
.walk-arm-l {
animation: walkArmL 0.5s ease-in-out infinite;
}
@keyframes walkArmL {
0%, 100% { d: path("M 450 258 L 432 278 L 430 293"); }
50% { d: path("M 450 258 L 468 278 L 470 293"); }
}
.walk-arm-r {
animation: walkArmR 0.5s ease-in-out infinite;
}
@keyframes walkArmR {
0%, 100% { d: path("M 450 258 L 468 278 L 470 293"); }
50% { d: path("M 450 258 L 432 278 L 430 293"); }
}
/* Bounce (apply to character group) */
.walk-bounce {
animation: walkBounce 0.5s ease-in-out infinite;
}
@keyframes walkBounce {
0%, 100% { transform: translateY(0); }
42% { transform: translateY(-5px); }
}run_cycle
Duration: 0.4s sub-loop Use: Faster locomotion. Wider stride, more body lean. Uses linear timing for smooth locomotion.
.run-leg-l {
animation: runLegL 0.4s linear infinite;
transform-origin: 450px 295px; /* hip position */
}
@keyframes runLegL {
0% { transform: rotate(-20deg); }
50% { transform: rotate(20deg); }
100% { transform: rotate(-20deg); }
}
.run-leg-r {
animation: runLegR 0.4s linear infinite;
transform-origin: 450px 295px;
}
@keyframes runLegR {
0% { transform: rotate(20deg); }
50% { transform: rotate(-20deg); }
100% { transform: rotate(20deg); }
}
.run-arm-l {
animation: runArmL 0.4s linear infinite;
transform-origin: 450px 258px; /* shoulder position */
}
@keyframes runArmL {
0% { transform: rotate(15deg); }
50% { transform: rotate(-15deg); }
100% { transform: rotate(15deg); }
}
.run-arm-r {
animation: runArmR 0.4s linear infinite;
transform-origin: 450px 258px;
}
@keyframes runArmR {
0% { transform: rotate(-15deg); }
50% { transform: rotate(15deg); }
100% { transform: rotate(-15deg); }
}sit_down
Duration: one-shot within 8s scene Use: Character transitions from standing to sitting.
.sit-down {
animation: sitDown 8s ease-in-out infinite;
}
@keyframes sitDown {
0%, 20% { transform: translateY(0); } /* standing */
40%, 100% { transform: translateY(20px); } /* seated (lower hips) */
}
/* Legs bend during sit */
.sit-legs {
animation: sitLegs 8s ease-in-out infinite;
}
@keyframes sitLegs {
0%, 20% { d: path("M 450 295 L 435 330 L 430 348"); }
40%, 100% { d: path("M 450 295 L 437 318 L 430 348"); }
}stand_up
Duration: one-shot within 8s scene Use: Reverse of sit_down.
.stand-up {
animation: standUp 8s ease-in-out infinite;
}
@keyframes standUp {
0%, 40% { transform: translateY(20px); } /* seated */
60%, 100% { transform: translateY(0); } /* standing */
}jump
Duration: one-shot within 8s scene Use: Character jumps up and lands.
.jump-char {
animation: jumpChar 8s ease-in-out infinite;
}
@keyframes jumpChar {
0%, 40% { transform: translateY(0); }
48% { transform: translateY(5px); } /* crouch */
55% { transform: translateY(-50px); } /* peak */
65% { transform: translateY(0); } /* land */
100% { transform: translateY(0); }
}wave
Duration: 1s sub-loop Use: Right hand oscillates in a wave gesture.
.wave-hand {
animation: waveHand 1s ease-in-out infinite;
transform-origin: 450px 258px; /* shoulder position */
}
@keyframes waveHand {
0%, 100% { transform: rotate(-5deg); }
50% { transform: rotate(15deg); }
}phase_transition
Use: Multi-moment scenes where different character states appear/disappear. Each phase gets equal screen time within the 8s loop.
/* 4-phase example (2s each) */
.phase1 { animation: phase1vis 8s infinite ease-in-out; }
@keyframes phase1vis {
0%, 20% { opacity: 1; }
25%, 100% { opacity: 0; }
}
.phase2 { animation: phase2vis 8s infinite ease-in-out; }
@keyframes phase2vis {
0%, 20% { opacity: 0; }
25%, 45% { opacity: 1; }
50%, 100% { opacity: 0; }
}
.phase3 { animation: phase3vis 8s infinite ease-in-out; }
@keyframes phase3vis {
0%, 45% { opacity: 0; }
50%, 70% { opacity: 1; }
75%, 100% { opacity: 0; }
}
.phase4 { animation: phase4vis 8s infinite ease-in-out; }
@keyframes phase4vis {
0%, 70% { opacity: 0; }
75%, 97% { opacity: 1; }
100% { opacity: 0; }
}Adjust percentage boundaries for 2-phase (50/50), 3-phase (33/33/33), etc.
character_translate
Use: Move character across the stage over the full 8s loop. Uses linear timing for smooth constant-speed locomotion.
/* Left to right walk */
.walk-across {
animation: walkAcross 8s linear infinite;
}
@keyframes walkAcross {
0% { transform: translateX(-120px); }
100% { transform: translateX(850px); }
}
/* Enter from left, stop at center */
.enter-left {
animation: enterLeft 8s ease-out infinite;
}
@keyframes enterLeft {
0% { transform: translateX(-200px); }
30%, 100% { transform: translateX(0); }
}
/* Exit right */
.exit-right {
animation: exitRight 8s ease-in infinite;
}
@keyframes exitRight {
0%, 60% { transform: translateX(0); opacity: 1; }
90% { transform: translateX(280px); opacity: 0.3; }
100% { transform: translateX(280px); opacity: 0; }
}emotion_shift
Use: Animate facial expression changes (mouth path morphing). Values below use default head center at (450, 235). Adjust per character position.
/* Neutral → sad */
.mouth-to-sad {
animation: mouthSad 8s ease-in-out infinite;
}
@keyframes mouthSad {
0%, 48% { d: path("M 442 240 L 458 240"); } /* neutral */
60%, 100% { d: path("M 442 242 Q 450 235 458 242"); } /* sad */
}
/* Neutral → happy */
.mouth-to-happy {
animation: mouthHappy 8s ease-in-out infinite;
}
@keyframes mouthHappy {
0%, 48% { d: path("M 442 240 L 458 240"); } /* neutral */
60%, 100% { d: path("M 442 240 Q 450 250 458 240"); } /* happy */
}head_drop
Use: Head tilts/droops downward (sadness, tiredness).
.head-drop {
animation: headDrop 8s ease-in-out infinite;
transform-origin: 450px 235px; /* head center position */
}
@keyframes headDrop {
0%, 72% { transform: translate(0, 0) rotate(0deg); }
85%, 100% { transform: translate(14px, 20px) rotate(25deg); }
}sweat_drops
Use: Effort/stress indicator. Small circles that appear and drift away.
.sweat-drop {
animation: sweatDrop 0.8s ease-out infinite;
}
@keyframes sweatDrop {
0% { transform: translate(0, 0); opacity: 0.9; }
100% { transform: translate(25px, -10px); opacity: 0; }
}Stagger multiple drops with animation-delay: 0.2s, 0.4s, etc.
MIT License
Copyright (c) 2026 Bassime Ledath
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
{
"name": "@bassimeledath/stix",
"version": "1.0.0",
"description": "Stick figure animation skill for AI coding agents — generate GIF/MP4 animations from natural language prompts",
"keywords": [
"claude-code",
"skill",
"agent-skill",
"animation",
"stick-figure",
"stix",
"codex",
"cursor",
"ai-coding"
],
"author": "Bassime Ledath",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/bassimeledath/stix"
},
"homepage": "https://github.com/bassimeledath/stix#readme",
"bin": {
"stix-skill": "./bin/install.sh"
},
"scripts": {
"postinstall": "node bin/postinstall.js"
},
"files": [
"bin/",
"SKILL.md",
"library/",
"references/",
"scripts/",
"README.md",
"LICENSE"
],
"engines": {
"node": ">=18"
}
}
/stix
A coding-agent skill that generates stick figure animations from natural language prompts.
/stix "a person waving hello"Outputs: .stix/output/animation.gif + .stix/output/animation.mp4
Installation
Via npx skills (recommended)
npx skills add bassimeledath/stixVia npm
npx @bassimeledath/stixManual
# Claude Code
git clone https://github.com/bassimeledath/stix ~/.claude/skills/stix
# Codex
git clone https://github.com/bassimeledath/stix ~/.codex/skills/stix
# Other agents
git clone https://github.com/bassimeledath/stix ~/.agents/skills/stixDependencies
- A supported AI coding agent CLI (
claude,codex, or setSTIX_AGENT_CLI) - agent-browser — headless browser for screenshot capture
- ffmpeg — frame stitching
Agent Configuration
stix auto-detects your agent CLI. Override with:
# Use a specific agent
export STIX_AGENT_CLI="codex --quiet --full-auto"
# Custom agent with env vars to unset
export STIX_AGENT_CLI="my-agent --pipe"
export STIX_AGENT_ENV_UNSET="MY_AGENT_PARENT_SESSION"How It Works
stix uses an asset-first pipeline inspired by animation studios: characters, backgrounds, and props are generated as reusable SVG assets upfront, then scenes are composed by positioning and animating those shared assets. This guarantees visual consistency across scenes.
1. Parameter inference — parses your prompt to determine scene count, mood, FPS 2. Story decomposition — breaks the prompt into scenes with an asset manifest and per-scene position contracts 3. Asset generation — parallel workers create reusable SVG assets (characters, background, props) 4. Asset validation — each asset is rendered and screenshotted for visual inspection 5. Scene composition — parallel workers read shared assets, position them with translate(), and add CSS @keyframes 6. Visual QC — eval-based keyframe capture, review-tier workers check against a rubric, auto-fixes failures 7. Frame capture + stitch — agent-browser eval seeks to exact frame positions, ffmpeg stitches into GIF/MP4
Examples
# Simple (1 scene)
/stix "a person waving hello"
# Medium (2-3 scenes)
/stix "a cat chasing a mouse across a park"
# Complex (3-4 scenes)
/stix "a person sitting sadly, then going to the gym, then walking confidently"Output
Files are written to .stix/output/ in the current working directory:
| File | Format | Typical Size |
|---|---|---|
animation.gif | Animated GIF | 200-400KB |
animation.mp4 | H.264 MP4 | 50-150KB |
Component Library
The skill includes a curated library of SVG components:
- 16 named poses — standing, walking, running, sitting, jumping, dancing, etc.
- 9 facial expressions — happy, sad, neutral, surprised, determined, angry, laughing, crying, smirk
- 21 props — bench, table, heart, barbell, sun, cloud, tree, pizza, salad, coffee mug, etc.
- 4 animals — cat, dog, bird, mouse with walk cycles
- 11 transition patterns — walk cycle, run cycle, sit down, jump, phase transition, etc.
Architecture
/stix prompt
|
|- Step 1: Infer parameters (scenes, fps, mood)
|- Step 2: Preflight checks (agent-browser, ffmpeg, agent CLI)
|- Step 3: Decompose into scenes + asset manifest -> story.json
|
|- Step 4: Generate assets (parallel workers)
| |- Characters -> .stix/assets/characters/*.svg
| |- Background -> .stix/assets/background.svg
| '- Props -> .stix/assets/props/*.svg
|
|- Step 5: Validate assets (render + screenshot each)
|- Step 6: Compose scenes (parallel workers read shared assets)
|- Step 7: QC loop (eval-based keyframe capture, review + fix)
|- Step 8: Capture frames (eval seek + screenshot) + stitch with ffmpeg
'- Step 9: Report output pathsBackground Asset Template
Generate a single SVG background that will be shared across ALL scenes in the animation. This ensures visual consistency — every scene has the exact same sky, ground, and environmental elements.
Output Format
Write an SVG file containing all background elements within the 900x400 viewBox coordinate space.
<!-- Sky gradient -->
<defs>
<linearGradient id="sky" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#87CEEB" />
<stop offset="100%" style="stop-color:#E0F0FF" />
</linearGradient>
</defs>
<rect x="0" y="0" width="900" height="355" fill="url(#sky)" />
<!-- Ground area -->
<rect x="0" y="355" width="900" height="45" fill="#c8d8a0" opacity="0.3" />
<!-- Background elements: trees, buildings, hills, etc. -->
<!-- Place behind characters (y < 355 for above-ground, use opacity for depth) -->
<!-- Example: distant tree -->
<g transform="translate(150, 280)" opacity="0.25">
<!-- tree SVG from props library -->
</g>
<!-- Example: sun -->
<g transform="translate(780, 60)">
<!-- sun SVG from props library -->
</g>Rules
1. 900x400 viewBox — all coordinates are absolute within this space. 2. Ground line at y=355 — the ground line itself is added by the scene template, but background ground fill should end at y=355. 3. Use props library — reference library/props.md for trees, sun, clouds, benches, etc. Copy their SVG exactly. 4. Depth via opacity — distant elements use opacity="0.25", mid-ground opacity="0.5". Foreground props (benches, etc.) are fully opaque. 5. No characters — the background contains only environmental elements. Characters are separate assets. 6. No animation — the background is static. Scene composers add any environmental animations (clouds moving, sun pulsing) if needed. 7. Match the description — include all environmental elements mentioned in the background spec (e.g., "park with trees and bench" should have trees and a bench). 8. Color palette — use colors from library/style-guide.md. Background #faf9f6 is handled by the HTML body, not the SVG. 9. Keep it clean — aim for 30-80 lines of SVG. Simple shapes, no excessive detail.
Character Asset Template
Generate a self-contained SVG <g> element for a stick figure character. This asset will be reused across multiple animation scenes — scene composers will position it via translate() and animate it with CSS @keyframes.
Output Format
Write a single SVG file containing a <g> element with the character's ID. The character is defined relative to hip at (0, 0) per the style guide.
<g id="CHARACTER_ID">
<!-- Head group: contains head circle + expression variants -->
<g class="head" transform="translate(0, -60)">
<circle cx="0" cy="0" r="18" class="char-head" />
<!-- Each expression is a separate group. Scene composers toggle via display:none/block -->
<g class="expr-happy">
<circle cx="-6" cy="-3" r="2" class="char-eyes" />
<circle cx="6" cy="-3" r="2" class="char-eyes" />
<path d="M -8 5 Q 0 15 8 5" class="char-mouth" />
</g>
<g class="expr-neutral" style="display:none">
<circle cx="-6" cy="-3" r="2" class="char-eyes" />
<circle cx="6" cy="-3" r="2" class="char-eyes" />
<line x1="-6" y1="5" x2="6" y2="5" class="char-mouth" />
</g>
<!-- Add more expressions as specified in the character description -->
</g>
<!-- Torso -->
<line x1="0" y1="-42" x2="0" y2="0" class="char-body" />
<!-- Left arm (2 segments: shoulder -> elbow -> hand) -->
<path class="left-arm char-body" d="M 0 -37 L -18 -17 L -20 0" />
<!-- Right arm -->
<path class="right-arm char-body" d="M 0 -37 L 18 -17 L 20 0" />
<!-- Left leg (2 segments: hip -> knee -> foot) -->
<path class="left-leg char-body" d="M 0 0 L -15 35 L -20 53" />
<!-- Right leg -->
<path class="right-leg char-body" d="M 0 0 L 15 35 L 20 53" />
</g>Rules
1. Hip at origin (0, 0) — all coordinates are relative. Scene composers position via translate(). 2. Named CSS classes on each limb — left-arm, right-arm, left-leg, right-leg, head. Scene composers animate these individually with @keyframes. 3. Expression variants — include a <g class="expr-EXPRESSION_NAME"> for each expression listed in the character spec's expressions_needed. The first expression should be visible (no display:none), all others should have style="display:none". 4. Use library poses as reference — the default pose should be standing from library/poses.md. Copy exact path data. 5. Use library expressions — copy expression SVG from library/expressions.md. Position them relative to head center at (0, 0) inside the head group. 6. Apply character-specific colors — if the character spec includes colors (e.g., blue jacket), add fill or stroke attributes to the appropriate elements. Keep the #2a2a2a outline on all body parts. 7. For animals — use library/animals.md for the body structure instead of human proportions. Still use hip-at-origin convention and named limb classes. 8. Keep it minimal — aim for 50-100 lines. No animation, no background, no HTML wrapper. Just the SVG <g>.
Proportions Reference
| Part | Relative to hip (0,0) |
|---|---|
| Head center | (0, -60) |
| Neck | (0, -42) |
| Shoulder | (0, -37) |
| Hip (origin) | (0, 0) |
| Knee | (+/-15, +35) |
| Feet | (+/-20, +53) |
| Total height | ~126px |
| Head radius | 18px (male) / 16px (female) |
Props Asset Template
Generate individual SVG files for each prop needed by the animation. Props are reusable across scenes — scene composers position them via translate().
Output Format
Write each prop as a separate SVG file. Each prop is a self-contained <g> element defined at origin (0, 0).
Example for a heart prop (heart.svg):
<g id="heart">
<path d="M 0 -8 C -10 -20 -25 -5 0 12 C 25 -5 10 -20 0 -8 Z"
fill="#e85c5c" stroke="#2a2a2a" stroke-width="1.5" />
</g>Example for a sparkle prop (sparkle.svg):
<g id="sparkle">
<path d="M 0 -12 L 2 -2 L 12 0 L 2 2 L 0 12 L -2 2 L -12 0 L -2 -2 Z"
fill="#f0c040" stroke="none" opacity="0.9" />
</g>Rules
1. Origin at (0, 0) — each prop is centered at its own origin. Scene composers position via translate(). 2. Use props library — reference library/props.md for exact SVG path data. Copy the shapes exactly. 3. One file per prop — each prop gets its own .svg file named after its ID (e.g., tree.svg, bench.svg, heart.svg). 4. Self-contained `<g>` — each file contains a single <g id="PROP_ID"> with all the prop's elements inside. 5. Color palette — use colors from library/style-guide.md. Common: #e85c5c (heart), #7ec87e (plants), #f0c040 (sun/sparkle), #87CEEB (sky elements). 6. No animation — props are static SVG. Scene composers add animations if needed. 7. Appropriate scale — props should be sized relative to characters (~126px tall). A bench is ~60px tall, a heart is ~24px, a tree is ~100px. 8. Keep it minimal — each prop should be 5-20 lines of SVG.
Visual QC Rubric
Use this rubric to evaluate each scene's 4 keyframe screenshots (captured at 0%, 33%, 66%, 100% of the animation loop via eval-based seeking).
Score each category PASS or FAIL. If any category is FAIL, the scene must be fixed.
Note: Character and prop integrity is validated separately at the asset validation step (Step 5). This rubric focuses on composition and animation quality in the assembled scene.
---
1. Animation Correctness
Check across keyframes:
- [ ] Characters move between keyframes (animation is actually happening)
- [ ] Walk/run cycles show alternating leg positions
- [ ] Arms swing in opposition to legs during locomotion
- [ ] Transitions between poses are smooth (no teleporting)
- [ ] Animation timing matches the scene's emotional beat
- [ ] No animation "freezes" — every moving element should change between at least 2 keyframes
- [ ] Expression changes happen at the right moments (matching scene spec timing)
FAIL if: Animation doesn't progress, limbs move in same direction, or poses teleport between keyframes.
Fix instructions: Check animation-duration, keyframe percentages, and transform values. Ensure leg/arm animations use opposite phase timing. Reference library/transitions.md. Do NOT modify the SVG asset shapes — only fix keyframes and timing.
---
2. Scene Composition
Check at every keyframe:
- [ ] Ground line is visible at y=355
- [ ] Characters are standing ON the ground (feet near y=348-355)
- [ ] Props are correctly positioned (not floating, not underground)
- [ ] Scene uses the full 900x400 viewBox reasonably (not cramped in corner)
- [ ] Background is visible and properly placed
- [ ] Characters don't clip through props or scene boundaries
- [ ] Start and end positions match the scene specification coordinates
FAIL if: Characters float above ground, are underground, or scene is empty/mis-composed.
Fix instructions: Check character translate() Y positions against the style guide. Ensure ground line exists. Verify prop placement coordinates. Do NOT modify asset SVGs.
---
3. Visual Quality
Check overall:
- [ ] Line widths are consistent (stroke-width: 2.4 for characters)
- [ ] Colors match the palette (see
library/style-guide.md) - [ ] No visual artifacts (stray lines, incorrect fills, clipping)
- [ ] Background is
#faf9f6(warm off-white) - [ ] Opacity values are reasonable (0.25 for background trees, etc.)
- [ ] CSS animations use
ease-in-outfor natural motion (not linear, unless intentional)
FAIL if: Major color discrepancy, incorrect line weights, or visual artifacts.
Fix instructions: Cross-reference all color values with style guide. Check stroke-width values.
---
4. Continuity (Multi-Scene Only)
Check between last keyframe of scene N and first keyframe of scene N+1:
- [ ] Character position is consistent (not teleporting across stage)
- [ ] Character pose is consistent or has logical transition
- [ ] Expression matches emotional progression
- [ ] Props that should persist are present
- [ ] Props that should be gone are removed
FAIL if: Character position, pose, or props are inconsistent between scenes.
Fix instructions: Adjust the starting translate() position of scene N+1 to match the ending position of scene N. Or adjust the ending position of scene N.
---
QC Report Format
Output as JSON:
{
"scene": 1,
"overall": "PASS",
"categories": {
"animation_correctness": { "result": "PASS", "notes": "" },
"scene_composition": { "result": "PASS", "notes": "" },
"visual_quality": { "result": "PASS", "notes": "" }
},
"fix_instructions": []
}<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Scene N: Title</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #faf9f6;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
overflow: hidden;
}
svg { width: 900px; height: 400px; }
.ground { stroke: #d4d0c8; stroke-width: 1.5; stroke-dasharray: 6 4; }
/* Character base styles */
.char-head { fill: none; stroke: #2a2a2a; stroke-width: 2.4; }
.char-body { stroke: #2a2a2a; stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; fill: none; }
.char-eyes { fill: #2a2a2a; }
.char-mouth { stroke: #2a2a2a; stroke-width: 1.8; stroke-linecap: round; fill: none; }
/* Expression toggling: hide all expressions by default, show active one */
[class^="expr-"] { display: none; }
.expr-active { display: block; }
/* === Scene-specific keyframes below === */
/* Example: character translation across stage
@keyframes move-character-name {
0% { transform: translate(START_X px, START_Y px); }
100% { transform: translate(END_X px, END_Y px); }
}
*/
/* Example: expression change at midpoint
@keyframes expr-toggle-character-name {
0%, 49% { }
50%, 100% { }
}
*/
</style>
</head>
<body>
<svg viewBox="0 0 900 400" xmlns="http://www.w3.org/2000/svg">
<!-- Background asset (copied from .stix/assets/background.svg) -->
<g id="background">
<!-- paste background SVG content here -->
</g>
<!-- Ground line -->
<line class="ground" x1="0" y1="355" x2="900" y2="355" />
<!-- Props (copied from .stix/assets/props/*.svg, positioned via translate) -->
<!--
<g id="prop-NAME" transform="translate(X, Y)">
paste prop SVG content here
</g>
-->
<!-- Characters (copied from .stix/assets/characters/*.svg, positioned via translate) -->
<!--
<g id="char-NAME-wrapper" style="animation: move-NAME 8s ease-in-out infinite;">
<g id="char-NAME">
paste character SVG content here
</g>
</g>
-->
</svg>
</body>
</html>
Scene Composition Instructions
You are composing an animation scene from pre-built SVG assets. Your job is positioning + keyframes + timing — NOT character or prop design. You must not modify the SVG shapes from the assets.
Workflow
1. Read the base HTML template (references/scene-base.html) 2. Read each asset SVG file listed in the prompt 3. Embed assets into the SVG, wrapped in positioning <g> elements 4. Add CSS `@keyframes` for movement, limb animation, and expression changes 5. Write the complete HTML file
Embedding Assets
Background
Copy the background SVG content into the <g id="background"> group. No transform needed — background coordinates are already in the 900x400 viewBox space.
<g id="background">
<!-- paste contents of .stix/assets/background.svg here -->
</g>Characters
Wrap each character asset in a positioning <g> with the starting translate position from the scene spec. Apply a CSS animation for movement.
<g id="char-NAME-wrapper" style="animation: move-NAME 8s ease-in-out infinite;">
<!-- paste contents of .stix/assets/characters/NAME.svg here, exactly as-is -->
</g>The @keyframes move-NAME handles the translate from start to end position:
@keyframes move-NAME {
0% { transform: translate(START_Xpx, START_Ypx); }
100% { transform: translate(END_Xpx, END_Ypx); }
}Props
Position each prop with a static translate:
<g id="prop-NAME" transform="translate(X, Y)">
<!-- paste contents of .stix/assets/props/NAME.svg here -->
</g>Adding Animation
Character Movement
Use the start/end coordinates from the scene spec:
@keyframes move-cat {
0% { transform: translate(30px, 310px); }
100% { transform: translate(600px, 310px); }
}Limb Animation (Walk/Run Cycles)
Target the named limb classes inside each character asset. These nest as sub-loops inside the 8s scene:
/* Walk cycle: 0.5s period, alternating legs */
#char-cat .left-leg {
animation: walk-left-leg 0.5s ease-in-out infinite;
}
#char-cat .right-leg {
animation: walk-right-leg 0.5s ease-in-out infinite;
}
#char-cat .left-arm {
animation: walk-left-arm 0.5s ease-in-out infinite;
}
#char-cat .right-arm {
animation: walk-right-arm 0.5s ease-in-out infinite;
}
@keyframes walk-left-leg {
0%, 100% { transform: rotate(-15deg); }
50% { transform: rotate(15deg); }
}
@keyframes walk-right-leg {
0%, 100% { transform: rotate(15deg); }
50% { transform: rotate(-15deg); }
}
/* Arms swing in opposition to legs */
@keyframes walk-left-arm {
0%, 100% { transform: rotate(15deg); }
50% { transform: rotate(-15deg); }
}
@keyframes walk-right-arm {
0%, 100% { transform: rotate(-15deg); }
50% { transform: rotate(15deg); }
}Use transform-origin on limb elements to set the rotation pivot (shoulder for arms, hip for legs).
Expression Changes
Toggle expression groups using opacity or display at specific keyframe percentages:
/* Switch from neutral to surprised at 60% */
#char-mouse .expr-neutral {
animation: hide-at-60 8s step-end infinite;
}
#char-mouse .expr-surprised {
animation: show-at-60 8s step-end infinite;
}
@keyframes hide-at-60 {
0%, 59% { display: block; }
60%, 100% { display: none; }
}
@keyframes show-at-60 {
0%, 59% { display: none; }
60%, 100% { display: block; }
}Timing Rules
- Scene duration: always 8s with
infiniteloop - Movement easing:
ease-in-outfor natural motion,linearfor constant-speed locomotion - Sub-loops: walk cycle 0.5s, run cycle 0.4s — these repeat independently inside the 8s scene
- Phase timing: use the
timingobject from the scene spec to map actions to percentages - Expression timing: align with emotional beats in the scene description
Critical Rules
1. DO NOT modify asset SVG paths — copy them exactly as-is from the asset files 2. Position via translate() only — never rewrite SVG coordinates to move characters 3. All timing via CSS — no JavaScript 4. Include the ground line — <line class="ground" x1="0" y1="355" x2="900" y2="355" /> 5. Use the transition library — reference library/transitions.md for animation patterns 6. Match scene spec coordinates — use the exact start/end [x, y] positions from the spec
#!/bin/bash
# Capture animated HTML scenes and stitch into GIF + MP4
# Uses agent-browser eval to pause CSS animations and seek to exact frame positions.
#
# Usage: ./capture.sh [options]
#
# Options:
# --scenes-dir DIR Directory containing scene-*.html files (default: .stix/scenes)
# --frames-dir DIR Directory to store captured frames (default: .stix/frames)
# --output-dir DIR Directory for output files (default: .stix/output)
# --fps N Frames per second for output (default: 8)
# --format FORMAT Output format: gif, mp4, both (default: both)
# --width N Output width in pixels (default: 800)
set -e
# Defaults
SCENES_DIR=".stix/scenes"
FRAMES_DIR=".stix/frames"
OUTPUT_DIR=".stix/output"
FPS=8
FORMAT="both"
WIDTH=800
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--scenes-dir) [[ -n "${2:-}" ]] || { echo "Error: --scenes-dir requires a value"; exit 1; }; SCENES_DIR="$2"; shift 2 ;;
--frames-dir) [[ -n "${2:-}" ]] || { echo "Error: --frames-dir requires a value"; exit 1; }; FRAMES_DIR="$2"; shift 2 ;;
--output-dir) [[ -n "${2:-}" ]] || { echo "Error: --output-dir requires a value"; exit 1; }; OUTPUT_DIR="$2"; shift 2 ;;
--fps) [[ -n "${2:-}" ]] || { echo "Error: --fps requires a value"; exit 1; }; FPS="$2"; shift 2 ;;
--format) [[ -n "${2:-}" ]] || { echo "Error: --format requires a value"; exit 1; }; FORMAT="$2"; shift 2 ;;
--width) [[ -n "${2:-}" ]] || { echo "Error: --width requires a value"; exit 1; }; WIDTH="$2"; shift 2 ;;
*) echo "Unknown option: $1"; exit 1 ;;
esac
done
# Validate numeric parameters
if ! [[ "$FPS" =~ ^[0-9]+$ ]] || [ "$FPS" -lt 1 ]; then
echo "Error: --fps must be a positive integer, got: $FPS"
exit 1
fi
if ! [[ "$WIDTH" =~ ^[0-9]+$ ]] || [ "$WIDTH" -lt 1 ]; then
echo "Error: --width must be a positive integer, got: $WIDTH"
exit 1
fi
# Validate format
if [[ "$FORMAT" != "gif" && "$FORMAT" != "mp4" && "$FORMAT" != "both" ]]; then
echo "Error: --format must be gif, mp4, or both, got: $FORMAT"
exit 1
fi
# Validate scenes directory
if [ ! -d "$SCENES_DIR" ]; then
echo "Error: scenes directory not found: $SCENES_DIR"
exit 1
fi
command -v agent-browser >/dev/null 2>&1 || { echo "Error: agent-browser not found. Install it first."; exit 1; }
command -v ffmpeg >/dev/null 2>&1 || { echo "Error: ffmpeg not found. Install it first."; exit 1; }
# Cleanup trap: ensure browser is closed on failure
BROWSER_OPEN=false
cleanup() {
if $BROWSER_OPEN; then
agent-browser close 2>/dev/null || true
fi
}
trap cleanup EXIT
# Setup frames directory (guard: only remove if it looks like our frames dir)
if [ -d "$FRAMES_DIR" ]; then
case "$FRAMES_DIR" in
*frames*|*.stix/*)
rm -rf "$FRAMES_DIR"
;;
*)
echo "Error: refusing to rm -rf '$FRAMES_DIR' — does not look like a frames directory"
exit 1
;;
esac
fi
mkdir -p "$FRAMES_DIR" "$OUTPUT_DIR"
# Discover scenes using glob (safe with spaces/special chars)
SCENE_FILES=()
for f in "$SCENES_DIR"/scene-*.html; do
[ -e "$f" ] && SCENE_FILES+=("$f")
done
# Sort the files
IFS=$'\n' SCENE_FILES=($(printf '%s\n' "${SCENE_FILES[@]}" | sort)); unset IFS
if [ ${#SCENE_FILES[@]} -eq 0 ]; then
echo "Error: no scene-*.html files found in $SCENES_DIR"
exit 1
fi
echo "Found ${#SCENE_FILES[@]} scene(s)"
# Calculate frames per scene: fps x 8 seconds per scene
FRAMES_PER_SCENE=$((FPS * 8))
# Scene duration in milliseconds
SCENE_DURATION_MS=8000
GLOBAL_FRAME=0
for scene_file in "${SCENE_FILES[@]}"; do
scene_name=$(basename "$scene_file" .html)
echo "Capturing $scene_name..."
# Open the scene in browser using absolute path
abs_path="$(cd "$(dirname "$scene_file")" && pwd)/$(basename "$scene_file")"
agent-browser open "file://$abs_path"
BROWSER_OPEN=true
sleep 0.5 # page load only
# Pause all CSS animations at the start
agent-browser eval "document.getAnimations().forEach(a => a.pause())"
for i in $(seq 0 $((FRAMES_PER_SCENE - 1))); do
# Calculate exact millisecond position for this frame
TIME_MS=$(( i * SCENE_DURATION_MS / FRAMES_PER_SCENE ))
PADDED=$(printf "%04d" $GLOBAL_FRAME)
# Seek all animations to exact millisecond
agent-browser eval "document.getAnimations().forEach(a => { a.currentTime = $TIME_MS })"
# Screenshot with absolute path (avoids CSS selector parsing bug)
agent-browser screenshot "$(cd "$FRAMES_DIR" && pwd)/frame-${PADDED}.png"
GLOBAL_FRAME=$((GLOBAL_FRAME + 1))
done
agent-browser close
BROWSER_OPEN=false
echo " $scene_name captured ($FRAMES_PER_SCENE frames)"
done
TOTAL=$GLOBAL_FRAME
echo ""
echo "Captured $TOTAL total frames across ${#SCENE_FILES[@]} scene(s)"
# Stitch into GIF
if [[ "$FORMAT" == "gif" || "$FORMAT" == "both" ]]; then
echo "Creating GIF..."
FFMPEG_LOG=$(mktemp)
if ffmpeg -y -framerate "$FPS" -start_number 0 -i "$FRAMES_DIR/frame-%04d.png" \
-filter_complex "[0:v] scale=${WIDTH}:-1:flags=lanczos,split[a][b];[a]palettegen=max_colors=196:stats_mode=diff[p];[b][p]paletteuse=dither=floyd_steinberg" \
-loop 0 \
"$OUTPUT_DIR/animation.gif" 2>"$FFMPEG_LOG"; then
GIF_SIZE=$(du -h "$OUTPUT_DIR/animation.gif" | cut -f1)
echo " GIF: $OUTPUT_DIR/animation.gif ($GIF_SIZE)"
else
echo " GIF encoding failed. ffmpeg output:"
cat "$FFMPEG_LOG"
rm -f "$FFMPEG_LOG"
exit 1
fi
rm -f "$FFMPEG_LOG"
fi
# Create MP4
if [[ "$FORMAT" == "mp4" || "$FORMAT" == "both" ]]; then
echo "Creating MP4..."
FFMPEG_LOG=$(mktemp)
if ffmpeg -y -framerate "$FPS" -start_number 0 -i "$FRAMES_DIR/frame-%04d.png" \
-vf "scale=${WIDTH}:-2:flags=lanczos" \
-c:v libx264 -preset slow -crf 18 -pix_fmt yuv420p \
-movflags +faststart \
"$OUTPUT_DIR/animation.mp4" 2>"$FFMPEG_LOG"; then
MP4_SIZE=$(du -h "$OUTPUT_DIR/animation.mp4" | cut -f1)
echo " MP4: $OUTPUT_DIR/animation.mp4 ($MP4_SIZE)"
else
echo " MP4 encoding failed. ffmpeg output:"
cat "$FFMPEG_LOG"
rm -f "$FFMPEG_LOG"
exit 1
fi
rm -f "$FFMPEG_LOG"
fi
# Clean up frames (guarded)
if [ -d "$FRAMES_DIR" ]; then
case "$FRAMES_DIR" in
*frames*|*.stix/*)
rm -rf "$FRAMES_DIR"
;;
esac
fi
echo ""
echo "Animation complete!"
echo " Scenes: ${#SCENE_FILES[@]}"
echo " Frames: $TOTAL"
echo " FPS: $FPS"
[[ "$FORMAT" == "gif" || "$FORMAT" == "both" ]] && echo " GIF: $OUTPUT_DIR/animation.gif"
[[ "$FORMAT" == "mp4" || "$FORMAT" == "both" ]] && echo " MP4: $OUTPUT_DIR/animation.mp4"
Related skills
FAQ
What does Stix output?
GIF and MP4 files by default, unless the user specifies one format.
How long is each scene?
Each scene is a fixed 8 seconds; the animation system is built around that duration.