import { fabric } from 'fabric'; const ARROW_ANGLE = 30; const CHEVRON_SIZE_RATIO = 2.7; const TRIANGLE_SIZE_RATIO = 1.7; const RADIAN_CONVERSION_VALUE = 180; const ArrowLine = fabric.util.createClass( fabric.Line, /** @lends Convolute.prototype */ { /** * Line type * @param {String} type * @default */ type: 'line', /** * Constructor * @param {Array} [points] Array of points * @param {Object} [options] Options object * @override */ initialize(points, options = {}) { this.callSuper('initialize', points, options); this.arrowType = options.arrowType; }, /** * Render ArrowLine * @private * @override */ _render(ctx) { const { x1: fromX, y1: fromY, x2: toX, y2: toY } = this.calcLinePoints(); const linePosition = { fromX, fromY, toX, toY, }; this.ctx = ctx; ctx.lineWidth = this.strokeWidth; this._renderBasicLinePath(linePosition); this._drawDecoratorPath(linePosition); this._renderStroke(ctx); }, /** * Render Basic line path * @param {Object} linePosition - line position * @param {number} option.fromX - line start position x * @param {number} option.fromY - line start position y * @param {number} option.toX - line end position x * @param {number} option.toY - line end position y * @private */ _renderBasicLinePath({ fromX, fromY, toX, toY }) { this.ctx.beginPath(); this.ctx.moveTo(fromX, fromY); this.ctx.lineTo(toX, toY); }, /** * Render Arrow Head * @param {Object} linePosition - line position * @param {number} option.fromX - line start position x * @param {number} option.fromY - line start position y * @param {number} option.toX - line end position x * @param {number} option.toY - line end position y * @private */ _drawDecoratorPath(linePosition) { this._drawDecoratorPathType('head', linePosition); this._drawDecoratorPathType('tail', linePosition); }, /** * Render Arrow Head * @param {string} type - 'head' or 'tail' * @param {Object} linePosition - line position * @param {number} option.fromX - line start position x * @param {number} option.fromY - line start position y * @param {number} option.toX - line end position x * @param {number} option.toY - line end position y * @private */ _drawDecoratorPathType(type, linePosition) { switch (this.arrowType[type]) { case 'triangle': this._drawTrianglePath(type, linePosition); break; case 'chevron': this._drawChevronPath(type, linePosition); break; default: break; } }, /** * Render Triangle Head * @param {string} type - 'head' or 'tail' * @param {Object} linePosition - line position * @param {number} option.fromX - line start position x * @param {number} option.fromY - line start position y * @param {number} option.toX - line end position x * @param {number} option.toY - line end position y * @private */ _drawTrianglePath(type, linePosition) { const decorateSize = this.ctx.lineWidth * TRIANGLE_SIZE_RATIO; this._drawChevronPath(type, linePosition, decorateSize); this.ctx.closePath(); }, /** * Render Chevron Head * @param {string} type - 'head' or 'tail' * @param {Object} linePosition - line position * @param {number} option.fromX - line start position x * @param {number} option.fromY - line start position y * @param {number} option.toX - line end position x * @param {number} option.toY - line end position y * @param {number} decorateSize - decorate size * @private */ _drawChevronPath(type, { fromX, fromY, toX, toY }, decorateSize) { const { ctx } = this; if (!decorateSize) { decorateSize = this.ctx.lineWidth * CHEVRON_SIZE_RATIO; } const [standardX, standardY] = type === 'head' ? [fromX, fromY] : [toX, toY]; const [compareX, compareY] = type === 'head' ? [toX, toY] : [fromX, fromY]; const angle = (Math.atan2(compareY - standardY, compareX - standardX) * RADIAN_CONVERSION_VALUE) / Math.PI; const rotatedPosition = (changeAngle) => this.getRotatePosition(decorateSize, changeAngle, { x: standardX, y: standardY, }); ctx.moveTo(...rotatedPosition(angle + ARROW_ANGLE)); ctx.lineTo(standardX, standardY); ctx.lineTo(...rotatedPosition(angle - ARROW_ANGLE)); }, /** * return position from change angle. * @param {number} distance - change distance * @param {number} angle - change angle * @param {Object} referencePosition - reference position * @returns {Array} * @private */ getRotatePosition(distance, angle, referencePosition) { const radian = (angle * Math.PI) / RADIAN_CONVERSION_VALUE; const { x, y } = referencePosition; return [distance * Math.cos(radian) + x, distance * Math.sin(radian) + y]; }, } ); export default ArrowLine;