Skip to content

Instantly share code, notes, and snippets.

@jesperlandberg
Last active March 7, 2019 15:35
Show Gist options
  • Select an option

  • Save jesperlandberg/d6ce3e7c1646ec276fb5ee4347767e1f to your computer and use it in GitHub Desktop.

Select an option

Save jesperlandberg/d6ce3e7c1646ec276fb5ee4347767e1f to your computer and use it in GitHub Desktop.
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
import Emitter from 'tiny-emitter'
export default new Emitter()
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
<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>
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
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
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
}
}
@jesperlandberg
Copy link
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment