Created
January 8, 2019 10:53
-
-
Save jesperlandberg/0f5375d9c949e110a88f869ff2f42b77 to your computer and use it in GitHub Desktop.
This file contains hidden or 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 imagesLoaded from 'imagesloaded' | |
| import TweenMax from 'gsap' | |
| export default class Smooth { | |
| constructor(options = {}) { | |
| this.bindAll() | |
| TweenMax.defaultEase = Linear.easeNone | |
| this.el = options.el || document.body | |
| const { | |
| sections = this.el.querySelectorAll('[data-smooth-section]'), | |
| elems = this.el.querySelectorAll('[data-from]'), | |
| threshold = 200, | |
| ease = 0.125, | |
| mouseMultiplier = 0.5, | |
| touchMultiplier = 2.5, | |
| firefoxMultiplier = 90, | |
| preventTouch = true, | |
| passive = false | |
| } = options | |
| this.dom = { | |
| el: this.el, | |
| sections: sections, | |
| elems: elems | |
| } | |
| this.state = { | |
| resizing: false, | |
| locked: false | |
| } | |
| this.data = { | |
| threshold: threshold, | |
| ease: ease, | |
| current: 0, | |
| last: 0, | |
| target: 0, | |
| bounding: 0, | |
| height: 0, | |
| max: 0, | |
| phone: window.matchMedia("(max-width: 640px)").matches | |
| } | |
| this.vs = new VirtualScroll({ | |
| el: this.el, | |
| mouseMultiplier: mouseMultiplier, | |
| touchMultiplier: touchMultiplier, | |
| firefoxMultiplier: firefoxMultiplier, | |
| preventTouch: preventTouch, | |
| passive: passive | |
| }) | |
| this.init() | |
| } | |
| bindAll() { | |
| ['run', 'event', 'resize'] | |
| .forEach(fn => this[fn] = this[fn].bind(this)) | |
| } | |
| init() { | |
| this.on() | |
| } | |
| on() { | |
| this.dom.el.classList.add('is-virtual-scroll') | |
| this.setStyles() | |
| this.getCache() | |
| this.getBounding() | |
| this.addListeners() | |
| this.preload() | |
| } | |
| setStyles() { | |
| this.dom.el.style.position = 'fixed' | |
| this.dom.el.style.top = 0 | |
| this.dom.el.style.left = 0 | |
| this.dom.el.style.width = '100%' | |
| } | |
| event(e) { | |
| this.data.target += Math.round(e.deltaY * -1) | |
| this.clamp() | |
| } | |
| clamp() { | |
| this.data.target = Math.round(Math.min(Math.max(this.data.target, 0), this.data.max)) | |
| } | |
| run() { | |
| if (this.state.resizing) return | |
| this.data.current += (this.data.target - this.data.current) * this.data.ease | |
| this.transformSections() | |
| this.animateElems() | |
| this.data.last = this.data.current | |
| } | |
| transformSections() { | |
| if (!this.sections) return | |
| const current = this.data.current | |
| const translate = this.data.current.toFixed(2) | |
| this.sections.forEach((data, index) => { | |
| const translate3d = `translate3d(0, ${-translate}px, 0)` | |
| const { isVisible } = this.isVisible(data) | |
| if (isVisible || this.state.resizing) this.dom.sections[index].style.transform = translate3d | |
| }) | |
| } | |
| animateElems() { | |
| if (!this.elems) return | |
| this.elems.forEach((data, index) => { | |
| const { isVisible, start, end } = this.isVisible(data, 0.01) | |
| if (isVisible) { | |
| this.intersectRatio(data, start, end) | |
| data.tl.progress(data.progress.current) | |
| } | |
| }) | |
| } | |
| intersectRatio(data, top, bottom) { | |
| const start = top - this.data.height | |
| const end = (this.data.height + bottom + data.height) * data.duration | |
| data.progress.current = Math.abs(start / end) | |
| data.progress.current = Math.max(0, Math.min(1, data.progress.current)) | |
| } | |
| isVisible(bounds, offset) { | |
| const current = this.data.current | |
| const threshold = !offset ? this.data.threshold : offset | |
| const start = bounds.top - current | |
| const end = bounds.bottom - current | |
| const isVisible = start < (threshold + this.data.height) && end > -threshold | |
| return { | |
| isVisible, | |
| start, | |
| end | |
| } | |
| } | |
| getCache() { | |
| this.getSections() | |
| this.getElems() | |
| } | |
| getSections() { | |
| if (!this.dom.sections) return | |
| this.sections = [] | |
| this.dom.sections.forEach((el) => { | |
| el.style.transform = '' | |
| const bounds = el.getBoundingClientRect() | |
| this.sections.push({ | |
| top: bounds.top, | |
| bottom: bounds.bottom | |
| }) | |
| }) | |
| } | |
| getElems() { | |
| if (!this.dom.elems) return | |
| this.elems = [] | |
| this.dom.elems.forEach(el => { | |
| if (el.dataset.animateMobile === undefined && this.data.phone) return | |
| const bounds = el.getBoundingClientRect() | |
| const tl = new TimelineLite({ paused: true }) | |
| const from = JSON.parse(el.dataset.from) | |
| const to = JSON.parse(el.dataset.to) | |
| tl.fromTo(el, 1, from, to) | |
| tl.progress(1) | |
| const boundsUpdated = el.getBoundingClientRect() | |
| tl.progress(0) | |
| this.elems.push({ | |
| el: el, | |
| tl: tl, | |
| top: bounds.top > this.data.height ? bounds.top : this.data.height, | |
| bottom: boundsUpdated.bottom, | |
| height: boundsUpdated.bottom - bounds.top, | |
| duration: el.dataset.duration ? el.dataset.duration : 1, | |
| progress: { | |
| current: 0 | |
| } | |
| }) | |
| }) | |
| } | |
| getBounding() { | |
| const bounding = this.dom.el.getBoundingClientRect() | |
| this.data.height = window.innerHeight | |
| this.data.bounding = bounding | |
| this.data.max = bounding.height - this.data.height | |
| } | |
| preload() { | |
| imagesLoaded(this.dom.el, (instance) => { | |
| this.resize() | |
| }) | |
| } | |
| resize() { | |
| this.state.resizing = true | |
| this.data.phone = window.matchMedia("(max-width: 640px)").matches | |
| this.getCache() | |
| this.transformSections() | |
| this.getBounding() | |
| this.state.resizing = false | |
| } | |
| destroy() { | |
| this.removeListeners() | |
| this.vs = null | |
| this.dom = null | |
| this.data = null | |
| this.elems = null | |
| this.sections = null | |
| } | |
| removeListeners() { | |
| this.vs.off(this.event) | |
| TweenMax.ticker.removeEventListener('tick', this.run) | |
| window.removeEventListener('resize', this.resize) | |
| } | |
| addListeners() { | |
| this.vs.on(this.event) | |
| TweenMax.ticker.addEventListener('tick', this.run) | |
| window.addEventListener('resize', this.resize) | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment