昨天晚上十一点多吧,我在公司楼下抽烟,手机里小李给我发语音:东哥你那个大屏能不能别老是静态图啊,老板说要“动起来”,要那种一眼看过去就…哎你懂的那种“哇哦”。我当时困得要死,还得装镇定,说行行行,回去给你整一个“看起来很贵”的动图。

说真的,很多人一提动图就想上很重的东西,什么三维啊渲染啊……其实你要的是公众号里那种“惊艳一下”的效果,核心就两点:运动轨迹要顺滑、画面要有“残影/呼吸感”。这俩事儿,Python 里一个老牌模块就够了:matplotlib.animation,别嫌它老,真用好了挺狠的。

我回到工位,电脑还热着,键盘上还有下午洒的咖啡渍(别学我),就开整。思路很简单: 1)先造一条“漂亮曲线”(别用直线,直线太像报表) 2)每一帧把点往前挪一点点 3)把过去 N 帧留下来当拖尾(残影就出来了) 4)再给点大小变化、透明度变化,那个“呼吸”就有了

你们直接抄下面这段跑就行,生成一个 GIF,发群里能装一波那种。代码我自己现写的,逻辑很直白,改参数就能换风格。

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation, PillowWriter
from collections import deque

# 你随便改这几个参数,风格差异巨大
FPS = 30
DURATION = 6          # 秒
TRAIL = 50            # 拖尾长度,越大越“丝滑”
SIZE_MIN, SIZE_MAX = 10180

frames = FPS * DURATION
t = np.linspace(02*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=(66), dpi=120)
ax.set_aspect("equal")
ax.set_xlim(-1.41.4)
ax.set_ylim(-1.41.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)

def ease_in_out(u: float) -> float:
    # 简单缓动,u∈[0,1]
    return u*u*(3 - 2*u)

def init():
    tail.set_offsets(np.empty((02)))
    head.set_offsets(np.empty((02)))
    return tail, head

def update(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(01, 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.950.951.00.95]])

    return tail, head

ani = 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 就变成“电光点点”的感觉。反正就是……参数别怕改,动图这玩意儿,调参比讲道理有用多了。

行了我不说了,刚小李又在群里@我说老板要加个“数据增长的动效”,我先去把咖啡擦了,不然键盘又黏了…哎等会儿我好像把烟落楼下了,算了算了先这样吧。