/* ============================= jQuery Scroll Path Plugin v1.1.1 Demo and Documentation: http://joelb.me/scrollpath ============================= A jQuery plugin for defining a custom path that the browser follows when scrolling. Comes with a custom scrollbar, which is styled in scrollpath.css. Author: Joel Besada (http://www.joelb.me) Date: 2012-02-01 Copyright 2012, Joel Besada MIT Licensed (http://www.opensource.org/licenses/mit-license.php) */ ( function ( $, window, document, undefined ) { var PREFIX = "-" + getVendorPrefix().toLowerCase() + "-", HAS_TRANSFORM_SUPPORT = supportsTransforms(), HAS_CANVAS_SUPPORT = supportsCanvas(), FPS = 60, STEP_SIZE = 50, // Number of actual path steps per scroll steps. // The extra steps are needed to make animations look smooth. BIG_STEP_SIZE = STEP_SIZE * 5, // Step size for space, page down/up isInitialized = false, isDragging = false, isAnimating = false, step, pathObject, pathList, element, scrollBar, scrollHandle, // Default speeds for scrolling and rotating (with path.rotate()) speeds = { scrollSpeed: 50, rotationSpeed: Math.PI/15 }, // Default plugin settings settings = { wrapAround: false, drawPath: false, scrollBar: true }, methods = { /* Initializes the plugin */ init: function( options ) { if ( this.length > 1 || isInitialized ) $.error( "jQuery.scrollPath can only be initialized on *one* element *once*" ); $.extend( settings, options ); isInitialized = true; element = this; pathList = pathObject.getPath(); initCanvas(); initScrollBar(); scrollToStep( 0 ); // Go to the first step immediately element.css( "position", "relative" ); $( document ).on({ "mousewheel": scrollHandler, "DOMMouseScroll": ("onmousewheel" in document) ? null : scrollHandler, // Firefox "keydown": keyHandler, "mousedown": function( e ) { if( e.button === 1 ) { e.preventDefault(); return false; } } }); $( window ).on( "resize", function() { scrollToStep( step ); } ); // Re-centers the screen return this; }, getPath: function( options ) { $.extend( speeds, options ); return pathObject || ( pathObject = new Path( speeds.scrollSpeed, speeds.rotationSpeed )); }, scrollTo: function( name, duration, easing, callback ) { var destination = findStep( name ); if ( destination === undefined ) $.error( "jQuery.scrollPath could not find scroll target with name '" + name + "'" ); var distance = destination - step; if ( settings.wrapAround && Math.abs( distance ) > pathList.length / 2) { if ( destination > step) { distance = -step - pathList.length + destination; } else { distance = pathList.length - step + destination; } } animateSteps( distance, duration, easing, callback ); return this; } }; /* The Path object serves as a context to "draw" the scroll path on before initializing the plugin */ function Path( scrollS, rotateS ) { var PADDING = 40, scrollSpeed = scrollS, rotationSpeed = rotateS, xPos = 0, yPos = 0, rotation = 0, width = 0, height = 0, offsetX = 0, offsetY = 0, canvasPath = [{ method: "moveTo", args: [ 0, 0 ] }], // Needed if first path operation isn't a moveTo path = [], nameMap = {}, defaults = { rotate: null, callback: null, name: null }; /* Rotates the screen while staying in place */ this.rotate = function( radians, options ) { var settings = $.extend( {}, defaults, options ), rotDistance = Math.abs( radians - rotation ), steps = Math.round( rotDistance / rotationSpeed ) * STEP_SIZE, rotStep = ( radians - rotation ) / steps, i = 1; if ( !HAS_TRANSFORM_SUPPORT ) { if ( settings.name || settings.callback ) { // In case there was a name or callback set to this path, we add an extra step with those // so they don't get lost in browsers without rotation support this.moveTo(xPos, yPos, { callback: settings.callback, name: settings.name }); } return this; } for( ; i <= steps; i++ ) { path.push({ x: xPos, y: yPos, rotate: rotation + rotStep * i, callback: i === steps ? settings.callback : null }); } if( settings.name ) nameMap[ settings.name ] = path.length - 1; rotation = radians % ( Math.PI*2 ); return this; }; /* Moves (jumps) directly to the given point */ this.moveTo = function( x, y, options ) { var settings = $.extend( {}, defaults, options ), steps = path.length ? STEP_SIZE : 1; i = 0; for( ; i < steps; i++ ) { path.push({ x: x, y: y, rotate: settings.rotate !== null ? settings.rotate : rotation, callback: i === steps - 1 ? settings.callback : null }); } if( settings.name ) nameMap[ settings.name ] = path.length - 1; setPos( x, y ); updateCanvas( x, y ); canvasPath.push({ method: "moveTo", args: arguments }); return this; }; /* Draws a straight path to the given point */ this.lineTo = function( x, y, options ) { var settings = $.extend( {}, defaults, options ), relX = x - xPos, relY = y - yPos, distance = hypotenuse( relX, relY ), steps = Math.round( distance/scrollSpeed ) * STEP_SIZE, xStep = relX / steps, yStep = relY / steps, canRotate = settings.rotate !== null && HAS_TRANSFORM_SUPPORT, rotStep = ( canRotate ? ( settings.rotate - rotation ) / steps : 0 ), i = 1; for ( ; i <= steps; i++ ) { path.push({ x: xPos + xStep * i, y: yPos + yStep * i, rotate: rotation + rotStep * i, callback: i === steps ? settings.callback : null }); } if( settings.name ) nameMap[ settings.name ] = path.length - 1; rotation = ( canRotate ? settings.rotate : rotation ); setPos( x, y ); updateCanvas( x, y ); canvasPath.push({ method: "lineTo", args: arguments }); return this; }; /* Draws an arced path with a given circle center, radius, start and end angle. */ this.arc = function( centerX, centerY, radius, startAngle, endAngle, counterclockwise, options ) { var settings = $.extend( {}, defaults, options ), startX = centerX + Math.cos( startAngle ) * radius, startY = centerY + Math.sin( startAngle ) * radius, endX = centerX + Math.cos( endAngle ) * radius, endY = centerY + Math.sin( endAngle ) * radius, angleDistance = sectorAngle( startAngle, endAngle, counterclockwise ), distance = radius * angleDistance, steps = Math.round( distance/scrollSpeed ) * STEP_SIZE, radStep = angleDistance / steps * ( counterclockwise ? -1 : 1 ), canRotate = settings.rotate !== null && HAS_TRANSFORM_SUPPORT, rotStep = ( canRotate ? (settings.rotate - rotation) / steps : 0 ), i = 1; // If the arc starting point isn't the same as the end point of the preceding path, // prepend a line to the starting point. This is the default behavior when drawing on // a canvas. if ( xPos !== startX || yPos !== startY ) { this.lineTo( startX, startY ); } for ( ; i <= steps; i++ ) { path.push({ x: centerX + radius * Math.cos( startAngle + radStep*i ), y: centerY + radius * Math.sin( startAngle + radStep*i ), rotate: rotation + rotStep * i, callback: i === steps ? settings.callback : null }); } if( settings.name ) nameMap[ settings.name ] = path.length - 1; rotation = ( canRotate ? settings.rotate : rotation ); setPos( endX, endY ); updateCanvas( centerX + radius, centerY + radius ); updateCanvas( centerX - radius, centerY - radius ); canvasPath.push({ method: "arc", args: arguments }); return this; }; this.getPath = function() { return path; }; this.getNameMap = function() { return nameMap; }; /* Appends offsets to all x and y coordinates before returning the canvas path */ this.getCanvasPath = function() { var i = 0; for( ; i < canvasPath.length; i++ ) { canvasPath[ i ].args[ 0 ] -= this.getPathOffsetX(); canvasPath[ i ].args[ 1 ] -= this.getPathOffsetY(); } return canvasPath; }; this.getPathWidth = function() { return width - offsetX + PADDING; }; this.getPathHeight = function() { return height - offsetY + PADDING; }; this.getPathOffsetX = function() { return offsetX - PADDING / 2; }; this.getPathOffsetY = function() { return offsetY - PADDING / 2; }; /* Sets the current position */ function setPos( x, y ) { xPos = x; yPos = y; } /* Updates width and height, if needed */ function updateCanvas( x, y ) { offsetX = Math.min( x, offsetX ); offsetY = Math.min( y, offsetY ); width = Math.max( x, width ); height = Math.max( y, height ); } } /* Plugin wrapper, handles method calling */ $.fn.scrollPath = function( method ) { if ( methods[method] ) { return methods[ method ].apply( this, Array.prototype.slice.call( arguments, 1 ) ); } else if ( typeof method === "object" || !method ) { return methods.init.apply( this, arguments ); } else { $.error( "Method " + method + " does not exist on jQuery.scrollPath" ); } }; /* Initialize the scroll bar */ function initScrollBar() { if ( !settings.scrollBar ) return; // TODO: Holding down the mouse on the bar should "rapidfire", like holding down space scrollBar = $( "