UNPKG

fabric

Version:

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

1,389 lines (1,282 loc) 43.6 kB
import { dragHandler } from '../controls/drag'; import { getActionFromCorner } from '../controls/util'; import { Point } from '../Point'; import { FabricObject } from '../shapes/Object/FabricObject'; import type { CanvasEvents, ModifierKey, TOptionalModifierKey, TPointerEvent, Transform, } from '../EventTypeDefs'; import { addTransformToObject, saveObjectTransform, } from '../util/misc/objectTransforms'; import type { TCanvasSizeOptions } from './StaticCanvas'; import { StaticCanvas } from './StaticCanvas'; import { isCollection } from '../Collection'; import { isTransparent } from '../util/misc/isTransparent'; import type { TMat2D, TOriginX, TOriginY, TSize, TSVGReviver, } from '../typedefs'; import { degreesToRadians } from '../util/misc/radiansDegreesConversion'; import { getPointer, isTouchEvent } from '../util/dom_event'; import type { IText } from '../shapes/IText/IText'; import type { BaseBrush } from '../brushes/BaseBrush'; import { pick } from '../util/misc/pick'; import { sendPointToPlane } from '../util/misc/planeChange'; import { cos, createCanvasElement, sin } from '../util'; import { CanvasDOMManager } from './DOMManagers/CanvasDOMManager'; import { BOTTOM, CENTER, LEFT, MODIFIED, RESIZING, RIGHT, ROTATE, SCALE, SCALE_X, SCALE_Y, SKEW_X, SKEW_Y, TOP, } from '../constants'; import type { CanvasOptions } from './CanvasOptions'; import { canvasDefaults } from './CanvasOptions'; import { Intersection } from '../Intersection'; import { isActiveSelection } from '../util/typeAssertions'; /** * Canvas class * @class Canvas * @extends StaticCanvas * @tutorial {@link http://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! * } * }); * */ export class SelectableCanvas<EventSpec extends CanvasEvents = CanvasEvents> extends StaticCanvas<EventSpec> implements Omit<CanvasOptions, 'enablePointerEvents'> { declare _objects: FabricObject[]; // transform config declare uniformScaling: boolean; declare uniScaleKey: TOptionalModifierKey; declare centeredScaling: boolean; declare centeredRotation: boolean; declare centeredKey: TOptionalModifierKey; declare altActionKey: TOptionalModifierKey; // selection config declare selection: boolean; declare selectionKey: TOptionalModifierKey | ModifierKey[]; declare altSelectionKey: TOptionalModifierKey; declare selectionColor: string; declare selectionDashArray: number[]; declare selectionBorderColor: string; declare selectionLineWidth: number; declare selectionFullyContained: boolean; // cursors declare hoverCursor: CSSStyleDeclaration['cursor']; declare moveCursor: CSSStyleDeclaration['cursor']; declare defaultCursor: CSSStyleDeclaration['cursor']; declare freeDrawingCursor: CSSStyleDeclaration['cursor']; declare notAllowedCursor: CSSStyleDeclaration['cursor']; declare containerClass: string; // target find config declare perPixelTargetFind: boolean; declare targetFindTolerance: number; declare skipTargetFind: boolean; /** * When true, mouse events on canvas (mousedown/mousemove/mouseup) result in free drawing. * After mousedown, mousemove creates a shape, * and then mouseup finalizes it and adds an instance of `fabric.Path` onto canvas. * @tutorial {@link http://fabricjs.com/fabric-intro-part-4#free_drawing} * @type Boolean * @default */ declare isDrawingMode: boolean; declare preserveObjectStacking: boolean; // event config declare stopContextMenu: boolean; declare fireRightClick: boolean; declare fireMiddleClick: boolean; /** * Keep track of the subTargets for Mouse Events, ordered bottom up from innermost nested subTarget * @type FabricObject[] */ targets: FabricObject[] = []; /** * Keep track of the hovered target * @type FabricObject | null * @private */ declare _hoveredTarget?: FabricObject; /** * hold the list of nested targets hovered * @type FabricObject[] * @private */ _hoveredTargets: FabricObject[] = []; /** * hold the list of objects to render * @type FabricObject[] * @private */ _objectsToRender?: FabricObject[]; /** * hold a reference to a data structure that contains information * on the current on going transform * @type * @private */ _currentTransform: Transform | null = null; /** * hold a reference to a data structure used to track the selection * box on canvas drag * on the current on going transform * x, y, deltaX and deltaY are in scene plane * @type * @private */ protected _groupSelector: { x: number; y: number; deltaX: number; deltaY: number; } | null = null; /** * 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; /** * During a mouse event we may need the pointer multiple times in multiple functions. * _absolutePointer holds a reference to the pointer in fabricCanvas/design coordinates that is valid for the event * lifespan. Every fabricJS mouse event create and delete the cache every time * We do this because there are some HTML DOM inspection functions to get the actual pointer coordinates * @type {Point} */ protected declare _absolutePointer?: Point; /** * During a mouse event we may need the pointer multiple times in multiple functions. * _pointer holds a reference to the pointer in html coordinates that is valid for the event * lifespan. Every fabricJS mouse event create and delete the cache every time * We do this because there are some HTML DOM inspection functions to get the actual pointer coordinates * @type {Point} */ protected declare _pointer?: Point; /** * During a mouse event we may need the target multiple times in multiple functions. * _target holds a reference to the target that is valid for the event * lifespan. Every fabricJS mouse event create and delete the cache every time * @type {FabricObject} */ protected declare _target?: FabricObject; static ownDefaults = canvasDefaults; static getDefaults(): Record<string, any> { return { ...super.getDefaults(), ...SelectableCanvas.ownDefaults }; } declare elements: CanvasDOMManager; get upperCanvasEl() { return this.elements.upper?.el; } get contextTop() { return this.elements.upper?.ctx; } get wrapperEl() { return this.elements.container; } private declare pixelFindCanvasEl: HTMLCanvasElement; private declare pixelFindContext: CanvasRenderingContext2D; protected declare _isCurrentlyDrawing: boolean; declare freeDrawingBrush?: BaseBrush; declare _activeObject?: FabricObject; protected initElements(el?: string | HTMLCanvasElement) { this.elements = new CanvasDOMManager(el, { allowTouchScrolling: this.allowTouchScrolling, containerClass: this.containerClass, }); this._createCacheCanvas(); } /** * @private * @param {FabricObject} obj Object that was added */ _onObjectAdded(obj: FabricObject) { this._objectsToRender = undefined; super._onObjectAdded(obj); } /** * @private * @param {FabricObject} obj Object that was removed */ _onObjectRemoved(obj: FabricObject) { this._objectsToRender = undefined; // removing active object should fire "selection:cleared" events 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 = undefined; this._hoveredTargets = []; } super._onObjectRemoved(obj); } _onStackOrderChanged() { this._objectsToRender = undefined; 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(): FabricObject[] { 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: CanvasRenderingContext2D): void { ctx.save(); if (this.isDrawingMode && this._isCurrentlyDrawing) { this.freeDrawingBrush && this.freeDrawingBrush._render(); this.contextTopDirty = true; } // we render the top context - last object 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); // todo: how do i know if the after:render is for the top or normal contex? this.fire('after:render', { ctx }); } /** * Set the canvas tolerance value for pixel taret find. * Use only integer numbers. * @private */ setTargetFindTolerance(value: number) { 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: FabricObject, x: number, y: number): boolean { 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(); // our canvas is square, and made around tolerance. // so tolerance in this case also represent the center of the canvas. 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: TPointerEvent): boolean { 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: TPointerEvent, target?: FabricObject, ): target is undefined { 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 */ private _shouldCenterTransform( target: FabricObject, action: string, modifierKeyPressed: boolean, ) { if (!target) { return; } let centerTransform; if ( action === SCALE || action === SCALE_X || action === SCALE_Y || 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 * @private * @deprecated */ _getOriginFromCorner( target: FabricObject, controlName: string, ): { x: TOriginX; y: TOriginY } { const origin = { x: target.originX, y: target.originY, }; if (!controlName) { return origin; } // is a left control ? if (['ml', 'tl', 'bl'].includes(controlName)) { origin.x = RIGHT; // is a right control ? } else if (['mr', 'tr', 'br'].includes(controlName)) { origin.x = LEFT; } // is a top control ? if (['tl', 'mt', 'tr'].includes(controlName)) { origin.y = BOTTOM; // is a bottom control ? } 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: TPointerEvent, target: FabricObject, alreadySelected: boolean, ): void { const pointer = target.group ? // transform pointer to target's containing coordinate plane sendPointToPlane( this.getScenePoint(e), undefined, target.group.calcTransformMatrix(), ) : this.getScenePoint(e); const { key: corner = '', control } = target.getActiveControl() || {}, actionHandler = alreadySelected && control ? control.getActionHandler(e, target, control)?.bind(control) : dragHandler, action = getActionFromCorner(alreadySelected, corner, e, target), altKey = e[this.centeredKey as ModifierKey], origin = this._shouldCenterTransform(target, action, altKey) ? ({ x: CENTER, y: CENTER } as const) : this._getOriginFromCorner(target, corner), /** * relative to target's containing coordinate plane * both agree on every point **/ transform: Transform = { target: target, action, actionHandler, actionPerformed: false, corner, scaleX: target.scaleX, scaleY: target.scaleY, skewX: target.skewX, skewY: target.skewY, offsetX: pointer.x - target.left, offsetY: pointer.y - target.top, originX: origin.x, originY: origin.y, ex: pointer.x, ey: pointer.y, lastX: pointer.x, lastY: pointer.y, theta: degreesToRadians(target.angle), width: target.width, height: target.height, shiftKey: e.shiftKey, altKey, original: { ...saveObjectTransform(target), originX: origin.x, originY: origin.y, }, }; 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: CSSStyleDeclaration['cursor']): void { this.upperCanvasEl.style.cursor = value; } /** * @private * @param {CanvasRenderingContext2D} ctx to draw the selection on */ _drawSelection(ctx: CanvasRenderingContext2D): void { 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; // selection border // @TODO: is _setLineDash still necessary on modern canvas? FabricObject.prototype._setLineDash.call( this, ctx, this.selectionDashArray, ); ctx.strokeRect(minX, minY, maxX - minX, maxY - minY); } /** * Method that determines what object we are clicking on * 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 {FabricObject | null} the target found */ findTarget(e: TPointerEvent): FabricObject | undefined { if (this.skipTargetFind) { return undefined; } const pointer = this.getViewportPoint(e), activeObject = this._activeObject, aObjects = this.getActiveObjects(); this.targets = []; if (activeObject && aObjects.length >= 1) { if (activeObject.findControl(pointer, isTouchEvent(e))) { // if we hit the corner of the active object, let's return that. return activeObject; } else if ( aObjects.length > 1 && // check pointer is over active selection and possibly perform `subTargetCheck` this.searchPossibleTargets([activeObject], pointer) ) { // active selection does not select sub targets like normal groups return activeObject; } else if ( activeObject === this.searchPossibleTargets([activeObject], pointer) ) { // active object is not an active selection if (!this.preserveObjectStacking) { return activeObject; } else { const subTargets = this.targets; this.targets = []; const target = this.searchPossibleTargets(this._objects, pointer); if ( e[this.altSelectionKey as ModifierKey] && target && target !== activeObject ) { // alt selection: select active object even though it is not the top most target // restore targets this.targets = subTargets; return activeObject; } return target; } } } return this.searchPossibleTargets(this._objects, pointer); } /** * 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 */ private _pointIsInObjectSelectionArea(obj: FabricObject, point: Point) { // getCoords will already take care of group de-nesting let coords = obj.getCoords(); const viewportZoom = this.getZoom(); const padding = obj.padding / viewportZoom; if (padding) { const [tl, tr, br, bl] = coords; // what is the angle of the object? // we could use getTotalAngle, but is way easier to look at it // from how coords are oriented, since if something went wrong // at least we are consistent. 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), ]; // in case of padding we calculate the new coords on the fly. // otherwise we have to maintain 2 sets of coordinates for everything. // we can reiterate on storing them. // if this is slow, for now the semplification is large and doesn't impact // rendering. // the idea behind this is that outside target check we don't need ot know // where those coords are } 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 {Object} [pointer] point from viewport. * @return {Boolean} true if point is contained within an area of given object * @private */ _checkTarget(obj: FabricObject, pointer: Point): boolean { if ( obj && obj.visible && obj.evented && this._pointIsInObjectSelectionArea( obj, sendPointToPlane(pointer, undefined, this.viewportTransform), ) ) { if ( (this.perPixelTargetFind || obj.perPixelTargetFind) && !(obj as unknown as IText).isEditing ) { if (!this.isTargetTransparent(obj, pointer.x, pointer.y)) { return true; } } else { return true; } } return false; } /** * Internal Function used to search inside objects an object that contains pointer in bounding box or that contains pointerOnCanvas when painted * @param {Array} [objects] objects array to look into * @param {Object} [pointer] x,y object of point coordinates we want to check. * @return {FabricObject} **top most object from given `objects`** that contains pointer * @private */ _searchPossibleTargets( objects: FabricObject[], pointer: Point, ): FabricObject | undefined { // Cache all targets where their bounding box contains point. let i = objects.length; // Do not check for currently grouped objects, since we check the parent group itself. // until we call this function specifically to search inside the activeGroup while (i--) { const target = objects[i]; if (this._checkTarget(target, pointer)) { if (isCollection(target) && target.subTargetCheck) { const subTarget = this._searchPossibleTargets( target._objects as FabricObject[], pointer, ); subTarget && this.targets.push(subTarget); } return target; } } } /** * Function used to search inside objects an object that contains pointer in bounding box or that contains pointerOnCanvas when painted * @see {@link _searchPossibleTargets} * @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: FabricObject[], pointer: Point, ): FabricObject | undefined { const target = this._searchPossibleTargets(objects, pointer); // if we found something in this.targets, and the group is interactive, return the innermost subTarget // that is still interactive // TODO: reverify why interactive. the target should be returned always, but selected only // if interactive. if ( target && isCollection(target) && target.interactive && this.targets[0] ) { /** targets[0] is the innermost nested target, but it could be inside non interactive groups and so not a selection target */ const targets = this.targets; for (let i = targets.length - 1; i > 0; i--) { const t = targets[i]; if (!(isCollection(t) && t.interactive)) { // one of the subtargets was not interactive. that is the last subtarget we can return. // we can't dig more deep; return t; } } return targets[0]; } return target; } /** * @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: TPointerEvent) { if (this._pointer) { return this._pointer; } return this.getPointer(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: TPointerEvent) { if (this._absolutePointer) { return this._absolutePointer; } return this.getPointer(e); } /** * Returns pointer relative to canvas. * * @deprecated This method is deprecated since v6 to protect you from misuse. * 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} */ getPointer(e: TPointerEvent, fromViewport = false): Point { 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, undefined, this.viewportTransform); } const retinaScaling = this.getRetinaScaling(); if (retinaScaling !== 1) { pointer.x /= retinaScaling; pointer.y /= retinaScaling; } // If bounds are not available (i.e. not visible), do not apply scale. 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 */ protected _setDimensionsImpl( dimensions: TSize, options?: TCanvasSizeOptions, ) { // @ts-expect-error this method exists in the subclass - should be moved or declared as abstract this._resetTransformEventData(); super._setDimensionsImpl(dimensions, options); if (this._isCurrentlyDrawing) { this.freeDrawingBrush && this.freeDrawingBrush._setBrushStyles(this.contextTop); } } protected _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(): CanvasRenderingContext2D { return this.elements.upper.ctx; } /** * Returns context of canvas where object selection is drawn * @alias * @return {CanvasRenderingContext2D} */ getSelectionContext(): CanvasRenderingContext2D { return this.elements.upper.ctx; } /** * Returns &lt;canvas> element on which object selection is drawn * @return {HTMLCanvasElement} */ getSelectionElement(): HTMLCanvasElement { return this.elements.upper.el; } /** * Returns currently active object * @return {FabricObject | null} active object */ getActiveObject(): FabricObject | undefined { return this._activeObject; } /** * Returns an array with the current selected objects * @return {FabricObject[]} active objects array */ getActiveObjects(): FabricObject[] { 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: FabricObject[], e?: TPointerEvent) { let somethingChanged = false, invalidate = false; const objects = this.getActiveObjects(), added: FabricObject[] = [], removed: FabricObject[] = []; 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 = undefined); } /** * 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: FabricObject, e?: TPointerEvent) { // we can't inline this, since _setActiveObject will change what getActiveObjects returns 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: FabricObject, e?: TPointerEvent) { const prevActiveObject = this._activeObject; if (prevActiveObject === object) { return false; } // after calling this._discardActiveObject, this,_activeObject could be undefined if (!this._discardActiveObject(e, object) && this._activeObject) { // refused to deselect 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?: TPointerEvent, object?: FabricObject, ): this is { _activeObject: undefined } { const obj = this._activeObject; if (obj) { // onDeselect return TRUE to cancel selection; 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 = undefined; } this._activeObject = undefined; 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?: TPointerEvent): this is { _activeObject: undefined } { 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?: TPointerEvent) { const transform = this._currentTransform; this._finalizeCurrentTransform(e); if (transform && transform.target) { // this could probably go inside _finalizeCurrentTransform 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?: TPointerEvent) { 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: TMat2D) { super.setViewportTransform(vpt); const activeObject = this._activeObject; if (activeObject) { activeObject.setCoords(); } } /** * @override clears active selection ref and interactive canvas elements and contexts */ destroy() { // dispose of active selection const activeObject = this._activeObject; if (isActiveSelection(activeObject)) { activeObject.removeAll(); activeObject.dispose(); } delete this._activeObject; super.destroy(); // free resources // pixel find canvas // @ts-expect-error disposing this.pixelFindContext = null; // @ts-expect-error disposing this.pixelFindCanvasEl = undefined; } /** * Clears all contexts (background, main, top) of an instance */ clear() { // discard active object and fire events this.discardActiveObject(); // make sure we clear the active object in case it refused to be discarded this._activeObject = undefined; this.clearContext(this.contextTop); super.clear(); } /** * Draws objects' controls (borders/controls) * @param {CanvasRenderingContext2D} ctx Context to render controls on */ drawControls(ctx: CanvasRenderingContext2D) { const activeObject = this._activeObject; if (activeObject) { activeObject._renderControls(ctx); } } /** * @private */ protected _toObject( instance: FabricObject, methodName: 'toObject' | 'toDatalessObject', propertiesToInclude: string[], ): Record<string, any> { // If the object is part of the current selection group, it should // be transformed appropriately // i.e. it should be serialised as it would appear if the selection group // were to be destroyed. const originalProperties = this._realizeGroupTransformOnObject(instance), object = super._toObject(instance, methodName, propertiesToInclude); //Undo the damage we did by changing all of its properties 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 */ private _realizeGroupTransformOnObject( instance: FabricObject, ): Partial<typeof instance> { const { group } = instance; if (group && isActiveSelection(group) && this._activeObject === group) { const layoutProps = [ 'angle', 'flipX', 'flipY', LEFT, SCALE_X, SCALE_Y, SKEW_X, SKEW_Y, TOP, ] as (keyof typeof instance)[]; const originalValues = pick<typeof instance>(instance, layoutProps); addTransformToObject(instance, group.calcOwnMatrix()); return originalValues; } else { return {}; } } /** * @private */ _setSVGObject( markup: string[], instance: FabricObject, reviver?: TSVGReviver, ) { // If the object is in a selection group, simulate what would happen to that // object when the group is deselected const originalProperties = this._realizeGroupTransformOnObject(instance); super._setSVGObject(markup, instance, reviver); instance.set(originalProperties); } }