effects/scatter.js

import _ from 'lodash';
import Graphics from '../graphics';
import Utility from '../utility';
import { colors } from '../variable';

/**
 * [Effect]{@link Effect} 에서 생성, 관리하는 Scatter 차트의 이펙트 클래스
 * @alias ScatterEffect
 */
export default class ScatterEffect {
  /**
   * @description Scatter차트 이펙트에서 필요한 값들을 설정
   * @param  { object } _object 이펙트 생성에 필요한 각종 오브젝트들
   * @param  { object } _option 이펙트 생성에 필요한 옵션
   */
  constructor(_object, _option) {
    /**
     * @description ChartJS 오브젝트
     * @type { ChartJS }
     */
    this.chart = _object.chart;

    /**
     * @description 이펙트를 생성할 PIXI 오브젝트
     * @type { PIXI }
     */
    this.pixi = _object.pixi;

    /**
     * @description Effect 클래스
     * @type { Effect }
     */
    this.parent = _object.effect;

    /**
     * @description 생성할 이펙트 옵션
     * @type { object }
     */
    this.option = _option;

    /**
     * @description 이펙트를 표현할 Graphics
     * @type { Graphics }
     */
    this.graphics = new Graphics(this.pixi);

    /**
     * @description 미사일 지속시간
     * @type { number }
     */
    this.missileDuration = 1;

    /**
     * @description 폭팔 지속시간
     * @type { number }
     */
    this.explosionDuration = 1;

    /**
     * @description 미사일 발사 횟수
     * @type { number }
     */
    this.missileIndex = 0;

    /**
     * @description 미사일 폭파 허가 된 인덱스
     * @type { number }
     */
    this.explosionAllowed = 0;

    /**
     * @description Timeline에 의해 미사일 궤도가 새로 그려져 더 이상 움직이지 않는 미사일의 progress
     * @type { object }
     */
    this.stopProgress = {};

    /**
     * @description 미사일의 마지막 위치
     * @type { array }
     */
    this.missileLastPosition = [];

    this.chart.config.options.animation.duration = 0;
    this.option.max = this.option.max || {};
    this.option.max.x = this.option.max.x || 100;
    this.option.max.y = this.option.max.y || 100;
    this.chart.options.scales.xAxes[0].ticks.suggestedMax = this.option.max.x;
    this.chart.options.scales.yAxes[0].ticks.suggestedMax = this.option.max.y;
    this.chart.update();
  }

  /**
   * 현재 좌표에서 변경되는 좌표로 미사일이 날아가는 듯한 효과를 생성한다.
   * @param {object} datasets 변경할 datasets
   * @memberof ScatterEffect
   * @instance
   */
  missile(datasets) {
    if (_.isUndefined(datasets)) return;
    this.prevProgress = 0;

    const index = this.missileIndex++;
    this.explosionAllowed = index;
    const originalDatasets = this.chart.config.data.datasets;
    for (let i = 0; i < originalDatasets.length; i++) {
      const originalData = originalDatasets[i].data;
      const changeData = datasets[i].data;
      for (let j = 0; j < originalData.length; j++) {
        const originalPoint = originalData[j];
        const changePoint = changeData[j];
        const angleBetween = Math.degrees(Utility.findAngleBetweenTwoPoints(changePoint, originalPoint)) + 180;
        const distanceBetween = Utility.findDistanceBetweenTwoPoints(originalPoint, changePoint);
        const UDFlag = Math.floor(Math.random() * 2) === 0 ? -1 : 1;
        const controlAngle = angleBetween + (70 + Math.floor(Math.random() * 20)) * UDFlag; // 45 ~ 90도
        const controlPoint = Utility.findNewPointByAngleWithDistance(originalPoint, controlAngle, Math.floor(Math.random() * (distanceBetween / 2)) + distanceBetween / 4);
        const startPoint = _.isUndefined(this.missileLastPosition[j]) ? this.toChart(originalPoint) : this.missileLastPosition[j];

        const graphics = new Graphics(this.pixi);
        const pointArray = this.makeBezierCurveWithTime(startPoint, this.toChart(changePoint), this.toChart(controlPoint));
        const startTime = new Date();
        this.addMissileInterval(pointArray, startTime, graphics, datasets, changePoint, index, j);
      }
    }
  }

  /**
   * missile 효과의 업데이트를 추가한다
   * @param {array} pointArray bezierCurve 포인터 정보가 담긴 배열
   * @param {Date} startTime missile 효과 시작 시간
   * @param {Graphics} graphics 효과를 그려주는 오브젝트
   * @param {object} datasets 변경될 데이터셋
   * @param {object} changePoint 변경될 지점
   * @param {number} missileIndex 미사일 인덱스
   * @param {number} groupIndex 한번에 발사될 때 몇번째로 발사되었는지 나타내는 인덱스
   * @memberof ScatterEffect
   * @instance
   */
  addMissileInterval(pointArray, startTime, graphics, datasets, changePoint, missileIndex, groupIndex) {
    const intervalIndex = setInterval(() => {
      this.missileUpdate(intervalIndex, pointArray, startTime, graphics, datasets, changePoint, missileIndex, groupIndex);
    }, 16);
  }

  /**
   * missile 효과의 업데이트
   * @param {number} intervalIndex 업데이트 interval 인덱스 (제거를 위해)
   * @param {array} pointArray bezierCurve 포인터 정보가 담긴 배열
   * @param {Date} startTime missile 효과 시작 시간
   * @param {Graphics} graphics 효과를 그려주는 오브젝트
   * @param {object} datasets 변경될 데이터셋
   * @param {object} changePoint 변경될 지점
   * @param {number} missileIndex 미사일 인덱스
   * @param {number} groupIndex 한번에 발사될 때 몇번째로 발사되었는지 나타내는 인덱스
   * @memberof ScatterEffect
   * @instance
   */
  missileUpdate(intervalIndex, pointArray, startTime, graphics, datasets, changePoint, missileIndex, groupIndex) {
    const elapsedTime = new Date() - startTime;

    if (this.explosionAllowed !== missileIndex && _.isUndefined(this.stopProgress[missileIndex])) {
      this.stopProgress[missileIndex] = elapsedTime / (this.missileDuration * 0.5 * 1000) > 1 ? 1 : elapsedTime / (this.missileDuration * 0.5 * 1000);
    }
    const calculateDrawProgres = elapsedTime / (this.missileDuration * 0.5 * 1000) > 1 ? 1 : elapsedTime / (this.missileDuration * 0.5 * 1000);
    const drawProgress = _.isUndefined(this.stopProgress[missileIndex]) ? calculateDrawProgres : this.stopProgress[missileIndex];
    const progress = elapsedTime / (this.missileDuration * 1000) > 1 ? 1 : elapsedTime / (this.missileDuration * 1000);

    graphics.object.clear();

    for (let i = 0; i < drawProgress * 100; i++) {
      const point = pointArray[i];
      if (this.prevPoint === undefined || i === 0) {
        this.prevPoint = point;
      }

      const alpha = 1 - (progress * 2) + (i * 0.01);
      if (this.prevPoint) {
        graphics.object.lineStyle(1, 0x0000FF, alpha);
        graphics.object.moveTo(this.prevPoint.x, this.prevPoint.y);
        graphics.object.lineTo(point.x, point.y);
        this.prevPoint = point;
      }

      if (missileIndex === this.explosionAllowed) {
        this.missileLastPosition[groupIndex] = point;
      }
    }
    this.prevProgress = drawProgress;


    graphics.isExplosion = graphics.isExplosion || false;
    if (!graphics.isExplosion && drawProgress === 1) {
      graphics.isExplosion = true;
      if (this.explosionAllowed === missileIndex) {
        this.explosion(this.toChart(changePoint));
        this.missileLastPosition[groupIndex] = undefined;
      }
      this.chart.config.data.datasets[0].data = datasets[0].data;
      this.chart.update();
    }

    if (progress >= 1) {
      graphics.object.clear();
      graphics.destroy();
      clearInterval(intervalIndex);
    }
  }

  /**
   * 현재 좌표에서 폭팔 효과를 생성한다 missile 효과와 연계
   * @param {object} point exlposion 효과가 생길 좌표
   * @memberof ScatterEffect
   * @instance
   */
  explosion(point) {
    const explosionCount = 5;
    let flagLR = 1; // -1 = left 1 = right

    for (let i = 0; i < explosionCount; i++) {
      flagLR *= -1;
      const controlAngle = Math.floor(Math.random() * 10) * flagLR - 90;
      const controlDistance = Math.floor(Math.random() * 40 + 50);
      const controlPoint = Utility.findNewPointByAngleWithDistance(point, controlAngle, controlDistance);
      const finalAngle = (Math.floor(Math.random() * 60) + 80) * flagLR - 90;
      const finalDistance = Math.floor(Math.random() * 90 + 10);
      const finalPoint = Utility.findNewPointByAngleWithDistance(point, finalAngle, finalDistance);
      const pointArray = this.makeBezierCurveWithTime(point, finalPoint, controlPoint);
      const graphics = new Graphics(this.pixi);
      const startTime = new Date();

      this.addExplositionInterval(pointArray, startTime, graphics);
    }
  }

  /**
   * missile 효과의 업데이트를 추가한다
   * @param {array} pointArray bezierCurve 포인터 정보가 담긴 배열
   * @param {Date} startTime 폭팔 효과 시작 시간
   * @param {Graphics} graphics 효과를 그려주는 오브젝트
   * @memberof ScatterEffect
   * @instance
   */
  addExplositionInterval(pointArray, startTime, graphics) {
    const colorIndex = Math.floor(Math.random() * (colors.length - 1));
    const colorRGB = colors[colorIndex];
    const color = Utility.rgb2hex(colorRGB);
    const intervalIndex = setInterval(() => {
      this.explosionUpdate(intervalIndex, pointArray, startTime, graphics, color);
    }, 16);
  }

  /**
   * missile 효과의 업데이트
   * @param {number} intervalIndex 업데이트 interval 인덱스 (제거를 위해)
   * @param {array} pointArray bezierCurve 포인터 정보가 담긴 배열
   * @param {Date} startTime 폭팔 효과 시작 시간
   * @param {Graphics} graphics 효과를 그려주는 오브젝트
   * @param {number} color 효과의 색상 값
   * @memberof ScatterEffect
   * @instance
   */
  explosionUpdate(intervalIndex, pointArray, startTime, graphics, color) {
    const elapsedTime = new Date() - startTime;
    const drawProgress = elapsedTime / (this.explosionDuration * 0.5 * 1000) > 1 ? 1 : elapsedTime / (this.explosionDuration * 0.5 * 1000);
    const progress = elapsedTime / (this.explosionDuration * 1000) > 1 ? 1 : elapsedTime / (this.explosionDuration * 1000);

    graphics.object.clear();
    for (let i = 0; i < drawProgress * 100; i++) {
      const point = pointArray[i];
      if (this.prevPoint === undefined || i === 0) {
        this.prevPoint = point;
      }

      const alpha = 1 - (progress * 2) + (i * 0.01);
      if (this.prevPoint) {
        graphics.object.lineStyle(1, color, alpha);
        graphics.object.moveTo(this.prevPoint.x, this.prevPoint.y);
        graphics.object.lineTo(point.x, point.y);
        this.prevPoint = point;
      }
    }

    if (progress >= 1) {
      graphics.destroy();
      clearInterval(intervalIndex);
    }
  }

  /**
   * 기본 좌표를 차트상에서 좌표로 변환
   * @param {object} point 기존 좌표
   * @memberof ScatterEffect
   * @instance
   * @returns {object} 좌표
   */
  toChart(point) {
    const max = {
      x: this.chart.config.options.scales.xAxes[0].ticks.suggestedMax,
      y: this.chart.config.options.scales.yAxes[0].ticks.suggestedMax,
    };

    const pixelPerOne = {
      x: (this.chart.chartArea.right - this.chart.chartArea.left) / max.x,
      y: (this.chart.chartArea.bottom - this.chart.chartArea.top) / max.y,
    };

    return {
      x: point.x * pixelPerOne.x + this.chart.chartArea.left,
      y: this.chart.chartArea.bottom - point.y * pixelPerOne.y,
    };
  }

  /**
   * bezierCurve를 시간에 의해 생성해줌
   * @param {object} first bezierCurve 시작 좌표
   * @param {object} second bezierCurve 도착 좌표
   * @param {object} control bezierCurve 제어 좌표
   * @memberof ScatterEffect
   * @instance
   * @returns {object} points
   */
  makeBezierCurveWithTime(first, second, control) {
    const points = [];
    for (let t = 0; t < 1; t += 0.01) {
      const pointFirstToControlByTime = {
        x: (1 - t) * first.x + t * control.x,
        y: (1 - t) * first.y + t * control.y,
      };

      const pointControlToSecondByTime = {
        x: (1 - t) * control.x + t * second.x,
        y: (1 - t) * control.y + t * second.y,
      };

      const pointFinalByTime = {
        x: (1 - t) * pointFirstToControlByTime.x + t * pointControlToSecondByTime.x,
        y: (1 - t) * pointFirstToControlByTime.y + t * pointControlToSecondByTime.y,
      };
      points.push(pointFinalByTime);
    }
    return points;
  }

  /**
   * 현재 생성되어 있는 Scatter Effect 제거
   * <p> [Effect]{@link Effect} 클래스에 의해 호출된다.
   * @memberof ScatterEffectEffect
   * @instance
   */
  destroy() {
  }
}