Creating an animated SVG elephant trunk

Darin Senneff
Jul 31, 2018 · 13 min read

[UPDATE 08/02/2018]: Carl from GreenSock was nice enough to let me know that they had added some functionality in the past that I wasn’t aware of. So, I’ve added an “Updated” section after “Step #3” which shows a less-complex way to animate the trunk.


In this tutorial, we’re going to combine SVG and GreenSock to create an elephant with animated path data and perspective trickery.

Recently, I shared a GIF on Twitter of this chef sliding calculator that I created for AppInstitute:

While I received some nice feedback and comments, I also received probably a dozen messages asking how I created the skillet, hand, and arm. Many wondered if I was using 3D, sprite sheets, some sort of After Effects workflow, or other advanced solution. The reality is that I’m simply using basic shapes and tweens to create the illusion of something much more complex.

By combining the right illustration style with the right type of movement, you can simulate very believable and complex “3D” effects like heads turning, arms moving, and other perspective-based motion. In the case of the chef above, his arm is quite simple: it’s just a handful of circles, lines, and rectangles.

In this tutorial, we’re going to create a similar SVG effect in the form of an elephant’s trunk, using SVG and GreenSock, a JavaScript animation library:


GreenSock

When animating SVG on the web, you have quite a few options at your disposal. But in my opinion, the GreenSock (GSAP) platform is by far the best, especially when it comes to SVG. GSAP makes creating web animations super easy, and actually handles a ton of cross-browser SVG quirks and issues. It sure beats writing your own tweening, timing, easing, and control functions that work in all browsers.

In this tutorial, we’ll be using GSAP’s core library, TweenMax. While I won’t be doing anything too complex, some exposure to and basic understanding of GSAP will be helpful for you to follow along.


SVG

Here’s the vector illustration of what we’re going to be building today (download the Adobe Illustrator file here if you’d like to grab it):

There’s just a few simple shapes: the head, eyes, mouth, and ears are circles or rounded rectangles. And the trunk is simply a couple of circles and a curved line. Aside from the curved line of the trunk, the only property that is being changed on all of the other elements is their position on the x-axis.

In the screenshot from Illustrator below, you can see that the trunk is just a curved line with an 80px stroke. That curved line is what we call a bezier curve (essentially just a smooth, curved line). The line has just two anchor points: the beginning and the end. But in a bezier curve, every anchor point also has a control point, which are those round handles you see in the image.

In a bezier curve, the control handles of each anchor point determine the way that the line curves.

If you’ve ever used a vector graphics editor like Adobe Illustrator or InkScape, you know that by moving those handles around, the curve of the line changes. The curve of the line moves from the anchor point in the direction of the control handle. When the control handles are angled toward one another, the line forms a “C” curve; if they are angled away from one another, they will form an “S” curve. So in our elephant animation, not only will we need to change the position of the two end points of the line, but we’ll need to change the position of those control handles in order to keep the curve of the trunk consistent.

To help visualize the control handles in the animation, I added two small red/green circles into the animation to make keeping track of their location easier.


Step #1: Setup

The first thing we need to do is to get our illustration into whatever development environment you’ll be using. If you wanted to use the elephant illustration that I created, you can download the Illustrator file here.

There’s a few ways to get the SVG code from Illustrator. I method I use the most frequently is actually to just copy from Illustrator and paste into my code editor. Illustrator has a handy feature that allows copy/pasting between it and text editors. In some cases we might want to fine-tune the export settings or maybe even run the SVG code through an optimizer, but in this case we’re fine just as-is.

In the pen below, you can see that I’ve pasted the SVG code into an HTML wrapper with some CSS added for display and positioning purposes, which isn’t really relevant to cover here.

So now we have our elephant displayed onscreen and we’re ready to start adding some animation. In the JavaScript tab, you can see that I’ve already set up some variables:

var  trunkPath,
trunkPoints = {
x1: 360, y1: 495,
cx1: 430, cy1: 500,
cx2: 470, cy2: 470,
x2: 512, y2: 385
},
ease = "Quad.easeInOut",
duration = 1.5
;

We have four variables being created here:

  • trunkPath: In order to animate the bezier curve of the trunk, we’ll actually need to modify and write new path data to the SVG element on each frame of the animation. This variable will hold the string of path data we build.
  • trunkPoints: This is an object which will hold the x and y location for each of the line’s endpoints, as well as the endpoints’ control handles. You can see that there’s four pairs of x,y coordinates that I’ve named x1,y1 (the coordinates of one endpoint), cx1,cy1 (for “control handle 1”), cx2/cy2 (for “control handle 2”), and x2,y2 (the other endpoint). We’ll cover this and trunkpath more in-depth in the next step.
  • ease: This is a string of text for the type of easing function that we’ll use in our tweens. Easing is the change in rate that a property is animated, which is how we create animation that speeds up or slows down smoothly. Learn more about GSAP’s easing functions here. This variable isn’t required, but when I think I’m going to use the same type of ease more than once, I’ll put it in a variable to avoid typing it multiple times and make changing it in multiple places much easier.
  • duration: Similar to ease, 1.5 is the duration, in seconds, our animation will run.

Step #2: Animating the trunk

Now that we have our graphics in place, we’re ready to start adding some functionality.

You’ll notice that the trunk now moves back and forth. If you look in the JavaScript panel, you can see I added a new chunk of code starting on Line 12 in the form of TweenMax’s fromTo method. fromTo is one of the available tweens at your disposal in GSAP’s platform to animate things. This method allows you to define both the starting and ending properties of an element, which it then animates. It’s structured like this:

TweenMax.fromTo(
target,
duration,
{target's starting properties},
{target's ending properties}
);

The first two parameters are the target element and tween duration, respectively. The third and fourth parameters are both objects containing the property/value pairs that you’d like to animate between. Note that TweenMax tweens can also contain optional animation parameters (such as easing, which I mentioned earlier), which you can insert in the fourth parameter with the ending property/value pairs.

So let’s go through this step-by-step:

TweenMax.fromTo(
trunkPoints,
duration,

..........

Here, we’re saying that we want to animate properties of the trunkPoints object (remember this is the object I discussed in the variables section which contained the coordinates of the bezier curve) and it’s going to last the amount of time contained in the duration variable.

The reason we’re targeting this object instead of the line element itself is because we can actually only animate CSS properties of an element directly. If we were moving an element around or changing its transparency, we could use the element itself as the target and then tween their x or opacity properties. But there’s no CSS property that contains a bezier curve’s path data (it’s contained in the d parameter on the SVG element itself – see line 30 of the HTML). So we’re doing a workaround that consists of storing the coordinates in our trunkPoints object, animating the numbers within that object, assembling the numbers into a formatted string (trunkPath), and then replacing our line element’s d value with that string.

Let’s add our starting animation values in the next line:

TweenMax.fromTo(
trunkPoints,
duration,
{
x1: 312,
cx1: 420,
cx2: 480,
x2: 482
},
..........

In that third parameter, you can see that I’ve listed 4 of the 8 coordinates listed in the trunkPoints object. This is because the vertical (y) position of the endpoints and control handles isn’t changing, so there’s no need to animate them. If the line was more complex or we wanted more control of our line, we could animate the y coordinates of the control points. But our line is fairly simple with a slight curve, so we’re fine leaving them the same.

You may be wondering where I am getting the numbers for these coordinates. If you refer to the Illustrator file that I linked to above, you’ll see that our document is 1024px wide. The x/y coordinate system that the browser uses starts from the top-left of the root SVG element. So, the center of the document would be at 512px. Knowing the middle point, we can now roughly start to map out the starting and ending values of the coordinates. I did this in my head, but I created the graphic below to help visualize it for you.

Hopefully, you’re able to visualize the x coordinates now. For each of the four coordinates we’re changing, we’re starting from a position on the left side a certain distance from the center, and then ending up on the right side the same distance from center. These are our starting and ending coordinates. We already had the starting coordinates, so let’s add the ending ones into our code:


TweenMax.fromTo(
trunkPoints,
duration,
{
x1: 312,
cx1: 420,
cx2: 480,
x2: 482
},
{
x1: 712,
cx1: 604,
cx2: 544,
x2: 542,

..........

Ok, so our starting and ending are in. If we closed the method now, the trunkPoints object’s properties would be tweened perfectly. But we wouldn’t be able to see them! The numbers are changing, but the path data of our line element isn’t being updated. We’ll tackle that next:

TweenMax.fromTo(
trunkPoints,
duration,
{
x1: 312,
cx1: 420,
cx2: 480,
x2: 482
},
{
x1: 712,
cx1: 604,
cx2: 544,
x2: 542,
ease: ease,
onUpdate: function() {
trunkPath = "M" + trunkPoints.x1 + "," + trunkPoints.y1 + "C"
+ trunkPoints.cx1 + "," + trunkPoints.cy1 + "," +
trunkPoints.cx2 + "," + trunkPoints.cy2 + "," +
trunkPoints.x2 + "," + trunkPoints.y2;

TweenMax.set('#trunk', {attr:{"d": trunkPath}});
TweenMax.set(['#trunkEnd', '#trunkHole'], {attr:{"cx":
trunkPoints.x1}});
TweenMax.set('#handle1', {attr:{"cx": trunkPoints.cx1, "cy":
trunkPoints.cy1}});
TweenMax.set('#handle2', {attr:{"cx": trunkPoints.cx2, "cy":
trunkPoints.cy2}});
},

yoyo: true,
repeat: -1,
repeatDelay: 1
}
);

Within the fourth parameter, I’ve added some of the optional TweenMax parameters that I mentioned earlier, ease, and onUpdate. ease is simple, that’s the easing function that our tween is using. I discussed this in Step #1.

onUpdate is an event that triggers on each frame of the tween. This allows us to run a function at the same rate that the numbers are changing. Inside of this function, on the first line (line 28) you’ll see that I’m assembling a string of path data and storing it in the trunkPath variable. Go and look at line 30 of the HTML and you’ll see the SVG element for the trunk line. Toward the end of the line is the element’s d attribute, which is where the path data lives. That jumbled mess of letters and numbers are instructions for the browser to draw our line.

d="M360,495C430,490,470,470,512,385"

To summarize what’s happening, theM tells the browser to move the “pen” to the coordinates immediately following it. Then, the C says to draw a curved line to another location. The three sets of coordinates following it are the location of the control handle for the point of origin, the control handle for the endpoint, and then the location of the endpoint itself. You can read more about SVG path data and the drawing commands here.

Now that we have our path data, we need to swap it into our line element. Line 30 of our JavaScript uses TweenMax’s handy “set” method. We could use JavaScript to change the d attribute of the SVG node directly, but this method makes it much easier. It also makes sense to keep the way we change values of our SVG elements consistent by using GSAP’s methods whether that is a tween doing so over time, or an immediate swap of a property’s value.

The other lines in our onUpdate function on lines 31, 33, and 34 all do something similar: change the cx attribute of the two circles that make up the end of the trunk, and the red/green circles for the control handle visualizers. The cx attribute is used by SVG circle elements to set the element’s horizontal center point. Now, as the bottom anchor point of the trunk line moves, the circles that make up the end of the trunk and the trunk’s hole move along with it.

This is what creates the perspective of the trunk (and the chef arm of my other example). Numerous objects appear stacked in front of one another the closer they are to the center of the screen, the same way they’d look if it were facing you. But the further they are from the center, the further apart they get, as if they’re now facing away from you and you’re viewing them from the side.

The last lines in our fromTo method on lines 36–38 are simply the rest of the optional parameters for the tween. yoyo: truetells the method to play back in reverse once it reaches the end; repeat: -1 tells the method to loop the tween infinitely; and repeatDelay: 1 just adds a one-second delay at the end of each play-through. Now, our trunk animation continues to play back and forth.


Step #3: Finishing touches

We now have our elephant’s trunk animating back and forth, but the rest of the face just sits there. By adding a couple of additional tweens, we can get the rest of the face moving, giving the elephant a slight head-turn effect.

If you look at lines 44 and 45 in the JavaScript panel, you’ll see that there’s two more fromTo tween methods. The first one moves the x position of the eyes and mouth back and forth, while the second moves the x position of the ears back and forth in the opposite direction.

This movement is consistent with a what you see when an actual head turns: as an object rotates, the items attached to the front of the element move in the direction of the turn; but the objects attached to the rear of the element move in the opposite direction.


Updated [08/02/2018]

After I published this article, Carl from GreenSock sent me a message letting me know that GSAP actually does have the ability to animate an SVG element’s d attribute directly! They added the functionality in an update awhile back that I never realized.

This makes accomplishing the animated trunk in our example here much easier. We no longer have to tween the trunkPoints object’s various coordinates and then put those numbers together and rewrite the line’s path as that tween updates. We can simply tween the trunk line itself and pass in starting and ending d values, respectively:

Let’s take a look at our fromTo tween that begins on line 5:

TweenMax.fromTo(
'#trunk',
duration,
{
attr:{d:"M312,495C420,500,480,470,482,385"}
},
{
attr:{d:"M712,495C604,500,544,470,542,385"},
ease: ease,
yoyo: true,
repeat: -1,
repeatDelay: 1
}
);

Now, instead of the trunkPoints object as the tween’s target, we’re using the SVG line element, #trunk. In the from/to object parameters, you’ll now see that instead of all of the coordinates we were tweening previously, we now just have a starting and an ending value for the d attribute.

In addition, the other elements that previously were being updated in our tween’s onUpdate function now have their own fromTo tweens below this one.

In our example here, this updated solution is way easier and much simpler to understand and read. However, the steps I walked us through originally are still essential for doing dynamic and complex animation.

This example uses static starting and ending numbers which don’t change. We know that the starting d value is “M312,495C420,500,480,470,482,385” and the ending d value is “M712,495C604,500,544,470,542,385” and that’s that. But what if instead of pre-determined location our trunk was following the user’s mouse, it moved based on how the user was tilting their smartphone, or some other ever-changing situation? That’s when knowing how to tween individual numbers, assemble them together in the right format, and update an element’s path data with the new numbers is important. So, while the original steps I explained are overkill in this simple scenario, a more robust example is going to require us to do it the long way that I showed, so having learned those skills is essential.


Conclusion

We now have a complete elephant animation. While the example that we built was simple, we can take the concepts and create more elaborate animations: a snake whose head moves and hisses, robotic arms that grab an onscreen object, an octopus that high-fives your mouse, and so on.

You can also begin to substitute the static values of the animation with dynamic values sourced from mouse position, the data entered into a form element, or the position of another onscreen element. Once you start animating with values that are constantly changing, you can create unique and exciting effects.

Have questions, comments, or suggestions for future tutorials? Hit me up on Twitter at @dsenneff. Just want to see what I’m working on? In addition to Twitter, you can find me on Instagram and Dribbble. Cheers!

Darin Senneff

Written by

I'm a creative designer/developer that specializes in UI/UX, SVG, motion graphics, and pixel art.