Skip to content

Instantly share code, notes, and snippets.

@baptistebriel
Created September 17, 2018 08:49
Show Gist options
  • Save baptistebriel/f046b0b14dd498b3f931b41bd456eed4 to your computer and use it in GitHub Desktop.
Save baptistebriel/f046b0b14dd498b3f931b41bd456eed4 to your computer and use it in GitHub Desktop.
smooth-scrolling
<div class="Scroll">
<div class="Scroll-bounding">
<div class="Scroll-section"></div>
<div class="Scroll-section"></div>
<div class="Scroll-section"></div>
<div class="Scroll-section"></div>
</div>
</div>
import { bindAll, assign, defer, defaults } from 'underscore'
import size from 'size'
import App from 'lib/app'
import Mn from 'backbone.marionette'
import prefix from 'vendor-prefix'
import VirtualScroll from 'virtual-scroll'
import template from './template.html'
import { props } from 'lib/decorators'
const transform = prefix('transform')
const visibility = 'visibility'
@props({
template,
className: 'inViewport'
})
export default class inViewport extends Mn.View {
initialize() {
bindAll(this, 'scroll', 'event', 'rAF', 'lock', 'unlock')
defaults(this.options, { event: 'animatedIn', direction: `y`, bounding: true })
super.initialize()
this.resizing = false
this.vs = null
this.sections = null
this.cache = null
this.data = {
ease: App.useNative ? 1 : App.isDesktop ? .1 : .25,
scrolling: false,
scrollable: true,
locked: false,
current: 0,
target: 0,
fixed: 0,
max: 0
}
}
onRender() {
this.once(this.options.event, () => {
if (!App.useNative) {
this.ui.scroll.addClass(`Scroll-virtual Scroll--${this.options.direction}`)
this.vs = new VirtualScroll({ passive: true, touchMultiplier: 3 })
this.vs.on(this.event, this)
} else {
this.ui.scroll.addClass(`Scroll-native Scroll--${this.options.direction}`)
this.ui.scroll.on('scroll', this.scroll)
}
App.raf.subscribe(`Scroll:${this.cid}`, this.rAF)
})
}
event(e) {
if (this.data.locked) return
this.data.target += Math.round(e.deltaY * -0.5)
this.clamp()
}
scroll(e) {
if (this.data.locked) return
const el = this.ui.scroll[0]
const scrollY = this.options.direction === `y` ? el.pageYOffset || el.scrollTop : el.pageXOffset || el.scrollLeft
this.data.target = scrollY
this.clamp()
}
lock() {
this.data.locked = true
this.ui.scroll[0].style.overflow = `hidden`
}
unlock() {
this.data.locked = false
this.ui.scroll[0].style.overflow = ``
}
clamp() {
this.data.target = Math.round(Math.min(Math.max(this.data.target, 0), this.data.max))
}
rAF() {
if (this.resizing) return
this.data.scrolling = this.data.target.toFixed() !== this.data.current.toFixed()
this.data.current += (this.data.target - this.data.current) * this.data.ease
this.data.fixed = this.data.scrolling ? 3 : 0
if (App.useNative) return
const el = this.ui.section[0]
if (!el.style) return
this.rAFSections()
this.rAFElements()
}
getSpeed(section, speed) {
const { start, middle } = section
const translate = this.data.current.toFixed(this.data.fixed)
const centered = translate - (start - middle)
return centered * speed
}
rAFSections() {
if (!this.sections) return
const { scrolling, fixed } = this.data
const translate = this.data.current.toFixed(fixed)
const translate3d = this.options.direction === 'y' ? `translate3d(0,-${translate}px,0)` : `translate3d(-${translate}px,0,0)`
this.sections.forEach((data, index) => {
const { inView } = this.isInView(data)
this.ui.section[index].style[transform] = inView ? translate3d : `none`
this.ui.section[index].style[visibility] = inView ? `visible` : `hidden`
if (!inView || data.fxs.length === 0) return
const { start, middle } = data
data.fxs.forEach((fx, i) => {
const { el, type, speed } = fx
const offset = this.getSpeed(data, speed)
let value = `none`
if (type === 'translateX') value = `translate3d(${(offset).toFixed(fixed)}px, 0, 0)`
if (type === 'translateY') value = `translate3d(0, ${(offset).toFixed(fixed)}px, 0)`
el.style[transform] = value
})
})
}
rAFElements() {
if (!this.cache || App.perf <= App.PERF_GOOD) return
const vertical = this.options.direction === 'y'
const threshold = this.options.threshold || 150
const boundary = (vertical ? size.height : size.width)
this.cache.forEach((data, index) => {
const el = this.ui.els[index]
const { inView, start, end, direction } = this.isInView(data)
if (!data.state && (start > -threshold) && (end < (boundary + threshold))) return
inView ? this.inView(el, data, direction) : this.outView(el, data, direction)
})
}
isInView(bounds) {
const { top, left } = bounds
const scrollY = this.data.current
const vertical = this.options.direction === 'y'
const threshold = this.options.threshold || (App.isDesktop ? 200 : 20)
const boundary = (vertical ? size.height : size.width)
const bottom = top + bounds.height
const right = left + bounds.width
const start = (vertical ? bounds.top : bounds.left) - scrollY
const end = (vertical ? bounds.bottom : bounds.right) - scrollY
const inView = start < (boundary + threshold) && end > -threshold
const direction = start < 0 ? 'top' : 'bottom'
return {
top, right, bottom, left,
start, end,
inView,
direction
}
}
inView(el, data, direction) {
if (data.state) return
data.state = true
const unit = 100
TweenMax.to(el, .2, {
opacity: 1,
ease: Linear.easeNone
})
if (App.perf >= App.PERF_GOOD) {
TweenMax.fromTo(el, 1, {
y: unit * (direction === 'top' ? -1 : 1)
}, {
y: 0,
ease: Expo.easeOut,
clearProps: 'transform'
})
}
}
outView(el, data, direction) {
if (!data.state) return
data.state = false
}
onResize(width, height) {
this.resizing = true
this.getSections()
this.getElements()
defer(() => {
!App.useNative && this.rAFSections()
this.getBounding(height, width)
this.resizing = false
})
}
getBounding(height, width) {
if (!this.options.bounding) return
if (!App.useNative) {
if (typeof this.ui.bounding[0] === 'string') return
const vertical = this.options.direction === 'y'
const scrollY = Math.round(this.data.target)
const bounding = this.ui.bounding[0].getBoundingClientRect()
const value = vertical ? bounding.height : bounding.width
this.data.max = value - (vertical ? height : width)
} else {
const el = this.ui.scroll[0]
this.data.max = Math.max(el.scrollHeight, el.offsetHeight, el.clientHeight)
}
}
getSections() {
this.sections = []
this.ui.section.each((index, el) => {
el.style[transform] = ''
defer(() => {
const bounds = el.getBoundingClientRect()
const { inView, top, left, right, bottom } = this.isInView(bounds)
const start = this.options.direction === `y` ? top + ((bottom - top) / 2) : left + ((right - left) / 2)
const middle = (this.options.direction === `y` ? size.height : size.width) / 2
const fxs = this.getEffects(el)
this.sections.push({
state: inView,
top: top,
left: left,
right: right,
bottom: bottom,
start: start,
middle: middle,
fxs: fxs
})
})
})
}
getEffects(el) {
const fxs = []
if (App.perf >= App.PERF_HIGH) {
const effects = Array.from(el.querySelectorAll('.Scroll-effect'))
effects.forEach((el, i) => {
const type = el.getAttribute('data-type')
const speed = el.getAttribute('data-speed') || 0
fxs.push({ el, type, speed })
})
}
return fxs
}
getElements() {
if (this.ui.els) {
this.cache = []
this.ui.els.each((index, el) => {
const bounds = el.getBoundingClientRect()
const { inView, top, left, right, bottom } = this.isInView(bounds)
this.cache.push({
state: inView,
top: top,
left: left,
right: right,
bottom: bottom
})
})
}
}
onBeforeDestroy() {
App.raf.unsubscribe(`Scroll:${this.cid}`)
if (!App.useNative) {
this.vs.off(this.event, this)
this.vs.destroy()
this.vs = null
} else {
this.ui.scroll.off('scroll', this.scroll)
}
this.sections = null
this.cache = null
this.data = null
}
}
.Scroll {
position: absolute;
top: 0; left: 0;
width: 100%;
height: 100%;
&.Scroll-native {
overflow-y: scroll;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
}
}
.Scroll-bounding {
position: absolute;
top: 0; left: 0;
width: 100%;
}
.Scroll-section {
position: relative;
width: 100%;
}
@baptistebriel
Copy link
Author

By the way, if App.useNative is equals to false, it will not transform any of the sections but rather will be a normal scroll—just like nothing would be initialized. I usually set App.useNative to false if it's a mobile device or if the performances of the client's device are poor. You could nevertheless apply some parallax transforms in the function rAF if it's a native scroll, it's up to you!

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