1/** 2 * jQuery || Zepto Parallax Plugin 3 * @author Matthew Wagerfield - @wagerfield 4 * @description Creates a parallax effect between an array of layers, 5 * driving the motion from the gyroscope output of a smartdevice. 6 * If no gyroscope is available, the cursor position is used. 7 */ 8;(function($, window, document, undefined) { 9 10 // Strict Mode 11 'use strict'; 12 13 // Constants 14 var NAME = 'parallax'; 15 var MAGIC_NUMBER = 30; 16 var DEFAULTS = { 17 relativeInput: false, 18 clipRelativeInput: false, 19 calibrationThreshold: 100, 20 calibrationDelay: 500, 21 supportDelay: 500, 22 calibrateX: false, 23 calibrateY: true, 24 invertX: true, 25 invertY: true, 26 limitX: false, 27 limitY: false, 28 scalarX: 10.0, 29 scalarY: 10.0, 30 frictionX: 0.1, 31 frictionY: 0.1, 32 originX: 0.5, 33 originY: 0.5, 34 pointerEvents: true, 35 precision: 1 36 }; 37 38 function Plugin(element, options) { 39 40 // DOM Context 41 this.element = element; 42 43 // Selections 44 this.$context = $(element).data('api', this); 45 this.$layers = this.$context.find('.layer'); 46 47 // Data Extraction 48 var data = { 49 calibrateX: this.$context.data('calibrate-x') || null, 50 calibrateY: this.$context.data('calibrate-y') || null, 51 invertX: this.$context.data('invert-x') || null, 52 invertY: this.$context.data('invert-y') || null, 53 limitX: parseFloat(this.$context.data('limit-x')) || null, 54 limitY: parseFloat(this.$context.data('limit-y')) || null, 55 scalarX: parseFloat(this.$context.data('scalar-x')) || null, 56 scalarY: parseFloat(this.$context.data('scalar-y')) || null, 57 frictionX: parseFloat(this.$context.data('friction-x')) || null, 58 frictionY: parseFloat(this.$context.data('friction-y')) || null, 59 originX: parseFloat(this.$context.data('origin-x')) || null, 60 originY: parseFloat(this.$context.data('origin-y')) || null, 61 pointerEvents: this.$context.data('pointer-events') || true, 62 precision: parseFloat(this.$context.data('precision')) || 1 63 }; 64 65 // Delete Null Data Values 66 for (var key in data) { 67 if (data[key] === null) delete data[key]; 68 } 69 70 // Compose Settings Object 71 $.extend(this, DEFAULTS, options, data); 72 73 // States 74 this.calibrationTimer = null; 75 this.calibrationFlag = true; 76 this.enabled = false; 77 this.depthsX = []; 78 this.depthsY = []; 79 this.raf = null; 80 81 // Element Bounds 82 this.bounds = null; 83 this.ex = 0; 84 this.ey = 0; 85 this.ew = 0; 86 this.eh = 0; 87 88 // Element Center 89 this.ecx = 0; 90 this.ecy = 0; 91 92 // Element Range 93 this.erx = 0; 94 this.ery = 0; 95 96 // Calibration 97 this.cx = 0; 98 this.cy = 0; 99 100 // Input 101 this.ix = 0; 102 this.iy = 0; 103 104 // Motion 105 this.mx = 0; 106 this.my = 0; 107 108 // Velocity 109 this.vx = 0; 110 this.vy = 0; 111 112 // Callbacks 113 this.onMouseMove = this.onMouseMove.bind(this); 114 this.onDeviceOrientation = this.onDeviceOrientation.bind(this); 115 this.onOrientationTimer = this.onOrientationTimer.bind(this); 116 this.onCalibrationTimer = this.onCalibrationTimer.bind(this); 117 this.onAnimationFrame = this.onAnimationFrame.bind(this); 118 this.onWindowResize = this.onWindowResize.bind(this); 119 120 // Initialise 121 this.initialise(); 122 } 123 124 Plugin.prototype.transformSupport = function(value) { 125 var element = document.createElement('div'); 126 var propertySupport = false; 127 var propertyValue = null; 128 var featureSupport = false; 129 var cssProperty = null; 130 var jsProperty = null; 131 for (var i = 0, l = this.vendors.length; i < l; i++) { 132 if (this.vendors[i] !== null) { 133 cssProperty = this.vendors[i][0] + 'transform'; 134 jsProperty = this.vendors[i][1] + 'Transform'; 135 } else { 136 cssProperty = 'transform'; 137 jsProperty = 'transform'; 138 } 139 if (element.style[jsProperty] !== undefined) { 140 propertySupport = true; 141 break; 142 } 143 } 144 switch(value) { 145 case '2D': 146 featureSupport = propertySupport; 147 break; 148 case '3D': 149 if (propertySupport) { 150 var body = document.body || document.createElement('body'); 151 var documentElement = document.documentElement; 152 var documentOverflow = documentElement.style.overflow; 153 var isCreatedBody = false;
154 if (!document.body) { 155 isCreatedBody = true; 156 documentElement.style.overflow = 'hidden'; 157 documentElement.appendChild(body); 158 body.style.overflow = 'hidden'; 159 body.style.background = ''; 160 } 161 body.appendChild(element); 162 element.style[jsProperty] = 'translate3d(1px,1px,1px)'; 163 propertyValue = window.getComputedStyle(element).getPropertyValue(cssProperty); 164 featureSupport = propertyValue !== undefined && propertyValue.length > 0 && propertyValue !== "none"; 165 documentElement.style.overflow = documentOverflow; 166 body.removeChild(element); 167 if ( isCreatedBody ) { 168 body.removeAttribute('style'); 169 body.parentNode.removeChild(body); 170 } 171 } 172 break; 173 } 174 return featureSupport; 175 }; 176 177 Plugin.prototype.ww = null; 178 Plugin.prototype.wh = null; 179 Plugin.prototype.wcx = null; 180 Plugin.prototype.wcy = null; 181 Plugin.prototype.wrx = null; 182 Plugin.prototype.wry = null; 183 Plugin.prototype.portrait = null; 184 Plugin.prototype.desktop = !navigator.userAgent.match(/(iPhone|iPod|iPad|Android|BlackBerry|BB10|mobi|tablet|opera mini|nexus 7)/i); 185 Plugin.prototype.vendors = [null,['-webkit-','webkit'],['-moz-','Moz'],['-o-','O'],['-ms-','ms']]; 186 Plugin.prototype.motionSupport = !!window.DeviceMotionEvent; 187 Plugin.prototype.orientationSupport = !!window.DeviceOrientationEvent; 188 Plugin.prototype.orientationStatus = 0; 189 Plugin.prototype.transform2DSupport = Plugin.prototype.transformSupport('2D'); 190 Plugin.prototype.transform3DSupport = Plugin.prototype.transformSupport('3D'); 191 Plugin.prototype.propertyCache = {}; 192 193 Plugin.prototype.initialise = function() { 194 195 // Configure Styles 196 if (this.$context.css('position') === 'static') { 197 this.$context.css({ 198 position:'relative' 199 }); 200 } 201 202 // Pointer events 203 if(!this.pointerEvents){ 204 this.$context.css({ 205 pointerEvents: 'none' 206 }); 207 } 208 209 // Hardware Accelerate Context 210 this.accelerate(this.$context); 211 212 // Setup 213 this.updateLayers(); 214 this.updateDimensions(); 215 this.enable(); 216 this.queueCalibration(this.calibrationDelay); 217 }; 218 219 Plugin.prototype.updateLayers = function() { 220 221 // Cache Layer Elements 222 this.$layers = this.$context.find('.layer'); 223 this.depthsX = []; 224 this.depthsY = []; 225 226 // Configure Layer Styles 227 this.$layers.css({ 228 position:'absolute', 229 display:'block', 230 left: 0, 231 top: 0 232 }); 233 this.$layers.first().css({ 234 position:'relative' 235 }); 236 237 // Hardware Accelerate Layers 238 this.accelerate(this.$layers); 239 240 // Cache Depths 241 this.$layers.each($.proxy(function(index, element) { 242 //Graceful fallback on depth if depth-x or depth-y is absent 243 var depth = $(element).data('depth') || 0; 244 this.depthsX.push($(element).data('depth-x') || depth); 245 this.depthsY.push($(element).data('depth-y') || depth); 246 }, this)); 247 }; 248 249 Plugin.prototype.updateDimensions = function() { 250 this.ww = window.innerWidth; 251 this.wh = window.innerHeight; 252 this.wcx = this.ww * this.originX; 253 this.wcy = this.wh * this.originY; 254 this.wrx = Math.max(this.wcx, this.ww - this.wcx); 255 this.wry = Math.max(this.wcy, this.wh - this.wcy); 256 }; 257 258 Plugin.prototype.updateBounds = function() { 259 this.bounds = this.element.getBoundingClientRect(); 260 this.ex = this.bounds.left; 261 this.ey = this.bounds.top; 262 this.ew = this.bounds.width; 263 this.eh = this.bounds.height; 264 this.ecx = this.ew * this.originX; 265 this.ecy = this.eh * this.originY; 266 this.erx = Math.max(this.ecx, this.ew - this.ecx); 267 this.ery = Math.max(this.ecy, this.eh - this.ecy); 268 }; 269 270 Plugin.prototype.queueCalibration = function(delay) { 271 clearTimeout(this.calibrationTimer); 272 this.calibrationTimer = setTimeout(this.onCalibrationTimer, delay); 273 }; 274 275 Plugin.prototype.enable = function() { 276 if (!this.enabled) { 277 this.enabled = true; 278 if (this.orientationSupport) { 279 this.portrait = null; 280 window.addEventListener('deviceorientation', this.onDeviceOrientation); 281 setTimeout(this.onOrientationTimer, this.supportDelay); 282 } else { 283 this.cx = 0; 284 this.cy = 0; 285 this.portrait = false;
286 window.addEventListener('mousemove', this.onMouseMove); 287 } 288 window.addEventListener('resize', this.onWindowResize); 289 this.raf = requestAnimationFrame(this.onAnimationFrame); 290 } 291 }; 292 293 Plugin.prototype.disable = function() { 294 if (this.enabled) { 295 this.enabled = false; 296 if (this.orientationSupport) { 297 window.removeEventListener('deviceorientation', this.onDeviceOrientation); 298 } else { 299 window.removeEventListener('mousemove', this.onMouseMove); 300 } 301 window.removeEventListener('resize', this.onWindowResize); 302 cancelAnimationFrame(this.raf); 303 } 304 }; 305 306 Plugin.prototype.calibrate = function(x, y) { 307 this.calibrateX = x === undefined ? this.calibrateX : x; 308 this.calibrateY = y === undefined ? this.calibrateY : y; 309 }; 310 311 Plugin.prototype.invert = function(x, y) { 312 this.invertX = x === undefined ? this.invertX : x; 313 this.invertY = y === undefined ? this.invertY : y; 314 }; 315 316 Plugin.prototype.friction = function(x, y) { 317 this.frictionX = x === undefined ? this.frictionX : x; 318 this.frictionY = y === undefined ? this.frictionY : y; 319 }; 320 321 Plugin.prototype.scalar = function(x, y) { 322 this.scalarX = x === undefined ? this.scalarX : x; 323 this.scalarY = y === undefined ? this.scalarY : y; 324 }; 325 326 Plugin.prototype.limit = function(x, y) { 327 this.limitX = x === undefined ? this.limitX : x; 328 this.limitY = y === undefined ? this.limitY : y; 329 }; 330 331 Plugin.prototype.origin = function(x, y) { 332 this.originX = x === undefined ? this.originX : x; 333 this.originY = y === undefined ? this.originY : y; 334 }; 335 336 Plugin.prototype.clamp = function(value, min, max) { 337 value = Math.max(value, min); 338 value = Math.min(value, max); 339 return value; 340 }; 341 342 Plugin.prototype.css = function(element, property, value) { 343 var jsProperty = this.propertyCache[property]; 344 if (!jsProperty) { 345 for (var i = 0, l = this.vendors.length; i < l; i++) { 346 if (this.vendors[i] !== null) { 347 jsProperty = $.camelCase(this.vendors[i][1] + '-' + property); 348 } else { 349 jsProperty = property; 350 } 351 if (element.style[jsProperty] !== undefined) { 352 this.propertyCache[property] = jsProperty; 353 break; 354 } 355 } 356 } 357 element.style[jsProperty] = value; 358 }; 359 360 Plugin.prototype.accelerate = function($element) { 361 for (var i = 0, l = $element.length; i < l; i++) { 362 var element = $element[i]; 363 this.css(element, 'transform', 'translate3d(0,0,0)'); 364 this.css(element, 'transform-style', 'preserve-3d'); 365 this.css(element, 'backface-visibility', 'hidden'); 366 } 367 }; 368 369 Plugin.prototype.setPosition = function(element, x, y) { 370 x += 'px'; 371 y += 'px'; 372 if (this.transform3DSupport) { 373 this.css(element, 'transform', 'translate3d('+x+','+y+',0)'); 374 } else if (this.transform2DSupport) { 375 this.css(element, 'transform', 'translate('+x+','+y+')'); 376 } else { 377 element.style.left = x; 378 element.style.top = y; 379 } 380 }; 381 382 Plugin.prototype.onOrientationTimer = function(event) { 383 if (this.orientationSupport && this.orientationStatus === 0) { 384 this.disable(); 385 this.orientationSupport = false; 386 this.enable(); 387 } 388 }; 389 390 Plugin.prototype.onCalibrationTimer = function(event) { 391 this.calibrationFlag = true; 392 }; 393 394 Plugin.prototype.onWindowResize = function(event) { 395 this.updateDimensions(); 396 }; 397 398 Plugin.prototype.onAnimationFrame = function() { 399 this.updateBounds(); 400 var dx = this.ix - this.cx; 401 var dy = this.iy - this.cy; 402 if ((Math.abs(dx) > this.calibrationThreshold) || (Math.abs(dy) > this.calibrationThreshold)) { 403 this.queueCalibration(0); 404 } 405 if (this.portrait) { 406 this.mx = this.calibrateX ? dy : this.iy; 407 this.my = this.calibrateY ? dx : this.ix; 408 } else { 409 this.mx = this.calibrateX ? dx : this.ix; 410 this.my = this.calibrateY ? dy : this.iy; 411 } 412 this.mx *= this.ew * (this.scalarX / 100); 413 this.my *= this.eh * (this.scalarY / 100); 414 if (!isNaN(parseFloat(this.limitX))) { 415 this.mx = this.clamp(this.mx, -this.limitX, this.limitX); 416 } 417 if (!isNaN(parseFloat(this.limitY))) { 418 this.my = this.clamp(this.my, -this.limitY, this.limitY); 419 } 420 this.vx += (this.mx - this.vx) * this.frictionX; 421 this.vy += (this.my - this.vy) * this.frictionY; 422 for (var i = 0, l = this.$layers.length; i < l; i++) { 423 var depthX = this.depthsX[i]; 424 var depthY = this.depthsY[i]; 425 var layer = this.$layers[i]; 426 var xOffset = this.vx * (depthX * (this.invertX ? -1 : 1)); 427 var yOffset = this.vy * (depthY * (this.invertY ? -1 : 1)); 428 this.setPosition(layer, xOffset, yOffset); 429 } 430 this.raf = requestAnimationFrame(this.onAnimationFrame); 431 }; 432 433 Plugin.prototype.onDeviceOrientation = function(event) { 434 435 // Validate environment and event properties. 436 if (!this.desktop && event.beta !== null && event.gamma !== null) { 437 438 // Set orientation status. 439 this.orientationStatus = 1; 440 441 // Extract Rotation 442 var x = (event.beta || 0) / MAGIC_NUMBER; // -90 :: 90 443 var y = (event.gamma || 0) / MAGIC_NUMBER; // -180 :: 180 444 445 // Detect Orientation Change 446 var portrait = window.innerHeight > window.innerWidth; 447 if (this.portrait !== portrait) { 448 this.portrait = portrait; 449 this.calibrationFlag = true; 450 } 451 452 // Set Calibration 453 if (this.calibrationFlag) { 454 this.calibrationFlag = false;
455 this.cx = x; 456 this.cy = y; 457 } 458 459 // Set Input 460 this.ix = x; 461 this.iy = y; 462 } 463 }; 464 465 Plugin.prototype.onMouseMove = function(event) { 466 467 // Cache mouse coordinates. 468 var clientX = event.clientX; 469 var clientY = event.clientY; 470 471 // Calculate Mouse Input 472 if (!this.orientationSupport && this.relativeInput) { 473 474 // Clip mouse coordinates inside element bounds. 475 if (this.clipRelativeInput) { 476 clientX = Math.max(clientX, this.ex); 477 clientX = Math.min(clientX, this.ex + this.ew); 478 clientY = Math.max(clientY, this.ey); 479 clientY = Math.min(clientY, this.ey + this.eh); 480 } 481 482 // Calculate input relative to the element. 483 this.ix = (clientX - this.ex - this.ecx) / this.erx; 484 this.iy = (clientY - this.ey - this.ecy) / this.ery; 485 486 } else { 487 488 // Calculate input relative to the window. 489 this.ix = (clientX - this.wcx) / this.wrx; 490 this.iy = (clientY - this.wcy) / this.wry; 491 } 492 }; 493 494 var API = { 495 enable: Plugin.prototype.enable, 496 disable: Plugin.prototype.disable, 497 updateLayers: Plugin.prototype.updateLayers, 498 calibrate: Plugin.prototype.calibrate, 499 friction: Plugin.prototype.friction, 500 invert: Plugin.prototype.invert, 501 scalar: Plugin.prototype.scalar, 502 limit: Plugin.prototype.limit, 503 origin: Plugin.prototype.origin 504 }; 505 506 $.fn[NAME] = function (value) { 507 var args = arguments; 508 return this.each(function () { 509 var $this = $(this); 510 var plugin = $this.data(NAME); 511 if (!plugin) { 512 plugin = new Plugin(this, value); 513 $this.data(NAME, plugin); 514 } 515 if (API[value]) { 516 plugin[value].apply(plugin, Array.prototype.slice.call(args, 1)); 517 } 518 }); 519 }; 520 521})(window.jQuery || window.Zepto, window, document);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.