很多刚接触网页动效的朋友,一看到那种好几个元素交错移动、互相穿插的动画,脑袋直接就大了,感觉要写好多个关键帧,还得精确算好每个元素的位置和运动轨迹,想想就头疼。其实啊,现在写CSS动画,真没必要这么折腾。咱们完全可以换个思路,不去管那些子元素怎么跑,而是直接“摇”它们的“老父亲”——也就是包裹这些元素的父容器。只要父容器一变,里面的子元素自然就跟着动起来了,有时候一个动画就能搞定好几个元素的复杂运动,既省事又高效。
为啥动父容器比挨个动孩子更香
想象一下,手里有四颗棋子,想让他们在棋盘上同时移动位置并且交错穿过对方。如果一颗一颗地去挪,那得记好几套走法,还得保证它们不撞车。但要是直接挪动整个棋盘呢?棋盘一旋转一收缩,上面的棋子自然就都跟着到了新地方,相对位置还都保持着,多省心。这里的关键就在于,咱们平时改变一个元素的尺寸、旋转角度这些变换(transform)操作时,它里面的子元素也会被“连累”,一起被拉伸、旋转。这个“连累”的特性,就是咱们要利用的“秘密武器”。下面咱们就通过一个实实在在的例子,看看怎么把几个圆点弄成交叉移动的效果。
搭建一个圆点家族
先把HTML架子搭起来,很简单,一个大盒子(<main>)里面装四个小圆点(.circle)。
<main>
<div class="circle"></div>
<div class="circle"></div>
<div class="circle"></div>
<div class="circle"></div>
</main>为了让每个小圆点能牢牢地“粘”在父容器的四个角上,需要给父容器加上一点定位上的限制,再让每个小圆点用绝对定位固定在对应的角落。这样,不管父容器怎么变,圆点们都会死死地守在自己的角上。
main {
contain: layout;
position: relative;
width: 200px;
height: 200px;
}
.circle {
position: absolute;
width: 60px;
height: 60px;
border-radius: 50%;
}
/* 分别固定在四个角 */
.circle:nth-of-type(1) { background: rgb(0, 76, 255); top: 0; left: 0; }
.circle:nth-of-type(2) { background: rgb(255, 60, 0); top: 0; right: 0; }
.circle:nth-of-type(3) { background: rgb(0, 128, 111); bottom: 0; left: 0; }
.circle:nth-of-type(4) { background: rgb(255, 238, 0); bottom: 0; right: 0; }让父容器表演“缩骨功”
现在,想要四个圆点往中心靠拢并且交换位置。不用管圆点,直接给父容器加一个动效:让它“瘦身”(宽度变小)的同时,再转个圈。一旦父容器宽度缩水,四个固定在角上的圆点就会被“挤”向中心;而父容器的旋转,则会带着这些圆点互换位置,看起来就像是它们交叉穿过彼此一样。
/* 给父容器加的动效class */
.animate {
width: 0;
transform: rotate(90deg);
transition: width 1s, transform 1.3s;
}这一步里,父容器的宽度从原本的尺寸变成了0,这个过程是平滑过渡的。同时,旋转90度也是平滑进行的。如果想让动画结束时,圆点们正好停留在另一个角上,可以调整旋转的角度和宽度的最终值。比如,想让左下角的圆点跑到右下角,可以通过不同的旋转和缩放组合来实现。
用JS轻点“播放键”
动画规则定好了,就差一个触发动作。当点击某个按钮时,就给父容器加上那个.animate的类名。为了让动画每次都能重新播放,需要先“重置”一下,把类名清空,再重新加上。这个小技巧里,读取一下offsetWidth这个属性,就能强制浏览器重新计算样式,确保动画能重复触发。
const mainBox = document.querySelector("main");
function triggerAnimation() {
mainBox.className = "";
// 强制浏览器重绘,确保动画重新开始
mainBox.offsetWidth;
mainBox.className = "animate";
}现在,每次调用triggerAnimation函数,四个圆点就会上演一场“交错换位”的戏码,而背后的代码却异常简洁,只需要控制父容器一个元素。这种方法在处理多个元素需要同步运动的场景时,简直不要太爽。
玩点花的:让父子动作“对着干”
刚才的例子是让父容器的变换直接带着子元素跑,但有时候想要更复杂的视觉效果,比如两个方块在运动时互相“抵消”掉父容器的某种变形。这里就可以用到“对着干”的思路,父容器怎么歪,子元素就怎么反着歪。
场景一:方块交错但不变形
假设有两个方块,想让它们在移动时相互穿过,但自身形状要保持方正,不能被父容器的倾斜给带歪了。这时,父容器做一个倾斜(skewY),而里面的每个子元素就做一个相反角度的倾斜,把父容器施加的变形给“中和”掉。
| 元素 | 变换(transform) | 作用 |
|---|---|---|
| 父容器 | skewY(30deg) | 整体倾斜 |
| 子元素 | skewY(-30deg) | 抵消父容器倾斜,保持方正 |
main {
/* 其他样式 */
transition: transform 1s;
}
main.animate {
transform: skewY(30deg);
}
main.animate .square {
transform: skewY(-30deg);
transition: inherit; /* 继承父容器的过渡时间 */
}这样,看到的动画效果就是两个方块本身是正正方方的,但它们的位置发生了交错的移动。父容器倾斜的力,被每个方块自己的反向倾斜给“卸”掉了。
场景二:方块分离又形变
如果想让效果再丰富点,比如在方块分离的过程中,它们自身的形状也从方块拉长成条形。那就可以让父容器同时做旋转、缩放和倾斜,而子元素在抵消倾斜的同时,额外做一个拉伸(scaleX)操作。
| 元素 | 变换(transform) | 组合效果 |
|---|---|---|
| 父容器 | rotate(-180deg) scale(.5) skewY(45deg) | 旋转、缩小、倾斜 |
| 子元素 | skewY(-45deg) scaleX(1.5) | 抵消倾斜,水平拉伸 |
main.animate {
transform: rotate(-180deg) scale(.5) skewY(45deg);
transition: 0.6s transform;
}
main.animate .square {
transform: skewY(-45deg) scaleX(1.5);
transition: inherit;
}这里父容器逆时针转180度并且缩小一半,子元素不仅纠正了倾斜,还把自己在水平方向拉长了1.5倍。最终呈现的效果就是两个方块在移动过程中,从方块逐渐变成了横条,视觉上非常有趣。
轻松驾驭更多动效组合
掌握了这种“父子联动”的思维,会发现很多看似复杂的动画,其实就是几种基础变换(旋转、缩放、倾斜、位移)的加减组合。比如,可以做一个点击就展开的交互面板(<details>元素),当它打开时,里面的图标就上演一场精彩的变形记。
不需要写复杂的JS去控制每个图标的动画,只需要在<details>处于打开状态时,改变其内部某个父级元素的样式,然后让子元素做好对应的“补偿”或“夸张”动作就行。这种技巧,让动效的实现成本大大降低,也让代码更容易维护。下次遇到多个元素需要协同运动时,不妨先问问自己:能不能只动它们的“老大”?
