跑马灯只有一行公式:x=w-mod(t*100,w+tw)。 mod 造循环,w+tw 保证先完整出画再进来,这就是全部。
上一节把 x、y 的表达式算清楚了,这一节把四行表达式拼成一个真效果。 跑马灯是「表达式」这一机制最典型的应用,也是实际项目里用得最多的。
一、逐帧算位置,看它进出画面
水印 70×22、画面 160×90、速度每秒 100 像素,每 0.2 秒取一个点:
t(秒) x 左边缘 右边缘 状态
0.0 160 160 230 还在右边界外 0.4 120 120 190 部分可见 0.8 80 80 150 完整可见 1.2 40 40 110 完整可见 1.6 0 0 70 完整可见 1.8 -20 -20 50 部分可见
t=0 时 x 正好等于画面宽度,文字整个在右边框外面; 再往后 x 一路减小,直到负数,从左边滑出去。
一圈要走 (w+tw) 除以速度,也就是 2.30 秒。 想调快慢就改那个 100,想改空隙就改 w+tw。
二、为什么必须用 mod
mod(t*100,w+tw) 是一个锯齿波:从 0 涨到 w+tw,然后瞬间归零。
w 减去它,就得到一条从「画面右边之外」走到「左边之外」的斜线。
不用 mod 会怎样?x 会一直变小。 播十分钟,t 是 600,x 是负六万——远在画外,而且再也不会回来。
更要紧的是精度:t 越大,t*100 的数值越大, 浮点在这样的量级上已经分辨不出「一帧走了 4 像素」这点差值,位置会开始跳。
所以 mod 不只是为了循环,也是为了把数值锁在一个小范围里。 长时间跑的东西,凡是「随时间累加」的量,都该考虑取模。
三、三个方向与一个假停顿
效果 x 写什么
从右往左 w-mod(t100,w+tw) 从左往右 mod(t100,w+tw)-tw 走快一倍 w-mod(t100,w+tw)2
第三行只是把速度乘 2,走得快、空隙长,看着像有停顿,其实没有。
真要停顿得用 if:if(lt(t,5),w, w-mod((t-5)*100,w+tw))。
这一行的意思是:前 5 秒把 x 按在 w 上不动,5 秒之后才开始跑。 用 if 拼时间轴,是画面上做「分阶段动画」的通用手法。
四、命令行怎么写,以及那个逗号
-vf "drawtext=text='内部资料':x=w-mod(t*100\,w+tw):y=h-40"
表达式里的逗号要转义成 \,。
因为逗号在滤镜语法里是「下一个滤镜」的分隔符,不转义会被截断。
这也是跑马灯命令最容易失败的地方:
mod 里那个逗号一漏,整条 -vf 就被切成两段,报错却完全指不到这里。
五、动手 Demo:把跑马灯算出来
用 6.3.5 的求值器,把跑马灯公式在 10 个时刻上算一遍。 每一帧同时给出左边缘、右边缘与可见状态。
from PIL import Image, ImageDraw, ImageFont
W, H = 160, 90
f = ImageFont.truetype(u"C:/Windows/Fonts/msyh.ttc", 16)
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" % (tw, th, W, H))
for i in range(0, 50, 5):
t = i / 25.0
m = t * 100
x = int(W - (m - (W + tw) * int(m / (W + tw))))
left, right = x, x + tw
if left >= W:
st = u"还在右边界外"
elif right <= 0:
st = u"已出左边界"
elif left >= 0 and right <= W:
st = u"完整可见"
else:
st = u"部分可见"
print(u"t=%.1f x=%4d 左 %4d 右 %4d %s" % (t, x, left, right, st))
print(u"一圈 %.2f 秒" % ((W + tw) / 100.0))
实跑输出:
--------------------------------------------------------------------
6.4 文字跑马灯案例实战——让 x 随时间走
--------------------------------------------------------------------
**水印 70×22,画面 160×90,速度取每秒 100 像素。**
**① 逐帧算位置,看它什么时候进出画面**
t(秒) x 左边缘 右边缘 状态
----------------------------------------------------------
0.0 160 160 230 还在右边界外
0.2 140 140 210 部分可见
0.4 120 120 190 部分可见
0.6 100 100 170 部分可见
0.8 80 80 150 完整可见
1.0 60 60 130 完整可见
1.2 40 40 110 完整可见
1.4 20 20 90 完整可见
1.6 0 0 70 完整可见
1.8 -20 -20 50 部分可见
**t=0 时 x 等于画面宽度,文字整个在右边框外面;**
**再往后 x 一路减小,直到变成负数,从左边滑出去。**
**一圈要走 (w+tw)/(速度) 秒,也就是 2.30 秒。**
**② 为什么用 mod,不用减到负数就算了**
**`mod(t*100,w+tw)` 是一个锯齿波:从 0 涨到 w+tw,然后瞬间归零。**
**`w` 减去它,就得到一条从「画面右边之外」一路走到「画面左边之外」的斜线。**
**不用 mod 的话,x 会一直变小,播到十分钟就变成一个巨大的负数,**
**表达式里的乘法越滚越大,精度也会掉——长片子必须用 mod 收住。**
**③ 三个方向都能跑**
效果 x 写什么
------------------------------------
从右往左 w-mod(t*100,w+tw)
从左往右 mod(t*100,w+tw)-tw
走快一倍 w-mod(t*100,w+tw)*2
**第三行只是把速度乘 2,走得更快、空隙更长,看着像有停顿,其实没有。**
**真要停顿得用 if 判断:**
`if(lt(t,5),w, w-mod((t-5)*100,w+tw))`,前 5 秒不动,之后才跑。
**④ 命令行怎么写**
**`-vf "drawtext=text='内部资料':x=w-mod(t*100\,w+tw):y=h-40"`**
**注意表达式里的逗号要转义成 `\,`——**
**因为逗号在滤镜语法里是「下一个滤镜」的分隔符,不转义会被截断。**
int(m / (W + tw)) 就是取模里的「商」,再乘回去减掉,得到余数。
Python 的 % 在这里也能用,但负数时行为不同,手工展开更稳妥。
left >= W 判断要放在最前面:t=0 时 x 正好等于 W,
那一刻文字还完全在画外,不能算「部分可见」。
真实做的时候不需要逐帧算,drawtext 每帧自己算一次。 这段代码的价值是「预览」:先用它确认节奏,再写进命令,省掉反复转码。
30 秒实验
- 改速度:把 100 改成 300,重新算一圈要几秒。
- 改方向:换成
mod(t*100,w+tw)-tw,看左边缘什么时候变正。 - 加停顿:用
if(lt(t,5),w,...)算出前 5 秒的位置,验证它确实不动。
常见误区
- 公式里少写 w+tw 的 tw。文字会「凭空出现」,衔接处很突兀。
- 不用 mod。短片子看不出来,播久了 x 变成巨大负数且精度受损。
- 漏转义逗号。整条 -vf 被切成两段,报错指不到真正的原因。
自测三问(点开对答案)
1. 跑马灯的公式是什么? —— `x=w-mod(t*100,w+tw)`,速度 100 像素每秒,循环长度 w+tw。 2. 为什么要加 tw? —— 让文字先完整走出画面再从另一侧进来,衔接处才看不出跳变。 3. 表达式里的逗号怎么处理? —— 转义成 `\,`,否则会被当成分隔下一个滤镜的符号。- ☐ (本节完,下一节 6.5.1 overlay 技术简介)