MobileMe Scrolling Image Viewer

My friend showed me photos that he has posted on-line in his MobileMe Gallery. Apple did a great job designing this web site. There is a lot to learn from this user experience. One element that I particularly like was the way a gallery is presented to the user in MobileMe. A gallery is presented as a thumbnail that indexes through photos in the gallery as you mouse over the image. During this post, I will replicate that experience. Here is a demonstration of the final design.

Demo

The trick to doing this is to leverage a ‘sprite’. A sprite is one image that is really a collection of images. Image a vertical filmstrip consisting of all the thumbnails we wish to display concatenated together. Using CSS we can set an offset into the sprite that determines which image is displayed. We will then add a bit of jQuery to dynamically update the sprite offset.

Note: This work was inspired by a Net Tuts article by Devon Govett. It seems that that article has been removed from Net Tuts.

1. Generate the Sprite

There are a lot of tools that you can use to generate a sprite. If you are going to generate a lot of sprites, then you will want to automate the process a bit. I wrote a quick C# console app to help with this. Here is the code:

static void Main(string[] args)
{
    string photoFolder = @"C:CodeDOTNETDemosMobileMePhotos";
    string thumbFolder = @"C:CodeDOTNETDemosMobileMeThumbs";

    // Get a list of images in the photos folder.
    //
    string[] fileNames = Directory.GetFiles(photoFolder, "*.jpg");

    // Create a thumbnail for each photo
    //
    List thumbNames = new List();
    foreach (string fileName in fileNames)
    {
        Bitmap bitmap = Images.CreateThumbNail(fileName,
            RotateFlipType.RotateNoneFlipNone, 160, 160);

        string thumbName = Path.Combine(thumbFolder,
            Path.GetFileName(fileName));
        bitmap.Save(thumbName);
        thumbNames.Add(thumbName);
    }

    // Create the spirtes
    //
    Bitmap vertSprite = Images.CreateSprite(thumbNames.ToArray(),
        Images.SpriteDirection.Vertical);
    string vertSpriteName = Path.Combine(thumbFolder, "SpriteV.jpg");
    vertSprite.Save(vertSpriteName);
    Bitmap horzSprite = Images.CreateSprite(thumbNames.ToArray(),
        Images.SpriteDirection.Horizontal);
    string horzSpriteName = Path.Combine(thumbFolder, "SpriteH.jpg");
    horzSprite.Save(horzSpriteName);
}

This code is was used to generate the sprite used for this demo. At the top, the paths of the image and thumbnail folder are hard-coded. Next we get a list of images in the image folder. Thumbnails are then generated. And finally a vertical and horizontal sprite is generated. This code uses two the ‘CreateThumbNail’ and ‘CreateSprite’ methods that I have in an image utility class. The code for these is provided below:

public static Bitmap CreateThumbNail(string imageFile,
    RotateFlipType rotateFlip, int thumbNailWidth, int thumbNailHeight)
{
    // Load the full size image.
    //
    System.Drawing.Image fullSizeImg;
    try
    {
        fullSizeImg = new System.Drawing.Bitmap(imageFile);

        // Do the rotation / flip on the image
        //
        fullSizeImg.RotateFlip(rotateFlip);
    }
    catch (Exception ex)
    {
        // If we could not load the full size image then exit early
        //
        return null;
    }

    // Create a blank thumbnail image
    //
    Bitmap thumbNailImg = new Bitmap(fullSizeImg,
        thumbNailWidth, thumbNailHeight);

    // Get the GDI drawing surface from the thumbnail image.
    //
    using (Graphics g = Graphics.FromImage(thumbNailImg))
    {
        // Initialize the drawing surface.
        //
        g.CompositingQuality = CompositingQuality.HighQuality;
        g.InterpolationMode = InterpolationMode.HighQualityBicubic;
        g.SmoothingMode = SmoothingMode.HighQuality;

        // Fill the image with white.
        //
        g.Clear(Color.White);

        // Create a rectangle the size of the new thumbnail
        //
        Rectangle destination = new Rectangle(0, 0,
            thumbNailWidth, thumbNailHeight);

        // Give a thumbnail size, the following code trys to
        //    maximize the pixels that are used to generate the
        //    thumbnail at the same time keeping the aspect
        //    ratio the same.
        //
        int viewPortWidth = fullSizeImg.Width;
        int viewPortHeight = fullSizeImg.Height;
        double alpha = (double)fullSizeImg.Width / (double)thumbNailWidth;
        double heightTest = alpha * (double)thumbNailHeight;
        if (heightTest > fullSizeImg.Height)
        {
            // Not enough pixels to scale width full size
            //    ...center horizontally
            viewPortWidth = (int)((double)thumbNailWidth *
                (double)fullSizeImg.Height / (double)thumbNailHeight);
        }
        else
        {
            // Not enough pixels to scale the height full size
            //    ...center vertically
            viewPortHeight = (int)((double)thumbNailHeight * alpha);
        }
        Rectangle source = new Rectangle(
            (fullSizeImg.Width - viewPortWidth) / 2,
            (fullSizeImg.Height - viewPortHeight) / 2,
            viewPortWidth,
            viewPortHeight);

        // Draw the selected portion of the original onto the
        //    thumbnail surface. This will scale the image.
        //
        g.DrawImage(fullSizeImg, destination, source, GraphicsUnit.Pixel);
    }

    return thumbNailImg;
}

This code use the .NET Graphics and Bitmap classes to generate the thumbnail. One trick is to specify the quality, interpolation, and smoothing that you want to use. There is also a bit of code to keep the aspect ratio of the thumbnail and at the same time maximize the pixels that are displayed in the thumbnail.

public enum SpriteDirection { Vertical, Horizontal };
public static Bitmap CreateSprite(string[] fileNames,
    SpriteDirection direction)
{
    // First load all images to determine extents of the images
    //
    List images = new List();
    int maxHeight = 0;
    int maxWidth = 0;
    foreach (string fileName in fileNames)
    {
        Bitmap bitmap = new Bitmap(fileName);
        images.Add(bitmap);

        maxHeight = Math.Max(bitmap.Height, maxHeight);
        maxWidth = Math.Max(bitmap.Width, maxWidth);
    }

    // Create a bitmap to hold the final image.
    //
    int finalHeight = maxHeight;
    int finalWidth = maxWidth;
    if (direction == SpriteDirection.Horizontal)
    {
        finalWidth *= images.Count;
    }
    else
    {
        finalHeight *= images.Count;
    }
    Bitmap sprite = new Bitmap(finalWidth, finalHeight,
        System.Drawing.Imaging.PixelFormat.Format32bppArgb);

    // Draw the bitmaps onto the images
    //
    using (Graphics g = Graphics.FromImage(sprite))
    {
        // First set the background to transparent. That way
        //    if the images are not the same size, those pixels
        //    will be transparent.
        //
        g.Clear(Color.Transparent);

        // Add each image to the sprite
        //
        int offset = 0;
        foreach (Bitmap image in images)
        {
            if (direction == SpriteDirection.Horizontal)
            {
                g.DrawImage(image, new Rectangle(offset, 0,
                    image.Width, image.Height));
                offset += maxWidth;
            }
            else
            {
                g.DrawImage(image, new Rectangle(0, offset,
                    image.Width, image.Height));
                offset += maxHeight;
            }
        }
    }

    return sprite;
}

This code takes a list of images (thumbnails in our case) and creates a sprite be concatenating them either vertically or horizontally. Again the .NET Graphics and Bitmap classes are used.

2. Create the HTML

The HTML for this demo is very simple.


    
 
by Bob Cravens

The only required element is the image div. The other two are used to provide feedback and a footer.

3. Add the CSS

The following CSS is used to style the document.

body
{
    margin: 0;
    padding: 0;
    background-color: #000;
    color: #FFF;
    text-align: center;
}

#image
{
    background: transparent url(images/SpriteV.jpg) no-repeat scroll 0 0;
    width: 160px;
    height: 160px;
    margin: 100px auto 10px auto;
    border-radius: 10px;
    -webkit-border-radius: 10px;
    -moz-border-radius: 10px;
}

The body element resets the margin and padding properties, sets a background color of black, a font color of white, and centers all text. The image element the sprite image set as its background image. The width and height constrain the visible area to a single thumbnail. The margin style is simply used to position the image with a bit of top margin and centered in the page. The last three lines set a rounded corner look. This is not supported by all browsers, but there are other ways to get the rounded corners.

4. Use jQuery to Select the Sprite Image

Now it is time to use jQuery to dynamically set the background position offset to show the image that corresponds to the mouse position. The following script takes care of that for us.

$(document).ready(function() {
    setupImage(10);
});
function setupImage(numPhotosInSprite) {
    $("#image").mousemove(function(e) {
        var image = $(this);
        var mouseX = e.pageX;
        var offset = image.offset();
        var imageLeft = offset.left;
        var relX = mouseX - imageLeft;
        var imageWidth = image.width();
        var imageHeight = image.height();
        var pixelPerImage = imageWidth / numPhotosInSprite;
        var index = Math.floor(relX / pixelPerImage);
        var posOffset = -imageHeight * index;
        image.css("background-position", "0 " + posOffset + "px");

        $("#mouse").html("relative x=" + relX + ", index=" + index);
    });
}

First, when the document loads we attach a ‘mousemove’ event to the image element. The callback function calculates the offset for the sprite and sets the CSS background-position style. The last line is not required, but provides position / index feedback for the demo.

Summary

Creating a scrolling image viewer to provide the user experience provided in MobileMe is fairly simple. It is a great experience.

Comments
  1. Robert Wilde

Leave a Reply to Robert Wilde Cancel reply

Your email address will not be published. Required fields are marked *

*