import _ from 'lodash';
import { Howl } from 'howler';
import * as PIXI from 'pixi.js';
import Graphics from '../graphics';
// eslint-disable-next-line
import tweenManager from 'pixi-tween';
import Text from '../text';
/**
* Line Chart의 Effect를 생성하는 클래스입니다.
* <p>
* @class
* @alias LineEffect
*/
export default class LineEffect {
/**
* @description 사용자 옵션을 받아서 Line Chart의 Effect를 생성합니다.
* @param { object } _object - ChartJS, PixiJS, 호출한 Effect 클래스
* @param { object } _option - 사용자 지정 Effect 옵션
*/
constructor(_object, _option) {
/**
* @description 생성한 ChartJS 오브젝트
* @type { ChartJS }
*/
this.chart = _object.chart;
/**
* @description Effect를 생성할 PIXI 오브젝트
* @type { PIXI }
*/
this.pixi = _object.pixi;
/**
* @description Effect를 호출한 상위 Effect Class
* @type { Effect }
*/
this.parent = _object.effect;
/**
* @description Effect에 대한 사용자 옵션
* @type { object }
*/
this.option = _option;
/**
* @description 차트가 업데이트 중인지 아닌지 여부
* @type { boolean }
*/
this.chartAnimation = false;
/**
* @description 알파 값 플래그
* @type { number }
*/
this.flag = 0;
/**
* @description 알파 값
* @type { number }
*/
this.alpha = 0;
/**
* @description lineTracer 차트 애니메이션 플래그
* @type { boolean }
*/
this.lineTracerOneTime = false;
/**
* @description upperLimit 차트 애니메이션 플래그
* @type { boolean }
*/
this.upperLimitOneTime = false;
/**
* @description lowerLimit 차트 애니메이션 플래그
* @type { boolean }
*/
this.lowerLimitOneTime = false;
/**
* @description 경고 아이콘 및 limit 선을 그리기 위한 PIXI.Graphics
* @type { Graphics }
*/
this.graphics = new Graphics(this.pixi);
/**
* @description lowerLimit 효과일 때 최초적용 옵션을 위한 변수
* @type { boolean }
*/
this.lowerLimitCompleteOneTime = false;
/**
* @description upperLimit 효과일 때 최초적용 옵션을 위한 변수
* @type { boolean }
*/
this.upperLimitCompleteOneTime = false;
/**
* @description 등록한 사운드가 로드되었는지 여부
* @type { boolean }
*/
this.isSoundReady = false;
if (!_.isUndefined(this.option.sound)) {
/**
* @description 사운드 제어 변수
* @type { Howler }
*/
this.sound = new Howl({
src: [this.option.sound],
});
this.sound.once('load', () => {
this.isSoundReady = true;
});
}
this._createTexts();
this._initRoundedTriangle();
}
/**
* 사용자가 지정한 이미지가 라인 그래프를 따라 움직이는 Effect 입니다.
* @memberof LineEffect
* @example
* const option = {
* dataset: 0, // 차트 데이터 셋 번호 지정
* height: 48, // 이미지 높이
* width: 48, // 이미지 너비
* src: '../img/img1.png', // 이미지 경로
* angle: true, // 이미지 각도 변화 여부
* easing: 'linear', // 애니메이션 Easing 이름
* fadeIn: false, // 이미지 페이드 인 여부
* fadeOut: false, // 이미지 페이드 아웃 여부
* loop: true, // 애니메이션 반복 여부
* time: 2.5, // 애니메이션 수행 시간(초)
* };
*
* window.chart[0].effect.add('lineTracer', option);
* @instance
*/
lineTracer() {
// 실행 된 이펙트 이름을 지정
this.executedEffect = 'lineTracer';
this.chartMetaData = this.chart.getDatasetMeta(this.option.dataset);
// 차트 데이터 값 중 첫 번째
const initModel = this.chartMetaData.data[0]._model;
const lineTension = this.chartMetaData.dataset._model.tension;
// 이미지 이동에 대한 트윈 패스 객체 생성
const tweenPath = new PIXI.tween.TweenPath();
const optionTime = this.option.time * 1000;
// 차트 애니메이션 업데이트 시 해야 할 작업을 지정
if (!this.lineTracerOneTime) {
this.chartAnimationProgressIndex = this.parent._addOnProgressFunction(() => {
this.chartAnimation = true;
if (!this.trigger) {
this._createTempSprite();
this.trigger = true;
}
this.tween.stop();
if (tweenPath !== undefined) {
tweenPath.clear();
}
PIXI.tweenManager.removeTween(this.tween);
this.pixi.ticker.remove(this.lineTracerUpdate);
this.tweenSprite.destroy();
this.parent._removeOnProgressFunction(this.chartAnimationProgressIndex);
this.parent._removeOnCompleteFunction(this.chartAnimationCompleteIndex);
this.lineTracerOneTime = false;
this.lineTracer();
});
this.lineTracerOneTime = true;
}
this.chartAnimationCompleteIndex = this.parent._addOnCompleteFunction(() => {
this.trigger = false;
this.pixi.ticker.remove(this.fadeOutTempSprite);
if (this.tmpSprite !== undefined) {
this.tmpSprite.destroy();
}
this.chartAnimation = false;
this.alpha = 0;
this.flag = 0;
});
// 베지어 곡선 계산에 필요한 데이터에 대한 현재, 이전 차트 데이터 값
let model;
let prevModel;
// 차트 데이터 값들에 대한 좌표를 트윈 패스에 추가
for (let i = 0; i < this.chartMetaData.data.length; i++) {
// 현재 차트 데이터 값을 가져옴
model = this.chartMetaData.data[i]._model;
// 첫 번째 차트 데이터의 좌표값으로 시작 점 세팅
if (i === 0) {
tweenPath.moveTo(model.x, model.y);
} else {
// 두 번째 부터 다음 이동 경로를 트윈 패스에 추가
prevModel = this.chartMetaData.data[i - 1]._model;
// 차트 옵션의 lineTension 값이 0인 경우는 일반적인 직선 이동
if (lineTension === undefined || lineTension === 0) {
tweenPath.lineTo(model.x, model.y);
// 아닐 경우 베지어 커브 이동
} else {
tweenPath.bezierCurveTo(prevModel.controlPointNextX, prevModel.controlPointNextY, model.controlPointPreviousX, model.controlPointPreviousY, model.x, model.y);
}
}
}
// 라인 차트의 특성에 따라 트윈 패스는 항상 열린 경로이다.
tweenPath.closed = false;
// 이미지 경로로 스프라이트 생성 및 기본 값 세팅
this.tweenSprite = PIXI.Sprite.fromImage(this.option.src);
this.tweenSprite.x = initModel.x;
this.tweenSprite.y = initModel.y;
this.tweenSprite.alpha = 0.0;
this.tweenSprite.anchor.set(0.5, 0.5);
if (this.option.width !== undefined) {
this.tweenSprite.width = this.option.width;
}
if (this.option.height !== undefined) {
this.tweenSprite.height = this.option.height;
}
// 생성한 스프라이트를 Pixi에 추가
this.pixi.stage.addChild(this.tweenSprite);
/** Tween animation setting */
// 각도 계산을 위해 스프라이트 이전 좌표를 저장하는 변수
const prevPoint = {
x: this.tweenSprite.x,
y: this.tweenSprite.y,
};
// 스프라이트가 실제로 나타나는 시점(밀리 초)
const appearDelay = 25;
// 스프라이트로 트윈 매니저 생성
this.tween = PIXI.tweenManager.createTween(this.tweenSprite);
// 경로를 생성한 트윈 패스로 세팅
this.tween.path = tweenPath;
// 애니메이션 시간 세팅
let animationTime;
let updateTime = this.parent.option.data.interval * 1000 - this.chart.options.animation.duration + 105;
if (Number.isNaN(this.chart.updateElapsedTime) || this.chart.updateElapsedTime === undefined || this.chart.updateElapsedTime === null) {
updateTime = this.parent.option.data.interval * 1000;
}
if (updateTime !== undefined && optionTime > updateTime) {
animationTime = updateTime;
} else {
animationTime = optionTime;
}
this.tween.time = animationTime;
// 전체 시간의 8분의 1 만큼 페이드 인 아웃시 할당해 줌
const dividedTime = animationTime / 8;
// 애니메이션 반복 여부
this.tween.loop = this.option.loop;
// Easing 옵션 세팅, 기본값은 linear
if (this.option.easing !== undefined && this.option.easing !== '') {
const name = this.option.easing;
this.tween.easing = PIXI.tween.Easing[name]();
}
// 트윈 매니저 시작
this.tween.start();
// lineTracer 업데이트 함수
this.lineTracerUpdate = () => {
PIXI.tweenManager.update();
// 애니메이션 경과 시간
const elapsedTime = this.tween._elapsedTime;
// 각도 번화 옵션이 true이면 이전 좌표와 현재 좌표로부터 스프라이트 각도를 계산
if (this.option.angle && this.option.angle !== undefined) {
this.tweenSprite.rotation = this._getLineAngle(prevPoint, this.tweenSprite);
prevPoint.x = this.tweenSprite.x;
prevPoint.y = this.tweenSprite.y;
}
// 차트 업데이트 중에는 효과 무시
if (this.chartAnimation) {
return;
}
// 스프라이트가 최소 딜레이 시간 이후에 나타남
if (elapsedTime > appearDelay && elapsedTime < (optionTime - appearDelay)) {
// 처음 애니메이션 시간의 5분의 1동안
if (elapsedTime < dividedTime) {
// 페이드 인 옵션이 true이면 알파 값 증가
if (this.option.fadeIn !== undefined && this.option.fadeIn) {
this.tweenSprite.alpha += (1 / dividedTime) * 25;
if (this.tweenSprite.alpha > 1.0) {
this.tweenSprite.alpha = 1.0;
}
// 페이드 인 옵션이 false이면 알파값 기본 1.0으로 세팅
} else {
this.tweenSprite.alpha = 1.0;
}
}
// 마지막 애니메이션 시간의 8분의 1동안
if (elapsedTime > dividedTime * 7) {
// 페이드 아웃 옵션이 true이면 알파 값 감소
if (this.option.fadeOut !== undefined && this.option.fadeOut) {
this.tweenSprite.alpha -= (1 / dividedTime) * 18;
if (this.tweenSprite.alpha < 0.0) {
this.tweenSprite.alpha = 0.0;
}
} else {
this.tweenSprite.alpha = 1.0;
}
}
// 애니메이션 실행 시간이 다 되면 반복여부 옵션을 체크해서 알파값을 유지시킬지 말지 결정
} else if (elapsedTime >= (optionTime - appearDelay)) {
// 반복 여부가 false이고 페이드 아웃이 false이면 알파값 1.0으로 유지 - 이미지를 끝에 멈춘 상태로 유지함
if (!this.option.loop && (!this.option.fadeOut || this.option.fadeOut === undefined)) {
this.tweenSprite.alpha = 1.0;
} else {
this.tweenSprite.alpha = 0.0;
}
}
};
// lineTracer 업데이트 함수를 Pixi의 ticker에 추가
this.pixi.ticker.add(this.lineTracerUpdate);
}
/**
* 기준치 초과시 경고 아이콘 깜박임과 함께 기준선이 표시되는 Effect 입니다.
* @memberof LineEffect
* @example
* const option = {
* dataset: [0, 2], // 지정할 데이터 셋 번호 배열(지정하지 않을 경우 전체 데이터 셋에 적용됨)
* value: 80 // 초과 값
* };
*
* window.chart[0].effect.add('upperLimit', option);
* @instance
*/
upperLimit() {
this.chart.options.tooltips.enabled = false;
this.chart.options.hover.mode = null;
this.limitInformation = [];
this.limitLines = [];
if (!this.upperLimitOneTime) {
this.chartAnimationProgressIndex = this.parent._addOnProgressFunction(() => {
this.chartAnimation = true;
this.parent._removeOnProgressFunction(this.chartAnimationProgressIndex);
this.parent._removeOnCompleteFunction(this.chartAnimationCompleteIndex);
this.upperLimitOneTime = false;
this.upperLimit();
});
this.upperLimitOneTime = true;
}
this.chartAnimationCompleteIndex = this.parent._addOnCompleteFunction(() => {
this.chartAnimation = false;
this.alpha = 0;
this.flag = 0;
if (!this.upperLimitCompleteOneTime) {
this.upperLimitCompleteOneTime = true;
this.upperLimit();
}
});
if (!this.upperLimitCompleteOneTime) { return; }
this.executedEffect = 'upperLimit';
const limit = this.option.value;
const { chartArea } = this.chart;
const eachWidth = (chartArea.right - chartArea.left) / (this.chart.data.labels.length - 1);
const chartData = this.chart.data;
let isDatasetDefined = false;
const optDataset = this.option.dataset;
this.chartMetaData = null;
if (optDataset === undefined || optDataset === '' || optDataset === -1) {
isDatasetDefined = false;
} else {
isDatasetDefined = true;
}
for (let i = 0; i < chartData.datasets.length; i++) {
const tmpDatasets = this.chart.data.datasets;
const dataset = (tmpDatasets[i]);
for (let j = 0; j < dataset.data.length; j++) {
const value = dataset.data[j];
this.chartMetaData = this.chart.getDatasetMeta(i);
const dataView = this.chartMetaData.data[j]._view;
const { max, margins, bottom } = this.chartMetaData.data[j]._yScale;
margins.left = this.chartMetaData.data[j]._xScale.margins.left;
margins.right = this.chartMetaData.data[j]._xScale.margins.right;
const eachHeight = (bottom - margins.top) / max;
const line = (max - limit) * eachHeight + margins.top;
const areaSize = (chartArea.right - chartArea.left) * (chartArea.bottom - chartArea.top);
let lineDivider = 1;
let left = 0.0;
let width = 0.0;
if (j === 0) {
left = dataView.x;
width = eachWidth / 2;
lineDivider = 2;
} else if (j === (dataset.data.length - 1)) {
left = dataView.x - eachWidth / 2;
width = eachWidth / 2;
lineDivider = 2;
} else {
left = dataView.x - eachWidth / 2;
width = eachWidth;
lineDivider = 1;
}
const lineDataInfo = {
x: dataView.x,
y: dataView.y,
};
const dottedLineSpace = width / (10 / lineDivider) / 5;
const dottedLineLength = width / (10 / lineDivider) - dottedLineSpace;
const lastDataX = this.chartMetaData.data[dataset.data.length - 1]._view.x;
const startX = [];
for (let k = 0; k < (10 / lineDivider); k++) {
startX.push(left + (dottedLineLength * k) + (dottedLineSpace * k));
}
const minChartValue = undefined;
const maxChartValue = Math.max.apply(null, dataset.data);
if (isDatasetDefined && _.includes(optDataset, i)) {
if (value > limit) {
this.limitInformation.push({
lineDataInfo, areaSize: areaSize / 100, value, index: j, lastDataX, minChartValue, maxChartValue,
});
}
} else if (!isDatasetDefined && value > limit) {
this.limitInformation.push({
lineDataInfo, areaSize: areaSize / 100, value, index: j, lastDataX, minChartValue, maxChartValue,
});
}
this.limitLines.push({
dottedLineLength, line, startX,
});
}
}
this._limitInterval = this._limitInterval || setInterval(() => { this._limitUpdate(); }, 16);
}
/**
* 기준치 미만시 경고 아이콘 깜박임과 함께 기준선이 표시되는 Effect 입니다.
* @memberof LineEffect
* @example
* const option = {
* dataset: [1, 3], // 지정할 데이터 셋 번호 배열(지정하지 않을 경우 전체 데이터 셋에 적용됨)
* value: 20 // 미만 값
* };
*
* window.chart[0].effect.add('lowerLimit', option);
* @instance
*/
lowerLimit() {
this.chart.options.tooltips.enabled = false;
this.chart.options.hover.mode = null;
this.limitInformation = [];
this.limitLines = [];
if (!this.lowerLimitOneTime) {
this.chartAnimationProgressIndex = this.parent._addOnProgressFunction(() => {
this.chartAnimation = true;
this.parent._removeOnProgressFunction(this.chartAnimationProgressIndex);
this.parent._removeOnCompleteFunction(this.chartAnimationCompleteIndex);
this.lowerLimitOneTime = false;
this.lowerLimit();
});
this.lowerLimitOneTime = true;
}
this.chartAnimationCompleteIndex = this.parent._addOnCompleteFunction(() => {
this.chartAnimation = false;
this.alpha = 0;
this.flag = 0;
if (!this.lowerLimitCompleteOneTime) {
this.lowerLimitCompleteOneTime = true;
this.lowerLimit();
}
});
if (!this.lowerLimitCompleteOneTime) { return; }
this.executedEffect = 'lowerLimit';
const limit = this.option.value;
const { chartArea } = this.chart;
const eachWidth = (chartArea.right - chartArea.left) / (this.chart.data.labels.length - 1);
const chartData = this.chart.data;
let isDatasetDefined = false;
const optDataset = this.option.dataset;
this.chartMetaData = null;
if (optDataset === undefined || optDataset === '' || optDataset === -1) {
isDatasetDefined = false;
} else {
isDatasetDefined = true;
}
for (let i = 0; i < chartData.datasets.length; i++) {
const tmpDatasets = this.chart.data.datasets;
const dataset = (tmpDatasets[i]);
for (let j = 0; j < dataset.data.length; j++) {
const value = dataset.data[j];
this.chartMetaData = this.chart.getDatasetMeta(i);
const dataView = this.chartMetaData.data[j]._view;
const { max, margins, bottom } = this.chartMetaData.data[j]._yScale;
margins.left = this.chartMetaData.data[j]._xScale.margins.left;
margins.right = this.chartMetaData.data[j]._xScale.margins.right;
const eachHeight = (bottom - margins.top) / max;
const line = (max - limit) * eachHeight + margins.top;
const areaSize = (chartArea.right - chartArea.left) * (chartArea.bottom - chartArea.top);
let lineDivider = 1;
let left = 0.0;
let width = 0.0;
if (j === 0) {
left = dataView.x;
width = eachWidth / 2;
lineDivider = 2;
} else if (j === (dataset.data.length - 1)) {
left = dataView.x - eachWidth / 2;
width = eachWidth / 2;
lineDivider = 2;
} else {
left = dataView.x - eachWidth / 2;
width = eachWidth;
lineDivider = 1;
}
const lineDataInfo = {
x: dataView.x,
y: dataView.y,
};
const dottedLineSpace = width / (10 / lineDivider) / 5;
const dottedLineLength = width / (10 / lineDivider) - dottedLineSpace;
const lastDataX = this.chartMetaData.data[dataset.data.length - 1]._view.x;
const startX = [];
for (let k = 0; k < (10 / lineDivider); k++) {
startX.push(left + (dottedLineLength * k) + (dottedLineSpace * k));
}
const minChartValue = Math.min.apply(null, dataset.data);
const maxChartValue = undefined;
if (isDatasetDefined && _.includes(optDataset, i)) {
if (value < limit) {
this.limitInformation.push({
lineDataInfo, areaSize: areaSize / 100, value, index: j, lastDataX, minChartValue, maxChartValue,
});
}
} else if (!isDatasetDefined && value < limit) {
this.limitInformation.push({
lineDataInfo, areaSize: areaSize / 100, value, index: j, lastDataX, minChartValue, maxChartValue,
});
}
this.limitLines.push({
dottedLineLength, line, startX,
});
}
}
this._limitInterval = this._limitInterval || setInterval(() => { this._limitUpdate(); }, 16);
}
/**
* <b>내부 함수</b>
* <p>lineTracer 에서 스프라이트 각도를 계산하는 함수입니다.
* @param { object } _start 첫 번째 지점의 x, y좌표 셋
* @param { object } _end 첫 번째 지점의 x, y좌표 셋
* @memberof LineEffect
* @returns { number } Sprite.rotation 라디안 값
* @instance
*/
_getLineAngle(_start, _end) {
const dx = _end.x - _start.x;
const dy = _end.y - _start.y;
let angle = Math.atan(dy / dx);
if (dx < 0.0) {
angle += 180.0;
}
return angle;
}
/**
* <b>내부 함수</b>
* <p>차트 데이터 업데이트 및 변화 시 lineTracer 페이드 아웃 처리를 위해 마지막 위치에 임시 이미지 생성하는 함수입니다.
* @memberof LineEffect
* @instance
*/
_createTempSprite() {
this.tmpSprite = PIXI.Sprite.fromImage(this.option.src);
this.tmpSprite.x = this.tweenSprite.x;
this.tmpSprite.y = this.tweenSprite.y;
this.tmpSprite.width = this.tweenSprite.width;
this.tmpSprite.height = this.tweenSprite.height;
this.tmpSprite.alpha = this.tweenSprite.alpha;
this.tmpSprite.anchor.set(0.5, 0.5);
this.tmpSprite.rotation = this.tweenSprite.rotation;
this.pixi.stage.addChild(this.tmpSprite);
this.fadeOutTempSprite = () => {
this.tmpSprite.alpha -= 0.067;
};
this.pixi.ticker.add(this.fadeOutTempSprite);
}
/**
* <b>내부 함수</b>
* <p>기준치 제한하는 Effect에 대한 업데이트 함수입니다.
* @memberof LineEffect
* @instance
*/
_limitUpdate() {
this.graphics.object.clear();
for (let i = 0; i < this.valueText.length; i++) {
this.valueText[i].setAlpha(0);
this.labelText[i].setAlpha(0);
}
if (this.chartAnimation) {
return;
}
if (this.flag && this.alpha > 0) {
this.alpha -= 0.05;
if (this.alpha <= 0) {
this.flag = 0;
}
} else if (this.alpha < 1) {
this.alpha += 0.05;
if (this.alpha > 1) {
if (this.isSoundReady) {
this.sound.play();
}
this.flag = 1;
}
}
const color = this.executedEffect === 'upperLimit' ? 0xD62A00 : 0x316B00;
for (let i = 0; i < this.limitLines.length; i++) {
const information = this.limitLines[i];
const { dottedLineLength, line } = information;
for (let k = 0; k < 10; k++) {
const startX = information.startX[k];
this.graphics.object.lineStyle(2, color, 1);
this.graphics.object.moveTo(startX, line);
this.graphics.object.lineTo(startX + dottedLineLength, line);
}
}
for (let i = 0; i < this.limitInformation.length; i++) {
const minSize = 16;
const sizeGauge = 400;
const information = this.limitInformation[i];
const {
lineDataInfo, areaSize, value, index, lastDataX, minChartValue, maxChartValue,
} = information;
const chartRightMargin = this.chart.legend.paddingRight;
this.valueText[i].setAlpha(1);
this.labelText[i].setAlpha(1);
this.valueText[i].object.text = value.toLocaleString();
this.labelText[i].object.text = this.chart.config.data.labels[index];
const labelTextHeight = this.labelText[i].object.height;
const labelTextWidth = this.labelText[i].object.width;
const valueTextWidth = this.valueText[i].object.width;
const size = ((areaSize / sizeGauge) < minSize) ? minSize : (areaSize / sizeGauge);
const textSize = size / 50;
let iconObjectMargin = (size * 2.56) / 2 + labelTextWidth * 0.5;
let textAlign = -(labelTextWidth / 2 - valueTextWidth * 0.5);
const labelsMaxWidth = Math.max(labelTextWidth, valueTextWidth);
if (lineDataInfo.x > lastDataX - size && chartRightMargin < labelsMaxWidth + this.textMargin * 2) {
iconObjectMargin *= -1;
textAlign *= -1;
}
const labelTextY = lineDataInfo.y - labelTextHeight * 0.667;
const valueTextY = lineDataInfo.y + labelTextHeight * 0.667;
const labelTextX = lineDataInfo.x + iconObjectMargin;
const valueTextX = lineDataInfo.x + iconObjectMargin + textAlign;
this.valueText[i].object.style.fill = color;
this.valueText[i].setPosition(valueTextX, valueTextY);
this.labelText[i].setPosition(labelTextX, labelTextY);
this.valueText[i].setScale(textSize);
this.labelText[i].setScale(textSize);
this.graphics.object.lineStyle(0);
if ((minChartValue !== undefined && value === minChartValue) || (maxChartValue !== undefined && value === maxChartValue)) {
this._drawWarningBackground(lineDataInfo.x, lineDataInfo.y, size, this.alpha);
this._drawWarning(lineDataInfo.x, lineDataInfo.y, size * 1.25, 0xFFFFFF, this.alpha);
} else {
this._drawWarningBackground(lineDataInfo.x, lineDataInfo.y, size, 1);
this._drawWarning(lineDataInfo.x, lineDataInfo.y, size * 1.25, 0xFFFFFF, 1);
}
}
}
/**
* <b>내부 함수</b>
* <p>경고 아이콘 그래픽을 생성하는 함수입니다.
* @memberof LineEffect
* @instance
*/
_drawWarning(_x, _y, size, color, alpha) {
// offset y
_y -= (size / 10);
const triangleHypotenuse = this.makeTriangleHypotenuse(size);
const triangleVertex = this.makeTriangleVertex(size);
const height = triangleVertex[1].y;
const width = triangleVertex[2].x;
const moveY = _y - height / 2;
this.graphics.object.lineStyle(size / 14.4, color, alpha);
for (let i = 0; i < triangleHypotenuse.length; i++) {
const hypotenuse = triangleHypotenuse[i];
this.graphics.object.moveTo(hypotenuse[0].x + _x, hypotenuse[0].y + moveY);
this.graphics.object.lineTo(hypotenuse[1].x + _x, hypotenuse[1].y + moveY);
const nextIndex = (i + 1 === triangleHypotenuse.length ? 0 : i + 1);
const nextHypotenuse = triangleHypotenuse[nextIndex];
const quadraticCurvePoint = triangleVertex[i + 1 > 2 ? 0 : i + 1];
this.graphics.object.moveTo(hypotenuse[1].x + _x, hypotenuse[1].y + moveY);
this.graphics.object.quadraticCurveTo(quadraticCurvePoint.x + _x, quadraticCurvePoint.y + moveY, nextHypotenuse[0].x + _x, nextHypotenuse[0].y + moveY);
}
this.graphics.object.lineStyle(0);
this.graphics.object.beginFill(0xFFFFFF, alpha);
this.graphics.object.drawRoundedRect(_x - width * 1 / 8 / 2, _y - height * 1 / 3 / 3, width * 1 / 8, height * 1 / 3, size / 30);
this.graphics.object.drawCircle(_x, _y + height / 3, size / 30);
this.graphics.object.endFill();
}
/**
* <b>내부 함수</b>
* <p>경고 아이콘 백그라운드 둥근 사각형 그래픽을 생성하는 함수입니다.
* @memberof LineEffect
* @instance
*/
_drawWarningBackground(_x, _y, size, alpha) {
// _y -= (size / 16);
const radius = size / 1.5;
const outerSize = size * 1.92;
const innerSize = size * 1.72;
const outerColor = this.executedEffect === 'upperLimit' ? 0xD62A00 : 0x316B00;
const innerColor = this.executedEffect === 'upperLimit' ? 0xFF3200 : 0x3B7F00;
// draw outer rect first
this.graphics.object.lineStyle(0);
this.graphics.object.beginFill(outerColor, alpha);
this.graphics.object.drawRoundedRect(_x - outerSize / 2, _y - outerSize / 2, outerSize, outerSize, radius);
this.graphics.object.endFill();
// next, draw inner rect
this.graphics.object.lineStyle(0);
this.graphics.object.beginFill(innerColor, alpha);
this.graphics.object.drawRoundedRect(_x - innerSize / 2, _y - innerSize / 2, innerSize, innerSize, radius * 0.84);
this.graphics.object.endFill();
}
/**
* <b>내부 함수</b>
* <p>모서리가 둥근 삼각형을 생성하는 핵심 함수입니다.
* @memberof LineEffect
* @instance
*/
_initRoundedTriangle() {
const findNewPoint = (point, angle, distance) => {
const result = {};
angle -= 90;
result.x = Math.round(Math.cos(angle * Math.PI / 180) * distance + point.x);
result.y = Math.round(Math.sin(angle * Math.PI / 180) * distance + point.y);
return result;
};
this.makeTriangleHypotenuse = (triangleLength) => {
const vertex = this.makeTriangleVertex(triangleLength);
const separatePoint1 = 1 / 5;
const separatePoint2 = 4 / 5;
return [
[{ x: vertex[1].x * separatePoint1, y: vertex[1].y * separatePoint1 }, { x: vertex[1].x * separatePoint2, y: vertex[1].y * separatePoint2 }],
[{ x: vertex[1].x + Math.abs(vertex[2].x - vertex[1].x) * separatePoint1, y: vertex[1].y }, { x: vertex[1].x + Math.abs(vertex[2].x - vertex[1].x) * separatePoint2, y: vertex[1].y }],
[{ x: vertex[2].x * separatePoint2, y: vertex[2].y * separatePoint2 }, { x: vertex[2].x * separatePoint1, y: vertex[2].y * separatePoint1 }],
];
};
this.makeTriangleVertex = (triangleLength) => {
const point1 = { x: 0, y: 0 };
const point2 = findNewPoint(point1, 210, triangleLength);
const point3 = findNewPoint(point2, 90, triangleLength);
return [point1, point2, point3];
};
}
/**
* <b>내부 함수</b>
* <p>upperLimit, lowerLimit에서 데이터 이름과 값을 표시하기 위한 PIXI.Text 객체를 생성하는 함수임니다.
* @memberof LineEffect
* @instance
*/
_createTexts() {
this.textMargin = 8;
const textScale = this.parent.ratio * 1.0;
this.valueText = [];
this.labelText = [];
for (let j = 0; j < this.chart.data.datasets.length; j++) {
for (let i = 0; i < this.chart.config.data.labels.length; i++) {
this.valueText.push(new Text(this.pixi, '', {
alpha: 0,
}));
this.valueText[i].setScale(textScale);
this.labelText.push(new Text(this.pixi, this.chart.config.data.labels[i], {
alpha: 0,
}));
this.labelText[i].setScale(textScale);
}
}
for (let i = 0; i < this.valueText.length; i++) {
this.valueText[i].setAlpha(0);
this.labelText[i].setAlpha(0);
}
}
/**
* 생성한 Effect를 제거하는 함수입니다.
* @memberof LineEffect
* @instance
*/
destroy() {
// 수행중인 Effect이름으로 제거 대상을 선택
switch (this.executedEffect) {
case 'lineTracer':
this.tween.stop();
this.pixi.ticker.remove(this.lineTracerUpdate);
this.tweenSprite.destroy();
break;
case 'upperLimit':
clearInterval(this._limitInterval);
delete this._limitInterval;
for (let i = 0; i < this.valueText.length; i++) {
this.valueText[i].destroy();
this.labelText[i].destroy();
}
this.graphics.destroy();
break;
case 'lowerLimit':
clearInterval(this._limitInterval);
delete this._limitInterval;
for (let i = 0; i < this.valueText.length; i++) {
this.valueText[i].destroy();
this.labelText[i].destroy();
}
this.graphics.destroy();
break;
default:
break;
}
// common destroy
this.parent._removeOnProgressFunction(this.chartAnimationProgressIndex);
this.parent._removeOnCompleteFunction(this.chartAnimationCompleteIndex);
}
}