UNPKG

fabric

Version:

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

1,115 lines (1,114 loc) 42.1 kB
import { _defineProperty } from "../../../_virtual/_@oxc-project_runtime@0.122.0/helpers/defineProperty.mjs"; import { config } from "../../config.mjs"; import { log } from "../../util/internals/console.mjs"; import { getDevicePixelRatio, getEnv } from "../../env/index.mjs"; import { cache } from "../../cache.mjs"; import { CENTER, FILL, STROKE, VERSION, iMatrix } from "../../constants.mjs"; import { classRegistry } from "../../ClassRegistry.mjs"; import { runningAnimations } from "../../util/animation/AnimationRegistry.mjs"; import { Point } from "../../Point.mjs"; import { createCanvasElement, createCanvasElementFor, toBlob, toDataURL } from "../../util/misc/dom.mjs"; import { invertTransform, qrDecompose } from "../../util/misc/matrix.mjs"; import { enlivenObjectEnlivables } from "../../util/misc/objectEnlive.mjs"; import { pick, pickBy } from "../../util/misc/pick.mjs"; import { toFixed } from "../../util/misc/toFixed.mjs"; import { isFiller, isSerializableFiller } from "../../util/typeAssertions.mjs"; import { StaticCanvas } from "../../canvas/StaticCanvas.mjs"; import { resetObjectTransform, saveObjectTransform } from "../../util/misc/objectTransforms.mjs"; import { sendObjectToPlane } from "../../util/misc/planeChange.mjs"; import { Shadow } from "../../Shadow.mjs"; import { capValue } from "../../util/misc/capValue.mjs"; import { cacheProperties, fabricObjectDefaultValues, stateProperties } from "./defaultValues.mjs"; import { animate, animateColor } from "../../util/animation/animate.mjs"; import { ObjectGeometry } from "./ObjectGeometry.mjs"; //#region src/shapes/Object/Object.ts /** * Root object class from which all 2d shape classes inherit from * @see {@link http://fabric5.fabricjs.com/fabric-intro-part-1#objects} * * @fires added * @fires removed * * @fires selected * @fires deselected * * @fires rotating * @fires scaling * @fires moving * @fires skewing * @fires modified * * @fires mousedown * @fires mouseup * @fires mouseover * @fires mouseout * @fires mousewheel * @fires mousedblclick * * @fires dragover * @fires dragenter * @fires dragleave * @fires drop */ var FabricObject = class FabricObject extends ObjectGeometry { static getDefaults() { return FabricObject.ownDefaults; } /** * Legacy identifier of the class. Prefer using utils like isType or instanceOf * Will be removed in fabric 7 or 8. * The setter exists to avoid type errors in old code and possibly current deserialization code. * DO NOT build new code around this type value * @TODO add sustainable warning message * @type string * @deprecated */ get type() { const name = this.constructor.type; if (name === "FabricObject") return "object"; return name.toLowerCase(); } set type(value) { log("warn", "Setting type has no effect", value); } /** * Constructor * @param {Object} [options] Options object */ constructor(options) { super(); _defineProperty( this, /** * Quick access for the _cacheCanvas rendering context * This is part of the objectCaching feature * since 1.7.0 * @type boolean * @default undefined * @private */ "_cacheContext", null ); Object.assign(this, FabricObject.ownDefaults); this.setOptions(options); } /** * Create a the canvas used to keep the cached copy of the object * @private */ _createCacheCanvas() { this._cacheCanvas = createCanvasElement(); this._cacheContext = this._cacheCanvas.getContext("2d"); this._updateCacheCanvas(); this.dirty = true; } /** * Limit the cache dimensions so that X * Y do not cross config.perfLimitSizeTotal * and each side do not cross fabric.cacheSideLimit * those numbers are configurable so that you can get as much detail as you want * making bargain with performances. * It mutates the input object dims. * @param {TCacheCanvasDimensions} dims * @return {TCacheCanvasDimensions} dims */ _limitCacheSize(dims) { const width = dims.width, height = dims.height, max = config.maxCacheSideLimit, min = config.minCacheSideLimit; if (width <= max && height <= max && width * height <= config.perfLimitSizeTotal) { if (width < min) dims.width = min; if (height < min) dims.height = min; return dims; } const ar = width / height, [limX, limY] = cache.limitDimsByArea(ar), x = capValue(min, limX, max), y = capValue(min, limY, max); if (width > x) { dims.zoomX /= width / x; dims.width = x; dims.capped = true; } if (height > y) { dims.zoomY /= height / y; dims.height = y; dims.capped = true; } return dims; } /** * Return the dimension and the zoom level needed to create a cache canvas * big enough to host the object to be cached. * @private * @return {TCacheCanvasDimensions} Informations about the object to be cached */ _getCacheCanvasDimensions() { const objectScale = this.getTotalObjectScaling(), dim = this._getTransformedDimensions({ skewX: 0, skewY: 0 }), neededX = dim.x * objectScale.x / this.scaleX, neededY = dim.y * objectScale.y / this.scaleY; return { width: Math.ceil(neededX + 2), height: Math.ceil(neededY + 2), zoomX: objectScale.x, zoomY: objectScale.y, x: neededX, y: neededY }; } /** * Update width and height of the canvas for cache * returns true or false if canvas needed resize. * @private * @return {Boolean} true if the canvas has been resized */ _updateCacheCanvas() { const canvas = this._cacheCanvas, context = this._cacheContext, { width, height, zoomX, zoomY, x, y } = this._limitCacheSize(this._getCacheCanvasDimensions()), dimensionsChanged = width !== canvas.width || height !== canvas.height, zoomChanged = this.zoomX !== zoomX || this.zoomY !== zoomY; if (!canvas || !context) return false; if (dimensionsChanged || zoomChanged) { if (width !== canvas.width || height !== canvas.height) { canvas.width = width; canvas.height = height; } else { context.setTransform(1, 0, 0, 1, 0, 0); context.clearRect(0, 0, canvas.width, canvas.height); } const drawingWidth = x / 2; const drawingHeight = y / 2; this.cacheTranslationX = Math.round(canvas.width / 2 - drawingWidth) + drawingWidth; this.cacheTranslationY = Math.round(canvas.height / 2 - drawingHeight) + drawingHeight; context.translate(this.cacheTranslationX, this.cacheTranslationY); context.scale(zoomX, zoomY); this.zoomX = zoomX; this.zoomY = zoomY; return true; } return false; } /** * Sets object's properties from options, for class constructor only. * Needs to be overridden for different defaults. * @protected * @param {Object} [options] Options object */ setOptions(options = {}) { this._setOptions(options); } /** * Transforms context when rendering an object * @param {CanvasRenderingContext2D} ctx Context */ transform(ctx) { const needFullTransform = this.group && !this.group._transformDone || this.group && this.canvas && ctx === this.canvas.contextTop; const m = this.calcTransformMatrix(!needFullTransform); ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); } /** * Return the object scale factor counting also the group scaling * @return {Point} */ getObjectScaling() { if (!this.group) return new Point(Math.abs(this.scaleX), Math.abs(this.scaleY)); const options = qrDecompose(this.calcTransformMatrix()); return new Point(Math.abs(options.scaleX), Math.abs(options.scaleY)); } /** * Return the object scale factor counting also the group scaling, zoom and retina * @return {Object} object with scaleX and scaleY properties */ getTotalObjectScaling() { const scale = this.getObjectScaling(); if (this.canvas) { const zoom = this.canvas.getZoom(); const retina = this.getCanvasRetinaScaling(); return scale.scalarMultiply(zoom * retina); } return scale; } /** * Return the object opacity counting also the group property * @return {Number} */ getObjectOpacity() { let opacity = this.opacity; if (this.group) opacity *= this.group.getObjectOpacity(); return opacity; } /** * Makes sure the scale is valid and modifies it if necessary * @todo: this is a control action issue, not a geometry one * @private * @param {Number} value, unconstrained * @return {Number} constrained value; */ _constrainScale(value) { if (Math.abs(value) < this.minScaleLimit) if (value < 0) return -this.minScaleLimit; else return this.minScaleLimit; else if (value === 0) return 1e-4; return value; } /** * Handles setting values on the instance and handling internal side effects * @protected * @param {String} key * @param {*} value */ _set(key, value) { if (key === "scaleX" || key === "scaleY") value = this._constrainScale(value); if (key === "scaleX" && value < 0) { this.flipX = !this.flipX; value *= -1; } else if (key === "scaleY" && value < 0) { this.flipY = !this.flipY; value *= -1; } else if (key === "shadow" && value && !(value instanceof Shadow)) value = new Shadow(value); const isChanged = this[key] !== value; this[key] = value; if (isChanged && this.constructor.cacheProperties.includes(key)) this.dirty = true; this.parent && (this.dirty || isChanged && this.constructor.stateProperties.includes(key)) && this.parent._set("dirty", true); return this; } /** * return if the object would be visible in rendering * @return {Boolean} */ isNotVisible() { return this.opacity === 0 || !this.width && !this.height && this.strokeWidth === 0 || !this.visible; } /** * Renders an object on a specified context * @param {CanvasRenderingContext2D} ctx Context to render on */ render(ctx) { if (this.isNotVisible()) return; if (this.canvas && this.canvas.skipOffscreen && !this.group && !this.isOnScreen()) return; ctx.save(); this._setupCompositeOperation(ctx); this.drawSelectionBackground(ctx); this.transform(ctx); this._setOpacity(ctx); this._setShadow(ctx); if (this.shouldCache()) { this.renderCache(); this.drawCacheOnCanvas(ctx); } else { this._removeCacheCanvas(); this.drawObject(ctx, false, {}); this.dirty = false; } ctx.restore(); } drawSelectionBackground(_ctx) {} renderCache(options) { options = options || {}; if (!this._cacheCanvas || !this._cacheContext) this._createCacheCanvas(); if (this.isCacheDirty() && this._cacheContext) { const { zoomX, zoomY, cacheTranslationX, cacheTranslationY } = this; const { width, height } = this._cacheCanvas; this.drawObject(this._cacheContext, options.forClipping, { zoomX, zoomY, cacheTranslationX, cacheTranslationY, width, height, parentClipPaths: [] }); this.dirty = false; } } /** * Remove cacheCanvas and its dimensions from the objects */ _removeCacheCanvas() { this._cacheCanvas = void 0; this._cacheContext = null; } /** * return true if the object will draw a stroke * Does not consider text styles. This is just a shortcut used at rendering time * We want it to be an approximation and be fast. * wrote to avoid extra caching, it has to return true when stroke happens, * can guess when it will not happen at 100% chance, does not matter if it misses * some use case where the stroke is invisible. * @since 3.0.0 * @returns Boolean */ hasStroke() { return !!this.stroke && this.stroke !== "transparent" && this.strokeWidth !== 0; } /** * return true if the object will draw a fill * Does not consider text styles. This is just a shortcut used at rendering time * We want it to be an approximation and be fast. * wrote to avoid extra caching, it has to return true when fill happens, * can guess when it will not happen at 100% chance, does not matter if it misses * some use case where the fill is invisible. * @since 3.0.0 * @returns Boolean */ hasFill() { return !!this.fill && this.fill !== "transparent"; } /** * When returns `true`, force the object to have its own cache, even if it is inside a group * it may be needed when your object behave in a particular way on the cache and always needs * its own isolated canvas to render correctly. * Created to be overridden * since 1.7.12 * @returns Boolean */ needsItsOwnCache() { if (this.paintFirst === "stroke" && this.hasFill() && this.hasStroke() && !!this.shadow) return true; if (this.clipPath) return true; return false; } /** * Decide if the object should cache or not. Create its own cache level * objectCaching is a global flag, wins over everything * needsItsOwnCache should be used when the object drawing method requires * a cache step. * Generally you do not cache objects in groups because the group outside is cached. * Read as: cache if is needed, or if the feature is enabled but we are not already caching. * @return {Boolean} */ shouldCache() { this.ownCaching = this.objectCaching && (!this.parent || !this.parent.isOnACache()) || this.needsItsOwnCache(); return this.ownCaching; } /** * Check if this object will cast a shadow with an offset. * used by Group.shouldCache to know if child has a shadow recursively * @return {Boolean} * @deprecated */ willDrawShadow() { return !!this.shadow && (this.shadow.offsetX !== 0 || this.shadow.offsetY !== 0); } /** * Execute the drawing operation for an object clipPath * @param {CanvasRenderingContext2D} ctx Context to render on * @param {FabricObject} clipPath */ drawClipPathOnCache(ctx, clipPath, canvasWithClipPath) { ctx.save(); if (clipPath.inverted) ctx.globalCompositeOperation = "destination-out"; else ctx.globalCompositeOperation = "destination-in"; ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.drawImage(canvasWithClipPath, 0, 0); ctx.restore(); } /** * Execute the drawing operation for an object on a specified context * @param {CanvasRenderingContext2D} ctx Context to render on * @param {boolean} forClipping apply clipping styles * @param {DrawContext} context additional context for rendering */ drawObject(ctx, forClipping, context) { const originalFill = this.fill, originalStroke = this.stroke; if (forClipping) { this.fill = "black"; this.stroke = ""; this._setClippingProperties(ctx); } else this._renderBackground(ctx); this.fire("before:render", { ctx }); this._render(ctx); this._drawClipPath(ctx, this.clipPath, context); this.fill = originalFill; this.stroke = originalStroke; } createClipPathLayer(clipPath, context) { const canvas = createCanvasElementFor(context); const ctx = canvas.getContext("2d"); ctx.translate(context.cacheTranslationX, context.cacheTranslationY); ctx.scale(context.zoomX, context.zoomY); clipPath._cacheCanvas = canvas; context.parentClipPaths.forEach((prevClipPath) => { prevClipPath.transform(ctx); }); context.parentClipPaths.push(clipPath); if (clipPath.absolutePositioned) { const m = invertTransform(this.calcTransformMatrix()); ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); } clipPath.transform(ctx); clipPath.drawObject(ctx, true, context); return canvas; } /** * Prepare clipPath state and cache and draw it on instance's cache * @param {CanvasRenderingContext2D} ctx * @param {FabricObject} clipPath */ _drawClipPath(ctx, clipPath, context) { if (!clipPath) return; clipPath._transformDone = true; const canvas = this.createClipPathLayer(clipPath, context); this.drawClipPathOnCache(ctx, clipPath, canvas); } /** * Paint the cached copy of the object on the target context. * @param {CanvasRenderingContext2D} ctx Context to render on */ drawCacheOnCanvas(ctx) { ctx.scale(1 / this.zoomX, 1 / this.zoomY); ctx.drawImage(this._cacheCanvas, -this.cacheTranslationX, -this.cacheTranslationY); } /** * Check if cache is dirty and if is dirty clear the context. * This check has a big side effect, it changes the underlying cache canvas if necessary. * Do not call this method on your own to check if the cache is dirty, because if it is, * it is also going to wipe the cache. This is badly designed and needs to be fixed. * @param {Boolean} skipCanvas skip canvas checks because this object is painted * on parent canvas. */ isCacheDirty(skipCanvas = false) { if (this.isNotVisible()) return false; const canvas = this._cacheCanvas; const ctx = this._cacheContext; if (canvas && ctx && !skipCanvas && this._updateCacheCanvas()) return true; else if (this.dirty || this.clipPath && this.clipPath.absolutePositioned) { if (canvas && ctx && !skipCanvas) { ctx.save(); ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.restore(); } return true; } return false; } /** * Draws a background for the object big as its untransformed dimensions * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderBackground(ctx) { if (!this.backgroundColor) return; const dim = this._getNonTransformedDimensions(); ctx.fillStyle = this.backgroundColor; ctx.fillRect(-dim.x / 2, -dim.y / 2, dim.x, dim.y); this._removeShadow(ctx); } /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _setOpacity(ctx) { if (this.group && !this.group._transformDone) ctx.globalAlpha = this.getObjectOpacity(); else ctx.globalAlpha *= this.opacity; } _setStrokeStyles(ctx, decl) { const stroke = decl.stroke; if (stroke) { ctx.lineWidth = decl.strokeWidth; ctx.lineCap = decl.strokeLineCap; ctx.lineDashOffset = decl.strokeDashOffset; ctx.lineJoin = decl.strokeLineJoin; ctx.miterLimit = decl.strokeMiterLimit; if (isFiller(stroke)) if (stroke.gradientUnits === "percentage" || stroke.gradientTransform || stroke.patternTransform) this._applyPatternForTransformedGradient(ctx, stroke); else { ctx.strokeStyle = stroke.toLive(ctx); this._applyPatternGradientTransform(ctx, stroke); } else ctx.strokeStyle = decl.stroke; } } _setFillStyles(ctx, { fill }) { if (fill) if (isFiller(fill)) { ctx.fillStyle = fill.toLive(ctx); this._applyPatternGradientTransform(ctx, fill); } else ctx.fillStyle = fill; } _setClippingProperties(ctx) { ctx.globalAlpha = 1; ctx.strokeStyle = "transparent"; ctx.fillStyle = "#000000"; } /** * @private * Sets line dash * @param {CanvasRenderingContext2D} ctx Context to set the dash line on * @param {Array} dashArray array representing dashes */ _setLineDash(ctx, dashArray) { if (!dashArray || dashArray.length === 0) return; ctx.setLineDash(dashArray); } /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _setShadow(ctx) { if (!this.shadow) return; const shadow = this.shadow, canvas = this.canvas, retinaScaling = this.getCanvasRetinaScaling(), [sx, , , sy] = (canvas === null || canvas === void 0 ? void 0 : canvas.viewportTransform) || iMatrix, multX = sx * retinaScaling, multY = sy * retinaScaling, scaling = shadow.nonScaling ? new Point(1, 1) : this.getObjectScaling(); ctx.shadowColor = shadow.color; ctx.shadowBlur = shadow.blur * config.browserShadowBlurConstant * (multX + multY) * (scaling.x + scaling.y) / 4; ctx.shadowOffsetX = shadow.offsetX * multX * scaling.x; ctx.shadowOffsetY = shadow.offsetY * multY * scaling.y; } /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _removeShadow(ctx) { if (!this.shadow) return; ctx.shadowColor = ""; ctx.shadowBlur = ctx.shadowOffsetX = ctx.shadowOffsetY = 0; } /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on * @param {TFiller} filler {@link Pattern} or {@link Gradient} */ _applyPatternGradientTransform(ctx, filler) { if (!isFiller(filler)) return { offsetX: 0, offsetY: 0 }; const t = filler.gradientTransform || filler.patternTransform; const offsetX = -this.width / 2 + filler.offsetX || 0, offsetY = -this.height / 2 + filler.offsetY || 0; if (filler.gradientUnits === "percentage") ctx.transform(this.width, 0, 0, this.height, offsetX, offsetY); else ctx.transform(1, 0, 0, 1, offsetX, offsetY); if (t) ctx.transform(t[0], t[1], t[2], t[3], t[4], t[5]); return { offsetX, offsetY }; } /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderPaintInOrder(ctx) { if (this.paintFirst === "stroke") { this._renderStroke(ctx); this._renderFill(ctx); } else { this._renderFill(ctx); this._renderStroke(ctx); } } /** * @private * function that actually render something on the context. * empty here to allow Obects to work on tests to benchmark fabric functionalites * not related to rendering * @param {CanvasRenderingContext2D} _ctx Context to render on */ _render(_ctx) {} /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderFill(ctx) { if (!this.fill) return; ctx.save(); this._setFillStyles(ctx, this); if (this.fillRule === "evenodd") ctx.fill("evenodd"); else ctx.fill(); ctx.restore(); } /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderStroke(ctx) { if (!this.stroke || this.strokeWidth === 0) return; if (this.shadow && !this.shadow.affectStroke) this._removeShadow(ctx); ctx.save(); if (this.strokeUniform) { const scaling = this.getObjectScaling(); ctx.scale(1 / scaling.x, 1 / scaling.y); } this._setLineDash(ctx, this.strokeDashArray); this._setStrokeStyles(ctx, this); ctx.stroke(); ctx.restore(); } /** * This function try to patch the missing gradientTransform on canvas gradients. * transforming a context to transform the gradient, is going to transform the stroke too. * we want to transform the gradient but not the stroke operation, so we create * a transformed gradient on a pattern and then we use the pattern instead of the gradient. * this method has drawbacks: is slow, is in low resolution, needs a patch for when the size * is limited. * @private * @param {CanvasRenderingContext2D} ctx Context to render on * @param {Gradient} filler */ _applyPatternForTransformedGradient(ctx, filler) { var _pCtx$createPattern; const dims = this._limitCacheSize(this._getCacheCanvasDimensions()), retinaScaling = this.getCanvasRetinaScaling(), width = dims.x / this.scaleX / retinaScaling, height = dims.y / this.scaleY / retinaScaling, pCanvas = createCanvasElementFor({ width: Math.ceil(width), height: Math.ceil(height) }); const pCtx = pCanvas.getContext("2d"); if (!pCtx) return; pCtx.beginPath(); pCtx.moveTo(0, 0); pCtx.lineTo(width, 0); pCtx.lineTo(width, height); pCtx.lineTo(0, height); pCtx.closePath(); pCtx.translate(width / 2, height / 2); pCtx.scale(dims.zoomX / this.scaleX / retinaScaling, dims.zoomY / this.scaleY / retinaScaling); this._applyPatternGradientTransform(pCtx, filler); pCtx.fillStyle = filler.toLive(ctx); pCtx.fill(); ctx.translate(-this.width / 2 - this.strokeWidth / 2, -this.height / 2 - this.strokeWidth / 2); ctx.scale(retinaScaling * this.scaleX / dims.zoomX, retinaScaling * this.scaleY / dims.zoomY); ctx.strokeStyle = (_pCtx$createPattern = pCtx.createPattern(pCanvas, "no-repeat")) !== null && _pCtx$createPattern !== void 0 ? _pCtx$createPattern : ""; } /** * This function is an helper for svg import. it returns the center of the object in the svg * untransformed coordinates * It doesn't matter where the objects origin are, svg has left and top in the top left corner, * And this method is only run once on the object after the fromElement parser. * @private * @return {Point} center point from element coordinates */ _findCenterFromElement() { return new Point(this.left + this.width / 2, this.top + this.height / 2); } /** * Clones an instance. * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output * @returns {Promise<FabricObject>} */ clone(propertiesToInclude) { const objectForm = this.toObject(propertiesToInclude); return this.constructor.fromObject(objectForm); } /** * Creates an instance of Image out of an object * makes use of toCanvasElement. * Once this method was based on toDataUrl and loadImage, so it also had a quality * and format option. toCanvasElement is faster and produce no loss of quality. * If you need to get a real Jpeg or Png from an object, using toDataURL is the right way to do it. * toCanvasElement and then toBlob from the obtained canvas is also a good option. * @todo fix the export type, it could not be Image but the type that getClass return for 'image'. * @param {ObjectToCanvasElementOptions} [options] for clone as image, passed to toDataURL * @param {Number} [options.multiplier=1] Multiplier to scale by * @param {Number} [options.left] Cropping left offset. Introduced in v1.2.14 * @param {Number} [options.top] Cropping top offset. Introduced in v1.2.14 * @param {Number} [options.width] Cropping width. Introduced in v1.2.14 * @param {Number} [options.height] Cropping height. Introduced in v1.2.14 * @param {Boolean} [options.enableRetinaScaling] Enable retina scaling for clone image. Introduce in 1.6.4 * @param {Boolean} [options.withoutTransform] Remove current object transform ( no scale , no angle, no flip, no skew ). Introduced in 2.3.4 * @param {Boolean} [options.withoutShadow] Remove current object shadow. Introduced in 2.4.2 * @return {FabricImage} Object cloned as image. */ cloneAsImage(options) { const canvasEl = this.toCanvasElement(options); return new (classRegistry.getClass("image"))(canvasEl); } /** * Converts an object into a HTMLCanvas element * @param {ObjectToCanvasElementOptions} options Options object * @param {Number} [options.multiplier=1] Multiplier to scale by * @param {Number} [options.left] Cropping left offset. Introduced in v1.2.14 * @param {Number} [options.top] Cropping top offset. Introduced in v1.2.14 * @param {Number} [options.width] Cropping width. Introduced in v1.2.14 * @param {Number} [options.height] Cropping height. Introduced in v1.2.14 * @param {Boolean} [options.enableRetinaScaling] Enable retina scaling for clone image. Introduce in 1.6.4 * @param {Boolean} [options.withoutTransform] Remove current object transform ( no scale , no angle, no flip, no skew ). Introduced in 2.3.4 * @param {Boolean} [options.withoutShadow] Remove current object shadow. Introduced in 2.4.2 * @param {Boolean} [options.viewportTransform] Account for canvas viewport transform * @param {(el?: HTMLCanvasElement) => StaticCanvas} [options.canvasProvider] Create the output canvas * @return {HTMLCanvasElement} Returns DOM element <canvas> with the FabricObject */ toCanvasElement(options = {}) { const origParams = saveObjectTransform(this), originalGroup = this.group, originalShadow = this.shadow, abs = Math.abs, retinaScaling = options.enableRetinaScaling ? getDevicePixelRatio() : 1, multiplier = (options.multiplier || 1) * retinaScaling, canvasProvider = options.canvasProvider || ((el) => new StaticCanvas(el, { enableRetinaScaling: false, renderOnAddRemove: false, skipOffscreen: false })); delete this.group; if (options.withoutTransform) resetObjectTransform(this); if (options.withoutShadow) this.shadow = null; if (options.viewportTransform) sendObjectToPlane(this, this.getViewportTransform()); this.setCoords(); const el = createCanvasElement(), boundingRect = this.getBoundingRect(), shadow = this.shadow, shadowOffset = new Point(); if (shadow) { const shadowBlur = shadow.blur; const scaling = shadow.nonScaling ? new Point(1, 1) : this.getObjectScaling(); shadowOffset.x = 2 * Math.round(abs(shadow.offsetX) + shadowBlur) * abs(scaling.x); shadowOffset.y = 2 * Math.round(abs(shadow.offsetY) + shadowBlur) * abs(scaling.y); } const width = boundingRect.width + shadowOffset.x, height = boundingRect.height + shadowOffset.y; el.width = Math.ceil(width); el.height = Math.ceil(height); const canvas = canvasProvider(el); if (options.format === "jpeg") canvas.backgroundColor = "#fff"; this.setPositionByOrigin(new Point(canvas.width / 2, canvas.height / 2), CENTER, CENTER); const originalCanvas = this.canvas; canvas._objects = [this]; this.set("canvas", canvas); this.setCoords(); const canvasEl = canvas.toCanvasElement(multiplier || 1, options); this.set("canvas", originalCanvas); this.shadow = originalShadow; if (originalGroup) this.group = originalGroup; this.set(origParams); this.setCoords(); canvas._objects = []; canvas.destroy(); return canvasEl; } /** * Converts an object into a data-url-like string * @param {Object} options Options object * @param {String} [options.format=png] The format of the output image. Either "jpeg" or "png" * @param {Number} [options.quality=1] Quality level (0..1). Only used for jpeg. * @param {Number} [options.multiplier=1] Multiplier to scale by * @param {Number} [options.left] Cropping left offset. Introduced in v1.2.14 * @param {Number} [options.top] Cropping top offset. Introduced in v1.2.14 * @param {Number} [options.width] Cropping width. Introduced in v1.2.14 * @param {Number} [options.height] Cropping height. Introduced in v1.2.14 * @param {Boolean} [options.enableRetinaScaling] Enable retina scaling for clone image. Introduce in 1.6.4 * @param {Boolean} [options.withoutTransform] Remove current object transform ( no scale , no angle, no flip, no skew ). Introduced in 2.3.4 * @param {Boolean} [options.withoutShadow] Remove current object shadow. Introduced in 2.4.2 * @return {String} Returns a data: URL containing a representation of the object in the format specified by options.format */ toDataURL(options = {}) { return toDataURL(this.toCanvasElement(options), options.format || "png", options.quality || 1); } toBlob(options = {}) { return toBlob(this.toCanvasElement(options), options.format || "png", options.quality || 1); } /** * Checks if the instance is of any of the specified types. * We use this to filter a list of objects for the `getObjects` function. * * For detecting an instance type `instanceOf` is a better check, * but to avoid to make specific classes a dependency of generic code * internally we use this. * * This compares both the static class `type` and the instance's own `type` property * against the provided list of types. * * @param types - A list of type strings to check against. * @returns `true` if the object's type or class type matches any in the list, otherwise `false`. */ isType(...types) { return types.includes(this.constructor.type) || types.includes(this.type); } /** * Returns complexity of an instance * @return {Number} complexity of this instance (is 1 unless subclassed) */ complexity() { return 1; } /** * Returns a JSON representation of an instance * @return {Object} JSON */ toJSON() { return this.toObject(); } /** * Sets "angle" of an instance with centered rotation * @param {TDegree} angle Angle value (in degrees) */ rotate(angle) { const { centeredRotation, originX, originY } = this; if (centeredRotation) { const { x, y } = this.getRelativeCenterPoint(); this.originX = CENTER; this.originY = CENTER; this.left = x; this.top = y; } this.set("angle", angle); if (centeredRotation) { const { x, y } = this.getPositionByOrigin(originX, originY); this.left = x; this.top = y; this.originX = originX; this.originY = originY; } } /** * This callback function is called by the parent group of an object every * time a non-delegated property changes on the group. It is passed the key * and value as parameters. Not adding in this function's signature to avoid * Travis build error about unused variables. */ setOnGroup() {} /** * Sets canvas globalCompositeOperation for specific object * custom composition operation for the particular object can be specified using globalCompositeOperation property * @param {CanvasRenderingContext2D} ctx Rendering canvas context */ _setupCompositeOperation(ctx) { if (this.globalCompositeOperation) ctx.globalCompositeOperation = this.globalCompositeOperation; } /** * cancel instance's running animations * override if necessary to dispose artifacts such as `clipPath` */ dispose() { runningAnimations.cancelByTarget(this); this.off(); this._set("canvas", void 0); this._cacheCanvas && getEnv().dispose(this._cacheCanvas); this._cacheCanvas = void 0; this._cacheContext = null; } /** * Animates object's properties * @param {Record<string, number | number[] | TColorArg>} animatable map of keys and end values * @param {Partial<AnimationOptions<T>>} options * @see {@link http://fabric5.fabricjs.com/fabric-intro-part-2#animation} * @return {Record<string, TAnimation<T>>} map of animation contexts * * As object — multiple properties * * object.animate({ left: ..., top: ... }); * object.animate({ left: ..., top: ... }, { duration: ... }); */ animate(animatable, options) { return Object.entries(animatable).reduce((acc, [key, endValue]) => { acc[key] = this._animate(key, endValue, options); return acc; }, {}); } /** * @private * @param {String} key Property to animate * @param {String} to Value to animate to * @param {Object} [options] Options object */ _animate(key, endValue, options = {}) { const path = key.split("."); const propIsColor = this.constructor.colorProperties.includes(path[path.length - 1]); const { abort, startValue, onChange, onComplete } = options; const animationOptions = { ...options, target: this, startValue: startValue !== null && startValue !== void 0 ? startValue : path.reduce((deep, key) => deep[key], this), endValue, abort: abort === null || abort === void 0 ? void 0 : abort.bind(this), onChange: (value, valueProgress, durationProgress) => { path.reduce((deep, key, index) => { if (index === path.length - 1) deep[key] = value; return deep[key]; }, this); onChange && onChange(value, valueProgress, durationProgress); }, onComplete: (value, valueProgress, durationProgress) => { this.setCoords(); onComplete && onComplete(value, valueProgress, durationProgress); } }; return propIsColor ? animateColor(animationOptions) : animate(animationOptions); } /** * Checks if object is descendant of target * Should be used instead of {@link Group.contains} or {@link StaticCanvas.contains} for performance reasons * @param {TAncestor} target * @returns {boolean} */ isDescendantOf(target) { const { parent, group } = this; return parent === target || group === target || !!parent && parent.isDescendantOf(target) || !!group && group !== parent && group.isDescendantOf(target); } /** * @returns {Ancestors} ancestors (excluding `ActiveSelection`) from bottom to top */ getAncestors() { const ancestors = []; let parent = this; do { parent = parent.parent; parent && ancestors.push(parent); } while (parent); return ancestors; } /** * Compare ancestors * * @param {StackedObject} other * @returns {AncestryComparison} an object that represent the ancestry situation. */ findCommonAncestors(other) { if (this === other) return { fork: [], otherFork: [], common: [this, ...this.getAncestors()] }; const ancestors = this.getAncestors(); const otherAncestors = other.getAncestors(); if (ancestors.length === 0 && otherAncestors.length > 0 && this === otherAncestors[otherAncestors.length - 1]) return { fork: [], otherFork: [other, ...otherAncestors.slice(0, otherAncestors.length - 1)], common: [this] }; for (let i = 0, ancestor; i < ancestors.length; i++) { ancestor = ancestors[i]; if (ancestor === other) return { fork: [this, ...ancestors.slice(0, i)], otherFork: [], common: ancestors.slice(i) }; for (let j = 0; j < otherAncestors.length; j++) { if (this === otherAncestors[j]) return { fork: [], otherFork: [other, ...otherAncestors.slice(0, j)], common: [this, ...ancestors] }; if (ancestor === otherAncestors[j]) return { fork: [this, ...ancestors.slice(0, i)], otherFork: [other, ...otherAncestors.slice(0, j)], common: ancestors.slice(i) }; } } return { fork: [this, ...ancestors], otherFork: [other, ...otherAncestors], common: [] }; } /** * * @param {StackedObject} other * @returns {boolean} */ hasCommonAncestors(other) { const commonAncestors = this.findCommonAncestors(other); return commonAncestors && !!commonAncestors.common.length; } /** * * @param {FabricObject} other object to compare against * @returns {boolean | undefined} if objects do not share a common ancestor or they are strictly equal it is impossible to determine which is in front of the other; in such cases the function returns `undefined` */ isInFrontOf(other) { if (this === other) return; const ancestorData = this.findCommonAncestors(other); if (ancestorData.fork.includes(other)) return true; if (ancestorData.otherFork.includes(this)) return false; const firstCommonAncestor = ancestorData.common[0] || this.canvas; if (!firstCommonAncestor) return; const headOfFork = ancestorData.fork.pop(), headOfOtherFork = ancestorData.otherFork.pop(), thisIndex = firstCommonAncestor._objects.indexOf(headOfFork), otherIndex = firstCommonAncestor._objects.indexOf(headOfOtherFork); return thisIndex > -1 && thisIndex > otherIndex; } /** * Returns an object representation of an instance * @param {string[]} [propertiesToInclude] Any properties that you might want to additionally include in the output * @return {Object} Object representation of an instance */ toObject(propertiesToInclude = []) { const propertiesToSerialize = propertiesToInclude.concat(FabricObject.customProperties, this.constructor.customProperties || []); let clipPathData; const NUM_FRACTION_DIGITS = config.NUM_FRACTION_DIGITS; const { clipPath, fill, stroke, shadow, strokeDashArray, left, top, originX, originY, width, height, strokeWidth, strokeLineCap, strokeDashOffset, strokeLineJoin, strokeUniform, strokeMiterLimit, scaleX, scaleY, angle, flipX, flipY, opacity, visible, backgroundColor, fillRule, paintFirst, globalCompositeOperation, skewX, skewY } = this; if (clipPath && !clipPath.excludeFromExport) clipPathData = clipPath.toObject(propertiesToSerialize.concat("inverted", "absolutePositioned")); const toFixedBound = (val) => toFixed(val, NUM_FRACTION_DIGITS); const object = { ...pick(this, propertiesToSerialize), type: this.constructor.type, version: VERSION, originX, originY, left: toFixedBound(left), top: toFixedBound(top), width: toFixedBound(width), height: toFixedBound(height), fill: isSerializableFiller(fill) ? fill.toObject() : fill, stroke: isSerializableFiller(stroke) ? stroke.toObject() : stroke, strokeWidth: toFixedBound(strokeWidth), strokeDashArray: strokeDashArray ? strokeDashArray.concat() : strokeDashArray, strokeLineCap, strokeDashOffset, strokeLineJoin, strokeUniform, strokeMiterLimit: toFixedBound(strokeMiterLimit), scaleX: toFixedBound(scaleX), scaleY: toFixedBound(scaleY), angle: toFixedBound(angle), flipX, flipY, opacity: toFixedBound(opacity), shadow: shadow ? shadow.toObject() : shadow, visible, backgroundColor, fillRule, paintFirst, globalCompositeOperation, skewX: toFixedBound(skewX), skewY: toFixedBound(skewY), ...clipPathData ? { clipPath: clipPathData } : null }; return !this.includeDefaultValues ? this._removeDefaultValues(object) : object; } /** * Returns (dataless) object representation of an instance * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output * @return {Object} Object representation of an instance */ toDatalessObject(propertiesToInclude) { return this.toObject(propertiesToInclude); } /** * @private * @param {Object} object */ _removeDefaultValues(object) { const defaults = this.constructor.getDefaults(); const baseValues = Object.keys(defaults).length > 0 ? defaults : Object.getPrototypeOf(this); return pickBy(object, (value, key) => { if (key === "left" || key === "top" || key === "type") return true; const baseValue = baseValues[key]; return value !== baseValue && !(Array.isArray(value) && Array.isArray(baseValue) && value.length === 0 && baseValue.length === 0); }); } /** * Returns a string representation of an instance * @return {String} */ toString() { return `#<${this.constructor.type}>`; } /** * * @param {Function} klass * @param {object} object * @param {object} [options] * @param {string} [options.extraParam] property to pass as first argument to the constructor * @param {AbortSignal} [options.signal] handle aborting, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal * @returns {Promise<FabricObject>} */ static _fromObject({ type, ...serializedObjectOptions }, { extraParam, ...options } = {}) { return enlivenObjectEnlivables(serializedObjectOptions, options).then((enlivedObjectOptions) => { if (extraParam) { delete enlivedObjectOptions[extraParam]; return new this(serializedObjectOptions[extraParam], enlivedObjectOptions); } else return new this(enlivedObjectOptions); }); } /** * * @param {object} object * @param {object} [options] * @param {AbortSignal} [options.signal] handle aborting, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal * @returns {Promise<FabricObject>} */ static fromObject(object, options) { return this._fromObject(object, options); } }; _defineProperty(FabricObject, "stateProperties", stateProperties); _defineProperty(FabricObject, "cacheProperties", cacheProperties); _defineProperty(FabricObject, "ownDefaults", fabricObjectDefaultValues); _defineProperty(FabricObject, "type", "FabricObject"); _defineProperty(FabricObject, "colorProperties", [ FILL, STROKE, "backgroundColor" ]); _defineProperty(FabricObject, "customProperties", []); classRegistry.setClass(FabricObject); classRegistry.setClass(FabricObject, "object"); //#endregion export { FabricObject }; //# sourceMappingURL=Object.mjs.map