{T}

帧动画与过渡动画:谁更适合你的业务场景?

众所周知,使用 CSS 制作 Web 动画有两种方式,即帧动画(Keyframe Animation)和过渡动画(Transition Animation)。然而,一个关键问题一直存在:在特定的业务场景中,究竟应该选择帧动画还是动渡动画?或者说,使用 CSS 制作动画,何时使用其中一种而不是另一种?它们似乎都是用来实现相同的效果。嗯,事实并非如此。

CSS 过渡(Transitions)和 CSS 动画(Animations)确实有一些相似之处和差异。主要的选择点将取决于你希望如何控制页面上的动画。简短的回答是:对于交互元素,请使用过渡,而对于连续的装饰性元素,请使用帧动画。但在选择 CSS 过渡动画与帧动画时,有一些重要因素需要考虑。

这节课将深入探讨 CSS 的帧动画和过渡动画,以帮助你更好地理解它们的特性、适用场景以及如何做出明智的选择。无论你是一名 Web 前端开发者、设计师还是项目经理,理解何时使用哪种动画类型对于成功实施 Web 动画至关重要。

我们先了解过渡动画和帧动画的基础理论和应用,然后再来比较这两种动画的相似之处和不同之处。在这个过程中,我们还会探讨一系列因素,如动画的复杂性、用户交互性和动画的目的。通过这个全面的讨论,你将能够为你的项目选择最适合的动画类型,从而提升用户体验并实现你的业务目标。

我们先从 CSS 的过渡动画开始吧!

CSS 过渡动画

与 Web 应用或网站交互时,你可能会注意到很多元素都有状态。列如:

  • 下拉菜单可以处于打开或关闭状态
  • 按钮或链接可以处于聚焦或悬停状态
  • 单选按钮或复选框可以处于选中或未选中状态
  • 表单控件可以处于有效或无效状态
  • 手风琴可以处于折叠或展开状态
  • 提示框可以处于隐藏或显示状态

用户与 Web 元素进行交互时,你会看到许多动画不是 CSS 动画类型,它们没有预定义的关键帧。相反,它们是对用户所做的事情的即时反应。通常情况下,CSS 会即时切换这些状态的样式。例如:

  • 悬停链接下划线即时出现
  • 点击按钮即时弹出下拉菜单选项
  • 按钮悬停时即时放大或改变颜色
  • 在表单控制获得焦点时变大
  • 以及无数其他情况

为了改变这些状态之间样式的即时切换,我们可以利用 CSS 的过渡(transition)特性,可以在元素的 A 状态(初始状态,比如按钮的默认状态)和元素的 B 状态(结束状态或目标状态,比如按钮悬停时向上移动或放大)之间进行插值计算。这个插值计算是由浏览器自动完成的,它会根据元素的初始状态 A 和结束状态 B 之间创建“中间”状态。如此一来,能够使元素从一种状态(A 状态)平滑过渡到另一种状态(B状态)。

为了更好地理解 CSS 过渡,让我们花一点时间来看一个实际例子。以下是一个按钮的示例,在悬停时会移动同时改变背景颜色,但不带有过渡效果:

HTML
<button class="btn">Hover Me!</button>
CSS
.btn {
    background-color: #0059ff;
    color: white;
    
    &:hover {
        background-color: #ffd429;
        translate: 0 -10px;
    }
}

请用鼠标悬停在按钮上面:

Demo 地址:https://codepen.io/airen/full/zYeOgxV

当你将鼠标悬停在按钮上时,请注意发生了什么?按钮瞬间向上移动,并且背景颜色也是从蓝色(#0059ff)瞬间变成了黄色(#ffd429)。这是因为,默认情况之下,CSS 中的变化是瞬间发生的。这样的效果与自然界是不符的,事物在自然界是逐渐发生变化的。

我们可以使用 CSS 的 transition 的属性来指示浏览器从一个状态插值到另一个状态:

CSS
.btn {
    transition: 
        translate 250ms linear,
        background-color 250ms linear;
}

Demo 地址:https://codepen.io/airen/full/yLZBmeQ

当你开启 transition 功能之后再将鼠标悬停在按钮上,按钮会平滑地向上移动,并且背景颜色也会从蓝色平滑过渡到黄色。然后,当你的鼠标光标移动到其他地方时,按钮会平滑地恢复到初始状态。所有这些都得益于 CSS 过渡(transition)的魔法。

刚才发生了什么?

使我们的过渡效果生效的秘密武器是 CSS 的 transition 属性。它的工作方式非常简单。它用于动画属性的变化。为了让它做到这一点,你只需要指定两个值:

  • 希望进行动画的属性名称
  • 动画的持续时间

如果计划对多个属性进行动画处理,可以将它们用逗号分隔的方式传递。例如上面的示例,我们对按钮的 translatebackground-color 两个属性进行了过渡监听。

CSS
.btn {
    transition: 
        translate 250ms linear, 
        background-color 250ms linear;
}

或者,你可以使 transition-property 属性接受一个 all 关键词值,来指示每个属性都应该进行过渡。这意味着,任何更改的 CSS 属性都将进行过渡。

尽管在动画化多个属性时使用 all 可以减少大量的编码工作,但个人建议你不要这样使用它。因为,随着产品的迭代,你或你团队的某人可能会在将来某个时候更新此代码。意外的动画可能就会出现。正如你在炒菜,添加过多的调料可能会破坏菜肴的美味。因此,在进行动画处理时,精确选择要过渡的属性是非常重要的

其次,示例指定了过渡动画的持续时间为 250ms ,即定义完成过渡所需的时间。在我们这个示例中按钮的两个属性的过渡动画都需要 250ms ,即:

  • 从原点位置沿着 Y 轴向上移动 10px ,即初始状态为 translate: 0 0 过渡到结束状态 translate: 0 -10px 共花费了 250ms
  • 从蓝色背景过渡到黄色背景,同样是花费了 250ms

当然,你可以根据过渡动画的需要,给不同的过渡属性指定不同的持续时间。

在 CSS 中,你可以通过 transition-duration 属性来指定完成过渡动画所需的时间。

除此之外,在上面的示例中,我们还指定了按钮的 translatebackground-color 属性在过渡时以恒速(linear)的方式进行。它对应的是 CSS 的 transition-timing-function 属性,即:

CSS
.btn {
    transition-timing-function: linear, linear; 
    
    /* 也可以写成 */
    transition-timing-function: linear;
}

不过,恒速(linear)过渡效果看起来有点不自然。因为,在现实生活中,对象是有重量的,不能立即停止或开始运动。我们可以通过时间函数(Timing Functions)来改变这种现象,使你的过渡效果更生成自然。

Demo 地址:https://codepen.io/airen/full/dyaybPZ

时间函数会告诉浏览器,当一个元素从一个状态过渡到另一个状态时,每个“中间状态”应该是什么样子。例如,假设我们有一个圆球从左向右滚动,整个过程花费了 1stransition-duration: 1s)。一个流畅的动画应该以 60fps 运行,这意味着我们需要在起始点和终止点之间得到 60 个不同的位置。时间函数则会影响到这 60 个位置。其实是使用 transition-timing-function 来调整 transition-duration 期间 CSS 过渡的速度

CSS 中有多个关键字可用作时间函数的预设值,它们可以用作 transition-timing-function 属性的值。比如 lineareaseease-inease-outease-in-out

  • linear (线性) :它导致元素在整个过渡期间以恒定的速度移动。它不模仿自然运动,通常在动画需要一致速度的情况下使用。
  • ease (默认值) : 它开始时较慢,然后逐渐加速,最后再次减速。这种时间函数常用于模拟物体自然运动,通常是默认的选择。
  • ease-in (加速) : 它以较慢的速度开始,然后逐渐加速。它通常用于元素进入屏幕视图并且在屏幕上有停止点的情况,例如弹出框。
  • ease-out (减速) : 与ease-in 相反,ease-out 时间函数以较快的速度开始,然后逐渐减速。它通常用于元素退出屏幕视图的情况,例如元素消失。
  • ease-in-out (先加速后减速) : 它在整个过渡中提供相等的加速和减速。它通常用于需要反复运动的动画,如元素循环淡入淡出。

我们先从 linear 开始。

先简单地解释一下上图:每个淡出的圆圈代表一段时间。当圆圈从左到右移动时,这些帧是向用户显示的。就像翻书一样。在这个动画中,我们使用的是线性(linear)时间函数。这意味着元素以恒定的速度移动;图中的圆圈每帧都以相同的距离移动。

linear 时间函数的特点是动画在整个过渡期间以恒定的速度进行,没有加速或减速效果。这意味着属性值在过渡期间均匀地从初始值过渡到结束值,而没有任何速度变化。

具体来说,linear 时间函数的速度曲线是一条直线,表示属性值在整个过渡期间以相同的速度改变。这导致动画效果起来非常直接,没有任何缓慢启动或减速的效果。这意味着整个过渡动画效果会比较生硬,不够自然。

以下是一个示例,演示如何在 CSS 中使用 linear 时间函数:

CSS
.animated {
    transition: scale 0.5s linear;
    
    &:hover {
        scale: 1.2;
    }
}

在这个示例中,元素在悬停时以linear 时间函数的速度曲线进行缩放动画,使动画呈现出恒定的速度变化,没有加速或减速效果。这种时间函数通常用于需要元素具有匀速变化的动画场景。

需要知道的是,线性(linear)函数通常不是最佳选择,毕竟在现实世界中几乎没有什么东西以这种方式移动。好的动画是来自于模仿自然界,所以我们应该选择更有生机的东西!比如 ease 时间函数。

ease 是默认的过渡时间函数(transition-timing-function 属性的默认值)。如果你没有显式指定时间函数,那么过渡就会使用 ease 时间函数。在大多数情况下,ease 通常是一个很好的选择。如果一个元素移动,而且没有进入或退出视口,通常情况下 ease 都是个不错的选择。

ease 时间函数的特点是具有渐进加速和减速效果,使元素在动画过程中呈现出更加自然和流畅的动态变化。具体来说,ease 时间函数的速度曲线是一个缓慢的开始,然后逐渐加速,最后再次减速的过程。这模仿了自然界中特体运动的方式,例如一个物体在受到力的影响下开始缓慢移动,然后逐渐加速,最后在接近目的地时减速,直到停止。

在 CSS 中,你可以使用 ease 时间函数来控制元素的平移、缩放、渐变等动画,使它们在开始和结束时具有平滑的变化,而在中间阶段逐渐加速和减速。以下是一个示例,演示如何在 CSS 中使用 ease 时间函数:

CSS
@layer transition {
    .card {
        & img {
            transition: scale 0.3s ease;
        }
    
        & figcaption {
            transition: translate 0.35s ease;
        }
    
        &:hover {
            & figcaption {
                translate: 0 -68%;
            }
            
            & img {
                scale: 1.32;
            }
        }
    }
}

Demo 地址:https://codepen.io/airen/full/vYbYYJY

在这个示例中,卡片悬停时会以 ease 时间函数的速度曲线对图片进行缩放动画和对卡片内容进行位移动画,使动画看起来更加流畅和自然。

在自然界中,物体可以做减速运动。好比一块石头从山顶滚落时,它一开始会以较快的速度往下滚,但随着时间的推移,它会受到重力的作用和地面摩擦力的阻碍,逐渐减速直至停下。CSS 中的 ease-out 时间函数也是如此,它定义过渡动画从开始到结束的速度变化,其特点是在动画开始时较快,然后逐渐减速,以便在动画结束时平稳过渡。

如果你将 ease-out 时间函数应用于一个元素的动画,它会以较高的速度开始动画,然后逐渐减速,使得动画的起始阶段非常明显,而结束阶段更加缓和。这种效果通常用于元素从屏幕外部(或容器外部)进入视图(或容器)或以某种方式引人注目的情况,因为它给人一种元素从远处急速进入,最终在用户面前停下的感觉。

CSS
@layer transition {
    .card {
        &::before {
            transform: skew(30deg) translateX(-80%);
        }
    
        &::after {
            transform: skew(-30deg) translateX(-70%);
        }
        
        &::before,
        &::after {
            transition: transform .4s ease-out;
        }
        
        :is(h3, p) {
            opacity: 0;
            transition: opacity .4s ease-out;
        }
        
        & figcaption {
            &::before {
                transform: skew(30deg) translateX(-100%);
            }
    
            &::after {
                transform: skew(-30deg) translateX(-90%);
            }
          
            &::before,
            &::after {
                transition: transform .4s ease-out;
            }
        }
        
        &:hover {
            :is(h3, p) {
                opacity: 0.9;
                transition-delay: 0.2s;
            }
    
            &::before {
                transform: skew(30deg) translateX(-20%);
                transition-delay: 0.05s;
            }
    
            &::after {
                transform: skew(-30deg) translateX(-10%);
            }
    
            & figcaption {
                &::before {
                    transform: skew(30deg) translateX(-40%);
                    transition-delay: 0.15s;
                }
                &::after {
                    transform: skew(-30deg) translateX(-30%);
                    transition-delay: 0.1s;
                }
            }
        }
    }
}

Demo 地址:https://codepen.io/airen/full/WNPNbxy

总之,ease-out 时间函数在创建动画时可以产生类似物体从远处加速靠近的效果,然后以平稳的方式停止,使其看起来更加引人注目和生动。

ease-in 时间函数刚好与 ease-out 相反。它表示在动画开始时,变化较为缓慢,然后逐渐加速,直到动画完成。这种时间函数的曲线看起来像一个平滑的加速曲线:

正如我们所见,ease-out 适用于从屏幕外进入视图的元素。ease-in 自然适用于相反的情况:将元素移动到视图范围之外。例如,模态框从屏幕上消失的动画效果,使用 ease-in 时间函数会更适合一些。

请注意,ease-in 几乎专门用于以元素不可见或屏幕之外结束的动画效果;否则,突然的停止可能会显得令人不适。

在我们现实生活中有一种摆锤(Pendulum)运动。摆锤在摆动的两个极端点之间来回摆动,它的速度在中间点最快,在两个极端点最慢:

CSS 中的 ease-in-out 时间函数就好比自然界中的一个摆锤的运动。它也被称为是一种对称的时间函数,既有 ease-out 时间函数的减速运动,又有 ease-in 时间函数的加速运动,它有相等的加速和减速。这种运动刚好符合迪士尼 12 大动画原则中的渐慢和渐快原则。

如果过渡函数使用 ease-in-out ,那么元素的变化速度会逐渐加快,然后再逐渐减慢,以使过渡效果更加自然和流畅。这种时间函数是 linear 时间函数的改进版本,它有相等的加速和减速,在动画中增加了更多的生动性,但并不过于夸张。这种曲线对于发生在循环中的任何事情最有用(例如,元素不断淡入淡出)。

如果预设的时间函数不符合你的需求,你可以考虑使用二次贝塞尔曲线(cubic-bezier())时间函数来定义符合需求的自定义时间函数(即自定义缓动曲线)。事实上,每个预设的时间函数(例如 ease)都引用了预定义的二次贝塞尔曲线。在 CSS 中,你可以使用 cubic-bezier() 函数直接定义缓动曲线,该函数接受四个数值:x1y1x2y2

CSS
.animated {
    transition-timing-function: cubic-bezier(.42, 0, .58, 1);
}

这些值沿 X 轴和 Y 轴绘制曲线的各个部分。

Demo 地址:https://codepen.io/airen/full/GRzRJab

创建自定义贝塞尔曲线可以帮助你实现特定的动画效果。然而,设计这些曲线可能相当复杂,这节课的范围无法覆盖所有细节。如果你想创建自己的贝塞尔曲线,你可以使用像 @Lea Verou 提供的工具,来进行实验和生成适应你动画需求的自定义曲线。这个工具可以帮助你可视化和微调贝塞尔曲线,从而更容易地设计适合你过渡效果的完美时间函数。

URL:https://cubic-bezier.com/

尽管二次贝塞尔曲线 cubic-bezier() 非常灵活,但它也有其限制,因为我们只能控制二次贝塞尔曲线的两个点。

即使我们使用 cubic-bezier() 函数可以模拟出 easings.net 中大部分缓动曲线,但不幸的是,我们无法使用 cubic-bezier() 来重现所有效果。

URL:https://easings.net/

例如,二次贝塞尔曲线 cubic-bezier() 就很难模拟出弹簧的运动曲线:

庆幸的是,CSS Eeasing Level 2 规范中定义的 linear() 函数允许我们实现弹跳、弹簧和弹性效果,这些效果以前只能通过复杂的 JavaScript 实现。

注意,这里所说的 linear() 函数是一个新的时间函数,它和前面所说的线性时间函数 linear 是不一样的。linear() 函数定义了一个分段线性函数,可以在其各个点之间线性插值,从而允许你模拟出更复杂的动画效果,比如弹跳和弹簧等效果。

换句话说,linear() 函数是一种绘制图表的方式,你可以在其中定义自定义缓动曲线的点数。这非常特别,为我们打开了新的可能性。这要是以前,我们是无法通过 CSS 动画实现的。

现在,我们可以像下面这样,使用 linear() 函数模拟出弹跳、弹簧和弹性等动画效果的缓动曲线:

CSS
:root {
    /* 弹跳缓动曲线 */
    --bounce-easing: linear(
        0, 0.0157, 0.0625, 0.1407, 0.25 18.18%, 0.5625, 1 36.36%, 0.8907, 0.8125,
        0.7657, 0.75, 0.7657, 0.8125, 0.8905, 1, 0.9532, 0.9375, 0.9531, 1, 0.9844, 1
    );
  
    /* 弹簧缓动曲线 */
    --spring-easing: linear(
        0, 0.009, 0.035 2.1%, 0.141, 0.281 6.7%, 0.723 12.9%, 0.938 16.7%, 1.017,
        1.077, 1.121, 1.149 24.3%, 1.159, 1.163, 1.161, 1.154 29.9%, 1.129 32.8%,
        1.051 39.6%, 1.017 43.1%, 0.991, 0.977 51%, 0.974 53.8%, 0.975 57.1%,
        0.997 69.8%, 1.003 76.9%, 1.004 83.8%, 1
    );
 
    /* 弹性缓动曲线 */
    --elastic-easing: linear(
        0, 0.218 2.1%, 0.862 6.5%, 1.114, 1.296 10.7%, 1.346, 1.37 12.9%, 1.373,
        1.364 14.5%, 1.315 16.2%, 1.032 21.8%, 0.941 24%, 0.891 25.9%, 0.877,
        0.869 27.8%, 0.87, 0.882 30.7%, 0.907 32.4%, 0.981 36.4%, 1.012 38.3%, 1.036,
        1.046 42.7% 44.1%, 1.042 45.7%, 0.996 53.3%, 0.988, 0.984 57.5%, 0.985 60.7%,
        1.001 68.1%, 1.006 72.2%, 0.998 86.7%, 1
    );
}

以下是它的实际效果:

Demo 地址:https://codepen.io/airen/full/yLZLYmx

我不认为我会亲自手写其中一个。你可以使用 @Jake Archibald 创建的 linear() 缓动曲线的生成器。该生成器可以将 JavaScript 或 SVG 缓动曲线转换为 linear() 函数的缓动曲线。

URL:https://linear-easing-generator.netlify.app/

特别声明,CSS 的 linear() 函数是 CSS 的新特性之一,在这节课中无法覆盖到它所有细节,本系列后面有一个章节会详细阐述它。

在 CSS 中,除了上述时间函数之外,还有一个小众的时间函数,那就是 steps() 时间函数,它是一个“步进函数”。它可以将动画分解为步骤,而不是我们通常所熟悉的渐进式运动。例如,如果我们想在 4s 内分四步将一个元素从左向右移动 400px ,那么元素将每秒向右跳跃 100px ,而不是以连续的运动动画。

CSS
.animated {
    transition: translate 4s steps(4);
    
    &:hover {
        translate: 400px 0;
    }
}

Demo 地址:https://codepen.io/airen/full/abXbdQN

正如你所看到的,当你的鼠标移出 .animated 元素之后,它同样会分四步回到初始位置。

steps() 函数可以接受两个参数,例如:

CSS
.animated {
    transition: translate 4s steps(4, end);
    
    &:hover {
        translate: 400px 0;
    }
}

第一个参数指定的是步数;第二个参数指定的是方向,其值可以是 jump-startjump-endjump-nonejump-bothstartend ,其中 startjump-start 表现行为一样,同样的 endjump-end 表现行为一样。

为了确保本概述真正全面,steps() 函数还有两个预定义的关键字:step-startstep-end。前者等同于 steps(1, start),而后者等同于 steps(1, end)

你可能不经常需要以 steps() 时间函数的方式动画化移动一个元素,但 steps() 时间函数还有很多其他很酷的用途。例如,如果你拥有一个基本动画的所有精灵图,你可以使用这种技巧逐帧播放它。例如下面这个示例:

Demo 地址:https://codepen.io/airen/full/XWOWdjN

这是由 16 个不同动作的圣诞老人拼接成的一张雪碧图,使用 steps()函数,背景图像可以在 16 个步骤中向左移动,确保每个帧都按照预期出现和消失。

CSS
.animated {
    transition: scale 0.2s ease;

    &::before {
        content: "";
        display: block;
        background: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/183204/char.png)
        no-repeat;
        background-position: 0 0;
        width: calc(2400px / 16); /* 背景图宽度除于帧数 */
        height: 150px;
    }

    &:hover {
        scale: 1.5;
        &::before {
            animation: walk 1s steps(16) infinite;
        }
    }


    @keyframes walk {
        to {
            background-position: -2400px 0;
        }
    }
}

注意,你可能已经发现了,该示例是在 animation-timing-function 中使用 steps() 函数,它实际上是一个帧动画。过渡动画实现出来的效果差强人意,因此这里换成帧动画向大家展示 steps() 时间函数的用途。有关于该函数更详细的阐述,将在分段动画制作中介绍。

我们花了较长的篇幅向大家阐述了过渡动画中的时间函数。主要是因为,时间函数对于动画太重要了。事实上,有关于动画中的时间函数还有很多内容需要向大家阐述,但碍于篇幅的限制,我将在后续章节还会分多个章节向大家介绍这方面的知识。在这里,你只需要有这方面的概念即可。

到此为止,我们介绍了 CSS 过渡动画中三个主要属性,即:

  • transition-property :监听需要动画的 CSS 属性
  • transition-duration :设置过渡动画所需要的时间
  • transition-timing-function :指定过渡动画的速度,即动画运动的方式

除此之外,制作过渡动画还有一个属性与时间有关,那就是 transition-delay 属性。如果你足够仔细的话,前面所展示的示例中,已经有 transition-delay 属性的身影。

使用 transition-delay 属性可指定过渡的开始时间。如果未指定 transition-duration,过渡动画将立即开始,因为默认值为 0s。此属性对于交错过渡非常有用,可以通过为组中的每个后续元素设置较长的 transition-delay 来实现。

CSS
@layer transition {
    li {
        opacity: 0;
        transform: scale(0.6) translateY(-8px) translateX(-8px);
        transition: 
            opacity 350ms calc(var(--transition-order) * 50ms) ease-in-out,
            transform 350ms calc(var(--transition-order) * 50ms) ease-in-out;
    }
    
    button:hover {
        + ul li {
            opacity: 1;
            transform: scale(1) translateY(0) translateX(0);
        }
    }
}

Demo 地址:https://codepen.io/airen/full/NWoWpJe

结合 CSS 的其他特性,例如 mask@property 等,你可以制作出更有创意的过渡动画效果:

Demo 地址:https://codepen.io/airen/full/ZEwEKQb

注意,上例这样的动画效果,采用 SCSS 编写会比 CSS 更省事些,具体原因不在这里阐述。上例关键代码如下:

SCSS
$x: 20;    /*  number of row */
$y: 20;   /*  number of columns */
$s: 0.8;  /* speed factor */
$o: 1.4;  /* overlapping factor*/

@for $i from 0 through ($x - 1) {
    @for $j from 0 through ($y - 1) {
        @property --c-#{$i}-#{$j} {
            syntax: "<number>";
            initial-value: 1;
            inherits: false;
        }
    }
}

@function transition($i, $j) {
    //@return (($s*($i+$j))/($x+$y))+s (($s*($i+$j))/($x+$y))+s;   /* diagonal */
    //@return (($s*$i)/$x)+s (($s*$j)/$y)+s;                       /* left to right */
    //@return (($s*$j)/$y)+s (($s*$i)/$x)+s;                        /* top to bottom */
    //@return  ($s*random())+s (($s*$j)/$y)+s;                      /* top to bottom random */
    //@return  ($s*random())+s (($s*$i)/$y)+s;                      /* left to right random */
    
    @return ($s * random()) + s(($s * ($i + $j))/ ($x + $y)) + s;   /* diagonal random */
    //@return ($s*random())+s ($s*random())+s;                      /* full random*/
}

@function gradient($c) {
    //@return linear-gradient(rgba(0,0,0,var($c)) 0 0);
    //@return linear-gradient(0deg,white calc(100%*var(#{$c})),transparent 0);
  
    @return radial-gradient(
        farthest-side,
        white calc(100% * var(#{$c})),
        transparent 0
    );
  
    //@return conic-gradient(white calc(100%*var(#{$c})),transparent 0);
}

@mixin image() {
    $all_t: ();
    $all_m: ();
    
    @for $i from 0 through ($x - 1) {
        @for $j from 0 through ($y - 1) {
            $all_t: append($all_t, --c-#{$i}-#{$j} transition($i, $j), comma);
            $all_m: append(
            $all_m,
                gradient(--c-#{$i}-#{$j})
                  calc(#{$i}* 100% / (#{$x} - 1))
                  calc(#{$j}* 100% / (#{$y} - 1)),
                comma
            );
        }
    }
    
    transition: $all_t;
    -webkit-mask: $all_m;
    mask: $all_m;
}

img {
    @include image();
  
    -webkit-mask-size: calc(#{$o}* 100% /#{$x}) calc(#{$o}* 100% /#{$y});
    -webkit-mask-repeat: no-repeat;
    mask-size: calc(#{$o}* 100% /#{$x}) calc(#{$o}* 100% /#{$y});
    mask-repeat: no-repeat;
}

img:hover {
    @for $i from 0 through ($x - 1) {
        @for $j from 0 through ($y - 1) {
            --c-#{$i}-#{$j}: 0;
        }
    }
}

哪些属性可以过渡,哪些不能过渡?

一般来说,只能过渡在其起始状态和最终状态之间具有“中间状态”的属性。例如,不可能为 font-family 添加过渡动画效果,因为浏览器不清楚 serifmonospace 之间的“中间状态”应该是什么样的。另一方面,你可以为 font-size 添加过渡效果,因为它的单位是可插值的长度。

以下是一些你可以过渡的常见属性。

  • 变换属性:CSS 的 transform 属性通常可用于过渡动画,因为它是 GPU 加速的属性,可以实现更流畅的动画,同时消耗更少的电量。通过此属性,你可以随意缩放、旋转、平移或倾斜元素。
  • 颜色属性:在互动之前、期间和之后,颜色可以很好地反映状态。例如,将鼠标悬停在按钮上时,按钮可能会更改颜色。这种颜色变化可向用户提供按钮可点击的反馈。colorbackground-colorborder-color 属性只是可以在互动时颜色的几个位置。
  • 阴影属性:阴影经常通过过渡来指示深度变化,例如从用户焦点开始。
  • 滤镜属性filter 是一个功能强大的 CSS 属性,可让你即时添加图形效果。在不同 filter 状态之间转换可以产生一些相当令人印象深刻的结果。
  • 混合模式属性:CSS 混合模式(mix-blend-modebackground-blend-mode)可以与过渡动画(transition)一起使用,以创建一些有趣的视觉效果。混合模式允许你控制元素的背景与其下方元素的交互方式,以产生各种混合效果。
  • 长度和尺寸属性:长度和尺寸属性通常可以用于过渡动画,允许你平滑地过渡元素的大小、位置、间距等属性。
  • 透明度属性:透明度属性(opacity)通常可以用于过渡动画,允许你平滑地改变元素的可见性。你可以通过过渡透明度属性来实现元素的淡入和淡出效果,或在用户交互时使元素逐渐出现或消失。
  • 位置属性:位置属性(toprightbottomleft 等)通常也可以用于过渡动画,以平滑地移动元素的位置。可用于创建各种动态效果,例如滑动菜单、平滑滚动到顶部按钮、拖拽元素等。通过控制元素的位置属性,你可以使元素在用户交互时以平滑的方式改变位置,增强网页的交互性和可视吸引力。
  • 剪切和蒙板属性:剪切(Clipping)和蒙板(Masking)属性可以用于过渡动画,以创建各种有趣的遮罩和剪切效果。这些属性可以让你改变元素的可见部分,以创建过渡动画效果。剪切属性,如clip-path,可以用于定义元素的可见区域,使元素只显示指定区域内的内容。你可以在过渡中改变这些属性,以实现从一个剪切区域到另一个的平滑过渡。这可以用于创建切换效果、元素的逐渐显示或隐藏,以及其他剪切动画。蒙板属性,如mask-imagemask-size,允许你使用蒙板图像来控制元素的可见性。你可以通过更改蒙板图像或其尺寸来实现过渡动画,从而创建从一个图案到另一个的平滑过渡。
  • 背景属性:背景中的 background-positionbackground-size 也可以用于过渡动画,只是相对而言要更少一些

过渡动画的触发器

CSS 过渡动画关键点就是必须包含两个状态,而且在这两个状态同一属性在样式上发生了变化。当然,如果希望 CSS 动画播放,我们还是需要相关的触发方式,即过渡动画触发器。

简单地说,你的 CSS 必须包含状态更改事件和可触发状态更改的事件,以便启用 CSS 过渡。过渡动画常见的触发方式有:

  • 悬停触发:鼠标悬停在元素上时触发过渡,这通常使用 :hover 伪类选择器来实现
  • 焦点触发:在元素获得或失去焦点时触发过渡,通常使用 :focus:focus-within:focus-visible 等伪类选择器来实现
  • 锚点链接触发:页面内部的锚链接被点击时触发过渡,通常使用 :target 伪类选择器来实现。使用 :target 伪类可以为单页应用或滚动到页面内特定部分的情况添加平滑的过渡效果
  • 激活状态触发:用户点击并按住元素(例如链接或按钮)时触发过渡,通常使用 :active 伪类选择器来激活元素状态
  • 表单状态触发:表单状态伪类选择器用于选择表单元素(如输入字段、复选框、单选按钮等)的不同状态,如 :checked:valid:invalid:required 等状态。你可以使用这些状态伪类选择器来触发过渡动画,以改善用户体验
  • 与 JavaScript 不同的 class 变化:当元素的 class 通过 JavaScript 发生更改时,CSS 会过渡已更改的符合条件的属性。

进入或退出时的不同过渡

在 CSS 过渡中,你可以为进入(Entering)和退出(Exiting)不同的状态定义不同的过渡效果。这通常用于处理进入和退出动画,例如当元素在页面加载时进入或在用户操作后退出。

在原生 CSS 中,通常会使用 :hover(鼠标悬停状态)和 :active(元素激活状态)等伪类选择器来创建进入和退出动画效果。另外,你可以使用 JavaScript 来添加或移除 CSS 类,以触发不同的过渡效果。

以下是一个示例,演示如何使用 CSS 和 :hover 伪类选择器创建不同的进入和退出过渡效果:

CSS
.bubble {
    /* 当不悬停时,此过渡被触发 */
    transition: all 5s ease-in 200ms;
    
    :is(&:hover, &:focus) {
        /* 在悬停时,此过渡被触发 */
        transition: transform 150ms cubic-bezier(0.5, 1.75, 0.75, 1.25);
    }
}

Demo 地址:https://codepen.io/airen/full/PoVomXr

离散属性的平滑过渡效果

离散属性是指属性值之间的过渡是不连续的,中间没有中间状态或过渡过程。这意味着属性的值只有一组有限的离散选项,而不是连续的范围。在 CSS 中,display 属性就是一个经典的离散属性,因为它的值通常是离散的,如 blockinlinenonegridflex ,而没有中间的过渡状态。

与之相反的是连续属性,它们的值可以在一个范围内连续变化,而不仅仅是一组离散的选项。例如,widthheight 属性是连续属性,因为它们的值可以在像素级别连续变化,而不仅仅是一些预定义的选项。

在 CSS 过渡动画中,离散属性通常需要特殊处理,因为它们的值不能平滑地过渡,而是立即从一个状态切换到另一个状态。比如我们提示框的显示和隐藏的过渡效果。如果你仅是使用 display 属性的 blocknone 对提示框入场和退出添加过渡效果,最终效果会令你感到失望。

CSS
@layer transition {
    .tooltip {
        display: none;
        transition: all 0.3s ease-in;
    
        .icon:hover & {
            display: block;
            transition: all 0.3s ease-out;
        }
    }
}

Demo 地址:https://codepen.io/airen/full/MWLWowo

在以往,如果你需要控制元素的显示(入场)和隐藏(退场),通常会使用 opacityvisibility 属性来创建淡入淡出的效果,而不是 display 属性:

CSS
@layer transition {
    .tooltip {
        opacity: 0;
        visibility: hidden;
        transition: all 0.5s ease-in;
    
        .icon:hover & {
            opacity: 1;
            visibility: visible;
            transition: all 0.8s ease-out;
        }
    }
}

Demo 地址:https://codepen.io/airen/full/gOqORPO

最近的 CSS 规范引入了 transition-behavior 属性的 allow-discrete 值,允许对离散属性进行一些平滑的过渡。这使得在离散属性上应用过渡变得更加灵活。这意味着,使用 allow-discrete 模式,你可以实现对离散属性的平滑过渡,而不是突然切换。这对于一些需要在离散属性上应用过渡的情况非常有用,使得在这些属性上创建更加流畅的动画效果成为可能。

CSS
@layer transition {
    .card {
        transition: opacity 0.25s, display 0.25s;
        transition-behavior: allow-discrete;
    }

    .fade-out {
        opacity: 0;
        display: none;
    }
}

Demo 地址:https://codepen.io/airen/full/xxMxrgy

transition-behavior 属性值 allow-discrete 也可以在 transition 属性的缩写形式中使用,因此你可以忽略单独的属性设置,并在 transition 缩写的末尾为每个需要过渡的属性使用 allow-discrete 关键字。

例如,如果你要为 .card 类的元素设置淡出效果,同时对 opacitydisplay 两个离散属性进行过渡,你可以这样做:

CSS
.card {
    transition: opacity 0.5s, display 0.5s allow-discrete;

    &.fade-out {
        opacity: 0;
        display: none;
    }
}

如果你需要为多个离散属性进行过渡,你需要在每个属性后面使用 allow-discrete,如下所示:

CSS
.card {
    transition: 
        opacity 0.5s, 
        display 0.5s allow-discrete, 
        overlay 0.5s allow-discrete;
    
    &.fade-out {
        opacity: 0;
        display: none;
    }
}

此外,如果你想在整个项目中启用离散属性的过渡动画,你可以像下面这样使用:

CSS
* {
    transition-behavior: allow-discrete;
}

不过,确保将这个设置放在你的 CSS 文件的末尾,以防止与 transition 缩写的优先级冲突。

上面示例展示的是退场过渡动效。如果要创建入场动画,比如前面的提示框示例,你需要使用 @starting-style 规则。

使用 @starting-style 来应用一个样式,使浏览器可以在元素在页面上打开之前查找它。这是“打开之前”的状态(从这个状态中你进行动画效果的进入)。例如:

CSS
/* 0. BEFORE-OPEN STATE (Transition 的起始状态) */
@starting-style {
    .tooltip {
        opacity: 0;
        display: none;
    }
}

提示框在打开之前的状态设置了 display: noneopacity: 0 。接下来,你可以定义元素在打开状态时的样式:

CSS
.icon:hover .tooltip {
    display: block;
    opacity: 1;
    transition: opacity 0.5s, display 0.5s allow-discrete;
}

最后,你还可以定义元素在退出状态时的样式:

CSS
.icon:not(:hover) .tooltip {
    display: none;
    opacity: 0;
    transition:  display 1s allow-discrete;
}

最终 CSS 代码如下:

CSS
@layer transition {
    @starting-style {
        .tooltip {
            display: none;
            opacity: 0;
        }
    }
  
    .icon:hover .tooltip {
        display: block;
        opacity: 1;
        transition:opacity 0.5s ease-in,  display .5s allow-discrete;
    }
  
    .icon:not(:hover) .tooltip {
        display: none;
        opacity: 0;
        transition:opacity 1s ease-out, display 1s allow-discrete;
    }
}

Demo 地址:https://codepen.io/airen/full/vYbYZJB

如果你觉得提示框这个示例还不太过瘾,你还可以看一下由 @Una Kravets 提供的 ToDoList 组件 ,同时拥有了列表项的进入和退出状态。

Demo 地址:https://codepen.io/airen/full/dyayRex (来源于 @Una Kravets

首先使用 @starting-style 规则来定义“打开之前”的状态:

CSS
/*  0. BEFORE-OPEN STATE: Transition的起始状态 */
@starting-style {
    .item {
        opacity: 0;
        height: 0;
    }
}

在这个示例中,.item 类的元素在“打开之前”的状态下设置了 opacityheight

接下来,你可以定义元素在打开状态时的样式:

CSS
/*  1. IS-OPEN STATE :元素打开后的状态 + 过渡效果 */
.item {
    height: 3rem;
    display: grid;
    overflow: hidden;
    transition: opacity 0.5s, transform 0.5s, height 0.5s, display 0.5s allow-discrete;
}

最后,你还可以定义元素在退出状态时的样式,以及删除元素之前的状态:

CSS
/*  2. EXITING STATE:在元素被删除之前,将其设置为退出状态,包括高度、透明度和变换,使元素倾斜并移动到左侧,然后将其设置为 display: none */
.is-deleting {
    opacity: 0;
    height: 0;
    display: none;
    transform: skewX(50deg) translateX(-25vw);
}

过渡动画的最佳实践

到目前为止,我想你对 CSS 过渡动画有了一定的了解,也可以使用它来制作过渡动画效果。接下来,我将通过真实的案例向大家阐述,应该如何使用 transition 才能制作出体验更好的动画效果。

基于交互的过渡动画

交互性是设计中的重要元素之一。通过过渡动画,我们可以在用户与产品进行交互时增加吸引力和可理解性。这就是基于交互的过渡动画的价值所在。

考虑以下场景:用户鼠标悬停在一个按钮或链接上,按钮会有一个发光、向上位移和放大的效果:

Demo 地址:https://codepen.io/airen/full/mdvdMzq

示例中,我们有一个“对称”的过渡动画,即进入动画(进场)和退出动画(退场),同时给它们设置了不一样的动画持续时间,进入动画的持续时间是 150ms ,退出动画的持续时间是 450ms 。对于悬停动画,我喜欢使用进入动画快速而敏捷,而退出动画可以更加放松和慢悠悠一些:

CSS
button {
    --transition: 450ms;
    transition: 
        box-shadow var(--transition), 
        scale var(--transition),
        translate var(--transition), 
        background var(--transition);
        
    :is(&:hover, &:focus-visible) {
        --transition: 125ms;
    }
}

这是一个常见的示例,Web 页面上的模态框、对话框和提示框等也是如此。对于模态框,它们可以以缓慢的 ease-in 动画进入,然后以更快的 ease-out 动画退出:

CSS
button {
    transition: scale 250ms ease;
        
    &:hover {
        transition: scale 150ms ease;
    }
}
  
.modal {
    transition: 
      translate 250ms cubic-bezier(0.75, 0, 1, 1),
      opacity 250ms ease-in-out;
    
    &.open {
        transition: 
            translate 500ms cubic-bezier(0.215, 0.61, 0.355, 1), 
            opacity 500ms ease-in-out;
    }
}

Demo 地址: https://codepen.io/airen/full/GRzRMpG

这可能只是一个小细节,但它反映了一个更大的思想。

大多数 Web 开发人员通常会基于元素的状态来思考。比如,按钮有一个“默认”状态和一个“悬停”状态。其实,我们可以换个角度来思考。可以基于用户与界面的交互来思考,思考事件,而不是状态。换句话说,就是基于用户的操作进行动画,例如,我们有一个鼠标进入动画(可能是“悬停”状态,也可能是“按下”状态)和一个鼠标离开动画(可能是“默认”状态)。

另外,这两个示例再次向大家阐述了,时间和速度对于一个过渡动效果的重要性,好比模态框的出现,更希望它是以缓慢的速度(ease-in)进入,以快速的方式消失(ease-out)。除了通过时间函数来控制过渡动画的速度之外,我们还可以通过过渡时间来控制,比如按钮的悬停动画,进入动画快速(过渡动画持续时间为 150ms),退出动画缓慢一些(过渡动画持续时间为 450ms)。

不管怎么说,我们在设计或制作一个过渡动画时,都应该将其效果与自然界的物体运动结合起来。只有符合更符合(或趋向于)自然界物体运动规律的动画效果才显得自然和流畅,才能给用户带来更好的体验。我们可以基于迪士尼的 12 大动画原则来设计基于交互动作或交互事件的过渡动画,这样设计出来的过渡动画效果才能更符合自然界物体的运动。

淡入淡出与不透明度

在 Web 开发中,淡入淡出的交互效果是很常见的,可以用于许多不同的场景,以增加用户体验和提供更流畅的页面过渡。以下是一些适合使用淡入淡出过渡动画效果的常见场景:

  • 导航菜单:在网站或应用中,当用户悬停鼠标在导航菜单上时,可以使用淡入动画使下拉菜单平滑显示,而在鼠标离开时使用淡出动画隐藏菜单。
  • 弹出窗口:当用户点击按钮或链接时,可以使用淡入效果来呈现弹出窗口或模态框。淡出效果可用于关闭或隐藏这些窗口。
  • 图像幻灯片:在图像幻灯片或画廊中,使用淡入淡出效果可以实现平滑的图像切换,使过渡更具吸引力。
  • 文本内容:当加载或展示页面内容时,可以使用淡入效果来逐渐显示文本段落、标题和图像。这有助于用户逐渐关注页面内容,而不是立即呈现所有内容。
  • 页面过渡:当用户从一个页面过渡到另一个页面时,可以使用淡出效果淡出当前页面,然后使用淡入效果淡入下一个页面,从而创造更流畅的页面切换。

不管是哪种场景,它们有一个共同的特性,淡入用于进场动画(元素从不可见到可见这个过程),淡出用于退场动画(元素从可见到不可见这个过程)。

使用 CSS 的透明度属性(opacity)制作淡入淡出过渡动画是一项最基本的原则。在这种动画中,元素的透明度值从不可见(0%)逐渐增加到可见(100%)以实现淡入效果;或从可见逐渐减小到不可见以实现淡出效果。为了能使淡入淡出过渡动画效果更贴合实际,在淡入的时候可以考虑使用 ease-in 时间函数,淡出则使用 ease-out 时间函数。

CSS
@layer transition {
    figure {
        & img {
            will-change: transform;
            opacity: 0;
          
            /* 淡出 */
            transition: opacity 1s ease-out;          
        }
        
        &.toggle img{
            opacity: 1;
            
            /* 淡入 */
            transition: opacity 1s ease-in;
        }
    }
}

Demo 地址:https://codepen.io/airen/full/jOdOadY

对于像下面这种淡入淡出效果,消息框不断的淡入淡出, 除了使用 opacity 实现淡入淡出过渡效果之外,在过渡函数上应该选择 ease-in-out 更适合一些,因为 ease-in-out 是遵循渐快渐慢的动画设计原则:

CSS
.message {
    &.fadein {
        opacity: 1;
        translate: 0 0;
        transition: opacity 500ms ease-in-out, translate 500ms ease-in-out;
    }
    &.fadeout {
        opacity: 0;
        translate: 1.5rem 0;
        transition: opacity 500ms ease-in-out, translate 500ms ease-in-out;
    }
}

Demo 地址:https://codepen.io/airen/full/YzBzYwq

使用缩放增添生动性

缩放引入了一种动态、生动和有方向性的元素到过渡中。例如,当切换到下一个屏幕时,你可以将现有的组件和层从 100% 缩小到 90%,并伴随淡出效果。例如,在显示覆盖层时,你可以将当前屏幕缩小。这会给人一种覆盖层从较高的层滑动出来的印象,突显了各个状态之间的连贯性。你可以根据需要上下调整缩放值。通常,缩放与淡入淡出一起使用。

在 CSS 中,我们可以通过 CSS 的 transform 属性中的 scale()scaleX()scaleY()scale3d() 等缩放函数或 CSS 的 scale (缩放)属性来缩放一个元素。例如:

CSS
@layer transition {
  figure {
    
    & img {
        transition: all 500ms ease-in-out;
        opacity: 0.5;
        scale: 1;
    }
      
    &:hover img {
        opacity: 1;
        scale: 1.3;
    }
}

Demo 地址:https://codepen.io/airen/full/XWOWVLm

结合 CSS 的 Clipping 和 Masking 以及 3D 变换相关的特性,还可以制作带有视差效果的过渡效果:

Demo 地址:https://codepen.io/airen/full/RwvwQwQ

具体代码不在这里展示,感兴趣的同学请查阅 Demo 源码。

保持一致的方向性

产品中的任何移动、缩放或动作本质上都向用户暗示了一个方向。我们在设计或制作过渡动画时,保持一致的方向是一个重要原则,可以提高用户界面的连贯性和可理解性。以下是一些有关于过渡动画保持一致方向的基本原则:

  • 定义清晰的方向:在设计过渡动画之前,明确指定动画的方向。这意味着决定元素或内容是向上、向下、向左还是向右移动,或者是从哪个方向出现或消失。
  • 文档化动画方向:为了确保整个团队了解和遵循动画的方向,将动画的方向信息记录在文档中。这包括元素的起始和结束位置,方向的参数等。
  • 一致性约定:在项目中建立一致性约定,确保所有的过渡动画都遵循相同的方向性规则。这可以包括使用一致的动画曲线或缓动函数。
  • 考虑上下文:确保过渡动画的方向与用户界面的整体布局和设计一致。方向应该符合用户的直觉和理解。
  • 用户可理解性:过渡动画的方向应该是用户易于理解的。例如,向上通常表示打开或增加,向下通常表示关闭或减少。
  • 避免混淆:避免过多的方向性变化,以保持一致性。过多的不一致方向性可能会让用户感到困惑。

通过遵循这些原则,你可以设计具有一致方向性的过渡动画,提高用户体验,同时保持界面的整体一致性和流畅性。例如,下图演示了在打开菜单或进行其他相关的过渡时,如何隐含地传达应用的整体方向。左侧的过渡效果中,打开菜单意味着用户可以在垂直方向浏览,右则菜单从左到右出现,表示用户可以水平方向浏览:

速度很重要

在本系列多个不同场合中都提到,正确的动画速度对于动画效果是有多重要,即正确的动画速度提供了实际的反馈和有意义的体验。这个原则同样也适用于过渡动画。过渡速度过慢可能会让用户感到乏味,而过快则缺乏意义。这意味着,应该给过渡动画一个平衡速度(速度的平衡点,既不能过快,也不能过慢)。在设计过渡动画时,你可以根据下面这几个关键因素来设计过渡动画的速度:

  • 反馈时间:过渡动画的速度应该提供足够的反馈时间,以便用户理解发生了什么变化。速度过快可能会让用户感到困惑,而速度过慢可能会让用户感到无聊。
  • 自然感:过渡动画的速度应该模拟自然运动的速度。例如,元素的移动速度应该与真实世界中的物体移动速度相符,以使过渡更加真实和易于理解。
  • 任务复杂性:过渡动画的速度应该根据任务的复杂性而变化。简单任务的过渡可以更快,而复杂任务可能需要更长的时间来为用户提供足够的信息。
  • 设备性能:考虑用户使用的设备性能。在较慢的设备上,过快的动画可能会出现卡顿或不流畅的情况,因此需要调整速度以适应不同设备。
  • 品牌和用户体验:过渡动画的速度应与品牌和用户体验目标一致。一些品牌可能希望速度更快,以传达活力和现代感,而其他品牌可能更喜欢慢速的、沉稳的过渡。

根据这些因素,你可以选择适当的速度来平衡过渡动画,以提供更好的用户体验。通常,100ms ~ 500ms 之间的速度范围适用于大多数情况,但最终的决策应根据你的产品和受众来做出。例如下面这个过渡效果,左侧按钮设置的过渡时间是 100ms ,而右侧按钮设置的过渡时间是 1000ms ,很明显左侧按钮能给你带来一个更好的体验,右内里按钮实在是让人等的有点不耐烦:

CSS
.button {
    transition: all 100ms linear;
    
    &:nth-child(2) {
        transition-duration: 1000ms;
    }
}

Demo 地址:https://codepen.io/airen/full/jOXoGZY

优先、排序和分组

有的时候你可能面临同一个场景中有多个元素需要有过渡动画的效果。对于这样的需求,屏幕上的所有元素不能同时进行动画,否则会让界面感到繁忙和复杂。我们应该按重要性对它们进行排序,将相似的项目分组,然后对这些组进行排名。不相关的组可以完全隐藏,以保持对关键部分的关注。例如,次要元素不如主要元素重要,可以紧随其后。这样设计过渡动画,可以帮助用户更好的理解和专注于关键元素。

你可以根据以下步骤来设计你的过渡动画:

  • 优先级:确定动画元素的优先级非常重要。这意味着要识别哪些元素对用户来说最重要,以便在过渡中使其突出显示。通常,关键的元素应该首先出现在动画中,以吸引用户的注意力。次要元素可以在后续动画中添加。
  • 排序:一旦确定了优先级,接下来需要对元素进行排序,以确保它们以适当的顺序出现。通常,你会希望元素按照其在用户界面中的逻辑关系和布局顺序进行排序。这样可以确保动画的流畅性和连贯性。
  • 分组:将相似的元素分组在一起是一种有用的方式,以便在动画中统一处理它们。这可以帮助减少混乱,使动画更具结构。例如,按钮可以成组处理,文本字段可以成组处理,以便它们可以按照其在界面中的分组出现。
  • 隐藏不相关元素:不相关的元素在过渡动画中可能会分散用户的注意力。如果某些元素在特定状态下不相关,可以考虑在过渡时将它们隐藏,以保持用户对关键元素的关注。

通过这几步,你可以创建有意义、流畅且易于理解的过渡动画,提高用户体验并减少混乱。

创建空间感

在过渡动画中创建空间感可以增强用户体验并使过渡更具吸引力。以下是一些方法,可以在过渡动画中建立空间感:

  • 层次结构:使用不同的层次结构元素,如卡片、面板或图层,以在动画中引入深度。这些元素可以相互叠加或以三维方式移动,使用户感到界面中存在空间感。
  • 立体效果:通过使用阴影、透视和立体效果,可以让元素在过渡中显得更加立体和有深度。这可以使用户感受到元素之间的相对位置和距离。
  • 移动方向:在过渡动画中,通过元素的移动方向和速度来创建空间感。元素可以在不同的方向上移动,以模拟它们在三维空间中的位置。
  • 3D 透视:使用透视效果,让元素在过渡中根据其距离观众的远近而变小或变大。这可以模拟远近感,增加了空间感。
  • 层叠效应:在动画中使用层叠效应,让元素在屏幕上叠加和重叠,以模拟它们在空间中的位置。这可以创建一种分层效果,增加了深度感。
  • 物理模拟:通过模拟物理效应,如弹跳、滚动或惯性,可以使元素在过渡中表现得更像真实世界中的物体。这有助于用户感受到空间感。

通过使用这些方法,你可以在过渡动画中引入空间感,增强用户对界面的理解和感知,提高用户体验。这尤其对于 3D 界面、游戏和虚拟现实应用来说,具有重要意义。

比如下面这样的 3D 按钮,当用户按下按钮时,会给用户带来一种按钮被按钮的视觉感:

Demo 地址:https://codepen.io/airen/full/zYeYMjJ

注意,3D 效果是通过 CSS 的伪元素 ::before 和 box-shadow 来实现的,具体代码请查阅 Demo 源码!

在一些 Web 应用中,你还可以看到带有视差效果的卡片,它主要是通过分层、3D 透视和层叠效应等方式制作的,整个效果是带有一定的 3D 空间感。还有像下面这种 3D 翻转卡片效果:

Demo 地址:https://codepen.io/airen/full/wvNvQOJ

你还可以发挥你的想象力,制作出更具创意的过渡效果。例如下面这个由 @Jhey 提供的这个案例——“选择你的队伍- KCD!🐨”。空间感,喜庆感十足:

Demo 地址:https://codepen.io/airen/full/yLZLQrK

使过渡有意义

在过渡动画中包含足够的相关信息,以清楚地说明状态变化。不要同时动画化太多元素,以防止用户分心。简单地说,在设计过渡动画时,要使过渡有意义,以提供更好的用户体验和传达有效的信息。你可以根据下面这几点来确保过渡动画是有意义的:

  • 明确状态变化:确保动画清楚地传达状态的变化。用户应该能够轻松理解元素是如何从一个状态过渡到另一个状态的。
  • 突出关键元素:如果有多个元素在过渡中发生变化,强调或突出显示最重要的元素。这可以通过动画速度、颜色或尺寸的变化来实现。
  • 避免分散注意力:不要同时过渡太多元素,以防止用户分心。优先考虑最重要的元素,以便用户可以专注于关键信息。
  • 配合上下文:确保过渡动画与应用或界面的上下文相关。动画应该与用户的预期和行为一致,以增强用户理解。
  • 顺序和延时:将元素的动画按照逻辑顺序进行排列,以便用户可以依次理解状态的变化。使用延时来避免过快的动画,使用户有时间理解和反应。
  • 一体化:确保过渡动画与用户界面的整体风格和品牌一致。动画应该感觉像界面的一部分,而不是突兀的插入。

特别强调一下,这些最佳实践规则也同样适用于 CSS 帧动画。

CSS 帧动画

聊完 CSS 过渡动画,我们接着来聊 CSS 的帧动画。在 Web 上创建动画,你几乎无法绕开 CSS 的帧动画(也就是大家常说的 CSS 动画)。

帧动画的独特之处

CSS 帧动画之所以独特,是因为它们就像电影导演。它们决定了确切的方式和时间,Web 上的某些元素随时间的推移发生何种变化。通过动画,你可以以复杂的方式操作多个关键帧上的任意属性。换句话说,CSS 的帧动画允许你通过指定 CSS 属性在不同时间点上的行为来创建动画。这些“时间点”有一个特定的名称,被称为关键帧(Keyframes) 。如果你以前使用过动画工具,关键帧这个词应该听起来很熟悉。在 CSS 中定义的关键帧是制作帧动画所必需的。它相当于你在 Flash、Animate、After Effects 或其他动画工具中可视化定义的关键帧。

如果你以前从未使用过动画工具,也不用担心。接下来,我们将从帧动画的必需物开始,带你更进一步的了解 CSS 帧动画的独特之处。

什么是关键帧?

要知道,CSS 关键帧动画的主要思想是它将在不同的 CSS 代码块之间插值。而这件事情主要是由 CSS 的关键帧 @keyframes 来完成的。它是你在时间轴的每个阶段上指定动画应该做什么的地方。你可以将其视为为 Web 元素编排舞蹈例程。如果用专业的术语来描述的话,CSS 关键帧是用于为时间戳和时间轴分配动画状态的机制

首先,你需要为动画命名(取个描述性的名字,比如 bounceslideInLeft ),然后定义在动画的不同阶段发生的事情。这些阶段(或关键帧)可以定义为总动画持续时间的百分比,从而精确控制事件序列。

我们以“脉搏器”的动画效果为例:

CSS
@keyframes pulse {
    0% {
        opacity: 0;
    }
    50% {
        scale: 1.4;
        opacity: 0.4;
    }
}

通过 @keyframes 定义了一个名为 pulse 的关键帧动画,在这个关键帧动画中,它主要做了两件事情:

  • 第一件事情,它将平滑地将元素的透明度从 0% 过渡到 40% 在过渡到 100%
  • 第二件事情,它将平滑地将元素从初始尺寸放大 1.4 倍,然后再回到初始尺寸

在 CSS 中,你可以将已定义好的关键帧动画(例如 pulse)视为全局变量。它是通用的和可重用的。我们可以使用 animation 属性将它应用于特定的选择器,例如:

CSS
.pulser::after {
    animation: pulse 1000ms cubic-bezier(0.9, 0.7, 0.5, 0.9) infinite;
}

与过渡动画一样,帧动画需要指定持续时间。在这里,我们规范了帧动画的持续时间是 1000ms ,即 animation-duration: 1000ms

Demo 地址:https://codepen.io/airen/full/gOqOqBE

正如你所看到的,整个动画(pulse)持续 1000ms ,并在两种种状态中运行。

其中每种动画状态的开始和结束点都有一个特定时间点。使用关键帧在时间轴上映射这些关系。

同样的,浏览器会在关键帧之间进行插值计算,使得帧与帧之间有更多的状态,动画才更平滑:

现在,你已经知道了什么是关键帧,这些内容应该可以帮助你了解 @keyframes 规则的工作原理。下面这个示例是具有两种状态的基本规则:

CSS
@keyframes fadeIn {
    from {
        opacity: 0;
    }
    
    to {
        opacity: 1;
    }
}

需要注意的是,每个 @keyframes 规则后面都需要一个自定义标识符,这个自定义标识符就是前面所说关键帧规则的名称,也是关键帧动画的名称。关键帧动画的名称是区分大小写的,因此 fadeinfadeIn 分别代表的是两个不同名称的关键帧动画。另外,在某些情况下,有些 CSS 关键词是不能被当作关键帧动画名称的,因此,我个人建议给关键帧动画命名时,尽量不要使用 CSS 中的关键词,比如 inheritinitialautononespanmin-contentmax-content 等。

在关键帧内,fromto 关键词分别表示的是 0%100% ,即关键帧动画的开始和结束位置。当然,你也可以在整个 @keyframes 规则代码块中使用百分比值来代表关键帧在时间轴上的位置,例如:

CSS
@keyframes fadeIn {
    0% {
        opacity: 0;
    }
    
    100% {
        opacity: 1;
    }
}

你也可以将 fromto 和百分比值一起混合使用,但要注意的是,from 始终代表的是 0%to 代表的是 100%

CSS
@keyframes pulse {
    from {
        opacity: 0;
    }
    
    50% {
        opacity: .4;
        scale: 1.4;
    }
    
    to {
        opacity: 1;
        scale: 1;
    }
}

@keyframes 中的百分比指的是动画的进度,你也可以根据动画的需求在里面添加更多的步骤,例如:

CSS
@keyframes spin {
    0% {
        rotate:0turn;
    }
    100% {
        rotate:1turn;
    }
}
  
@keyframes multi-step-spin {
    0% {
        rotate:0turn;
    }
    25% {
        rotate:.25turn;
    }
    50% {
        rotate:.5turn;
    }
    75% {
        rotate:.75turn;
    }
    100% {
        rotate:1turn;
    }
}

同样是旋转一圈的动画,帧动画 spin 分两步完成(下图中蓝色进度条),帧动画 multi-step-spin 则五步完成(下图中草绿色进度条)。

Demo 地址:https://codepen.io/airen/full/MWLWRpz

刚才发生了什么?

在介绍 CSS 过渡动画的时候,我们说过,要让过渡动画动起来,要在 transition 属性上指定两个必要的值:

  • 希望进行动画的属性的名称,例如 transition-property: color
  • 动画的持续时间,例如 transition-duration: 200ms

那么,CSS 的帧动画又需要具备什么样的条件,它才能动起来。是不是只需要使用 @keyframes 定义一个关键帧就可以?这显然是不可能的。

首先要说的是,@keyframes 规则仅仅是定义了全局都可以使用的关键帧,而且在这个关键帧指定了动画的相关系列,即编排好了动画动作。即使如此,要是没有具体对象(需要动画化的元素)通过 animation 引用它,那么它不起任何作用,它仅存的作用,就是躺在你样式表中的一段 CSS 代码。

换句话说,要使 @keyframes 定义好的关键帧动画让元素动起来,也需要像过渡动画一样,通过 animation 来实现。例如前面那个“脉搏器”的动画:

CSS
.pulser::after {
    animation: pulse 1000ms cubic-bezier(0.9, 0.7, 0.5, 0.9) infinite;
}

CSS 的 animation 属性负责控制你的动画。上面代码中的 animation 属性是一个简写 CSS 属性,它相当于你给动画指定了四个值:

  • 动画名称,即 animation-name: pulse
  • 动画持续时间,即 animation-duration: 1000ms
  • 动画时间函数,即 animation-timing-function: cubic-bezier(0.9, 0.7, 0.5, 0.9)
  • 动画播放次数,即 animation-iteration-count: infinite

它的意思是,名为 .pulser 元素的 ::after 伪元素应用了 pulse 动画,该动画播放完一次需要 1000ms ,同时动画运动的速度曲线是 cubic-bezier(0.9, 0.7, 0.5, 0.9),而且该动画会无限次的播放。

CSS 的关键帧动画的启用和过渡动画有所不同,它其实只需要在动画化的元素上使用 animation-name 引用了可匹配的帧动画(也就是 @keyframes 定义好的帧动画)即可。它不会像过渡动画那样,必须要指定动画的持续时间。这意味着,animation-duration 未显式设置(这个时候它是 0s),动画仍会运行,只是播放速度太快了,你无法看到。因此,为了使你的帧动画正常化,最好是通过 animation-duration 属性显式指定动画播放所需的时间。

也就是说,一个 CSS 帧动画,它主要由两个关键部分组成:

  • 动画运动的细节由 @keyframes 来定义
  • 动画播放或执行的细节则由 animation 属性来控制

这两者是紧密相连,没有另一个存在,CSS 帧动画就不会真正起作用。既然如此,我们就有必要来看看 animation 属性和 @keyframes 规则是如何相互关联的。

动画名称

在介绍 @keyframes 时就说过了,紧跟该规则后面的名称只是充当一个标识符。你可以想象成,在 CSS 样式表中做了一个记号,表示有这么一个东西存在。只有当 CSS 的 animation 属性(或它的子属性 animation-name)引用了它,才知道关键帧在哪里:

CSS
@keyframes heartBeat {
    0%, 28%, 70% {
        scale:1;
    }

    14%, 42% {
        scale:1.3;
    }
}

.heartBeat {
    animation: heartBeat 1.3s infinite;
}

这并不是一种巧合, animation 属性引用了 heartBeat,而 @keyframes 规则的名称也是 heartBeat。如果名称不一致,你的动画将无法正常工作。

Demo 地址:https://codepen.io/airen/full/OJdJYgr

持续时间和关键帧

现在我们知道了 animation 属性是如何知道它的关键帧的。这仅解决了帧动画中的一个谜团。接下来,我们要来解决它的另一个谜团,即关键帧中的样式规则何时执行(运动起来)。

@keyframes 规则中定义关键帧样式规则时,我们的选择器并不是一个实际的时间值,而是一个百分比值。例如:

CSS
@keyframes pulse {
    0% {
        scale: 1;
    }

    50% {
        scale: 1.05;
    }

    100% {
        scale: 1;
    }
}

这些值(0%50%100% 等)代表的是动画完成的百分比。例如上面示例中的值,0% 关键帧代表 pulse 动画的开始,50% 关键帧代表 pulse 动画的中间点,100% 关键帧代表 pulse 动画的结束。

在现实生活中,我们考虑动画中发生的事情时,并不会按百分比值来考虑的。我们通常是按时间点来考虑的。为了理解 CSS 中期望的值和现实世界中人类期望的值之间的差异,我们需要了解动画的持续时间值的作用。动画的持续时间值不仅指定了动画的总长度,还有助于指定特定关键帧何时执行。简单地说,动画的持续时间定义了关键帧(@keyframes)时间轴的时长。

假设我们在 CSS 中执行一次 pulse 动画用时为 1000ms

CSS
.animated {
    animation-name: pulse;
    animation-duration: 1000ms;
}

我们可以使用下图来阐述 CSS 中关键帧的百分比值是如何映射到动画时间单位。即 CSS 关键帧(@keyframes)中百分比值与 CSS 的 animation-duration 单位(一般是 sms)之间的关系:

  • 位于关键帧的 0% 位置的 scale: 10ms 后开始生效
  • 位于关键帧的 50% 位置的 scale: 1.05500ms 后开始生效
  • 位于关键帧的 100% 位置的 scale: 11000ms 后结束时生效

正如你所看到的,如果你不希望把事情搞得复杂化,可以这么来理解,关键帧( @keyframes )中的百分比值计算是相对于动画的持续时间( animation-duration )来计算的

有了这个基础之后,不管关键帧有多复杂,你都能知道动画随着时间向前推移时会发生什么事情。拿心跳(heartBeat)动画效果为例:

CSS
@keyframes heartBeat {
    0%, 28%, 70% {
        scale:1;
    }

    14%, 42% {
        scale:1.3;
    }
}

.heartBeat {
    animation: heartBeat 1.3s infinite;
}

heartBeat 关键帧分别是 0%14%28%42%70% 。动画的持续时间为 1.3s 。要弄清楚关键帧 heartBeat 在这个示例中何时生效,你只需要拿出你的计算器,将动画的持续时间值 1.3 乘以关键值的百分比值。很容易就能得出,heartBeat 关键值分别在:

  • 动画播放到 0s (动画开始)时将会执行关键帧 0% 位置的样式,即 scale: 1
  • 动画播放到 0.182s (即 1.3s × 14% = 0.182s )时将会执行关键帧 14% 位置的样式,即 scale: 1.3
  • 动画播放到 0.364s (即 1.3s × 28% = 0.364s)时将会执行关键帧 28% 位置的样式,即 scale: 1
  • 动画播放到 0.546s (即 1.3s × 42% = 0.546s)时将会执行关键帧 42% 位置的样式,即 scale: 1.3
  • 动画播放到 0.910s (即 1.3s × 70% = 0.910s)时将会执行关键帧 70% 位置的样式,即 scale: 1
  • 动画播放到 1.3s (即 1.3s × 100% = 1.3s)时将会执行关键帧 100% 位置的样式,在这个示例中,它将回到 scale 最初值 1 ,不做任何缩放

持续时间和关键帧之间映射关系会使大多数初学 Web 制作者感到困惑。如果上面这两个示例还不能帮你解惑,那么请记住,CSS 关键帧没有时间的概念,它只有一个百分比完成的概念。CSS 的 animation-duration 将创建的关键帧与时间联系在一起。一旦你理解了这一切,你将不再为此感到困惑,甚至帮你解决了帧动画的另一个谜团。

时间函数和延迟播放

CSS 的帧动画和过渡动画一样,可以为动画设置时间函数和延迟动画播放的时间。不同之处是,CSS 帧动画通过 animation-timing-function 指定动画的时间函数(过渡动画是 transition-timing-function);通过 animation-delay 指定动画延迟播放的时间。它们所起的作用都是相似的。

我们一起来看一个动画延迟时间的案例。

Demo 地址:https://codepen.io/airen/full/OJdPJyL

上面示例中,封面上的多个元素都有动画效果,而且每个动画效果出现的时机都是有所不一样的。我们可以通过动画延迟(animation-delay)来达到目标效果:

CSS
@layer animation {
    @keyframes fadeIn {
        from {
            opacity: 0;
        }
        to {
            opacity: 1;
        }
    }

    @keyframes fadeInUp {
        from {
            opacity: 0;
            translate: 0 30px;
        }
        to {
            opacity: 1;
            translate: 0 0;
        }
    }

    .playing {
        & h1 {
            animation: fadeIn 2s ease-in both;
        }
        
        :is(p, .btn) {
            animation: fadeInUp 1s ease-in 0.1s both;
        }
    
        & p {
            animation-delay:0.1s;
        }
    
        .btn--primary {
            animation-delay: 0.7s;
        }
    
        .btn--outline {
            animation-delay: 0.8s;
        }
    }
}

动画循环播放

CSS 关键帧动画默认情况下只会运行一次,但你可以使用 animation-iteration-count 属性来控制它的重复次数。这个属性可以设置为不同的值,以控制动画的循环次数,或者设置为 infinite 来使动画无限循环。

animation-iteration-count 属性的特殊值 infinite 非常适用于创建循环动画,例如下面这个按钮动画效果,允许动画无限循环,直到用户采取某种操作停止它。

CSS
@layer animation {
    @property --r {
        syntax: "<angle>";
        inherits: false;
        initial-value: 0deg;
    }
    
    @keyframes rotating {
        0% {
            --r: 0deg;
        }
        100% {
            --r: 360deg;
        }
    }
    
    button {
        animation: rotating 3s linear infinite;
    
        &:hover {
            animation-play-state: paused;
        }
    }
}

Demo 地址:https://codepen.io/airen/full/abXzyyg

注意,在这个示例中,我们在按钮的悬停状态指定 animation-play-state 属性的值为 paused ,使帧动画在按钮悬停状态下暂停播放。

正如你所看到的,你可以使用 animation-play-state 属性播放和暂停动画。默认值为 running;如果将其设置为 paused,则会暂停动画。

交替动画

交替的动画指的是在动画的每个迭代之间来回切换,通常用于创建连续的往复效果。这意味着动画在正常方向(从 0%100%)和反向(从 100%0%)之间交替播放。这种技术可用于制作各种效果,例如元素的呼吸、闪烁和摆锤等。

在 CSS 中,你可以使用 animation-direction 属性来控制动画的方向。将其设置为 alternate 将启用交替的动画效果。例如下面这个旋转动画:

CSS
@keyframes rotate {
    to {
        rotate: 360deg;
    }
}

.sun {
    animation:rotate 4s linear infinite alternate;
}

Demo 地址:https://codepen.io/airen/full/zYexdeO

在这个示例中,太阳将以 4s 的间隔交替旋转。它们先用 4s 时间顺时针旋转 360deg ,第一次动画播放完之后,它会再用 4s 时间逆时针旋转 360deg 。依此类推,交替旋转。

我们还可以将其设置为reverse。这将使动画以相反的顺序播放,从 100%0%。继续以上面示例为例,不同的是,当用户鼠标悬停在太阳上时,太阳需要逆时针旋转。

Demo 地址:https://codepen.io/airen/full/LYqEOBq

对于上面这个效果,我们只需要在 :hover 状态将 animation-direction 属性的值指定为 reverse 即可:

CSS
@keyframes rotate {
    to {
        rotate: 360deg;
    }
}

.sun {
    animation:rotate 4s linear infinite;
    
    &:hover {
        animation-direction: reverse;
    }
}

reverse 值还适用于下面这样的动画场景,这个加载动画效果有两个摆锤的动画效果,每个摆锤的摆动方式都是一样的,只是动画播放方式相反。

Demo 地址:https://codepen.io/airen/full/gOqbXNP

CSS
@layer animation {
    @keyframes fadeInOut {
        50% {
            opacity: 0.4;
        }
    }
    
    @keyframes rotate {
        to {
            rotate: 360deg;
        }
    }
    
    @keyframes pendulum {
        0%,
        100% {
            rotate: -45deg;
        }
        50% {
            rotate: 45deg;
        }
    }
  
    @keyframes pendulumReverse {
        0%,
        100% {
            rotate: 45deg;
        }
        50% {
            rotate: -45deg;
        }
    }
    
    .pendulum-loader span {
        animation: fadeInOut 1s infinite;
    }

    .pendulum {
        animation: pendulum 2s ease-in-out infinite;
    
        &::after {
            animation: rotate 2s linear infinite;
        }
    
        &:nth-of-type(2) {
            animation-name: pendulumReverse;
            
            &::after {
                animation-direction:reverse;
            }
        }
    }
}

填充模式

CSS 帧动画最令人感到困惑的一点可能是填充模式,它是我们在掌握关键帧时最大的障碍。假设你给一张图片添加了淡出的动画效果。

CSS
@keyframes fadeOut {
    to {
        opacity: 0;
    }
}

.animated {
    animation: fadeOut 1s ease-out;
}

Demo 地址:https://codepen.io/airen/full/LYqEeyY

fadeOut 动画本身运行正常,但在动画结束后,元素突然又出现了,这并不符合我们所期望的动画效果。令人感到困惑的是,为什么元素(示例中的图片)会突然变得完全可见呢?其中原委是这样的。

首先,图片初始状态是完全可见的,因为我们在 CSS 中并没有显式的设置 img 的透明度相关的样式。我们只是在 @keyframes 中的 to 代码中显式指定 opacity 属性的值为 0 ,它的声明只在动画运行时适用。

也就是说,fadeOut 动画经过 1s 之后,动画结束了,那么 fadeOut 关键帧中 to 代码块中声明的 opacity 样式也就消失了,元素将使用其他地方定义的 CSS 声明。由于我们并没有在样式表的其他地方为这个元素指定 opacity 属性的值,因此动画结束之后,它会恢复到默认值 1 。这就是为什么动画结束之后,图片又突然变得完全可见的原因。

解决这个问题的一种方法是在样式表中给动画元素(本例是 img)指定 opacity 属性的值为 0

CSS
@layer animation {
    @keyframes fadeOut {
        from {
            opacity: 1;
        }
        to {
            opacity: 0;
        }
    }

    .playing img {
        animation: fadeOut 1s ease-out;
        opacity: 0;
    } 
}

Demo 地址:https://codepen.io/airen/full/KKJwZoB

fadeOut 动画正在运行时,@keyframes 规则中的 opacity 声明会覆盖样式表中给元素设置的 opacity 值。一旦动画结束,样式表中给元素 (.playing img)指定的 opacity 将生效,保持图片继续不可见。

其实,在 CSS 帧动画中,我们不需要采用类似上面示例的解决方案,可以直接通过 CSS 的 animation-fill-mode 属性来控制。它定义了 @keyframes 时间轴中的哪些值会在动画开始播放之前或结束之后持续保留。

简单地说,animation-fill-mode 定义了关键帧动画在播放之前和之后如何应用样式,旨在用于解决动画结束后元素样式的状态问题。其默认值为 none,表示在动画播放完毕后,@keyframes 规则中声明的样式值将失效,动画结束之后元素会恢复到原始样式状态。除 none 之外,它还有以下几种常见的填充模式:

  • forwards:动画结束后,元素将保持最终关键帧的样式,持续应用于元素。
  • backwards:动画在播放前,元素将应用第一个关键帧的样式。这意味着在动画播放之前,元素将处于动画的初始状态。
  • both:动画结束后,元素将保持最终关键帧的样式,并且在动画播放前也会应用第一个关键帧的样式。

比如上面那个图片淡出的动画效果,将 animation-fill-mode 属性的值指定为 forwards ,可以让元素(img)的 opacity 属性的值保持在 fadeOut 帧动画结束后的值,即 opacity: 0

CSS
.playing img {
    animation: fadeOut 1s ease-out forwards;
}

Demo 地址:

在这里,我们无法涵盖到 animation-fill-mode 属性的全部细节,本系列后续会有单独的一个节课来阐述。因此,你现在不需要过于担心,随着后续课程的学习,你将能彻底掌握它的所有知识和细节。

过渡动画和帧动画相似之处

通过前面课程的学习,我想你对 CSS 的过渡动画和帧动画都有了一定的认知,甚至可以使用它们给 Web 上的元素添加动画效果。在 CSS 中,过渡动画和帧动画有一些相似之处,它们都是用于创建动态效果和动画的技术。它们有一些相似之处:

  • 动态效果:无论是CSS过渡动画还是帧动画,它们都用于创建动态效果,使网页内容更具吸引力和交互性。
  • CSS 属性控制:过渡动画和帧动画都允许你控制 CSS 属性的变化。你可以定义哪些 CSS 属性会随时间而变化,从而实现动画效果。这些属性可以包括位置、颜色、大小、透明度等。
  • 使用CSS规则:无论是过渡动画还是帧动画,它们都是使用 CSS 规则来定义动画效果的。你可以在 CSS 样式表中编写动画规则,指定动画的属性、持续时间、延迟时间、缓动函数等。
  • 时间控制:两者都允许你控制动画的时间。你可以指定动画的持续时间,确定动画从初始状态到结束状态所需的时间。你也可以指定动画延迟播放的时间。
  • 缓动函数:过渡动画和帧动画都支持缓动函数。缓动函数用于定义动画在不同时间点的速度变化,例如加速、减速、匀速等。这增加了动画的视觉吸引力和真实感。
  • 触发方式:两者都可以通过触发事件来启动动画。例如,你可以在鼠标悬停在元素上或在按钮点击时触发动画。这使得你可以根据用户的交互来控制动画的触发。
  • 插值计算:两者都会对动画元素进行视觉更改并自动生成中间值
  • 事件监听: 无论是过渡动画还是帧动画,你可以使用 JavaScript 来监听它们的事件。你可以注册事件处理程序,以便在动画的不同阶段触发事件,例如开始、结束、重复等。
  • 事件处理程序: 你可以使用 JavaScript 来定义事件处理程序,以响应动画事件。这允许你执行自定义操作,如在动画开始时执行某些操作,或在动画结束时触发其他动作。

为了帮助大家更好理解它们相似之处,我用下图来展示过渡动画和帧动画在 CSS 属性上使用的相似之处:

过渡动画与帧动画差异之处

CSS 过渡动画与帧动画两者之间除了上面提到的相似之处,它们之间还存在一些不同之处,尤其是在触发方式、循环、值状态、提前指定属性、插值与 JavaScript 的互动方面。接下来,我们一起来探讨过渡动画和帧动画之间的这些差异。

触发方式

CSS 过渡动画和帧动画之间的一个主要差异就是动画播放的触发方式:

  • 过渡动画:它们在 DOM 元素发生变化时触发或可以被触发。正如前面所述,Web 上的大多数元素具有状态,如 :hover:active:checked 等。使用这些状态,我们可以触发过渡动画。
  • 帧动画:通常情况之下,能触发过渡动画的方式都可以触发帧动画。不同之处是,帧动画可以通过 animation 属性声明,它可以具有无限的生命周期。一旦触发,帧动画可以运行无限次数,除非你在代码中指定了它运行的次数

例如下面这个示例,左侧的是过渡动画,它通过悬停(:hover)来触发,所以你要将鼠标移上去,才能看到动画效果;右侧的是帧动画,文档加载完就触发了它。

CSS
@layer animation {
    @keyframes move {
        100% {
            translate: 50vw 0;
        }
    }
  
    .car {
        /* CSS Animation */
        .animation & {
            animation: move 2s ease-in infinite ;
        }
        
        /* CSS Transition */
        .transition & {
            transition: translate 2s ease-in;
        }
        .transition:hover & {
            translate: 50vw 0;
        }
    }
}

Demo 地址:https://codepen.io/airen/full/qBgEYKd

注意,CSS 动画还可以通过滚动来触发,即 CSS 滚动驱动动效。

循环播放

默认情况下,过渡动画和帧动画都只运行一次。不同的是,CSS 帧动画可能通过设置 animation-iteration-count属性来指定动画循环播放的次数:

CSS
.animated {
    animation-iteration-count: 5;
}

上面代码会告诉浏览器,动画重复播放 5 次,然后再停止。如果你想让动画无限次循环播放,也可以这样做:

CSS
.animated {
    animation-iteration-count: infinite;
}

对于过渡动画而言,它并没有 transition-iteration-count 属性用来设置动画重复播放的次数。当触发时,过渡动画只运行一次。当然,你可以通过调整 transitionEnd 事件来使过渡动画重复播放,但这相对于帧动画而言,要复杂的多。

Demo 地址:https://codepen.io/airen/full/jOdEeOX

状态差异

过渡动画和帧动画最大差异之一就是状态的差异:

  • 过渡动画:过渡动画只有两个状态,动画的开始状态和结束状态。相同属性值可以在这两个值之间进行过渡,例如动画元素的 opacity 属性值,在动画起始状态是 0 ,在动画结束状态是 1 ,模拟一个淡入的动画效果
  • 帧动画:帧动画则不同,它可以有多个状态的值变化。同样以 opacity 为例,它可能通过时间轴从 0%100% 的中间点(或任意你想要的点)进行操作。例如,时间轴的 0% 位置时,opacity0 ;时间轴的 50% 位置时,opacity1 ;时间轴的 100% 位置时,opacity0 ,模拟一个淡入淡出的动画效果

简单地说,CSS 帧动画可以根据动画需求,通过 @keyframes 设置任意多的状态(不仅限于开始和结束状态)。这使你能够创建更具参与性的动画,制作出来的动画更像是一个 Flash 动画。

CSS
@layer animation {
    @keyframes move {
        0% {
            translate: 0 0;
        }
        20% {
            translate: 30vw 0;
        }
        80% {
            translate: 20vw 0;
        }
        100% {
            translate: 50vw 0;
        }
    }

    .car {
        .animation & {
            animation: move 2s ease-in infinite;
        }
    
        .transition & {
            transition: translate 2s ease-in;
        }
    
        .transition:hover & {
            translate: 50vw 0;
        }
    }
}

Demo:https://codepen.io/airen/full/zYexmyN

正如示例所示,在 CSS 过渡动画中,除了最终结果,你几乎不能控制任何东西。换句话说,CSS 过渡动画是线性的,只是从初始状态到最终状态的简单过程。它不能像帧动画那样,根据需要可以在任意点设置不同的状态。对于一些复杂的动画,缺少中间状态是无法实现的。

控制播放方式

为了能给用户提供更多交互性和用户体验的掌控,Web 动画就应该提供一些更友好的操作方式。就好比现实生活中的视频播放一样,提供暂停和重新播放等功能。事实上,过渡动画和帧动画在这方面还是有很大的差异:

  • 过渡动画:过渡动画通常是与状态变化或交互相关的,它只会在属性上从一个初始值到最终值的变化时发生一次。一旦变化完成,属性将保持在最终值,直到再次触发状态变化。这意味着,过渡动画不会在中途停止,也不会保留属性的中间值
  • 帧动画:帧动画通常不依赖于状态或交互,可以根据定义的迭代次数无限循环播放。它有能力在特定时间点停止。这也意味着,它可以在播放过程中暂停,并在特定时间保持属性的中间值。你可以手动控制帧动画的播放,包括暂停、继续和跳转到特定帧。

换句话说,帧动画提供了更多控制,可以在播放过程中操作属性的中间状态,而过渡动画通常只处理属性的起始和结束状态的变化。

CSS
.animation:hover .car{
    animation-play-state: paused;
}

Demo 地址:https://codepen.io/airen/full/NWoPEyN

正如上面示例所呈现的一样,对于过渡动画,只有用户鼠标悬停到元素上,动画才开始播放,当用户鼠标移动元素之后,动画会立即停止,并且回到初始状态。帧动画则不同,用户鼠标悬停到元素上时,动画会暂停播放,用户鼠标移动之后,动画会继续向前播放。就好比我们播放视频一样,你可以暂停视频播放:

总之,过渡动画通常与状态变化相关,播放一次且不能在中途停止,而帧动画可以无限循环播放,可以手动控制播放进度并在播放过程中保持属性的中间值。

动画停止状态的差异

过渡动画和帧动画在动画停止状态下样式控制方面存在一些重要的差异:

  • 过渡动画:过渡动画主要用于在状态变化时创建平滑的过渡效果。通常情况之下,过渡动画完成之后,元素将保持在最终状态(过渡动画的结束状态),但元素无法在停止状态应用不同的样式。例如一个淡出的过渡动画,动画元素初始状态是可见的(即元素的 opacity:1),当鼠标悬停在元素上时,它的 opacity0 。只不过,用户鼠标移出元素时,元素立即回到可见状态(opacity: 1),它无法让元素停留在不可见状态(opacity: 0
  • 帧动画:帧动画允许你通过 animation-fill-mode 属性来指定动画停止状态下元素应用不同的样式。这意味着,你可以在特定帧停止时指定元素的样式。这使得可以实现动画的任何停止状态。
CSS
 @keyframes move {
    0% {
        translate: 0 0;
    }
    
    100% {
        translate: 20vw 0;
    }
}

.car {
    animation: move 2s ease-in both;
}

上面这个示例,帧动画播放结束之后,小汽车会保持 @keyframes 中的最后一帧的位置,即在水平方向距初始位置 20vwtranslate: 20vw 0)不动:

Demo 地址:https://codepen.io/airen/full/rNPaoWq

总的来说,帧动画在样式控制方面更加灵活,可以实现更多高级的动画效果和交互,而过渡动画主要用于简单的状态过渡和变化效果。选择哪种方法取决于你的具体需求以及需要实现的动画效果。如果需要更多的交互和停止状态下的样式控制,帧动画可能更合适。

属性动画化的差异

过渡动画和帧动画之间的另一个差异是属性方面的。

首先以过渡动画为例,假设你想让过渡识别的每个 CSS 属性都必须显式声明。例如:

CSS
.transition { 
    background-color: #CC0000; 
    transition: background-color .5s ease-in; 
    
    &:hover { 
        cursor: pointer; 
        background-color: #000000; 
        width: 500px;
    }
}

上面代码的意思是,.transition 元素在悬停状态(:hover)为 background-colorwidth 属性指定了不同的值。但在过渡(transition)中仅指定了 background-color 。这意味着,你的浏览器只监听了 background-color 属性的改变。

如果我们希望过渡动画同时能监听 background-colorwidth 属性的变化,我们需要显式地为 width 属性添加过渡效果:

CSS
.transition { 
    background-color: #CC0000;
    transition: background-color .5s ease-in, width .5s ease-in;

    &:hover { 
        cursor: pointer; 
        background-color: #000000; 
        width: 500px;
    }
}

当然,你可以将 transition-property 属性的值设置为 all 来简化上面的代码:

CSS
.transition { 
    background-color: #CC0000;
    transition: all .5s ease-in;

    &:hover { 
        cursor: pointer; 
        background-color: #000000; 
        width: 500px;
    }
}

就我个人而言,我不建议这样做。这样做首先可能会影响到 Web 的性能,其次它也很可能造成样式上的冲突。除非你确实有这样的需求,不然我建议单独指定每个要过渡的 CSS 属性会更合理。

对于帧动画来说,它没有这方面的限制,因为你可以在每个关键帧中随意指定 CSS 属性,而且不需要像过渡动画那样声明。例如:

CSS
@keyframes animatedProperty { 
    0% { 
        translate: -150px 0;
    } 
 
    20% { 
        translate: 50px 0; 
        height: 200px;
    } 
 
    80% { 
        translate: 200px 0; 
        height: 300px;
    } 
 
    100% { 
        translate: 600px 0;  
        background-color: #FFFFFF;
    } 
}

简而言之,过渡需要显式指定要过渡的每个属性,而动画允许在关键帧中随意指定属性,而不必像过渡那样明确声明它们。这使动画在某些情况下更加灵活。

与 JavaScript 事件交互的差异

不管是过渡动画还是帧动画,它们都提供了一些 JavaScript 方面的 API。只不过,JavaScript 在处理过渡动画和帧动画时有一些关键的差异:

  • 过渡动画:在过渡动画中,你可以使用 JavaScript 操作过渡的属性有延迟(transition-delay)、持续时间(transition-duration)、属性(transition-property)和缓动函数(transition-timing-function)。另外就是触发方式,你可以通过 JavaScript 来添加和删除 CSS 类名触发过渡动画,或者通过设置元素的内联样式来修改 CSS 属性值。
  • 帧动画:对于帧动画,使用 JavaScript 除了可以操作帧动画的延迟(animation-delay)、持续时间(animation-duration)和缓动函数(animation-timing-function)之外,你还可以操作帧动画的方向(animation-direction)、填充模式(animation-fill-mode)、播放次数(animation-iteration-count)、动画名称(animation-name)和播放状态(animation-play-state)等。通过 JavaScript,你可以根据用户行为动态调整帧动画的属性值,创造丰富的互动体验。

相对而言,JavaScript 在帧动画中提供更多的控制和灵活性,因为它可以操作更多的属性和关键帧。在过渡动画中,JavaScript 主要用于触发和控制整个过渡过程。选择哪种方法取决于你的动画需求和交互性要求。

总的来说,过渡动画和帧动画在不同方面具有不同特点。过渡动画适用于简单的过渡效果,适合与 JavaScript 结合使用以响应用户交互。而帧动画适用于更复杂、高度可控的动画效果,通常不需要与 JavaScript 结合使用。选择使用哪种技术取决于你的项目需求和所要实现的效果。

过渡动画还是帧动画?

阅读到这里,说明你知道如何使用 CSS 的 transitionanimation 来制作过渡动画和帧动画了,并且知晓它们两者之间的异同。即使如此,在实际生产过程中,面对 Web 动画需求的时候,你还是会小小的纠结一下,应该使用过渡动画还是帧动画?或者说,哪种动画更适合?

事实上,CSS 过渡动画和帧动画在不同情境之下都有各自的优势。在总结如何选择过渡动画还是帧动画之前,我先向大家呈现下面这张流程略,因为从图中,你就可以一探究竟,甚至是知道如何选择更为合适。

过渡动画适用于从一个状态快速过渡到另一个状态的简单变化。它是轻量级的,设置要求不高。或者说,当你希望对用户的交互作出响应并实现简单的动画效果时,比如悬停效果、按钮点击时的颜色变化等。

简而言之,过渡动画注重简单和细微的效果,通常出现在与用户交互的情境中。例如下面这个效果:

CSS
@layer transition {
    ul {
        --ease-elastic-out-4: cubic-bezier(0.5, 1.5, 0.75, 1.25);
        --radius: 0;
        --middle: calc(calc(var(--sibling-count) + 1) / 2);
    
        > li {
          --angle: calc((var(--sibling-index) - var(--middle)) * var(--radius));
    
          transform: rotateZ(var(--angle));    
          transform-origin: -200% 50%;
          
          transition: transform 0.3s var(--ease-elastic-out-4);
    
          &:nth-child(odd) a:hover {
              transform: perspective(500px) rotateY(45deg);
          }
    
          &:nth-child(even) a:hover {
              transform: perspective(500px) rotateY(-45deg);
          }
        }
    
        &:hover li {
            --radius: 4deg;
        }
    
        & a {
            transition: transform 0.3s ease-in-out;
        }
    }
}

Demo 地址:https://codepen.io/airen/full/MWLwyYR

正如你所看到的,用户鼠标悬停在菜单上时,整个菜单会过渡到一个扇形状态,同时鼠标悬停在菜单项上时,它会有一个小翻转的效果。

CSS 帧动画提供更多控制,可以编排更复杂的动画序列。如果你需要一个更复杂的动画,比如加载动画、弹跳球或需要沿指定路径移动的元素时,那么 CSS 帧动画更为合适。例如下面这个动画效果,导航菜单当前项有一个呼吸灯的效果,另外最后一个菜单项中的图片一直有一个抖动的微动效:

Demo 地址:https://codepen.io/airen/full/MWLwyzw

CSS
@layer animation {
    @keyframes pulse {
        0% {
            opacity: 0.5;
            sacle: 1;
        }
        100% {
            opacity: 0;
            scale: 2.5;
        }
    }

    @keyframes tada {
        0% {
            scale: 1 1 1;
        }
    
        10%,
        20% {
            scale: .9 .9 .9;
            rotate: 0 0 1 -3deg;
        }
    
        30%,
        50%,
        70%,
        90% {
            scale: 1 1 1;
            rotate: 0 0 1 3deg;
        }
    
        40%,
        60%,
        80% {
            scale: 1.1 1.1 1.1;
            rotate: 0 0 1 -3deg;
        }
    
        100% {
            scale: 1 1 1;
        }
    }
  
    .nav-rail span::after {
        animation: pulse 1s infinite linear;
    }
  
    .nav-item svg {
        transform-origin: left bottom;
        animation: tada 1s infinite linear;
    }
}

总的来说,过渡动画适用于简单的、与用户交互相关的状态变化,而帧动画适用于更复杂的、需要精细控制的动画效果。根据你的项目需求,选择适当的动画类型。

记住,过渡和动画都可以让你的网页生动起来。关键是要知道在何时使用哪种,就像在烹饪中何时加入一点盐或撒一些糖一样。因此,明智地使用这些工具,创造出引人注目的效果!

最后再强调一点,虽然过渡动画和帧动画都有适用自己的情境,但决不是以一概全,在 Web 上的很多动画效果,我们是可以将两者结合起来一起使用的。如果你把两者用好了,将会制作出更有创意和优雅的动画效果。比如下面这个由 @Jhey 提供的“闪光按钮”的动画效果,就同时有过渡动画和帧动画。

Demo 地址:https://codepen.io/airen/full/NWoqrYx (来源于 @Jhey

详细代码不在这里展示了,感兴趣的同学,可以查看 Demo 源码

动画性能

不管是 CSS 的过渡动画还是帧动画,都会涉及到性能相关的问题。

在介绍 Web 动画基本原理的时候,就提到动画应该以 60fps 的速度运行,这也意味着浏览器每帧只有 16.6ms 的时间来绘制。这时间确实不多,因为我们人类眨一下眼睛就需要大约 100ms ~ 300ms 的时间!如果我们的动画计算负担过重,它将显得不流畅,会出现卡顿。帧将会被丢弃,因为设备无法跟上。

通过调整新的“每秒帧数”控件,你可以亲自体验到这一点:

在实际应用中,性能不佳通常表现为可变的帧速率,因此这不是一个完美的模拟。

动画性能对于 Web 开发者而言,一直以来都是具有挑战性,而且该领域涉及到的知识也很深很广,这方面的知识完全超出了这节课的范畴。只不过,动画性能是动画开发中不可缺少的话题之一。正因为如此,在这里我们只对一些关键点做简单的阐述,相关的细节将留到后续的课程中来阐述。

在过渡动画和帧动画中,选择要动画化的 CSS 属性非常重要。因为有一些属性值的计算是非常昂贵的,比如元素的尺寸属性 widthheight ,因为它们会影响布局。当一个元素尺寸变化时(变小或变大),它会引发连锁反应;所有兄弟元素也需要调整位置,避免造成布局破裂。即影响 Web 的重排

其次,颜色相关的属性在动画中成本也是较高的。虽然它们不会影响到 Web 的布局(重排),但每一帧都需要进行重新绘制,这也是影响 Web 性能另一重要因素,即重绘

也就是说,我们在过度动画和帧动画中,应该尽可能选择计算成本低的 CSS 属性。比如 transformopacity 。它们在动画中的成本就非常低。如果一个动画当前调整的是像 widthleft 这样的属性,那么可以考虑使用 transform 来替代它们(尽管不一定总是能达到完全相同的效果),这可以大大改进动画性能。

在动画中的成本非常低。如果一个动画当前调整的是像width(宽度)或left(左边距)这样的属性,通过将其移到transform(变换)可以大大改进它(尽管不一定总是可能达到完全相同的效果)。

简单地说,在可能的情况下,避免使用会触发页面布局重新计算的属性,因为这些属性通常更昂贵

除了考虑选择计算成本较低的 CSS 属性之外,还可以考虑开启“硬件加速”。正如前面所说,当我们使用 transformopacity 来动画化元素时,浏览器有时会尝试优化此动画。浏览器不会在每一帧上都对像素进行光栅化,而是将所有内容传输到 GPU 作为纹理。GPU 擅长执行这些基于纹理的转换,因此我们得到了非常流畅且性能出色的动画。

不过,有一个小问题存在,即 GPU 和 CPU 以稍微不同的方式渲染事物。当 CPU 将其交给 GPU,反之亦然,事物会轻微地移动,从而导致小幅的抖动。为此,我们需要使用 will-change 属性来避免这个现象:

CSS
.animated {
    will-change: transform;
}

will-change 是一个属性,允许我们提示浏览器我们将要动画所选元素,并且它应该进行优化处理。实际上,这意味着浏览器将始终让 GPU 处理此元素。不再在 CPU 和 GPU 之间切换,不再有显眼的“就位移动”。

另外,will-change 允许我们有意识地选择哪些元素应该进行硬件加速。浏览器有自己难以理解的逻辑,我宁愿不将其交给机会。

简而言之,通过使用硬件加速(例如 will-change 属性)可以提高动画性能。这有助于减少 CPU 和 GPU 之间的切换,从而提供更顺畅的动画效果

总的来说,性能优化是过渡动画和帧动画设计的关键部分,旨在提供无缝、流畅的用户体验。在开发和测试过程中,密切关注性能因素,并根据目标设备和浏览器进行优化。例如避免布局抖动、使用 CSS 硬件加速、减少不必要的动画帧数等。

小结

阅读到这里,那么也表示帧动画与过渡动画的学习之旅就要结束了。

我们从基础知识开始,学习了 CSS 过渡动画和帧动画的魔力以及它们将 Web 带到生活中的能力。然后,我们深入探讨了它们的内部工作原理,以及使用它们的一些最佳实践。我们比较了过渡动画和帧动画,讨论了它们的优缺点,并学会了在具体的业务场景或情境之下,应该选择哪种动画更为合适。

  • 如果要创建交互式元素,请使用过渡动画。
  • 如果要创建复杂的关键帧动画,请使用帧动画。

还记得,学习的最佳方式是实际操作。所以,前进吧,打开你的代码编辑器,开始尝试使用 CSS 过渡和动画。