// jquery steller js // ;(function($, window, document, undefined) { var pluginname = 'stellar', defaults = { scrollproperty: 'scroll', positionproperty: 'position', horizontalscrolling: true, verticalscrolling: true, horizontaloffset: 0, verticaloffset: 0, responsive: false, parallaxbackgrounds: true, parallaxelements: true, hidedistantelements: true, hideelement: function($elem) { $elem.hide(); }, showelement: function($elem) { $elem.show(); } }, scrollproperty = { scroll: { getleft: function($elem) { return $elem.scrollleft(); }, setleft: function($elem, val) { $elem.scrollleft(val); }, gettop: function($elem) { return $elem.scrolltop(); }, settop: function($elem, val) { $elem.scrolltop(val); } }, position: { getleft: function($elem) { return parseint($elem.css('left'), 10) * -1; }, gettop: function($elem) { return parseint($elem.css('top'), 10) * -1; } }, margin: { getleft: function($elem) { return parseint($elem.css('margin-left'), 10) * -1; }, gettop: function($elem) { return parseint($elem.css('margin-top'), 10) * -1; } }, transform: { getleft: function($elem) { var computedtransform = getcomputedstyle($elem[0])[prefixedtransform]; return (computedtransform !== 'none' ? parseint(computedtransform.match(/(-?[0-9]+)/g)[4], 10) * -1 : 0); }, gettop: function($elem) { var computedtransform = getcomputedstyle($elem[0])[prefixedtransform]; return (computedtransform !== 'none' ? parseint(computedtransform.match(/(-?[0-9]+)/g)[5], 10) * -1 : 0); } } }, positionproperty = { position: { setleft: function($elem, left) { $elem.css('left', left); }, settop: function($elem, top) { $elem.css('top', top); } }, transform: { setposition: function($elem, left, startingleft, top, startingtop) { $elem[0].style[prefixedtransform] = 'translate3d(' + (left - startingleft) + 'px, ' + (top - startingtop) + 'px, 0)'; } } }, // returns a function which adds a vendor prefix to any css property name vendorprefix = (function() { var prefixes = /^(moz|webkit|khtml|o|ms|icab)(?=[a-z])/, style = $('script')[0].style, prefix = '', prop; for (prop in style) { if (prefixes.test(prop)) { prefix = prop.match(prefixes)[0]; break; } } if ('webkitopacity' in style) { prefix = 'webkit'; } if ('khtmlopacity' in style) { prefix = 'khtml'; } return function(property) { return prefix + (prefix.length > 0 ? property.charat(0).touppercase() + property.slice(1) : property); }; }()), prefixedtransform = vendorprefix('transform'), supportsbackgroundpositionxy = $('
', { style: 'background:#fff' }).css('background-position-x') !== undefined, setbackgroundposition = (supportsbackgroundpositionxy ? function($elem, x, y) { $elem.css({ 'background-position-x': x, 'background-position-y': y }); } : function($elem, x, y) { $elem.css('background-position', x + ' ' + y); } ), getbackgroundposition = (supportsbackgroundpositionxy ? function($elem) { return [ $elem.css('background-position-x'), $elem.css('background-position-y') ]; } : function($elem) { return $elem.css('background-position').split(' '); } ), requestanimframe = ( window.requestanimationframe || window.webkitrequestanimationframe || window.mozrequestanimationframe || window.orequestanimationframe || window.msrequestanimationframe || function(callback) { settimeout(callback, 1000 / 60); } ); function plugin(element, options) { this.element = element; this.options = $.extend({}, defaults, options); this._defaults = defaults; this._name = pluginname; this.init(); } plugin.prototype = { init: function() { this.options.name = pluginname + '_' + math.floor(math.random() * 1e9); this._defineelements(); this._definegetters(); this._definesetters(); this._handlewindowloadandresize(); this._detectviewport(); this.refresh({ firstload: true }); if (this.options.scrollproperty === 'scroll') { this._handlescrollevent(); } else { this._startanimationloop(); } }, _defineelements: function() { if (this.element === document.body) this.element = window; this.$scrollelement = $(this.element); this.$element = (this.element === window ? $('body') : this.$scrollelement); this.$viewportelement = (this.options.viewportelement !== undefined ? $(this.options.viewportelement) : (this.$scrollelement[0] === window || this.options.scrollproperty === 'scroll' ? this.$scrollelement : this.$scrollelement.parent()) ); }, _definegetters: function() { var self = this, scrollpropertyadapter = scrollproperty[self.options.scrollproperty]; this._getscrollleft = function() { return scrollpropertyadapter.getleft(self.$scrollelement); }; this._getscrolltop = function() { return scrollpropertyadapter.gettop(self.$scrollelement); }; }, _definesetters: function() { var self = this, scrollpropertyadapter = scrollproperty[self.options.scrollproperty], positionpropertyadapter = positionproperty[self.options.positionproperty], setscrollleft = scrollpropertyadapter.setleft, setscrolltop = scrollpropertyadapter.settop; this._setscrollleft = (typeof setscrollleft === 'function' ? function(val) { setscrollleft(self.$scrollelement, val); } : $.noop); this._setscrolltop = (typeof setscrolltop === 'function' ? function(val) { setscrolltop(self.$scrollelement, val); } : $.noop); this._setposition = positionpropertyadapter.setposition || function($elem, left, startingleft, top, startingtop) { if (self.options.horizontalscrolling) { positionpropertyadapter.setleft($elem, left, startingleft); } if (self.options.verticalscrolling) { positionpropertyadapter.settop($elem, top, startingtop); } }; }, _handlewindowloadandresize: function() { var self = this, $window = $(window); if (self.options.responsive) { $window.bind('load.' + this.name, function() { self.refresh(); }); } $window.bind('resize.' + this.name, function() { self._detectviewport(); if (self.options.responsive) { self.refresh(); } }); }, refresh: function(options) { var self = this, oldleft = self._getscrollleft(), oldtop = self._getscrolltop(); if (!options || !options.firstload) { this._reset(); } this._setscrollleft(0); this._setscrolltop(0); this._setoffsets(); this._findparticles(); this._findbackgrounds(); // fix for webkit background rendering bug if (options && options.firstload && /webkit/.test(navigator.useragent)) { $(window).load(function() { var oldleft = self._getscrollleft(), oldtop = self._getscrolltop(); self._setscrollleft(oldleft + 1); self._setscrolltop(oldtop + 1); self._setscrollleft(oldleft); self._setscrolltop(oldtop); }); } this._setscrollleft(oldleft); this._setscrolltop(oldtop); }, _detectviewport: function() { var viewportoffsets = this.$viewportelement.offset(), hasoffsets = viewportoffsets !== null && viewportoffsets !== undefined; this.viewportwidth = this.$viewportelement.width(); this.viewportheight = this.$viewportelement.height(); this.viewportoffsettop = (hasoffsets ? viewportoffsets.top : 0); this.viewportoffsetleft = (hasoffsets ? viewportoffsets.left : 0); }, _findparticles: function() { var self = this, scrollleft = this._getscrollleft(), scrolltop = this._getscrolltop(); if (this.particles !== undefined) { for (var i = this.particles.length - 1; i >= 0; i--) { this.particles[i].$element.data('stellar-elementisactive', undefined); } } this.particles = []; if (!this.options.parallaxelements) return; this.$element.find('[data-stellar-ratio]').each(function(i) { var $this = $(this), horizontaloffset, verticaloffset, positionleft, positiontop, marginleft, margintop, $offsetparent, offsetleft, offsettop, parentoffsetleft = 0, parentoffsettop = 0, tempparentoffsetleft = 0, tempparentoffsettop = 0; // ensure this element isn't already part of another scrolling element if (!$this.data('stellar-elementisactive')) { $this.data('stellar-elementisactive', this); } else if ($this.data('stellar-elementisactive') !== this) { return; } self.options.showelement($this); // save/restore the original top and left css values in case we refresh the particles or destroy the instance if (!$this.data('stellar-startingleft')) { $this.data('stellar-startingleft', $this.css('left')); $this.data('stellar-startingtop', $this.css('top')); } else { $this.css('left', $this.data('stellar-startingleft')); $this.css('top', $this.data('stellar-startingtop')); } positionleft = $this.position().left; positiontop = $this.position().top; // catch-all for margin top/left properties (these evaluate to 'auto' in ie7 and ie8) marginleft = ($this.css('margin-left') === 'auto') ? 0 : parseint($this.css('margin-left'), 10); margintop = ($this.css('margin-top') === 'auto') ? 0 : parseint($this.css('margin-top'), 10); offsetleft = $this.offset().left - marginleft; offsettop = $this.offset().top - margintop; // calculate the offset parent $this.parents().each(function() { var $this = $(this); if ($this.data('stellar-offset-parent') === true) { parentoffsetleft = tempparentoffsetleft; parentoffsettop = tempparentoffsettop; $offsetparent = $this; return false; } else { tempparentoffsetleft += $this.position().left; tempparentoffsettop += $this.position().top; } }); // detect the offsets horizontaloffset = ($this.data('stellar-horizontal-offset') !== undefined ? $this.data('stellar-horizontal-offset') : ($offsetparent !== undefined && $offsetparent.data('stellar-horizontal-offset') !== undefined ? $offsetparent.data('stellar-horizontal-offset') : self.horizontaloffset)); verticaloffset = ($this.data('stellar-vertical-offset') !== undefined ? $this.data('stellar-vertical-offset') : ($offsetparent !== undefined && $offsetparent.data('stellar-vertical-offset') !== undefined ? $offsetparent.data('stellar-vertical-offset') : self.verticaloffset)); // add our object to the particles collection self.particles.push({ $element: $this, $offsetparent: $offsetparent, isfixed: $this.css('position') === 'fixed', horizontaloffset: horizontaloffset, verticaloffset: verticaloffset, startingpositionleft: positionleft, startingpositiontop: positiontop, startingoffsetleft: offsetleft, startingoffsettop: offsettop, parentoffsetleft: parentoffsetleft, parentoffsettop: parentoffsettop, stellarratio: ($this.data('stellar-ratio') !== undefined ? $this.data('stellar-ratio') : 1), width: $this.outerwidth(true), height: $this.outerheight(true), ishidden: false }); }); }, _findbackgrounds: function() { var self = this, scrollleft = this._getscrollleft(), scrolltop = this._getscrolltop(), $backgroundelements; this.backgrounds = []; if (!this.options.parallaxbackgrounds) return; $backgroundelements = this.$element.find('[data-stellar-background-ratio]'); if (this.$element.data('stellar-background-ratio')) { $backgroundelements = $backgroundelements.add(this.$element); } $backgroundelements.each(function() { var $this = $(this), backgroundposition = getbackgroundposition($this), horizontaloffset, verticaloffset, positionleft, positiontop, marginleft, margintop, offsetleft, offsettop, $offsetparent, parentoffsetleft = 0, parentoffsettop = 0, tempparentoffsetleft = 0, tempparentoffsettop = 0; // ensure this element isn't already part of another scrolling element if (!$this.data('stellar-backgroundisactive')) { $this.data('stellar-backgroundisactive', this); } else if ($this.data('stellar-backgroundisactive') !== this) { return; } // save/restore the original top and left css values in case we destroy the instance if (!$this.data('stellar-backgroundstartingleft')) { $this.data('stellar-backgroundstartingleft', backgroundposition[0]); $this.data('stellar-backgroundstartingtop', backgroundposition[1]); } else { setbackgroundposition($this, $this.data('stellar-backgroundstartingleft'), $this.data('stellar-backgroundstartingtop')); } // catch-all for margin top/left properties (these evaluate to 'auto' in ie7 and ie8) marginleft = ($this.css('margin-left') === 'auto') ? 0 : parseint($this.css('margin-left'), 10); margintop = ($this.css('margin-top') === 'auto') ? 0 : parseint($this.css('margin-top'), 10); offsetleft = $this.offset().left - marginleft - scrollleft; offsettop = $this.offset().top - margintop - scrolltop; // calculate the offset parent $this.parents().each(function() { var $this = $(this); if ($this.data('stellar-offset-parent') === true) { parentoffsetleft = tempparentoffsetleft; parentoffsettop = tempparentoffsettop; $offsetparent = $this; return false; } else { tempparentoffsetleft += $this.position().left; tempparentoffsettop += $this.position().top; } }); // detect the offsets horizontaloffset = ($this.data('stellar-horizontal-offset') !== undefined ? $this.data('stellar-horizontal-offset') : ($offsetparent !== undefined && $offsetparent.data('stellar-horizontal-offset') !== undefined ? $offsetparent.data('stellar-horizontal-offset') : self.horizontaloffset)); verticaloffset = ($this.data('stellar-vertical-offset') !== undefined ? $this.data('stellar-vertical-offset') : ($offsetparent !== undefined && $offsetparent.data('stellar-vertical-offset') !== undefined ? $offsetparent.data('stellar-vertical-offset') : self.verticaloffset)); self.backgrounds.push({ $element: $this, $offsetparent: $offsetparent, isfixed: $this.css('background-attachment') === 'fixed', horizontaloffset: horizontaloffset, verticaloffset: verticaloffset, startingvalueleft: backgroundposition[0], startingvaluetop: backgroundposition[1], startingbackgroundpositionleft: (isnan(parseint(backgroundposition[0], 10)) ? 0 : parseint(backgroundposition[0], 10)), startingbackgroundpositiontop: (isnan(parseint(backgroundposition[1], 10)) ? 0 : parseint(backgroundposition[1], 10)), startingpositionleft: $this.position().left, startingpositiontop: $this.position().top, startingoffsetleft: offsetleft, startingoffsettop: offsettop, parentoffsetleft: parentoffsetleft, parentoffsettop: parentoffsettop, stellarratio: ($this.data('stellar-background-ratio') === undefined ? 1 : $this.data('stellar-background-ratio')) }); }); }, _reset: function() { var particle, startingpositionleft, startingpositiontop, background, i; for (i = this.particles.length - 1; i >= 0; i--) { particle = this.particles[i]; startingpositionleft = particle.$element.data('stellar-startingleft'); startingpositiontop = particle.$element.data('stellar-startingtop'); this._setposition(particle.$element, startingpositionleft, startingpositionleft, startingpositiontop, startingpositiontop); this.options.showelement(particle.$element); particle.$element.data('stellar-startingleft', null).data('stellar-elementisactive', null).data('stellar-backgroundisactive', null); } for (i = this.backgrounds.length - 1; i >= 0; i--) { background = this.backgrounds[i]; background.$element.data('stellar-backgroundstartingleft', null).data('stellar-backgroundstartingtop', null); setbackgroundposition(background.$element, background.startingvalueleft, background.startingvaluetop); } }, destroy: function() { this._reset(); this.$scrollelement.unbind('resize.' + this.name).unbind('scroll.' + this.name); this._animationloop = $.noop; $(window).unbind('load.' + this.name).unbind('resize.' + this.name); }, _setoffsets: function() { var self = this, $window = $(window); $window.unbind('resize.horizontal-' + this.name).unbind('resize.vertical-' + this.name); if (typeof this.options.horizontaloffset === 'function') { this.horizontaloffset = this.options.horizontaloffset(); $window.bind('resize.horizontal-' + this.name, function() { self.horizontaloffset = self.options.horizontaloffset(); }); } else { this.horizontaloffset = this.options.horizontaloffset; } if (typeof this.options.verticaloffset === 'function') { this.verticaloffset = this.options.verticaloffset(); $window.bind('resize.vertical-' + this.name, function() { self.verticaloffset = self.options.verticaloffset(); }); } else { this.verticaloffset = this.options.verticaloffset; } }, _repositionelements: function() { var scrollleft = this._getscrollleft(), scrolltop = this._getscrolltop(), horizontaloffset, verticaloffset, particle, fixedratiooffset, background, bgleft, bgtop, isvisiblevertical = true, isvisiblehorizontal = true, newpositionleft, newpositiontop, newoffsetleft, newoffsettop, i; // first check that the scroll position or container size has changed if (this.currentscrollleft === scrollleft && this.currentscrolltop === scrolltop && this.currentwidth === this.viewportwidth && this.currentheight === this.viewportheight) { return; } else { this.currentscrollleft = scrollleft; this.currentscrolltop = scrolltop; this.currentwidth = this.viewportwidth; this.currentheight = this.viewportheight; } // reposition elements for (i = this.particles.length - 1; i >= 0; i--) { particle = this.particles[i]; fixedratiooffset = (particle.isfixed ? 1 : 0); // calculate position, then calculate what the particle's new offset will be (for visibility check) if (this.options.horizontalscrolling) { newpositionleft = (scrollleft + particle.horizontaloffset + this.viewportoffsetleft + particle.startingpositionleft - particle.startingoffsetleft + particle.parentoffsetleft) * -(particle.stellarratio + fixedratiooffset - 1) + particle.startingpositionleft; newoffsetleft = newpositionleft - particle.startingpositionleft + particle.startingoffsetleft; } else { newpositionleft = particle.startingpositionleft; newoffsetleft = particle.startingoffsetleft; } if (this.options.verticalscrolling) { newpositiontop = (scrolltop + particle.verticaloffset + this.viewportoffsettop + particle.startingpositiontop - particle.startingoffsettop + particle.parentoffsettop) * -(particle.stellarratio + fixedratiooffset - 1) + particle.startingpositiontop; newoffsettop = newpositiontop - particle.startingpositiontop + particle.startingoffsettop; } else { newpositiontop = particle.startingpositiontop; newoffsettop = particle.startingoffsettop; } // check visibility if (this.options.hidedistantelements) { isvisiblehorizontal = !this.options.horizontalscrolling || newoffsetleft + particle.width > (particle.isfixed ? 0 : scrollleft) && newoffsetleft < (particle.isfixed ? 0 : scrollleft) + this.viewportwidth + this.viewportoffsetleft; isvisiblevertical = !this.options.verticalscrolling || newoffsettop + particle.height > (particle.isfixed ? 0 : scrolltop) && newoffsettop < (particle.isfixed ? 0 : scrolltop) + this.viewportheight + this.viewportoffsettop; } if (isvisiblehorizontal && isvisiblevertical) { if (particle.ishidden) { this.options.showelement(particle.$element); particle.ishidden = false; } this._setposition(particle.$element, newpositionleft, particle.startingpositionleft, newpositiontop, particle.startingpositiontop); } else { if (!particle.ishidden) { this.options.hideelement(particle.$element); particle.ishidden = true; } } } // reposition backgrounds for (i = this.backgrounds.length - 1; i >= 0; i--) { background = this.backgrounds[i]; fixedratiooffset = (background.isfixed ? 0 : 1); bgleft = (this.options.horizontalscrolling ? (scrollleft + background.horizontaloffset - this.viewportoffsetleft - background.startingoffsetleft + background.parentoffsetleft - background.startingbackgroundpositionleft) * (fixedratiooffset - background.stellarratio) + 'px' : background.startingvalueleft); bgtop = (this.options.verticalscrolling ? (scrolltop + background.verticaloffset - this.viewportoffsettop - background.startingoffsettop + background.parentoffsettop - background.startingbackgroundpositiontop) * (fixedratiooffset - background.stellarratio) + 'px' : background.startingvaluetop); setbackgroundposition(background.$element, bgleft, bgtop); } }, _handlescrollevent: function() { var self = this, ticking = false; var update = function() { self._repositionelements(); ticking = false; }; var requesttick = function() { if (!ticking) { requestanimframe(update); ticking = true; } }; this.$scrollelement.bind('scroll.' + this.name, requesttick); requesttick(); }, _startanimationloop: function() { var self = this; this._animationloop = function() { requestanimframe(self._animationloop); self._repositionelements(); }; this._animationloop(); } }; $.fn[pluginname] = function (options) { var args = arguments; if (options === undefined || typeof options === 'object') { return this.each(function () { if (!$.data(this, 'plugin_' + pluginname)) { $.data(this, 'plugin_' + pluginname, new plugin(this, options)); } }); } else if (typeof options === 'string' && options[0] !== '_' && options !== 'init') { return this.each(function () { var instance = $.data(this, 'plugin_' + pluginname); if (instance instanceof plugin && typeof instance[options] === 'function') { instance[options].apply(instance, array.prototype.slice.call(args, 1)); } if (options === 'destroy') { $.data(this, 'plugin_' + pluginname, null); } }); } }; $[pluginname] = function(options) { var $window = $(window); return $window.stellar.apply($window, array.prototype.slice.call(arguments, 0)); }; // expose the scroll and position property function hashes so they can be extended $[pluginname].scrollproperty = scrollproperty; $[pluginname].positionproperty = positionproperty; // expose the plugin class so it can be modified window.stellar = plugin; }(jquery, this, document));