1. 为什么用SVG做动画先把技术选型这关过了做前端或独立开发的朋友遇到画一个2D动画的需求时第一反应多半是Canvas、CSS动画甚至直接上WebGL。但如果你只想做一个轻量、清晰、可缩放的矢量图形动画——比如一个鹈鹕骑着自行车在页面上跑——SVG反而是性价比最高的选择。它不需要任何第三方库浏览器原生支持代码写出来就是文本文件随处可改缩放不失真。SVG全称Scalable Vector Graphics本质是XML描述的矢量图形。跟Canvas那种画完就变成像素矩阵的思路不同SVG里的每个元素都是独立的DOM节点。这意味着你可以用CSS、JavaScript甚至SVG自带的SMIL动画系统直接对某个圆、某条路径做变换操作。比如想让自行车轮子转起来只需要给轮子包一个animateTransform标签指定从0度旋转到360度就行。不需要自己算坐标、不需要重绘、不需要requestAnimationFrame浏览器帮你把每个中间帧都渲染好了。这次我以一个鹈鹕骑自行车的动画为例子把SVG的基础知识串一遍。鹈鹕元素多——大嘴、长脖子、圆滚滚的身体、翅膀再加上一辆自行车正好覆盖路径绘制、图形组合、坐标系统、动画控制这些核心知识点。适合刚入门前端、想系统学一下SVG的同学也适合那些做过一点SVG但总搞不清viewBox和transform关系的朋友。要提醒的是我下面讲的方案不一定是最简方案但一定是维护性和可读性比较好的方案。SVG写起来容易写烂了更容易——一堆写死的坐标、嵌套混乱的g标签、动画硬编码到最后想改一个小部件都像拆炸弹。这个案例会把拆解思路一并讲清楚你可以直接照着敲一遍再去发挥自己的创意。2. 先搞懂SVG的地基坐标系、viewBox与基础图形2.1 viewBox才是真正的画布不是width和height很多初学者第一个迷惑的地方就在这里。svg width800 height500只决定SVG在页面上的显示尺寸真正决定你画图坐标基准的是viewBox。可以这样理解width和height是窗口尺寸viewBox是窗口里的世界坐标范围。举个例子svg width800 height500 viewBox0 0 800 500 xmlnshttp://www.w3.org/2000/svg /svgviewBox0 0 800 500的意思是把坐标原点(0,0)放在SVG区域的左上角横向范围0到800纵向范围0到500。在内部绘制的所有图形都按这个虚拟坐标系定位。窗口被拉伸或缩小时坐标系会自动缩放适配。这个特性日常特别有用。比如你在手机上想缩小展示整个动画不需要改任何图形坐标只需要调整外层容器的CSS宽度viewBox会自动等比缩放。反过来如果你想放大画面中的某一部分作为聚焦镜头直接改viewBox的前两个值即可——把原点偏移到想要的位置把宽高范围缩小视觉上就像镜头拉近了。我一般习惯先定viewBox范围再反推width和height。这个案例里我把viewBox设为0 0 900 600宽度留足给鹈鹕和自行车移动的空间。2.2 基础图形圆、椭圆、直线、折线的坐标法则SVG的基础图形很直观每个都有自己的关键属性。简单梳理一下圆形circle cx150 cy400 r40cx和cy是圆心坐标r是半径。椭圆ellipse cx300 cy200 rx80 ry50rx是横向半径ry是纵向半径。直线line x150 y1100 x2200 y2100两个端点坐标。折线polyline points10,10 50,30 80,20一串坐标点。多边形polygon points...自动闭合。矩形rect x100 y100 width60 height40 rx5rx是圆角半径。画鹈鹕和自行车时圆、椭圆、直线就够用了。车圈用圆车架用直线车座用椭圆鹈鹕身体用椭圆加路径。坐标的规则跟数学里的直角坐标系有一个关键区别y轴向下是正方向。也就是说数值越大越往下。初学者经常把图形画到天上去就是因为没绕过这个弯——想往上移动某个部件应该减小y值。circle cx450 cy430 r55 fillnone stroke#333 stroke-width6/ circle cx270 cy430 r55 fillnone stroke#333 stroke-width6/这两行是自行车两个轮子。前轮在右横坐标450后轮在左横坐标270共同高度在430。这个案例里地面在y485轮子半径55轮心在430正好贴合地面。2.3 路径pathSVG里最强大也最容易劝退的东西基础图形之外的任意形状基本都得靠path来完成。鹈鹕的大嘴、弯曲的脖子、翅膀的弧度用圆和直线拼出来会很僵硬但path可以用贝塞尔曲线轻松搞定。path的d属性是一串命令加坐标M x y // move to把画笔移到某点 L x y // line to从当前点画直线到某点 C x1 y1 x2 y2 x y // 三次贝塞尔曲线两个控制点加一个终点 Q x1 y1 x y // 二次贝塞尔曲线一个控制点加终点 Z // 闭合路径连回起点所有命令都区分大小写。大写命令是绝对坐标小写命令是相对坐标。对于新手我建议先用绝对坐标因为画的时候脑子里是某个点在哪的思路相对坐标容易越画越乱。鹈鹕的嘴巴我这样写path dM 400 180 Q 520 170 560 220 Q 530 220 500 200 Q 450 205 400 205 Z fill#f0a030 stroke#c47720 stroke-width3/我来拆解一下起点在鹈鹕头部的下缘(400,180)第一段贝塞尔曲线从嘴角向左上方延伸到喙尖(560,220)第二段从喙尖收回上喙下边缘第三段回到嘴基底部。最终闭合形成一个鸭梨形的长喙。控制点的位置是反复试出来的——画路径没有一次成功的都是改两三次坐标才顺眼。3. 图形画出来了怎么让它们组织起来分组与transform3.1 用g做逻辑分组能救你的命一个动画场景里有十几二十个元素如果不分组后面改起来会崩溃。g标签就是一个容器可以把多个图形包在一起统一的样式、统一的变换都能直接作用于整组。这个鹈鹕骑自行车的案例我用g做三层组织最外层整个自行车鹈鹕作为一个整体用于整体位移比如让画面往右移动。中层自行车是一组鹈鹕是一组背景元素太阳、云朵、地面线是一组。内层自行车内部两个轮子各自再单独分组——因为轮子要独立旋转。代码结构长这样g idbike-and-pelican g idbackground.../g g idbike g idrear-wheel.../g g idfront-wheel.../g line idframe-top .../ ... /g g idpelican ellipse idbody .../ path idneck .../ path idbeak .../ g idwing.../g /g /g有了id和分组动画只管引用对应的id就行。比如给轮子加旋转动画只需要animateTransform指向rear-wheel这个分组。分组不理想、层级错乱带来的最大问题是动画作用在错误的元素上或者一个元素的样式被另一个容器的属性干扰。3.2 transform的叠加顺序真的要较真SVG的transform属性支持translate平移、rotate旋转、scale缩放、skew倾斜。这里有一个非常容易踩坑的细节多个变换写在一起时从右往左生效。比如transformtranslate(100, 0) rotate(45)实际执行顺序是先旋转45度再向右平移100。反过来的话旋转中心的位置会不一样画出来的结果完全不是你想要的。更安全的做法是分开写transformtranslate(100, 0)然后旋转另开一个标签写或在同一个transform里按预期顺序排列。初学者建议先只用translate把旋转和缩放独立到内层分组里——这样内层旋转不会影响外层平移。3.3 transform-origin是旋转动画的定海神针在CSS里用transform做动画时transform-origin控制旋转中心。SMIL动画后面详说也一样但SMIL中变化的是rot角的含义rotate(角度, cx, cy)可以直接指定旋转中心。更通用的做法是用CSS#rear-wheel { transform-origin: 270px 430px; animation: spin 2s linear infinite; } keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }这个例子里后轮圆心在(270,430)旋转中心必须也是(270,430)轮子才会原地转动。如果写成默认的中心点(0,0)轮子就会沿着大圆弧甩出去像一个失控的回旋镖。我第一天写这个案例时就犯了这个错——轮子画出来后一旋转整个轮子跑到画面右上角去了。最后发现就是transform-origin的问题。4. 逐步实操鹈鹕骑自行车的完整绘制过程4.1 先把场景布局在脑子里过一遍动手写代码之前我一般在纸上画个简略的布局图。比例很重要不然东挪西挪半天都拼不成一个整体。这个案例的布局大致是背景宽度900高度600天空用浅蓝色填充一个矩形地面用一条浅绿色矩形表示右侧加一个太阳、两朵云。自行车后轮圆心在(270,430)前轮圆心在(450,430)轮距180。车架用直线连接轮心到中轴(360,430)再到车座(350,340)、车把(470,300)。鹈鹕骑在车座上身体底部大概在(340,300)附近整体中心略微偏后。脖子向上并向左侧弯头部在(400,180)附近大嘴向外伸到(560,220)附近。动画两个轮子绕各自圆心旋转中轴脚蹬绕(360,430)旋转鹈鹕身体做小幅度的上下颠簸翅膀做扇动云朵缓慢水平漂移。草图画完坐标心里就有谱了。不用精确到像素先把相对位置关系定死——轮距多少、鹈鹕坐姿高度多少、嘴伸多长这些决定了整个画面是否协调。4.2 搭建SVG骨架并绘制自行车自行车部分是整个场景中最工程化的部分需要靠直线和圆形精确拼接。我给出完整代码svg width900 height600 viewBox0 0 900 600 xmlnshttp://www.w3.org/2000/svg !-- 背景 -- rect x0 y0 width900 height600 fill#dff0fa/ rect x0 y485 width900 height115 fill#cde6c3/ !-- 后轮和外胎 -- g idrear-wheel circle cx270 cy430 r55 fillnone stroke#333 stroke-width8/ circle cx270 cy430 r48 fillnone stroke#666 stroke-width3 stroke-dasharray8 8/ line x1270 y1430 x2270 y2382 stroke#888 stroke-width4/ circle cx270 cy430 r5 fill#333/ /g !-- 前轮 -- g idfront-wheel circle cx450 cy430 r55 fillnone stroke#333 stroke-width8/ circle cx450 cy430 r48 fillnone stroke#666 stroke-width3 stroke-dasharray8 8/ line x1450 y1430 x2450 y2382 stroke#888 stroke-width4/ circle cx450 cy430 r5 fill#333/ /g !-- 车架 -- g stroke#e04a2f stroke-width5 stroke-linecapround line x1270 y1430 x2360 y2430/ line x1360 y1430 x2350 y2340/ line x1350 y1340 x2270 y2430/ line x1360 y1430 x2450 y2430/ line x1450 y1430 x2470 y2300/ line x1470 y1300 x2350 y2340/ /g !-- 车座和车把 -- ellipse cx350 cy332 rx22 ry8 fill#333/ line x1470 y1300 x2450 y2275 stroke#555 stroke-width4 stroke-linecapround/ !-- 脚蹬和链条 -- g idcrank circle cx360 cy430 r18 fillnone stroke#555 stroke-width4/ line x1360 y1430 x2340 y2410 stroke#777 stroke-width4 stroke-linecapround/ line x1360 y1430 x2380 y2450 stroke#777 stroke-width4 stroke-linecapround/ /g /svg注意车架那几根线我故意用三角形结构——自行车车架本来就是多个三角形拼接这种结构在视觉上最稳定。车把和车座的坐标要根据坐姿调整多试几次就有手感了。脚蹬的crank分组画了两个相对180度的踏杆动画时旋转这个分组就会有踩踏板的错觉。4.3 绘制鹈鹕从哪下笔都不会崩的分步法鹈鹕的整体可以拆成身体、尾巴、脖子、头、嘴、眼睛、翅膀、腿。每画一个部分就换一种颜色先用大形状占位再精修小部件。我的经验是先画最大的身体再画脖子连接身体和头最后补嘴和翅膀。!-- 鹈鹕主体 -- g idpelican !-- 尾巴 -- path dM 245 290 Q 210 270 230 310 Z fill#f5f0e0 stroke#d0c8b0 stroke-width2/ !-- 身体 -- ellipse cx315 cy285 rx75 ry45 fill#f5f0e0 stroke#d0c8b0 stroke-width3/ !-- 腿 -- line x1330 y1330 x2330 y2365 stroke#e8a040 stroke-width5 stroke-linecapround/ line x1360 y1330 x2360 y2360 stroke#e8a040 stroke-width5 stroke-linecapround/ !-- 脖子和头 -- path dM 360 270 Q 390 200 400 180 fillnone stroke#f5f0e0 stroke-width28 stroke-linecapround/ circle cx400 cy175 r28 fill#f5f0e0 stroke#d0c8b0 stroke-width3/ !-- 嘴 -- path dM 400 180 Q 520 170 560 220 Q 530 220 500 200 Q 450 205 400 205 Z fill#f0a030 stroke#c47720 stroke-width3/ !-- 眼睛 -- circle cx410 cy162 r4 fill#222/ circle cx414 cy162 r2 fill#fff/ !-- 翅膀 -- g idwing path dM 300 260 Q 350 240 380 275 Q 340 285 300 280 Z fill#e8dcc8 stroke#d0c0a0 stroke-width2/ /g /g脖子我直接用粗线条加圆角端点来画比用路径拼轮廓省事得多。你想要更真实的脖子形状可以把stroke加宽到28像素然后让路径自然弯曲——这是偷懒但很实用的技巧。注意身体的y坐标和车座的关系车座在(350,332)鹈鹕身体圆心在(315,285)身体下缘在330附近刚好落在车座上。视觉上就是鹈鹕稳稳坐在车上。4.4 用SMIL让画面动起来轮子、脚蹬、颠簸和飞行的翅膀静态的SVG画好之后动画就有地方施展了。SMIL全称Synchronized Multimedia Integration Language已经内置于浏览器中不需要额外JS库。最基础的动画标签是animate用于驱动数字属性比如坐标、透明度、颜色。还有animateTransform驱动transform变化。再看轮子的旋转g idrear-wheel ... animateTransform attributeNametransform typerotate from0 270 430 to360 270 430 dur2s repeatCountindefinite/ /gfrom0 270 430表示从0度开始旋转中心是(270,430)to360 270 430表示转到360度。dur2s是一圈2秒repeatCountindefinite表示无限循环。这样轮子就连续转起来了。前轮的写法完全一样只是中心点换成(450,430)。脚蹬旋转animateTransform attributeNametransform typerotate from0 360 430 to360 360 430 dur2s repeatCountindefinite/为了让鹈鹕有骑车颠簸的感觉我把整个鹈鹕组做一个上下小幅度位移animateTransform attributeNametransform typetranslate values0 0; 0 -8; 0 0 dur0.5s repeatCountindefinite/values可以是分号分隔的多个值SVG会自动插值。这个动画让鹈鹕每0.5秒完成原位-上移8像素-回原位的循环模拟被颠起来的节奏。翅膀扇动我选了在wing组上做旋转但旋转中心要放在翅根处我用CSS的transform-origin来控制g idwing styletransform-origin: 300px 260px; path dM 300 260 Q 350 240 380 275 Q 340 285 300 280 Z .../ animateTransform attributeNametransform typerotate values0 300 260; -25 300 260; 0 300 260 dur0.8s repeatCountindefinite/ /g翅膀扇动比旋转更复杂因为它在上下拍打——我用了values序列初始角度0度然后向上翻到-25度再回0度。这个往复运动就是拍翅膀的效果。云朵水平漂移用animateTransform的translate类型g idcloud-left ellipse cx150 cy100 rx45 ry22 fill#fff opacity0.8/ ellipse cx190 cy90 rx35 ry20 fill#fff opacity0.8/ animateTransform attributeNametransform typetranslate values0 0; 120 0; 0 0 dur12s repeatCountindefinite/ /g云向右漂120像素之后回到原位重复循环。如果想让云一直往一个方向走到头后重新出现可以把values改成-100 0; 900 0然后再配合第二朵云交替出现视觉上就会衔接自然。5. 常见问题与排查技巧实录5.1 动画完全不动八成是引号或单位写错了SMIL动画的dur值如果忘了写s比如写成dur2某些浏览器会直接忽略这个动画。repeatCountindefinite这个单词也容易拼错——我在本地调试时就拼成过indefinites结果动画只播一遍就停了。这种问题基本不在控制台报错只能靠肉眼排查。更隐秘的问题是from和to里的坐标没带空格例如from0 270 430写成了from0,270,430——大部分浏览器能容忍逗号分隔但有部分老版本只认空格。为了保险我统一用空格分隔。5.2 旋转动画把图形甩飞了问题永远在中心点所有诡异的旋转错位99%的原因都是旋转中心不对。检查顺序是先确认被旋转元素的实际坐标包围盒再确认from里的第三个参数是不是这个包围盒的几何中心。还有一种情况是元素位于某个外层g内部而外层g有一个translate位移。此时SMIL里的旋转中心应该使用该元素相对于整个SVG画布的坐标而不是它在内层g中的相对坐标。如果你不确定直接在浏览器开发者工具中查看该元素渲染后的getBoundingClientRect()值反推它在SVG坐标系中的位置再去写旋转中心。这个排查方法屡试不爽。5.3 路径形状扭曲多半是M命令的起点没选好画鹈鹕嘴的路径时我一开始把起点设在了上喙的中间结果整个形状像被人拧过一样。路径的起点最好选在形状最平实的低点比如嘴的根部然后沿一个方向画一圈。闭合路径Z会从终点自动连回起点如果起点和终点距离太远中间会拉出一条刺眼的直线。实操建议一定要给path加fill哪怕只是调试用的半透明颜色。路径默认填充是黑色如果你只画了描边没设置fill闭合后不明显的异常会被扩大化。先填充、后描边的顺序调试能快速看出路径围成的区域是否正确。5.4 不同浏览器效果不一致尽量避开SMIL盲区SMIL的兼容性在主流现代浏览器中已经非常稳定但如果你在旧版Edge非Chromium内核或某些移动端WebView上遇到动画不生效备选方案是用CSS动画替代。CSS动画的transform和transform-origin可以做到同样效果而且兼容性更好。反过来CSS动画无法直接驱动d属性路径的坐标数据这时SMIL就有独特优势——不过驱动d属性本身就是进阶玩法新手可以先避开。如果遇到动画在浏览器里正常但在某些SVG编辑器/导出工具里失效原因可能是工具把animateTransform当成不认识的标签给过滤了。解决办法是导出后再手工检查代码或者直接用文本编辑器保留SMIL标签。5.5 性能优化经验元素多了一定要做这三件事SVG动画卡顿最常见的原因不是元素太多而是触发了大量无谓的重绘。我的优化清单动画尽量加在g分组上而不是几十个独立子元素上。使用transform做位移和旋转不要直接修改cx、cy、x、y属性——transform的变化由合成器单独处理不强制整页重绘。设置will-change: transform或contain: layout paintCSS样式可以提前告诉浏览器这些区域会独立变化。但这个属性不要滥用给一两组关键动画加就行。实测下来同样的鹈鹕动画如果每个轮子的8根辐条都单独动画帧率会掉一半改成整组旋转后基本全程60帧。5.6 坐标画到画布外面去了善用开发者工具的可视化调试当你发现某个图形就是画不出来或者画出来半截但代码看起来没问题一定先看看它是否超出了viewBox的范围。SVG默认不会裁剪超出部分的可见性除非外层容器有overflow:hidden但隐藏的图形很容易被忽略。我调试时有一个习惯先给所有元素加一个高亮的stroke#f00或fillrgba(255,0,0,0.3)然后在浏览器里逐个检查坐标。确认无误后再改回正常配色。这个笨办法对新手特别友好可以避免在白白猜测中浪费时间。6. 让这个案例变得更丰富从静态动画到可交互的升级思路6.1 让SVG动的是动画让它活的是交互动画做出来只是第一步。你可以给轮子加一个点击事件点击后加速也可以监听页面滚动位置让鹈鹕的位移跟着滚动走。SVG是DOM的一部分所有DOM事件都能直接挂。一个简单而实用的交互点击鹈鹕触发叫声和跳跃。跳跃的本质是让整个鹈鹕组做一段更大幅度的位移动画同时嘴巴路径切换成一张一合的状态。我通常用JavaScript动态插入/移除animateTransform标签来实现一次性动画const pelican document.getElementById(pelican); pelican.addEventListener(click, () { const jump document.createElementNS(http://www.w3.org/2000/svg, animateTransform); jump.setAttribute(attributeName, transform); jump.setAttribute(type, translate); jump.setAttribute(values, 0 0; 0 -40; 0 0); jump.setAttribute(dur, 0.6s); jump.setAttribute(fill, freeze); pelican.appendChild(jump); });这里注意动态创建的SVG元素必须用document.createElementNS加上SVG命名空间创建否则浏览器会当成普通HTML标签处理完全不生效。这个坑我踩过不止一次。6.2 用JavaScript驱动更复杂的运动轨迹SMIL的动画适合简单的循环运动但如果鹈鹕要沿一条曲线从画面左边驶入、中间绕过一棵树、最后停在右边用SMIL写会比较笨拙。更通用的方案是用JavaScript直接修改transformlet t 0; function animatePath() { t 0.02; const x 100 t * 300; // 匀速前进 const y 300 Math.sin(t * 3) * 20; // 上下颠簸 bikeGroup.setAttribute(transform, translate(${x}, ${y})); requestAnimationFrame(animatePath); } animatePath();这是最原始但最灵活的做法。你需要自己控制帧循环、缓动、插值——相当于亲手实现一个微型动画引擎。好处是运动轨迹完全由代码掌控想怎么变态都行坏处是代码量明显增加而且不如SMIL那样声明式好维护。我的建议是简单循环用SMIL复杂轨迹用JavaScript两种方式互补不是谁替代谁的关系。6.3 顺便聊聊SVG的可访问性SVG毕竟是XML屏幕阅读器可以读里面的元素但默认可读性很差。给每个关键分组加一个title或roleimg是很好的习惯g idpelican roleimg aria-label一只鹈鹕正骑着自行车 title鹈鹕/title ... /g这样做不仅对屏幕阅读器友好对SEO也有帮助——搜索引擎能更好理解你页面上图形的内容。千万不要在一大段SVG里裸奔几十个形状却连个说明都没有。7. 一份可直接运行的完整SVG代码到这里一个可运行的完整示例就拼好了。把下面整个代码块保存成html文件用浏览器打开就能看到鹈鹕骑着自行车在页面里跑。代码我把动画和结构都保留方便你逐段对照、修改坐标练手。!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title鹈鹕骑自行车 - SVG动画示例/title style body { margin: 0; min-height: 100vh; display: flex; align-items: center; justify-content: center; background: #f7f3e9; } svg { max-width: 900px; width: 100%; height: auto; box-shadow: 0 15px 40px rgba(0,0,0,0.12); border-radius: 14px; } /style /head body svg width900 height600 viewBox0 0 900 600 xmlnshttp://www.w3.org/2000/svg !-- 背景 -- rect x0 y0 width900 height600 fill#dff0fa/ rect x0 y485 width900 height115 fill#cde6c3/ !-- 太阳 -- circle cx800 cy90 r45 fill#ffd166/ !-- 云朵 -- g idcloud-left ellipse cx120 cy100 rx45 ry22 fill#fff opacity0.85/ ellipse cx155 cy90 rx33 ry18 fill#fff opacity0.85/ ellipse cx190 cy100 rx38 ry20 fill#fff opacity0.85/ animateTransform attributeNametransform typetranslate values0 0; 110 0; 0 0 dur14s repeatCountindefinite/ /g !-- 地面点缀 -- line x180 y1520 x2130 y2520 stroke#91b882 stroke-width4 stroke-linecapround/ line x1600 y1510 x2660 y2510 stroke#91b882 stroke-width4 stroke-linecapround/ line x1750 y1540 x2820 y2540 stroke#91b882 stroke-width4 stroke-linecapround/ !-- 后轮 -- g idrear-wheel circle cx270 cy430 r55 fillnone stroke#333 stroke-width8/ circle cx270 cy430 r48 fillnone stroke#666 stroke-width3 stroke-dasharray8 8/ line x1270 y1430 x2270 y2382 stroke#888 stroke-width4/ line x1270 y1430 x2320 y2430 stroke#888 stroke-width4/ line x1270 y1430 x2270 y2478 stroke#888 stroke-width4/ line x1270 y1430 x2220 y2430 stroke#888 stroke-width4/ circle cx270 cy430 r5 fill#333/ animateTransform attributeNametransform typerotate from0 270 430 to360 270 430 dur2s repeatCountindefinite/ /g !-- 前轮 -- g idfront-wheel circle cx450 cy430 r55 fillnone stroke#333 stroke-width8/ circle cx450 cy430 r48 fillnone stroke#666 stroke-width3 stroke-dasharray8 8/ line x1450 y1430 x2450 y2382 stroke#888 stroke-width4/ line x1450 y1430 x2500 y2430 stroke#888 stroke-width4/ line x1450 y1430 x2450 y2478 stroke#888 stroke-width4/ circle cx450 cy430 r5 fill#333/ animateTransform attributeNametransform typerotate from0 450 430 to360 450 430 dur2s repeatCountindefinite/ /g !-- 车架 -- g stroke#e04a2f stroke-width5 stroke-linecapround fillnone line x1270 y1430 x2360 y2430/ line x1360 y1430 x2350 y2340/ line x1350 y1340 x2270 y2430/ line x1360 y1430 x2450 y2430/ line x1450 y1430 x2470 y2300/ line x1470 y1300 x2350 y2340/ /g !-- 车座 -- ellipse cx350 cy328 rx24 ry9 fill#333/ !-- 车把 -- line x1470 y1300 x2450 y2272 stroke#555 stroke-width5 stroke-linecapround/ !-- 脚蹬中轴 -- circle cx360 cy430 r20 fillnone stroke#555 stroke-width4/ line x1360 y1430 x2340 y2410 stroke#777 stroke-width5 stroke-linecapround/ line x1360 y1430 x2380 y2450 stroke#777 stroke-width5 stroke-linecapround/ animateTransform attributeNametransform typerotate from0 360 430 to360 360 430 dur2s repeatCountindefinite/ !-- 鹈鹕整体分组做上下颠簸 -- g idpelican animateTransform attributeNametransform typetranslate values0 0; 0 -8; 0 0 dur0.5s repeatCountindefinite/ !-- 尾巴 -- path dM 245 290 Q 210 265 230 310 Z fill#f5f0e0 stroke#d0c8b0 stroke-width2/ !-- 身体 -- ellipse cx315 cy285 rx75 ry45 fill#f5f0e0 stroke#d0c8b0 stroke-width3/ !-- 腿 -- line x1330 y1330 x2330 y2365 stroke#e8a040 stroke-width6 stroke-linecapround/ line x1372 y1330 x2370 y2360 stroke#e8a040 stroke-width6 stroke-linecapround/ !-- 脖子 -- path dM 360 270 Q 390 200 400 180 fillnone stroke#f5f0e0 stroke-width28 stroke-linecapround/ !-- 头 -- circle cx400 cy175 r28 fill#f5f0e0 stroke#d0c8b0 stroke-width3/ !-- 嘴 -- path dM 400 180 Q 520 170 560 220 Q 530 220 500 200 Q 450 205 400 205 Z fill#f0a030 stroke#c47720 stroke-width3/ !-- 眼睛 -- circle cx410 cy162 r4 fill#222/ circle cx413 cy161 r2 fill#fff/ !-- 翅膀 -- g idwing path dM 300 260 Q 350 235 385 270 Q 340 285 300 280 Z fill#e8dcc8 stroke#d0c0a0 stroke-width2/ animateTransform attributeNametransform typerotate values0 300 260; -25 300 260; 0 300 260 dur0.8s repeatCountindefinite/ /g /g /svg /body /html这段代码可以直接运行。如果复制到本地后看不到动画先检查浏览器版本或换一个现代浏览器试一下。我实际运行这个文件的感觉是轮子转动和脚蹬旋转同步进行鹈鹕身体轻微颠簸翅膀持续扇动云慢慢飘过——整个画面就活起来了。虽然算不上精致但作为学习案例它把SVG从绘图到动画的关键点都串起来了。8. 一些发自肺腑的实操总结把这一整套SVG案例做下来我最想分享的经验其实就三条第一先画静态图再做动画。这句话听起来像废话但我见过太多新手一上来就打算让轮子转起来结果连车架都还没画就卡住了。静态图和动画是两个独立的问题分开解决每个都容易混在一起会互相干扰。我写这个案例时先把整个画面完整画出来确认每个坐标、每个颜色都满意了才开始加动画标签。第二坐标系的数学化思维很重要。SVG初看只要填坐标就行但真正写起来你会发现如果随机给坐标画面永远乱糟糟。先确定关键基准点比如轮心、地面线再从基准点推演其他坐标是让画面整齐的最有效方法。它不考验美术功底只考验你有没有耐心去算。第三动画的关键不是动而是节奏。轮子转得太快像电风扇鹈鹕颠簸幅度太大像在坐过山车翅膀扇得太慢像濒死挣扎。这些都需要根据画面内容反复调参数没有一步到位的方案。我建议把动画参数集中放在文件头部或末尾注释里方便统一调整——比如dur2s、values中的偏移量都记在一个地方后面调参效率会高很多。如果你对着这个案例刷完了下一步可以试着改变鹈鹕的颜色、换成不同地形背景、给自行车加个车筐或者干脆换一只动物——画一只鸭子骑滑板车也行。SVG就是这样学会了基础图形和坐标变换这两个核心剩下的就是组合和创意了。
企业数字化 ERP 产品动态
相关推荐
大学生网页作业防复制破解指南:用F12提取题目文本 1. 这不是黑客教程,而是一份写给大学生的“网页作业解围指南”你有没有遇到过这样的场景:老师发来一份在线作业系统链接,点开后发现——复制不了题目、右键被禁用、CtrlC毫无反应,连截图都提示“禁止截屏”?页面底部还… · 2026/9/25 9:03:28
西瓜视频超清地址获取:基于DASH协议与Base64解码的前端定位方法 1. 项目本质与实操边界:这不是“破解”,而是前端资源定位的常规技术路径“超清西瓜视频真实视频地址获取方法”这个标题,表面看像某种灰色技巧,但实际它指向的是一个在前端开发、内容审核、数字存档、教育素材采集等场景中真实存在… · 2026/9/25 9:03:22
PHP实战EOS区块链开发:节点通信、交易签名与合约交互 先交代一个背景。熟悉PHP的人都知道heredoc语法,赋值字符串的时候以<<<EOS开头,以EOS;结束,这里的EOS只是随手起的结束标识符,本身是个占位符。但到了区块链开发圈子里,EOS是另外一个完全不同的东西——一个采… · 2026/9/25 9:03:22
xberg C FFI 插件管理实践:xberg_list_validators 列出已注册验证器的完整实现解析 后端AI 应用NLP 【免费下载链接】xberg Polyglot document intelligence with a Rust core: extract text, metadata, images, tables, and structured data from 106 formats across 140 file extensions, plus code intelligence for 371 languages. Fifteen bindings, with … · 2026/9/25 10:18:46
开源代码审查新范式:CLI+Git Diff+LLM Agent协同实践 1. 这不是另一个“代码审查工具”,而是一套可落地的开源协作新范式“open-code-review”这个词,最近在开发者 Slack 群、GitHub Trending 和内部技术分享会上出现频率陡增——但它绝不是又一个带 UI 的 PR 检查插件,也不是把 ChatGPT 套个壳扔… · 2026/9/25 10:18:40
Copilot时代的程序员:用TaoToken统一Key管理这5个能力比写代码更重要 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 10:18:16
学生党实测:用 TaoToken 统一 Key 接入 DeepSeek 与豆包做论文写作辅助的配置清单 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 10:18:16
创维E900V22D刷机全攻略:S905L3SB芯片兼容性解析与救砖实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:31
MQTT协议原理与Broker服务器搭建实战:从Mosquitto到EMQX /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:37