前阵子做了一张动画长图,纯手拼 SVG 字符串,一千多行 Python。做完就有人推荐 DrawSVG,说这是 Python 里画矢量图最成熟的库。
这个周末正好要再做一张,就拿它试了一遍。结论先说:它能把「坐标→字符串」这一层省掉,但动画机制还得自己接。
一、原来的做法
手拼字符串长这样:
def line(self, x1, y1, x2, y2, w=3, col='#C9C1AE'):
L = math.hypot(x2 - x1, y2 - y1)
self.p.append(
f'<path class="gw" style="--len:{L:.1f}" '
f'd="M{x1} {y1} L{x2} {y2}" fill="none" stroke="{col}" '
f'stroke-width="{w}" stroke-linecap="round" '
f'stroke-dasharray="{L:.1f} {L:.1f}" stroke-dashoffset="{L:.1f}"/>')
每一行都要记住三件事:属性顺序、路径长度算对、CSS 变量塞进 style。
画一个圆要写:
f'<circle cx="{cx}" cy="{cy}" r="{r}" fill="none" stroke="{col}" stroke-width="{sw}"/>'
嵌套引号多了眼睛会花,而且打错一个属性名不会报错,浏览器直接忽略,你只会看到「那个东西没出来」。
二、DrawSVG 的写法
同一个圆:
d.append(dw.Circle(cx, cy, r, fill='none', stroke=col, stroke_width=sw))
同一根线:
d.append(dw.Line(x1, y1, x2, y2, stroke=col, stroke_width=w,
stroke_linecap='round', class_='gw', style=f'--len:{L}'))
差别在三点:
① 不用拼字符串 属性是关键字参数,写错直接 TypeError
② 不用记 SVG 语法 stroke-width → stroke_width,下划线自动转连字符
③ 元素可以复用 Drawing 对象能 append 到另一个 Drawing 里
第三点手写做不到。多个长图共用一套页头页脚,DrawSVG 可以真的复用,手写只能复制粘贴字符串。
三、它替你算的东西
DrawSVG 提供了一组 Path 的构造器,几个常用的几何都封好了:
dw.Path(stroke='#000', fill='none')
.M(x, y).L(x2, y2) # 折线
.Q(cx, cy, x, y) # 二次贝塞尔
.C(cx1, cy1, cx2, cy2, x, y) # 三次贝塞尔
.A(rx, ry, rot, arc, sweep, x, y) # 圆弧
.Z() # 闭合
手写时这些 d 属性字符串全得自己拼,一条曲线写到一半发现参数顺序记错了,只能去查 MDN。现在是关键字参数,IDE 有补全。
四、动画还得自己接
这是最要紧的一点。 DrawSVG 能生成 SVG 动画,但它的方式是把每一帧算出来:
d.as_animation_frames(frames=60) # 60 帧
d.as_gif() # 转 GIF
d.as_mp4() # 转 MP4
它走的是「逐帧渲染」路线,不是「CSS 动画」路线。
对长图来说这不合适:一张 7000px 的图,逐帧渲染 60 帧就是 60 张 7000px 的位图,体积和计算量都不划算。滚动触发更是没法逐帧做——用户滚到哪才播哪,这是运行时行为。
所以最终方案还是:
DrawSVG → 生成静态 SVG 骨架
手写 CSS → 定义动画(transition)
手写 JS → IntersectionObserver 触发
DrawSVG 的 append_css() 能把 CSS 塞进 SVG 里,但触发逻辑还是得自己在页面里写。
五、实测数据
同一张图(1080×1500,一个时序图 + 三段说明 + 一个结论块):
手拼字符串 DrawSVG
代码行数 116 98
生成体积 5.2 KB 4.9 KB
元素数 35 35
出错次数 2 1 ← 一次是 count() 参数写错
代码少 18 行,主要是省掉了那些 f'...{x1} {y1}...' 的拼接。
出错次数没本质差别——DrawSVG 报的是 TypeError,手写是「某个东西不显示」,前者更容易定位。
六、两个坑
① count() 不是无参的
我下意识写了 d.count() 想看元素数,报错:
TypeError: Drawing.count() missing 1 required positional argument: 'element'
它的语义是「数某类元素有几个」,不是「总共有多少」。要总数得:
len(list(d.all_elements()))
② class 参数是 class_ 不是 class
Python 关键字冲突,DrawSVG 用下划线转义:
dw.Text('标题', 24, x, y, class_='fu') # 对
dw.Text('标题', 24, x, y, class='fu') # SyntaxError
同理 style 不用转义,但 class 要。
七、什么时候用它
✓ 要画复杂的几何图形 贝塞尔、圆弧、箭头,省大量字符串拼接
✓ 要复用图形组件 页头页脚、图标,Drawing 可以嵌套
✓ 要导出 GIF/MP4 as_gif() / as_mp4() 一行搞定
✓ 要 Jupyter 里交互 原生支持
✗ 只要几根线几个字 手写更快,不必引入依赖
✗ 要做滚动触发动画 它不管触发,还是得手写 JS
✗ 要精确控制 SVG 结构 有些属性封装得不够细,想加自定义数据属性得绕
八、我最后怎么用的
留着当主力,但动画层不动。
画图 DrawSVG(省掉字符串拼接)
动画 CSS transition(手写)
触发 IntersectionObserver(手写)
部署 单文件 HTML(不上构建链)
理由很实际:DrawSVG 解决的是「怎么把图形描述成代码」,不解决「怎么让它在浏览器里动起来」。 这两件事本来就不该由同一个库负责。
如果你想看生成效果,我做了一个样例:DrawSVG 实测演示
一句话总结:DrawSVG 把「手拼 SVG 字符串」这件苦差事消掉了,值得用;但它不是动画框架,别指望它替你解决触发和时序。
