A tutorial on how to create an animated and interactive SVG drum kit with GSAP and jQuery.

Today we’re going to create an animated SVG drum kit that can be played by clicking, tapping or using your keyboard, and that can also be programmed to play by itself! We’ll be making use of GreenSock’s TweenMax animation library, jQuery and the <audio> element.

Creating the SVG

Open your vector editing software of choice (I used Adobe Illustrator but there are many alternatives to choose from, such as Affinity Designer, Sketch or Inkscape) and get drawing! I find it helpful to have a few photos of what I’m trying to draw dotted around my artboard for reference. Keeping the style flat and fairly simple will make life easier, both in terms of drawing and animation, and less powerful devices will thank you for having less stuff to render. In this style a drum kit is actually a fairly simple set of (mostly) rectangular objects. While drawing any SVG that you intend to animate there are a couple of things to keep in mind:

<g> is Key

“The g element is a container used to group other SVG elements. Transformations applied to the g element are performed on all of its child elements, and any of its attributes are inherited by its child elements.” MDN

The key part of this for us is “Transformations applied to the g element are performed on all of its child elements” this will come in very handy when animating our SVG, e.g. instead of applying animations to each individual component of the snare drum, we can simply group the elements we want to animate and apply a single animation to that group. Well organised, well named groups will make manipulating the SVG much easier later on!

If your drawing app has an option to do so, enable XML id names (in Illustrator Preferences > Units > Identify Objects By: > XML ID) this allows you to edit/see the actual id attribute of the elements and groups that we’ll use to target them.

There are two main sets of elements that we need to group together:

1. Tap/Click Targets

We want to be able to trigger our animations and sounds based on their respective drum being tapped/clicked, so we need to group the tap/click “target” elements together. For example, group the entire snare drum, including the stand, so any part the user touches will trigger the animation/sound.

2. Animation Targets

Group elements together that we want to animate. For example, group the snare drum; when the snare drum animation is triggered we only want to animate the drum, not the stand as well.

Draw it all, then reveal

“I find it’s useful to draw everything then reveal parts when needed.” Chris Gannon

We want to be able to play the drums with a keyboard, so some indication of which keys trigger the drums would be useful. Choose the keys you want to “play” each drum with and add some indication to your SVG. Group these together so we can hide/show these later.

Exporting the SVG

Save your file with all your groups visible as a .svg and open it up in a text editor.

We can put this straight into our HTML document. However, I recommend optimising the SVG for the Web first. There are plenty of tools available to do this, with SVGOMG probably being one of the simplest to use.

See Drums, Hear Drums

We don’t just want our drums to look like they’re being played; we want to hear them, too! We’ll need separate audio files for each of our drum sounds. I exported the audio used in the demo from Logic, using some of the packaged royalty-free loops. However, there are many other resources for getting free drum samples on the Web, such as 99 Sounds and Bedroom Producers Blog.

We can include our audio files using the <audio> element. Then we set the src attribute on either the <audio> element or a child <source> element where we also need to include its type (the audio file format).

We give the audio elements IDs so that we can target them when they need to be played.

<audio id="Snare-Audio"> <source src="mp3/Snare.mp3" type="audio/mp3"> </audio>

(Pre)Load those Beats

By adding the attribute preload="auto" we can tell the browser “the user needs this, make sure it gets downloaded”. That’s how it works in some browsers anyway. Other browsers (particularly those on mobile devices) actually ignore this, which can lead to some odd behavior, but we’ll come back to this later.

<audio id="Snare-Audio" preload="auto"> <source src="mp3/Snare.mp3" type="audio/mp3"> </audio>

Time to JavaScript!

We’ve got a lot to do before we can play our drums, but each drum part can be broken down into three steps:

Audio. Animation. Trigger these on user interaction.

Let’s start by storing the DOM elements we need as variables. E.g., for the snare we want the snare audio, and snare drum, both bits we want to animate and the parts that will trigger the animation and audio.

Tunes

Playing our audio is actually pretty straightforward: we simply use the .play() method on the audio element we’d like to play.

var snareAudio = $('#Snare-Audio'); snareAudio.get(0).play();

That’s it! Well almost… This works great, but if we call the method again while the audio is already playing, nothing will happen. We want the sound to be triggered from the beginning of the audio file every time a drum is triggered. So we need to set where the audio start position is every time it’s being triggered.

var snareAudio = $('#Snare-Audio'); var snareAudioEl = snareAudio.get(0); snareAudioEl.currentTime = 0; snareAudioEl.play();

Tweens

For those not familiar, TweenMax is a JavaScript library that handles “tweening” one or more properties of any object (or array of objects) over time. I’m not going to go too deep into the anatomy of every animation but each can be broken down into a few simple steps:

Create a new timeline Tween a property (or lots of properties) of the element Tween back to the original state

We’re simply trying to emulate something close to how we “expect” the drum to move when it’s hit. Sometimes it pays to exaggerate the amount of movement to get the desired effect. This is partly trial and error but fortunately there are lots of great examples of GreenSock in action you can check out for inspiration.Also, following people like Chris Gannon is a great way of seeing what GreenSock can do.

Finally, easing is one of the most useful tools GreenSock provides to give your animation the right “feel”. GreenSock have a great ease visualizer which can help you decide what’s right for your animation.

snareDrum = $('#Snare-Drum'); // Create a new timeline, that's paused by default var snaretl = new TimelineMax({ paused: true }); // The animation tweens snaretl.to(snareDrum, 0.1, {scaleX: 1.04, transformOrigin: "50% 50%", ease: Expo.easeOut}) .to(snareDrum, 0.1, {scaleY: 0.9, transformOrigin: "50% 100%", ease: Expo.easeOut}, '0') // The last tween, returns the element to it's original properties .to(snareDrum, 0.4, {scale: 1, transformOrigin: "50% 100%", ease: Elastic.easeOut});

Stop. Trigger Time

We have our audio and our animation ready. Now we just need to put it together and trigger it on a user interaction. We’ll wrap the audio and animation methods in a function:

function snare(){ snaretl.restart(); snaretl.play(); var snareAudioEl = snareAudio.get(0); snareAudioEl.currentTime = 0; snareAudioEl.play(); }

Then, call that function on a click/touch event:

var clickTouchSnareDone = false; snareDrumAll.on("touchstart click", function() { if(!clickTouchSnareDone) { clickTouchSnareDone = true; setTimeout(function() { clickTouchSnareDone = false; }, 100); snare(); return false; } });

When the user clicks or touches the drum it’ll trigger the animation and the audio. The next step is to hook up the keyboard.

document.onkeydown = function(e) { switch (e.keyCode) { case 72: snare(); break; } };

We can even animate the key guides when the keyboard is used:

function animateKey(key) { keytl = new TimelineMax({ paused: true }); keytl.to(key, 0.1, {scale: 1.1, transformOrigin: "50% 50%", ease: Expo.easeOut}) .to(key, 0.4, {scale: 1, transformOrigin: "50% 50%", ease: Elastic.easeOut}); keytl.restart(); keytl.play(); } document.onkeydown = function(e) { // This string will have to match the id of your key guides thisKeyID = 'Key-' + e.keyCode; thisKey = $('#' + thisKeyID); switch (e.keyCode) { case 72: snare(); animateKey(thisKey); break; } };

Rinse and repeat this process for each drum in our kit.

Now we’ve got a fully functioning drum kit we can play with a mouse, keyboard or on touch. But let’s not stop here.

Program the Programmable (Beats)

We’ve got playable drums, but could make our drums programmable. We’re going to build a sequencer that can be programmed to play loops. This is potentially quite complicated, but again, it can be broken down into simple steps:

Create a “matrix” that represents the drums and time (or beats)

Cycle through the columns or beats

If a drum is active in the current beat, play the drum

Build the Matrix

“Unfortunately, no one can be told what the Matrix is. You have to see it for yourself.” Morpheus

Fortunately, I can tell you exactly what our matrix is! It’s a simple grid where:

Rows will represent our drums

Columns will represent the beat

We need to be able to choose whether a drum is played or not on each beat in our sequencer. The < input> element with the type="checkbox" attribute is perfectly suited for this, however, it’s not that easy to style, but there’s a hack for that.

We’ll create a row for each drum and a column for each beat. In this demo we are going to use 8 beats. We’ll add a data-target-drum attribute (this could be called anything) with the name of our drum functions we created earlier. We can use the attribute to help us identify which corresponding function to call.

<!-- A "row" of our sequencer, we'll need one of these for each drum --> <div class="row" data-target-drum="snare"> <!-- This image indicates which drum the row controls --> <img src="img/snare.png"> <label><input type="checkbox"><div></div></label> <label><input type="checkbox"><div></div></label> <label><input type="checkbox"><div></div></label> <label><input type="checkbox"><div></div></label> <label><input type="checkbox"><div></div></label> <label><input type="checkbox"><div></div></label> <label><input type="checkbox"><div></div></label> <label><input type="checkbox"><div></div></label> </div>

And the function to cycle through the “rows” and “beat” at a time looks as follows:

rows = $('.row'); rowLength = rows.first().children().length; labels = $('label'); // Beat starts at 1 because 0 is the img for each row beat = 1; // Sequencer function sequencer () { labels.removeClass('active'); // Do this function for each .row $(rows).each(function() { // Select the child element at the "beat" index current = $(this).children().eq(beat); current.addClass('active'); // If the current input is checked do some stuff! if (current.find('input').is(":checked")) { targetDrum = (current.parent().attr('data-target-drum')); // If there a function that shares the same name as the data attribute, do it! fn = window[targetDrum]; if (typeof fn === "function") { fn(); } } }); // If we get to the last child, start over if ( beat < (rowLength - 1) ) { ++beat; } else { beat = 1; } }

Timing is Everything

We’ve got a function to play our drums on the current beat. But we need to cycle through these beats. We can do this using the .setInterval() method.

It essentially defines the frequency (in milliseconds) with which the function should be executed. So we can use this to start the sequencer and set its speed. In musical terms the frequency of beats (or tempo) is typically referred to as bpm or beats per minute. So the bpm can be defined simply as the number of milliseconds in a minute divided by the bpm. We’ll set the bpm to a sensible default of 150. And then we set the interval when we click the play button.

sequencerOn = false; // Start Sequencer $('#sequencer-active-btn').click(function () { intervalId = window.setInterval(sequencer, interval); sequencerOn = true; });

That works great, but what about when we need to stop the sequencer? We can stop the sequencer by passing the ID returned from the .setInterval() method to the .clearInterval() method:

sequencerOn = false; // Start/Stop Sequencer $('#sequencer-active-btn').click(function () { if (sequencerOn === false) { intervalId = window.setInterval(sequencer, interval); sequencerOn = true; } else { window.clearInterval(intervalId); sequencerOn = false; } });

Go Fast, Go Slow

“All right. Let me go for a few bars. Come in soft, but then finish strong.” Brennan Huff

It would be even more cool if we could change the bpm of our beats for faster or slower loops:

bpm = 150; interval = 60000 / bpm; // Set tempo function setTempo() { window.clearInterval(intervalId); intervalId = window.setInterval(sequencer, interval); } // Increase tempo $('#bpm-increase-btn').click(function() { if ( bpm < 300 ) { bpm = parseInt($('#bpm-indicator').val()); bpm += 10; interval = 60000 / bpm; $('#bpm-indicator').val(bpm); if(sequencerOn === true) { setTempo(); } } }); //Decrease tempo $('#bpm-decrease-btn').click(function() { if ( bpm > 100 ) { bpm = parseInt($('#bpm-indicator').val()); bpm -= 10; interval = 60000 / bpm; $('#bpm-indicator').val(bpm); if(sequencerOn === true) { setTempo(); } } });

Phoning it in

A lot of mobile browsers not only ignore the preload="auto" but will actually only load the audio on a user touch event. This basically means that our sequencer won’t trigger any audio until it has already been triggered by the user. So we need to load the audio on the first user interaction. This is a bit a of a hack, but it means the sequencer will work on mobile devices.

// Load audio on iOS devices on the first user interaction $('#sequencer-visible-btn').one('click', function() { $("audio").each(function(i) { this.play(); this.pause(); }); });

That’s it! We’ve built a drum kit we can play and we can program to play by itself. I hope this tutorial has inspired you to create your own interactive, animated SVG. Using the same techniques you could build other musical instruments, or anything! If you do, please let me know, I’d love to see what you create!

Chrome Supported

Firefox Supported

Internet Explorer Supported from version 11

Safari Supported

Opera Not supported

Note that while the functionality works from IE 9 on, the layout for the demo is only supported by modern browsers.