ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

手写SVG鹈鹕骑自行车:从viewBox到SMIL动画实战

2026/9/26 21:52:24 拓冰建站 浏览量
手写SVG鹈鹕骑自行车:从viewBox到SMIL动画实战 上一篇我写了SVG的基础形状和颜色接下来肯定不能只停在会画圆和长方形早晚要碰路径、坐标系统和动画。这一篇我打算用一个能跑出画面的小项目来把这些东西串起来在HTML里用SVG绘制一只鹈鹕骑自行车的2D动画。别看这个题材听起来有点无厘头它其实特别好用——大嘴、翅膀、脚蹬这些结构能逼着你用路径和贝塞尔曲线自行车的轮子、踏板能逼着你理解坐标系和动画。网上甚至经常有人拿“generate an svg of a pelican riding a bicycle”当提示词去测AI绘图能力但手写一遍绝对比看AI生成的代码更管用。这篇文章适合刚看完第一篇SVG基础、想动手做完整作品的读者也适合那些正在找免费SVG图标、想学SVG优化和服务器配置的朋友。1. viewBox和坐标系所有SVG问题的起点1.1 viewBox四个数字决定你“看见”什么很多人在第一篇已经学会了画矩形和圆形但一遇到viewBox就懵。其实可以把它理解成一架摄像机的取景框SVG里面有一个无限大的画布viewBox告诉你“我现在从画布的哪个位置、看多大区域”。svg width200 height200 viewBox0 0 100 100 rect x10 y10 width50 height50 filltomato / /svg这段代码里viewBox是0 0 100 100表示左上角从(0,0)开始取宽100、高100的区域。这个区域内的内容会被缩放后塞进width200 height200的显示区域里。所以那个50x50的矩形看起来被放大了两倍。如果写成viewBox50 50 100 100相当于取景框往右下移了50个像素原本在(10,10)的矩形就只剩下一小部分露在外面。很多初学者遇到“我的SVG怎么被裁掉了”时第一反应是改width和height其实问题往往出在viewBox的前两个数字上。在鹈鹕骑自行车这个案例里我统一用viewBox0 0 800 500。800宽、500高这个尺寸足够摆下两只轮子和一只鹈鹕也方便后面做动画时计算坐标。画之前先想清楚坐标系比画到一半再回头调整省事得多。1.2 preserveAspectRatio拉宽还是留白这是个问题viewport就是width和height决定的显示区域和viewBox的宽高比不一定会保持一致。假如viewBox是800x500但容器强制把SVG拉伸成400x500浏览器就必须决定怎么处理。默认行为是xMidYMid meet意思是等比缩放并居中多出来的空间留白。如果不想留白可以设成slice也就是等比缩放并且让较小的一边填满视口但边缘会被裁掉。svg viewBox0 0 800 500 preserveAspectRatioxMidYMid meet /svg实际使用中最常见的目标是把SVG做成响应式。我会这样写svg viewBox0 0 800 500 width100% height100%/svg不写固定的width和height只写100%让SVG跟随父容器缩放。但只要viewBox宽高比和容器不一致就会出现留白。这种问题通常可以通过设置preserveAspectRationone来强行拉伸代价是图像变形。我的建议是图标、插画场景优先用meet背景图、全屏装饰层可以用none但要接受一定程度的形变。注意在HTML里内联SVG时如果只写svg viewBox0 0 800 500而不写width和height默认的显示大小是300x150不是自动适应容器。很多“SVG显示不全”的坑都从这里来。2. 绘制鹈鹕和自行车从基本形状到路径2.1 先拆图再画图拿到“鹈鹕骑自行车”这个需求第一步不是急着写代码而是把画面拆成可绘制的对象。我的拆分方案是背景一个浅蓝色矩形一条地面线。自行车两个轮子、车架、踏板、车座、车把。鹈鹕身体、翅膀、脖子、头、大眼睛、巨大的嘴、两条腿。拆完之后你会发现每个部件都不是非用某种固定标签不可。轮子可以用circle车架可以用path鹈鹕身体可以用ellipse大嘴则适合用path画曲线。这种“先拆后画”的习惯在SVG里特别重要因为SVG没有图层概念如果不提前规划分组和顺序后画的内容会盖住先画的改起来会非常痛苦。2.2 path的d属性从M、L、C到QSVG里最强大的图形元素是path它通过d属性来描述路径。初学只需要掌握几个命令M x y把笔移动到某个点不画线。L x y从当前点画直线到目标点。C x1 y1 x2 y2 x y三次贝塞尔曲线需要两个控制点和一个终点。Q x1 y1 x y二次贝塞尔曲线只需要一个控制点和一个终点。Z闭合路径自动连回起点。鹈鹕的嘴就是个很好的练习。你不需要用很多点去描一条Q曲线就能画出上翘的弧线。比如上喙可以这样写path dM462 172 Q540 160 540 205 Q500 195 470 185 Z fill#f5a623 /这行代码的意思是从点(462,172)出发用二次贝塞尔曲线控制点(540,160)到达(540,205)再用一条Q曲线从末端折回(470,185)最后Z闭合。控制点往哪个方向拉弧线就往哪个方向鼓多试几次就能找到手感。2.3 一个能用来做动画的静态骨架为了不让你被零散代码绕晕我先给出一版可运行的静态SVG。它没有动画但已经包含了整个画面的关键结构svg viewBox0 0 800 500 xmlnshttp://www.w3.org/2000/svg rect width800 height500 fill#eaf7ff/ !-- 地面 -- line x140 y1445 x2760 y2445 stroke#69a84f stroke-width6 stroke-linecapround/ !-- 后轮 -- g idbackWheel transformtranslate(250,390) circle r80 fillnone stroke#333 stroke-width10/ line x1-70 y10 x270 y20 stroke#999 stroke-width3/ line x10 y1-70 x20 y270 stroke#999 stroke-width3/ /g !-- 前轮 -- g idfrontWheel transformtranslate(550,390) circle r80 fillnone stroke#333 stroke-width10/ line x1-70 y10 x270 y20 stroke#999 stroke-width3/ line x10 y1-70 x20 y270 stroke#999 stroke-width3/ /g !-- 车架 -- path dM250 390 L320 270 L390 390 L550 390 M320 270 L480 240 L550 390 M390 390 L480 240 fillnone stroke#e74c3c stroke-width10 stroke-linecapround stroke-linejoinround/ !-- 踏板 -- g transformtranslate(390,390) line x1-30 y10 x230 y20 stroke#555 stroke-width6/ rect x-36 y-7 width16 height14 rx3 fill#555/ rect x20 y-7 width16 height14 rx3 fill#555/ /g !-- 车身坐垫 -- rect x290 y250 width60 height16 rx8 fill#6c5b3e/ !-- 车把 -- path dM480 240 L500 215 M480 240 L450 215 stroke#333 stroke-width8 stroke-linecapround/ !-- 鹈鹕 -- g idpelican ellipse cx340 cy235 rx52 ry38 fill#fff stroke#333 stroke-width5/ path dM295 255 C255 250 245 215 255 195 C270 220 295 230 300 255 Z fill#fff stroke#333 stroke-width4/ path dM320 245 C355 250 385 240 395 220 C360 230 340 225 320 235 Z fill#d9d9d9 stroke#333 stroke-width4/ path dM370 205 C400 175 420 165 440 175 fillnone stroke#fff stroke-width22 stroke-linecapround/ circle cx450 cy172 r24 fill#fff stroke#333 stroke-width5/ circle cx458 cy166 r5 fill#222/ path dM468 168 Q545 155 545 200 Q505 195 475 182 Z fill#f5a623 stroke#333 stroke-width4/ path dM470 184 Q510 215 520 232 Q490 225 455 195 Z fill#e68a00 stroke#333 stroke-width4/ line x1330 y1270 x2380 y2380 stroke#f5a623 stroke-width7 stroke-linecapround/ line x1360 y1270 x2410 y2365 stroke#f5a623 stroke-width7 stroke-linecapround/ /g /svg这段代码里我把轮子圆心分别定位在(250,390)和(550,390)车架的中轴位置在(390,390)。后轮、前轮本身就是一个带transform的组后面做旋转动画时不需要修改内部每个点只需要对整个组施加rotate。鹈鹕则把身体、嘴、腿放在同一个组里之后做整体浮动动画就方便了。2.4 为什么我坚持用分组而不是逐个元素移动很多新手画图时会直接给每个元素单独写坐标这样画完之后再做动画你会发现需要分别修改好几个坐标。比如想让鹈鹕整体向上飘8个像素如果直接写在body、head、beak这些元素的y值里你得改七八处。但用一个g idpelican包起来之后只需要给这个组加一个animateTransform就能让整只鹈鹕上下浮动。分组带来的是“局部坐标”的便利。在g transformtranslate(250,390)内部轮子的圆心就是(0,0)辐条也围绕(0,0)展开。这样旋转动画的圆心计算就会变成一件很轻松的事。3. 让画面动起来SMIL和CSS动画这两条路线3.1 SMILSVG原生的动画机制SVG其实自带一套动画能力叫作SMIL用法非常简单。在需要动的元素里插入animate或animateTransform即可g idbackSpokes animateTransform attributeNametransform typerotate from0 0 0 to360 0 0 dur2s repeatCountindefinite/ line x1-70 y10 x270 y20 stroke#999 stroke-width3/ line x10 y1-70 x20 y270 stroke#999 stroke-width3/ /g这里的from0 0 0和to360 0 0三个数字分别是“旋转角度旋转中心x旋转中心y”。因为backSpokes所在的组已经有transformtranslate(250,390)所以在这个组内部局部坐标里圆心正好是(0,0)。于是旋转中心就写在0 0。如果你写成from0 250 390反而会造成双重偏移。SMIL最大的好处是它不需要依赖CSS和浏览器的某些兼容逻辑只要SVG支持它就能跑。而且像“无限循环旋转”“来回摆动”这类动画写起来非常直观。3.2 CSS动画与transform-box的坑用CSS动画也完全没问题比如给元素加一个class然后写.spokes { animation: spin 2s linear infinite; } keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }但在SVG上使用CSS动画有一个容易踩的坑transform-origin的默认值跟HTML元素不一样。在普通HTML里transform-origin默认是元素的中心在SVG里默认是0 0也就是SVG画布的原点。如果不设置你会发现轮子不是绕自己的圆心转而是绕画布左上角转。解决方式有两种显式设置transform-origin: 250px 390px;精确到轮子圆心。使用transform-box: fill-box;配合transform-origin: center;让浏览器以元素自身包围盒中心作为原点。但transform-box对旧浏览器的支持不太稳定。所以我在基础学习阶段更推荐直接用SMIL。等你对坐标系和SVG足够熟悉了再去折腾CSS动画也不迟。3.3 组合动画让鹈鹕自行车真正跑起来下面这版代码是在静态骨架的基础上加了动画效果。我保留了前后轮辐条的旋转、踏板的旋转以及鹈鹕整体的上下浮动。为了阅读方便这里只列出一个车轮的旋转组和鹈鹕浮动组其他部件沿用上面的静态代码。g idbackWheel transformtranslate(250,390) circle r80 fillnone stroke#333 stroke-width10/ g idbackSpokes animateTransform attributeNametransform typerotate from0 0 0 to360 0 0 dur2s repeatCountindefinite/ line x1-70 y10 x270 y20 stroke#999 stroke-width3/ line x10 y1-70 x20 y270 stroke#999 stroke-width3/ line x1-49.5 y1-49.5 x249.5 y249.5 stroke#999 stroke-width3/ line x1-49.5 y149.5 x249.5 y2-49.5 stroke#999 stroke-width3/ /g /g g idpedal transformtranslate(390,390) g animateTransform attributeNametransform typerotate from0 0 0 to360 0 0 dur2s repeatCountindefinite/ line x1-30 y10 x230 y20 stroke#555 stroke-width6/ rect x-36 y-7 width16 height14 rx3 fill#555/ rect x20 y-7 width16 height14 rx3 fill#555/ /g /g g idpelicanWrapper animateTransform attributeNametransform typetranslate values0 0; 0 -8; 0 0 dur0.6s repeatCountindefinite/ g idpelican !-- 这里放鹈鹕的所有身体部件 -- /g /g把车轮辐条和踏板旋转设成同样的dur2s会让画面看起来像是骑车的人在匀速踩踏。鹈鹕的上下浮动频率设成0.6s整体看起来既有节奏又不至于太夸张。注意SMIL的values可以写多段动画关键状态用分号分隔。0 0; 0 -8; 0 0表示“从原位到向上8像素再回原位”不写keyTimes的时候动画会把时间均匀分配到这几段状态上。3.4 动画性能的一个小建议SVG动画最常见的性能问题是大量元素同时平移、旋转、改变颜色。现代浏览器对transform和opacity的动画优化得比较好但对x、y、width、height等属性的动画往往较差。所以能给轮子只旋转辐条就不要整个轮子重绘能让鹈鹕整体translate就不要一个部件一个部件地改坐标。这个习惯对后续做复杂SVG交互很重要。4. SVG从画画到落地下载、图标站、服务器MIME和真实项目4.1 画完怎么下载现成图标去哪找SVG本质是文本文件保存的时候直接另存为.svg或者复制内部代码到你的工程里都行。很多初学者会把“下载SVG图片”理解成从网页上保存一张图片但如果网站把SVG嵌入在HTML里右键另存为往往只能存到PNG正确的做法是通过开发者工具复制SVG元素或找到对应的.svg文件地址。如果是找现成图标常用的免费SVG图标站有SVG Repo、Iconify、Tabler Icons等。搜索“svg下载”的时候最好看清图标的许可协议尤其是商用项目避免把带版权限制的图标顺手就拖进代码里。实际项目里我自己更喜欢用Iconify这种按图标名称直接引用的库既能按需加载又不用本地维护一堆SVG文件。4.2 SVG的真实业务场景一次接线图、地图边界数据除了页面图标和动图SVG在很多专业场景里也很有分量。比如电力行业常用的一次接线图大量设备符号和线路连接都需要矢量图形。用SVG画接线图的好处是它允许你用defs定义一次设备符号然后用use在多个位置复用既保持风格统一又让文件体积可控。地理信息场景里SVG也经常被用来描绘行政区域边界。像一些乡镇、村级的边界轮廓转换成SVG路径后可以在页面上做点击高亮、区域联动。这类数据通常需要从GIS软件或GeoJSON转换而来路径可能很长但只要加载一次后续的缩放和交互都很流畅。4.3 IIS和Nginx里配置svg的MIME类型有一个特别常见的现象本地直接双击SVG文件能打开放到服务器上浏览器却直接下载它或者显示成一段乱码。这多半是服务器没有声明image/svgxml这个MIME类型。如果你用Windows IIS需要在IIS管理器的“MIME类型”里新增一条扩展名.svgMIME类型image/svgxml如果在Nginx里通常已经内置了image/svgxml但有些精简配置可能会遗漏。可以在http块或server块里显式加types { image/svgxml svg; }Apache则是在.htaccess或虚拟主机配置里加AddType image/svgxml .svg配置完成后重启服务浏览器就能正常把SVG当图片渲染了。4.4 文件瘦身从手写SVG到生产级SVG手写SVG一般会用很多便于阅读的命名和缩进但生产环境需要更小的体积。SVGO是最常用的SVG压缩工具它会把冗余属性、空元素、不必要的分组全部去掉。如果你用命令行一行就能搞定npx svgo --inputpelican.svg --outputpelican.min.svg压缩后你会发现坐标精度被优化路径合并文件可能从几十KB降到几KB。另外在项目里做SVG图标时建议把fill设置成currentColorpath fillcurrentColor d.../这样CSS只要改color属性就能控制图标颜色不需要每个场景复制一份不同颜色的SVG。5. 调试SVG时最常见的高频事故现场5.1 动画旋转中心不对轮子转得飞起但完全不是绕圆心或者转着转着整辆车飞走了通常是旋转中心坐标写错了。记住一个检查原则animateTransform里的旋转中心是相对于你当前所在的坐标系。如果你在外层已经写了transformtranslate(250,390)那么旋转中心应该是0 0不需要再写250 390。如果是直接作用于绝对坐标元素才需要把旋转中心写为元素的绝对坐标。遇到动画跑偏先用浏览器开发者工具关掉动画手动改transform临时验证中心点。5.2 图片被裁切或变形“SVG显示不全”十有八九是viewBox的问题。用了一个很大的viewBox但容器很小内容就会被缩小viewBox只写了两位数字而不是四位浏览器会直接忽略它。调试时优先检查svg viewBox0 0 800 500 width100% height100%只要viewBox范围能完整包住所有图形元素显示区域和它宽高比不一致时宁可留白也别用none作死。5.3 CSS动画不生效但SMIL正常有时候你发现给某个SVG元素加了class和animation浏览器里纹丝不动。不要怀疑CSS语法先想想SVG坐标系统对transform-origin的影响。可以先在控制台给元素临时加一段transition测试看它能不能动。如果还是不动检查是不是元素被其他层盖住或者透明度设成了0。对于复杂SVG动画我个人的习惯是一位数到几位的旋转、往返平移优先SMIL涉及交互状态切换再用CSS类。两种方式并不是谁取代谁配合使用效率最高。5.4 文字在别人电脑上变了样SVG里用text元素字体依赖系统。同一段SVG在Windows上显示微软雅黑在macOS上可能显示苹方在Linux服务器渲染时可能直接变默认字体。对视觉要求高的场景最好把文本转成路径或者用一套稳定的Web字体栈text { font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif; }但要注意SVG内嵌的文本内容如果在CMS里需要被搜索和翻译那就要保留真实文本不要全部转路径。5.5 大段坐标路径看得眼晕怎么办鹈鹕的大嘴或者一张复杂的接线图很可能出现几百行dM... L... C...的情况。不要试图在SVG编辑器里纯手工维护这种长路径。平时我会把复杂形状先用Inkscape或Figma画好再导出成SVG最后用SVGO清理一遍。手写SVG适合理解原理和画简单图形复杂图形让专业工具干专业的事。另外给长路径加上明确的id或class注释非常重要。你来回头看一个月前的代码或者同事接手你的文件时一个写清楚场景的分组名能省掉大量的推测时间。最后分享一个小习惯无论画SVG还是调SVG动画我都会刻意把“静态版”和“动画版”拆成两个步骤。先确认画面本身好看、坐标正确再往里加动画。因为这个顺序能帮你把“画图问题”和“动画问题”彻底分开不会出现“轮子不转”和“画面错位”混在一起根本查不出哪里的情况。从基础标签到完整案例你只差动手画一只鹈鹕骑自行车的距离。