p5-paint-animation

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

p5 Paint Animation

p5 绘画动画

Three flows, one engine. Pick by input:
InputFlowOutput
Text prompt / phraseA: Write-onHandwriting animates onto ruled paper, pen-tracked for camera
PhotoB: Repaint + paint-onBrushstroke painting, still or painting-itself film, camera pull
Video / Live PhotoC: Living paintingEvery frame repainted over a locked stroke lattice
三种流程,一个引擎。根据输入类型选择:
输入流程输出
文本提示/短语A:书写动画手写文字在格纸上逐笔呈现,镜头追踪笔的轨迹
照片B:重绘+绘画动画笔触风格画作,静态图或逐步绘制的影片,镜头拉远效果
视频/Live PhotosC:动态画作每一帧都基于固定笔触网格重绘

When to use / when not

适用场景与不适用场景

Use for: hand-made-looking motion — handwriting that writes itself, photos as brushstroke paintings (still or animating), short clips as living paintings. Do NOT use for: crisp typography or brand-accurate text (use real fonts), photoreal output, footage longer than ~10s (render cost scales per frame ≈ 5–20s each), or live/interactive pages (this renders offline MP4/PNG).
适用场景:需要手绘风格动态效果的场景——自动书写的手写文字、笔触风格的画作(静态或动态绘制)、短视频转动态画作。 不适用场景:需要清晰排版或品牌精准文字的场景(请使用真实字体)、照片级写实输出、时长超过约10秒的素材(渲染成本随帧数增长,每帧约5-20秒)、实时/交互式页面(本工具仅生成离线MP4/PNG文件)。

Requirements and side effects

运行要求与副作用

  • Requires: Node 22.12+, ffmpeg on PATH, ~500MB disk (Chromium).
  • sh scripts/setup.sh
    (run manually, once) uses the committed lockfile to download pinned dependencies from the npm registry: puppeteer 25.9.0, p5 2.3.2 (LGPL-2.1), and p5.brush 2.2.1 (MIT). Puppeteer downloads its pinned Chrome for Testing build from Google's download service. Read-only downloads; no credentials, no cost, nothing uploaded.
  • Renders launch sandboxed headless Chromium on a blank page. Network and local file requests from sketches are blocked; sketches receive inputs as data URLs.
  • Writes only inside paths you pass (
    out.png
    /
    out.mp4
    , plus a sibling
    .track.json
    and, with
    --keep-frames 1
    , a
    _frames_*
    directory).
  • Optional, only for flow C subject matting:
    npx hyperframes remove-background
    (downloads the HyperFrames CLI from npm; local CoreML inference, no data leaves the machine).
  • 运行要求:Node 22.12+、ffmpeg已添加至PATH、约500MB磁盘空间(用于Chromium)。
  • 执行
    sh scripts/setup.sh
    (手动运行一次)会使用已提交的锁文件从npm注册表下载固定版本的依赖:puppeteer 25.9.0、p5 2.3.2(LGPL-2.1协议)、p5.brush 2.2.1(MIT协议)。Puppeteer会从谷歌下载服务下载其固定版本的Chrome for Testing构建包。所有下载均为只读;无需凭证、无费用、无数据上传。
  • 渲染时会启动沙箱化的无头Chromium,在空白页面运行。草图的网络和本地文件请求会被阻止;草图通过数据URL接收输入。
  • 仅在指定路径内写入文件(
    out.png
    /
    out.mp4
    ,以及同级的
    .track.json
    文件,若指定
    --keep-frames 1
    则会生成
    _frames_*
    目录)。
  • 可选功能(仅适用于流程C的主体抠图):
    npx hyperframes remove-background
    (从npm下载HyperFrames CLI;本地CoreML推理,数据不会离开设备)。

Verify success

验证运行成功

Render any command below, then open the output: the last frame must show the complete artwork (text fully written / painting fully covered), and re-running with the same
--seed
must produce identical bytes on the canvas. The smoke test used for review:
node render-anim.mjs sketches/handwriting.anim.js /tmp/t.mp4 --size 1600x900 --seed 7 --inject 'PHRASE="go make something"'
.
运行以下任意命令,然后打开输出文件:最后一帧必须显示完整作品(文字完全书写完毕/画作完全覆盖画布),且使用相同
--seed
重新运行必须生成完全相同的画布字节数据。审核用冒烟测试命令:
node render-anim.mjs sketches/handwriting.anim.js /tmp/t.mp4 --size 1600x900 --seed 7 --inject 'PHRASE="go make something"'
。

Setup (once)

首次配置

sh scripts/setup.sh
Installs the exact dependency tree recorded in
scripts/package-lock.json
. All commands below run from
scripts/
.
sh scripts/setup.sh
该命令会安装
scripts/package-lock.json
中记录的精确依赖树。以下所有命令均需在
scripts/
目录下运行。

Flow A — prompt → write-on animation

流程A——文本提示→书写动画

node render-anim.mjs sketches/handwriting.anim.js out.mp4 \
  --size 1600x900 --fps 24 --seed 7 --hold 12 \
  --inject 'PHRASE="your text here";SIZE=170'
  • Full alphabet: lowercase, caps, digits,
    ? . , - / @
    . Wraps automatically.
  • Dials via
    --inject
    :
    SIZE
    ,
    INK
    (hex),
    BRUSH
    (
    "2B"
    pencil,
    "crayon"
    ,
    "charcoal"
    ),
    PAPER=1
    for blank paper.
  • The run writes
    out.track.json
    — per-frame pen position. For a camera-follows-pen move, crop each frame along the smoothed track (exponential follow, gain ~0.14, camera y LOCKED to the baseline — never follow the pen tip's y or the camera bounces on every ascender).
  • For scraggly/messy writing raise the per-letter jitter in the sketch; for word-sync to a voiceover, transcribe first and start each word's strokes at its spoken time.
node render-anim.mjs sketches/handwriting.anim.js out.mp4 \
  --size 1600x900 --fps 24 --seed 7 --hold 12 \
  --inject 'PHRASE="your text here";SIZE=170'
  • 支持全字符集:小写字母、大写字母、数字、
    ? . , - / @
    。自动换行。
  • 通过
    --inject
    参数调整:
    SIZE
    (字号)、
    INK
    (十六进制颜色值)、
    BRUSH
    (画笔类型,如
    "2B"
    铅笔、
    "crayon"
    蜡笔、
    "charcoal"
    炭笔)、
    PAPER=1
    (使用空白纸张)。
  • 运行后会生成
    out.track.json
    文件——记录每帧的笔位置。若要实现镜头跟随笔移动的效果,需沿平滑后的轨迹裁剪每一帧(指数跟随,增益约0.14,镜头Y轴锁定至基线——切勿跟随笔尖Y轴,否则镜头会在每个字母的上升部分跳动)。
  • 若要生成潦草/随性的字迹,可提高草图中每个字母的抖动值;若要实现文字与旁白同步,需先转录旁白,再在对应时间点开始每个单词的笔画渲染。

Flow B — photo → painting (+ paint-on film + camera)

流程B——照片→绘画(含绘画动画+镜头效果)

Still:
node render.mjs sketches/repaint.anim.js out.png \
  --size 720x900 --seed 3 --image photo.jpg --inject "STYLE=0;DETAIL=3"
Paint-on film (the painting paints itself, coarse-to-fine, painter-ordered — calm regions first, the face last):
node render-anim.mjs sketches/repaint.anim.js out.mp4 \
  --size 720x900 --seed 3 --image photo.jpg --fps 14 --hold 40 \
  --inject 'STYLE=0;DETAIL=3;SUBJECT="0.5,0.5,0.44,0.55"'
  • Crop to the subject first. Face size in frame is destiny: selfie-distance faces resolve beautifully; small faces stay figures. Portrait 4:5 or 9:16.
  • Dials:
    STYLE
    0 pastel / 1 gouache / 2 charcoal-sketch.
    DETAIL
    1 painterly, 2 faces resolve, 3 soft features (noses), 5 portrait-grade (adds a second fine lap).
    LOOSE
    0.7–1.5 stroke wildness.
    SAT
    color compensation (~1.1).
  • SUBJECT="cx,cy,rx,ry"
    (image fractions) paints everything inside that ellipse through all passes FIRST and drops a
    subject-done
    frame marker. Camera pull-out recipe: hold tight on the subject, then at the marker frame ease wide with ffmpeg zoompan (cosine ease, ~2s):
    zoompan=z='if(lte(in,M),1.9,1.85-0.85*(0.5-0.5*cos(PI*min((in-M)/28,1))))'
    (upscale 2x lanczos first so the tight phase stays sharp).
静态图:
node render.mjs sketches/repaint.anim.js out.png \
  --size 720x900 --seed 3 --image photo.jpg --inject "STYLE=0;DETAIL=3"
绘画动画(画作逐步完成,从粗糙到精细,按绘画顺序——先绘制平静区域,最后绘制面部):
node render-anim.mjs sketches/repaint.anim.js out.mp4 \
  --size 720x900 --seed 3 --image photo.jpg --fps 14 --hold 40 \
  --inject 'STYLE=0;DETAIL=3;SUBJECT="0.5,0.5,0.44,0.55"'
  • 先裁剪主体区域。画面中面部的大小决定最终效果:自拍距离的面部会呈现极佳细节;小面部仅能保留轮廓。建议使用4:5或9:16的竖版比例。
  • 可调参数:
    STYLE
    0=淡彩/1=水粉/2=炭笔素描;
    DETAIL
    1=写意风格、2=面部可识别、3=柔和五官(如鼻子)、5=人像级精度(添加第二层精细笔触);
    LOOSE
    0.7–1.5=笔触随性程度;
    SAT
    ≈1.1=色彩补偿。
  • SUBJECT="cx,cy,rx,ry"
    (图像比例值)会优先绘制椭圆区域内的所有内容,并标记
    subject-done
    帧。镜头拉远方案:先聚焦主体,在标记帧处通过ffmpeg的zoompan实现平滑拉远(余弦缓动,约2秒):
    zoompan=z='if(lte(in,M),1.9,1.85-0.85*(0.5-0.5*cos(PI*min((in-M)/28,1))))'
    (先使用lanczos算法放大2倍,确保聚焦阶段画面清晰)。

Flow C — video / Live Photo → living painting

流程C——视频/Live Photos→动态画作

node render-video.mjs clip.mov out.mp4 \
  --size 720x900 --fps 12 --seed 7 --inject "STYLE=0;DETAIL=2"
  • The stroke lattice is built ONCE from frame 1 and locked; only colors and orientations resample per frame. Strokes hold still — the painting never "boils" while the subject moves.
  • Live Photos: get the paired
    .mov
    (share-sheet Options → Live Photo ON, or Photos.app → Export Unmodified Original). If it arrives as bare JPEG the motion was stripped in transit — ask for a 2–3s normal video instead; that's better input anyway.
  • Motion triage first. Live Photos include the phone swinging. Measure per-frame motion (
    ffprobe -f lavfi "movie=in.mov,signalstats"
    → YDIF), paint only the calm window with
    --start/--dur
    , and ping-pong the result (forward+reverse concat) for a seamless breathing loop.
  • Big camera motion?
    WORLD=1
    tracks the camera by image correlation and lets strokes live in world coordinates — the camera pans across one growing painting. Translation-only; fast zooms drift.
  • Subject-only painting: matte frames first (
    npx hyperframes remove-background
    ), pass the matted PNGs with
    --frames DIR
    and
    MASKED=1
    , and set
    BG
    to your paper color.
  • Dark footage: grade with gamma only (
    eq=gamma=1.2
    ). Never boost saturation before painting — shadow chroma noise becomes literal teal paint.
node render-video.mjs clip.mov out.mp4 \
  --size 720x900 --fps 12 --seed 7 --inject "STYLE=0;DETAIL=2"
  • 笔触网格仅基于第一帧构建并锁定;仅每帧重新采样颜色和笔触方向。笔触保持静止——主体移动时画作不会出现“抖动”。
  • Live Photos:获取配对的
    .mov
    文件(分享选项→开启Live Photos,或通过照片应用→导出未修改的原始文件)。若仅收到JPEG文件,说明传输过程中丢失了动态信息——请改用2-3秒的普通视频作为输入,效果更佳。
  • 先筛选动态片段。Live Photos通常包含手机晃动的画面。使用
    ffprobe -f lavfi "movie=in.mov,signalstats"
    测量每帧运动幅度(查看YDIF值),仅使用
    --start/--dur
    参数渲染平稳片段,再将结果正反向拼接形成无缝循环的呼吸效果。
  • 镜头大幅移动?设置
    WORLD=1
    可通过图像关联追踪镜头,让笔触处于世界坐标系——镜头在一幅不断生成的画作上平移。仅支持平移;快速缩放会出现偏移。
  • 仅绘制主体:先对帧进行抠图(
    npx hyperframes remove-background
    ),通过
    --frames DIR
    和
    MASKED=1
    传入抠图后的PNG文件,并设置
    BG
    为纸张颜色。
  • 深色素材:仅调整gamma值(
    eq=gamma=1.2
    )。切勿在绘画前提高饱和度——阴影中的色度噪点会变成明显的蓝绿色笔触。

QC gates (all flows)

质量检查标准(所有流程)

  • Read the actual output frames; never ship on intent. Check: faces resolve, no bare-canvas gaps, first/mid/last frames coherent.
  • Same seed = identical render. Iterate one dial at a time against a fixed seed.
  • For anything joining a larger film, verify seams numerically per motion-doctrine — this skill produces material, not final grammar.
references/flows.md
has full worked recipes;
references/gotchas.md
is the list of failure modes already hit so you don't hit them again.
  • 查看实际输出帧;切勿仅凭预期交付。检查要点:面部细节清晰、画布无空白区域、首尾及中间帧连贯。
  • 相同seed值应生成完全相同的渲染结果。每次仅调整一个参数并固定seed值进行迭代。
  • 若要将生成内容整合到更长影片中,请根据运动规则验证衔接处的数值一致性——本工具生成素材,不提供最终剪辑逻辑。
references/flows.md
包含完整的实践方案;
references/gotchas.md
记录了已遇到的失败场景,帮助你避免重复踩坑。