//============================================================ // // the mit license // // copyright (c) 2014 matthew wagerfield - @wagerfield // // permission is hereby granted, free of charge, to any // person obtaining a copy of this software and associated // documentation files (the "software"), to deal in the // software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, // sublicense, and/or sell copies of the software, and to // permit persons to whom the software is furnished to do // so, subject to the following conditions: // // the above copyright notice and this permission notice // shall be included in all copies or substantial portions // of the software. // // the software is provided "as is", without warranty // of any kind, express or implied, including but not // limited to the warranties of merchantability, fitness // for a particular purpose and noninfringement. in no // event shall the authors or copyright holders be liable // for any claim, damages or other liability, whether in // an action of contract, tort or otherwise, arising from, // out of or in connection with the software or the use // or other dealings in the software. // //============================================================ /** * jquery || zepto parallax plugin * @author matthew wagerfield - @wagerfield * @description creates a parallax effect between an array of layers, * driving the motion from the gyroscope output of a smartdevice. * if no gyroscope is available, the cursor position is used. */ ;(function($, window, document, undefined) { // strict mode 'use strict'; // constants var name = 'parallax'; var magic_number = 30; var defaults = { relativeinput: false, cliprelativeinput: false, calibrationthreshold: 100, calibrationdelay: 500, supportdelay: 500, calibratex: false, calibratey: true, invertx: true, inverty: true, limitx: false, limity: false, scalarx: 10.0, scalary: 10.0, frictionx: 0.1, frictiony: 0.1, originx: 0.5, originy: 0.5 }; function plugin(element, options) { // dom context this.element = element; // selections this.$context = $(element).data('api', this); this.$layers = this.$context.find('.layer'); // data extraction var data = { calibratex: this.$context.data('calibrate-x') || null, calibratey: this.$context.data('calibrate-y') || null, invertx: this.$context.data('invert-x') || null, inverty: this.$context.data('invert-y') || null, limitx: parsefloat(this.$context.data('limit-x')) || null, limity: parsefloat(this.$context.data('limit-y')) || null, scalarx: parsefloat(this.$context.data('scalar-x')) || null, scalary: parsefloat(this.$context.data('scalar-y')) || null, frictionx: parsefloat(this.$context.data('friction-x')) || null, frictiony: parsefloat(this.$context.data('friction-y')) || null, originx: parsefloat(this.$context.data('origin-x')) || null, originy: parsefloat(this.$context.data('origin-y')) || null }; // delete null data values for (var key in data) { if (data[key] === null) delete data[key]; } // compose settings object $.extend(this, defaults, options, data); // states this.calibrationtimer = null; this.calibrationflag = true; this.enabled = false; this.depths = []; this.raf = null; // element bounds this.bounds = null; this.ex = 0; this.ey = 0; this.ew = 0; this.eh = 0; // element center this.ecx = 0; this.ecy = 0; // element range this.erx = 0; this.ery = 0; // calibration this.cx = 0; this.cy = 0; // input this.ix = 0; this.iy = 0; // motion this.mx = 0; this.my = 0; // velocity this.vx = 0; this.vy = 0; // callbacks this.onmousemove = this.onmousemove.bind(this); this.ondeviceorientation = this.ondeviceorientation.bind(this); this.onorientationtimer = this.onorientationtimer.bind(this); this.oncalibrationtimer = this.oncalibrationtimer.bind(this); this.onanimationframe = this.onanimationframe.bind(this); this.onwindowresize = this.onwindowresize.bind(this); // initialise this.initialise(); } plugin.prototype.transformsupport = function(value) { var element = document.createelement('div'); var propertysupport = false; var propertyvalue = null; var featuresupport = false; var cssproperty = null; var jsproperty = null; for (var i = 0, l = this.vendors.length; i < l; i++) { if (this.vendors[i] !== null) { cssproperty = this.vendors[i][0] + 'transform'; jsproperty = this.vendors[i][1] + 'transform'; } else { cssproperty = 'transform'; jsproperty = 'transform'; } if (element.style[jsproperty] !== undefined) { propertysupport = true; break; } } switch(value) { case '2d': featuresupport = propertysupport; break; case '3d': if (propertysupport) { var body = document.body || document.createelement('body'); var documentelement = document.documentelement; var documentoverflow = documentelement.style.overflow; if (!document.body) { documentelement.style.overflow = 'hidden'; documentelement.appendchild(body); body.style.overflow = 'hidden'; body.style.background = ''; } body.appendchild(element); element.style[jsproperty] = 'translate3d(1px,1px,1px)'; propertyvalue = window.getcomputedstyle(element).getpropertyvalue(cssproperty); featuresupport = propertyvalue !== undefined && propertyvalue.length > 0 && propertyvalue !== "none"; documentelement.style.overflow = documentoverflow; body.removechild(element); } break; } return featuresupport; }; plugin.prototype.ww = null; plugin.prototype.wh = null; plugin.prototype.wcx = null; plugin.prototype.wcy = null; plugin.prototype.wrx = null; plugin.prototype.wry = null; plugin.prototype.portrait = null; plugin.prototype.desktop = !navigator.useragent.match(/(iphone|ipod|ipad|android|blackberry|bb10|mobi|tablet|opera mini|nexus 7)/i); plugin.prototype.vendors = [null,['-webkit-','webkit'],['-moz-','moz'],['-o-','o'],['-ms-','ms']]; plugin.prototype.motionsupport = !!window.devicemotionevent; plugin.prototype.orientationsupport = !!window.deviceorientationevent; plugin.prototype.orientationstatus = 0; plugin.prototype.transform2dsupport = plugin.prototype.transformsupport('2d'); plugin.prototype.transform3dsupport = plugin.prototype.transformsupport('3d'); plugin.prototype.propertycache = {}; plugin.prototype.initialise = function() { // configure styles if (this.$context.css('position') === 'static') { this.$context.css({ position:'relative' }); } // hardware accelerate context this.accelerate(this.$context); // setup this.updatelayers(); this.updatedimensions(); this.enable(); this.queuecalibration(this.calibrationdelay); }; plugin.prototype.updatelayers = function() { // cache layer elements this.$layers = this.$context.find('.layer'); this.depths = []; // configure layer styles this.$layers.css({ position:'absolute', display:'block', left: 0, top: 0 }); this.$layers.first().css({ position:'relative' }); // hardware accelerate layers this.accelerate(this.$layers); // cache depths this.$layers.each($.proxy(function(index, element) { this.depths.push($(element).data('depth') || 0); }, this)); }; plugin.prototype.updatedimensions = function() { this.ww = window.innerwidth; this.wh = window.innerheight; this.wcx = this.ww * this.originx; this.wcy = this.wh * this.originy; this.wrx = math.max(this.wcx, this.ww - this.wcx); this.wry = math.max(this.wcy, this.wh - this.wcy); }; plugin.prototype.updatebounds = function() { this.bounds = this.element.getboundingclientrect(); this.ex = this.bounds.left; this.ey = this.bounds.top; this.ew = this.bounds.width; this.eh = this.bounds.height; this.ecx = this.ew * this.originx; this.ecy = this.eh * this.originy; this.erx = math.max(this.ecx, this.ew - this.ecx); this.ery = math.max(this.ecy, this.eh - this.ecy); }; plugin.prototype.queuecalibration = function(delay) { cleartimeout(this.calibrationtimer); this.calibrationtimer = settimeout(this.oncalibrationtimer, delay); }; plugin.prototype.enable = function() { if (!this.enabled) { this.enabled = true; if (this.orientationsupport) { this.portrait = null; this.$context.attr('data-mode', 'orientation'); window.addeventlistener('deviceorientation', this.ondeviceorientation); settimeout(this.onorientationtimer, this.supportdelay); } else { this.cx = 0; this.cy = 0; this.portrait = false; this.$context.attr('data-mode', 'cursor'); window.addeventlistener('mousemove', this.onmousemove); } window.addeventlistener('resize', this.onwindowresize); this.raf = requestanimationframe(this.onanimationframe); } }; plugin.prototype.disable = function() { if (this.enabled) { this.enabled = false; if (this.orientationsupport) { window.removeeventlistener('deviceorientation', this.ondeviceorientation); } else { window.removeeventlistener('mousemove', this.onmousemove); } window.removeeventlistener('resize', this.onwindowresize); cancelanimationframe(this.raf); } }; plugin.prototype.calibrate = function(x, y) { this.calibratex = x === undefined ? this.calibratex : x; this.calibratey = y === undefined ? this.calibratey : y; }; plugin.prototype.invert = function(x, y) { this.invertx = x === undefined ? this.invertx : x; this.inverty = y === undefined ? this.inverty : y; }; plugin.prototype.friction = function(x, y) { this.frictionx = x === undefined ? this.frictionx : x; this.frictiony = y === undefined ? this.frictiony : y; }; plugin.prototype.scalar = function(x, y) { this.scalarx = x === undefined ? this.scalarx : x; this.scalary = y === undefined ? this.scalary : y; }; plugin.prototype.limit = function(x, y) { this.limitx = x === undefined ? this.limitx : x; this.limity = y === undefined ? this.limity : y; }; plugin.prototype.origin = function(x, y) { this.originx = x === undefined ? this.originx : x; this.originy = y === undefined ? this.originy : y; }; plugin.prototype.clamp = function(value, min, max) { value = math.max(value, min); value = math.min(value, max); return value; }; plugin.prototype.css = function(element, property, value) { var jsproperty = this.propertycache[property]; if (!jsproperty) { for (var i = 0, l = this.vendors.length; i < l; i++) { if (this.vendors[i] !== null) { jsproperty = $.camelcase(this.vendors[i][1] + '-' + property); } else { jsproperty = property; } if (element.style[jsproperty] !== undefined) { this.propertycache[property] = jsproperty; break; } } } element.style[jsproperty] = value; }; plugin.prototype.accelerate = function($element) { for (var i = 0, l = $element.length; i < l; i++) { var element = $element[i]; this.css(element, 'transform', 'translate3d(0,0,0)'); this.css(element, 'transform-style', 'preserve-3d'); this.css(element, 'backface-visibility', 'hidden'); } }; plugin.prototype.setposition = function(element, x, y) { x += 'px'; y += 'px'; if (this.transform3dsupport) { this.css(element, 'transform', 'translate3d('+x+','+y+',0)'); } else if (this.transform2dsupport) { this.css(element, 'transform', 'translate('+x+','+y+')'); } else { element.style.left = x; element.style.top = y; } }; plugin.prototype.onorientationtimer = function(event) { if (this.orientationsupport && this.orientationstatus === 0) { this.disable(); this.orientationsupport = false; this.enable(); } }; plugin.prototype.oncalibrationtimer = function(event) { this.calibrationflag = true; }; plugin.prototype.onwindowresize = function(event) { this.updatedimensions(); }; plugin.prototype.onanimationframe = function() { this.updatebounds(); var dx = this.ix - this.cx; var dy = this.iy - this.cy; if ((math.abs(dx) > this.calibrationthreshold) || (math.abs(dy) > this.calibrationthreshold)) { this.queuecalibration(0); } if (this.portrait) { this.mx = this.calibratex ? dy : this.iy; this.my = this.calibratey ? dx : this.ix; } else { this.mx = this.calibratex ? dx : this.ix; this.my = this.calibratey ? dy : this.iy; } this.mx *= this.ew * (this.scalarx / 100); this.my *= this.eh * (this.scalary / 100); if (!isnan(parsefloat(this.limitx))) { this.mx = this.clamp(this.mx, -this.limitx, this.limitx); } if (!isnan(parsefloat(this.limity))) { this.my = this.clamp(this.my, -this.limity, this.limity); } this.vx += (this.mx - this.vx) * this.frictionx; this.vy += (this.my - this.vy) * this.frictiony; for (var i = 0, l = this.$layers.length; i < l; i++) { var depth = this.depths[i]; var layer = this.$layers[i]; var xoffset = this.vx * depth * (this.invertx ? -1 : 1); var yoffset = this.vy * depth * (this.inverty ? -1 : 1); this.setposition(layer, xoffset, yoffset); } this.raf = requestanimationframe(this.onanimationframe); }; plugin.prototype.ondeviceorientation = function(event) { // validate environment and event properties. if (!this.desktop && event.beta !== null && event.gamma !== null) { // set orientation status. this.orientationstatus = 1; // extract rotation var x = (event.beta || 0) / magic_number; // -90 :: 90 var y = (event.gamma || 0) / magic_number; // -180 :: 180 // detect orientation change var portrait = window.innerheight > window.innerwidth; if (this.portrait !== portrait) { this.portrait = portrait; this.calibrationflag = true; } // set calibration if (this.calibrationflag) { this.calibrationflag = false; this.cx = x; this.cy = y; } // set input this.ix = x; this.iy = y; } }; plugin.prototype.onmousemove = function(event) { // cache mouse coordinates. var clientx = event.clientx; var clienty = event.clienty; // calculate mouse input if (!this.orientationsupport && this.relativeinput) { // clip mouse coordinates inside element bounds. if (this.cliprelativeinput) { clientx = math.max(clientx, this.ex); clientx = math.min(clientx, this.ex + this.ew); clienty = math.max(clienty, this.ey); clienty = math.min(clienty, this.ey + this.eh); } // calculate input relative to the element. this.ix = (clientx - this.ex - this.ecx) / this.erx; this.iy = (clienty - this.ey - this.ecy) / this.ery; } else { // calculate input relative to the window. this.ix = (clientx - this.wcx) / this.wrx; this.iy = (clienty - this.wcy) / this.wry; } }; var api = { enable: plugin.prototype.enable, disable: plugin.prototype.disable, updatelayers: plugin.prototype.updatelayers, calibrate: plugin.prototype.calibrate, friction: plugin.prototype.friction, invert: plugin.prototype.invert, scalar: plugin.prototype.scalar, limit: plugin.prototype.limit, origin: plugin.prototype.origin }; $.fn[name] = function (value) { var args = arguments; return this.each(function () { var $this = $(this); var plugin = $this.data(name); if (!plugin) { plugin = new plugin(this, value); $this.data(name, plugin); } if (api[value]) { plugin[value].apply(plugin, array.prototype.slice.call(args, 1)); } }); }; })(window.jquery || window.zepto, window, document); /** * request animation frame polyfill. * @author tino zijdel * @author paul irish * @see https://gist.github.com/paulirish/1579671 */ ;(function() { var lasttime = 0; var vendors = ['ms', 'moz', 'webkit', 'o']; for(var x = 0; x < vendors.length && !window.requestanimationframe; ++x) { window.requestanimationframe = window[vendors[x]+'requestanimationframe']; window.cancelanimationframe = window[vendors[x]+'cancelanimationframe'] || window[vendors[x]+'cancelrequestanimationframe']; } if (!window.requestanimationframe) { window.requestanimationframe = function(callback, element) { var currtime = new date().gettime(); var timetocall = math.max(0, 16 - (currtime - lasttime)); var id = window.settimeout(function() { callback(currtime + timetocall); }, timetocall); lasttime = currtime + timetocall; return id; }; } if (!window.cancelanimationframe) { window.cancelanimationframe = function(id) { cleartimeout(id); }; } }());