You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

503 lines
18KB

  1. /**
  2. * jQuery Roundabout - v1.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) 2010, 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. // creates a default shape to be used for pathing
  42. jQuery.extend({
  43. roundabout_shape: {
  44. def: 'lazySusan',
  45. lazySusan: function(r, a, t) {
  46. return {
  47. x: Math.sin(r + a),
  48. y: (Math.sin(r + 3*Math.PI/2 + a) / 8) * t,
  49. z: (Math.cos(r + a) + 1) / 2,
  50. scale: (Math.sin(r + Math.PI/2 + a) / 2) + 0.5
  51. };
  52. }
  53. }
  54. });
  55. jQuery.fn.roundabout = function() {
  56. var options = (typeof arguments[0] != 'object') ? {} : arguments[0];
  57. // set options and fill in defaults
  58. options = {
  59. bearing: (typeof options.bearing == 'undefined') ? 0.0 : jQuery.roundabout_toFloat(options.bearing % 360.0),
  60. tilt: (typeof options.tilt == 'undefined') ? 0.0 : jQuery.roundabout_toFloat(options.tilt),
  61. minZ: (typeof options.minZ == 'undefined') ? 100 : parseInt(options.minZ, 10),
  62. maxZ: (typeof options.maxZ == 'undefined') ? 400 : parseInt(options.maxZ, 10),
  63. minOpacity: (typeof options.minOpacity == 'undefined') ? 0.40 : jQuery.roundabout_toFloat(options.minOpacity),
  64. maxOpacity: (typeof options.maxOpacity == 'undefined') ? 1.00 : jQuery.roundabout_toFloat(options.maxOpacity),
  65. minScale: (typeof options.minScale == 'undefined') ? 0.40 : jQuery.roundabout_toFloat(options.minScale),
  66. maxScale: (typeof options.maxScale == 'undefined') ? 1.00 : jQuery.roundabout_toFloat(options.maxScale),
  67. duration: (typeof options.duration == 'undefined') ? 600 : parseInt(options.duration, 10),
  68. btnNext: options.btnNext || null,
  69. btnPrev: options.btnPrev || null,
  70. easing: options.easing || 'swing',
  71. clickToFocus: (options.clickToFocus !== false),
  72. focusBearing: (typeof options.focusBearing == 'undefined') ? 0.0 : jQuery.roundabout_toFloat(options.focusBearing % 360.0),
  73. shape: options.shape || 'lazySusan',
  74. debug: options.debug || false,
  75. childSelector: options.childSelector || 'li',
  76. startingChild: (typeof options.startingChild == 'undefined') ? null : parseInt(options.startingChild, 10),
  77. reflect: (typeof options.reflect == 'undefined' || options.reflect === false) ? false : true
  78. };
  79. // assign things
  80. this.each(function(i) {
  81. var ref = jQuery(this);
  82. var period = jQuery.roundabout_toFloat(360.0 / ref.children(options.childSelector).length);
  83. var startingBearing = (options.startingChild === null) ? options.bearing : options.startingChild * period;
  84. // set starting styles
  85. ref
  86. .addClass('roundabout-holder')
  87. .css('padding', 0)
  88. .css('position', 'relative')
  89. .css('z-index', options.minZ);
  90. // set starting options
  91. ref.data('roundabout', {
  92. 'bearing': startingBearing,
  93. 'tilt': options.tilt,
  94. 'minZ': options.minZ,
  95. 'maxZ': options.maxZ,
  96. 'minOpacity': options.minOpacity,
  97. 'maxOpacity': options.maxOpacity,
  98. 'minScale': options.minScale,
  99. 'maxScale': options.maxScale,
  100. 'duration': options.duration,
  101. 'easing': options.easing,
  102. 'clickToFocus': options.clickToFocus,
  103. 'focusBearing': options.focusBearing,
  104. 'animating': 0,
  105. 'childInFocus': -1,
  106. 'shape': options.shape,
  107. 'period': period,
  108. 'debug': options.debug,
  109. 'childSelector': options.childSelector,
  110. 'reflect': options.reflect
  111. });
  112. // bind click events
  113. if (options.clickToFocus === true) {
  114. ref.children(options.childSelector).each(function(i) {
  115. jQuery(this).click(function(e) {
  116. var degrees = (options.reflect === true) ? 360.0 - (period * i) : period * i;
  117. degrees = jQuery.roundabout_toFloat(degrees);
  118. if (!jQuery.roundabout_isInFocus(ref, degrees)) {
  119. e.preventDefault();
  120. if (ref.data('roundabout').animating === 0) {
  121. ref.roundabout_animateAngleToFocus(degrees);
  122. }
  123. return false;
  124. }
  125. });
  126. });
  127. }
  128. // bind next buttons
  129. if (options.btnNext) {
  130. jQuery(options.btnNext).bind('click.roundabout', function(e) {
  131. e.preventDefault();
  132. if (ref.data('roundabout').animating === 0) {
  133. ref.roundabout_animateToNextChild();
  134. }
  135. return false;
  136. });
  137. }
  138. // bind previous buttons
  139. if (options.btnPrev) {
  140. jQuery(options.btnPrev).bind('click.roundabout', function(e) {
  141. e.preventDefault();
  142. if (ref.data('roundabout').animating === 0) {
  143. ref.roundabout_animateToPreviousChild();
  144. }
  145. return false;
  146. });
  147. }
  148. });
  149. // start children
  150. this.roundabout_startChildren();
  151. // callback once ready
  152. if (typeof arguments[1] === 'function') {
  153. var callback = arguments[1], ref = this;
  154. setTimeout(function() { callback(ref); }, 0);
  155. }
  156. return this;
  157. };
  158. jQuery.fn.roundabout_startChildren = function() {
  159. this.each(function(i) {
  160. var ref = jQuery(this);
  161. var data = ref.data('roundabout');
  162. var children = ref.children(data.childSelector);
  163. children.each(function(i) {
  164. var degrees = (data.reflect === true) ? 360.0 - (data.period * i) : data.period * i;
  165. // apply classes and css first
  166. jQuery(this)
  167. .addClass('roundabout-moveable-item')
  168. .css('position', 'absolute');
  169. // then measure
  170. jQuery(this).data('roundabout', {
  171. 'startWidth': jQuery(this).width(),
  172. 'startHeight': jQuery(this).height(),
  173. 'startFontSize': parseInt(jQuery(this).css('font-size'), 10),
  174. 'degrees': degrees
  175. });
  176. });
  177. ref.roundabout_updateChildPositions();
  178. });
  179. return this;
  180. };
  181. jQuery.fn.roundabout_setTilt = function(newTilt) {
  182. this.each(function(i) {
  183. jQuery(this).data('roundabout').tilt = newTilt;
  184. jQuery(this).roundabout_updateChildPositions();
  185. });
  186. if (typeof arguments[1] === 'function') {
  187. var callback = arguments[1], ref = this;
  188. setTimeout(function() { callback(ref); }, 0);
  189. }
  190. return this;
  191. };
  192. jQuery.fn.roundabout_setBearing = function(newBearing) {
  193. this.each(function(i) {
  194. jQuery(this).data('roundabout').bearing = jQuery.roundabout_toFloat(newBearing % 360, 2);
  195. jQuery(this).roundabout_updateChildPositions();
  196. });
  197. if (typeof arguments[1] === 'function') {
  198. var callback = arguments[1], ref = this;
  199. setTimeout(function() { callback(ref); }, 0);
  200. }
  201. return this;
  202. };
  203. jQuery.fn.roundabout_adjustBearing = function(delta) {
  204. delta = jQuery.roundabout_toFloat(delta);
  205. if (delta !== 0) {
  206. this.each(function(i) {
  207. jQuery(this).data('roundabout').bearing = jQuery.roundabout_getBearing(jQuery(this)) + delta;
  208. jQuery(this).roundabout_updateChildPositions();
  209. });
  210. }
  211. if (typeof arguments[1] === 'function') {
  212. var callback = arguments[1], ref = this;
  213. setTimeout(function() { callback(ref); }, 0);
  214. }
  215. return this;
  216. };
  217. jQuery.fn.roundabout_adjustTilt = function(delta) {
  218. delta = jQuery.roundabout_toFloat(delta);
  219. if (delta !== 0) {
  220. this.each(function(i) {
  221. jQuery(this).data('roundabout').tilt = jQuery.roundabout_toFloat(jQuery(this).roundabout_get('tilt') + delta);
  222. jQuery(this).roundabout_updateChildPositions();
  223. });
  224. }
  225. if (typeof arguments[1] === 'function') {
  226. var callback = arguments[1], ref = this;
  227. setTimeout(function() { callback(ref); }, 0);
  228. }
  229. return this;
  230. };
  231. jQuery.fn.roundabout_animateToBearing = function(bearing) {
  232. bearing = jQuery.roundabout_toFloat(bearing);
  233. var currentTime = new Date();
  234. var duration = (typeof arguments[1] == 'undefined') ? null : arguments[1];
  235. var easingType = (typeof arguments[2] == 'undefined') ? null : arguments[2];
  236. var passedData = (typeof arguments[3] !== 'object') ? null : arguments[3];
  237. this.each(function(i) {
  238. var ref = jQuery(this), data = ref.data('roundabout'), timer, easingFn, newBearing;
  239. var thisDuration = (duration === null) ? data.duration : duration;
  240. var thisEasingType = (easingType !== null) ? easingType : data.easing || 'swing';
  241. if (passedData === null) {
  242. passedData = {
  243. timerStart: currentTime,
  244. start: jQuery.roundabout_getBearing(ref),
  245. totalTime: thisDuration
  246. };
  247. }
  248. timer = currentTime - passedData.timerStart;
  249. if (timer < thisDuration) {
  250. data.animating = 1;
  251. if (typeof jQuery.easing.def == 'string') {
  252. easingFn = jQuery.easing[thisEasingType] || jQuery.easing[jQuery.easing.def];
  253. newBearing = easingFn(null, timer, passedData.start, bearing - passedData.start, passedData.totalTime);
  254. } else {
  255. newBearing = jQuery.easing[thisEasingType]((timer / passedData.totalTime), timer, passedData.start, bearing - passedData.start, passedData.totalTime);
  256. }
  257. ref.roundabout_setBearing(newBearing, function() { ref.roundabout_animateToBearing(bearing, thisDuration, thisEasingType, passedData); });
  258. } else {
  259. bearing = (bearing < 0) ? bearing + 360 : bearing % 360;
  260. data.animating = 0;
  261. ref.roundabout_setBearing(bearing);
  262. }
  263. });
  264. return this;
  265. };
  266. jQuery.fn.roundabout_animateToDelta = function(delta) {
  267. var duration = arguments[1], easing = arguments[2];
  268. this.each(function(i) {
  269. delta = jQuery.roundabout_getBearing(jQuery(this)) + jQuery.roundabout_toFloat(delta);
  270. jQuery(this).roundabout_animateToBearing(delta, duration, easing);
  271. });
  272. return this;
  273. };
  274. jQuery.fn.roundabout_animateToChild = function(childPos) {
  275. var duration = arguments[1], easing = arguments[2];
  276. this.each(function(i) {
  277. var ref = jQuery(this), data = ref.data('roundabout');
  278. if (data.childInFocus !== childPos && data.animating === 0) {
  279. var child = jQuery(ref.children(data.childSelector)[childPos]);
  280. ref.roundabout_animateAngleToFocus(child.data('roundabout').degrees, duration, easing);
  281. }
  282. });
  283. return this;
  284. };
  285. jQuery.fn.roundabout_animateToNearbyChild = function(passedArgs, which) {
  286. var duration = passedArgs[0], easing = passedArgs[1];
  287. this.each(function(i) {
  288. var data = jQuery(this).data('roundabout');
  289. var bearing = jQuery.roundabout_toFloat(360.0 - jQuery.roundabout_getBearing(jQuery(this)));
  290. var period = data.period, j = 0, range;
  291. var reflect = data.reflect;
  292. var length = jQuery(this).children(data.childSelector).length;
  293. bearing = (reflect === true) ? bearing % 360.0 : bearing;
  294. if (data.animating === 0) {
  295. // if we're not reflecting and we're moving to next or
  296. // we are reflecting and we're moving previous
  297. if ((reflect === false && which === 'next') || (reflect === true && which !== 'next')) {
  298. bearing = (bearing === 0) ? 360 : bearing;
  299. // counterclockwise
  300. while (true && j < length) {
  301. range = { lower: jQuery.roundabout_toFloat(period * j), upper: jQuery.roundabout_toFloat(period * (j + 1)) };
  302. range.upper = (j == length - 1) ? 360.0 : range.upper; // adjust for javascript being bad at floats
  303. if (bearing <= range.upper && bearing > range.lower) {
  304. jQuery(this).roundabout_animateToDelta(bearing - range.lower, duration, easing);
  305. break;
  306. }
  307. j++;
  308. }
  309. } else {
  310. // clockwise
  311. while (true) {
  312. range = { lower: jQuery.roundabout_toFloat(period * j), upper: jQuery.roundabout_toFloat(period * (j + 1)) };
  313. range.upper = (j == length - 1) ? 360.0 : range.upper; // adjust for javascript being bad at floats
  314. if (bearing >= range.lower && bearing < range.upper) {
  315. jQuery(this).roundabout_animateToDelta(bearing - range.upper, duration, easing);
  316. break;
  317. }
  318. j++;
  319. }
  320. }
  321. }
  322. });
  323. return this;
  324. };
  325. jQuery.fn.roundabout_animateToNextChild = function() {
  326. return this.roundabout_animateToNearbyChild(arguments, 'next');
  327. };
  328. jQuery.fn.roundabout_animateToPreviousChild = function() {
  329. return this.roundabout_animateToNearbyChild(arguments, 'previous');
  330. };
  331. // moves a given angle to the focus by the shortest means possible
  332. jQuery.fn.roundabout_animateAngleToFocus = function(target) {
  333. var duration = arguments[1], easing = arguments[2];
  334. this.each(function(i) {
  335. var delta = jQuery.roundabout_getBearing(jQuery(this)) - target;
  336. delta = (Math.abs(360.0 - delta) < Math.abs(0.0 - delta)) ? 360.0 - delta : 0.0 - delta;
  337. delta = (delta > 180) ? -(360.0 - delta) : delta;
  338. if (delta !== 0) {
  339. jQuery(this).roundabout_animateToDelta(delta, duration, easing);
  340. }
  341. });
  342. return this;
  343. };
  344. jQuery.fn.roundabout_updateChildPositions = function() {
  345. this.each(function(i) {
  346. var ref = jQuery(this), data = ref.data('roundabout');
  347. var inFocus = -1;
  348. var info = {
  349. bearing: jQuery.roundabout_getBearing(ref),
  350. tilt: data.tilt,
  351. stage: { width: Math.floor(ref.width() * 0.9), height: Math.floor(ref.height() * 0.9) },
  352. animating: data.animating,
  353. inFocus: data.childInFocus,
  354. focusBearingRad: jQuery.roundabout_degToRad(data.focusBearing),
  355. shape: jQuery.roundabout_shape[data.shape] || jQuery.roundabout_shape[jQuery.roundabout_shape.def]
  356. };
  357. info.midStage = { width: info.stage.width / 2, height: info.stage.height / 2 };
  358. info.nudge = { width: info.midStage.width + info.stage.width * 0.05, height: info.midStage.height + info.stage.height * 0.05 };
  359. info.zValues = { min: data.minZ, max: data.maxZ, diff: data.maxZ - data.minZ };
  360. info.opacity = { min: data.minOpacity, max: data.maxOpacity, diff: data.maxOpacity - data.minOpacity };
  361. info.scale = { min: data.minScale, max: data.maxScale, diff: data.maxScale - data.minScale };
  362. // update child positions
  363. ref.children(data.childSelector).each(function(i) {
  364. if (jQuery.roundabout_updateChildPosition(jQuery(this), ref, info, i) && info.animating === 0) {
  365. inFocus = i;
  366. jQuery(this).addClass('roundabout-in-focus');
  367. } else {
  368. jQuery(this).removeClass('roundabout-in-focus');
  369. }
  370. });
  371. // update status of who is in focus
  372. if (inFocus !== info.inFocus) {
  373. jQuery.roundabout_triggerEvent(ref, info.inFocus, 'blur');
  374. if (inFocus !== -1) {
  375. jQuery.roundabout_triggerEvent(ref, inFocus, 'focus');
  376. }
  377. data.childInFocus = inFocus;
  378. }
  379. });
  380. return this;
  381. };
  382. //----------------
  383. jQuery.roundabout_getBearing = function(el) {
  384. return jQuery.roundabout_toFloat(el.data('roundabout').bearing) % 360;
  385. };
  386. jQuery.roundabout_degToRad = function(degrees) {
  387. return (degrees % 360.0) * Math.PI / 180.0;
  388. };
  389. jQuery.roundabout_isInFocus = function(el, target) {
  390. return (jQuery.roundabout_getBearing(el) % 360 === (target % 360));
  391. };
  392. jQuery.roundabout_triggerEvent = function(el, child, eventType) {
  393. return (child < 0) ? this : jQuery(el.children(el.data('roundabout').childSelector)[child]).trigger(eventType);
  394. };
  395. jQuery.roundabout_toFloat = function(number) {
  396. number = Math.round(parseFloat(number) * 1000) / 1000;
  397. return parseFloat(number.toFixed(2));
  398. };
  399. jQuery.roundabout_updateChildPosition = function(child, container, info, childPos) {
  400. var ref = jQuery(child), data = ref.data('roundabout'), out = [];
  401. var rad = jQuery.roundabout_degToRad((360.0 - ref.data('roundabout').degrees) + info.bearing);
  402. // adjust radians to be between 0 and Math.PI * 2
  403. while (rad < 0) {
  404. rad = rad + Math.PI * 2;
  405. }
  406. while (rad > Math.PI * 2) {
  407. rad = rad - Math.PI * 2;
  408. }
  409. var factors = info.shape(rad, info.focusBearingRad, info.tilt); // obj with x, y, z, and scale values
  410. // correct
  411. factors.scale = (factors.scale > 1) ? 1 : factors.scale;
  412. factors.adjustedScale = (info.scale.min + (info.scale.diff * factors.scale)).toFixed(4);
  413. factors.width = (factors.adjustedScale * data.startWidth).toFixed(4);
  414. factors.height = (factors.adjustedScale * data.startHeight).toFixed(4);
  415. // alter item
  416. ref
  417. .css('left', ((factors.x * info.midStage.width + info.nudge.width) - factors.width / 2.0).toFixed(1) + 'px')
  418. .css('top', ((factors.y * info.midStage.height + info.nudge.height) - factors.height / 2.0).toFixed(1) + 'px')
  419. .css('width', factors.width + 'px')
  420. .css('height', factors.height + 'px')
  421. .css('opacity', (info.opacity.min + (info.opacity.diff * factors.scale)).toFixed(2))
  422. .css('z-index', Math.round(info.zValues.min + (info.zValues.diff * factors.z)))
  423. .css('font-size', (factors.adjustedScale * data.startFontSize).toFixed(2) + 'px')
  424. .attr('current-scale', factors.adjustedScale);
  425. if (container.data('roundabout').debug === true) {
  426. out.push('<div style="font-weight: normal; font-size: 10px; padding: 2px; width: ' + ref.css('width') + '; background-color: #ffc;">');
  427. out.push('<strong style="font-size: 12px; white-space: nowrap;">Child ' + childPos + '</strong><br />');
  428. out.push('<strong>left:</strong> ' + ref.css('left') + '<br /><strong>top:</strong> ' + ref.css('top') + '<br />');
  429. out.push('<strong>width:</strong> ' + ref.css('width') + '<br /><strong>opacity:</strong> ' + ref.css('opacity') + '<br />');
  430. out.push('<strong>z-index:</strong> ' + ref.css('z-index') + '<br /><strong>font-size:</strong> ' + ref.css('font-size') + '<br />');
  431. out.push('<strong>scale:</strong> ' + ref.attr('current-scale'));
  432. out.push('</div>');
  433. ref.html(out.join(''));
  434. }
  435. return jQuery.roundabout_isInFocus(container, ref.data('roundabout').degrees);
  436. };