/*! * waterwheel carousel * version 2.3.0 * http://www.bkosborne.com * * copyright 2011-2013 brian osborne * dual licensed under gplv3 or mit * copies of the licenses have been distributed * with this plugin. * * plugin written by brian osborne * for use with the jquery javascript framework * http://www.jquery.com */ ;(function ($) { 'use strict'; $.fn.waterwheelcarousel = function (startingoptions) { // adds support for intializing multiple carousels from the same selector group if (this.length > 1) { this.each(function() { $(this).waterwheelcarousel(startingoptions); }); return this; // allow chaining } var carousel = this; var options = {}; var data = {}; function initializecarouseldata() { data = { itemscontainer: $(carousel), totalitems: $(carousel).find('img').length, containerwidth: $(carousel).width(), containerheight: $(carousel).height(), currentcenteritem: null, previouscenteritem: null, items: [], calculations: [], carouselrotationsleft: 0, currentlymoving: false, itemsanimating: 0, currentspeed: options.speed, intervaltimer: null, currentdirection: 'forward', leftitemscount: 0, rightitemscount: 0, performingsetup: true }; data.itemscontainer.find('img').removeclass(options.activeclassname); } /** * this function will set the autoplay for the carousel to * automatically rotate it given the time in the options * can clear the autoplay by passing in true */ function autoplay(stop) { // clear timer cleartimeout(data.autoplaytimer); // as long as no stop command, and autoplay isn't zeroed... if (!stop && options.autoplay !== 0) { // set timer... data.autoplaytimer = settimeout(function () { // to move the carousl in either direction... if (options.autoplay > 0) { moveonce('forward'); } else { moveonce('backward'); } }, math.abs(options.autoplay)); } } /** * this function will preload all the images in the carousel before * calling the passed in callback function. this is only used so we can * properly determine the width and height of the items. this is not needed * if a user instead manually specifies that information. */ function preload(callback) { if (options.preloadimages === false) { callback(); return; } var $imageelements = data.itemscontainer.find('img'), loadedimages = 0, totalimages = $imageelements.length; $imageelements.each(function () { $(this).bind('load', function () { // add to number of images loaded and see if they are all done yet loadedimages += 1; if (loadedimages === totalimages) { // all done, perform callback callback(); return; } }); // may need to manually reset the src to get the load event to fire // http://stackoverflow.com/questions/7137737/ie9-problems-with-jquery-load-event-not-firing $(this).attr('src', $(this).attr('src')); // if browser has cached the images, it may not call trigger a load. detect this and do it ourselves if (this.complete) { $(this).trigger('load'); } }); } /** * makes a record of the original width and height of all the items in the carousel. * if we re-intialize the carousel, these values can be used to re-establish their * original dimensions. */ function setoriginalitemdimensions() { data.itemscontainer.find('img').each(function () { if ($(this).data('original_width') == undefined || options.forcedimagewidth > 0) { $(this).data('original_width', $(this).width()); } if ($(this).data('original_height') == undefined || options.forcedimageheight > 0) { $(this).data('original_height', $(this).height()); } }); } /** * users can pass in a specific width and height that should be applied to every image. * while this option can be used in conjunction with the image preloader, the intended * use case is for when the preloader is turned off and the images don't have defined * dimensions in css. the carousel needs dimensions one way or another to work properly. */ function forceimagedimensionsifenabled() { if (options.forcedimagewidth && options.forcedimageheight) { data.itemscontainer.find('img').each(function () { $(this).width(options.forcedimagewidth); $(this).height(options.forcedimageheight); }); } } /** * for each "visible" item slot (# of flanking items plus the middle), * we pre-calculate all of the properties that the item should possess while * occupying that slot. this saves us some time during the actual animation. */ function precalculatepositionproperties() { // the 0 index is the center item in the carousel var $firstitem = data.itemscontainer.find('img:first'); data.calculations[0] = { distance: 0, offset: 0, opacity: 1 } // then, for each number of flanking items (plus one more, see below), we // perform the calcations based on our user options var horizonoffset = options.horizonoffset; var separation = options.separation; for (var i = 1; i <= options.flankingitems + 2; i++) { if (i > 1) { horizonoffset *= options.horizonoffsetmultiplier; separation *= options.separationmultiplier; } data.calculations[i] = { distance: data.calculations[i-1].distance + separation, offset: data.calculations[i-1].offset + horizonoffset, opacity: data.calculations[i-1].opacity * options.opacitymultiplier } } // we performed 1 extra set of calculations above so that the items that // are moving out of sight (based on # of flanking items) gracefully animate there // however, we need them to animate to hidden, so we set the opacity to 0 for // that last item if (options.edgefadeenabled) { data.calculations[options.flankingitems+1].opacity = 0; } else { data.calculations[options.flankingitems+1] = { distance: 0, offset: 0, opacity: 0 } } } /** * here we prep the carousel and its items, like setting default css * attributes. all items start in the middle position by default * and will "fan out" from there during the first animation */ function setupcarousel() { // fill in a data array with jquery objects of all the images data.items = data.itemscontainer.find('img'); for (var i = 0; i < data.totalitems; i++) { data.items[i] = $(data.items[i]); } // may need to set the horizon if it was set to auto if (options.horizon === 0) { if (options.orientation === 'horizontal') { options.horizon = data.containerheight / 2; } else { options.horizon = data.containerwidth / 2; } } // default all the items to the center position data.itemscontainer .css('position','relative') .find('img') .each(function () { // figure out where the top and left positions for center should be var centerposleft, centerpostop; if (options.orientation === 'horizontal') { centerposleft = (data.containerwidth / 2) - ($(this).data('original_width') / 2); centerpostop = options.horizon - ($(this).data('original_height') / 2); } else { centerposleft = options.horizon - ($(this).data('original_width') / 2); centerpostop = (data.containerheight / 2) - ($(this).data('original_height') / 2); } $(this) // apply positioning and layering to the images .css({ 'left': centerposleft, 'top': centerpostop, 'visibility': 'visible', 'position': 'absolute', 'z-index': 0, 'opacity': 0 }) // give each image a data object so it remembers specific data about // it's original form .data({ top: centerpostop, left: centerposleft, oldposition: 0, currentposition: 0, depth: 0, opacity: 0 }) // the image has been setup... now we can show it .show(); }); } /** * all the items to the left and right of the center item need to be * animated to their starting positions. this function will * figure out what items go where and will animate them there */ function setupstarterrotation() { options.startingitem = (options.startingitem === 0) ? math.round(data.totalitems / 2) : options.startingitem; data.rightitemscount = math.ceil((data.totalitems-1) / 2); data.leftitemscount = math.floor((data.totalitems-1) / 2); // we are in effect rotating the carousel, so we need to set that data.carouselrotationsleft = 1; // center item moveitem(data.items[options.startingitem-1], 0); data.items[options.startingitem-1].css('opacity', 1); // all the items to the right of center var itemindex = options.startingitem - 1; for (var pos = 1; pos <= data.rightitemscount; pos++) { (itemindex < data.totalitems - 1) ? itemindex += 1 : itemindex = 0; data.items[itemindex].css('opacity', 1); moveitem(data.items[itemindex], pos); } // all items to left of center var itemindex = options.startingitem - 1; for (var pos = -1; pos >= data.leftitemscount*-1; pos--) { (itemindex > 0) ? itemindex -= 1 : itemindex = data.totalitems - 1; data.items[itemindex].css('opacity', 1); moveitem(data.items[itemindex], pos); } } /** * given the item and position, this function will calculate the new data * for the item. one the calculations are done, it will store that data in * the items data object */ function performcalculations($item, newposition) { var newdistancefromcenter = math.abs(newposition); // distance to the center if (newdistancefromcenter < options.flankingitems + 1) { var calculations = data.calculations[newdistancefromcenter]; } else { var calculations = data.calculations[options.flankingitems + 1]; } var distancefactor = math.pow(options.sizemultiplier, newdistancefromcenter) var newwidth = distancefactor * $item.data('original_width'); var newheight = distancefactor * $item.data('original_height'); var widthdifference = math.abs($item.width() - newwidth); var heightdifference = math.abs($item.height() - newheight); var newoffset = calculations.offset var newdistance = calculations.distance; if (newposition < 0) { newdistance *= -1; } if (options.orientation == 'horizontal') { var center = data.containerwidth / 2; var newleft = center + newdistance - (newwidth / 2); var newtop = options.horizon - newoffset - (newheight / 2); } else { var center = data.containerheight / 2; var newleft = options.horizon - newoffset - (newwidth / 2); var newtop = center + newdistance - (newheight / 2); } var newopacity; if (newposition === 0) { newopacity = 1; } else { newopacity = calculations.opacity; } // depth will be reverse distance from center var newdepth = options.flankingitems + 2 - newdistancefromcenter; $item.data('width',newwidth); $item.data('height',newheight); $item.data('top',newtop); $item.data('left',newleft); $item.data('oldposition',$item.data('currentposition')); $item.data('depth',newdepth); $item.data('opacity',newopacity); } function moveitem($item, newposition) { // only want to physically move the item if it is within the boundaries // or in the first position just outside either boundary if (math.abs(newposition) <= options.flankingitems + 1) { performcalculations($item, newposition); data.itemsanimating++; $item .css('z-index',$item.data().depth) // animate the items to their new position values .animate({ left: $item.data().left, width: $item.data().width, height: $item.data().height, top: $item.data().top, opacity: $item.data().opacity }, data.currentspeed, options.animationeasing, function () { // animation for the item has completed, call method itemanimationcomplete($item, newposition); }); } else { $item.data('currentposition', newposition) // move the item to the 'hidden' position if hasn't been moved yet // this is for the intitial setup if ($item.data('oldposition') === 0) { $item.css({ 'left': $item.data().left, 'width': $item.data().width, 'height': $item.data().height, 'top': $item.data().top, 'opacity': $item.data().opacity, 'z-index': $item.data().depth }); } } } /** * this function is called once an item has finished animating to its * given position. several different statements are executed here, such as * dealing with the animation queue */ function itemanimationcomplete($item, newposition) { data.itemsanimating--; $item.data('currentposition', newposition); // keep track of what items came and left the center position, // so we can fire callbacks when all the rotations are completed if (newposition === 0) { data.currentcenteritem = $item; } // all items have finished their rotation, lets clean up if (data.itemsanimating === 0) { data.carouselrotationsleft -= 1; data.currentlymoving = false; // if there are still rotations left in the queue, rotate the carousel again // we pass in zero because we don't want to add any additional rotations if (data.carouselrotationsleft > 0) { rotatecarousel(0); // otherwise there are no more rotations and... } else { // reset the speed of the carousel to original data.currentspeed = options.speed; data.currentcenteritem.addclass(options.activeclassname); if (data.performingsetup === false) { options.movedtocenter(data.currentcenteritem); options.movedfromcenter(data.previouscenteritem); } data.performingsetup = false; // reset & initate the autoplay autoplay(); } } } /** * function called to rotate the carousel the given number of rotations * in the given direciton. will check to make sure the carousel should * be able to move, and then adjust speed and move items */ function rotatecarousel(rotations) { // check to see that a rotation is allowed if (data.currentlymoving === false) { // remove active class from the center item while we rotate data.currentcenteritem.removeclass(options.activeclassname); data.currentlymoving = true; data.itemsanimating = 0; data.carouselrotationsleft += rotations; if (options.quickerforfurther === true) { // figure out how fast the carousel should rotate if (rotations > 1) { data.currentspeed = options.speed / rotations; } // assure the speed is above the minimum to avoid weird results data.currentspeed = (data.currentspeed < 100) ? 100 : data.currentspeed; } // iterate thru each item and move it for (var i = 0; i < data.totalitems; i++) { var $item = $(data.items[i]); var currentposition = $item.data('currentposition'); var newposition; if (data.currentdirection == 'forward') { newposition = currentposition - 1; } else { newposition = currentposition + 1; } // we keep both sides as even as possible to allow circular rotation to work. // we will "wrap" the item arround to the other side by negating its current position var flankingallowance = (newposition > 0) ? data.rightitemscount : data.leftitemscount; if (math.abs(newposition) > flankingallowance) { newposition = currentposition * -1; // if there's an uneven number of "flanking" items, we need to compenstate for that // when we have an item switch sides. the right side will always have 1 more in that case if (data.totalitems % 2 == 0) { newposition += 1; } } moveitem($item, newposition); } } } /** * the event handler when an image within the carousel is clicked * this function will rotate the carousel the correct number of rotations * to get the clicked item to the center, or will fire the custom event * the user passed in if the center item is clicked */ $(this).find('img').bind("click", function () { var itemposition = $(this).data().currentposition; if (options.imagenav == false) { return; } // don't allow hidden items to be clicked if (math.abs(itemposition) >= options.flankingitems + 1) { return; } // do nothing if the carousel is already moving if (data.currentlymoving) { return; } data.previouscenteritem = data.currentcenteritem; // remove autoplay autoplay(true); options.autoplay = 0; var rotations = math.abs(itemposition); if (itemposition == 0) { options.clickedcenter($(this)); } else { // fire the 'moving' callbacks options.movingfromcenter(data.currentcenteritem); options.movingtocenter($(this)); if (itemposition < 0) { data.currentdirection = 'backward'; rotatecarousel(rotations); } else if (itemposition > 0) { data.currentdirection = 'forward'; rotatecarousel(rotations); } } }); /** * the user may choose to wrap the images is link tags. if they do this, we need to * make sure that they aren't active for certain situations */ $(this).find('a').bind("click", function (event) { var iscenter = $(this).find('img').data('currentposition') == 0; // should we disable the links? if (options.linkhandling === 1 || // turn off all links (options.linkhandling === 2 && !iscenter)) // turn off all links except center { event.preventdefault(); return false; } }); function nextitemfromcenter() { var $next = data.currentcenteritem.next(); if ($next.length <= 0) { $next = data.currentcenteritem.parent().children().first(); } return $next; } function previtemfromcenter() { var $prev = data.currentcenteritem.prev(); if ($prev.length <= 0) { $prev = data.currentcenteritem.parent().children().last(); } return $prev; } /** * intiate a move of the carousel in either direction. takes care of firing * the 'moving' callbacks */ function moveonce(direction) { if (data.currentlymoving === false) { data.previouscenteritem = data.currentcenteritem; options.movingfromcenter(data.currentcenteritem); if (direction == 'backward') { options.movingtocenter(previtemfromcenter()); data.currentdirection = 'backward'; } else if (direction == 'forward') { options.movingtocenter(nextitemfromcenter()); data.currentdirection = 'forward'; } } rotatecarousel(1); } /** * navigation with arrow keys */ $(document).keydown(function(e) { if (options.keyboardnav) { // arrow left or up if ((e.which === 37 && options.orientation == 'horizontal') || (e.which === 38 && options.orientation == 'vertical')) { autoplay(true); options.autoplay = 0; moveonce('backward'); // arrow right or down } else if ((e.which === 39 && options.orientation == 'horizontal') || (e.which === 40 && options.orientation == 'vertical')) { autoplay(true); options.autoplay = 0; moveonce('forward'); } // should we override the normal functionality for the arrow keys? if (options.keyboardnavoverride && ( (options.orientation == 'horizontal' && (e.which === 37 || e.which === 39)) || (options.orientation == 'vertical' && (e.which === 38 || e.which === 40)) )) { e.preventdefault(); return false; } } }); /** * public api methods */ this.reload = function (newoptions) { if (typeof newoptions === "object") { var combinedefaultwith = newoptions; } else { var combinedefaultwith = {}; } options = $.extend({}, $.fn.waterwheelcarousel.defaults, newoptions); initializecarouseldata(); data.itemscontainer.find('img').hide(); forceimagedimensionsifenabled(); preload(function () { setoriginalitemdimensions(); precalculatepositionproperties(); setupcarousel(); setupstarterrotation(); }); } this.next = function() { autoplay(true); options.autoplay = 0; moveonce('forward'); } this.prev = function () { autoplay(true); options.autoplay = 0; moveonce('backward'); } this.reload(startingoptions); return this; }; $.fn.waterwheelcarousel.defaults = { // number tweeks to change apperance startingitem: 1, // item to place in the center of the carousel. set to 0 for auto separation: 175, // distance between items in carousel separationmultiplier: 0.6, // multipled by separation distance to increase/decrease distance for each additional item horizonoffset: 0, // offset each item from the "horizon" by this amount (causes arching) horizonoffsetmultiplier: 1, // multipled by horizon offset to increase/decrease offset for each additional item sizemultiplier: 0.7, // determines how drastically the size of each item changes opacitymultiplier: 0.8, // determines how drastically the opacity of each item changes horizon: 0, // how "far in" the horizontal/vertical horizon should be set from the container wall. 0 for auto flankingitems: 3, // the number of items visible on either side of the center // animation speed: 300, // speed in milliseconds it will take to rotate from one to the next animationeasing: 'linear', // the easing effect to use when animating quickerforfurther: true, // set to true to make animations faster when clicking an item that is far away from the center edgefadeenabled: false, // when true, items fade off into nothingness when reaching the edge. false to have them move behind the center image // misc linkhandling: 2, // 1 to disable all (used for facebox), 2 to disable all but center (to link images out) autoplay: 0, // indicate the speed in milliseconds to wait before autorotating. 0 to turn off. can be negative orientation: 'horizontal', // indicate if the carousel should be 'horizontal' or 'vertical' activeclassname: 'carousel-center', // the name of the class given to the current item in the center keyboardnav: false, // set to true to move the carousel with the arrow keys keyboardnavoverride: true, // set to true to override the normal functionality of the arrow keys (prevents scrolling) imagenav: true, // clicking a non-center image will rotate that image to the center // preloader preloadimages: true, // disable/enable the image preloader. forcedimagewidth: 0, // specify width of all images; otherwise the carousel tries to calculate it forcedimageheight: 0, // specify height of all images; otherwise the carousel tries to calculate it // callback functions movingtocenter: $.noop, // fired when an item is about to move to the center position movedtocenter: $.noop, // fired when an item has finished moving to the center clickedcenter: $.noop, // fired when the center item has been clicked movingfromcenter: $.noop, // fired when an item is about to leave the center position movedfromcenter: $.noop // fired when an item has finished moving from the center }; })(jquery);