Featured image of post DrawSVG 实测

DrawSVG 实测

用一个真实需求测 DrawSVG:画动画长图。结论是坐标计算省了一大半,但动画还得自己接

   
文章摘要
用一个真实需求测 DrawSVG:代码量少了三分之一,坐标不用自己拼字符串,但滚动触发和 CSS 还得手写

前阵子做了一张动画长图,纯手拼 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 字符串」这件苦差事消掉了,值得用;但它不是动画框架,别指望它替你解决触发和时序。

CC BY-NC-SA 4.0 创意的非商业派对入场券
最后更新于 2026-11-09 17:26
晚来天欲雪,能饮一杯无