jquery.cloud9carousel.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. /*
  2. * Cloud 9 Carousel 2.2.0
  3. *
  4. * Pseudo-3D carousel plugin for jQuery/Zepto focused on performance.
  5. *
  6. * Based on the original CloudCarousel by R. Cecco.
  7. *
  8. * See the demo and download the latest version:
  9. * http://specious.github.io/cloud9carousel/
  10. *
  11. * Copyright (c) 2017 by Ildar Sagdejev ( http://specious.github.io )
  12. * Copyright (c) 2011 by R. Cecco ( http://www.professorcloud.com )
  13. *
  14. * MIT License
  15. *
  16. * Please retain this copyright header in all versions of the software
  17. *
  18. * Requires:
  19. * - jQuery >= 1.3.0 or Zepto >= 1.1.1
  20. *
  21. * Optional (jQuery only):
  22. * - Reflection support via reflection.js plugin by Christophe Beyls
  23. * http://www.digitalia.be/software/reflectionjs-for-jquery
  24. * - Mousewheel support via mousewheel plugin
  25. * http://plugins.jquery.com/mousewheel/
  26. */
  27. ;(function($) {
  28. //
  29. // Detect CSS transform support
  30. //
  31. var transform = (function() {
  32. var vendors = ['webkit', 'moz', 'ms'];
  33. var style = document.createElement( "div" ).style;
  34. var trans = 'transform' in style ? 'transform' : undefined;
  35. for( var i = 0, count = vendors.length; i < count; i++ ) {
  36. var prop = vendors[i] + 'Transform';
  37. if( prop in style ) {
  38. trans = prop;
  39. break;
  40. }
  41. }
  42. return trans;
  43. })();
  44. var Item = function( element, options ) {
  45. element.item = this;
  46. this.element = element;
  47. if( element.tagName === 'IMG' ) {
  48. this.fullWidth = element.width;
  49. this.fullHeight = element.height;
  50. } else {
  51. element.style.display = "inline-block";
  52. this.fullWidth = element.offsetWidth;
  53. this.fullHeight = element.offsetHeight;
  54. }
  55. element.style.position = 'absolute';
  56. if( options.mirror && this.element.tagName === 'IMG' ) {
  57. // Wrap image in a div together with its generated reflection
  58. this.reflection = $(element).reflect( options.mirror ).next()[0];
  59. var $reflection = $(this.reflection);
  60. this.reflection.fullHeight = $reflection.height();
  61. $reflection.css( 'margin-top', options.mirror.gap + 'px' );
  62. $reflection.css( 'width', '100%' );
  63. element.style.width = "100%";
  64. // The item element now contains the image and reflection
  65. this.element = this.element.parentNode;
  66. this.element.item = this;
  67. this.element.alt = element.alt;
  68. this.element.title = element.title;
  69. }
  70. if( transform && options.transforms )
  71. this.element.style[transform + "Origin"] = "0 0";
  72. this.moveTo = function( x, y, scale ) {
  73. this.width = this.fullWidth * scale;
  74. this.height = this.fullHeight * scale;
  75. this.x = x;
  76. this.y = y;
  77. this.scale = scale;
  78. var style = this.element.style;
  79. style.zIndex = "" + (scale * 100) | 0;
  80. if( transform && options.transforms ) {
  81. style[transform] = "translate(" + x + "px, " + y + "px) scale(" + scale + ")";
  82. } else {
  83. // Manually resize the gap between the image and its reflection
  84. if( options.mirror && this.element.tagName === 'IMG' )
  85. this.reflection.style.marginTop = (options.mirror.gap * scale) + "px";
  86. style.width = this.width + "px";
  87. style.left = x + "px";
  88. style.top = y + "px";
  89. }
  90. }
  91. }
  92. var time = !window.performance || !window.performance.now ?
  93. function() { return +new Date() } :
  94. function() { return performance.now() };
  95. //
  96. // Detect requestAnimationFrame() support
  97. //
  98. // Support legacy browsers:
  99. // http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/
  100. //
  101. var cancelFrame = window.cancelAnimationFrame || window.cancelRequestAnimationFrame;
  102. var requestFrame = window.requestAnimationFrame;
  103. (function() {
  104. var vendors = ['webkit', 'moz', 'ms'];
  105. for( var i = 0, count = vendors.length; i < count && !cancelFrame; i++ ) {
  106. cancelFrame = window[vendors[i]+'CancelAnimationFrame'] || window[vendors[i]+'CancelRequestAnimationFrame'];
  107. requestFrame = requestFrame && window[vendors[i]+'RequestAnimationFrame'];
  108. }
  109. }());
  110. var Carousel = function( element, options ) {
  111. var self = this;
  112. var $container = $(element);
  113. this.items = [];
  114. this.xOrigin = (options.xOrigin === null) ? $container.width() * 0.5 : options.xOrigin;
  115. this.yOrigin = (options.yOrigin === null) ? $container.height() * 0.1 : options.yOrigin;
  116. this.xRadius = (options.xRadius === null) ? $container.width() / 2.3 : options.xRadius;
  117. this.yRadius = (options.yRadius === null) ? $container.height() / 6 : options.yRadius;
  118. this.farScale = options.farScale;
  119. this.rotation = this.destRotation = Math.PI/2; // start with the first item positioned in front
  120. this.speed = options.speed;
  121. this.smooth = options.smooth;
  122. this.fps = options.fps;
  123. this.timer = 0;
  124. this.autoPlayAmount = options.autoPlay;
  125. this.autoPlayDelay = options.autoPlayDelay;
  126. this.autoPlayTimer = 0;
  127. this.frontItemClass = options.frontItemClass;
  128. this.onLoaded = options.onLoaded;
  129. this.onRendered = options.onRendered;
  130. this.onAnimationFinished = options.onAnimationFinished;
  131. this.itemOptions = {
  132. transforms: options.transforms
  133. }
  134. if( options.mirror ) {
  135. this.itemOptions.mirror = $.extend( { gap: 2 }, options.mirror );
  136. }
  137. $container.css( { position: 'relative', overflow: 'hidden' } );
  138. // Rotation:
  139. // * 0 : right
  140. // * Pi/2 : front
  141. // * Pi : left
  142. // * 3 Pi/2 : back
  143. this.renderItem = function( itemIndex, rotation ) {
  144. var item = this.items[itemIndex];
  145. var sin = Math.sin(rotation);
  146. var farScale = this.farScale;
  147. var scale = farScale + ((1-farScale) * (sin+1) * 0.5);
  148. item.moveTo(
  149. this.xOrigin + (scale * ((Math.cos(rotation) * this.xRadius) - (item.fullWidth * 0.5))),
  150. this.yOrigin + (scale * sin * this.yRadius),
  151. scale
  152. );
  153. return item;
  154. }
  155. this.render = function() {
  156. var count = this.items.length;
  157. var spacing = 2 * Math.PI / count;
  158. var radians = this.rotation;
  159. var nearest = this.nearestIndex();
  160. for( var i = 0; i < count; i++ ) {
  161. var item = this.renderItem( i, radians );
  162. if( i === nearest )
  163. $(item.element).addClass( this.frontItemClass );
  164. else
  165. $(item.element).removeClass( this.frontItemClass );
  166. radians += spacing;
  167. }
  168. if( typeof this.onRendered === 'function' )
  169. this.onRendered( this );
  170. }
  171. this.playFrame = function() {
  172. var rem = self.destRotation - self.rotation;
  173. var now = time();
  174. var dt = (now - self.lastTime) * 0.002;
  175. self.lastTime = now;
  176. if( Math.abs(rem) < 0.003 ) {
  177. self.rotation = self.destRotation;
  178. self.pause();
  179. if( typeof self.onAnimationFinished === 'function' )
  180. self.onAnimationFinished();
  181. } else {
  182. // Asymptotically approach the destination
  183. self.rotation = self.destRotation - rem / (1 + (self.speed * dt));
  184. self.scheduleNextFrame();
  185. }
  186. self.render();
  187. }
  188. this.scheduleNextFrame = function() {
  189. this.lastTime = time();
  190. this.timer = this.smooth && cancelFrame ?
  191. requestFrame( self.playFrame ) :
  192. setTimeout( self.playFrame, 1000 / this.fps );
  193. }
  194. this.itemsRotated = function() {
  195. return this.items.length * ((Math.PI/2) - this.rotation) / (2*Math.PI);
  196. }
  197. this.floatIndex = function() {
  198. var count = this.items.length;
  199. var floatIndex = this.itemsRotated() % count;
  200. // Make sure float-index is positive
  201. return (floatIndex < 0) ? floatIndex + count : floatIndex;
  202. }
  203. this.nearestIndex = function() {
  204. return Math.round( this.floatIndex() ) % this.items.length;
  205. }
  206. this.nearestItem = function() {
  207. return this.items[this.nearestIndex()];
  208. }
  209. this.play = function() {
  210. if( this.timer === 0 )
  211. this.scheduleNextFrame();
  212. }
  213. this.pause = function() {
  214. this.smooth && cancelFrame ? cancelFrame( this.timer ) : clearTimeout( this.timer );
  215. this.timer = 0;
  216. }
  217. //
  218. // Spin the carousel by (+-) count items
  219. //
  220. this.go = function( count ) {
  221. this.destRotation += (2 * Math.PI / this.items.length) * count;
  222. this.play();
  223. }
  224. this.goTo = function( index ) {
  225. var count = this.items.length;
  226. // Find the shortest way to rotate item to front
  227. var diff = index - (this.floatIndex() % count);
  228. if( 2 * Math.abs(diff) > count )
  229. diff -= (diff > 0) ? count : -count;
  230. // Halt any rotation already in progress
  231. this.destRotation = this.rotation;
  232. // Spin the opposite way to bring item to front
  233. this.go( -diff );
  234. // Return rotational distance (in items) to the target
  235. return diff;
  236. }
  237. this.deactivate = function() {
  238. this.pause();
  239. clearInterval( this.autoPlayTimer );
  240. if( options.buttonLeft ) options.buttonLeft.unbind( 'click' );
  241. if( options.buttonRight ) options.buttonRight.unbind( 'click' );
  242. $container.unbind( '.cloud9' );
  243. }
  244. this.autoPlay = function() {
  245. this.autoPlayTimer = setInterval(
  246. function() { self.go( self.autoPlayAmount ) },
  247. this.autoPlayDelay
  248. );
  249. }
  250. this.enableAutoPlay = function() {
  251. // Stop auto-play on mouse over
  252. $container.bind( 'mouseover.cloud9', function() {
  253. clearInterval( self.autoPlayTimer );
  254. } );
  255. // Resume auto-play when mouse leaves the container
  256. $container.bind( 'mouseout.cloud9', function() {
  257. self.autoPlay();
  258. } );
  259. this.autoPlay();
  260. }
  261. this.bindControls = function() {
  262. if( options.buttonLeft ) {
  263. options.buttonLeft.bind( 'click', function() {
  264. self.go( -1 );
  265. return false;
  266. } );
  267. }
  268. if( options.buttonRight ) {
  269. options.buttonRight.bind( 'click', function() {
  270. self.go( 1 );
  271. return false;
  272. } );
  273. }
  274. if( options.mouseWheel ) {
  275. $container.bind( 'mousewheel.cloud9', function( event, delta ) {
  276. self.go( (delta > 0) ? 1 : -1 );
  277. return false;
  278. } );
  279. }
  280. if( options.bringToFront ) {
  281. $container.bind( 'click.cloud9', function( event ) {
  282. var hits = $(event.target).closest( '.' + options.itemClass );
  283. if( hits.length !== 0 ) {
  284. var diff = self.goTo( self.items.indexOf( hits[0].item ) );
  285. // Suppress default browser action if the item isn't roughly in front
  286. if( Math.abs(diff) > 0.5 )
  287. event.preventDefault();
  288. }
  289. } );
  290. }
  291. }
  292. var items = $container.find( '.' + options.itemClass );
  293. this.finishInit = function() {
  294. //
  295. // Wait until all images have completely loaded
  296. //
  297. for( var i = 0; i < items.length; i++ ) {
  298. var item = items[i];
  299. if( (item.tagName === 'IMG') &&
  300. ((item.width === undefined) || ((item.complete !== undefined) && !item.complete)) )
  301. return;
  302. }
  303. clearInterval( this.initTimer );
  304. // Init items
  305. for( i = 0; i < items.length; i++ )
  306. this.items.push( new Item( items[i], this.itemOptions ) );
  307. // Disable click-dragging of items
  308. $container.bind( 'mousedown onselectstart', function() { return false } );
  309. if( this.autoPlayAmount !== 0 ) this.enableAutoPlay();
  310. this.bindControls();
  311. this.render();
  312. if( typeof this.onLoaded === 'function' )
  313. this.onLoaded( this );
  314. };
  315. this.initTimer = setInterval( function() { self.finishInit() }, 50 );
  316. }
  317. //
  318. // The jQuery plugin
  319. //
  320. $.fn.Cloud9Carousel = function( options ) {
  321. return this.each( function() {
  322. /* For full list of options see the README */
  323. options = $.extend( {
  324. xOrigin: null, // null: calculated automatically
  325. yOrigin: null,
  326. xRadius: null,
  327. yRadius: null,
  328. farScale: 0.5, // scale of the farthest item
  329. transforms: true, // enable CSS transforms
  330. smooth: true, // enable smooth animation via requestAnimationFrame()
  331. fps: 30, // fixed frames per second (if smooth animation is off)
  332. speed: 4, // positive number
  333. autoPlay: 0, // [ 0: off | number of items (integer recommended, positive is clockwise) ]
  334. autoPlayDelay: 4000,
  335. bringToFront: false,
  336. itemClass: 'cloud9-item',
  337. frontItemClass: null,
  338. handle: 'carousel'
  339. }, options );
  340. $(this).data( options.handle, new Carousel( this, options ) );
  341. } );
  342. }
  343. })( window.jQuery || window.Zepto );