├── demo ├── test-images │ ├── roto-test-01.jpg │ ├── roto-test-02.jpg │ ├── roto-test-03.jpg │ ├── roto-test-04.jpg │ ├── roto-test-05.jpg │ ├── roto-test-06.jpg │ ├── roto-test-07.jpg │ ├── roto-test-08.jpg │ ├── roto-test-09.jpg │ ├── roto-test-10.jpg │ ├── roto-test-11.jpg │ ├── roto-test-12.jpg │ ├── roto-test-13.jpg │ ├── roto-test-14.jpg │ ├── roto-test-15.jpg │ └── roto-test-16.jpg ├── roto.css ├── index.html └── jquery.roto.min.js ├── .gitmodules ├── LICENSE.txt ├── Makefile ├── test.html ├── README.md ├── jquery.roto.min.js └── src └── jquery.roto.js /demo/test-images/roto-test-01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdallasgray/roto/HEAD/demo/test-images/roto-test-01.jpg -------------------------------------------------------------------------------- /demo/test-images/roto-test-02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdallasgray/roto/HEAD/demo/test-images/roto-test-02.jpg -------------------------------------------------------------------------------- /demo/test-images/roto-test-03.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdallasgray/roto/HEAD/demo/test-images/roto-test-03.jpg -------------------------------------------------------------------------------- /demo/test-images/roto-test-04.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdallasgray/roto/HEAD/demo/test-images/roto-test-04.jpg -------------------------------------------------------------------------------- /demo/test-images/roto-test-05.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdallasgray/roto/HEAD/demo/test-images/roto-test-05.jpg -------------------------------------------------------------------------------- /demo/test-images/roto-test-06.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdallasgray/roto/HEAD/demo/test-images/roto-test-06.jpg -------------------------------------------------------------------------------- /demo/test-images/roto-test-07.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdallasgray/roto/HEAD/demo/test-images/roto-test-07.jpg -------------------------------------------------------------------------------- /demo/test-images/roto-test-08.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdallasgray/roto/HEAD/demo/test-images/roto-test-08.jpg -------------------------------------------------------------------------------- /demo/test-images/roto-test-09.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdallasgray/roto/HEAD/demo/test-images/roto-test-09.jpg -------------------------------------------------------------------------------- /demo/test-images/roto-test-10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdallasgray/roto/HEAD/demo/test-images/roto-test-10.jpg -------------------------------------------------------------------------------- /demo/test-images/roto-test-11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdallasgray/roto/HEAD/demo/test-images/roto-test-11.jpg -------------------------------------------------------------------------------- /demo/test-images/roto-test-12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdallasgray/roto/HEAD/demo/test-images/roto-test-12.jpg -------------------------------------------------------------------------------- /demo/test-images/roto-test-13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdallasgray/roto/HEAD/demo/test-images/roto-test-13.jpg -------------------------------------------------------------------------------- /demo/test-images/roto-test-14.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdallasgray/roto/HEAD/demo/test-images/roto-test-14.jpg -------------------------------------------------------------------------------- /demo/test-images/roto-test-15.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdallasgray/roto/HEAD/demo/test-images/roto-test-15.jpg -------------------------------------------------------------------------------- /demo/test-images/roto-test-16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdallasgray/roto/HEAD/demo/test-images/roto-test-16.jpg -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/bez"] 2 | path = lib/bez 3 | url = git://github.com/rdallasgray/bez.git 4 | [submodule "lib/qunit"] 5 | path = lib/qunit 6 | url = git://github.com/jquery/qunit.git 7 | -------------------------------------------------------------------------------- /demo/roto.css: -------------------------------------------------------------------------------- 1 | h3 { 2 | margin-top: 2em; 3 | } 4 | 5 | .roto { 6 | visibility: hidden; 7 | } 8 | 9 | #slideshow { 10 | width: 400px; 11 | height: 300px; 12 | background-color: rgb(60,60,60); 13 | border: solid black 1px; 14 | } 15 | 16 | #slideshow li { 17 | width: 390px; 18 | height: 290px; 19 | border: solid white 5px; 20 | text-align: center; 21 | font-size: 2em; 22 | line-height: 290px; 23 | color: white; 24 | } 25 | 26 | #listbox { 27 | width: 320px; 28 | height: 400px; 29 | border: solid black 1px; 30 | background-color: rgb(240,240,240); 31 | } 32 | 33 | #listbox span { 34 | width: 318px; 35 | height: 48px; 36 | border: solid rgb(240,240,240) 1px; 37 | text-align: center; 38 | line-height: 48px; 39 | background-color: white; 40 | } 41 | 42 | #vertical-multi { 43 | width: 450px; 44 | height: 450px; 45 | } 46 | 47 | #vertical-multi li { 48 | width: 150px; 49 | height: 150px; 50 | text-align: center; 51 | overflow: hidden; 52 | } 53 | 54 | #vertical-multi img { 55 | display: inline; 56 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2011 Robert Dallas Gray 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are 4 | permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of 7 | conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, this list 10 | of conditions and the following disclaimer in the documentation and/or other materials 11 | provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY ''AS IS'' AND ANY EXPRESS OR IMPLIED 14 | WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 15 | FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL OR 16 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 17 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 18 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 20 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 21 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECT = Roto 2 | SRC_DIR = src 3 | BUILD_DIR = . 4 | DEMO_DIR = demo 5 | MAIN = jquery.roto 6 | BEZ = lib/bez/jquery.bez.min.js 7 | 8 | JS_ENGINE ?= `which node nodejs` 9 | COMPILER ?= `which uglifyjs` 10 | 11 | VERSION = $(shell git describe --tags --long | sed s/\-/\./) 12 | YEAR = $(shell date +"%Y") 13 | 14 | all: main min bez copy_demo clean 15 | 16 | nomin: main bez clean 17 | 18 | main: submodules version 19 | 20 | submodules: 21 | @@echo "Updating submodules" 22 | @@git submodule init 23 | @@git submodule update 24 | @@git submodule foreach "git checkout master" 25 | @@git submodule foreach "git pull" 26 | @@git submodule summary 27 | 28 | version: 29 | @@echo "Setting version number (${VERSION}) and year (${YEAR})" 30 | @@sed 's/@VERSION/${VERSION}/' <${SRC_DIR}/${MAIN}.js | sed 's/@YEAR/${YEAR}/' > ${BUILD_DIR}/${MAIN}.tmp 31 | 32 | min: 33 | @@if test ! -z ${JS_ENGINE} && test ! -z ${COMPILER}; then \ 34 | echo "Minifying ${PROJECT}"; \ 35 | ${COMPILER} ${BUILD_DIR}/${MAIN}.tmp > ${BUILD_DIR}/${MAIN}.min.js; \ 36 | else \ 37 | echo "You must have NodeJS and UglifyJS installed in order to minify ${PROJECT}."; exit 2;\ 38 | fi 39 | 40 | bez: 41 | @@echo "Attaching Bez to Roto" 42 | @@cp ${BEZ} ${BUILD_DIR}/${MAIN}.tmp 43 | @@cat ${BUILD_DIR}/${MAIN}.min.js >> ${BUILD_DIR}/${MAIN}.tmp 44 | @@echo ";" >> ${BUILD_DIR}/${MAIN}.tmp 45 | @@cp ${BUILD_DIR}/${MAIN}.tmp ${BUILD_DIR}/${MAIN}.min.js 46 | 47 | copy_demo: 48 | @@echo "Copying ${MAIN}.min.js to ${DEMO_DIR}" 49 | @@cp ${BUILD_DIR}/${MAIN}.min.js ${DEMO_DIR} 50 | 51 | clean: 52 | @@echo "Removing temp files" 53 | @@rm -f ${BUILD_DIR}/${MAIN}.tmp 54 | @@echo "Done" 55 | 56 | pages: 57 | @@echo "Updating gh-pages" 58 | @@git checkout gh-pages 59 | @@rm -Rf lib 60 | @@git archive master -o tmp.zip /demo 61 | @@unzip tmp.zip 62 | @@cp -R demo/* . 63 | @@rm tmp.zip 64 | @@rm -Rf demo 65 | @@git add * 66 | @@git commit -a -m "Updated demo to ${VERSION}" 67 | @@git push origin gh-pages 68 | @@git checkout master 69 | @@echo "Done" 70 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |

Roto demo

10 |

A flexible-width image carousel:

11 | 33 | 34 |

A slideshow:

35 |
36 |
    37 |
  • Slide 1
  • 38 |
  • Slide 2
  • 39 |
  • Slide 3
  • 40 |
  • Slide 4
  • 41 |
  • Slide 5
  • 42 |
  • Slide 6
  • 43 |
44 |
45 | 46 | 47 | 48 |

A listbox:

49 |
50 |
51 | List item 1 52 | List item 2 53 | List item 3 54 | List item 4 55 | List item 5 56 | List item 6 57 | List item 7 58 | List item 8 59 | List item 9 60 | List item 10 61 | List item 11 62 | List item 12 63 | List item 13 64 | List item 14 65 | List item 15 66 | List item 16 67 | List item 17 68 | List item 18 69 | List item 19 70 | List item 20 71 |
72 |
73 | 74 | 75 | 76 |

A vertical, multiple carousel with snapping:

77 |
78 |
    79 |
  • 80 |
  • 81 |
  • 82 |
  • 83 |
  • 84 |
  • 85 |
  • 86 |
  • 87 |
  • 88 |
  • 89 |
  • 90 |
  • 91 |
  • 92 |
  • 93 |
  • 94 |
  • 95 |
96 |
97 | 98 | 99 | 100 | 101 | 110 | -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |

Roto demo

11 |

A flexible-width image carousel:

12 | 34 | 35 |

A slideshow:

36 |
37 |
    38 |
  • Slide 1
  • 39 |
  • Slide 2
  • 40 |
  • Slide 3
  • 41 |
  • Slide 4
  • 42 |
  • Slide 5
  • 43 |
  • Slide 6
  • 44 |
45 |
46 | 47 | 48 | 49 |

A listbox:

50 |
51 |
52 | List item 1 53 | 54 |
55 |
56 | List item 3 57 | List item 4 58 | List item 5 59 | List item 6 60 | List item 7 61 | List item 8 62 | List item 9 63 | List item 10 64 | List item 11 65 | List item 12 66 | List item 13 67 | List item 14 68 | List item 15 69 | List item 16 70 | List item 17 71 | List item 18 72 | List item 19 73 | List item 20 74 |
75 |
76 | 77 | 78 | 79 |

A vertical, multiple carousel with snapping:

80 |
81 |
    82 |
  • 83 |
  • 84 |
  • 85 |
  • 86 |
  • 87 |
  • 88 |
  • 89 |
  • 90 |
  • 91 |
  • 92 |
  • 93 |
  • 94 |
  • 95 |
  • 96 |
  • 97 |
  • 98 |
99 |
100 | 101 | 102 | 103 | 104 | 114 | 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | What is Roto? 2 | ============= 3 | A simple, flexible, touch-capable scrolling plugin for jQuery. 4 | See a demo at http://rdallasgray.github.com/roto/ 5 | 6 | Roto takes an html unordered list (of anything) and makes it smoothly, swipeably scrollable on both desktop and touch devices. 7 | 8 | Example uses include image carousels, listboxes, slideshows, etc. 9 | 10 | Roto comes with a minimum of styling, so that you can make it look and behave however you want. It plays nicely with other plugins (Fancybox being an example), making it lightweight but very functional and flexible. 11 | 12 | 13 | How do I use it? 14 | ---------------- 15 | 1. Create a div or other block-level element. Give it an id or a classname that you can use to identify it. Let's call this the container. 16 | 2. Put another element inside it; let's call this element the rotoFrame. If you want to be explicit, give this element the class "rotoFrame". That's not necessary, but it prevents Roto presuming that the first element inside the container is the rotoFrame, thus allowing you to put other elements into the container first. You can also specify a selector to use instead of ".rotoFrame" in the options, if you want. 17 | 3. Put some more elements inside the rotoFrame, containing whatever you want to be rotoed (normally images, or images inside links, but whatever you like). 18 | 4. Optionally, add a couple of buttons with classnames 'prev' and 'next' inside the containing element (you can put them outside too -- see the section below on options); 19 | 5. Call roto on the containing element, e.g. `$("#roto-div").roto()`. 20 | 21 | See the demo for an example. 22 | 23 | 24 | Are there dependencies? 25 | ----------------------- 26 | The only dependency is my own Bez plugin (https://github.com/rdallasgray/bez), which is used to create jQuery-compatible easing functions from cubic-bezier co-ordinates. That's just for compatibility with browsers which don't support CSS transitions, so if you're only supporting newer browsers, you don't need it. Bez is compiled into the minified distribution of Roto by default, so you don't need to separately include it. 27 | 28 | 29 | What else do I need to know? 30 | ---------------------------- 31 | Roto works best if you call it using $(window).load() rather than $(document).ready(). This is because Webkit-based browsers don't know the dimensions of images at $(document).ready() time, and Roto relies on those dimensions. This can mean you see a 'Flash of Unstyled Content' as the script hasn't applied styles to the rotoed elements before the page begins to display, so you may want to style your rotoed elements with "visibility: hidden" until the window is loaded. Of course, if you're giving all your images explicit dimensions anyway, you can use $(document).ready(). 32 | 33 | Roto will also auto-disable a rotoed element if its container becomes larger than its contents (so it doesn't actually need to scroll). You can also programmatically enable and disable rotoed elements using events (see below). 34 | 35 | 36 | What about events? 37 | ------------------ 38 | Roto fires an event called `rotoChange` when the position of the roto content is changed (and on completion of any animations). You can listen for this event on the rotoed container and use it, for example, to change other content in your page when the roto is moved. The event passes the element leftmost or topmost in the roto as data. So you could do something like: 39 | 40 | `$("#roto").bind("rotoChange", function(event, element) { $(element).css("color", "red") });` 41 | 42 | Roto also listens for the events `rotoGoto`, `rotoShift`, `rotoContentChange`, `rotoEnable` and `rotoDisable`. 43 | 44 | You can pass one of three types of values as data to the "rotoGoto" event: a number, a jQuery-wrapped element, or a string. 45 | 46 | The number should be an index into the set of elements contained in the rotoed container (starting from zero); 47 | The jQuery-wrapped element should be one of the elements in the container (e.g. `$("#myelement")`). 48 | In each of those cases, roto will zip to the given item. 49 | 50 | The string should be either "next" or "prev", in which case the roto will advance or retreat one item, or an integer followed by "px", in which case the roto will go to the specified offset (e.g. "-200px"); 51 | 52 | The `rotoShift` event takes one argument as data -- a number, 1 or -1. Passing -1 will advance the roto one container width; passing 1 will retreat one container width. 53 | 54 | You can use these events to programatically move a roto around based on other events on your page. 55 | 56 | If you trigger the `rotoContentChange` event on the container, the roto will remeasure itself -- allowing you to dynamically add or remove content. 57 | 58 | The enable and disable events allow you to programmatically enable or disable the drag-to-scroll functionality; disabling a rotoed element will cause it to return to its maximum offset (normally zero) and ignore any drag events. 59 | 60 | 61 | What options do I have, and what are the defaults? 62 | -------------------------------------------------- 63 | Lots of options. The ones you'll generally want to use are: 64 | 65 | - btnPrev: the css selector for the 'Previous' button. Default is '.prev'. 66 | - btnNext: the css selector for the 'Next' button. Default is '.next'. 67 | - btnAction: 'step' or 'shift' -- determines whether the buttons advance/retreat one item or one screen at a time. Default is 'shift'. 68 | - direction: 'h' for horizontal, 'v' for vertical. Default is 'h'. 69 | - snap: whether to snap to individual elements. Default is true. 70 | - startOffset: offsets the entire roto the given number of pixels (you may want the 'active' element to be somewhere other than the left side of the container; maybe the centre of the screen) 71 | - endOffset: creates the given number of pixels in 'dummy' space at the end of the roto. Best used in combination with startOffset. 72 | 73 | There are more options -- power users can read the source and experiment. 74 | 75 | The button defaults work when the buttons are INSIDE the overall containing element. If you want to put them outside (which can be useful), you need to give the containing element an id (say, 'roto'), and then id your buttons as '[id]-prev' and '[id]-next' ('roto-prev'/'roto-next'). Otherwise you can specify precisely how to identify them in the options. 76 | 77 | 78 | How do I style Roto? 79 | -------------------- 80 | Any way you like. I haven't supplied any examples, for reasons you can read above. You don't NEED to include any CSS for Roto to work -- the script sets the basic required styles for you. But it can certainly look (and sometimes work) better with a bit of design nous. 81 | 82 | There are certain things that will break Roto, though: 83 | 84 | 1. The outer container must be set with overflow: hidden, position: relative. 85 | 2. The inner container must be set with position: relative, and padding and margin 0. 86 | 3. The elements must be set with display: block, float: left. 87 | 88 | You DON'T need to set these -- the script does it for you. But be aware that if you contradict them in your own stylesheets or scripts, you could run into problems. 89 | 90 | Otherwise, you shouldn't need to treat Roto with kid gloves. It'll take most of what you throw at it. 91 | 92 | 93 | What doesn't work? 94 | ------------------ 95 | It might not work in IE6. I haven't tested it. Life's too short! 96 | 97 | 98 | Acknowledgements 99 | ---------------- 100 | I must acknowledge Ganeshji Marwaha's jCarouselLite (http://www.gmarwaha.com/jquery/jcarousellite), which I used somewhat before writing Roto. Some of the option names are lifted from jCarouselLite, because there was no point being awkward by changing them. 101 | -------------------------------------------------------------------------------- /jquery.roto.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bez v1.0.10-g5ae0136 3 | * http://github.com/rdallasgray/bez 4 | * 5 | * A plugin to convert CSS3 cubic-bezier co-ordinates to jQuery-compatible easing functions 6 | * 7 | * With thanks to Nikolay Nemshilov for clarification on the cubic-bezier maths 8 | * See http://st-on-it.blogspot.com/2011/05/calculating-cubic-bezier-function.html 9 | * 10 | * Copyright 2011 Robert Dallas Gray. All rights reserved. 11 | * Provided under the FreeBSD license: https://github.com/rdallasgray/bez/blob/master/LICENSE.txt 12 | */jQuery.extend({bez:function(a){var b="bez_"+$.makeArray(arguments).join("_").replace(".","p");if(typeof jQuery.easing[b]!="function"){var c=function(a,b){var c=[null,null],d=[null,null],e=[null,null],f=function(f,g){return e[g]=3*a[g],d[g]=3*(b[g]-a[g])-e[g],c[g]=1-e[g]-d[g],f*(e[g]+f*(d[g]+f*c[g]))},g=function(a){return e[0]+a*(2*d[0]+3*c[0]*a)},h=function(a){var b=a,c=0,d;while(++c<14){d=f(b,0)-a;if(Math.abs(d)<.001)break;b-=d/g(b)}return b};return function(a){return f(h(a),1)}};jQuery.easing[b]=function(b,d,e,f,g){return f*c([a[0],a[1]],[a[2],a[3]])(d/g)+e}}return b}}); 13 | /*! 14 | * Roto v1.5.5-gfe1c278 15 | * http://github.com/rdallasgray/roto 16 | * 17 | * A simple, flexible, touch-capable scrolling plugin for jQuery 18 | * 19 | * Copyright 2012 Robert Dallas Gray. All rights reserved. 20 | * Provided under the FreeBSD license: https://github.com/rdallasgray/roto/blob/master/LICENSE.txt 21 | */(function(e,t,n,r){e.fn.roto=function(i){var s={rotoSelector:".rotoFrame",btnPrev:".prev",btnNext:".next",btnAction:"shift",direction:"h",shift_duration:200,shift_bezier:[0,0,0,1],drift_duration:1800,drift_factor:500,drift_bezier:[0,0,.3,1],bounce_duration:400,bounce_bezier:[0,.5,.5,1],pull_divisor:1.7,timer_interval:50,disable_transitions:!1,startOffset:0,endOffset:0,snap:!0,clickables:"a, img",auto_disable:!0},i=e.extend(s,i||{}),o=1e3,u="client",a=function(){try{return n.createEvent("TouchEvent"),!0}catch(e){return!1}}(),f=a?{start:"touchstart",move:"touchmove",end:"touchend"}:{start:"mousedown",move:"mousemove",end:"mouseup"},l=function(e){return a&&typeof e.originalEvent.touches!="undefined"?e.originalEvent.touches[0]:e},c={h:{measure:"Width",offsetName:"left",coOrd:"X",opp:"v"},v:{measure:"Height",offsetName:"top",coOrd:"Y",opp:"h"}},h=c[i.direction],p=null,d=null,v=null,m=null;if(!i.disable_transitions){var g=n.body||n.documentElement,y={transform:"transform",MozTransform:"-moz-transform",WebkitTransform:"-webkit-transform"},b={transition:{prop:"transition",event:"transitionend"},MozTransition:{prop:"-moz-transition",event:"transitionend"},WebkitTransition:{prop:"-webkit-transition",event:"webkitTransitionEnd"}};for(var w in y)if(typeof g.style[w]!="undefined"){p=y[w];break}for(var w in b)if(typeof g.style[w]!="undefined"){d=b[w].prop,v=b[w].event;break}}var E=d!==null;return this.each(function(){var m=e(this),g=m.children(i.rotoSelector).length>0?m.children(i.rotoSelector).first():m.children("ul").length>0?m.children("ul").first():m.children().first(),y=g.children(),b=0,w=0,S=0,x=0,T=-1,N=0,C=0,k=typeof m.attr("id")!==r?m.attr("id"):"roto"+(new Date).getTime(),L=null,A=null,O={ready:"ready",tracking:"tracking",drifting:"drifting",shifting:"shifting",bouncing:"bouncing"},M=O.ready,_=!1,D=m.find(i.btnPrev),P=m.find(i.btnNext),H=null,B=!0,j=function(t,n,s,u){var a=u,f=i[s+"_duration"];if(!_||!E)a=function(){G(),u()},_=!0;if(E){var l=I();l.timingFunction[s]===r&&(l.timingFunction[s]=["cubic-bezier(",i[s+"_bezier"].join(","),")"].join(""));var c={};c[l.durationProp]=f/o+"s",c[l.timingFunctionProp]=A.timingFunction[s],t.css(c),t.data("animationCallback",a),t.unbind(v),t.one(v,function(){t.data("animationCallback",null),a()}),t.css(n)}else t.animate(n,f,e.bez(i[s+"_bezier"]),a)},F=function(e){if(E){var t=R();e.unbind(v),e.css(q(t)),typeof e.data("animationCallback")=="function"&&(e.data("animationCallback")(),e.data("animationCallback",null))}else e.stop()},I=function(){return A===null&&(A={durationProp:d+"-duration",timingFunctionProp:d+"-timing-function",timingFunction:{}}),A},q=function(e){var t={};if(E){if(L===null){var n=a?"3d":"",r=n==="3d"?"(Xpx,Ypx,0px)":"(Xpx,Ypx)",i=c[h.opp].coOrd;L=["translate",n,r.replace(i,"0")].join("")}t[p]=L.replace(h.coOrd,e)}else t[h.offsetName]=e+"px";return t},R=function(){var e;if(!E)e=g.position()[h.offsetName]-b;else{var t=g.css(p);if(t==="none")return 0;var n=t.match(/\-?[0-9]+/g),r=h.coOrd==="X"?n[4]:n[5];e=parseInt(r)}return e-i.startOffset},U=function(t,n){var r=0,i,s,o=n>0?y.get().reverse():y,u,a,f="outer"+h.measure;return e.each(o,function(o,l){a=e(l),r=-1*Math.round(a.position()[h.offsetName]),u=l;if(r*n>=t*n)return!1;n<0?(i=-1*r+a[f](!0),s=-1*t):(i=a.prev().length>0?r+a.prev()[f](!0):r,s=t);if(i>s)return!1}),[u,r]},z=function(t,n){var r=n<0?"next":"prev",i=e(U(t,n)[0]),s=i;while(s[r]().length>0&&s.position()[h.offsetName]===i.position()[h.offsetName])s=s[r]();return s},W=function(e,t,n){var t=t===0?T:t;return n?-1*Math.round(z(e,t).position()[h.offsetName]):U(e,t)[1]},X=function(){var e=R();return e===W(e,T,!1)},V=function(){C=0,y=g.children(),y.css({display:"block","float":"left"});if(i.direction==="h")y.each(function(t,n){C+=Math.round(e(n)["outer"+h.measure](!0))}),g[h.measure.toLowerCase()](C);else{var t=e(y.last());C=Math.round(t.position()[h.offsetName]+t["outer"+h.measure](!0))}N=Math.round(m[h.measure.toLowerCase()]()),S=-1*Math.round(Math.max(0,C-N)+b+i.endOffset);if(i.snap){var n=W(S,-1,!1);S=n>S?W(S,-1,!0):n}i.auto_disable&&J(S!==w)},J=function(e){e||nt(w),B=!!e},K=function(){return H===null&&(H=typeof D=="object"&&typeof D.click=="function"&&typeof P=="object"&&typeof P.click=="function"),H},Q=function(){if(!K())return;var e=R();e>S?P.removeAttr("disabled"):P.attr("disabled","disabled"),e=y.length)return;var n=e(y.get(t));et(n)},et=function(t){var n=e(t);if(!n.parent()===g)return;nt(-1*n.position()[h.offsetName])},tt=function(e){var t=e==="prev"?1:-1;et(z(R(),t))},nt=function(e,t){t=t||"shift",e>w?e=w:eS)n=t;else var r=t>=w?t-w:t-S,n=t-r/i.pull_divisor;if(M!==O.tracking){var s={},o=I();s[o.durationProp]="0s",s[o.timingFunctionProp]="none",g.css(s)}M=O.tracking,g.css(q(n))},ot=function(){var e=ft.getPointerSpeed(),t=e[0],n=e[1],r=R(),s=t*i.drift_factor*n,o=s+r;o>w?o=w:o0&&e(n).one(f.move+".roto-"+k,function(t){c.data("events")!==r&&(e.each(c.data("events"),function(t,n){p[t]=[],e.each(n,function(e,n){n.namespace!=="roto-"+k&&p[t].push(n)})}),c.unbind(),v=!0),g.delegate(i.clickables,"click.roto-trackclick-"+k,function(e){e.preventDefault()})})),s=l(s);var m=s[d];ft.setCurrentCoOrd(m),e(n).bind(f.move+".roto-"+k,function(e){if(!B)return;e.preventDefault(),e=l(e),ft.setCurrentCoOrd(e[d]),st(e[d]-m)}),e(n).bind(f.end+".roto-"+k,function(){ft.stop();var r=R();r>w||r0?m.children(i.rotoSelector).first():m.children("ul").length>0?m.children("ul").first():m.children().first(),y=g.children(),b=0,w=0,S=0,x=0,T=-1,N=0,C=0,k=typeof m.attr("id")!==r?m.attr("id"):"roto"+(new Date).getTime(),L=null,A=null,O={ready:"ready",tracking:"tracking",drifting:"drifting",shifting:"shifting",bouncing:"bouncing"},M=O.ready,_=!1,D=m.find(i.btnPrev),P=m.find(i.btnNext),H=null,B=!0,j=function(t,n,s,u){var a=u,f=i[s+"_duration"];if(!_||!E)a=function(){G(),u()},_=!0;if(E){var l=I();l.timingFunction[s]===r&&(l.timingFunction[s]=["cubic-bezier(",i[s+"_bezier"].join(","),")"].join(""));var c={};c[l.durationProp]=f/o+"s",c[l.timingFunctionProp]=A.timingFunction[s],t.css(c),t.data("animationCallback",a),t.unbind(v),t.one(v,function(){t.data("animationCallback",null),a()}),t.css(n)}else t.animate(n,f,e.bez(i[s+"_bezier"]),a)},F=function(e){if(E){var t=R();e.unbind(v),e.css(q(t)),typeof e.data("animationCallback")=="function"&&(e.data("animationCallback")(),e.data("animationCallback",null))}else e.stop()},I=function(){return A===null&&(A={durationProp:d+"-duration",timingFunctionProp:d+"-timing-function",timingFunction:{}}),A},q=function(e){var t={};if(E){if(L===null){var n=a?"3d":"",r=n==="3d"?"(Xpx,Ypx,0px)":"(Xpx,Ypx)",i=c[h.opp].coOrd;L=["translate",n,r.replace(i,"0")].join("")}t[p]=L.replace(h.coOrd,e)}else t[h.offsetName]=e+"px";return t},R=function(){var e;if(!E)e=g.position()[h.offsetName]-b;else{var t=g.css(p);if(t==="none")return 0;var n=t.match(/\-?[0-9]+/g),r=h.coOrd==="X"?n[4]:n[5];e=parseInt(r)}return e-i.startOffset},U=function(t,n){var r=0,i,s,o=n>0?y.get().reverse():y,u,a,f="outer"+h.measure;return e.each(o,function(o,l){a=e(l),r=-1*Math.round(a.position()[h.offsetName]),u=l;if(r*n>=t*n)return!1;n<0?(i=-1*r+a[f](!0),s=-1*t):(i=a.prev().length>0?r+a.prev()[f](!0):r,s=t);if(i>s)return!1}),[u,r]},z=function(t,n){var r=n<0?"next":"prev",i=e(U(t,n)[0]),s=i;while(s[r]().length>0&&s.position()[h.offsetName]===i.position()[h.offsetName])s=s[r]();return s},W=function(e,t,n){var t=t===0?T:t;return n?-1*Math.round(z(e,t).position()[h.offsetName]):U(e,t)[1]},X=function(){var e=R();return e===W(e,T,!1)},V=function(){C=0,y=g.children(),y.css({display:"block","float":"left"});if(i.direction==="h")y.each(function(t,n){C+=Math.round(e(n)["outer"+h.measure](!0))}),g[h.measure.toLowerCase()](C);else{var t=e(y.last());C=Math.round(t.position()[h.offsetName]+t["outer"+h.measure](!0))}N=Math.round(m[h.measure.toLowerCase()]()),S=-1*Math.round(Math.max(0,C-N)+b+i.endOffset);if(i.snap){var n=W(S,-1,!1);S=n>S?W(S,-1,!0):n}i.auto_disable&&J(S!==w)},J=function(e){e||nt(w),B=!!e},K=function(){return H===null&&(H=typeof D=="object"&&typeof D.click=="function"&&typeof P=="object"&&typeof P.click=="function"),H},Q=function(){if(!K())return;var e=R();e>S?P.removeAttr("disabled"):P.attr("disabled","disabled"),e=y.length)return;var n=e(y.get(t));et(n)},et=function(t){var n=e(t);if(!n.parent()===g)return;nt(-1*n.position()[h.offsetName])},tt=function(e){var t=e==="prev"?1:-1;et(z(R(),t))},nt=function(e,t){t=t||"shift",e>w?e=w:eS)n=t;else var r=t>=w?t-w:t-S,n=t-r/i.pull_divisor;if(M!==O.tracking){var s={},o=I();s[o.durationProp]="0s",s[o.timingFunctionProp]="none",g.css(s)}M=O.tracking,g.css(q(n))},ot=function(){var e=ft.getPointerSpeed(),t=e[0],n=e[1],r=R(),s=t*i.drift_factor*n,o=s+r;o>w?o=w:o0&&e(n).one(f.move+".roto-"+k,function(t){c.data("events")!==r&&(e.each(c.data("events"),function(t,n){p[t]=[],e.each(n,function(e,n){n.namespace!=="roto-"+k&&p[t].push(n)})}),c.unbind(),v=!0),g.delegate(i.clickables,"click.roto-trackclick-"+k,function(e){e.preventDefault()})})),s=l(s);var m=s[d];ft.setCurrentCoOrd(m),e(n).bind(f.move+".roto-"+k,function(e){if(!B)return;e.preventDefault(),e=l(e),ft.setCurrentCoOrd(e[d]),st(e[d]-m)}),e(n).bind(f.end+".roto-"+k,function(){ft.stop();var r=R();r>w||r 0 ? 116 | container.children(options.rotoSelector).first() : 117 | container.children("ul").length > 0 ? container.children("ul").first() : container.children().first(), 118 | rotoKids = rotoFrame.children(), 119 | // the offset measured before the rotoFrame is moved (to prevent problems in IE7) 120 | offsetCorrection = 0, 121 | // the maximum offset from starting position that the roto can be moved 122 | maxOffset = 0, 123 | // the minimum offset from starting position that the roto can be moved (to be calculated below) 124 | minOffset = 0, 125 | // the offset to pointer tracking 126 | trackingOffset = 0, 127 | // the last non-zero direction of travel measured 128 | lastValidDir = -1, 129 | // the inner width or height of the container element 130 | containerMeasure = 0, 131 | // the total width or height of the contents of the rotoFrame element 132 | rotoMeasure = 0, 133 | // unique identification of the overall container, to be used in namespacing events 134 | containerId = (typeof container.attr("id") !== undefined) ? container.attr("id") : "roto" + new Date().getTime(), 135 | // if transforms are supported, the string giving the css property to be animated 136 | animatedProp = null, 137 | // look-up table for css transition properties 138 | transitionLUT = null, 139 | // current state of the roto as regards animations etc. 140 | states = { ready: "ready", tracking: "tracking", drifting: "drifting", shifting: "shifting", bouncing: "bouncing" }, 141 | state = states.ready, 142 | // whether the rotoChange event has already been primed 143 | changeEventPrimed = false, 144 | // cache of the previous and next button elements 145 | prevButton = container.find(options.btnPrev), nextButton = container.find(options.btnNext), 146 | // whether we're using the prev and next buttons 147 | buttonsUsed = null, 148 | // whether scrolling is currently enabled 149 | scrollingEnabled = true; 150 | 151 | 152 | 153 | /*** LOW-LEVEL ANIMATION FUNCTIONS ***/ 154 | 155 | // support both jQuery.animate and css transitions 156 | var doAnimation = function(element, css, easing, callback) { 157 | var _callback = callback, duration = options[easing + "_duration"]; 158 | // don't add a change notification to the callback if one has already been added -- 159 | // unless we're not using css transitions, in which case do 160 | if (!changeEventPrimed || !usingTransitions) { 161 | _callback = function() { 162 | notifyChanged(); 163 | callback(); 164 | } 165 | changeEventPrimed = true; 166 | } 167 | if (usingTransitions) { 168 | var LUT = getTransitionLUT(); 169 | if (LUT.timingFunction[easing] === undefined) { 170 | LUT.timingFunction[easing] = ["cubic-bezier(", options[easing + "_bezier"].join(","), ")"].join(""); 171 | } 172 | // construct a simple object to set as CSS props 173 | var opt = {}; 174 | opt[LUT.durationProp] = duration/msToS + "s"; 175 | opt[LUT.timingFunctionProp] = transitionLUT.timingFunction[easing]; 176 | element.css(opt); 177 | 178 | // store the callback as data to use in case animation is stopped 179 | element.data("animationCallback", _callback); 180 | // and bind it to the transitionEvent, to run if animation completes 181 | element.unbind(transitionEvent); 182 | element.one(transitionEvent, function() { 183 | element.data("animationCallback", null); 184 | _callback(); 185 | }); 186 | //finally, set the new css properties to initiate the transition 187 | element.css(css); 188 | } 189 | else { 190 | // we're not using transitions -- use jQuery.animate instead 191 | element.animate(css, duration, $.bez(options[easing + "_bezier"]), _callback); 192 | } 193 | }, 194 | stopAnimation = function(element) { 195 | if (usingTransitions) { 196 | var offset = getCurrentOffset(); 197 | element.unbind(transitionEvent); 198 | element.css(getAnimatedProp(offset)); 199 | // run the callback stored in the element's data, then remove it 200 | if (typeof element.data("animationCallback") === "function") { 201 | element.data("animationCallback")(); 202 | element.data("animationCallback", null); 203 | } 204 | } 205 | else { 206 | // using jQuery.animate, so stop() will work fine 207 | element.stop(); 208 | } 209 | }, 210 | getTransitionLUT = function() { 211 | // build the transition LUT if necessary 212 | if (transitionLUT === null) { 213 | transitionLUT = { 214 | durationProp: transitionProp + "-duration", 215 | timingFunctionProp: transitionProp + "-timing-function", 216 | timingFunction: {} 217 | }; 218 | } 219 | return transitionLUT; 220 | }, 221 | 222 | // get the css property to animate based on whether transforms are supported 223 | getAnimatedProp = function(move) { 224 | var opt = {}; 225 | if (usingTransitions) { 226 | if (animatedProp === null) { 227 | var use3d = isTouchDevice ? "3d" : "", 228 | translateStr = (use3d === "3d") ? "(Xpx,Ypx,0px)" : "(Xpx,Ypx)", 229 | oppCoOrd = orientations[dimensions.opp].coOrd; 230 | animatedProp = ["translate", use3d, 231 | translateStr.replace(oppCoOrd, "0")].join(""); 232 | } 233 | opt[transformProp] = animatedProp.replace(dimensions.coOrd, move); 234 | } 235 | else { 236 | opt[dimensions.offsetName] = move + "px"; 237 | } 238 | return opt; 239 | }, 240 | 241 | 242 | 243 | /*** FINDING OFFSETS ***/ 244 | 245 | // get the current offset position of the rotoFrame, dependent on whether transforms are supported 246 | getCurrentOffset = function() { 247 | var offset; 248 | if (!usingTransitions) { 249 | offset = rotoFrame.position()[dimensions.offsetName] - offsetCorrection 250 | } 251 | else { 252 | var transformStr = rotoFrame.css(transformProp); 253 | if (transformStr === "none") return 0; 254 | 255 | var matches = transformStr.match(/\-?[0-9]+/g), 256 | val = (dimensions.coOrd === 'X') ? matches[4] : matches[5]; 257 | offset = parseInt(val); 258 | } 259 | return offset - options.startOffset; 260 | }, 261 | 262 | // find the rotoKid nearest the given offset, and its position 263 | getNearestRotoKidTo = function(offset, dir) { 264 | var pos = 0, extent, bound, 265 | kids = (dir > 0) ? rotoKids.get().reverse() : rotoKids, 266 | kid, _el, measure = "outer" + dimensions.measure; 267 | $.each(kids, function(idx, el) { 268 | _el = $(el); 269 | // set pos to the position of the current rotoKid 270 | pos = -1 * Math.round(_el.position()[dimensions.offsetName]); 271 | kid = el; 272 | // break the loop early if pos has overshot offset 273 | if (pos * dir >= offset * dir) { 274 | return false; 275 | } 276 | if (dir < 0) { 277 | // if we're searching start-to-end, the extent is the current kid's width or height, plus pos; 278 | // the bound is the offset 279 | extent = (-1 * pos) + _el[measure](true); 280 | bound = -1 * offset; 281 | } 282 | else { 283 | // if we're searching end-to-start, the extent is the previous kid's width or height, plus pos; 284 | // the bound is the offset 285 | extent = _el.prev().length > 0 ? pos + _el.prev()[measure](true) : pos; 286 | bound = offset; 287 | } 288 | // if the extent of the current kid has overshot the offset, break the loop 289 | if (extent > bound) { 290 | return false; 291 | } 292 | }); 293 | return [kid, pos]; 294 | }, 295 | 296 | // find the next (by direction) rotoKid to the given offset 297 | getNextRotoKidTo = function(offset, dir) { 298 | var func = dir < 0 ? "next" : "prev", 299 | curr = $(getNearestRotoKidTo(offset, dir)[0]), 300 | next = curr; 301 | // make sure we don't return a kid at the same offset (which can happen with stacked rotos) 302 | while (next[func]().length > 0 303 | && next.position()[dimensions.offsetName] === curr.position()[dimensions.offsetName]) { 304 | next = next[func](); 305 | } 306 | return next; 307 | }, 308 | 309 | // get the position of the rotoKid nearest the given offset 310 | getSnapMove = function(offset, dir, next) { 311 | var dir = (dir === 0) ? lastValidDir : dir 312 | return next ? 313 | -1 * Math.round(getNextRotoKidTo(offset, dir).position()[dimensions.offsetName]) 314 | : getNearestRotoKidTo(offset, dir)[1]; 315 | }, 316 | 317 | // is the roto already snapped to a rotoKid? 318 | isSnapped = function() { 319 | var offset = getCurrentOffset(); 320 | return offset === getSnapMove(offset, lastValidDir, false); 321 | }, 322 | 323 | 324 | 325 | /*** CHECKING MEASUREMENTS AND OFFSETS ***/ 326 | 327 | // remeasure the container and derive the minimum offset allowed 328 | // the minimum offset is the total measure of the rotoKids - the measure of the rotoFrame 329 | remeasure = function() { 330 | // measure the total width or height of the elements contained in the rotoFrame 331 | // if roto is horizontal, we have to individually measure each rotoKid 332 | rotoMeasure = 0; 333 | rotoKids = rotoFrame.children(); 334 | rotoKids.css({ display: "block", "float": "left" }); 335 | if (options.direction === 'h') { 336 | // for each element, add the outer dimension of the element including margin and padding 337 | rotoKids.each(function(idx, el) { 338 | rotoMeasure += Math.round($(el)["outer"+dimensions.measure](true)); 339 | }); 340 | // set the dimension of the rotoFrame to what we measured, just to be sure 341 | rotoFrame[dimensions.measure.toLowerCase()](rotoMeasure); 342 | } 343 | else { 344 | // if roto is vertical we can use a simpler method to calculate size: 345 | // just find the position of the last element and add its outer dimension, including margin and padding 346 | var last = $(rotoKids.last()); 347 | rotoMeasure = Math.round(last.position()[dimensions.offsetName] + last["outer"+dimensions.measure](true)); 348 | } 349 | containerMeasure = Math.round(container[dimensions.measure.toLowerCase()]()), 350 | minOffset = -1 * Math.round( 351 | Math.max(0, rotoMeasure - containerMeasure) 352 | + offsetCorrection 353 | + options.endOffset 354 | ); 355 | if (options.snap) { 356 | var offset = getSnapMove(minOffset, -1, false); 357 | // check if the offset we got is less than the non-snap minOffset; if so, use the offset of the next rotoKid 358 | minOffset = offset > minOffset ? getSnapMove(minOffset, -1, true) : offset; 359 | } 360 | if (options.auto_disable) setScrollingEnabled(minOffset !== maxOffset); 361 | }, 362 | 363 | // set whether scrolling is enabled; if not, return to maxOffset and prevent drag events. 364 | setScrollingEnabled = function(enabled) { 365 | if (!enabled) { 366 | gotoOffset(maxOffset); 367 | } 368 | scrollingEnabled = !!enabled; 369 | }, 370 | 371 | // check if prev & next buttons are being used 372 | usingButtons = function() { 373 | if (buttonsUsed === null) { 374 | buttonsUsed = (typeof prevButton === "object" && typeof prevButton.click === "function") 375 | && (typeof nextButton === "object" && typeof nextButton.click === "function"); 376 | } 377 | return buttonsUsed; 378 | }, 379 | 380 | // enable or disable the previous and next buttons based on roto conditions 381 | switchButtons = function() { 382 | if (!usingButtons()) return; 383 | var offset = getCurrentOffset(); 384 | 385 | // if the total measure of the rotoKids extends beyond the end of the rotoFrame, enable the next button 386 | if (offset > minOffset) { 387 | nextButton.removeAttr("disabled"); 388 | } 389 | else nextButton.attr("disabled", "disabled"); 390 | 391 | // if the rotoKids are offset beyond the start of the rotoFrame, enable the previous button 392 | if (offset < maxOffset) { 393 | prevButton.removeAttr("disabled"); 394 | } 395 | else prevButton.attr("disabled", "disabled"); 396 | }, 397 | 398 | 399 | 400 | /*** TRIGGERING AND RESPONDING TO EVENTS ***/ 401 | 402 | // trigger event on completion of move 403 | notifyChanged = function() { 404 | container.trigger("rotoChange", [getNearestRotoKidTo(getCurrentOffset(), 1)[0]]); 405 | changeEventPrimed = false; 406 | }, 407 | 408 | // respond to a goto event 409 | rotoGoto = function(data) { 410 | var type = typeof data, matches; 411 | switch(type) { 412 | case "number": 413 | gotoNumber(data); 414 | break; 415 | case "object": 416 | gotoElement(data); 417 | break; 418 | case "string": 419 | if (matches = data.match(/(-?[0-9]+)(px$)/)) { 420 | gotoOffset(parseInt(matches[1])); 421 | } 422 | else if (matches = data.match(/prev|next/)) { 423 | gotoNext(data); 424 | } 425 | break; 426 | } 427 | }, 428 | 429 | // goto a numbered element 430 | gotoNumber = function(num) { 431 | if (num < 0 || num >= rotoKids.length) return; 432 | var el = $(rotoKids.get(num)); 433 | gotoElement(el); 434 | }, 435 | 436 | // goto an element 437 | gotoElement = function(el) { 438 | var _el = $(el); 439 | if (!_el.parent() === rotoFrame) return; 440 | gotoOffset(-1 * _el.position()[dimensions.offsetName]); 441 | }, 442 | 443 | // goto prev or next 444 | gotoNext = function(dirStr) { 445 | var dir = (dirStr === "prev") ? 1 : -1; 446 | gotoElement(getNextRotoKidTo(getCurrentOffset(), dir)); 447 | }, 448 | 449 | // goto an offset 450 | gotoOffset = function(offset, animation) { 451 | animation = animation || "shift"; 452 | if (offset > maxOffset) offset = maxOffset; 453 | else if (offset < minOffset) offset = minOffset; 454 | offset += options.startOffset; 455 | doAnimation(rotoFrame, getAnimatedProp(offset), animation, function() { 456 | switchButtons(); 457 | state = states.ready; 458 | }); 459 | }, 460 | 461 | 462 | 463 | /*** MOVING THE ROTOFRAME ***/ 464 | 465 | // shift the rotoKids one rotoFrame width in the given direction 466 | rotoShift = function(dir) { 467 | // do nothing if the animation is already running 468 | if (state === states.shifting) return; 469 | var move = 0; 470 | state = states.shifting; 471 | lastValidDir = dir; 472 | 473 | if (dir < 0) { 474 | // if we're moving forwards, find the element nearest the end of the container 475 | move = Math.max( 476 | getSnapMove(getCurrentOffset() - (containerMeasure - options.startOffset), dir, false), 477 | minOffset 478 | ); 479 | } 480 | else { 481 | // if we're moving backwards, find the element one container width towards the start of the container 482 | move = Math.min( 483 | getSnapMove(getCurrentOffset() + (containerMeasure - options.startOffset), dir, false), 484 | maxOffset 485 | ); 486 | } 487 | // move the offsetElement to the start of the container 488 | gotoOffset(move); 489 | }, 490 | 491 | rotoStep = function(dir) { 492 | var dirStr = dir < 0 ? "next" : "prev"; 493 | gotoNext(dirStr); 494 | }, 495 | 496 | // track the rotoFrame to movement of the pointer 497 | rotoTrack = function(pointerMove) { 498 | var drag = Math.round(pointerMove + trackingOffset), 499 | move; 500 | // allow user to pull the rotoFrame beyond the max/min offsets 501 | if (drag < (maxOffset) && drag > (minOffset)) { 502 | move = drag; 503 | } 504 | else { 505 | var diff = (drag >= maxOffset) ? drag - maxOffset : drag - minOffset, 506 | move = drag - diff/options.pull_divisor; 507 | } 508 | if (state !== states.tracking) { 509 | var opt = {}, LUT = getTransitionLUT(); 510 | opt[LUT.durationProp] = "0s"; 511 | opt[LUT.timingFunctionProp] = "none"; 512 | rotoFrame.css(opt); 513 | } 514 | state = states.tracking; 515 | rotoFrame.css(getAnimatedProp(move)); 516 | }, 517 | 518 | // continue rotoFrame movement inertially based on pointer speed 519 | rotoDrift = function() { 520 | var speed_dir = timer.getPointerSpeed(), 521 | speed = speed_dir[0], dir = speed_dir[1], 522 | cOffset = getCurrentOffset(), 523 | distance = speed * options.drift_factor * dir, 524 | move = distance + cOffset; 525 | 526 | if (move > maxOffset) move = maxOffset; 527 | else if (move < minOffset) move = minOffset; 528 | else if (options.snap && !isSnapped()) { 529 | move = getSnapMove(move, dir, true); 530 | } 531 | if (move === cOffset) { 532 | notifyChanged(); 533 | return; 534 | } 535 | state = states.drifting; 536 | gotoOffset(move, "drift"); 537 | }, 538 | 539 | // bounce the rotoFrame elastically after it's pulled beyond max or min offsets 540 | bounceBack = function(dir) { 541 | var end = (dir < 0) ? minOffset : maxOffset; 542 | state = states.bouncing; 543 | gotoOffset(end, "bounce"); 544 | }, 545 | 546 | // whether the buttons shift or step the roto 547 | buttonAction = options.btnAction === "shift" ? rotoShift : rotoStep, 548 | 549 | 550 | /*** STARTUP AND UTILITY FUNCTIONS ***/ 551 | 552 | // timer to calculate speed of pointer movement 553 | timer = (function() { 554 | var startCoOrd = 0, currentCoOrd = 0, initialCoOrd = 0, 555 | chunker = null, 556 | chunk = { startCoOrd: 0, endCoOrd: 0 }; 557 | 558 | return { 559 | start: function() { 560 | initialCoOrd = startCoOrd = currentCoOrd; 561 | //only measure speed in the final 50ms of movement 562 | chunker = window.setInterval(function() { 563 | chunk.startCoOrd = startCoOrd; 564 | chunk.endCoOrd = currentCoOrd; 565 | startCoOrd = currentCoOrd; 566 | }, options.timer_interval); 567 | }, 568 | stop: function() { 569 | clearInterval(chunker); 570 | chunk.endCoOrd = currentCoOrd; 571 | }, 572 | getPointerSpeed: function() { 573 | var translation = chunk.endCoOrd - chunk.startCoOrd, 574 | distance = Math.abs(translation), 575 | speed = distance/options.timer_interval, 576 | dir_value = (translation === 0) ? chunk.endCoOrd - initialCoOrd : translation, 577 | dir = (dir_value <= 0) ? ((dir_value === 0) ? 0 : -1) : 1; 578 | if (dir !== 0) lastValidDir = dir; 579 | return [speed, dir]; 580 | }, 581 | setCurrentCoOrd: function(coOrd) { 582 | currentCoOrd = coOrd; 583 | } 584 | } 585 | }()), 586 | 587 | // boot or reboot the roto 588 | boot = function() { 589 | remeasure(); 590 | switchButtons(); 591 | if (options.setTestVars) setTestVars(); 592 | }; 593 | 594 | 595 | 596 | /*** STARTING UP THE ROTO ***/ 597 | 598 | // bind scroll events 599 | rotoFrame.bind(scrollEvents.start + ".roto-" + containerId, function(scrollStartEvent) { 600 | var stateNow = state; 601 | 602 | trackingOffset = getCurrentOffset(); 603 | stopAnimation(rotoFrame); 604 | 605 | var linkElements = rotoFrame.find("a"), 606 | oldLinkEvents = {}, 607 | coOrdStr = coOrdRef + dimensions.coOrd, 608 | eventsDetached = false; 609 | 610 | if (stateNow !== states.drifting) { 611 | rotoFrame.undelegate(options.clickables, "click.roto-trackclick-" + containerId); 612 | } 613 | 614 | state = states.ready; 615 | 616 | if (!isTouchDevice) { 617 | if (scrollStartEvent.target.draggable) { 618 | scrollStartEvent.preventDefault(); // prevent drag behaviour 619 | } 620 | if (document.ondragstart !== undefined) { 621 | // need to test this in IE before changing to delegate() 622 | rotoFrame.find(options.clickables).one("dragstart.roto-" + containerId, function(dragStartEvent) { 623 | dragStartEvent.preventDefault(); 624 | }); 625 | } 626 | if (linkElements.length > 0) { 627 | $(document).one(scrollEvents.move + ".roto-" + containerId, function(moveStartEvent) { 628 | if (linkElements.data("events") !== undefined) { 629 | // gather any events attached to linkElements before unbinding 630 | $.each(linkElements.data("events"), function(eventName, events) { 631 | oldLinkEvents[eventName] = []; 632 | $.each(events, function(i, event) { 633 | if (event.namespace !== "roto-" + containerId) { 634 | oldLinkEvents[eventName].push(event); 635 | } 636 | }); 637 | }); 638 | // prevent linkElements responding to other events during rotoFrame tracking 639 | linkElements.unbind(); 640 | eventsDetached = true; 641 | } 642 | // prevent linkElements responding to clicks during rotoFrame tracking 643 | rotoFrame.delegate(options.clickables, "click.roto-trackclick-" + containerId, function(linkTrackClickEvent) { 644 | linkTrackClickEvent.preventDefault(); 645 | }); 646 | }); 647 | } 648 | } 649 | scrollStartEvent = wrapScrollEvent(scrollStartEvent); 650 | var startCoOrd = scrollStartEvent[coOrdStr]; 651 | timer.setCurrentCoOrd(startCoOrd); 652 | 653 | // scrolling has started, so begin tracking pointer movement and measuring speed 654 | $(document).bind(scrollEvents.move + ".roto-" + containerId, function(trackEvent) { 655 | if (!scrollingEnabled) return; 656 | trackEvent.preventDefault(); 657 | trackEvent = wrapScrollEvent(trackEvent); 658 | timer.setCurrentCoOrd(trackEvent[coOrdStr]); 659 | rotoTrack(trackEvent[coOrdStr] - startCoOrd); 660 | }); 661 | 662 | // user stopped scrolling 663 | $(document).bind(scrollEvents.end + ".roto-" + containerId, function() { 664 | timer.stop(); 665 | var offset = getCurrentOffset(); 666 | if (offset > maxOffset || offset < minOffset) { 667 | bounceBack(getCurrentOffset() - maxOffset); 668 | } 669 | else { 670 | rotoDrift(); 671 | } 672 | $(document).unbind(scrollEvents.move + ".roto-" + containerId); 673 | window.setTimeout(function() { 674 | if (eventsDetached) { 675 | // reattach old events to linkElements after a short delay 676 | $.each(oldLinkEvents, function(eventName, events) { 677 | $.each(events, function(f, event) { 678 | var eventStr = event.type; 679 | if (event.namespace !== "") { 680 | eventStr = [eventStr, ".", event.namespace].join(""); 681 | } 682 | linkElements.bind(eventStr, event.data, event.handler); 683 | }); 684 | }); 685 | } 686 | }, 250); 687 | $(this).unbind(); 688 | }); 689 | timer.start(); 690 | }); 691 | 692 | // if prev/next buttons don't seem to be inside the container, look for them outside 693 | if (prevButton.length === 0 && options.btnPrev === defaults.btnPrev) { 694 | if (container.attr("id")) { 695 | prevButton = $("#"+container.attr("id")+"-prev"); 696 | nextButton = $("#"+container.attr("id")+"-next"); 697 | } 698 | } 699 | 700 | // bind button presses 701 | if (usingButtons()) { 702 | prevButton.click(function() { 703 | return buttonAction(1); 704 | }); 705 | nextButton.click(function() { 706 | return buttonAction(-1); 707 | }); 708 | } 709 | 710 | // remeasure everything on window resize, in case there are fluid elements involved 711 | $(window).resize(function() { 712 | boot(); 713 | }); 714 | 715 | // prevent webkit flicker 716 | if (transitionProp === "-webkit-transition") rotoFrame.css("-webkit-backface-visibility", "hidden"); 717 | 718 | // set required styles 719 | container.css({ display: "block", overflow: "hidden", position: "relative" }); 720 | rotoFrame.css({ position: "relative", padding: 0, margin: 0 }); 721 | 722 | // let's get started 723 | container.bind("rotoGoto", function(e, d) { rotoGoto(d); }); 724 | container.bind("rotoShift", function(e, d) { rotoShift(d); }); 725 | container.bind("rotoContentChange", function() { boot(); }); 726 | container.bind("rotoDisable", function() { setScrollingEnabled(false); }); 727 | container.bind("rotoEnable", function() { setScrollingEnabled(true); }); 728 | boot(); 729 | 730 | // move to startOffset 731 | if (options.startOffset !== 0) { 732 | rotoFrame.css(getAnimatedProp(options.startOffset)); 733 | switchButtons(); 734 | } 735 | 736 | // make IE7 sane 737 | offsetCorrection = Math.round(rotoFrame.position()[dimensions.offsetName]); 738 | if (offsetCorrection !== options.startOffset) remeasure(); 739 | }); 740 | } 741 | })(jQuery, window, document); 742 | 743 | --------------------------------------------------------------------------------