Skip to content

Instantly share code, notes, and snippets.

@jesperlandberg
Created May 10, 2019 08:25
Show Gist options
  • Select an option

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

Select an option

Save jesperlandberg/22eb6b08837a19d5dc3042e793546fe4 to your computer and use it in GitHub Desktop.
import sniffer from 'sniffer'
import GlobalRAF from './utils/GlobalRAF'
import Smooth from './scripts/Smooth'
class App {
constructor() {
if (sniffer.isDesktop) {
GlobalRAF.update()
new Smooth()
}
}
}
new App()
export default {
html: document.documentElement,
body: document.body,
width: window.innerWidth,
height: window.innerHeight,
container: document.querySelector('.js-smooth'),
docHeight: 0,
}
import Emitter from 'tiny-emitter';
export default new Emitter();
import sniffer from 'sniffer';
import preload from './preload';
import config from '../config';
import EventBus from './EventBus';
import { Events as ScrollControllerEvents } from './ScrollController';
import { Events as GlobalResizeEvents } from './GlobalResize';
class GlobalRAF {
constructor() {
TweenMax.ticker.addEventListener('tick', this.tick);
this.data = {
ease: 0.125,
};
this.scroll = {
target: 0,
current: 0,
};
this.addListeners();
}
setMaxHeight = () => {
Object.assign(config, {
docHeight: config.container.getBoundingClientRect().height - window.innerHeight,
});
}
tick = () => {
if (!sniffer.isDevice) {
this.scroll.current += (this.scroll.target - this.scroll.current) * this.data.ease;
} else {
this.scroll.current = this.scroll.target;
}
EventBus.emit(GlobalRAF.events.TICK, {
target: this.scroll.target,
smooth: this.scroll.current,
});
}
event = ({ y }) => {
if (!sniffer.isDevice) {
this.scroll.target += y;
this.clampTarget();
} else {
this.scroll.target = y;
}
}
clampTarget() {
this.scroll.target = Math.round(Math.min(Math.max(this.scroll.target, 0), config.docHeight));
}
onResize = () => {
Object.assign(config, { width: window.innerWidth, height: window.innerHeight });
if (!sniffer.isDevice) {
this.setMaxHeight();
this.clampTarget();
}
}
update = () => {
this.scroll.current = 0;
this.scroll.target = 0;
this.setMaxHeight();
preload(this.setMaxHeight);
}
addListeners() {
EventBus.on(ScrollControllerEvents.SCROLL, this.event);
EventBus.on(GlobalResizeEvents.RESIZE, this.onResize);
}
}
GlobalRAF.events = {
TICK: 'TICK',
};
export default new GlobalRAF();
export const Events = GlobalRAF.events;
import debounce from 'lodash.debounce';
import EventBus from './EventBus';
class GlobalResize {
constructor() {
window.addEventListener('resize', debounce(this.onResize, 200));
}
onResize = () => {
EventBus.emit(GlobalResize.events.RESIZE);
}
}
GlobalResize.events = {
RESIZE: 'GlobalResize.events.RESIZE',
};
export default new GlobalResize();
export const Events = GlobalResize.events;
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);
if (images.length === 0) callback();
});
img.src = image.src;
});
}
export default preload;
import VirtualScroll from 'virtual-scroll';
import sniffer from 'sniffer';
import EventBus from './EventBus';
class ScrollController {
constructor() {
this.setup();
}
setup() {
if (sniffer.isDevice) {
window.addEventListener('scroll', this.onNativeScroll, { passive: true });
} else {
this.vs = new VirtualScroll({
limitInertia: false,
mouseMultiplier: 0.4,
touchMultiplier: 3,
firefoxMultiplier: 90,
passive: true,
});
this.vs.on(this.onScroll);
}
}
onScroll = (e) => {
EventBus.emit(ScrollController.events.SCROLL, {
y: Math.round(e.deltaY * -1),
});
}
onNativeScroll = () => {
EventBus.emit(ScrollController.events.SCROLL, {
y: window.scrollY,
});
}
}
ScrollController.events = {
SCROLL: 'ScrollController.events.SCROLL',
};
export default new ScrollController();
export const Events = ScrollController.events;
import config from '../config'
import preload from '../utils/preload'
import EventBus from '../utils/EventBus'
import { Events as GlobalRAFEvents } from '../utils/GlobalRAF'
import { Events as GlobalResizeEvents } from '../utils/GlobalResize'
class Smooth {
constructor() {
this.el = config.container
this.dom = {
el: this.el,
sections: document.querySelectorAll('.js-smooth-section')
}
this.state = {
current: 0,
target: 0,
threshold: 100,
isResizing: false,
toggled: false
}
this.init()
}
init() {
this.on()
}
on() {
this.setStyles()
this.getCache()
this.addListeners()
preload(this.onResize)
}
setStyles() {
config.body.classList.add('is-virtual-scroll')
Object.assign(this.dom.el.style, {
position: 'fixed',
top: 0,
left: 0,
width: '100%',
})
}
run = ({ smooth }) => {
this.state.current = smooth
this.transformSections()
}
transformSections() {
const { current, isResizing } = this.state
const translate3d = `translate3d(0, ${-current.toFixed(2)}px, 0)`
this.sections.forEach((data) => {
const { el, bounds } = data
const isVisible = this.isVisible(bounds)
if (isVisible || isResizing) {
Object.assign(data, { out: false })
el.style.transform = translate3d
} else if (!data.out) {
Object.assign(data, { out: true })
el.style.transform = translate3d
}
})
}
isVisible(bounds) {
const { current, threshold } = this.state
const { top, bottom } = bounds
const start = top - current
const end = bottom - current
const isVisible = start < (threshold + config.height) && end > -threshold
return isVisible
}
getCache() {
this.getSections()
}
getSections() {
if (!this.dom.sections) return
this.sections = []
this.dom.sections.forEach((el) => {
Object.assign(el.style, { transform: '' })
const { top, bottom } = el.getBoundingClientRect()
const state = {
el,
bounds: {
top,
bottom,
},
out: true,
}
this.sections.push(state)
})
}
onResize = () => {
this.state.isResizing = true
if (this.sections) {
this.sections.forEach((section) => {
section.el.style.transform = ''
const bounds = section.el.getBoundingClientRect()
section.bounds.top = bounds.top
section.bounds.bottom = bounds.bottom
})
this.transformSections()
}
this.state.isResizing = false
}
addListeners() {
EventBus.on(GlobalRAFEvents.TICK, this.run)
EventBus.on(GlobalResizeEvents.RESIZE, this.onResize)
}
}
export default Smooth
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment