/** * parallax.js * @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, pointerevents: true, precision: 1 }; function parallax(element, options) { // dom context this.element = element; this.layers = element.getelementsbyclassname('layer'); // data extraction var data = { calibratex: this.data(this.element, 'calibrate-x'), calibratey: this.data(this.element, 'calibrate-y'), invertx: this.data(this.element, 'invert-x'), inverty: this.data(this.element, 'invert-y'), limitx: this.data(this.element, 'limit-x'), limity: this.data(this.element, 'limit-y'), scalarx: this.data(this.element, 'scalar-x'), scalary: this.data(this.element, 'scalar-y'), frictionx: this.data(this.element, 'friction-x'), frictiony: this.data(this.element, 'friction-y'), originx: this.data(this.element, 'origin-x'), originy: this.data(this.element, 'origin-y'), pointerevents: this.data(this.element, 'pointer-events'), precision: this.data(this.element, 'precision') }; // delete null data values for (var key in data) { if (data[key] === null) delete data[key]; } // compose settings object this.extend(this, defaults, options, data); // states this.calibrationtimer = null; this.calibrationflag = true; this.enabled = false; this.depthsx = []; this.depthsy = []; 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(); } parallax.prototype.extend = function() { if (arguments.length > 1) { var master = arguments[0]; for (var i = 1, l = arguments.length; i < l; i++) { var object = arguments[i]; for (var key in object) { master[key] = object[key]; } } } }; parallax.prototype.data = function(element, name) { return this.deserialize(element.getattribute('data-'+name)); }; parallax.prototype.deserialize = function(value) { if (value === 'true') { return true; } else if (value === 'false') { return false; } else if (value === 'null') { return null; } else if (!isnan(parsefloat(value)) && isfinite(value)) { return parsefloat(value); } else { return value; } }; parallax.prototype.camelcase = function(value) { return value.replace(/-+(.)?/g, function(match, character){ return character ? character.touppercase() : ''; }); }; parallax.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; var iscreatedbody = false; if (!document.body) { iscreatedbody = true; 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); if ( iscreatedbody ) { body.removeattribute('style'); body.parentnode.removechild(body); } } break; } return featuresupport; }; parallax.prototype.ww = null; parallax.prototype.wh = null; parallax.prototype.wcx = null; parallax.prototype.wcy = null; parallax.prototype.wrx = null; parallax.prototype.wry = null; parallax.prototype.portrait = null; parallax.prototype.desktop = !navigator.useragent.match(/(iphone|ipod|ipad|android|blackberry|bb10|mobi|tablet|opera mini|nexus 7)/i); parallax.prototype.vendors = [null,['-webkit-','webkit'],['-moz-','moz'],['-o-','o'],['-ms-','ms']]; parallax.prototype.motionsupport = !!window.devicemotionevent; parallax.prototype.orientationsupport = !!window.deviceorientationevent; parallax.prototype.orientationstatus = 0; parallax.prototype.motionstatus = 0; parallax.prototype.propertycache = {}; parallax.prototype.initialise = function() { if (parallax.prototype.transform2dsupport === undefined) { parallax.prototype.transform2dsupport = parallax.prototype.transformsupport('2d'); parallax.prototype.transform3dsupport = parallax.prototype.transformsupport('3d'); } // configure context styles if (this.transform3dsupport) this.accelerate(this.element); var style = window.getcomputedstyle(this.element); if (style.getpropertyvalue('position') === 'static') { this.element.style.position = 'relative'; } // pointer events if(!this.pointerevents){ this.element.style.pointerevents = 'none'; } // setup this.updatelayers(); this.updatedimensions(); this.enable(); this.queuecalibration(this.calibrationdelay); }; parallax.prototype.updatelayers = function() { // cache layer elements this.layers = this.element.getelementsbyclassname('layer'); this.depthsx = []; this.depthsy = []; // configure layer styles for (var i = 0, l = this.layers.length; i < l; i++) { var layer = this.layers[i]; if (this.transform3dsupport) this.accelerate(layer); layer.style.position = i ? 'absolute' : 'relative'; layer.style.display = 'block'; layer.style.left = 0; layer.style.top = 0; // cache layer depth //graceful fallback on depth if depth-x or depth-y is absent var depth = this.data(layer, 'depth') || 0; this.depthsx.push(this.data(layer, 'depth-x') || depth); this.depthsy.push(this.data(layer, 'depth-y') || depth); } }; parallax.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); }; parallax.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); }; parallax.prototype.queuecalibration = function(delay) { cleartimeout(this.calibrationtimer); this.calibrationtimer = settimeout(this.oncalibrationtimer, delay); }; parallax.prototype.enable = function() { if (!this.enabled) { this.enabled = true; if (!this.desktop && this.orientationsupport) { this.portrait = null; window.addeventlistener('deviceorientation', this.ondeviceorientation); settimeout(this.onorientationtimer, this.supportdelay); } else if (!this.desktop && this.motionsupport) { this.portrait = null; window.addeventlistener('devicemotion', this.ondevicemotion); settimeout(this.onmotiontimer, this.supportdelay); } else { this.cx = 0; this.cy = 0; this.portrait = false; window.addeventlistener('mousemove', this.onmousemove); } window.addeventlistener('resize', this.onwindowresize); this.raf = requestanimationframe(this.onanimationframe); } }; parallax.prototype.disable = function() { if (this.enabled) { this.enabled = false; if (this.orientationsupport) { window.removeeventlistener('deviceorientation', this.ondeviceorientation); } else if (this.motionsupport) { window.removeeventlistener('devicemotion', this.ondevicemotion); } else { window.removeeventlistener('mousemove', this.onmousemove); } window.removeeventlistener('resize', this.onwindowresize); cancelanimationframe(this.raf); } }; parallax.prototype.calibrate = function(x, y) { this.calibratex = x === undefined ? this.calibratex : x; this.calibratey = y === undefined ? this.calibratey : y; }; parallax.prototype.invert = function(x, y) { this.invertx = x === undefined ? this.invertx : x; this.inverty = y === undefined ? this.inverty : y; }; parallax.prototype.friction = function(x, y) { this.frictionx = x === undefined ? this.frictionx : x; this.frictiony = y === undefined ? this.frictiony : y; }; parallax.prototype.scalar = function(x, y) { this.scalarx = x === undefined ? this.scalarx : x; this.scalary = y === undefined ? this.scalary : y; }; parallax.prototype.limit = function(x, y) { this.limitx = x === undefined ? this.limitx : x; this.limity = y === undefined ? this.limity : y; }; parallax.prototype.origin = function(x, y) { this.originx = x === undefined ? this.originx : x; this.originy = y === undefined ? this.originy : y; }; parallax.prototype.clamp = function(value, min, max) { value = math.max(value, min); value = math.min(value, max); return value; }; parallax.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 = this.camelcase(this.vendors[i][1] + '-' + property); } else { jsproperty = property; } if (element.style[jsproperty] !== undefined) { this.propertycache[property] = jsproperty; break; } } } element.style[jsproperty] = value; }; parallax.prototype.accelerate = function(element) { this.css(element, 'transform', 'translate3d(0,0,0)'); this.css(element, 'transform-style', 'preserve-3d'); this.css(element, 'backface-visibility', 'hidden'); }; parallax.prototype.setposition = function(element, x, y) { x = x.tofixed(this.precision) + 'px'; y = y.tofixed(this.precision) + '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; } }; parallax.prototype.onorientationtimer = function() { if (this.orientationsupport && this.orientationstatus === 0) { this.disable(); this.orientationsupport = false; this.enable(); } }; parallax.prototype.onmotiontimer = function() { if (this.motionsupport && this.motionstatus === 0) { this.disable(); this.motionsupport = false; this.enable(); } }; parallax.prototype.oncalibrationtimer = function() { this.calibrationflag = true; }; parallax.prototype.onwindowresize = function() { this.updatedimensions(); }; parallax.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 layer = this.layers[i]; var depthx = this.depthsx[i]; var depthy = this.depthsy[i]; var xoffset = this.vx * (depthx * (this.invertx ? -1 : 1)); var yoffset = this.vy * (depthy * (this.inverty ? -1 : 1)); this.setposition(layer, xoffset, yoffset); } this.raf = requestanimationframe(this.onanimationframe); }; parallax.prototype.rotate = function(beta,gamma){ // 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 = this.wh > this.ww; 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; } parallax.prototype.ondeviceorientation = function(event) { // validate environment and event properties. var beta = event.beta; var gamma = event.gamma; if (!this.desktop && beta !== null && gamma !== null) { // set orientation status. this.orientationstatus = 1; this.rotate(beta,gamma); } }; parallax.prototype.ondevicemotion = function(event) { // validate environment and event properties. var beta = event.rotationrate.beta; var gamma = event.rotationrate.gamma; if (!this.desktop && beta !== null && gamma !== null) { // set motion status. this.motionstatus = 1; this.rotate(beta,gamma); } }; parallax.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; } }; // expose parallax window[name] = parallax; })(window, document);