import { Num, } from './anvil' export interface ScrollaContinuousSetting { action?: Function isActive?: boolean isElementBound?: boolean isPaused?: boolean n?: number normalizeEdges?: boolean offsetBottom?: number offsetTop?: number ta?: number tb?: number tc?: number triggerDuration?: number triggerEl?: HTMLElement triggerLocation?: number } export interface ScrollaDiscreteSetting { action?: Function direction?: number isElementBound?: boolean isPaused?: boolean isTriggered?: boolean n?: number offset?: number ta?: number tb?: number triggerEl?: HTMLElement triggerInitial?: boolean triggerLocation?: number } export class Scrolla { public cxa: number = 0 public cxb: number = 0 public cxc: number = 0 public cya: number = 0 public cyb: number = 0 public cyc: number = 0 public continuousTriggers: ScrollaContinuousSetting[] public discreteTriggers: ScrollaDiscreteSetting[] public documentHeight: number = 0 public documentWidth: number = 0 public isResizing: boolean = false public isScrolling: boolean = false public resizeDebounceTimeout: number = 0.4 public scrollDebounceTimeout: number = 0.4 public scrollX: number = 0 public scrollXDelta: number = 0 public scrollY: number = 0 public scrollYDelta: number = 0 public viewHeightDelta: number = 0 public viewWidthDelta: number = 0 public onScroll: Function public onScrollEnd: Function public onScrollStart: Function constructor(properties) { this.continuousTriggers = new Array this.discreteTriggers = new Array return this } public initialize() { this.startListening() this._handleResize() this._handleScroll() this.computeAll() return this } public setProperties(properties: object): Scrolla { for (let key in properties) { this[key] = properties[key] } return this } public addContinuousTrigger(name: string, settings: ScrollaContinuousSetting): Scrolla { this.continuousTriggers[name] = this.applyContinuousTriggerSettings(settings) return this } public updateContinuousTrigger(name: string, settings: ScrollaContinuousSetting): Scrolla { if (typeof this.continuousTriggers[name] !== 'undefined') { for (let key in settings) { this.continuousTriggers[name][key] = settings[key] } } return this } public removeContinuousTrigger(name: string): Scrolla { if (typeof this.continuousTriggers[name] !== 'undefined') { delete this.continuousTriggers[name] } return this } public addDiscreteTrigger(name: string, settings: ScrollaDiscreteSetting): Scrolla { this.discreteTriggers[name] = this.applyDiscreteTriggerSettings(settings) return this } public updateDiscreteTrigger(name: string, settings: ScrollaDiscreteSetting): Scrolla { if (typeof this.discreteTriggers[name] !== 'undefined') { for (let key in settings) { this.discreteTriggers[name][key] = settings[key] } } return this } public removeDiscreteTrigger(name: string): Scrolla { if (typeof this.discreteTriggers[name] !== 'undefined') { delete this.discreteTriggers[name] } return this } public startListening(): Scrolla { window.addEventListener('scroll', this._handleScroll.bind(this)) window.addEventListener('resize', this._handleResize.bind(this)) return this } public stopListening(): Scrolla { window.removeEventListener('scroll', this._handleScroll) window.removeEventListener('resize', this._handleResize) return this } // private // continuous accessors private applyContinuousTriggerSettings(settings: ScrollaContinuousSetting): ScrollaContinuousSetting { let defaultSettings: ScrollaContinuousSetting = { action: (n: number, obj: object): void => { }, isActive: false, isElementBound: false, isPaused: false, n: 0, normalizeEdges: true, offsetBottom: 0, offsetTop: 0, ta: 0, tb: 0, tc: 0, triggerDuration: 0, triggerEl: undefined, triggerLocation: 0 } for (let key in settings) { defaultSettings[key] = settings[key] } return defaultSettings } private callContinuousAction(key: string, n: number): Scrolla { let action: Function = this.continuousTriggers[key].action window.requestAnimationFrame(action(n).bind(this)) return this } private computeNContinuous(): Scrolla { let c: number = this.cya + this.cyb let d: number = this.documentHeight for (let name in this.continuousTriggers) { let v: ScrollaContinuousSetting = this.continuousTriggers[name] let ot: number = v.normalizeEdges && this.cyb > v.ta ? v.ta - this.cyb : 0 let ob: number = v.normalizeEdges && this.cyb > v.tc ? v.tc - this.cyb : 0 let i: number = v.ta - Math.abs(ot) let e: number = (v.ta + v.tb + this.cyb) - Math.abs(ob) let n: number = Num.modulate(c, [i, e], 1) if (v.isActive && n <= 0) { if (v.isPaused === false) { this.callContinuousAction(name, n) } } else if (n >= 0 && n <= 1) { if (v.isPaused === false) { this.callContinuousAction(name, n) v.isActive = true } } else if (v.isActive && n >= 1) { if (v.isPaused === false) { this.callContinuousAction(name, n) v.isActive = false } } } return this } private mapContinuousTriggers() { for (let name in this.continuousTriggers) { let v: ScrollaContinuousSetting = this.continuousTriggers[name] // Ff trigger is element bound if ( v.isElementBound == true && typeof v.triggerEl !== 'undefined' && v.triggerEl.nodeType === 1 ) { let el = this.getElementLocation(v.triggerEl) v.ta = el[1] v.tb = v.triggerEl.offsetHeight } else { v.ta = v.triggerLocation v.tb = v.triggerDuration } v.ta += v.offsetTop v.tb += v.offsetBottom v.tc = this.documentHeight - (v.ta + v.tb) // Apply new settings this.continuousTriggers[name] = v } return this } private applyDiscreteTriggerSettings(settings: ScrollaDiscreteSetting): ScrollaDiscreteSetting { let defaultSettings: ScrollaDiscreteSetting = { action: (n, obj) => { }, direction: 0, isElementBound: false, isPaused: false, isTriggered: false, n: 0, offset: 0, ta: 0, tb: 0, triggerEl: undefined, triggerInitial: true, triggerLocation: 0 } for (let key in settings) { defaultSettings[key] = settings[key] } return defaultSettings } private computeNDiscrete() { // Need to calculate time difference between each computation // Compile variables let c: number = this.cya + this.cyb let d: number = this.scrollYDelta for (let name in this.discreteTriggers) { let v: ScrollaDiscreteSetting = this.discreteTriggers[name] if ( v.triggerInitial === true && v.isTriggered === false && (c >= v.ta) ) { v.isTriggered = true if (v.isPaused === false) { v.action(d, this) } } else if ( d >= 0 && c >= v.ta && (c - d) < v.ta ) { v.isTriggered = true if (v.isPaused === false) { v.action(d, this) } // going up } else if ( d < 0 && c <= v.ta && (c + d) > v.ta ) { v.isTriggered = true if (v.isPaused === false) { v.action(d, this) } } } return this } private mapDiscreteTriggers(): Scrolla { // loop through existing settings for (let name in this.discreteTriggers) { let v: ScrollaDiscreteSetting = this.discreteTriggers[name] // if trigger is element bound if ( v.isElementBound === true && v.triggerEl && v.triggerEl.nodeType === 1 ) { let el = this.getElementLocation(v.triggerEl) v.ta = el[1] + v.offset } else { v.ta = v.triggerLocation + v.offset } v.tb = this.documentHeight - v.ta this.discreteTriggers[name] = v } return this } private compute(): Scrolla { this.cxa = this.scrollX this.cya = this.scrollY this.cxb = window.innerWidth this.cyb = window.innerHeight this.cxc = this.documentWidth - (this.scrollX + window.innerWidth) this.cyc = this.documentHeight - (this.scrollY + window.innerHeight) return this } private mapAll(): Scrolla { this.mapContinuousTriggers() this.mapDiscreteTriggers() return this } private computeAll(): Scrolla { this.updateMetrics() this.compute() this.mapAll() this.computeNDiscrete() this.computeNContinuous() return this } private getElementLocation(element: HTMLElement): Array { let rect = element.getBoundingClientRect() return [this.scrollX + rect.left, this.scrollY + rect.top] } private updateMetrics(): Scrolla { this.getScrollValues() this.getDocumentDimensions() return this } private getDocumentDimensions(): Scrolla { let bodyEl: HTMLBodyElement = document.body let htmlEl: HTMLHtmlElement = document.documentElement this.documentHeight = Math.max( bodyEl.scrollHeight, bodyEl.offsetHeight, htmlEl.clientHeight, htmlEl.scrollHeight, htmlEl.offsetHeight ) this.documentWidth = Math.max( bodyEl.scrollWidth, bodyEl.offsetWidth, htmlEl.clientWidth, htmlEl.scrollWidth, htmlEl.offsetWidth ) return this } private getScrollValues(): Scrolla { this.scrollX = window.scrollX || window.pageXOffset || document.body.scrollLeft this.scrollY = window.scrollY || window.pageYOffset || document.body.scrollTop return this } private _handleScroll(): Scrolla { this.isScrolling = true let scrollXDelta: number = this.scrollX let scrollYDelta: number = this.scrollY this.getScrollValues() this.scrollXDelta = (scrollXDelta - this.scrollX) * -1 this.scrollYDelta = (scrollYDelta - this.scrollY) * -1 this.computeAll() return this } private _handleResize(): Scrolla { this.isResizing = true let viewHeightDelta: number = window.innerHeight let viewWidthDelta: number = window.innerWidth this.getDocumentDimensions() this.viewHeightDelta = (viewHeightDelta - window.innerHeight) * -1 this.viewWidthDelta = (viewWidthDelta - window.innerWidth) * -1 this.computeAll() return this } }