Advancing & Removal

Here we will allow the graphic to animate between titles and cover how to handle the remove function.

Dealing with Asynchronous Animations

To begin with, let's find a bug in our current graphic. If you run the play function then the stop function in quick succession, the animation will break. To solve this, we will utilize a new JS feature called Promises. Promises allow us to delay the the execution of code until an asynchronous function, or function that happens in parallel to our other code, completes. A more technical breakdown of asynchronous code can be found on MDN's. For now, we just need to know that our GSAP animation happens asynchronously so we want to wait until it completes to continue executing our code. Let's wrap our animateIn function inside of a new Promise.

function animateIn() {
    return new Promise((resolve, reject) => {
        const graphic = document.querySelector('.lt-style-one .graphic');
        /* Other Element Variable Definitions */
        
        const tl = new gsap.timeline({
            duration: 1, 
            ease: 'power1.out',
            onComplete: resolve // Will resolve when the animation is complete
        });
        /* Timeline animation */
    });
}

We will return the new Promise so we can chain multiple animation together. Our Animate In Promise will allow the next Promise to execute once the resolve function is called. In this case, we resolve the Promise when the timeline completes all it's animations. Let's wrap outanimateOut function in a Promise as well.

function animateOut() {
    return new Promise((resolve, reject) => {
        const graphic = document.querySelector('.lt-style-one .graphic');
        /* Other Element Variable Definitions */
        
        const tl = new gsap.timeline({
            duration: 1, 
            ease: 'power1.out',
            onComplete: resolve // Will resolve when the animation is complete
        });
        /* Timeline animation */
    });
}

Wrapping the animateOut function in a Promise works the same was as the aniamteIn function.

Running Animations in Succession

We are going to need a function that handles executing and waiting for our animations to complete. Right below our inner IIFE, or knock-off constructor function, let's add an executePlayOutCommand function and a addPlayOutCommandfunction.

const _graphic = (function() {
    (function() {
        /* Play Out commands attached to window object */
    })();
    
    function executePlayOutCommand() {
        
    }
    
    function addPlayOutCommand(prom) {
    
    }
    
    function applyStyles() { }
    // Other function definitions
}

The appPlayOutCommand function will take a Promise as an argument and add it to an array of Promises to execute. We will need to store the array somewhere and we will also want to throttle the number of animations being ran in succession. The amount of throttling needed for each graphic will vary but, for our example, three animations in the queue should be plenty. Let's add an animationQueue and animationThreshold variables to out graphic.

const _graphic = (function() {
    let state = 0;
    let activeStep = 0;
    let currentStep = 0;
    let data = [];
    let style;
    const animationQueue = [];
    const animationThreshold = 3;

    (function() {
        /* CasparCG commands attached to window object */
    })();
    
    function executePlayOutCommand(prom) { }
    
    function addPlayOutCommand(prom) { }
    
    function applyStyles() { }
    // Other function definitions
}

We can now write the logic for how our animations will be executed in executePlayOutCommand function and adding the Promise to our list in addPlayOutCommand function.

function executePlayOutCommand() {
    // Run the first Promise
    animationQueue[0]().then(() => {
        animationQueue.splice(0, 1);
        // If there are more, run them
        if(animationQueue.length) executePlayOutCommand();
    }).catch(e => HandleError(e));
}

function addPlayOutCommand(prom) {
    if(animationQueue.length < aniamtionThreshold) 
        animationQueue.push(prom);
    // Warn user about threshold
    if(animationQueue.length === aniamtionThreshold)
        handleWarning('Animation threshold met');
    // If there is only one comamnd, run it
    if(animationQueue.length === 1) executePlayOutCommand();
}

The addPlayOutCommand function simply takes our Promise and adds it to the list of commands to execute. Then if there is only the one command we just added, we will run it in executePlayOutCommand. We will also log a warning when we meet the animation threshold. This will help with debugging errors. The executePlayOutCommand function will then run the first Promise in the array and when it is complete, we will check for another Promise. If there is one, call the executePlayOutCommand function again. This process will repeat until there are now more Promises.

Handling an Advance Command

Now that we are handling our asynchronous animations, let's take advantage of those Promises to create an advance animation. We will add the code to update the title and subtitle between the animateOut and animateIn functions by using a then function. then functions are chained onto Promises and represent the next portion of script to be run. Let's create our next function and break it down.

function next() {
    if(state === 1) { // Graphic can be played
        play();
    } else if(state === 2) { // Graphic can be advanced
        if(data.length > currentStep + 1) { // There is another title to show
            currentStep++;
            const animation = () => animateOut().then(() => {
                activeStep++;
                applyData();
                return;
            }).then(animateIn);
            addPlayOutCommand(animation);
        } else {
            handleError('Graphic is out of titles to display');
        }
    } else {
        handleError('Graphic cannot be advanced while in state ' + state);
    }
}

We begin by checking which state the graphic is in. If we are in state 1 then we can simply play the graphic. Although the play command should be used, we will give the graphics operators a bit of wiggle room. If the graphic is in state 2 we can advance it but, we need to check and see if there is some data to advance to. We check that by making sure the datavariable has a length greater than what currentStep we are on. Remember, currentStep is what title we are going to see once all the animations are complete. When we add the reset function, this will become more important because we could advance, reset, then advance again in once animation queue before reaching our threshold. We would want to know that after all of that, if we run another next command, we will be out of titles.

If the data is an array and meets our currentStep rule, we will increment the currentStep variable and create a Promise chain that will animate out our graphic, change the data, increment the activeStep, and then animate the graphic back in. Finally we call addPlayOutCommand with our Promise chain to allow the graphic to run the advance animation with our other animations.

Removing the Graphic

Thankfully we do not have any clean up to do in our remove function. This is typically where you would make any API calls or store some temporary data in local storage. Both of those concepts are for more advanced graphics but we would recommend reading MDN's page on Local Storage for more info. For now, we will just check if the graphic has been played and if so, run the stop function. This will allow our graphic to gracefully animate off if remove is called.

function stop() { /* Stop function script */ }

async function remove() {
    if(state === 2) await animateOut(); // Wait here until animateOut resolves
}

function handleError(e) {console.error(e)}

There is one additional trick we will use in our remove function to make the code cleaner and that is async and await. async and await are used to make Promises act like synchronous functions. We tell the JS engine that a function contains some asynchronous code by labeling the function with async. Then when we want to run a Promise and pause our code execution, we use await. The example above states that the remove function contains some asynchronous code and we want to stop the code execution when the animateOut function runs. Any other code, if there was some, after the animateOut function would then run.

Resources

Last updated