Changing facial expressions with Vanilla JS and GSAP

My post before this one featured my first demo of the GSAP (Greensock Animation API) library for animation of SVG (Scalar Vector Graphics) and included a quick explanation of why I used it. Here is the demo again - click on the buttons (the default is set to “happy”) and you should be able to see the critter change expressions.

In this post, let’s take a close look at the code. First, here is an abbreviated version of the inline SVG that shows two path elements and one ellipse element. The full object has more paths and ellipses that draw the full creature. Notice that paths have a d attribute with special syntax that determines the path shape. The d attribute of the mouth path is what gets animated.




And here are the button controls. Notice the data attributes! They come into play later as the Javascript will grab them to animate the face to the right expression.




Below is a breakdown the Javascript code I use to enable the buttons and animate the mouth, followed by the full code once again.

At the top of my JS file, I first define a set of SVG paths in a Javascript object. Drill down one level and you get the expression; drill down one more layer and you get the specific part of the face as a key-value pair. Right now I only have the mouth. The key is “mouth” and the value is the SVG path; basically an equation in SVG-speak that draws the line.


var expressions = {
  'happy':{
    'mouth':'M38.54 26.603c-.53.67-1.268 1.12-2.078 1.607-4.607 2.764-6.116 5.245-11.907 2.93-1.539-.617-2.989-2.372-4.252-3.214-.704-.47-2.457-1.733-2.457-2.362 0-.073.265.506.473.661.74.555 1.195 1.034 1.984 1.701 1.19.892 2.354 1.994 3.73 3.04 1.153.877 1.95.647 3.09.92 1.072.37 3.02-.037 4.84-.884 3.425-2.283 5.784-3.48 6.577-4.399z'
   },
   ...
   'dismayed':{
    'mouth':'M38.338 31.51c-.53-.564-1.27-.944-2.08-1.354-4.606-2.332-6.115-4.424-11.905-2.471-1.54.52-2.99 2-4.252 2.71-.705.396-2.457 1.46-2.457 1.992 0 .06.265-.426.472-.558.74-.468 1.196-.872 1.985-1.434 1.189-.753 2.353.39 3.73-.493 1.153-.74 4.956-.565 6.098-.795 1.07-.312 2.55-.37 4.37.344 3.426 1.926 3.246 1.285 4.039 2.06z'
  }
};
 

I have these two levels for expandability, as I want to include parts besides the mouth later.

I then have the rest of the code in an init() function. The first part of it is a small helper function called drill that drills into the above object using a variable called str for the name of the expression, and retrieves the SVG path for the mouth.


  const drill = (str) => {
      return expressions[str]['mouth'];
   }

The next line of code grabs the div#controls element that holds the buttons, and the line after that grabs all the buttons within the this div element.


// define the button container
let controls = document.querySelector('#controls');

// define the series of buttons
let buttons = controls.querySelectorAll('button');

I want to iterate through each button element and add a click listener using foreach so that it animates the mouth with a click. The list of elements is a node list, and not an array, which means we don’t yet have access to foreach. So use the spread ... operator to expand the node list into elements and then I enclose it in brackets ([...buttons]) to make it an array. Boom! We are good to go with foreach now.


[...buttons].forEach(function(button,idx){
	// next up, we write the function...
})

Now in the full foreach function we get to see how I use GSAP.

Here is the whole function.


 [...buttons].forEach(function(button,idx){

       // get the button's data attribute
       let expression = button.getAttribute('data-expression');

       // add a click listener
       button.addEventListener('click',function(e){

         // instantiate a GSAP timeline object
         var tl = new TimelineLite();

         var svg_path = drill(expression);

         // create a tween on the mouth to the designated expression
         tl.to('#mouth',0.5,{attr:{d:svg_path}});
       })
      
   })

You may recall that the HTML code for a button has a data-expression attribute.




So I first grab the expression name from the button’s data-expression attribute.



let expression = button.getAttribute('data-expression');

I then use my drill helper function to drill into that big JSON object using the expression name to get the svg path.

Here is the function again:



const drill = (str) => {
      return expressions[str]['mouth'];
}


Here is what I do with it:



let svg_path = drill(expression);


After I get the expression name, I add the click listener, which tells the button to animate the face when clicked.

First, the code creates GSAP’s TimelineLite object.


var tl = new TimelineLite();

Then, the code tells this object to tween the mouth to the new expression over 0.5 seconds. Note that it uses the svg_path as the d attribute of the path, which determines the path’s shape.


 tl.to('#mouth',0.5,{attr:{d:svg_path}});

Below is the full code.




#critter-container svg {
    display: block;
    margin: 0px auto;
    
}
#controls {
  	display: flex;
  	justify-content: center;	
    flex-wrap: wrap;
}
button {
  	background-color: #116fca;
  	color: white;
  	padding: 8px 5px;
  	border-width: 0px;
  	border-radius: 2px;
  	min-width: 80px;
  	cursor: pointer;
  	margin: 10px 3px 2px 3px;
}


That’s a wrap

I hope that helps you understand the underlying code, as well as a few Vanilla JS techniques and how to use the Greensock library. If you are interested in going further, I recommend creating a few test SVG’s, perhaps optimizing them with SVG OMG, and then inlining them in an HTML document. Then import the Greensock library in a JS file and try a few simple tweens to get going. Good luck!