Svelte 和 Spring 动画

Avatar of Adam Rackis
Adam Rackis

DigitalOcean 为您的旅程的每个阶段提供云产品。立即开始使用 $200 免费积分!

弹簧动画是让 UI 交互栩栩如生的绝佳方式。弹簧允许我们使用弹簧物理学来移动物体,而不是在一段时间内以恒定速率更改属性,这会给人一种真实物体移动的感觉,并且对用户来说可能显得更加自然。

我之前写过关于弹簧动画的文章 以前。那篇文章是基于 React,使用 react-spring 进行动画。这篇文章将探讨 Svelte 中类似的想法。

CSS 开发人员!在控制动画的感觉时,通常会想到缓动。您可以将“弹簧”动画视为基于现实世界物理学的缓动的一个子类别。

Svelte 实际上在框架中内置了弹簧,不需要任何外部库。我们将回顾之前关于 react-spring 的文章中前半部分的内容。但之后,我们将深入探讨这些弹簧与 Svelte 交互的所有方式,并将现实世界的实现留给以后的文章。虽然这可能看起来令人失望,但 Svelte 拥有一些 React 中没有的奇妙、独特的特性,可以有效地与这些动画基元集成。我们将花一些时间谈论它们。

另一个说明:一些分布在各处的演示可能看起来很奇怪,因为我配置了弹簧以变得格外“弹性”,以创造更明显的效果。如果您查看任何演示的代码,请务必找到适合您的弹簧配置。

这里有一个 很棒的 REPL Rich Harris 制作的,用于展示所有不同的弹簧配置以及它们的行为方式。

Svelte Store 的简要介绍

在我们开始之前,让我们快速浏览一下 Svelte Store。虽然 Svelte 的组件非常适合存储和更新状态,但 Svelte 还包含 Store 的概念,它允许您在组件之外存储状态。由于 Svelte 的 Spring API 使用 Store,因此我们将在此快速介绍其主要部分。

要创建一个 Store 实例,我们可以导入 writable 类型,并像这样创建它

import { writable } from "svelte/store";
const clicks = writable(0);

clicks 变量是一个值为 0 的 Store。有两种方法可以设置 Store 的新值:setupdate 方法。前者接收您要设置的 Store 的值,而后者接收一个回调函数,它接受当前值并返回新值。

function increment() {
  clicks.update(val => val + 1);
}
function setTo5() {
  clicks.set(5);
}

如果您不能实际使用状态,那么状态就毫无用处。为此,Store 提供了一个 subscribe 方法,它允许您在有新值时收到通知——但在组件内部使用它时,您可以在 Store 的名称前加上 $ 字符,这告诉 Svelte 不仅要显示 Store 的当前值,还要在值更改时进行更新。例如

<h1>Value {$clicks}</h1>
<button on:click={increment}>Increment</button>
<button on:click={setTo5}>Set to 5</button>

这是一个完整的、可运行的示例。Store 提供了许多其他功能,例如派生 Store(允许您将 Store 连接在一起)、只读 Store,甚至能够在首次观察 Store 时以及不再观察 Store 时收到通知。但对于本文的目的,上述代码是我们需要担心的全部内容。请查阅 Svelte 的 文档交互式教程 以了解更多信息。

弹簧速成课程

让我们快速了解一下弹簧以及它们的功能。我们将查看一个简单的 UI,它会更改某些元素的表示方面——不透明度和变换——然后看看如何对这些更改进行动画处理。

这是一个最小的 Svelte 组件,它会切换一个 <div>opacity,并切换另一个 <div> 的 x 轴 transform(不带任何动画)。

<script>
  let shown = true;
  let moved = 0;

  const toggleShow = () => (shown = !shown);
  const toggleMove = () => (moved = moved ? 0 : 500);
</script>

<div style="opacity: {shown ? 1 : 0}">Content to toggle</div>
<br />
<button on:click={toggleShow}>Toggle</button>
<hr />
<div class="box" style="transform: translateX({moved}px)">I'm a box.</div>
<br />
<button on:click={toggleMove}>Move it!</button>

这些更改会立即应用,因此让我们看看如何对它们进行动画处理。这就是弹簧的用武之地。在 Svelte 中,弹簧是一个 Store,我们会在其上设置所需的值,但它不会立即更改,而是 Store 会在内部使用弹簧物理学来逐渐更改值。然后,我们可以将我们的 UI 绑定到这个不断变化的值,以获得不错的动画效果。让我们看看它是如何运作的。

<script>
  import { spring } from "svelte/motion";

  const fadeSpring = spring(1, { stiffness: 0.1, damping: 0.5 });
  const transformSpring = spring(0, { stiffness: 0.2, damping: 0.1 });

  const toggleFade = () => fadeSpring.update(val => (val ? 0 : 1));
  const toggleTransform = () => transformSpring.update(val => (val ? 0 : 500));
  const snapTransform = () => transformSpring.update(val => val, { hard: true });
</script>

<div style="opacity: {$fadeSpring}">Content to fade</div>
<br />
<button on:click={toggleFade}>Fade Toggle</button>

<hr />

<div class="box" style="transform: translateX({$transformSpring}px)">I'm a box.</div>
<br />
<button on:click={toggleTransform}>Move it!</button>
<button on:click={snapTransform}>Snap into place</button>

我们从 Svelte 获取弹簧函数,并为我们的不透明度和变换动画设置不同的弹簧实例。变换弹簧配置被故意设置为格外弹性,以帮助说明稍后我们如何暂时关闭弹簧动画并立即应用所需更改(这将在稍后派上用场)。脚本块的最后是我们用来设置所需属性的点击处理程序。然后,在 HTML 中,我们将不断变化的值直接绑定到我们的元素……就是这样!这就是 Svelte 中基本弹簧动画的全部内容。

剩下的唯一一项是 snapTransform 函数,其中我们将变换弹簧设置为其当前值,但也传递了一个对象作为第二个参数,其中包含 hard: true。这会立即应用所需的值,没有任何动画效果。

此演示以及我们将在这篇文章中查看的其他基本示例,可以在此处找到

高度动画

height 进行动画处理比其他 CSS 属性更棘手,因为我们必须知道要动画处理到的实际高度。不幸的是,我们不能对 auto 值进行动画处理。这对于弹簧来说没有意义,因为弹簧需要一个真实的数字才能通过弹簧物理学插值正确的值。而且碰巧的是,您甚至无法使用常规 CSS 过渡对 auto 高度进行动画处理。幸运的是,Web 平台为我们提供了一个方便的工具来获取元素的高度:一个 ResizeObserver,它在浏览器中的 支持度相当不错

让我们从元素的原始高度动画开始,生成一个“向下滑动”效果,我们在其他示例中会逐渐对其进行细化。我们将使用 ResizeObserver 来绑定到元素的高度。我应该指出,Svelte 确实有一个 offsetHeight 绑定,可以用于更直接地绑定元素的高度,但它使用了一些 <iframe> 技巧,导致它只能在可以接收子元素的元素上工作。这可能足以满足大多数用例,但我将使用 ResizeObserver,因为它最终允许进行一些不错的抽象。

首先,我们将绑定元素的高度。它将接收元素并返回一个可写入的 Store,该 Store 初始化一个 ResizeObserver,该观察器会在发生更改时更新 height 值。下面是代码示例

export default function syncHeight(el) {
  return writable(null, (set) => {
    if (!el) {
      return;
    }
    let ro = new ResizeObserver(() => el && set(el.offsetHeight));
    ro.observe(el);
    return () => ro.disconnect();
  });
}

我们将 Store 初始化为 null 值,我们将将其解释为“尚未测量”。writable 的第二个参数由 Svelte 在 Store 变得活跃时调用,一旦它在组件中使用,它就会变得活跃。此时,我们将启动 ResizeObserver 并开始观察元素。然后,我们返回一个清理函数,Svelte 会在我们不再在任何地方使用 Store 时为我们调用它。

让我们看看它是如何运作的

<script>
  import syncHeight from "../syncHeight";
  import { spring } from "svelte/motion";

  let el;
  let shown = false;
  let open = false;
  let secondParagraph = false;

  const heightSpring = spring(0, { stiffness: 0.1, damping: 0.3 });
  $: heightStore = syncHeight(el);
  $: heightSpring.set(open ? $heightStore || 0 : 0);

  const toggleOpen = () => (open = !open);
  const toggleSecondParagraph = () => (secondParagraph = !secondParagraph);
</script>

<button on:click={ toggleOpen }>Toggle</button>
<button on:click={ toggleSecondParagraph }>Toggle More</button>
<div style="overflow: hidden; height: { $heightSpring }px">
  <div bind:this={el}>
    <div>...</div>
    <br />
    {#if secondParagraph}
    <div>...</div>
    {/if}
  </div>
</div>

我们的 el 变量保存了我们正在对它进行动画处理的元素。我们告诉 Svelte 通过 bind:this={el} 将其设置为 DOM 元素。heightSpring 是我们的弹簧,它保存了元素打开时的高度值,以及关闭时的零值。我们的 heightStore 会不断使用元素的当前高度来更新它。el 最初是未定义的,syncHeight 返回一个无用的可写入 Store,它基本上什么都不做。一旦 el 被分配到 <div> 节点,该行将重新执行——这要归功于 $: 语法——并获得包含 ResizeObserver 监听器的可写入 Store。

然后,这行代码

$: heightSpring.set(open ? $heightStore || 0 : 0);

…监听 open 值和高度值的更改。无论哪种情况,它都会更新我们的弹簧 Store。我们在 HTML 中绑定了高度,我们就完成了!

请务必记住,在该外部元素上将 overflow 设置为 hidden,以便在元素在打开和关闭状态之间切换时正确剪裁内容。此外,元素高度的更改也会动画处理到位,您可以在“切换更多”按钮上看到这一点。您可以在上一节的嵌入式演示中运行此代码。

请注意,上面的这行代码

$: heightStore = syncHeight(el);

…在使用服务器端渲染 (SSR) 时会导致错误,如 此错误 中所述。如果您没有使用 SSR,则不必担心,当然,当您阅读本文时,该错误可能已经被修复。但是,解决方法仅仅是执行以下操作

let heightStore;
$: heightStore = syncHeight(el);

…这有效,但不太理想。

我们可能不希望 <div> 在首次渲染时弹性打开。此外,打开弹簧效果很好,但在关闭时,效果会很糟糕,因为一些内容会闪烁。我们可以解决这个问题。为了防止首次渲染进行动画处理,我们可以使用我们之前看到的 { hard: true } 选项。让我们将对 heightSpring.set 的调用更改为以下内容

$: heightSpring.set(open ? $heightStore || 0 : 0, getConfig($heightStore));

…然后看看如何编写 getConfig 函数,该函数返回一个包含 hard 属性的对象,该属性在首次渲染时被设置为 true。以下是我想到的

let shown = false;

const getConfig = val => {
  let active = typeof val === "number";
  let immediate = !shown && active;
  //once we've had a proper height registered, we can animate in the future
  shown = shown || active;
  return immediate ? { hard: true } : {};
};

请记住,我们的高度存储器最初保存的是 null,只有在 ResizeObserver 开始运行时才会获得数字。我们利用这一点来检查实际的数字。如果我们有一个数字,并且我们还没有显示任何内容,那么我们知道立即显示我们的内容,我们通过设置立即值来做到这一点。该值最终会触发弹簧中的 hard 配置值,我们之前已经看到了。

现在让我们调整动画,使其在关闭内容时稍微不那么弹性。这样,它们关闭时就不会闪烁。当我们最初创建弹簧时,我们指定了刚度和阻尼,如下所示

const heightSpring = spring(0, { stiffness: 0.1, damping: 0.3 });

事实证明,spring 对象本身维护这些属性,这些属性可以在任何时候设置。让我们更新这一行

$: heightSpring.set(open ? $heightStore || 0 : 0, getConfig($heightStore));

这会检测打开值(以及 heightStore 本身)的变化以更新弹簧。让我们还根据我们是打开还是关闭来更新弹簧的设置。下面是它的样子

$: {
  heightSpring.set(open ? $heightStore || 0 : 0, getConfig($heightStore));
  Object.assign(
    heightSpring,
    open ? { stiffness: 0.1, damping: 0.3 } : { stiffness: 0.1, damping: 0.5 }
  );
}

现在,当我们获得新的 openheight 值时,我们会像以前一样调用 heightSpring.set,但我们还会根据元素是否打开来设置弹簧上的 stiffnessdamping 值。如果它已关闭,我们将 damping 设置为 0.5,这会减少弹性。当然,您可以随意调整所有这些值并根据需要配置它们!您可以在 演示 的“Animate Height Different Springs”部分看到这一点。

您可能会注意到我们的代码开始快速增长。我们添加了许多样板代码来涵盖其中一些用例,所以让我们清理一下。具体来说,我们将创建一个函数来创建我们的弹簧,该函数还导出一个 sync 函数来处理我们的弹簧配置、初始渲染等。

import { spring } from "svelte/motion";

const OPEN_SPRING = { stiffness: 0.1, damping: 0.3 };
const CLOSE_SPRING = { stiffness: 0.1, damping: 0.5 };

export default function getHeightSpring() {
  const heightSpring = spring(0);
  let shown = false;

  const getConfig = (open, val) => {
    let active = typeof val === "number";
    let immediate = open && !shown && active;
    // once we've had a proper height registered, we can animate in the future
    shown = shown || active;
    return immediate ? { hard: true } : {};
  };

  const sync = (open, height) => {
    heightSpring.set(open ? height || 0 : 0, getConfig(open, height));
    Object.assign(heightSpring, open ? OPEN_SPRING : CLOSE_SPRING);
  };

  return { sync, heightSpring };
}

这里有很多代码,但它们都是我们一直在编写的代码,只是打包到一个函数中。现在我们使用此动画的代码简化为以下内容

const { heightSpring, sync } = getHeightSpring();
$: heightStore = syncHeight(el);
$: sync(open, $heightStore);

您可以在 演示 的“Animate Height Cleanup”部分看到这一点。

一些 Svelte 特定的技巧

让我们暂停一下,考虑一下 Svelte 与 React 的一些区别,以及我们如何利用这些区别来进一步改进我们已经拥有的内容。

首先,我们一直在用来保存弹簧和更改高度值的存储与 React 的钩子不同,它们没有绑定到组件渲染。它们是可以在任何地方使用的普通 JavaScript 对象。而且,正如上面提到的,我们可以强制性地订阅它们,以便它们手动观察变化的值。

Svelte 还提供了一种名为 actions 的东西。这些是在 DOM 元素上添加的函数。当元素被创建时,Svelte 会调用该函数并将该元素作为第一个参数传递。我们还可以指定 Svelte 要传递的其他参数,并提供一个 update 函数,以便在这些值发生更改时重新运行该函数。我们可以做的另一件事是提供一个 cleanup 函数,以便 Svelte 在销毁元素时调用该函数。

让我们将这些工具组合到一个单独的操作中,我们可以简单地将其放到一个元素上以处理我们到目前为止编写的所有动画

export default function slideAnimate(el, open) {
  el.parentNode.style.overflow = "hidden";

  const { heightSpring, sync } = getHeightSpring();
  const doUpdate = () => sync(open, el.offsetHeight);
  const ro = new ResizeObserver(doUpdate);

  const springCleanup = heightSpring.subscribe((height) => {
    el.parentNode.style.height = `${ height }px`;
  });

  ro.observe(el);

  return {
    update(isOpen) {
      open = isOpen;
      doUpdate();
    },
    destroy() {
      ro.disconnect();
      springCleanup();
    }
  };
}

我们的函数使用我们要动画化的元素以及打开值来调用。我们将设置该元素的父元素使其具有 overflow: hidden。然后,我们使用之前相同的 getHeightSpring 函数,设置我们的 ResizeObserver 等。真正的魔力就在这里。

const springCleanup = heightSpring.subscribe((height) => {
  el.parentNode.style.height = `${height}px`;
});

我们没有将我们的 heightSpring 绑定到 DOM,而是手动订阅更改,然后手动设置高度。通常情况下,我们在使用 Svelte 等 JavaScript 框架时不会进行手动 DOM 更新,但在这种情况下,它是针对帮助程序库的,在我看来这很好。

在我们要返回的对象中,我们定义了一个 update 函数,Svelte 将在 open 值发生更改时调用该函数。我们更新传递给此函数的原始参数,该函数围绕该参数创建闭包,然后调用我们的 update 函数来同步所有内容。Svelte 在我们的 DOM 节点被销毁时调用 destroy 函数。

最重要的是,使用此操作非常简单

<div use:slideAnimate={open}>

就是这样。当 open 发生更改时,Svelte 会调用我们的 update 函数。

在我们继续之前,让我们再进行一个调整。请注意,我们通过在使用“切换”按钮折叠窗格时更改弹簧配置来消除弹性;但是,当我们通过单击“切换更多”按钮使元素变小时,它会以通常的弹性缩小。我不喜欢那样,我更喜欢缩小尺寸以使用我们用于折叠的相同物理方式移动。

让我们从 getHeightSpring 函数中删除这一行开始

Object.assign(heightSpring, open ? OPEN_SPRING : CLOSE_SPRING);

该行位于 getHeightSpring 创建的 sync 函数中,该函数基于 open 值在每次更改时更新我们的弹簧设置。删除它后,我们可以使用“打开”弹簧配置启动我们的弹簧

const heightSpring = spring(0, OPEN_SPRING);

现在,当我们的内容的高度发生更改或 open 值发生更改时,让我们更改弹簧设置。我们已经具备观察这两者发生变化的能力——我们的 ResizeObserver 回调在内容大小发生变化时触发,而我们操作的 update 函数在 open 发生变化时触发。

我们的 ResizeObserver 回调可以更改,如下所示

let currentHeight = null;
const ro = new ResizeObserver(() => {
  const newHeight = el.offsetHeight;
  const bigger = newHeight > currentHeight;

  if (typeof currentHeight === "number") {
    Object.assign(heightSpring, bigger ? OPEN_SPRING : CLOSE_SPRING);
  }
  currentHeight = newHeight;
  doUpdate();
});

currentHeight 保存当前值,我们在大小发生变化时检查它以查看我们正在移动的方向。接下来是 update 函数。以下是更改后的样子

update(isOpen) {
  open = isOpen;
  Object.assign(heightSpring, open ? OPEN_SPRING : CLOSE_SPRING);
  doUpdate();
},

同样的想法,但现在我们只检查 open 是否为 truefalse。您可以在 演示 的“Slide Animate”和“Slide Animate 2”部分看到这些迭代。

过渡

到目前为止,我们已经讨论了如何为页面上已存在的项目制作动画,但如何为首次渲染的对象制作动画呢?以及在它卸载时?这称为过渡,它内置于 Svelte中。文档非常出色地涵盖了常见用例,但有一点还没有(直接)得到支持:基于弹簧的过渡。

/explanation 请注意,Svelte 称为“过渡”和 CSS 称为“过渡”是截然不同的。CSS 意味着将一个值过渡到另一个值。Svelte 指的是元素完全“过渡”进出 DOM(CSS 几乎无法帮助做到这一点)。

明确地说,我们在这里进行的工作是为了将基于弹簧的动画添加到 Svelte 的过渡中。目前不支持这一点,因此需要一些技巧和变通方法,我们将在后面介绍。如果您不关心使用弹簧,那么可以使用 Svelte 的内置过渡,它们非常简单。再次查看文档以获取更多信息。

Svelte 中的过渡工作方式是,我们提供一个以毫秒(ms)为单位的持续时间以及一个可选的缓动函数,然后 Svelte 为我们提供一个回调,该回调具有从 0 到 1 运行的值,表示过渡进行到何种程度,我们将此值转换为我们想要的任何 CSS。例如

const animateIn = () => {
  return {
    duration: 2000,
    css: t => `transform: translateY(${t * 50 - 50}px)`
  };
};

…的使用方式如下

<div in:animateIn out:animateOut class="box">
  Hello World!
</div>

当该 <div> 首次装载时,Svelte 会

  • 调用我们的 animateIn 函数,
  • 超前方式快速使用从 0 到 1 的值调用我们结果对象上的 CSS 函数,
  • 收集我们不断变化的 CSS 结果,然后
  • 将这些结果编译成 CSS 关键帧动画,然后将其应用于传入的 <div>

这意味着我们的动画将作为 CSS 动画运行——而不是作为主线程上的 JavaScript——免费提供不错的性能提升。

变量 t 从 0 开始,导致平移 -50px。随着 t 越来越接近 1,平移越来越接近 0,即其最终值。出过渡基本上相同,但方向相反,并增加了检测盒子当前平移值的功能,从该位置开始。因此,如果我们添加它,然后快速删除它,该盒子将从其当前位置开始离开,而不是向前跳跃。但是,如果我们在它离开时重新添加它,它跳跃,我们将在稍后讨论这一点。

您可以在 演示 的“Basic Transition”部分运行此代码。

过渡,但使用弹簧

虽然有很多缓动函数可以改变动画的流动,但没有能力直接使用弹簧。但我们可以做的是找到一种方法来提前运行弹簧,收集产生的值,然后在我们的 css 函数使用从 0 到 1 运行的 t 值调用时,查找正确的弹簧值。因此,如果 t 为 0,我们显然需要弹簧的第一个值。当 t 为 0.5 时,我们希望处于中间的值,依此类推。我们还需要一个持续时间,它是 number_of_spring_values * 1000 / 60,因为每秒有 60 帧。

我们不会在这里编写该代码。相反,我们将使用已经存在于 svelte-helpers 库中的解决方案,这是我启动的一个项目。我从 Svelte 代码库中获取了一个小函数 spring_tick,然后编写了一个单独的函数来重复调用它,直到它完成,并在此过程中收集值。再加上从 t 到该数组中正确元素的转换(如果不存在直接匹配,则为加权平均值),这就是我们所需要的。Rich Harris 在后者方面提供了帮助,对此我表示感谢。

动画进入

让我们假设一个大的红色<div>是一个我们想要进行动画进入和退出的模态框。下面是animateIn函数的示例。

import { springIn, springOut } from "svelte-helpers/animation";
const SPRING_IN = { stiffness: 0.1, damping: 0.1 };

const animateIn = node => {
  const { duration, tickToValue } = springIn(-80, 0, SPRING_IN);
  return {
    duration,
    css: t => `transform: translateY(${ tickToValue(t) }px)`
  };
};

我们将想要弹簧到的值以及我们的弹簧配置传递给springIn函数。这将给我们提供一个持续时间,以及一个将当前tickToValue转换为当前值以应用于 CSS 的函数。就是这样!

动画退出

关闭模态框是相同的事情,只是有一个小小的调整。

const SPRING_OUT = { stiffness: 0.1, damping: 0.5, precision: 3 };

const animateOut = node => {
  const current = currentYTranslation(node);
  const { duration, tickToValue } = springOut(current ? current : 0, 80, SPRING_OUT);
  return {
    duration: duration,
    css: t => `transform: translateY(${ tickToValue(t) }px)`
  };
};

在这里,我们检查模态框的当前平移位置,然后将其用作动画的起点。这样,如果用户打开并快速关闭模态框,它将从其当前位置退出,而不是传送到 0,然后退出。这是因为animateOut函数是在元素卸载调用的,此时我们生成包含duration属性和css函数的对象,以便可以计算动画。

不幸的是,在元素退出过程中重新挂载该元素似乎不起作用,至少效果不好。animateIn函数不会重新调用,而是重复使用原始动画,这意味着它将始终从 -80 开始。幸运的是,对于典型的模态组件来说,这几乎肯定不会有任何问题,因为模态框通常通过单击某些东西来移除,例如背景覆盖,这意味着我们无法重新显示它,直到该覆盖层完成动画退出。此外,重复添加和删除具有双向过渡的元素可能会 tạo ra một bản demo thú vị, nhưng chúng không thực sự phổ biến trong thực tế, ít nhất là theo kinh nghiệm của tôi.

最后关于传出弹簧配置的一个快速说明:您可能已经注意到,我将精度设置得非常高(3,默认值为 0.01)。这告诉 Svelte 在判定“完成”之前,要有多接近目标值。如果您将默认值保留为 0.01,模态框将(几乎)到达其目的地,然后花费相当长的时间以不可察觉的方式越来越接近,然后判定它已完成,然后从 DOM 中将其移除。这会让人感觉模态框卡住了,或者有延迟。将精度移到 3 的值可以解决这个问题。现在模态框会动画到它应该去的地方(或足够接近),然后迅速消失。

更多动画

让我们在模态框示例中添加最后的调整。让我们在动画的同时让它淡入淡出。我们不能为此使用弹簧,因为,再次,我们需要为过渡有一个规范的持续时间,而我们的运动弹簧已经提供了该持续时间。但是弹簧动画通常适用于实际移动的项目,而不是其他任何东西。因此,让我们使用缓动函数来创建淡入淡出动画。

如果您需要帮助选择合适的缓动函数,请务必查看来自 Svelte 文档的这个便捷的可视化。我将使用quintOutquadIn函数。

import { quintOut, quadIn } from "svelte/easing";

我们新的animateIn函数看起来非常相似。我们的css函数做了之前做的事情,但也通过quintOut缓动函数运行tickToValue值以获取我们的opacity值。由于t在进入过渡期间从 0 运行到 1,在退出过渡期间从 1 运行到 0,因此我们无需在应用于opacity之前对其进行任何进一步操作。

const SPRING_IN = { stiffness: 0.1, damping: 0.1 };
const animateIn = node =>; {
  const { duration, tickToValue } = springIn(-80, 0, SPRING_IN);
  return {
    duration,
    css: t => {
      const transform = tickToValue(t);
      const opacity = quintOut(t);
      return `transform: translateY(${ transform }px); opacity: ${ opacity };`;
    }
  };
};

我们的animateOut函数类似,只是我们要获取元素的当前opacity值,并强制动画从那里开始。因此,如果元素正在淡入过程中,其不透明度为 0.3,我们希望将其重置为 1,然后淡出。相反,我们希望从 0.3 淡出。

将该起始不透明度乘以缓动函数返回的任何值将实现这一点。如果我们的t值从 1 开始,那么1 * 0.3就是 0.3。如果t是 0.95,我们执行0.95 * 0.3以获取一个值,它略小于 0.3,依此类推。

这是函数

const animateOut = node => {
  const currentT = currentYTranslation(node);
  const startOpacity = +getComputedStyle(node).opacity;
  const { duration, tickToValue } = springOut(
    currentT ? currentT : 0,
    80,
    SPRING_OUT
  );
  return {
    duration,
    css: t => {
      const transform = tickToValue(t);
      const opacity = quadIn(t);
      return `transform: translateY(${ transform }px); opacity: ${ startOpacity * opacity }`;
    }
  };
};

您可以在演示中使用“带有淡入淡出效果的弹簧过渡组件”运行此示例。

临别感言

Svelte 很有趣!在我( admittedly limited )的经验中,它往往会提供非常简单的原语,然后让你自己编写你需要的任何东西。我希望这篇文章能帮助解释如何在您的网络应用程序中将弹簧动画用得很好。

还有,提醒一下,在使用弹簧时要考虑可访问性,就像您对任何其他动画所做的那样。将这些技术与类似于prefers-reduced-motion的内容相结合,可以确保只有那些喜欢动画的人才能获得动画。