高效编写动画代码的技巧

Avatar of Zach Saucier
Zach Saucier 发布

DigitalOcean 提供适用于旅程各个阶段的云产品。 立即使用 200 美元免费赠送额度!

我已经编写了多年的网页动画代码并帮助他人做同样的事情。 然而,我还没有看到一份简洁的技巧清单,重点介绍如何*高效地*构建动画,所以现在就给你吧!

我将使用 GreenSock Animation Platform (GSAP)。 它提供了一个简单易读的 API 并解决了跨浏览器的不一致性,以便你可以专注于动画。 即使你以前从未使用过 GSAP,代码和概念也应该易于理解。 如果你想先熟悉 GSAP 的基础知识以便充分利用本文,最好的起点是 GSAP 的入门页面(包括视频)。

技巧 #1:使用动画库

一些开发者认为使用动画库是浪费的,因为他们可以使用原生浏览器技术,如 CSS 转换、CSS 动画或 Web Animations API (WAAPI) 来完成相同的事情,而无需加载库。 在某些情况下,这是真的。 然而,还有以下几点因素需要考虑

  • 浏览器错误、不一致性和兼容性: 动画库,如 GSAP,为您解决了这些问题,并且具有普遍兼容性。 您甚至可以在 IE9 中使用运动路径! 在跨浏览器问题方面,存在许多问题区域,包括处理 SVG 元素上的 transform-origin、路径笔划测量、Safari 中的 3D 原点,以及我们没有空间列出的许多其他问题。
  • 动画工作流程: 使用 GSAP 等工具构建即使是中等复杂度的动画也要快得多,也更有趣。 您可以模块化动画,将它们嵌套到您想要的任何深度,并让它们的计时自动调整。 这使得实验变得容易得多。 相信我: 一旦你尝试构建一个动画序列 在 CSS 中,然后 在 GSAP 中,你就会明白我的意思。 天壤之别! 未来的编辑速度也更快。
  • 超越 DOM 的动画: Canvas、WebGL、通用对象和复杂字符串无法使用原生技术进行动画处理。 对所有动画使用一个一致的工具要*干净得多*。
  • 运行时控制: 使用一个好的动画库可以使你能够暂停、恢复、反转、搜索甚至逐渐改变整个动画序列的速度。 您可以独立控制每个转换组件(旋转、缩放、x、y、倾斜等)。 您也可以随时检索这些值。 JavaScript 动画为您提供了终极灵活性。
  • 缓动选项(弹跳、弹性等): CSS 仅为缓动提供了两个控制点。 GSAP 的 CustomEase 允许您从字面上创建您能想到的任何缓动。
  • 滞后平滑: GSAP 可以优先考虑绝对计时或在 CPU 陷入困境时动态调整内容以避免跳跃。
  • 高级功能: 使用 GSAP,可以轻松地 变形 SVG,添加物理/惯性,直接在浏览器中编辑 运动路径,使用位置感知交错等等。

行业中大多数顶尖动画师都使用 GSAP 等工具,因为他们多年来已经学到了这些相同的东西。 一旦你超越了非常基本的动画,JavaScript 库将让你的生活变得轻松得多,并打开全新的可能性。

技巧 #2:使用时间轴

一个好的动画库将提供一些方法来创建单个动画(称为补间动画)以及在时间轴中排序动画的方法。 将时间轴视为补间动画的容器,您可以在其中根据彼此的位置对它们进行定位。

大多数情况下,只要您需要动画按顺序运行,就应该使用时间轴。

const tl = gsap.timeline(); 
tl.to(".box", { duration: 1, x: 100 })
  .to(".box", { duration: 1, backgroundColor: "#f38630" }, "+=0.5") 
  .to(".box", { duration: 1, x: 0, rotation: -360 }, "+=0.5")

在 GSAP 中,默认情况下,添加到时间轴的补间动画将等待之前的补间动画完成之后再运行。 +=0.5 还会添加 0.5 秒的额外偏移量或延迟,因此无论第一个补间动画的持续时间多长,第二个补间动画将在第一个补间动画结束后的 0.5 秒后开始。

要将补间动画之间的时间间隔增加到 1 秒,只需将 +=0.5 更改为 +=1! 超级简单。 使用这种方法,您可以快速迭代动画,而不必担心计算组合之前持续时间。

技巧 #3:使用相对值

我所说的“相对值”是指以下三点

  1. 将值相对于其当前值进行动画处理。 GSAP 识别 +=-= 前缀。 因此 x: "+=200" 将在当前 x 上添加 200 个单位(通常是像素)。 并且 x: "-=200" 将从当前值中减去 200。 这在 GSAP 的 位置参数 中也很有用,在将补间动画彼此定位时。
  2. 当值需要响应视窗大小变化时,使用相对单位(如 vwvh 以及某些情况下的 %)。
  3. 尽可能使用 .to().from() 方法(而不是 .fromTo()),以便从当前值动态填充开始值或结束值。 这样,你就不需要在每个补间动画中声明开始值和结束值。 太棒了,更少的输入! 例如,如果你有一堆颜色不同的元素,你可以将它们全部动画处理为黑色,例如 gsap.to(".class", {backgroundColor: "black" })

技巧 #4:使用关键帧

如果你发现自己在同一行代码中一次又一次地对同一个目标进行动画处理,那么这就是使用关键帧的完美时机! 你可以这样操作

gsap.to(".box", { keyframes: [
  { duration: 1, x: 100 },
  { duration: 1, backgroundColor: "#f38630", delay: 0.5 }, 
  { duration: 1, x: 0, rotation: -360, delay: 0.5 }
]});

无需时间轴! 要间隔补间动画,我们只需在每个关键帧中使用 delay 属性。(它可以为负数以创建重叠。)

技巧 #5:使用智能默认值

GSAP 为 ease("power1.out")和 duration(0.5 秒)等属性设置了默认值。 因此,以下是一个有效的补间动画,它将进行半秒钟的动画处理。

gsap.to(".box", { color: "black" })

要更改 GSAP 的全局默认值,请使用 gsap.defaults()

// Use a linear ease and a duration of 1 instead
gsap.defaults({ ease: "none", duration: 1 });

这可能很方便,但更常见的是为特定时间轴设置默认值,以便它只影响其子级。 例如,我们可以通过在父时间轴上设置默认值来避免为每个子补间动画键入 duration: 1

const tl = gsap.timeline({ defaults: { duration: 1 } }); 
tl.to(".box", { x: 100 })
  .to(".box", { backgroundColor: "#f38630" }, "+=0.5") 
  .to(".box", { x: 0, rotation: -360 }, "+=0.5")

技巧 #6:一次对多个元素进行动画处理

我们在第三个技巧中简要提到过这一点,但它值得拥有它自己的技巧。

如果你有多个元素共享同一个 .box 类,上面的代码将同时对所有元素进行动画处理!

您也可以使用更复杂的 selector 字符串来选择多个具有不同选择器的元素

gsap.to(".box, .circle", { ... });

或者,只要元素类型相同(selector 字符串、变量引用、通用对象等),您可以传递一个变量引用的数组。

var box = document.querySelector(".box");
var circle = document.querySelector(".circle");

// some time later…
gsap.to([box, circle], { ... });

技巧 #7:使用基于函数的值、交错和/或循环

基于函数的值

在几乎任何属性中使用函数代替数字/字符串,GSAP 将在首次渲染动画时为每个目标调用该函数一次。此外,它将使用函数返回的任何内容作为属性值!这对于使用单个动画创建大量不同的动画以及添加变化非常有用。

GSAP 将传递以下参数到函数中

  1. 索引
  2. 正在影响的特定元素
  3. 受动画影响的所有元素的数组

例如,您可以根据索引设置移动方向

或者您可以从数组中选择项目

交错

通过使用交错来偏移开始时间,使您的动画看起来更加动态和有趣。对于单个动画中的简单交错偏移,只需使用stagger: 0.2 在每个动画的开始时间之间添加 0.2 秒。

您还可以传入一个对象以获得更复杂的交错效果,包括从网格中心向外发散的效果或随机化时间

有关 GSAP 交错的更多信息,请查看交错文档

循环

循环遍历元素列表以创建或应用动画可能很有用,尤其是在它们基于某些事件时,例如用户的交互(我将在后面讨论)。 

要循环遍历项目列表,最简单的方法是使用.forEach()。但由于这在 IE 中使用.querySelectorAll() 选择的元素上不受支持,您可以改用 GSAP 的utils.toArray() 函数。

在下面的示例中,我们循环遍历每个容器,为其子元素添加动画,这些动画的作用域限定在该容器内。

技巧 #8:模块化您的动画

模块化是编程的关键原则之一。它允许您构建小的、易于理解的块,您可以将它们组合成更大的创作,同时保持代码整洁、可重用且易于修改。它还允许您使用参数函数作用域,提高代码的可重用性。

函数

使用函数返回动画或时间轴,然后将它们插入主时间轴中

function doAnimation() {
  // do something, like calculations, maybe using arguments passed into the function
  
  // return a tween, maybe using the calculations above
  return gsap.to(".myElem", { duration: 1, color: "red"});
}

tl.add( doAnimation() );

嵌套时间轴可以真正改变您动画的方式。它让您可以轻松地对非常复杂的动画进行排序,同时保持代码模块化和可读性。

function doAnimation() {
  const tl = gsap.timeline();
  tl.to(...);
  tl.to(...);
  // ...as many animations as you’d like!

  // When you’re all done, return the timeline
  return tl;
}

const master = gsap.timeline();
master.add( doAnimation() );
master.add( doAnotherAnimation() );
// Add even more timelines!

这是一个从 Carl Schooff 的“编写更智能的动画代码”文章中修改的真实案例。

这是一个更复杂的演示,展示了使用 Craig Roblewsky 的星球大战主题的相同技术

将您的动画构建例程包装在函数中还可以轻松地重新创建动画(例如,在调整大小后)!

var tl; // keep an accessible reference to our timeline

function buildAnimation() {
  var time = tl ? tl.time() : 0; // save the old time if it exists

  // kill off the old timeline if it exists
  if (tl) {
    tl.kill();
  }

  // create a new timeline
  tl = gsap.timeline();
  tl.to(...)
    .to(...); // do your animation
  tl.time(time); // set the playhead to match where it was
}

buildAnimation(); //kick things off

window.addEventListener("resize", buildAnimation); // handle resize

如果您发现自己重复相同的代码并用另一个变量替换一个变量,这通常表明您应该创建一个通用函数或改用循环,以使代码 DRY(不要重复自己)。

效果

使用效果,您可以将自定义动画转换为命名效果,该效果可以随时使用新目标和配置调用。当您对动画有标准或将从不同的上下文调用相同的动画时,这尤其有用。

这是一个非常简单的“淡入淡出”效果,用于演示该概念

// register the effect with GSAP:
gsap.registerEffect({
    name: "fade",
    defaults: {duration: 2}, //defaults get applied to the "config" object passed to the effect below
    effect: (targets, config) => {
        return gsap.to(targets, {duration: config.duration, opacity:0});
    }
});

// now we can use it like this:
gsap.effects.fade(".box");
// Or override the defaults:
gsap.effects.fade(".box", {duration: 1});

技巧 #9:使用控制方法

GSAP 提供了许多方法来控制动画或时间轴的状态。它们包括.play().pause().reverse().progress().seek().restart().timeScale() 和其他一些方法。 

使用控制方法可以使动画之间的过渡更加流畅(例如能够在中途反转)以及更高效(通过重用相同的动画/时间轴,而不是每次都创建新实例)。通过让您更精细地控制动画的状态,它还可以帮助调试。

这是一个简单的示例

一个很棒的用例是对时间轴的timeScale 进行动画处理!

用例:触发动画的交互事件

在用户交互事件的事件侦听器中,我们可以使用控制方法来精细控制动画的播放状态。

在下面的示例中,我们为每个元素创建一个时间轴(以便它不会在所有实例上触发相同的动画),将该时间轴的引用附加到元素本身,然后在悬停元素时播放相关时间轴,在鼠标离开时反转它。

用例:在时间轴的多个状态之间进行动画处理

您可能希望一组动画影响相同元素的相同属性,但仅在特定顺序中(例如活动/非活动状态,每个状态都有鼠标悬停/鼠标离开状态)。管理起来可能会很棘手。我们可以使用时间轴的状态和控制事件来简化它。 

用例:根据滚动位置进行动画处理

我们可以轻松地使用控制方法根据滚动位置触发动画。例如,此演示在达到滚动位置后会播放完整的动画

您还可以将动画的进度附加到滚动位置以获得更花哨的滚动效果!

但是,如果您要这样做,最好为性能原因对滚动侦听器进行节流

但是,每次您需要在滚动时进行动画时都设置起来很麻烦。您可以使用 GreenSock 的官方滚动插件,称为ScrollTrigger 来为您执行此操作(以及更多操作)!

额外提示:使用 GSAP 的插件、实用方法和辅助函数

GSAP 插件为 GSAP 的核心添加了额外的功能。一些插件使与渲染库(如 PixiJS 或 EaselJS)一起工作变得更容易,而其他插件则添加了变形 SVG、拖放功能等超能力。这使 GSAP 核心保持相对较小,并允许您在需要时添加功能。

插件

ScrollTrigger 使用最少的代码创建令人惊叹的基于滚动的动画。或者触发任何与滚动相关的操作,即使它与动画无关。

MorphSVG 在任何两个 SVG 形状之间进行变形,无论点数多少,并提供对形状变形方式的精细控制。

DrawSVG 逐步显示(或隐藏)SVG 元素的笔划,使其看起来像是正在绘制。它绕过了影响典型笔划偏移动画的各种浏览器错误。

MotionPath任何浏览器中沿运动路径动画化任何内容(SVG、DOM、canvas、通用对象等)。您甚至可以使用 MotionPathHelper 在浏览器中编辑路径!

GSDevTools 为您提供与 GSAP 动画交互和调试的视觉 UI,包含高级播放控制、键盘快捷键、全局同步等功能。

Draggable 提供了一种非常简单的方法,可以使用鼠标或触摸事件使几乎任何 DOM 元素可拖动、可旋转、可抛掷,甚至可使用轻扫滚动。Draggable 与 InertiaPlugin 完美集成(可选),因此用户可以轻扫并让运动根据动量平滑地减速。

CustomEase(以及 CustomBounceCustomWiggle)通过允许您注册任何您喜欢的缓动来增强 GSAP 已经很强大的缓动功能。

SplitText 是一种易于使用的 JavaScript 工具,允许您将 HTML 文本拆分为字符、单词和行。它易于使用,非常灵活,可以追溯到 IE9,并为您处理特殊字符。

ScrambleText 使用随机字符对 DOM 元素中的文本进行混排,以定期刷新新的随机字符,同时在补间动画过程中逐渐显示新文本(或原始文本)。从视觉上看,它就像一台电脑解码文本字符串一样。

Physics2D 允许您基于速度和加速度对元素的位置进行补间动画,而不是转到特定的值。 PhysicsProps 类似,但适用于任何属性,而不仅仅是二维坐标。

实用方法

GSAP 内置了 实用方法,可以使一些常见任务更轻松。大多数方法侧重于以特定方式操作值,这在生成或修改动画值时尤其有用。我经常使用的实用方法是 .wrap().random.interpolate().distribute().pipe().unitize(),但还有 许多其他方法 可能会对您有所帮助。

辅助函数

类似地,但并非内置于 GSAP 的核心,GreenSock 这些年来创建了一些 辅助函数 来处理特定的用例。这些函数可以轻松地将动画 FLIP,根据缓动曲线返回随机数,混合两个缓动曲线等等。我强烈推荐 查看它们

结论

您已经看到了最后!希望您在此过程中学到了一些东西,并且这篇文章将在未来几年继续成为您的资源。

与往常一样,如果您对 GSAP 有任何问题,请随时访问 GreenSock 论坛。它们非常有用且欢迎您!作为 GreenSock 的员工,我经常在那里闲逛;我喜欢帮助人们解决与动画相关的挑战!