///
///
// namespace
namespace cf {
export interface IScrollControllerOptions{
interactionListener: HTMLElement;
eventTarget: EventDispatcher;
listToScroll: HTMLElement;
listNavButtons: NodeListOf;
}
export class ScrollController{
public static acceleration: number = 0.1;
private eventTarget: EventDispatcher;
private interactionListener: HTMLElement;
private listToScroll: HTMLElement;
private listWidth: number = 0;
private prevButton: Element;
private nextButton: Element;
private rAF: number;
private visibleAreaWidth: number = 0;
private max: number = 0;
private onListNavButtonsClickCallback: () => void;
private documentLeaveCallback: () => void;
private onInteractStartCallback: () => void;
private onInteractEndCallback: () => void;
private onInteractMoveCallback: () => void;
private interacting: boolean = false;
private x: number = 0;
private xTarget: number = 0;
private startX: number = 0;
private startXTarget: number = 0;
private mouseSpeed: number = 0;
private mouseSpeedTarget: number = 0;
private direction: number = 0;
private directionTarget: number = 0;
private inputAccerlation: number = 0;
private inputAccerlationTarget: number = 0;
constructor(options: IScrollControllerOptions){
this.interactionListener = options.interactionListener;
this.eventTarget = options.eventTarget;
this.listToScroll = options.listToScroll;
this.prevButton = options.listNavButtons[0];
this.nextButton = options.listNavButtons[1];
this.onListNavButtonsClickCallback = this.onListNavButtonsClick.bind(this);
this.prevButton.addEventListener("click", this.onListNavButtonsClickCallback, false);
this.nextButton.addEventListener("click", this.onListNavButtonsClickCallback, false);
this.documentLeaveCallback = this.documentLeave.bind(this);
this.onInteractStartCallback = this.onInteractStart.bind(this);
this.onInteractEndCallback = this.onInteractEnd.bind(this);
this.onInteractMoveCallback = this.onInteractMove.bind(this);
document.addEventListener("mouseleave", this.documentLeaveCallback, false);
document.addEventListener(Helpers.getMouseEvent("mouseup"), this.documentLeaveCallback, false);
this.interactionListener.addEventListener(Helpers.getMouseEvent("mousedown"), this.onInteractStartCallback, false);
this.interactionListener.addEventListener(Helpers.getMouseEvent("mouseup"), this.onInteractEndCallback, false);
this.interactionListener.addEventListener(Helpers.getMouseEvent("mousemove"), this.onInteractMoveCallback, false);
}
private onListNavButtonsClick(event: MouseEvent){
const dirClick: string = ( event.currentTarget).getAttribute("direction");
this.pushDirection(dirClick == "next" ? -1 : 1);
}
private documentLeave(event: MouseEvent | TouchEvent){
this.onInteractEnd(event);
}
private onInteractStart(event: MouseEvent | TouchEvent){
const vector: TouchVector2d = Helpers.getXYFromMouseTouchEvent(event);
this.interacting = true;
this.startX = vector.x;
this.startXTarget = this.startX;
this.inputAccerlation = 0;
this.render();
}
private onInteractEnd(event: MouseEvent | TouchEvent){
this.interacting = false;
}
private onInteractMove(event: MouseEvent | TouchEvent){
if(this.interacting){
const vector: TouchVector2d = Helpers.getXYFromMouseTouchEvent(event);
const newAcc: number = vector.x - this.startX;
const magnifier: number = 6.2;
this.inputAccerlationTarget = newAcc * magnifier;
this.directionTarget = this.inputAccerlationTarget < 0 ? -1 : 1;
this.startXTarget = vector.x;
}
}
private render(){
if(this.rAF)
cancelAnimationFrame(this.rAF);
// normalise startX
this.startX += (this.startXTarget - this.startX) * 0.2;
// animate accerlaration
this.inputAccerlation += (this.inputAccerlationTarget - this.inputAccerlation) * (this.interacting ? Math.min(ScrollController.acceleration + 0.1, 1) : ScrollController.acceleration);
const accDamping: number = 0.25;
this.inputAccerlationTarget *= accDamping;
// animate directions
this.direction += (this.directionTarget - this.direction) * 0.2;
// extra extra
this.mouseSpeed += (this.mouseSpeedTarget - this.mouseSpeed) * 0.2;
this.direction += this.mouseSpeed;
// animate x
this.xTarget += this.inputAccerlation * 0.05;
// bounce back when over
if(this.xTarget > 0)
this.xTarget += (0 - this.xTarget) * Helpers.lerp(ScrollController.acceleration, 0.3, 0.8);
if(this.xTarget < this.max)
this.xTarget += (this.max - this.xTarget) * Helpers.lerp(ScrollController.acceleration, 0.3, 0.8);
this.x += (this.xTarget - this.x) * 0.4;
// toggle visibility on nav arrows
const xRounded: number = Math.round(this.x);
if(xRounded < 0){
if(!this.prevButton.classList.contains("active"))
this.prevButton.classList.add("active");
if(!this.prevButton.classList.contains("cf-gradient"))
this.prevButton.classList.add("cf-gradient");
}
if(xRounded == 0){
if(this.prevButton.classList.contains("active"))
this.prevButton.classList.remove("active");
if(this.prevButton.classList.contains("cf-gradient"))
this.prevButton.classList.remove("cf-gradient");
}
if(xRounded > this.max){
if(!this.nextButton.classList.contains("active"))
this.nextButton.classList.add("active");
if(!this.nextButton.classList.contains("cf-gradient"))
this.nextButton.classList.add("cf-gradient");
}
if(xRounded <= this.max){
if(this.nextButton.classList.contains("active"))
this.nextButton.classList.remove("active");
if(this.nextButton.classList.contains("cf-gradient"))
this.nextButton.classList.remove("cf-gradient");
}
// set css transforms
const xx: number = this.x;
Helpers.setTransform(this.listToScroll, "translateX("+xx+"px)");
// cycle render
if(this.interacting || (Math.abs(this.x -this.xTarget) > 0.02 && !this.interacting))
this.rAF = window.requestAnimationFrame(() => this.render());
}
public setScroll(x: number, y: number){
this.xTarget = this.visibleAreaWidth == this.listWidth ? 0 : x;
this.render();
}
public pushDirection(dir: number){
this.inputAccerlationTarget += (5000) * dir;
this.render();
}
public dealloc(){
this.prevButton.removeEventListener("click", this.onListNavButtonsClickCallback, false);
this.nextButton.removeEventListener("click", this.onListNavButtonsClickCallback, false);
this.onListNavButtonsClickCallback = null;
this.prevButton = null;
this.nextButton = null;
document.removeEventListener("mouseleave", this.documentLeaveCallback, false);
document.removeEventListener(Helpers.getMouseEvent("mouseup"), this.documentLeaveCallback, false);
this.interactionListener.removeEventListener(Helpers.getMouseEvent("mousedown"), this.onInteractStartCallback, false);
this.interactionListener.removeEventListener(Helpers.getMouseEvent("mouseup"), this.onInteractEndCallback, false);
this.interactionListener.removeEventListener(Helpers.getMouseEvent("mousemove"), this.onInteractMoveCallback, false);
this.documentLeaveCallback = null;
this.onInteractStartCallback = null;
this.onInteractEndCallback = null;
this.onInteractMoveCallback = null;
}
public reset(){
this.interacting = false;
this.startX = 0;
this.startXTarget = this.startX;
this.inputAccerlation = 0;
this.x = 0;
this.xTarget = 0;
Helpers.setTransform(this.listToScroll, "translateX(0px)");
this.render();
this.prevButton.classList.remove("active");
this.nextButton.classList.remove("active");
}
public resize(listWidth: number, visibleAreaWidth: number){
this.reset();
this.visibleAreaWidth = visibleAreaWidth;
this.listWidth = Math.max(visibleAreaWidth, listWidth);
this.max = (this.listWidth - this.visibleAreaWidth) * -1;
this.render();
}
}
}