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() {
}
}