jQuery UI CoverFlow 2.0 Using $.widget
November 21, 2010
Hey guys. Today I would like to show you how to create and use a jQuery UI CoverFlow component powered by $.widget and CSS3 transforms. It's highly extensible, supports click, keyboard and mousewheel interaction and works with the latest versions of both jQuery and jQuery UI (1.4.4 and 1.8.6 respectively).
The iTunes CoverFlow effect is one of my favorite user-interface patterns - it's excellent for navigating around large collections of images (or data represented by images) and I thought it would be of great benefit to put out an updated version of this component out there.
If you would like to take a look at a demo or download the sources feel free to grab them below or at the end of this post.
Screenshot:
CoverFlow: A History Of Implementations
Origins
You may be surprised to know that there’s been quite a substantial body of work that has gone into bringing the CoverFlow effect to the web over the past few years.
The CoverFlow effect was originally conceived by Andrew Coulter Enright and implemented by independant Mac developer Jonathan del Strother in 2005. In 2006, Apple purchased the rights to CoverFlow and began including it in their bundled OSX applications, beginning with iTunes 7.0.
Ever since then we in the web development community have been fascinated with bringing this component in all it’s glory to the web. This has however been (and continues to be) something of a challenging feat.
When you look at the iTunes CoverFlow (see above) the first thing you’ll notice is that it’s almost 3D in nature. The shape of it’s covers are presented like trapeziums and there’s a level of depth to it that’s quite essential to the effect being correctly implemented. Striking the right balance in the CoverFlow’s 'stack' animation is also quite important for getting it right.
Before we go about building a new CoverFlow component, let’s take a look at how CoverFlow on the web has evolved over the years first for some context (feel free to skip this section if you would like to get right into the implementation being released today).
PHP + GD/ImageMagick (2006-2008)
The earliest attempts at implementing CoverFlow relied quite heavily on server-side image processing. The images one wished to be rendered as CoverFlow’s trapezium ‘covers’ would be stored on the sever, then read through a PHP script which would perform image-processing on your picture and then dump it back to the browser with the correct shapes, angles and depth in place.
The downsides to this were that CoverFlow is actually quite animation-focused and just rendering a single frame of this animation meant you lost quite a lot of the elegance CoverFlow had to offer. Server-side image processing is also not very efficient and I generally disapprove of using it in lieu of client-side alternatives.
Flash (2007)
Once developers realized the limitations of a pure PHP solution, they turned their heads to using Adobe’s Flash to implement the CoverFlow effect instead. Despite any criticisms we may have about Flash itself, a few years ago it was actually quite a decent framework for user interface ideas requiring math, transformations and even light image processing.
The Flash Coverflow implementations that were created would have been perfect but a non-plugin approach that was more ‘lightweight’ was still desired so our efforts again continued in search of a better approach to implementation.
JavaScript + Canvas Element (2007-2010)
From a development perspective the Canvas element is ideal for CoverFlow - it essentially has all the drawing capabilities you’d find in Flash with the added benefit of not requiring a plugin to view it in the browser. Trapeziums (arguably the most difficult part to get right in a CoverFlow) are also trivial to draw using canvas and as it seamlessly interacts with JavaScript, such implementations are quite easily extensible.
I created my first Canvas based CoverFlow component about two years ago and released an improved version last year. Canvas implementations of CoverFlow remain quite viable but their major drawback is that they aren’t as consistently fast nor as snappy as the next option I’ll be presenting.
jQuery CSS3 CoverFlow (2008-2010)
When implementing CoverFlow a developer always has to bare in mind that unlike a simple carousel, each ‘cover’ in a CoverFlow actually has to have it’s various depth and angular properties updated when you switch between covers. This is usually an expensive operation to achieve (if you’re using Flash or Canvas) but significantly less so when using CSS3.
CSS3 supports a number of transformations (include matrix transformations) which make it possible for us to ‘transform’ an image into a shape which closely resembles a trapezium. Through JavaScript (jQuery) we are then able to dynamically generate the transformation parameters required for each ‘cover’ in a relatively straight-forward manner anytime a user skips to the next or previous covers.
For today’s release, I revisited an experimental CSS3 CoverFlow project by the very talented Paul Bakaus which was released a few years ago. With this I’ve made a number of both visual and internal changes to it to make it more ‘CoverFlow’ like as I felt it was missing some of the depth and anglular requirements for the effect.
I also introduced a synchronized playlist and interaction features to the project, but one important update was that the component now works with jQuery 1.4.4 and jQuery UI 1.8.6.
This re-write will allow you to use the component in any new projects without any trouble.
Lets take a look at how the component was created.
Coding CoverFlow With The jQuery UI Widget Factory Using $.widget
The UI Widget Factory is a part of the jQuery UI Library that provides easy-to-use, object oriented ways to create jQuery plugins that are stateful - the new CoverFlow component is built using this. A stateful plugin is an advanced plugin that is considered self-aware. They maintain both their own state and they also often provide external interfaces that allow code outside of the plugin to interact with it’s state.
Widgets (another name for plugins that are stateful - known thus as we use $.widget to create them) often trigger events and offer callback hooks that connect with important aspects of their functionality. Plugins of this nature often focus on solving specific tasks and can be both very simple or quite complex. Examples of the latter include widgets like the Sliders and Dialogs you find in jQuery UI.
The Widget Factory is quite useful as it offers consistent, well defined structures for creating and interacting with stateful plugins. Remember that you don’t have to use the Factory to create a stateful plugin, however it does simplify the process by setting up a standard configuration. When using the Widget Factory the one thing to remember is that your end product isn’t a jQuery UI Widget - it’s still a jQuery plugin, albeit one with an API similar to jQuery UI.
The benefit of this is that developers who use your plugins may find it easier to learn how to use them and work with your plugin’s codebase.
Let’s Get Started With $.widget
To create our CoverFlow component using the Widget Factory, we’re first going to take a look at the standard structure of a Widget Factory $.widget plugin. For ease of understandability you can find inline comments that explain the different parts of the plugin in the code sample below.
Notes: The following default methods are available for each instance of a $.widget plugin:
- destroy(): Removes the instance from the encapsulated DOM element, which was stored on instance creation
- option(String key[, String value]): Gets or sets an option for this instance
- enable(): Set disabled option to false, it's up to the instance to respect the option
- disable(): Set disabled option to true, it's up to the instance to respect the option
and the following property is also available by default:
- options: The options for this widget instance, a mix of defaults with settings provided by the user
$.widget('namespace.nameOfPlugin', { // default options options: { value: 10, display: false }, _create: function(){ // anything called on initialization of the plugin //this.options is a combination of the defualt options and the ones passed in during the plugin initialization //here is an example of using this.options if (this.options.display) { //check the display parameter } }, _someprivatemethod: function(value) { //private internal functions //note: private functions should be named with a leading underscore //as will only be able to be called from inside the plugin //here we will return whatever value is passed + 100. return value + 100; }, somepublicmethod: function() { //this is a public function that can be called outside of the plugin //calling the private method from inside the public method this._privatemethod(); }, value: function(value) { //this is a public function that is defined as a getter //meaning it returns a value and not a jQuery object // no value passed, act as a getter if (value === undefined) { return this.options.value; // value passed, act as a setter } else { this.options.value = this._someprivatemethod(value); } }, destory: function() { $.widget.prototype.apply(this, arguments); // default destroy //this is where you might want to undo anything applied to the page } }));
With the above structure you can then simply call the following to initialize the plugin you have created:
$('#myelement').nameOfPlugin();
You can also call the public methods (such as ‘somepublicmethod()’) as follows, once the plugin has been initialized:
$('#myelement').nameOfPlugin('somepublicmethod'); //Initializing the plugin with a default value $('#myelement').nameOfPlugin({ value: 70 }; //Get the current value of value (a getter action) alert($('#myelement').nameOfPlugin('value'); //Set the current value of value (a setter action) $('#myelement').nameOfPlugin('value’, 45);
As you can see, creating stateful jQuery plugins doesn’t take very long at all and $.widget provides a convenient way to write widgets that follow a structure which can be a little easier to follow than plugins which don’t necessarily follow any set structures or design patterns.
If you would like to read up more on using $.widget, I recommending reading the following chapter of Rebecca Murphy’s jQuery Fundamentals book.
Implementing & Using CoverFlow
In this section I will be taking you through some of the core code involved in creating and using CoverFlow.
Digging Into CoverFlow
There are many different functions used to generate the overall CoverFlow animation effects but two of the core functions defined are called select() and _refresh(). select() performs the animation step when you select a CoverFlow item whilst what refresh() does is it regenerates the transformation information needed for a cover when you perform an action such as initializing the CoverFlow or clicking on a Cover to bring it into the main view.
What we’re doing in the below code sample is firstly establishing whether a cover is on the right or left of the CoverFlow (slightly different parameter values are used depending on which side). We’re then setting a z-Index for the cover relative to how high up the side’s stack an image is and then calculating the transformation matrix and scale values needed to shape the image in the form of a trapezium.
Note that vendor prefixes to be used are also set here depending on the browsers supported/being used.
select():
select: function(item, noPropagation) { this.previous = this.current; this.current = !isNaN(parseInt(item,10)) ? parseInt(item,10) : this.items.index(item); //If clicking on the same item, don't animate if(this.previous == this.current) return false; //Required: Overwrite the $.fx.step.coverflow everytime with //a custom scoped values for this specific animation var self = this, to = Math.abs(self.previous-self.current) <=1 ? self.previous : self.current+(self.previous < self.current ? -1 : 1); $.fx.step.coverflow = function(fx) { self._refresh(fx.now, to, self.current); }; // Stop the previous animation from running // Animate the parent's left/top property so //the current item is in the center // Use our custom coverflow animation which animates the item var animation = { coverflow: 1 }; animation[this.props[2]] = ( (this.options.recenter ? -this.current * this.itemSize/2 : 0) //Center the items container + (this.options.center ? this.element.parent()[0]['offset'+this.props[1]]/2 - this.itemSize/2 : 0) //Subtract the padding of the items container - (this.options.center ? parseInt(this.element.css('padding'+this.props[3]),10) || 0 : 0) ); //Trigger the 'select' event/callback if(!noPropagation) this._trigger('select', null, this._uiHash()); //Perform animation routine with easing. this.element.stop().animate(animation, { duration: this.options.duration, easing: 'easeOutQuint' }); }
_refresh():
this.items.each(function(i){ var side = (i == to && from-to < 0 ) || i-to > 0 ? 'left' : 'right', mod = i == to ? (1-state) : ( i == from ? state : 1 ), before = (i > from && i != to), css = { zIndex: self.items.length + (side == "left" ? to-i : i-to) }; css[($.browser.safari ? 'webkit' : 'Moz')+'Transform'] = 'matrix(1,'+(mod * (side == 'right' ? -0.2 : 0.2))+',0,1,0,0) scale('+(1+((1-mod)*0.3)) + ')'; css[self.props[2]] = ( (-i * (self.itemSize/2)) + (side == 'right'? -self.itemSize/2 : self.itemSize/2) * mod ); //handle browsers that don’t support transforms or //aren’t supported by CF if(!supportsTransforms) { css.width = self.itemWidth * (1+((1-mod)*0.5)); css.height = css.width * (self.itemHeight / self.itemWidth); css.top = -((css.height - self.itemHeight) / 2); } $(this).css(css); });
If you would like to dig deeper into the code, check out ui.coverflow.js in the release folder as this contains all of the code for this particular jQuery UI plugin.
Using CoverFlow
To use CoverFlow we first need to define a list of items which we which to use for our covers. This can be done using some simple HTML. We will also define some mark-up for the slider you see below the CoverFow as well as the image caption holder.
etc.Sample Text
For the purposes of this tutorial we will not be covering the scrollable vertical playlist, but you can find full details of this in the source code download pack. Next we’ll define some default CSS for the CoverFlow wrapper, the image list (#coverflow) and finally the image covers themselves.
div.wrapper { height: 390px; width: 800px; /*600*/ padding: 10px; overflow: hidden; position: relative; margin: 0 auto; } #coverflow { height: 300px; width: 2600px; padding: 42px; position: absolute; top: 0px; left: 0px; margin-top: 50px; } #coverflow img { width: 260px; height: 260px; float: left; position: relative; margin: -35px; }
Let’s now take a look at the JavaScript. As my demo includes quite a bit of additional interaction added on top-of it, I’ve kept all my code for it inside a separate app.js file. Let’s go through some of the core functionality defined here:
Initialization & Basic Navigation
//cache core component references var html = $('#demo-frame div.wrapper').html(); var imageCaption = $('#imageCaption'); $('#demo-frame div.wrapper').parent().append(html).end().remove(); $sliderCtrl = $('#slider'); $coverflowCtrl = $('#coverflow'); $coverflowImages = $coverflowCtrl.find('img'); $sliderVertical = $("#slider-vertical"); //app defaults var defaultItem = 0; var listContent = ""; //Set the default image index. setDefault(7); //Set the default item to display on load. //Correct indexing function setDefault($n){ defaultItem = $n-1; } //set the image caption function setCaption($t){ imageCaption.html($t); } //Initialize CoverFlow $coverflowCtrl.coverflow({ item: defaultItem, duration:1200, select: function(event, sky) { skipTo(sky.value); } }); //Initialize Horizontal Slider $sliderCtrl.slider({ min: 0, max: $('#coverflow > *').length-1, value: defaultItem, slide: function(event, ui) { $coverflowCtrl.coverflow('select', ui.value, true); $('.coverflowItem').removeClass('ui-selected'); $('.coverflowItem:eq(' + (ui.value) +')').addClass('ui-selected'); setCaption($('.coverflowItem:eq(' + (ui.value) +')').html()); } }); //Skip to an item in the CoverFlow function skipTo($itemNumber) { $sliderCtrl.slider( "option", "value", $itemNumber); $coverflowCtrl.coverflow('select', $itemNumber, true); $('.coverflowItem').removeClass('ui-selected'); $('.coverflowItem:eq(' + ($itemNumber) +')').addClass('ui-selected'); setCaption($('.coverflowItem:eq(' + ($itemNumber) +')').html()); } //Skip all controls to the current default item $('#sortable').html(listContent); skipTo(defaultItem); //Assign click event for coverflow images $('body').delegate('.coverflowItem','click', function(){ skipTo($(this).data('itemlink')); });
Keyboard Navigation
//Handle keyboard events (note: keydown is used rather than //keypress as Chrome/Webkit has known issues using the latter) $(document).keydown(function(e){ $current = $sliderCtrl.slider('value'); switch(e.keyCode){ case 37: if($current > 0){ $current--; skipTo($current); } break; case 39: if($current < $('#coverflow > *').length-1){ $current++; skipTo($current); } break; } });
MouseWheel Support
$(document).mousewheel(function(event, delta){ var speed = 1; var sliderVal = $sliderCtrl.slider("value"); var coverflowItem = 0; var cflowlength = $('#coverflow > *').length-1; //check the deltas to find out if the user //has scrolled up or down if(delta > 0 && sliderVal > 0){ sliderVal -=1; }else{ if(delta < 0 && sliderVal < cflowlength){ sliderVal +=1; } } //calculate the content top from the slider position var leftValue = -((100-sliderVal)*difference/100); //stop the content scrolling down too much if (leftValue>0) leftValue = 0; //stop the content scrolling up beyond point desired if (Math.abs(leftValue)>difference) leftValue = (-1)*difference; coverflowItem = Math.floor(sliderVal); skipTo(coverflowItem); //stop any default behaviour event.preventDefault(); });
Demos and Downloads Demo, Source, Github link.
You can try out a demo of CoverFlow below or download all the sources for today's release. Alternatively if you would like to make your own changes to the CoverFlow source you can use the GitHub link below to fork the project.
Conclusions
And that’s it!. Thanks for checking out this article - I hope it helps!. If you’ve found it useful please feel free to share it with your friends and colleagues just by hitting the retweet button below. Until next time, good luck with all your JavaScript projects!.
- Addy.