// jquery.parallax.js // 2.0 // Stephen Band // // Project and documentation site: // webdev.stephband.info/jparallax/ // // Repository: // github.com/stephband/jparallax (function(jQuery, undefined) { // VAR var debug = true, options = { mouseport: 'body', // jQuery object or selector of DOM node to use as mouseport xparallax: true, // boolean | 0-1 | 'npx' | 'n%' yparallax: true, // xorigin: 0.1, // 0-1 - Sets default alignment. Only has effect when parallax values are something other than 1 (or true, or '100%') yorigin: 0.1, // decay: 0.22, // 0-1 (0 instant, 1 forever) - Sets rate of decay curve for catching up with target mouse position frameDuration: 30, // Int (milliseconds) freezeClass: 'freeze' // String - Class added to layer when frozen }, value = { left: 0, top: 0, middle: 0.1, center: 0.1, right: 1, bottom: 1 }, rpx = /^\d+\s?px$/, rpercent = /^\d+\s?%$/, win = jQuery(window), doc = jQuery(document), mouse = [0, 0]; var Timer = (function(){ var debug = false; // Shim for requestAnimationFrame, falling back to timer. See: // see http://paulirish.com/2011/requestanimationframe-for-smart-animating/ var requestFrame = (function(){ return ( window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function(fn, node){ return window.setTimeout(function(){ fn(); }, 25); } ); })(); function Timer() { var callbacks = [], nextFrame; function noop() {} function frame(){ var cbs = callbacks.slice(0), l = cbs.length, i = -1; if (debug) { console.log('timer frame()', l); } while(++i < l) { cbs[i].call(this); } requestFrame(nextFrame); } function start() { if (debug) { console.log('timer start()'); } this.start = noop; this.stop = stop; nextFrame = frame; requestFrame(nextFrame); } function stop() { if (debug) { console.log('timer stop()'); } this.start = start; this.stop = noop; nextFrame = noop; } this.callbacks = callbacks; this.start = start; this.stop = stop; } Timer.prototype = { add: function(fn) { var callbacks = this.callbacks, l = callbacks.length; // Check to see if this callback is already in the list. // Don't add it twice. while (l--) { if (callbacks[l] === fn) { return; } } this.callbacks.push(fn); if (debug) { console.log('timer add()', this.callbacks.length); } }, remove: function(fn) { var callbacks = this.callbacks, l = callbacks.length; // Remove all instances of this callback. while (l--) { if (callbacks[l] === fn) { callbacks.splice(l, 1); } } if (debug) { console.log('timer remove()', this.callbacks.length); } if (callbacks.length === 0) { this.stop(); } } }; return Timer; })(); function parseCoord(x) { return (rpercent.exec(x)) ? parseFloat(x)/100 : x; } function parseBool(x) { return typeof x === "boolean" ? x : !!( parseFloat(x) ) ; } function portData(port) { var events = { 'mouseenter.parallax': mouseenter, 'mouseleave.parallax': mouseleave }, winEvents = { 'resize.parallax': resize }, data = { elem: port, events: events, winEvents: winEvents, timer: new Timer() }, layers, size, offset; function updatePointer() { data.pointer = getPointer(mouse, [true, true], offset, size); } function resize() { size = getSize(port); offset = getOffset(port); data.threshold = getThreshold(size); } function mouseenter() { data.timer.add(updatePointer); } function mouseleave(e) { data.timer.remove(updatePointer); data.pointer = getPointer([e.pageX, e.pageY], [true, true], offset, size); } win.on(winEvents); port.on(events); resize(); return data; } function getData(elem, name, fn) { var data = elem.data(name); if (!data) { data = fn ? fn(elem) : {} ; elem.data(name, data); } return data; } function getPointer(mouse, parallax, offset, size){ var pointer = [], x = 2; while (x--) { pointer[x] = (mouse[x] - offset[x]) / size[x] ; pointer[x] = pointer[x] < 0 ? 0 : pointer[x] > 1 ? 1 : pointer[x] ; } return pointer; } function getSize(elem) { return [elem.width(), elem.height()]; } function getOffset(elem) { var offset = elem.offset() || {left: 0, top: 0}, borderLeft = elem.css('borderLeftStyle') === 'none' ? 0 : parseInt(elem.css('borderLeftWidth'), 10), borderTop = elem.css('borderTopStyle') === 'none' ? 0 : parseInt(elem.css('borderTopWidth'), 10), paddingLeft = parseInt(elem.css('paddingLeft'), 10), paddingTop = parseInt(elem.css('paddingTop'), 10); return [offset.left + borderLeft + paddingLeft, offset.top + borderTop + paddingTop]; } function getThreshold(size) { return [1/size[0], 1/size[1]]; } function layerSize(elem, x, y) { return [x || elem.outerWidth(), y || elem.outerHeight()]; } function layerOrigin(xo, yo) { var o = [xo, yo], i = 2, origin = []; while (i--) { origin[i] = typeof o[i] === 'string' ? o[i] === undefined ? 1 : value[origin[i]] || parseCoord(origin[i]) : o[i] ; } return origin; } function layerPx(xp, yp) { return [rpx.test(xp), rpx.test(yp)]; } function layerParallax(xp, yp, px) { var p = [xp, yp], i = 2, parallax = []; while (i--) { parallax[i] = px[i] ? parseInt(p[i], 10) : parallax[i] = p[i] === true ? 1 : parseCoord(p[i]) ; } return parallax; } function layerOffset(parallax, px, origin, size) { var i = 2, offset = []; while (i--) { offset[i] = px[i] ? origin[i] * (size[i] - parallax[i]) : parallax[i] ? origin[i] * ( 1 - parallax[i] ) : 0 ; } return offset; } function layerPosition(px, origin) { var i = 2, position = []; while (i--) { if (px[i]) { // Set css position constant position[i] = origin[i] * 100 + '%'; } else { } } return position; } function layerPointer(elem, parallax, px, offset, size) { var viewport = elem.offsetParent(), pos = elem.position(), position = [], pointer = [], i = 2; // Reverse calculate ratio from elem's current position while (i--) { position[i] = px[i] ? // TODO: reverse calculation for pixel case 0 : pos[i === 0 ? 'left' : 'top'] / (viewport[i === 0 ? 'outerWidth' : 'outerHeight']() - size[i]) ; pointer[i] = (position[i] - offset[i]) / parallax[i] ; } return pointer; } function layerCss(parallax, px, offset, size, position, pointer) { var pos = [], cssPosition, cssMargin, x = 2, css = {}; while (x--) { if (parallax[x]) { pos[x] = parallax[x] * pointer[x] + offset[x]; // We're working in pixels if (px[x]) { cssPosition = position[x]; cssMargin = pos[x] * -1; } // We're working by ratio else { cssPosition = pos[x] * 100 + '%'; cssMargin = pos[x] * size[x] * -1; } // Fill in css object if (x === 0) { css.left = cssPosition; css.marginLeft = cssMargin; } else { css.top = cssPosition; css.marginTop = cssMargin; } } } return css; } function pointerOffTarget(targetPointer, prevPointer, threshold, decay, parallax, targetFn, updateFn) { var pointer, x; if ((!parallax[0] || Math.abs(targetPointer[0] - prevPointer[0]) < threshold[0]) && (!parallax[1] || Math.abs(targetPointer[1] - prevPointer[1]) < threshold[1])) { // Pointer has hit the target if (targetFn) { targetFn(); } return updateFn(targetPointer); } // Pointer is nowhere near the target pointer = []; x = 2; while (x--) { if (parallax[x]) { pointer[x] = targetPointer[x] + decay * (prevPointer[x] - targetPointer[x]); } } return updateFn(pointer); } function pointerOnTarget(targetPointer, prevPointer, threshold, decay, parallax, targetFn, updateFn) { // Don't bother updating if the pointer hasn't changed. if (targetPointer[0] === prevPointer[0] && targetPointer[1] === prevPointer[1]) { return; } return updateFn(targetPointer); } function unport(elem, events, winEvents) { elem.off(events).removeData('parallax_port'); win.off(winEvents); } function unparallax(node, port, events) { port.elem.off(events); // Remove this node from layers port.layers = port.layers.not(node); // If port.layers is empty, destroy the port if (port.layers.length === 0) { unport(port.elem, port.events, port.winEvents); } } function unstyle(parallax) { var css = {}; if (parallax[0]) { css.left = ''; css.marginLeft = ''; } if (parallax[1]) { css.top = ''; css.marginTop = ''; } elem.css(css); } jQuery.fn.parallax = function(o){ var options = jQuery.extend({}, jQuery.fn.parallax.options, o), args = arguments, elem = options.mouseport instanceof jQuery ? options.mouseport : jQuery(options.mouseport) , port = getData(elem, 'parallax_port', portData), timer = port.timer; return this.each(function(i) { var node = this, elem = jQuery(this), opts = args[i + 1] ? jQuery.extend({}, options, args[i + 1]) : options, decay = opts.decay, size = layerSize(elem, opts.width, opts.height), origin = layerOrigin(opts.xorigin, opts.yorigin), px = layerPx(opts.xparallax, opts.yparallax), parallax = layerParallax(opts.xparallax, opts.yparallax, px), offset = layerOffset(parallax, px, origin, size), position = layerPosition(px, origin), pointer = layerPointer(elem, parallax, px, offset, size), pointerFn = pointerOffTarget, targetFn = targetInside, events = { 'mouseenter.parallax': function mouseenter(e) { pointerFn = pointerOffTarget; targetFn = targetInside; timer.add(frame); timer.start(); }, 'mouseleave.parallax': function mouseleave(e) { // Make the layer come to rest at it's limit with inertia pointerFn = pointerOffTarget; // Stop the timer when the the pointer hits target targetFn = targetOutside; } }; function updateCss(newPointer) { var css = layerCss(parallax, px, offset, size, position, newPointer); elem.css(css); pointer = newPointer; } function frame() { pointerFn(port.pointer, pointer, port.threshold, decay, parallax, targetFn, updateCss); } function targetInside() { // Pointer hits the target pointer inside the port pointerFn = pointerOnTarget; } function targetOutside() { // Pointer hits the target pointer outside the port timer.remove(frame); } if (jQuery.data(node, 'parallax')) { elem.unparallax(); } jQuery.data(node, 'parallax', { port: port, events: events, parallax: parallax }); port.elem.on(events); port.layers = port.layers? port.layers.add(node): jQuery(node); /*function freeze() { freeze = true; } function unfreeze() { freeze = false; }*/ /*jQuery.event.add(this, 'freeze.parallax', freeze); jQuery.event.add(this, 'unfreeze.parallax', unfreeze);*/ }); }; jQuery.fn.unparallax = function(bool) { return this.each(function() { var data = jQuery.data(this, 'parallax'); // This elem is not parallaxed if (!data) { return; } jQuery.removeData(this, 'parallax'); unparallax(this, data.port, data.events); if (bool) { unstyle(data.parallax); } }); }; jQuery.fn.parallax.options = options; // Pick up and store mouse position on document: IE does not register // mousemove on window. doc.on('mousemove.parallax', function(e){ mouse = [e.pageX, e.pageY]; }); }(jQuery));