Created
June 27, 2018 00:41
-
-
Save natew/31193abcd5845f4deab7f7c09310a916 to your computer and use it in GitHub Desktop.
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 from 'react' | |
import PropTypes from 'prop-types' | |
import { | |
controller, | |
AnimatedValue, | |
createAnimatedComponent, | |
SpringAnimation, | |
config, | |
} from 'react-spring' | |
const AnimatedDiv = createAnimatedComponent('div') | |
const { Provider, Consumer } = React.createContext(null) | |
function getScrollType(horizontal) { | |
return horizontal ? 'scrollLeft' : 'scrollTop' | |
} | |
const START_TRANSLATE_3D = 'translate3d(0px,0px,0px)' | |
const START_TRANSLATE = 'translate(0px,0px)' | |
export class ParallaxLayer extends React.PureComponent { | |
static propTypes = { | |
factor: PropTypes.number, | |
offset: PropTypes.number, | |
speed: PropTypes.number, | |
} | |
static defaultProps = { | |
factor: 1, | |
offset: 0, | |
speed: 0, | |
} | |
componentDidMount() { | |
const parent = this.parent | |
if (parent) { | |
parent.layers = parent.layers.concat(this) | |
parent.update() | |
} | |
} | |
componentWillUnmount() { | |
const parent = this.parent | |
if (parent) { | |
parent.layers = parent.layers.filter(layer => layer !== this) | |
parent.update() | |
} | |
} | |
setPosition(height, scrollTop, immediate = false) { | |
const targetScroll = Math.floor(this.props.offset) * height | |
const offset = height * this.props.offset + targetScroll * this.props.speed | |
const to = parseFloat(-(scrollTop * this.props.speed) + offset) | |
this.updateEffect(this.animatedTranslate, to, immediate) | |
const distanceFromTarget = scrollTop + height - targetScroll | |
const toPlain = distanceFromTarget / (height * this.props.offset) | |
if (this.props.debug) { | |
console.log(this.props.debug, distanceFromTarget, toPlain) | |
} | |
this.updateCustomEffects(toPlain, immediate) | |
} | |
setHeight(height, immediate = false) { | |
const to = parseFloat(height * this.props.factor) | |
this.updateEffect(this.animatedSpace, to, immediate) | |
} | |
updateEffect(effect, to, immediate) { | |
const { config, impl } = this.parent.props | |
if (!immediate) { | |
controller(effect, { to, ...config }, impl).start() | |
} else { | |
effect.setValue(to) | |
} | |
} | |
updateCustomEffects(to, immediate) { | |
if (!this.effects) { | |
return | |
} | |
for (const key of Object.keys(this.effects)) { | |
this.updateEffect(this.effects[key], to, immediate) | |
} | |
} | |
initialize() { | |
const props = this.props | |
const parent = this.parent | |
const targetScroll = Math.floor(props.offset) * parent.space | |
const offset = parent.space * props.offset + targetScroll * props.speed | |
const to = parseFloat(-(parent.current * props.speed) + offset) | |
this.animatedTranslate = new AnimatedValue(to) | |
this.animatedSpace = new AnimatedValue(parent.space * props.factor) | |
if (this.props.effects) { | |
this.effects = {} | |
for (const key of Object.keys(this.props.effects)) { | |
this.effects[key] = new AnimatedValue(to) | |
} | |
} | |
} | |
renderLayer() { | |
const { | |
style, | |
children, | |
offset, | |
speed, | |
factor, | |
className, | |
effects, | |
...props | |
} = this.props | |
const horizontal = this.parent.props.horizontal | |
const translate3d = this.animatedTranslate.interpolate({ | |
range: [0, 1], | |
output: horizontal | |
? [START_TRANSLATE_3D, 'translate3d(1px,0,0)'] | |
: [START_TRANSLATE_3D, 'translate3d(0,1px,0)'], | |
}) | |
const customEffects = {} | |
if (effects) { | |
for (const key of Object.keys(effects)) { | |
customEffects[key] = this.effects[key].interpolate(effects[key]) | |
} | |
} | |
return ( | |
<AnimatedDiv | |
{...props} | |
className={className} | |
style={{ | |
position: 'absolute', | |
backgroundSize: 'auto', | |
backgroundRepeat: 'no-repeat', | |
willChange: 'transform', | |
[horizontal ? 'height' : 'width']: '100%', | |
[horizontal ? 'width' : 'height']: this.animatedSpace, | |
WebkitTransform: translate3d, | |
MsTransform: translate3d, | |
transform: translate3d, | |
...style, | |
...customEffects, | |
}} | |
> | |
{children} | |
</AnimatedDiv> | |
) | |
} | |
render() { | |
return ( | |
<Consumer> | |
{parent => { | |
if (parent && !this.parent) { | |
this.parent = parent | |
this.initialize() | |
} | |
return this.renderLayer() | |
}} | |
</Consumer> | |
) | |
} | |
} | |
export class Parallax extends React.PureComponent { | |
// TODO keep until major release | |
static Layer = ParallaxLayer | |
static propTypes = { | |
pages: PropTypes.number.isRequired, | |
config: PropTypes.object, | |
scrolling: PropTypes.bool, | |
horizontal: PropTypes.bool, | |
impl: PropTypes.func, | |
} | |
static defaultProps = { | |
config: config.slow, | |
scrolling: true, | |
horizontal: false, | |
impl: SpringAnimation, | |
} | |
state = { ready: false } | |
layers = [] | |
space = 0 | |
current = 0 | |
offset = 0 | |
busy = false | |
moveItems = () => { | |
this.layers.forEach(layer => layer.setPosition(this.space, this.current)) | |
this.busy = false | |
} | |
scrollerRaf = () => requestAnimationFrame(this.moveItems) | |
onScroll = event => { | |
const { horizontal } = this.props | |
if (!this.busy) { | |
this.busy = true | |
this.scrollerRaf() | |
this.current = event.target[getScrollType(horizontal)] | |
} | |
} | |
update = () => { | |
const { scrolling, horizontal } = this.props | |
const scrollType = getScrollType(horizontal) | |
if (!this.container) return | |
this.space = this.container[horizontal ? 'clientWidth' : 'clientHeight'] | |
if (scrolling) this.current = this.container[scrollType] | |
else this.container[scrollType] = this.current = this.offset * this.space | |
if (this.content) | |
this.content.style[horizontal ? 'width' : 'height'] = `${this.space * | |
this.props.pages}px` | |
this.layers.forEach(layer => { | |
layer.setHeight(this.space, true) | |
layer.setPosition(this.space, this.current, true) | |
}) | |
} | |
updateRaf = () => { | |
requestAnimationFrame(this.update) | |
// Some browsers don't fire on maximize | |
setTimeout(this.update, 150) | |
} | |
scrollStop = event => | |
this.animatedScroll && this.animatedScroll.stopAnimation() | |
scrollTo(offset) { | |
const { horizontal, config, impl } = this.props | |
const scrollType = getScrollType(horizontal) | |
this.scrollStop() | |
this.offset = offset | |
const target = this.container | |
this.animatedScroll = new AnimatedValue(target[scrollType]) | |
this.animatedScroll.addListener(({ value }) => (target[scrollType] = value)) | |
controller( | |
this.animatedScroll, | |
{ to: offset * this.space, ...config }, | |
impl, | |
).start() | |
} | |
componentDidMount() { | |
window.addEventListener('resize', this.updateRaf, false) | |
this.update() | |
this.setState({ ready: true }) | |
} | |
componentWillUnmount() { | |
window.removeEventListener('resize', this.updateRaf, false) | |
} | |
componentDidUpdate() { | |
this.update() | |
} | |
render() { | |
const { | |
style, | |
innerStyle, | |
pages, | |
className, | |
scrolling, | |
children, | |
horizontal, | |
} = this.props | |
const overflow = scrolling ? 'scroll' : 'hidden' | |
return ( | |
<div | |
ref={node => (this.container = node)} | |
onScroll={this.onScroll} | |
onWheel={scrolling ? this.scrollStop : null} | |
onTouchStart={scrolling ? this.scrollStop : null} | |
style={{ | |
position: 'absolute', | |
width: '100%', | |
height: '100%', | |
overflow, | |
overflowY: horizontal ? 'hidden' : overflow, | |
overflowX: horizontal ? overflow : 'hidden', | |
WebkitOverflowScrolling: 'touch', | |
WebkitTransform: START_TRANSLATE, | |
MsTransform: START_TRANSLATE, | |
transform: START_TRANSLATE_3D, | |
...style, | |
}} | |
className={className} | |
> | |
{this.state.ready && ( | |
<div | |
ref={node => (this.content = node)} | |
style={{ | |
position: 'absolute', | |
[horizontal ? 'height' : 'width']: '100%', | |
WebkitTransform: START_TRANSLATE, | |
MsTransform: START_TRANSLATE, | |
transform: START_TRANSLATE_3D, | |
overflow: 'hidden', | |
[horizontal ? 'width' : 'height']: this.space * pages, | |
...innerStyle, | |
}} | |
> | |
<Provider value={this}>{children}</Provider> | |
</div> | |
)} | |
</div> | |
) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment