Sponsored by

  • Intel
  • HP

Web design

Create super-efficient SVG characters with localStorage

Jim Morrison explains how to paint SVG sprites using HTML5 localStorage as an intelligent cache, plus how to achieve the same effect in IE8 and working around the lack of native SVG support

  • Knowledge needed: Intermediate Javascript, basic Illustrator
  • Requires: Prototype.js and some SVG
  • Project time: 1-2 hours and drawing time

Download source files

View demo

SVG is a great way to paint and animate high quality, scalable vector graphics on the web. It's a form of XML and can be easily exported from vector packages, such as Illustrator, for use in your website. Once rendered it can be scaled to any size without losing quality and animated or manipulated like a collection of DOM objects.

At twiDAQ we have been using SVG to create our animated game character. Each player of the game has an imaginary stock broker, Jim, who affords himself better, smarter clothes and accessories as the player's portfolio develops. Each character is therefore slightly different for each player. Here is my broker for example.

Because he's quite detailed and there are a lot of different accessories and outfits the full SVG file is quite large (~2MB). So we need a way to download as little of the SVG as possible while being sure to avoid downloading any of the components or sprites required to build each character more than once, ever.

To achieve this we leverage the browser's localStorage to store any SVG sprite data we download. We can then use localStorage to access the SVG data next time we need it and paint the character on the screen using the localStorage data rather than re-downloading those sprites from the server.

Moreover when we come across a new character or when the player gets an upgrade we re-use as much data as we can and only download the new sprites we don't know about; radically reducing the amount of data required from the server over time.

The twiDAQ avatar, a stock broker called Jim, is a composite of SVG sprites stored in your browser's localStorage

Overview

Each character is described as as string of sprites that is given to us a part of each player's JSON object collected via the API:

  1. { jim: "trousers shirt blue-tie hair coat"}

When we need to paint a character we're going to look in localStorage for each of the sprites required and keep a list of any sprites that are missing.

If there are any missing sprites we request them from an API endpoint set up to serve any SVG sprites we need. SVG is a form of XML so each sprite is sent as a JSON string containing the XML data required to draw that particular component; an arm, green tie or shoe.

We store the new sprites in localStorage in case we need them again in the future and finally, now we have everything we need, we paint our character to the page using an SVG object or in the case of IE8, in Flash via svgweb.

localStorage and JSON

localStorage is a simple key/value pair database that allows you to store around 5MB of data per domain. Because it doesn't natively support the storage of JSON objects we will need to serialise our data to store it and deserialise it when we want to use it. Here's how we get and set our data in localStorage.

  1. // Store
  2. localStorage.setItem('hat', JSON.stringify( sprite ));
  3.  
  4. // Fetch
  5. sprite = JSON.parse( localStorage.getItem('hat' ));

The object sprite is a json object containing a string of XML from the API.

The API Endpoint

The endpoint takes a comma separated list of sprites and returns them as a JSON object containing the name and reference of the sprite and the sprite XML itself.

  1. // Grab the SVG from the database
  2. $oSprites = dbo::get('SELECT * FROM svg WHERE code IN (?)', $_GET['parts']);
  3.  
  4. // Construct our json
  5. $oJson = array();
  6. foreach ( $oSprites as $oSprite ){
  7.         $oJson[ $oSprite->code ] = array(
  8.                 'name'  => $oSprite->name,
  9.                 'code'  => $oSprite->code,
  10.                 'xml'   => $oSprite->xml,
  11.         );
  12. }
  13.  
  14. // Return our json
  15. print json_encode($oJson);

Making the follow request to the endpoint would provide us with the SVG for the character's hat and tie.

Each individual character is made up from dozens of smaller sprites including different suits, cuff links, ties, hairstyles and hats

Make sure we have everything we need

A basic character will be made up of a dozen or more sprites which have to be painted in order so that they layer correctly. We describe our character as a space delimited string which is split using prototype.js's $w(); function.

Once we have a list of character sprites we build a list of any that are missing, fetch those and then try again.

  1. function build_character(){
  2.        
  3.         // A stack for the missing sprites..
  4.         var missing = [];
  5.        
  6.         // Find missing items..
  7.         $w('hat coat tie').each(function( sprite_id ){
  8.                 if ( !localStorage.getItem( sprite_id ) ){
  9.                         // Record the missing sprite..
  10.                         missing.push( sprite_id );
  11.                 }
  12.         });
  13.        
  14.         // If there are missing items fetch them, otherwise just paint.
  15.         if ( !missing.length ) {
  16.                
  17.                 // Great!
  18.                 paint();
  19.                
  20.         } else {
  21.                
  22.                 // Fetch missing sprites..
  23.                 new Ajax({
  24.                         url: '/1/jim/about/elements.json',
  25.                         parameters: {
  26.                                 parts: missing.join(','),
  27.                         },
  28.                         // Callback the build_character() method when we're done
  29.                         oncomplete: function(response) {
  30.                                 response.responseJSON.each(function( sprite, sprite_id ){
  31.                                         // Store each sprite separately in local storage
  32.                                         localStorage.setItem( sprite_id, JSON.stringify( sprite ));
  33.                                 });
  34.                                 // and try again.
  35.                                 build_character();
  36.                         }
  37.                 });
  38.         }
  39.        
  40. }

Creating the SVG document

We're going to need somewhere to paint our SVG once we've got it ready so the next thing we need is a blank SVG document. This will work for all modern browsers and we'll look at an IE8 alternative later.

Our base SVG is a virtually blank document and can be heavily cached but it does carry a few key components; dimensions and a callback. Note particularly the onload event. When the browser has loaded the SVG document successfully it will fire our build_character(); method to start the process of downloading and painting our character.

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
  3. <svg id="svg-dom" version="1.1"
  4.         xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve"
  5.         x="0px" y="0px" viewBox="0 0 680 878"
  6.         onload="top.document.build_character();">
  7.                 <g id="svg_root"></g>
  8. </svg>

To put this blank document on the page and thereby start the whole process of loading and painting the character we add a single object tag onto the page.

  1. <object id="mySVG" data="blank.svg" type="image/svg+xml" width="500" height="500"></object>

And finally we're going to need a handle on the SVG's DOM for later.

  1. svg = $('mySVG').contentDocument;
With all the SVG sprites in localStorage the character can be painted without any further download

Painting the character

Now that we have all the sprite elements we need and our SVG object ready to receive our character we can paint him on the page.

Remember that our SVG document is an XML document that can be accessed much like any other XML document or the DOM. So for Chrome, Safari, Opera, Firefox and IE9 we can construct each sprite as an XML object and append it our SVG document. We're going to need a consistent method for parsing the XML in different browsers which we've called xml_parser().

  1. // Can we parse natively or do we need our own way of doing it?
  2. xml_parser = (window['parseXML']) ? window.parseXML : function (s,doc) {
  3.         doc = doc || document;
  4.         if (window.DOMParser) {
  5.                 parser=new DOMParser();
  6.                 xmlDoc=parser.parseFromString(s,"text/xml");
  7.                 return doc.adoptNode(xmlDoc.documentElement);
  8.         } else {
  9.                 xmlDoc=new ActiveXObject("Microsoft.XMLDOM");
  10.                 xmlDoc.async="false";
  11.                 xmlDoc.loadXML(s);
  12.                 return xmlDoc.documentElement;
  13.         }
  14. };

Now we have everything ready we iterate through our character's list of sprites, this time parsing each JSON object fetched from localStorage back to it's original JavasScript object state and grabbing the XML string from within. We use our xml_parser() method to construct an XML object of each sprite and then simply append it to our SVG document.

  1. // Iterate through our sprites and paint the character
  2. $w('hat coat tie').each(function( sprite_id ){
  3.  
  4.         if ( sprite = JSON.parse( localStorage.getItem( sprite_id ) ) ) {
  5.        
  6.                 // Create our SVG object
  7.                 var sprite_object       = xml_parser(
  8.                         '<svg xmlns="http://www.w3.org/2000/svg"><g id="g-wrap">'
  9.                         + sprite.svg +
  10.                         '</g></svg>'
  11.                 );
  12.                
  13.                 // Add it to the SVG DOM 'svg' we created earlier.
  14.                 svg.getElementById('svg_root').appendChild( sprite_object.childNodes[0] );
  15.                
  16.         }
  17.        
  18. });

Including Internet Explorer 8

The final piece of the jigsaw is making everything work for those Windows XP users who still use Internet Explorer. IE8 does have localStorage but it can't paint our SVG so in this one exception we're going to do the SVG painting using Flash and the brilliant svgweb JavaScript library.

We found that svgweb works best on page load and because twiDAQ is a fully AJAX/JSON site which doesn't often reload our solution for IE8 is to create a transparent <iframe/> whenever we need to paint a character. The <iframe/> itself contains a static, cacheable .html file that does the work for us.

The list of sprites is transferred to the <iframe/> using a hash so the browser can cache the file once.

  1. <iframe src="static/ie-svg.html#hat,tie,coat"></iframe>

The hash is then read from within the <iframe/>, the SVG is pulled from localStorage, concatenated and written into the page during load via a call to document.write().

  1. <!doctype html>
  2. <html>          
  3.         <head>
  4.                 <meta charset="utf-8" />       
  5.                 <meta name="svg.config.data-path" content="/images/js/libs/svgweb/">
  6.                 <meta name="svg.render.forceflash" content="true">
  7.                 <script type="text/javascript" src="/images/js/libs/svgweb/svg.js"></script>
  8.                 <script type="text/javascript">
  9.                         // Work out the sprites we want to use: ie.html#hat,tie,coat,pipe
  10.                         var hash        = document.location.hash.substr(1);
  11.                         var sprites     = hash.split(',');
  12.                        
  13.                         // Define string where we'll store our XML..
  14.                         var svg         = '';
  15.                        
  16.                         // Run through the hash and collect from localStorage
  17.                         for (i=0; i < sprites.length; i++){
  18.                        
  19.                                 if ( sprite = localStorage.getItem(sprites[i]) ){
  20.                        
  21.                                         // Concatenate our XML source
  22.                                         svg += JSON.parse(sprite).svg;
  23.                        
  24.                                 }
  25.                        
  26.                         }
  27.                        
  28.                 </script>
  29.         </head>
  30.         <body style="background:transparent;margin:0;padding:0;overflow:hidden;">
  31.                 <script type="text/javascript">
  32.                         if ( svg ) {
  33.                                 // Write the SVGWeb'esque script/object tag
  34.                                 document.write(
  35.                                         '<sc'+'ript type="image/svg+xml">'
  36.                                                 +'<svg xmlns="http://www.w3.org/2000/svg" width="200" height="500" version="1.1" baseProfile="full" id="twiDAQ_JIM">'
  37.                                                         +svg
  38.                                                 +'</svg>'
  39.                                         +'</scri'+'pt>'
  40.                                 );
  41.                                 // Whoop!
  42.                         }
  43.                 </script>
  44.         </body>
  45. </html>

A successful solution

If you could only see your own character on twiDAQ, it might have been practical to simply download your character in pure SVG. For a single character we were looking at between 400 and 800KB and the SVG could be cached. The problem really is that every time there's a slight variation you would have to make that full download again.

This solution allows us to show you anyone's character with a download requirement that reduces dramatically and quickly vanishes with each new character you view. It allows us to arbitrarily add features and mood to each character with little or no further download impact and finally because it's SVG rather than a static image; we can make him blink!

Just two examples of the twiDAQ characters available as you move through the game

If you find that you have a project where you're dealing with a lot of complex graphics that can be broken down into smaller sprites and that are similar but not always the same from page to page, perhaps this solution will prove useful to you.

I have to admit, because of the lack of localStorage, this solution isn't practical for IE6 and 7. One possible work-around is to generate images on the server and serve them up for browsers that lack localStorage. This is something we've chosen not to do at this stage but it is entirely possible with tools such as ImageMagick.

Words: Jim Morrison

Jim is the founder of twiDAQ and owner of Deep Blue Sky. He can be followed on App.net @jimbo or Twitter @jimbomorrison.

Subscription offer

Log in to Creative Bloq with your preferred social network to comment

OR

Log in with your Creative Bloq account

site stat collection