Image

animating-vue

6 min read
Last update: December 19, 2021

Nested Timelines

Sometimes there are situations when just a single timeline is not sufficient for more complex animations. In this lesson, we’ll learn how we can create more scalable, modular, and reusable animations by nesting multiple timelines within one master timeline.

Let’s open up the Master.vue component from our starting code. It currently has only one timeline, so we’re going to add a new timeline and see how we can nest both timelines within a newly created Master timeline.

📃 src/views/Master.vue

<template>
    <div>
        <div id="container">
            <img class="paws first" src="../assets/paws.png" alt="fox-paws" />
            <img class="paws second" src="../assets/paws.png" alt="fox-paws" />
            <img class="paws third" src="../assets/paws.png" alt="fox-paws" />
            <img class="paws fourth" src="../assets/paws.png" alt="fox-paws" />

            <img id="fox" src="../assets/fox.png" alt="fox-logo" />
        </div>
    </div>
</template>

<script>
    import gsap from 'gsap'

    export default {
        mounted() {
            let tl = gsap.timeline()
            tl.to('.first', {
                opacity: 4,
                scale: 1,
                duration: 0.5,
                ease: 'bounce.out'
            })
            tl.to(
                '.second', {
                    opacity: 1,
                    scale: 1,
                    duration: 0.5,
                    ease: 'bounce.out'
                },
                '<.3'
            )
            tl.to(
                '.third', {
                    opacity: 1,
                    scale: 1,
                    duration: 0.5,
                    ease: 'bounce.out'
                },
                '<.3'
            )
            tl.to(
                '.fourth', {
                    opacity: 1,
                    scale: 1,
                    duration: 0.5,
                    ease: 'bounce.out'
                },
                '<.3'
            )
        }
    }
</script>

<style scoped>
    #container {
        display: flex;
        flex-direction: row;
        justify-content: center;
        margin-top: 5em;
    }

    #fox {
        height: 8em;
        width: 8em;
    }

    .paws {
        transform: scale(0);
        width: 2.5em;
        height: 2.5em;
        margin-top: 50px;
        margin-right: 0.8em;
        opacity: 0;
    }

    button {
        margin-top: 5em;
    }
</style>

Let’s break this down. In our template, we have several images. Four of them are pngs of paws and the other is a fox. Down in our mounted hook, we have a GSAP timeline and each paw has an animation informing it to fade in (opacity: 1) and scale up (scale: 1) over a 0.5 second duration, with a bouncy ease. Notice how the paws have a position of '<.3', meaning they bounce in .3 seconds before the animation before it ends. Conceptually, there’s nothing new here from what we covered in the previous example. But what if our animation needs start to get more complex. For example, we might need to add animations to our fox as well. In that case, it could be helpful if we created a timeline that was exclusively used to sequence our paws-based animations, and another timeline for our fox-based animations. Then we could add, or nest, both of those to a master timeline. We can use methods to create these timelines. Let’s refactor our paws-based timeline into a method now.

src/views/Master.vue

<script>
...
export default {
methods: {
pawsTL() {
let tl = gsap.timeline()
tl.to('.first', {
opacity: 4,
scale: 1,
duration: 0.5,
ease: 'bounce.out'
})
tl.to(
'.second',
{ opacity: 1, scale: 1, duration: 0.5, ease: 'bounce.out' },
'<.3' ) tl.to( '.third' , { opacity: 1, scale: 1, duration: 0.5, ease: 'bounce.out' }, '<.3' ) tl.to( '.fourth' , { opacity: 1, scale: 1, duration: 0.5, ease: 'bounce.out' }, '<.3' ) return tl } } }
</script>

Now we have a method that we can call, which will create our timeline and return it. In a moment, you’ll see how we can make this timeline available to a master timeline, but first let’s make a method that creates a new timeline for a fox-based animation.

📃 src/views/Master.vue

foxTL() {
let tl = gsap.timeline()
tl.to(
'#fox',
{
opacity: 1,
filter: 'blur(0)',
scale: 1,
duration: 0.4,
ease: 'slow'
}
)
return tl
}

Now we have a separate timeline to enclose any fox-based animations we currently have, which we can add to in the future .

Sidenote: Because our timeline is fading the opacity and removing the blur of our fox, we’ll just need to add some styles to the fox’ CSS, like so:

📃 src/views/Master.vue

<style scoped>
    #fox {
        height: 8em;
        width: 8em;
        opacity: 0;
        filter: blur(2px);
    }
</style>

This way, our fox will start out invisible and blurred, so as it animates into view, it un-blurs into focus.


Creating the Master Timeline

Now that we have two methods, each creating its own timeline, we can create a master timeline and add those timelines to it. We’ll initialize our master timeline just below our import statement:

📃 src/views/Master.vue

<script>
    import gsap from 'gsap'
    let masterTL = gsap.timeline()

        ...
</script>

While we could certainly make use of our mounted hook like before, I want to show you how we can add interactivity to trigger our master timeline, so let’s add a new play method, which, when run, will add our other timelines to the master timeline, then play() it.

📃 src/views/Master.vue

...
methods: {
play() {
masterTL.add(this.pawsTL())
masterTL.add(this.foxTL())
masterTL.play()
},
...

Now we just need a button to trigger this method, which we’ll add to our template now:

📃 src/views/Master.vue

<template>
    <div id="container">
        ...
        <button @click="play">Play</button>
        <div>

</template>

Now we have a button to press, which will add our nested paws and fox timelines to our master timeline, and then play that entire timeline.

And here’s the component’s code all together:

📃 src/views/Master.vue

<template>
    <div>
        <div id="container">
            <img class="paws" id="paw1" src="../assets/paws.png" alt="fox-paws" />
            <img class="paws" id="paw2" src="../assets/paws.png" alt="fox-paws" />
            <img class="paws" id="paw3" src="../assets/paws.png" alt="fox-paws" />
            <img class="paws" id="paw4" src="../assets/paws.png" alt="fox-paws" />

            <img id="fox" src="../assets/fox.png" alt="fox-logo" />
        </div>

        <button @click="play">Play</button>

    </div>
</template>

<script>
    import gsap from 'gsap'

    let masterTL = gsap.timeline()

    export default {
        methods: {
            play() {
                masterTL.add(this.pawsTL())
                masterTL.add(this.foxTL())
                masterTL.play()
            },
            pawsTL() {
                let tl = gsap.timeline()
                tl.to('#paw1', {
                    opacity: 4,
                    scale: 1,
                    duration: 0.5,
                    ease: 'bounce.out'
                })
                tl.to(
                    '#paw2', {
                        opacity: 1,
                        scale: 1,
                        duration: 0.5,
                        ease: 'bounce.out'
                    },
                    '<.3'
                )
                tl.to(
                    '#paw3', {
                        opacity: 1,
                        scale: 1,
                        duration: 0.5,
                        ease: 'bounce.out'
                    },
                    '<.3'
                )
                tl.to(
                    '#paw4', {
                        opacity: 1,
                        scale: 1,
                        duration: 0.5,
                        ease: 'bounce.out'
                    },
                    '<.3'
                )
                return tl
            },
            foxTL() {
                let tl = gsap.timeline()
                tl.to(
                    '#fox', {
                        opacity: 1,
                        filter: 'blur(0)',
                        scale: 1,
                        duration: 0.4,
                        ease: 'slow'
                    },
                    '<.2'
                )
                return tl
            }
        }
    }
</script>

<style scoped>
    #container {
        display: flex;
        flex-direction: row;
        justify-content: center;
        margin-top: 5em;
    }

    #fox {
        height: 8em;
        width: 8em;
        opacity: 0;
        filter: blur(2px);
    }

    .paws {
        transform: scale(0);
        width: 2.5em;
        height: 2.5em;
        margin-top: 50px;
        margin-right: 0.8em;
        opacity: 0;
    }

    button {
        margin-top: 5em;
    }
</style>

What about SVGs?

You might have noticed that in these examples, for the sake of simplicity in teaching these concepts, we are simply using pngs. In a production-level app that is widely used across multiple device sizes, you’ll want to instead use SVGs (scalable vector graphics). Working with SVGs is a very deep and broad topic that we don’t have time to cover in this lesson, but we do touch on it in our Real World Vue course.


Let’s ReVue

In this lesson, we learned how we can nest timelines within timelines to create even more modular and scalable animations in our apps. Congratulations on finishing Animating Vue. I hope you now feel more confident and capable of introducing more motion, flow, and visual originality to your Vue apps.