It goes without saying that SVG isn't as widely used as many people in the web development community might believe it should be. Setting the debate aside, in this tutorial, I will demonstrate how to use inline SVGs, covering a range of techniques and exploring the interaction between web page and graphic. When used in conjunction with other emerging standards in HTML5, JavaScript and CSS3, inline SVGs can significantly add to the user experience.
Step 1: Create an HTML5 Page
You can interact with the SVG elements using DOM techniques, as you would with other web page elements.
In this tutorial, we are going to work through a practical example of an inline SVG by creating a simple component representing a record deck. The record will spin, and the user will be able to interact with it - pressing to slow it down, and releasing to speed it up again. The SVG will also contain a button which users can click to change the record, causing its appearance to alter slightly. The visible SVG elements will be enhanced with gradients and drop-shadow filters.
Check out the demo now so that you have a clear idea of what we are building.
As you likely will be aware, inline SVGs fall within the emerging HTML5 standards, so the code we use in this tutorial will not be fully supported across all browsers. Although inline SVGs are theoretically supported in all current versions of the major browsers, the internal animations and interactive effects that we'll use are not so well supported just yet. The final result should function correctly in current versions of Firefox, Chrome and Opera. As always with HTML5 techniques, make sure you don't rely on these effects in any live sites you work on, and include alternatives where possible.
Let's dig in, and begin by creating an HTML5 page outline, like so:
<!DOCTYPE html> <html> <head> <script> </script> <style> </style> </head> <body> </body> </html>
One of the primary advantages to using SVG is how scalable it is. To exploit this, we are primarily going to use relative values to define the SVG content. Add a container element for the SVG in the page body:
<div id="picHolder"> </div>
To see how the SVG sits within the containing element, add the following to the style section in your page head:
#picHolder {background:#dedeff; border:1px solid #666666;}
Step 2: Add the SVG Element
In the container element in the body of your page, add the SVG element outline, as follows:
<svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg" height="100%" width="100%"> </svg>
We've set the width and height to 100%, as we are going to specify the width of the containing element. However, rather than specifying the dimensions explicitly, we'll instead use a JavaScript function to reveal how easily you can scale the SVG up and down. You can include a fixed width and height in the opening SVG tag or the style section.
Step 3: Create the Definitions
The visible shapes in the graphic will be defined inside the SVG element. Before that, we will work on the defs section. The defs section is where you place definitions that you can later refer to when creating your shapes. For this tutorial, the defs section is going to contain definitions for a few gradient fills and a couple of drop shadows. Add this new section inside the SVG element:
<defs> </defs>
The items we place in this section will not actually appear in the image, but will be used as fills and filters for the shapes which do. We'll include several gradient fills; so let's work through each one in turn.
Gradients
First up is a linear gradient:
<linearGradient id="backGrad" x1="0%" y1="0%" x2="0%" y2="100%"> <stop offset="10%" style="stop-color:#990000; stop-opacity:1" /> <stop offset="90%" style="stop-color:#cccc00; stop-opacity:1" /> </linearGradient>
This gradient is going to be specified as the fill for the background rectangle area. The x1 and y1 attributes represent the starting points of the gradient within the filled shape, with the gradient unfolding from there to the point represented by x2 and y2. In this case, the gradient will run from top to bottom. The stop elements represent color points in the gradient. The first one states that 10% from the start of the gradient will be a solid dark red color and the second stop that 90% from the end of the gradient will be a yellow color. Between these two points, the gradient will blend the colors into one another. Both colors have full opacity.
Next let's add a gradient for the record itself. This one is a bit more complex - it's a radial gradient with several color stops:
<radialGradient id="recordGrad" cx="50%" cy="50%" r="50%" fx="50%" fy="50%"> <stop offset="30%" style="stop-color:#000000; stop-opacity:1" /> <stop offset="35%" style="stop-color:#222222; stop-opacity:1" /> <stop offset="45%" style="stop-color:#000000; stop-opacity:1" /> <stop offset="85%" style="stop-color:#000000; stop-opacity:1" /> <stop offset="95%" style="stop-color:#222222; stop-opacity:1" /> <stop offset="100%" style="stop-color:#000000; stop-opacity:1" /> </radialGradient>
A radial gradient starts from the inside of the circle, with the innermost and outermost parts of the circle defined by cx, cy, fx and fy, listed alongside the radius. In this case, the radial gradient is going to occupy the whole of the circular record shape. The bulk of the record will be black, with two rings of slightly lighter color representing the smoother sections at center of the record and its edges. We will be placing a label on the record at its center too, so the first patch of lighter color on the record will appear just outside that. Add the label gradient fill next:
<linearGradient id="labelGrad0" x1="0%" y1="0%" x2="100%" y2="0%"> <stop offset="40%" style="stop-color:#000099; stop-opacity:1" /> <stop offset="60%" style="stop-color:#009900; stop-opacity:1" /> </linearGradient>
This is a simple linear gradient that will be used as fill for the circular record label. However, notice that the gradient ID has a zero on the end of it. This is due to the fact that we're going to add an interactive function, allowing the user to "change the record". A JavaScript function will toggle between a range of gradient fills for the label element. For this purpose, add another couple of gradients:
<linearGradient id="labelGrad1" x1="0%" y1="0%" x2="100%" y2="0%"> <stop offset="0%" style="stop-color:#990000; stop-opacity:1" /> <stop offset="20%" style="stop-color:#ff6600; stop-opacity:1" /> <stop offset="40%" style="stop-color:#cccc00; stop-opacity:1" /> <stop offset="60%" style="stop-color:#009900; stop-opacity:1" /> <stop offset="80%" style="stop-color:#000099; stop-opacity:1" /> <stop offset="100%" style="stop-color:#990099; stop-opacity:1" /> </linearGradient> <linearGradient id="labelGrad2" x1="0%" y1="0%" x2="100%" y2="0%"> <stop offset="0%" style="stop-color:#330033; stop-opacity:1" /> <stop offset="100%" style="stop-color:#cc00cc; stop-opacity:1" /> </linearGradient>
The gradients each have an ID ending with an incrementing integer, so that we can iterate through them in JavaScript. Now, define another gradient to create a shine effect on top of the record:
<linearGradient id="shineGrad" x1="0%" y1="0%" x2="100%" y2="100%"> <stop offset="35%" style="stop-color:#000000; stop-opacity:0" /> <stop offset="50%" style="stop-color:#ffffff; stop-opacity:0.2" /> <stop offset="65%" style="stop-color:#000000; stop-opacity:0" /> </linearGradient>
This time, the gradient uses opaque and alpha transparent color stops; the effect will be a subtle shine across the record. Finally, we need a metallic fill for the button and spindle:
<radialGradient id="dialGrad" cx="50%" cy="60%" r="60%" fx="40%" fy="40%"> <stop offset="30%" style="stop-color:#cccccc;stop-opacity:1" /> <stop offset="100%" style="stop-color:#333333;stop-opacity:1" /> </radialGradient>
This time, the radial gradient is slightly off center to create a sense of depth and light, which will be complemented by a drop shadow filter.
Filters
Before we finish with the defs section, add a couple of drop shadows to give some of the shapes a bit more depth:
<filter id="recordShadow" x="0" y="0" width="200%" height="200%"> <feOffset result="offOut" in="SourceAlpha" dx="5" dy="5" /> <feGaussianBlur result="blurOut" in="offOut" stdDeviation="3" /> </filter>
This one is going to appear behind the record area. The x, y, width and height properties refer to the position and dimensions within the shape using this filter. The offset defines the shadow relative to the original shape. The blur prevents the offset shape from being solid color, so that it appears as a shadow. In this case only the shadow will appear, not the shape itself - the shadow is going to be defined by a dedicated shape which will be placed behind the record shape. For the user controls, which are circular and metallic, we also want a drop shadow but we want the shape itself to display as well:
<filter id="dialShadow" x="0" y="0" width="200%" height="200%"> <feOffset result="offOut" in="SourceAlpha" dx="2" dy="2" /> <feGaussianBlur result="blurOut" in="offOut" stdDeviation="1" /> <feBlend in="SourceGraphic" in2="blurOut" mode="normal" /> </filter>
This main difference here, apart from the scale of the shadow, is the blend element, which will preserve the original shape while also showing the shadow around it.
Step 4: Create the Shapes
That's enough preparation; let's get on with the graphics! Each item you add to the body of the SVG will be displayed on top of previously listed elements, so we will work from the bottom up, starting with the shapes at the back and ending with those at the front.
Background Rectangle
First, add a rectangle shape for the background:
<rect width="90%" height="90%" x="5%" y="5%" fill="url(#backGrad)" />
The rect element dimensions and position are specified relative to the containing SVG, which, if you remember, is relative to the size of the containing element. We will set this later in JavaScript. We will use relative size and position values wherever possible, so that the entire image plus animation and interaction can scale up or down on demand. Notice that the element fill specifies one of the gradients we defined, using its ID attribute.
Shadow
Next up from the bottom is the record shadow, using one of the drop shadow filters we created:
<circle cx="50%" cy="50%" r="33%" fill="#000000" filter="url(#recordShadow)" />
The shadow is going to lie behind the record, as a circular shape with a radius that is roughly a third of the space allocated to the image, placed in the center. Since the filter in this case does not apply blending with the image, the circle itself will not appear, just its shadow.
Record
Next up is the record itself:
<circle cx="50%" cy="50%" r="33%" fill="url(#recordGrad)" />
As with the shadow, the cx and cy attributes represent the center of the record, which is centered in the image horizontally and vertically, with a radius of about a third. Again, we use one of the gradients we defined, which we will do in each shape.
Label
On top of the record is its label, so add it next:
<circle id="recordLabel" cx="50%" cy="50%" r="10%" fill="url(#labelGrad0)" />
The label circle has the same central point as the record, across which it stretches about a third of the way. We start with the first of the label gradient options we defined, and will implement the user switching between these later - we include an ID attribute here to refer to this element in JavaScript.
Shine
Now, let's put some shine on top of the record:
<circle id="shine" cx="50%" cy="50%" r="32%" fill="url(#shineGrad)" />
When the record spins, it is going to move to the right and down just a little, so we keep the shine slightly smaller than the record so that it does not appear to spread beyond it when it moves. This element also has an ID attribute to detect user interaction.
Spindle
For completeness, let's add a little spindle in the center of the record:
<circle cx="50%" cy="50%" r="1%" fill="url(#dialGrad)" filter="url(#dialShadow)" />
This shape uses the metallic gradient we created. We also apply the second drop shadow filter, which includes blending so that the shape and shadow both appear.
Button
Last but not least, we need a little button for users to control changing the record, using the same fill and filter as the spindle:
<circle cx="83%" cy="83%" r="3%" fill="url(#dialGrad)" filter="url(#dialShadow)"> </circle>
This time, instead of a self-closing element, we separate the opening and closing circle tags. This is because we are going to animate the button when users click it, and will include the animation effect between these tags. Notice that we have been able to reuse fill and filter elements from the defs section. Here is the initial appearance of the graphic once the page dimensions are in place:
Step 5: Add Animation
Each item you add to the body of the SVG will be displayed on top of previously listed elements.
Now we have our visual elements in place, let's add some animation. We can make the record spin using SVG animation transformations, which are an extension of SMIL animation. These animated effects are defined within the SVG markup. An effect applies to whatever SVG element it appears within. You can use CSS3 transforms on SVG elements, but the SMIL-based alternatives give you a greater level of control.
We are going to include two simple animations: the record is going to spin and the button is going to move a little when the user clicks it. Let's start with the slightly more straight-forward animation for the button.
Inside the button shape element, between the opening and closing circle tags we created, add the animate transform as follows:
<animateTransform attributeType="XML" attributeName="transform" type="translate" from="0, 0" to="1, 1" dur="0.1s" begin="click" repeatCount="1" />
The animateTransform applies to an XML attribute within the element it appears in. In this case, it is a translate transform. The from and to attributes represent the start and end positions for the element - these are relative to its starting position, so the button is going to move to the right and down by a single pixel. The transform will begin when a user clicks, elapse over one tenth of a second, and execute once. The button will return to its original position when the animation completes. Tip: To keep an element in the end position after an animation, specify fill="freeze".
Now for spinning the record. An animateTransform applies to an SVG element, but we need the spin to apply to more than one element - specifically to the record and the label (not to the shine or shadow). Rather than creating separate animations for each and executing them concurrently, we can use a single transform by grouping these elements together. Before the circle element representing the record (with "recordGrad" as its fill) add an opening group tag:
<g>
After the circle representing the label, close the group:
</g>
Now add the transform before this closing group tag so that it applies to the whole group:
<animateTransform id="spinTrans" attributeType="XML" attributeName="transform" type="rotate" by="360, 1, 1" dur="1s" repeatCount="indefinite" />
This time, the animated effect is a rotate transform. The element will rotate by 360 degrees, and, to add to the effect, it will move to the right and down by a single pixel on each rotation, over a period of one second, repeating indefinitely. This transform will also include a from attribute, as it is necessary to specify the initial position of the elements being rotated. If you do not specify this position, the elements rotate around the 0, 0 point by default. However, at the moment, you cannot supply relative (i.e. percentage) values to these attributes, only fixed values. For this reason, we are going to set the from attribute when we specify the SVG dimensions in JavaScript.
Step 6: Add Interaction
Now let's implement our interactive functions: clicking the button to change the record and pressing the record to slow it down.
Change the Record
First, in the script section of your page head, add these variables to count and keep track of the label designs:
//keep track of current record label var currLabel = 0; //alter this for a different number of labels var numLabels = 3;
Now, inside the opening tag for the circle element representing the button (which now has an animation between its tags), add the following click event listener:
onclick="changeRecord()"
Back in the head script section, add the function outline:
function changeRecord() { }
Each time the user presses the button, we will move to the next label, moving back to the first when we reach the last one:
//move to next label currLabel++; //reset if at highest number if (currLabel > numLabels - 1) currLabel = 0; //set the fill attribute to the next gradient document.getElementById("recordLabel").setAttribute("fill", "url(#labelGrad"+currLabel+")");
The last line here demonstrates how you can interact with the SVG elements using DOM techniques, as you would with other web page elements. Here, we set the fill attribute of the label circle element to use the next gradient fill, specifying the fill ID.
Slow it Down
Now add the following event attributes to the record shine element (with "shineGrad" as its fill), as we are going to use mouse down and up events on it to trigger slowing the record down and speeding it up again:
onmousedown="onRecord()" onmouseup="offRecord()"
Back in the script section, add the function for when a user is pressing the record:
//function called when user is pressing record function onRecord() { }
Inside this function, we can slow the record spinning animation by altering the animateTransform duration attribute. We also alter the shine opacity to create the impression of pressing down:
//slow the animation duration document.getElementById("spinTrans").setAttribute("dur", "5s"); //decrease the shine opacity document.getElementById("shine").style.opacity="0.7";
When the user releases the record, we want it to go back to normal speed and appearance, so add the "mouse up" function next:
//function called when user releases record function offRecord() { //reset to normal speed document.getElementById("spinTrans").setAttribute("dur", "1s"); //set opacity back to normal document.getElementById("shine").style.opacity="1.0"; }
Step 7: Specify the Size
We can finally set the overall size of the SVG now. At the top of the script section, add a new variable:
//desired size of SVG var size = 300;
We will initially use 300
pixels for both the width and height of the graphic, but you are free to alter this at any point. Define a function in the script section to set these dimensions:
//function to set SVG dimensions function setSize() { //set css and transform size var holder = document.getElementById("picHolder"); holder.style.height=size+"px"; holder.style.width=size+"px"; document.getElementById("spinTrans").setAttribute("from", "0, "+size/2+", "+size/2+""); }
We set the size on the containing div element. Take a moment to look at the final line in this function. Since the rotate transform animation cannot use relative percentage values, we must set the from element using the size variable (divided by two for the central point of the record). With 300 as the SVG size, this is how the transform would appear with fixed values:
<animateTransform id="spinTrans" attributeType="XML" attributeName="transform" type="rotate" from="0, 150, 150" by="360, 1, 1" dur="1s" repeatCount="indefinite" />
If you want to use fixed values in your SVG you can do so. We are only using this technique to demonstrate using relative dimensions. Finally, call this function at the end of the script section:
window.addEventListener("DOMContentLoaded", setSize, false);
Conclusion
Our interactive SVG animation is now complete! Open your page in a supporting browser to view the effect; don't forget to try interacting with the record and button. Try altering the size variable to see how the SVG elements all adapt to suit, including the animations and interactions.
If you'd like to explore SVG further, some topics to consider include paths, text, masking and clipping. There are also a range of additional animation options to consider. Of course, these effects will not work for all users right now, but hopefully one day soon...
Comments