Last active
May 26, 2017 21:52
-
-
Save furf/69755d9d2e8862a5b973dbc30d27595a to your computer and use it in GitHub Desktop.
React components for parallax scrolling.
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
import React, { Component } from 'react'; | |
import PropTypes from 'prop-types'; | |
import classnames from 'classnames/bind'; | |
const now = Date.now(); | |
const PERSPECTIVE_KEY = `__perspective--${now}__`; | |
const DISABLED_KEY = `__perspective--${now}--disabled__`; | |
/** | |
* Parallax | |
*/ | |
export class Parallax extends Component { | |
static propTypes = { | |
nodeName: PropTypes.string, | |
perspective: PropTypes.number, | |
disabled: PropTypes.bool, | |
onScroll: PropTypes.func, | |
}; | |
static defaultProps = { | |
nodeName: 'div', | |
perspective: 1, | |
disabled: false, | |
onScroll: null, | |
}; | |
static className = 'parallax'; | |
static defaultStyle = { | |
height: '100vh', | |
}; | |
static requiredStyle = { | |
overflowX: 'hidden', | |
overflowY: 'auto', | |
perspectiveOriginX: '100%', | |
}; | |
handleScroll = event => { | |
const { scrollTop, scrollHeight, offsetHeight } = event.target; | |
this.props.onScroll(scrollTop / (scrollHeight - offsetHeight)); | |
} | |
render() { | |
const { | |
nodeName, | |
perspective, | |
disabled, | |
onScroll, | |
className, | |
style, | |
children, | |
...rest, | |
} = this.props; | |
return React.createElement(nodeName, { | |
className: classnames(className, Parallax.className), | |
style: { | |
...Parallax.defaultStyle, | |
...style, | |
...Parallax.requiredStyle, | |
perspective: disabled ? 'none' : `${perspective}px`, | |
}, | |
onScroll: onScroll ? this.handleScroll : null, | |
...rest, | |
}, applyPropsToChildren(children, { | |
[PERSPECTIVE_KEY]: perspective, | |
[DISABLED_KEY]: disabled, | |
})); | |
} | |
} | |
/** | |
* ParallaxGroup | |
*/ | |
export class ParallaxGroup extends Component { | |
static propTypes = { | |
nodeName: PropTypes.string, | |
flat: PropTypes.bool, | |
}; | |
static defaultProps = { | |
nodeName: 'div', | |
flat: false, | |
}; | |
static className = 'parallax__group'; | |
static defaultStyle = { | |
height: '100%', | |
}; | |
static requiredStyle = { | |
position: 'relative', | |
}; | |
render() { | |
const { | |
nodeName, | |
flat, | |
className, | |
style, | |
children, | |
[PERSPECTIVE_KEY]: perspective, | |
[DISABLED_KEY]: disabled, | |
...rest, | |
} = this.props; | |
return React.createElement(nodeName, { | |
className: classnames(className, ParallaxGroup.className), | |
style: { | |
...ParallaxGroup.defaultStyle, | |
...style, | |
...ParallaxGroup.requiredStyle, | |
transformStyle: flat ? 'flat' : 'preserve-3d', | |
}, | |
...rest, | |
}, applyPropsToChildren(children, { | |
[PERSPECTIVE_KEY]: perspective, | |
[DISABLED_KEY]: disabled, | |
})); | |
} | |
} | |
/** | |
* ParallaxLayer | |
*/ | |
export class ParallaxLayer extends Component { | |
static propTypes = { | |
nodeName: PropTypes.string, | |
depth: PropTypes.number, | |
}; | |
static defaultProps = { | |
nodeName: 'div', | |
depth: 0, | |
}; | |
static className = 'parallax__layer'; | |
static requiredStyle = { | |
position: 'absolute', | |
top: 0, | |
right: 0, | |
bottom: 0, | |
left: 0, | |
transformOriginX: '100%', | |
}; | |
render() { | |
const { | |
nodeName, | |
depth, | |
className, | |
style, | |
children, | |
[PERSPECTIVE_KEY]: perspective, | |
[DISABLED_KEY]: disabled, | |
...rest | |
} = this.props; | |
const translateZ = -depth; | |
const scale = (1 - translateZ) / perspective; | |
return React.createElement(nodeName, { | |
className: classnames(className, ParallaxLayer.className), | |
style: { | |
...style, | |
...ParallaxLayer.requiredStyle, | |
transform: disabled ? 'none' : `translateZ(${translateZ}px) scale(${scale})`, | |
}, | |
...rest, | |
}, children); | |
} | |
} | |
/** | |
* ensureArray | |
*/ | |
function ensureArray(value) { | |
return Array.isArray(value) ? value : [value]; | |
} | |
/** | |
* applyPropsToChildren | |
*/ | |
function applyPropsToChildren(children, props) { | |
return ensureArray(children).map(function(child, key) { | |
if (child.type === ParallaxGroup || child.type === ParallaxLayer) { | |
return React.cloneElement(child, Object.assign({}, props, { key })); | |
} else { | |
return child; | |
} | |
}); | |
} |
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
<Parallax nodeName="article" onScroll={this.handleScroll}> | |
<ParallaxGroup nodeName="section"> | |
<ParallaxLayer depth={5}> | |
background | |
</ParallaxLayer> | |
<ParallaxLayer depth={2}> | |
middle | |
</ParallaxLayer> | |
<ParallaxLayer> | |
foreground | |
</ParallaxLayer> | |
</ParallaxGroup> | |
<ParallaxGroup nodeName="section" flat={true}> | |
<ParallaxLayer depth={5}> | |
background | |
</ParallaxLayer> | |
<ParallaxLayer depth={2}> | |
middle | |
</ParallaxLayer> | |
<ParallaxLayer> | |
foreground | |
</ParallaxLayer> | |
</ParallaxGroup> | |
</Parallax> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment