Last active
March 7, 2019 15:35
-
-
Save jesperlandberg/d6ce3e7c1646ec276fb5ee4347767e1f 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
| function bindAll(self, toBind) { | |
| const l = toBind.length | |
| for (let i = 0; i < l; i++) { | |
| self[toBind[i]] = self[toBind[i]].bind(self) | |
| } | |
| } | |
| export default bindAll |
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 TweenMax from 'gsap' | |
| import bindAll from './bindAll' | |
| import preload from './preload' | |
| import config from '../config' | |
| import EventBus from './EventBus' | |
| import { Events as ScrollControllerEvents } from './ScrollController' | |
| import { Events as GlobalResizeEvents } from '../utils/GlobalResize' | |
| class GlobalRAF { | |
| constructor() { | |
| bindAll(this, ['tick', 'event', 'update', 'setMaxHeight', 'onResize']) | |
| TweenMax.ticker.addEventListener('tick', this.tick) | |
| this.data = { | |
| ease: config.isDevice ? 0.1 : 0.135 | |
| } | |
| this.scroll = { | |
| target: 0, | |
| current: 0 | |
| } | |
| EventBus.on(ScrollControllerEvents.SCROLL, this.event) | |
| EventBus.on(GlobalResizeEvents.RESIZE, this.onResize) | |
| } | |
| tick() { | |
| this.scroll.current += (this.scroll.target - this.scroll.current) * this.data.ease | |
| EventBus.emit(GlobalRAF.events.TICK, { | |
| smooth: this.scroll.current | |
| }) | |
| } | |
| event({ y }) { | |
| this.scroll.target += y | |
| this.clampTarget() | |
| } | |
| clampTarget() { | |
| this.scroll.target = Math.round(Math.min(Math.max(this.scroll.target, 0), config.docHeight)) | |
| } | |
| setMaxHeight() { | |
| Object.assign(config, { docHeight: config.container.getBoundingClientRect().height - window.innerHeight }) | |
| } | |
| onResize() { | |
| Object.assign(config, { width: window.innerWidth, height: window.innerHeight }) | |
| this.setMaxHeight() | |
| this.clampTarget() | |
| } | |
| update() { | |
| this.scroll.current = this.scroll.target = 0 | |
| this.setMaxHeight() | |
| preload(this.setMaxHeight) | |
| } | |
| } | |
| 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
| <div class="js-smooth"> | |
| <div class="js-smooth-section"></div> | |
| <div class="js-smooth-section"></div> | |
| <div class="js-smooth-section"></div> | |
| <div class="js-smooth-section"></div> | |
| <div class="js-smooth-section"></div> | |
| </div> |
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
| function preload(callback) { | |
| const images = [].slice.call(document.querySelectorAll('img'), 0) | |
| images.forEach(image => { | |
| const img = document.createElement('img') | |
| img.addEventListener('load', () => { | |
| images.splice(images.indexOf(image), 1) | |
| images.length === 0 && callback | |
| }) | |
| img.src = image.src | |
| }) | |
| } | |
| export default preload |
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 VirtualScroll from 'virtual-scroll' | |
| class ScrollController { | |
| constructor() { | |
| this.el = document.querySelector('.js-smooth') | |
| this.setup() | |
| } | |
| setup() { | |
| this.vs = new VirtualScroll({ | |
| el: this.el, | |
| limitInertia: false, | |
| mouseMultiplier: 0.5, | |
| touchMultiplier: 3, | |
| firefoxMultiplier: 90, | |
| preventTouch: true, | |
| }) | |
| this.vs.on(this.onScroll) | |
| } | |
| onScroll(e) { | |
| EventBus.emit(ScrollController.events.SCROLL, { | |
| y: Math.round(e.deltaY * -1) | |
| }) | |
| } | |
| } | |
| ScrollController.events = { | |
| SCROLL: 'ScrollController.events.SCROLL' | |
| } | |
| export default new ScrollController() | |
| export const Events = ScrollController.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 TweenMax from 'gsap' | |
| import config from '../config' | |
| import math from '../utils/math' | |
| import bindAll from '../utils/bindAll' | |
| import preload from '../utils/preload' | |
| import EventBus from '../utils/EventBus' | |
| import { Events as GlobalRAFEvents } from '../utils/GlobalRAF' | |
| import { Events as GlobalResizeEvents } from '../utils/GlobalResize' | |
| import { Events as GlobalMouseEvents } from '../utils/GlobalMouse' | |
| export default class Smooth { | |
| constructor(options = {}) { | |
| bindAll(this, ['run', 'resize']) | |
| TweenMax.defaultEase = Linear.easeNone | |
| this.el = document.querySelector('.js-smooth') | |
| const { | |
| sections = document.querySelectorAll('.js-smooth-section'), | |
| elems = this.el.querySelectorAll('[data-from]'), | |
| threshold = 100 | |
| } = options | |
| this.dom = { | |
| el: this.el, | |
| sections: sections, | |
| elems: elems | |
| } | |
| this.state = { | |
| resizing: false | |
| } | |
| this.data = { | |
| threshold: threshold | |
| } | |
| this.init() | |
| } | |
| init() { | |
| this.on() | |
| } | |
| on() { | |
| this.dom.el.classList.add('is-virtual-scroll') | |
| this.setStyles() | |
| this.getCache() | |
| this.addListeners() | |
| preload(this.resize) | |
| } | |
| 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%' | |
| } | |
| run({ smooth }) { | |
| this.data.current = smooth | |
| this.transformSections() | |
| this.animateElems() | |
| } | |
| transformSections() { | |
| if (!this.sections) return | |
| const current = this.data.current | |
| const translate3d = `translate3d(0, ${-current.toFixed(2)}px, 0)` | |
| this.sections.forEach((data, index) => { | |
| const { isVisible } = this.isVisible(data) | |
| if (isVisible || this.state.resizing) { | |
| data.out = false | |
| data.el.style.transform = translate3d | |
| } else if (!data.out) { | |
| data.out = true | |
| data.el.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 - config.height | |
| const end = (config.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 + config.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({ | |
| el: el, | |
| top: bounds.top, | |
| bottom: bounds.bottom, | |
| out: true | |
| }) | |
| }) | |
| } | |
| getElems() { | |
| if (!this.dom.elems) return | |
| this.elems = [] | |
| this.dom.elems.forEach(el => { | |
| if (el.dataset.animateMobile === undefined && config.isSmall) 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 > config.height ? bounds.top : config.height, | |
| bottom: boundsUpdated.bottom, | |
| height: boundsUpdated.bottom - bounds.top, | |
| duration: el.dataset.duration ? el.dataset.duration : 1, | |
| progress: { | |
| current: 0 | |
| } | |
| }) | |
| }) | |
| } | |
| resize() { | |
| this.state.resizing = true | |
| this.sections.forEach(section => { | |
| section.el.style.transform = `` | |
| const bounds = section.el.getBoundingClientRect() | |
| section.top = bounds.top | |
| section.bottom = bounds.bottom | |
| }) | |
| this.transformSections() | |
| this.state.resizing = false | |
| } | |
| addListeners() { | |
| EventBus.on(GlobalRAFEvents.TICK, this.run) | |
| EventBus.on(GlobalResizeEvents.RESIZE, this.resize) | |
| } | |
| removeListeners() { | |
| EventBus.off(GlobalRAFEvents.TICK, this.run) | |
| EventBus.off(GlobalResizeEvents.RESIZE, this.resize) | |
| } | |
| destroy() { | |
| this.removeListeners() | |
| this.dom = null | |
| this.data = null | |
| this.state = null | |
| this.elems = null | |
| this.sections = null | |
| } | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
In ScrollController.js I setup a new Virtual scroll, and emit the scroll value
In GlobalRAF.js I import the ScrollController, do the calcs needed for the smooth scroll value, update global variables holding container and viewport height
in Smooth.js I import the GlobalRAF as seen at the imports, which I then add under addListeners. In this.run() I can now access { smooth } as a parameter. And I can do that in any file that uses the raf.