XylotrechusZ
/*
* Parallax Support
* @since 5.0 added
*/
(function($)
{
"use strict";
$.avia_utilities = $.avia_utilities || {};
$( function()
{
//activate parallax scrolling for background images (sections) and objects.
if( $.fn.avia_parallax )
{
$( '.av-parallax,.av-parallax-object' ).avia_parallax();
}
});
/**
* Object Parallax scrolling feature
*
* - horizontal or vertical parallax for objects like images with responsive support
* - background image parallax (up to 5.0 handled by $.AviaParallaxElement) - was used for sections
*
* @since 5.0
* @param {} options
* @param {} element
*/
var AviaObjectParallaxElement = function( options, element )
{
// do not use in old browsers that do not support this CSS
if( ! ( this.transform || this.transform3d ) )
{
return;
}
this.options = $.extend( {}, options );
this.win = $( window );
this.body = $( 'body' );
this.isMobile = $.avia_utilities.isMobile, // not defined in constructor
this.winHeight = this.win.height();
this.winWidth = this.win.width();
this.el = $( element ).addClass( 'active-parallax' );
this.objectType = this.el.hasClass( 'av-parallax-object' ) ? 'object' : 'background-image';
this.elInner = this.el; // container to use for parallax calculation - may be smaller than element e.g. due to position rules
this.elBackgroundParent = this.el.parent(); // parent container when 'background-image' parallax
this.elParallax = this.el.data( 'parallax' ) || {};
this.direction = '';
this.speed = 0.5;
this.elProperty = {};
this.ticking = false,
this.isTransformed = false; // needed for responsive to reset transform when no parallax !!!
// set the browser transition string
if( $.avia_utilities.supported.transition === undefined )
{
$.avia_utilities.supported.transition = $.avia_utilities.supports( 'transition' );
}
this._init( options );
};
AviaObjectParallaxElement.prototype =
{
mediaQueries: {
'av-mini-': '(max-width: 479px)',
'av-small-': '(min-width: 480px) and (max-width: 767px)',
'av-medium-': '(min-width: 768px) and (max-width: 989px)',
'av-desktop-': '(min-width: 990px)'
},
transform: document.documentElement.className.indexOf( 'avia_transform' ) !== -1,
transform3d: document.documentElement.className.indexOf( 'avia_transform3d' ) !== -1,
mobileNoAnimation: $( 'body' ).hasClass( 'avia-mobile-no-animations' ),
defaultSpeed: 0.5,
defaultDirections: [ 'bottom_top', 'left_right', 'right_left', 'no_parallax' ],
transformCSSProps: [ 'transform', '-webkit-transform', '-moz-transform', '-ms-transform', '-o-transform' ],
matrixDef: [ 1, 0, 0, 1, 0, 0 ],
matrix3dDef: [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ],
_init: function()
{
var _self = this;
// select inner container if necessary for positioning
if( typeof this.el.data( 'parallax-selector') != 'undefined' && this.el.data( 'parallax-selector') !== '' )
{
this.elInner = this.el.find( this.el.data( 'parallax-selector' ) );
if( this.elInner.length == 0 )
{
this.elInner = this.el;
}
}
if( 'background-image' == this.objectType )
{
// shortcut and exit
if( this.isMobile && this.mobileNoAnimation )
{
return;
}
// by default we slow down
this.elParallax.parallax = 'bottom_top';
this.elParallax.parallax_speed = parseFloat( this.el.data( 'avia-parallax-ratio' ) ) || 0.5;
}
//fetch window constants
setTimeout( function()
{
_self._fetchProperties();
}, 30 );
this.win.on( 'debouncedresize av-height-change', _self._fetchProperties.bind( _self ) );
this.body.on( 'av_resize_finished', _self._fetchProperties.bind( _self ) );
//activate the scrolling
setTimeout( function()
{
_self.win.on( 'scroll', _self._onScroll.bind( _self ) );
}, 100 );
},
_setParallaxProps: function()
{
if( 'background-image' == this.objectType )
{
this.direction = this.elParallax.parallax;
this.speed = this.elParallax.parallax_speed;
return;
}
var all_direction = this.elParallax.parallax || '',
all_speed = this.elParallax.parallax_speed || '',
resp_direction = '',
resp_speed = '',
media = 'all';
if( this.defaultDirections.indexOf( all_direction ) < 0 )
{
all_direction = 'no_parallax';
}
if( typeof window.matchMedia == 'function' )
{
$.each( this.mediaQueries, function( key, query )
{
var mql = window.matchMedia( query );
if( mql.matches )
{
media = key;
return false;
}
});
}
if( 'all' == media )
{
this.direction = all_direction;
this.speed = '' == all_speed ? this.defaultSpeed : parseFloat( all_speed ) / 100.0;
return;
}
resp_direction = this.elParallax[ media + 'parallax' ] || '';
resp_speed = this.elParallax[ media + 'parallax_speed' ] || '';
if( 'inherit' == resp_direction )
{
resp_direction = all_direction;
resp_speed = all_speed;
}
if( this.defaultDirections.indexOf( resp_direction ) < 0 )
{
resp_direction = 'no_parallax';
}
this.direction = resp_direction;
this.speed = '' == resp_speed ? this.defaultSpeed : parseFloat( resp_speed ) / 100.0;
},
_getTranslateObject: function( element )
{
// https://zellwk.com/blog/css-translate-values-in-javascript/
// This function might not work properly if stacked transform operations are used - this is a limitation
var translate = {
type: '',
matrix: [],
x: 0,
y: 0,
z: 0
};
$.each( this.transformCSSProps, function( i, prop )
{
var found = element.css( prop );
if( 'string' != typeof found || 'none' == found )
{
return;
}
if( found.indexOf( 'matrix' ) >= 0 )
{
var matrixValues = found.match( /matrix.*\((.+)\)/)[1].split( ', ' );
if( found.indexOf( 'matrix3d' ) >= 0 )
{
translate.type = '3d';
translate.matrix = matrixValues;
// 3d have 16 values
translate.x = matrixValues[12];
translate.y = matrixValues[13];
translate.z = matrixValues[14];
}
else
{
translate.type = '2d';
translate.matrix = matrixValues;
// 2d have 6 values
translate.x = matrixValues[4];
translate.y = matrixValues[5];
}
return false;
}
else
{
translate.type = '';
// translateX
var matchX = found.match( /translateX\((-?\d+\.?\d*px)\)/ );
if( matchX )
{
translate.x = parseInt( matchX[1], 10 );
}
// translateY
var matchY = found.match( /translateY\((-?\d+\.?\d*px)\)/ );
if( matchY )
{
translate.y = parseInt( matchY[1], 10 );
}
}
});
return translate;
},
_getTranslateMatrix: function( translateObj, changes )
{
// matrix( a, b, c, d, tx, ty )
// matrix3d( a, b, 0, 0, c, d, 0, 0, 0, 0, 1, 0, tx, ty, 0, 1 )
var matrix = '';
$.each( changes, function( key, value )
{
translateObj[key] = value;
});
if( this.transform3d )
{
var matrix3d = this.matrix3dDef.slice( 0 );
switch( translateObj.type )
{
case '2d':
// 2d have 6 values
matrix3d[0] = translateObj.matrix[0];
matrix3d[1] = translateObj.matrix[1];
matrix3d[4] = translateObj.matrix[2];
matrix3d[5] = translateObj.matrix[3];
matrix3d[12] = translateObj.x;
matrix3d[13] = translateObj.y;
break;
case '3d':
// 3d have 16 values
matrix3d = translateObj.matrix.slice( 0 );
matrix3d[12] = translateObj.x;
matrix3d[13] = translateObj.y;
matrix3d[14] = translateObj.z;
break;
default:
matrix3d[12] = translateObj.x;
matrix3d[13] = translateObj.y;
break;
}
matrix = 'matrix3d(' + matrix3d.join( ', ' ) + ')';
}
else if( this.transform )
{
var matrix2d = this.matrixDef.slice( 0 );
switch( translateObj.type )
{
case '2d':
// 2d have 6 values
matrix2d = translateObj.matrix.slice( 0 );
matrix2d[4] = translateObj.x;
matrix2d[5] = translateObj.y;
break;
case '3d': // fallback only
// 3d have 16 values
matrix2d[0] = translateObj.matrix[0];
matrix2d[1] = translateObj.matrix[1];
matrix2d[2] = translateObj.matrix[4];
matrix2d[3] = translateObj.matrix[5];
matrix2d[4] = translateObj.x;
matrix2d[5] = translateObj.y;
break;
default:
matrix2d[4] = translateObj.x;
matrix2d[5] = translateObj.y;
break;
}
matrix = 'matrix(' + matrix2d.join( ', ' ) + ')';
}
return matrix;
},
_fetchProperties: function()
{
this._setParallaxProps();
// unset any added transform styles to get real CSS position before apply parallax transforms again
this.el.css( $.avia_utilities.supported.transition + 'transform', '' );
// cache values that only change on resize of viewport
this.winHeight = this.win.height();
this.winWidth = this.win.width();
if( 'background-image' == this.objectType )
{
// special case where we have a div with background image
this.elProperty.top = this.elBackgroundParent.offset().top;
this.elProperty.height = this.elBackgroundParent.outerHeight();
// set the height of the element based on the windows height, offset ratio and parent height
this.el.height( Math.ceil( ( this.winHeight * Math.abs( this.speed ) ) + this.elProperty.height ) );
}
else
{
this.elProperty.top = this.elInner.offset().top;
this.elProperty.left = this.elInner.offset().left;
this.elProperty.height = this.elInner.outerHeight();
this.elProperty.width = this.elInner.outerWidth();
this.elProperty.bottom = this.elProperty.top + this.elProperty.height;
this.elProperty.right = this.elProperty.left + this.elProperty.width;
this.elProperty.distanceLeft = this.elProperty.right;
this.elProperty.distanceRight = this.winWidth - this.elProperty.left;
}
// Save original position of element relative to container
this.elProperty.translateObj = this._getTranslateObject( this.el );
//re-position the element
this._parallaxScroll();
},
_onScroll: function( e )
{
var _self = this;
if( ! _self.ticking )
{
_self.ticking = true;
window.requestAnimationFrame( _self._parallaxRequest.bind( _self ) );
}
},
_inViewport: function( elTop, elRight, elBottom, elLeft, winTop, winBottom, winLeft, winRight )
{
// add a few pixel to be on safe side
return ! ( elTop > winBottom + 10 || elBottom < winTop - 10 || elLeft > winRight + 10 || elRight < winLeft - 10 );
},
_parallaxRequest: function( e )
{
// https://stackoverflow.com/questions/47184298/why-is-it-recommend-to-nest-settimeout-in-requestanimationframe-when-scheduling
var _self = this;
setTimeout( _self._parallaxScroll.bind( _self ), 0 );
},
_parallaxScroll: function( e )
{
// shortcut
if( ( 'no_parallax' == this.direction || '' == this.direction ) && ! this.isTransformed )
{
this.ticking = false;
return;
}
var winTop = this.win.scrollTop(),
winLeft = this.win.scrollLeft(),
winRight = winLeft + this.winWidth,
winBottom = winTop + this.winHeight,
scrollPos = 0,
matrix = '';
// special case where we have a div with background image - 'bottom_top'
if( 'background-image' == this.objectType )
{
// shift element when it moves into viewport
if( this.elProperty.top < winBottom && winTop <= this.elProperty.top + this.elProperty.height )
{
scrollPos = Math.ceil( ( winBottom - this.elProperty.top ) * this.speed );
matrix = this._getTranslateMatrix( this.elProperty.translateObj, { y: scrollPos } );
this.el.css( $.avia_utilities.supported.transition + 'transform', matrix );
}
this.ticking = false;
return;
}
// reset and shortcut
if( ( 'no_parallax' == this.direction || '' == this.direction ) )
{
matrix = this._getTranslateMatrix( this.elProperty.translateObj, { x: 0, y: 0 } );
this.el.css( $.avia_utilities.supported.transition + 'transform', matrix );
this.ticking = false;
this.isTransformed = false;
return;
}
// Get current coordinates for element
var scroll_px_toTop = Math.ceil( this.elProperty.top - winTop ),
scroll_px_el = Math.ceil( winBottom - this.elProperty.top ),
scrolled_pc_toTop = 0,
reduceDistanceX = 0,
transform = { x: 0, y: 0 };
// if element is initially in viewport on unscrolled screen we leave it and reduce distance to move
if( this.elProperty.top < this.winHeight )
{
reduceDistanceX = Math.ceil( this.winHeight - this.elProperty.top );
}
// Calculate transform value
if( this.elProperty.top > winBottom )
{
// element below viewport
scrolled_pc_toTop = 0;
scroll_px_el = 0;
}
else
{
// container is inside viewport or above ( scroll_px_toTop is negative)
scrolled_pc_toTop = 1 - ( scroll_px_toTop + reduceDistanceX ) / this.winHeight;
}
switch( this.direction )
{
case 'bottom_top':
scrollPos = Math.ceil( ( scroll_px_el - reduceDistanceX ) * this.speed );
transform.y = -scrollPos;
matrix = this._getTranslateMatrix( this.elProperty.translateObj, { y: -scrollPos } );
break;
case 'left_right':
scrollPos = Math.ceil( this.elProperty.distanceRight * scrolled_pc_toTop * this.speed );
transform.x = scrollPos;
matrix = this._getTranslateMatrix( this.elProperty.translateObj, { x: scrollPos } );
break;
case 'right_left':
scrollPos = Math.ceil( this.elProperty.distanceLeft * scrolled_pc_toTop * this.speed );
transform.x = -scrollPos;
matrix = this._getTranslateMatrix( this.elProperty.translateObj, { x: -scrollPos } );
break;
default:
break;
}
var elInViewport = this._inViewport( this.elProperty.top, this.elProperty.right, this.elProperty.bottom, this.elProperty.left, winTop, winBottom, winLeft, winRight ),
transformedInViewport = this._inViewport( this.elProperty.top + transform.y, this.elProperty.right + transform.x, this.elProperty.bottom + transform.y, this.elProperty.left + transform.x, winTop, winBottom, winLeft, winRight );
if( elInViewport || transformedInViewport )
{
this.el.css( $.avia_utilities.supported.transition + 'transform', matrix );
}
this.ticking = false;
this.isTransformed = true;
}
};
/**
* Wrapper to avoid double initialization of object
*
* @param {} options
*/
$.fn.avia_parallax = function( options )
{
// if( window.location.search.includes('new-parallax') ) //for testing 2 versions in 2 browser windows
// {
// return this;
// }
return this.each( function()
{
var obj = $( this );
var self = obj.data( 'aviaParallax' );
if( ! self )
{
self = obj.data( 'aviaParallax', new AviaObjectParallaxElement( options, this ) );
}
});
};
})( jQuery );