Skip to content

Instantly share code, notes, and snippets.

@fvonellerts
Last active October 29, 2019 11:08
Show Gist options
  • Save fvonellerts/cb80e62ceaae729eeff941e2bd266a4d to your computer and use it in GitHub Desktop.
Save fvonellerts/cb80e62ceaae729eeff941e2bd266a4d to your computer and use it in GitHub Desktop.
ES6 Google CSS parallax helper
/**
* Copyright 2016 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
function initializeParallax(clip) {
const parallax = clip.querySelectorAll('*[parallax]'),
parallaxDetails = []
let sticky = false
// Edge requires a transform on the document body and a fixed position element // TODO: only run on edge
// in order for it to properly render the parallax effect as you scroll.
// See https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/5084491/
if (getComputedStyle(document.body).transform === 'none') {
document.body.style.transform = 'translateZ(0)'
}
const fixedPos = document.createElement('div')
fixedPos.style.position = 'fixed'
fixedPos.style.top = '0'
fixedPos.style.width = '1px'
fixedPos.style.height = '1px'
fixedPos.style.zIndex = '1'
document.body.insertBefore(fixedPos, document.body.firstChild)
for (let i = 0; i < parallax.length; i++) {
const elem = parallax[i],
container = elem.parentNode
if (getComputedStyle(container).overflow !== 'visible') {
console.error('Need non-scrollable container to apply perspective for', elem)
continue
}
if (clip && container.parentNode !== clip) {
console.warn('Currently we only track a single overflow clip, but elements from multiple clips found.', elem)
}
clip = container.parentNode
if (getComputedStyle(clip).overflow === 'visible') {
console.error('Parent of sticky container should be scrollable element', elem)
}
// TODO(flackr): optimize to not redo this for the same clip/container.
let perspectiveElement
if (sticky || getComputedStyle(clip).webkitOverflowScrolling) {
sticky = true
perspectiveElement = container
} else {
perspectiveElement = clip
container.style.transformStyle = 'preserve-3d'
}
perspectiveElement.style.perspectiveOrigin = 'bottom right'
perspectiveElement.style.perspective = '1px'
if (sticky) {
elem.style.position = '-webkit-sticky'
}
if (sticky) {
elem.style.top = '0'
}
elem.style.transformOrigin = 'bottom right'
// Find the previous and next elements to parallax between.
let previousCover = parallax[i].previousElementSibling
while (previousCover && previousCover.hasAttribute('parallax')) {
previousCover = previousCover.previousElementSibling
}
let nextCover = parallax[i].nextElementSibling
while (nextCover && !nextCover.hasAttribute('parallax-cover')) {
nextCover = nextCover.nextElementSibling
}
parallaxDetails.push({
'node': parallax[i],
'top': parallax[i].offsetTop,
'sticky': !!sticky,
nextCover,
previousCover
})
}
/* Add a scroll listener to hide perspective elements when they should no longer be visible.
clip.addEventListener('scroll', function () {
for (let i = 0; i < parallaxDetails.length; i++) {
const container = parallaxDetails[i].node.parentNode,
previousCover = parallaxDetails[i].previousCover,
nextCover = parallaxDetails[i].nextCover,
parallaxStart = previousCover ? (previousCover.offsetTop + previousCover.offsetHeight) : 0,
parallaxEnd = nextCover ? nextCover.offsetTop : container.offsetHeight,
threshold = 200,
visible = parallaxStart - threshold - clip.clientHeight < clip.scrollTop &&
parallaxEnd + threshold > clip.scrollTop,
// FIXME: Repainting the images while scrolling can cause jank.
// For now, keep them all.
// var display = visible ? 'block' : 'none'
display = 'block'
if (parallaxDetails[i].node.style.display !== display) {
parallaxDetails[i].node.style.display = display
}
}
})*/
window.addEventListener('resize', onResize.bind(null, parallaxDetails))
onResize(parallaxDetails)
for (let i = 0; i < parallax.length; i++) {
parallax[i].parentNode.insertBefore(parallax[i], parallax[i].parentNode.firstChild)
}
}
function onResize(details) {
for (let i = 0; i < details.length; i++) {
const container = details[i].node.parentNode,
clip = container.parentNode,
previousCover = details[i].previousCover,
nextCover = details[i].nextCover,
rate = details[i].node.getAttribute('parallax'),
parallaxStart = previousCover ? (previousCover.offsetTop + previousCover.offsetHeight) : 0,
scrollbarWidth = details[i].sticky ? 0 : clip.offsetWidth - clip.clientWidth,
height = details[i].node.offsetHeight
let depth
if (rate) {
depth = 1 - (1 / rate)
} else {
const parallaxEnd = nextCover ? nextCover.offsetTop : container.offsetHeight
depth = (height - parallaxEnd + parallaxStart) / (height - clip.clientHeight)
}
if (details[i].sticky) {
depth = 1.0 / depth
}
const scale = 1.0 / (1.0 - depth),
// The scrollbar is included in the 'bottom right' perspective origin.
dx = scrollbarWidth * (scale - 1),
// Offset for the position within the container.
dy = details[i].sticky
? -(clip.scrollHeight - parallaxStart - height) * (1 - scale)
: (parallaxStart - depth * (height - clip.clientHeight)) * scale
details[i].node.style.transform = 'scale(' + (1 - depth) + ') translate3d(' + dx + 'px, ' + dy + 'px, ' + depth + 'px)'
}
}
export default initializeParallax
@fvonellerts
Copy link
Author

Copied from https://developers.google.com/web/updates/2016/12/performant-parallaxing and made ES6 compliant. I also commented out the incomplete scroll listener.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment