;/** * 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 }; 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' ) }; // 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.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(); } 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; 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; }; 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.transform2DSupport = Parallax.prototype.transformSupport( '2D' ); Parallax.prototype.transform3DSupport = Parallax.prototype.transformSupport( '3D' ); Parallax.prototype.propertyCache = {}; Parallax.prototype.initialise = function() { // 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'; } // 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.depths = []; // 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 this.depths.push( this.data( layer, 'depth' ) || 0 ); } }; 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.orientationSupport) { this.portrait = null; window.addEventListener( 'deviceorientation', this.onDeviceOrientation ); setTimeout( this.onOrientationTimer, 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 { 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 += '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; } }; Parallax.prototype.onOrientationTimer = function(event) { if (this.orientationSupport && this.orientationStatus === 0) { this.disable(); this.orientationSupport = false; this.enable(); } }; Parallax.prototype.onCalibrationTimer = function(event) { this.calibrationFlag = true; }; Parallax.prototype.onWindowResize = function(event) { 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 depth = this.depths[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 ); }; Parallax.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 = 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.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 );