ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SMIL SVG 动画实战:描边生长、路径跟随与加载态

SMIL SVG 动画实战:描边生长、路径跟随与加载态 前阵子给一个后台系统做加载态UI 丢过来一张静态 SVG 图标说让它转起来顺便有点呼吸感。我第一反应是写 CSS 动画结果发现图标里有一段弧形描边我想让它像被笔画出来一样生长CSS 那边调了半小时stroke-dashoffset是动起来了但和旋转一叠加圆心就跑偏了。最后我把那堆 CSS 全删了回到 HTML 里写了几行 SMIL三分钟收工。这件事让我重新捡起了这个被很多人判了死刑的老标准。SMIL 是 SVG 原生的动画能力写在animate、animateTransform、animateMotion这几个标签里不用一行 JS不用一个 class把 SVG 文件单独丢给谁都能自己动起来。这篇笔记就是我把这套东西重新梳理一遍的结果从坐标系、时间模型讲到描边生长、路径跟随再把我这些年踩过的坑一次性倒出来适合已经会写 HTML 和 CSS、想让 SVG 真正活起来的前端和设计交付方向的同学。1. SMIL 到底还能不能用一份迟到的现状盘点1.1 从计划废弃到悄悄撤回的这段公案很多人对 SMIL 的印象停留在一句话上Chrome 要废弃它了。这事确实有出处。2015 年前后浏览器厂商公开发过关于移除 SMIL 支持的意向理由也很实在SMIL 的实现复杂、和 CSS 动画功能重叠、在渲染管线里不好做硬件加速。那段时间不少团队开始把已有的 SVG 动画往 CSS 上迁。但后面的事情没有按剧本走。撤回的理由同样充分SMIL 有两个别的方案很难平替的能力一个是animateMotion让图形沿着任意路径运动一个是当 SVG 被当作图片资源引用时img srcxxx.svg或者 CSS 的background-image外部 CSS 和 JS 根本加载不进去只有 SMIL 还能继续播放。这两条把一大批图标库、数据可视化导出文件、邮件签名图钉死在了 SMIL 上。于是这个废弃计划就这么搁置了一搁就是十年。到现在为止的真实情况是这样的环境SMIL 支持情况备注Chrome / EdgeChromium 内核支持从未真正移除新版本依然正常Firefox支持一直是支持得最完整的一个Safari桌面 / iOS支持移动端同样可用旧版 EdgeEdgeHTML不支持已经基本退出历史舞台IE 系列不支持忽略即可各类小程序 WebView大多支持取决于内核版本实测微信内置浏览器可以所以结论很直接如果你不做 IE 兼容SMIL 现在是可以放心用的。它属于不推荐作为唯一方案、但在特定场景下最优解的那一类技术而不是已经死了的技术。1.2 三条路线摆在一起对比才知道该选谁我习惯在动手前先花两分钟做一次选型判断因为动画方案选错后面返工的成本比写代码本身高得多。下面这张表是我自己实际用下来总结的对照不是从文档里抄的对比维度SMILCSS 动画JSWeb Animations API / rAF在img引用的 SVG 中播放可以不行不行在 CSSbackground-image中播放可以不行不行动画d属性路径形变原生支持支持面窄d: path()兼容性有限可以但要手写插值沿任意路径运动animateMotion一行搞定需要offset-path且对 SVG 子元素支持不统一需要自己算切线缓动曲线keySplines写法接近 cubic-bezier完善工具链成熟完全自由性能主线程计算难以合成加速transform / opacity 可上合成层可控但需要自己优化调试体验较差DevTools 面板里不可见好时间轴可视化好与脚本联动的能力弱只能靠begin事件模型可监听animationend完全可控交付独立性极高单文件自包含需要内联 SVG 或额外样式需要额外脚本看完这张表就知道该往哪边走。图形结构本身在变路径形变、沿路径跑、描边生长SMIL 赢只是透明度、位移、缩放这类CSS 更省事需要用户拖动进度、需要实时响应数据那就只能上 JS。1.3 我实际会优先选 SMIL 的四种场景第一种需要以img或背景图形式交付的单文件动效。比如给运营做一个带呼吸效果的活动标识对方要把它塞进邮件模板、塞进后台的img标签这时候 SMIL 是唯一能动的选项。第二种路径跟随类动画。像物流轨迹、流程图上一个小圆点沿着连线跑animateMotion配合rotateauto让箭头自动转向代码量少得感人。第三种描边生长动画。尤其是那种手写风格的字标、线框图标让笔画一笔一笔画出来SMIL 的stroke-dashoffset方案是最干净的。第四种几何形变动画比如一个图标在两种轮廓之间来回过渡。反过来说凡是涉及用户点一下才播要从接口拿数据决定动画轨迹需要精确暂停和拖拽进度的我会果断放弃 SMIL直接上 JS因为它的时间轴模型真的不适合被外部控制。2. 写动画之前先把坐标系和时间模型理清楚2.1 viewBox 决定了动画数字的真实含义这是新手最容易栽的地方动画里写的from0 to100这个 100 不是像素是用户单位。用户单位的实际大小由viewBox和渲染尺寸共同决定。举个具体的例子viewBox0 0 100 100配合width400那么 1 个用户单位在屏幕上就等于 4px你的动画移动 100 个单位视觉上就是移动了 400px。如果你把width改成200动画代码一个字不动视觉位移就缩短了一半。这意味着一个很好用的技巧给图标做动画时先把viewBox设成一个整数坐标系比如0 0 100 100或者0 0 24 24让圆心落在50,50或12,12所有动画数值都能心算出来不用在浏览器里反复试。我之前改过一个设计师给的图标viewBox0 0 841.9 595.3A4 尺寸导出的圆心在420.95,297.65光算这个中心点就花了我五分钟后来我直接用编辑器缩放重设了坐标系后面所有动画都顺了。还有一个容易忽略的点SVG 里的transform旋转默认是绕坐标原点 (0,0)转的不像 CSS 里有transform-origin默认居中。你想让一个图标绕自己的中心旋转必须写成rotate(360 50 50)把圆心显式传进去否则它会绕着左上角飞出屏幕。这个坑我见过太多人踩。2.2 时间值怎么写才不出错SMIL 的时间值比 CSS 灵活但也更啰嗦。可用的格式有这么几种2s、800ms、1.5min、2h— 带单位的常用写法最推荐02:30— 时钟值表示 2 分 30 秒01:20:30.500— 完整时钟值裸数字 — 按秒解析但我不建议这么写我的习惯是永远带单位而且毫秒和秒不混用同一段代码。原因很实际在begin里做偏移加法的时候1s 200ms效果是对的但一旦后面交给别人维护眼球要来回切换单位很容易看错数量级。我统一用秒0.2s而不是200ms。另外有一点要注意带引号的时钟值在begin和end里做加减时不能参与运算像end02:30-1s是可以的但begin02:305s就得写成begin02:35。所以在需要偏移的场景下老老实实用带单位的小数写法最省心。2.3 begin 和 end 才是 SMIL 真正的编排语言大多数人第一次学 SMIL 只关心dur和from/to但真正让 SMIL 有编排能力的是begin和end支持的这几种写法。第一种是相对文档时间轴的偏移begin0s、begin1.5s。注意这里的起点是 SVG 文档的时间零点对于内联在 HTML 里的 SVG起点大约在 SVG 被解析的时刻。第二种是锚定到另一个动画的生命周期beginringGrow.end、beginringGrow.end 0.2s。这是它最像时间轴编排的地方你可以像搭流水线一样串起一串动画。第三种是事件触发beginclick、beginmouseover、beginmyAnchor.click。第四种是手动触发beginindefinite然后由脚本调用element.beginElement()。还有一个专门用来截断别人的属性end。比如你想让某个循环动画在被点击时停下来可以写endstopBtn.click这比在 JS 里到处取消要优雅得多。这里有个新手必踩的坑如果某个动画设了repeatCountindefinite那另一个动画写begin它.end是永远不会触发的因为一个无限重复的动画没有结束这个时刻。你需要的是begin它.begin 2s这类基于起点的写法。2.4 fill 属性的语义和 CSS 里的 fill 完全是两回事这个坑我刚学的时候踩得很难受。在 SVG 里path fill#f00的fill是填充色但在animate标签上fill是一个完全不同的东西它描述的是动画结束后元素的呈现状态fillremove默认— 动画结束后元素回到没有任何动画值的状态也就是基础属性值fillfreeze— 动画结束后元素保持动画最后一帧的值我最初做一个生长后定格的线条动画只写了from和to结果动画跑完线又缩回去了盯着屏幕看了半天才反应过来是默认的remove在起作用。后来我在所有需要播完保持的动画上都显式写了fillfreeze这个习惯救了我很多次。3. 四个动画元素各自负责什么活3.1 animate属性值的插值主力animate是最基础的一个它负责把某个属性从 A 值平滑过渡到 B 值。核心属性有这么几个attributeName指定要动哪个属性from和to指定起止值dur指定时长repeatCount控制循环次数。如果要做多点路径用values替代from/to写成values0;50;30;100这种分号分隔的形式。svg width240 height120 viewBox0 0 240 120 xmlnshttp://www.w3.org/2000/svg circle cy60 r18 fill#3b82f6 animate attributeNamecx values30;210;30 dur2.4s repeatCountindefinite calcModespline keyTimes0;0.5;1 keySplines0.4 0 0.6 1;0.4 0 0.6 1/ /circle /svgattributeName是大小写敏感的而且必须写 SVG 里规范的属性名。带连字符的还是带连字符stroke-width、stroke-dashoffset驼峰的还是驼峰viewBox。我曾经把stroke-dashoffset写成strokeDashoffset浏览器不报任何错动画就是不跑排查了二十分钟。attributeType我一般不写。它有三个取值CSS、XML、auto默认auto会自动判断。手动指定反而容易在这个属性既像 CSS 又像 XML的时候猜错交给浏览器判断更稳。3.2 animateTransform旋转、缩放、平移的独立通道animateTransform专门管transform属性用type指定变换类型有translate、scale、rotate、skewX、skewY五种。svg width160 height160 viewBox0 0 160 160 xmlnshttp://www.w3.org/2000/svg g animateTransform attributeNametransform typerotate from0 80 80 to360 80 80 dur1.2s repeatCountindefinite/ rect x70 y20 width20 height60 rx6 fill#f97316/ /g /svg这里from0 80 80里的后两个数字就是旋转中心。很多教程会省略这两个值省了之后就是绕原点转如果你的图形本来就以原点为中心那没问题否则一定要显式写。多个 animateTransform 同时作用在一个元素上时是最容易翻车的地方。默认additivereplace也就是后一个动画的值会整个替换掉前一个而不是叠加。你想同时做一个平移和一个旋转就必须给它们都加上additivesumg animateTransform attributeNametransform typetranslate values0 0;40 0;0 0 dur3s repeatCountindefinite additivesum/ animateTransform attributeNametransform typerotate from0 to360 dur3s repeatCountindefinite additivesum/ rect x-10 y-10 width20 height20 fill#8b5cf6/ /g即便加了additivesum多个变换的应用顺序也是会互相影响的先平移再旋转和先旋转再平移结果完全不同。我的经验法则是如果一个元素要同时做多种变换宁可多套一层g每个g只负责一种变换。这样顺序一目了然调试起来也能逐个禁用比在additive上反复试探省时间得多。3.3 animateMotion 与 mpath让图形沿着自己的路线跑这是 SMIL 最不可替代的一个能力。animateMotion让元素沿着一条路径运动路径可以直接写在path属性里也可以用mpath引用一个已有的path元素。svg width420 height200 viewBox0 0 420 200 xmlnshttp://www.w3.org/2000/svg defs path idroute dM30,170 C130,20 290,20 390,170 fillnone/ /defs use href#route stroke#cbd5e1 stroke-width2 stroke-dasharray6 6/ g circle r10 fill#ef4444/ animateMotion dur3s repeatCountindefinite rotateauto mpath href#route/ /animateMotion /g /svgrotateauto是这里的灵魂它会让元素自动按照路径的切线方向旋转也就是车头朝着前进方向。另外还有rotateauto-reverse方向反过来适合做那种箭头朝后的效果。如果你写具体的角度值比如rotate45那就是固定角度不跟随路径。mpath引用路径时注意href和xlink:href的区别。新标准里直接写href就行现代浏览器都支持。如果你还需要兼容很老的实现可以两个都写上浏览器会优先用href。配合use把路径本身也画出来用户就能看到轨道这个组合在流程演示类页面里特别好用。我做物流轨迹演示时就是用一个淡灰色虚线画路线一个实心圆点跑在上面加上keyPoints让它在几个关键节点上停顿一下。keyPoints的用法是配合keyTimes一起的keyPoints0;0.3;0.3;0.7;0.7;1配合keyTimes0;0.1;0.2;0.5;0.6;1就能让点在路径的某些位置停一会儿再走。这个技巧在做分步讲解类动画时非常管用。3.4 set不插值直接切set是animate的简化版它不做任何插值只在一个时间点把属性切成新值。语法很干净circle cx60 cy60 r22 fill#94a3b8 set attributeNamefill to#ef4444 begin1.2s fillfreeze/ /circle注意set自己也有fill属性默认是remove也就是说它会在某一个时间点把颜色切成红色然后又切回去。涉及定格的场合必须写fillfreeze。我一般用set来切换visibility、fill这类非连续的值需要平滑过渡的时候还是老老实实用animate。4. 描边生长stroke-dasharray 加 stroke-dashoffset 的手工活4.1 为什么虚线能画出线来这套原理其实很直观只是第一次听会觉得绕。stroke-dasharray是把一条实线切成一段段虚线值是实线段长度 空白段长度的循环。stroke-dashoffset是让这个虚线图案整体沿着路径方向平移。现在关键来了如果你把stroke-dasharray设成总长度 总长度那么整条路径上就只有一段实线和一段等长的空白。再把stroke-dashoffset从总长度动到0视觉上就是实线段从路径末端一点点推进到了整个路径看起来就像有人在用笔把它画出来。svg width200 height200 viewBox0 0 200 200 xmlnshttp://www.w3.org/2000/svg path idring dM50,100 a50,50 0 1,1 100,0 a50,50 0 1,1 -100,0 fillnone stroke#0ea5e9 stroke-width6 stroke-linecapround stroke-dasharray314.16 314.16 stroke-dashoffset314.16 animate attributeNamestroke-dashoffset from314.16 to0 dur1.4s fillfreeze/ /path /svg半径 50 的圆周长是 2π×50 ≈ 314.16这个数字就是这么来的。4.2 路径总长度怎么拿到手上面这个圆好算但现实里大多是手绘路径、logo 轮廓长度没法心算。有三种拿法我按优先级排一下。第一种用浏览器的getTotalLength()。打开开发者工具选中那个path元素在 Console 里输入$0.getTotalLength()$0是 DevTools 里代表当前选中元素的一个快捷变量直接就能拿到精确的浮点数值。这是我用得最多的方法一次拿到写进 SVG 里后面就不需要 JS 了。// 如果你需要批量处理多个路径 document.querySelectorAll(.draw-line).forEach((p) { const len p.getTotalLength(); p.setAttribute(stroke-dasharray, ${len} ${len}); p.setAttribute(stroke-dashoffset, len); });第二种用一个足够大的值当近似。只要stroke-dasharray的第一个值大于等于真实路径长度效果就是对的只是会多出一小段空白时间。对于长度不确定的路径我经常直接写stroke-dasharray2000 2000能动就行多几毫秒的空白肉眼根本看不出来。第三种用getPointAtLength()采样估算。这个用法比较冷门但有时候对调试有用沿着路径取若干个点累加相邻点之间的距离。一般不推荐除非你在做一个纯静态导出的工具链手里没有浏览器环境。4.3 多条路径接力生长怎么排一个 logo 通常由好几段路径组成想让它们一段接一段地画出来有两种思路。第一种是用begin串时间。每一段的begin都指向上一段的结束path classseg d... stroke-dasharray300 300 stroke-dashoffset300 animate ids1 attributeNamestroke-dashoffset from300 to0 dur0.8s begin0s fillfreeze/ /path path classseg d... stroke-dasharray400 400 stroke-dashoffset400 animate attributeNamestroke-dashoffset from400 to0 dur0.9s begins1.end fillfreeze/ /path path classseg d... stroke-dasharray260 260 stroke-dashoffset260 animate attributeNamestroke-dashoffset from260 to0 dur0.6s begins1.end 0.9s fillfreeze/ /path这种写法的好处是每一段的时长可以独立调改一段不影响其他段的时长坏处是只要你改了任意一段的dur后面所有的偏移量都得重新手算一遍。所以我在实际项目里更倾向于用第二种。第二种是把时间压缩到同一个动画的keyTimes里。缺点是需要把所有路径的长度归一化但好处是整条时间轴只有一个源头调总时长只改一个dur。项目时间紧的时候我会选第一种需要反复调优节奏的场合我会选第二种。这里插一个我踩过的坑stroke-linecapround会让描边两端各多出半个线宽的圆头视觉上长度就比真实路径长了一点生长动画结束时会有轻微溢出感。做精细的线标时我会把linecap设成butt或者干脆在两端各留一点点dashoffset余量比如从315动到1而不是从314.16到0。5. 时间编排进阶同步、缓动和循环的边界5.1 用 begin 的依赖链搭一条流水线SMIL 的时间编排能力是被严重低估的。只要给每个动画加个id后面的动画就能引用前面的生命周期。这套机制写起来像这样svg width240 height240 viewBox0 0 240 240 xmlnshttp://www.w3.org/2000/svg circle cx120 cy120 r60 fillnone stroke#e2e8f0 stroke-width10/ circle cx120 cy120 r60 fillnone stroke#2563eb stroke-width10 stroke-linecapround stroke-dasharray376.99 376.99 stroke-dashoffset376.99 transformrotate(-90 120 120) animate idgrow attributeNamestroke-dashoffset from376.99 to0 dur1.2s fillfreeze/ /circle circle cx120 cy120 r26 fill#2563eb opacity0 animate idpulse attributeNameopacity values0;1;1;0 keyTimes0;0.15;0.6;1 dur1.6s begingrow.end - 0.3s repeatCountindefinite/ /circle /svg注意begingrow.end - 0.3s这个写法它让第二个动画提前 0.3 秒开始制造出重叠这样节奏不会显得一顿一顿的。允许重叠是让动画显得高级的关键纯串行的时间线看久了很生硬。还有一个我强烈建议养成的习惯给所有被引用的动画 id 加前缀比如anim-ring-grow。因为 SVG 的 id 是整个 HTML 文档级的如果你在一个页面里内联了三个 SVG每个里面都有一个叫grow的动画begingrow.end到底指向谁就说不清了。这个问题在组件化开发里尤其明显同一个图标组件被复用了好几次id 全撞在一起。稳妥的做法是让组件在挂载时动态给 id 加个唯一后缀或者干脆在 SVG 内部用use的 shadow DOM 把结构封起来。5.2 calcMode 和 keySplines 决定了动画的手感calcMode控制的是关键帧之间怎么插值有四个值calcMode含义适用场景linear匀速插值默认旋转、循环类动画discrete不插值直接跳变逐帧动画、状态切换paced按距离匀速忽略 keyTimes路径运动想保持恒定速度时spline用贝塞尔曲线控制速度需要手感、需要缓动时大多数人做动画都不会用spline结果就是所有动画都硬邦邦的匀速运动。而keySplines的写法和 CSS 的cubic-bezier()完全一致四个数字前两个是起点的控制点后两个是终点的控制点。animate attributeNamer from6 to14 dur0.9s repeatCountindefinite calcModespline keyTimes0;1 keySplines0.34 1.56 0.64 1/这个0.34 1.56 0.64 1的第二个值是大于 1 的会产生过冲效果也就是俗称的弹性回弹。这种参数在 CSS 里也能用但在 SMIL 里配合 SVG 图形的位置变化观感更自然。使用spline有三条硬性规则违反任意一条浏览器都不会报错只会让动画静默失效这是最难排查的一类问题keyTimes的项数 关键帧数量keySplines的项数 keyTimes项数 − 1values的项数 ≥keyTimes的项数我建议每次写完spline动画都在脑子里默数一遍这三个数字别嫌麻烦这个检查比在浏览器里瞎猜快得多。5.3 restart、repeatCount 与 accumulate 的边界restart控制动画能不能被重新触发有三个值always默认任何时候都能重启、whenNotActive只在没跑的时候才能重启、never跑一次就锁死。这个属性在交互触发的动画里很关键。比如你做一个点击放大的效果用户狂点的时候如果restartalways动画会被打断重来看起来一卡一卡的改成whenNotActive就顺滑了。repeatCount除了写数字和indefinite之外还可以写小数比如repeatCount2.5表示跑两遍半。repeatDur则是从另一个角度限制最多跑 5 秒不管你跑了几遍。这两个一起用的时候谁先到谁生效。accumulatesum这个属性冷门但有用。默认情况下每次循环都是从from重新开始如果加上accumulatesum每次循环会在上一次结束值的基础上继续累加。做那种转一圈叠一圈的效果时配合additivesum能省掉很多手动计算。6. 实战把一枚静态图标改成会自己动的加载动画6.1 拿到素材先做减法这一步很多人会跳过直接开始加动画然后越加越乱。我的流程是先把素材洗干净。从 drawio 这类工具导出的 SVG往往会在最外层塞一个foreignObject或者一堆metadata还有大段的style内联样式。LeaferJS 这类库导出的 SVG 则是一大堆嵌套的g每一层都带着自动生成的transform矩阵。这两种都不适合直接加 SMIL 动画原因是动画的坐标系被这些无意义的变换搞乱了你写rotate(360 50 50)的时候根本不知道 50,50 在最终渲染里落在哪。我的做法是在编辑器里把图形重新整理成一个干净坐标系 少数几层分组删掉所有空g、无用的id、以及没有被引用的defs。这一步做完图标的代码量通常会掉一半后面加动画反而更快。6.2 把时间轴在纸上先画一遍我习惯在写代码前先用文字把时间轴列出来形式大概是这样的时间段动作实现方式0 → 1.2s蓝色圆环从无到有生长stroke-dashoffset从 314.16 到 01.2s → 之后圆环持续旋转animateTransformrotate无限循环1.2s → 1.6s中心图标从 0 弹入animateTransformscale带缓动1.6s → 之后中心图标轻微呼吸animate改 opacity无限循环这样列完之后代码基本就是照着填了。跳过这一步直接写代码的结果通常是动画能跑但节奏乱改起来像在拆毛线球。6.3 完整代码svg width200 height200 viewBox0 0 200 200 xmlnshttp://www.w3.org/2000/svg roleimg aria-label加载中 defs path idloader-ring dM-50,0 a50,50 0 1,1 100,0 a50,50 0 1,1 -100,0 fillnone/ /defs !-- 外层负责把图形挪到画布中心 -- g transformtranslate(100,100) !-- 内层负责整体旋转绕 (0,0) 转正好是圆心 -- g animateTransform attributeNametransform typerotate from0 to360 begin1.2s dur1.6s repeatCountindefinite/ !-- 底轨 -- use href#loader-ring stroke#e2e8f0 stroke-width10/ !-- 生长的主环 -- use href#loader-ring stroke#2563eb stroke-width10 stroke-linecapround stroke-dasharray314.16 314.16 stroke-dashoffset314.16 animate attributeNamestroke-dashoffset from314.16 to0 dur1.2s begin0s fillfreeze/ /use /g !-- 中心图标带弹性缩放入场 -- g transformscale(0) animateTransform attributeNametransform typescale values0;1.15;1 keyTimes0;0.7;1 calcModespline keySplines0.2 0.8 0.3 1;0.3 0 0.7 1 begin1.05s dur0.45s fillfreeze/ circle r26 fill#2563eb/ path dM-9,1 l6,6 l12,-13 fillnone stroke#ffffff stroke-width4.5 stroke-linecapround stroke-linejoinround/ /g /g /svg6.4 实测之后我改过的几处细节第一处主环的stroke-dashoffset初值必须写在元素属性上。我第一版只写了animate里的from314.16结果页面加载的第一帧闪了一下完整的圆环然后才开始生长。原因是动画开始之前的那一瞬间元素用的是基础属性值而基础值是0也就是完整的圆。把初值同步写到stroke-dashoffset属性上之后这个问题就没了。第二处旋转动画的begin不能写0s。我一开始想让它一开始就转结果圆环一边生长一边转视觉上有点晕。改成等生长结束再开始转之后整个动画的叙事就清楚了先画出来再转起来。第三处中心图标的入场我用了小于 1 秒的时长加过冲缓动。0.45 秒加1.15的过冲比 1 秒的匀速缩放进场要精神得多。这个参数我是试了好几轮才定下来的太短的过冲看不见太长会显得拖沓0.4 到 0.5 秒是比较好用的区间。第四处整个动画在img里也能跑。这一点我专门验证过把上面这段 SVG 存成一个.svg文件用img srcloader.svg引用动画照常播放。这是 SMIL 相对 CSS 方案最实在的一个优势做交付的时候就少了很多你必须把它内联到页面里的解释成本。7. 踩坑记录从工具链到浏览器行为7.1 优化工具会把 SMIL 的引用关系剪断这个坑很隐蔽。我习惯在交付前用 SVGO 之类的工具压一遍 SVG通常能砍掉 30% 到 50% 的体积。但这类工具的默认预设里有个清理id的插件它会扫描文档里所有use、url(#xxx)之类的引用把没被引用到的id删掉。问题在于mpath href#route和begingrow.end这两种引用很多优化工具是不认识的。于是它会把idroute和idgrow当成垃圾删掉你的动画就直接失效了而且文件打开看还在没有任何报错就是不动。我的处理办法有两个一是在做优化时关掉清理 id 的那一步或者把它配置成只压缩不删除二是在 id 命名上做标记比如统一用smil-前缀这样你在 diff 里一眼就能看出哪个 id 是被删掉的。这两种我都在用前者更省事后者更保险。顺带说一句从 drawio 导出的 SVG 里经常带着foreignObject包裹的文本图层有些浏览器对这类结构的动画支持不太一致。如果你的图标里有文字建议让设计把文字转成路径再导出会省掉一堆麻烦。7.2 display:none 和页面不可见时的行为这是一个真实存在但很少被讨论的差异。当包含动画的 SVG 被display:none隐藏时不同浏览器对动画时间轴的处理不完全一致有的会继续推进时间有的会暂停。这就导致一个很典型的现象——你在一个隐藏的 Tab 面板里放了一个加载动画等用户切过去的时候动画已经跑完了或者卡在了奇怪的位置。我的处理方式是凡是可能被隐藏的动画都用beginindefinite声明然后在显示的时候用脚本触发。const svg document.querySelector(#loader-svg); const anim svg.querySelector(#grow); // 面板显示时再启动 function startWhenVisible() { if (anim anim.beginElement) { anim.beginElement(); } }另一个容易忽略的情况是浏览器后台标签页的节流。用户切到别的标签页五分钟再切回来动画的时间轴会怎么表现各浏览器的策略并不统一。所以不要用 SMIL 的时序去做业务逻辑比如动画播完就发一个埋点请求这种设计在后台标签页场景下一定会出问题。动画就让它安安静静地做动画。7.3 和 CSS 混用时transform 是必争之地SMIL 的animateTransform和 CSS 的transform都在改同一个属性。这两个东西同时作用在一个元素上时优先级关系会变得非常微妙而且各浏览器的实现细节还有差别。我实测下来的建议只有一条一个元素上的 transform要么全交给 SMIL要么全交给 CSS绝不混用。需要SMIL 管位置、CSS 管悬停缩放这种需求时多包一层g各管各的。opacity、fill这类属性混用时冲突会小一些但也需要注意CSS 里写的transition会去追 SMIL 正在改的值两者会打架表现是动画抖或者卡顿。我的原则是只要这个元素上有 SMIL 动画就不要给它挂transition。7.4 一个不得不提的可访问性问题这是我在做无障碍改造时发现的CSS 的prefers-reduced-motion媒体查询管不到 SMIL 动画。这个媒体查询是用来尊重减少动态效果这个系统偏好的用户开了这个开关CSS 动画会自动降级但 SMIL 完全不受影响照样在转。对于有前庭功能障碍的用户一个满屏旋转的加载动画是实实在在的困扰。目前唯一的办法是走脚本const reduceMotion window.matchMedia((prefers-reduced-motion: reduce)); const svgRoot document.querySelector(#animated-svg); if (reduceMotion.matches svgRoot svgRoot.pauseAnimations) { svgRoot.pauseAnimations(); } reduceMotion.addEventListener(change, (e) { if (!svgRoot || !svgRoot.pauseAnimations) return; e.matches ? svgRoot.pauseAnimations() : svgRoot.unpauseAnimations(); });pauseAnimations()会把所有 SMIL 动画定格在当前位置。如果你想要更彻底的降级可以在检测到偏好之后直接把整棵 SVG 换成一张静态的替代图。这比让它定格在某个中间状态更干净。最后再说一个性能上的经验值。SMIL 动画在 Chrome 里是在主线程上跑的不像 CSS 的transform和opacity能上合成层。这意味着一屏里同时跑的 SMIL 动画数量是有上限的。我自己的经验是单个页面里同时活跃的 SMIL 动画元素控制在十几个以内问题不大超过三十个就明显能感觉到滚动时的掉帧了。如果你真的需要几十上百个元素一起动那这个场景本来也不该交给 SMIL换成用canvas或者干脆做成一整张序列帧会更现实。
返回列表