【译】使用 CSS 分层动画实现曲线运动

@JChehe 2018-08-13 11:13:04发表于 JChehe/blog

原文:Moving along a curved path in CSS with layered animation

CSS animation 和 transition 均能很好地实现 A 到 B 的过渡,但这仅限于直线运动。无论你如何调整元素的 animation 或 transition 的 贝塞尔曲线*-timing-function),均不能使其沿曲线运动。当然也包括自定义过渡函数,如弹簧效果。这是因为 X 轴与 Y 轴的相对位移总是相等的。

无疑 JavaScript 能轻易实现曲线运动,但这里有一个简单方法能突破这个限制:分层动画。通过使用两个或以上的元素去驱动一个动画,即对元素的路径进行细粒度的控制,分别为 X 轴和 Y 轴应用不同的过渡函数(*-timing-function)。

问题所在

See the Pen css-curve-1 by Jc (@JChehe) on CodePen.

<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

在深入研究解决方案前,先仔细研究一个问题。CSS animation 和 transition 限制着元素仅能沿直线运动,即总是执行 A 到 B 的最短路径,这很适合大多数情况。但却缺乏一种方式告诉 CSS 应使用“更佳路径”而不是“最短路径”。

在 CSS 中,两点位移过渡的最直接方式是使用 transform 的 translate 属性,但这仅能产生直线运动。在以下 @keyframes 中,元素会在 (0, 0) 和 (100, -100) 间来回移动:

@keyframes straightLine {
  50% {
    transform: translate3D(100px, -100px, 0);
  }
}

.dot {
  animation: straightLine 2.5s infinite linear;
}

这并不复杂。为了得到问题的解决方案,我们需要将动画进行拆分(至少在视觉上)。

我们从 0% 的 (0, 0) 开始,并在 50% 使用 translate3d(100px, -100px, 0) 将元素位移至 (100, -100),最后原路返回。换一种思考方式,元素分别向右位移 100px 和向上位移 100px,两者一结合就会产生一定角度的直线位移。

See the Pen css-curve-2 by Jc (@JChehe) on CodePen.

<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

解决方案:为每轴分别指定一个过渡函数

那么我们如何创建前面案例提及的曲线运动呢?要创建非直线运动,需要为 X 轴与 Y 轴指定不同的运动速度

前面案例均使用 linear 过渡函数。但这里我们要为元素添加一个父元素,并为父元素 X 轴与 Y 轴分别应用不同的过渡函数。下面将为 X 轴应用 ease-in,为 Y 轴应用 ease-out

See the Pen css-curve-3 by Jc (@JChehe) on CodePen.

<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

实现:一轴对应一元素

不幸的是,CSS 不支持为 transform 叠加多个 animation,否则仅最后声明的 animation 有效。那么我们该如何组合两个 animation 呢?首先,先将一个元素放在另一个元素中,然后对容器元素指定一个 animation,再对子元素指定另一个不同的 animation 即可。

在上述所有曲线运动中,我们均能看到两个分离的元素在运动,而(曲线运动的)容器元素(即父元素)则完全透明。为了能清楚看到两个元素如何结合得到曲线运动,我们为容器元素添加边框:

See the Pen css-curve-4 by Jc (@JChehe) on CodePen.

<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

dot 元素是边框元素的子元素。边框元素沿 X 轴水平运动,而 dot 自身沿 Y 轴上下竖直运动。移除父元素的边框后,就可得到我们预期中的曲线运动。利用伪元素则无需在 HTML 中创建两个元素。

译者注:部分移动设备中,animation / transition 对伪元素无效。

假设有以下 HTML 代码:

<div class="dot"></div>

那么可以像下面那样添加伪元素:

.dot {
  /* Container. Animate along the X-axis */
}

.dot::after {
  /* Render dot, and animate along Y-axis */
}

然后添加两个独立的 animation 代码块:一个用于 X 轴,一个用于 Y 轴。其中第一个使用 ease-in,第二个使用 ease-out

.dot {
  /* Some layout code… */
  animation: xAxis 2.5s infinite ease-in;
}

.dot::after {
  /* Render dot */
  animation: yAxis 2.5s infinite ease-out;
}

@keyframes xAxis {
  50% {
    animation-timing-function: ease-in;
    transform: translateX(100px);
  }
}

@keyframes yAxis {
  50% {
    animation-timing-function: ease-out;
    transform: translateY(-100px);
  }
}

带上 WebKit 内核前缀,并使用自定义贝塞尔曲线替换 ease-inease-out,就能得到文章最开始的效果:

.demo-dot {
  -webkit-animation: xAxis 2.5s infinite cubic-bezier(0.02, 0.01, 0.21, 1);
  animation: xAxis 2.5s infinite cubic-bezier(0.02, 0.01, 0.21, 1);
}

.demo-dot::after {
  content: '';
  display: block;
  width: 20px;
  height: 20px;
  border-radius: 20px;
  background-color: #fff;
  -webkit-animation: yAxis 2.5s infinite cubic-bezier(0.3, 0.27, 0.07, 1.64);
  animation: yAxis 2.5s infinite cubic-bezier(0.3, 0.27, 0.07, 1.64);
}

@-webkit-keyframes yAxis {
  50% {
    -webkit-animation-timing-function: cubic-bezier(0.02, 0.01, 0.21, 1);
    animation-timing-function: cubic-bezier(0.02, 0.01, 0.21, 1);
    -webkit-transform: translateY(-100px);
    transform: translateY(-100px);
  }
}

@keyframes yAxis {
  50% {
    -webkit-animation-timing-function: cubic-bezier(0.02, 0.01, 0.21, 1);
    animation-timing-function: cubic-bezier(0.02, 0.01, 0.21, 1);
    -webkit-transform: translateY(-100px);
    transform: translateY(-100px);
  }
}

@-webkit-keyframes xAxis {
  50% {
    -webkit-animation-timing-function: cubic-bezier(0.3, 0.27, 0.07, 1.64);
    animation-timing-function: cubic-bezier(0.3, 0.27, 0.07, 1.64);
    -webkit-transform: translateX(100px);
    transform: translateX(100px);
  }
}

@keyframes xAxis {
  50% {
    -webkit-animation-timing-function: cubic-bezier(0.3, 0.27, 0.07, 1.64);
    animation-timing-function: cubic-bezier(0.3, 0.27, 0.07, 1.64);
    -webkit-transform: translateX(100px);
    transform: translateX(100px);
  }
}

这就是文章最开始的效果:

See the Pen css-curve-5 by Jc (@JChehe) on CodePen.

<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

你可能注意到:目前所有案例均使用了 @keyframes 代码块,但这纯粹是因为需要几个关键帧来实现来回移动的动画。换句话说,分层动画也适用于 transition 属性,特别是 A 到 B 的这类动画。

对于绝对定位的元素,你可以通过设置单个元素的 leftbottom 属性实现曲线运动,从而避免使用容器元素。但这并不推荐,因为动画的每一帧均会触发重绘,导致性能低下。使用具有伪元素的分层动画,能利用硬件加速的 translate 属性产生漂亮丝滑的动画。


译者基于本文实现了以下效果:

See the Pen css-curve-final by Jc (@JChehe) on CodePen.

<script async src="https://static.codepen.io/assets/embed/ei.js"></script>