Tween.js使用指南与详解(最新推荐)
作者:sKK07
介绍与定位
做3D项目开发需要运用到大量动画,由此使用到了Tween插件。可以简单地实现流畅丝滑的动画效果。通过参数的配置和事件的监听能实现复杂多变的动画形式。也可用于普通web项目的动画上。
安装
- npm 添加依赖
npm install @tweenjs/tween.js
- 引入
- 使用构建工具
import * as TWEEN from '@tweenjs/tween.js'
- 没有构建工具
// 如果将 node_modules 作为网站的一部分提供服务,则可以使用 importmap script 标签从 node_modules 导入。 首先,假设 node_modules 位于网站的根目录,可以编写一个导入映射: <script type="importmap"> { "imports": { "@tweenjs/tween.js": "/node_modules/@tweenjs/tween.js/dist/tween.esm.js" } } </script> // 其他页面 import * as TWEEN from '@tweenjs/tween.js'
- 使用构建工具
- 其他引入——从 CDN 安装
//cdnjs: <script src="https://cdnjs.cloudflare.com/ajax/libs/tween.js/23.1.3/tween.umd.js"></script> // unpkg.com: <script src="https://unpkg.com/@tweenjs/tween.js@^23.1.3/dist/tween.umd.js"></script>
什么是 tween ?tween 是如何工作的?为什么要使用 tween ?
补间(动画)(来自 in-between)是一个概念,允许以平滑的方式更改对象的属性。只需告诉它哪些属性要更改,当动画结束运行时它们应该具有哪些最终值,以及这需要多长时间,动画引擎将负责计算从起始点到结束点的值。
例如,position
对象拥有 x
和 y
两个坐标,需要将 x
坐标的值从 100
变成 200
:
const position = {x: 100, y: 0} // 首先为位置创建一个补间(tween) const tween = new TWEEN.Tween(position) // 然后告诉 tween 我们想要在1000毫秒内以动画的形式改变 x 的位置 tween.to({x: 200}, 1000)
如此就创建好了动画过程,但是发现没有效果是因为它还没被激活(使用),需要启动:
// 启动 tween.start()
此时发现还是没有变化,那是因为动画执行了,但是视图没有更新,需要在主函数中调用 TWEEN.update
,如下使用:
animate() function animate() { // 动画帧函数,跟设备的刷新频率有关,每刷新一次调用一次 requestAnimationFrame(animate) // [...] TWEEN.update() // [...] }
如此在更新每帧的时候都会运行补间动画;经过 1
秒(1000
毫秒)后 position.x
将会变成 200
。
除非在控制台中打印出 x
的值,不然看不到它的变化。你可能想要使用 onUpdate 回调:
tween.onUpdate(function (object) { console.log(object.x) })
每次更新补间时都会调用此函数; 这种情况发生的频率取决于许多因素——例如,计算机或设备的速度有多快(以及有多繁忙!)。
到目前为止,只使用 tweens
将值打印到控制台,也可以将它用于 three.js
对象的动画位置之类的事情:
const tween = new TWEEN.Tween(cube.position).to({x: 100, y: 100, z: 100}, 10000).start() animate() function animate() { requestAnimationFrame(animate) TWEEN.update() threeRenderer.render(scene, camera) }
在这种情况下,因为 three.js
渲染器将在渲染之前查看对象的位置,所以不需要使用的 onUpdate
回调。
由上可见:tween.js
可以链式调用! 每个 tween
函数都会返回 ``tween` 实例,所以可以重写下面的代码:
const tween = new TWEEN.Tween(position) tween.to({x: 200}, 1000) tween.start()
改成如下:
const tween = new TWEEN.Tween(position).to({x: 200}, 1000).start()
如此会方便很多,之后的例子也多采用这种方式。
tween.js 的动画
Tween.js
不会自行运行。需要显式的调用 update
方法来告诉它何时运行。推荐的方法是在主动画循环中执行这个操作。使用 ``requestAnimationFrame` 调用此循环以获得最佳的图形性能。
比如:
animate() function animate() { requestAnimationFrame(animate) // [...] TWEEN.update() // [...] }
如果不带参数调用,update
将会判断当前时间点,以便找出自上次运行以来已经过了多长时间。
当然也可以传递一个明确的时间参数给 update 来更新:
TWEEN.update(startTime + 100)
意思是“更新时间 = 开始时间后的 100 毫秒”。可以使用它来确保代码中的所有时间相关函数都使用相同的时间值。例如,假设有一个播放器,并希望同步运行补间:
// 创建一个对象,其属性将被补间 const object = { x: 0 ,y:0}; // 获取当前时间戳 const startTime = Date.now(); // 创建一个补间动画,将对象的 x 属性从 0 渐变到 100,持续时间为 1000 毫秒 const tween1 = new TWEEN.Tween(object) .to({ x: 100 }, 1000) .start(startTime); // 创建另一个补间动画,将对象的 y 属性从 0 渐变到 200,持续时间为 1000 毫秒 const tween2 = new TWEEN.Tween(object) .to({ y: 200 }, 1000) .start(startTime); // 手动更新补间动画,传入相对于起始时间 500 毫秒后的时间戳 TWEEN.update(startTime + 500); // 两者采用了同一开始时间点,所以它们在 500 毫秒后都完成 50% 的动画。 console.log(object.x); // 输出应该为 50,因为动画完成了 50% console.log(object.y); // 输出应该为 100,因为动画完成了 50%
注:必须在
start()
中使用 同一个 开始时间点,在update()
中使用和start()
中一样的时间点并后延对应时间才有效果。
控制一个补间
start 和 stop
到目前为止,我们已经了解了 Tween.start
方法,但是还有更多的方法来控制单个补间。也许最重要的一个是 start
对应的方法:stop
。 如果想取消一个补间,只要通过一个单独的补间调用这个方法:
tween.stop()
注:停止一个从未开始或已经停止的补间没有任何效果。 没有错误被抛出。
start
方法可接受一个time
参数,应为时间戳。如果使用它,补间直到那个特定的时刻才会开始;否则它将立即开始(即在下一次调用TWEEN.update
时,不调用update
补间对象的值是 不变的 )。start
方法也可接受第二个布尔
参数:当为true
时,使用的补间将从目标对象中止或完成
的值开始,而不是从初始值
开始。用于停止一个动画,然后开启一个新的动画,新动画从上一个动画结束的位置开始。
startFromCurrentValues
这是 tween.start(undefined, true)
的别名,使当前使用的补间从目标对象的上一个补间的最后一个值开始,而不是从头开始。
update
补间有一个 update
方法(不常用)。 这实际上是由 TWEEN.update
调用的,用于仅使用一个简单动画构建的补间。
在下面的示例中,第二个参数告诉新的 Tween
不要将自己添加到默认组
(Tween
是 TWEEN.Group
的一个实例)。如果补间不与组关联(请注意,可以通过将 组
作为第二个参数传递给构造函数来关联组),则补间需要使用其 update
方法手动更新,如下所示:
const tween = new TWEEN.Tween(someObject, false).to(/*...*/).start() function animate() { // 通过自己的update来更新补间。此时使用TWEEN.update(),tween补间不会执行。 tween.update() requestAnimationFrame(animate) }
注: 如果你使用
TWEEN.update()
作为默认控制所有补间的方式,则无需直接调用tween.update()
,但是建议直接如上例所示 创建自己的补间组 或手动更新补间。 使用组或单独控制的补间的概念很像避免在JavaScript
代码中使用 全局变量 的做法:它可以防止一个组件意外破坏其他一些不相关组件的行为。
chain
当按顺序排列不同的补间时,事情会变得更有趣,例如在上一个补间结束的时候立即启动另外一个补间。称此为 链接补间 ,它是通过 chain
方法完成的。因此,要使 tweenB
在 tweenA
完成后开始:
tweenA.chain(tweenB)
或者,可以创造一个无限的链式,tweenA
完成时开始 tweenB
,tweenB
完成时开始 tweenA
:
tweenA.chain(tweenB) tweenB.chain(tweenA)
在其他情况下,你可能希望将多个补间链接到另一个补间,使它们(链接的补间)都同时开始动画:
tweenA.chain(tweenB, tweenC)
注: 调用
tweenA.chain(tweenB)
实际上修改了tweenA
,所以tweenB
总是在tweenA
完成时启动。chain
的返回值只是tweenA
,不是一个新的tween
。类比js中数组的 concat 方法。
repeat
如果想让一个补间永远重复,可以链接到自己,但更好的方法是使用 repeat
方法。它接受一个参数,描述第一个补间完成后需要多少次重复:
tween.repeat(10) // 循环10次 tween.repeat(Infinity) // 无限循环
若重复参数值为 n
则补间总次数为 n+1
。
yoyo
此功能仅在与 repeat
一起使用时才有效。 激活时,补间的行为将 像溜溜球 一样,即它会在开始值和结束值之间来回跳动,而不是仅仅从头开始重复相同的顺序:
tween.yoyo(false) // 默认值,动画只会从开始到结束值 tween.yoyo(true) // tween 将在起始值和结束值之间“yoyo”
delay
更复杂的安排可能需要在实际开始运行之前延迟执行补间。 可以使用 delay
方法来实现:
tween.delay(1000) tween.start()
将在调用 start
方法后的 1
秒钟后开始执行。
repeatDelay
通常,delay
时间应用于不同补间的衔接之间,但如果向 repeatDelay
函数提供了一个值,则该值将确定补间重复(repeat)之间经过的总时间。
tween.delay(1000) tween.repeatDelay(500) tween.start()
补间的第一次迭代将在一秒后发生,第二次迭代将在第一次迭代结束后半秒发生,第三次迭代将在第二次迭代结束后半秒发生,依此类推。如果之想延迟初始迭代但不希望迭代之间有任何延迟,请确保调用 tween.repeatDelay(0)
,默认就是 0
。
dynamic
如果 dynamic
设置为 true
(默认为 false
),则传递给 tween.to()
的对象可以在补间动画的外部进行修改。这可用于在运行时动态修改补间的结果。
请参阅 dynamic 示例。在那个例子中,在这两个场景中,兔子的位置都在动画期间更新。 兔子的位置恰好是传递给狐狸的 tween.to()
方法的对象。 随着兔子位置的更新,在第一个带有 .dynamic(false)
的场景中,狐狸向兔子的初始位置移动并且不跟随兔子,而在第二个带有 .dynamic(true)
的场景中,狐狸移动到兔子的最终目的地 狐狸因此也被更新,这使得狐狸跟随兔子。
注: 当
dynamic
设置为false
时,Tween
复制传递给tween.to()
的对象并且永远不会修改它(因此从外部更新原始对象不是动态的)。当dynamic
为true
时,Tween
在动画期间使用原始对象作为值的来源(每次更新都读取值,因此可以动态修改它们) 但要注意,在dynamic
模式下,Tween
将修改传递给tween.to()
的对象的任何插值数组,这可能会对也依赖于同一对象的任何外部代码造成副作用。
pause 和 resume
暂停当前正在运行的动画。恢复暂停的动画。
const tween = new TWEEN.Tween(obj).to({ x: 100 }, 1000); tween.start(); // 一段时间后暂停动画 setTimeout(() => { tween.pause(); }, 300); // 再过一段时间后恢复动画 setTimeout(() => { tween.resume(); }, 600);
stopChainedTweens
停止所有链接在当前 Tween
对象之后的动画。当使用 chain
方法链接多个动画时,调用此方法可以终止后续动画的执行。
const obj = { x: 0 }; const tween1 = new TWEEN.Tween(obj).to({ x: 100 }, 1000); const tween2 = new TWEEN.Tween(obj).to({ x: 200 }, 1000); tween1.chain(tween2); tween1.start(); // 停止链接的 tween2 动画 tween1.stopChainedTweens();
控制所有 补间
在 TWEEN
全局对象中可以找到以下方法,除了 update
之外,通常不需要使用其中的大部分方法。
TWEEN.update(time)
用于更新所有活动的补间。 如果 time 不指定,它将使用当前时间。
TWEEN.getAll and TWEEN.removeAll
用于获取对活动 tweens
数组的引用,并分别通过一次调用将它们从数组中移除。
TWEEN.add(tween) and TWEEN.remove(tween)
分别用于将补间添加到活动补间列表,或从列表中删除特定补间。
控制补间集
使用 TWEEN
单例来管理补间可能会导致包含许多组件的大型应用程序出现问题。在这些情况下,可能需要创建更小的补间集。
示例:交叉组件冲突
如果有多个组件使用 TWEEN
,并且每个组件都想管理自己的补间集,则可能会发生冲突。 如果一个组件调用 TWEEN.update()
或 TWEEN.removeAll()
,其他组件的补间也将被更新或删除。
创建自己的补间集
为了解决这个问题,每个组件都可以创建自己的 TWEEN.Group
实例(这是全局 TWEEN
对象在内部使用的实例)。 在实例化新补间时,这些组可以作为第二个可选参数传入:
const groupA = new TWEEN.Group() const groupB = new TWEEN.Group() const tweenA = new TWEEN.Tween({x: 1}, groupA).to({x: 10}, 100).start() const tweenB = new TWEEN.Tween({x: 1}, groupB).to({x: 10}, 100).start() const tweenC = new TWEEN.Tween({x: 1}).to({x: 10}, 100).start() groupA.update() // 只更新tweenA groupB.update() // 只更新tweenB TWEEN.update() // 只更新tweenC groupA.removeAll() // 只移除tweenA groupB.removeAll() // 只移除tweenB TWEEN.removeAll() // 只移除tweenC
这样,每个组件都可以创建、更新和销毁自己的补间集。
改变缓动功能(别名:让它弹)
Tween.js
默认以线性方式执行值之间的插值(即缓动),因此变化与经过的时间成正比。 也可以使用缓动方法轻松更改此行为。 例如:
tween.easing(TWEEN.Easing.Quadratic.In)
这将导致补间缓慢开始,逐渐加速,然后快速达到其最终值。 相比之下,TWEEN.Easing.Quadratic.Out
开始快速变化,逐渐减速,在接近最终值时会非常慢。
可用的缓动函数:TWEEN.Easing
tween.js
提供了一些现成的缓动功能。它们按照它们表示的方程类型进行分组:线性Linear
、二次Quadratic
、三次Cubic
、四次Quartic
、五次Quintic
、正弦Sinusoidal
、指数Exponential
、圆形Circular
、弹性Elastic
、后退Back
和反弹Bounce
,然后按缓动类型:In
、Out
和 InOut
。图表示例
注:
Linear
没有In
、Out
和InOut
只有None
。
使用自定义缓动功能
不仅可以使用现有的缓动函数,还可以制作自己的函数,只要它遵循一些约定即可:
- 它必须接受一个参数:
k
: 缓动过程,或我们的补间所处的时间有多长。允许的值在[0, 1]
的范围内。 - 它必须根据输入参数返回一个值。
无论要更改多少属性,每次更新时每个补间只调用一次缓动函数。 然后将结果与初始值以及此值和最终值之间的差值(deltas)一起使用,如以下伪代码所示:easedElapsed = easing(k); for each property: newPropertyValue = initialPropertyValue + propertyDelta * easedElapsed;
对于更注重性能表现的人来说:只有在补间上调用 start()
时才会计算 deltas
。
因此,假设想要使用自定义缓动函数来缓动值,将 Math.floor
应用于输出,因为只会返回整数部分,从而产生一种阶梯式输出:
function tenStepEasing(k) { return Math.floor(k * 10) / 10 }
可以通过简单地调用它的缓动方法在补间中使用它,就像之前看到的那样:查看示例
tween.easing(tenStepEasing)
回调函数
能够在每个补间的生命周期的特定时间运行自己的功能。 当更改属性不能实现时,通常需要这样做。
例如,假设正在试图给一些不能直接访问属性的对象设置动画,需要调用 setter
。 可以使用 update
回调来读取新的更新值,然后手动调用 setters
。 所有的回调函数都将 补间对象 作为唯一的参数。
const trickyObjTween = new TWEEN.Tween({ propertyA: trickyObj.getPropertyA(), propertyB: trickyObj.getPropertyB(), }) .to({propertyA: 100, propertyB: 200}) .onUpdate(function (object) { object.setA(object.propertyA) object.setB(object.propertyB) })
或者想在开始补间时播放声音。 可以使用 start
回调:
const tween = new TWEEN.Tween(obj).to({x: 100}).onStart(function () { sound.play() })
每个回调的范围是补间对象——在本例中为 obj。
onStart
在补间开始动画之前执行,在 delay
方法指定的任何延迟时间之后。 每个补间只会执行一次,即当补间通过 repeat()
重复时不会运行。
onStart
非常适合与其他事件同步,或触发需要在补间开始时执行的操作。
onStop
当补间通过 stop()
显式停止执行时,该补间的 onComplete
将不会执行,并且在所有后续链接补间的 onStart
也不会执行。
// 创建一个对象,用于存储需要补间的属性 const object = { x: 0 }; // 创建第一个补间,将 object.x 从 0 过渡到 100,持续时间为 5000 毫秒 const tween1 = new TWEEN.Tween(object) .to({ x: 100 }, 5000) .onUpdate((obj) => { // 输出的是本次更新之后的值 console.log("Tween 1 update", obj.x); }) .onStop(() => { console.log("Tween 1 被手动停止"); }) .onComplete(() => { console.log("Tween 1 完成"); }); // 创建第二个补间,将 object.x 从 100 过渡到 200,持续时间为 3000 毫秒 const tween2 = new TWEEN.Tween(object) .to({ x: 200 }, 3000) .onStart(() => { // 因为第一次补间显式的停止,所以不会触发后续链接补间的 onStart 事件 console.log("Tween 2 开始"); }) .onUpdate((obj) => { console.log("t2 update", obj.x); }) .onStop(() => { console.log("Tween 2 被手动停止"); }) .onComplete(() => { console.log("Tween 1 完成"); }); // 链接两个补间 tween1.chain(tween2); // 启动第一个补间 tween1.start(); // 在 2000 毫秒后停止第一个补间 setTimeout(() => { tween1.stop(); }, 2000); function animate() { // 更新补间动画的状态 TWEEN.update(); // 如果补间动画还在进行中,继续调用 animate 函数 if (TWEEN.getAll().length > 0) { requestAnimationFrame(animate); } } // 开始动画循环 animate();
onUpdate
每次更新补间时执行,在实际更新值之后执行。
onComplete
当补间正常完成(即未停止)时执行。
onRepeat
每当补间刚刚完成一个重复并将开始另一个重复时执行。
补间状态
isPlaying
开始时为 true
(即使是暂停)。
当补间 停止 时,isPlaying
和 isPaused
都将为 false
。
isPaused
暂停时为 true
。 isPlaying
也将为 true
。 如果补间已启动但未暂停,则 isPlaying
将为 true
而 isPaused
将为 false。
高级补间
相对值
使用 to
方法时,也可以使用 相对值 。 当 tween
启动时,Tween.js
将读取当前属性值并应用相对值来找出新的最终值。 但是需要使用 引号 ,否则这些值将被视为绝对的。查看示例。
// 这将使 `x` 属性始终为 100 const absoluteTween = new TWEEN.Tween(absoluteObj).to({x: 100}) // 假设 absoluteObj.x 现在为 0 absoluteTween.start() // 使 x 变为 100 // 假设 absoluteObj.x 现在是 -100 absoluteTween.start() // 使 x 变为 100 // 相比之下... // 这将使 `x` 属性相对于开始时的实际值多 100 个单位 const relativeTween = new TWEEN.Tween(relativeObj).to({x: '+100'}) // 假设 relativeObj.x 现在是 0 relativeTween.start() // 使 x 变为 0 +100 = 100 // 假设 relativeObj.x 现在是 -100 relativeTween.start() // 使 x 变为 -100 +100 = 0
补间嵌套对象
Tween.js 还可以跨嵌套对象更改属性。 例如:
const nestedObject = {scale: {x: 0, y: 0}, alpha: 0} const tween = new TWEEN.Tween(nestedObject).to({scale: {x: 100, y: 100}, alpha: 1})
补间值的数组
除了补间到绝对值或相对值之外,还可以让 Tween.js
更改一系列值的属性。 为此,只需为属性指定一个值 数组 而不是单个值。 例如:
const tween = new TWEEN.Tween(relativeObj).to({x: [0, -100, 100]})
将使 x 从初始值变为 0,-100 和 100。
这些值的计算方法如下:
- 首先,补间进度如常计算
- 进度(从 0 到 1)用作插值函数的输入
- 基于进度和值的数组,生成内插值
例如,当补间刚刚 启动(进度为 0)时,插值函数将返回数组中的 第一 个值。 当补间到 一半 时,插值函数将返回一个大约在数组 中间 的值,当补间结束时,插值函数将返回最后一个值。
你可以使用插值方法更改插值函数。 例如:
tween.interpolation(TWEEN.Interpolation.Bezier)
以下值可用:
- TWEEN.Interpolation.Linear
- TWEEN.Interpolation.Bezier
- TWEEN.Interpolation.CatmullRom
默认是Linear
。
注:插值函数对于在同一补间中与数组补间的所有属性都是全局的。 不能使用数组和线性函数更改属性 A,也不能使用数组和使用相同补间的贝塞尔函数更改属性 B; 应该使用两个运行在同一对象上但修改不同属性并使用不同插值函数的补间对象。
查看示例。
获得最佳性能
虽然 Tween.js
试图靠自己发挥性能,但没有什么能阻止以反性能的方式使用它。 以下是一些在使用 Tween.js
时(或通常在 Web 中制作动画时)可以避免拖慢项目速度的方法。
使用高性能的 CSS
当你尝试为页面中元素的位置设置动画时,最简单的解决方案是为 top 和 left 样式属性设置动画,如下所示:
const element = document.getElementById('myElement') const tween = new TWEEN.Tween({top: 0, left: 0}).to({top: 100, left: 100}, 1000).onUpdate(function (object) { element.style.top = object.top + 'px' element.style.left = object.left + 'px' })
但这确实效率低下,因为更改这些属性会强制浏览器在每次更新时重新计算布局,这是一项非常消耗性能的操作。应该使用 transform
,它不会使布局无效,并且在可能的情况下也会进行硬件加速,如下所示:
const element = document.getElementById('myElement') const tween = new TWEEN.Tween({top: 0, left: 0}).to({top: 100, left: 100}, 1000).onUpdate(function (object) { element.style.transform = 'translate(' + object.left + 'px, ' + object.top + 'px);' })
如果动画需求就这么简单,最好只使用 CSS 动画或过渡(在适用的情况下),这样浏览器就可以尽可能地进行优化。 当动画需要涉及复杂的操作时,Tween.js
是非常有用,也就是说,需要将多个补间同步在一起,在一个完成后开始,循环多次,具有不是使用 CSS
而是使用 Canvas
渲染的图形或 WebGL
等等。
对垃圾收集器(别名 GC)
如果使用 onUpdate 回调,需要非常小心放在它上面的东西。 这个函数每秒会被调用很多次,如果每次更新时都做代价高昂的操作,可能会阻塞主线程并导致可怕的卡顿,或者——如果操作涉及内存分配,会使垃圾收集器运行过于频繁,也会导致卡顿。 所以不要做这两件事。 保持 onUpdate
回调非常轻量级,并确保在开发时也使用内存分析器。
总结
到此这篇关于Tween.js使用指南与详解的文章就介绍到这了,更多相关tween.js使用内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!