Skip to content

Instantly share code, notes, and snippets.

@jesperlandberg
Last active February 13, 2019 14:56
Show Gist options
  • Select an option

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

Select an option

Save jesperlandberg/55e651c61398b3322278ce78d2ec903c to your computer and use it in GitHub Desktop.
Smooth
import Emitter from 'tiny-emitter'
export default new Emitter()
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
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
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