You are previewing Supercharged JavaScript Graphics.

Supercharged JavaScript Graphics

Cover of Supercharged JavaScript Graphics by Raffaele Cecco Published by O'Reilly Media, Inc.
  1. Supercharged JavaScript Graphics
    1. SPECIAL OFFER: Upgrade this ebook with O’Reilly
    2. Preface
      1. Audience and Assumptions
      2. Organization
      3. Conventions Used in This Book
      4. Using Code Examples
      5. Safari® Books Online
      6. How to Contact Us
      7. Acknowledgments
    3. 1. Code Reuse and Optimization
      1. Keeping It Fast
      2. What and When to Optimize
      3. Homespun Code Profiling
      4. Optimizing JavaScript
      5. Optimizing jQuery and DOM Interaction
      6. Other Resources
    4. 2. DHTML Essentials
      1. Creating DHTML Sprites
      2. Converting into a jQuery Plug-in
      3. Timers, Speed, and Frame Rate
      4. Internet Explorer 6 Background Image Caching
    5. 3. Scrolling
      1. CSS-Only Scrolling Effects
      2. Scrolling with JavaScript
    6. 4. Advanced UI
      1. HTML5 Forms
      2. Using JavaScript UI Libraries
      3. Creating UI Elements from Scratch
    7. 5. Introduction to JavaScript Games
      1. Game Objects Overview
      2. The Game Code
    8. 6. HTML5 Canvas
      1. Canvas Support
      2. Bitmaps, Vectors, or Both?
      3. Canvas Limitations
      4. Canvas Versus SVG
      5. Canvas Versus Adobe Flash
      6. Canvas Exporters
      7. Canvas Drawing Basics
      8. Animating with Canvas
      9. Canvas and Recursive Drawing
      10. Replacing DHTML Sprites with Canvas Sprites
      11. A Graphical Chat Application with Canvas and WebSockets
    9. 7. Vectors for Games and Simulations
      1. Operations on Vectors
      2. Creating a JavaScript Vector Object
      3. A Cannon Simulation Using Vectors
      4. Rocket Simulation
    10. 8. Google Visualizations
      1. Limitations
      2. Chart Glossary
      3. Image Charts
      4. Interactive Charts
    11. 9. Reaching the Small Screen with jQuery Mobile
      1. jQuery Mobile
      2. TilePic: A Mobile-Friendly Web Application
      3. PhoneGap
    12. 10. Creating Android Apps with PhoneGap
      1. Installing PhoneGap
      2. Creating a PhoneGap Project in Eclipse
    13. Index
    14. About the Author
    15. Colophon
    16. SPECIAL OFFER: Upgrade this ebook with O’Reilly
O'Reilly logo

Creating UI Elements from Scratch

Using existing UI libraries makes perfect sense in many applications, but there are times when only a completely custom-coded widget will do. Frameworks like jQuery make this sort of thing a lot easier to develop, and you can tweak the element’s appearance and behavior in a completely free manner without having to worry about it “fitting in” with a UI framework.

You can also employ some of the techniques used in the sprites and games sections of this book to create dynamic widgets; for example:

  • Absolutely positioned DOM elements (position: absolute) for free-roaming widget elements

  • Timers for animation (setInterval(), setTimeout())

  • Background image position manipulation to reveal limited portions of a larger bitmap image

jQuery does have some animation facilities, as you’ll see in the TilePic game in Chapter 9, but writing customized animation code gives you the flexibility to apply more interesting effects. The following section describes how to create a 3D carousel widget that uses custom animation to scale and move elements in elliptical paths.

Creating a 3D Carousel

In this section, we will develop a carousel widget plug-in from scratch using jQuery. It takes a bunch of regular HTML images on a page (Figure 4-8) and transforms them into a spinning carousel widget with a 3D scaling effect (Figure 4-9).

Regular images, ripe for converting into a carousel

Figure 4-8. Regular images, ripe for converting into a carousel

Regular images converted into a 3D spinning carousel

Figure 4-9. Regular images converted into a 3D spinning carousel

Why would we want to do this?

  • It looks nice and adds visual interest.

  • Groups of images can take up less space.

  • It allows a varying number of images to occupy the same space.

Carousel specifications

When developing a user interface element like this, we need to take into account the diversity of target browsers and circumstances under which the page may be viewed. For example:

  • What happens if JavaScript is turned off?

  • What happens if a text-only screen reader is being used?

  • What happens on older browsers like IE6?

The user should be presented with the regular images (or text equivalents from alt tags) if the carousel cannot be initialized. It is up to the carousel plug-in to take these normal images and turn them into something more interesting if the browser environment facilitates this. It’s unacceptable for the images to simply disappear in their entirety if the carousel cannot be initialized. Also, the page’s HTML should not have to be compromised in terms of WC3 validation or semantics in order to use the carousel.

Although it’s not one of our deliberate goals, the carousel should work with older browsers such as IE6/7. Although the popularity of these insecure browsers is (thankfully) declining, there is still a substantial minority of people using them. According to Microsoft’s IE6 Countdown website (http://www.theie6countdown.com), a site designed to discourage use of IE6, 11.4% of Internet users were using IE6 as of April 2011.

Note

Although the carousel works with IE6, the PNG images used in the following example do not render correctly. If this is an issue, the simple fix is to instead use JPEG images, which render correctly on all browsers.

There should be no limit to the number of carousels that can be visible on the page. This means that we’ll need to develop the widget with nice encapsulated code that can be instanced an unlimited number of times. Implementing the carousel as a jQuery plug-in makes it easy to initialize multiple carousels. We just need to wrap the carousel images in elements that jQuery can identify, and apply the plug-in call to them. For example, the following code initializes a carousel on all wrapping elements with a CSS class of carousel3d:

$('.carousel3d').Carousel();

These additional specifications will also improve the look and feel of the carousel:

  • All images should retain their attributes and any event-based functionality attached to them.

  • Links surrounding the images should not be affected by the carousel.

  • The appearance of the carousel should be flexible in terms of the overall dimensions and scaling of the carousel items.

  • The carousel will automatically evenly space a variable number of elements.

  • The carousel elements should neatly fade in when their images load, avoiding any flickering or jerking effects as the DOM is changed.

  • When the user hovers his mouse over carousel items, the carousel will stop spinning, and start again when he moves the mouse away. This will make it easier to select items.

Carousel image loading

For the carousel to be initialized correctly, we must know the width and height of the image items in order to perform all the calculations related to carousel item positions and scaling. In an ideal world, we’d know the sizes of all images being used in the carousel before they’re loaded. In practice, this won’t necessarily be the case, but we can find the size of an image once it has loaded by reading its width and height properties.

However, detecting when an image has loaded is a more frustrating task than you might expect. It is not as simple as attaching a load event to an image and acting when the event occurs. Unfortunately, image load events are inconsistent across different browsers. Browsers may or may not trigger the load event for image loading, and if they do, they may not trigger the event when the image is loaded from the browser cache instead of the network. One fail-safe way of ensuring that images have been loaded is to listen for the window load event. When this event is fired, it means that all the page assets have been loaded. The drawback of this method is that the entire page must be loaded before the user can start interacting with the contents.

It might seem wasteful to trigger the loading of images that are already specified within image elements in the DOM. In fact, there is very little overhead involved, as the images will be obtained from the browser cache if they have been loaded previously.

The following loadImage() function facilitates image-loading initialization and detection. It takes into account the various browser idiosyncrasies, enabling image loading to be initialized and executing a callback function when the image has arrived either from the network or browser cache. The function works with existing image elements already in the DOM, or with image elements created with new Image(). loadImage() expects an image element, the source URL of the image, and a callback function as arguments.

// Function to execute a callback when an image has been loaded,
// either from the network or from the browser cache.

var loadImage = function ($image, src, callback) {

    // Bind the load event BEFORE setting the src.
    $image.bind("load", function (evt) {

        // Image has loaded, so unbind event and call callback.
        $image.unbind("load");
        callback($image);

    }).each(function () {
        // For Gecko-based browsers, check the complete property,
        // and trigger the event manually if image loaded.
        if ($image[0].complete) {
            $image.trigger("load");
        }
    });
    // For Webkit browsers, the following line ensures load event fires if
    // image src is the same as last image src. This is done by setting
    // the src to an empty string initially.
    if ($.browser.webkit) {
        $image.attr('src', '');
    }
    $image.attr('src', src);
};

Notice how the event is bound before the image source is set. This prevents a load event from being triggered for instantly loaded cached images before the event handler has been set up.

Carousel item objects

The carousel is composed of several carousel items that spin around a central point, shrinking into the distance to create a 3D effect. Each carousel item is treated as an individual object instance, created via the createItem() function. This function performs various tasks related to handling a single carousel item:

  • It triggers the initial image loading (via loadImage()) for the item (the image may already be in the browser cache).

  • Once the image has loaded, it fades in, and saves the width and height (orgWidth, orgHeight) for the scaling calculations in the update() function.

  • The update() function alters the item’s position, scale, and z depth according to the item’s rotation angle.

// Create a single carousel item.
var createItem = function ($image, angle, options) {
    var loaded = false, // Flag to indicate image has loaded.
        orgWidth,       // Original, unscaled width of image.
        orgHeight,      // Original, unscaled height of image.
        $originDiv,     // Image is attached to this div.

        // A range used in the scale calculation to ensure
        // the frontmost item has a scale of 1,
        // and the farthest item has a scale as defined
        // in options.minScale.
        sizeRange = (1 - options.minScale) * 0.5,

        // An object to store the public update function.
        that;

    // Make image invisible and
    // set its positioning to absolute.
    $image.css({
        opacity: 0,
        position: 'absolute'
    });
    // Create a div element ($originDiv). The image
    // will be attached to it.
    $originDiv = $image.wrap('<div style="position:absolute;">').parent();

    that = {
        update: function (ang) {
            var sinVal, scale, x, y;

            // Rotate the item.
            ang += angle;

            // Calculate scale.
            sinVal = Math.sin(ang);
            scale = ((sinVal + 1) * sizeRange) + options.minScale;

            // Calculate position and zIndex of origin div.
            x = ((Math.cos(ang) * options.radiusX) * scale) + options.width / 2;
            y = ((sinVal * options.radiusY) * scale) + options.height / 2;
            $originDiv.css({
                left: (x >> 0) + 'px',
                top: (y >> 0) + 'px',
                zIndex: (scale * 100) >> 0
            });
            // If image has loaded, update its dimensions according to
            // the calculated scale.
            // Position it relative to the origin div, so the
            // origin div is in the center.
            if (loaded) {
                $image.css({
                    width: (orgWidth * scale) + 'px',
                    height: (orgHeight * scale) + 'px',
                    top: ((-orgHeight * scale) / 2) + 'px',
                    left: ((-orgWidth * scale) / 2) + 'px'
                });
            }
        }
    };

    // Load the image and set the callback function.
    loadImage($image, $image.attr('src'), function ($image) {
        loaded = true;
        // Save the image width and height for the scaling calculations.
        orgWidth = $image.width();
        orgHeight = $image.height();
        // Make the item fade-in.
        $image.animate({
            opacity: 1
        }, 1000);

    });
    return that;
};

The image element passed to the createItem() function is the original one from the DOM. Apart from some minor CSS changes and being attached to a “handle” div element, the image element retains any events attached to it, and any wrapping anchor elements will still work.

The carousel object

The carousel object is the “brains” of the carousel, performing various initialization and processing tasks to handle the individual carousel items:

  • It iterates through all the image children of a wrapping element, initializing a carousel item for each image. It stores a reference to each carousel item in the items[] array.

  • It listens for mouseover and mouseout events that bubble up from the carousel items. When it detects a mouseover event on an image, the carousel pauses. When it detects a mouseout event, the carousel restarts after a small delay; the delay prevents sudden stop-start behavior as the user moves her mouse over the gaps between carousel items.

Finally, we create a setInterval() loop that updates a carousel rotation value and passes this to each carousel item by calling its update() function. The carousel performs this action every 30ms (or as specified in the options in the frameRate property). The default value of 30ms ensures smooth animation. Larger values will be less smooth but tax the CPU less; they may be suitable if the page contains several carousels.

// Create a carousel.
var createCarousel = function ($wrap, options) {
    var items = [],
        rot = 0,
        pause = false,
        unpauseTimeout = 0,
        // Now calculate the amount to rotate per frameRate tick.
        rotAmount = ( Math.PI * 2) * (options.frameRate/options.rotRate),
        $images = $('img', $wrap),
        // Calculate the angular spacing between items.
        spacing = (Math.PI / $images.length) * 2,
        // This is the angle of the first item at
        // the front of the carousel.
        angle = Math.PI / 2,
        i;

    // Create a function that is called when the mouse moves over
    // or out of an item.
    $wrap.bind('mouseover mouseout', function (evt) {
        // Has the event been triggered on an image? Return if not.
        if (!$(evt.target).is('img')) {
            return;
        }

        // If mouseover, then pause the carousel.
        if (evt.type === 'mouseover') {
            // Stop the unpause timeout if it's running.
            clearTimeout(unpauseTimeout);
            // Indicate carousel is paused.
            pause = true;
        } else {
            // If mouseout, restart carousel, but after a small
            // delay to avoid jerking movements as the mouse moves
            // between items.
            unpauseTimeout = setTimeout(function () {
                pause = false;
            }, 200);
        }

    });

    // This loop runs through the list of images and creates
    // a carousel item for each one.
    for (i = 0; i < $images.length; i++) {
        var image = $images[i];
        var item = createItem($(image), angle, options);
        items.push(item);
        angle += spacing;
    }

    // The setInterval will rotate all items in the carousel
    // every 30ms, unless the carousel is paused.
    setInterval(function () {
        if (!pause) {
            rot += rotAmount;
        }
        for (i = 0; i < items.length; i++) {
            items[i].update(rot);
        }
    }, options.frameRate);
};

The jQuery plug-in part

We initialize carousels via a standard jQuery plug-in function. This allows carousels to be initialized on any selector in the usual way. We could define the HTML layout of a five-element carousel and three-element carousel like this:

<div class="carousel" ><!-- This is the wrapping element -->
    <img src="pic1.png" alt="Pic 1"/>
    <img src="pic2.png" alt="Pic 2"/>
    <img src="pic3.png" alt="Pic 3"/>
    <img src="pic4.png" alt="Pic 4"/>
    <img src="pic5.png" alt="Pic 5"/>
</div>

<div class="carousel" ><!-- This is the wrapping element -->
    <img src="pic1.png" alt="Pic 1"/>
    <img src="pic2.png" alt="Pic 2"/>
    <img src="pic3.png" alt="Pic 3"/>
</div>

Notice the use of a wrapping div to define which elements are actually part of the carousel. In this example, we’ve applied the CSS class carousel to identify the wrapping elements, but you could use any other combination of selectors. You could wrap the individual image elements with link anchor elements, or bind events to them. Links and events will continue to work when the images become part of a carousel.

To initialize the two carousels, we make a standard jQuery plug-in call:

$('.carousel').Carousel();

Or with options:

$('.carousel').Carousel({option1:value1, option2:value2...});

Here is the plug-in code:

// This is the jQuery plug-in part. It iterates through
// the list of DOM elements that wrap groups of images.
// These groups of images are turned into carousels.
$.fn.Carousel = function(options) {
    this.each( function() {
        // User options are merged with default options.
        options = $.extend({}, $.fn.Carousel.defaults, options);
        // Each wrapping element is given relative positioning
        // (so the absolute positioning of the carousel items works),
        // and the width and height are set as specified in the options.
        $(this).css({
            position:'relative',
            width: options.width+'px',
            height: options.height +'px'
        });
        createCarousel($(this),options);
    });
};

We also define a set of default options. You can override these when initializing the carousel.

// These are the default options.
$.fn.Carousel.defaults = {
    radiusX:230,   // Horizontal radius.
    radiusY:80,    // Vertical radius.
    width:512,     // Width of wrapping element.
    height:300,    // Height of wrapping element.
    frameRate: 30, // Frame rate in milliseconds.
    rotRate: 5000, // Time it takes for carousel to make one complete rotation.
    minScale:0.60  // This is the smallest scale applied to the farthest item.
};

Carousel page layout

The following page layout (Example 4-3) defines a single carousel with nine carousel items. For demonstration purposes, one of the items is a link (the Leonardo da Vinci self-portrait), and one has a click event bound to it (the Mona Lisa).

Example 4-3. Two carousels set up on a page

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>Carousel</title>
    <style type="text/css">
        img { border:none;}
    </style>
    <script
        src="http://ajax.googleapis.com/ajax/libs/jquery/1.5.1/jquery.min.js">
    </script>

    <script type="text/javascript">

    // Start of jQuery carousel plug-in.
    (function($) {

        // Function to execute a callback when an image has been loaded,
        // either from the network or from the browser cache.
        var loadImage = function ($image, src, callback) {
            /*** CODE REMOVED FOR CONCISENESS ***/
        };

        // Create a single carousel item.
        var createItem = function ($image, angle, options) {
            /*** CODE REMOVED FOR CONCISENESS ***/
        };
        // Create a carousel.
        var createCarousel = function ($wrap, options) {
             /*** CODE REMOVED FOR CONCISENESS ***/
        };

        // This is the jQuery plug-in part. It iterates through
        // the list of DOM elements that wrap groups of images.
        // These groups of images are turned into carousels.
        $.fn.Carousel = function(options) {
              /*** CODE REMOVED FOR CONCISENESS ***/
        };

        // These are the default options.
        $.fn.Carousel.defaults = {
            /*** CODE REMOVED FOR CONCISENESS ***/
        };
    })(jQuery);
    // End of jQuery carousel plug-in.

    $(function(){
        // Create a carousel on all wrapping elements
        // with a class of .carousel.
        $('.carousel').Carousel({
            width:512, height:300,  // Set wrapping element size.
            radiusX:220,radiusY:70, // Set carousel radii.
            minScale:0.6           // Set min scale of rearmost item.

        });

        // Bind a click event to one of the pictures (Mona Lisa)
        // to show events are preserved after images become
        // carousel items.
        $('#pic2').bind('click', function() {
            alert('Pic 2 clicked!');
        });
    });
    </script>

</head>
<body>

    <div class="carousel" ><!-- This is the wrapping element -->
        <a href="http://en.wikipedia.org/wiki/Self-portrait_(Leonardo_da_Vinci)" 
            target="_blank">
            <img src="pic1.png" alt="Pic 1"/>
        </a>
        <img id="pic2" src="pic2.png" alt="Pic 2"/>
        <img src="pic3.png" alt="Pic 3"/>
        <img src="pic4.png" alt="Pic 4"/>
        <img src="pic5.png" alt="Pic 5"/>
        <img src="pic6.png" alt="Pic 6"/>
        <img src="pic7.png" alt="Pic 7"/>
        <img src="pic8.png" alt="Pic 8"/>
        <img src="pic9.png" alt="Pic 9"/>
    </div>

</body>
</html>

Try adjusting the code to include additional carousels with varying numbers of elements. Add more click functionality to some of the other images, or create links out of them.

The days of web applications looking like an inadequate homage to fancy native desktop applications is long gone. With all the tools currently available, there is no reason why a modern web application cannot look even better than its desktop equivalent. Indeed, with ever-improving browsers, JavaScript performance, and libraries, cloud-based web applications are a viable alternative to traditional native applications in many situations. And with the web-based approach, users get the added benefit of being able to keep their software up to date without any client installation and update hassles.

The best content for your career. Discover unlimited learning on demand for around $1/day.