diff --git a/jquery.roundabout.js b/jquery.roundabout.js
new file mode 100644
index 0000000..a38210e
--- /dev/null
+++ b/jquery.roundabout.js
@@ -0,0 +1,1194 @@
+/**
+ * jQuery Roundabout - v2.0
+ * http://fredhq.com/projects/roundabout
+ *
+ * Moves list-items of enabled ordered and unordered lists long
+ * a chosen path. Includes the default "lazySusan" path, that
+ * moves items long a spinning turntable.
+ *
+ * Terms of Use // jQuery Roundabout
+ *
+ * Open source under the BSD license
+ *
+ * Copyright (c) 2011, Fred LeBlanc
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * - Neither the name of the author nor the names of its contributors
+ * may be used to endorse or promote products derived from this
+ * software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+;(function($) {
+ var defaults, internalData, methods;
+
+ // add default shape
+ $.extend({
+ roundaboutShapes: {
+ def: "lazySusan",
+ lazySusan: function (r, a, t) {
+ return {
+ x: Math.sin(r + a),
+ y: (Math.sin(r + 3*Math.PI/2 + a) / 8) * t,
+ z: (Math.cos(r + a) + 1) / 2,
+ scale: (Math.sin(r + Math.PI/2 + a) / 2) + 0.5
+ };
+ }
+ }
+ });
+
+ defaults = {
+ bearing: 0.0,
+ tilt: 0.0,
+ minZ: 100,
+ maxZ: 280,
+ minOpacity: 0.4,
+ maxOpacity: 1.0,
+ minScale: 0.4,
+ maxScale: 1.0,
+ duration: 600,
+ btnNext: null,
+ btnNextCallback: function() {},
+ btnPrev: null,
+ btnPrevCallback: function() {},
+ btnToggleAutoplay: null,
+ btnStartAutoplay: null,
+ btnStopAutoplay: null,
+ easing: "swing",
+ clickToFocus: true,
+ clickToFocusCallback: function() {},
+ focusBearing: 0.0,
+ shape: "lazySusan",
+ debug: false,
+ childSelector: "li",
+ startingChild: null,
+ reflect: false,
+ floatComparisonThreshold: 0.001,
+ autoplay: false,
+ autoplayDuration: 1000,
+ autoplayPauseOnHover: false,
+ autoplayCallback: function() {},
+ enableDrag: false,
+ dropDuration: 600,
+ dropEasing: "easeOutBounce",
+ dropAnimateTo: "nearest",
+ dropCallback: function() {},
+ dragAxis: "x",
+ dragFactor: 4,
+ triggerChildFocusEvents: true,
+ triggerChildBlurEvents: true
+ };
+
+ internalData = {
+ autoplayInterval: null,
+ autoplayResumeTimeout: null,
+ autoplayIsRunning: false,
+ autoplayResumeDuration: null,
+ animating: false,
+ childInFocus: -1,
+ touchMoveStartPosition: null,
+ stopAnimation: false,
+ lastAnimationStep: false
+ };
+
+ methods = {
+
+ // starters
+ // -----------------------------------------------------------------------
+
+ // init
+ // starts up roundabout
+ init: function(options, callback) {
+ var settings,
+ now = $.now();
+
+ options = (typeof options === "object") ? options : {};
+ callback = ($.isFunction(callback)) ? callback : function() {};
+ callback = ($.isFunction(options)) ? options : callback;
+ settings = $.extend({}, defaults, options, internalData);
+
+ return this
+ .each(function() {
+ // make options
+ var self = $(this),
+ childCount = self.children(settings.childSelector).length,
+ period = 360.0 / childCount,
+ startingChild = (settings.startingChild && settings.startingChild > (childCount - 1)) ? (childCount - 1) : settings.startingChild,
+ startBearing = (settings.startingChild === null) ? settings.bearing : 360 - (startingChild * period),
+ holderCSSPosition = (self.css("position") !== "static") ? self.css("position") : "relative";
+
+ self
+ .css({ // starting styles
+ padding: 0,
+ position: holderCSSPosition,
+ zIndex: settings.minZ
+ })
+ .addClass("roundabout-holder")
+ .data( // starting options
+ "roundabout",
+ $.extend(
+ {},
+ settings,
+ {
+ startingChild: startingChild,
+ bearing: startBearing,
+ oppositeOfFocusBearing: methods.normalize.apply(null, [settings.focusBearing - 180]),
+ autoplayLastMove: now,
+ dragBearing: startBearing,
+ period: period
+ }
+ )
+ );
+
+ // bind click-to-focus
+ if (settings.clickToFocus) {
+ self
+ .children(settings.childSelector)
+ .each(function(i) {
+ $(this)
+ .bind("click", function() {
+ var degrees = methods.getPlacement.apply(self, [i]);
+
+ if (!methods.isInFocus.apply(self, [degrees])) {
+ methods.stopAnimation.apply($(this));
+ if (!self.data("roundabout").animating) {
+ methods.animateAngleToFocus.apply(self, [degrees, self.data("roundabout").clickToFocusCallback]);
+ }
+ return false;
+ }
+ });
+ });
+ }
+
+ // bind next buttons
+ if (settings.btnNext) {
+ $(settings.btnNext)
+ .bind("click", function() {
+ if (!self.data("roundabout").animating) {
+ methods.animateToNextChild.apply(self, [self.data("roundabout").btnNextCallback]);
+ }
+ return false;
+ });
+ }
+
+ // bind previous buttons
+ if (settings.btnPrev) {
+ $(settings.btnPrev)
+ .bind("click", function() {
+ methods.animateToPreviousChild.apply(self, [self.data("roundabout").btnPrevCallback]);
+ return false;
+ });
+ }
+
+ // bind toggle autoplay buttons
+ if (settings.btnToggleAutoplay) {
+ $(settings.btnToggleAutoplay)
+ .bind("click", function() {
+ methods.toggleAutoplay.apply(self);
+ return false;
+ });
+ }
+
+ // bind start autoplay buttons
+ if (settings.btnStartAutoplay) {
+ $(settings.btnStartAutoplay)
+ .bind("click", function() {
+ methods.startOrResumeAutoplay.apply(self);
+ return false;
+ });
+ }
+
+ // bind stop autoplay buttons
+ if (settings.btnStopAutoplay) {
+ $(settings.btnStopAutoplay)
+ .bind("click", function() {
+ methods.pauseAutoplay.apply(self);
+ return false;
+ });
+ }
+
+ // autoplay pause on hover
+ if (settings.autoplayPauseOnHover) {
+ self
+ .bind("mouseenter", function() {
+ methods.pauseAutoplay.apply(self);
+ })
+ .bind("mouseleave", function() {
+ methods.resumeAutoplay.apply(self);
+ });
+ }
+
+ // drag and drop
+ if (settings.enableDrag) {
+ // on screen
+ if (!$.isFunction(self.drag)) {
+ if (settings.debug) {
+ alert("You do not have the drag plugin loaded.");
+ }
+ } else if (!$.isFunction(self.drop)) {
+ if (settings.debug) {
+ alert("You do not have the drop plugin loaded.")
+ }
+ } else {
+ self
+ .drag(function(e, properties) {
+ var data = self.data("roundabout"),
+ delta = (data.dragAxis.toLowerCase() === "x") ? "deltaX" : "deltaY";
+ methods.stopAnimation.apply(self);
+ methods.setBearing.apply(self, [data.dragBearing + properties[delta] / data.dragFactor]);
+ })
+ .drop(function(e) {
+ var data = self.data("roundabout"),
+ method = methods.getAnimateToMethod(data.dropAnimateTo);
+ methods.allowAnimation.apply(self);
+ methods[method].apply(self, [data.dropDuration, data.dropEasing, data.dropCallback]);
+ data.dragBearing = data.period * methods.getNearestChild.apply(self);
+ });
+ }
+
+ // on mobile
+ self
+ .each(function() {
+ var element = $(this).get(0),
+ data = $(this).data("roundabout"),
+ page = (data.dragAxis.toLowerCase() === "x") ? "pageX" : "pageY",
+ method = methods.getAnimateToMethod(data.dropAnimateTo);
+
+ // some versions of IE don't like this
+ if (element.addEventListener) {
+ element.addEventListener("touchstart", function(e) {
+ data.touchMoveStartPosition = e.touches[0][page];
+ }, false);
+
+ element.addEventListener("touchmove", function(e) {
+ var delta = (e.touches[0][page] - data.touchMoveStartPosition) / data.dragFactor;
+ e.preventDefault();
+ methods.stopAnimation.apply($(this));
+ methods.setBearing.apply($(this), [data.dragBearing + delta]);
+ }, false);
+
+ element.addEventListener("touchend", function(e) {
+ e.preventDefault();
+ methods.allowAnimation.apply($(this));
+ method = methods.getAnimateToMethod(data.dropAnimateTo);
+ methods[method].apply($(this), [data.dropDuration, data.dropEasing, data.dropCallback]);
+ data.dragBearing = data.period * methods.getNearestChild.apply($(this));
+ }, false);
+ }
+ });
+ }
+
+ // start children
+ methods.initChildren.apply(self, [callback]);
+ });
+ },
+
+
+ // initChildren
+ // applys settings to child elements, starts roundabout
+ initChildren: function(callback) {
+ var self = $(this),
+ data = self.data("roundabout");
+
+ callback = callback || function() {};
+
+ self.children(data.childSelector).each(function(i) {
+ var startWidth, startHeight, startFontSize,
+ degrees = methods.getPlacement.apply(self, [i]);
+
+ // check to see if starting sizes have been set already
+ // this allows roundabout to be re-laid-out
+ if (typeof $(this).data("roundabout") === "object") {
+ startWidth = $(this).data("roundabout").startWidth;
+ startHeight = $(this).data("roundabout").startHeight;
+ startFontSize = $(this).data("roundabout").startFontSize;
+ }
+
+ // apply classes and css first
+ $(this)
+ .addClass("roundabout-moveable-item")
+ .css("position", "absolute");
+
+ // now measure
+ $(this)
+ .data(
+ "roundabout",
+ {
+ startWidth: startWidth || $(this).width(),
+ startHeight: startHeight || $(this).height(),
+ startFontSize: startFontSize || parseInt($(this).css("font-size"), 10),
+ degrees: degrees,
+ backDegrees: methods.normalize.apply(null, [degrees - 180]),
+ childNumber: i,
+ currentScale: 1,
+ parent: self
+ }
+ );
+ });
+
+ methods.updateChildren.apply(self);
+
+ // start autoplay if necessary
+ if (data.autoplay) {
+ methods.startAutoplay.apply(self);
+ }
+
+ self.trigger('ready');
+ callback.apply(self);
+ return self;
+ },
+
+
+
+ // positioning
+ // -----------------------------------------------------------------------
+
+ // updateChildren
+ // move children elements into their proper locations
+ updateChildren: function() {
+ return this
+ .each(function() {
+ var self = $(this),
+ data = self.data("roundabout"),
+ inFocus = -1,
+ info = {
+ bearing: data.bearing,
+ tilt: data.tilt,
+ stage: {
+ width: Math.floor($(this).width() * 0.9),
+ height: Math.floor($(this).height() * 0.9)
+ },
+ animating: data.animating,
+ inFocus: data.childInFocus,
+ focusBearingRadian: methods.degToRad.apply(null, [data.focusBearing]),
+ shape: $.roundaboutShapes[data.shape] || $.roundaboutShapes[$.roundaboutShapes.def]
+ };
+
+ // calculations
+ info.midStage = {
+ width: info.stage.width / 2,
+ height: info.stage.height / 2
+ };
+
+ info.nudge = {
+ width: info.midStage.width + (info.stage.width * 0.05),
+ height: info.midStage.height + (info.stage.height * 0.05)
+ };
+
+ info.zValues = {
+ min: data.minZ,
+ max: data.maxZ,
+ diff: data.maxZ - data.minZ
+ };
+
+ info.opacity = {
+ min: data.minOpacity,
+ max: data.maxOpacity,
+ diff: data.maxOpacity - data.minOpacity
+ };
+
+ info.scale = {
+ min: data.minScale,
+ max: data.maxScale,
+ diff: data.maxScale - data.minScale
+ };
+
+ // update child positions
+ self.children(data.childSelector)
+ .each(function(i) {
+ if (methods.updateChild.apply(self, [$(this), info, i, function() { $(this).trigger('ready'); }]) && (!info.animating || data.lastAnimationStep)) {
+ inFocus = i;
+ $(this).addClass("roundabout-in-focus");
+ } else {
+ $(this).removeClass("roundabout-in-focus");
+ }
+ });
+
+ if (inFocus !== info.inFocus) {
+ // blur old child
+ if (data.triggerChildBlurEvents) {
+ self.children(data.childSelector)
+ .eq(info.inFocus)
+ .trigger("blur");
+ }
+
+ data.childInFocus = inFocus;
+
+ if (data.triggerChildFocusEvents && inFocus !== -1) {
+ // focus new child
+ self.children(data.childSelector)
+ .eq(inFocus)
+ .trigger("focus");
+ }
+ }
+
+ self.trigger("childrenUpdated");
+ });
+ },
+
+
+ // updateChild
+ // repositions a child element into its new position
+ updateChild: function(childElement, info, childPos, callback) {
+ var factors,
+ self = this,
+ child = $(childElement),
+ data = child.data("roundabout"),
+ out = [],
+ rad = methods.degToRad.apply(null, [(360.0 - data.degrees) + info.bearing]);
+
+ callback = callback || function() {};
+
+ // adjust radians to be between 0 and Math.PI * 2
+ rad = methods.normalizeRad.apply(null, [rad]);
+
+ // get factors from shape
+ factors = info.shape(rad, info.focusBearingRadian, info.tilt);
+
+ // correct
+ factors.scale = (factors.scale > 1) ? 1 : factors.scale;
+ factors.adjustedScale = (info.scale.min + (info.scale.diff * factors.scale)).toFixed(4);
+ factors.width = (factors.adjustedScale * data.startWidth).toFixed(4);
+ factors.height = (factors.adjustedScale * data.startHeight).toFixed(4);
+
+ // update item
+ child
+ .css({
+ left: ((factors.x * info.midStage.width + info.nudge.width) - factors.width / 2.0).toFixed(0) + "px",
+ top: ((factors.y * info.midStage.height + info.nudge.height) - factors.height / 2.0).toFixed(0) + "px",
+ width: factors.width + "px",
+ height: factors.height + "px",
+ opacity: (info.opacity.min + (info.opacity.diff * factors.scale)).toFixed(2),
+ zIndex: Math.round(info.zValues.min + (info.zValues.diff * factors.z)),
+ fontSize: (factors.adjustedScale * data.startFontSize).toFixed(0) + "px"
+ });
+ data.currentScale = factors.adjustedScale;
+
+ // for debugging purposes
+ if (self.data("roundabout").debug) {
+ out.push("
");
+ out.push("Child " + childPos + "
");
+ out.push("left: " + child.css("left") + "
");
+ out.push("top: " + child.css("top") + "
");
+ out.push("width: " + child.css("width") + "
");
+ out.push("opacity: " + child.css("opacity") + "
");
+ out.push("height: " + child.css("height") + "
");
+ out.push("z-index: " + child.css("z-index") + "
");
+ out.push("font-size: " + child.css("font-size") + "
");
+ out.push("scale: " + child.data("roundabout").currentScale);
+ out.push("
");
+
+ child.html(out.join(""));
+ }
+
+ // trigger event
+ child.trigger("reposition");
+
+ // callback
+ callback.apply(self);
+
+ return methods.isInFocus.apply(self, [data.degrees]);
+ },
+
+
+
+ // manipulation
+ // -----------------------------------------------------------------------
+
+ // setBearing
+ // changes the bearing of the roundabout
+ setBearing: function(bearing, callback) {
+ callback = callback || function() {};
+ bearing = methods.normalize.apply(null, [bearing]);
+
+ this
+ .each(function() {
+ var diff, lowerValue, higherValue,
+ self = $(this),
+ data = self.data("roundabout"),
+ oldBearing = data.bearing;
+
+ // set bearing
+ data.bearing = bearing;
+ self.trigger("bearingSet");
+ methods.updateChildren.apply(self);
+
+ // not animating? we're done here
+ diff = Math.abs(oldBearing - bearing);
+ if (!data.animating || diff > 180) {
+ return;
+ }
+
+ // check to see if any of the children went through the back
+ diff = Math.abs(oldBearing - bearing);
+ self.children(data.childSelector).each(function(i) {
+ var eventType;
+
+ if (methods.isChildBackDegreesBetween.apply($(this), [bearing, oldBearing])) {
+ eventType = (oldBearing > bearing) ? "Clockwise" : "Counterclockwise";
+ $(this).trigger("move" + eventType + "ThroughBack");
+ }
+ });
+ });
+
+ // call callback if one was given
+ callback.apply(this);
+ return this;
+ },
+
+
+ // adjustBearing
+ // change the bearing of the roundabout by a given degree
+ adjustBearing: function(delta, callback) {
+ callback = callback || function() {};
+ if (delta === 0) {
+ return this;
+ }
+
+ this
+ .each(function() {
+ methods.setBearing.apply($(this), [$(this).data("roundabout").bearing + delta]);
+ });
+
+ callback.apply(this);
+ return this;
+ },
+
+
+ // setTilt
+ // changes the tilt of the roundabout
+ setTilt: function(tilt, callback) {
+ callback = callback || function() {};
+
+ this
+ .each(function() {
+ $(this).data("roundabout").tilt = tilt;
+ methods.updateChildren.apply($(this));
+ });
+
+ // call callback if one was given
+ callback.apply(this);
+ return this;
+ },
+
+
+ // adjustTilt
+ // changes the tilt of the roundabout
+ adjustTilt: function(delta, callback) {
+ callback = callback || function() {};
+
+ this
+ .each(function() {
+ methods.setTilt.apply($(this), [$(this).data("roundabout").tilt + delta]);
+ });
+
+ callback.apply(this);
+ return this;
+ },
+
+
+
+ // animation
+ // -----------------------------------------------------------------------
+
+ // animateToBearing
+ // animates the roundabout to a given bearing, all animations come through here
+ animateToBearing: function(bearing, duration, easing, passedData, callback) {
+ var now = $.now(),
+ callback = callback || function() {};
+
+ // find callback function in arguments
+ if ($.isFunction(passedData)) {
+ callback = passedData;
+ passedData = null;
+ } else if ($.isFunction(easing)) {
+ callback = easing;
+ easing = null;
+ } else if ($.isFunction(duration)) {
+ callback = duration;
+ duration = null;
+ }
+
+ this
+ .each(function() {
+ var timer, easingFn, newBearing,
+ self = $(this),
+ data = self.data("roundabout"),
+ thisDuration = (!duration) ? data.duration : duration,
+ thisEasingType = (easing) ? easing : data.easing || "swing";
+
+ // is this your first time?
+ if (!passedData) {
+ passedData = {
+ timerStart: now,
+ start: data.bearing,
+ totalTime: thisDuration
+ };
+ }
+
+ // update the timer
+ timer = now - passedData.timerStart;
+
+ if (data.stopAnimation) {
+ methods.allowAnimation.apply(self);
+ data.animating = false;
+ return;
+ }
+
+ // we need to animate more
+ if (timer < thisDuration) {
+ if (!data.animating) {
+ self.trigger("animationStart");
+ }
+
+ data.animating = true;
+
+ if (typeof $.easing.def === "string") {
+ easingFn = $.easing[thisEasingType] || $.easing[$.easing.def];
+ newBearing = easingFn(null, timer, passedData.start, bearing - passedData.start, passedData.totalTime);
+ } else {
+ newBearing = $.easing[thisEasingType]((timer / passedData.totalTime), timer, passedData.start, bearing - passedData.start, passedData.totalTime);
+ }
+
+ newBearing = methods.normalize.apply(null, [newBearing]);
+ data.dragBearing = newBearing;
+
+ methods.setBearing.apply(self, [newBearing, function() {
+ setTimeout(function() { // done with a timeout so that each step is displayed
+ methods.animateToBearing.apply(self, [bearing, thisDuration, thisEasingType, passedData, callback]);
+ }, 0);
+ }]);
+
+ // we're done animating
+ } else {
+ if (data.animating) {
+ self.trigger("animationEnd");
+ }
+
+ data.lastAnimationStep = true;
+
+ bearing = methods.normalize.apply(null, [bearing]);
+ methods.setBearing.apply(self, [bearing]);
+ data.animating = false;
+ data.lastAnimationStep = false;
+ data.dragBearing = bearing;
+
+ callback.apply(self);
+ }
+ });
+
+ return this;
+ },
+
+
+ // animateToNearbyChild
+ // animates roundabout to a nearby child
+ animateToNearbyChild: function(passedArgs, which) {
+ var duration = passedArgs[0],
+ easing = passedArgs[1],
+ callback = passedArgs[2] || function() {};
+
+ // find callback
+ if ($.isFunction(easing)) {
+ callback = easing;
+ easing = null;
+ } else if ($.isFunction(duration)) {
+ callback = duration;
+ duration = null;
+ }
+
+ return this
+ .each(function() {
+ var j, range,
+ self = $(this),
+ data = self.data("roundabout"),
+ bearing = (!data.reflect) ? data.bearing % 360 : data.bearing,
+ length = self.children(data.childSelector).length;
+
+ if (!data.animating) {
+ // reflecting, not moving to previous || not reflecting, moving to next
+ if ((data.reflect && which === "previous") || (!data.reflect && which === "next")) {
+ // slightly adjust for rounding issues
+ bearing = (Math.abs(bearing) < data.floatComparisonThreshold) ? 360 : bearing;
+
+ // clockwise
+ for (j = 0; j < length; j += 1) {
+ range = {
+ lower: (data.period * j),
+ upper: (data.period * (j + 1))
+ };
+ range.upper = (j === length - 1) ? 360 : range.upper;
+
+ if (bearing <= Math.ceil(range.upper) && bearing >= Math.floor(range.lower)) {
+ if (length === 2 && bearing === 360) {
+ methods.animateToDelta.apply(self, [-180, duration, easing, callback]);
+ } else {
+ methods.animateAngleToFocus.apply(self, [range.lower, duration, easing, callback]);
+ }
+ break;
+ }
+ }
+ } else {
+ // slightly adjust for rounding issues
+ bearing = (Math.abs(bearing) < data.floatComparisonThreshold || 360 - Math.abs(bearing) < data.floatComparisonThreshold) ? 0 : bearing;
+
+ // counterclockwise
+ for (j = length - 1; j >= 0; j -= 1) {
+ range = {
+ lower: data.period * j,
+ upper: data.period * (j + 1)
+ };
+ range.upper = (j === length - 1) ? 360 : range.upper;
+
+ if (bearing >= Math.floor(range.lower) && bearing < Math.ceil(range.upper)) {
+ if (length === 2 && bearing === 360) {
+ methods.animateToDelta.apply(self, [180, duration, easing, callback]);
+ } else {
+ methods.animateAngleToFocus.apply(self, [range.upper, duration, easing, callback]);
+ }
+ break;
+ }
+ }
+ }
+ }
+ });
+ },
+
+
+ // animateToNearestChild
+ // animates roundabout to the nearest child
+ animateToNearestChild: function(duration, easing, callback) {
+ callback = callback || function() {};
+
+ // find callback
+ if ($.isFunction(easing)) {
+ callback = easing;
+ easing = null;
+ } else if ($.isFunction(duration)) {
+ callback = duration;
+ duration = null;
+ }
+
+ return this
+ .each(function() {
+ var nearest = methods.getNearestChild.apply($(this));
+ methods.animateToChild.apply($(this), [nearest, duration, easing, callback]);
+ });
+ },
+
+
+ // animateToChild
+ // animates roundabout to a given child position
+ animateToChild: function(childPosition, duration, easing, callback) {
+ callback = callback || function() {};
+
+ // find callback
+ if ($.isFunction(easing)) {
+ callback = easing;
+ easing = null;
+ } else if ($.isFunction(duration)) {
+ callback = duration;
+ duration = null;
+ }
+
+ return this
+ .each(function() {
+ var child,
+ self = $(this),
+ data = self.data("roundabout");
+
+ if (data.childInFocus !== childPosition && !data.animating) {
+ child = self.children(data.childSelector).eq(childPosition);
+ methods.animateAngleToFocus.apply(self, [child.data("roundabout").degrees, duration, easing, callback]);
+ }
+ });
+ },
+
+
+ // animateToNextChild
+ // animates roundabout to the next child
+ animateToNextChild: function(duration, easing, callback) {
+ return methods.animateToNearbyChild.apply(this, [arguments, "next"]);
+ },
+
+
+ // animateToPreviousChild
+ // animates roundabout to the preious child
+ animateToPreviousChild: function(duration, easing, callback) {
+ return methods.animateToNearbyChild.apply(this, [arguments, "previous"]);
+ },
+
+
+ // animateToDelta
+ // animates roundabout to a given delta (in degrees)
+ animateToDelta: function(degrees, duration, easing, callback) {
+ callback = callback || function() {};
+
+ // find callback
+ if ($.isFunction(easing)) {
+ callback = easing;
+ easing = null;
+ } else if ($.isFunction(duration)) {
+ callback = duration;
+ duration = null;
+ }
+
+ return this
+ .each(function() {
+ var delta = $(this).data("roundabout").bearing + degrees;
+ methods.animateToBearing.apply($(this), [delta, duration, easing, callback]);
+ });
+ },
+
+
+ // animateAngleToFocus
+ // animates roundabout to bring a given angle into focus
+ animateAngleToFocus: function(degrees, duration, easing, callback) {
+ callback = callback || function() {};
+
+ // find callback
+ if ($.isFunction(easing)) {
+ callback = easing;
+ easing = null;
+ } else if ($.isFunction(duration)) {
+ callback = duration;
+ duration = null;
+ }
+
+ return this
+ .each(function() {
+ var delta = $(this).data("roundabout").bearing - degrees;
+ delta = (Math.abs(360 - delta) < Math.abs(delta)) ? 360 - delta : -delta;
+ delta = (delta > 180) ? -(360 - delta) : delta;
+
+ if (delta !== 0) {
+ methods.animateToDelta.apply($(this), [delta, duration, easing, callback]);
+ }
+ });
+ },
+
+
+ // stopAnimation
+ // if an animation is currently in progress, stop it
+ stopAnimation: function() {
+ return this
+ .each(function() {
+ $(this).data("roundabout").stopAnimation = true;
+ });
+ },
+
+
+ // allowAnimation
+ // clears the stop-animation hold placed by stopAnimation
+ allowAnimation: function() {
+ return this
+ .each(function() {
+ $(this).data("roundabout").stopAnimation = false;
+ });
+ },
+
+
+
+ // autoplay
+ // -----------------------------------------------------------------------
+
+ // startAutoplay
+ // starts autoplaying this roundabout
+ startAutoplay: function(callback) {
+ return this
+ .each(function() {
+ var self = $(this),
+ data = self.data("roundabout");
+
+ callback = callback || data.autoplayCallback || function() {};
+
+ clearTimeout(data.autoplayResumeDuration);
+ clearInterval(data.autoplayInterval);
+ data.autoplayInterval = setInterval(function() {
+ data.autoplayLastMove = $.now();
+ methods.animateToNextChild.apply(self, [callback]);
+ }, data.autoplayDuration);
+ data.autoplayIsRunning = true;
+ self.trigger("autoplayStart");
+ });
+ },
+
+
+ // stopAutoplay
+ // stops autoplaying this roundabout
+ stopAutoplay: function() {
+ return this
+ .each(function() {
+ clearTimeout($(this).data("roundabout").autoplayResumeDuration);
+ clearInterval($(this).data("roundabout").autoplayInterval);
+ $(this).data("roundabout").autoplayInterval = null;
+ $(this).data("roundabout").autoplayIsRunning = false;
+ $(this).trigger("autoplayStop");
+ });
+ },
+
+
+ // pauseAutoplay
+ // pauses autoplay temporarily
+ pauseAutoplay: function() {
+ return this
+ .each(function() {
+ var self = $(this),
+ data = self.data("roundabout");
+
+ clearTimeout(data.autoplayResumeTimeout);
+ clearInterval(data.autoplayInterval);
+ data.autoplayInterval = null;
+ data.autoplayResumeTimeout = null;
+ data.autoplayResumeDuration = $.now() - data.autoplayLastMove;
+ self.trigger("autoplayPause");
+ });
+ },
+
+
+ // resumeAutoplay
+ // resumes autoplay if paused
+ resumeAutoplay: function(callback) {
+ return this
+ .each(function() {
+ var self = $(this),
+ data = self.data("roundabout");
+
+ callback = callback || data.autoplayCallback || function() {};
+
+ if (data.autoplayInterval === null && !methods.isAutoplaying.apply(self)) {
+ // autoplay is not running
+ return;
+ }
+
+ data.autoplayResumeTimeout = setTimeout(function() {
+ methods.startAutoplay.apply(self);
+ methods.animateToNextChild.apply(self, [callback]);
+ }, data.autoplayResumeDuration);
+
+ data.autoplayLastMove = $.now();
+ data.autoplayResumeDuration = null;
+ self.trigger("autoplayResume");
+ });
+ },
+
+
+ // startOrResumeAutoplay
+ // starts or resumes autoplay if paused or stopped
+ startOrResumeAutoplay: function(callback) {
+ return this
+ .each(function() {
+ callback = callback || $(this).data("roundabout").autoplayCallback || function() {};
+
+ if (!methods.isAutoplaying.apply($(this))) {
+ // stopped, start it
+ methods.startAutoplay.apply($(this));
+ } else if (!$(this).data("roundabout").autoplayInterval) {
+ // paused, resume it
+ methods.resumeAutoplay.apply($(this));
+ }
+ });
+ },
+
+
+ // toggleAutoplay
+ // toggles autoplay pause/resume
+ toggleAutoplay: function(callback) {
+ return this
+ .each(function() {
+ var self = $(this),
+ data = self.data("roundabout");
+
+ callback = callback || data.autoplayCallback || function() {};
+
+ if (!methods.isAutoplaying.apply($(this))) {
+ // start autoplay
+ methods.startAutoplay.apply($(this), [callback]);
+ } else {
+ if (!data.autoplayInterval) {
+ // currently paused
+ methods.resumeAutoplay.apply($(this), [callback]);
+ } else {
+ // currently resumed
+ methods.pauseAutoplay.apply($(this), [callback]);
+ }
+ }
+ });
+ },
+
+
+ // isAutoplaying
+ // is this roundabout currently autoplaying?
+ isAutoplaying: function() {
+ return (this.data("roundabout").autoplayIsRunning);
+ },
+
+
+ // changeAutoplayDuration
+ // stops the autoplay, changes the duration, restarts autoplay
+ changeAutoplayDuration: function(duration) {
+ return this
+ .each(function() {
+ var self = $(this),
+ data = self.data("roundabout");
+
+ data.autoplayDuration = duration;
+
+ if (methods.isAutoplaying.apply(self)) {
+ methods.stopAutoplay.apply(self);
+ setTimeout(function() {
+ methods.startAutoplay.apply(self);
+ }, 10);
+ }
+ });
+ },
+
+
+
+ // helpers
+ // -----------------------------------------------------------------------
+
+ // normalize
+ // regulates degrees to be >= 0.0 and < 360
+ normalize: function(degrees) {
+ var inRange = degrees % 360.0;
+ return (inRange < 0) ? 360 + inRange : inRange;
+ },
+
+
+ // normalizeRad
+ // regulates radians to be >= 0 and < Math.PI * 2
+ normalizeRad: function(radians) {
+ while (radians < 0) {
+ radians += (Math.PI * 2);
+ }
+
+ while (radians > (Math.PI * 2)) {
+ radians -= (Math.PI * 2);
+ }
+
+ return radians;
+ },
+
+
+ // isChildBackDegreesBetween
+ // checks that a given child's backDegrees is between two values
+ isChildBackDegreesBetween: function(value1, value2) {
+ var backDegrees = $(this).data("roundabout").backDegrees;
+
+ if (value1 > value2) {
+ return (backDegrees >= value2 && backDegrees < value1);
+ } else {
+ return (backDegrees < value2 && backDegrees >= value1);
+ }
+ },
+
+
+ // getAnimateToMethod
+ // takes a user-entered option and maps it to an animation method
+ getAnimateToMethod: function(effect) {
+ effect = effect.toLowerCase();
+
+ if (effect === "next") {
+ return "animateToNextChild";
+ } else if (effect === "previous") {
+ return "animateToPreviousChild";
+ }
+
+ // default selection
+ return "animateToNearestChild";
+ },
+
+
+ // relayoutChildren
+ // lays out children again with new contextual information
+ relayoutChildren: function() {
+ return this
+ .each(function() {
+ var self = $(this),
+ settings = $.extend({}, self.data("roundabout"));
+
+ settings.startingChild = self.data("roundabout").childInFocus;
+ methods.init.apply(self, [settings]);
+ });
+ },
+
+
+ // getNearestChild
+ // gets the nearest child from the current bearing
+ getNearestChild: function() {
+ var self = $(this),
+ data = self.data("roundabout"),
+ length = self.children(data.childSelector).length;
+
+ if (!data.reflect) {
+ return ((length) - (Math.round(data.bearing / data.period) % length)) % length;
+ } else {
+ return (Math.round(data.bearing / data.period) % length);
+ }
+ },
+
+
+ // degToRad
+ // converts degrees to radians
+ degToRad: function(degrees) {
+ return methods.normalize.apply(null, [degrees]) * Math.PI / 180.0;
+ },
+
+
+ // getPlacement
+ // returns the starting degree for a given child
+ getPlacement: function(child) {
+ var data = this.data("roundabout");
+ return (!data.reflect) ? 360.0 - (data.period * child) : data.period * child;
+ },
+
+
+ // isInFocus
+ // is this roundabout currently in focus?
+ isInFocus: function(degrees) {
+ var diff,
+ self = this,
+ data = self.data("roundabout"),
+ bearing = methods.normalize.apply(null, [data.bearing]);
+
+ degrees = methods.normalize.apply(null, [degrees]);
+ diff = Math.abs(bearing - degrees);
+
+ // this calculation gives a bit of room for javascript float rounding
+ // errors, it looks on both 0deg and 360deg ends of the spectrum
+ return (diff <= data.floatComparisonThreshold || diff >= 360 - data.floatComparisonThreshold);
+ }
+ };
+
+
+ // start the plugin
+ $.fn.roundabout = function(method) {
+ if (methods[method]) {
+ return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
+ } else if (typeof method === "object" || $.isFunction(method) || !method) {
+ return methods.init.apply(this, arguments);
+ } else {
+ $.error("Method " + method + " does not exist for jQuery.roundabout.");
+ }
+ };
+})(jQuery);
\ No newline at end of file