1/*! 2 * Waterwheel Carousel 3 * Version 2.3.0 4 * http://www.bkosborne.com 5 * 6 * Copyright 2011-2013 Brian Osborne 7 * Dual licensed under GPLv3 or MIT 8 * Copies of the licenses have been distributed 9 * with this plugin. 10 * 11 * Plugin written by Brian Osborne 12 * for use with the jQuery JavaScript Framework 13 * http://www.jquery.com 14 */ 15; 16(function ($) { 17 'use strict'; 18 19 $.fn.waterwheelCarousel = function (startingOptions) { 20 21 // Adds support for intializing multiple carousels from the same selector group 22 if (this.length > 1) { 23 this.each(function () { 24 $(this).waterwheelCarousel(startingOptions); 25 }); 26 return this; // allow chaining 27 } 28 29 var carousel = this; 30 var options = {}; 31 var data = {}; 32 33 function initializeCarouselData() { 34 data = { 35 itemsContainer: $(carousel), 36 totalItems: $(carousel).children().length, 37 containerWidth: $(carousel).width(), 38 containerHeight: $(carousel).height(), 39 currentCenterItem: null, 40 previousCenterItem: null, 41 items: [], 42 calculations: [], 43 carouselRotationsLeft: 0, 44 currentlyMoving: false, 45 itemsAnimating: 0, 46 currentSpeed: options.speed, 47 intervalTimer: null, 48 currentDirection: 'forward', 49 leftItemsCount: 0, 50 rightItemsCount: 0, 51 performingSetup: true 52 }; 53 data.itemsContainer.children().removeClass(options.activeClassName); 54 } 55 56 /** 57 * This function will set the autoplay for the carousel to 58 * automatically rotate it given the time in the options 59 * Can clear the autoplay by passing in true 60 */ 61 function autoPlay(stop) { 62 // clear timer 63 clearTimeout(data.autoPlayTimer); 64 // as long as no stop command, and autoplay isn't zeroed... 65 if (!stop && options.autoPlay !== 0) { 66 // set timer... 67 data.autoPlayTimer = setTimeout(function () { 68 // to move the carousl in either direction... 69 if (options.autoPlay > 0) { 70 moveOnce('forward'); 71 } else { 72 moveOnce('backward'); 73 } 74 }, Math.abs(options.autoPlay)); 75 } 76 } 77 78 /** 79 * This function will preload all the images in the carousel before 80 * calling the passed in callback function. This is only used so we can 81 * properly determine the width and height of the items. This is not needed 82 * if a user instead manually specifies that information. 83 */ 84 function preload(callback) { 85 86 var $imageElements = data.itemsContainer.find('img'), 87 totalImages = $imageElements.length, 88 loadedImages = 0; 89 90 if (options.preloadImages === false || $imageElements.length === 0) { 91 callback(); 92 return; 93 } 94 95 $imageElements.each(function () { 96 $(this).bind('load', function () { 97 // Add to number of images loaded and see if they are all done yet 98 loadedImages += 1; 99 if (loadedImages === totalImages) { 100 // All done, perform callback 101 callback(); 102 return; 103 } 104 }); 105 // May need to manually reset the src to get the load event to fire 106 // http://stackoverflow.com/questions/7137737/ie9-problems-with-jquery-load-event-not-firing 107 $(this).attr('src', $(this).attr('src')); 108 109 // If browser has cached the images, it may not call trigger a load. Detect this and do it ourselves 110 if (this.complete) { 111 $(this).trigger('load'); 112 } 113 }); 114 } 115 116 /** 117 * Makes a record of the original width and height of all the items in the carousel. 118 * If we re-intialize the carousel, these values can be used to re-establish their 119 * original dimensions. 120 */ 121 function setOriginalItemDimensions() { 122 data.itemsContainer.children().each(function () { 123 if ($(this).data('original_width') == undefined || options.forcedImageWidth > 0) { 124 $(this).data('original_width', $(this).width()); 125 } 126 if ($(this).data('original_height') == undefined || options.forcedImageHeight > 0) { 127 $(this).data('original_height', $(this).height()); 128 } 129 }); 130 } 131 132 /** 133 * Users can pass in a specific width and height that should be applied to every image. 134 * While this option can be used in conjunction with the image preloader, the intended 135 * use case is for when the preloader is turned off and the images don't have defined 136 * dimensions in CSS. The carousel needs dimensions one way or another to work properly. 137 */ 138 function forceImageDimensionsIfEnabled() { 139 if (options.forcedImageWidth && options.forcedImageHeight) { 140 data.itemsContainer.children().each(function () { 141 $(this).width(options.forcedImageWidth);
142 $(this).height(options.forcedImageHeight); 143 }); 144 } 145 } 146 147 /** 148 * For each "visible" item slot (# of flanking items plus the middle), 149 * we pre-calculate all of the properties that the item should possess while 150 * occupying that slot. This saves us some time during the actual animation. 151 */ 152 function preCalculatePositionProperties() { 153 // The 0 index is the center item in the carousel 154 var $firstItem = data.itemsContainer.children().eq(0); 155 156 data.calculations[0] = { 157 distance: 0, 158 offset: 0, 159 opacity: 1 160 } 161 162 // Then, for each number of flanking items (plus one more, see below), we 163 // perform the calcations based on our user options 164 var horizonOffset = options.horizonOffset; 165 var separation = options.separation; 166 for (var i = 1; i <= options.flankingItems + 2; i++) { 167 if (i > 1) { 168 horizonOffset *= options.horizonOffsetMultiplier; 169 separation *= options.separationMultiplier; 170 } 171 data.calculations[i] = { 172 distance: data.calculations[i - 1].distance + separation, 173 offset: data.calculations[i - 1].offset + horizonOffset, 174 opacity: data.calculations[i - 1].opacity * options.opacityMultiplier 175 } 176 } 177 // We performed 1 extra set of calculations above so that the items that 178 // are moving out of sight (based on # of flanking items) gracefully animate there 179 // However, we need them to animate to hidden, so we set the opacity to 0 for 180 // that last item 181 if (options.edgeFadeEnabled) { 182 data.calculations[options.flankingItems + 1].opacity = 0; 183 } else { 184 data.calculations[options.flankingItems + 1] = { 185 distance: 0, 186 offset: 0, 187 opacity: 0 188 } 189 } 190 } 191 192 /** 193 * Here we prep the carousel and its items, like setting default CSS 194 * attributes. All items start in the middle position by default 195 * and will "fan out" from there during the first animation 196 */ 197 function setupCarousel() { 198 // Fill in a data array with jQuery objects of all the images 199 data.items = data.itemsContainer.children(); 200 for (var i = 0; i < data.totalItems; i++) { 201 data.items[i] = $(data.items[i]); 202 } 203 204 // May need to set the horizon if it was set to auto 205 if (options.horizon === 0) { 206 if (options.orientation === 'horizontal') { 207 options.horizon = data.containerHeight / 2; 208 } else { 209 options.horizon = data.containerWidth / 2; 210 } 211 } 212 213 // Default all the items to the center position 214 data.itemsContainer 215 .css('position', 'relative') 216 .children() 217 .each(function () { 218 // Figure out where the top and left positions for center should be 219 var centerPosLeft, centerPosTop; 220 if (options.orientation === 'horizontal') { 221 centerPosLeft = (data.containerWidth / 2) - ($(this).data('original_width') / 2); 222 centerPosTop = options.horizon - ($(this).data('original_height') / 2); 223 } else { 224 centerPosLeft = options.horizon - ($(this).data('original_width') / 2); 225 centerPosTop = (data.containerHeight / 2) - ($(this).data('original_height') / 2); 226 } 227 $(this) 228 // Apply positioning and layering to the images 229 .css({ 230 'left': centerPosLeft, 231 'top': centerPosTop, 232 'visibility': 'visible', 233 'position': 'absolute', 234 'z-index': 0, 235 'opacity': 0 236 }) 237 // Give each image a data object so it remembers specific data about 238 // it's original form 239 .data({ 240 top: centerPosTop, 241 left: centerPosLeft, 242 oldPosition: 0, 243 currentPosition: 0, 244 depth: 0, 245 opacity: 0 246 }) 247 // The image has been setup... Now we can show it 248 .show(); 249 }); 250 } 251 252 /** 253 * All the items to the left and right of the center item need to be 254 * animated to their starting positions. This function will 255 * figure out what items go where and will animate them there 256 */ 257 function setupStarterRotation() { 258 options.startingItem = (options.startingItem === 0) ? Math.round(data.totalItems / 2) : options.startingItem; 259
260 data.rightItemsCount = Math.ceil((data.totalItems - 1) / 2); 261 data.leftItemsCount = Math.floor((data.totalItems - 1) / 2); 262 263 // We are in effect rotating the carousel, so we need to set that 264 data.carouselRotationsLeft = 1; 265 266 // Center item 267 moveItem(data.items[options.startingItem - 1], 0); 268 data.items[options.startingItem - 1].css('opacity', 1); 269 270 // All the items to the right of center 271 var itemIndex = options.startingItem - 1; 272 for (var pos = 1; pos <= data.rightItemsCount; pos++) { 273 (itemIndex < data.totalItems - 1) ? itemIndex += 1 : itemIndex = 0; 274 275 data.items[itemIndex].css('opacity', 1); 276 moveItem(data.items[itemIndex], pos); 277 } 278 279 // All items to left of center 280 var itemIndex = options.startingItem - 1; 281 for (var pos = -1; pos >= data.leftItemsCount * -1; pos--) { 282 (itemIndex > 0) ? itemIndex -= 1 : itemIndex = data.totalItems - 1; 283 284 data.items[itemIndex].css('opacity', 1); 285 moveItem(data.items[itemIndex], pos); 286 } 287 } 288 289 /** 290 * Given the item and position, this function will calculate the new data 291 * for the item. One the calculations are done, it will store that data in 292 * the items data object 293 */ 294 function performCalculations($item, newPosition) { 295 var newDistanceFromCenter = Math.abs(newPosition); 296 297 // Distance to the center 298 if (newDistanceFromCenter < options.flankingItems + 1) { 299 var calculations = data.calculations[newDistanceFromCenter]; 300 } else { 301 var calculations = data.calculations[options.flankingItems + 1]; 302 } 303 304 var distanceFactor = Math.pow(options.sizeMultiplier, newDistanceFromCenter) 305 var newWidth = distanceFactor * $item.data('original_width'); 306 var newHeight = distanceFactor * $item.data('original_height'); 307 var widthDifference = Math.abs($item.width() - newWidth); 308 var heightDifference = Math.abs($item.height() - newHeight); 309 310 var newOffset = calculations.offset 311 var newDistance = calculations.distance; 312 if (newPosition < 0) { 313 newDistance *= -1; 314 } 315 316 if (options.orientation == 'horizontal') { 317 var center = data.containerWidth / 2; 318 var newLeft = center + newDistance - (newWidth / 2); 319 var newTop = options.horizon - newOffset - (newHeight / 2); 320 } else { 321 var center = data.containerHeight / 2; 322 var newLeft = options.horizon - newOffset - (newWidth / 2); 323 var newTop = center + newDistance - (newHeight / 2); 324 } 325 326 newTop -= heightDifference / 2; 327 newLeft -= widthDifference / 2; 328 329 var newOpacity; 330 if (newPosition === 0) { 331 newOpacity = 1; 332 } else { 333 newOpacity = calculations.opacity; 334 } 335 336 // Depth will be reverse distance from center 337 var newDepth = options.flankingItems + 2 - newDistanceFromCenter; 338 339 $item.data('width', newWidth); 340 $item.data('height', newHeight); 341 $item.data('top', newTop); 342 $item.data('left', newLeft); 343 $item.data('oldPosition', $item.data('currentPosition')); 344 $item.data('depth', newDepth); 345 $item.data('opacity', newOpacity); 346 $item.data('distanceFactor', distanceFactor); 347 } 348 349 function moveItem($item, newPosition) { 350 // Only want to physically move the item if it is within the boundaries 351 // or in the first position just outside either boundary 352 if (Math.abs(newPosition) <= options.flankingItems + 1) { 353 performCalculations($item, newPosition); 354 355 data.itemsAnimating++; 356 357 $item 358 .css('z-index', $item.data().depth) 359 // Animate the items to their new position values 360 .animate({ 361 'left': $item.data().left, 362 'top': $item.data().top, 363 'opacity': $item.data().opacity 364 }, data.currentSpeed, options.animationEasing, function () { 365 // Animation for the item has completed, call method 366 itemAnimationComplete($item, newPosition); 367 }); 368 369 TweenMax.to($item, data.currentSpeed / 1000, { 370 scaleX: $item.data('distanceFactor'), 371 scaleY: $item.data('distanceFactor'), 372 ease: options.animationEasing 373 }); 374 375 } else { 376 $item.data('currentPosition', newPosition) 377 // Move the item to the 'hidden' position if hasn't been moved yet 378 // This is for the intitial setup 379 if ($item.data('oldPosition') === 0) { 380 $item 381 .css({ 382 'left': $item.data().left, 383 'top': $item.data().top, 384 'opacity': $item.data().opacity, 385 'z-index': $item.data().depth 386 }); 387 388 TweenMax.to($item, 0, { 389 scaleX: $item.data('distanceFactor'), 390 scaleY: $item.data('distanceFactor') 391 }); 392 } 393 } 394 395 } 396 397 /** 398 * This function is called once an item has finished animating to its 399 * given position. Several different statements are executed here, such as 400 * dealing with the animation queue 401 */ 402 function itemAnimationComplete($item, newPosition) { 403 data.itemsAnimating--; 404 405 $item.data('currentPosition', newPosition); 406 407 // Keep track of what items came and left the center position, 408 // so we can fire callbacks when all the rotations are completed 409 if (newPosition === 0) { 410 data.currentCenterItem = $item; 411 } 412 413 // all items have finished their rotation, lets clean up 414 if (data.itemsAnimating === 0) { 415 data.carouselRotationsLeft -= 1; 416 data.currentlyMoving = false; 417 418 // If there are still rotations left in the queue, rotate the carousel again 419 // we pass in zero because we don't want to add any additional rotations 420 if (data.carouselRotationsLeft > 0) { 421 rotateCarousel(0); 422 // Otherwise there are no more rotations and... 423 } else { 424 // Reset the speed of the carousel to original 425 data.currentSpeed = options.speed; 426 427 data.currentCenterItem.addClass(options.activeClassName); 428 429 if (data.performingSetup === false) { 430 options.movedToCenter(data.currentCenterItem); 431 options.movedFromCenter(data.previousCenterItem); 432 } 433 434 data.performingSetup = false; 435 // reset & initate the autoPlay 436 autoPlay(); 437 } 438 } 439 } 440 441 /** 442 * Function called to rotate the carousel the given number of rotations 443 * in the given direciton. Will check to make sure the carousel should 444 * be able to move, and then adjust speed and move items 445 */ 446 function rotateCarousel(rotations) { 447 // Check to see that a rotation is allowed 448 if (data.currentlyMoving === false) { 449 450 // Remove active class from the center item while we rotate 451 data.currentCenterItem.removeClass(options.activeClassName); 452 453 data.currentlyMoving = true; 454 data.itemsAnimating = 0; 455 data.carouselRotationsLeft += rotations; 456 457 if (options.quickerForFurther === true) { 458 // Figure out how fast the carousel should rotate 459 if (rotations > 1) { 460 data.currentSpeed = options.speed / rotations; 461 } 462 // Assure the speed is above the minimum to avoid weird results 463 data.currentSpeed = (data.currentSpeed < 100) ? 100 : data.currentSpeed; 464 } 465 466 // Iterate thru each item and move it 467 for (var i = 0; i < data.totalItems; i++) { 468 var $item = $(data.items[i]); 469 var currentPosition = $item.data('currentPosition'); 470 471 var newPosition; 472 if (data.currentDirection == 'forward') { 473 newPosition = currentPosition - 1; 474 } else { 475 newPosition = currentPosition + 1; 476 } 477 // We keep both sides as even as possible to allow circular rotation to work. 478 // We will "wrap" the item arround to the other side by negating its current position 479 var flankingAllowance = (newPosition > 0) ? data.rightItemsCount : data.leftItemsCount; 480 if (Math.abs(newPosition) > flankingAllowance) { 481 newPosition = currentPosition * -1; 482 // If there's an uneven number of "flanking" items, we need to compenstate for that 483 // when we have an item switch sides. The right side will always have 1 more in that case 484 if (data.totalItems % 2 == 0) { 485 newPosition += 1;
486 } 487 } 488 489 moveItem($item, newPosition); 490 } 491 } 492 } 493 494 /** 495 * The event handler when an image within the carousel is clicked 496 * This function will rotate the carousel the correct number of rotations 497 * to get the clicked item to the center, or will fire the custom event 498 * the user passed in if the center item is clicked 499 */ 500 $(this).children().bind("click", function () { 501 var itemPosition = $(this).data().currentPosition; 502 503 if (options.imageNav == false) { 504 return; 505 } 506 // Don't allow hidden items to be clicked 507 if (Math.abs(itemPosition) >= options.flankingItems + 1) { 508 return; 509 } 510 // Do nothing if the carousel is already moving 511 if (data.currentlyMoving) { 512 return; 513 } 514 515 data.previousCenterItem = data.currentCenterItem; 516 517 // Remove autoplay 518 autoPlay(true); 519 options.autoPlay = 0; 520 521 var rotations = Math.abs(itemPosition); 522 if (itemPosition == 0) { 523 options.clickedCenter($(this)); 524 } else { 525 // Fire the 'moving' callbacks 526 options.movingFromCenter(data.currentCenterItem); 527 options.movingToCenter($(this)); 528 if (itemPosition < 0) { 529 data.currentDirection = 'backward'; 530 rotateCarousel(rotations); 531 } else if (itemPosition > 0) { 532 data.currentDirection = 'forward'; 533 rotateCarousel(rotations); 534 } 535 } 536 }); 537 538 function nextItemFromCenter() { 539 var $next = data.currentCenterItem.next(); 540 if ($next.length <= 0) { 541 $next = data.currentCenterItem.parent().children().first(); 542 } 543 return $next; 544 } 545 546 function prevItemFromCenter() { 547 var $prev = data.currentCenterItem.prev(); 548 if ($prev.length <= 0) { 549 $prev = data.currentCenterItem.parent().children().last(); 550 } 551 return $prev; 552 } 553 554 /** 555 * Intiate a move of the carousel in either direction. Takes care of firing 556 * the 'moving' callbacks 557 */ 558 function moveOnce(direction) { 559 if (data.currentlyMoving === false) { 560 data.previousCenterItem = data.currentCenterItem; 561 562 options.movingFromCenter(data.currentCenterItem); 563 if (direction == 'backward') { 564 options.movingToCenter(prevItemFromCenter()); 565 data.currentDirection = 'backward'; 566 } else if (direction == 'forward') { 567 options.movingToCenter(nextItemFromCenter()); 568 data.currentDirection = 'forward'; 569 } 570 } 571 572 rotateCarousel(1); 573 } 574 575 /** 576 * Navigation with arrow keys 577 */ 578 $(document).keydown(function (e) { 579 if (options.keyboardNav) { 580 // arrow left or up 581 if ((e.which === 37 && options.orientation == 'horizontal') || (e.which === 38 && options.orientation == 'vertical')) { 582 autoPlay(true); 583 options.autoPlay = 0; 584 moveOnce('backward'); 585 // arrow right or down 586 } else if ((e.which === 39 && options.orientation == 'horizontal') || (e.which === 40 && options.orientation == 'vertical')) { 587 autoPlay(true); 588 options.autoPlay = 0; 589 moveOnce('forward'); 590 } 591 // should we override the normal functionality for the arrow keys? 592 if (options.keyboardNavOverride && ( 593 (options.orientation == 'horizontal' && (e.which === 37 || e.which === 39)) || 594 (options.orientation == 'vertical' && (e.which === 38 || e.which === 40)) 595 )) { 596 e.preventDefault(); 597 return false; 598 } 599 } 600 }); 601 602 /** 603 * Public API methods 604 */ 605 this.reload = function (newOptions) { 606 if (typeof newOptions === "object") { 607 var combineDefaultWith = newOptions; 608 } else { 609 var combineDefaultWith = {}; 610 } 611 options = $.extend({}, $.fn.waterwheelCarousel.defaults, newOptions); 612 613 initializeCarouselData(); 614 data.itemsContainer.children().hide(); 615 forceImageDimensionsIfEnabled(); 616 617 preload(function () { 618 setOriginalItemDimensions(); 619 preCalculatePositionProperties(); 620 setupCarousel(); 621 setupStarterRotation(); 622 }); 623 } 624 625 this.next = function () { 626 autoPlay(true); 627 options.autoPlay = 0; 628 629 moveOnce('forward'); 630 } 631 this.prev = function () { 632 autoPlay(true); 633 options.autoPlay = 0; 634 635 moveOnce('backward'); 636 } 637 638 this.reload(startingOptions); 639 640 return this; 641 }; 642 643 $.fn.waterwheelCarousel.defaults = { 644 // number tweeks to change apperance 645 startingItem: 1, // item to place in the center of the carousel. Set to 0 for auto 646 separation: 175, // distance between items in carousel 647 separationMultiplier: 0.6, // multipled by separation distance to increase/decrease distance for each additional item 648 horizonOffset: 0, // offset each item from the "horizon" by this amount (causes arching) 649 horizonOffsetMultiplier: 1, // multipled by horizon offset to increase/decrease offset for each additional item 650 sizeMultiplier: 0.7, // determines how drastically the size of each item changes 651 opacityMultiplier: 0.8, // determines how drastically the opacity of each item changes 652 horizon: 0, // how "far in" the horizontal/vertical horizon should be set from the container wall. 0 for auto 653 flankingItems: 3, // the number of items visible on either side of the center 654 655 // animation 656 speed: 300, // speed in milliseconds it will take to rotate from one to the next 657 animationEasing: 'linear', // the easing effect to use when animating 658 quickerForFurther: true, // set to true to make animations faster when clicking an item that is far away from the center 659 edgeFadeEnabled: false, // when true, items fade off into nothingness when reaching the edge. false to have them move behind the center image 660 661 // misc 662 linkHandling: 2, // 1 to disable all (used for facebox), 2 to disable all but center (to link images out) 663 autoPlay: 0, // indicate the speed in milliseconds to wait before autorotating. 0 to turn off. Can be negative 664 orientation: 'horizontal', // indicate if the carousel should be 'horizontal' or 'vertical' 665 activeClassName: 'carousel-center', // the name of the class given to the current item in the center 666 keyboardNav: false, // set to true to move the carousel with the arrow keys 667 keyboardNavOverride: true, // set to true to override the normal functionality of the arrow keys (prevents scrolling) 668 imageNav: true, // clicking a non-center image will rotate that image to the center 669 670 // preloader 671 preloadImages: true, // disable/enable the image preloader. 672 forcedImageWidth: 0, // specify width of all images; otherwise the carousel tries to calculate it 673 forcedImageHeight: 0, // specify height of all images; otherwise the carousel tries to calculate it 674 675 // callback functions 676 movingToCenter: $.noop, // fired when an item is about to move to the center position 677 movedToCenter: $.noop, // fired when an item has finished moving to the center 678 clickedCenter: $.noop, // fired when the center item has been clicked 679 movingFromCenter: $.noop, // fired when an item is about to leave the center position 680 movedFromCenter: $.noop // fired when an item has finished moving from the center 681 }; 682 683})(jQuery);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.