Click here to Skip to main content
Click here to Skip to main content

Scattered Polaroids

, 29 Aug 2011
Rate this:
Please Sign up or sign in to vote.
Providing image animations with CSS3 and jQuery.
Prize winner in Competition "Best Web Development article of August 2011"

Table of Contents

Introduction

Scattered Polaroids is the first part of a project I'm developing for my friend Janaina Rossi Moreira, who works in a Brazilian theater company named "As Bárbaras do Multeatro".

The goal of the project is to publicize the work of the company, and at the same time provide the visitors with a nice audio and visual experience, relying on multimedia, videos, imagery, poems, and recollections that better capture the artistic essence of the company.

There are many ways one could develop to navigate through collections of photographs, usually in an orderly fashion. Scattered Polaroids is a different method, like when you take an old box full of pictures, drop them all on a table, and start picking them randomly. This article deals with this technique, and hopefully will be helpful for you in some way.

System Requirements

In order to run the Scattered Polaroids sample attached to this article, you must have a browser that supports CSS3 2D transforms:

  • Internet Explorer 9 or superior
  • Firefox
  • Safari
  • Chrome

The Polaroid Box

The Polaroid Box is the element that will hold the pictures and also receive some of the events needed for the mouse functionality.

It is mandatory that you have a polaroid_box div in your page, or another one that could serve as a holder for the pictures.

<body>
    <div class="polaroid_box">
    </div>
</body>

Loading the Images

The images are loaded in a simple array of strings that describes the URL path for the images:

$(document).ready(function () {
    var images = new Array();
    images[0] = 'Images/Barbaras1.jpg';
    images[1] = 'Images/Barbaras2.jpg';
    images[2] = 'Images/Barbaras3.jpg';
    images[3] = 'Images/Barbaras4.jpg';
    images[4] = 'Images/Barbaras5.jpg';
    images[5] = 'Images/Barbaras6.jpg';
    images[6] = 'Images/Barbaras7.jpg';
    images[7] = 'Images/Barbaras8.jpg';
    images[8] = 'Images/Barbaras9.jpg';
    images[9] = 'Images/Barbaras10.jpg';
    images[10] = 'Images/Barbaras11.jpg';
    images[11] = 'Images/Barbaras12.jpg';
    images[12] = 'Images/Barbaras13.jpg';
    images[13] = 'Images/Barbaras14.jpg';
    images[14] = 'Images/Barbaras15.jpg';
    images[15] = 'Images/Barbaras16.jpg';
    images[16] = 'Images/Barbaras17.jpg';
    images[17] = 'Images/Barbaras18.jpg';
    images[18] = 'Images/Barbaras19.jpg';
    images[19] = 'Images/Barbaras20.jpg';
    ...

Once the array is loaded, you can call the createPolaroids function to render all needed DOM elements inside the given parent element:

...
    maxZIndex = images.length;
    createPolaroids($('.polaroid_box'), images);
...

Creating the Polaroids

Here, the most important part is obviously creating the DOM elements that will render the polaroids in the page for us. Notice that we will rely heavily on the jQuery JavaScript framework. This will both reduce most of the boring work and also ensure that we will avoid cross-browser problems.

function createPolaroids($parentEl, images) {

    var parentWidth = $parentEl.width();
    var parentHeight = $parentEl.height();

    for (var i = 0; i < images.length; i++) {

        var polaroidHtml = 
            '<div id="polaroid' + (i + 1) + '" class="polaroid">' +
            '    <img id="img' + (i + 1) + '" class="photo" />' +
            '    <img src="Images/Polaroid_2.png" class="polaroid_photo" />' +
            '</div>';

        $parentEl.append(polaroidHtml);
        var $polaroid = $('#polaroid' + (i + 1));
        var $img = $('#img' + (i + 1));

        var width = $polaroid.width();
        var height = $polaroid.height();

        $img.attr('src', images[i]);
        $polaroid.css('z-index', 1000);
    }
...

Generating Random Positions and Rotations

We need to generate random positions and rotations for each polaroid, otherwise this couldn't be called "scattered" polaroids. So, all we need to do here is generate numbers that fall between acceptable boundaries of our screen (that is, the pictures are contained in a box, so we don't want our pictures falling off the screen).

for (var i = 0; i < images.length; i++) {
    var $el = $('#polaroid' + (i + 1));
    var windowWidth = $(window).width();
    var windowHeight = $(window).height();
    var offset = (windowWidth - windowHeight) / 2;
    var width = $el.width();
    var height = $el.height();
    var rotation = Math.random() * maxRotationAngle * 2 - maxRotationAngle;
    var left = offset + Math.random() * (parentHeight - width);
    var top = Math.random() * (parentHeight - height);
    ...
}

Now that we have gathered all the data needed for a polaroid, we create a new polaroid type that hold this information. This will be useful to encapsulate data and provide methods for manipulating the polaroids.

for (var i = 0; i < images.length; i++) {
    ...
    polaroids[i] = new polaroid(i + 1, $el, $parentEl, minScale, 
                   rotation, left, top, width / 2, height / 2, i);
}

The information is then provided to the polaroid function as a constructor, that will then hold the parameters in the instance:

function polaroid(id, $el, $parentEl, scale, rotation, left, 
         top, xTransOrigin, yTransOrigin, zIndex) {
    this.id = id;
    this.$el = $el;
    this.$parentEl = $parentEl;
    this.scale = scale;
    this.rotation = rotation;
    this.originalScale = scale;
    this.originalRotation = rotation;
    this.originalLeft = left;
    this.originalTop = top;
    this.originalZIndex = zIndex;
    this.startRotation = rotation;
    this.left = left;
    this.top = top;
    this.xTransOrigin = xTransOrigin
    this.yTransOrigin = yTransOrigin;
    this.interval = null;
    this.isMoving = false;
    this.zIndex = zIndex;
    this.xOffset = 0;
    this.yOffset = 0;
}

Positioning and Rotating the Polaroids

Once the polaroid data is loaded, we can then call the updateLayout prototype function that will update the required CSS attributes accordingly:

polaroid.prototype.updateLayout = function () {
    this.$el.css('left', this.left + 'px');
    this.$el.css('top', this.top + 'px');
    this.$el.css('z-index', this.zIndex);
    var transform = 
        'rotate(' + this.rotation + 'deg) scale(' + this.scale + ')';
    this.$el.css({ "-webkit-transform": transform, 
         "-moz-transform": transform, msTransform: transform });
};

It's important to notice that the last line in the code snippet above is providing the CSS transforms for the major browsers:

  • -webkit-transform: Chrome and Safari
  • -moz-transform: Firefox
  • msTransform: Internet Explorer

Using CSS3 Box Shadows

CSS3 box shadows provide the shadow that outlines our pictures. It's a nice feature and makes our pages visually appealing. But as happens with most of the nice things in this world, you should use it with moderation. A box shadow can require intense processing, depending on how you use it. Our polaroid CSS class defines a box shadow with an offset of (4,4) and a depth of 32 pixels:

.polaroid
{
    ...
    -moz-box-shadow: 4px 4px 32px #000;
    -webkit-box-shadow: 4px 4px 32px #000;
    box-shadow: 4px 4px 32px #000;
    ...
}

As you will see later on in this article, we remove the box shadow during the polaroid animations, for the benefit of better performance. As a tip, it could be a good idea to drop the use of box shadows at all, replacing them by a static .png image (PNG images allow transparency), thus improving the performance.

Dragging and Dropping Polaroids

The application knows which polaroid is being dragged by storing the polaroid instance in the selectedPolaroid variable. When you click on some polaroid, the mousedown event of the polaroid will be triggered and the selectedPolaroid will be stored.

$('.polaroid').mousedown(function (event) {
    var id = $(this).attr('id');
    var i = id.replace('polaroid', '');

    var polaroid = polaroids[i - 1];

    if (polaroid.scale == minScale) {
        polaroid.zIndex = maxZIndex++;
        polaroid.updateLayout();

        if (selectedPolaroid) {
            selectedPolaroid = null;
        }
        else {
            selectedPolaroid = polaroid;
            selectedPolaroid.xOffset = event.pageX - selectedPolaroid.left;
            selectedPolaroid.yOffset = event.pageY - selectedPolaroid.top;
        }
    }
    else {
        if (zoomedPolaroid.id == polaroid.id) {
            polaroid.drop();
        }
    }
});

From this point on, any mouse movement will be triggered by the mousemove event of the polaroid box, and the selected polaroid position will be updated:

$polaroid_box.mousemove(function (event) {
    if (selectedPolaroid) {
        selectedPolaroid.left = event.pageX - selectedPolaroid.xOffset;
        selectedPolaroid.top = event.pageY - selectedPolaroid.yOffset;
        selectedPolaroid.updateLayout();
    }
});

The drag-and-drop is finished when you release the mouse button. At this point, we set the selectedPolaroid variable to null.

$polaroid_box.mouseup(function (event) {
    if (selectedPolaroid) {
        selectedPolaroid.originalLeft = selectedPolaroid.left;
        selectedPolaroid.originalTop = selectedPolaroid.top;
        selectedPolaroid = null;
    }
});

Pulling a Polaroid

At some point, you find a picture interesting and want to see it closer. So you double-click it. This will trigger the animation functionality in our application that will bring a polaroid to the front. The picture will look greater and perfectly straight in the center of the screen. This is done by an animation, which is the more complex part of this application, to be explained better later on. Once a polaroid is taken, we set the zoomedPolaroid variable and wait until the user drops it or double-clicks another picture.

$('.polaroid').dblclick(function () {
    var id = $(this).attr('id');
    var i = id.replace('polaroid', '');

    var polaroid = polaroids[i - 1];

    if (zoomedPolaroid) {
        zoomedPolaroid.zIndex = maxZIndex++;
        zoomedPolaroid.updateLayout();
        zoomedPolaroid.drop(null, function () {
            zoomedPolaroid = null;
        });
    }

    if (polaroid.scale == minScale) {
        polaroid.originalZIndex = maxZIndex++;
        polaroid.zIndex = maxZIndex + 1000;
        polaroid.updateLayout();
        polaroid.pull(null, function () {
            zoomedPolaroid = polaroids[polaroid.id - 1];
            selectedPolaroid = null;
        });
    } else {
        polaroid.originalZIndex = maxZIndex++;
        polaroid.zIndex = maxZIndex++;
        polaroid.drop();
    }
});

Dropping a Polaroid

The zoomed polaroid is dropped when the user clicks on it or double-clicks another polaroid. This will start the reverse animation, and the polaroid will leave the center of the screen and go back to its previous position, scale, and rotation angle.

Animating Transitions via JavaScript

The animations are the complex part. They require the target scale, target rotation angle, and target position. Since the polaroid may be going to the center or coming back to its previous location, we avoid code duplication by encapsulating the core functionality inside the animate function.

First, we calculate the deltas (one for the angle, another one for the scale, and two deltas for the location). Then we calculate the steps (that is, the increment to be applied to the current animation value at each iteration). Then we start the setInterval function to reposition the polaroid, rotate it, and update the underlying DOM element, until the animation reaches the target values.

polaroid.prototype.animate = function (targetScale, targetRotation, 
         targetLeft, targetTop, beginCallback, endCallback) {
    var pol = this;

    if (pol.isMoving)
        return;

    pol.isMoving = true;

    var parentWidth = this.$parentEl.width();
    var parentHeight = this.$parentEl.height();

    var polaroid = pol;

    if (beginCallback)
        beginCallback(polaroid);

    var angleDelta = targetRotation - polaroid.rotation;
    var scaleDelta = targetScale - polaroid.scale;
    var leftDelta = targetLeft - polaroid.left;
    var topDelta = targetTop - polaroid.top;

    var steps = animationSteps;
    var scaleStep = scaleDelta / steps;
    var rotationStep = angleDelta / steps;
    var leftStep = leftDelta / steps;
    var topStep = topDelta / steps;

    var interval;

    var rotation = polaroid.rotation;
    var scale = polaroid.scale;
    var left = polaroid.left;
    var top = polaroid.top;

    var $polaroid = $('#polaroid' + pol.id);

    $polaroid.css('z-index', 100);

    $polaroid.css({ "-moz-box-shadow": "0 0 0 #fff" });
    $polaroid.css({ "-webkit-box-shadow": "0 0 0 #fff" });
    $polaroid.css({ "box-shadow": "0 0 0 #fff" });

    var interval = setInterval(function () {
        if (
                (scaleStep > 0 && scale >= targetScale) ||
                (scaleStep < 0 && scale <= targetScale)
            ) {
            scale = targetScale;
        }

        $polaroid.css({ WebkitTransform: 'rotate(' + rotation + 
                        'deg) scale(' + scale + ')' });
        $polaroid.css({ '-moz-transform': 'rotate(' + rotation + 
                        'deg) scale(' + scale + ')' });
        $polaroid.css({ msTransform: 'rotate(' + rotation + 
                        'deg) scale(' + scale + ')' });
        $polaroid.css('left', left + 'px');
        $polaroid.css('top', top + 'px');

        if (scale == targetScale) {
            $polaroid.css({ "-moz-box-shadow": "4px 4px 32px #000" });
            $polaroid.css({ "-webkit-box-shadow": "4px 4px 32px #000" });
            $polaroid.css({ "box-shadow": "4px 4px 32px #000" });

            pol.isMoving = false;

            clearInterval(interval);

            rotation = targetRotation;
            scale = targetScale;
            left = targetLeft;
            top = targetTop;

            pol.rotation = targetRotation;
            pol.scale = targetScale;
            pol.left = targetLeft;
            pol.top = targetTop;

            pol.updateLayout();

            if (endCallback)
                endCallback(polaroid);
        }

        rotation += rotationStep;
        scale += scaleStep;
        left += leftStep;
        top += topStep;
    }, 5);
};

Final Considerations

I hope you liked both the article and the code. I think the techniques presented here are simple and useful. Please let me know what you think about it, by leaving a comment in the section below.

History

  • 2011-08-28: Initial version.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Marcelo Ricardo de Oliveira
Software Developer
Brazil Brazil
Marcelo Ricardo de Oliveira is a senior software developer who lives with his lovely wife Luciana and his little buddy and stepson Kauê in Guarulhos, Brazil, is co-founder of the Brazilian TV Guide TV Map and currently works for ILang Educação.
 
He is often working with serious, enterprise projects, although in spare time he's trying to write fun Code Project articles involving WPF, Silverlight, XNA, HTML5 canvas, Windows Phone app development, game development and music.
 
Published Windows Phone apps:
 
 
Awards:
 
CodeProject MVP 2012
CodeProject MVP 2011
 
Best Web Dev article of March 2013
Best Web Dev article of August 2012
Best Web Dev article of May 2012
Best Mobile article of January 2012
Best Mobile article of December 2011
Best Mobile article of October 2011
Best Web Dev article of September 2011
Best Web Dev article of August 2011
HTML5 / CSS3 Competition - Second Prize
Best ASP.NET article of June 2011
Best ASP.NET article of May 2011
Best ASP.NET article of April 2011
Best C# article of November 2010
Best overall article of November 2010
Best C# article of October 2010
Best C# article of September 2010
Best overall article of September 2010
Best overall article of February 2010
Best C# article of November 2009

Comments and Discussions

 
Question5 from me Pinmember_Gandalf - The White_12-Jan-14 23:05 
GeneralMy vote of 5 Pinmembercsharpbd13-Nov-12 19:04 
GeneralMy vote of 5 Pinmemberofsarac13-Jul-12 1:37 
GeneralMy vote of 5 PinmemberSophia_xd21-Dec-11 0:48 
GeneralRe: My vote of 5 PinmvpMarcelo Ricardo de Oliveira1-Mar-12 15:46 
QuestionNice article. About performance issue. PinmemberSyed BASHAR17-Sep-11 9:05 
AnswerRe: Nice article. About performance issue. PinmvpMarcelo Ricardo de Oliveira17-Sep-11 12:39 
GeneralRe: Nice article. About performance issue. PinmemberSyed BASHAR17-Sep-11 17:18 
GeneralMy vote of 5 PinmemberDave Kerr12-Sep-11 21:12 
GeneralRe: My vote of 5 PinmvpMarcelo Ricardo de Oliveira13-Sep-11 2:19 
QuestionMy Vote 5 PinmemberShakeel Iqbal10-Sep-11 10:44 
AnswerRe: My Vote 5 PinmvpMarcelo Ricardo de Oliveira11-Sep-11 8:58 
GeneralMy vote of 5 Pinmemberfrazzle-me7-Sep-11 12:08 
GeneralRe: My vote of 5 PinmvpMarcelo Ricardo de Oliveira8-Sep-11 4:37 
GeneralRe: My vote of 5 Pinmemberfrazzle-me8-Sep-11 4:43 
General5 PinmemberMDL=>Moshu7-Sep-11 7:49 
GeneralRe: 5 PinmvpMarcelo Ricardo de Oliveira8-Sep-11 4:35 
GeneralMy vote of 5 PingroupSaraf Talukder6-Sep-11 9:32 
GeneralRe: My vote of 5 PinmvpMarcelo Ricardo de Oliveira6-Sep-11 11:02 
GeneralMy vote of 5 PinmemberBrianBissell1-Sep-11 10:10 
GeneralRe: My vote of 5 PinmvpMarcelo Ricardo de Oliveira1-Sep-11 11:31 
GeneralMy vote of 5 Pinmembermahendren30-Aug-11 19:44 
GeneralRe: My vote of 5 PinmvpMarcelo Ricardo de Oliveira31-Aug-11 3:28 
QuestionVery well written article PinmemberEspen Harlinn30-Aug-11 13:17 
AnswerRe: Very well written article PinmvpMarcelo Ricardo de Oliveira30-Aug-11 15:03 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140814.1 | Last Updated 29 Aug 2011
Article Copyright 2011 by Marcelo Ricardo de Oliveira
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid