昨天晚上十一点多吧,我在公司楼下抽烟,手机里小李给我发语音:东哥你那个大屏能不能别老是静态图啊,老板说要“动起来”,要那种一眼看过去就…哎你懂的那种“哇哦”。我当时困得要死,还得装镇定,说行行行,回去给你整一个“看起来很贵”的动图。
说真的,很多人一提动图就想上很重的东西,什么三维啊渲染啊……其实你要的是公众号里那种“惊艳一下”的效果,核心就两点:运动轨迹要顺滑、画面要有“残影/呼吸感”。这俩事儿,Python 里一个老牌模块就够了:matplotlib.animation,别嫌它老,真用好了挺狠的。
我回到工位,电脑还热着,键盘上还有下午洒的咖啡渍(别学我),就开整。思路很简单: 1)先造一条“漂亮曲线”(别用直线,直线太像报表) 2)每一帧把点往前挪一点点 3)把过去 N 帧留下来当拖尾(残影就出来了) 4)再给点大小变化、透明度变化,那个“呼吸”就有了
你们直接抄下面这段跑就行,生成一个 GIF,发群里能装一波那种。代码我自己现写的,逻辑很直白,改参数就能换风格。
import numpy as npimport matplotlib.pyplot as pltfrom matplotlib.animation import FuncAnimation, PillowWriterfrom collections import deque# 你随便改这几个参数,风格差异巨大FPS = 30DURATION = 6# 秒TRAIL = 50# 拖尾长度,越大越“丝滑”SIZE_MIN, SIZE_MAX = 10, 180frames = FPS * DURATIONt = np.linspace(0, 2*np.pi, frames)# 造一条“看起来高级”的轨迹:Lissajous + 轻微扰动x = np.sin(3 * t + 0.3*np.sin(2*t))y = np.cos(2 * t) * np.sin(t) + 0.15*np.cos(5*t)# 画布fig, ax = plt.subplots(figsize=(6, 6), dpi=120)ax.set_aspect("equal")ax.set_xlim(-1.4, 1.4)ax.set_ylim(-1.4, 1.4)ax.axis("off") # 干净!别留坐标轴,像报表就完了fig.patch.set_alpha(0) # 背景透明(公众号贴深色底更好看)# 用 deque 存拖尾hx, hy = deque(maxlen=TRAIL), deque(maxlen=TRAIL)# 两层:一层拖尾散点 + 一层头部“亮点”tail = ax.scatter([], [], s=[], alpha=1.0)head = ax.scatter([], [], s=SIZE_MAX, alpha=1.0)defease_in_out(u: float) -> float:# 简单缓动,u∈[0,1]return u*u*(3 - 2*u)definit(): tail.set_offsets(np.empty((0, 2))) head.set_offsets(np.empty((0, 2)))return tail, headdefupdate(i): hx.append(x[i]) hy.append(y[i])# 拖尾点 pts = np.column_stack([np.array(hx), np.array(hy)]) n = len(pts)# 透明度从“老”到“新”逐渐变强(注意:matplotlib scatter 没有 per-point alpha 的直接参数,# 我们用 RGBA 颜色来做) u = np.linspace(0, 1, n) a = 0.05 + 0.95 * ease_in_out(u) # alpha s = SIZE_MIN + (SIZE_MAX - SIZE_MIN) * (u**2) # size# 颜色也做个渐变(看着会更“贵”)# 这里用一个简单的紫->蓝->青的过渡,你也可以自己换 r = 0.6 - 0.3*u g = 0.2 + 0.7*u b = 0.9 - 0.2*np.sin(np.pi*u) colors = np.column_stack([r, g, b, a]) tail.set_offsets(pts) tail.set_sizes(s) tail.set_facecolors(colors)# 头部亮点做“呼吸” breathe = 0.6 + 0.4*np.sin(2*np.pi * i / (FPS*1.2)) head.set_offsets([[x[i], y[i]]]) head.set_sizes([SIZE_MAX * breathe]) head.set_facecolors([[0.95, 0.95, 1.0, 0.95]])return tail, headani = FuncAnimation(fig, update, frames=frames, init_func=init, interval=1000/FPS, blit=True)# 输出 GIF(不依赖 ffmpeg,装个 pillow 就行)ani.save("wow_trail.gif", writer=PillowWriter(fps=FPS))print("done -> wow_trail.gif")
跑完你会得到一个 wow_trail.gif,那个拖尾一出来,基本就有“高级感”了。然后你再干一件特别俗但特别有效的事:把公众号背景弄深一点(深灰/黑),这个透明背景的 GIF 贴上去立刻出效果。哎我知道你们会说这算投机取巧,但做展示嘛,先赢再说。
对了,我中途还踩了个小坑,顺便说下,省得你们半夜骂娘:有的人动图生成出来“卡顿”,不是代码慢,是帧太多 + dpi 太高。你就记住这句土话:先保证顺,再保证清。FPS 先 24~30,dpi 先 100~150,尺寸别太大,真要高清你就导 mp4 再转(当然那就扯到 ffmpeg 了,今天不展开,我怕我又讲嗨了)。
还有一个小技巧,特别像“做特效”:你把 TRAIL 调到 80,再把 SIZE_MAX 调到 240,整个就像彗星拖尾;反过来 TRAIL 调到 20 就变成“电光点点”的感觉。反正就是……参数别怕改,动图这玩意儿,调参比讲道理有用多了。
行了我不说了,刚小李又在群里@我说老板要加个“数据增长的动效”,我先去把咖啡擦了,不然键盘又黏了…哎等会儿我好像把烟落楼下了,算了算了先这样吧。