Sunday, June 10, 2012

Javascript Slideshow With Fade

I wanted a simple slideshow for our Analog Perfection™ website (don't bother going there--we won't have it up until just before launching the Kickstarter project).

The requirements were:
  • Free
  • Source code provided
  • Fade transitions
  • Small file size
  • Fast initial load (implying lazy loading of thumbnails)
  • Plain old Javascript (no dependencies on frameworks such as JQuery, MooTools, Dojo, Flash, etc.)
Nothing quite matched the requirements, so I wrote one, cobbling pieces together from answers on Stack Overflow, some help from Tyler Riding, Priyajeet Hora, Fro Rosqueta, and Doug Crossley (colleagues at Intuit), and a couple of key insights from the Tigra Fader sample code.

The result is under 4k, non-minified.

The code puts all of the slide thumbnails in one <a> element, as imgs, on top of each other, but with different z-ordering, and then adjusts the opacity of the current and next thumbnails.

You can see the pairwise opacity changes by looking at the elements in your browser's developer-tools console.

The HTML for all but the first thumbnail is generated dynamically, when the thumbnail lazy loads.

When the next thumbnail has crossed a threshold, the href is updated to point to that thumbnail's corresponding full-size picture.

Mousing into the thumbnail region accelerates the fading to 100%, then pauses the program. Mousing out of the thumbnail region resumes the program (as does clicking into the thumbnail).

I've only tested in on IE9 and Chrome so far, but have tried not to use anything quirksmode says won't work on older browsers. Eventually this will be deployed on a live site, and then I can start using something like browsershots to test, and then the cursing begins.

I embed the Javascript in my HTML like this:

<div id="slides">
<a id="slideLarge" href="./images/FrontView.jpg" target="_blank">
<img id="slideSmall0" src="./images/FrontViewSmall.jpg" alt="[unable to display picture]"/>
<!--INCLUDE("slides.js")INCLUDE-->
</a>
</div>

and use the Includer to expand the contents before deployment. If you don't want to do that, just lexically replace the INCLUDE line with the contents at the bottom of this post, or use a regular script src include.

Note that for browsers that have Javascript disabled, the first thumbnail and its associated full-size picture will still display correctly.

There are a number of arguments to the main function:
  • imgdir: Directory containing the thumbnails and full-size pictures--must end with '/'. If current directory, use "./".
  • fullid: HTML ID of the full-size picture.
  • idprefix: Shared prefix for the HTML IDs of all of the thumbnails.
  • width: Width in px (but without "px") of the thumbnails.
  • holdtime: How long in milliseconds to hold one thumbnail image constant before starting to fade.
  • fadetime: How long in milliseconds to take to perform a fade.
  • steps: How many steps to use to perform a fade. More is smoother but uses more CPU.
  • fastfadetime: How long in milliseconds to take to perform a fade when the mouse is over the thumbnail.
  • faststeps: How many steps to use to perform a fade when the mouse is over the thumbnail. More is smoother but uses more CPU.
  • flip: Percent opacity (as a number from 0.0 to 1.0) next thumbnail must have before clicking on it brings up its full-size picture instead of the current thumbnail's full-size picture.
  • names: Array of arrays where the inner arrays each have two elements: [the simple name of a thumbnail, the simple name of the full-size picture corresponding to the thumbnail]. The names of the first thumbnail and first full-size picture must not be included (they are extracted from the non-generated HTML during initialization).
The arguments are not passed in using name/value pairs, but are instead just positionally associated.

The thumbnails all have to be the same size (width and height) for the overlaying to work.

Using a naming convention for the shared ID prefix was done on purpose. If you don't like it, you can modify the code to add the prefix to the names as another element in each array, for all but the first thumbnail, and get rid of idprefix entirely. (But to do that, you need to obtain the ID of the 0th thumbnail by walking the DOM starting at the fullid. It's not difficult, but seemed like overkill.)

I considered using a naming convention to eliminate the need to pass in an array of arrays, and just pass in an array of names. But then what if the extensions differ between the two sizes of files?

Things left to do:
  • Add a row of numbers (or circles, or some other kind of icon) that indicates which slide is currently active, and to allow direct navigation to that slide by clicking on the number/icon, similar to how National Geographic and menucool do it. We don't really need it for our site, but it's an interesting exercise. To keep the layout clean, it could perhaps only display the row when the mouse is over the thumbnail, and display it over the thumbnail, at the bottom (not outside the thumbnail region), similar to how Dynamic Drive does it.
  • Add drag support, so the user can slide the thumbnails left or right by mouse down followed by mouse move. Don't treat the eventual mouse up as a click that opens the large picture.
  • Figure out how to get the width from the 0th slide, instead of having to pass it in, which is annoying. I tried style.clientWidth, style.offsetWidth, and various other suggestions online. Nothing worked, perhaps because at the point where the width is needed, the page hasn't been rendered yet, so the viewport size isn't known. If someone knows how to solve this, please post the solution!
Misc notes:
  • I tried a different approach before this one, but couldn't get it to work: manipulate the opacity of the link's background image and one img, and once whichever one of those is set to full opacity, replace the other one with a new image, then repeat the seesawing.
Here's the code, plus the CSS I used. (To get the pictures to line up, they need to share a margin, and to do that the margins have to be set on the elements--the CSS will help you get it working.)

CODE:
<script type="text/javascript" id="slidesjs">
<!--
// Copyright © 2012 jimandlisa.com. All rights reserved.
function slides(imgdir, fullid, idprefix, width, holdtime, fadetime, steps, fastfadetime, faststeps, flip, names) {

    "use strict";

    var full, thumbs, pos, cur, next, initial, paused, stepsize, steptime, curstep, step, up, down, flipped, dofast;

    function getname(url) {
        return url.substring(url.lastIndexOf('/') + 1);
    }

    function setsteps(fast) {
        if (fast) {
            stepsize = 1.00 / faststeps;
            steptime = Math.round(fastfadetime / faststeps);
            dofast = true;
        } else {
            stepsize = 1.00 / steps;
            steptime = Math.round(fadetime / steps);
            dofast = false;
        }
    }

    function setopac(style, opac) {
        style.opacity = opac;
        style.filters = "alpha(opacity=" + Math.round(opac * 100) + ')';
    }

    function fade() {

        if (paused) {
            if (!dofast) {
                setsteps(true);
            }
        }

        step = curstep * stepsize;
        up = (step > 1.000) ? 1.000 : step;
        down = ((up === 1.000) || ((1.000 - step) < 0.000)) ? 0.000 : (1.000 - step);
        up = (down === 0.000) ? 1.000 : step;
        curstep += 1;

        setopac(cur.style, down);
        setopac(next.style, up);

        if (!flipped && (up > flip)) {
            full.href = imgdir + names[pos][1];
            full.target = names[pos][1];
            flipped = true;
        }

        if (up === 1.000) {
            return;
        }

        setTimeout(function () { fade(); }, steptime);
    }

    function run() {

        if (!initial && !paused) {
            cur = thumbs[pos];
            pos = (pos + 1) % names.length;
            if (pos === thumbs.length) {
                next = document.createElement("img");
                next.id = idprefix + pos;
                next.src = imgdir + names[pos][0];
                next.alt = "[unable to display picture]";
                full.appendChild(next);
                setopac(next.style, 0.000);
                next.style.marginLeft = "-" + width + "px";
                next.style.zIndex = names.length - pos - 1;
                thumbs.push(next);
            } else {
                next = thumbs[pos];
            }
            curstep = 1;
            flipped = false;
            setsteps(false);
            fade();
        }

        initial = false;

        setTimeout(function () { run(); }, holdtime);
    }

    function noprop(e) {
        if (!e) {
            e = window.event;
        }
        e.cancelBubble = true;
        if (e.stopPropagation) {
            e.stopPropagation();
        }
    }
    function doAddHandler(obj, name, newhandler) {
        var curhandler = obj[name];
        if (curhandler === null) {
            obj[name] = newhandler;
        } else {
            obj[name] = function () {
                curhandler();
                newhandler();
            };
        }
    }

    function pause(e) {
        noprop(e);
        if (paused) {
            return;
        }
        paused = true;
    }

    function resume(e) {
        noprop(e);
        if (!paused) {
            return;
        }
        paused = false;
    }

    function init() {

        pos = 0;

        full = document.getElementById(fullid);
        full.target = names[pos][1];

        cur = document.getElementById(idprefix + '0');
        names.splice(0, 0, [getname(cur.src), getname(full.href)]);
        //width = cur.clientWidth;
        thumbs = [];
        setopac(cur.style, 1.000);
        cur.style.zIndex = names.length - 1;
        thumbs.push(cur);

        next = null;
        initial = true;
        paused = false;
        setsteps(fadetime, steps);

        doAddHandler(full, "onmouseover", pause);
        doAddHandler(full, "onmouseout", resume);

        run();
    }

    doAddHandler(window, "onload", init);
}

slides("./images/", "slideLarge", "slideSmall", 650, 5200, 1750, 80, 700, 70, 0.6, [["SideViewSmall.jpg", "SideView.jpg"], ["RearViewSmall.jpg", "RearView.jpg"]]);
//-->
</script>

CSS:
#slides {
    float:left;
    width:650px;
    padding:0px;
    margin-left:100px;
    display:inline;
}

#slides img {
    float:left;
    border:0px;
    position:relative;
}

No comments:

Post a Comment