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
JavaScript
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