Spinner

Background Image Cross-Fading with Pre-loader

I wanted to implement an image gallery by cross-fading background images for Planet Diego. I was looking for a solution that would be performant and flexible. Performance is critical since the widget is an essential part of the homepage and the first image needs to be displayed immediately while continuing to load an arbitrary number of images in the background without affecting the rendering speed of the site. Furthermore, I wasn’t too keen on loading heavy weight JavaScript slider libraries with lots of features for a simple effect. Moreover, not too many offer cross-fading effects and don’t leverage hardware accelerated CSS transitions which would perform much smoother on a mobile dominated ecosystem.

Let’s assume we start with the following JSON structure stored at myApp.imageGallery. We have a “fallback” image in case the image array is returned empty and there is no gallery to display. The rotation speed (“rotateEvery”) is expressed in seconds and it is the trigger to advance to the next image. The “base” is simply the base URL for the images array. Lastly, the “images” is an array of filenames to be displayed.

{
    "fallback": "/static/images/home/default.jpg",
    "rotateEvery": 12,
    "base": "http://cdn.planetdiego.com/web/home/",
    "images": [ "image1.jpg", "image2.jpg", "image3.jpg", "image4.jpg", "image5.jpg" ] 
}

The following is the minimal CSS needed for the element holding the background image. Let’s assume for this example: <figure id='main-image'></figure>

#main-image {
    z-index: 1;
    background-size: cover;
    background-position: center center;
    background-blend-mode: darken;
    transition: 3s;
}

Here is the solution I came up with. Once simple validation is passed, “imageSequencer” is invoked a startup image from the array is display. Immediately after, the image preloading mechanism is started, thus caching the upcoming images for a smooth, flicker-free transition. Once caching is complete, setInterval timer is initiated to take over the automatic transition based on the interval specified.

$(function () {
    "use strict";

    var setMainImage = function (url) {
        $('#main-image').css('background-image', 'url(' + url + ')');
    };

    var rotateMainImage = function (args) {
        if (typeof args.images != 'undefined' && args.images != null && args.images.length > 0) {
            console.log('Initiating main image sequencer...');
            imageSequencer(args.base, args.images, args.rotateEvery);
        } else {
            console.log('Setting main image:' + args.fallback);
            setMainImage(args.fallback);
        }
    };

    var preloadImages = function (base, imagesArray, callback) {
        console.log('Caching main images');
        var i,
            j,
            loaded = 0;

        for (i = 0, j = imagesArray.length; i < j; i++) {
            (function (img, src) {
                img.onload = function () {
                    if (++loaded === imagesArray.length && callback) {
                        console.log('Firing image pre-loader callback.');
                        callback();
                    }
                };

                img.src = src;
            }(new Image(), base + imagesArray[i]));
        }
    };

    var imageSequencer = function (base, imagesArray, intervalSecs) {
        console.log('Rotating main image every ' + intervalSecs.toString() + ' seconds');
        var currImage = 0,
            totalImages = imagesArray.length,
            speed = intervalSecs * 1000;

        // Display initial image (last one to prevent repeat on cycle)
        setMainImage(base + imagesArray[totalImages - 1]);

        // Initiate sequencer
        preloadImages(base, imagesArray, function () {

            var siId = setInterval(function () {
                if (currImage >= totalImages) {
                    console.log('Restarting image sequencer.');
                    currImage = 0;
                }

                console.log('Setting main image through sequencer: ' + base + imagesArray[currImage]);
                setMainImage(base + imagesArray[currImage]);
                currImage++;

            }, speed);
        });
    };

    // Initiate Image crossfade
    rotateMainImage(myApp.imageGallery);

});

A couple of important things to note:

  • This solution works well on WebKit based browsers like Safari, Chrome and Opera.
  • Crossfading also works well on mobile devices.
  • On Firefox and Internet Explorer the image will just transition without any effects.
  • Make sure that “rotateEvery” is higher than the CSS Transition value, preferable a multiple of it.
  • For best results, make sure all the images have the same dimensions, or at least the same aspect ratio.
The resulting effect can be seen at http://planetdiego.com/

No comments: