搞不懂复杂动画咋写?试试让父容器动起来,子元素跟着变

3,120字
13–20 分钟
in

很多刚接触网页动效的朋友,一看到那种好几个元素交错移动、互相穿插的动画,脑袋直接就大了,感觉要写好多个关键帧,还得精确算好每个元素的位置和运动轨迹,想想就头疼。其实啊,现在写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>处于打开状态时,改变其内部某个父级元素的样式,然后让子元素做好对应的“补偿”或“夸张”动作就行。这种技巧,让动效的实现成本大大降低,也让代码更容易维护。下次遇到多个元素需要协同运动时,不妨先问问自己:能不能只动它们的“老大”?