Last active
February 13, 2019 14:56
-
-
Save jesperlandberg/55e651c61398b3322278ce78d2ec903c to your computer and use it in GitHub Desktop.
Smooth
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 Emitter from 'tiny-emitter' | |
| export default new Emitter() |
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 EventBus from './EventBus' | |
| import TweenMax from 'gsap' | |
| class GlobalRAF { | |
| constructor() { | |
| this.tick = this.tick.bind(this) | |
| TweenMax.ticker.addEventListener('tick', this.tick) | |
| } | |
| tick() { | |
| EventBus.emit(GlobalRAF.events.TICK) | |
| } | |
| } | |
| GlobalRAF.events = { | |
| TICK: 'TICK' | |
| } | |
| export default new GlobalRAF() | |
| export const Events = GlobalRAF.events |
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 throttle from 'lodash.throttle' | |
| import EventBus from './EventBus' | |
| class GlobalResize { | |
| constructor() { | |
| this.onResize = throttle(this.onResize.bind(this), 200) | |
| window.addEventListener('resize', this.onResize) | |
| } | |
| onResize() { | |
| EventBus.emit(GlobalResize.events.RESIZE) | |
| } | |
| } | |
| GlobalResize.events = { | |
| RESIZE: 'GlobalResize.events.RESIZE', | |
| } | |
| export default new GlobalResize() | |
| export const Events = GlobalResize.events |
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' | |
| import VirtualScroll from 'virtual-scroll' | |
| // Import this in all files that use a ticker/raf | |
| import EventBus from '../utils/EventBus' | |
| import { Events as GlobalRAFEvents } from '../utils/GlobalRAF' | |
| import { Events as GlobalResizeEvents } from '../utils/GlobalResize' | |
| 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) | |
| // Tick off | |
| EventBus.off(GlobalRAFEvents.TICK, this.run) | |
| // Resize off | |
| EventBus.off(GlobalResizeEvents.RESIZE, this.resize) | |
| } | |
| addListeners() { | |
| this.vs.on(this.event) | |
| // Tick on | |
| EventBus.on(GlobalRAFEvents.TICK, this.run) | |
| // Resize on | |
| EventBus.on(GlobalResizeEvents.RESIZE, this.resize) | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment