Skip to main content

CodeAnim

用 Python 代码描述动画,一条命令渲染成视频文件。无窗口、无交互、不弹播放器,跑完就落盘。

python main.py examples/demo.py
# -> out.mp4

默认零落盘:帧经管道直接进 ffmpeg,中途不写任何临时文件。 磁盘占用只有最终视频那么大 —— 4K / 高质 / 长片也敢跑。


安装

需要 Python 3.8+、Pillow、ffmpeg。

pip install codeanim-py      # 发行名带 -py 后缀(codeanim 已被占用)
codeanim doctor              # 检查依赖,缺什么会告诉你装什么

导入名仍然是 codeanim —— 写代码时不用管发行名:

from codeanim import Scene      # 不是 codeanim_py

装完会有两个等价入口:

codeanim demo.py -o out.mp4
python -m codeanim demo.py -o out.mp4      # 效果一样

不装包也能直接跑(开发引擎本身时最方便):

python -m pip install -r requirements.txt
python main.py doctor

命令

python main.py <脚本.py> [选项]
python main.py doctor
选项 作用 默认
-o, --output 输出路径,扩展名决定格式 out.mp4
--fps 帧率 30
--quality low / medium / high / lossless high
--workers, -w 并行进程数 CPU 核数−1
--snapshot <秒> 只出一张单帧 PNG —
--frames <目录> 导出 PNG 序列 —
--gif 导出 GIF —
--keep-frames 保留中间帧(调试用) 关
--supersample <倍数> 超采样倍数 2
--mode <模式> 帧存储:stream / ram / disk 自动
--alpha 输出带透明通道(.mov / .webm) 自动
--ffmpeg <路径> 指定 ffmpeg ffmpeg
--watch, -W 热重载:改代码自动刷新预览 关
--watch-at <秒> 热重载预览的时间点 0
--watch-output <路径> 热重载预览图路径 .codeanim/watch_preview.png
--inline 热重载时在终端直接画画面 自动判断
--no-inline 不画终端内嵌画面 —
--preview <秒> 快速预览:渲一张图并尝试打开 —
--no-audio 忽略场景里的音频轨道 关
--list-audio 只列出音频轨道,不渲染 —
--layers 只打印图层绘制顺序,不渲染 —
--layers-at <秒> --layers 求值的时间点 0
--quiet, -q 只输出错误 关
--free-margin <大小> 保留的空闲磁盘红线 256MB
--force 空间检查不通过也硬渲(不推荐) 关
--no-disk-check 跳过所有磁盘检查 关
python main.py demo.py --watch               # 开着热重载改代码
python main.py demo.py --watch --watch-at 2.5  # 盯第 2.5 秒那一帧
python main.py demo.py --preview 2.0         # 渲一张图并打开
python main.py demo.py --snapshot 2.0        # 只出图,不打开
python main.py demo.py --gif                 # 出 GIF
python main.py demo.py --frames pngs/        # 导出 PNG 序列
python main.py demo.py -o out.mov --alpha    # 带透明通道

开发流程:热重载

写动画最烦的是「改一个数字 → 跑一条命令 → 等 → 打开文件看」。 --watch 把中间全砍掉:

python main.py examples/demo.py --watch
CodeAnim 热重载
────────────────────────────────────────────────────────
  入口    examples/demo.py
  监视    7 个 .py 文件
          examples/ball.py
          examples/demo.py
          examples/logo.py
          ... 还有 4 个
  预览    .codeanim/watch_preview.png
  时间点  t=2.00s
────────────────────────────────────────────────────────
  改文件即自动重渲染,Ctrl-C 退出。

✓ 已更新  1280x720  8.60s  30 fps  11 个图元
   预览  .codeanim/watch_preview.png  (t=2.00s)
   耗时  814 ms

↻ 检测到改动: examples/demo.py
✓ 已更新  1280x720  8.60s  30 fps  11 个图元
   耗时  225 ms

行为:

  • 监视入口脚本和它 import 的所有同项目 .py —— 改片段文件也会触发
  • 只渲染一帧,不跑整段视频,通常 200~800 ms 出结果
  • 报错不退出:语法错 / 运行错都只打印,等你下次保存
  • 编辑器保存时的多次写事件会防抖合并成一次
  • 新增的 .py 文件会被自动纳入监视
  • 无第三方依赖,纯轮询 mtime,跨平台

无图形界面的环境(服务器 / Termux)也能用:预览图就是普通 PNG, 你可以用任何方式查看,或者 codeanim 会尝试 xdg-open / open / am start 帮你打开。


终端内嵌画面

不想开图片?直接让画面在终端里出现:

python main.py examples/demo.py --watch --inline
CodeAnim 热重载
────────────────────────────────────────────────────────
  入口    examples/demo.py
  预览    .codeanim/watch_preview.png
  时间点  t=4.00s
────────────────────────────────────────────────────────
  改文件即自动重渲染,Ctrl-C 退出。
✓ 已更新  1280x720  8.60s  30 fps  11 个图元   耗时 1.46 s

  ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
  ▄▄▄▄  ▄▄▄▄▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄▄▄▄▄▄▄
  ▄▄▄  CodeAnim  用 Python 写动画  ▄▄▄
  ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
       ●                                ●
  t=4.00s   11 个图元   .codeanim/watch_preview.png

原理:终端字符格子通常宽:高 ≈ 1:2,用 上下半块 ▄ 让一个字符格表示两个垂直像素, 宽高比自然正确。80×24 的终端 ≈ 80×48 个颜色块 —— 够看构图、配色、位置。

  • 真彩色(24-bit)优先,退化为 256 色,再退化为灰度字符
  • 每次重渲染原地覆盖(ANSI 光标移动),不刷屏
  • 自动判断终端能力;用 --no-inline 关掉,--inline 强制开
  • 也可以用 API:
from codeanim import render_terminal_preview

print(render_terminal_preview(scene, at=2.0))          # 打印一段带色字符串
lines = preview_lines(scene, at=2.0, max_cols=80)      # 拿行列表

手感糖

写起来更短、更少心算。都是可选的,不改引擎语义。

一行建并加

dot = scene.add_circle(cx=100, cy=100, r=20, fill="red", z=5)
box = scene.add_rect(cx=640, cy=360, w=200, h=100, radius=16)
cap = scene.add_text("标题", cx=640, cy=120, font_size=48)
ln  = scene.add_line(x=0, y=600, x2=1280, y2=600, stroke="#333")

自动算时长(不用再手算 start)

片段里打了一堆 key(),但不想手算它多长:

title_objs = add_title(scene)
print(span_of(title_objs))        # => 3.2   (最晚的关键帧时间)

# 或者直接让场景撑到刚好
add_title(scene)
add_ball(scene)
scene.fit_duration(0.5)           # 撑到最长 + 末尾留 0.5 秒

span_of 认图元、Group(递归子项)、属性句柄、嵌套列表。

改文案不用重建

title = scene.add_text("旧标题", cx=640, cy=100, font_size=64)

set_text(title, "新标题")          # 函数式
title.set_text("再改一次")          # 方法式,可链式
title.content = "也可以直接赋值"     # 属性式

print(title.content)              # 读当前文案

批量添加

scene.add_all(make_particles(), z=5)               # 列表
scene.add_all(Circle(r=3) for _ in range(100))     # 生成器
scene.add_all([[a, b], c])                          # 嵌套
g = scene.add_group(Rect(...), Circle(...), cx=640, cy=360, z=10)

自动排布:row / grid

from codeanim import row, grid

# 一排点,按自身尺寸 + 间距自动推进
dots = row(scene, [{"r": 8}] * 5, cls=Circle,
           gap=10, start=50, axis="x", cy=100, fill="#38bdf8")
# => 中心落在 x = 50, 76, 102, 128, 154

# 网格
tiles = grid(scene, rows=4, cols=6, cell_w=30, cell_h=30,
             gap_x=10, gap_y=10, origin=(100, 100),
             make_kwargs=lambda r, c: {"fill": COLORS[(r * 6 + c) % 7]})

Digits 更短的写法

d = Digits(value=0, cx=640, cy=120, font_size=64, thousands=True)

d.from_to(0, 12345.678, start=0.5, end=2.5, ease="outCubic")  # 一行滚完
d.set_value(100)        # 链式设值
print(d.content)        # 当前显示的文本

Digits 自定义格式化

内置的 decimals / thousands / prefix / suffix 不够用时,用 formatter=:

# 1) 传函数 —— 任意逻辑
Digits(value=0, formatter=lambda v: f"¥{v:,.2f}")
Digits(value=0, formatter=lambda v: f"{v/10000:.2f} 万" if v >= 10000 else f"{v:.0f}")

# 2) 传 format spec —— 短
Digits(value=0, formatter="{:,.1f} kg")
Digits(value=0, formatter="{:+.2f}")
Digits(value=0, formatter="{:05.0f}")

想在自定义逻辑里复用内置规则,用 default_formatter 造基础格式器:

from codeanim import Digits, default_formatter

base = default_formatter(decimals=1, thousands=True, suffix=" 元")
Digits(value=0, formatter=lambda v: (
    "%.2f 万" % (v / 10000) if v >= 10000 else base(v)))

给了 formatter 后,decimals / thousands / prefix / suffix / sign / min_int_digits 会被忽略 —— 格式化完全由你的函数接管。 函数抛异常不会让渲染挂掉,那一帧会退回原始数值。


磁盘安全(重要)

高质动画最容易踩的坑就是把磁盘写爆:4K/60fps/60s 的全 PNG 序列约 21 GB。 CodeAnim 用三层防护解决:

1. 默认零落盘

默认走 stream 模式:渲染好的帧直接经管道喂给 ffmpeg 的 stdin, 全程不产生任何中间文件,磁盘占用 = 最终视频大小。

python main.py demo.py -o out.mp4      # 4K 也不会爆盘

2. 要落盘就提前算账

只有你明确要求 --frames 或 --keep-frames 时才走落盘模式。 这时会先预估需要多少空间,不够就直接拒绝并给出建议,而不是跑到一半炸掉:

磁盘空间不够,无法把中间帧全部存成 PNG。

  画面尺寸:   3840 x 2160
  帧数:       3600
  预计需要:   15.30 GB
  可用空间:   2.10 GB(/mnt/data,已预留 15% 余量)

三个解决办法(推荐第一个):
  1. 去掉 --frames / --keep-frames,改用默认的零落盘流式渲染:
       python main.py 你的脚本.py -o out.mp4
     整个过程不写任何中间帧,磁盘只占最终视频那点大小。
  2. 换个空间充足的输出目录:
       python main.py 你的脚本.py --frames /别的/盘/目录/
  3. 降低分辨率或帧率,减少总数据量。

3. 渲染途中还有一道红线

流式模式也会每 40 帧查一次剩余空间,低于 --free-margin 就中止渲染, 避免把设备写到卡死。临时文件在正常结束 / 报错 / Ctrl-C 时都会被清掉。


写一个动画

from codeanim import Scene, Circle, Digits, Text

scene = Scene(1280, 720, duration=5, fps=30, background="#0b0f19")

# 用 cx/cy 按"中心"定位,不用手算偏移
ball = Circle(cx=200, cy=360, r=60, fill="red")
ball.cx.keys([(0, 200), (2, 1000)])          # 中心从 x=200 到 x=1000
ball.fill.keys([(0, "red"), (2, "#00aaff")])  # 颜色平滑过渡
scene.add(ball)

# 会滚动的数字
n = Digits(value=0, cx=640, cy=120, font_size=64, fill="white",
           thousands=True, suffix=" fps")
n.value.keys([(0, 0), (2, 3000)], ease="outCubic")
scene.add(n)

scene.add(Text("你好,CodeAnim", cx=640, cy=640, font_size=48, fill="#e2e8f0"))
python main.py my_anim.py

定位:x/y 与 cx/cy

写法 含义
x=, y= 自然锚点。圆是圆心,矩形是左上角,线是起点,文字由 anchor 决定
cx=, cy= 几何中心。给矩形/文字用最省心,旋转也绕中心

两种可以混用:Rect(cx=100, y=50, ...) 表示"水平中心在 100,顶边在 50"。

想让中心动起来:

box.cx.keys([(0, 100), (2, 800)])   # 或者 box.center_x().keys([...])

cx / cy 走独立的"中心通道",x / y 始终是绘制坐标(左上角)。 即使 w / h / r 同时在变,中心点依然精确。

时间系统

时间全是绝对秒,不是相对偏移。

ball.x.key(0, 100)          # 第 0 秒在 x=100
ball.x.key(2.5, 800)        # 第 2.5 秒在 x=800

ease= 写在起始关键帧上,描述"从这一帧走向下一帧"的曲线:

ball.x.key(0, 0, ease="outBack").key(1, 500)   # 回弹
ball.x.key(1, 500, ease="linear").key(2, 0)    # 匀速

批量写:

ball.opacity.keys([(0, 0), (0.5, 1), (4, 1), (5, 0)])

25 条内置曲线:linear、inQuad/outQuad/inOutQuad、inCubic/outCubic/inOutCubic、 inQuart/outQuart/inOutQuart、inSine/outSine/inOutSine、 inExpo/outExpo/inOutExpo、inBack/outBack/inOutBack、 inElastic/outElastic/inOutElastic、inBounce/outBounce/inOutBounce。

名字不区分大小写,"out-cubic" 也能用。

图元

图元 位置 主要参数
Circle 圆心 r, fill, stroke, stroke_width
Rect 左上角 w, h, radius(圆角), fill, stroke
Line x,y → x2,y2 stroke, stroke_width
Text 锚点 text, font_size, anchor, fill, weight, letter_spacing
Image 左上角 source, w, h, crop, slice, tint, radius, fit
Digits 锚点 value, decimals, thousands, prefix, suffix, sign, min_int_digits, formatter
Group cx,cy rotation, scale, scale_x/y, opacity

坐标原点在左上角,y 轴向下。

可动画属性

x y cx cy w h r radius rotation scale scale_x scale_y opacity fill stroke stroke_width text font_size letter_spacing value

颜色平滑插值;text 按时点切换;Digits.value 是数值,插值后自动格式化。

图片:把素材当普通物体

from codeanim import Image, load_image, load_or_placeholder

# 一行建并加
logo = scene.add_image("logo.png", cx=640, cy=360, w=200, radius=16)
logo.rotation.keys([(0, 0), (2, 360)])

# 只给 w,按原图比例算 h
scene.add_image("hero.png", cx=300, cy=200, w=240)

# 同一张图复用多次(只加载一次)
tex = load_image("icon.png")
scene.add_image(tex, cx=200, cy=500, w=80)
scene.add_image(tex, cx=320, cy=500, w=80, tint="#22d3ee")

# 还没图?用占位棋盘格,先跑起来
scene.add_image(load_or_placeholder("还没上传.png"), cx=100, cy=100, w=60)
# 或者直接
scene.add_image(None, cx=100, cy=100, w=60, h=60)
参数 作用
w / h 只给一个,按原图比例算另一个;都不给就用原尺寸
crop=(x,y,w,h) 从原图裁一块(雪碧图)
slice= 九宫格拉伸:12 或 (12,) 或 (l,t,r,b) —— 缩放时边角不变形
radius= 圆角
tint= + tint_amount= 着色,做单色图标
fit= stretch(默认)/ contain / cover

图片是普通图元:cx/cy 定位、rotation、scale、opacity、z、 丢进 Group —— 全都照常工作。

支持 PNG / JPG / WEBP / BMP / GIF(首帧)等常见格式。 位图带缓存:尺寸/着色没变时每帧几乎零成本。

音频:导入 + 生成

配置刻意保持朴素 —— 一句话说清"什么声音、第几秒响、多大声"。

from codeanim import Audio, beep, chime, sweep, whoosh, arpeggio

# ── 1. 导入现成文件 ──
scene.add_audio(Audio("bgm.mp3", at=0.0, volume=0.35,
                      loop=True, fade_in=0.5, fade_out=2.0))

scene.add_audio(Audio("whoosh.wav", at=3.5))

# 只要源文件第 3-8 秒那一段,放到第 2 秒处
scene.add_audio(Audio("speech.mp3", at=2.0, trim=(3.0, 8.0)))

# ── 2. 用代码生成,不需要任何素材 ──
scene.add_audio(beep(880, duration=0.2, at=1.0))        # 一声"哔"
scene.add_audio(chime(at=5.0))                           # 有余韵的"叮"
scene.add_audio(whoosh(at=2.6))                          # 转场"嗖"
scene.add_audio(sweep(200, 1600, duration=0.35, at=3.0)) # 上升扫频
scene.add_audio(arpeggio([523, 587, 659, 784], at=7.4))  # 上行音阶

Audio 的参数:

参数 含义
at 从第几秒开始播(绝对时间,毫秒精度)
volume 音量倍数,1.0 为原声
loop 循环填满剩下的时长
fade_in / fade_out 淡入 / 淡出秒数
trim=(起, 止) 只取源文件的一段
speed 变速(会同时改音调)
volume_curve 音量曲线,见下
pan 声像:-1 全左 / 0 居中 / +1 全右
delay 回声:秒数 / (延迟, 衰减) / (延迟, 衰减, 次数)
reverb 混响量 0-1,越大越"空旷"
tone 亮度:负数更闷、正数更亮

音频:用音名写音乐

不用记频率,直接写音名 / 简谱:

from codeanim import note, chord, scale, melody, parse_melody

note("A4")                   # -> 440.0
note("C#5")                  # -> 554.37(升号)
note("Db5")                  # 同音(降号)
chord("A3", "m7")            # -> [220.0, 261.6, 329.6, 392.0]
scale("C4", "major")         # 大调 7 个音
scale("A3", "minor_pentatonic")   # 五声音阶

可用的和弦:maj min dim aug sus2 sus4 5 6 m6 7 maj7 m7 mmaj7 dim7 m7b5 add9 9 m9 maj9

可用的音阶:major minor harmonic_minor melodic_minor dorian phrygian lydian mixolydian locrian major_pentatonic minor_pentatonic blues chromatic whole_tone

写出旋律:

# 音名
scene.add_audio(melody("C4 D4 E4 F4 G4", note_dur=0.2, at=0.0))

# 简谱(1=do … 7=si;' 升八度,, 降八度)
scene.add_audio(melody("1 2 3 4 5 6 7 1'", note_dur=0.18))
scene.add_audio(melody("1, 2, 3,"))          # 低八度
scene.add_audio(melody("1 2 3", solfege_key="G"))   # G 调
scene.add_audio(melody("#1 b3"))             # 升 / 降号

# 和弦进行
scene.add_audio(melody([["C4","E4","G4"], ["F4","A4","C4'"]], note_dur=0.8))

# 单独弹一个和弦
scene.add_audio(chord_sound("C4", "maj", duration=1.5, at=0.0))

音频:音色与包络

默认是干巴巴的正弦。加音色和 ADSR 包络就自然多了:

from codeanim import Tone, Envelope

# 音色:用泛音表堆出来的
Tone(freq=note("C4"), duration=1.5, voice="epiano")
Tone(freq=note("C5"), duration=2.0, voice="bell", reverb=0.6)
Tone(freq=note("E4"), duration=1.0, voice="strings", vibrato=(5, 25))

# ADSR 包络:起音 / 衰减 / 延音 / 释音
Tone(freq=note("C4"), duration=1.5, voice="epiano",
     envelope=Envelope(attack=0.01, decay=0.25,
                       sustain=0.45, release=0.5))
# 也能直接给四元组
Tone(freq=440, duration=1.0, envelope=(0.01, 0.2, 0.5, 0.4))
预置音色 像什么
sine square triangle saw 基础波形
flute 长笛 / 管乐
strings 弦乐(泛音丰富)
epiano 电钢琴
bell 铃 / 钟
marimba 马林巴 / 木琴
bass 低音贝斯
music_box 八音盒

也能自己给泛音表:

Tone(freq=440, partials=[(1, 1.0), (2, 0.5), (3, 0.25), (5, 0.1)])

其他音色微调:

Tone(freq=440, detune=12)          # 轻微失谐(音分),更"暖"
Tone(freq=440, vibrato=(5, 30))    # 颤音(速率Hz, 深度音分)
Tone(freq=440, tremolo=(8, 0.6))   # 音量颤动(速率Hz, 深度0-1)

音频:音量曲线

想让 BGM 前 3 秒渐强、后面渐弱:

Audio("bgm.mp3", at=0, volume_curve=[(0, 0), (3, 1), (6, 0)])
Audio("x.mp3", volume_curve=[0.0, 0.5, 1.0])        # 每秒一个的简写
Audio("x.mp3", at=1.0, volume_curve=[(1,0),(3,1)])  # 绝对秒自动换算

音频:分组批量调

from codeanim import AudioGroup

sfx = AudioGroup([whoosh(at=2.0), beep(880, at=3.0), pulse(4, at=5.0)],
                 name="音效")

sfx.volume = 0.6        # 整组音量乘 0.6(可反复设)
sfx.shift(0.5)          # 整组往后挪 0.5 秒
sfx.scale_time(1.5)     # 整组时间拉长 50%
sfx.apply(fade_out=0.3) # 批量设参数
scene.add_audio(sfx)

sfx.mute()              # 整组静音(排查画面时很有用)
sfx.unmute()            # 还原

音频:其他音效

from codeanim import noise, pulse

# 噪声:风声 / 摩擦声 / 底噪
scene.add_audio(noise(duration=3.0, volume=0.12, color="brown"))  # 远处风声
scene.add_audio(noise(0.5, color="pink"))    # 更"闷"
scene.add_audio(noise(0.3, color="white"))   # 全频

# 脉冲串:打字机 / 雷达
scene.add_audio(pulse(8, interval=0.08))            # 8 声"嗒"
scene.add_audio(pulse(6, decay=True))               # 每声渐轻

生成的音用 Tone 家族(beep chime sweep whoosh arpeggio melody chord_sound noise pulse)。纯标准库实现,不需要 numpy。

混音交给 ffmpeg(adelay + amix + aecho + pan),不自己算采样。

排查工具:

python main.py 脚本.py --list-audio    # 列出现有音轨(含音名、起始时间、音量)
python main.py 脚本.py --no-audio      # 忽略音轨,只出画面

GIF 不支持音频。带声音的动画请输出 .mp4 / .webm; 如果场景有音轨却要出 GIF,引擎会明确提示,而不是静默丢掉声音。

路径:曲线 + 沿路径运动

圆和矩形摆不出"小球沿弧线飞过去"。用 Path:

from codeanim import Path, follow, along

# 三次贝塞尔:起点,控制点1,控制点2,终点
curve = scene.add_path([(100, 500), (300, 50), (700, 50), (900, 500)],
                       stroke="#38bdf8", stroke_width=6)

# "画出来"的进度动画
curve.progress.keys([(0, 0.0), (2, 1.0)], ease="inOutCubic")

# 让小球沿它跑
ball = scene.add_circle(r=14, fill="#fbbf24")
follow(curve, ball, start=0.5, end=2.5)

# 沿线静态摆东西
x, y = along(curve, 0.5)      # 曲线正中间
scene.add_circle(cx=x, cy=y, r=6, fill="#e2e8f0")
点数 曲线类型
2 个 直线
3 个 二次贝塞尔
4 个 三次贝塞尔
N 个 N-1 阶贝塞尔
任意(spline=True) Catmull-Rom 样条,穿过每个控制点

Path 的常用参数:

参数 作用
progress 绘制进度 0-1(可动画)
closed=True 闭合(首尾相连)
spline=True 用平滑样条而不是贝塞尔
stroke / stroke_width 线色 / 线宽
fill 填充色(闭合时有效)
arrow=True 末端画箭头
dash=10 虚线间隔
samples= 采样密度(越大越圆滑)

Scene 上的快捷方法:

scene.add_arc(cx=640, cy=400, radius=200, start_deg=180, end_deg=360)
scene.add_polygon([(0,0), (100,0), (50,80)], fill="#38bdf8")
scene.add_spline([(0,0), (100,100), (200,0), (300,100)])

底层几何工具也能直接用:

from codeanim import bezier, catmull_rom

bezier([(0,0), (0,100), (100,100), (100,0)], 0.5)   # -> (50.0, 75.0)
catmull_rom([(0,0), (50,100), (100,0)], 16)          # 采样成折线

按弧长取点(point_at(u))而不是按参数 —— 所以沿路径运动是匀速的, 不会在控制点附近忽快忽慢。实测 10 等分长度偏差 < 1%。

分组:把一堆图元当一个用

from codeanim import Group, Circle, Rect

card = Group(cx=640, cy=360)              # 组的中心
card.add(Rect(cx=0, cy=0, w=300, h=200, fill="#1e293b"))
card.add(Circle(cx=0, cy=-60, r=30, fill="#38bdf8"))

card.rotation.keys([(0, 0), (2, 360)])    # 整张卡片转一圈
card.scale.key(0, 1.0).key(0.5, 1.2).key(1.0, 1.0)
card.opacity.keys([(0, 0), (0.6, 1)])
scene.add(card)

# 嵌套也支持
outer = Group(cx=200, cy=200)
inner = Group(cx=50, cy=0)                # 相对 outer 的坐标
inner.add(Circle(r=10))
outer.add(inner)

组内坐标的 (0, 0) 就是组的中心。父组的旋转/缩放/透明度会正确传递到子树。

图层顺序

默认按添加顺序(先加的在下)。用 z= 强制排序,数值越大越靠上:

scene.add(background, z=-100)   # 压到最底
scene.add(watermark, z=100)     # 永远在最上层

同 z 时保持添加顺序。

图层顺序看不清?让引擎打出来

图元一多,"谁盖着谁"就容易糊。直接问它:

codeanim 脚本.py --layers              # 打印绘制顺序
codeanim 脚本.py --layers --layers-at 2.5   # 按第 2.5 秒的状态算位置
图层顺序(从下到上绘制,共 30 项)
────────────────────────────────────────────────────────────────────────
    #  z    类型      名称        位置 / 尺寸
────────────────────────────────────────────────────────────────────────
    1  -100  rect    <rect>    (0,0) 1280x720
    2  0     text    <text>    c(640,340) '一条命令,一份视频'
    3  10    text    <text>    c(640,330) 'CodeAnim'
    4  20    digits  <digits>  c(1220,650) '0,000 帧'
────────────────────────────────────────────────────────────────────────
⚠ 有多个图元共用同一个 z:0(25项), 10(3项)。
    同 z 时按添加顺序画 —— 后加的盖在前面之上。
⚠ 有 1 个静态图元(无关键帧)会被预合成到底层,它们的绘制顺序和动态图元分开处理。
    如果你打算让静态图元盖住动态内容,就给它加个无用关键帧。

脚本里也能调:

from codeanim import describe_layers, layer_warnings

print(describe_layers(scene, t=2.0))   # 完整表格
layer_warnings(scene)                   # 只要提示

会提醒的几种情况:

  • 所有 z 都是同一个值 → 顺序全靠添加顺序,容易和预期不符
  • 同 z 有多项 → 明确告诉你同 z 的规则
  • 有静态图元 → 它们会被预合成到底层,不参与逐帧排序
  • z 出现下降 → 通常是组内和组外混排导致的

时间线:不写魔法数字

把片段写成"接受起始时间、返回时长"的函数,然后排列:

from codeanim import seq, par

total = seq(scene, [
    lambda s: add_title(scene, start=s),                  # 自动排 0
    lambda s: par(scene, [                                # 两个同时
        lambda t: add_logo(scene, start=t),
        lambda t: add_counter(scene, start=t),
    ]),
    lambda s: add_outro(scene, start=s),
])
scene.duration = total          # 场景时长自动跟上

片段不返回时长也行,引擎会去问图元最晚的关键帧(不如显式返回精确)。

也可以直接指定每段占多久

不想让片段自己算?用 (片段, 时长):

total = seq(scene, [
    add_title,                    # 片段自己 return
    (add_ball, 2.0),              # 至少占 2 秒
    (add_logo, 1.5, 0.3),         # 至少 1.5 秒,后面再空 0.3 秒
    {"clip": add_outro, "span": 2.0, "gap": 0.5},   # 完整写法
])

指定时长是下界,不会截断动画:

片段自然时长 指定时长 实际占用
0.5s 2.0 2.0s(拉长,留静止尾巴)
3.0s 1.0 3.0s(以片段为准,不切半截)

段间统一留白:

seq(scene, [a, b, c], gap=0.5)     # 每段之间空 0.5 秒

par 和 Timeline 也一样:

par(scene, [(fade_in, 2.5), (music_bar, 1.0)])   # 取最长 2.5

tl = Timeline(scene)
tl.add((add_ball, 2.0))
tl.add(add_logo, span=1.5)          # 也可以走关键字

链式风格的 Timeline:

from codeanim import Timeline

tl = Timeline(scene)
tl.at(0.0).add(add_title)
tl.after(0.5).add(add_ball)     # 接在上一段结束后 0.5 秒
tl.fit()                        # 撑长场景时长

文件组织

引擎是个包,你的项目里只放自己的 .py:

my_project/
├── main.py          入口
├── title.py         标题片段
├── ball.py          小球片段
└── logo.py          片头片段

每个片段导出一个小函数并返回时长:

# title.py
from codeanim import Text

def add_title(scene, start=0.0, title="Hello"):
    t = Text(title, cx=scene.width/2, cy=scene.height/2,
             font_size=96, fill="white", anchor="center")
    t.opacity.keys([(start, 0), (start + 0.9, 1)])
    scene.add(t, z=10)
    return 1.5          # 这一段占 1.5 秒

同目录的 .py 可以直接 import,引擎会自动把脚本目录加进 sys.path。 脚本里定义名为 scene 的变量,或一个返回 Scene 的 build() 函数,都行。


输出格式

格式 用途 触发方式 透明
MP4 (H.264) 默认,兼容性最好 -o out.mp4 —
WebM (VP9) 体积小 -o out.webm ✅*
MOV 苹果生态 -o out.mov ✅ ProRes 4444
MKV 通用容器 -o out.mkv —
GIF 发群、贴文档 --gif —
PNG 序列 交给 AE / Blender --frames 目录/ ✅
单帧 PNG 快速检查某一刻 --snapshot 2.0 ✅

* WebM 的 alpha 取决于本机 ffmpeg 的 libvpx 是否编译了该特性。 CodeAnim 会实测探测(编一段带透明的数据再解回来验), 不支持时明确报错并建议改用 MOV,而不是静默输出一个不透明的文件。

背景透明

scene = Scene(1280, 720, background=None)   # None = 透明
python main.py overlay.py -o overlay.mov    # ProRes 4444

拿去 Premiere / Final Cut / 网页 <video> 上直接叠。


设计约束

  1. 无 GUI。 永远不弹窗口,不做播放器。
  2. 纯函数场景。 任意时刻 f(t) 可独立求值,不依赖前一帧 —— 因此能任意跳帧、 倒放、多进程并行。tests/test_codeanim.py 里有专门的回归用例守住这条。
  3. 可脚本化。 能塞进 Makefile、CI、GitHub Actions。
  4. 命令行优先。 所有能力都能通过参数触发。
  5. 错误说人话。 缺 ffmpeg 告诉你怎么装;脚本报错只显示你自己的文件和行号; 空间不足、容器不支持透明这类问题给出具体解决办法,不糊一屏内部堆栈。

引擎能力

  • 零落盘流式渲染:磁盘占用 = 最终视频大小
  • 抗锯齿:2 倍超采样 + LANCZOS 高质量缩放
  • 并行渲染:帧之间零依赖,多进程打满 CPU
  • 静态预合成:不动的内容只画一次,之后每帧复用
  • 快速路径:不需要旋转/缩放的图元直接画在底图,省掉整张图层分配(实测快 8 倍)
  • 中文字体:自动查找系统字体,支持 .ttc;也可用 CODEANIM_FONT 或项目 fonts/

测试

python tests/test_codeanim.py

527 项断言,覆盖颜色、缓动、关键帧、纯函数性、中心定位、分组父子变换与 组内定位回归、图片图元、帧动画、音频(导入 + 生成 + 混音方案 + 音量曲线 + 音色 + ADSR 包络 + 声像/回声/混响 + 分组)、乐理(音名/和弦/ 音阶/简谱)、路径(贝塞尔/样条/沿路径运动)、Digits(含自定义格式化)、 时间线、片段时长指定、图层顺序、手感糖、热重载监视与按键、终端内嵌预览、 参数校验、磁盘预算、编码参数、超采样。


例子

文件 演示内容
examples/promo/ 完整宣传片(19s/1080p,四个片段文件,见下)
examples/demo.py 完整片头:seq/par 编排 + Group + Digits + z 图层
examples/chart.py 柱状图 + 折线 + 数值滚动
examples/particles.py 300+ 图元压性能
examples/media.py 图片 + 音频:图片复用、着色、圆角;声音按秒排布
examples/music_path.py 乐理 + 路径:音名写旋律、三种音色、小球沿贝塞尔飞
examples/alpha_overlay.py 背景透明,供后期叠加
examples/title.py ball.py logo.py 可复用的片段模块

宣传片

用引擎自己的 API 做的 19 秒 1080p 宣传片,顺便当"这引擎能做什么"的实证:

python main.py examples/promo/__main__.py
python main.py examples/promo/__main__.py --watch --inline   # 改代码看效果

四段式,每段一个文件:

文件 内容
promo/intro.py 片头:CodeAnim 弹入 + 定位语
promo/code_panel.py 左边打真实 Python 代码(逐行浮现 + 光标),右边实时出画面
promo/showcase.py 四张能力卡,卡内各自跑真动效(公转圆、三种缓动对比、整体旋转、数字滚动)
promo/outro.py $ python main.py demo.py → out.mp4 已生成 → Slogan

各段用 segment_visibility() 管生命周期,seq 自动排布:

total = seq(scene, [
    lambda s: add_intro(scene, start=s),
    lambda s: add_code_panel(scene, start=s),
    lambda s: add_showcase(scene, start=s),
    lambda s: add_outro(scene, start=s),
])
scene.duration = total

全部特性一览

渲染

  • 零落盘流式渲染(磁盘占用 = 最终视频大小)
  • 2 倍超采样抗锯齿
  • 多进程并行(帧间零依赖)
  • 静态预合成 + 快速路径(不需要变换的图元直接画)
  • 中文字体自动查找(支持 .ttc)

API

  • 图元:Circle Rect Line Text Image Path Digits Group
  • 路径:Path(贝塞尔 / 样条 / 弧 / 多边形)+ follow() 沿路径运动
  • 定位:cx / cy 按几何中心,center_x() / center_y() 动态句柄
  • 图层:z=,--layers 打印绘制顺序
  • 时间线:seq / par / Timeline
  • 手感:add_circle / add_image / add_path / add_arc / span_of / fit_duration / add_all / set_text / row / grid
  • Digits 自定义格式化:formatter= / default_formatter()
  • 25 条缓动曲线
  • 颜色插值

素材

  • 图片:任意路径或内存位图,支持裁剪 / 九宫格 / 圆角 / 着色 / 等比适配
  • 帧动画:雪碧图自动切帧(sprite=)或手动切(frames=),支持 play(fps=) / frame 关键帧 / 倒放
  • 音频:导入任意格式,或用代码生成
  • 音频按"第几秒响"配置,支持音量 / 音量曲线 / 淡入淡出 / 循环 / 裁切 / 变速
  • 音频效果:声像 / 回声 / 混响 / 亮度;分组批量调
  • 乐理:音名 note("C4")、和弦 chord("Am7")、音阶 scale("C4","blues")、简谱 "1 2 3 4 5"
  • 音色:11 种预置(电钢 / 铃 / 弦乐 / 长笛 / 马林巴…)+ 自定义泛音表
  • 包络:ADSR(起音 / 衰减 / 延音 / 释音),比单纯淡入淡出自然

工作流

  • --watch 热重载(含按键交互:暂停 / 单步 / 换时间点)
  • --inline 终端内嵌画面(半块字符直接画)
  • --preview 快速看图
  • --layers 图层顺序调试
  • doctor 环境检查
  • 磁盘空间预估与红线保护
  • 报错只显示用户文件 + 行号

输出

  • MP4 / WebM / MOV / MKV / GIF / PNG 序列 / 单帧
  • 背景透明的 MOV (ProRes 4444) 与 WebM (VP9)

Metadata

Release files for codeanim-py 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for codeanim-py 0.3.0
File Size Uploaded
codeanim_py-0.3.0.tar.gz 192.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for codeanim-py 0.3.0
File Interpreter ABI Platform
codeanim_py-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 346.9 kB

Release files / codeanim_py-0.3.0.tar.gz

Download URL codeanim_py-0.3.0.tar.gz
Size 192.5 kB
Tags Source
SHA-256 checksum
How to use checksums
f5b67d97f1be99b4fd21db94cf166d939dd16cbd5a00dd65beb46d34088fe15a
BLAKE2b-256 checksum
How to use checksums
064bd77003290e479d7a5c0eff6d72ef9afb10a8e72afa4aa35ac791010c38d3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / codeanim_py-0.3.0-py3-none-any.whl

Download URL codeanim_py-0.3.0-py3-none-any.whl
Size 154.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ef5cce61e7ac6822cae9a12a52c64d184cff25a2b871d68440691b5aadd53117
BLAKE2b-256 checksum
How to use checksums
54aed3105ebea3d47f24773b76536fdd3d6a0604fd389d62478ada5d9404a1e7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release history Release notifications | RSS feed

0.4.0

2 release files

This release

0.3.0 This release

2 release files

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page