ARTICLE DETAIL

建站实战干货

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

纯CSS打造奔跑骏马:从雪碧图到关键帧动画的完整实战

2026/9/15 3:33:35 拓冰建站 浏览量
纯CSS打造奔跑骏马:从雪碧图到关键帧动画的完整实战 有时候项目做久了总会想搞点“花活”调剂一下。午休时间刷到“马年了还不会整活”这种标题我一琢磨——确实CSS折腾点小动画是最好的放松方式既能练手又出效果。今天就手把手带大家做一匹纯CSS驱动的奔跑骏马不依赖任何JS库也没有一张GIF动图全靠CSS动画硬撑。这篇短文适合刚学完CSS基础选择器、想试试动画效果的入门选手也适合写腻了业务样式、想找回一点“玩CSS初心”的老人。读完你不仅能复刻这匹马还能顺手整出一套自己的“CSS整活方法论”。1. 内容整体设计与思路拆解1.1 为什么选“纯CSS跑马”这个项目练手先说说选题逻辑。CSS动画很多人在入门的时候都接触过transition和animation但真正能拿出来“整活”的作品并不多。为什么?因为日常业务里,动画大多是按钮悬停、弹窗过渡、滚动渐显这些“小动作”并不需要把帧动画、分层运动、背景视差这些概念揉在一起。而制作一匹会奔跑的马恰恰能把以下核心知识点全部串起来CSS关键帧动画keyframes的写法与参数含义雪碧图CSS Sprites在动画中的正确打开方式steps()阶梯函数与linear线性函数的本质区别定位与布局绝对定位、transform配合位移的实战用法伪元素::before/::after减少冗余DOM节点的技巧无限循环动画的参数控制infinitealternate的组合效果换句话说这个项目是“把CSS从入门到精通的零散知识点揉成一个能跑的真东西”。做完你会突然发现之前看教程时那些“懂了但不会用”的概念全都打通了。1.2 先定方案雪碧图动画还是逐帧DOM动画做帧动画通常有两条路一条是用一张包含所有动作帧的雪碧图配合background-position的位移来播放另一条是把每一帧做成一整个DOM元素通过切换显隐或位移来达到动画效果。我的建议很直接用雪碧图方案。理由有三第一雪碧图只需一个元素加载一张图片即可性能开销远低于多个DOM节点轮番切换。第二背景定位的位移计算是确定性的只要帧数算对、步数设对动画循环一定精准。第三雪碧图是前端基本功掌握后不光能跑马做任何逐帧动画人物行走、表情切换、UI反馈动效都是一套通用逻辑。可能会有朋友说“那我直接用GIF不就行了。”确实GIF很省事但GIF在颜色数量、透明通道、尺寸控制上都有先天短板而且无法在运行时通过CSS控制播放速度、暂停状态、方向反转。相比之下CSS雪碧图动画才是更“可控”的整活方案。1.3 动画分层的总体架构整个页面我打算拆成三层来写各司其职第一层是“背景环境”包括地面、太阳、远山营造出一个开阔的奔跑场景第二层是“骏马本体”采用雪碧图播放奔跑动作第三层是“动态点缀”比如奔跑时扬起的尘土、身后的云朵移动进一步强化速度感这种“分层”思路本质上和游戏开发里的前景、中景、背景视差是同一个逻辑。每一层独立运动叠加起来就有“马在往前跑”的错觉。这个设计思路也方便大家后面自己扩展比如想加个骑手、换背景风格只需改动对应层级的HTML和CSS互不干扰。2. 核心细节解析与实操要点2.1 雪碧图制作与帧动画原理先来聊聊雪碧图本身。我们需要的不是一张随便截的骏马图而是“动作分解图”。所谓动作分解就是把一匹马奔跑的一个完整循环比如四蹄腾空、前蹄落地、后蹄蹬地等几个关键姿态按顺序横向排列在一张图上。每一帧就是马的某一个姿态连续播放这些姿态人眼就会产生奔跑的视觉残留。常见做法是用Photoshop或在线工具把8帧、12帧或16帧的动作序列拼成一张长条图。帧数越多动作越细腻但图片体积也越大。做Demo的话我建议8帧起步既能看清动作变化又不需要拼图拼到崩溃。在CSS中播放雪碧图动画核心是这样一个规则.m-horse { width: 200px; /* 单帧宽度 */ height: 200px; /* 单帧高度 */ background-image: url(horse-sprite.png); background-repeat: no-repeat; animation: horse-run 0.8s steps(8) infinite; } keyframes horse-run { from { background-position: 0 0; } to { background-position: -1600px 0; } /* 8帧总宽度取负值 */ }这里的关键也是很多人第一次接触时最容易懵的点steps(8)到底是干什么的。steps(8)是animation-timing-function的一个特殊取值。普通的linear或ease会让背景位置在两个关键帧之间连续过渡呈现出来的效果是“这张图被平滑地拖动”——你看到的是图片在快速滑动而不是马的奔跑动作。而steps(8)会把整个变化过程切成8段每一段内不做插值直接跳跃到下一帧。这样一来背景位置就会一格一格地跳刚好对应雪碧图里的8个动作帧。一句话总结steps()就是“动画的顿挫感开关”逐帧动画必须用它不然全露馅。2.2 定位方式选型为什么推荐transform而非top/left马的本体确定好后要让马产生“向前奔跑”的位移感通常有两种实现一种是修改left或top属性一种是用transform: translateX()。很多新手写动画会直接改left因为直观。但这里我建议使用transform。原因在于性能left的改动会触发浏览器的重排Reflow也就是浏览器需要重新计算元素的位置和布局再重绘页面而transform只触发合成Composite不会影响其他元素的布局性能开销小得多。实测下来在移动端低端机或者页面有大量背景动画的情况下用left跑动画会出现明显卡顿而transform基本能维持60帧满帧。这个差异在小项目里感受不明显但一开始养成好习惯后面做复杂动效会省很多心。2.3 背景环境的营造纯CSS能做的远比想象多背景层我打算全部用CSS画不引入额外图片。核心技能就是渐变、盒阴影和伪元素。先看天空。一个简单的线性渐变就能做出从湛蓝到浅蓝的自然过渡。太阳可以用一个圆角元素加box-shadow的光晕效果来模拟。地面更简单一个height有限的容器背景用重复线性渐变拉出不断后退的斑马线。这些装饰不能占据太多代码量但能瞬间把“光秃秃的马”变成“场景中的马”。移动的云朵、被马蹄扬起的尘土颗粒都是keyframes加上透明度、位移、缩放这几个属性的组合拳。你会发现纯CSS能实现的视觉丰富度远超过大多数人的预期关键是要敢于组合。2.4 鼠标交互让整活更进一步有人问了“用CSS实现动画还能处理鼠标移入事件吗?”答案是能。CSS伪类:hover就是干这个的。我打算让马在默认状态下自由奔跑鼠标移上去时切换成“疾驰模式”——加快动画速度同时让背景滚动更快形成一种“马被追上就加速”的趣味交互。这个需求不需要JS只需要在选择器上加一个:hover再改变动画时长即可。这里体现了CSS选择器体系里范围很大的一个知识点:hover不只作用于被悬停的元素本身还可以通过父元素控制子元素。经常和“后代选择器”“子选择器”配合使用。所以别小看这个交互它的本质是“父容器状态变化驱动多个子元素联动”这个思路以后做导航悬浮、卡片特效都通用。3. 实操过程与核心环节实现3.1 第一步准备HTML骨架我们先搭一个极简的HTML结构语义清晰、层级分明方便后面灵活控制各层。div classscene div classsky/div div classsun/div div classcloud cloud-1/div div classcloud cloud-2/div div classground/div figure classhorse div classhorse-runner/div /figure div classdust/div /div里面几乎所有元素都是空的纯通过CSS来赋予它们视觉。figure作为马的语义化容器内部有一个.horse-runner作为雪碧图的承载元素。为什么多套一层?因为有的时候我们需要把“动作动画”和“位移动画”分开处理动作由内部元素播放雪碧图位移由外部容器做平移两者互不干扰。3.2 第二步全局基础样式先写全局样式把盒子模型统一避免默认边距干扰。* { margin: 0; padding: 0; box-sizing: border-box; } body { min-height: 100vh; display: flex; justify-content: center; align-items: center; background: #1a1a2e; font-family: system-ui, -apple-system, sans-serif; overflow: hidden; }这里顺手用flex完成了页面居中相当于顺带复习了“文本垂直水平居中”之外的场景——容器居中不只是文本能用到整个舞台都能用。:root里我打算定义一些CSS变量方便改主题色和统一动画时长:root { --horse-w: 200px; --horse-h: 200px; --frames: 8; --animate-duration: 0.8s; }用变量管理尺寸和时长最大的好处是后文可以一次性调整动画调优时效率极高。3.3 第三步场景层——天空、太阳、地面与云朵先铺背景建立空间感。代码如下.scene { position: relative; width: 100vw; height: 100vh; overflow: hidden; background: linear-gradient(180deg, #87CEEB 0%, #E0F6FF 80%, #F5F5DC 100%); } .sky { position: absolute; inset: 0; } .sun { position: absolute; top: 60px; right: 80px; width: 80px; height: 80px; border-radius: 50%; background: radial-gradient(circle, #FFD700, #FFA500); box-shadow: 0 0 40px 10px rgba(255, 215, 0, 0.6); }box-shadow的用法在这出现了两次一次是做太阳的光晕一次是后面给某些元素做“伪实体”效果。这是纯CSS视觉里最常用的招式之一值得记下来。地面是这类动画中最出效果的装饰之一。我采用重复线性渐变制造出“地面向后退”的视觉线.ground { position: absolute; bottom: 0; width: 200%; height: 160px; background: repeating-linear-gradient( 90deg, #8B7355 0px, #8B7355 60px, #A0522D 60px, #A0522D 120px ); animation: groundMove 1.2s linear infinite; } keyframes groundMove { from { transform: translateX(0); } to { transform: translateX(-120px); } }这里让地面本身以linear方式平移平淡无奇但配合马的动作就能形成“地面在向后滑动”的感觉。之所以宽度设成200%是为了给translateX留出足够的位移空间避免露出背景可以理解为“跑道比马跑得远”。云朵呢我用伪元素来做减少无意义的DOM节点.cloud { position: absolute; width: 180px; height: 60px; background: rgba(255, 255, 255, 0.8); border-radius: 50px; animation: cloudMove 30s linear infinite; } .cloud::before, .cloud::after { content: ; position: absolute; background: rgba(255, 255, 255, 0.8); border-radius: 50%; } .cloud::before { width: 80px; height: 80px; top: -30px; left: 30px; } .cloud::after { width: 100px; height: 100px; top: -40px; left: 80px; }当然这只是最基本的云朵造型。有兴趣的可以多加几层::before或者干脆套多个子节点模拟更立体的积云效果。原则不变能用伪元素就绝不新增无意义的标签。3.4 第四步骏马雪碧图动画接下来的重头戏。先看马的静态样式.horse { position: absolute; bottom: 100px; left: 50%; width: 200px; height: 200px; margin-left: -100px; z-index: 2; animation: horseMove 3s ease-in-out infinite alternate; } .horse-runner { width: 100%; height: 100%; background-image: url(https://example.com/horse-sprite.png); background-repeat: no-repeat; background-size: 800% 100%; animation: horseRun 0.8s steps(8) infinite; }这里background-size: 800% 100%比较特别。正常思维是“让雪碧图的每一帧显示为容器大小”写background-size: cover或直接写200px 200px即可。但如果你想让容器尺寸随意调整用百分比最省心。800%表示背景图宽度是容器的8倍正好容纳8帧横向排列的图案每一帧就能对齐到容器宽度。这个技巧在做响应式雪碧图动画时特别管用。马的“原地奔跑”动画刚才已经写过这里再补充位移的动画keyframes horseMove { from { transform: translateX(-50px); } to { transform: translateX(50px); } }这个位移是马整体的前后晃动幅度不需要大给画面增加一点呼吸感。而真正的“前进感”是靠地面、云朵和尘土共同完成的——其实马始终在屏幕中间附近跑但视觉欺骗下观众会觉得它一直在前进。这种“相对运动”的用法在动画和游戏领域极其常见值得多想一层。3.5 第五步尘土飞扬与交互反馈为了让奔跑更真实我在马蹄附近加了一些尘土粒子。用伪元素和nth-child选择器分别控制几个小圆点的动画延迟模拟尘土被先后扬起.dust::before, .dust::after { content: ; position: absolute; bottom: 0; width: 20px; height: 20px; background: rgba(139, 115, 85, 0.5); border-radius: 50%; animation: dustPuff 0.6s ease-out infinite; } .dust::after { width: 14px; height: 14px; animation-delay: 0.3s; } keyframes dustPuff { 0% { transform: translate(0, 0) scale(1); opacity: 0.8; } 100% { transform: translate(-60px, -30px) scale(2); opacity: 0; } }鼠标移入的交互利用.scene:hover来控制全局节奏.scene:hover .horse-runner { animation-duration: 0.4s; /* 马跑得更快 */ } .scene:hover .ground { animation-duration: 0.6s; /* 地面退得更快 */ } .scene:hover .cloud { animation-duration: 15s; /* 云也飘得更快 */ }这里就是利用“父容器状态给子孙后代施加影响”也正好呼应了热词里的“css 鼠标移入事件”。这种关系型选择器思路在写组件库的时候尤其好用一个开关状态驱动多个子部件一起响应。3.6 第六步细节打磨与性能优化动画完成后个人建议做三件事第一检查是否存在不必要的will-change。will-change: transform可以提示浏览器把元素单独提升一层但也别滥用滥用会导致内存占用暴增。我只会给常年动的.horse和.ground加上。第二确认动画是否无限循环且无停顿。infinite是无限循环但有些动画比如尘土在首轮开始前会有延迟空档用animation-delay的负值例如-0.3s可以消除这个空档让动画从中间某帧开始播放视觉上无缝衔接。第三控制好z-index层级。场景中的元素按照“背景天空 - 云朵 - 太阳 - 地面 - 骏马 - 尘土”的关系设定层叠顺序避免出现马跑到地面后面的低级错误。这里隐藏的知识点就是z-index只对定位元素生效很多新手在非定位元素上加z-index怎么调都没反应就是这个原因。测试完以上几项效果已经很完整了。整段代码不依赖任何JS浏览器打开即运行。你可以把代码丢进CodePen或本地HTML文件中直接看效果建议同时打开Chrome DevTools的性能面板观察帧率正常情况下应该是平稳的60帧。4. 常见问题与排查技巧实录4.1 雪碧图动画“鬼畜”而不是奔跑这个现象十有八九是steps()参数和帧数不匹配。比如雪碧图有8帧你却写在steps(6)那么背景定位会在8帧图片上跳6次必然错位。排查方法先把animation暂停手动设置background-position为0 0、-200px 0、-400px 0等逐帧确认单帧显示是否正确。如果手动切帧没问题再回来检查steps()数值是否等于帧数同时确认background-size的百分比是否和帧数对应一致。4.2 动画循环时总有短暂“闪烁”这种闪烁通常由两处引入一是雪碧图最后一帧到第一帧之间因为位移计算是0 - -1600px刚好覆盖8个位置但如果总宽度算多或者算少循环点就露馅了二是background-color的默认值透出来。补救办法是给.horse-runner加一个和图纸底色一致的background-color或者确认雪碧图本身完全覆盖容器。另一种闪烁来源于元素透明度和位移组合时浏览器合层偶发Bug。此时可以给动画元素加上translateZ(0)强制触发GPU合成通常能缓解。4.3 马跑起来了但地面像在“蠕动”出现这个情况说明地面滚动速度和马的摆动频率不匹配。理想的处理方式是调整地面动画时长让它与马蹄落地的节奏大致对应。经验值是马的雪碧图动画时长0.8秒地面平移一个循环也建议控制在0.8秒到1.2秒之间具体微调即可。4.4 代码放上去没有动画效果先看CSS是否成功引入。有三种方式外部样式表、内部样式、内联样式。在做纯CSS小Demo时用内部样式style标签最方便不容易出现文件路径问题。其次是检查keyframes名称是否和animation中的名称完全一致这个看起来低级但真的很容易犯。再不行打开DevTools在Elements面板找到元素看Styles里有没有出现删除线。如果出现过期的属性如老旧的animation-timing-function写法浏览器会直接忽略。4.5 背景图片死活不显示优先检查URL。如果是相对路径确认HTML页面和图片的位置关系。本地调试时我建议直接使用图片转Base64的方式或本地绝对路径省得路径问题干扰你验证动画逻辑。如果你用的是在线示例图务必确认图片支持跨域且资源未过期否则建议先下载到本地再引用。5. 从“跑马”到“思路自由”的进阶指南到这里一匹纯CSS奔跑的骏马就完成了。但比起照抄代码我更希望你能从中抽出一套“CSS整活公式”以后看到任何题材都能快速拆解成可执行的小步骤。我的个人经验是任何CSS动画项目先拆成“环境、主体、特效”三层再分别问自己三个问题。环境层能用渐变或阴影画出来吗主体层能拆帧吗能用雪碧图实现动作吗特效层需要几个伪元素动画是位移、旋转、缩放还是透明度变化问完这三个问题再复杂的动画也能落地。最后分享一个我实际踩过的坑别在动画里过度使用选择器嵌套过深的结构。有时候为了样式我们习惯写.scene .wrapper .container .horse-runner这种路径但浏览器做样式匹配时会沿着层级一路查找动辄几十个元素还好一旦动画复杂起来性能损耗会被放大。能用类名独立解决问题就尽量扁平化结构。这一点对做大型前端项目同样适用。现在轮到你打开编辑器了。找一张骏马雪碧图或者自己画一匹简笔马跟着文章的思路做一遍。跑起来那一步你会觉得CSS其实特别有意思。