/*! * 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);