Tips for Writing Animation Code Efficiently

我已经从事Web动画并帮助他人做同样的事情多年了。然而,我还没有看到一个简明的提示列表,告诉人们该如何高效地构建动画,所以在我写了这篇文章!

我将使用GreenSock动画平台(GSAP)。它提供了一个简单易读的API,并解决了跨浏览器的不一致性,让您可以专注于动画制作。即使您从未使用过GSAP,代码和概念也应该是可以理解的。如果您想先熟悉GSAP的基础知识,以便能够更好的学习这篇文章的内容,最好的开始方式是通过GSAP’s getting started page这篇入门文章进行学习(包括视频)。

Tip #1:使用一个动画库

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

浏览器错误、不一致性和兼容性:像GSAP这样的动画库可以为您解决这些问题,并具有普遍的兼容性。您甚至可以在IE9中使用运动路径!在处理跨浏览器问题时,有许多问题,包括处理SVG元素上的transform-origin、路径描边测量、Safari中的3D起点等等,当然还有很多其他问题,这里就不一一列举了。

动画工作流程:使用GSAP这样的工具构建即使是中等复杂度的动画也更快、更有趣。您可以将动画模块化,嵌套它们,调整它们的时间轴。这使得作一些效果实验变得更加容易。相信我:一旦您尝试在CSSGSAP中构建时间线模式的动画效果,您就会明白我的意思。天壤之别!后期的修改调整也会更快。

不仅仅是DOM的动画CanvasWebGL、通用对象和复杂字符串不能使用原生技术进行动画处理。对于所有动画使用一个一致的工具更加简洁。

运行时控制:使用一个好的动画库可以使您暂停、恢复、反转、搜索或甚至逐渐改变整个时间线动画的速度。你可以独立地控制每个变换组件(旋转、缩放、xy、倾斜等)。你也可以随时检索这些值。JavaScript动画为你提供了终极的灵活性。

缓动选项(弹跳、弹性等)CSS只为缓动提供了两个控制点。GSAPCustomEase让您可以实现任何您想象得到的缓动效果。

延迟平滑:如果CPU变慢,GSAP可以优先考虑绝对时间或即时调整以避免动画卡顿跳跃。

高级功能:使用GSAP,很容易就可以变形SVG、添加物理/惯性、直接在浏览器中编辑运动路径、使用位置感知的交错动画等等。

行业中大多数顶级动画师都使用像GSAP这样的工具,因为他们多年来已经了解到了靠原生技术来实现复杂的动画效果就是会遇到很多问题。一旦你开始写复杂或者高级的动画效果,JavaScript动画库将使您的生活变得更加轻松,开启全新的可能性。

Tip #2:使用时间线

一个好的动画库将提供一种创建单个动画(称为tween)和按顺序排列动画的时间线的方法。将时间线视为 tweens 的容器,你可以在时间线中对各个动画进行组织排列他们的执行顺序。

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

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中,默认情况下,添加到时间轴中的tweens将等待到前一个 tweens 完成后才会运行。+=0.5还会添加额外的偏移或半秒延迟,因此第二个tween将在第一个tween完成后0.5秒开始,无论第一个tween的持续时间有多长。

要将tween之间的间隔时间增加到1秒,您只需要将+=0.5更改为+=1!非常容易。使用这种方法,您可以快速迭代动画,而不必担心要计算先前持续时间等问题。

Tip #3:使用相对值

关于相对值,我主要是想说以下三点

通过当前值来产生变化的值。GSAP识别+=-=前缀。因此,x:"+=200”将在当前x的基础上添加200个单位(通常是像素)。而x:“-=200”将从当前值中减去200。在GSAP的位置参数中,这也对相对于彼此定位的tweens非常有用。

1

当值需要响应视口大小更改时,请使用相对单位(如vwvh和在某些情况下为)。

2

尽可能使用.to().from()方法(而不是.fromTo()),以便动态从其当前值填充起始或结束值。这样,您不需要在每个tween中声明起始和结束值。耶,少打字!例如,如果您有一堆不同颜色的元素,您可以将它们全部动画化为黑色,如gsap.to(“.class”,{backgroundColor:“black”})

3

Tip #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的设置让每个tween之间产生一定的间隔(如果你设置负数,那么动画会重叠)

Tip #5:使用默认值

GSAP有默认值,例如ease(“power1.out”)duration0.5秒)等属性。因此,以下是有效的tween,将动画时长为0.5秒。

gsap.to(".box", { color: "black" })
// 使用线性缓动曲线 和 1秒的动画时长
gsap.defaults({ ease: "none", duration: 1 });

GSAP可以改变全局的默认设置,使用gsap.default()进行设置

这很方便,但更常见的是为特定时间轴设置默认值,以便仅影响其子级。例如,我们可以通过在父时间轴上设置默认值来避免为每个子tween输入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")

Tip #6:同时驱动多个元素

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

如果您有多个共享相同类别的.box元素,则上面的代码将同时动画化所有元素!

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

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

或者,只要是相同类型(选择器字符串,变量引用,通用对象等),您就可以传递使用数组进行一起传递:

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

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

Tip #7:使用基于函数的值、stagger、loop

基于函数的值

对于几乎任何属性,使用函数而不是数字/字符串,GSAP将在第一次呈现tween时为每个目标调用该函数。此外,它将使用函数返回的任何内容作为属性值!这对于使用单个tween创建大量不同的动画以及添加差异非常方便。

GSAP将向函数传递以下参数:

  • 索引

  • 正在受影响的特定元素

  • tween影响的所有元素的数组

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

或者你可以从数组中选择元素:

Staggers

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

您还可以传递一个对象以获得更复杂的stagger效果,包括从网格中心向外辐射的效果或随机化时间:

关于GSAP中的Stagger效果的更多信息,可以查看stagger文档

Loops 循环

循环遍历元素列表以创建或应用动画可能很有帮助,特别是当它们基于某些事件时,例如用户的交互(稍后我将讨论此问题)。

要循环遍历元素,最简单的方法是使用.forEach()。但是由于IE不支持在使用.querySelectorAll()选择的元素上使用.forEach(),因此可以改用GSAPutils.toArray()函数。

在下面的示例中,我们正在循环遍历每个容器,以向其子元素添加仅适用于该容器的动画。

Tip #8:模块化你的动画

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

Functions 函数

使用函数去返回tween对象或者时间线对象然后把他们放到主时间线上:

function doAnimation() {
  // 可以进行一些处理,比如计算,使用传入该函数的参数数据
  
  // r返回一个tween,可以使用上面计算后的数据
  return gsap.to(".myElem", { duration: 1, color: "red"});
}

tl.add( doAnimation() );
function doAnimation() {
  const tl = gsap.timeline();
  tl.to(...);
  tl.to(...);
  // 可以添加任意多的动画效果

  // 结束之后返回时间线实例
  return tl;
}

const master = gsap.timeline();
master.add( doAnimation() );
master.add( doAnotherAnimation() );
// 可以添加更多的时间线

嵌套的时间线的方式真的可以改变你的动画组织逻辑。它能让你轻松的实现各种复杂的动画逻辑,同时保持你代码的简洁的逻辑和可读性。

这是一个从真实的使用案例调整而来的效果:

这是一个更复杂的案例,用的也是类似的实现思路:

var tl; // 设置一个引用,可以访问到时间线实例

function buildAnimation() {
  var time = tl ? tl.time() : 0; // 储存实例动画已经进行了多长的时间

  // 如果实例已经存在,就销毁它
  if (tl) {
    tl.kill();
  }

  // 创建一个新的时间线实例
  tl = gsap.timeline();
  tl.to(...)
    .to(...); // 开始动画
  tl.time(time); // 把动画时间线设置到它已经进行到的位置
}

buildAnimation(); // 启动

window.addEventListener("resize", buildAnimation); // 处理浏览器窗口变化

将动画构建例程包装在函数中,还可以使重新创建动画(例如,在调整大小时)变得轻而易举!

Effects 效果

如果您发现自己在写重复相同的代码并切换一个变量来代替另一个变量,通常这是您应该制作一个通用函数或使用循环的信号,以使您的代码保持简洁(不要重复自己)。

使用效果,您可以将自定义动画转换为一个命名效果,可以随时使用新的目标和配置调用它。当你对动画有标准或者如果你将从不同的上下文中调用相同的动画时,这将非常有帮助。

这里有一个超级简单的“淡入淡出”效果来展示这个概念:

// 在GSAP上注册effect:
gsap.registerEffect({
    name: "fade",
    defaults: {duration: 2}, // 在这个defaults设置的数据可以通过下方的config参数获取到进行设置
    effect: (targets, config) => {
        return gsap.to(targets, {duration: config.duration, opacity:0});
    }
});

// 然后我们可以这样使用了:
gsap.effects.fade(".box");
//或者还可以把默认的设置覆盖掉:
gsap.effects.fade(".box", {duration: 1});

Tip #9:使用control方法

GSAP 提供了许多控制补间或时间线状态的方法。它们包括 .play()、.pause()、.reverse()、.progress()、.seek()、.restart()、.timeScale() 等等。

使用控制方法可以使动画之间的过渡更加流畅(例如能够在部分过程中反转)并且更高效(通过重用相同的补间/时间线而不是每次创建新实例)。通过给您更精细的控制动画的状态,它也可以帮助调试。

这里是一个简单的案例:

有个很棒的案例是我们通过控制timeScale来控制时间线动画来实现的效果:

案例:交互事件触发动画

在用户交互事件的事件回调函数内部,我们可以使用控制方法来对动画的播放状态进行精细控制。

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

案例:在时间线上多个状态之间实现变化

您可能希望一组动画影响相同元素的相同属性,但仅在特定序列中(例如,活动/非活动状态,每个状态具有鼠标悬停/鼠标移出状态)。这可能会变得棘手。我们可以使用时间线的状态和控制事件来简化它。

案例:基于滚动位置的动画

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

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

但是,如果要这样做,出于性能原因最好限制滚动监听器:

然而,每次需要在滚动时执行动画时都设置这些内容是很麻烦的。您可以使用GreenSock的官方滚动插件ScrollTrigger来为您完成这个过程(以及更多)。

Bonus Tip:使用GSAP的插件、utility方法、以及helper functions

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

Plugins 插件

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

MorphSVG在任何两个SVG形状之间进行变形,无论点数如何,并且可以精细控制形状的变形。

DrawSVG逐步显示(或隐藏)SVG元素的描边,使其看起来像正在被绘制。它可以解决影响典型描边偏移量动画的各种浏览器错误。

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

GSDevTools为您提供了一个可视化UI,用于与GSAP动画进行交互和调试,具有高级播放控件、键盘快捷键、全局同步等功能。

Draggable提供了一种出乎意料地简单的方法,可以使用鼠标或触摸事件使几乎任何DOM元素可拖动、可旋转、可抛掷,甚至可使用轻扫滚动。DraggableInertiaPlugin美观地(并且可选地)集成在一起,因此用户可以轻扫并根据动量平稳地减速。

CustomEase(以及CustomBounceCustomWiggle)通过使您能够注册任何所需缓动来增加了GSAP已经广泛的缓动能力。

SplitText是一个易于使用的JavaScript功能模块,允许您将HTML文本拆分为字符、单词和行。它易于使用、极其灵活,可以一直使用到IE9,并且可以为您处理特殊字符。

ScrambleText使用随机字符混淆DOM元素中的文本,定期刷新新的随机字符,同时在缓动过程中逐渐显示您的新文本(或原始文本)。在视觉上,它看起来像计算机正在解码一串文本。

Physics2D允许您根据速度和加速度而不是特定值来补间元素的位置。PhysicsProps类似,但适用于任何属性,而不仅仅是2D坐标。

Utility 方法

GSAP内置了实用程序方法,可以使一些常见任务更容易完成。大多数方法都专注于以特定方式操纵值,在生成或修改动画值时特别有帮助。我经常使用的是.wrap()、.random、.interpolate()、.distribute()、.pipe()和.unitize(),但还有许多其他方法可能会对您有所帮助。

Helper functions 有用的代码

除了在GSAP中的程序方法,还有一些更轻量的,是一些帮助函数,它们不是内置于GSAP核心中,而是GreenSock多年来为处理特定用例而创建的一些帮助函数。这些函数使FLIP动画、基于缓动曲线返回随机数、混合两个缓动曲线等变得容易。我强烈建议您查看它们!