
说实话第一次看到有人用纯CSS画出动物的时候我就觉得这事特别有意思。当时正好赶上马年朋友圈里全是换装照、祝福图但真正用代码“整活”的还真不多。于是我也手痒了花了几个午休时间用最基本的div加上几段keyframes搞出了一匹能撒开蹄子跑的骏马。今天就把这套思路完整拆给你看代码不复杂但里面有几个关键点理解了之后你甚至可以拿这套方法去画别的四足动物。这篇文章适合谁只要你写过一点CSS知道div、border-radius和animation大概是什么就能跟着做出来。我会从结构设计讲到动画落地最后再把调试过程中遇到的坑和排查思路一并交代清楚。1. 先把一匹马拆成一堆div设计思路与结构规划用CSS画东西第一步永远不是写样式而是“拆解”。马在视觉上可以拆成哪些部分头、耳朵、脖子、身体、四条腿、尾巴、鬃毛。拆得越细后面调整越容易。但也不能拆得太碎否则HTML结构会变得臃肿动画反而不好组织。我的做法是分成三层来规划最外层是一个场景容器负责整体位置、大小以及后续的背景移动。第二层是马的“身体组”把身体、脖子、头、尾巴这些都放进去用于做整体的上下颠簸和轻微前倾。第三层才是最细的部件比如四条腿要单独拆开因为奔跑动画的核心就是腿部的交替摆动。这里有一个很重要的设计原则结构层级和动画层级要一一对应。如果马的身体要整体上下跳那这个动画得挂在“身体组”上如果只是某条腿在甩那动画就只作用在那条腿的div上。一开始我把所有部件都平铺在场景底下结果想加个整体颠簸效果的时候就发现需要写一堆重复代码。后来改成三层结构清爽多了。另外一个值得提前想清楚的点是马要朝哪个方向跑。这会影响你画的时候怎么安排“前后腿”。我选择让马朝画面左侧跑因为这样视觉上比较符合阅读习惯而且背景向右移动也能制造出前进的错觉。如果你想让马朝右只需要把transform: scaleX(-1)加在最外层容器上镜像翻转一下就行所有动画不用动。2. 静态建模用最朴素的CSS画出马的轮廓先别急着做动画静态形状搞不定动起来也是四不像。我用的绘制手段非常基础divborder-radiustransform没有任何SVG也没有clip-path。原因有两个一是border-radius的兼容性最好二是它配合圆角矩形能模拟出大部分动物身体的弧线。2.1 身体的画法圆角矩形的组合艺术马的身体我用了两个叠加的圆角矩形来完成直接上代码div classhorse div classhorse-body/div div classhorse-neck div classhorse-head div classear left-ear/div div classear right-ear/div /div /div div classhorse-tail/div div classhorse-mane/div div classleg leg-front-left/div div classleg leg-front-right/div div classleg leg-back-left/div div classleg leg-back-right/div /div.horse { position: relative; width: 220px; height: 160px; } .horse-body { position: absolute; left: 20px; top: 40px; width: 160px; height: 75px; background: #8b5a2b; border-radius: 45% 55% 40% 40%; }border-radius这里我用的是两个值分别控制水平和竖直方向的圆角比例这样可以让身体前段稍微隆起后段收窄更接近真实马匹的胸腔轮廓。你如果觉得圆润度不够可以微调这组数值比如50% 50% 30% 30%会显得更胖一点40% 60% 35% 35%会更修长。这就是纯CSS绘图的乐趣——改一个数字气质就变了。2.2 脖子和头的处理用旋转让姿态自然脖子这块是一个容易翻车的细节。如果用一个直直的长方形立在身体上那看起来不像马像长颈鹿。我的做法是画一个略微倾斜的矩形用transform-origin控制旋转轴心.horse-neck { position: absolute; left: 130px; top: -15px; width: 36px; height: 65px; background: #7a4a1e; border-radius: 30% 20% 10% 10%; transform: rotate(18deg); transform-origin: bottom left; z-index: 2; }旋转角度18deg并不是随手写的。我先把脖子放在身体靠前的位置然后反复调整角度观察它和身体的衔接是否自然。如果角度太大头会仰到天上去太小又显得低头耷脑。最后锁定了18度配合z-index: 2盖在身体上面衔接缝用和身体同色系的深一档颜色来弱化视觉上顺眼很多。头部的画法同理我只是在一个圆角矩形上面加了两个小三角形耳朵.horse-head { position: absolute; left: 14px; top: -42px; width: 58px; height: 38px; background: #8b5a2b; border-radius: 40% 60% 20% 30%; } .ear { position: absolute; top: -16px; width: 12px; height: 20px; background: #6b3f14; clip-path: polygon(0 100%, 50% 0, 100% 100%); } .left-ear { left: 6px; transform: rotate(-15deg); } .right-ear { right: 4px; transform: rotate(15deg); }注意耳朵我用了clip-path来切三角形。这是整篇代码里唯一用到clip-path的地方。为什么不直接用border三角形因为border画三角形在动画中如果涉及缩放或旋转边缘会出现锯齿和变形而clip-path是直接裁剪元素本身运动时边缘更平滑。这是我在做了几次对比之后的一个小心得。2.3 尾巴和鬃毛动态感从静态就开始埋伏笔尾巴和鬃毛是马的“表情”它们看起来不起眼但在动画中能极大增强动感。静态阶段我只需要把尾巴画成一个带弧度的长条形鬃毛画出几撮毛的错落感.horse-tail { position: absolute; right: 6px; top: 38px; width: 10px; height: 48px; background: #3a2210; border-radius: 60% 40% 50% 50%; transform-origin: top center; } .horse-mane { position: absolute; left: 118px; top: -8px; width: 32px; height: 50px; background: #3a2210; border-radius: 40%; transform: rotate(6deg); }尾巴的transform-origin设置在顶部中心这是为后面的摆动动画做的准备。鬃毛则直接放在脖子和头部的交界处用深色突出轮廓。到这里静态的马已经能看了。先停在一步打开浏览器确认形状没问题再进入动画环节。不要急着往下写基础形状没调好后面返工的代价更大。3. 让马跑起来腿部、身体、背景三层动画协同奔跑的视觉效果本质上是三层动画的叠加第一层是腿部的交替摆动第二层是身体的上下颠簸第三层是背景的横向移动。三层各司其职缺一不可。如果只有腿在动马就像踩了滑板如果只有身体在颠那叫抽搐如果只有背景在滚那不是奔跑是拉洋片。3.1 腿部动画相位差是关键四条腿的奔跑节奏遵循一个基本规律对角腿同步。也就是说左前腿和右后腿同时往前迈右前腿和左后腿同时往后蹬。理解了这一点动画就好写了。我给每条腿写完全相同的动画只是用animation-delay错开相位.leg { position: absolute; bottom: 0; width: 18px; height: 52px; background: #6b3f14; border-radius: 30% 30% 0 0; transform-origin: top center; animation: leg-swing 1s ease-in-out infinite; } .leg-front-left { left: 48px; animation-delay: 0s; } .leg-front-right { left: 68px; animation-delay: -0.5s; } .leg-back-left { right: 48px; animation-delay: -0.5s; } .leg-back-right { right: 28px; animation-delay: 0s; } keyframes leg-swing { 0%, 100% { transform: rotate(-25deg); } 50% { transform: rotate(25deg); } }这里有几个细节值得展开说。第一transform-origin设为top center意思是腿以顶部中心为轴心摆动。这模拟了腿和身体连接的关节位置。如果忘了设置这一行默认的旋转中心是元素的几何中心腿就会在中间打转效果特别诡异。第二animation-delay的负值运用。你可以用0.5s但负值-0.5s意味着动画已经执行到一半了省去了等待第一个周期的时间。页面一加载四条腿就处在一个“当前状态”里而不是先整齐划一地同步摆动半秒再突然错开观感会自然很多。第三为了更接近马奔跑时“四蹄腾空”的感觉你其实可以让每条腿的动画做成非对称的。我用的是对称摆动这在慢跑时没问题但如果想要更剧烈的狂奔效果可以把关键帧改成类似0%是前摆极限30%回到中间60%是后蹬极限100%回到前摆极限。这样每条腿在腾空阶段停留时间更短更有爆发力。我建议你先做对称版跑通了再优化成非对称版。3.2 身体上下颠簸给马一点“浮空感”没有身体起伏的奔跑动画看起来绝对是不对劲的。真实马匹在奔跑时身体会随着步伐节奏上下弹动这个频率和腿部摆动是对应的。我给“身体组”整体加了一个轻盈的上下位移动画.horse { animation: gallop-bounce 1s ease-in-out infinite; } keyframes gallop-bounce { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-12px); } }动画时长和腿部保持一致都是1s这样身体每颠一次恰好对应腿部完成一个完整的前后摆动周期。这里我踩过一个坑第一次做的时候身体位移和腿部摆动没有对齐时间轴结果马的身体颠簸频率跟腿部动作完全是两回事看起来像在踩蹦床。解决办法很简单——所有相关动画统一使用同一个animation-duration并且确保每个关键帧阶段在时间轴上对应。你可以把gallop-bounce的50%那一帧看作“身体最高点”此时四条腿应该处于前后张开的极限状态。你可以调整腿部的animation-delay来让这个对应关系成立比反过来调容易得多。3.3 要不要加一点前倾和旋转跑起来的马身体应该是微微前倾的有一种往前冲的势头。这个效果不用单独做动画我直接在.horse上加一个静态的transform: rotate(-2deg)。别小看这2度它让整个姿态从“站立”变成了“冲刺”。另外如果想在颠簸的同时增加一点动态旋转可以把gallop-bounce升级一下keyframes gallop-bounce { 0%, 100% { transform: translateY(0) rotate(0deg); } 50% { transform: translateY(-12px) rotate(-1deg); } }颠到最高点的时候身体轻微向前倾斜落地时恢复水平。这会让奔跑的节奏感更强。但注意旋转角度不宜过大一旦超过3度马看起来就像在跳街舞了。3.4 背景移动制造“空间在后退”的错觉马本身在原地奔跑想要制造出“前进”的效果需要让背景动起来。这个原理和你坐在火车里看窗外是一样的——参照物在移动大脑就会认为自己在前进。我没有用复杂的背景素材只是简单地画了一条地平线和几个圆点当作远处的小树.scene { position: relative; width: 100%; height: 260px; overflow: hidden; background: linear-gradient(to bottom, #e0f0ff 0%, #e0f0ff 65%, #7cb342 65%, #7cb342 100%); } .scene::before { content: ; position: absolute; top: 70px; left: 0; width: 200%; height: 10px; background: repeating-linear-gradient(90deg, #5d4037 0 20px, transparent 20px 60px); animation: bg-scroll 2s linear infinite; } keyframes bg-scroll { from { transform: translateX(0); } to { transform: translateX(-50%); } }背景滚动的关键点是元素的宽度是容器的两倍200%动画从translateX(0)移动到translateX(-50%)。这样移动的距离恰好是一半周期结束后无缝衔接不会出现跳变。背景滚动速度我设置在2s一个循环和马的1s奔跑周期形成2:1的比例也就是说每跑两步背景才滚动一个完整周期。这个比例视觉上看起来比较自然不会觉得地面滑得太快。如果你想让背景跟着马一起颠簸一点可以在背景容器上也挂一个微小的上下位移但幅度要控制在2-3像素以内否则会让人觉得摄像机在抖动容易头晕。4. 细节打磨用渐变、阴影和动画曲线提升质感纯色块堆出来的马是能动的但谈不上好看。想要质感更好可以在不增加任何额外DOM节点的情况下用CSS渐变和阴影做修饰。这个阶段是“画龙点睛”同时也是最容易看出一个前端基本功扎不扎实的地方。4.1 用渐变给马身上点“光影”最简单的办法就是用background: linear-gradient(...)替代纯色背景。我给马身体设置了一个轻微的角度渐变.horse-body { background: linear-gradient(135deg, #a06a30 0%, #7a4a1e 70%, #5a3413 100%); }这样做的好处是身体前半段显得亮一点后半段暗一点制造出一个虚拟的光源方向。配合头部的同色系渐变马的整体立体感会强很多不会像个剪影贴在页面上。这个方法同样可以应用到腿和脖子上只要保证几个部件的明暗方向一致就行。4.2 阴影是免费的“体积感”另一个性价比极高的细节是盒子阴影。我给身体加了一个内阴影模拟肚皮下方的暗部.horse-body { box-shadow: inset 0 -8px 12px rgba(0, 0, 0, 0.25), 0 4px 8px rgba(0, 0, 0, 0.2); }前一组inset阴影让腹部有下沉的暗区后一组普通阴影则让马整体的轮廓从背景中脱离出来。你可能不需要阴影有多精确但只要加了视觉上马上就不一样。4.3 动画曲线ease-in-out是默认选项但未必是最优解在我给出的腿部动画里用了ease-in-out作为animation-timing-function。这会让腿在摆动到两端时减速、中间位置时最快符合钟摆的运动规律。但如果你想模拟“马腿快速蹬地然后收腿”那种瞬间爆发力可以考虑改用cubic-bezieranimation: leg-swing 0.8s cubic-bezier(0.45, 0.05, 0.55, 0.95) infinite;这个贝塞尔曲线比较接近自然肌肉的发力状态腿部出腿快回收稍慢。跟ease-in-out比它多了一点点“利落感”。你完全可以两个都试试然后决定自己更喜欢哪一种风格。反正代码就改一行多试几次不亏。4.4 加入CSS变量让参数调节变成“填空题”当你的动画涉及多个部件的时间同步时每个地方都写死数值会非常痛苦。比如你想把奔跑速度从1s改成0.8s如果每个动画都写的是1s你得改五六处还容易漏。这时候用CSS变量可以大幅提升调试效率.horse { --gallop-speed: 1s; --bounce-height: 12px; animation: gallop-bounce var(--gallop-speed) ease-in-out infinite; } .leg { animation-duration: var(--gallop-speed); }之后你想调速度只需要改:root或.horse上面的变量值就行。这个技巧在后期打磨动画节奏的时候特别实用强烈建议从一开始就用上。5. 常见问题与排查技巧实录代码写完了动画也能跑了但真正调到“看起来舒服”是一个不断试错的过程。我把实操中撞见的几个典型问题和对应的排查思路整理一下这些都是文档里一般不写的经验。5.1 动起来之后马的身体部件“散架”了现象静态预览没问题但动画一跑马身和脖子之间出现缝隙或者头歪到一边去了。原因这是旋转导致的经典问题。一个div旋转后它的视觉包围盒会变大溢出了父容器或者和相邻部件的位置关系发生了变化。排查思路先给每个部件加上outline: 1px solid red在浏览器里逐帧动画暂停看哪个部件在什么关键帧处越界了。然后调整它的初始位置或者旋转轴心。大部分情况下transform-origin的设置能解决80%的问题。另外给父容器设置一个overflow: visible排除是容器裁剪的锅。5.2 腿的摆动看起来“假”像在踩水车现象腿是前后摆了但整体观感就是不对劲没有一点奔跑的味儿。原因大概率是腿的摆动角度太小或者太大也可能是摆动节奏没有和身体的上下起伏对齐。排查思路把leg-swing的旋转角度从25度往下调或者往上调找一个肉眼觉得自然的范围。如果角度太小小于15度腿看起来就是在颤抖如果太大大于40度就像在踢正步。另外检查一下gallop-bounce的50%关键帧是否和腿部动画的50%重合如果不重合调整腿部动画的animation-delay让“身体最高点”恰好对应“前后腿张到最大”的时刻。5.3 动画跑一段时间之后部件开始抖动现象页面刚加载时正常循环几秒后马的某个部位开始轻微抖动或者出现残影。原因这是动画中出现小数像素位移的典型表现尤其在translateY(-12px)这类位移过程中浏览器会在非整数像素之间插值导致渲染上出现微小的抖动。另一个可能是背景循环动画的边缘接缝没有对齐在-50%位置处出现了视觉跳变。排查思路把位移量改成整数像素比如translateY(-12px)本身就满足但如果用了-12.5px这类值建议改为整数。背景的repeating-linear-gradient要保证图案周期能整除容器宽度否则循环肯定有跳变。最后可以给动画部件加上will-change: transform来提示浏览器提前优化但这招不要滥用加太多反而会占用内存。5.4 动画在低配手机上卡顿现象电脑上很流畅手机上一打开就掉帧。原因动画涉及了太多的重绘区域或者阴影和渐变的开销在移动端GPU上被放大了。排查思路优先保证动画只使用transform和opacity属性不要让left、top、margin这类属性参与动画因为它们会触发布局重排。阴影如果用得比较多可以考虑在动画运行时去掉外侧阴影或者缩小阴影模糊半径。最后动画元素少用position: absolute的浮动子层级尽量让它们在同一层合成。6. 还能怎么玩把这匹马扩展到任意四足动物这套“div拆件 相位错开 整体颠簸 背景滚动”的方法论具备很强的复用性。马年做完马明年如果做只羊、做头牛核心思路完全一样只需要调整身体比例、颜色和腿部数量。比如要改成一匹马到一只鹿身体画长一点脖子抬高一点头画小一点四条腿换成更细的颜色更浅的矩形再把鹿角加上就行。动画关键帧几乎不用动最多把摆动幅度调小一点。要改成一只猫身体缩短腿变短把尾巴的摆动动画做得更灵活一点奔跑动作就变成小碎步了。这就是我把每个部件都拆成独立div的最终红利——改一行结构动一个数字就是新生物种。另外还有一个我后来尝试过的有趣扩展把耳朵、尾巴这些“可动小部件”一并做成动画。比如尾巴在奔跑时除了横向甩动还可以加一个轻微的旋转鬃毛在身体颠簸时稍微滞后一点点要做出“被风吹起”的飘逸感可以在原来的位置上加一个短暂延迟的translateX偏移。这些细节加多了以后整匹马从“会动”进化成“有生命感”观感提升特别明显。我强烈建议你在主体动画跑通之后花十几分钟试一下这个扩展。如果你愿意还可以把整个场景抽象成组件用CSS变量暴露速度、颠簸幅度、奔跑方向等参数封装成一小段通用的“四足跑步机”代码。以后不管什么生肖年改几个变量就能交作业这一手在朋友圈绝对能收割一波效果。