graceful CSS or MooTools Photo Stack Animations Effect

My favorite technological piece of Google Plus is its image upload and display handling.  You can drag the images from your OS right into a browser’s DIV element, the images upload right before your eyes, and the albums page displays a sexy photo deck animation when you hover over them.  Outstanding!  I’ve researched the effect a bit and was able to pull it off with both pure CSS and some MooTools JavaScript.

Initial Setup

The HTML setup is quite simple:






I’ve used an A elements so that we can also play the animation with focus via tab. With the images in place, we need to style the A and images themselves:

/* the A tag */
.album {
position: relative;
z-index: 5;
width: 250px;
vertical-align: top;
display:block;
}

/* the images */
.album img {
position: absolute;
top: 0;
left: 0;
border: 5px solid #f3f3f3;
box-shadow: 1px 1px 2px #666;
-webkit-box-shadow: 1px 1px 2px #666;
-moz-box-shadow: 1px 1px 2px #666;
width:250px;
height:250px;
display:block;
}

These styles act as a base to the CSS and MooTools methods of animation.

The CSS Version

Both the CSS-only and MooTools versions will rely on browser-provided CSS3 animations via transforms.  The transforms we’ll be using are rotate, translate, and scale.  The scale will be kept relatively small so that the magnification isn’t abrupt.

The first step is getting our @-keyframes properties set up so that we can use the animations later. The following CSS assumes there will be 3 photos and user Google’s animation values:

/* first image:  webkit */
@-webkit-keyframes image1 {
0%   { -webkit-transform: rotate(0deg) translate(0, 0) scale(1.0); }
100% { -webkit-transform: rotate(-6deg) translate(-100px, -3px) scale(1.1); }
}

/* first image:  firefox */
@-moz-keyframes image1 {
0%   { -moz-transform: rotate(0deg) translate(0, 0) scale(1.0); }
100% { -moz-transform: rotate(-6deg) translate(-100px, -3px) scale(1.1); }
}

/* first image:  opera */
@-o-keyframes image1 {
0%   { -o-transform: rotate(0deg) translate(0, 0) scale(1.0); }
100% { -o-transform: rotate(-6deg) translate(-100px, -3px) scale(1.1); }
}

/* second image:  webkit */
@-webkit-keyframes image2 {
0%   { -webkit-transform: rotate(0deg) translate(0, 0) scale(1.0); }
100% { -webkit-transform: rotate(0deg) translate(0, -3px) scale(1.1); }
}
/* second image:  firefox */
@-moz-keyframes image2 {
0%   { -moz-transform: rotate(0deg) translate(0, 0) scale(1.0); }
100% { -moz-transform: rotate(0deg) translate(0, -3px) scale(1.1); }
}
/* second image:  opera */
@-o-keyframes image2 {
0%   { -o-transform: rotate(0deg) translate(0, 0) scale(1.0); }
100% { -o-transform: rotate(0deg) translate(0, -3px) scale(1.1); }
}

/* third image:  webkit */
@-webkit-keyframes image3 {
0%   { -webkit-transform: rotate(0deg) translate(0, 0) scale(1.0); }
100% { -webkit-transform: rotate(6deg) translate(100px, -3px) scale(1.1); }
}

/* third image:  firefox */
@-moz-keyframes image3 {
0%   { -moz-transform: rotate(0deg) translate(0, 0) scale(1.0); }
100% { -moz-transform: rotate(6deg) translate(100px, -3px) scale(1.1); }
}

/* third image:  opera */
@-o-keyframes image3 {
0%   { -o-transform: rotate(0deg) translate(0, 0) scale(1.0); }
100% { -o-transform: rotate(6deg) translate(100px, -3px) scale(1.1); }
}

With the keyframes ready, it’s time to implement the CSS settings within the selectors themselves:

/* first image animation properties */
.album:hover .photo1, .album:focus .photo1 {

/* webkit animation properties */
-webkit-animation-name: image1;
-webkit-animation-duration: .2s;
-webkit-transform: rotate(-6deg) translate(-100px, -3px) scale(1.1);

/* firefox animation properties */
-moz-animation-name: image1;
-moz-animation-duration: .2s;
-moz-transform: rotate(-6deg) translate(-100px, -3px) scale(1.1);

/* opera animation properties */
-o-animation-name: image1;
-o-animation-duration: .2s;
-o-transform: rotate(-6deg) translate(-100px, -3px) scale(1.1);

/* microsoft animation properties */
-ms-transform: rotate(-6deg) translate(-100px, -3px) scale(1.1);
}

/* second image animation properties */
.album:hover .photo2, .album:focus .photo2 {

/* webkit animation properties */
-webkit-animation-name: image2;
-webkit-animation-duration: .2s;
-webkit-transform: rotate(0deg) translate(0, -3px) scale(1.1);

/* firefox animation properties */
-moz-animation-name: image2;
-moz-animation-duration: .2s;
-moz-transform: rotate(0deg) translate(0, -3px) scale(1.1);

/* opera animation properties */
-o-animation-name: image2;
-o-animation-duration: .2s;
-o-transform: rotate(-6deg) translate(-100px, -3px) scale(1.1);

/* microsoft animation properties */
-ms-transform: rotate(0deg) translate(0, -3px) scale(1.1);
}

/* third image animation properties */
.album:hover .photo3, .album:focus .photo3 {

/* webkit animation properties */
-webkit-animation-name: image3;
-webkit-animation-duration: .2s;
-webkit-transform: rotate(6deg) translate(100px, -3px) scale(1.1);

/* firefox animation properties */
-moz-animation-name: image3;
-moz-animation-duration: .2s;
-moz-transform: rotate(6deg) translate(100px, -3px) scale(1.1);

/* opera animation properties */
-o-animation-name: image3;
-o-animation-duration: .2s;
-o-transform: rotate(-6deg) translate(-100px, -3px) scale(1.1);

/* microsoft animation properties */
-ms-transform: rotate(6deg) translate(100px, -3px) scale(1.1);
}

The :hover state signals showtime, and the animation is wonderful.  You’ll see that I’ve duplicated the 100% setting within the :hover state as well, and that ensures the elements stay at their destination transformation after the animation is playing (otherwise the elements would abruptly revert to original state one the animation was done playing).

While this animation looks great, and requires no JavaScript, the abruptness in return to original state is slightly off-putting.  Time for some MooTools magic.

The MooTools Version

Since there was no method for animating each image to its original position with just CSS, and considering MooTools has the smoothest animations on the web, it was only natural that I use MooTools to complete the effect.  JavaScript is also nice in that we can do variable animations based on developer configuration and variable numbers of images.

After creating an initial inline-script, I refactored what I had to provide the following MooTools JavaScript class:

var PhotoStack = new Class({

Implements: [Options],

options: {
images: "img", // Which images inside the wrapper should we grab?
rotationMax: 6, // Rotation max (both positive and negative)
translationChange: 100, // Positive and negative,
translationPxChange: 3, // Only positive
scaleMax: 1.1, // Only positive, obviously,
duration: 100 // Animation duration
},

initialize: function(wrapper, options) {
this.setOptions(options);

// Sort out elements
this.wrapper = wrapper;
this.images = [];

// Add images
wrapper.getElements(this.options.images).each(this.addImage, this);
this.initialAdded = true;
this.calculateSteps();

// Add events
this.addEvents();

// This string will hold the proper calculation
this.calculatedStyle = "";
},

calculateSteps: function() {
// Get the images and calculation transformation values based on those images
var images = this.images,
numImages = images.length,
halfImages = (numImages / 2),
options = this.options;

// Calculate the fx properties
this.rotationIncrement = (options.rotationMax * 2 / (numImages - 1));
this.rotationStart = options.rotationMax * -1;
this.translationIncrement = options.translationChange / (numImages - 1);
this.translationStart = options.translationChange * -1;
this.translationPx = options.translationPxChange * -1;
},

addImage: function(image) {
this.images.push(image);
if(this.initialAdded) this.calculateSteps();
},

createFx: function(image) {
if(image.retrieve("photostack")) return;

// Create an instance of select
var fx = new Fx({ duration: this.options.duration });
fx.set = function(value) {

// Calculate image settings specific to this instance
var index = image.retrieve("photostack-index"),
targetRotation = (this.rotationStart + (index * this.rotationIncrement)), // deg
targetTranslation = (this.translationStart + (index * this.translationIncrement)), // px
targetTranslationPx = this.translationPx; //px

// Create the style string for this spot in the animation
var style = "rotate(" + (targetRotation * value) + "deg) translate(" + (targetTranslation * value) + "px, " + (targetTranslationPx * value) + "px) scale(" + (1 + (value * (this.options.scaleMax - 1))) + ")";

// Update those styles accordingly
image.setStyles({
"-webkit-transform": style,
"-moz-transform": style,
"-o-transform": style,
"-ms-transform": style,
transform: style
});
}.bind(this);

// Store the fx object
image.store("photostack", fx);
},

addEvents: function() {
var images = this.images, wrapper = this.wrapper;

// Create an event to lazyload photodeck fx creation
var lazyFxEvent = function() {
images.each(this.createFx, this);
wrapper.removeEvent("mouseenter", lazyFxEvent);
wrapper.removeEvent("focus", lazyFxEvent);
}.bind(this);

// Add the proper events
wrapper.addEvent("mouseenter", lazyFxEvent);
wrapper.addEvent("focus", lazyFxEvent);

// Create basic mouseenter/leave events
var todo = function(images, to, from) {
return function() {
images.each(function(image, index) {
image.store("photostack-index", index);
image.retrieve("photostack").start(to, from);
});
};
};

// Add the mouseenter and leave events to the album wrapper
wrapper.addEvents({
mouseenter: todo(images, 0, 1),
focus: todo(images, 0, 1),
mouseleave: todo(images, 1, 0),
blur: todo(images, 1, 0)
});
}
});

There can be a lot of math involved with allowing for a variable number of images, so to ease the burden of those calculations, I’ve placed “max” options within the class to manage those calculations for the developer.  Using the class is as simple as:

window.addEvent("domready", function() {
$$(".album2").each(function(album) {
new PhotoStack(album);
});
});

The animation back to original state is a nice change from the abrupt state restoration from the CSS-only solution.  Feel free to take and update the class however you’d like!

Download: http://www.htmldrive.net/items/download/927

Leave a comment