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