Spaces:
Running
Running
| import { fabric } from 'fabric'; | |
| import extend from 'tui-code-snippet/object/extend'; | |
| import isArray from 'tui-code-snippet/type/isArray'; | |
| import isString from 'tui-code-snippet/type/isString'; | |
| import forEachArray from 'tui-code-snippet/collection/forEachArray'; | |
| import forEachOwnProperties from 'tui-code-snippet/collection/forEachOwnProperties'; | |
| import CustomEvents from 'tui-code-snippet/customEvents/customEvents'; | |
| import ImageLoader from '@/component/imageLoader'; | |
| import Cropper from '@/component/cropper'; | |
| import Flip from '@/component/flip'; | |
| import Rotation from '@/component/rotation'; | |
| import FreeDrawing from '@/component/freeDrawing'; | |
| import Line from '@/component/line'; | |
| import Text from '@/component/text'; | |
| import Icon from '@/component/icon'; | |
| import Filter from '@/component/filter'; | |
| import Shape from '@/component/shape'; | |
| import Zoom from '@/component/zoom'; | |
| import CropperDrawingMode from '@/drawingMode/cropper'; | |
| import FreeDrawingMode from '@/drawingMode/freeDrawing'; | |
| import LineDrawingMode from '@/drawingMode/lineDrawing'; | |
| import ShapeDrawingMode from '@/drawingMode/shape'; | |
| import TextDrawingMode from '@/drawingMode/text'; | |
| import IconDrawingMode from '@/drawingMode/icon'; | |
| import ZoomDrawingMode from '@/drawingMode/zoom'; | |
| import { | |
| makeSelectionUndoData, | |
| makeSelectionUndoDatum, | |
| setCachedUndoDataForDimension, | |
| } from '@/helper/selectionModifyHelper'; | |
| import { getProperties, includes, isShape, stamp } from '@/util'; | |
| import { | |
| componentNames as components, | |
| eventNames as events, | |
| drawingModes, | |
| fObjectOptions, | |
| } from '@/consts'; | |
| import Resize from '@/component/resize'; | |
| import ResizeDrawingMode from '@/drawingMode/resize'; | |
| const DEFAULT_CSS_MAX_WIDTH = 1000; | |
| const DEFAULT_CSS_MAX_HEIGHT = 800; | |
| const EXTRA_PX_FOR_PASTE = 10; | |
| const cssOnly = { | |
| cssOnly: true, | |
| }; | |
| const backstoreOnly = { | |
| backstoreOnly: true, | |
| }; | |
| /** | |
| * Graphics class | |
| * @class | |
| * @param {string|HTMLElement} wrapper - Wrapper's element or selector | |
| * @param {Object} [option] - Canvas max width & height of css | |
| * @param {number} option.cssMaxWidth - Canvas css-max-width | |
| * @param {number} option.cssMaxHeight - Canvas css-max-height | |
| * @ignore | |
| */ | |
| class Graphics { | |
| constructor(element, { cssMaxWidth, cssMaxHeight } = {}) { | |
| /** | |
| * Fabric image instance | |
| * @type {fabric.Image} | |
| */ | |
| this.canvasImage = null; | |
| /** | |
| * Max width of canvas elements | |
| * @type {number} | |
| */ | |
| this.cssMaxWidth = cssMaxWidth || DEFAULT_CSS_MAX_WIDTH; | |
| /** | |
| * Max height of canvas elements | |
| * @type {number} | |
| */ | |
| this.cssMaxHeight = cssMaxHeight || DEFAULT_CSS_MAX_HEIGHT; | |
| /** | |
| * cropper Selection Style | |
| * @type {Object} | |
| */ | |
| this.cropSelectionStyle = {}; | |
| /** | |
| * target fabric object for copy paste feature | |
| * @type {fabric.Object} | |
| * @private | |
| */ | |
| this.targetObjectForCopyPaste = null; | |
| /** | |
| * Image name | |
| * @type {string} | |
| */ | |
| this.imageName = ''; | |
| /** | |
| * Object Map | |
| * @type {Object} | |
| * @private | |
| */ | |
| this._objects = {}; | |
| /** | |
| * Fabric-Canvas instance | |
| * @type {fabric.Canvas} | |
| * @private | |
| */ | |
| this._canvas = null; | |
| /** | |
| * Drawing mode | |
| * @type {string} | |
| * @private | |
| */ | |
| this._drawingMode = drawingModes.NORMAL; | |
| /** | |
| * DrawingMode map | |
| * @type {Object.<string, DrawingMode>} | |
| * @private | |
| */ | |
| this._drawingModeMap = {}; | |
| /** | |
| * Component map | |
| * @type {Object.<string, Component>} | |
| * @private | |
| */ | |
| this._componentMap = {}; | |
| /** | |
| * fabric event handlers | |
| * @type {Object.<string, function>} | |
| * @private | |
| */ | |
| this._handler = { | |
| onMouseDown: this._onMouseDown.bind(this), | |
| onObjectAdded: this._onObjectAdded.bind(this), | |
| onObjectRemoved: this._onObjectRemoved.bind(this), | |
| onObjectMoved: this._onObjectMoved.bind(this), | |
| onObjectScaled: this._onObjectScaled.bind(this), | |
| onObjectModified: this._onObjectModified.bind(this), | |
| onObjectRotated: this._onObjectRotated.bind(this), | |
| onObjectSelected: this._onObjectSelected.bind(this), | |
| onPathCreated: this._onPathCreated.bind(this), | |
| onSelectionCleared: this._onSelectionCleared.bind(this), | |
| onSelectionCreated: this._onSelectionCreated.bind(this), | |
| }; | |
| this._setObjectCachingToFalse(); | |
| this._setCanvasElement(element); | |
| this._createDrawingModeInstances(); | |
| this._createComponents(); | |
| this._attachCanvasEvents(); | |
| this._attachZoomEvents(); | |
| } | |
| /** | |
| * Destroy canvas element | |
| */ | |
| destroy() { | |
| const { wrapperEl } = this._canvas; | |
| this._canvas.clear(); | |
| wrapperEl.parentNode.removeChild(wrapperEl); | |
| this._detachZoomEvents(); | |
| } | |
| /** | |
| * Attach zoom events | |
| */ | |
| _attachZoomEvents() { | |
| const zoom = this.getComponent(components.ZOOM); | |
| zoom.attachKeyboardZoomEvents(); | |
| } | |
| /** | |
| * Detach zoom events | |
| */ | |
| _detachZoomEvents() { | |
| const zoom = this.getComponent(components.ZOOM); | |
| zoom.detachKeyboardZoomEvents(); | |
| } | |
| /** | |
| * Deactivates all objects on canvas | |
| * @returns {Graphics} this | |
| */ | |
| deactivateAll() { | |
| this._canvas.discardActiveObject(); | |
| return this; | |
| } | |
| /** | |
| * Renders all objects on canvas | |
| * @returns {Graphics} this | |
| */ | |
| renderAll() { | |
| this._canvas.renderAll(); | |
| return this; | |
| } | |
| /** | |
| * Adds objects on canvas | |
| * @param {Object|Array} objects - objects | |
| */ | |
| add(objects) { | |
| let theArgs = []; | |
| if (isArray(objects)) { | |
| theArgs = objects; | |
| } else { | |
| theArgs.push(objects); | |
| } | |
| this._canvas.add(...theArgs); | |
| } | |
| /** | |
| * Removes the object or group | |
| * @param {Object} target - graphics object or group | |
| * @returns {boolean} true if contains or false | |
| */ | |
| contains(target) { | |
| return this._canvas.contains(target); | |
| } | |
| /** | |
| * Gets all objects or group | |
| * @returns {Array} all objects, shallow copy | |
| */ | |
| getObjects() { | |
| return this._canvas.getObjects().slice(); | |
| } | |
| /** | |
| * Get an object by id | |
| * @param {number} id - object id | |
| * @returns {fabric.Object} object corresponding id | |
| */ | |
| getObject(id) { | |
| return this._objects[id]; | |
| } | |
| /** | |
| * Removes the object or group | |
| * @param {Object} target - graphics object or group | |
| */ | |
| remove(target) { | |
| this._canvas.remove(target); | |
| } | |
| /** | |
| * Removes all object or group | |
| * @param {boolean} includesBackground - remove the background image or not | |
| * @returns {Array} all objects array which is removed | |
| */ | |
| removeAll(includesBackground) { | |
| const canvas = this._canvas; | |
| const objects = canvas.getObjects().slice(); | |
| canvas.remove(...this._canvas.getObjects()); | |
| if (includesBackground) { | |
| canvas.clear(); | |
| } | |
| return objects; | |
| } | |
| /** | |
| * Removes an object or group by id | |
| * @param {number} id - object id | |
| * @returns {Array} removed objects | |
| */ | |
| removeObjectById(id) { | |
| const objects = []; | |
| const canvas = this._canvas; | |
| const target = this.getObject(id); | |
| const isValidGroup = target && target.isType('group') && !target.isEmpty(); | |
| if (isValidGroup) { | |
| canvas.discardActiveObject(); // restore states for each objects | |
| target.forEachObject((obj) => { | |
| objects.push(obj); | |
| canvas.remove(obj); | |
| }); | |
| } else if (canvas.contains(target)) { | |
| objects.push(target); | |
| canvas.remove(target); | |
| } | |
| return objects; | |
| } | |
| /** | |
| * Get an id by object instance | |
| * @param {fabric.Object} object object | |
| * @returns {number} object id if it exists or null | |
| */ | |
| getObjectId(object) { | |
| let key = null; | |
| for (key in this._objects) { | |
| if (this._objects.hasOwnProperty(key)) { | |
| if (object === this._objects[key]) { | |
| return key; | |
| } | |
| } | |
| } | |
| return null; | |
| } | |
| /** | |
| * Gets an active object or group | |
| * @returns {Object} active object or group instance | |
| */ | |
| getActiveObject() { | |
| return this._canvas._activeObject; | |
| } | |
| /** | |
| * Returns the object ID to delete the object. | |
| * @returns {number} object id for remove | |
| */ | |
| getActiveObjectIdForRemove() { | |
| const activeObject = this.getActiveObject(); | |
| const { type, left, top } = activeObject; | |
| const isSelection = type === 'activeSelection'; | |
| if (isSelection) { | |
| const group = new fabric.Group([...activeObject.getObjects()], { | |
| left, | |
| top, | |
| }); | |
| return this._addFabricObject(group); | |
| } | |
| return this.getObjectId(activeObject); | |
| } | |
| /** | |
| * Verify that you are ready to erase the object. | |
| * @returns {boolean} ready for object remove | |
| */ | |
| isReadyRemoveObject() { | |
| const activeObject = this.getActiveObject(); | |
| return activeObject && !activeObject.isEditing; | |
| } | |
| /** | |
| * Gets an active group object | |
| * @returns {Object} active group object instance | |
| */ | |
| getActiveObjects() { | |
| const activeObject = this._canvas._activeObject; | |
| return activeObject && activeObject.type === 'activeSelection' ? activeObject : null; | |
| } | |
| /** | |
| * Get Active object Selection from object ids | |
| * @param {Array.<Object>} objects - fabric objects | |
| * @returns {Object} target - target object group | |
| */ | |
| getActiveSelectionFromObjects(objects) { | |
| const canvas = this.getCanvas(); | |
| return new fabric.ActiveSelection(objects, { canvas }); | |
| } | |
| /** | |
| * Activates an object or group | |
| * @param {Object} target - target object or group | |
| */ | |
| setActiveObject(target) { | |
| this._canvas.setActiveObject(target); | |
| } | |
| /** | |
| * Set Crop selection style | |
| * @param {Object} style - Selection styles | |
| */ | |
| setCropSelectionStyle(style) { | |
| this.cropSelectionStyle = style; | |
| } | |
| /** | |
| * Get component | |
| * @param {string} name - Component name | |
| * @returns {Component} | |
| */ | |
| getComponent(name) { | |
| return this._componentMap[name]; | |
| } | |
| /** | |
| * Get current drawing mode | |
| * @returns {string} | |
| */ | |
| getDrawingMode() { | |
| return this._drawingMode; | |
| } | |
| /** | |
| * Start a drawing mode. If the current mode is not 'NORMAL', 'stopDrawingMode()' will be called first. | |
| * @param {String} mode Can be one of <I>'CROPPER', 'FREE_DRAWING', 'LINE', 'TEXT', 'SHAPE'</I> | |
| * @param {Object} [option] parameters of drawing mode, it's available with 'FREE_DRAWING', 'LINE_DRAWING' | |
| * @param {Number} [option.width] brush width | |
| * @param {String} [option.color] brush color | |
| * @returns {boolean} true if success or false | |
| */ | |
| startDrawingMode(mode, option) { | |
| if (this._isSameDrawingMode(mode)) { | |
| return true; | |
| } | |
| // If the current mode is not 'NORMAL', 'stopDrawingMode()' will be called first. | |
| this.stopDrawingMode(); | |
| const drawingModeInstance = this._getDrawingModeInstance(mode); | |
| if (drawingModeInstance && drawingModeInstance.start) { | |
| drawingModeInstance.start(this, option); | |
| this._drawingMode = mode; | |
| } | |
| return !!drawingModeInstance; | |
| } | |
| /** | |
| * Stop the current drawing mode and back to the 'NORMAL' mode | |
| */ | |
| stopDrawingMode() { | |
| if (this._isSameDrawingMode(drawingModes.NORMAL)) { | |
| return; | |
| } | |
| const drawingModeInstance = this._getDrawingModeInstance(this.getDrawingMode()); | |
| if (drawingModeInstance && drawingModeInstance.end) { | |
| drawingModeInstance.end(this); | |
| } | |
| this._drawingMode = drawingModes.NORMAL; | |
| } | |
| /** | |
| * Change zoom of canvas | |
| * @param {{x: number, y: number}} center - center of zoom | |
| * @param {number} zoomLevel - zoom level | |
| */ | |
| zoom({ x, y }, zoomLevel) { | |
| const zoom = this.getComponent(components.ZOOM); | |
| zoom.zoom({ x, y }, zoomLevel); | |
| } | |
| /** | |
| * Get zoom mode | |
| * @returns {string} | |
| */ | |
| getZoomMode() { | |
| const zoom = this.getComponent(components.ZOOM); | |
| return zoom.mode; | |
| } | |
| /** | |
| * Start zoom-in mode | |
| */ | |
| startZoomInMode() { | |
| const zoom = this.getComponent(components.ZOOM); | |
| zoom.startZoomInMode(); | |
| } | |
| /** | |
| * Stop zoom-in mode | |
| */ | |
| endZoomInMode() { | |
| const zoom = this.getComponent(components.ZOOM); | |
| zoom.endZoomInMode(); | |
| } | |
| /** | |
| * Zoom out one step | |
| */ | |
| zoomOut() { | |
| const zoom = this.getComponent(components.ZOOM); | |
| zoom.zoomOut(); | |
| } | |
| /** | |
| * Start hand mode | |
| */ | |
| startHandMode() { | |
| const zoom = this.getComponent(components.ZOOM); | |
| zoom.startHandMode(); | |
| } | |
| /** | |
| * Stop hand mode | |
| */ | |
| endHandMode() { | |
| const zoom = this.getComponent(components.ZOOM); | |
| zoom.endHandMode(); | |
| } | |
| /** | |
| * Zoom reset | |
| */ | |
| resetZoom() { | |
| const zoom = this.getComponent(components.ZOOM); | |
| zoom.resetZoom(); | |
| } | |
| /** | |
| * To data url from canvas | |
| * @param {Object} options - options for toDataURL | |
| * @param {String} [options.format=png] The format of the output image. Either "jpeg" or "png" | |
| * @param {Number} [options.quality=1] Quality level (0..1). Only used for jpeg. | |
| * @param {Number} [options.multiplier=1] Multiplier to scale by | |
| * @param {Number} [options.left] Cropping left offset. Introduced in fabric v1.2.14 | |
| * @param {Number} [options.top] Cropping top offset. Introduced in fabric v1.2.14 | |
| * @param {Number} [options.width] Cropping width. Introduced in fabric v1.2.14 | |
| * @param {Number} [options.height] Cropping height. Introduced in fabric v1.2.14 | |
| * @returns {string} A DOMString containing the requested data URI. | |
| */ | |
| toDataURL(options) { | |
| const cropper = this.getComponent(components.CROPPER); | |
| cropper.changeVisibility(false); | |
| const dataUrl = this._canvas && this._canvas.toDataURL(options); | |
| cropper.changeVisibility(true); | |
| return dataUrl; | |
| } | |
| /** | |
| * Save image(background) of canvas | |
| * @param {string} name - Name of image | |
| * @param {?fabric.Image} canvasImage - Fabric image instance | |
| */ | |
| setCanvasImage(name, canvasImage) { | |
| if (canvasImage) { | |
| stamp(canvasImage); | |
| } | |
| this.imageName = name; | |
| this.canvasImage = canvasImage; | |
| } | |
| /** | |
| * Set css max dimension | |
| * @param {{width: number, height: number}} maxDimension - Max width & Max height | |
| */ | |
| setCssMaxDimension(maxDimension) { | |
| this.cssMaxWidth = maxDimension.width || this.cssMaxWidth; | |
| this.cssMaxHeight = maxDimension.height || this.cssMaxHeight; | |
| } | |
| /** | |
| * Adjust canvas dimension with scaling image | |
| */ | |
| adjustCanvasDimension() { | |
| this.adjustCanvasDimensionBase(this.canvasImage.scale(1)); | |
| } | |
| adjustCanvasDimensionBase(canvasImage = null) { | |
| if (!canvasImage) { | |
| canvasImage = this.canvasImage; | |
| } | |
| const { width, height } = canvasImage.getBoundingRect(); | |
| const maxDimension = this._calcMaxDimension(width, height); | |
| this.setCanvasCssDimension({ | |
| width: '100%', | |
| height: '100%', // Set height '' for IE9 | |
| 'max-width': `${maxDimension.width}px`, | |
| 'max-height': `${maxDimension.height}px`, | |
| }); | |
| this.setCanvasBackstoreDimension({ | |
| width, | |
| height, | |
| }); | |
| this._canvas.centerObject(canvasImage); | |
| } | |
| /** | |
| * Set canvas dimension - css only | |
| * {@link http://fabricjs.com/docs/fabric.Canvas.html#setDimensions} | |
| * @param {Object} dimension - Canvas css dimension | |
| */ | |
| setCanvasCssDimension(dimension) { | |
| this._canvas.setDimensions(dimension, cssOnly); | |
| } | |
| /** | |
| * Set canvas dimension - backstore only | |
| * {@link http://fabricjs.com/docs/fabric.Canvas.html#setDimensions} | |
| * @param {Object} dimension - Canvas backstore dimension | |
| */ | |
| setCanvasBackstoreDimension(dimension) { | |
| this._canvas.setDimensions(dimension, backstoreOnly); | |
| } | |
| /** | |
| * Set image properties | |
| * {@link http://fabricjs.com/docs/fabric.Image.html#set} | |
| * @param {Object} setting - Image properties | |
| * @param {boolean} [withRendering] - If true, The changed image will be reflected in the canvas | |
| */ | |
| setImageProperties(setting, withRendering) { | |
| const { canvasImage } = this; | |
| if (!canvasImage) { | |
| return; | |
| } | |
| canvasImage.set(setting).setCoords(); | |
| if (withRendering) { | |
| this._canvas.renderAll(); | |
| } | |
| } | |
| /** | |
| * Returns canvas element of fabric.Canvas[[lower-canvas]] | |
| * @returns {HTMLCanvasElement} | |
| */ | |
| getCanvasElement() { | |
| return this._canvas.getElement(); | |
| } | |
| /** | |
| * Get fabric.Canvas instance | |
| * @returns {fabric.Canvas} | |
| */ | |
| getCanvas() { | |
| return this._canvas; | |
| } | |
| /** | |
| * Get canvasImage (fabric.Image instance) | |
| * @returns {fabric.Image} | |
| */ | |
| getCanvasImage() { | |
| return this.canvasImage; | |
| } | |
| /** | |
| * Get image name | |
| * @returns {string} | |
| */ | |
| getImageName() { | |
| return this.imageName; | |
| } | |
| /** | |
| * Add image object on canvas | |
| * @param {string} imgUrl - Image url to make object | |
| * @returns {Promise} | |
| */ | |
| addImageObject(imgUrl) { | |
| const callback = this._callbackAfterLoadingImageObject.bind(this); | |
| return new Promise((resolve) => { | |
| fabric.Image.fromURL( | |
| imgUrl, | |
| (image) => { | |
| callback(image); | |
| resolve(this.createObjectProperties(image)); | |
| }, | |
| { | |
| crossOrigin: 'Anonymous', | |
| } | |
| ); | |
| }); | |
| } | |
| /** | |
| * Get center position of canvas | |
| * @returns {Object} {left, top} | |
| */ | |
| getCenter() { | |
| return this._canvas.getCenter(); | |
| } | |
| /** | |
| * Get cropped rect | |
| * @returns {Object} rect | |
| */ | |
| getCropzoneRect() { | |
| return this.getComponent(components.CROPPER).getCropzoneRect(); | |
| } | |
| /** | |
| * Get cropped rect | |
| * @param {number} [mode] cropzone rect mode | |
| */ | |
| setCropzoneRect(mode) { | |
| this.getComponent(components.CROPPER).setCropzoneRect(mode); | |
| } | |
| /** | |
| * Get cropped image data | |
| * @param {Object} cropRect cropzone rect | |
| * @param {Number} cropRect.left left position | |
| * @param {Number} cropRect.top top position | |
| * @param {Number} cropRect.width width | |
| * @param {Number} cropRect.height height | |
| * @returns {?{imageName: string, url: string}} cropped Image data | |
| */ | |
| getCroppedImageData(cropRect) { | |
| return this.getComponent(components.CROPPER).getCroppedImageData(cropRect); | |
| } | |
| /** | |
| * Set brush option | |
| * @param {Object} option brush option | |
| * @param {Number} option.width width | |
| * @param {String} option.color color like 'FFFFFF', 'rgba(0, 0, 0, 0.5)' | |
| */ | |
| setBrush(option) { | |
| const drawingMode = this._drawingMode; | |
| let compName = components.FREE_DRAWING; | |
| if (drawingMode === drawingModes.LINE_DRAWING) { | |
| compName = components.LINE; | |
| } | |
| this.getComponent(compName).setBrush(option); | |
| } | |
| /** | |
| * Set states of current drawing shape | |
| * @param {string} type - Shape type (ex: 'rect', 'circle', 'triangle') | |
| * @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) | |
| * @param {number} [options.isRegular] - Whether resizing shape has 1:1 ratio or not | |
| */ | |
| setDrawingShape(type, options) { | |
| this.getComponent(components.SHAPE).setStates(type, options); | |
| } | |
| /** | |
| * Set style of current drawing icon | |
| * @param {string} type - icon type (ex: 'icon-arrow', 'icon-star') | |
| * @param {Object} [iconColor] - Icon color | |
| */ | |
| setIconStyle(type, iconColor) { | |
| this.getComponent(components.ICON).setStates(type, iconColor); | |
| } | |
| /** | |
| * Register icon paths | |
| * @param {Object} pathInfos - Path infos | |
| * @param {string} pathInfos.key - key | |
| * @param {string} pathInfos.value - value | |
| */ | |
| registerPaths(pathInfos) { | |
| this.getComponent(components.ICON).registerPaths(pathInfos); | |
| } | |
| /** | |
| * Change cursor style | |
| * @param {string} cursorType - cursor type | |
| */ | |
| changeCursor(cursorType) { | |
| const canvas = this.getCanvas(); | |
| canvas.defaultCursor = cursorType; | |
| canvas.renderAll(); | |
| } | |
| /** | |
| * Whether it has the filter or not | |
| * @param {string} type - Filter type | |
| * @returns {boolean} true if it has the filter | |
| */ | |
| hasFilter(type) { | |
| return this.getComponent(components.FILTER).hasFilter(type); | |
| } | |
| /** | |
| * Set selection style of fabric object by init option | |
| * @param {Object} styles - Selection styles | |
| */ | |
| setSelectionStyle(styles) { | |
| extend(fObjectOptions.SELECTION_STYLE, styles); | |
| } | |
| /** | |
| * Set object properties | |
| * @param {number} id - object id | |
| * @param {Object} props - props | |
| * @param {string} [props.fill] Color | |
| * @param {string} [props.fontFamily] Font type for text | |
| * @param {number} [props.fontSize] Size | |
| * @param {string} [props.fontStyle] Type of inclination (normal / italic) | |
| * @param {string} [props.fontWeight] Type of thicker or thinner looking (normal / bold) | |
| * @param {string} [props.textAlign] Type of text align (left / center / right) | |
| * @param {string} [props.textDecoration] Type of line (underline / line-through / overline) | |
| * @returns {Object} applied properties | |
| */ | |
| setObjectProperties(id, props) { | |
| const object = this.getObject(id); | |
| const clone = extend({}, props); | |
| object.set(clone); | |
| object.setCoords(); | |
| this.getCanvas().renderAll(); | |
| return clone; | |
| } | |
| /** | |
| * Get object properties corresponding key | |
| * @param {number} id - object id | |
| * @param {Array<string>|ObjectProps|string} keys - property's key | |
| * @returns {Object} properties | |
| */ | |
| getObjectProperties(id, keys) { | |
| const object = this.getObject(id); | |
| const props = {}; | |
| if (isString(keys)) { | |
| props[keys] = object[keys]; | |
| } else if (isArray(keys)) { | |
| forEachArray(keys, (value) => { | |
| props[value] = object[value]; | |
| }); | |
| } else { | |
| forEachOwnProperties(keys, (value, key) => { | |
| props[key] = object[key]; | |
| }); | |
| } | |
| return props; | |
| } | |
| /** | |
| * Get object position by originX, originY | |
| * @param {number} id - object id | |
| * @param {string} originX - can be 'left', 'center', 'right' | |
| * @param {string} originY - can be 'top', 'center', 'bottom' | |
| * @returns {Object} {{x:number, y: number}} position by origin if id is valid, or null | |
| */ | |
| getObjectPosition(id, originX, originY) { | |
| const targetObj = this.getObject(id); | |
| if (!targetObj) { | |
| return null; | |
| } | |
| return targetObj.getPointByOrigin(originX, originY); | |
| } | |
| /** | |
| * Set object position by originX, originY | |
| * @param {number} id - object id | |
| * @param {Object} posInfo - position object | |
| * @param {number} posInfo.x - x position | |
| * @param {number} posInfo.y - y position | |
| * @param {string} posInfo.originX - can be 'left', 'center', 'right' | |
| * @param {string} posInfo.originY - can be 'top', 'center', 'bottom' | |
| * @returns {boolean} true if target id is valid or false | |
| */ | |
| setObjectPosition(id, posInfo) { | |
| const targetObj = this.getObject(id); | |
| const { x, y, originX, originY } = posInfo; | |
| if (!targetObj) { | |
| return false; | |
| } | |
| const targetOrigin = targetObj.getPointByOrigin(originX, originY); | |
| const centerOrigin = targetObj.getPointByOrigin('center', 'center'); | |
| const diffX = centerOrigin.x - targetOrigin.x; | |
| const diffY = centerOrigin.y - targetOrigin.y; | |
| targetObj.set({ | |
| left: x + diffX, | |
| top: y + diffY, | |
| }); | |
| targetObj.setCoords(); | |
| return true; | |
| } | |
| /** | |
| * Get the canvas size | |
| * @returns {Object} {{width: number, height: number}} image size | |
| */ | |
| getCanvasSize() { | |
| const image = this.getCanvasImage(); | |
| return { | |
| width: image ? image.width : 0, | |
| height: image ? image.height : 0, | |
| }; | |
| } | |
| /** | |
| * Create fabric static canvas | |
| * @returns {Object} {{width: number, height: number}} image size | |
| */ | |
| createStaticCanvas() { | |
| const staticCanvas = new fabric.StaticCanvas(); | |
| staticCanvas.set({ | |
| enableRetinaScaling: false, | |
| }); | |
| return staticCanvas; | |
| } | |
| /** | |
| * Get a DrawingMode instance | |
| * @param {string} modeName - DrawingMode Class Name | |
| * @returns {DrawingMode} DrawingMode instance | |
| * @private | |
| */ | |
| _getDrawingModeInstance(modeName) { | |
| return this._drawingModeMap[modeName]; | |
| } | |
| /** | |
| * Set object caching to false. This brought many bugs when draw Shape & cropzone | |
| * @see http://fabricjs.com/fabric-object-caching | |
| * @private | |
| */ | |
| _setObjectCachingToFalse() { | |
| fabric.Object.prototype.objectCaching = false; | |
| } | |
| /** | |
| * Set canvas element to fabric.Canvas | |
| * @param {Element|string} element - Wrapper or canvas element or selector | |
| * @private | |
| */ | |
| _setCanvasElement(element) { | |
| let selectedElement; | |
| let canvasElement; | |
| if (element.nodeType) { | |
| selectedElement = element; | |
| } else { | |
| selectedElement = document.querySelector(element); | |
| } | |
| if (selectedElement.nodeName.toUpperCase() !== 'CANVAS') { | |
| canvasElement = document.createElement('canvas'); | |
| selectedElement.appendChild(canvasElement); | |
| } | |
| this._canvas = new fabric.Canvas(canvasElement, { | |
| containerClass: 'tui-image-editor-canvas-container', | |
| enableRetinaScaling: false, | |
| }); | |
| } | |
| /** | |
| * Creates DrawingMode instances | |
| * @private | |
| */ | |
| _createDrawingModeInstances() { | |
| this._register(this._drawingModeMap, new CropperDrawingMode()); | |
| this._register(this._drawingModeMap, new FreeDrawingMode()); | |
| this._register(this._drawingModeMap, new LineDrawingMode()); | |
| this._register(this._drawingModeMap, new ShapeDrawingMode()); | |
| this._register(this._drawingModeMap, new TextDrawingMode()); | |
| this._register(this._drawingModeMap, new IconDrawingMode()); | |
| this._register(this._drawingModeMap, new ZoomDrawingMode()); | |
| this._register(this._drawingModeMap, new ResizeDrawingMode()); | |
| } | |
| /** | |
| * Create components | |
| * @private | |
| */ | |
| _createComponents() { | |
| this._register(this._componentMap, new ImageLoader(this)); | |
| this._register(this._componentMap, new Cropper(this)); | |
| this._register(this._componentMap, new Flip(this)); | |
| this._register(this._componentMap, new Rotation(this)); | |
| this._register(this._componentMap, new FreeDrawing(this)); | |
| this._register(this._componentMap, new Line(this)); | |
| this._register(this._componentMap, new Text(this)); | |
| this._register(this._componentMap, new Icon(this)); | |
| this._register(this._componentMap, new Filter(this)); | |
| this._register(this._componentMap, new Shape(this)); | |
| this._register(this._componentMap, new Zoom(this)); | |
| this._register(this._componentMap, new Resize(this)); | |
| } | |
| /** | |
| * Register component | |
| * @param {Object} map - map object | |
| * @param {Object} module - module which has getName method | |
| * @private | |
| */ | |
| _register(map, module) { | |
| map[module.getName()] = module; | |
| } | |
| /** | |
| * Get the current drawing mode is same with given mode | |
| * @param {string} mode drawing mode | |
| * @returns {boolean} true if same or false | |
| */ | |
| _isSameDrawingMode(mode) { | |
| return this.getDrawingMode() === mode; | |
| } | |
| /** | |
| * Calculate max dimension of canvas | |
| * The css-max dimension is dynamically decided with maintaining image ratio | |
| * The css-max dimension is lower than canvas dimension (attribute of canvas, not css) | |
| * @param {number} width - Canvas width | |
| * @param {number} height - Canvas height | |
| * @returns {{width: number, height: number}} - Max width & Max height | |
| * @private | |
| */ | |
| _calcMaxDimension(width, height) { | |
| const wScaleFactor = this.cssMaxWidth / width; | |
| const hScaleFactor = this.cssMaxHeight / height; | |
| let cssMaxWidth = Math.min(width, this.cssMaxWidth); | |
| let cssMaxHeight = Math.min(height, this.cssMaxHeight); | |
| if (wScaleFactor < 1 && wScaleFactor < hScaleFactor) { | |
| cssMaxWidth = width * wScaleFactor; | |
| cssMaxHeight = height * wScaleFactor; | |
| } else if (hScaleFactor < 1 && hScaleFactor < wScaleFactor) { | |
| cssMaxWidth = width * hScaleFactor; | |
| cssMaxHeight = height * hScaleFactor; | |
| } | |
| return { | |
| width: Math.floor(cssMaxWidth), | |
| height: Math.floor(cssMaxHeight), | |
| }; | |
| } | |
| /** | |
| * Callback function after loading image | |
| * @param {fabric.Image} obj - Fabric image object | |
| * @private | |
| */ | |
| _callbackAfterLoadingImageObject(obj) { | |
| const centerPos = this.getCanvasImage().getCenterPoint(); | |
| obj.set(fObjectOptions.SELECTION_STYLE); | |
| obj.set({ | |
| left: centerPos.x, | |
| top: centerPos.y, | |
| crossOrigin: 'Anonymous', | |
| }); | |
| this.getCanvas().add(obj).setActiveObject(obj); | |
| } | |
| /** | |
| * Attach canvas's events | |
| */ | |
| _attachCanvasEvents() { | |
| const canvas = this._canvas; | |
| const handler = this._handler; | |
| canvas.on({ | |
| 'mouse:down': handler.onMouseDown, | |
| 'object:added': handler.onObjectAdded, | |
| 'object:removed': handler.onObjectRemoved, | |
| 'object:moving': handler.onObjectMoved, | |
| 'object:scaling': handler.onObjectScaled, | |
| 'object:modified': handler.onObjectModified, | |
| 'object:rotating': handler.onObjectRotated, | |
| 'path:created': handler.onPathCreated, | |
| 'selection:cleared': handler.onSelectionCleared, | |
| 'selection:created': handler.onSelectionCreated, | |
| 'selection:updated': handler.onObjectSelected, | |
| }); | |
| } | |
| /** | |
| * "mouse:down" canvas event handler | |
| * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event | |
| * @private | |
| */ | |
| _onMouseDown(fEvent) { | |
| const { e: event, target } = fEvent; | |
| const originPointer = this._canvas.getPointer(event); | |
| if (target) { | |
| const { type } = target; | |
| const undoData = makeSelectionUndoData(target, (item) => | |
| makeSelectionUndoDatum(this.getObjectId(item), item, type === 'activeSelection') | |
| ); | |
| setCachedUndoDataForDimension(undoData); | |
| } | |
| this.fire(events.MOUSE_DOWN, event, originPointer); | |
| } | |
| /** | |
| * "object:added" canvas event handler | |
| * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event | |
| * @private | |
| */ | |
| _onObjectAdded(fEvent) { | |
| const obj = fEvent.target; | |
| if (obj.isType('cropzone')) { | |
| return; | |
| } | |
| this._addFabricObject(obj); | |
| } | |
| /** | |
| * "object:removed" canvas event handler | |
| * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event | |
| * @private | |
| */ | |
| _onObjectRemoved(fEvent) { | |
| const obj = fEvent.target; | |
| this._removeFabricObject(stamp(obj)); | |
| } | |
| /** | |
| * "object:moving" canvas event handler | |
| * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event | |
| * @private | |
| */ | |
| _onObjectMoved(fEvent) { | |
| this._lazyFire( | |
| events.OBJECT_MOVED, | |
| (object) => this.createObjectProperties(object), | |
| fEvent.target | |
| ); | |
| } | |
| /** | |
| * "object:scaling" canvas event handler | |
| * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event | |
| * @private | |
| */ | |
| _onObjectScaled(fEvent) { | |
| this._lazyFire( | |
| events.OBJECT_SCALED, | |
| (object) => this.createObjectProperties(object), | |
| fEvent.target | |
| ); | |
| } | |
| /** | |
| * "object:modified" canvas event handler | |
| * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event | |
| * @private | |
| */ | |
| _onObjectModified(fEvent) { | |
| const { target } = fEvent; | |
| if (target.type === 'activeSelection') { | |
| const items = target.getObjects(); | |
| items.forEach((item) => item.fire('modifiedInGroup', target)); | |
| } | |
| this.fire(events.OBJECT_MODIFIED, target, this.getObjectId(target)); | |
| } | |
| /** | |
| * "object:rotating" canvas event handler | |
| * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event | |
| * @private | |
| */ | |
| _onObjectRotated(fEvent) { | |
| this._lazyFire( | |
| events.OBJECT_ROTATED, | |
| (object) => this.createObjectProperties(object), | |
| fEvent.target | |
| ); | |
| } | |
| /** | |
| * Lazy event emitter | |
| * @param {string} eventName - event name | |
| * @param {Function} paramsMaker - make param function | |
| * @param {Object} [target] - Object of the event owner. | |
| * @private | |
| */ | |
| _lazyFire(eventName, paramsMaker, target) { | |
| const existEventDelegation = target && target.canvasEventDelegation; | |
| const delegationState = existEventDelegation ? target.canvasEventDelegation(eventName) : 'none'; | |
| if (delegationState === 'unregistered') { | |
| target.canvasEventRegister(eventName, (object) => { | |
| this.fire(eventName, paramsMaker(object)); | |
| }); | |
| } | |
| if (delegationState === 'none') { | |
| this.fire(eventName, paramsMaker(target)); | |
| } | |
| } | |
| /** | |
| * "object:selected" canvas event handler | |
| * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event | |
| * @private | |
| */ | |
| _onObjectSelected(fEvent) { | |
| const { target } = fEvent; | |
| const params = this.createObjectProperties(target); | |
| this.fire(events.OBJECT_ACTIVATED, params); | |
| } | |
| /** | |
| * "path:created" canvas event handler | |
| * @param {{path: fabric.Path}} obj - Path object | |
| * @private | |
| */ | |
| _onPathCreated(obj) { | |
| const { x: left, y: top } = obj.path.getCenterPoint(); | |
| obj.path.set( | |
| extend( | |
| { | |
| left, | |
| top, | |
| }, | |
| fObjectOptions.SELECTION_STYLE | |
| ) | |
| ); | |
| const params = this.createObjectProperties(obj.path); | |
| this.fire(events.ADD_OBJECT, params); | |
| } | |
| /** | |
| * "selction:cleared" canvas event handler | |
| * @private | |
| */ | |
| _onSelectionCleared() { | |
| this.fire(events.SELECTION_CLEARED); | |
| } | |
| /** | |
| * "selction:created" canvas event handler | |
| * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event | |
| * @private | |
| */ | |
| _onSelectionCreated(fEvent) { | |
| const { target } = fEvent; | |
| const params = this.createObjectProperties(target); | |
| this.fire(events.OBJECT_ACTIVATED, params); | |
| this.fire(events.SELECTION_CREATED, fEvent.target); | |
| } | |
| /** | |
| * Canvas discard selection all | |
| */ | |
| discardSelection() { | |
| this._canvas.discardActiveObject(); | |
| this._canvas.renderAll(); | |
| } | |
| /** | |
| * Canvas Selectable status change | |
| * @param {boolean} selectable - expect status | |
| */ | |
| changeSelectableAll(selectable) { | |
| this._canvas.forEachObject((obj) => { | |
| obj.selectable = selectable; | |
| obj.hoverCursor = selectable ? 'move' : 'crosshair'; | |
| }); | |
| } | |
| /** | |
| * Return object's properties | |
| * @param {fabric.Object} obj - fabric object | |
| * @returns {Object} properties object | |
| */ | |
| createObjectProperties(obj) { | |
| const predefinedKeys = [ | |
| 'left', | |
| 'top', | |
| 'width', | |
| 'height', | |
| 'fill', | |
| 'stroke', | |
| 'strokeWidth', | |
| 'opacity', | |
| 'angle', | |
| ]; | |
| const props = { | |
| id: stamp(obj), | |
| type: obj.type, | |
| }; | |
| extend(props, getProperties(obj, predefinedKeys)); | |
| if (includes(['i-text', 'text'], obj.type)) { | |
| extend(props, this._createTextProperties(obj, props)); | |
| } else if (includes(['rect', 'triangle', 'circle'], obj.type)) { | |
| const shapeComp = this.getComponent(components.SHAPE); | |
| extend(props, { | |
| fill: shapeComp.makeFillPropertyForUserEvent(obj), | |
| }); | |
| } | |
| return props; | |
| } | |
| /** | |
| * Get text object's properties | |
| * @param {fabric.Object} obj - fabric text object | |
| * @param {Object} props - properties | |
| * @returns {Object} properties object | |
| */ | |
| _createTextProperties(obj) { | |
| const predefinedKeys = [ | |
| 'text', | |
| 'fontFamily', | |
| 'fontSize', | |
| 'fontStyle', | |
| 'textAlign', | |
| 'textDecoration', | |
| 'fontWeight', | |
| ]; | |
| const props = {}; | |
| extend(props, getProperties(obj, predefinedKeys)); | |
| return props; | |
| } | |
| /** | |
| * Add object array by id | |
| * @param {fabric.Object} obj - fabric object | |
| * @returns {number} object id | |
| */ | |
| _addFabricObject(obj) { | |
| const id = stamp(obj); | |
| this._objects[id] = obj; | |
| return id; | |
| } | |
| /** | |
| * Remove an object in array yb id | |
| * @param {number} id - object id | |
| */ | |
| _removeFabricObject(id) { | |
| delete this._objects[id]; | |
| } | |
| /** | |
| * Reset targetObjectForCopyPaste value from activeObject | |
| */ | |
| resetTargetObjectForCopyPaste() { | |
| const activeObject = this.getActiveObject(); | |
| if (activeObject) { | |
| this.targetObjectForCopyPaste = activeObject; | |
| } | |
| } | |
| /** | |
| * Paste fabric object | |
| * @returns {Promise} | |
| */ | |
| pasteObject() { | |
| if (!this.targetObjectForCopyPaste) { | |
| return Promise.resolve([]); | |
| } | |
| const targetObject = this.targetObjectForCopyPaste; | |
| const isGroupSelect = targetObject.type === 'activeSelection'; | |
| const targetObjects = isGroupSelect ? targetObject.getObjects() : [targetObject]; | |
| let newTargetObject = null; | |
| this.discardSelection(); | |
| return this._cloneObject(targetObjects).then((addedObjects) => { | |
| if (addedObjects.length > 1) { | |
| newTargetObject = this.getActiveSelectionFromObjects(addedObjects); | |
| } else { | |
| [newTargetObject] = addedObjects; | |
| } | |
| this.targetObjectForCopyPaste = newTargetObject; | |
| this.setActiveObject(newTargetObject); | |
| }); | |
| } | |
| /** | |
| * Clone object | |
| * @param {fabric.Object} targetObjects - fabric object | |
| * @returns {Promise} | |
| * @private | |
| */ | |
| _cloneObject(targetObjects) { | |
| const addedObjects = targetObjects.map((targetObject) => this._cloneObjectItem(targetObject)); | |
| return Promise.all(addedObjects); | |
| } | |
| /** | |
| * Clone object one item | |
| * @param {fabric.Object} targetObject - fabric object | |
| * @returns {Promise} | |
| * @private | |
| */ | |
| _cloneObjectItem(targetObject) { | |
| return this._copyFabricObjectForPaste(targetObject).then((clonedObject) => { | |
| const objectProperties = this.createObjectProperties(clonedObject); | |
| this.add(clonedObject); | |
| this.fire(events.ADD_OBJECT, objectProperties); | |
| return clonedObject; | |
| }); | |
| } | |
| /** | |
| * Copy fabric object with Changed position for copy and paste | |
| * @param {fabric.Object} targetObject - fabric object | |
| * @returns {Promise} | |
| * @private | |
| */ | |
| _copyFabricObjectForPaste(targetObject) { | |
| const addExtraPx = (value, isReverse) => | |
| isReverse ? value - EXTRA_PX_FOR_PASTE : value + EXTRA_PX_FOR_PASTE; | |
| return this._copyFabricObject(targetObject).then((clonedObject) => { | |
| const { left, top, width, height } = clonedObject; | |
| const { width: canvasWidth, height: canvasHeight } = this.getCanvasSize(); | |
| const rightEdge = left + width / 2; | |
| const bottomEdge = top + height / 2; | |
| clonedObject.set( | |
| extend( | |
| { | |
| left: addExtraPx(left, rightEdge + EXTRA_PX_FOR_PASTE > canvasWidth), | |
| top: addExtraPx(top, bottomEdge + EXTRA_PX_FOR_PASTE > canvasHeight), | |
| }, | |
| fObjectOptions.SELECTION_STYLE | |
| ) | |
| ); | |
| return clonedObject; | |
| }); | |
| } | |
| /** | |
| * Copy fabric object | |
| * @param {fabric.Object} targetObject - fabric object | |
| * @returns {Promise} | |
| * @private | |
| */ | |
| _copyFabricObject(targetObject) { | |
| return new Promise((resolve) => { | |
| targetObject.clone((cloned) => { | |
| const shapeComp = this.getComponent(components.SHAPE); | |
| if (isShape(cloned)) { | |
| shapeComp.processForCopiedObject(cloned, targetObject); | |
| } | |
| resolve(cloned); | |
| }); | |
| }); | |
| } | |
| /** | |
| * Get current dimensions | |
| * @returns {object} | |
| */ | |
| getCurrentDimensions() { | |
| const resize = this.getComponent(components.RESIZE); | |
| return resize.getCurrentDimensions(); | |
| } | |
| /** | |
| * Get original dimensions | |
| * @returns {object} | |
| */ | |
| getOriginalDimensions() { | |
| const resize = this.getComponent(components.RESIZE); | |
| return resize.getOriginalDimensions(); | |
| } | |
| /** | |
| * Set original dimensions | |
| * @param {object} dimensions - Dimensions | |
| */ | |
| setOriginalDimensions(dimensions) { | |
| const resize = this.getComponent(components.RESIZE); | |
| resize.setOriginalDimensions(dimensions); | |
| } | |
| /** | |
| * Resize Image | |
| * @param {Object} dimensions - Resize dimensions | |
| * @returns {Promise} | |
| */ | |
| resize(dimensions) { | |
| const resize = this.getComponent(components.RESIZE); | |
| return resize.resize(dimensions); | |
| } | |
| } | |
| CustomEvents.mixin(Graphics); | |
| export default Graphics; | |