UNPKG

jab-image-editor

Version:
540 lines (482 loc) 17.1 kB
/** * @author NHN Ent. FE Development Team <dl_javascript@nhn.com> * @fileoverview Cropzone extending fabric.Rect */ import snippet from 'tui-code-snippet'; import fabric from 'fabric'; import {clamp} from '../util'; import {eventNames as events} from '../consts'; const CORNER_TYPE_TOP_LEFT = 'tl'; const CORNER_TYPE_TOP_RIGHT = 'tr'; const CORNER_TYPE_MIDDLE_TOP = 'mt'; const CORNER_TYPE_MIDDLE_LEFT = 'ml'; const CORNER_TYPE_MIDDLE_RIGHT = 'mr'; const CORNER_TYPE_MIDDLE_BOTTOM = 'mb'; const CORNER_TYPE_BOTTOM_LEFT = 'bl'; const CORNER_TYPE_BOTTOM_RIGHT = 'br'; const CORNER_TYPE_LIST = [ CORNER_TYPE_TOP_LEFT, CORNER_TYPE_TOP_RIGHT, CORNER_TYPE_MIDDLE_TOP, CORNER_TYPE_MIDDLE_LEFT, CORNER_TYPE_MIDDLE_RIGHT, CORNER_TYPE_MIDDLE_BOTTOM, CORNER_TYPE_BOTTOM_LEFT, CORNER_TYPE_BOTTOM_RIGHT ]; const NOOP_FUNCTION = () => {}; /** * Align with cropzone ratio * @param {string} selectedCorner - selected corner type * @returns {{width: number, height: number}} * @private */ function cornerTypeValid(selectedCorner) { return CORNER_TYPE_LIST.indexOf(selectedCorner) >= 0; } /** * return scale basis type * @param {number} diffX - X distance of the cursor and corner. * @param {number} diffY - Y distance of the cursor and corner. * @returns {string} * @private */ function getScaleBasis(diffX, diffY) { return diffX > diffY ? 'width' : 'height'; } /** * Cropzone object * Issue: IE7, 8(with excanvas) * - Cropzone is a black zone without transparency. * @class Cropzone * @extends {fabric.Rect} * @ignore */ const Cropzone = fabric.util.createClass(fabric.Rect, /** @lends Cropzone.prototype */{ /** * Constructor * @param {Object} canvas canvas * @param {Object} options Options object * @param {Object} extendsOptions object for extends "options" * @override */ initialize(canvas, options, extendsOptions) { options = snippet.extend(options, extendsOptions); options.type = 'cropzone'; this.callSuper('initialize', options); this._addEventHandler(); this.canvas = canvas; this.options = options; }, canvasEventDelegation(eventName) { let delegationState = 'unregisted'; const isRegisted = this.canvasEventTrigger[eventName] !== NOOP_FUNCTION; if (isRegisted) { delegationState = 'registed'; } else if ([events.OBJECT_MOVED, events.OBJECT_SCALED].indexOf(eventName) < 0) { delegationState = 'none'; } return delegationState; }, canvasEventRegister(eventName, eventTrigger) { this.canvasEventTrigger[eventName] = eventTrigger; }, _addEventHandler() { this.canvasEventTrigger = { [events.OBJECT_MOVED]: NOOP_FUNCTION, [events.OBJECT_SCALED]: NOOP_FUNCTION }; this.on({ 'moving': this._onMoving.bind(this), 'scaling': this._onScaling.bind(this) }); }, _renderCropzone(ctx) { const cropzoneDashLineWidth = 7; const cropzoneDashLineOffset = 7; // Calc original scale const originalFlipX = this.flipX ? -1 : 1; const originalFlipY = this.flipY ? -1 : 1; const originalScaleX = originalFlipX / this.scaleX; const originalScaleY = originalFlipY / this.scaleY; // Set original scale ctx.scale(originalScaleX, originalScaleY); // Render outer rect this._fillOuterRect(ctx, 'rgba(0, 0, 0, 0.5)'); if (this.options.lineWidth) { this._fillInnerRect(ctx); this._strokeBorder(ctx, 'rgb(255, 255, 255)', { lineWidth: this.options.lineWidth }); } else { // Black dash line this._strokeBorder(ctx, 'rgb(0, 0, 0)', { lineDashWidth: cropzoneDashLineWidth }); // White dash line this._strokeBorder(ctx, 'rgb(255, 255, 255)', { lineDashWidth: cropzoneDashLineWidth, lineDashOffset: cropzoneDashLineOffset }); } // Reset scale ctx.scale(1 / originalScaleX, 1 / originalScaleY); }, /** * Render Crop-zone * @private * @override */ _render(ctx) { this.callSuper('_render', ctx); this._renderCropzone(ctx); }, /** * Cropzone-coordinates with outer rectangle * * x0 x1 x2 x3 * y0 +--------------------------+ * |///////|//////////|///////| // <--- "Outer-rectangle" * |///////|//////////|///////| * y1 +-------+----------+-------+ * |///////| Cropzone |///////| Cropzone is the "Inner-rectangle" * |///////| (0, 0) |///////| Center point (0, 0) * y2 +-------+----------+-------+ * |///////|//////////|///////| * |///////|//////////|///////| * y3 +--------------------------+ * * @typedef {{x: Array<number>, y: Array<number>}} cropzoneCoordinates * @ignore */ /** * Fill outer rectangle * @param {CanvasRenderingContext2D} ctx - Context * @param {string|CanvasGradient|CanvasPattern} fillStyle - Fill-style * @private */ _fillOuterRect(ctx, fillStyle) { const {x, y} = this._getCoordinates(); ctx.save(); ctx.fillStyle = fillStyle; ctx.beginPath(); // Outer rectangle // Numbers are +/-1 so that overlay edges don't get blurry. ctx.moveTo(x[0] - 1, y[0] - 1); ctx.lineTo(x[3] + 1, y[0] - 1); ctx.lineTo(x[3] + 1, y[3] + 1); ctx.lineTo(x[0] - 1, y[3] + 1); ctx.lineTo(x[0] - 1, y[0] - 1); ctx.closePath(); // Inner rectangle ctx.moveTo(x[1], y[1]); ctx.lineTo(x[1], y[2]); ctx.lineTo(x[2], y[2]); ctx.lineTo(x[2], y[1]); ctx.lineTo(x[1], y[1]); ctx.closePath(); ctx.fill(); ctx.restore(); }, /** * Draw Inner grid line * @param {CanvasRenderingContext2D} ctx - Context * @private */ _fillInnerRect(ctx) { const {x: outerX, y: outerY} = this._getCoordinates(); const x = this._caculateInnerPosition(outerX, (outerX[2] - outerX[1]) / 3); const y = this._caculateInnerPosition(outerY, (outerY[2] - outerY[1]) / 3); ctx.save(); ctx.strokeStyle = 'rgba(255, 255, 255, 0.7)'; ctx.lineWidth = this.options.lineWidth; ctx.beginPath(); ctx.moveTo(x[0], y[1]); ctx.lineTo(x[3], y[1]); ctx.moveTo(x[0], y[2]); ctx.lineTo(x[3], y[2]); ctx.moveTo(x[1], y[0]); ctx.lineTo(x[1], y[3]); ctx.moveTo(x[2], y[0]); ctx.lineTo(x[2], y[3]); ctx.stroke(); ctx.closePath(); ctx.restore(); }, /** * Calculate Inner Position * @param {Array} outer - outer position * @param {number} size - interval for calculate * @returns {Array} - inner position * @private */ _caculateInnerPosition(outer, size) { const position = []; position[0] = outer[1]; position[1] = outer[1] + size; position[2] = outer[1] + (size * 2); position[3] = outer[2]; return position; }, /** * Get coordinates * @returns {cropzoneCoordinates} - {@link cropzoneCoordinates} * @private */ _getCoordinates() { const {canvas, width, height, left, top} = this; const halfWidth = width / 2; const halfHeight = height / 2; const canvasHeight = canvas.getHeight(); // fabric object const canvasWidth = canvas.getWidth(); // fabric object return { x: snippet.map([ -(halfWidth + left), // x0 -(halfWidth), // x1 halfWidth, // x2 halfWidth + (canvasWidth - left - width) // x3 ], Math.ceil), y: snippet.map([ -(halfHeight + top), // y0 -(halfHeight), // y1 halfHeight, // y2 halfHeight + (canvasHeight - top - height) // y3 ], Math.ceil) }; }, /** * Stroke border * @param {CanvasRenderingContext2D} ctx - Context * @param {string|CanvasGradient|CanvasPattern} strokeStyle - Stroke-style * @param {number} lineDashWidth - Dash width * @param {number} [lineDashOffset] - Dash offset * @param {number} [lineWidth] - line width * @private */ _strokeBorder(ctx, strokeStyle, {lineDashWidth, lineDashOffset, lineWidth}) { const halfWidth = this.width / 2; const halfHeight = this.height / 2; ctx.save(); ctx.strokeStyle = strokeStyle; if (ctx.setLineDash) { ctx.setLineDash([lineDashWidth, lineDashWidth]); } if (lineDashOffset) { ctx.lineDashOffset = lineDashOffset; } if (lineWidth) { ctx.lineWidth = lineWidth; } ctx.beginPath(); ctx.moveTo(-halfWidth, -halfHeight); ctx.lineTo(halfWidth, -halfHeight); ctx.lineTo(halfWidth, halfHeight); ctx.lineTo(-halfWidth, halfHeight); ctx.lineTo(-halfWidth, -halfHeight); ctx.stroke(); ctx.restore(); }, /** * onMoving event listener * @private */ _onMoving() { const {height, width, left, top} = this; const maxLeft = this.canvas.getWidth() - width; const maxTop = this.canvas.getHeight() - height; this.left = clamp(left, 0, maxLeft); this.top = clamp(top, 0, maxTop); this.canvasEventTrigger[events.OBJECT_MOVED](this); }, /** * onScaling event listener * @param {{e: MouseEvent}} fEvent - Fabric event * @private */ _onScaling(fEvent) { const selectedCorner = fEvent.transform.corner; const pointer = this.canvas.getPointer(fEvent.e); const settings = this._calcScalingSizeFromPointer(pointer, selectedCorner); // On scaling cropzone, // change real width and height and fix scaleFactor to 1 this.scale(1).set(settings); this.canvasEventTrigger[events.OBJECT_SCALED](this); }, /** * Calc scaled size from mouse pointer with selected corner * @param {{x: number, y: number}} pointer - Mouse position * @param {string} selectedCorner - selected corner type * @returns {Object} Having left or(and) top or(and) width or(and) height. * @private */ _calcScalingSizeFromPointer(pointer, selectedCorner) { const isCornerTypeValid = cornerTypeValid(selectedCorner); return isCornerTypeValid && this._resizeCropZone(pointer, selectedCorner); }, /** * Align with cropzone ratio * @param {number} width - cropzone width * @param {number} height - cropzone height * @param {number} maxWidth - limit max width * @param {number} maxHeight - limit max height * @param {number} scaleTo - cropzone ratio * @returns {{width: number, height: number}} * @private */ adjustRatioCropzoneSize({width, height, leftMaker, topMaker, maxWidth, maxHeight, scaleTo}) { width = maxWidth ? clamp(width, 1, maxWidth) : width; height = maxHeight ? clamp(height, 1, maxHeight) : height; if (!this.presetRatio) { return { width, height, left: leftMaker(width), top: topMaker(height) }; } if (scaleTo === 'width') { height = width / this.presetRatio; } else { width = height * this.presetRatio; } const maxScaleFactor = Math.min(maxWidth / width, maxHeight / height); if (maxScaleFactor <= 1) { [width, height] = [width, height].map(v => v * maxScaleFactor); } return { width, height, left: leftMaker(width), top: topMaker(height) }; }, /** * Get dimension last state cropzone * @returns {{rectTop: number, rectLeft: number, rectWidth: number, rectHeight: number}} * @private */ _getCropzoneRectInfo() { const {width: canvasWidth, height: canvasHeight} = this.canvas; const { top: rectTop, left: rectLeft, width: rectWidth, height: rectHeight } = this.getBoundingRect(false, true); return { rectTop, rectLeft, rectWidth, rectHeight, rectRight: rectLeft + rectWidth, rectBottom: rectTop + rectHeight, canvasWidth, canvasHeight }; }, /** * Calc scaling dimension * @param {Object} position - Mouse position * @param {string} corner - corner type * @returns {{left: number, top: number, width: number, height: number}} * @private */ _resizeCropZone({x, y}, corner) { const {rectWidth, rectHeight, rectTop, rectLeft, rectBottom, rectRight, canvasWidth, canvasHeight} = this._getCropzoneRectInfo(); const resizeInfoMap = { tl: { width: rectRight - x, height: rectBottom - y, leftMaker: newWidth => rectRight - newWidth, topMaker: newHeight => rectBottom - newHeight, maxWidth: rectRight, maxHeight: rectBottom, scaleTo: getScaleBasis(rectLeft - x, rectTop - y) }, tr: { width: x - rectLeft, height: rectBottom - y, leftMaker: () => rectLeft, topMaker: newHeight => rectBottom - newHeight, maxWidth: canvasWidth - rectLeft, maxHeight: rectBottom, scaleTo: getScaleBasis(x - rectRight, rectTop - y) }, mt: { width: rectWidth, height: rectBottom - y, leftMaker: () => rectLeft, topMaker: newHeight => rectBottom - newHeight, maxWidth: canvasWidth - rectLeft, maxHeight: rectBottom, scaleTo: 'height' }, ml: { width: rectRight - x, height: rectHeight, leftMaker: newWidth => rectRight - newWidth, topMaker: () => rectTop, maxWidth: rectRight, maxHeight: canvasHeight - rectTop, scaleTo: 'width' }, mr: { width: x - rectLeft, height: rectHeight, leftMaker: () => rectLeft, topMaker: () => rectTop, maxWidth: canvasWidth - rectLeft, maxHeight: canvasHeight - rectTop, scaleTo: 'width' }, mb: { width: rectWidth, height: y - rectTop, leftMaker: () => rectLeft, topMaker: () => rectTop, maxWidth: canvasWidth - rectLeft, maxHeight: canvasHeight - rectTop, scaleTo: 'height' }, bl: { width: rectRight - x, height: y - rectTop, leftMaker: newWidth => rectRight - newWidth, topMaker: () => rectTop, maxWidth: rectRight, maxHeight: canvasHeight - rectTop, scaleTo: getScaleBasis(rectLeft - x, y - rectBottom) }, br: { width: x - rectLeft, height: y - rectTop, leftMaker: () => rectLeft, topMaker: () => rectTop, maxWidth: canvasWidth - rectLeft, maxHeight: canvasHeight - rectTop, scaleTo: getScaleBasis(x - rectRight, y - rectBottom) } }; return this.adjustRatioCropzoneSize(resizeInfoMap[corner]); }, /** * Return the whether this cropzone is valid * @returns {boolean} */ isValid() { return ( this.left >= 0 && this.top >= 0 && this.width > 0 && this.height > 0 ); } }); export default Cropzone;