Animating an SVG Menu Icon with Segment

Animating an SVG Menu Icon with Segment


A tutorial on how to implement an animated menu icon based on the Dribbble shot by Tamas Kojo using SVG and Segment, a JavaScript library for drawing and animating SVG paths.

AnimatedSVGMenuIcon

View demo Download source

Today we are very happy to share an interesting menu icon effect with you. The idea is based on the Dribbble shot hamburger menu by Tamas Kojo. At first, the icon is the classic burger menu icon. But when you click on it, it becomes a close icon with a fun “ninja” effect. The animation is reversed when you click on the close icon which turns it into the initial hamburger icon again. Take a look:

Awesome Burguer Animation

In this tutorial we are going to recreate this effect using SVG and a new library called Segment. First, we will do some initial planning, then we’ll introduce Segment a bit and later on we will draw and animate our hamburger icon.

Planning

To achieve this effect, I cannot imagine anything better than SVG. And the new library Segment (which is an alternative to the DrawSVGPlugin from GSAP) provides the necessary utilities to implement it.

The main idea is to create three paths that describe the trajectory of each bar on the burger icon when it transforms to the close icon. The Segment library will allow us to animate the path strokes in the way we want. To draw paths, any vector editor (like Adobe Illustrator or Inkscape) can be used; in this case we’ll be drawing the paths manually (tying lines, curves and arcs), because we want to get the best possible accuracy. Keep in mind that we are doing an animation that contains “elastic” movements, therefore these must be considered in the length of each path. But before we continue, let’s have a look at Segment.

Introducing Segment

The main tool we’ll be using is Segment, a little JavaScript class (without dependencies) for drawing and animating SVG path strokes. Using Segment is pretty straightforward:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<span class="hljs-comment">&lt;!-- Add the segment script (less than 2kb) --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-title">script</span> <span class="hljs-attribute">src</span>=<span class="hljs-value">"/dist/segment.min.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-title">script</span>&gt;</span>

<span class="hljs-comment">&lt;!-- Define a path somewhere --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-title">svg</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-title">path</span> <span class="hljs-attribute">id</span>=<span class="hljs-value">"my-path"</span> <span class="hljs-attribute">...</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-title">svg</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-title">script</span>&gt;</span><span class="javascript">
    <span class="hljs-comment">// Initialize a new Segment with the path</span>
    <span class="hljs-keyword">var</span> myPath = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">"my-path"</span>),
        segment = <span class="hljs-keyword">new</span> Segment(myPath);

    <span class="hljs-comment">// Draw a segment of a stroke at the time you want</span>
    <span class="hljs-comment">// Syntax: .draw(begin, end[, duration, options])</span>
    segment.draw(<span class="hljs-string">"25%"</span>, <span class="hljs-string">"75% - 10"</span>, <span class="hljs-number">1</span>);

    <span class="hljs-comment">/* Full example with all possible options */</span>

    <span class="hljs-comment">// Define a normalized easing function (t parameter will be in the range [0, 1])</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">cubicIn</span><span class="hljs-params">(t)</span> </span>{
        <span class="hljs-keyword">return</span> t * t * t;
    }

    <span class="hljs-comment">// Define a callback function</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">done</span><span class="hljs-params">()</span> </span>{
        alert(<span class="hljs-string">"Done!"</span>);
    }

    <span class="hljs-comment">// Draw the complete path</span>
    segment.draw(<span class="hljs-number">0</span>, <span class="hljs-string">"100%"</span>, <span class="hljs-number">1</span>, {delay: <span class="hljs-number">1</span>, easing: cubicIn, callback: done});
</span><span class="hljs-tag">&lt;/<span class="hljs-title">script</span>&gt;</span>

To learn more you can play with the demo and check out the documentation on GitHub. Also, if you want to understand how Segment works, you can read more about it in this article.

It is important to note that Segment does not include any easing function (except the default linear one), so we will be using the excellent d3-ease library for this.

Drawing

It’s a very quick animation, but if we analyze the animation frame by frame, we can draw each path. The result is something like this:

menuicon

Created from the following code we’ve developed piece by piece:


1
2
3
4
5
<span class="hljs-tag">&lt;<span class="hljs-title">svg</span> <span class="hljs-attribute">width</span>=<span class="hljs-value">"100px"</span> <span class="hljs-attribute">height</span>=<span class="hljs-value">"100px"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-title">path</span> <span class="hljs-attribute">d</span>=<span class="hljs-value">"M 30 40 L 70 40 C 90 40 90 75 60 85 A 40 40 0 0 1 20 20 L 80 80"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-title">path</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-title">path</span> <span class="hljs-attribute">d</span>=<span class="hljs-value">"M 30 50 L 70 50"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-title">path</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-title">path</span> <span class="hljs-attribute">d</span>=<span class="hljs-value">"M 70 60 L 30 60 C 10 60 10 20 40 15 A 40 38 0 1 1 20 80 L 80 20"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-title">path</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-title">svg</span>&gt;</span>

Now we need to add the proper CSS styles to the paths to achieve the desired effect, and an id to access them easily from our script. This is the HTML structure we’ll be using:


1
2
3
4
5
6
7
8
9
10
11
<span class="hljs-comment">&lt;!-- Wrapper --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-title">div</span> <span class="hljs-attribute">id</span>=<span class="hljs-value">"menu-icon-wrapper"</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"menu-icon-wrapper"</span>&gt;</span>
    <span class="hljs-comment">&lt;!-- SVG element with paths --&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-title">svg</span> <span class="hljs-attribute">width</span>=<span class="hljs-value">"100px"</span> <span class="hljs-attribute">height</span>=<span class="hljs-value">"100px"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-title">path</span> <span class="hljs-attribute">id</span>=<span class="hljs-value">"pathA"</span> <span class="hljs-attribute">d</span>=<span class="hljs-value">"M 30 40 L 70 40 C 90 40 90 75 60 85 A 40 40 0 0 1 20 20 L 80 80"</span>/&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-title">path</span> <span class="hljs-attribute">id</span>=<span class="hljs-value">"pathB"</span> <span class="hljs-attribute">d</span>=<span class="hljs-value">"M 30 50 L 70 50"</span>/&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-title">path</span> <span class="hljs-attribute">id</span>=<span class="hljs-value">"pathC"</span> <span class="hljs-attribute">d</span>=<span class="hljs-value">"M 70 60 L 30 60 C 10 60 10 20 40 15 A 40 38 0 1 1 20 80 L 80 20"</span>/&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-title">svg</span>&gt;</span>
    <span class="hljs-comment">&lt;!-- Trigger to perform the animations --&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-title">button</span> <span class="hljs-attribute">id</span>=<span class="hljs-value">"menu-icon-trigger"</span> <span class="hljs-attribute">class</span>=<span class="hljs-value">"menu-icon-trigger"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-title">button</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-title">div</span>&gt;</span>

And the CSS styles:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<span class="hljs-comment">// The wrapper was defined with a fixed width and height</span>
<span class="hljs-comment">// Note, that the pointer-events property is set to 'none'. </span>
<span class="hljs-comment">// We don't need any pointer events in the entire element.</span>
<span class="hljs-class">.menu-icon-wrapper</span>{
    <span class="hljs-attribute">position</span><span class="hljs-value">: relative;</span>
    <span class="hljs-attribute">display</span><span class="hljs-value">: inline-block;</span>
    <span class="hljs-attribute">width</span><span class="hljs-value">: <span class="hljs-number">34px</span>;</span>
    <span class="hljs-attribute">height</span><span class="hljs-value">: <span class="hljs-number">34px</span>;</span>
    <span class="hljs-attribute">pointer-events</span><span class="hljs-value">: none;</span>
    <span class="hljs-attribute">transition</span><span class="hljs-value">: <span class="hljs-number">0.1s</span>;</span>
}

<span class="hljs-comment">// To perform the scaled transform for the second demo</span>
<span class="hljs-class">.menu-icon-wrapper</span><span class="hljs-class">.scaled</span>{
    <span class="hljs-attribute">transform</span><span class="hljs-value">: <span class="hljs-function">scale</span>(<span class="hljs-number">0.5</span>);</span>
}

<span class="hljs-comment">// Adjusting the position of the SVG element</span>
<span class="hljs-class">.menu-icon-wrapper</span> svg{
    <span class="hljs-attribute">position</span><span class="hljs-value">: absolute;</span>
    <span class="hljs-attribute">top</span><span class="hljs-value">: -<span class="hljs-number">33px</span>;</span>
    <span class="hljs-attribute">left</span><span class="hljs-value">: -<span class="hljs-number">33px</span>;</span>
}

<span class="hljs-comment">// Defining the styles for the path elements</span>
<span class="hljs-class">.menu-icon-wrapper</span> svg path{
    stroke<span class="hljs-value">: <span class="hljs-hexcolor">#fff</span>;</span>
    stroke-<span class="hljs-attribute">width</span><span class="hljs-value">: <span class="hljs-number">6px</span>;</span>
    stroke-linecap<span class="hljs-value">: round;</span>
    fill<span class="hljs-value">: transparent;</span>
}

<span class="hljs-comment">// Setting the pointer-events property to 'auto', </span>
<span class="hljs-comment">// and allowing only events for the trigger element</span>
<span class="hljs-class">.menu-icon-wrapper</span> <span class="hljs-class">.menu-icon-trigger</span>{
    <span class="hljs-attribute">position</span><span class="hljs-value">: relative;</span>
    <span class="hljs-attribute">width</span><span class="hljs-value">: <span class="hljs-number">100%</span>;</span>
    <span class="hljs-attribute">height</span><span class="hljs-value">: <span class="hljs-number">100%</span>;</span>
    <span class="hljs-attribute">cursor</span><span class="hljs-value">: pointer;</span>
    <span class="hljs-attribute">pointer-events</span><span class="hljs-value">: auto;</span>
    <span class="hljs-attribute">background</span><span class="hljs-value">: none;</span>
    <span class="hljs-attribute">border</span><span class="hljs-value">: none;</span>
    <span class="hljs-attribute">margin</span><span class="hljs-value">: <span class="hljs-number">0</span>;</span>
    <span class="hljs-attribute">padding</span><span class="hljs-value">: <span class="hljs-number">0</span>;</span>
}

Animating

With the SVG code ready, our task now is to figure out or to guess the easing functions used in each section of the animation, and to achieve a proper synchronization, always guided by the animated GIF. Let’s see how to animate the top and bottom bars of the hamburger icon. First, we need to initialize a segment for each bar with the initial begin and end values. Because we don’t have the information at hand but only the visual animation of the GIF, this is a trial and error process until we find the right values.


1
2
3
4
<span class="hljs-keyword">var</span> pathA = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'pathA'</span>),
    pathC = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'pathC'</span>),
    segmentA = <span class="hljs-keyword">new</span> Segment(pathA, <span class="hljs-number">8</span>, <span class="hljs-number">32</span>),
    segmentC = <span class="hljs-keyword">new</span> Segment(pathC, <span class="hljs-number">8</span>, <span class="hljs-number">32</span>);

With that we are ready to animate, always keeping the same length (end – begin = 24) during the whole animation. Analyzing the animation sequence, we can see that the first part starts with a linear easing function, and ends with an elastic one. We’ll be using functions that receive the segment as a parameter to reuse the same function with the top and bottom bars, because they will be animated in the same way.


1
2
3
4
5
6
7
8
9
<span class="hljs-comment">// Linear section, with a callback to the next</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">inAC</span><span class="hljs-params">(s)</span> </span>{ s.draw(<span class="hljs-string">'80% - 24'</span>, <span class="hljs-string">'80%'</span>, <span class="hljs-number">0.3</span>, {delay: <span class="hljs-number">0.1</span>, callback: <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">()</span></span>{ inAC2(s) }}); }

<span class="hljs-comment">// Elastic section, using elastic-out easing function</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">inAC2</span><span class="hljs-params">(s)</span> </span>{ s.draw(<span class="hljs-string">'100% - 54.5'</span>, <span class="hljs-string">'100% - 30.5'</span>, <span class="hljs-number">0.6</span>, {easing: ease.ease(<span class="hljs-string">'elastic-out'</span>, <span class="hljs-number">1</span>, <span class="hljs-number">0.3</span>)}); }

<span class="hljs-comment">// Running the animations</span>
inAC(segmentA); <span class="hljs-comment">// top bar</span>
inAC(segmentC); <span class="hljs-comment">// bottom bar</span>

We just need to repeat the same process for the middle bar:


1
2
3
4
5
6
7
8
9
10
11
12
<span class="hljs-comment">// Initialize</span>
<span class="hljs-keyword">var</span> pathB = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'pathB'</span>),
    segmentB = <span class="hljs-keyword">new</span> Segment(pathB, <span class="hljs-number">8</span>, <span class="hljs-number">32</span>);

<span class="hljs-comment">// Expand the bar a bit</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">inB</span><span class="hljs-params">(s)</span> </span>{ s.draw(<span class="hljs-number">8</span> - <span class="hljs-number">6</span>, <span class="hljs-number">32</span> + <span class="hljs-number">6</span>, <span class="hljs-number">0.1</span>, {callback: <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">()</span></span>{ inB2(s) }}); }

<span class="hljs-comment">// Reduce with a bounce effect</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">inB2</span><span class="hljs-params">(s)</span> </span>{ s.draw(<span class="hljs-number">8</span> + <span class="hljs-number">12</span>, <span class="hljs-number">32</span> - <span class="hljs-number">12</span>, <span class="hljs-number">0.3</span>, {easing: ease.ease(<span class="hljs-string">'bounce-out'</span>, <span class="hljs-number">1</span>, <span class="hljs-number">0.3</span>)}); }

<span class="hljs-comment">// Run the animation</span>
inB(segmentB);

To reverse the animation back to the hamburger icon we’ll be using:


1
2
3
4
5
6
7
8
9
10
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">outAC</span><span class="hljs-params">(s)</span> </span>{ s.draw(<span class="hljs-string">'90% - 24'</span>, <span class="hljs-string">'90%'</span>, <span class="hljs-number">0.1</span>, {easing: ease.ease(<span class="hljs-string">'elastic-in'</span>, <span class="hljs-number">1</span>, <span class="hljs-number">0.3</span>), callback: <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">()</span></span>{ outAC2(s) }}); }
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">outAC2</span><span class="hljs-params">(s)</span> </span>{ s.draw(<span class="hljs-string">'20% - 24'</span>, <span class="hljs-string">'20%'</span>, <span class="hljs-number">0.3</span>, {callback: <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">()</span></span>{ outAC3(s) }}); }
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">outAC3</span><span class="hljs-params">(s)</span> </span>{ s.draw(<span class="hljs-number">8</span>, <span class="hljs-number">32</span>, <span class="hljs-number">0.7</span>, {easing: ease.ease(<span class="hljs-string">'elastic-out'</span>, <span class="hljs-number">1</span>, <span class="hljs-number">0.3</span>)}); }

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">outB</span><span class="hljs-params">(s)</span> </span>{ s.draw(<span class="hljs-number">8</span>, <span class="hljs-number">32</span>, <span class="hljs-number">0.7</span>, {delay: <span class="hljs-number">0.1</span>, easing: ease.ease(<span class="hljs-string">'elastic-out'</span>, <span class="hljs-number">2</span>, <span class="hljs-number">0.4</span>)}); }

<span class="hljs-comment">// Run the animations</span>
outAC(segmentA);
outB(segmentB);
outAC(segmentC);

Finally, in order to perform the respective animation with the a click event, we can do something like this:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<span class="hljs-keyword">var</span> trigger = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'menu-icon-trigger'</span>),
    toCloseIcon = <span class="hljs-literal">true</span>;

trigger.onclick = <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">()</span> </span>{
    <span class="hljs-keyword">if</span> (toCloseIcon) {
        inAC(segmentA);
        inB(segmentB);
        inAC(segmentC);
    } <span class="hljs-keyword">else</span> {
        outAC(segmentA);
        outB(segmentB);
        outAC(segmentC);
    }
    toCloseIcon = !toCloseIcon;
};

The animation is complete, but there is a little problem. It does not look exactly the same in all browsers. The path lengths seem to be calculated slightly different and so there is a small (but significant) difference, mainly between Firefox and Chrome. How do we fix it?

The solution is quite simple. We can simply make our SVG larger so that the paths are much longer, and then resize or scale down to the desired dimensions. In this case we have resized our SVG drawing to be 10 times larger than before, so we have the following code:


1
2
3
4
5
<span class="hljs-tag">&lt;<span class="hljs-title">svg</span> <span class="hljs-attribute">width</span>=<span class="hljs-value">"1000px"</span> <span class="hljs-attribute">height</span>=<span class="hljs-value">"1000px"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-title">path</span> <span class="hljs-attribute">id</span>=<span class="hljs-value">"pathA"</span> <span class="hljs-attribute">d</span>=<span class="hljs-value">"M 300 400 L 700 400 C 900 400 900 750 600 850 A 400 400 0 0 1 200 200 L 800 800"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-title">path</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-title">path</span> <span class="hljs-attribute">id</span>=<span class="hljs-value">"pathB"</span> <span class="hljs-attribute">d</span>=<span class="hljs-value">"M 300 500 L 700 500"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-title">path</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-title">path</span> <span class="hljs-attribute">id</span>=<span class="hljs-value">"pathC"</span> <span class="hljs-attribute">d</span>=<span class="hljs-value">"M 700 600 L 300 600 C 100 600 100 200 400 150 A 400 380 0 1 1 200 800 L 800 200"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-title">path</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-title">svg</span>&gt;</span>

Then we have scaled down to the original dimensions with CSS:


1
2
3
4
<span class="hljs-class">.menu-icon-wrapper</span> <span class="hljs-tag">svg</span> <span class="hljs-rules">{
    <span class="hljs-rule"><span class="hljs-attribute">transform</span>:<span class="hljs-value"> <span class="hljs-function">scale</span>(<span class="hljs-number">0.1</span>)</span></span>;
    <span class="hljs-rule"><span class="hljs-attribute">transform-origin</span>:<span class="hljs-value"> <span class="hljs-number">0</span> <span class="hljs-number">0</span></span></span>;
<span class="hljs-rule">}</span></span>

Note that we also need to increase the float values in the JavaScript code (multiply by ten) and we’ll have to adjust the stroke-width attribute in the CSS. If you don’t mind very small cross-browser differences then you can also stick to the original size, but this workaround might help you troubleshoot some differences.

Today we’ve explored how to use the Segment library to achieve an elastic SVG animation. This is one of the possible ways to achieve this kind of effect. Now it’s your turn to do some creative SVG animations 🙂

We hope you enjoyed this tutorial and find it useful!

Browser Support:

  • Chrome
  • Firefox
  • Internet Explorer
  • Safari
  • Opera

View demo Download source

Let`s Go

Comments

comments

Categories
Tags

+ There are no comments

Add yours