AI Coding Stickman Animations: Measured Cost Savings and Stable Output from Packaging the Workflow as a Skill

    Generating explainer stickman animations with AI coding tools through one-shot prompts tends to produce inconsistent character proportions and unpredictable API spend. This article documents a measured baseline, then shows how to package character specs, a rendering engine, and an export script into a reusable Skill. It covers a minimal skill.md template, the engine.js joint-calculation approach that keeps proportions stable across scenes, a Playwright plus ffmpeg export pipeline, an ffprobe verification step, and a cost comparison with its assumptions stated explicitly — including the cases where the approach does not apply.

    1. Where the Problem Comes From: Why Bare Prompting Is Expensive and Unstable

    When people use AI coding tools to generate stickman explainer animations, the usual habit is to paste the whole request into the terminal at once — for example, “generate a 15-second stickman programmer animation with 3 scenes, output as a single-page HTML.” It looks fast, but two problems almost always surface.

    The first is unstable quality. Without an explicit skeleton baseline, the model has to guess head-to-body proportions, limb lengths, and joint positions on the fly. The first version might be barely acceptable, but as soon as you change the camera, revise a motion, or rework a scene, the character proportions drift and the arm and leg lines start to look stiff.

    The second is uncontrolled cost. Every regeneration is close to a stateless inference: the model has to re-read the requirements, reassemble the code, and redesign the animation structure. If the structure you already validated in the previous round was never captured anywhere, the next round starts from zero, and your usage quota gets burned on repeated rework.

    I ran a bare-prompting baseline test. The setup: a general-purpose code model, driven through an AI coding tool with local file read access and terminal execution; the task was a 15-second, 3-scene, 1920×1080, 60fps single-page stickman animation. The first bare-prompt generation took about 64 seconds and produced roughly 11.6 KB of HTML. Opening it showed cards for topic, shooting, and editing stages, but the head-to-body ratio was visibly off, the arm lines were crooked, and HTML, CSS, and JavaScript were all mixed into one file, which made later maintenance difficult.

    The takeaway from this baseline: without a specification, output quality depends largely on whatever the model improvises, and cost depends on how many rework rounds you need. So the real problem to solve is not “write a longer prompt” — it is capturing the structure you have already validated.

    2. The Core Idea: Package the Spec, the Engine, and the Export Script into a Skill

    To make stickman animations come out consistently and to lower iteration cost, three kinds of content can be packaged into one reusable Skill: character specifications, a rendering engine, and an export script. After receiving a task, the AI coding tool loads this Skill first, then generates and adjusts the animation against the established baseline instead of reasoning from scratch every time.

    A typical Skill directory can be organized like this:

    1. skills/stickman-animator/
    2. ├── skill.md # Skeleton spec, motion rules, delivery constraints
    3. ├── engine.js # Skeleton rendering and animation driver
    4. └── export-mp4.js # Browser recording and MP4 export

    The payoff is twofold:

    1. A more reliable quality floor: character proportions, joint positions, and motion ranges are documented, which reduces random drift.
    2. Lower iteration cost: new scenes or motions only require extending the existing engine rather than rewriting the whole logic.

    Minimal skill.md Template

    Think of skill.md as the instruction manual for this Skill. Write it as executable constraints rather than vague description. Here is a minimal template you can reuse directly:

    1. # Stickman Animator Skill
    2. ## Character Baseline
    3. - Character: stickman programmer wearing black-framed glasses
    4. - Base unit: head height
    5. - headRatio: 1.0
    6. - torso: 2.2
    7. - arm: 1.8
    8. - leg: 2.6
    9. ## Joint Limits
    10. - Movable joints: neck, shoulderL, shoulderR, elbowL, elbowR, hip, kneeL, kneeR
    11. - Arm swing range: -35° to +35°
    12. - Leg swing range: -25° to +25°
    13. - Motion interpolation: easeInOut
    14. ## Delivery Constraints
    15. - Frame rate: target 60fps
    16. - Resolution: 1920×1080, 16:9 landscape
    17. - Output: single-page HTML with engine.js inlined
    18. - Video: MP4, H.264, no letterboxing
    19. - Forbidden: irrelevant decoration, random fonts, random character proportions
    20. ## Generation Rules
    21. - Reuse the SKELETON constants and joint calculation functions in engine.js first
    22. - New scenes may only change motion parameters and caption cards
    23. - Never rewrite the skeleton structure directly

    headRatio, torso, arm, and leg here are not fixed pixel values but relative base units. In engine.js, the base unit is usually something like unit = height / 12, and the actual pixel length comes from multiplying unit by these coefficients. That way, character proportions are easier to keep consistent even when the resolution changes.

    The more specific the spec, the less room the model has to improvise and the more stable the output. This matters most when several scenes and motions are generated in sequence, where a documented spec significantly reduces the “guess what the character looks like every time” problem.

    3. engine.js: Turning the Skeleton Spec into Renderable Frames

    engine.js converts the abstract spec in skill.md into actual pixels. The critical chain is:

    1. SKELETON parameters → pixel coordinate conversion → joint position calculation → Canvas/SVG drawing → requestAnimationFrame driver

    Below is a minimal runnable rendering example that draws the stickman skeleton on Canvas. It is not production-ready code, but it is enough to show how to avoid regenerating proportions at random each time.

    1. const SKELETON = {
    2. headRatio: 1.0,
    3. torso: 2.2,
    4. arm: 1.8,
    5. leg: 2.6
    6. };
    7. function easeInOut(t) {
    8. return t < 0.5
    9. ? 2 * t * t
    10. : 1 - Math.pow(-2 * t + 2, 2) / 2;
    11. }
    12. function calcJoints(time, width, height) {
    13. const unit = height / 12;
    14. const headR = unit * SKELETON.headRatio;
    15. const cx = width / 2;
    16. const headY = height * 0.25;
    17. const head = { x: cx, y: headY, r: headR };
    18. const neck = { x: cx, y: headY + headR * 1.2 };
    19. const hip = { x: cx, y: neck.y + unit * SKELETON.torso };
    20. const phase = (Math.sin(time * Math.PI * 2) + 1) / 2;
    21. const swing = easeInOut(phase);
    22. const armAngle = -0.6 + swing * 1.2;
    23. const legAngle = -0.4 + swing * 0.8;
    24. const shoulderL = {
    25. x: neck.x - unit * 0.5,
    26. y: neck.y + unit * 0.2
    27. };
    28. const shoulderR = {
    29. x: neck.x + unit * 0.5,
    30. y: neck.y + unit * 0.2
    31. };
    32. const elbowL = {
    33. x: shoulderL.x + Math.cos(armAngle) * unit * SKELETON.arm,
    34. y: shoulderL.y + Math.sin(armAngle) * unit * SKELETON.arm
    35. };
    36. const elbowR = {
    37. x: shoulderR.x - Math.cos(armAngle) * unit * SKELETON.arm,
    38. y: shoulderR.y + Math.sin(armAngle) * unit * SKELETON.arm
    39. };
    40. const kneeL = {
    41. x: hip.x - unit * 0.5 + Math.cos(legAngle) * unit * SKELETON.leg,
    42. y: hip.y + Math.sin(legAngle) * unit * SKELETON.leg
    43. };
    44. const kneeR = {
    45. x: hip.x + unit * 0.5 - Math.cos(legAngle) * unit * SKELETON.leg,
    46. y: hip.y + Math.sin(legAngle) * unit * SKELETON.leg
    47. };
    48. return {
    49. head,
    50. neck,
    51. hip,
    52. shoulderL,
    53. shoulderR,
    54. elbowL,
    55. elbowR,
    56. kneeL,
    57. kneeR
    58. };
    59. }

    The point here: proportions are controlled centrally by SKELETON, motion is driven by easeInOut, and joint coordinates are derived from mathematical relationships. So in a new scene you only adjust time, angles, or captions — you do not regenerate the entire character structure.

    One caveat: this baseline substantially lowers the chance of proportion distortion, but it is not a fit for every case. Complex perspective, extreme exaggeration, cross-resolution adaptation, and character turns still require manual review.

    4. export-mp4.js: Exporting the Web Animation to a Deliverable Video

    Once the animation is done, it still has to be exported to MP4. A more reliable approach is not to download a large browser binary on the fly, but to reuse a browser already installed locally, or the Chromium bundled with Playwright, and then pair it with ffmpeg for recording and transcoding.

    • Node.js
    • Playwright
    • Chromium bundled with Playwright, or a local Chrome / Edge / Chromium
    • ffmpeg

    Installation example:

    1. npm init -y
    2. npm i -D playwright
    3. npx playwright install chromium
    4. ffmpeg -version

    Using Playwright’s bundled Chromium is the preferred option, since it means you do not need to configure executablePath in the script. If you would rather reuse a local Chrome, you can pass the path in through an environment variable, for example:

    1. export CHROME_PATH="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
    2. node export-mp4.js --url "file:///absolute/path/dist/index.html" --out "./dist/stickman.mp4" --width 1920 --height 1080 --duration 15000

    If CHROME_PATH points to a file that does not exist, browser launch will fail. During debugging, headed mode is recommended; a server environment may additionally need xvfb, screen recording permissions, or local file access permissions configured.

    The script accepts the following arguments:

    1. node export-mp4.js \
    2. --url "file:///absolute/path/dist/index.html" \
    3. --out "./dist/stickman.mp4" \
    4. --width 1920 \
    5. --height 1080 \
    6. --duration 15000

    Three parameters need to be aligned during export:

    • Viewport size: fix it at 1920×1080 to avoid letterboxing, cropping, or page scaling.
    • Duration: record for the total scene length — for example, 15000 ms covers 3 scenes.
    • Frame rate: the page animation should render as stably as possible, but the frame rate of a browser recording is not inherently a strict 60fps.

    Simplified Export Script

    The following is a Playwright-based export sketch. It opens the local HTML, records video, then converts it to MP4 with ffmpeg.

    1. const { chromium } = require("playwright");
    2. const { spawnSync } = require("child_process");
    3. const fs = require("fs");
    4. const path = require("path");
    5. function getArg(name, fallback) {
    6. const index = process.argv.indexOf(name);
    7. return index !== -1 && process.argv[index + 1]
    8. ? process.argv[index + 1]
    9. : fallback;
    10. }
    11. async function main() {
    12. const url = getArg("--url", "file:///./dist/index.html");
    13. const out = getArg("--out", "./dist/stickman.mp4");
    14. const width = parseInt(getArg("--width", "1920"), 10);
    15. const height = parseInt(getArg("--height", "1080"), 10);
    16. const duration = parseInt(getArg("--duration", "15000"), 10);
    17. const tempDir = "./tmp-record";
    18. fs.mkdirSync(tempDir, { recursive: true });
    19. fs.mkdirSync(path.dirname(out), { recursive: true });
    20. let browser;
    21. let context;
    22. try {
    23. const launchOptions = {
    24. headless: false
    25. };
    26. if (process.env.CHROME_PATH) {
    27. launchOptions.executablePath = process.env.CHROME_PATH;
    28. }
    29. browser = await chromium.launch(launchOptions);
    30. context = await browser.newContext({
    31. viewport: { width, height },
    32. recordVideo: {
    33. dir: tempDir,
    34. size: { width, height }
    35. }
    36. });
    37. const page = await context.newPage();
    38. await page.goto(url, { waitUntil: "networkidle" });
    39. await page.waitForTimeout(duration);
    40. const video = page.video();
    41. await context.close();
    42. context = null;
    43. const videoPath = await video.path();
    44. const ffmpeg = spawnSync(
    45. "ffmpeg",
    46. [
    47. "-y",
    48. "-i", videoPath,
    49. "-c:v", "libx264",
    50. "-pix_fmt", "yuv420p",
    51. "-movflags", "+faststart",
    52. out
    53. ],
    54. { stdio: "inherit" }
    55. );
    56. if (ffmpeg.status !== 0) {
    57. throw new Error(
    58. "ffmpeg transcoding failed; note that -y overwrites an existing output file with the same name, so verify the ffmpeg environment, the output path, and the source video path."
    59. );
    60. }
    61. console.log(`Export finished: ${out}`);
    62. } finally {
    63. if (context) {
    64. await context.close().catch(() => {});
    65. }
    66. if (browser) {
    67. await browser.close().catch(() => {});
    68. }
    69. fs.rmSync(tempDir, { recursive: true, force: true });
    70. }
    71. }
    72. main().catch((err) => {
    73. console.error(err);
    74. process.exit(1);
    75. });

    This script uses try/finally to guarantee that the browser is closed and the tmp-record temporary directory is cleaned up even when ffmpeg fails. Note in particular that the -y flag in the ffmpeg command overwrites an existing output file with the same name; if you need to keep the old file, delete it first or change the output filename.

    Frame Rate and Export Verification

    What Playwright’s recordVideo produces is a browser screen recording, which is not strictly 60fps. Even adding -r 60 in ffmpeg only sets an output frame rate parameter — it cannot guarantee that every frame of the source animation actually appeared on schedule. If the task requires a strict 60fps, use fixed-timestep rendering instead: call the draw function once per frame and capture a screenshot, then assemble the frames with ffmpeg.

    1. ffmpeg -y -framerate 60 -i ./frames/frame-%06d.png \
    2. -c:v libx264 -pix_fmt yuv420p -r 60 \
    3. ./dist/stickman.mp4

    After export, verify the key parameters with ffprobe:

    1. ffprobe -v error -select_streams v:0 \
    2. -show_entries stream=r_frame_rate,width,height,codec_name \
    3. -of default=noprint_wrappers=1 \
    4. ./dist/stickman.mp4

    If r_frame_rate is not the expected value, or the resolution or codec does not meet the delivery requirement, go back and investigate the page rendering, the recording configuration, or the transcoding parameters.

    Under sample conditions of a working local browser, 1920×1080, a 15-second video, a cached page, and a non-first load, a single export takes about one minute. Actual timing varies with browser version, machine performance, video duration, frame rate, and first-load cost.

    5. Cost and Benefit: Read It With the Assumptions Attached

    Once the spec, engine, and export script are captured as a Skill, the same animation request no longer requires reasoning from scratch every time. To avoid “claiming savings without evidence,” here is a reproducible example with its assumptions stated.

    Cost formula:

    1. Call cost = input tokens / 1,000,000 × input price + output tokens / 1,000,000 × output price

    Example assumptions:

    • Model input price: USD 3 / million tokens
    • Model output price: USD 6 / million tokens
    • Measurement window: 10 consecutive working days
    • Workload: 3 tasks per day, 30 tasks total
    • Task complexity: rule-based 2D stickman animation
    Metric Bare-prompting group Skill group
    Tasks 30 30
    Average generations 2.7 per task 1.2 per task
    Average rework rounds 12 2
    Average input tokens 700,000 / task 120,000 / task
    Average output tokens 120,000 / task 30,000 / task
    Monthly input total 21,000,000 3,600,000
    Monthly output total 3,600,000 900,000
    Input cost USD 63 USD 10.8
    Output cost USD 21.6 USD 5.4
    Total cost ~USD 84.6, roughly USD 86 ~USD 16.2, roughly USD 16

    This result holds only under the assumptions of this example. If the tasks are more complex, the model prices are higher, there are more motions, or heavy manual review is required, the cost picture will differ. Put differently, the value of a Skill is not a guarantee of savings in every scenario — it is converting “repeated reasoning” into “incremental edits.”

    The benefits show up in three areas:

    1. Less rework: fixed character proportions and motion rules reduce structure-level rewrites.
    2. Lighter iteration: a new scene only changes motion parameters, captions, or the timeline.
    3. More complete delivery: rendering and export are scripted, so a finished animation can be turned into video directly.

    6. Common Mistakes and a Troubleshooting Checklist

    In practice, check the following first:

    • Proportion drift: verify that skill.md explicitly defines headRatio, torso, arm, and leg, and confirm that engine.js actually reads those parameters.
    • Stuttering motion: check whether the page renders stably, whether recording and transcoding use a consistent frame rate, and whether the browser is dropping frames.
    • Letterboxing on export: check whether the viewport is 1920×1080, whether the page is being scaled, and whether transcoding preserves the aspect ratio.
    • Inconsistent frame rate: confirm that animation time is based on performance.now() or a fixed timestep rather than an uncontrolled timer.
    • Abnormal file size: check whether an overly long blank segment was recorded, whether the export ran twice, or whether temporary files were left behind.
    • Model rewrote the character structure: check whether skill.md requires reusing engine.js first and forbids rewriting the skeleton structure directly.
    • Wrong browser path: if you are not using Playwright’s bundled Chromium, confirm that CHROME_PATH points to a real executable.
    • Export parameters do not match: use ffprobe to check r_frame_rate, width, height, and codec_name, and investigate duration deviations and a missing audio track if needed.

    7. FAQ: Who Is This Approach For?

    Which AI coding tools does a Skill work with?
    Tools that can read project context, create files, execute in a terminal, or call scripts. If a tool can only chat and cannot access files or run scripts, the reuse value of a Skill drops considerably.

    How do you avoid wasting usage quota?
    Establish a bare-prompting baseline first to confirm the problem, then write the stable structure into a Skill, and from then on have the model make only incremental edits rather than rewriting the whole thing.

    How detailed should the spec file be?
    As close to executable constraints as possible. At minimum it should cover character proportions, joint limits, motion ranges, frame rate, resolution, output format, and prohibitions.

    Does this work for complex animations?
    It fits rule-based, 2D, single-page stickman or simple-line character animation best. Complex 3D, interactive animation, and high-fidelity rendering are not recommended as a direct fit — they need additional engines and manual review.

    Bottom Line

    The problem with hand-building animations is not that a single generation is too slow; it is that the experience never gets captured. The practical order of operations:

    1. Run one bare-prompting baseline first, recording elapsed time, output quality, rework rounds, and cost.
    2. Write character proportions, motion rules, and the rendering engine into skill.md and engine.js.
    3. Export MP4 through a script, and verify resolution, frame rate, and codec with ffprobe.
    4. From then on, have the model make only incremental edits instead of rewriting from zero.

    With the skeleton baseline, motion rules, rendering engine, and export script packaged as a Skill, an AI coding tool moves from “gambling every time” to “iterating incrementally against a spec.” In rule-based animation scenarios, that improves both visual consistency and the predictability of call costs.