
1. 钟摆看着简单为什么小白一上来就卡壳前端圈里有个很经典的入门练手题用HTML和CSS做一个会晃动的钟摆。听起来是不是特别简单一根线下面挂个球左右晃就行。但你去B站、掘金、CSDN翻一圈评论区永远有人在问为什么我的钟摆不是以悬挂点为轴转而是整个框都在转为什么我的钟摆晃起来像僵尸摇头为什么动画结束还会“咔哒”跳一下先别慌这些问题我当年全部踩过一遍。这篇博文就用最直白的方式从零开始手搓一个钟摆动效。核心思路是不写一行JavaScript纯HTML加CSS搞定。CSS动画走的是transform和animation这两套体系只要你把旋转中心、关键帧、动画曲线这三件事弄明白钟摆就八九不离十了。这篇文章适合完全没写过动画的新手也适合那些“照着教程敲了一遍但不知道原理”的朋友。先说结论一个能看的钟摆动效代码量可以控制在60行以内但其中真正决定成败的并不是代码量而是你对CSS transform-origin这个属性的理解。很多人直接复制的代码跑起来效果不对往往就是在这个属性上栽了跟头。为了让讨论不悬空我先给一个最最基础、一眼能看懂的版本。这个版本可能不是最优解但它是理解后面所有优化的基石。!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title基础版钟摆/title style * { margin: 0; padding: 0; box-sizing: border-box; } body { background: #1a1a2e; height: 100vh; display: flex; justify-content: center; align-items: center; } .clock-frame { width: 320px; height: 400px; background: #16213e; border-radius: 24px; box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5); display: flex; justify-content: center; position: relative; } .pendulum { position: absolute; top: 40px; width: 4px; height: 260px; background: #e2c275; transform-origin: top center; animation: swing 2s ease-in-out infinite alternate; } .ball { position: absolute; bottom: -18px; left: 50%; transform: translateX(-50%); width: 40px; height: 40px; background: radial-gradient(circle at 30% 25%, #f8e08e, #b8860b 70%, #7a5c00); border-radius: 50%; } keyframes swing { from { transform: rotate(12deg); } to { transform: rotate(-12deg); } } /style /head body div classclock-frame div classpendulum div classball/div /div /div /body /html这个版本为什么能晃起来很多人会脱口而出因为有animation。但一旦追问“为什么它是围绕顶部那个点转而不是围绕自己中心转”就有一大堆人答不上来。答案藏在这行代码里transform-origin: top center;这句话的意思是旋转的轴心放在元素自身的顶部正中。钟摆的杆从顶部往下垂所以以顶部为轴旋转正好模拟了绳子挂在钉子上的效果。如果你把这行删掉默认的transform-origin是50% 50%也就是元素正中心。那时候钟摆会“拦腰折断”式地原地打转看起来非常滑稽。不过这个60行不到的版本只是“能晃”离“像真钟摆”还差得远。真正的钟摆在左右两头会减速、在中间会加速而不是匀速地左右摆动。这个感觉用一句大白话说就是两端慢中间快。而CSS里控制这个节奏的是easing缓动函数。很多人不知道钟摆质感的关键恰恰就藏在这个细节里。2. 手把手拆解结构怎么让“杆子”和“球”一起动很多教程喜欢把杆和球画成一张图片然后让整张图片旋转。这种方案简单但灵活性很差——你想改球的颜色、大小都得重新做图。我更推荐用HTML结构把杆和球分开画这样你随时可以独立调整它们的样式也更容易理解动画作用在了谁身上。2.1 基础结构设计一个父容器包住杆和球按照上面那版代码.pendulum是杆.ball是挂在杆底部的球。CSS的position关系要特别注意.pendulum设置了position: absolute而它的父级.clock-frame设置了position: relative所以杆的位置是相对于时钟框架算的。球设置position: absolute它的定位基准是.pendulum这个杆而不是页面。杆的高度直接决定钟摆的“绳长”。绳长不同摆动周期也不同。现实中单摆的周期公式是T 2π √(L / g)其中L是摆长g是重力加速度约9.8m/s²。但在CSS动画里我们没有真实的物理引擎动画周期是你在animation里手动指定的。换句话说CSS只会听你的指令在2秒里来回摆一次它并不关心摆长是200px还是260px。所以你需要自己按照视觉直觉去调周期摆杆越长、球越重视觉上摆得越慢越稳。我个人建议初学者在做基础版本的时候把杆的高度设成240px到280px之间球的直径40px到48px之间。这个比例在屏幕上看起来很接近真实时钟里的钟摆。如果杆太短球太大它更像一个逗猫棒而不是钟摆。2.2 能不能只用单标签搞定钟摆伪元素玩法进阶如果你对DOM结构有洁癖或者想看看更“高级”的写法可以只用一个div外加两个伪元素来实现杆和球。这样HTML结构极简但CSS会相对复杂一点。先看代码!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title单标签钟摆/title style * { margin: 0; padding: 0; box-sizing: border-box; } body { background: #1a1a2e; height: 100vh; display: flex; justify-content: center; align-items: center; } .clock-frame { width: 320px; height: 400px; background: #16213e; border-radius: 24px; box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5); display: flex; justify-content: center; position: relative; } .pendulum { position: absolute; top: 40px; width: 4px; height: 260px; background: transparent; transform-origin: top center; animation: swing 2s ease-in-out infinite alternate; } /* 杆子 */ .pendulum::before { content: ; display: block; width: 4px; height: 260px; background: linear-gradient(to bottom, #f5d78e, #b8860b); border-radius: 4px; } /* 球 */ .pendulum::after { content: ; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 42px; height: 42px; background: radial-gradient(circle at 30% 25%, #f8e08e, #b8860b 70%, #7a5c00); border-radius: 50%; box-shadow: inset 0 -6px 10px rgba(0, 0, 0, 0.3), 0 8px 12px rgba(0, 0, 0, 0.4); } keyframes swing { from { transform: rotate(15deg); } to { transform: rotate(-15deg); } } /style /head body div classclock-frame div classpendulum/div /div /body /html这个方案的核心技巧是.pendulum本身不绘制任何可见内容它的作用只有一个——提供一个可以旋转的容器。:before画杆:after画球。旋转动画作用在.pendulum上所有子元素跟随一起动。伪元素的优势是让HTML更干净但缺点也很明显伪元素不能再用其他伪元素嵌套如果你想在球上面加一串英文字母或装饰圈就会有点捉襟见肘。所以我的建议是学习阶段用双标签结构结构透明、好调参等你对动画属性都熟了再尝试单标签方案也不迟。3. 从“能动”到“像真的动”动画曲线是灵魂如果你把第一版代码里的动画改成linear也就是这样animation: swing 2s linear infinite alternate;你会立刻发现钟摆变成了一个“电风扇摇头”的感觉。左右匀速摆完全没有钟摆该有的重力感。问题出在线性动画让整个过程的速度恒定摆锤在最高点和最低点时速度完全一样这在物理上是不成立的。一个真实的单摆在最高点速度接近0然后在最低点速度最快再冲向另一侧最高点减速到0。放到CSS里这就是一条“先慢后快再慢”的速度曲线。CSS中控制这个曲线的是animation-timing-function也就是easing。默认值是ease它会先加速后减速但不够精准。真正能模拟钟摆质感的是cubic-bezier贝塞尔曲线。3.1 cubic-bezier到底是什么cubic-bezier是CSS动画中定义缓动函数的一种方式用四个参数控制一条三次贝塞尔曲线的形状。格式长这样animation-timing-function: cubic-bezier(0.5, 0, 0.5, 1);这四个数字的含义用一个尽量通俗的说法解释它描述的是动效过程中“进度与时间”的关系曲线。横轴是时间进度纵轴是动画完成度。曲线的弯曲程度决定物体运动是先快后慢、还是先慢后快、还是又慢又快又慢。第一个参数x1和第三个参数x2通常控制在0到1之间它们影响的是曲线“横向”的拉伸。第二个参数y1和第四个参数y2可以突破0到1影响的是“纵向”的加速度。说实话纯记忆参数没有意义因为你根本不可能靠脑子算出哪组参数最好。实际工作中大家都是用浏览器开发者工具去调的。Chrome调试技巧F12打开开发者工具找到元素面板里的transition或animation选项卡。点击easing旁边的曲线图标就能打开可视化编辑面板。这时候你可以直接拖拽曲线上的控制点一边拖一边看页面上的钟摆实时变化。这种方式调到满意为止比手动改数值高效太多了。3.2 一个方向用一条曲线两个方向怎么办细心的朋友会发现问题CSS的keyframes里from和to之间的动画统一使用同一个animation-timing-function。但真实钟摆从左边到右边和从右边到左边过程完全对称用的曲线应该也是对称的。好在我们用alternate让动画正放再倒放ease-in-out或cubic-bezier(0.5, 0, 0.5, 1)这类两头慢中间快的曲线正好在来回两个方向都成立。不过这里有个容易忽略的细节alternate是让整个from→to过程正放一遍、再倒放一遍。如果你把cubic-bezier的第一参数和第三参数设为不对称比如cubic-bezier(0.2, 0, 0.8, 1)那它正放的时候是“先慢后快”倒放的时候变成了“先快后慢”整体观感会很不均匀。所以在钟摆这种往返动画中尽量用对称的曲线即第一和第三参数相加等于1。我给一个自己常用的参数组cubic-bezier(0.45, 0, 0.55, 1)。这个曲线在演示里非常接近钟摆的重力感。你也可以在此基础上微调但别偏离太多否则就失去“钟摆感”了。3.3 摆动幅度、动画周期、次数怎么配钟摆摆动的“幅度”在代码里就是keyframes中rotate的角度。我建议初学者从±12deg到±15deg开始调。角度太小比如只有2度视觉效果会像杆子没挂稳在颤动角度太大比如30度看起来又像大风天里的户外广告牌。悬停角度换到屏幕坐标里12度到15度大概是钟摆最优雅的区间。动画周期就是animation-duration。2秒一次完整往复视觉上比较从容。如果含alternate一个周期等于从左边到右边一次所以在2秒里其实只走了一半的“物理摆动”完整的一次左右往返是4秒。想让钟摆有一种更慢更沉的错觉把duration调成2.4秒或2.6秒想要轻快一点1.6秒也够。动画次数建议直接infinite因为钟摆如果没有外力影响理论上会一直摆动。当然如果你在做页面引导动画只让钟摆晃两三下就停那可以把animation-iteration-count设成3并且配合animation-fill-mode: forwards让动画停在最后一帧。这里还给一个实用的调参对照表方便你边看效果边改参数位置调整方式视觉变化rotate角度从12deg改为20deg摆动幅度变大更狂野animation-duration从2s改为1.4s摆动速度变快更急促animation-duration从2s改为3s摆动速度变慢更沉稳cubic-bezier(0.45, 0, 0.55, 1)改为ease-in左侧到右侧加速明显回程时感觉生硬alternate删除并改为normal动画会从右跳回左出现“瞬移”感4. 避坑指南我踩过的四个高频问题这部分是整篇文章的精华。下面每一个坑都是我实打实摔过的不是网上抄来的理论。我把排查思路也写在里面以后你遇到类似问题照着链路走一遍就能定位。4.1 坑一动画每次结束都会“卡”一下像视频掉帧这个问题的典型表现是钟摆从左晃到右然后在右边界停住瞬间又跳回左边重新开始。即使设了alternate有些人也会看到不太对劲的跳变。原因通常有两个。第一个原因是漏掉了alternate。没有它动画每执行完一轮就会回到from状态再来一次。这个“回到from状态”是瞬间发生的所以你看到的不是完整来回而是“右→左瞬间跳跃”。解决办法就是你的animation链式缩写里一定要包含alternate关键字。第二个原因比较隐蔽你写了alternate但设置了animation-fill-mode: forwards。这两者叠加时在某些浏览器版本上会导致动画在最后一次结束后强制停留在to状态下一次循环开始前会发生一次明显的跳位。解决办法是infinite循环时一般不需要显式设置fill-mode。如果你非要设置建议用both而不是forwards因为both会同时应用from和to两端的填充循环更连贯。4.2 坑二钟摆的“球”也在自转看起来像肚皮朝前翻很多人的代码里杆和球是整体旋转的。如果球是纯圆形不管怎么转都看不出来。但只要你在球上加了文字、纹理、高光渐变问题就暴露了球本身跟着rotation一起转动上面的高光位置也在变这就破坏了真实感。真实世界里的钟摆球不管怎么晃球体本身并不自转。解决思路有两种。第一种是把球单独提出来放到.pendulum外面只让杆旋转球用JS或CSS变量跟着杆底部的坐标走。但这对新手来说太复杂而且需要用transform配合三角函数不是纯CSS能优雅搞定的。第二种是更“小聪明”但非常实用的做法在动画里对球做反向旋转补偿。比如杆旋转了10度你就让球在同一时间内旋转-10度。这样视觉上球始终保持正立。CSS里可以写成两个嵌套动画或者更快的方式是直接给.ball加一个和父级相反的animation.pendulum { animation: swing 2s ease-in-out infinite alternate; } .ball { animation: swing 2s ease-in-out infinite alternate-reverse; }注意这里用了同一套keyframes swing但alternate-reverse让球的旋转方向和父级完全相反时间也对齐。视觉结果就是球本身“保持竖直”只有杆在晃动。这个方法简单到令人发指但效果非常明显。如果你想让球在晃动中保持不转身这个反向动画补偿是最省事的做法。4.3 坑三写完代码浏览器打开后只有背景没有钟摆这个问题的“经典三连”是漏了position: relative、漏了.pendulum的高度、把display: flex写错了作用对象。我见过最多的情况是.clock-frame设了height: 400px但没有显式设置position: relative而.pendulum用的是position: absolute。结果杆子不是相对于时钟框架定位而是相对于页面左上角定位。它在页面顶部一个你看不到的位置旋转。尤其是当body设置了flex居中后clock-frame在视口中间但.pendulum却跑到了页面最顶端。不要怀疑直接把.clock-frame加上position: relative问题就能解决。第二个常见情况是把height: 100vh写在了.clock-frame上而不是body上。这样框架会撑满整个屏幕钟摆其实在页面里但看起来背景和框架融为一体杆子消失在了大色块中。检查的时候记住一个原则全屏布局背景下最外层容器控制占满视口内部卡片控制实际尺寸。4.4 坑四伪元素写了动画但死活不生效单标签方案里你可能会想在.pendulum::after上额外加一个闪烁或浮动动画。比如让小球在摆动的同时还有金属反光的感觉于是你写了.pendulum::after { animation: glow 1s infinite alternate; }结果发现小球直接消失或者完全不动。排查位置依次是伪元素有没有content: 。没有content伪元素根本不会被渲染。display属性是不是block或inline-block。父级.pendulum是不是设置了overflow: hidden把伪元素裁掉了。小球有没有设置绝对定位的left和top如果没定位它可能跑到了父级可视区之外或者被其他元素遮挡。动画名称是否和keyframes名称完全一致包括大小写。CSS动画名称是区分大小写的Glow和glow是两个东西。这种情况我推荐按顺序检查别一上来就改代码。很多时候就漏了个content浪费半小时找原因。5. 从“能看”到“耐看”我常用的几组高级微调基础钟摆做完之后如果你想把它放进个人主页、404页面或者课堂作业里光溜溜的一根杆一个球显然不够吸引人。下面这几个优化方向是我自己反复磨过的成本低、效果明显。5.1 渐变与阴影让杆和球有金属和玻璃质感杆的默认背景色可以改成纵向渐变模拟金属圆杆的高光反射。球的颜色用径向渐变高光点放在左上角阴影放在右下角立刻就有立体感。.pendulum { background: linear-gradient(to right, #f7e6a8, #a67c00 40%, #6e4e00); } .ball { background: radial-gradient(circle at 35% 25%, #fff4c2, #c9992e 45%, #684c0a 90%); box-shadow: inset 0 -8px 12px rgba(0, 0, 0, 0.35), inset 8px 0 12px rgba(255, 255, 255, 0.15), 0 10px 18px rgba(0, 0, 0, 0.5); }这里的关键是inset内阴影。外阴影负责给球体的底部一个落地感内阴影负责制造金属的厚度感。没有内阴影的球看起来就是一坨扁平的圆形贴纸。5.2 背景增强加一圈半透明的时钟刻度盘钟摆挂在空空的方框里氛围感不够。我习惯给.clock-frame加一层背景刻度方法是再加一个圆形的伪元素或绝对定位的子元素用透明边框加分段线。由于钟摆动画本来就只有20行左右再加20行背景增强代码整体复杂度依然可控。一个特别省事的方案用background: repeating-conic-gradient直接画出12个刻度的扇形底色然后用一个半径更小的圆形遮罩盖在中间形成环形刻度盘.clock-frame::before { content: ; position: absolute; inset: 30px; border-radius: 50%; background: repeating-conic-gradient( rgba(255, 255, 255, 0.25) 0deg 3deg, transparent 3deg 30deg ); -webkit-mask: radial-gradient(circle, transparent 60%, #000 61%); mask: radial-gradient(circle, transparent 60%, #000 61%); }加这段代码后框架内会多出一圈细细的刻度光晕钟摆荡过去会压住刻度立刻有了“时钟内部机械”的感觉。5.3 给“动效敏感用户”一点尊重prefers-reduced-motion这是一个很多人忽略、但很体现功底的细节。部分用户因为前庭系统问题在操作系统中开启了“减少动态效果”的选项。你的钟摆如果一直晃对这类用户可能造成不适。所以记得加一行媒体查询media (prefers-reduced-motion: reduce) { .pendulum { animation: none; transform: rotate(0deg); } }这样当用户偏好减少动态效果时钟摆会安静地垂直挂在那里。这行代码不会给视觉效果加分但会让你的页面更专业、更有人情味。说实话面试时如果主动提到这个细节往往是加分项。最后送一个小技巧写完动画以后打开Chrome的Performance面板录制几秒页面运行看看有没有Layout Shift警告。如果钟摆的晃动导致周围元素布局偏移那你的绝对定位八成少了某个父级的relative。正常情况下transform动画只会触发合成不会触发重排。如果看到重排优先检查是否存在宽度、高度、边距的动画——那些属性才是重排的元凶。钟摆只用transform就不该有这个问题。说句掏心窝的话第一次看到钟摆顺畅晃动的时候那股成就感真不亚于第一次画出一个完整的Canvas小游戏。CSS动画的乐趣就在于你掌握的原理越多能让页面“活”起来的花样就越多。这个钟摆做明白之后你可以继续挑战联动多摆、鼠标拖动跟随、加速度摆动变幅思路都是从今天这些基础属性里延伸出去的。先把上面这几步跑通你就已经赢过大多数“复制完就忘”的新手了。