/* Copyright (C) 2013 Ziad Saab ziad(dot)saab(at)gmail(dot)com Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ ;(function($) { var ParallaxManager = function(options) { this.options = options; this.vendor_prefixes = ['webkit', 'moz', 'o', 'ms']; this.num_vendor_prefixes = this.vendor_prefixes.length; // Function used to detect support for a certain CSS style var thisBrowserSupportsStyle = function(style) { var vendors = ['Webkit', 'Moz', 'ms', 'O']; var num_vendors = vendors.length; var dummy_el = window.document.createElement('parallax'); // First test the bare style without prefix if (dummy_el.style[style] !== undefined) { return true; } // Test the camel-cased vendor-prefixed styles style = style.replace(/./, function(first) {return first.toUpperCase();}); for (var i = 0; i < num_vendors; i++) { var pfx_style = vendors[i] + style; // The browser will return an empty string if a style is supported but not present, and undefined if the style is not supported if (dummy_el.style[pfx_style] !== undefined) { return true; } } return false; }; // Detect if we have 2d or 3d transforms by checking for support of the transform (2d) and perspective (3d) styles this.has_3dtransforms = thisBrowserSupportsStyle('perspective'); // Additional test due to false positives returned with WebKit on some browsers: they support perspective but can't do 3d transforms if (this.has_3dtransforms && thisBrowserSupportsStyle('WebkitPerspective')) { var $test_el = $('
').appendTo('body'); var $el = $('#parallax-3dtest'); this.has_3dtransforms = $el.height() == 5 && $el.offset().left == 9; $test_el.remove(); } this.has_2dtransforms = thisBrowserSupportsStyle('transform'); }; // Define "methods" on the ParallaxManager class $.extend(ParallaxManager.prototype, { init: function() { this.scroll_factor = this.options.scroll_factor; var parallax_blocks = this.parallax_blocks = []; var image_attr = this.options.image_attr var $body = $('body'); var $origins = this.options.origins; $origins.each(function() { var $origin = $(this); var $parallax_block; if ($origin.data(image_attr)) { $parallax_block = $('
'); parallax_blocks.push({ origin: $origin, block: $parallax_block, bg_ratio: $origin.data('width') / $origin.data('height') }); $body.prepend($parallax_block); } else if ($origin.data('tile')) { $parallax_block = $('
') parallax_blocks.push({ origin: $origin, block: $parallax_block, bg_ratio: 1 }); $body.prepend($parallax_block); } }); var manager = this; var reconfigure = function() { manager.redrawBlocks(); manager.render(); }; var $window = $(window); $window.on('load', reconfigure); $window.on('resize', reconfigure); $window.on('hwparallax.reconfigure', reconfigure); $window.on('scroll', function() {manager.render();}); }, redrawBlocks: function() { var window_width = $(window).width(); var window_height = this.window_height = $(window).height(); // Calculate document height to fix Chrome issue when scrolling past the content... var body = document.body; var html = document.documentElement; var document_height = Math.max( body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight); this.max_scrolltop = Math.max(0, document_height - window_height); var num_parallax_blocks = this.parallax_blocks.length; for (var i = 0; i < num_parallax_blocks; i++) { var parallax_block_data = this.parallax_blocks[i]; var $parallax_block = parallax_block_data.block; var bg_ratio = parallax_block_data.bg_ratio; var $parallax_image = $parallax_block.children('.parallax-image'); var $origin = parallax_block_data.origin; // Calculate the minimum background image height so that we may scroll by the scroll factor and keep the image in the bounds of its container var origin_height = $origin.outerHeight(); var min_height = window_height - ((window_height - origin_height) * this.scroll_factor); // Calculate the size of the parallax background image so that it covers the target block while still respecting the previously calculated minimum height // Also, if the image's width is larger than the window's, center it about its parent element var img_width = window_width; var img_height = Math.ceil(img_width / bg_ratio); var img_xoff = 0; if (img_height < min_height) { img_height = min_height; img_width = img_height * bg_ratio; img_xoff = Math.floor(img_width - window_width) / 2; } $parallax_image.width(img_width).height(img_height); // Set the dimensions of the parallax block and hide it $parallax_block.width(window_width).height(origin_height).css('visibility', 'hidden'); // Cache some data about the parallax block so that we don't have to recalculate it on every tick // This data potentially changes, only changes when the window is resized $.extend(parallax_block_data, { origin_position: $origin.offset().top, origin_height: origin_height, image: $parallax_image, image_xoff: img_xoff, image_height: img_height }); } }, render: function() { if (!this.drawing) { this.drawing = true; var manager = this; if (window.requestAnimationFrame) { window.requestAnimationFrame(function() {manager.draw();}, document); } else { manager.draw(); } } }, draw: function() { var scroll_top = Math.min(Math.max(0, $(window).scrollTop()), this.max_scrolltop); // Loop thru all the parallax blocks var num_blocks = this.parallax_blocks.length; var data; for (var i = 0; i < num_blocks; i++) { data = this.parallax_blocks[i]; // Determine if parallax block is in view // i.e. its bottom is below the window's upper bound and its top is above the window's lower bound if (data.origin_position < scroll_top + this.window_height && data.origin_position + data.origin_height > scroll_top) { // The block's y position in its fixed plane is the position of the original block relative to the top of the window var new_block_position = data.origin_position - scroll_top; // The background image's position is parallaxed to follow the block by the scroll factor var new_image_position = new_block_position * (this.scroll_factor - 1); // Apply the translations to block and image using the best method possible: 3d transform, 2d transform or regular top/left var block_styles = { visibility: 'visible' }; var image_styles = {}; var block_transform, image_transform, prefixed_style; var j; if (this.has_3dtransforms) { block_transform = block_styles.transform = 'translate3d(0px, ' + new_block_position + 'px, 0px)'; image_transform = image_styles.transform = 'translate3d(-' + data.image_xoff + 'px, ' + new_image_position + 'px, 0px)'; for (j = 0; j < this.num_vendor_prefixes; j++) { prefixed_style = '-' + this.vendor_prefixes[j] + '-transform'; block_styles[prefixed_style] = block_transform; image_styles[prefixed_style] = image_transform; } } else if (this.has_2dtransforms) { block_transform = block_styles.transform = 'translate(0px, ' + new_block_position + 'px)'; image_transform = image_styles.transform = 'translate(-' + data.image_xoff + 'px, ' + new_image_position + 'px)'; for (j = 0; j < this.num_vendor_prefixes; j++) { prefixed_style = '-' + this.vendor_prefixes[j] + '-transform'; block_styles[prefixed_style] = block_transform; image_styles[prefixed_style] = image_transform; } } else { block_styles.top = new_block_position + 'px'; block_styles.left = 0 + 'px'; image_styles.top = new_image_position + 'px'; image_styles.left = -data.image_xoff + 'px'; } data.block.css(block_styles); data.image.css(image_styles); } else { data.block.css('visibility', 'hidden'); } } this.drawing = false; } }); // Hook into jQuery $.extend($.fn, { parallax: function(options) { var settings = $.extend( { scroll_factor: 0.2, image_attr: 'image' }, options, { origins: $(this) } ); var pm = new ParallaxManager(settings); pm.init(); } }); })(jQuery);