Last active
July 6, 2016 12:55
-
-
Save tbusser/f81e363b1f128f3546869ece0a8fca56 to your computer and use it in GitHub Desktop.
Parallax effect, see http://codepen.io/tbusser/pen/QEgmmq for an example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function() { | |
var imgData = [], | |
windowHeight = window.innerHeight; | |
lastScrollPosition = 0; | |
function getOffset(element) { | |
var rect = element.getBoundingClientRect(); | |
return { | |
top: rect.top + document.body.scrollTop, | |
left: rect.left + document.body.scrollLeft | |
}; | |
} | |
function updatePosition(image, position) { | |
requestAnimationFrame(function updateImageStyle() { | |
image.style.transform = 'translateY(' + position + 'px)'; | |
}); | |
} | |
function updateParallaxes() { | |
var index = 0, | |
ubound = imgData.length; | |
for (; index < ubound; index++) { | |
data = imgData[index]; | |
if (data.imageHeight == null || data.imageHeight === 0) { | |
data.imageHeight = data.imageElement.offsetHeight; | |
if (data.imageHeight === 0) { | |
continue; | |
} | |
data.parentOffset = getOffset(data.imageElement.parentNode).top; | |
data.imageOverflow = data.imageHeight - data.parentHeight; | |
data.travelCorrection = (data.parentOffset < windowHeight) ? (windowHeight - data.parentOffset) : 0; | |
} | |
// Calculate the distance from the top of the parallax element to the | |
// top of the viewport. When this is a positive number the image is | |
// below the top. A negative number means the top of the parallax | |
// element is above the viewport. | |
var frameTop = data.parentOffset - lastScrollPosition; | |
// If the frame top is more than the window height it means the top of | |
// parallax item is still beneath the viewport. If the absolute number of | |
// the frame top is more than the height of the parallax item it means the | |
// bottom of the parallax item is above the viewport. | |
// When either of these is true it means the parallax item is outside of | |
// the viewport and we don't have to bother updating the parallax effect | |
// for this item. | |
if (frameTop > windowHeight || -frameTop > data.parentHeight) { | |
// Continue with the next parallax item, this one can't be seen | |
// by the visitor | |
continue; | |
} | |
// 1: We need to calculate the window height, taking into account if the parallax | |
// item starts in the viewport. If the parallax item starts in the viewport we | |
// need to correct the height by substracting the parallax item's offset. Basically | |
// we're changing the number of pixels the element has to travel in order to disappear | |
// off screen. | |
// 2: First we need to calculate how many pixels of the image we can't | |
// show in its container. Next we need to know how many pixels the top | |
// of the parallax container can travel before the entire parallax | |
// container leaves the viewport. When we divide these two values we have | |
// the number of pixels the parallax image should move per pixel the document | |
// moves. This difference in speed is what creates the parallax effect. | |
// 3: Calculate how many pixels the image should be moved based on the actual | |
// travelled distance | |
var travelDistance = windowHeight - data.travelCorrection, // [1] | |
scrollRatio = (data.imageOverflow) / (travelDistance + data.parentHeight), // [2] | |
delta = 0 - (travelDistance - frameTop) * scrollRatio; // [3] | |
// Make sure the delta we have is within the bounds so we don't wind up moving | |
// the image too much. The chances this happens are minimal (I think) but if it | |
// happens you might wind up with some ugly whitespace in the UI | |
if (delta > 0) { | |
delta = 0 | |
} else if (-delta > data.imageOverlow) { | |
delta = -data.imageOverflow; | |
} | |
// Update the position of the image in the next animation frame | |
updatePosition(data.imageElement, delta); | |
} | |
} | |
/* ============== | |
EVENT HANDLERS | |
============== */ | |
function onScrollHandler(event) { | |
// Get the current scroll position. We do this here instead of in | |
// updateParallaxes as Chrome warns of a possible forced repaint | |
// when we access this property inside that method. scrollY is the | |
// standard property, pageYOffset is for older IE versions. | |
lastScrollPosition = window.scrollY || window.pageYOffset; | |
// Update the parallax effect in the next animation frame, we do this | |
// to maximize the performance. | |
requestAnimationFrame(updateParallaxes); | |
} | |
/* == EVENT HANDLERS == */ | |
/* ============ | |
INIT METHODS | |
============ */ | |
function init() { | |
// 1: Get all the parallax images on the page | |
// 2: Initialize the loop control variables | |
var images = document.querySelectorAll('.parallax__image'), // [1] | |
index = 0, // [2] | |
ubound = images.length; // [2] | |
// When there are no parallax images, there is nothing left for us | |
// to do | |
if (ubound === 0) { | |
return; | |
} | |
// Loop over all the images | |
for (; index < ubound; index++) { | |
// Get an easy reference to the current image | |
var image = images[index], | |
parentElement = image.parentNode; | |
// Store all the information we need for the parallex effect | |
// in the array so we don't have to access the DOM for these | |
// values each time we have to apply the parallax effect. | |
// 1: We need the image element itself, this is the element we will | |
// apply the transform on to create the parallax effect. | |
// 2: We need to know the height of the image, we assume this won't | |
// change over time and is a value we can cache without a problem. | |
// 3: Calculate the number of pixels of the image that don't fit inside | |
// the parent container. These are the pixels we will make visible | |
// with the parallax effect. | |
// 4: We need to know the height of the parent element, the parallax | |
// container. We will make the assumption that its height doesn't change | |
// over time and is thus a safe value to cache. | |
// 5: The number of pixels the parent container is positioned from the top | |
// of the document. It is an expensive value to calculate which is why | |
// we're caching it. We assume the element is always on the same vertical | |
// position and thus a safe value to cache. | |
// 6: If the top of the parallax container puts it in the viewport when the | |
// document is all the way on the top we need to calculate how many pixels | |
// of the parallax container are visible. | |
imgData.push({ | |
imageElement : image, // [1] | |
imageHeight : image.offsetHeight, // [2] | |
imageOverflow : image.offsetHeight - parentElement.offsetHeight, // [3] | |
parentHeight : parentElement.offsetHeight, // [4] | |
parentOffset : getOffset(parentElement).top, // [5] | |
travelCorrection : (getOffset(parentElement).top < windowHeight) ? (windowHeight - getOffset(parentElement).top) : 0 // [6] | |
}); | |
} | |
// Listen to the scroll event, it is our cue to show | |
// the parallax behaviour | |
window.addEventListener('scroll', onScrollHandler); | |
} | |
/* == INIT METHODS == */ | |
init(); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment