UNPKG

fabric

Version:

Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.

935 lines (934 loc) 32.4 kB
import { _defineProperty } from "../../_virtual/_@oxc-project_runtime@0.122.0/helpers/defineProperty.mjs"; import { BOTTOM, CENTER, LEFT, MODIFIED, RIGHT, SCALE_X, SCALE_Y, SKEW_X, SKEW_Y } from "../constants.mjs"; import { cos } from "../util/misc/cos.mjs"; import { sin } from "../util/misc/sin.mjs"; import { Point } from "../Point.mjs"; import { isCollection } from "../Collection.mjs"; import { createCanvasElement } from "../util/misc/dom.mjs"; import { degreesToRadians } from "../util/misc/radiansDegreesConversion.mjs"; import { pick } from "../util/misc/pick.mjs"; import { isActiveSelection } from "../util/typeAssertions.mjs"; import { StaticCanvas } from "./StaticCanvas.mjs"; import { getPointer, isTouchEvent } from "../util/dom_event.mjs"; import { addTransformToObject, saveObjectTransform } from "../util/misc/objectTransforms.mjs"; import { sendPointToPlane } from "../util/misc/planeChange.mjs"; import { getActionFromCorner } from "../controls/util.mjs"; import { Intersection } from "../Intersection.mjs"; import { FabricObject } from "../shapes/Object/FabricObject.mjs"; import { isTransparent } from "../util/misc/isTransparent.mjs"; import { CanvasDOMManager } from "./DOMManagers/CanvasDOMManager.mjs"; import { canvasDefaults } from "./CanvasOptions.mjs"; import { dragHandler } from "../controls/drag.mjs"; //#region src/canvas/SelectableCanvas.ts /** * Canvas class * @class Canvas * @extends StaticCanvas * @see {@link http://fabric5.fabricjs.com/fabric-intro-part-1#canvas} * * @fires object:modified at the end of a transform * @fires object:rotating while an object is being rotated from the control * @fires object:scaling while an object is being scaled by controls * @fires object:moving while an object is being dragged * @fires object:skewing while an object is being skewed from the controls * * @fires before:transform before a transform is is started * @fires before:selection:cleared * @fires selection:cleared * @fires selection:updated * @fires selection:created * * @fires path:created after a drawing operation ends and the path is added * @fires mouse:down * @fires mouse:move * @fires mouse:up * @fires mouse:down:before on mouse down, before the inner fabric logic runs * @fires mouse:move:before on mouse move, before the inner fabric logic runs * @fires mouse:up:before on mouse up, before the inner fabric logic runs * @fires mouse:over * @fires mouse:out * @fires mouse:dblclick whenever a native dbl click event fires on the canvas. * * @fires dragover * @fires dragenter * @fires dragleave * @fires drag:enter object drag enter * @fires drag:leave object drag leave * @fires drop:before before drop event. Prepare for the drop event (same native event). * @fires drop * @fires drop:after after drop event. Run logic on canvas after event has been accepted/declined (same native event). * @example * let a: fabric.Object, b: fabric.Object; * let flag = false; * canvas.add(a, b); * a.on('drop:before', opt => { * // we want a to accept the drop even though it's below b in the stack * flag = this.canDrop(opt.e); * }); * b.canDrop = function(e) { * !flag && this.draggableTextDelegate.canDrop(e); * } * b.on('dragover', opt => b.set('fill', opt.dropTarget === b ? 'pink' : 'black')); * a.on('drop', opt => { * opt.e.defaultPrevented // drop occurred * opt.didDrop // drop occurred on canvas * opt.target // drop target * opt.target !== a && a.set('text', 'I lost'); * }); * canvas.on('drop:after', opt => { * // inform user who won * if(!opt.e.defaultPrevented) { * // no winners * } * else if(!opt.didDrop) { * // my objects didn't win, some other lucky object * } * else { * // we have a winner it's opt.target!! * } * }) * * @fires after:render at the end of the render process, receives the context in the callback * @fires before:render at start the render process, receives the context in the callback * * @fires contextmenu:before * @fires contextmenu * @example * let handler; * targets.forEach(target => { * target.on('contextmenu:before', opt => { * // decide which target should handle the event before canvas hijacks it * if (someCaseHappens && opt.targets.includes(target)) { * handler = target; * } * }); * target.on('contextmenu', opt => { * // do something fantastic * }); * }); * canvas.on('contextmenu', opt => { * if (!handler) { * // no one takes responsibility, it's always left to me * // let's show them how it's done! * } * }); * */ var SelectableCanvas = class SelectableCanvas extends StaticCanvas { constructor(..._args) { super(..._args); _defineProperty( this, /** * hold the list of nested targets hovered in the previous events * @type FabricObject[] * @private */ "_hoveredTargets", [] ); _defineProperty( this, /** * hold a reference to a data structure that contains information * on the current on going transform * @type * @private */ "_currentTransform", null ); _defineProperty(this, "_groupSelector", null); _defineProperty( this, /** * internal flag used to understand if the context top requires a cleanup * in case this is true, the contextTop will be cleared at the next render * @type boolean * @private */ "contextTopDirty", false ); } static getDefaults() { return { ...super.getDefaults(), ...SelectableCanvas.ownDefaults }; } get upperCanvasEl() { var _this$elements$upper; return (_this$elements$upper = this.elements.upper) === null || _this$elements$upper === void 0 ? void 0 : _this$elements$upper.el; } get contextTop() { var _this$elements$upper2; return (_this$elements$upper2 = this.elements.upper) === null || _this$elements$upper2 === void 0 ? void 0 : _this$elements$upper2.ctx; } get wrapperEl() { return this.elements.container; } initElements(el) { this.elements = new CanvasDOMManager(el, { allowTouchScrolling: this.allowTouchScrolling, containerClass: this.containerClass }); this._createCacheCanvas(); } /** * @private * @param {FabricObject} obj Object that was added */ _onObjectAdded(obj) { this._objectsToRender = void 0; super._onObjectAdded(obj); } /** * @private * @param {FabricObject} obj Object that was removed */ _onObjectRemoved(obj) { this._objectsToRender = void 0; if (obj === this._activeObject) { this.fire("before:selection:cleared", { deselected: [obj] }); this._discardActiveObject(); this.fire("selection:cleared", { deselected: [obj] }); obj.fire("deselected", { target: obj }); } if (obj === this._hoveredTarget) { this._hoveredTarget = void 0; this._hoveredTargets = []; } super._onObjectRemoved(obj); } _onStackOrderChanged() { this._objectsToRender = void 0; super._onStackOrderChanged(); } /** * Divides objects in two groups, one to render immediately * and one to render as activeGroup. * @return {Array} objects to render immediately and pushes the other in the activeGroup. */ _chooseObjectsToRender() { const activeObject = this._activeObject; return !this.preserveObjectStacking && activeObject ? this._objects.filter((object) => !object.group && object !== activeObject).concat(activeObject) : this._objects; } /** * Renders both the top canvas and the secondary container canvas. */ renderAll() { this.cancelRequestedRender(); if (this.destroyed) return; if (this.contextTopDirty && !this._groupSelector && !this.isDrawingMode) { this.clearContext(this.contextTop); this.contextTopDirty = false; } if (this.hasLostContext) { this.renderTopLayer(this.contextTop); this.hasLostContext = false; } !this._objectsToRender && (this._objectsToRender = this._chooseObjectsToRender()); this.renderCanvas(this.getContext(), this._objectsToRender); } /** * text selection is rendered by the active text instance during the rendering cycle */ renderTopLayer(ctx) { ctx.save(); if (this.isDrawingMode && this._isCurrentlyDrawing) { this.freeDrawingBrush && this.freeDrawingBrush._render(); this.contextTopDirty = true; } if (this.selection && this._groupSelector) { this._drawSelection(ctx); this.contextTopDirty = true; } ctx.restore(); } /** * Method to render only the top canvas. * Also used to render the group selection box. * Does not render text selection. */ renderTop() { const ctx = this.contextTop; this.clearContext(ctx); this.renderTopLayer(ctx); this.fire("after:render", { ctx }); } /** * Set the canvas tolerance value for pixel taret find. * Use only integer numbers. * @private */ setTargetFindTolerance(value) { value = Math.round(value); this.targetFindTolerance = value; const retina = this.getRetinaScaling(); const size = Math.ceil((value * 2 + 1) * retina); this.pixelFindCanvasEl.width = this.pixelFindCanvasEl.height = size; this.pixelFindContext.scale(retina, retina); } /** * Returns true if object is transparent at a certain location * Clarification: this is `is target transparent at location X or are controls there` * @TODO this seems dumb that we treat controls with transparency. we can find controls * programmatically without painting them, the cache canvas optimization is always valid * @param {FabricObject} target Object to check * @param {Number} x Left coordinate in viewport space * @param {Number} y Top coordinate in viewport space * @return {Boolean} */ isTargetTransparent(target, x, y) { const tolerance = this.targetFindTolerance; const ctx = this.pixelFindContext; this.clearContext(ctx); ctx.save(); ctx.translate(-x + tolerance, -y + tolerance); ctx.transform(...this.viewportTransform); const selectionBgc = target.selectionBackgroundColor; target.selectionBackgroundColor = ""; target.render(ctx); target.selectionBackgroundColor = selectionBgc; ctx.restore(); const enhancedTolerance = Math.round(tolerance * this.getRetinaScaling()); return isTransparent(ctx, enhancedTolerance, enhancedTolerance, enhancedTolerance); } /** * takes an event and determines if selection key has been pressed * @private * @param {TPointerEvent} e Event object */ _isSelectionKeyPressed(e) { const sKey = this.selectionKey; if (!sKey) return false; if (Array.isArray(sKey)) return !!sKey.find((key) => !!key && e[key] === true); else return e[sKey]; } /** * @private * @param {TPointerEvent} e Event object * @param {FabricObject} target */ _shouldClearSelection(e, target) { const activeObjects = this.getActiveObjects(), activeObject = this._activeObject; return !!(!target || target && activeObject && activeObjects.length > 1 && activeObjects.indexOf(target) === -1 && activeObject !== target && !this._isSelectionKeyPressed(e) || target && !target.evented || target && !target.selectable && activeObject && activeObject !== target); } /** * This method will take in consideration a modifier key pressed and the control we are * about to drag, and try to guess the anchor point ( origin ) of the transormation. * This should be really in the realm of controls, and we should remove specific code for legacy * embedded actions. * @TODO this probably deserve discussion/rediscovery and change/refactor * @private * @deprecated * @param {FabricObject} target * @param {string} action * @param {boolean} altKey * @returns {boolean} true if the transformation should be centered */ _shouldCenterTransform(target, action, modifierKeyPressed) { if (!target) return; let centerTransform; if (action === "scale" || action === "scaleX" || action === "scaleY" || action === "resizing") centerTransform = this.centeredScaling || target.centeredScaling; else if (action === "rotate") centerTransform = this.centeredRotation || target.centeredRotation; return centerTransform ? !modifierKeyPressed : modifierKeyPressed; } /** * Given the control clicked, determine the origin of the transform. * This is bad because controls can totally have custom names * should disappear before release 4.0 * Fabric 7.1, jan 2026 we are still using this. * Needs to go. * @private * @deprecated */ _getOriginFromCorner(target, controlName) { const origin = controlName ? target.controls[controlName].getTransformAnchorPoint() : { x: target.originX, y: target.originY }; if (!controlName) return origin; if ([ "ml", "tl", "bl" ].includes(controlName)) origin.x = RIGHT; else if ([ "mr", "tr", "br" ].includes(controlName)) origin.x = LEFT; if ([ "tl", "mt", "tr" ].includes(controlName)) origin.y = BOTTOM; else if ([ "bl", "mb", "br" ].includes(controlName)) origin.y = "top"; return origin; } /** * @private * @param {Event} e Event object * @param {FabricObject} target * @param {boolean} [alreadySelected] pass true to setup the active control */ _setupCurrentTransform(e, target, alreadySelected) { var _control$getActionHan; const pointer = target.group ? sendPointToPlane(this.getScenePoint(e), void 0, target.group.calcTransformMatrix()) : this.getScenePoint(e); const { key: corner = "", control } = target.getActiveControl() || {}, actionHandler = alreadySelected && control ? (_control$getActionHan = control.getActionHandler(e, target, control)) === null || _control$getActionHan === void 0 ? void 0 : _control$getActionHan.bind(control) : dragHandler, action = getActionFromCorner(alreadySelected, corner, e, target), altKey = e[this.centeredKey], origin = this._shouldCenterTransform(target, action, altKey) ? { x: CENTER, y: CENTER } : this._getOriginFromCorner(target, corner), { scaleX, scaleY, skewX, skewY, left, top, angle, width, height, cropX, cropY } = target, transform = { target, action, actionHandler, actionPerformed: false, corner, scaleX, scaleY, skewX, skewY, offsetX: pointer.x - left, offsetY: pointer.y - top, originX: origin.x, originY: origin.y, ex: pointer.x, ey: pointer.y, lastX: pointer.x, lastY: pointer.y, theta: degreesToRadians(angle), width, height, shiftKey: e.shiftKey, altKey, original: { ...saveObjectTransform(target), originX: origin.x, originY: origin.y, cropX, cropY } }; this._currentTransform = transform; this.fire("before:transform", { e, transform }); } /** * Set the cursor type of the canvas element * @param {String} value Cursor type of the canvas element. * @see http://www.w3.org/TR/css3-ui/#cursor */ setCursor(value) { this.upperCanvasEl.style.cursor = value; } /** * @private * @param {CanvasRenderingContext2D} ctx to draw the selection on */ _drawSelection(ctx) { const { x, y, deltaX, deltaY } = this._groupSelector, start = new Point(x, y).transform(this.viewportTransform), extent = new Point(x + deltaX, y + deltaY).transform(this.viewportTransform), strokeOffset = this.selectionLineWidth / 2; let minX = Math.min(start.x, extent.x), minY = Math.min(start.y, extent.y), maxX = Math.max(start.x, extent.x), maxY = Math.max(start.y, extent.y); if (this.selectionColor) { ctx.fillStyle = this.selectionColor; ctx.fillRect(minX, minY, maxX - minX, maxY - minY); } if (!this.selectionLineWidth || !this.selectionBorderColor) return; ctx.lineWidth = this.selectionLineWidth; ctx.strokeStyle = this.selectionBorderColor; minX += strokeOffset; minY += strokeOffset; maxX -= strokeOffset; maxY -= strokeOffset; FabricObject.prototype._setLineDash.call(this, ctx, this.selectionDashArray); ctx.strokeRect(minX, minY, maxX - minX, maxY - minY); } /** * This function is in charge of deciding which is the object that is the current target of an interaction event. * For interaction event we mean a pointer related action on the canvas. * Which is the * 11/09/2018 TODO: would be cool if findTarget could discern between being a full target * or the outside part of the corner. * @param {Event} e mouse event * @return {TargetsInfoWithContainer} the target found */ findTarget(e) { if (this._targetInfo) return this._targetInfo; if (this.skipTargetFind) return { subTargets: [], currentSubTargets: [] }; const pointer = this.getScenePoint(e), activeObject = this._activeObject, aObjects = this.getActiveObjects(), targetInfo = this.searchPossibleTargets(this._objects, pointer); const { subTargets: currentSubTargets, container: currentContainer, target: currentTarget } = targetInfo; const fullTargetInfo = { ...targetInfo, currentSubTargets, currentContainer, currentTarget }; if (!activeObject) return fullTargetInfo; const activeObjectTargetInfo = { ...this.searchPossibleTargets([activeObject], pointer), currentSubTargets, currentContainer, currentTarget }; if (activeObject.findControl(this.getViewportPoint(e), isTouchEvent(e))) return { ...activeObjectTargetInfo, target: activeObject }; if (activeObjectTargetInfo.target) { if (aObjects.length > 1) return activeObjectTargetInfo; if (!this.preserveObjectStacking) return activeObjectTargetInfo; if (this.preserveObjectStacking && e[this.altSelectionKey]) return activeObjectTargetInfo; } return fullTargetInfo; } /** * Checks if the point is inside the object selection area including padding * @param {FabricObject} obj Object to test against * @param {Object} [pointer] point in scene coordinates * @return {Boolean} true if point is contained within an area of given object * @private */ _pointIsInObjectSelectionArea(obj, point) { let coords = obj.getCoords(); const viewportZoom = this.getZoom(); const padding = obj.padding / viewportZoom; if (padding) { const [tl, tr, br, bl] = coords; const angleRadians = Math.atan2(tr.y - tl.y, tr.x - tl.x), cosP = cos(angleRadians) * padding, sinP = sin(angleRadians) * padding, cosPSinP = cosP + sinP, cosPMinusSinP = cosP - sinP; coords = [ new Point(tl.x - cosPMinusSinP, tl.y - cosPSinP), new Point(tr.x + cosPSinP, tr.y - cosPMinusSinP), new Point(br.x + cosPMinusSinP, br.y + cosPSinP), new Point(bl.x - cosPSinP, bl.y + cosPMinusSinP) ]; } return Intersection.isPointInPolygon(point, coords); } /** * Checks point is inside the object selection condition. Either area with padding * or over pixels if perPixelTargetFind is enabled * @param {FabricObject} obj Object to test against * @param {Point} pointer point from scene. * @return {Boolean} true if point is contained within an area of given object * @private */ _checkTarget(obj, pointer) { if (obj && obj.visible && obj.evented && this._pointIsInObjectSelectionArea(obj, pointer)) if ((this.perPixelTargetFind || obj.perPixelTargetFind) && !obj.isEditing) { const viewportPoint = pointer.transform(this.viewportTransform); if (!this.isTargetTransparent(obj, viewportPoint.x, viewportPoint.y)) return true; } else return true; return false; } /** * Given an array of objects search possible targets under the pointer position * Returns an * @param {Array} objects objects array to look into * @param {Object} pointer x,y object of point of scene coordinates we want to check. * @param {Object} subTargets If passed, subtargets will be collected inside the array * @return {TargetsInfo} **top most object from given `objects`** that contains pointer * @private */ _searchPossibleTargets(objects, pointer, subTargets) { let i = objects.length; while (i--) { const target = objects[i]; if (this._checkTarget(target, pointer)) { if (isCollection(target) && target.subTargetCheck) { const { target: subTarget } = this._searchPossibleTargets(target._objects, pointer, subTargets); subTarget && subTargets.push(subTarget); } return { target, subTargets }; } } return { subTargets: [] }; } /** * Search inside an objects array the fiurst object that contains pointer * Collect subTargets of that object inside the subTargets array passed as parameter * @param {FabricObject[]} objects objects array to look into * @param {Point} pointer coordinates from viewport to check. * @return {FabricObject} **top most object on screen** that contains pointer */ searchPossibleTargets(objects, pointer) { const targetInfo = this._searchPossibleTargets(objects, pointer, []); targetInfo.container = targetInfo.target; const { container, subTargets } = targetInfo; if (container && isCollection(container) && container.interactive && subTargets[0]) { /** subTargets[0] is the innermost nested target, but it could be inside non interactive groups * and so not a possible selection target. * We loop the array from the end that is outermost innertarget. */ for (let i = subTargets.length - 1; i > 0; i--) { const t = subTargets[i]; if (!(isCollection(t) && t.interactive)) { targetInfo.target = t; return targetInfo; } } targetInfo.target = subTargets[0]; return targetInfo; } return targetInfo; } /** * @returns point existing in the same plane as the {@link HTMLCanvasElement}, * `(0, 0)` being the top left corner of the {@link HTMLCanvasElement}. * This means that changes to the {@link viewportTransform} do not change the values of the point * and it remains unchanged from the viewer's perspective. * * @example * const scenePoint = sendPointToPlane( * this.getViewportPoint(e), * undefined, * canvas.viewportTransform * ); * */ getViewportPoint(e) { if (this._viewportPoint) return this._viewportPoint; return this._getPointerImpl(e, true); } /** * @returns point existing in the scene (the same plane as the plane {@link FabricObject#getCenterPoint} exists in). * This means that changes to the {@link viewportTransform} do not change the values of the point, * however, from the viewer's perspective, the point is changed. * * @example * const viewportPoint = sendPointToPlane( * this.getScenePoint(e), * canvas.viewportTransform * ); * */ getScenePoint(e) { if (this._scenePoint) return this._scenePoint; return this._getPointerImpl(e); } /** * Returns pointer relative to canvas. * * Use {@link getViewportPoint} or {@link getScenePoint} instead. * * @param {Event} e * @param {Boolean} [fromViewport] whether to return the point from the viewport or in the scene * @return {Point} */ _getPointerImpl(e, fromViewport = false) { const upperCanvasEl = this.upperCanvasEl, bounds = upperCanvasEl.getBoundingClientRect(); let pointer = getPointer(e), boundsWidth = bounds.width || 0, boundsHeight = bounds.height || 0; if (!boundsWidth || !boundsHeight) { if ("top" in bounds && "bottom" in bounds) boundsHeight = Math.abs(bounds.top - bounds.bottom); if ("right" in bounds && "left" in bounds) boundsWidth = Math.abs(bounds.right - bounds.left); } this.calcOffset(); pointer.x = pointer.x - this._offset.left; pointer.y = pointer.y - this._offset.top; if (!fromViewport) pointer = sendPointToPlane(pointer, void 0, this.viewportTransform); const retinaScaling = this.getRetinaScaling(); if (retinaScaling !== 1) { pointer.x /= retinaScaling; pointer.y /= retinaScaling; } const cssScale = boundsWidth === 0 || boundsHeight === 0 ? new Point(1, 1) : new Point(upperCanvasEl.width / boundsWidth, upperCanvasEl.height / boundsHeight); return pointer.multiply(cssScale); } /** * Internal use only * @protected */ _setDimensionsImpl(dimensions, options) { this._resetTransformEventData(); super._setDimensionsImpl(dimensions, options); if (this._isCurrentlyDrawing) this.freeDrawingBrush && this.freeDrawingBrush._setBrushStyles(this.contextTop); } _createCacheCanvas() { this.pixelFindCanvasEl = createCanvasElement(); this.pixelFindContext = this.pixelFindCanvasEl.getContext("2d", { willReadFrequently: true }); this.setTargetFindTolerance(this.targetFindTolerance); } /** * Returns context of top canvas where interactions are drawn * @returns {CanvasRenderingContext2D} */ getTopContext() { return this.elements.upper.ctx; } /** * Returns context of canvas where object selection is drawn * @alias * @return {CanvasRenderingContext2D} */ getSelectionContext() { return this.elements.upper.ctx; } /** * Returns <canvas> element on which object selection is drawn * @return {HTMLCanvasElement} */ getSelectionElement() { return this.elements.upper.el; } /** * Returns currently active object * @return {FabricObject | null} active object */ getActiveObject() { return this._activeObject; } /** * Returns an array with the current selected objects * @return {FabricObject[]} active objects array */ getActiveObjects() { const active = this._activeObject; return isActiveSelection(active) ? active.getObjects() : active ? [active] : []; } /** * @private * Compares the old activeObject with the current one and fires correct events * @param {FabricObject[]} oldObjects old activeObject * @param {TPointerEvent} e mouse event triggering the selection events */ _fireSelectionEvents(oldObjects, e) { let somethingChanged = false, invalidate = false; const objects = this.getActiveObjects(), added = [], removed = []; oldObjects.forEach((target) => { if (!objects.includes(target)) { somethingChanged = true; target.fire("deselected", { e, target }); removed.push(target); } }); objects.forEach((target) => { if (!oldObjects.includes(target)) { somethingChanged = true; target.fire("selected", { e, target }); added.push(target); } }); if (oldObjects.length > 0 && objects.length > 0) { invalidate = true; somethingChanged && this.fire("selection:updated", { e, selected: added, deselected: removed }); } else if (objects.length > 0) { invalidate = true; this.fire("selection:created", { e, selected: added }); } else if (oldObjects.length > 0) { invalidate = true; this.fire("selection:cleared", { e, deselected: removed }); } invalidate && (this._objectsToRender = void 0); } /** * Sets given object as the only active object on canvas * @param {FabricObject} object Object to set as an active one * @param {TPointerEvent} [e] Event (passed along when firing "object:selected") * @return {Boolean} true if the object has been selected */ setActiveObject(object, e) { const currentActives = this.getActiveObjects(); const selected = this._setActiveObject(object, e); this._fireSelectionEvents(currentActives, e); return selected; } /** * This is supposed to be equivalent to setActiveObject but without firing * any event. There is commitment to have this stay this way. * This is the functional part of setActiveObject. * @param {Object} object to set as active * @param {Event} [e] Event (passed along when firing "object:selected") * @return {Boolean} true if the object has been selected */ _setActiveObject(object, e) { const prevActiveObject = this._activeObject; if (prevActiveObject === object) return false; if (!this._discardActiveObject(e, object) && this._activeObject) return false; if (object.onSelect({ e })) return false; this._activeObject = object; if (isActiveSelection(object) && prevActiveObject !== object) object.set("canvas", this); object.setCoords(); return true; } /** * This is supposed to be equivalent to discardActiveObject but without firing * any selection events ( can still fire object transformation events ). There is commitment to have this stay this way. * This is the functional part of discardActiveObject. * @param {Event} [e] Event (passed along when firing "object:deselected") * @param {Object} object the next object to set as active, reason why we are discarding this * @return {Boolean} true if the active object has been discarded */ _discardActiveObject(e, object) { const obj = this._activeObject; if (obj) { if (obj.onDeselect({ e, object })) return false; if (this._currentTransform && this._currentTransform.target === obj) this.endCurrentTransform(e); if (isActiveSelection(obj) && obj === this._hoveredTarget) this._hoveredTarget = void 0; this._activeObject = void 0; return true; } return false; } /** * Discards currently active object and fire events. If the function is called by fabric * as a consequence of a mouse event, the event is passed as a parameter and * sent to the fire function for the custom events. When used as a method the * e param does not have any application. * @param {event} e * @return {Boolean} true if the active object has been discarded */ discardActiveObject(e) { const currentActives = this.getActiveObjects(), activeObject = this.getActiveObject(); if (currentActives.length) this.fire("before:selection:cleared", { e, deselected: [activeObject] }); const discarded = this._discardActiveObject(e); this._fireSelectionEvents(currentActives, e); return discarded; } /** * End the current transform. * You don't usually need to call this method unless you are interrupting a user initiated transform * because of some other event ( a press of key combination, or something that block the user UX ) * @param {Event} [e] send the mouse event that generate the finalize down, so it can be used in the event */ endCurrentTransform(e) { const transform = this._currentTransform; this._finalizeCurrentTransform(e); if (transform && transform.target) transform.target.isMoving = false; this._currentTransform = null; } /** * @private * @param {Event} e send the mouse event that generate the finalize down, so it can be used in the event */ _finalizeCurrentTransform(e) { const transform = this._currentTransform, target = transform.target, options = { e, target, transform, action: transform.action }; if (target._scaling) target._scaling = false; target.setCoords(); if (transform.actionPerformed) { this.fire("object:modified", options); target.fire(MODIFIED, options); } } /** * Sets viewport transformation of this canvas instance * @param {Array} vpt a Canvas 2D API transform matrix */ setViewportTransform(vpt) { super.setViewportTransform(vpt); const activeObject = this._activeObject; if (activeObject) activeObject.setCoords(); } /** * @override clears active selection ref and interactive canvas elements and contexts */ destroy() { const activeObject = this._activeObject; if (isActiveSelection(activeObject)) { activeObject.removeAll(); activeObject.dispose(); } delete this._activeObject; super.destroy(); this.pixelFindContext = null; this.pixelFindCanvasEl = void 0; } /** * Clears all contexts (background, main, top) of an instance */ clear() { this.discardActiveObject(); this._activeObject = void 0; this.clearContext(this.contextTop); super.clear(); } /** * Draws objects' controls (borders/controls) * @param {CanvasRenderingContext2D} ctx Context to render controls on */ drawControls(ctx) { const activeObject = this._activeObject; if (activeObject) activeObject._renderControls(ctx); } /** * @private */ _toObject(instance, methodName, propertiesToInclude) { const originalProperties = this._realizeGroupTransformOnObject(instance), object = super._toObject(instance, methodName, propertiesToInclude); instance.set(originalProperties); return object; } /** * Realizes an object's group transformation on it * @private * @param {FabricObject} [instance] the object to transform (gets mutated) * @returns the original values of instance which were changed */ _realizeGroupTransformOnObject(instance) { const { group } = instance; if (group && isActiveSelection(group) && this._activeObject === group) { const originalValues = pick(instance, [ "angle", "flipX", "flipY", LEFT, SCALE_X, SCALE_Y, SKEW_X, SKEW_Y, "top" ]); addTransformToObject(instance, group.calcOwnMatrix()); return originalValues; } else return {}; } /** * @private */ _setSVGObject(markup, instance, reviver) { const originalProperties = this._realizeGroupTransformOnObject(instance); super._setSVGObject(markup, instance, reviver); instance.set(originalProperties); } }; _defineProperty(SelectableCanvas, "ownDefaults", canvasDefaults); //#endregion export { SelectableCanvas }; //# sourceMappingURL=SelectableCanvas.mjs.map