import React from 'react'; import { gsap } from 'gsap'; import { nullishCoalescing } from '../helper'; import Provider from '../Provider'; export enum TriggerPosition { top = 'top', bottom = 'bottom', } export type ScrollerProps = { children: (progress: number) => React.ReactNode; heightVh: number; resolution: number; triggerPosition: TriggerPosition; }; type ScrollerState = { progress: number; }; class Scroller extends Provider { static displayName = 'ScrollReveal'; static defaultProps = { heightVh: 100, resolution: 100, triggerPosition: TriggerPosition.bottom, }; state: ScrollerState = { progress: 0, }; timeline: any; heights: number[] = []; targetRefs: HTMLElement[] = []; observer: IntersectionObserver | null = null; constructor(props: ScrollerProps) { super(props); this.heights = this.getTargetHeights(this.props.heightVh); } getTargetHeights(heightVh: number) { const numberTimes = heightVh / 100; const numberTargets = Math.ceil(numberTimes); const numberFull = Math.floor(numberTimes); const lastHeight = numberTargets === numberFull ? 100 : (numberTimes % numberFull) * 100; const heights = Array.from({ length: numberTargets }, () => 100); heights[numberTargets - 1] = lastHeight; return heights; } componentDidMount() { this.createTimeline(); this.createIntersectionObserver(); } componentWillUnmount() { this.timeline.kill(); } getSnapshotBeforeUpdate() { // this.targets = []; return null; } componentDidUpdate(prevProps: ScrollerProps) { const { children } = this.props; // if children change create a new timeline // TODO: replace easy length check with fast equal check // TODO: same for props.target? if (React.Children.count(prevProps.children) !== React.Children.count(children)) { this.createTimeline(); } } createTimeline() { if (this.timeline) { this.timeline.kill(); } // init timeline this.timeline = gsap.timeline({ smoothChildTiming: true, paused: true, }); // add consumers this.consumers.forEach(consumer => { const { position } = consumer.props; this.timeline.add(consumer.getGSAP().play(), nullishCoalescing(position, 0)); }); } createIntersectionObserver() { const { resolution } = this.props; const options = { // root: this.fixedWrapperRef, root: null, rootMargin: '0px', threshold: Array.from({ length: resolution + 1 }, (v, i) => i / resolution), }; this.observer = new IntersectionObserver(this.intersectionObserverCallback, options); this.targetRefs.forEach(target => { this.observer && this.observer.observe(target); }); } unobserveAll() { this.targetRefs.forEach(target => { this.observer && this.observer.unobserve(target); }); } intersectionObserverCallback = (entries: any) => { const { triggerPosition } = this.props; const progresses = Array.from({ length: this.heights.length }, () => 0); const { heightVh } = this.props; for (const entry of entries) { console.log('rootBounds.height', entry.rootBounds.height); console.log('boundingClientRect.top', entry.boundingClientRect.top); console.log('boundingClientRect.height', entry.boundingClientRect.height); console.log('intersectionRatio', entry.intersectionRatio); console.log('intersectionRect.top', entry.intersectionRect.top); console.log('intersectionRect.height', entry.intersectionRect.height); console.log('isIntersecting', entry.isIntersecting); let progress = 0; if (triggerPosition === TriggerPosition.top) { const height = entry.boundingClientRect.height; const top = entry.boundingClientRect.top; const position = top <= 0 ? -top : 0; progress = position / height; } else if (triggerPosition === TriggerPosition.bottom) { const height = entry.boundingClientRect.height; const position = height - Math.max(Math.min(entry.boundingClientRect.top, height), 0); progress = position / height; } // console.log('progress', progress); const key = entry.target.dataset.key; progresses[parseInt(key, 10)] = progress; } if (this.emptyProgresses(progresses)) { // this.setState({ progress: 0 }); return; } const totalProgress = this.getTotalProgress(progresses); const progress = (totalProgress * 100) / heightVh; console.log('progresses', progresses); console.log('totalProgress', totalProgress); console.log('progress', progress); this.setState({ progress }); }; getTotalProgress(progresses: number[]) { // const length = progresses.length; return progresses.reduceRight((previousValue, currentValue) => { if (previousValue) { return Math.min(currentValue || 1, 1) + previousValue; } return currentValue; }); } emptyProgresses(progresses: number[]) { for (const progress of progresses) { if (progress) { return false; } } return true; } getGSAP() { return this.timeline; } render() { const { children } = this.props; const { progress } = this.state; const wrapper = ( <> {this.heights.map((height: number, index: number) => (
this.targetRefs.push(target)} key={index} data-key={index} > {index === 0 ? children(progress) : null}
))} ); return this.renderWithProvider(wrapper); } } export default Scroller;