CSS秘密花园: 沿着路径的动画

CSS Secrets》是@Lea Verou最新著作,这本书讲解了有关于CSS中一些小秘密。是一本CSSer值得一读的一本书,经过一段时间的阅读,我、@南北@彦子一起将在W3cplus发布一系列相关的读后感,与大家一起分享。

CSS Secrets

问题

几年前,当CSS动画刚出来的时候是多么的令人兴奋,那时Chris Coyier问我,有没有什么方式使用CSS让元素绕一个圆形的路径运动。当时,它只是一个有趣的想法,但我在无意中发现有很多这方面的用例。例如,Google+添加新成员就使用了这样一个动画。如下图所示:

沿着路径的动画

一个与众不同而又有趣的例子可以看看俄罗斯科技网站的404页面,如下图所示:

沿着路径的动画

通常在404页面上是一个很好的实践地方,下如上图所示,他提供网站主要几个领域的导航菜单。这几个菜单绕着一个圆形路径运动。

然而,每一个菜单项类似行星绕着地球转上一圈,而且上有的文字写着“飞往其他星星的宇宙”。当然,如果只移动行星绕着循环的路径转而文字不旋转,这将使这些文本几乎无法阅读。这只是众多例子之中的一个。但我们怎样才能使用CSS动画达到这样的效果呢?

我们来写一个非常简单的示例,一个头动绕着圆形的路径循环的旋转,这个有点像前面提到的Google+效果的简化版。其结构如下:

<div class="path">
    <img src="lea.jpg" class="avatar" />
</div>

在还没有开始制作动画之前,我们先设置一些基本样式(例如:大小、背景、外距等),看起来如下图所示:

沿着路径的动画

因为这些都是基本样式,这里没有写出来,但如果你有什么困难,可以查看示例中的代码。最主要的要记住,路径直径是300px,因此半径是150px

我们已经完成了基本样式之后,就可以开始考虑动画怎么写。将头像移到圆中,绕着橙色的路径旋转一圈。我们可能使用CSS动画来实现,那这样动画怎么写呢?当面对这个问题时,我们想到的是这样:

@keyframes spin {
    to { transform: rotate(1turn); }
}
.avatar {
    animation: spin 3s infinite linear;
    transform-origin: 50% 150px; /* 150px = path radius */
}

虽然这朝着正确的方向迈进了一步,头像在绕着圆形的路径旋转,如下图所示:

沿着路径的动画

正如上图所示,头像在绕圆形路径旋转时,你也发现头像自身也颠倒了。如果是文本,文本也会被翻个底朝天,这对于阅读来说是一个相当糟糕的事情。我们只希望头像绕着圆路径运动,同时自身保持同一个方向。

当时我和Chris都没有想出一个合理的方式来解决这个问题。我们可以想出的最好方法是通过多个关键帧绘制近似一个圆形的路径,显然这不是一个好的主意,也没有任何方式能定义出来这样的圆形路径。那么我们必须得想出一个更好的方法,对吗?

使用两个元素的解决方案

我根据Chris提供的参考意见,我终于想出了一个解决方案。这个解决方案背后的主要思想是来自于前面介绍的"平形四边形"和"钻石图片":通过取消嵌套中的transform。然而,这是一个动画,它发生在每一帧的动画之中。需要特别说明的是,就像前面提到的内容,这里需要两个元素。因此,我们需要在HTML中添加一个额外的HTML元素<div>来包裹头像:

<div class="path">
    <div class="avatar">
        <img src="lea.jpg" />
    </div>
</div>

让我们把前面的动画效果用到.avatar容器上。现在我们看到的效果和前面出现的效果是一样的,这并不是我们需要的,因为它也旋转元素自身。但是,如果我们给.avatar设置一个旋转,并且给头像img设置一个相反的旋转,而且他们旋转的值都是相同的,将会发生什么呢?如此一来,两个旋转将相互对冲,我们只会看到他们绕着旋转的原点做圆周运动。

不过有一个问题:这里没有表态的旋转,他们都是经过一系列的角度旋转。例如,如果角度是60deg,那么取消的旋转角度应该是-60deg(或300deg),如果旋转的角度是70deg,那么取消的旋转角度应该是-70deg(或290deg)。它们都是发生在0360deg之间(或01turn之间)。那么我们要怎么设置角度呢?答案比看起来要容易得多。我们只需要给动画的反向设置360deg0deg,如下所示:

@keyframes spin {
    to { 
        transform: rotate(1turn); 
    }
}
@keyframes spin-reverse {
    from { 
        transform: rotate(1turn); 
    }
}
.avatar {
    animation: spin 3s infinite linear;
    transform-origin: 50% 150px; /* 150px = path radius */
}
.avatar > img {
    animation: spin-reverse 3s infinite linear;
}

现在,在任何时间,当第一个动画旋转x deg,第二个旋转360 - x deg,其中一个增加多少,另一个就要减少多少。这才是我们想要的,比如下图所示的效果就是我们期望的效果。

沿着路径的动画

代码我们需要改进一些。首先,我们动画的所有参数重复两次。如果我们要调整时间,我们就需要调整两次,这样做较为麻烦。其实可以通过inherit属性继承父元素的animation

@keyframes spin {
    to { transform: rotate(1turn); }
}
@keyframes spin-reverse {
    from { transform: rotate(1turn); }
}
.avatar {
    animation: spin 3s infinite linear;
    transform-origin: 50% 150px; /* 150px = path radius */
}
.avatar > img {
    animation: inherit;
    animation-name: spin-reverse;
}

然而,我们不需要一个全新的动画,只需要最初的一个动画。记得我们在介绍“闪烁”动画一节中,有介绍过animation-direction属性,其中有一个alternate值是非常有用的。在这里我们将使用reverse值,得到一个反向的原动画,因此不需要创建第二个动画:

@keyframes spin {
    to { transform: rotate(1turn); }
}
.avatar {
    animation: spin 3s infinite linear;
    transform-origin: 50% 150px; /* 150px = path radius */
}
.avatar > img {
    animation: inherit;
    animation-direction: reverse;
}

我们继续吧!这可能不是最理想的解决方案,因为他需要添加额外的元素,但只使用了不到十行的CSS代码实现了相当复杂的动画效果。

使用单个元素的解决方案

在前一节中,我们解决了问题,但这不是最优的方案,因为它需要修改HTML。当初我给CSS工作组提了一个建议,建议可以在相同的元素指定多个转换源。这应该能够在一个元素上实现,也似乎是一个合理的要求。

在讨论过程中,Aryeh Gregor给CSS的transform规范中提了这样一个声明,似乎令人困惑不解:

transform-origin就是一个语法糖。你可以使用translate()替代。—— @Aryeh Gregor

有关于相关的讨论可以点击这里阅读

事实证明,每个transform-origin可以模拟两次translate()。例如下面的两个代码段是等价的:

transform: rotate(30deg);
transform-origin: 200px 300px;

transform: translate(200px, 300px)
           rotate(30deg)
           translate(-200px, -300px);
transform-origin: 0 0;

这看起来很奇怪,让我们对transform了解的更清楚,transform函数不是独立的。每个transform属性不仅应用在元素上,而且整个坐标系统运用在同一个元素上,也将影响所有的transform。这也说明为什么不同的transfrom顺序很重要,不同顺序的相同转换可能前生的结果会完全不同。如果你还不了解这一点,下图可以帮助你更好的理解:

沿着路径的动画

因此,我们可以利用这个方法来处理我们的动画:

@keyframes spin {
  from {
    transform: translate(50%, 150px) rotate(0turn) translate(-50%, -150px);
  }
  to {
    transform: translate(50%, 150px) rotate(1turn) translate(-50%, -150px);
  }
}
@keyframes spin-reverse {
   from {
     transform: translate(50%,50%) rotate(1turn) translate(-50%, -50%);
  }
  to {
    transform: translate(50%, 150px) rotate(0turn) translate(-50%, -50%);
  }
}
.avatar {
    animation: spin 3s infinite linear;
}
.avatar > img {
    animation: inherit;
    animation-name: spin-reverse;
}

这样看起来很笨拙,但不要担心,接下来我们会改善。请注意,我们现在不再有不同的transform-origin,但我们要记住,我们要需要两个元素和两个动画。现在所有都使用相同的transform-origin,可以在.avatar上结合这两个动画:

@keyframes spin {
  from {
    transform: translate(50%, 150px)
      rotate(0turn)
      translate(-50%, -150px)
      translate(50%,50%)
      rotate(1turn)
      translate(-50%,-50%)
  }
  to {
    transform: translate(50%, 150px)
      rotate(1turn)
      translate(-50%, -150px)
      translate(50%,50%)
      rotate(0turn)
      translate(-50%, -50%);
  }
}
.avatar { 
    animation: spin 3s infinite linear; 
}

这代码是得到了改善,但仍然让人感到困惑。我们还能让它变得更简洁吗?我们进一步来改进它:

我们连续做了多次translate()特别是translate(-50%,-150px)translate(50%,50%)。不幸的是,百分比和绝对长度不能结合在一起(除非我们使用calc())。然而,水平的translate相互取消了,但还有两个次translateY(translateY(-150px)translateY(50%))。因为为了取消翻转,我们可以对关键帧这样做:

@keyframes spin{
  from{
    transform: translateY(150px) translateY(-50%) rotate(0turn) translateY(-150px) translateY(50%) rotate(1turn);
  }
  to {
    transform: translateY(150px) translateY(-150%) rotate(1turn) translateY(-150px) translateY(50%) rotate(0turn);
  }
}
.avatar {
  animation: spin 3s infinite linear;
}

代码变得更少了,重复的也变得更少,但仍然不是很好。我们可以做到更好吗?如果我们的头像在圆的中心,如下图所示:

沿着路径的动画

我们可以继续减少两个translate,然后动画就变成:

@keyframes spin{
  from{
    transform: rotate(0turn) translateY(-150px) translateY(50%) rotate(1turn);
  }
  to {
    transform: rotate(1turn) translateY(-150px) translateY(50%) rotate(0turn);
  }
}
.avatar {
    animation: spin 3s infinite linear;
}

这似乎是我们今天做得最好的了。这可能是最短的代码。现在最少的重复和没有多余的HTML元素。

如需转载,烦请注明出处:http://www.w3cplus.com/css3/css-secrets/animation-along-a-circular-path.html

返回顶部