x 与 y 是每帧重算的表达式,不是坐标。 跑马灯、时间戳、自适应位置,全靠这一点;其余参数都是固定值。
上一节解决了「文字从哪来」,这一节解决「它放在哪、怎么动」。 把 x、y 讲透,下一节的跑马灯就只剩一行公式。
一、四条位置表达式,按帧算一遍
画面 160×90、水印 54×29,把 t 从 0 算到 2 秒:
表达式 t=0 t=0.5 t=1 t=1.5 t=2
10 10 10 10 10 10 (w-tw)/2 53 53 53 53 53 w-tw-10 96 96 96 96 96 w-mod(t*100,w+tw) 160 110 60 10 -40
前三行每帧都算出同一个数,与 t 无关——它们是「常量表达式」。 第四行每帧都在变,这就是跑马灯的全部秘密。
mod 让 x 涨到顶之后回到起点,w+tw 保证它先完全走出画面再进来。
这两个设计合起来,才是一个「看不出接缝」的循环。
二、参数分五类,按类记
类别 参数 说明
内容 text、textfile 写什么字 位置 x、y 表达式,每帧重算 外观 fontsize、fontcolor 多大、什么颜色 装饰 box、borderw 底衬与描边 时间 enable、alpha 哪几帧出现、多透明
五类里只有「位置」是表达式,其余都是固定值。
这一点很关键:很多人以为 fontsize 也能写 h/30,其实它只接受数字。
想让它随分辨率变,只能在外层先算好、再拼进命令。 这也是为什么真正做项目时,命令几乎都是脚本拼出来的,而不是手敲的。
三、y 的常用写法与一个易错点
想放的位置 y 写什么
顶部留 10 10 垂直居中 (h-th)/2 底部留 10 h-th-10 垂直正中偏上 h/3
th 是文字的包围高度,不是字号。字号写 24,字形实际只有 23 高。
拿字号当高度算,贴底的文字会有一截跑出画面。
这一行差一两个像素不致命,但贴底的场景会露馅: 最后一行字被切掉一半,看上去像分辨率出了问题,其实是 th 用错了。
四、动手 Demo:把表达式按帧算出来
写一个小求值器,把 w、h、tw、th、t 代进去,看四条表达式在五个时刻的值。 再量一下字号 24 的真实字形高度。
from PIL import Image, ImageDraw, ImageFont
W, H = 160, 90
f = ImageFont.truetype(u"C:/Windows/Fonts/msyh.ttc", 24)
bb = ImageDraw.Draw(Image.new(u"RGBA", (8, 8))).textbbox(
(0, 0), u"水印", font=f)
tw, th = bb[2] - bb[0] + 6, bb[3] - bb[1] + 6
print(u"画面 %dx%d 水印 %dx%d" % (W, H, tw, th))
def ev(expr, t):
mod = lambda a, b: a - b * int(a / b)
return eval(expr, {u"__builtins__": {}},
dict(w=W, h=H, tw=tw, th=th, t=t, mod=mod))
for expr in (u"10", u"(w-tw)/2", u"w-tw-10",
u"w-mod(t*100,w+tw)"):
print(u"%-22s %s" % (expr,
u" ".join([u"%4d" % ev(expr, t)
for t in (0, 0.5, 1.0, 1.5, 2.0)])))
实跑输出:
--------------------------------------------------------------------
6.3.5 drawtext 的主要参数——x 与 y 是表达式,不是坐标
--------------------------------------------------------------------
**① 四条常用位置表达式,按帧算一遍**
**画面 160×90,水印 54×29,把 t 从 0 算到 2 秒:**
表达式 t=0 t=0.5 t=1.0 t=1.5 t=2.0
---------------------------------------------------------------------
10 10 10 10 10 10
(w-tw)/2 53 53 53 53 53
w-tw-10 96 96 96 96 96
w-mod(t*100,w+tw) 160 110 60 10 -40
**前三行算出来的都是同一个数,与 t 无关——它们是「常量表达式」。**
**第四行每帧都在变,这就是跑马灯的全部秘密:**
**`mod` 让 x 到顶之后回到起点,`w+tw` 保证它先完全走出画面再进来。**
**② 参数分五类,记的时候按类记**
类别 参数 说明
----------------------------------------------------------
内容 text、textfile 写什么字
位置 x、y 表达式,每帧重算
外观 fontsize、fontcolor 多大、什么颜色
装饰 box、boxcolor、shadowx 底衬与阴影
时间 enable、alpha 哪几帧出现、多透明
**五类里只有「位置」是表达式,其余都是固定值。**
**这一点很关键:很多人以为 `fontsize` 也能写 `h/30`,其实它只接受数字,**
**想让它随分辨率变,得在外层先算好再拼进命令。**
**③ y 常用表达式**
想放的位置 y 写什么
---------------------------------
顶部留 10 10
垂直居中 (h-th)/2
底部留 10 h-th-10
画面外从左往右移 h/2
**记住 `th` 是文字的包围高度,不是字号:字号写 24,字形实际只有 23 高,**
**这里量到 29,是因为水印图上下各留了 3 像素空边。**
**拿字号当高度算,贴底的文字会有一截跑出画面。**
eval 加 {u"__builtins__": {}} 是为了禁掉内置函数。
只放 w、h、tw、th、t、mod 六个名字进去,表达式就只能用这几个。
mod 得自己实现:Python 的 % 对负数取模的结果与 FFmpeg 不同,
用 a - b * int(a / b) 才与 drawtext 里的 mod() 行为一致。
这段求值器不是玩具:写跑马灯之前先用它算几帧, 能提前看出文字什么时候出画、循环点在哪,省掉反复转码试错。
30 秒实验
- 改速度:把
t*100改成t*300,看一圈要多久。 - 改方向:写成
mod(t*100,w+tw)-tw,看它是从左往右还是从右往左。 - 加停顿:用
if(lt(t,5),w,...)的写法算一遍,验证前 5 秒确实不动。
常见误区
- 把 x、y 当死坐标。它们是表达式,每帧重算。
- 以为 fontsize 也能写表达式。只有 x、y 支持。
- 拿字号当 th。字号 24 的字形只有 23 高,贴底会切一截。
自测三问(点开对答案)
1. `w-tw-10` 是什么意思? —— 画面宽度减去文字宽度再减 10,也就是把文字贴到右边、留 10 像素边距。 2. 跑马灯为什么用 mod? —— 让 x 到顶后回到起点,形成循环;不用 mod 的话 x 会一直变小。 3. 五类参数里哪一类是表达式? —— 只有位置(x、y)。其余都是固定值。- ☐ (本节完,下一节 6.3.6 -vf 的 drawtext 添加系统时间水印)