export interface ITweenElement extends HTMLElement { tweenId: number; } /** * Basic tweening implementation. * * @export * @class Tween */ export class Tween { /** * Translates the element to position. * * @static * @param {HTMLElement} element Element to translate. * @param {number} x X position. * @param {number} y Y position. * @param {number} duration Duration in millis. * @param {() => void} [end] Animation end callback. * @memberof Tween */ public static translate(element: HTMLElement, x: number, y: number, duration: number, end?: () => void) { const source = this.getTranslate(element); const id = this.animationId++; (element as ITweenElement).tweenId = id; const update = (progress: number) => { if ((element as ITweenElement).tweenId !== id) { return false; } const value = { x: this.interpolate(source.x, x, progress), y: this.interpolate(source.y, y, progress), }; this.setTranslate(element, value); return true; }; this.animate(duration, update, end); } /** * Unique animation ID. * * @private * @static * @type {number} * @memberof Tween */ private static animationId: number = 1; /** * Animates using callback loop. * * @private * @static * @param {number} duration Duration in millis. * @param {(progress: number) => boolean} update Animation callback. Must return true for animation to continue. * @param {() => void} [end] Animation end callback. * @memberof Tween */ private static animate(duration: number, update: (progress: number) => boolean, end?: () => void) { if (duration === 0) { update(1); if (end != null) { end(); } return; } const start = new Date().getTime(); const loop = () => { const now = new Date().getTime(); const relative = (now - start) / duration; const progress = Math.min(relative, 1); const shouldContinue = update(progress); if (shouldContinue) { if (progress < 1) { this.requestFrame(loop); } else if (end != null) { end(); } } }; this.requestFrame(loop); } /** * Gets current translate value. * * @private * @static * @param {HTMLElement} element Element to get value. * @returns {{ x: number, y: number }} Translate value. * @memberof Tween */ private static getTranslate(element: HTMLElement): { x: number; y: number } { const source = { x: 0, y: 0 }; const regex = /translate\(([0-9.-]+)px,\s?([0-9.-]+)px\)/; const value = (element.style as any)["-ms-transform"] || (element.style as any)["-webkit-transform"] || (element.style as any)["-moz-transform"] || (element.style as any)["-o-transform"] || element.style.transform; if (value != null) { const match = regex.exec(value); if (match != null) { source.x = parseFloat(match[1]); source.y = parseFloat(match[2]); } } return source; } /** * Sets translate value. * * @private * @static * @param {HTMLElement} element Element to set value. * @param {{ x: number, y: number }} value Translate value. * @memberof Tween */ private static setTranslate(element: HTMLElement, value: { x: number; y: number }) { const transform = `translate(${Math.ceil(value.x)}px, ${Math.ceil(value.y)}px)`; (element.style as any)["-ms-transform"] = transform; (element.style as any)["-webkit-transform"] = transform; (element.style as any)["-moz-transform"] = transform; (element.style as any)["-o-transform"] = transform; element.style.transform = transform; } /** * Request animation frame helper. * * @private * @static * @param {() => void} callback Animation frame callback. * @memberof Tween */ private static requestFrame(callback: () => void) { if (requestAnimationFrame != null) { requestAnimationFrame(callback); } else { window.setTimeout(callback, 1000 / 60); } } /** * Interpolates two numbers. * * @private * @static * @param {number} source Source value. * @param {number} dest Destination value. * @param {number} amount Percentage. * @returns {number} Interpolated value. * @memberof Tween */ private static interpolate(source: number, dest: number, amount: number): number { return this.easeOutCubic(amount, source, dest - source, 1); } /** * Cubic easing out function. * Inspired from: http://robertpenner.com/easing/penner_easing_as1.txt * * @private * @static * @param {number} t Time. * @param {number} b Base value. * @param {number} c Change in value. * @param {number} d Duration. * @returns {number} Interpolated value. * @memberof Tween */ private static easeOutCubic(t: number, b: number, c: number, d: number): number { return c * ((t = t / d - 1) * t * t + 1) + b; } }