GSAP中的timeline

发布于 大漠

如果你阅读过上一篇《初探GSAP》文章,我想你对GSAP有了一个初步的认识。而且文章中也提到过,使用gsap.timeline()这个API可以帮助我们轻易的控制动画的时序。换句话说,让我们控制动画变得更简单,效果更好。今天这篇文章,我想花更多的时间和大家一起来探讨GSAP中的Timeline,即 **gsap.timeline()**的使用。

什么是时序

时序”是“Sequencing with Timelines”的中文意思,简单地说就是顺序和时间,我喜欢把它称为时序。换句话说,就是通过时间线来控制顺序。在Web动效中,这是非常普遍的一种现象。比如说:

  • 一个动画对象,有多个动画效果,每个效果都会由一个时间管理器来控制出现的先后顺序,这就是“时序”
  • 另一种是多个动画对象具有同一个动画效果,每个动画对象播放该动画效果也会由一个时间管理器来控制动画对象播放动效的先后顺序,这也是“时序”

在Web动画中这种“时序”(顺序和时间)往往是最为复杂的。

回到我们熟悉的动画开发中来,比如说CSS Animations 和 Transitions,控制动画时序主要依赖的是动画的持续时间(Duration Timeline)和 延迟时间(Delay Timeline)。对应的话就是:animation-durationanimation-delaytransition-durationtransition-delay。如果你对Web Animation API有过一定接触的话,控制时序有两个相应的API,即 durationdelay

就拿CSS Animations为例:

上图向我们展示了前面所说的两种不同的时序情景。

不过我们在制作Web动画的时候,场景会比这个复杂的多。如果仅仅依赖于durationdelay来控制动画时序,那么会非常的困难,而且在特定的环境之下还会让整个动效效果错乱。面对这样的一个场景,我们就需要一个更好的时间管理器,即 Timeline。帮助我们更好的管理动画中的时序。

什么是Timeline?

Timeline常被称为时间轴,它也是个容器。就是用来控制时间的,我们可以让时间之间有间隙、也可以让时间之间重叠。每个时间轴都有一个起始位置(也称为播放头)和一个终点位置(也称为停止点)。

在动画制作的世界中,时间轴主要是用来控制动画对象对应的动画效果的播放时间和持续时间。也就是说,在制作动画的时候,一旦你掌握了时间轴,一个全新的充满可能性的世界就会被你打开。那是因为,时间轴提供了一种绝妙的方法来模块化你的动画代码。

在CSS Animations 和 Transitions以及不太熟知的Web Animation API世界中,如果不使用第三方面时间轴插件(或动画库)的话,时间轴的功能有着很大的极限性。但在GSAP的世界中,时间轴就非常的强大,除了GSAP自带的时间轴API(即gsap.timeline())之外,还有 TimelineLite()TimelineMax()TweenLiteTweenMax

今天我们主要和大家一起探讨是gsap.timeline(),对于TimelineLite()TimelineMax()TweenLiteTweenMax将会放到后面的章节中和大家一起讨论

何时使用Timeline?

前面说过了,Timeline是用来管理动画或动画对象的。好比一个人在一个时间范围内做了不同的动作,什么时候走?什么时候跳?什么时候蹲下等等。如果运用到Web动效果的开发中,当你具备下面的任一条件时,都可以使用时间轴来管理:

  • 需要控制一组动画
  • 为了构建一个序列而不干扰大量的延迟值
  • 模块化你的动画代码
  • 做任何复杂的动画场景
  • 要根据一组动画触发回调(比如在某个动画结束时高用myFunction()

如果你只是在某个地方或某个时间做一个动画(或让一个元素动起来),那么使用Timeline就有点大才小用了。

如何使用gsap.timeline()

在这篇文章中,我们主要讨论的是GSAP中自带的Timeline的API的使用,即 gsap.timeline()

声明一个Timeline

在GSAP中,我们可以像下面这样来声明一个Timeline:

const tl = gsap.timeline()

如果声明的Timeline带有一定的默认值的话,可以像下面这样声明:

const tl2 = gsap.timeline({
    repeat: -1,
    defaults: {
        duration: 1.5
    }
})

如果你在控制台上把tltl2打印出来,结果如下图所示:

这样一来,我们就可以在已声明的Timeline上调用一些GSAP的API。比如tl.to(),甚至可以不断的链式调用,比如tl.to().to()...。也可以像tl.play()tl.pause()这样使用。

gsap.timeline()的基本运用

为了能更好的理解gsap.timeline()的使用,我们还是从最基础的地方开始。暂时回到前一篇文章《初探GSAP》中来,使用gsap.to()创建动画效果:

gsap.to('.ball', {
    duration: 1.5,
    x: 200,
    scale: 2
})

gsap.to('.ball', {
    duration: 1.5,
    delay: 1.5,
    x: 0,
    scale: 1
})

效果如下:

点击示例中的“Play”按钮,你将看到效果像下图这样:

你可能已经注意到了,上面的示例是一个链式动画。动画对象.ball先向右移动200px,并且放大2倍,这个动画效果持续的时间是1.5s,接着动画对象.ball1.5s之后,再向左移动到初始位置,并且缩小到原始大小。为了让第二个动画效果能在第一个动画效果结束时播放,我们在第二个gsap.to()中设置了delay的值为1.5s,也就是第一个gsap.to()中的duration值。对于这样一个简单地链式动画效果,这样做是可行的,但它也是很脆弱的。如果第一个动画的duration改变了,为了让第二个动效也在第一个动效结束时播放,就必须调整第二个动效的delay。另外, 如果我们在第二个动画效果之后还有第三个,第四个,第N个时,像前面这种手动调整durationdelay的做法,会让我们无比的痛苦,而且还易于出错。

如果我们把这个时序的控制交给Timeline来管理的话,事情会变得简单地多。我们尝试着使用gsap.timeline()来改变上面的示例:

const tl = gsap.timeline()

tl.to('.ball', {
    duration: 1.5,
    x: 200,
    scale: 2
})
.to('.ball', {
    duration: 1.5,
    x: 0,
    scale: 1
})

你会发现效果是一样的:

上面的示例使用gsap.timeline()实例化了一个时间轴tl,然后使用tl.to().to()将两个动画效果连接在一起。

在这两个动画效果中,你可能已经发现了,他们有一些参数是相同的,比如duration的值都是1.5。在gsap.timeline()中,我们可以把一些相同的参数当作时间轴实例的默认参数。这样一来,上面的示例,我们可以这样来声明一个时间轴实例:

const tl = gsap.timeline({
    defaults: {
        duration: 1.5
    }
})

上面的动画效果还不能很好的展示出Timeline的强大。我们添加一点点代码,让上面的动画效果变得更复杂一点点。比如说,动画对象.ball运动到另一个方向(反射这个运动)。并且让这个动画不断的播放,我们可以像下面这样来改造上面的示例:

const tl = gsap.timeline({
    repeat: -1,
    defaults: {
        duration: 1.5
    }
})

tl.to('.ball', {
    x: 200,
    scale: 2
})
.to('.ball', {
    x: 0,
    scale: 1
})
.to('.ball', {
    x: -200,
    scale: 2
})
.to('.ball', {
    x: 0,
    scale: 1
})

效果如下:

在这个示例中,声明时间轴实例tl时,添加了另一个默认参数repeat: -1,它的作用就是让动画循环播放(有点类似于 animation-iteration-count: infinite):

注意,repeat:-1也可以设置在单独的动画效果中

我们不仅可以让动画效果循环播放,而且还可以像溜溜球一样反复播放。即,使用GSAP中的另一个API:yoyoGSAP API文档是这样描述yoyoyoyo的值是一个布尔值,即可取值为truefalse。不过取不同的值,产生的效果是有所不同的:

  • **yoyo: true**对应的效果是A » B » B » A
  • **yoyo: false**对应的效果是A » B » A » B

我们来看一个这方面的示例:

const tl = gsap.timeline({
    repeat: -1,
    yoyo: false,
    defaults: {
        duration: 1.5,
        ease: "bounce"
    }
});

tl.to(".ball", {
    y: 50,
    scale: 2
}).to(".ball", {
    x: 200,
    scale: 2
});

当我们点击radio按钮时,通过timeline.yoyo()来改变他的值,从false改变成true

const radioBox = document.getElementById("yoyo");
radioBox.addEventListener("click", (etv) => {
    tl.yoyo(etv.target.checked);
});

默认播放的是yoyo:false的效果:

当你选中示例中的单选按钮时,yoyo的值变成了true,这个时候.ball的动画效果如下:

上面我们看到的是在同一个动画对象上使用不同的动画效果。但往往很多时候,我们是需要在不同的动画对象上使用不同的动画效果,比如说,对象A动画完成了播放对象B的动画,然后对象B动画结束之后再播放对象C的动画。这几个动画对象可以使用的是相同的动画效果,也可以使用的是不同的动画效果。

比如下面这个示例,我们在不同的动画对象上使用相同的动画效果:

const tl = gsap.timeline({
defaults: {
    duration: 1.5,
    ease: "bounce"
}
});

tl.to(".blue", {
    rotation: 360,
    scale: 2,
    borderRadius: "50%"
})
.to(".pink", {
    rotation: 360,
    scale: 2,
    borderRadius: "50%"
    })
.to(".yellow", {
    rotation: 360,
    scale: 2,
    borderRadius: "50%"
});

效果如下:

点击“Play”按钮之后,你可以看到三个动画对象依次播放了相同的动画效果:

GSAP的Timeline还有一些其他的API,这里不一一介绍,感兴趣的话可以查阅相关文档

如何使用gsap.timeline()还原CSS Animation动效

在GSAP中,有了gsap.timeline()就可以轻易的实现Animation.css库中的任意动效。比如bounceInDown效果

CSS Animation使用@keyframes写的bounceInDown的代码如下:

@keyframes bounceInDown {
    from,
    60%,
    75%,
    90%,
    to {
        animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
    }

    0% {
        opacity: 0;
        transform: translate3d(0, -3000px, 0) scaleY(3);
    }

    60% {
        opacity: 1;
        transform: translate3d(0, 25px, 0) scaleY(0.9);
    }

    75% {
        transform: translate3d(0, -10px, 0) scaleY(0.95);
    }

    90% {
        transform: translate3d(0, 5px, 0) scaleY(0.985);
    }

    to {
        transform: translate3d(0, 0, 0);
    }
}

我们换成gsap.timeline()就可以像下面这样使用:

const bounceInDown = gsap.timeline({
defaults: {
    duration: 1
}
});

bounceInDown.to('.aniEle', {
    opacity: 0,
    y: -3000,
    scaleY: 3
})
.to('.aniEle', {
    opacity: 1,
    y: 25,
    scaleY: .9,
    ease: 'cubic-bezier(0.215, 0.61, 0.355, 1)'
})
.to('.aniEle', {
    y: -10,
    scaleY: .95,
    ease: 'cubic-bezier(0.215, 0.61, 0.355, 1)'
})
.to('.aniEle',{
    y: 5,
    scaleY: .985,
    ease: 'cubic-bezier(0.215, 0.61, 0.355, 1)'
})
.to('.aniEle', {
    y: 0
})

效果如下:

Timeline技巧:位置参数

在Timeline中有一个重要参数position,可以构建复杂时间轴,创建更复杂的动画效果。这个参数可以帮助开发者更好控制Tweens、标签(Labels)、回调、暂停甚至嵌套时间线的位置。换句话说,它可以告诉时间轴在哪里插入动画。它通常以第三个参数出现在gsap.timeline()中:

gsap.timeline().to(target, vars, position)

position参数有多种行为。我们用一些示例和图来描述Timeline的position。这样更易于理解。

无位置参数:直接顺序

如果不提供position参数,所有补间(Tweens)将直接运行:

const tl = gsap.timeline();

tl.to("#green", {duration: 1, x: 750})
    .to("#blue", {duration: 1, x: 750})
    .to("#orange", {duration: 1, x: 750})

这个时候时间轴如下图所示:

对应的动画效果如下:

相对值(正值):时间差或延迟

使用一个正数的相对值,比如+=1,在前一个动画结束后的1s放置一个新的动画:

const tl = gsap.timeline();

tl.to("#green", {duration: 1, x: 750})
    .to("#blue", {duration: 1, x: 750}, "+=1")
    .to("#orange", {duration: 1, x: 750}, "+=1")

上例中的时间轴如下图所示:

对应的动画效果如下:

相对值(负值):重叠

使用一个负数的相对值,比如-=1,在前一个动画结束前1s放置新的动画:

const tl = gsap.timeline();

tl.to("#green", {duration: 2, x: 750})
    .to("#blue", {duration: 2, x: 750}, "-=1")
    .to("#orange", {duration: 2, x: 750}, "-=1");

对应的时间轴如下:

对应的动画效果如下:

绝对值

使用绝对值(数字)来指定动画开始的确切时间:

const tl = gsap.timeline();

tl.to("#green", {duration: 4, x: 750})
    .to("#blue", {duration: 2, x: 750}, 1)
    .to("#orange", {duration: 2, x: 750}, 1);

时间轴效果如下:

对应的动画效果如下:

标签

使用标签(“字符串”),比如blueGreenSpin,用来指定动画应该放在哪里:

const tl = gsap.timeline();

tl.to("#green", {duration: 1, x: 750})
    .add("blueGreenSpin", "+=1")
    .to("#blue", {duration: 2, x: 750, rotation: 360}, "blueGreenSpin")
    .to("#orange", {duration: 2, x: 750, rotation: 360}, "blueGreenSpin+=0.5");

时间轴的效果如下:

对应的动画效果如下:

相对于其他动画(Tweens)

使用<符号,引用最近添加的动画的开始时间。使用>符号,引用最近添加的动画的结束时间:

const tl = gsap.timeline();

tl.to("#green", {duration: 4, x: 750})
    .to("#blue", {duration: 2, x: 750}, ">")
    .to("#orange", {duration: 2, x: 750}, "<");

对应的时间轴如下:

相应的动画效果:

使用position 参数构建动效示例

上面通过视图和动画案例向大家展示了Timeline中第三个参数position不同行为的效果。接下来,用一个实例向大家演示怎么在实际中使用。

就拿@Jon Kantner在Codepen上用CSS Animation写的一个效果(Glowing Slinky:

这个效果,今天被推荐到CodePen的首页

从代码中不难发现,这个效果用了13div叠加在一起,看起来像一个3D的环形柱形(有点像呼啦圈):

<!-- HTML -->
<div class="slinky">
    <div class="slinky__ring"></div>
    <div class="slinky__ring"></div>
    <div class="slinky__ring"></div>
    <div class="slinky__ring"></div>
    <div class="slinky__ring"></div>
    <div class="slinky__ring"></div>
    <div class="slinky__ring"></div>
    <div class="slinky__ring"></div>
    <div class="slinky__ring"></div>
    <div class="slinky__ring"></div>
    <div class="slinky__ring"></div>
    <div class="slinky__ring"></div>
    <div class="slinky__ring"></div>
</div>

对于CSS代码,这里不展示了,感兴趣的可以查看上面的Demo。我们主要来看看使用GSAP怎么实现一个类似的效果。

使用gsap.timeline()创建了两个Timeline,分别是camera(运用于容器.slinky)和 ringsTl(运用于.slinky__ring):

const camera = gsap.timeline({
    repeat: -1
});

let ringsTl = gsap.timeline({
    repeat: -1,
    defaults: {
        druation: 3,
        ease: "linear",
        rotationX: 180,
        rotationZ: 180,
        x: -0.75
    }
});

接下来使用.to()方法,分别让动画对象具有相应的动画效果:

camera.to(".slinky", {
    x: -9.5,
    duration: 3,
    ease: "linear"
});

.slinky对象向左移动9.5em

rings.forEach((ring, index) => {
    const delay = index * 0.065 + 0.5;
    const yVal = -3 + index * 0.5;
    ringsTl.to(ring, { y: yVal }, `-=${delay}`);
});

每个.slinky__ring对象动画效果。其中他们具有一些共同的效果,在声明ringsTl实例时,在defaults中定义了,不同的是每个.slinky__ringtranslateY(即y)值不同。

需要注意的是,我们在ringsTl.to()中使用了第三个参数,也就是我们这一节中说的position参数,即index * 0.065 + 0.5。是一个相对的负值,对应的时间轴会有重叠。

最终你看到的效果如下:

注意,在这个示例中,我们使用了GSAP的开发者工具“GSDevTools”,用来更好的演示动画效果。有关于该工具更详细的介绍,可以阅读对应的官方文档

我想你已经领会到了position参数真正的力量和灵活性。上面的示例中,向大家演示的都是timeline.to()中如何使用position参数。其实,能运用在timeline.to()中的position行为方式,都可以同样的运用于timeline.from()timeline.fromTo()timeline.add()timeline.call()timeline.addPause()中。

GSAP的嵌套时间轴

在GSAP中,时间轴内可以嵌套时间轴。这样可以将动画效果使用模块化的方式来管理。比如说,可以分段构建你的动画,并将它们在主时间轴上嵌套在一起。

同样的,我们拿@Alexey Peterson写的Demo为例

首先,使用GSAP的gsap.timeline()把上面示例中用的动画效果转换出来:

function body() {
    var bodyTimeline = gsap.timeline();

    bodyTimeline.to("body", {
        ease: "ease",
        duration: 1,
        backgroundColor: "#fbc02d"
    });

    return bodyTimeline;
}

function card() {
    var cardTimeline = gsap.timeline();

    cardTimeline.to(".card", {
        ease: "ease",
        duration: 1,
        opacity: 1
    });

    return cardTimeline;
}

function cardHeader() {
    var cardHeaderTimeline = gsap.timeline();

    cardHeaderTimeline.to(".card__header", {
        opacity: 1,
        ease: "ease",
        duration: 1
    });

    return cardHeaderTimeline;
}

function cardFooter() {
    var cardFooterTimeline = gsap.timeline();

    cardFooterTimeline.to(".card__footer", {
        ease: "ease",
        duration: 1,
        opacity: 1
    });
}

function formInput() {
    var formInputTimeline = gsap.timeline();

    formInputTimeline.to(".form__input", {
        ease: "ease",
        duration: 0.5,
        scaleX: 1
    });

    return formInputTimeline;
}

function formSend() {
    var formSendTimeline = gsap.timeline();

    formSendTimeline.to(".form__send", {
        ease: "ease",
        duration: 0.5,
        scale: 1
    });

    return formSendTimeline;
}

function burgerLineSecond() {
    var burgerLineSecondTimeline = gsap.timeline();

    burgerLineSecondTimeline.to(".burger__line_second", {
        duration: 0.5,
        scaleX: 0.5,
        x: 15
    });
    return burgerLineSecondTimeline;
}

function cardMessage() {
    var cardMessageTimeline = gsap.timeline();
    cardMessageTimeline.to(".card__message", {
        ease: "ease",
        duration: 1,
        scale: 1
    });
    return cardMessageTimeline;
}

function loaderElement() {
    var loaderElementTimeline = gsap.timeline({
        repeat: -1,
        defaults: {
        duration: 0.9,
        ease: "ease-in-out"
        }
    });


    const loaders = document.querySelectorAll(".loader__element");

    loaders.forEach((loader, index) => {
        const delay = index * 0.3;
        loaderElementTimeline.to(
        loader,
        {
            borderWidth: "2px",
            borderColor: "#3f51b5",
            scale: 2
        },
        `-=${delay}`
        );
    });

    return loaderElementTimeline;
}

master
    .add(body())
    .add(card())
    .add(cardHeader())
    .add(cardFooter())
    .add(formInput())
    .add(formSend())
    .add(burgerLineSecond())
    .add(cardMessage())
    .add(loaderElement());

这个时候的效果如下图所示:

从效果上来看,CSS Animations版本差太远,而且顺序也是乱的。我们回过头来,通过浏览器动画调试工具,把CSS Animation版本的时间轴复制下来:

根据上轴,我们来改变主轴的加载嵌套时间轴的顺序:

master
    .add(body())
    .add(card())
    .add(cardHeader())
    .add(burgerLineSecond())
    .add(cardMessage())
    .add(cardFooter())
    .add(loaderElement())
    .add(formInput())
    .add(formSend())
    

现在离我们的效果越来越近了:

小结

今天我们主要以GSAP中的自带时间轴API,即:gsap.timeline()主线,并且以timeline().to()为例,介绍了使用GSAP在开发动效时,时间轴给我们带来的便利和灵活性。在GSAP中还有其他的API可以像timeline()一样帮助开发者控制时间轴,管理动效。如果你感兴趣的话,欢迎持续关注后续相关更新。