Spaces:
Running
Running
| import { fabric } from 'fabric'; | |
| import extend from 'tui-code-snippet/object/extend'; | |
| import Component from '@/interface/component'; | |
| import Cropzone from '@/extension/cropzone'; | |
| import { keyCodes, componentNames, CROPZONE_DEFAULT_OPTIONS } from '@/consts'; | |
| import { clamp, fixFloatingPoint } from '@/util'; | |
| const MOUSE_MOVE_THRESHOLD = 10; | |
| const DEFAULT_OPTION = { | |
| presetRatio: null, | |
| top: -10, | |
| left: -10, | |
| height: 1, | |
| width: 1, | |
| }; | |
| /** | |
| * Cropper components | |
| * @param {Graphics} graphics - Graphics instance | |
| * @extends {Component} | |
| * @class Cropper | |
| * @ignore | |
| */ | |
| class Cropper extends Component { | |
| constructor(graphics) { | |
| super(componentNames.CROPPER, graphics); | |
| /** | |
| * Cropzone | |
| * @type {Cropzone} | |
| * @private | |
| */ | |
| this._cropzone = null; | |
| /** | |
| * StartX of Cropzone | |
| * @type {number} | |
| * @private | |
| */ | |
| this._startX = null; | |
| /** | |
| * StartY of Cropzone | |
| * @type {number} | |
| * @private | |
| */ | |
| this._startY = null; | |
| /** | |
| * State whether shortcut key is pressed or not | |
| * @type {boolean} | |
| * @private | |
| */ | |
| this._withShiftKey = false; | |
| /** | |
| * Listeners | |
| * @type {object.<string, function>} | |
| * @private | |
| */ | |
| this._listeners = { | |
| keydown: this._onKeyDown.bind(this), | |
| keyup: this._onKeyUp.bind(this), | |
| mousedown: this._onFabricMouseDown.bind(this), | |
| mousemove: this._onFabricMouseMove.bind(this), | |
| mouseup: this._onFabricMouseUp.bind(this), | |
| }; | |
| } | |
| /** | |
| * Start cropping | |
| */ | |
| start() { | |
| if (this._cropzone) { | |
| return; | |
| } | |
| const canvas = this.getCanvas(); | |
| canvas.forEachObject((obj) => { | |
| // {@link http://fabricjs.com/docs/fabric.Object.html#evented} | |
| obj.evented = false; | |
| }); | |
| this._cropzone = new Cropzone( | |
| canvas, | |
| extend( | |
| { | |
| left: 0, | |
| top: 0, | |
| width: 0.5, | |
| height: 0.5, | |
| strokeWidth: 0, // {@link https://github.com/kangax/fabric.js/issues/2860} | |
| cornerSize: 10, | |
| cornerColor: 'black', | |
| fill: 'transparent', | |
| }, | |
| CROPZONE_DEFAULT_OPTIONS, | |
| this.graphics.cropSelectionStyle | |
| ) | |
| ); | |
| canvas.discardActiveObject(); | |
| canvas.add(this._cropzone); | |
| canvas.on('mouse:down', this._listeners.mousedown); | |
| canvas.selection = false; | |
| canvas.defaultCursor = 'crosshair'; | |
| fabric.util.addListener(document, 'keydown', this._listeners.keydown); | |
| fabric.util.addListener(document, 'keyup', this._listeners.keyup); | |
| } | |
| /** | |
| * End cropping | |
| */ | |
| end() { | |
| const canvas = this.getCanvas(); | |
| const cropzone = this._cropzone; | |
| if (!cropzone) { | |
| return; | |
| } | |
| canvas.remove(cropzone); | |
| canvas.selection = true; | |
| canvas.defaultCursor = 'default'; | |
| canvas.off('mouse:down', this._listeners.mousedown); | |
| canvas.forEachObject((obj) => { | |
| obj.evented = true; | |
| }); | |
| this._cropzone = null; | |
| fabric.util.removeListener(document, 'keydown', this._listeners.keydown); | |
| fabric.util.removeListener(document, 'keyup', this._listeners.keyup); | |
| } | |
| /** | |
| * Change cropzone visible | |
| * @param {boolean} visible - cropzone visible state | |
| */ | |
| changeVisibility(visible) { | |
| if (this._cropzone) { | |
| this._cropzone.set({ visible }); | |
| } | |
| } | |
| /** | |
| * onMousedown handler in fabric canvas | |
| * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event | |
| * @private | |
| */ | |
| _onFabricMouseDown(fEvent) { | |
| const canvas = this.getCanvas(); | |
| if (fEvent.target) { | |
| return; | |
| } | |
| canvas.selection = false; | |
| const coord = canvas.getPointer(fEvent.e); | |
| this._startX = coord.x; | |
| this._startY = coord.y; | |
| canvas.on({ | |
| 'mouse:move': this._listeners.mousemove, | |
| 'mouse:up': this._listeners.mouseup, | |
| }); | |
| } | |
| /** | |
| * onMousemove handler in fabric canvas | |
| * @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event | |
| * @private | |
| */ | |
| _onFabricMouseMove(fEvent) { | |
| const canvas = this.getCanvas(); | |
| const pointer = canvas.getPointer(fEvent.e); | |
| const { x, y } = pointer; | |
| const cropzone = this._cropzone; | |
| if (Math.abs(x - this._startX) + Math.abs(y - this._startY) > MOUSE_MOVE_THRESHOLD) { | |
| canvas.remove(cropzone); | |
| cropzone.set(this._calcRectDimensionFromPoint(x, y, cropzone.presetRatio)); | |
| canvas.add(cropzone); | |
| canvas.setActiveObject(cropzone); | |
| } | |
| } | |
| /** | |
| * Get rect dimension setting from Canvas-Mouse-Position(x, y) | |
| * @param {number} x - Canvas-Mouse-Position x | |
| * @param {number} y - Canvas-Mouse-Position Y | |
| * @param {number|null} presetRatio - fixed aspect ratio (width/height) of the cropzone (null if not set) | |
| * @returns {{left: number, top: number, width: number, height: number}} | |
| * @private | |
| */ | |
| _calcRectDimensionFromPoint(x, y, presetRatio = null) { | |
| const canvas = this.getCanvas(); | |
| const canvasWidth = canvas.getWidth(); | |
| const canvasHeight = canvas.getHeight(); | |
| const startX = this._startX; | |
| const startY = this._startY; | |
| let left = clamp(x, 0, startX); | |
| let top = clamp(y, 0, startY); | |
| let width = clamp(x, startX, canvasWidth) - left; // (startX <= x(mouse) <= canvasWidth) - left | |
| let height = clamp(y, startY, canvasHeight) - top; // (startY <= y(mouse) <= canvasHeight) - top | |
| if (this._withShiftKey && !presetRatio) { | |
| // make fixed ratio cropzone | |
| if (width > height) { | |
| height = width; | |
| } else if (height > width) { | |
| width = height; | |
| } | |
| if (startX >= x) { | |
| left = startX - width; | |
| } | |
| if (startY >= y) { | |
| top = startY - height; | |
| } | |
| } else if (presetRatio) { | |
| // Restrict cropzone to given presetRatio | |
| height = width / presetRatio; | |
| // If moving in a direction where the top left corner moves (ie. top-left, bottom-left, top-right) | |
| // the left and/or top values has to be changed based on the new height/width | |
| if (startX >= x) { | |
| left = clamp(startX - width, 0, canvasWidth); | |
| } | |
| if (startY >= y) { | |
| top = clamp(startY - height, 0, canvasHeight); | |
| } | |
| // Check if the new height is too large | |
| if (top + height > canvasHeight) { | |
| height = canvasHeight - top; // Set height to max available height | |
| width = height * presetRatio; // Restrict cropzone to given presetRatio based on the new height | |
| // If moving in a direction where the top left corner moves (ie. top-left, bottom-left, top-right) | |
| // the left and/or top values has to be changed based on the new height/width | |
| if (startX >= x) { | |
| left = clamp(startX - width, 0, canvasWidth); | |
| } | |
| if (startY >= y) { | |
| top = clamp(startY - height, 0, canvasHeight); | |
| } | |
| } | |
| } | |
| return { | |
| left, | |
| top, | |
| width, | |
| height, | |
| }; | |
| } | |
| /** | |
| * onMouseup handler in fabric canvas | |
| * @private | |
| */ | |
| _onFabricMouseUp() { | |
| const cropzone = this._cropzone; | |
| const listeners = this._listeners; | |
| const canvas = this.getCanvas(); | |
| canvas.setActiveObject(cropzone); | |
| canvas.off({ | |
| 'mouse:move': listeners.mousemove, | |
| 'mouse:up': listeners.mouseup, | |
| }); | |
| } | |
| /** | |
| * 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) { | |
| const canvas = this.getCanvas(); | |
| const containsCropzone = canvas.contains(this._cropzone); | |
| if (!cropRect) { | |
| return null; | |
| } | |
| if (containsCropzone) { | |
| canvas.remove(this._cropzone); | |
| } | |
| const imageData = { | |
| imageName: this.getImageName(), | |
| url: canvas.toDataURL(cropRect), | |
| }; | |
| if (containsCropzone) { | |
| canvas.add(this._cropzone); | |
| } | |
| return imageData; | |
| } | |
| /** | |
| * Get cropped rect | |
| * @returns {Object} rect | |
| */ | |
| getCropzoneRect() { | |
| const cropzone = this._cropzone; | |
| if (!cropzone.isValid()) { | |
| return null; | |
| } | |
| return { | |
| left: cropzone.left, | |
| top: cropzone.top, | |
| width: cropzone.width, | |
| height: cropzone.height, | |
| }; | |
| } | |
| /** | |
| * Set a cropzone square | |
| * @param {number} [presetRatio] - preset ratio | |
| */ | |
| setCropzoneRect(presetRatio) { | |
| const canvas = this.getCanvas(); | |
| const cropzone = this._cropzone; | |
| canvas.discardActiveObject(); | |
| canvas.selection = false; | |
| canvas.remove(cropzone); | |
| cropzone.set(presetRatio ? this._getPresetPropertiesForCropSize(presetRatio) : DEFAULT_OPTION); | |
| canvas.add(cropzone); | |
| canvas.selection = true; | |
| if (presetRatio) { | |
| canvas.setActiveObject(cropzone); | |
| } | |
| } | |
| /** | |
| * get a cropzone square info | |
| * @param {number} presetRatio - preset ratio | |
| * @returns {{presetRatio: number, left: number, top: number, width: number, height: number}} | |
| * @private | |
| */ | |
| _getPresetPropertiesForCropSize(presetRatio) { | |
| const canvas = this.getCanvas(); | |
| const originalWidth = canvas.getWidth(); | |
| const originalHeight = canvas.getHeight(); | |
| const standardSize = originalWidth >= originalHeight ? originalWidth : originalHeight; | |
| const getScale = (value, orignalValue) => (value > orignalValue ? orignalValue / value : 1); | |
| let width = standardSize * presetRatio; | |
| let height = standardSize; | |
| const scaleWidth = getScale(width, originalWidth); | |
| [width, height] = [width, height].map((sizeValue) => sizeValue * scaleWidth); | |
| const scaleHeight = getScale(height, originalHeight); | |
| [width, height] = [width, height].map((sizeValue) => fixFloatingPoint(sizeValue * scaleHeight)); | |
| return { | |
| presetRatio, | |
| top: (originalHeight - height) / 2, | |
| left: (originalWidth - width) / 2, | |
| width, | |
| height, | |
| }; | |
| } | |
| /** | |
| * Keydown event handler | |
| * @param {KeyboardEvent} e - Event object | |
| * @private | |
| */ | |
| _onKeyDown(e) { | |
| if (e.keyCode === keyCodes.SHIFT) { | |
| this._withShiftKey = true; | |
| } | |
| } | |
| /** | |
| * Keyup event handler | |
| * @param {KeyboardEvent} e - Event object | |
| * @private | |
| */ | |
| _onKeyUp(e) { | |
| if (e.keyCode === keyCodes.SHIFT) { | |
| this._withShiftKey = false; | |
| } | |
| } | |
| } | |
| export default Cropper; | |