rx/decelerate.ts

/**
 * @packageDocumentation
 * @module rx
 */
import { Observable, Subject } from 'rxjs';

/**
 * 감속 설정값
 * @interface
 * @property start {number} 시작 값
 * @property end {number} 도달하려는 값
 * @property friction {number} 0 < n <= 1 감속 마찰값(높을 수록 빠르게 도달)
 * @property delay {number} 감속 계산 사이 대기시간(ms)
 */
interface DecelerateConfig {
  start: number;
  end: number;
  friction?: number;
  delay?: number;
}

/**
 * start(시작값) 부터 end(도달값) 까지 friction(마찰값)에 영향을 받아 감속하며 도달
 * @export
 * @param {DecelerateConfig} config
 * @returns {Observable<number>}
 * @example
 *  decelerate({ start: 0, end: 100, friction: 0.1, delay: 0 }).subscribe(value => {
 *    console.log(value); // 0 ~ ...... ~ 100
 *  });
 */
export function decelerate(config: DecelerateConfig): Observable<number> {
  const { start: targetStart, end: targetEnd, friction = 0.1, delay = 0 } = config;
  if (friction <= 0) {
    throw new Error(`Friction can't be set below zero.`);
  }
  const subject = new Subject<number>();
  const observable = subject.asObservable();
  const finish = targetEnd - 0.01;
  const finishGap = 0.1;
  const targetFriction = Math.min(1, friction);
  const targetDelay = Math.max(0, delay);
  const isDeley = 0 < delay;
  let targetTick = targetStart;
  let requestTimer: any;

  function tick() {
    const isFinish = Math.abs(targetTick - finish) < finishGap;
    const value: number = isFinish ? targetEnd : targetTick;
    if (isFinish) {
      dispose();
    } else {
      nextTick();
    }
    dispatch(value);
    targetTick = targetTick + (targetEnd - targetTick) * targetFriction;
  }

  function nextTick() {
    if (isDeley) {
      requestTimer = setTimeout(tick, targetDelay);
    } else {
      requestTimer = requestAnimationFrame(tick);
    }
  }

  function dispatch(value: number) {
    subject.next(value);
  }

  function dispose() {
    if (isDeley) {
      clearTimeout(requestTimer);
    } else {
      cancelAnimationFrame(requestTimer);
    }
    requestTimer = null;
  }

  nextTick();

  return observable;
}