2024 年 5 月 29 日

你还需要 Framer Motion 吗?

Matt Perry

24/11/12 更新:Framer Motion 现在是 Motion for React。此页面上的一些引用可能已过时。阅读公告.

时光飞逝:我发布了第一个版本的Framer Motion五年多以前。我的目标是(并且仍然是)创建一个比使用 CSS 动画更简单,但具有 JS 库所有高级功能的 API。

CSS 中的动画一直受到限制,而且常常以令人惊讶的方式。例如,使用 CSS 始终无法在元素进入 DOM 时对其进行动画处理。使用 Framer Motion,这就像

<motion.li
  initial={{ opacity: 0 }}
  animate={{ opacity: 1 }}
/>

一样简单。同样,始终无法独立地为变换设置动画、使用弹簧物理效果、添加与滚动链接的动画或制作复杂的布局动画。

但是五年是很长一段时间,CSS 也在不断改进。许多过去困难或不可能的事情现在都变得非常容易。因此,在 Framer Motion 五周年之际,以下是五个新的 CSS 功能,这意味着您可能不再需要它。

1. 进入动画

我们刚刚看到了在 Framer Motion 中制作进入动画是多么容易。如此简单,甚至平淡无奇,以至于多年来我忘记了这在技术上是一个功能。

当 Framer Motion 首次创建时,仅使用 CSS 从初始视觉状态为元素设置动画是不可能的。您需要少量 JavaScript,甚至还需要一点技巧。

首先,使用 CSS 设置元素的样式。

#my-element {
  opacity: 0;
  transition: opacity 0.5s;
}

然后,在将元素添加到 DOM 后,使用少量 JavaScript 更改我们要动画的值(在本例中为 opacity

const element = document.getElementById("my-element")

element.style.opacity = 1

您可能会认为,考虑到 CSS 中定义的 transition,这足以触发动画。但事实并非如此。

因为 1 是在计算元素的样式之前设置的,所以 1 现在被认为是“初始”值,而不是 0

要解决此问题,您可以首先强制重新计算样式。

element.getBoundingClientRect()
element.style.opacity = 1

这种分布在整个应用程序中的读/写操作,如果管理不善,可能会导致样式和布局抖动,这对性能非常不利。

或者,您可以等待几个动画帧,以确保元素已绘制。

requestAnimationFrame(() => {
  requestAnimationFrame(() => {
    element.style.opacity = 1
  })
})

任何一种方法我都只能说是“非常糟糕的东西”。与替代方案相比,Framer Motion 的 animate 属性的轻描淡写感觉更像是一个更大的功能。

然而,CSS 有一个新的绝招,@starting-style

使用 @starting-style,我们可以定义元素在渲染后将从中动画进入的样式。

#my-element {
  opacity: 1;
  transition: opacity 0.5s;

  @starting-style {
    opacity: 0;
  }
}

@starting-style 在所有现代浏览器中都可用。旧浏览器只会不显示动画,因此我们今天可以安全地使用它。

不过,此 API 的一个有趣的细节是,起始样式需要定义在正常样式之后,因为它们莫名其妙地共享相同的特异性。所以尽管像这样写可能是您的倾向

#my-element {
  @starting-style {
    opacity: 0;
  }

  opacity: 1;
  transition: opacity 0.5s;
}

但它不会像预期的那样动画。

2. 独立变换

在 Framer Motion(以及所有 JS 动画库)中,诸如 xscaleXrotate 之类的变换都可以彼此独立地进行动画处理。

<motion.div
  initial={{ y: 10 }}
  whileInView={{ y: 0 }}
  whileHover={{ scale: 1.2 }}
  whileTap={{ scale: 0.9, rotateX: 5 }}
/>

这在 CSS 中曾经是不可能的,因为 transform 是一个单值,因此必须整体进行动画处理。所有值都使用相同的过渡设置一起进行动画处理。

而 JS 库可以每帧构造和渲染一个新的 transform 字符串,因此其组成值可以独立地开始和停止动画,所有这些都使用不同的过渡设置。

经过多年的失败尝试和死胡同提案后,CSS 最近获得了新的简写属性 translatescalerotate。与 transform 不同,这些可以彼此独立地设置和动画处理

button {
  translate: 0px 0px;
  transition:
    translate 0.2s ease-out,
    scale 0.5s ease-in-out;
}

@starting-style {
  button {
    translate: 0px 10px;
  }
}

button:hover {
  scale: 1.2;
}

与在 JS 库中构建 transform 字符串相比,使用这些值的好处是这些动画可以进行硬件加速。

缺点是它们仍然是通往真正独立变换的中间站。任何一个轴都不能独立控制。因此,如果我们想要基于速度的 x/y 动画,或者将 scaleXscaleY 分开动画,我们需要依赖另一个新的 CSS 功能,@property

@property 允许我们向浏览器提供有关 CSS 变量的一些类型信息。这解锁了使用 CSS/WAAPI 对它们进行动画处理的能力。

例如,如果我们想使用不同的缓动曲线为 rotateXrotateY 设置动画,我们可以首先使用 @property 定义变量

@property --rotate-x {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

@property --rotate-y {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

然后在 transform/rotate 字符串中使用这些变量。

button {
  transform: rotateX(var(--rotate-x)) rotateY(var(--rotate-y));
  // rotate: var(--rotate-x) var(--rotate-y);
  transition:
    --rotate-x 0.2s ease-out,
    --rotate-y 0.3s linear;
}

button:hover {
  --rotate-x: 10deg;
  --rotate-y: 20deg;
}

然而,使用 CSS 变量进行动画处理的最大缺点是它们很慢,因为它们总是触发绘制。即使我们只在 transform 中使用这两个值,但由于这种绘制,使用 CSS 变量进行动画处理仍然比通过 JS 库每帧构建 transform 字符串慢得多。

3. 弹簧

在到目前为止给出的所有 Framer Motion 示例中,您可能已经注意到缺少 transition 设置。这是因为 Motion 尝试提供一些合理的动态默认值,对于变换,这些默认值是弹簧。

<motion.div
  initial={{ x: -100 }}
  animate={{ x: 0 }}
  transition={{ type: "spring", stiffness: 300, damping: 10 }}
/>

弹簧是库的基石。来自手势或中断动画的速度被馈送到下一个动画中,因此 UI 感觉更具触感、响应性,甚至更具趣味性。

长期以来,弹簧在 CSS 中是不可能的,但最近它获得了 linear()缓动函数.

linear() 是一个如此出色的想法,以至于它可能是我见过的 API 从提出到在臭名昭著的落后者 Safari 中发布的最短时间。

不要与 linear(没有函数括号)混淆,linear() 缓动函数接受一系列点并在它们之间线性插值(因此得名)。提供足够的点,它们可以“绘制”弹簧、弹跳或任何其他自定义缓动曲线的缓动曲线。

通过 linear() 定义的弹簧可能如下所示

transition: transform 2s 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
);

显然,不希望您自己写出 linear() 定义。我实际上直接从在线linear()生成器.

中提取了这个定义。这里的开发者体验非常糟糕,不得不从外部工具复制/粘贴这些定义,而不是仅仅将选项传递给 spring() 函数。弹簧的感觉可能难以捉摸,因此在这些工具之间来回切换很繁琐。

另一个缺点是它们是预定义的缓动曲线,而不是在每个样式的实际值和速度上运行的真实物理模拟。因此,当从用户手势或中断动画中获取速度时,它们不会提供相同的感觉。

但是,对于某些动画,它们看起来可能非常令人信服,因此在许多情况下,它可以是一个足够好、轻量级的选项。

4. 与滚动链接的动画

滚动动画有两种类型:滚动触发和滚动链接

滚动触发的动画是正常的基于时间的动画,当元素出现在视图中时会被触发。由于 whileInView 属性,这些在 Framer Motion 中非常简单。

<motion.div initial={{ opacity: 0 }} whileInView={{ opacity: 1 }} />

虽然有可能拼凑在一起,但今天 CSS 中仍然没有针对滚动触发动画的良好解决方案。

相反,滚动链接的动画不是由时间驱动,而是将值直接链接到滚动进度。在 Framer Motion 中,这些动画是通过组合 MotionValues:

const { scrollYProgress } = useScroll()

// Map scroll progress to x
const x = useTransform(scrollYProgress, [0, 1], [0, 500])

return <motion.div style={{ x }} />

创建的。CSS 确实有一个用于滚动链接动画的新功能。实际上是两个:新的 scroll()view() 动画时间线。

可以将任一时间线分配给 animation-timeline 样式,以通过滚动进度而不是时间来驱动动画

div {
  animation-name: fadeAnimation;
  animation-timeline: scroll();
}

@keyframes fadeAnimation {
  from {
    transform: translateX(0px);
  }
  to {
    transform: translateX(500px);
  }
}

两者之间的区别在于 scroll() 用于跟踪视口或可滚动元素的滚动进度,而 view() 用于检测元素在视口/元素中移动时的进度。

这些新时间线的伟大之处在于,当样式动画可以进行硬件加速时,例如 transformopacity,这些滚动动画也将完全在主线程之外运行,即使您的站点正在执行繁重的工作,也能确保滚动动画保持流畅。

Framer Motion 通过新的 ScrollTimeline JS API 确实初步支持加速滚动动画,同时保持与旧浏览器的兼容性。但还需要做更多工作来支持 ViewTimeline

CSS 时间线的一个缺点是,当我们想要做任何更复杂的事情时,例如基于滚动速度或阻尼运动来制作动画,我们必须求助于CSS 变量技巧.

这些效果不仅可以说更容易使用 Framer Motion 的 useVelocityuseSpring 组合,而且因为它们依赖于 CSS 变量,所以它们在主线程上运行并触发绘制,因此它们的性能实际上不如使用 JavaScript。

5. 布局动画

Framer Motion 具有强大的布局动画 API,可以使用 transform 在任意两个布局之间进行动画处理。

它非常适合为通常无法动画的值设置动画,例如 justify-content

对于这个简单的示例,它似乎有点过分,但需要注意的好处是,它说明了您可以跨不同的断点使用您首选的 CSS(例如网格、flexbox 等)创建布局,而 Motion 将实时计算所需的 transform 动画。

API 也非常简单。只需使用 layout 属性标记动画元素即可。

<motion.div layout />

Framer Motion 的布局动画远远超出了经典的 FLIP 技术,因为它们对无限深度的树执行比例校正,包括对 border-radiusbox-shadow 的扭曲,确保任何布局都可以动画,而不仅仅是顶层元素。

它还可以通过为两个元素提供相同的 layoutId 属性,在完全不同的树之间执行共享布局动画。

<motion.div layoutId="modal" />

如果没有 Framer Motion,像这样的动画将非常复杂,难以编写和维护。

但是现在,有了视图转换 API,这是一个浏览器原生 API,可以在两个不同的视图之间进行动画处理,并且已被某些人定位为 Motion 布局动画的替代品。

差异很大,可能需要单独写一篇文章,因此我将尝试简洁地说明。

它的工作原理是用 document.startViewTransition() 包裹 DOM 更新

document.startViewTransition(updateDOM)

默认情况下,这将使整个视口交叉淡入其新的视觉状态。

注意:以下演示仅在支持视图转换 API 的浏览器中进行动画处理。

这种方法实际的工作原理是在页面顶部创建一个新的伪 DOM。它由先前视图的屏幕截图和新视图的实时屏幕截图组成。然后将它们交叉淡化。

对于此切换示例,这种默认效果非常差,但对于完整页面过渡,效果非常好。本网站在页面之间导航时使用了类似的效果。

对于此开关,我们可以通过向切换元素添加唯一的 view-transition-name 样式来改进它。

<div style={{ viewTransitionName: "toggle" }} />

现在,页面仍将交叉淡化,但 toggle 元素将在伪 DOM 中的其自身图层中进行屏幕截图和交叉淡化。它的大小和位置也将进行动画处理。

对于这个简单的示例,代码看起来比 Framer Motion 的 API 稍微复杂一些,而且 IMO 为每个元素提供唯一的 ID 也很麻烦。这是不必要的复杂性,并且在组件树中变得难以管理。view-transition: persist 或类似的东西对于这些相同元素的动画会更好。

另一方面,创建共享元素转换并不复杂。具有 view-transition-name 的元素在转换前后可以不同。

就 API 而言,当我们打开模态框时,我们必须删除列表项上的 view-transition-name 样式,这不太好。

viewTransitionName: isOpen ? undefined : "container"

Framer Motion 维护 layoutId 堆栈,因此您可以添加多个具有相同 ID 的元素,它会知道要动画到哪些元素以及从哪些元素返回。

您还可以看到图像在其位置之间动画的效果不如 Framer Motion 示例中的效果好。这是因为该效果由两层组成,外部 container 裁剪内部 image-container。由于每个命名的元素都在新的伪元素中进行屏幕截图和动画处理,因此它与其他图层并行(而不是维护其 DOM 层次结构),因此您可能会遇到视觉伪影,其中元素先前被裁剪

在 Framer Motion 中,元素本身是动画的,因此您不会遇到这些情况。

也就是说,伪 DOM 是一个完全独特和全新的设置,它开启了 DOM 本身布局动画工作方式所不具备的可能性。

例如,我们可以将图层从页面上抬起,同时在下方使用动画渐变蒙版进行交叉擦除

其他基本差异也存在,既有缺点也有机会。

滚动增量

视图转换实际上是:两个视图之间的转换。这意味着如果在 DOM 更新期间滚动位置发生变化,则每个具有 view-transition-name 的元素都将按该滚动距离在视口中进行动画处理。

而布局动画顾名思义实际上是:两个布局之间的转换。滚动已被考虑在内,因此仅因滚动而在视口中更改位置的元素不会进行动画处理。或者,如果它们确实因为布局也发生了变化而进行动画处理,它们将从其新的滚动位置而不是它们过去在屏幕上绘制的位置进行动画处理。

变换动画

变换不是布局,因此在 Framer Motion 中,它们可以单独进行动画处理。请注意,当在此处切换布局时,旋转动画会继续不间断地进行

混合过渡

当使用不同的过渡设置为多个图层设置动画时,视图转换中没有相对布局的概念。父图层可能会以延迟或较慢的过渡速度从子图层动画移开(反之亦然)

而 Framer Motion 知道这两个元素之间的关系,并将确保父元素和子元素不会彼此动画移开

可中断的动画

尝试快速单击 Framer Motion 开关示例,然后单击视图转换开关示例。Motion 的布局动画是可中断的,而视图转换则不是。默认情况下,伪 DOM 会阻止指针事件,但如果您手动中断视图转换,则下一个动画将从实际 DOM 元素在视口中的实际位置开始,而不是它在视图转换中看起来的位置开始。

可中断性是表格中的动画功能,因此最重要的是,这使得它们完全不适合这些类型的微交互。

总而言之

其中一些差异正在得到解决,而另一些差异是视图转换概念固有的。

总而言之,在视图转换和布局动画之间做出选择是一种虚假的二分法。在Framer中,我们两者都使用,各取所长。

视图转换非常擅长将整个视图从一种状态动画到另一种状态,因此我们将它们用于不同页面之间的动画。您可以创建布局动画根本不可能实现的独特效果。

而布局动画是可中断的,不受滚动影响,可以混合过渡设置,并且更适用于页面的隔离部分。因此,我们将这些用于动画组件变体。

因此,根据您要动画处理的内容,视图转换可能也可能不是有效的替代方案。

奖励:自动高度

好的,我保证了五个理由,但这个理由真的令人兴奋。我会长话短说。

长期以来,Framer Motion 一直能够在固定高度和 auto 之间进行动画处理。

<motion.div animate={{ height: isOpen ? "auto" : 0 }} />

CSS 不支持动画到/从 auto,但借助 CSS5 全新的 calc-size 提案,最终可以实现相同的效果。

li {
  height: 0px;
  transition: height 0.3s ease-out;
  
  .open {
    height: calc-size(auto);
  }
}

没有陷阱,只是一个很棒的新功能。

那么,你还需要 Framer Motion 吗?

这是一组令人惊叹的新功能,这些功能已经或即将登陆 CSS。如果您仅出于非常具体的原因使用 Framer Motion,例如进入动画或 height: auto,那么有一个令人信服的论点是首先从 CSS 开始,仅在您遇到其限制时才引入 Framer Motion。

对我而言,承认存在偏见,我只是更喜欢 Framer Motion API。考虑到 CSS API 的特殊性,例如 @starting-style 特异性,或 linear() 的汇编级美学,大多数这些新功能在 Motion 中仍然更容易实现。简单易于编写,并且更易于维护。

此外,Framer Motion(以及一般的 JS 动画库)具有 CSS 尚未达到的保真度。无论是真正的基于速度的弹簧,还是可中断的布局动画,甚至是朴实的悬停手势,由于 2007 年的网络状态,悬停手势不会奇怪地 polyfill 到触摸设备,在 Framer Motion 中构建的动画和手势应该感觉更好。

CSS 仍然缺少大量功能,例如复杂的时间线排序, 退出动画, 交错, 滚动触发的动画、基于速度的弹簧等等。

那么,你还需要 Framer Motion 吗?感谢这五个新的 CSS 功能,计算方式已经改变,但我的答案与五年前相同:去和你爱的人一起玩玩吧。

阅读更多

Vue 版 Motion 介绍

Motion 终于登陆 Vue,完整配备了变体、滚动、布局动画以及您从 Framer Motion 中喜爱的一切。

Motion 终于登陆 Vue,完整配备了变体、滚动、布局动画以及您从 Framer Motion 中喜爱的一切。

揭秘:React 的实验性动画 API

React 正在试验一种基于视图转换 API 的新动画 API。它是如何工作的?它能做什么?我们在这篇博文中揭示一切。

React 正在试验一种基于视图转换 API 的新动画 API。它是如何工作的?它能做什么?我们在这篇博文中揭示一切。

如何将 cmd-k 搜索快捷方式添加到您的 Framer 网站

默认情况下,Framer 搜索组件不支持 cmd-k 键盘快捷方式。以下是如何将其添加到您的 Framer 网站。

默认情况下,Framer 搜索组件不支持 cmd-k 键盘快捷方式。以下是如何将其添加到您的 Framer 网站。

Framer Motion 现在是独立的,推出 Motion

Framer Motion 现在是独立的。推出 Motion,一个用于 React 和所有 JavaScript 环境的新动画库。这对您意味着什么。

Framer Motion 现在是独立的。推出 Motion,一个用于 React 和所有 JavaScript 环境的新动画库。这对您意味着什么。

何时浏览器会限制 requestAnimationFrame

在特定情况下,Safari 和 Firefox 可能会限制 requestAnimationFrame。这就是为什么您的 JavaScript 动画会卡顿的原因。

在特定情况下,Safari 和 Firefox 可能会限制 requestAnimationFrame。这就是为什么您的 JavaScript 动画会卡顿的原因。

Motion 的实现离不开我们出色的赞助商。

保持联系

订阅以获取最新新闻和更新。

保持联系

订阅以获取最新新闻和更新。

保持联系

订阅以获取最新新闻和更新。