UNPKG

fabric

Version:

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

1 lines 109 kB
{"version":3,"file":"Object.mjs","sources":["../../../../src/shapes/Object/Object.ts"],"sourcesContent":["import { cache } from '../../cache';\nimport { config } from '../../config';\nimport {\n ALIASING_LIMIT,\n CENTER,\n iMatrix,\n LEFT,\n SCALE_X,\n SCALE_Y,\n STROKE,\n FILL,\n TOP,\n VERSION,\n} from '../../constants';\nimport type { ObjectEvents } from '../../EventTypeDefs';\nimport { Point } from '../../Point';\nimport { Shadow } from '../../Shadow';\nimport type {\n TDegree,\n TFiller,\n TSize,\n TCacheCanvasDimensions,\n Abortable,\n TOptions,\n ImageFormat,\n} from '../../typedefs';\nimport { classRegistry } from '../../ClassRegistry';\nimport { runningAnimations } from '../../util/animation/AnimationRegistry';\nimport { capValue } from '../../util/misc/capValue';\nimport {\n createCanvasElement,\n createCanvasElementFor,\n toDataURL,\n toBlob,\n} from '../../util/misc/dom';\nimport { invertTransform, qrDecompose } from '../../util/misc/matrix';\nimport { enlivenObjectEnlivables } from '../../util/misc/objectEnlive';\nimport {\n resetObjectTransform,\n saveObjectTransform,\n} from '../../util/misc/objectTransforms';\nimport { sendObjectToPlane } from '../../util/misc/planeChange';\nimport { pick, pickBy } from '../../util/misc/pick';\nimport { toFixed } from '../../util/misc/toFixed';\nimport type { Group } from '../Group';\nimport { StaticCanvas } from '../../canvas/StaticCanvas';\nimport { isFiller, isSerializableFiller } from '../../util/typeAssertions';\nimport type { FabricImage } from '../Image';\nimport {\n cacheProperties,\n fabricObjectDefaultValues,\n stateProperties,\n} from './defaultValues';\nimport type { Gradient } from '../../gradient/Gradient';\nimport type { Pattern } from '../../Pattern';\nimport type { Canvas } from '../../canvas/Canvas';\nimport type { SerializedObjectProps } from './types/SerializedObjectProps';\nimport type { ObjectProps } from './types/ObjectProps';\nimport { getDevicePixelRatio, getEnv } from '../../env';\nimport { log } from '../../util/internals/console';\nimport type { TColorArg } from '../../color/typedefs';\nimport type { TAnimation } from '../../util/animation/animate';\nimport { animate, animateColor } from '../../util/animation/animate';\nimport type {\n AnimationOptions,\n ArrayAnimationOptions,\n ColorAnimationOptions,\n ValueAnimationOptions,\n} from '../../util/animation/types';\nimport { ObjectGeometry } from './ObjectGeometry';\n\ntype TAncestor = FabricObject;\ntype TCollection = Group;\n\nexport type Ancestors =\n | [FabricObject | Group]\n | [FabricObject | Group, ...Group[]]\n | Group[];\n\nexport type AncestryComparison = {\n /**\n * common ancestors of `this` and`other`(may include`this` | `other`)\n */\n common: Ancestors;\n /**\n * ancestors that are of `this` only\n */\n fork: Ancestors;\n /**\n * ancestors that are of `other` only\n */\n otherFork: Ancestors;\n};\n\nexport type TCachedFabricObject<T extends FabricObject = FabricObject> = T &\n Required<\n Pick<\n T,\n | 'zoomX'\n | 'zoomY'\n | '_cacheCanvas'\n | '_cacheContext'\n | 'cacheTranslationX'\n | 'cacheTranslationY'\n >\n > & {\n _cacheContext: CanvasRenderingContext2D;\n };\n\nexport type ObjectToCanvasElementOptions = {\n format?: ImageFormat;\n /** Multiplier to scale by */\n multiplier?: number;\n /** Cropping left offset. Introduced in v1.2.14 */\n left?: number;\n /** Cropping top offset. Introduced in v1.2.14 */\n top?: number;\n /** Cropping width. Introduced in v1.2.14 */\n width?: number;\n /** Cropping height. Introduced in v1.2.14 */\n height?: number;\n /** Enable retina scaling for clone image. Introduce in 1.6.4 */\n enableRetinaScaling?: boolean;\n /** Remove current object transform ( no scale , no angle, no flip, no skew ). Introduced in 2.3.4 */\n withoutTransform?: boolean;\n /** Remove current object shadow. Introduced in 2.4.2 */\n withoutShadow?: boolean;\n /** Account for canvas viewport transform */\n viewportTransform?: boolean;\n /** Function to create the output canvas to export onto */\n canvasProvider?: <T extends StaticCanvas>(el?: HTMLCanvasElement) => T;\n};\n\ntype toDataURLOptions = ObjectToCanvasElementOptions & {\n quality?: number;\n};\n\nexport type DrawContext =\n | {\n parentClipPaths: FabricObject[];\n width: number;\n height: number;\n cacheTranslationX: number;\n cacheTranslationY: number;\n zoomX: number;\n zoomY: number;\n }\n | Record<string, never>;\n\n/**\n * Root object class from which all 2d shape classes inherit from\n * @tutorial {@link http://fabricjs.com/fabric-intro-part-1#objects}\n *\n * @fires added\n * @fires removed\n *\n * @fires selected\n * @fires deselected\n *\n * @fires rotating\n * @fires scaling\n * @fires moving\n * @fires skewing\n * @fires modified\n *\n * @fires mousedown\n * @fires mouseup\n * @fires mouseover\n * @fires mouseout\n * @fires mousewheel\n * @fires mousedblclick\n *\n * @fires dragover\n * @fires dragenter\n * @fires dragleave\n * @fires drop\n */\nexport class FabricObject<\n Props extends TOptions<ObjectProps> = Partial<ObjectProps>,\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n SProps extends SerializedObjectProps = SerializedObjectProps,\n EventSpec extends ObjectEvents = ObjectEvents,\n >\n extends ObjectGeometry<EventSpec>\n implements ObjectProps\n{\n declare minScaleLimit: number;\n\n declare opacity: number;\n\n declare paintFirst: 'fill' | 'stroke';\n declare fill: string | TFiller | null;\n declare fillRule: CanvasFillRule;\n declare stroke: string | TFiller | null;\n declare strokeDashArray: number[] | null;\n declare strokeDashOffset: number;\n declare strokeLineCap: CanvasLineCap;\n declare strokeLineJoin: CanvasLineJoin;\n declare strokeMiterLimit: number;\n\n declare globalCompositeOperation: GlobalCompositeOperation;\n declare backgroundColor: string;\n\n declare shadow: Shadow | null;\n\n declare visible: boolean;\n\n declare includeDefaultValues: boolean;\n declare excludeFromExport: boolean;\n\n declare objectCaching: boolean;\n\n declare clipPath?: FabricObject;\n declare inverted: boolean;\n declare absolutePositioned: boolean;\n declare centeredRotation: boolean;\n declare centeredScaling: boolean;\n\n /**\n * This list of properties is used to check if the state of an object is changed.\n * This state change now is only used for children of groups to understand if a group\n * needs its cache regenerated during a .set call\n * @type Array\n */\n static stateProperties: string[] = stateProperties;\n\n /**\n * List of properties to consider when checking if cache needs refresh\n * Those properties are checked by\n * calls to Object.set(key, value). If the key is in this list, the object is marked as dirty\n * and refreshed at the next render\n * @type Array\n */\n static cacheProperties: string[] = cacheProperties;\n\n /**\n * When set to `true`, object's cache will be rerendered next render call.\n * since 1.7.0\n * @type Boolean\n * @default true\n */\n declare dirty: boolean;\n\n /**\n * Quick access for the _cacheCanvas rendering context\n * This is part of the objectCaching feature\n * since 1.7.0\n * @type boolean\n * @default undefined\n * @private\n */\n _cacheContext: CanvasRenderingContext2D | null = null;\n\n /**\n * A reference to the HTMLCanvasElement that is used to contain the cache of the object\n * this canvas element is resized and cleared as needed\n * Is marked private, you can read it, don't use it since it is handled by fabric\n * since 1.7.0\n * @type HTMLCanvasElement\n * @default undefined\n * @private\n */\n declare _cacheCanvas?: HTMLCanvasElement;\n\n /**\n * zoom level used on the cacheCanvas to draw the cache, X axe\n * since 1.7.0\n * @type number\n * @default undefined\n * @private\n */\n declare zoomX?: number;\n\n /**\n * zoom level used on the cacheCanvas to draw the cache, Y axe\n * since 1.7.0\n * @type number\n * @default undefined\n * @private\n */\n declare zoomY?: number;\n\n /**\n * zoom level used on the cacheCanvas to draw the cache, Y axe\n * since 1.7.0\n * @type number\n * @default undefined\n * @private\n */\n declare cacheTranslationX?: number;\n\n /**\n * translation of the cacheCanvas away from the center, for subpixel accuracy and crispness\n * since 1.7.0\n * @type number\n * @default undefined\n * @private\n */\n declare cacheTranslationY?: number;\n\n /**\n * A reference to the parent of the object, usually a Group\n * @type number\n * @default undefined\n * @private\n */\n declare group?: Group;\n\n /**\n * Indicate if the object is sitting on a cache dedicated to it\n * or is part of a larger cache for many object ( a group for example)\n * @type number\n * @default undefined\n * @private\n */\n declare ownCaching?: boolean;\n\n /**\n * Private. indicates if the object inside a group is on a transformed context or not\n * or is part of a larger cache for many object ( a group for example)\n * @type boolean\n * @default undefined\n * @private\n */\n declare _transformDone?: boolean;\n\n static ownDefaults = fabricObjectDefaultValues;\n\n static getDefaults(): Record<string, any> {\n return FabricObject.ownDefaults;\n }\n\n /**\n * The class type.\n * This is used for serialization and deserialization purposes and internally it can be used\n * to identify classes.\n * When we transform a class in a plain JS object we need a way to recognize which class it was,\n * and the type is the way we do that. It has no other purposes and you should not give one.\n * Hard to reach on instances and please do not use to drive instance's logic (this.constructor.type).\n * To idenfity a class use instanceof class ( instanceof Rect ).\n * We do not do that in fabricJS code because we want to try to have code splitting possible.\n */\n static type = 'FabricObject';\n\n /**\n * Legacy identifier of the class. Prefer using utils like isType or instanceOf\n * Will be removed in fabric 7 or 8.\n * The setter exists to avoid type errors in old code and possibly current deserialization code.\n * DO NOT build new code around this type value\n * @TODO add sustainable warning message\n * @type string\n * @deprecated\n */\n get type() {\n const name = (this.constructor as typeof FabricObject).type;\n if (name === 'FabricObject') {\n return 'object';\n }\n return name.toLowerCase();\n }\n\n set type(value) {\n log('warn', 'Setting type has no effect', value);\n }\n\n /**\n * Constructor\n * @param {Object} [options] Options object\n */\n constructor(options?: Props) {\n super();\n Object.assign(this, FabricObject.ownDefaults);\n this.setOptions(options);\n }\n\n /**\n * Create a the canvas used to keep the cached copy of the object\n * @private\n */\n _createCacheCanvas() {\n this._cacheCanvas = createCanvasElement();\n this._cacheContext = this._cacheCanvas.getContext('2d');\n this._updateCacheCanvas();\n // if canvas gets created, is empty, so dirty.\n this.dirty = true;\n }\n\n /**\n * Limit the cache dimensions so that X * Y do not cross config.perfLimitSizeTotal\n * and each side do not cross fabric.cacheSideLimit\n * those numbers are configurable so that you can get as much detail as you want\n * making bargain with performances.\n * @param {Object} dims\n * @param {Object} dims.width width of canvas\n * @param {Object} dims.height height of canvas\n * @param {Object} dims.zoomX zoomX zoom value to unscale the canvas before drawing cache\n * @param {Object} dims.zoomY zoomY zoom value to unscale the canvas before drawing cache\n * @return {Object}.width width of canvas\n * @return {Object}.height height of canvas\n * @return {Object}.zoomX zoomX zoom value to unscale the canvas before drawing cache\n * @return {Object}.zoomY zoomY zoom value to unscale the canvas before drawing cache\n */\n _limitCacheSize(\n dims: TSize & { zoomX: number; zoomY: number; capped: boolean } & any,\n ) {\n const width = dims.width,\n height = dims.height,\n max = config.maxCacheSideLimit,\n min = config.minCacheSideLimit;\n if (\n width <= max &&\n height <= max &&\n width * height <= config.perfLimitSizeTotal\n ) {\n if (width < min) {\n dims.width = min;\n }\n if (height < min) {\n dims.height = min;\n }\n return dims;\n }\n const ar = width / height,\n [limX, limY] = cache.limitDimsByArea(ar),\n x = capValue(min, limX, max),\n y = capValue(min, limY, max);\n if (width > x) {\n dims.zoomX /= width / x;\n dims.width = x;\n dims.capped = true;\n }\n if (height > y) {\n dims.zoomY /= height / y;\n dims.height = y;\n dims.capped = true;\n }\n return dims;\n }\n\n /**\n * Return the dimension and the zoom level needed to create a cache canvas\n * big enough to host the object to be cached.\n * @private\n * @return {Object}.x width of object to be cached\n * @return {Object}.y height of object to be cached\n * @return {Object}.width width of canvas\n * @return {Object}.height height of canvas\n * @return {Object}.zoomX zoomX zoom value to unscale the canvas before drawing cache\n * @return {Object}.zoomY zoomY zoom value to unscale the canvas before drawing cache\n */\n _getCacheCanvasDimensions(): TCacheCanvasDimensions {\n const objectScale = this.getTotalObjectScaling(),\n // calculate dimensions without skewing\n dim = this._getTransformedDimensions({ skewX: 0, skewY: 0 }),\n neededX = (dim.x * objectScale.x) / this.scaleX,\n neededY = (dim.y * objectScale.y) / this.scaleY;\n return {\n // for sure this ALIASING_LIMIT is slightly creating problem\n // in situation in which the cache canvas gets an upper limit\n // also objectScale contains already scaleX and scaleY\n width: Math.ceil(neededX + ALIASING_LIMIT),\n height: Math.ceil(neededY + ALIASING_LIMIT),\n zoomX: objectScale.x,\n zoomY: objectScale.y,\n x: neededX,\n y: neededY,\n };\n }\n\n /**\n * Update width and height of the canvas for cache\n * returns true or false if canvas needed resize.\n * @private\n * @return {Boolean} true if the canvas has been resized\n */\n _updateCacheCanvas() {\n const canvas = this._cacheCanvas!,\n context = this._cacheContext,\n { width, height, zoomX, zoomY, x, y } = this._limitCacheSize(\n this._getCacheCanvasDimensions(),\n ),\n dimensionsChanged = width !== canvas.width || height !== canvas.height,\n zoomChanged = this.zoomX !== zoomX || this.zoomY !== zoomY;\n\n if (!canvas || !context) {\n return false;\n }\n\n const shouldRedraw = dimensionsChanged || zoomChanged;\n\n if (shouldRedraw) {\n if (width !== canvas.width || height !== canvas.height) {\n canvas.width = width;\n canvas.height = height;\n } else {\n context.setTransform(1, 0, 0, 1, 0, 0);\n context.clearRect(0, 0, canvas.width, canvas.height);\n }\n const drawingWidth = x / 2;\n const drawingHeight = y / 2;\n this.cacheTranslationX =\n Math.round(canvas.width / 2 - drawingWidth) + drawingWidth;\n this.cacheTranslationY =\n Math.round(canvas.height / 2 - drawingHeight) + drawingHeight;\n context.translate(this.cacheTranslationX, this.cacheTranslationY);\n context.scale(zoomX, zoomY);\n this.zoomX = zoomX;\n this.zoomY = zoomY;\n return true;\n }\n return false;\n }\n\n /**\n * Sets object's properties from options, for class constructor only.\n * Needs to be overridden for different defaults.\n * @protected\n * @param {Object} [options] Options object\n */\n protected setOptions(options: Record<string, any> = {}) {\n this._setOptions(options);\n }\n\n /**\n * Transforms context when rendering an object\n * @param {CanvasRenderingContext2D} ctx Context\n */\n transform(ctx: CanvasRenderingContext2D) {\n const needFullTransform =\n (this.group && !this.group._transformDone) ||\n (this.group && this.canvas && ctx === (this.canvas as Canvas).contextTop);\n const m = this.calcTransformMatrix(!needFullTransform);\n ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]);\n }\n\n /**\n * Return the object scale factor counting also the group scaling\n * @return {Point}\n */\n getObjectScaling() {\n // if the object is a top level one, on the canvas, we go for simple aritmetic\n // otherwise the complex method with angles will return approximations and decimals\n // and will likely kill the cache when not needed\n // https://github.com/fabricjs/fabric.js/issues/7157\n if (!this.group) {\n return new Point(Math.abs(this.scaleX), Math.abs(this.scaleY));\n }\n // if we are inside a group total zoom calculation is complex, we defer to generic matrices\n const options = qrDecompose(this.calcTransformMatrix());\n return new Point(Math.abs(options.scaleX), Math.abs(options.scaleY));\n }\n\n /**\n * Return the object scale factor counting also the group scaling, zoom and retina\n * @return {Object} object with scaleX and scaleY properties\n */\n getTotalObjectScaling() {\n const scale = this.getObjectScaling();\n if (this.canvas) {\n const zoom = this.canvas.getZoom();\n const retina = this.getCanvasRetinaScaling();\n return scale.scalarMultiply(zoom * retina);\n }\n return scale;\n }\n\n /**\n * Return the object opacity counting also the group property\n * @return {Number}\n */\n getObjectOpacity() {\n let opacity = this.opacity;\n if (this.group) {\n opacity *= this.group.getObjectOpacity();\n }\n return opacity;\n }\n\n /**\n * Makes sure the scale is valid and modifies it if necessary\n * @todo: this is a control action issue, not a geometry one\n * @private\n * @param {Number} value, unconstrained\n * @return {Number} constrained value;\n */\n _constrainScale(value: number): number {\n if (Math.abs(value) < this.minScaleLimit) {\n if (value < 0) {\n return -this.minScaleLimit;\n } else {\n return this.minScaleLimit;\n }\n } else if (value === 0) {\n return 0.0001;\n }\n return value;\n }\n\n /**\n * Handles setting values on the instance and handling internal side effects\n * @protected\n * @param {String} key\n * @param {*} value\n */\n _set(key: string, value: any) {\n if (key === SCALE_X || key === SCALE_Y) {\n value = this._constrainScale(value);\n }\n if (key === SCALE_X && value < 0) {\n this.flipX = !this.flipX;\n value *= -1;\n } else if (key === 'scaleY' && value < 0) {\n this.flipY = !this.flipY;\n value *= -1;\n // i don't like this automatic initialization here\n } else if (key === 'shadow' && value && !(value instanceof Shadow)) {\n value = new Shadow(value);\n }\n\n const isChanged = this[key as keyof this] !== value;\n this[key as keyof this] = value;\n\n // invalidate caches\n if (\n isChanged &&\n (this.constructor as typeof FabricObject).cacheProperties.includes(key)\n ) {\n this.dirty = true;\n }\n // a dirty child makes the parent dirty.\n // but a non dirty child does not make the parent not dirty.\n // the parent could be dirty for some other reason.\n this.parent &&\n (this.dirty ||\n (isChanged &&\n (this.constructor as typeof FabricObject).stateProperties.includes(\n key,\n ))) &&\n this.parent._set('dirty', true);\n\n return this;\n }\n\n /*\n * @private\n * return if the object would be visible in rendering\n * @memberOf FabricObject.prototype\n * @return {Boolean}\n */\n isNotVisible() {\n return (\n this.opacity === 0 ||\n (!this.width && !this.height && this.strokeWidth === 0) ||\n !this.visible\n );\n }\n\n /**\n * Renders an object on a specified context\n * @param {CanvasRenderingContext2D} ctx Context to render on\n */\n render(ctx: CanvasRenderingContext2D) {\n // do not render if width/height are zeros or object is not visible\n if (this.isNotVisible()) {\n return;\n }\n if (\n this.canvas &&\n this.canvas.skipOffscreen &&\n !this.group &&\n !this.isOnScreen()\n ) {\n return;\n }\n ctx.save();\n this._setupCompositeOperation(ctx);\n this.drawSelectionBackground(ctx);\n this.transform(ctx);\n this._setOpacity(ctx);\n this._setShadow(ctx);\n if (this.shouldCache()) {\n (this as TCachedFabricObject).renderCache();\n (this as TCachedFabricObject).drawCacheOnCanvas(ctx);\n } else {\n this._removeCacheCanvas();\n this.drawObject(ctx, false, {});\n this.dirty = false;\n }\n ctx.restore();\n }\n\n drawSelectionBackground(_ctx: CanvasRenderingContext2D) {\n /* no op */\n }\n\n renderCache(this: TCachedFabricObject, options?: any) {\n options = options || {};\n if (!this._cacheCanvas || !this._cacheContext) {\n this._createCacheCanvas();\n }\n if (this.isCacheDirty() && this._cacheContext) {\n const { zoomX, zoomY, cacheTranslationX, cacheTranslationY } = this;\n const { width, height } = this._cacheCanvas;\n this.drawObject(this._cacheContext, options.forClipping, {\n zoomX,\n zoomY,\n cacheTranslationX,\n cacheTranslationY,\n width,\n height,\n parentClipPaths: [],\n });\n this.dirty = false;\n }\n }\n\n /**\n * Remove cacheCanvas and its dimensions from the objects\n */\n _removeCacheCanvas() {\n this._cacheCanvas = undefined;\n this._cacheContext = null;\n }\n\n /**\n * return true if the object will draw a stroke\n * Does not consider text styles. This is just a shortcut used at rendering time\n * We want it to be an approximation and be fast.\n * wrote to avoid extra caching, it has to return true when stroke happens,\n * can guess when it will not happen at 100% chance, does not matter if it misses\n * some use case where the stroke is invisible.\n * @since 3.0.0\n * @returns Boolean\n */\n hasStroke() {\n return (\n this.stroke && this.stroke !== 'transparent' && this.strokeWidth !== 0\n );\n }\n\n /**\n * return true if the object will draw a fill\n * Does not consider text styles. This is just a shortcut used at rendering time\n * We want it to be an approximation and be fast.\n * wrote to avoid extra caching, it has to return true when fill happens,\n * can guess when it will not happen at 100% chance, does not matter if it misses\n * some use case where the fill is invisible.\n * @since 3.0.0\n * @returns Boolean\n */\n hasFill() {\n return this.fill && this.fill !== 'transparent';\n }\n\n /**\n * When returns `true`, force the object to have its own cache, even if it is inside a group\n * it may be needed when your object behave in a particular way on the cache and always needs\n * its own isolated canvas to render correctly.\n * Created to be overridden\n * since 1.7.12\n * @returns Boolean\n */\n needsItsOwnCache() {\n // TODO re-evaluate this shadow condition\n if (\n this.paintFirst === STROKE &&\n this.hasFill() &&\n this.hasStroke() &&\n !!this.shadow\n ) {\n return true;\n }\n if (this.clipPath) {\n return true;\n }\n return false;\n }\n\n /**\n * Decide if the object should cache or not. Create its own cache level\n * objectCaching is a global flag, wins over everything\n * needsItsOwnCache should be used when the object drawing method requires\n * a cache step.\n * Generally you do not cache objects in groups because the group outside is cached.\n * Read as: cache if is needed, or if the feature is enabled but we are not already caching.\n * @return {Boolean}\n */\n shouldCache() {\n this.ownCaching =\n (this.objectCaching && (!this.parent || !this.parent.isOnACache())) ||\n this.needsItsOwnCache();\n return this.ownCaching;\n }\n\n /**\n * Check if this object will cast a shadow with an offset.\n * used by Group.shouldCache to know if child has a shadow recursively\n * @return {Boolean}\n * @deprecated\n */\n willDrawShadow() {\n return (\n !!this.shadow && (this.shadow.offsetX !== 0 || this.shadow.offsetY !== 0)\n );\n }\n\n /**\n * Execute the drawing operation for an object clipPath\n * @param {CanvasRenderingContext2D} ctx Context to render on\n * @param {FabricObject} clipPath\n */\n drawClipPathOnCache(\n ctx: CanvasRenderingContext2D,\n clipPath: FabricObject,\n canvasWithClipPath: HTMLCanvasElement,\n ) {\n ctx.save();\n // DEBUG: uncomment this line, comment the following\n // ctx.globalAlpha = 0.4\n if (clipPath.inverted) {\n ctx.globalCompositeOperation = 'destination-out';\n } else {\n ctx.globalCompositeOperation = 'destination-in';\n }\n ctx.setTransform(1, 0, 0, 1, 0, 0);\n ctx.drawImage(canvasWithClipPath, 0, 0);\n ctx.restore();\n }\n\n /**\n * Execute the drawing operation for an object on a specified context\n * @param {CanvasRenderingContext2D} ctx Context to render on\n * @param {boolean} forClipping apply clipping styles\n * @param {DrawContext} context additional context for rendering\n */\n drawObject(\n ctx: CanvasRenderingContext2D,\n forClipping: boolean | undefined,\n context: DrawContext,\n ) {\n const originalFill = this.fill,\n originalStroke = this.stroke;\n if (forClipping) {\n this.fill = 'black';\n this.stroke = '';\n this._setClippingProperties(ctx);\n } else {\n this._renderBackground(ctx);\n }\n this._render(ctx);\n this._drawClipPath(ctx, this.clipPath, context);\n this.fill = originalFill;\n this.stroke = originalStroke;\n }\n\n private createClipPathLayer(\n this: TCachedFabricObject,\n clipPath: FabricObject,\n context: DrawContext,\n ) {\n const canvas = createCanvasElementFor(context as TSize);\n const ctx = canvas.getContext('2d')!;\n ctx.translate(context.cacheTranslationX, context.cacheTranslationY);\n ctx.scale(context.zoomX, context.zoomY);\n clipPath._cacheCanvas = canvas;\n context.parentClipPaths.forEach((prevClipPath) => {\n prevClipPath.transform(ctx);\n });\n context.parentClipPaths.push(clipPath);\n if (clipPath.absolutePositioned) {\n const m = invertTransform(this.calcTransformMatrix());\n ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]);\n }\n clipPath.transform(ctx);\n clipPath.drawObject(ctx, true, context);\n return canvas;\n }\n\n /**\n * Prepare clipPath state and cache and draw it on instance's cache\n * @param {CanvasRenderingContext2D} ctx\n * @param {FabricObject} clipPath\n */\n _drawClipPath(\n ctx: CanvasRenderingContext2D,\n clipPath: FabricObject | undefined,\n context: DrawContext,\n ) {\n if (!clipPath) {\n return;\n }\n // needed to setup _transformDone\n // TODO find a better solution?\n clipPath._transformDone = true;\n const canvas = (this as TCachedFabricObject).createClipPathLayer(\n clipPath,\n context,\n );\n this.drawClipPathOnCache(ctx, clipPath, canvas);\n }\n\n /**\n * Paint the cached copy of the object on the target context.\n * @param {CanvasRenderingContext2D} ctx Context to render on\n */\n drawCacheOnCanvas(this: TCachedFabricObject, ctx: CanvasRenderingContext2D) {\n ctx.scale(1 / this.zoomX, 1 / this.zoomY);\n ctx.drawImage(\n this._cacheCanvas,\n -this.cacheTranslationX,\n -this.cacheTranslationY,\n );\n }\n\n /**\n * Check if cache is dirty and if is dirty clear the context.\n * This check has a big side effect, it changes the underlying cache canvas if necessary.\n * Do not call this method on your own to check if the cache is dirty, because if it is,\n * it is also going to wipe the cache. This is badly designed and needs to be fixed.\n * @param {Boolean} skipCanvas skip canvas checks because this object is painted\n * on parent canvas.\n */\n isCacheDirty(skipCanvas = false) {\n if (this.isNotVisible()) {\n return false;\n }\n const canvas = this._cacheCanvas;\n const ctx = this._cacheContext;\n if (canvas && ctx && !skipCanvas && this._updateCacheCanvas()) {\n // in this case the context is already cleared.\n return true;\n } else {\n if (this.dirty || (this.clipPath && this.clipPath.absolutePositioned)) {\n if (canvas && ctx && !skipCanvas) {\n ctx.save();\n ctx.setTransform(1, 0, 0, 1, 0, 0);\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n ctx.restore();\n }\n return true;\n }\n }\n return false;\n }\n\n /**\n * Draws a background for the object big as its untransformed dimensions\n * @private\n * @param {CanvasRenderingContext2D} ctx Context to render on\n */\n _renderBackground(ctx: CanvasRenderingContext2D) {\n if (!this.backgroundColor) {\n return;\n }\n const dim = this._getNonTransformedDimensions();\n ctx.fillStyle = this.backgroundColor;\n\n ctx.fillRect(-dim.x / 2, -dim.y / 2, dim.x, dim.y);\n // if there is background color no other shadows\n // should be casted\n this._removeShadow(ctx);\n }\n\n /**\n * @private\n * @param {CanvasRenderingContext2D} ctx Context to render on\n */\n _setOpacity(ctx: CanvasRenderingContext2D) {\n if (this.group && !this.group._transformDone) {\n ctx.globalAlpha = this.getObjectOpacity();\n } else {\n ctx.globalAlpha *= this.opacity;\n }\n }\n\n _setStrokeStyles(\n ctx: CanvasRenderingContext2D,\n decl: Pick<\n this,\n | 'stroke'\n | 'strokeWidth'\n | 'strokeLineCap'\n | 'strokeDashOffset'\n | 'strokeLineJoin'\n | 'strokeMiterLimit'\n >,\n ) {\n const stroke = decl.stroke;\n if (stroke) {\n ctx.lineWidth = decl.strokeWidth;\n ctx.lineCap = decl.strokeLineCap;\n ctx.lineDashOffset = decl.strokeDashOffset;\n ctx.lineJoin = decl.strokeLineJoin;\n ctx.miterLimit = decl.strokeMiterLimit;\n if (isFiller(stroke)) {\n if (\n (stroke as Gradient<'linear'>).gradientUnits === 'percentage' ||\n (stroke as Gradient<'linear'>).gradientTransform ||\n (stroke as Pattern).patternTransform\n ) {\n // need to transform gradient in a pattern.\n // this is a slow process. If you are hitting this codepath, and the object\n // is not using caching, you should consider switching it on.\n // we need a canvas as big as the current object caching canvas.\n this._applyPatternForTransformedGradient(ctx, stroke);\n } else {\n // is a simple gradient or pattern\n ctx.strokeStyle = stroke.toLive(ctx)!;\n this._applyPatternGradientTransform(ctx, stroke);\n }\n } else {\n // is a color\n ctx.strokeStyle = decl.stroke as string;\n }\n }\n }\n\n _setFillStyles(ctx: CanvasRenderingContext2D, { fill }: Pick<this, 'fill'>) {\n if (fill) {\n if (isFiller(fill)) {\n ctx.fillStyle = fill.toLive(ctx)!;\n this._applyPatternGradientTransform(ctx, fill);\n } else {\n ctx.fillStyle = fill;\n }\n }\n }\n\n _setClippingProperties(ctx: CanvasRenderingContext2D) {\n ctx.globalAlpha = 1;\n ctx.strokeStyle = 'transparent';\n ctx.fillStyle = '#000000';\n }\n\n /**\n * @private\n * Sets line dash\n * @param {CanvasRenderingContext2D} ctx Context to set the dash line on\n * @param {Array} dashArray array representing dashes\n */\n _setLineDash(ctx: CanvasRenderingContext2D, dashArray?: number[] | null) {\n if (!dashArray || dashArray.length === 0) {\n return;\n }\n ctx.setLineDash(dashArray);\n }\n\n /**\n * @private\n * @param {CanvasRenderingContext2D} ctx Context to render on\n */\n _setShadow(ctx: CanvasRenderingContext2D) {\n if (!this.shadow) {\n return;\n }\n\n const shadow = this.shadow,\n canvas = this.canvas,\n retinaScaling = this.getCanvasRetinaScaling(),\n [sx, , , sy] = canvas?.viewportTransform || iMatrix,\n multX = sx * retinaScaling,\n multY = sy * retinaScaling,\n scaling = shadow.nonScaling ? new Point(1, 1) : this.getObjectScaling();\n ctx.shadowColor = shadow.color;\n ctx.shadowBlur =\n (shadow.blur *\n config.browserShadowBlurConstant *\n (multX + multY) *\n (scaling.x + scaling.y)) /\n 4;\n ctx.shadowOffsetX = shadow.offsetX * multX * scaling.x;\n ctx.shadowOffsetY = shadow.offsetY * multY * scaling.y;\n }\n\n /**\n * @private\n * @param {CanvasRenderingContext2D} ctx Context to render on\n */\n _removeShadow(ctx: CanvasRenderingContext2D) {\n if (!this.shadow) {\n return;\n }\n\n ctx.shadowColor = '';\n ctx.shadowBlur = ctx.shadowOffsetX = ctx.shadowOffsetY = 0;\n }\n\n /**\n * @private\n * @param {CanvasRenderingContext2D} ctx Context to render on\n * @param {TFiller} filler {@link Pattern} or {@link Gradient}\n */\n _applyPatternGradientTransform(\n ctx: CanvasRenderingContext2D,\n filler: TFiller,\n ) {\n if (!isFiller(filler)) {\n return { offsetX: 0, offsetY: 0 };\n }\n const t =\n (filler as Gradient<'linear'>).gradientTransform ||\n (filler as Pattern).patternTransform;\n const offsetX = -this.width / 2 + filler.offsetX || 0,\n offsetY = -this.height / 2 + filler.offsetY || 0;\n\n if ((filler as Gradient<'linear'>).gradientUnits === 'percentage') {\n ctx.transform(this.width, 0, 0, this.height, offsetX, offsetY);\n } else {\n ctx.transform(1, 0, 0, 1, offsetX, offsetY);\n }\n if (t) {\n ctx.transform(t[0], t[1], t[2], t[3], t[4], t[5]);\n }\n return { offsetX: offsetX, offsetY: offsetY };\n }\n\n /**\n * @private\n * @param {CanvasRenderingContext2D} ctx Context to render on\n */\n _renderPaintInOrder(ctx: CanvasRenderingContext2D) {\n if (this.paintFirst === STROKE) {\n this._renderStroke(ctx);\n this._renderFill(ctx);\n } else {\n this._renderFill(ctx);\n this._renderStroke(ctx);\n }\n }\n\n /**\n * @private\n * function that actually render something on the context.\n * empty here to allow Obects to work on tests to benchmark fabric functionalites\n * not related to rendering\n * @param {CanvasRenderingContext2D} _ctx Context to render on\n */\n _render(_ctx: CanvasRenderingContext2D) {\n // placeholder to be overridden\n }\n\n /**\n * @private\n * @param {CanvasRenderingContext2D} ctx Context to render on\n */\n _renderFill(ctx: CanvasRenderingContext2D) {\n if (!this.fill) {\n return;\n }\n\n ctx.save();\n this._setFillStyles(ctx, this);\n if (this.fillRule === 'evenodd') {\n ctx.fill('evenodd');\n } else {\n ctx.fill();\n }\n ctx.restore();\n }\n\n /**\n * @private\n * @param {CanvasRenderingContext2D} ctx Context to render on\n */\n _renderStroke(ctx: CanvasRenderingContext2D) {\n if (!this.stroke || this.strokeWidth === 0) {\n return;\n }\n\n if (this.shadow && !this.shadow.affectStroke) {\n this._removeShadow(ctx);\n }\n\n ctx.save();\n if (this.strokeUniform) {\n const scaling = this.getObjectScaling();\n ctx.scale(1 / scaling.x, 1 / scaling.y);\n }\n this._setLineDash(ctx, this.strokeDashArray);\n this._setStrokeStyles(ctx, this);\n ctx.stroke();\n ctx.restore();\n }\n\n /**\n * This function try to patch the missing gradientTransform on canvas gradients.\n * transforming a context to transform the gradient, is going to transform the stroke too.\n * we want to transform the gradient but not the stroke operation, so we create\n * a transformed gradient on a pattern and then we use the pattern instead of the gradient.\n * this method has drawbacks: is slow, is in low resolution, needs a patch for when the size\n * is limited.\n * @private\n * @param {CanvasRenderingContext2D} ctx Context to render on\n * @param {Gradient} filler\n */\n _applyPatternForTransformedGradient(\n ctx: CanvasRenderingContext2D,\n filler: TFiller,\n ) {\n const dims = this._limitCacheSize(this._getCacheCanvasDimensions()),\n retinaScaling = this.getCanvasRetinaScaling(),\n width = dims.x / this.scaleX / retinaScaling,\n height = dims.y / this.scaleY / retinaScaling,\n pCanvas = createCanvasElementFor({\n // in case width and height are less than 1px, we have to round up.\n // since the pattern is no-repeat, this is fine\n width: Math.ceil(width),\n height: Math.ceil(height),\n });\n\n const pCtx = pCanvas.getContext('2d');\n if (!pCtx) {\n return;\n }\n pCtx.beginPath();\n pCtx.moveTo(0, 0);\n pCtx.lineTo(width, 0);\n pCtx.lineTo(width, height);\n pCtx.lineTo(0, height);\n pCtx.closePath();\n pCtx.translate(width / 2, height / 2);\n pCtx.scale(\n dims.zoomX / this.scaleX / retinaScaling,\n dims.zoomY / this.scaleY / retinaScaling,\n );\n this._applyPatternGradientTransform(pCtx, filler);\n pCtx.fillStyle = filler.toLive(ctx)!;\n pCtx.fill();\n ctx.translate(\n -this.width / 2 - this.strokeWidth / 2,\n -this.height / 2 - this.strokeWidth / 2,\n );\n ctx.scale(\n (retinaScaling * this.scaleX) / dims.zoomX,\n (retinaScaling * this.scaleY) / dims.zoomY,\n );\n ctx.strokeStyle = pCtx.createPattern(pCanvas, 'no-repeat') ?? '';\n }\n\n /**\n * This function is an helper for svg import. it returns the center of the object in the svg\n * untransformed coordinates\n * @private\n * @return {Point} center point from element coordinates\n */\n _findCenterFromElement() {\n return new Point(this.left + this.width / 2, this.top + this.height / 2);\n }\n\n /**\n * Clones an instance.\n * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output\n * @returns {Promise<FabricObject>}\n */\n clone(propertiesToInclude?: string[]): Promise<this> {\n const objectForm = this.toObject(propertiesToInclude);\n return (this.constructor as typeof FabricObject).fromObject(\n objectForm,\n ) as unknown as Promise<this>;\n }\n\n /**\n * Creates an instance of Image out of an object\n * makes use of toCanvasElement.\n * Once this method was based on toDataUrl and loadImage, so it also had a quality\n * and format option. toCanvasElement is faster and produce no loss of quality.\n * If you need to get a real Jpeg or Png from an object, using toDataURL is the right way to do it.\n * toCanvasElement and then toBlob from the obtained canvas is also a good option.\n * @todo fix the export type, it could not be Image but the type that getClass return for 'image'.\n * @param {ObjectToCanvasElementOptions} [options] for clone as image, passed to toDataURL\n * @param {Number} [options.multiplier=1] Multiplier to scale by\n * @param {Number} [options.left] Cropping left offset. Introduced in v1.2.14\n * @param {Number} [options.top] Cropping top offset. Introduced in v1.2.14\n * @param {Number} [options.width] Cropping width. Introduced in v1.2.14\n * @param {Number} [options.height] Cropping height. Introduced in v1.2.14\n * @param {Boolean} [options.enableRetinaScaling] Enable retina scaling for clone image. Introduce in 1.6.4\n * @param {Boolean} [options.withoutTransform] Remove current object transform ( no scale , no angle, no flip, no skew ). Introduced in 2.3.4\n * @param {Boolean} [options.withoutShadow] Remove current object shadow. Introduced in 2.4.2\n * @return {FabricImage} Object cloned as image.\n */\n cloneAsImage(options: ObjectToCanvasElementOptions): FabricImage {\n const canvasEl = this.toCanvasElement(options);\n // TODO: how to import Image w/o an import cycle?\n const ImageClass = classRegistry.getClass<typeof FabricImage>('image');\n return new ImageClass(canvasEl);\n }\n\n /**\n * Converts an object into a HTMLCanvas element\n * @param {ObjectToCanvasElementOptions} options Options object\n * @param {Number} [options.multiplier=1] Multiplier to scale by\n * @param {Number} [options.left] Cropping left offset. Introduced in v1.2.14\n * @param {Number} [options.top] Cropping top offset. Introduced in v1.2.14\n * @param {Number} [options.width] Cropping width. Introduced in v1.2.14\n * @param {Number} [options.height] Cropping height. Introduced in v1.2.14\n * @param {Boolean} [options.enableRetinaScaling] Enable retina scaling for clone image. Introduce in 1.6.4\n * @param {Boolean} [options.withoutTransform] Remove current object transform ( no scale , no angle, no flip, no skew ). Introduced in 2.3.4\n * @param {Boolean} [options.withoutShadow] Remove current object shadow. Introduced in 2.4.2\n * @param {Boolean} [options.viewportTransform] Account for canvas viewport transform\n * @param {(el?: HTMLCanvasElement) => StaticCanvas} [options.canvasProvider] Create the output canvas\n * @return {HTMLCanvasElement} Returns DOM element <canvas> with the FabricObject\n */\n toCanvasElement(options: ObjectToCanvasElementOptions = {}) {\n const origParams = saveObjectTransform(this),\n originalGroup = this.group,\n originalShadow = this.shadow,\n abs = Math.abs,\n retinaScaling = options.enableRetinaScaling ? getDevicePixelRatio() : 1,\n multiplier = (options.multiplier || 1) * retinaScaling,\n canvasProvider: (el: HTMLCanvasElement) => StaticCanvas =\n options.canvasProvider ||\n ((el: HTMLCanvasElement) =>\n new StaticCanvas(el, {\n enableRetinaScaling: false,\n renderOnAddRemove: false,\n skipOffscreen: false,\n }));\n delete this.group;\n if (options.withoutTransform) {\n resetObjectTransform(this);\n }\n if (options.withoutShadow) {\n this.shadow = null;\n }\n if (options.viewportTransform) {\n sendObjectToPlane(this, this.getViewportTransform());\n }\n\n this.setCoords();\n const el = createCanvasElement(),\n boundingRect = this.getBoundingRect(),\n shadow = this.shadow,\n shadowOffset = new Point();\n\n if (shadow) {\n const shadowBlur = shadow.blur;\n const scaling = shadow.nonScaling\n ? new Point(1, 1)\n : this.getObjectScaling();\n // consider non scaling shadow.\n shadowOffset.x =\n 2 * Math.round(abs(shadow.offsetX) + shadowBlur) * abs(scaling.x);\n shadowOffset.y =\n 2 * Math.round(abs(shadow.offsetY) + shadowBlur) * abs(scaling.y);\n }\n const width = boundingRect.width + shadowOffset.x,\n height = boundingRect.height + shadowOffset.y;\n // if the current width/height is not an integer\n // we need to make it so.\n el.width = Math.ceil(width);\n el.height = Math.ceil(height);\n const canvas = canvasProvider(el);\n if (options.format === 'jpeg') {\n canvas.backgroundColor = '#fff';\n }\n this.setPositionByOrigin(\n new Point(canvas.width / 2, canvas.height / 2),\n CENTER,\n CENTER,\n );\n const originalCanvas = this.canvas;\n // static canvas and canvas have both an array of InteractiveObjects\n // @ts-expect-error this needs to be fixed somehow, or ignored globally\n canvas._objects = [this];\n this.set('canvas', canvas);\n this.setCoords();\n const canvasEl = canvas.toCanvasElement(multiplier || 1, options);\n this.set('canvas', originalCanvas);\n this.shadow = originalShadow;\n if (originalGroup) {\n this.group = originalGroup;\n }\n this.set(origParams);\n this.setCoords();\n // canvas.dispose will call image.dispose that will nullify the elements\n // since this canvas is a simple element for the process, we remove references\n // to objects in this way in order to avoid object trashing.\n canvas._objects = [];\n // since render has settled it is safe to destroy canvas\n canvas.destroy();\n return canvasEl;\n }\n\n /**\n * Converts an object into a data-url-like string\n * @param {Object} options Options object\n * @param {String} [options.format=png] The format of the output image. Either \"jpeg\" or \"png\"\n * @param {Number} [options.quality=1] Quality level (0..1). Only used for jpeg.\n * @param {Number} [options.multiplier=1] Multiplier to scale by\n * @param {Number} [options.left] Cropping left offset. Introduced in v1.2.14\n * @param {Number} [options.top] Cropping top offset. Introduced in v1.2.14\n * @param {Number} [options.width] Cropping width. Introduced in v1.2.14\n * @param {Number} [options.height] Cropping height. Introduced in v1.2.14\n * @param {Boolean} [options.enableRetinaScaling] Enable retina scaling for clone image. Introduce in 1.6.4\n * @param {Boolean} [options.withoutTransform] Remove current object transform ( no scale , no angle, no flip, no skew ). Introduced in 2.3.4\n * @param {Boolean} [options.withoutShadow] Remove current object shadow. Introduced in 2.4.2\n * @return {String} Returns a data: URL containing a representation of the object in the format specified by options.format\n */\n toDataURL(options: toDataURLOptions = {}) {\n return toDataURL(\n this.toCanvasElement(options),\n options.format || 'png',\n options.quality || 1,\n );\n }\n toBlob(options: toDataURLOptions = {}) {\n return toBlob(\n this.toCanvasElement(options),\n options.format || 'png',\n options.quality || 1,\n );\n }\n\n /**\n * Returns true if any of the specified types is identical to the type of an instance\n * @param {String} type Type to check against\n * @return {Boolean}\n */\n isType(...types: string[]) {\n return (\n types.includes((this.constructor as typeof FabricObject).type) ||\n types.includes(this.type)\n );\n }\n\n /**\n * Returns complexity of an instance\n * @return {Number} complexity of this instance (is 1 unless subclassed)\n */\n complexity() {\n return 1;\n }\n\n /**\n * Returns a JSON representation of an instance\n * @return {Object} JSON\n */\n toJSON() {\n // delegate, not alias\n return this.toObject();\n }\n\n /**\n * Sets \"angle\" of an instance with centered rotation\n * @param {TDegree} angle Angle value (in degrees)\n */\n rotate(angle: TDegree) {\n const { centeredRotation, originX, originY } = this;\n\n if (centeredRotation) {\n const { x, y } = this.getRelativeCenterPoint();\n this.originX = CENTER;\n this.originY = CENTER;\n this.left = x;\n this.top = y;\n }\n\n this.set('angle', angle);\n\n if (centeredRotation) {\n const { x, y } = this.translateToOriginPoint(\n this.getRelativeCenterPoint(),\n originX,\n originY,\n );\n this.left = x;\n this.top = y;\n this.originX = originX;\n this.originY = originY;\n }\n }\n\n /**\n * This callback function is called by the parent group of an object every\n * time a non-delegated property changes on the group. It is passed the key\n * and value as parameters. Not adding in this function's signature to avoid\n * Travis build error about unused variables.\n */\n setOnGroup() {\n // implemented by sub-classes, as needed.\n }\n\n /**\n * Sets canvas globalCompositeOperation for specific object\n * custom composition operation for the particular object can be specified using globalCompositeOperation property\n * @param {CanvasRenderingContext2D} ctx Rendering canvas context\n */\n _setupCompositeOperation(ctx: CanvasRenderingContext2D) {\n if (this.globalCompositeOperation) {\n ctx.globalCompositeOperation = this.globalCompositeOperation;\n }\n }\n\n /**\n * cancel instance's running animations\n * override if necessary to dispose artifacts such as `clipPath`\n */\n dispose() {\n runningAnimations.cancelByTarget(this);\n this.off();\n this._set('canvas', undefined);\n // clear caches\n this._cacheCanvas && getEnv().dispose(this._cacheCanvas);\n this._cacheCanvas = undefined;\n this._cacheContext = null;\n }\n\n // #region Animation methods\n /**\n * List of properties to consider for animating colors.\n * @type String[]\n */\n static colorProperties: string[] = [FILL, STROKE, 'backgroundColor'];\n\n /**\n * Animates object's properties\n * @param {Record<string, number | number[] | TColorArg>} animatable map of keys and end values\n * @param {Partial<AnimationOptions<T>>} options\n * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#animation}\n * @return {Record<string, TAnimation<T>>} map of animation contexts\n *\n * As object — multiple properties\n *\n * object.animate({ left: ..., top: ... });\n * object.animate({ left: ..., top: ... }, { duration: ... });\n */\n animate<T extends number | number[] | TColorArg>(\n animatable: Record<string, T>,\n options?: Partial<AnimationOptions<T>>,\n ): Record<string, TAnimation<T>> {\n return Object.entries(animatable).reduce(\n (acc, [key, endValue]) => {\n