tooltip.js

import * as PIXI from 'pixi.js';
import Text from './text';
import { EasingFunctions } from './variable';

/**
 * Chart.js가 아닌 자체적으로 생성한 chart에서 사용하기 위한 Tooltip 클래스
 * @alias Tooltip
 */
export default class Tooltip {
  /*
      mode
      0 = hide
      1 = show

      direction
      -1  = left
      1   = right
  */

  /**
   * Tooltip을 띄우기 위해 PIXI의 Graphics, Text를 생성하고 필요한 값을 셋팅
   * @param {PIXI} pixi PIXI 오브젝트
   */
  constructor(pixi) {
    /**
     * @description PIXI 오브젝트
     * @type { PIXI }
     */
    this.pixi = pixi;

    /**
     * @description Tooltip의 배경을 위한 PIXI.Graphics 오브젝트
     * @type { PIXI.Graphics }
     */
    this.graphics = new PIXI.Graphics();
    this.graphics.zOrder = -999;
    pixi.stage.enableSort = true;
    pixi.stage.addChild(this.graphics);

    /**
     * @description Tooltip의 title 텍스트를 위한 PIXI.Text 오브젝트
     * @type { PIXI.Text }
     */
    this.title = new Text(pixi, 'title', {
      fill: '#ffffff',
      fontWeight: 'bold',
    });
    this.title.object.zOrder = this.graphics.zOrder - 1;

    /**
     * @description Tooltip의 value 텍스트를 위한 PIXI.Text의 오브젝트
     * @type { PIXI.Text }
     */
    this.value = new Text(pixi, 'value', {
      fill: '#ffffff',
    });
    this.value.object.zOrder = this.graphics.zOrder - 1;

    this.graphics.alpha = 0;
    this.title.object.alpha = 0;
    this.value.object.alpha = 0;

    /**
     * @description Tooltip의 투명도 값 0.0 ~ 1.0
     * @type { number }
     */
    this.alpha = 0;

    /**
     * @description Tooltip이 투명도 0에서 1까지 걸리는 시간 (단위 : 초)
     * @type { number }
     */
    this.duration = 0.2;

    /**
     * @description duration에 따른 초당 설정해야하는 투명도 값
     * @type { number }
     */
    this.velocity = 1 / 60 / this.duration;

    /**
     * @description Tooltip 표현 여부 0 = 숨김, 1 = 표현
     * @type { number }
     */
    this.mode = 0;

    /**
     * @description Tooltip의 안쪽 여백
     * @type { object }
     * @property { number } width 좌우 여백
     * @property { number } height 상하 여백
     */
    this.padding = {
      width: 10,
      height: 5,
    };

    /**
     * @description Tooltip 말풍선 모양에서 삼각형의 크기
     * @type { number }
     */
    this.triangleWidth = 5;

    /**
     * @description Tooltip 생성 방향 1 = 오른쪽, -1 = 왼쪽
     * @type { number }
     */
    this.direction = 1;

    /**
     * @description Tooltip x 좌표
     * @type { number }
     */
    this.x = 110;

    /**
     * @description Tooltip y 좌표
     * @type { number }
     */
    this.y = 110;

    /**
     * @description Tooltip value에 표시될 단위
     * @type { string }
     */
    this.unit = '';
    const textScale = 0.5 * pixi.renderer.width * (1 / 339);
    this.title.object.scale.set(textScale > 1 ? 1 : textScale);
    this.value.object.scale.set(textScale > 1 ? 1 : textScale);

    this.setPosition(this.x, this.y);
  }

  /**
   * Tooltip의 상태를 변경하여 사용자에게 보여주는 함수
   * @memberof Tooltip
   * @instance
   */
  show() {
    if (this.updateInterval === undefined) {
      this.updateInterval = setInterval(() => this.update(), 16);
    }
    this.mode = 1;
  }

  /**
   * Tooltip의 상태를 변경하여 사용자로부터 숨기는 함수
   * @memberof Tooltip
   * @instance
   */
  hide() {
    if (this.updateInterval === undefined) {
      this.updateInterval = setInterval(() => this.update(), 16);
    }
    this.mode = 0;
  }


  /**
   * Tooltip의 위치를 설정
   * @param {number} x Tooltip의 x좌표
   * @param {number} y Tooltip의 y좌표
   * @memberof Tooltip
   * @instance
   */
  setPosition(x, y) {
    let destinationX = x;
    let destinationY = y;

    this.determineBackgroundSize();

    if (x < this.pixi.renderer.width / 2) {
      this.direction = 1;
    } else {
      this.direction = -1;
    }
    destinationX += ((this.backgroundWidth / 2 + this.triangleWidth) * this.direction);

    // 툴팁이 화면을 넘어갈 때
    if (y + this.backgroundHeight / 2 > this.pixi.renderer.height) {
      destinationY = this.pixi.renderer.height - this.backgroundHeight / 2;
    } else if (y - this.backgroundHeight / 2 < 0) {
      destinationY = this.backgroundHeight / 2;
    }

    this.destinationX = destinationX;
    this.destinationY = destinationY;
    if (this.positionUpdateInterval === undefined) {
      this.moveStartTime = new Date();
      this.positionUpdateInterval = setInterval(() => { this.positionUpdate(); }, 16);
    }
  }

  /**
   * Tooltip의 위치를 이동
   * <p><b>내부 함수</b>
   * @memberof Tooltip
   * @instance
   */
  positionUpdate() {
    const elapsedTime = new Date() - this.moveStartTime;
    const easingTime = elapsedTime / (this.duration * 1000);
    const easing = EasingFunctions.easeOutQuart;
    const distanceX = this.destinationX - this.x;
    const distnaceY = this.destinationY - this.y;
    const progress = easingTime < 1 ? easing(easingTime) : 1;
    this.currentX = this.x + (distanceX * progress);
    this.currentY = this.y + (distnaceY * progress);

    this.title.setPosition(this.currentX, this.currentY - this.title.object.height / 2);
    this.value.setPosition(this.currentX, this.currentY + this.value.object.height / 2);
    this.draw();

    if (progress >= 1) {
      clearInterval(this.positionUpdateInterval);
      delete this.positionUpdateInterval;
      this.x = this.destinationX;
      this.y = this.destinationY;
    }
  }

  /**
   * Tooltip의 데이터를 설정
   * @param {string} title Tooltip의 title 설정
   * @param {string} value Tooltip의 value 설정
   * @memberof Tooltip
   * @instance
   */
  setData(title, value) {
    this.title.object.text = title;
    this.value.object.text = value.toLocaleString() + this.unit;
    this.determineBackgroundSize();
    this.draw();
  }

  /**
   * Tooltip의 단위를 설정
   * @param {string} unit Tooltip의 단위
   * @memberof Tooltip
   * @instance
   */
  setUnit(unit) {
    this.unit = unit || '';
  }

  /**
   * Tooltip의 크기를 설정
   * @memberof Tooltip
   * @instance
   */
  determineBackgroundSize() {
    const textWidth = this.title.object.width > this.value.object.width ? this.title.object.width : this.value.object.width;
    this.backgroundWidth = textWidth + this.padding.width * 2;

    const textHeight = (this.value.object.y + this.value.object.height / 2) - (this.title.object.y - this.title.object.height / 2);
    this.backgroundHeight = textHeight + this.padding.height * 2;
  }

  /**
   * Tooltip의 삼각형을 그림
   * @param {number} size 화면크기에 Tooltip의 크기가 달라질 수 있게 size를 받아 조절
   * @memberof Tooltip
   * @instance
   */
  triangle(size) {
    this.graphics.moveTo(this.currentX + (this.backgroundWidth / 2 * -this.direction), this.currentY + (-size / 2));
    this.graphics.lineTo(this.currentX + (this.backgroundWidth / 2 * -this.direction) + (size / 2 * -this.direction), this.currentY);
    this.graphics.lineTo(this.currentX + (this.backgroundWidth / 2 * -this.direction), this.currentY + size / 2);
    this.graphics.lineTo(this.currentX + (this.backgroundWidth / 2 * -this.direction), this.currentY);
  }

  /**
   * Tooltip의 배경을 그림
   * @memberof Tooltip
   * @instance
   */
  draw() {
    this.graphics.clear();
    this.graphics.beginFill(0x000000, 0.7);
    this.graphics.drawRoundedRect(this.currentX - this.backgroundWidth / 2, this.currentY - this.backgroundHeight / 2, this.backgroundWidth, this.backgroundHeight, 7);
    this.triangle(10);
    this.graphics.endFill();
  }

  /**
   * Tooltip의 투명도를 관리하고 적용
   * @memberof Tooltip
   * @instance
   */
  update() {
    switch (this.mode) {
      case 0:
        if (this.alpha > 0) {
          this.alpha -= this.velocity;
        } else {
          this.alpha = 0;
          clearInterval(this.updateInterval);
          delete this.updateInterval;
        }
        break;
      case 1:
        if (this.alpha < 1) {
          this.alpha += this.velocity;
        } else {
          this.alpha = 1;
          clearInterval(this.updateInterval);
          delete this.updateInterval;
        }
        break;
      default:
        break;
    }
    this.graphics.alpha = this.alpha;
    this.title.object.alpha = this.alpha;
    this.value.object.alpha = this.alpha;
  }
}