Spaces:
Running
Running
| import { fabric } from 'fabric'; | |
| import extend from 'tui-code-snippet/object/extend'; | |
| import Component from '@/interface/component'; | |
| import resizeHelper from '@/helper/shapeResizeHelper'; | |
| import { | |
| getFillImageFromShape, | |
| rePositionFilterTypeFillImage, | |
| reMakePatternImageSource, | |
| makeFillPatternForFilter, | |
| makeFilterOptionFromFabricImage, | |
| resetFillPatternCanvas, | |
| } from '@/helper/shapeFilterFillHelper'; | |
| import { | |
| changeOrigin, | |
| getCustomProperty, | |
| getFillTypeFromOption, | |
| getFillTypeFromObject, | |
| isShape, | |
| } from '@/util'; | |
| import { | |
| rejectMessages, | |
| eventNames, | |
| keyCodes as KEY_CODES, | |
| componentNames, | |
| fObjectOptions, | |
| SHAPE_DEFAULT_OPTIONS, | |
| SHAPE_FILL_TYPE, | |
| } from '@/consts'; | |
| const SHAPE_INIT_OPTIONS = extend( | |
| { | |
| strokeWidth: 1, | |
| stroke: '#000000', | |
| fill: '#ffffff', | |
| width: 1, | |
| height: 1, | |
| rx: 0, | |
| ry: 0, | |
| }, | |
| SHAPE_DEFAULT_OPTIONS | |
| ); | |
| const DEFAULT_TYPE = 'rect'; | |
| const DEFAULT_WIDTH = 20; | |
| const DEFAULT_HEIGHT = 20; | |
| /** | |
| * Make fill option | |
| * @param {Object} options - Options to create the shape | |
| * @param {Object.Image} canvasImage - canvas background image | |
| * @param {Function} createStaticCanvas - static canvas creater | |
| * @returns {Object} - shape option | |
| * @private | |
| */ | |
| function makeFabricFillOption(options, canvasImage, createStaticCanvas) { | |
| const fillOption = options.fill; | |
| const fillType = getFillTypeFromOption(options.fill); | |
| let fill = fillOption; | |
| if (fillOption.color) { | |
| fill = fillOption.color; | |
| } | |
| let extOption = null; | |
| if (fillType === 'filter') { | |
| const newStaticCanvas = createStaticCanvas(); | |
| extOption = makeFillPatternForFilter(canvasImage, fillOption.filter, newStaticCanvas); | |
| } else { | |
| extOption = { fill }; | |
| } | |
| return extend({}, options, extOption); | |
| } | |
| /** | |
| * Shape | |
| * @class Shape | |
| * @param {Graphics} graphics - Graphics instance | |
| * @extends {Component} | |
| * @ignore | |
| */ | |
| export default class Shape extends Component { | |
| constructor(graphics) { | |
| super(componentNames.SHAPE, graphics); | |
| /** | |
| * Object of The drawing shape | |
| * @type {fabric.Object} | |
| * @private | |
| */ | |
| this._shapeObj = null; | |
| /** | |
| * Type of the drawing shape | |
| * @type {string} | |
| * @private | |
| */ | |
| this._type = DEFAULT_TYPE; | |
| /** | |
| * Options to draw the shape | |
| * @type {Object} | |
| * @private | |
| */ | |
| this._options = extend({}, SHAPE_INIT_OPTIONS); | |
| /** | |
| * Whether the shape object is selected or not | |
| * @type {boolean} | |
| * @private | |
| */ | |
| this._isSelected = false; | |
| /** | |
| * Pointer for drawing shape (x, y) | |
| * @type {Object} | |
| * @private | |
| */ | |
| this._startPoint = {}; | |
| /** | |
| * Using shortcut on drawing shape | |
| * @type {boolean} | |
| * @private | |
| */ | |
| this._withShiftKey = false; | |
| /** | |
| * Event handler list | |
| * @type {Object} | |
| * @private | |
| */ | |
| this._handlers = { | |
| mousedown: this._onFabricMouseDown.bind(this), | |
| mousemove: this._onFabricMouseMove.bind(this), | |
| mouseup: this._onFabricMouseUp.bind(this), | |
| keydown: this._onKeyDown.bind(this), | |
| keyup: this._onKeyUp.bind(this), | |
| }; | |
| } | |
| /** | |
| * Start to draw the shape on canvas | |
| * @ignore | |
| */ | |
| start() { | |
| const canvas = this.getCanvas(); | |
| this._isSelected = false; | |
| canvas.defaultCursor = 'crosshair'; | |
| canvas.selection = false; | |
| canvas.uniformScaling = true; | |
| canvas.on({ | |
| 'mouse:down': this._handlers.mousedown, | |
| }); | |
| fabric.util.addListener(document, 'keydown', this._handlers.keydown); | |
| fabric.util.addListener(document, 'keyup', this._handlers.keyup); | |
| } | |
| /** | |
| * End to draw the shape on canvas | |
| * @ignore | |
| */ | |
| end() { | |
| const canvas = this.getCanvas(); | |
| this._isSelected = false; | |
| canvas.defaultCursor = 'default'; | |
| canvas.selection = true; | |
| canvas.uniformScaling = false; | |
| canvas.off({ | |
| 'mouse:down': this._handlers.mousedown, | |
| }); | |
| fabric.util.removeListener(document, 'keydown', this._handlers.keydown); | |
| fabric.util.removeListener(document, 'keyup', this._handlers.keyup); | |
| } | |
| /** | |
| * Set states of the current drawing shape | |
| * @ignore | |
| * @param {string} type - Shape type (ex: 'rect', 'circle') | |
| * @param {Object} [options] - Shape options | |
| * @param {(ShapeFillOption | string)} [options.fill] - {@link ShapeFillOption} or | |
| * Shape foreground color (ex: '#fff', 'transparent') | |
| * @param {string} [options.stoke] - Shape outline color | |
| * @param {number} [options.strokeWidth] - Shape outline width | |
| * @param {number} [options.width] - Width value (When type option is 'rect', this options can use) | |
| * @param {number} [options.height] - Height value (When type option is 'rect', this options can use) | |
| * @param {number} [options.rx] - Radius x value (When type option is 'circle', this options can use) | |
| * @param {number} [options.ry] - Radius y value (When type option is 'circle', this options can use) | |
| */ | |
| setStates(type, options) { | |
| this._type = type; | |
| if (options) { | |
| this._options = extend(this._options, options); | |
| } | |
| } | |
| /** | |
| * Add the shape | |
| * @ignore | |
| * @param {string} type - Shape type (ex: 'rect', 'circle') | |
| * @param {Object} options - Shape options | |
| * @param {(ShapeFillOption | string)} [options.fill] - ShapeFillOption or Shape foreground color (ex: '#fff', 'transparent') or ShapeFillOption object | |
| * @param {string} [options.stroke] - Shape outline color | |
| * @param {number} [options.strokeWidth] - Shape outline width | |
| * @param {number} [options.width] - Width value (When type option is 'rect', this options can use) | |
| * @param {number} [options.height] - Height value (When type option is 'rect', this options can use) | |
| * @param {number} [options.rx] - Radius x value (When type option is 'circle', this options can use) | |
| * @param {number} [options.ry] - Radius y value (When type option is 'circle', this options can use) | |
| * @param {number} [options.isRegular] - Whether scaling shape has 1:1 ratio or not | |
| * @returns {Promise} | |
| */ | |
| add(type, options) { | |
| return new Promise((resolve) => { | |
| const canvas = this.getCanvas(); | |
| const extendOption = this._extendOptions(options); | |
| const shapeObj = this._createInstance(type, extendOption); | |
| const objectProperties = this.graphics.createObjectProperties(shapeObj); | |
| this._bindEventOnShape(shapeObj); | |
| canvas.add(shapeObj).setActiveObject(shapeObj); | |
| this._resetPositionFillFilter(shapeObj); | |
| resolve(objectProperties); | |
| }); | |
| } | |
| /** | |
| * Change the shape | |
| * @ignore | |
| * @param {fabric.Object} shapeObj - Selected shape object on canvas | |
| * @param {Object} options - Shape options | |
| * @param {(ShapeFillOption | string)} [options.fill] - {@link ShapeFillOption} or | |
| * Shape foreground color (ex: '#fff', 'transparent') | |
| * @param {string} [options.stroke] - Shape outline color | |
| * @param {number} [options.strokeWidth] - Shape outline width | |
| * @param {number} [options.width] - Width value (When type option is 'rect', this options can use) | |
| * @param {number} [options.height] - Height value (When type option is 'rect', this options can use) | |
| * @param {number} [options.rx] - Radius x value (When type option is 'circle', this options can use) | |
| * @param {number} [options.ry] - Radius y value (When type option is 'circle', this options can use) | |
| * @param {number} [options.isRegular] - Whether scaling shape has 1:1 ratio or not | |
| * @returns {Promise} | |
| */ | |
| change(shapeObj, options) { | |
| return new Promise((resolve, reject) => { | |
| if (!isShape(shapeObj)) { | |
| reject(rejectMessages.unsupportedType); | |
| } | |
| const hasFillOption = getFillTypeFromOption(options.fill) === 'filter'; | |
| const { canvasImage, createStaticCanvas } = this.graphics; | |
| shapeObj.set( | |
| hasFillOption ? makeFabricFillOption(options, canvasImage, createStaticCanvas) : options | |
| ); | |
| if (hasFillOption) { | |
| this._resetPositionFillFilter(shapeObj); | |
| } | |
| this.getCanvas().renderAll(); | |
| resolve(); | |
| }); | |
| } | |
| /** | |
| * make fill property for user event | |
| * @param {fabric.Object} shapeObj - fabric object | |
| * @returns {Object} | |
| */ | |
| makeFillPropertyForUserEvent(shapeObj) { | |
| const fillType = getFillTypeFromObject(shapeObj); | |
| const fillProp = {}; | |
| if (fillType === SHAPE_FILL_TYPE.FILTER) { | |
| const fillImage = getFillImageFromShape(shapeObj); | |
| const filterOption = makeFilterOptionFromFabricImage(fillImage); | |
| fillProp.type = fillType; | |
| fillProp.filter = filterOption; | |
| } else { | |
| fillProp.type = SHAPE_FILL_TYPE.COLOR; | |
| fillProp.color = shapeObj.fill || 'transparent'; | |
| } | |
| return fillProp; | |
| } | |
| /** | |
| * Copy object handling. | |
| * @param {fabric.Object} shapeObj - Shape object | |
| * @param {fabric.Object} originalShapeObj - Shape object | |
| */ | |
| processForCopiedObject(shapeObj, originalShapeObj) { | |
| this._bindEventOnShape(shapeObj); | |
| if (getFillTypeFromObject(shapeObj) === 'filter') { | |
| const fillImage = getFillImageFromShape(originalShapeObj); | |
| const filterOption = makeFilterOptionFromFabricImage(fillImage); | |
| const newStaticCanvas = this.graphics.createStaticCanvas(); | |
| shapeObj.set( | |
| makeFillPatternForFilter(this.graphics.canvasImage, filterOption, newStaticCanvas) | |
| ); | |
| this._resetPositionFillFilter(shapeObj); | |
| } | |
| } | |
| /** | |
| * Create the instance of shape | |
| * @param {string} type - Shape type | |
| * @param {Object} options - Options to creat the shape | |
| * @returns {fabric.Object} Shape instance | |
| * @private | |
| */ | |
| _createInstance(type, options) { | |
| let instance; | |
| switch (type) { | |
| case 'rect': | |
| instance = new fabric.Rect(options); | |
| break; | |
| case 'circle': | |
| instance = new fabric.Ellipse( | |
| extend( | |
| { | |
| type: 'circle', | |
| }, | |
| options | |
| ) | |
| ); | |
| break; | |
| case 'triangle': | |
| instance = new fabric.Triangle(options); | |
| break; | |
| default: | |
| instance = {}; | |
| } | |
| return instance; | |
| } | |
| /** | |
| * Get the options to create the shape | |
| * @param {Object} options - Options to creat the shape | |
| * @returns {Object} Shape options | |
| * @private | |
| */ | |
| _extendOptions(options) { | |
| const selectionStyles = fObjectOptions.SELECTION_STYLE; | |
| const { canvasImage, createStaticCanvas } = this.graphics; | |
| options = extend({}, SHAPE_INIT_OPTIONS, this._options, selectionStyles, options); | |
| return makeFabricFillOption(options, canvasImage, createStaticCanvas); | |
| } | |
| /** | |
| * Bind fabric events on the creating shape object | |
| * @param {fabric.Object} shapeObj - Shape object | |
| * @private | |
| */ | |
| _bindEventOnShape(shapeObj) { | |
| const self = this; | |
| const canvas = this.getCanvas(); | |
| shapeObj.on({ | |
| added() { | |
| self._shapeObj = this; | |
| resizeHelper.setOrigins(self._shapeObj); | |
| }, | |
| selected() { | |
| self._isSelected = true; | |
| self._shapeObj = this; | |
| canvas.uniformScaling = true; | |
| canvas.defaultCursor = 'default'; | |
| resizeHelper.setOrigins(self._shapeObj); | |
| }, | |
| deselected() { | |
| self._isSelected = false; | |
| self._shapeObj = null; | |
| canvas.defaultCursor = 'crosshair'; | |
| canvas.uniformScaling = false; | |
| }, | |
| modified() { | |
| const currentObj = self._shapeObj; | |
| resizeHelper.adjustOriginToCenter(currentObj); | |
| resizeHelper.setOrigins(currentObj); | |
| }, | |
| modifiedInGroup(activeSelection) { | |
| self._fillFilterRePositionInGroupSelection(shapeObj, activeSelection); | |
| }, | |
| moving() { | |
| self._resetPositionFillFilter(this); | |
| }, | |
| rotating() { | |
| self._resetPositionFillFilter(this); | |
| }, | |
| scaling(fEvent) { | |
| const pointer = canvas.getPointer(fEvent.e); | |
| const currentObj = self._shapeObj; | |
| canvas.setCursor('crosshair'); | |
| resizeHelper.resize(currentObj, pointer, true); | |
| self._resetPositionFillFilter(this); | |
| }, | |
| }); | |
| } | |
| /** | |
| * MouseDown event handler on canvas | |
| * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event object | |
| * @private | |
| */ | |
| _onFabricMouseDown(fEvent) { | |
| if (!fEvent.target) { | |
| this._isSelected = false; | |
| this._shapeObj = false; | |
| } | |
| if (!this._isSelected && !this._shapeObj) { | |
| const canvas = this.getCanvas(); | |
| this._startPoint = canvas.getPointer(fEvent.e); | |
| canvas.on({ | |
| 'mouse:move': this._handlers.mousemove, | |
| 'mouse:up': this._handlers.mouseup, | |
| }); | |
| } | |
| } | |
| /** | |
| * MouseDown event handler on canvas | |
| * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event object | |
| * @private | |
| */ | |
| _onFabricMouseMove(fEvent) { | |
| const canvas = this.getCanvas(); | |
| const pointer = canvas.getPointer(fEvent.e); | |
| const startPointX = this._startPoint.x; | |
| const startPointY = this._startPoint.y; | |
| const width = startPointX - pointer.x; | |
| const height = startPointY - pointer.y; | |
| const shape = this._shapeObj; | |
| if (!shape) { | |
| this.add(this._type, { | |
| left: startPointX, | |
| top: startPointY, | |
| width, | |
| height, | |
| }).then((objectProps) => { | |
| this.fire(eventNames.ADD_OBJECT, objectProps); | |
| }); | |
| } else { | |
| this._shapeObj.set({ | |
| isRegular: this._withShiftKey, | |
| }); | |
| resizeHelper.resize(shape, pointer); | |
| canvas.renderAll(); | |
| this._resetPositionFillFilter(shape); | |
| } | |
| } | |
| /** | |
| * MouseUp event handler on canvas | |
| * @private | |
| */ | |
| _onFabricMouseUp() { | |
| const canvas = this.getCanvas(); | |
| const startPointX = this._startPoint.x; | |
| const startPointY = this._startPoint.y; | |
| const shape = this._shapeObj; | |
| if (!shape) { | |
| this.add(this._type, { | |
| left: startPointX, | |
| top: startPointY, | |
| width: DEFAULT_WIDTH, | |
| height: DEFAULT_HEIGHT, | |
| }).then((objectProps) => { | |
| this.fire(eventNames.ADD_OBJECT, objectProps); | |
| }); | |
| } else if (shape) { | |
| resizeHelper.adjustOriginToCenter(shape); | |
| this.fire(eventNames.OBJECT_ADDED, this.graphics.createObjectProperties(shape)); | |
| } | |
| canvas.off({ | |
| 'mouse:move': this._handlers.mousemove, | |
| 'mouse:up': this._handlers.mouseup, | |
| }); | |
| } | |
| /** | |
| * Keydown event handler on document | |
| * @param {KeyboardEvent} e - Event object | |
| * @private | |
| */ | |
| _onKeyDown(e) { | |
| if (e.keyCode === KEY_CODES.SHIFT) { | |
| this._withShiftKey = true; | |
| if (this._shapeObj) { | |
| this._shapeObj.isRegular = true; | |
| } | |
| } | |
| } | |
| /** | |
| * Keyup event handler on document | |
| * @param {KeyboardEvent} e - Event object | |
| * @private | |
| */ | |
| _onKeyUp(e) { | |
| if (e.keyCode === KEY_CODES.SHIFT) { | |
| this._withShiftKey = false; | |
| if (this._shapeObj) { | |
| this._shapeObj.isRegular = false; | |
| } | |
| } | |
| } | |
| /** | |
| * Reset shape position and internal proportions in the filter type fill area. | |
| * @param {fabric.Object} shapeObj - Shape object | |
| * @private | |
| */ | |
| _resetPositionFillFilter(shapeObj) { | |
| if (getFillTypeFromObject(shapeObj) !== 'filter') { | |
| return; | |
| } | |
| const { patternSourceCanvas } = getCustomProperty(shapeObj, 'patternSourceCanvas'); | |
| const fillImage = getFillImageFromShape(shapeObj); | |
| const { originalAngle } = getCustomProperty(fillImage, 'originalAngle'); | |
| if (this.graphics.canvasImage.angle !== originalAngle) { | |
| reMakePatternImageSource(shapeObj, this.graphics.canvasImage); | |
| } | |
| const { originX, originY } = shapeObj; | |
| resizeHelper.adjustOriginToCenter(shapeObj); | |
| shapeObj.width *= shapeObj.scaleX; | |
| shapeObj.height *= shapeObj.scaleY; | |
| shapeObj.rx *= shapeObj.scaleX; | |
| shapeObj.ry *= shapeObj.scaleY; | |
| shapeObj.scaleX = 1; | |
| shapeObj.scaleY = 1; | |
| rePositionFilterTypeFillImage(shapeObj); | |
| changeOrigin(shapeObj, { | |
| originX, | |
| originY, | |
| }); | |
| resetFillPatternCanvas(patternSourceCanvas); | |
| } | |
| /** | |
| * Reset filter area position within group selection. | |
| * @param {fabric.Object} shapeObj - Shape object | |
| * @param {fabric.ActiveSelection} activeSelection - Shape object | |
| * @private | |
| */ | |
| _fillFilterRePositionInGroupSelection(shapeObj, activeSelection) { | |
| if (activeSelection.scaleX !== 1 || activeSelection.scaleY !== 1) { | |
| // This is necessary because the group's scale transition state affects the relative size of the fill area. | |
| // The only way to reset the object transformation scale state to neutral. | |
| // {@link https://github.com/fabricjs/fabric.js/issues/5372} | |
| activeSelection.addWithUpdate(); | |
| } | |
| const { angle, left, top } = shapeObj; | |
| fabric.util.addTransformToObject(shapeObj, activeSelection.calcTransformMatrix()); | |
| this._resetPositionFillFilter(shapeObj); | |
| shapeObj.set({ | |
| angle, | |
| left, | |
| top, | |
| }); | |
| } | |
| } | |