/*!
* jQuery Roundrr Plugin (2010)
*
* This version written by Addy Osmani extending Nirvana Tikku's RadMenu
*
* Roundrr is a plugin for plotting interactive image or multimedia content
* around a circle. It is based on RadMenu with an extended model for events
* occurring in the pre and post animation phases. It also contains further
* configuration for automated playback of a wheel of content and support for
* 'pick' interfaces which allow you to perform an action when an object
* falls between two other objects of fixed known position.
*
* For further documentation please see addyosmani.com or Roundrr on github
* at http://github.com/legacye/jquery-roundrr.
*
* This is an initial release and further forks/bug-patches are more than welcome
* for any behaviour you feel may require some fixing or improvement.
*
* For further information about plotting items in a circle or oval outside of this
* plugin, I recommend checking out my minimalist javascript library ShapeLib.
*
*/
;(function($){
var roundrrwheel = ".roundrrwheel",
OPTS = "options"+roundrrwheel,
PREVOPTS = "prevoptions"+roundrrwheel;
// private :: defaults
var defaults = {
mode: 'standard',
autoplay: false,
autoplayDirection: 'clockwise',
autoplayDuration: 5000,
centerClass: "roundrr_center",
listClass: "list",
itemClass: "item",
activeItemClass: "active",
selectEvent: null, // click, mouseenter etc
onSelect: function($selected){},
onImageFocus: function($selected){}, //when the image is under the pick/viewer
onImageBlur: function($selected){}, //when the image is not under the pick/viewer
onNextBegin: function($selected){}, //called before the animation moves to next
onPrevBegin: function($selected){}, //called before the animation moves to prev
radius: 10, // in pixels
animationEffect:1,
initialScale: 1,
angleOffset: 0, // in radians
centerImageSrc: 'images/placeholder2.png',
centerX: 0,
centerY: 0,
requiredLeftA:'-55.1904px', // override for positioning: main image n-1's left position
requiredTopA:'-143.253px', // main image n-1's top
requiredLeftB:'113.19px', //main image n+1's left
requiredTopB:'-143.253px', //main image n+1's top
animSpeed: 500,
scaleAnimSpeed: 400,
scaleAnimOpts: {},
onAnimationComplete: function($m){},
onShow: function($items){$items.show();},
onHide: function($items){$items.hide();}
};
// set default valeus
$.roundrrwheel = {
container: {
clz: "roundrr_div",
itemClz: "roundrr_div_item",
html: "
",
css: { "position": "relative" }
}
};
/**
* jQuery roundrrwheel Plugin
* @params
* > input, dealt with by type
* if empty - assumes initialization
* if object - assumes initialization
* if string - assumes trigger method
* if number - select a particular menu item
*/
$.fn.roundrrwheel = function(input, param){
try {
var $this = $(this);
var type = typeof input;
if(arguments.length==0 || type=="object")
return init($this, input);
else if(type=="string")
return input=="items" ?
$this.triggerHandler(input+roundrrwheel) :
$this.trigger(input+roundrrwheel, param||null);
else if(type=="number"){
return $this.trigger("select"+roundrrwheel,input);
}
} catch (e){ return "error : "+e; }
};
/**
* private :: init fn
* @params
* $menu - the jQuery obj / array w/ menu target
* opts - options object, to be merged with defaults
*/
function init($menu, opts){
var o = $.extend({}, defaults, opts);
//inner-wrap the center class
jQuery('.' + o.centerClass).wrapInner("
");
return $menu.each(function(m){
var $this = $(this);
var $list = $this.find("."+o.listClass);
$list.find("."+o.itemClass).hide(); // ensure its hidden
// set the options within the data for the elem & bind evts
$this.data(OPTS, updateRadius(o, o.initialScale, o.radius));
for(e in MENU) $this.bind(e+roundrrwheel, $this, MENU[e]);
});
};
/**
* selectMenuitem
* @param
* evt - the event object
* triggers select event on roundrrwheel container
* using the index of the 'target object'
*/
function selectMenuitem(evt){
var $this = $(this);
var $element = $(evt.target);
var container = $.roundrrwheel.container;
if(!$element.hasClass(container.itemClz))
$element = $element.closest("."+container.itemClz);
var isInNested = $element.parents("."+container.itemClz).length>0;
var index = $element.index();
if(!isInNested)$this.parents("."+container.clz).roundrrwheel(index);
else $this.roundrrwheel(index);
cancelBubble(evt);
};
/**
* cancel event bubbling - x-browser friendly
* @param
* evt - the event object
*/
function cancelBubble(evt){
if(!$.support.opacity) window.event.cancelBubble = true;
else evt.stopPropagation();
};
/**
* All the MENU events to be bound to the radial menu
*/
var MENU = {
show: function(evt, fn){ // fn = user input onshow
var $m = getMenu(evt);
var container = $.roundrrwheel.container;
// clear any existing radial menus within the menu
$m.menu.find("."+container.clz).remove();
// grab the desired menu items to be used in building the roundrrwheel
var $menuitems = $m.menu.find("."+$m.opts.itemClass);
// create a div that will be the roundrrwheel & create the HTML for the items
var $radialMenu = $(container.html)
.addClass(container.clz).css(container.css)
.html(buildMenuHTML($menuitems, $m.opts));
// assign a selection event if the user has specified something
var $menuitems = $radialMenu.find("."+container.itemClz);
if($m.opts.selectEvent!=null)
$menuitems.bind($m.opts.selectEvent,selectMenuitem);
// append the roundrrwheel items inside the menu
$radialMenu.appendTo($m.menu);
//
var doAutoplay = $m.opts.autoplay;
if(doAutoplay)
{
switch($m.opts.autoplayDirection)
{
case 'clockwise':
setInterval(function()
{
switchItems($m, $m.raditems().length-1, 0, $m.opts.animationEffect, 'next');
}, $m.opts.autoplayDuration);
break;
case 'anticlockwise':
setInterval(function()
{
switchItems($m, 0, $m.raditems().length-1, 1, 'prev');
}, $m.opts.autoplayDuration);
break;
}
}
if(typeof(fn) == "function") fn($menuitems);
else $m.opts.onShow($menuitems); // user can do what they want
cancelBubble(evt);
},
hide: function(evt){
var $m = getMenu(evt);
// remove the roundrrwheel that was built and appended inside the menu
var $menu = $m.menu.find("."+$.roundrrwheel.container.clz);
$m.opts.onHide($menu.find("."+$.roundrrwheel.container.itemClz));
$menu.remove();
cancelBubble(evt);
},
select: function(evt, selectIndex){
var $m = getMenu(evt);
// with a specific index specified, grab the item
var $selected = $($m.raditems().get(selectIndex));
// remove the active class on the elements siblings
$selected.siblings().removeClass($m.opts.activeItemClass);
// add the active class on the selected item
$selected.addClass($m.opts.activeItemClass);
// pass the selected item to a customizable function
$m.opts.onSelect($selected);
cancelBubble(evt);
},
next: function(evt){ // clockwise
var $m = getMenu(evt);
$m.opts.onNextBegin($m);
// switch the first and last items and then animate
switchItems($m, $m.raditems().length-1, 0, $m.opts.animationEffect, 'next');
},
prev: function(evt){ // anticlockwise
var $m = getMenu(evt);
$m.opts.onPrevBegin($m);
// switch the last and first items and then animate
switchItems($m, 0, $m.raditems().length-1, 1, 'prev');
},
shuffle: function(evt,rndOffset){
var $m = getMenu(evt);
var len = $m.raditems().length;
// swap some random item with another random item, and add some shuffling effects
switchItems($m, rnd(len), rnd(len), rnd(rndOffset||15));
},
destroy: function(evt){
var $m = getMenu(evt);
$m.menu.data(OPTS, null).data(PREVOPTS, null).unbind(roundrrwheel);
return $m.menu;
},
items: function(evt){return getMenu(evt).raditems();}
};
function updateRadius(opts, radius, factor){
return $.extend({},opts,{radius:(factor*radius)});
};
// random int offset
function rnd(i){return parseInt(Math.random()*i);};
/**
* getMenu
* @params
* evt - the event object
* @return
* Object
* > menu - jQueryfied menu
* > opts - the options
* > raditems - the radial menu items
*/
function getMenu(evt){
var $menu = evt.data;
return {
menu: $menu,
opts: $menu.data(OPTS),
raditems: function(){
// you will want to trigger raditems() if the contents get modified
return $menu.find("."+$.roundrrwheel.container.itemClz);
}
};
};
/**
* switchItems
* @params
* $m - the menu package
* remove - the index of the menuitem to replace in the swap
* add - the index of the menuitem to use in the swap (a placeholder)
* direction - the direction in which the user wishes to animate 'prev' (right)
* or 'next' (left)
*/
function switchItems($m, remove, add, posOffset, direction){
if(remove==add) add = remove - 1; // ensure that we don't lose any items
var $remove = $($m.raditems()[remove]); // grab the replacement item
var toAddto = $m.raditems()[add]; // grab the placeholder
// insertion is dependent on index of items
if(remove>add) $remove.insertBefore(toAddto);
else $remove.insertAfter(toAddto);
animateWheel($m,posOffset, direction); // posOffset = 5:neat, 10:fireworksesque, 15:subtleish
};
/**
* buildMenuHTML - returns string instead of objects
* for performance
* @params
* $menuitems - the jQueryified menu items
* opts - the radial menu's options
* @return
* String
* > each item is wrapped with an
* absolute positioned div at an
* offset determined by it's location
* on a circle
*/
function buildMenuHTML($menuitems, opts){
var ret = "";
$menuitems.each(function(i){ // for each item we will want to build the HTML
var $this = $(this);
var coords = getCoords(i+1, $menuitems.length, opts); // each item has a position
ret += "";
ret += $this.html(); // append the HTML _within_ the user's defined 'item'
ret += "
";
});
return ret;
};
/**
* getCoords - returns coordinates for menuitems
* @params
* idx - the instance index (1st, 2nd, 3rd, etc..)
* num - the number of menuitems to spread
* opts - the options provided by the user customizations
* @return
* Object - (x, y) coords
*/
function getCoords(idx, num, opts){
var radius = opts.radius; // user specified radius
var angleOffset = opts.angleOffset; // provide flexibility of angle
var angle = 2 * Math.PI * (parseFloat(idx/num)); // radians
// assuming: hypotenuse (hyp) = radius
//
// opposite |\ hypotenuse
// | \
// 90deg |__\ (*theta* - angle)
// adjacent
//
// x-axis offset: cos(theta) = adjacent / hypotenuse
// ==> adjacent = left = cos(theta) * radius
// y-axis offset: sin(theta) = opposite / hypotenuse
// ==> opposite = top = sin(theta) * radius
var l = opts.centerX + (Math.cos(angle + angleOffset) * radius), // "left"
t = opts.centerY + (Math.sin(angle + angleOffset) * radius); // "top"
return {x: l, y: t}; // return the x,y coords
};
/**
* animateWheel - performs animation
* @params
* $m - object holding menu & options
* posOffset - the position offset for the initial menuitem
* direction - the direction in which the user wishes to animate 'prev' (right)
* or 'next' (left)
*/
function animateWheel($m, posOffset, direction){
// get the menu from the $m menu package
var $menuitems = $m.menu.find("."+$.roundrrwheel.container.itemClz);
// get a handle on the number of items
var len = $menuitems.length;
// for each item, we're going to animate left/top attributes
/*Retrieve the desired positions of the n+1,n-1 elements if custom-values provided */
var reqLeftA = Math.floor($m.opts.requiredLeftA.replace('px',''));
var reqTopA = Math.floor($m.opts.requiredTopA.replace('px',''));
var reqLeftB = Math.floor($m.opts.requiredLeftB.replace('px',''));
var reqTopB = Math.floor($m.opts.requiredTopB.replace('px',''));
$menuitems.each(function(i)
{
var $this = $(this);
//left and top coordinates of this particular element
var thisLeft = Math.floor($this.css('left').replace('px',''));
var thisTop = Math.floor($this.css('top').replace('px',''));
if($m.opts.mode == 'standard')
{
if(i==0)
{
$m.opts.onImageFocus($this);
$this.find('img').addClass('selected');
}else{
$this.find('img').removeClass('selected');
}
}
// establish the new coordinates with a customizable offset; len*(Math.PI+(Math.sqrt(5)))
var coords = getCoords(i+posOffset, len, $m.opts);
if($m.opts.mode == 'pick')
{
//attempt to override custom case positions by extracting them from the
//array
switch(i)
{
case 2:
reqLeftA = Math.floor(coords.x);
reqTopA = Math.floor(coords.y);
break;
case 4:
reqLeftB = Math.floor(coords.x);
reqTopB = Math.floor(coords.y);
break;
}
//effectively check the the switch of image position for n+1, n-1
//for the current element and handle accordingly.
switch(direction)
{
case 'next':
if( (thisLeft == reqLeftA && thisTop == reqTopA))
{
$m.opts.onImageFocus($this);
}
else{
$m.opts.onImageBlur($this);
}
break;
case 'prev':
if( (thisLeft == reqLeftB && thisTop == reqTopB))
{
$m.opts.onImageFocus($this);
}
else{
$m.opts.onImageBlur($this);
}
break;
}
}
// playing with this is fun - this basically just
// performs the animation with new coordinates
$this.animate({
left: coords.x, top: coords.y
}, $m.opts.animSpeed, i==(len-1)?function(){
// allow the user to do something after completing an animation
$m.opts.onAnimationComplete($m);
}:undefined);
});
};
})(jQuery);