搞网页动画的时候,谁还没写过几段又臭又长的CSS代码?一个公转月球动画愣是整出141行,结果在手机上还闹脾气——行星跟着乱晃。这波翻车经历正好拿来盘一盘,怎么把冗余动画代码榨干到28行,还让运动轨迹更丝滑。CSS动画的核心就是@keyframes,它定义了元素在不同时间点的样式状态,浏览器自动补间生成动画。再加上transform(变形)、translate(位移)、scale(缩放)这些属性,就能拼出各种花活。但代码写嗨了就容易埋坑,比如重复的关键帧、小数点后七八位的精确度、混在一起没法拆开的位移缩放。
冗余代码体检
打开那个60秒一圈的月球动画,里面从0%到20%的关键帧居然一模一样复制了五遍。这就好比做西红柿炒蛋,非要把“放盐、翻炒”这个步骤重复写五次,完全没必要。原始动画的@keyframes里,0%-20%、20%-40%…直到100%,每组都是同样的五个位移缩放点。更离谱的是,明明可以把一组关键帧无限循环,偏要写满整个百分比区间。
/* 原始憨憨写法:重复堆叠 */
@keyframes moon-old {
0% { transform: translate(0,0) scale(1); }
20% { transform: translate(0,0) scale(1); }
40% { transform: translate(0,0) scale(1); }
/* ... 一直重复到100% */
}这种写法除了让代码量膨胀,没有任何收益。另外那些精确到小数点后七八位的vw值,比如-3.51217391vw,在一个像素都可能看不见的精度上纠结,纯属自我感动。一个1000像素宽的屏幕,这串数字算出来是35.1217391像素,多出来的0.0000001像素连电子显微镜都看不出来。
瘦身实操步骤
第一步直接把重复的关键帧砍掉。原来动画跑一圈60秒,里面五个相同段落,那么把时长缩到12秒(60除以5),然后只保留0%、5%、10%、15%、20%这一组关键帧。改完之后代码从141行跳水到36行。
#moon {
animation: moon-orbit 12s infinite;
}
@keyframes moon-orbit {
0% { transform: translate(0,0) scale(1); }
5% { transform: translate(-3.5vw, 3.5vw) scale(1.5); }
10% { transform: translate(-5vw, 6.5vw) scale(1); }
15% { transform: translate(1vw, 2.5vw) scale(0.25); }
20% { transform: translate(0,0) scale(1); }
}第二步把百分比重新映射。因为0%和20%的状态完全一样,所以合并成0%,100%。再把5%、10%、15%对应改成25%、50%、75%。那些用来控制z-index切换时机的9.9%和19.9%,分别挪到49.9%和99.9%。这一步要留神:z-index在月球飞到行星前面和后面时需要切换,如果时机不准,月球就会从行星肚子里穿模或者提前露头。
@keyframes moon-orbit {
0%, 100% { transform: translate(0,0) scale(1); z-index: 2; }
25% { transform: translate(-3.5vw, 3.5vw) scale(1.5); z-index: 2; }
49.9% { z-index: 2; }
50% { transform: translate(-5vw, 6.5vw) scale(1); z-index: -1; }
75% { transform: translate(1vw, 2.5vw) scale(0.25); z-index: -1; }
99.9% { z-index: -1; }
}第三步把那些离谱的小数四舍五入到一位。-3.51217391vw直接变-3.5vw,6.511304348vw缩成6.5vw。反正屏幕渲染也吃不到那么细的精度,省得代码看着像一串乱码。另外z-index在25%和75%这两个点实际上没变化,可以直接删掉,只在变化的节点保留就行。
分离位移缩放
这个动画有个硬伤:月球飞到行星正上方的时候,轨迹会拐弯。因为原本位移和缩放全挤在transform属性里,浏览器没法单独插值,只能猜中间点。比如从左上飞到右下,同时从1倍大小变成1.5倍再变回0.25倍,混在一起就成了一条弧线而不是直线。解决办法是用独立的translate和scale属性替代transform,这两个属性从2020年开始主流浏览器都支持得挺稳。
#moon {
animation: moon-orbit 12s infinite ease-in-out;
}
@keyframes moon-orbit {
0%, 100% {
translate: 0 0;
scale: 1;
z-index: 2;
}
25% {
scale: 1.5;
}
49.9% {
z-index: 2;
}
50% {
translate: -5vw 6.5vw;
scale: 1;
z-index: -1;
}
75% {
scale: 0.25;
}
99.9% {
z-index: -1;
}
}分离之后,25%这个关键帧里只写了scale:1.5,没有写translate,浏览器会自动从上一帧的translate(0,0)平滑过渡到下一帧的translate(-5vw,6.5vw)。同样75%只改了缩放比例,位移继续按直线插值。这样一来月球就从右上角笔直飞到左下角,同时体积先变大再缩回去,符合“靠近观察者时变大”的视觉逻辑。最后把分散在各个关键帧里的animation-timing-function统一提到动画声明里,用ease-in-out让加减速更自然。
另一种解法
如果浏览器版本太老不支持独立translate和scale属性,还有一个备用方案:套一层父容器。外层做位移,内层做缩放,两个元素各干各的。虽然多了个DOM节点,但兼容性好到能跑在IE10上。
<div class="moon-orbit">
<div class="moon-scale"></div>
</div>.moon-orbit {
animation: move 12s infinite ease-in-out;
}
.moon-scale {
animation: resize 12s infinite ease-in-out;
}
@keyframes move {
0%, 100% { transform: translate(0,0); }
50% { transform: translate(-5vw, 6.5vw); }
}
@keyframes resize {
0%, 100% { transform: scale(1); }
25% { transform: scale(1.5); }
75% { transform: scale(0.25); }
}这个方案的缺点是需要维护两个动画的时长和缓动函数完全同步,不然位移和缩放就会各跑各的,造成月球像鬼畜一样乱抖。一般还是优先用独立属性,代码量少还不用操心同步问题。最后对比一下,原始141行代码跑起来手机掉帧,精简到28行之后月球公转稳如老狗,连行星那几像素的乱晃也消失了——原来是因为动画里反复重绘导致父元素位置漂移,代码干净了浏览器渲染压力小了,连带bug也修好了。
