jquery.roundabout2.js 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137
  1. /**
  2. * jQuery Roundabout - v2.1.1
  3. * http://fredhq.com/projects/roundabout
  4. *
  5. * Moves list-items of enabled ordered and unordered lists long
  6. * a chosen path. Includes the default "lazySusan" path, that
  7. * moves items long a spinning turntable.
  8. *
  9. * Terms of Use // jQuery Roundabout
  10. *
  11. * Open source under the BSD license
  12. *
  13. * Copyright (c) 2011, Fred LeBlanc
  14. * All rights reserved.
  15. *
  16. * Redistribution and use in source and binary forms, with or without
  17. * modification, are permitted provided that the following conditions are met:
  18. *
  19. * - Redistributions of source code must retain the above copyright
  20. * notice, this list of conditions and the following disclaimer.
  21. * - Redistributions in binary form must reproduce the above
  22. * copyright notice, this list of conditions and the following
  23. * disclaimer in the documentation and/or other materials provided
  24. * with the distribution.
  25. * - Neither the name of the author nor the names of its contributors
  26. * may be used to endorse or promote products derived from this
  27. * software without specific prior written permission.
  28. *
  29. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  30. * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  31. * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  32. * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
  33. * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  34. * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  35. * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  36. * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  37. * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  38. * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  39. * POSSIBILITY OF SUCH DAMAGE.
  40. */
  41. (function($) {
  42. "use strict";
  43. var defaults, internalData, methods;
  44. // add default shape
  45. $.extend({
  46. roundaboutShapes: {
  47. def: "lazySusan",
  48. lazySusan: function (r, a, t) {
  49. return {
  50. x: Math.sin(r + a),
  51. y: (Math.sin(r + 3*Math.PI/2 + a) / 8) * t,
  52. z: (Math.cos(r + a) + 1) / 2,
  53. scale: (Math.sin(r + Math.PI/2 + a) / 2) + 0.5
  54. };
  55. }
  56. }
  57. });
  58. defaults = {
  59. bearing: 0.0,
  60. tilt: 0.0,
  61. minZ: 100,
  62. maxZ: 280,
  63. minOpacity: 0.4,
  64. maxOpacity: 1.0,
  65. minScale: 0.4,
  66. maxScale: 1.0,
  67. duration: 600,
  68. btnNext: null,
  69. btnNextCallback: function() {},
  70. btnPrev: null,
  71. btnPrevCallback: function() {},
  72. btnToggleAutoplay: null,
  73. btnStartAutoplay: null,
  74. btnStopAutoplay: null,
  75. easing: "swing",
  76. clickToFocus: true,
  77. clickToFocusCallback: function() {},
  78. focusBearing: 0.0,
  79. shape: "lazySusan",
  80. debug: false,
  81. childSelector: "li",
  82. startingChild: null,
  83. reflect: false,
  84. floatComparisonThreshold: 0.001,
  85. autoplay: false,
  86. autoplayDuration: 1000,
  87. autoplayPauseOnHover: false,
  88. autoplayCallback: function() {},
  89. enableDrag: false,
  90. dropDuration: 600,
  91. dropEasing: "swing",
  92. dropAnimateTo: "nearest",
  93. dropCallback: function() {},
  94. dragAxis: "x",
  95. dragFactor: 4,
  96. triggerFocusEvents: true,
  97. triggerBlurEvents: true,
  98. responsive: false
  99. };
  100. internalData = {
  101. autoplayInterval: null,
  102. autoplayIsRunning: false,
  103. animating: false,
  104. childInFocus: -1,
  105. touchMoveStartPosition: null,
  106. stopAnimation: false,
  107. lastAnimationStep: false
  108. };
  109. methods = {
  110. // starters
  111. // -----------------------------------------------------------------------
  112. // init
  113. // starts up roundabout
  114. init: function(options, callback, relayout) {
  115. var settings,
  116. now = (new Date()).getTime();
  117. options = (typeof options === "object") ? options : {};
  118. callback = ($.isFunction(callback)) ? callback : function() {};
  119. callback = ($.isFunction(options)) ? options : callback;
  120. settings = $.extend({}, defaults, options, internalData);
  121. return this
  122. .each(function() {
  123. // make options
  124. var self = $(this),
  125. childCount = self.children(settings.childSelector).length,
  126. period = 360.0 / childCount,
  127. startingChild = (settings.startingChild && settings.startingChild > (childCount - 1)) ? (childCount - 1) : settings.startingChild,
  128. startBearing = (settings.startingChild === null) ? settings.bearing : 360 - (startingChild * period),
  129. holderCSSPosition = (self.css("position") !== "static") ? self.css("position") : "relative";
  130. self
  131. .css({ // starting styles
  132. padding: 0,
  133. position: holderCSSPosition
  134. })
  135. .addClass("roundabout-holder")
  136. .data( // starting options
  137. "roundabout",
  138. $.extend(
  139. {},
  140. settings,
  141. {
  142. startingChild: startingChild,
  143. bearing: startBearing,
  144. oppositeOfFocusBearing: methods.normalize.apply(null, [settings.focusBearing - 180]),
  145. dragBearing: startBearing,
  146. period: period
  147. }
  148. )
  149. );
  150. // bind based on settings if this init call was not a relayout
  151. if (!relayout) {
  152. // bind click-to-focus
  153. if (settings.clickToFocus) {
  154. self
  155. .children(settings.childSelector)
  156. .each(function(i) {
  157. $(this)
  158. .bind("click.roundabout", function() {
  159. var degrees = methods.getPlacement.apply(self, [i]);
  160. if (!methods.isInFocus.apply(self, [degrees])) {
  161. methods.stopAnimation.apply($(this));
  162. if (!self.data("roundabout").animating) {
  163. methods.animateBearingToFocus.apply(self, [degrees, self.data("roundabout").clickToFocusCallback]);
  164. }
  165. return false;
  166. }
  167. });
  168. });
  169. }
  170. // bind next buttons
  171. if (settings.btnNext) {
  172. $(settings.btnNext)
  173. .bind("click.roundabout", function() {
  174. if (!self.data("roundabout").animating) {
  175. methods.animateToNextChild.apply(self, [self.data("roundabout").btnNextCallback]);
  176. }
  177. return false;
  178. });
  179. }
  180. // bind previous buttons
  181. if (settings.btnPrev) {
  182. $(settings.btnPrev)
  183. .bind("click.roundabout", function() {
  184. methods.animateToPreviousChild.apply(self, [self.data("roundabout").btnPrevCallback]);
  185. return false;
  186. });
  187. }
  188. // bind toggle autoplay buttons
  189. if (settings.btnToggleAutoplay) {
  190. $(settings.btnToggleAutoplay)
  191. .bind("click.roundabout", function() {
  192. methods.toggleAutoplay.apply(self);
  193. return false;
  194. });
  195. }
  196. // bind start autoplay buttons
  197. if (settings.btnStartAutoplay) {
  198. $(settings.btnStartAutoplay)
  199. .bind("click.roundabout", function() {
  200. methods.startAutoplay.apply(self);
  201. return false;
  202. });
  203. }
  204. // bind stop autoplay buttons
  205. if (settings.btnStopAutoplay) {
  206. $(settings.btnStopAutoplay)
  207. .bind("click.roundabout", function() {
  208. methods.stopAutoplay.apply(self);
  209. return false;
  210. });
  211. }
  212. // autoplay pause on hover
  213. if (settings.autoplayPauseOnHover) {
  214. self
  215. .bind("mouseenter.roundabout.autoplay", function() {
  216. methods.stopAutoplay.apply(self, [true]);
  217. })
  218. .bind("mouseleave.roundabout.autoplay", function() {
  219. methods.startAutoplay.apply(self);
  220. });
  221. }
  222. // drag and drop
  223. if (settings.enableDrag) {
  224. // on screen
  225. if (!$.isFunction(self.drag)) {
  226. if (settings.debug) {
  227. alert("You do not have the drag plugin loaded.");
  228. }
  229. } else if (!$.isFunction(self.drop)) {
  230. if (settings.debug) {
  231. alert("You do not have the drop plugin loaded.");
  232. }
  233. } else {
  234. self
  235. .drag(function(e, properties) {
  236. var data = self.data("roundabout"),
  237. delta = (data.dragAxis.toLowerCase() === "x") ? "deltaX" : "deltaY";
  238. methods.stopAnimation.apply(self);
  239. methods.setBearing.apply(self, [data.dragBearing + properties[delta] / data.dragFactor]);
  240. })
  241. .drop(function(e) {
  242. var data = self.data("roundabout"),
  243. method = methods.getAnimateToMethod(data.dropAnimateTo);
  244. methods.allowAnimation.apply(self);
  245. methods[method].apply(self, [data.dropDuration, data.dropEasing, data.dropCallback]);
  246. data.dragBearing = data.period * methods.getNearestChild.apply(self);
  247. });
  248. }
  249. // on mobile
  250. self
  251. .each(function() {
  252. var element = $(this).get(0),
  253. data = $(this).data("roundabout"),
  254. page = (data.dragAxis.toLowerCase() === "x") ? "pageX" : "pageY",
  255. method = methods.getAnimateToMethod(data.dropAnimateTo);
  256. // some versions of IE don't like this
  257. if (element.addEventListener) {
  258. element.addEventListener("touchstart", function(e) {
  259. data.touchMoveStartPosition = e.touches[0][page];
  260. }, false);
  261. element.addEventListener("touchmove", function(e) {
  262. var delta = (e.touches[0][page] - data.touchMoveStartPosition) / data.dragFactor;
  263. e.preventDefault();
  264. methods.stopAnimation.apply($(this));
  265. methods.setBearing.apply($(this), [data.dragBearing + delta]);
  266. }, false);
  267. element.addEventListener("touchend", function(e) {
  268. e.preventDefault();
  269. methods.allowAnimation.apply($(this));
  270. method = methods.getAnimateToMethod(data.dropAnimateTo);
  271. methods[method].apply($(this), [data.dropDuration, data.dropEasing, data.dropCallback]);
  272. data.dragBearing = data.period * methods.getNearestChild.apply($(this));
  273. }, false);
  274. }
  275. });
  276. }
  277. // responsive
  278. if (settings.responsive) {
  279. $(window).resize(function() {
  280. methods.relayoutChildren.apply(self);
  281. });
  282. }
  283. }
  284. // start children
  285. methods.initChildren.apply(self, [callback, relayout]);
  286. });
  287. },
  288. // initChildren
  289. // applys settings to child elements, starts roundabout
  290. initChildren: function(callback, relayout) {
  291. var self = $(this),
  292. data = self.data("roundabout");
  293. callback = callback || function() {};
  294. self.children(data.childSelector).each(function(i) {
  295. var startWidth, startHeight, startFontSize,
  296. degrees = methods.getPlacement.apply(self, [i]);
  297. // on relayout, grab these values from current data
  298. if (relayout) {
  299. startWidth = $(this).data("roundabout").startWidth;
  300. startHeight = $(this).data("roundabout").startHeight;
  301. startFontSize = $(this).data("roundabout").startFontSize;
  302. }
  303. // apply classes and css first
  304. $(this)
  305. .addClass("roundabout-moveable-item")
  306. .css("position", "absolute");
  307. // now measure
  308. $(this)
  309. .data(
  310. "roundabout",
  311. {
  312. startWidth: startWidth || $(this).width(),
  313. startHeight: startHeight || $(this).height(),
  314. startFontSize: startFontSize || parseInt($(this).css("font-size"), 10),
  315. degrees: degrees,
  316. backDegrees: methods.normalize.apply(null, [degrees - 180]),
  317. childNumber: i,
  318. currentScale: 1,
  319. parent: self
  320. }
  321. );
  322. });
  323. methods.updateChildren.apply(self);
  324. // start autoplay if necessary
  325. if (data.autoplay) {
  326. methods.startAutoplay.apply(self);
  327. }
  328. self.trigger('ready');
  329. callback.apply(self);
  330. return self;
  331. },
  332. // positioning
  333. // -----------------------------------------------------------------------
  334. // updateChildren
  335. // move children elements into their proper locations
  336. updateChildren: function() {
  337. return this
  338. .each(function() {
  339. var self = $(this),
  340. data = self.data("roundabout"),
  341. inFocus = -1,
  342. info = {
  343. bearing: data.bearing,
  344. tilt: data.tilt,
  345. stage: {
  346. width: Math.floor($(this).width() * 0.9),
  347. height: Math.floor($(this).height() * 0.9)
  348. },
  349. animating: data.animating,
  350. inFocus: data.childInFocus,
  351. focusBearingRadian: methods.degToRad.apply(null, [data.focusBearing]),
  352. shape: $.roundaboutShapes[data.shape] || $.roundaboutShapes[$.roundaboutShapes.def]
  353. };
  354. // calculations
  355. info.midStage = {
  356. width: info.stage.width / 2,
  357. height: info.stage.height / 2
  358. };
  359. info.nudge = {
  360. width: info.midStage.width + (info.stage.width * 0.05),
  361. height: info.midStage.height + (info.stage.height * 0.05)
  362. };
  363. info.zValues = {
  364. min: data.minZ,
  365. max: data.maxZ,
  366. diff: data.maxZ - data.minZ
  367. };
  368. info.opacity = {
  369. min: data.minOpacity,
  370. max: data.maxOpacity,
  371. diff: data.maxOpacity - data.minOpacity
  372. };
  373. info.scale = {
  374. min: data.minScale,
  375. max: data.maxScale,
  376. diff: data.maxScale - data.minScale
  377. };
  378. // update child positions
  379. self.children(data.childSelector)
  380. .each(function(i) {
  381. if (methods.updateChild.apply(self, [$(this), info, i, function() { $(this).trigger('ready'); }]) && (!info.animating || data.lastAnimationStep)) {
  382. inFocus = i;
  383. $(this).addClass("roundabout-in-focus");
  384. } else {
  385. $(this).removeClass("roundabout-in-focus");
  386. }
  387. });
  388. if (inFocus !== info.inFocus) {
  389. // blur old child
  390. if (data.triggerBlurEvents) {
  391. self.children(data.childSelector)
  392. .eq(info.inFocus)
  393. .trigger("blur");
  394. }
  395. data.childInFocus = inFocus;
  396. if (data.triggerFocusEvents && inFocus !== -1) {
  397. // focus new child
  398. self.children(data.childSelector)
  399. .eq(inFocus)
  400. .trigger("focus");
  401. }
  402. }
  403. self.trigger("childrenUpdated");
  404. });
  405. },
  406. // updateChild
  407. // repositions a child element into its new position
  408. updateChild: function(childElement, info, childPos, callback) {
  409. var factors,
  410. self = this,
  411. child = $(childElement),
  412. data = child.data("roundabout"),
  413. out = [],
  414. rad = methods.degToRad.apply(null, [(360.0 - data.degrees) + info.bearing]);
  415. callback = callback || function() {};
  416. // adjust radians to be between 0 and Math.PI * 2
  417. rad = methods.normalizeRad.apply(null, [rad]);
  418. // get factors from shape
  419. factors = info.shape(rad, info.focusBearingRadian, info.tilt);
  420. // correct
  421. factors.scale = (factors.scale > 1) ? 1 : factors.scale;
  422. factors.adjustedScale = (info.scale.min + (info.scale.diff * factors.scale)).toFixed(4);
  423. factors.width = (factors.adjustedScale * data.startWidth).toFixed(4);
  424. factors.height = (factors.adjustedScale * data.startHeight).toFixed(4);
  425. // update item
  426. child
  427. .css({
  428. left: ((factors.x * info.midStage.width + info.nudge.width) - factors.width / 2.0).toFixed(0) + "px",
  429. top: ((factors.y * info.midStage.height + info.nudge.height) - factors.height / 2.0).toFixed(0) + "px",
  430. width: factors.width + "px",
  431. height: factors.height + "px",
  432. opacity: (info.opacity.min + (info.opacity.diff * factors.scale)).toFixed(2),
  433. zIndex: Math.round(info.zValues.min + (info.zValues.diff * factors.z)),
  434. fontSize: (factors.adjustedScale * data.startFontSize).toFixed(1) + "px"
  435. });
  436. data.currentScale = factors.adjustedScale;
  437. // for debugging purposes
  438. if (self.data("roundabout").debug) {
  439. out.push("<div style=\"font-weight: normal; font-size: 10px; padding: 2px; width: " + child.css("width") + "; background-color: #ffc;\">");
  440. out.push("<strong style=\"font-size: 12px; white-space: nowrap;\">Child " + childPos + "</strong><br />");
  441. out.push("<strong>left:</strong> " + child.css("left") + "<br />");
  442. out.push("<strong>top:</strong> " + child.css("top") + "<br />");
  443. out.push("<strong>width:</strong> " + child.css("width") + "<br />");
  444. out.push("<strong>opacity:</strong> " + child.css("opacity") + "<br />");
  445. out.push("<strong>height:</strong> " + child.css("height") + "<br />");
  446. out.push("<strong>z-index:</strong> " + child.css("z-index") + "<br />");
  447. out.push("<strong>font-size:</strong> " + child.css("font-size") + "<br />");
  448. out.push("<strong>scale:</strong> " + child.data("roundabout").currentScale);
  449. out.push("</div>");
  450. child.html(out.join(""));
  451. }
  452. // trigger event
  453. child.trigger("reposition");
  454. // callback
  455. callback.apply(self);
  456. return methods.isInFocus.apply(self, [data.degrees]);
  457. },
  458. // manipulation
  459. // -----------------------------------------------------------------------
  460. // setBearing
  461. // changes the bearing of the roundabout
  462. setBearing: function(bearing, callback) {
  463. callback = callback || function() {};
  464. bearing = methods.normalize.apply(null, [bearing]);
  465. this
  466. .each(function() {
  467. var diff, lowerValue, higherValue,
  468. self = $(this),
  469. data = self.data("roundabout"),
  470. oldBearing = data.bearing;
  471. // set bearing
  472. data.bearing = bearing;
  473. self.trigger("bearingSet");
  474. methods.updateChildren.apply(self);
  475. // not animating? we're done here
  476. diff = Math.abs(oldBearing - bearing);
  477. if (!data.animating || diff > 180) {
  478. return;
  479. }
  480. // check to see if any of the children went through the back
  481. diff = Math.abs(oldBearing - bearing);
  482. self.children(data.childSelector).each(function(i) {
  483. var eventType;
  484. if (methods.isChildBackDegreesBetween.apply($(this), [bearing, oldBearing])) {
  485. eventType = (oldBearing > bearing) ? "Clockwise" : "Counterclockwise";
  486. $(this).trigger("move" + eventType + "ThroughBack");
  487. }
  488. });
  489. });
  490. // call callback if one was given
  491. callback.apply(this);
  492. return this;
  493. },
  494. // adjustBearing
  495. // change the bearing of the roundabout by a given degree
  496. adjustBearing: function(delta, callback) {
  497. callback = callback || function() {};
  498. if (delta === 0) {
  499. return this;
  500. }
  501. this
  502. .each(function() {
  503. methods.setBearing.apply($(this), [$(this).data("roundabout").bearing + delta]);
  504. });
  505. callback.apply(this);
  506. return this;
  507. },
  508. // setTilt
  509. // changes the tilt of the roundabout
  510. setTilt: function(tilt, callback) {
  511. callback = callback || function() {};
  512. this
  513. .each(function() {
  514. $(this).data("roundabout").tilt = tilt;
  515. methods.updateChildren.apply($(this));
  516. });
  517. // call callback if one was given
  518. callback.apply(this);
  519. return this;
  520. },
  521. // adjustTilt
  522. // changes the tilt of the roundabout
  523. adjustTilt: function(delta, callback) {
  524. callback = callback || function() {};
  525. this
  526. .each(function() {
  527. methods.setTilt.apply($(this), [$(this).data("roundabout").tilt + delta]);
  528. });
  529. callback.apply(this);
  530. return this;
  531. },
  532. // animation
  533. // -----------------------------------------------------------------------
  534. // animateToBearing
  535. // animates the roundabout to a given bearing, all animations come through here
  536. animateToBearing: function(bearing, duration, easing, passedData, callback) {
  537. var now = (new Date()).getTime();
  538. callback = callback || function() {};
  539. // find callback function in arguments
  540. if ($.isFunction(passedData)) {
  541. callback = passedData;
  542. passedData = null;
  543. } else if ($.isFunction(easing)) {
  544. callback = easing;
  545. easing = null;
  546. } else if ($.isFunction(duration)) {
  547. callback = duration;
  548. duration = null;
  549. }
  550. this
  551. .each(function() {
  552. var timer, easingFn, newBearing,
  553. self = $(this),
  554. data = self.data("roundabout"),
  555. thisDuration = (!duration) ? data.duration : duration,
  556. thisEasingType = (easing) ? easing : data.easing || "swing";
  557. // is this your first time?
  558. if (!passedData) {
  559. passedData = {
  560. timerStart: now,
  561. start: data.bearing,
  562. totalTime: thisDuration
  563. };
  564. }
  565. // update the timer
  566. timer = now - passedData.timerStart;
  567. if (data.stopAnimation) {
  568. methods.allowAnimation.apply(self);
  569. data.animating = false;
  570. return;
  571. }
  572. // we need to animate more
  573. if (timer < thisDuration) {
  574. if (!data.animating) {
  575. self.trigger("animationStart");
  576. }
  577. data.animating = true;
  578. if (typeof $.easing.def === "string") {
  579. easingFn = $.easing[thisEasingType] || $.easing[$.easing.def];
  580. newBearing = easingFn(null, timer, passedData.start, bearing - passedData.start, passedData.totalTime);
  581. } else {
  582. newBearing = $.easing[thisEasingType]((timer / passedData.totalTime), timer, passedData.start, bearing - passedData.start, passedData.totalTime);
  583. }
  584. newBearing = methods.normalize.apply(null, [newBearing]);
  585. data.dragBearing = newBearing;
  586. methods.setBearing.apply(self, [newBearing, function() {
  587. setTimeout(function() { // done with a timeout so that each step is displayed
  588. methods.animateToBearing.apply(self, [bearing, thisDuration, thisEasingType, passedData, callback]);
  589. }, 0);
  590. }]);
  591. // we're done animating
  592. } else {
  593. if (data.animating) {
  594. self.trigger("animationEnd");
  595. }
  596. data.lastAnimationStep = true;
  597. bearing = methods.normalize.apply(null, [bearing]);
  598. methods.setBearing.apply(self, [bearing]);
  599. data.animating = false;
  600. data.lastAnimationStep = false;
  601. data.dragBearing = bearing;
  602. callback.apply(self);
  603. }
  604. });
  605. return this;
  606. },
  607. // animateToNearbyChild
  608. // animates roundabout to a nearby child
  609. animateToNearbyChild: function(passedArgs, which) {
  610. var duration = passedArgs[0],
  611. easing = passedArgs[1],
  612. callback = passedArgs[2] || function() {};
  613. // find callback
  614. if ($.isFunction(easing)) {
  615. callback = easing;
  616. easing = null;
  617. } else if ($.isFunction(duration)) {
  618. callback = duration;
  619. duration = null;
  620. }
  621. return this
  622. .each(function() {
  623. var j, range,
  624. self = $(this),
  625. data = self.data("roundabout"),
  626. bearing = (!data.reflect) ? data.bearing % 360 : data.bearing,
  627. length = self.children(data.childSelector).length;
  628. if (!data.animating) {
  629. // reflecting, not moving to previous || not reflecting, moving to next
  630. if ((data.reflect && which === "previous") || (!data.reflect && which === "next")) {
  631. // slightly adjust for rounding issues
  632. bearing = (Math.abs(bearing) < data.floatComparisonThreshold) ? 360 : bearing;
  633. // clockwise
  634. for (j = 0; j < length; j += 1) {
  635. range = {
  636. lower: (data.period * j),
  637. upper: (data.period * (j + 1))
  638. };
  639. range.upper = (j === length - 1) ? 360 : range.upper;
  640. if (bearing <= Math.ceil(range.upper) && bearing >= Math.floor(range.lower)) {
  641. if (length === 2 && bearing === 360) {
  642. methods.animateToDelta.apply(self, [-180, duration, easing, callback]);
  643. } else {
  644. methods.animateBearingToFocus.apply(self, [range.lower, duration, easing, callback]);
  645. }
  646. break;
  647. }
  648. }
  649. } else {
  650. // slightly adjust for rounding issues
  651. bearing = (Math.abs(bearing) < data.floatComparisonThreshold || 360 - Math.abs(bearing) < data.floatComparisonThreshold) ? 0 : bearing;
  652. // counterclockwise
  653. for (j = length - 1; j >= 0; j -= 1) {
  654. range = {
  655. lower: data.period * j,
  656. upper: data.period * (j + 1)
  657. };
  658. range.upper = (j === length - 1) ? 360 : range.upper;
  659. if (bearing >= Math.floor(range.lower) && bearing < Math.ceil(range.upper)) {
  660. if (length === 2 && bearing === 360) {
  661. methods.animateToDelta.apply(self, [180, duration, easing, callback]);
  662. } else {
  663. methods.animateBearingToFocus.apply(self, [range.upper, duration, easing, callback]);
  664. }
  665. break;
  666. }
  667. }
  668. }
  669. }
  670. });
  671. },
  672. // animateToNearestChild
  673. // animates roundabout to the nearest child
  674. animateToNearestChild: function(duration, easing, callback) {
  675. callback = callback || function() {};
  676. // find callback
  677. if ($.isFunction(easing)) {
  678. callback = easing;
  679. easing = null;
  680. } else if ($.isFunction(duration)) {
  681. callback = duration;
  682. duration = null;
  683. }
  684. return this
  685. .each(function() {
  686. var nearest = methods.getNearestChild.apply($(this));
  687. methods.animateToChild.apply($(this), [nearest, duration, easing, callback]);
  688. });
  689. },
  690. // animateToChild
  691. // animates roundabout to a given child position
  692. animateToChild: function(childPosition, duration, easing, callback) {
  693. callback = callback || function() {};
  694. // find callback
  695. if ($.isFunction(easing)) {
  696. callback = easing;
  697. easing = null;
  698. } else if ($.isFunction(duration)) {
  699. callback = duration;
  700. duration = null;
  701. }
  702. return this
  703. .each(function() {
  704. var child,
  705. self = $(this),
  706. data = self.data("roundabout");
  707. if (data.childInFocus !== childPosition && !data.animating) {
  708. child = self.children(data.childSelector).eq(childPosition);
  709. methods.animateBearingToFocus.apply(self, [child.data("roundabout").degrees, duration, easing, callback]);
  710. }
  711. });
  712. },
  713. // animateToNextChild
  714. // animates roundabout to the next child
  715. animateToNextChild: function(duration, easing, callback) {
  716. return methods.animateToNearbyChild.apply(this, [arguments, "next"]);
  717. },
  718. // animateToPreviousChild
  719. // animates roundabout to the preious child
  720. animateToPreviousChild: function(duration, easing, callback) {
  721. return methods.animateToNearbyChild.apply(this, [arguments, "previous"]);
  722. },
  723. // animateToDelta
  724. // animates roundabout to a given delta (in degrees)
  725. animateToDelta: function(degrees, duration, easing, callback) {
  726. callback = callback || function() {};
  727. // find callback
  728. if ($.isFunction(easing)) {
  729. callback = easing;
  730. easing = null;
  731. } else if ($.isFunction(duration)) {
  732. callback = duration;
  733. duration = null;
  734. }
  735. return this
  736. .each(function() {
  737. var delta = $(this).data("roundabout").bearing + degrees;
  738. methods.animateToBearing.apply($(this), [delta, duration, easing, callback]);
  739. });
  740. },
  741. // animateBearingToFocus
  742. // animates roundabout to bring a given angle into focus
  743. animateBearingToFocus: function(degrees, duration, easing, callback) {
  744. callback = callback || function() {};
  745. // find callback
  746. if ($.isFunction(easing)) {
  747. callback = easing;
  748. easing = null;
  749. } else if ($.isFunction(duration)) {
  750. callback = duration;
  751. duration = null;
  752. }
  753. return this
  754. .each(function() {
  755. var delta = $(this).data("roundabout").bearing - degrees;
  756. delta = (Math.abs(360 - delta) < Math.abs(delta)) ? 360 - delta : -delta;
  757. delta = (delta > 180) ? -(360 - delta) : delta;
  758. if (delta !== 0) {
  759. methods.animateToDelta.apply($(this), [delta, duration, easing, callback]);
  760. }
  761. });
  762. },
  763. // stopAnimation
  764. // if an animation is currently in progress, stop it
  765. stopAnimation: function() {
  766. return this
  767. .each(function() {
  768. $(this).data("roundabout").stopAnimation = true;
  769. });
  770. },
  771. // allowAnimation
  772. // clears the stop-animation hold placed by stopAnimation
  773. allowAnimation: function() {
  774. return this
  775. .each(function() {
  776. $(this).data("roundabout").stopAnimation = false;
  777. });
  778. },
  779. // autoplay
  780. // -----------------------------------------------------------------------
  781. // startAutoplay
  782. // starts autoplaying this roundabout
  783. startAutoplay: function(callback) {
  784. return this
  785. .each(function() {
  786. var self = $(this),
  787. data = self.data("roundabout");
  788. callback = callback || data.autoplayCallback || function() {};
  789. clearInterval(data.autoplayInterval);
  790. data.autoplayInterval = setInterval(function() {
  791. methods.animateToNextChild.apply(self, [callback]);
  792. }, data.autoplayDuration);
  793. data.autoplayIsRunning = true;
  794. self.trigger("autoplayStart");
  795. });
  796. },
  797. // stopAutoplay
  798. // stops autoplaying this roundabout
  799. stopAutoplay: function(keepAutoplayBindings) {
  800. return this
  801. .each(function() {
  802. clearInterval($(this).data("roundabout").autoplayInterval);
  803. $(this).data("roundabout").autoplayInterval = null;
  804. $(this).data("roundabout").autoplayIsRunning = false;
  805. // this will prevent autoplayPauseOnHover from restarting autoplay
  806. if (!keepAutoplayBindings) {
  807. $(this).unbind(".autoplay")
  808. }
  809. $(this).trigger("autoplayStop");
  810. });
  811. },
  812. // toggleAutoplay
  813. // toggles autoplay pause/resume
  814. toggleAutoplay: function(callback) {
  815. return this
  816. .each(function() {
  817. var self = $(this),
  818. data = self.data("roundabout");
  819. callback = callback || data.autoplayCallback || function() {};
  820. if (!methods.isAutoplaying.apply($(this))) {
  821. methods.startAutoplay.apply($(this), [callback]);
  822. } else {
  823. methods.stopAutoplay.apply($(this), [callback]);
  824. }
  825. });
  826. },
  827. // isAutoplaying
  828. // is this roundabout currently autoplaying?
  829. isAutoplaying: function() {
  830. return (this.data("roundabout").autoplayIsRunning);
  831. },
  832. // changeAutoplayDuration
  833. // stops the autoplay, changes the duration, restarts autoplay
  834. changeAutoplayDuration: function(duration) {
  835. return this
  836. .each(function() {
  837. var self = $(this),
  838. data = self.data("roundabout");
  839. data.autoplayDuration = duration;
  840. if (methods.isAutoplaying.apply(self)) {
  841. methods.stopAutoplay.apply(self);
  842. setTimeout(function() {
  843. methods.startAutoplay.apply(self);
  844. }, 10);
  845. }
  846. });
  847. },
  848. // helpers
  849. // -----------------------------------------------------------------------
  850. // normalize
  851. // regulates degrees to be >= 0.0 and < 360
  852. normalize: function(degrees) {
  853. var inRange = degrees % 360.0;
  854. return (inRange < 0) ? 360 + inRange : inRange;
  855. },
  856. // normalizeRad
  857. // regulates radians to be >= 0 and < Math.PI * 2
  858. normalizeRad: function(radians) {
  859. while (radians < 0) {
  860. radians += (Math.PI * 2);
  861. }
  862. while (radians > (Math.PI * 2)) {
  863. radians -= (Math.PI * 2);
  864. }
  865. return radians;
  866. },
  867. // isChildBackDegreesBetween
  868. // checks that a given child's backDegrees is between two values
  869. isChildBackDegreesBetween: function(value1, value2) {
  870. var backDegrees = $(this).data("roundabout").backDegrees;
  871. if (value1 > value2) {
  872. return (backDegrees >= value2 && backDegrees < value1);
  873. } else {
  874. return (backDegrees < value2 && backDegrees >= value1);
  875. }
  876. },
  877. // getAnimateToMethod
  878. // takes a user-entered option and maps it to an animation method
  879. getAnimateToMethod: function(effect) {
  880. effect = effect.toLowerCase();
  881. if (effect === "next") {
  882. return "animateToNextChild";
  883. } else if (effect === "previous") {
  884. return "animateToPreviousChild";
  885. }
  886. // default selection
  887. return "animateToNearestChild";
  888. },
  889. // relayoutChildren
  890. // lays out children again with new contextual information
  891. relayoutChildren: function() {
  892. return this
  893. .each(function() {
  894. var self = $(this),
  895. settings = $.extend({}, self.data("roundabout"));
  896. settings.startingChild = self.data("roundabout").childInFocus;
  897. methods.init.apply(self, [settings, null, true]);
  898. });
  899. },
  900. // getNearestChild
  901. // gets the nearest child from the current bearing
  902. getNearestChild: function() {
  903. var self = $(this),
  904. data = self.data("roundabout"),
  905. length = self.children(data.childSelector).length;
  906. if (!data.reflect) {
  907. return ((length) - (Math.round(data.bearing / data.period) % length)) % length;
  908. } else {
  909. return (Math.round(data.bearing / data.period) % length);
  910. }
  911. },
  912. // degToRad
  913. // converts degrees to radians
  914. degToRad: function(degrees) {
  915. return methods.normalize.apply(null, [degrees]) * Math.PI / 180.0;
  916. },
  917. // getPlacement
  918. // returns the starting degree for a given child
  919. getPlacement: function(child) {
  920. var data = this.data("roundabout");
  921. return (!data.reflect) ? 360.0 - (data.period * child) : data.period * child;
  922. },
  923. // isInFocus
  924. // is this roundabout currently in focus?
  925. isInFocus: function(degrees) {
  926. var diff,
  927. self = this,
  928. data = self.data("roundabout"),
  929. bearing = methods.normalize.apply(null, [data.bearing]);
  930. degrees = methods.normalize.apply(null, [degrees]);
  931. diff = Math.abs(bearing - degrees);
  932. // this calculation gives a bit of room for javascript float rounding
  933. // errors, it looks on both 0deg and 360deg ends of the spectrum
  934. return (diff <= data.floatComparisonThreshold || diff >= 360 - data.floatComparisonThreshold);
  935. }
  936. };
  937. // start the plugin
  938. $.fn.roundabout = function(method) {
  939. if (methods[method]) {
  940. return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
  941. } else if (typeof method === "object" || $.isFunction(method) || !method) {
  942. return methods.init.apply(this, arguments);
  943. } else {
  944. $.error("Method " + method + " does not exist for jQuery.roundabout.");
  945. }
  946. };
  947. })(jQuery);