fabric
Version:
Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.
1,633 lines (1,632 loc) • 783 kB
JavaScript
import { JSDOM } from "jsdom";
import utils from "jsdom/lib/jsdom/living/generated/utils.js";
//#region \0rolldown/runtime.js
var __defProp = Object.defineProperty;
var __exportAll = (all, no_symbols) => {
let target = {};
for (var name in all) __defProp(target, name, {
get: all[name],
enumerable: true
});
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
return target;
};
//#endregion
//#region src/config.ts
var BaseConfiguration = class {
/**
* Browser-specific constant to adjust CanvasRenderingContext2D.shadowBlur value,
* which is unitless and not rendered equally across browsers.
*
* Values that work quite well (as of October 2017) are:
* - Chrome: 1.5
* - Edge: 1.75
* - Firefox: 0.9
* - Safari: 0.95
*
* @since 2.0.0
* @type Number
* @default 1
*/
browserShadowBlurConstant = 1;
/**
* Pixel per Inch as a default value set to 96. Can be changed for more realistic conversion.
*/
DPI = 96;
/**
* Device Pixel Ratio
* @see https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/HTML-canvas-guide/SettingUptheCanvas/SettingUptheCanvas.html
*/
devicePixelRatio = typeof window !== "undefined" ? window.devicePixelRatio : 1;
/**
* Pixel limit for cache canvases. 1Mpx , 4Mpx should be fine.
* @since 1.7.14
* @type Number
*/
perfLimitSizeTotal = 2097152;
/**
* Pixel limit for cache canvases width or height. IE fixes the maximum at 5000
* @since 1.7.14
* @type Number
*/
maxCacheSideLimit = 4096;
/**
* Lowest pixel limit for cache canvases, set at 256PX
* @since 1.7.14
* @type Number
*/
minCacheSideLimit = 256;
/**
* When 'true', style information is not retained when copy/pasting text, making
* pasted text use destination style.
* Defaults to 'false'.
* @type Boolean
* @deprecated
*/
disableStyleCopyPaste = false;
/**
* Enable webgl for filtering picture is available
* A filtering backend will be initialized, this will both take memory and
* time since a default 2048x2048 canvas will be created for the gl context
* @since 2.0.0
* @type Boolean
*/
enableGLFiltering = true;
/**
* if webgl is enabled and available, textureSize will determine the size
* of the canvas backend
*
* In order to support old hardware set to `2048` to avoid OOM
*
* @since 2.0.0
* @type Number
*/
textureSize = 4096;
/**
* Skip performance testing of setupGLContext and force the use of putImageData that seems to be the one that works best on
* Chrome + old hardware. if your users are experiencing empty images after filtering you may try to force this to true
* this has to be set before instantiating the filtering backend ( before filtering the first image )
* @type Boolean
* @default false
*/
forceGLPutImageData = false;
/**
* If disabled boundsOfCurveCache is not used. For apps that make heavy usage of pencil drawing probably disabling it is better
* With the standard behaviour of fabric to translate all curves in absolute commands and by not subtracting the starting point from
* the curve is very hard to hit any cache.
* Enable only if you know why it could be useful.
* Candidate for removal/simplification
* @default false
*/
cachesBoundsOfCurve = false;
/**
* Map of font files
* Map<fontFamily, pathToFile> of font files
*/
fontPaths = {};
/**
* Defines the number of fraction digits to use when serializing object values.
* Used in exporting methods (`toObject`, `toJSON`, `toSVG`)
* You can use it to increase/decrease precision of such values like left, top, scaleX, scaleY, etc.
*/
NUM_FRACTION_DIGITS = 4;
};
var Configuration = class extends BaseConfiguration {
constructor(config) {
super();
this.configure(config);
}
configure(config = {}) {
Object.assign(this, config);
}
/**
* Map<fontFamily, pathToFile> of font files
*/
addFonts(paths = {}) {
this.fontPaths = {
...this.fontPaths,
...paths
};
}
removeFonts(fontFamilys = []) {
fontFamilys.forEach((fontFamily) => {
delete this.fontPaths[fontFamily];
});
}
clearFonts() {
this.fontPaths = {};
}
restoreDefaults(keys) {
const defaults = new BaseConfiguration();
const config = keys?.reduce((acc, key) => {
acc[key] = defaults[key];
return acc;
}, {}) || defaults;
this.configure(config);
}
};
const config = new Configuration();
//#endregion
//#region src/util/internals/console.ts
const log = (severity, ...optionalParams) => console[severity]("fabric", ...optionalParams);
var FabricError = class extends Error {
constructor(message, options) {
super(`fabric: ${message}`, options);
}
};
var SignalAbortedError = class extends FabricError {
constructor(context) {
super(`${context} 'options.signal' is in 'aborted' state`);
}
};
//#endregion
//#region src/filters/GLProbes/GLProbe.ts
var GLProbe = class {};
//#endregion
//#region src/filters/GLProbes/WebGLProbe.ts
/**
* Lazy initialize WebGL constants
*/
var WebGLProbe = class extends GLProbe {
/**
* Tests if webgl supports certain precision
* @param {WebGL} Canvas WebGL context to test on
* @param {GLPrecision} Precision to test can be any of following
* @returns {Boolean} Whether the user's browser WebGL supports given precision.
*/
testPrecision(gl, precision) {
const fragmentSource = `precision ${precision} float;\nvoid main(){}`;
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
if (!fragmentShader) return false;
gl.shaderSource(fragmentShader, fragmentSource);
gl.compileShader(fragmentShader);
return !!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS);
}
/**
* query browser for WebGL
*/
queryWebGL(canvas) {
const gl = canvas.getContext("webgl");
if (gl) {
this.maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE);
this.GLPrecision = [
"highp",
"mediump",
"lowp"
].find((precision) => this.testPrecision(gl, precision));
gl.getExtension("WEBGL_lose_context").loseContext();
log("log", `WebGL: max texture size ${this.maxTextureSize}`);
}
}
isSupported(textureSize) {
return !!this.maxTextureSize && this.maxTextureSize >= textureSize;
}
};
//#endregion
//#region src/env/browser.ts
const copyPasteData$1 = {};
const getEnv$2 = () => {
return {
document,
window,
isTouchSupported: "ontouchstart" in window || "ontouchstart" in document || window && window.navigator && window.navigator.maxTouchPoints > 0,
WebGLProbe: new WebGLProbe(),
dispose() {},
copyPasteData: copyPasteData$1
};
};
//#endregion
//#region src/env/index.ts
/**
* This file is consumed by fabric.
* The `./node` and `./browser` files define the env variable that is used by this module.
* The `./browser` module is defined to be the default env and doesn't set the env at all.
* This is done in order to support isomorphic usage for browser and node applications
* since window and document aren't defined at time of import in SSR, we can't set env so we avoid it by deferring to the default env.
*/
let env;
/**
* Sets the environment variables used by fabric.\
* This is exposed for special cases, such as configuring a test environment, and should be used with care.
*
* **CAUTION**: Must be called before using the package.
*
* @example
* <caption>Passing `window` and `document` objects to fabric (in case they are mocked or something)</caption>
* import { getEnv, setEnv } from 'fabric';
* // we want fabric to use the `window` and `document` objects exposed by the environment we are running in.
* setEnv({ ...getEnv(), window, document });
* // done with setup, using fabric is now safe
*/
const setEnv = (value) => {
env = value;
};
/**
* In order to support SSR we **MUST** access the browser env only after the window has loaded
*/
const getEnv = () => env || (env = getEnv$2());
const getFabricDocument = () => getEnv().document;
const getFabricWindow = () => getEnv().window;
/**
* @returns the config value if defined, fallbacks to the environment value
*/
const getDevicePixelRatio = () => Math.max(config.devicePixelRatio ?? getFabricWindow().devicePixelRatio, 1);
//#endregion
//#region src/filters/GLProbes/NodeGLProbe.ts
/**
* @todo GL rendering in node is possible:
* - https://github.com/stackgl/headless-gl
* - https://github.com/akira-cn/node-canvas-webgl
*/
var NodeGLProbe = class extends GLProbe {
queryWebGL() {}
isSupported() {
return false;
}
};
//#endregion
//#region src/env/node.ts
const { implForWrapper: jsdomImplForWrapper } = utils;
const copyPasteData = {};
const { window: JSDOMWindow } = new JSDOM(decodeURIComponent("%3C!DOCTYPE%20html%3E%3Chtml%3E%3Chead%3E%3C%2Fhead%3E%3Cbody%3E%3C%2Fbody%3E%3C%2Fhtml%3E"), {
resources: "usable",
pretendToBeVisual: true
});
const getNodeCanvas = (canvasEl) => {
const impl = jsdomImplForWrapper(canvasEl);
return impl._canvas || impl._image;
};
const dispose = (element) => {
const impl = jsdomImplForWrapper(element);
if (impl) {
impl._image = null;
impl._canvas = null;
impl._currentSrc = null;
impl._attributes = null;
impl._classList = null;
}
};
const getEnv$1 = () => {
return {
document: JSDOMWindow.document,
window: JSDOMWindow,
isTouchSupported: false,
WebGLProbe: new NodeGLProbe(),
dispose,
copyPasteData
};
};
//#endregion
//#region src/cache.ts
var Cache = class {
constructor() {
this.charWidthsCache = /* @__PURE__ */ new Map();
}
/**
* @return {Object} reference to cache
*/
getFontCache({ fontFamily, fontStyle, fontWeight }) {
fontFamily = fontFamily.toLowerCase();
const cache = this.charWidthsCache;
if (!cache.has(fontFamily)) cache.set(fontFamily, /* @__PURE__ */ new Map());
const fontCache = cache.get(fontFamily);
const cacheKey = `${fontStyle.toLowerCase()}_${(fontWeight + "").toLowerCase()}`;
if (!fontCache.has(cacheKey)) fontCache.set(cacheKey, /* @__PURE__ */ new Map());
return fontCache.get(cacheKey);
}
/**
* Clear char widths cache for the given font family or all the cache if no
* fontFamily is specified.
* Use it if you know you are loading fonts in a lazy way and you are not waiting
* for custom fonts to load properly when adding text objects to the canvas.
* If a text object is added when its own font is not loaded yet, you will get wrong
* measurement and so wrong bounding boxes.
* After the font cache is cleared, either change the textObject text content or call
* initDimensions() to trigger a recalculation
* @param {String} [fontFamily] font family to clear
*/
clearFontCache(fontFamily) {
if (!fontFamily) this.charWidthsCache = /* @__PURE__ */ new Map();
else this.charWidthsCache.delete((fontFamily || "").toLowerCase());
}
/**
* Given current aspect ratio, determines the max width and height that can
* respect the total allowed area for the cache.
* @param {number} ar aspect ratio
* @return {number[]} Limited dimensions X and Y
*/
limitDimsByArea(ar) {
const { perfLimitSizeTotal } = config;
const roughWidth = Math.sqrt(perfLimitSizeTotal * ar);
return [Math.floor(roughWidth), Math.floor(perfLimitSizeTotal / roughWidth)];
}
/**
* This object keeps the results of the boundsOfCurve calculation mapped by the joined arguments necessary to calculate it.
* It does speed up calculation, if you parse and add always the same paths, but in case of heavy usage of freedrawing
* you do not get any speed benefit and you get a big object in memory.
* The object was a private variable before, while now is appended to the lib so that you have access to it and you
* can eventually clear it.
* It was an internal variable, is accessible since version 2.3.4
*/
boundsOfCurveCache = {};
};
const cache = new Cache();
//#endregion
//#region src/constants.ts
const VERSION = "7.3.1";
function noop() {}
const halfPI = Math.PI / 2;
const quarterPI = Math.PI / 4;
const twoMathPi = Math.PI * 2;
const PiBy180 = Math.PI / 180;
const iMatrix = Object.freeze([
1,
0,
0,
1,
0,
0
]);
const CENTER = "center";
const LEFT = "left";
const BOTTOM = "bottom";
const RIGHT = "right";
const NONE = "none";
const reNewline = /\r?\n/;
const MOVING = "moving";
const SCALING = "scaling";
const ROTATING = "rotating";
const ROTATE = "rotate";
const SKEWING = "skewing";
const RESIZING = "resizing";
const MODIFY_POLY = "modifyPoly";
const MODIFY_PATH = "modifyPath";
const CHANGED = "changed";
const SCALE = "scale";
const SCALE_X = "scaleX";
const SCALE_Y = "scaleY";
const SKEW_X = "skewX";
const SKEW_Y = "skewY";
const FILL = "fill";
const STROKE = "stroke";
const MODIFIED = "modified";
const NORMAL = "normal";
//#endregion
//#region src/ClassRegistry.ts
const JSON$1 = "json";
var ClassRegistry = class {
constructor() {
this[JSON$1] = /* @__PURE__ */ new Map();
this["svg"] = /* @__PURE__ */ new Map();
}
has(classType) {
return this[JSON$1].has(classType);
}
getClass(classType) {
const constructor = this[JSON$1].get(classType);
if (!constructor) throw new FabricError(`No class registered for ${classType}`);
return constructor;
}
setClass(classConstructor, classType) {
if (classType) this[JSON$1].set(classType, classConstructor);
else {
this[JSON$1].set(classConstructor.type, classConstructor);
this[JSON$1].set(classConstructor.type.toLowerCase(), classConstructor);
}
}
getSVGClass(SVGTagName) {
return this["svg"].get(SVGTagName);
}
setSVGClass(classConstructor, SVGTagName) {
this["svg"].set(SVGTagName ?? classConstructor.type.toLowerCase(), classConstructor);
}
};
const classRegistry = new ClassRegistry();
//#endregion
//#region src/util/animation/AnimationRegistry.ts
/**
* Array holding all running animations
*/
var AnimationRegistry = class extends Array {
/**
* Remove a single animation using an animation context
* @param {AnimationBase} context
*/
remove(context) {
const index = this.indexOf(context);
index > -1 && this.splice(index, 1);
}
/**
* Cancel all running animations on the next frame
*/
cancelAll() {
const animations = this.splice(0);
animations.forEach((animation) => animation.abort());
return animations;
}
/**
* Cancel all running animations attached to a canvas on the next frame
* @param {StaticCanvas} canvas
*/
cancelByCanvas(canvas) {
if (!canvas) return [];
const animations = this.filter((animation) => animation.target === canvas || typeof animation.target === "object" && animation.target?.canvas === canvas);
animations.forEach((animation) => animation.abort());
return animations;
}
/**
* Cancel all running animations for target on the next frame
* @param target
*/
cancelByTarget(target) {
if (!target) return [];
const animations = this.filter((animation) => animation.target === target);
animations.forEach((animation) => animation.abort());
return animations;
}
};
const runningAnimations = new AnimationRegistry();
//#endregion
//#region src/Observable.ts
/**
* @see {@link http://fabric5.fabricjs.com/fabric-intro-part-2#events}
* @see {@link http://fabric5.fabricjs.com/events|Events demo}
*/
var Observable = class {
__eventListeners = {};
on(arg0, handler) {
if (!this.__eventListeners) this.__eventListeners = {};
if (typeof arg0 === "object") {
Object.entries(arg0).forEach(([eventName, handler]) => {
this.on(eventName, handler);
});
return () => this.off(arg0);
} else if (handler) {
const eventName = arg0;
if (!this.__eventListeners[eventName]) this.__eventListeners[eventName] = [];
this.__eventListeners[eventName].push(handler);
return () => this.off(eventName, handler);
} else return () => false;
}
once(arg0, handler) {
if (typeof arg0 === "object") {
const disposers = [];
Object.entries(arg0).forEach(([eventName, handler]) => {
disposers.push(this.once(eventName, handler));
});
return () => disposers.forEach((d) => d());
} else if (handler) {
const disposer = this.on(arg0, function onceHandler(...args) {
handler.call(this, ...args);
disposer();
});
return disposer;
} else return () => false;
}
/**
* @private
* @param {string} eventName
* @param {Function} [handler]
*/
_removeEventListener(eventName, handler) {
if (!this.__eventListeners[eventName]) return;
if (handler) {
const eventListener = this.__eventListeners[eventName];
const index = eventListener.indexOf(handler);
index > -1 && eventListener.splice(index, 1);
} else this.__eventListeners[eventName] = [];
}
off(arg0, handler) {
if (!this.__eventListeners) return;
if (typeof arg0 === "undefined") for (const eventName in this.__eventListeners) this._removeEventListener(eventName);
else if (typeof arg0 === "object") Object.entries(arg0).forEach(([eventName, handler]) => {
this._removeEventListener(eventName, handler);
});
else this._removeEventListener(arg0, handler);
}
/**
* Fires event with an optional options object
* @param {String} eventName Event name to fire
* @param {Object} [options] Options object
*/
fire(eventName, options) {
if (!this.__eventListeners) return;
const listenersForEvent = this.__eventListeners[eventName]?.concat();
if (listenersForEvent) for (let i = 0; i < listenersForEvent.length; i++) listenersForEvent[i].call(this, options || {});
}
};
//#endregion
//#region src/util/internals/removeFromArray.ts
/**
* Removes value from an array.
* Presence of value (and its position in an array) is determined via `Array.prototype.indexOf`
* @param {Array} array
* @param {*} value
* @return {Array} original array
*/
const removeFromArray = (array, value) => {
const idx = array.indexOf(value);
if (idx !== -1) array.splice(idx, 1);
return array;
};
//#endregion
//#region src/util/misc/cos.ts
/**
* Calculate the cos of an angle, avoiding returning floats for known results
* This function is here just to avoid getting 0.999999999999999 when dealing
* with numbers that are really 1 or 0.
* @param {TRadian} angle the angle
* @return {Number} the cosin value for angle.
*/
const cos = (angle) => {
if (angle === 0) return 1;
switch (Math.abs(angle) / halfPI) {
case 1:
case 3: return 0;
case 2: return -1;
}
return Math.cos(angle);
};
//#endregion
//#region src/util/misc/sin.ts
/**
* Calculate the cos of an angle, avoiding returning floats for known results
* This function is here just to avoid getting 0.999999999999999 when dealing
* with numbers that are really 1 or 0.
* @param {TRadian} angle the angle
* @return {Number} the sin value for angle.
*/
const sin = (angle) => {
if (angle === 0) return 0;
const angleSlice = angle / halfPI;
const value = Math.sign(angle);
switch (angleSlice) {
case 1: return value;
case 2: return 0;
case 3: return -value;
}
return Math.sin(angle);
};
//#endregion
//#region src/Point.ts
/**
* Adaptation of work of Kevin Lindsey(kevin@kevlindev.com)
*/
var Point = class Point {
constructor(arg0 = 0, y = 0) {
if (typeof arg0 === "object") {
this.x = arg0.x;
this.y = arg0.y;
} else {
this.x = arg0;
this.y = y;
}
}
/**
* Adds another point to this one and returns a new one with the sum
* @param {XY} that
* @return {Point} new Point instance with added values
*/
add(that) {
return new Point(this.x + that.x, this.y + that.y);
}
/**
* Adds another point to this one
* @param {XY} that
* @return {Point} thisArg
* @deprecated
*/
addEquals(that) {
this.x += that.x;
this.y += that.y;
return this;
}
/**
* Adds value to this point and returns a new one
* @param {Number} scalar
* @return {Point} new Point with added value
*/
scalarAdd(scalar) {
return new Point(this.x + scalar, this.y + scalar);
}
/**
* Adds value to this point
* @param {Number} scalar
* @return {Point} thisArg
* @deprecated
*/
scalarAddEquals(scalar) {
this.x += scalar;
this.y += scalar;
return this;
}
/**
* Subtracts another point from this point and returns a new one
* @param {XY} that
* @return {Point} new Point object with subtracted values
*/
subtract(that) {
return new Point(this.x - that.x, this.y - that.y);
}
/**
* Subtracts another point from this point
* @param {XY} that
* @return {Point} thisArg
* @deprecated
*/
subtractEquals(that) {
this.x -= that.x;
this.y -= that.y;
return this;
}
/**
* Subtracts value from this point and returns a new one
* @param {Number} scalar
* @return {Point}
*/
scalarSubtract(scalar) {
return new Point(this.x - scalar, this.y - scalar);
}
/**
* Subtracts value from this point
* @param {Number} scalar
* @return {Point} thisArg
* @deprecated
*/
scalarSubtractEquals(scalar) {
this.x -= scalar;
this.y -= scalar;
return this;
}
/**
* Multiplies this point by another value and returns a new one
* @param {XY} that
* @return {Point}
*/
multiply(that) {
return new Point(this.x * that.x, this.y * that.y);
}
/**
* Multiplies this point by a value and returns a new one
* @param {Number} scalar
* @return {Point}
*/
scalarMultiply(scalar) {
return new Point(this.x * scalar, this.y * scalar);
}
/**
* Multiplies this point by a value
* @param {Number} scalar
* @return {Point} thisArg
* @deprecated
*/
scalarMultiplyEquals(scalar) {
this.x *= scalar;
this.y *= scalar;
return this;
}
/**
* Divides this point by another and returns a new one
* @param {XY} that
* @return {Point}
*/
divide(that) {
return new Point(this.x / that.x, this.y / that.y);
}
/**
* Divides this point by a value and returns a new one
* @param {Number} scalar
* @return {Point}
*/
scalarDivide(scalar) {
return new Point(this.x / scalar, this.y / scalar);
}
/**
* Divides this point by a value
* @param {Number} scalar
* @return {Point} thisArg
* @deprecated
*/
scalarDivideEquals(scalar) {
this.x /= scalar;
this.y /= scalar;
return this;
}
/**
* Returns true if this point is equal to another one
* @param {XY} that
* @return {Boolean}
*/
eq(that) {
return this.x === that.x && this.y === that.y;
}
/**
* Returns true if this point is less than another one
* @param {XY} that
* @return {Boolean}
*/
lt(that) {
return this.x < that.x && this.y < that.y;
}
/**
* Returns true if this point is less than or equal to another one
* @param {XY} that
* @return {Boolean}
*/
lte(that) {
return this.x <= that.x && this.y <= that.y;
}
/**
* Returns true if this point is greater another one
* @param {XY} that
* @return {Boolean}
*/
gt(that) {
return this.x > that.x && this.y > that.y;
}
/**
* Returns true if this point is greater than or equal to another one
* @param {XY} that
* @return {Boolean}
*/
gte(that) {
return this.x >= that.x && this.y >= that.y;
}
/**
* Returns new point which is the result of linear interpolation with this one and another one
* @param {XY} that
* @param {Number} t , position of interpolation, between 0 and 1 default 0.5
* @return {Point}
*/
lerp(that, t = .5) {
t = Math.max(Math.min(1, t), 0);
return new Point(this.x + (that.x - this.x) * t, this.y + (that.y - this.y) * t);
}
/**
* Returns distance from this point and another one
* @param {XY} that
* @return {Number}
*/
distanceFrom(that) {
const dx = this.x - that.x, dy = this.y - that.y;
return Math.sqrt(dx * dx + dy * dy);
}
/**
* Returns the point between this point and another one
* @param {XY} that
* @return {Point}
*/
midPointFrom(that) {
return this.lerp(that);
}
/**
* Returns a new point which is the min of this and another one
* @param {XY} that
* @return {Point}
*/
min(that) {
return new Point(Math.min(this.x, that.x), Math.min(this.y, that.y));
}
/**
* Returns a new point which is the max of this and another one
* @param {XY} that
* @return {Point}
*/
max(that) {
return new Point(Math.max(this.x, that.x), Math.max(this.y, that.y));
}
/**
* Returns string representation of this point
* @return {String}
*/
toString() {
return `${this.x},${this.y}`;
}
/**
* Sets x/y of this point
* @param {Number} x
* @param {Number} y
*/
setXY(x, y) {
this.x = x;
this.y = y;
return this;
}
/**
* Sets x of this point
* @param {Number} x
*/
setX(x) {
this.x = x;
return this;
}
/**
* Sets y of this point
* @param {Number} y
*/
setY(y) {
this.y = y;
return this;
}
/**
* Sets x/y of this point from another point
* @param {XY} that
*/
setFromPoint(that) {
this.x = that.x;
this.y = that.y;
return this;
}
/**
* Swaps x/y of this point and another point
* @param {XY} that
*/
swap(that) {
const x = this.x, y = this.y;
this.x = that.x;
this.y = that.y;
that.x = x;
that.y = y;
}
/**
* return a cloned instance of the point
* @return {Point}
*/
clone() {
return new Point(this.x, this.y);
}
/**
* Rotates `point` around `origin` with `radians`
* @param {XY} origin The origin of the rotation
* @param {TRadian} radians The radians of the angle for the rotation
* @return {Point} The new rotated point
*/
rotate(radians, origin = ZERO) {
const sinus = sin(radians), cosinus = cos(radians);
const p = this.subtract(origin);
return new Point(p.x * cosinus - p.y * sinus, p.x * sinus + p.y * cosinus).add(origin);
}
/**
* Apply transform t to point p
* @param {TMat2D} t The transform
* @param {Boolean} [ignoreOffset] Indicates that the offset should not be applied
* @return {Point} The transformed point
*/
transform(t, ignoreOffset = false) {
return new Point(t[0] * this.x + t[2] * this.y + (ignoreOffset ? 0 : t[4]), t[1] * this.x + t[3] * this.y + (ignoreOffset ? 0 : t[5]));
}
};
const ZERO = new Point(0, 0);
//#endregion
//#region src/Collection.ts
const isCollection = (fabricObject) => {
return !!fabricObject && Array.isArray(fabricObject._objects);
};
function createCollectionMixin(Base) {
class Collection extends Base {
/**
* @type {FabricObject[]}
* @TODO needs to end up in the constructor too
*/
_objects = [];
_onObjectAdded(object) {}
_onObjectRemoved(object) {}
_onStackOrderChanged(object) {}
/**
* Adds objects to collection
* Objects should be instances of (or inherit from) FabricObject
* @param {...FabricObject[]} objects to add
* @returns {number} new array length
*/
add(...objects) {
const size = this._objects.push(...objects);
objects.forEach((object) => this._onObjectAdded(object));
return size;
}
/**
* Inserts an object into collection at specified index
* @param {number} index Index to insert object at
* @param {...FabricObject[]} objects Object(s) to insert
* @returns {number} new array length
*/
insertAt(index, ...objects) {
this._objects.splice(index, 0, ...objects);
objects.forEach((object) => this._onObjectAdded(object));
return this._objects.length;
}
/**
* Removes objects from a collection, then renders canvas (if `renderOnAddRemove` is not `false`)
* @private
* @param {...FabricObject[]} objects objects to remove
* @returns {FabricObject[]} removed objects
*/
remove(...objects) {
const array = this._objects, removed = [];
objects.forEach((object) => {
const index = array.indexOf(object);
if (index !== -1) {
array.splice(index, 1);
removed.push(object);
this._onObjectRemoved(object);
}
});
return removed;
}
/**
* Executes given function for each object in this group
* A simple shortcut for getObjects().forEach, before es6 was more complicated,
* now is just a shortcut.
* @param {Function} callback
* Callback invoked with current object as first argument,
* index - as second and an array of all objects - as third.
*/
forEachObject(callback) {
this.getObjects().forEach((object, index, objects) => callback(object, index, objects));
}
/**
* Returns an array of children objects of this instance
* @param {...String} [types] When specified, only objects of these types are returned
* @return {Array}
*/
getObjects(...types) {
if (types.length === 0) return [...this._objects];
return this._objects.filter((o) => o.isType(...types));
}
/**
* Returns object at specified index
* @param {Number} index
* @return {Object} object at index
*/
item(index) {
return this._objects[index];
}
/**
* Returns true if collection contains no objects
* @return {Boolean} true if collection is empty
*/
isEmpty() {
return this._objects.length === 0;
}
/**
* Returns a size of a collection (i.e: length of an array containing its objects)
* @return {Number} Collection size
*/
size() {
return this._objects.length;
}
/**
* Returns true if collection contains an object.\
* **Prefer using {@link FabricObject#isDescendantOf} for performance reasons**
* instead of `a.contains(b)` use `b.isDescendantOf(a)`
* @param {Object} object Object to check against
* @param {Boolean} [deep=false] `true` to check all descendants, `false` to check only `_objects`
* @return {Boolean} `true` if collection contains an object
*/
contains(object, deep) {
if (this._objects.includes(object)) return true;
else if (deep) return this._objects.some((obj) => obj instanceof Collection && obj.contains(object, true));
return false;
}
/**
* Returns number representation of a collection complexity
* @return {Number} complexity
*/
complexity() {
return this._objects.reduce((memo, current) => {
memo += current.complexity ? current.complexity() : 0;
return memo;
}, 0);
}
/**
* Moves an object or the objects of a multiple selection
* to the bottom of the stack of drawn objects
* @param {fabric.Object} object Object to send to back
* @returns {boolean} true if change occurred
*/
sendObjectToBack(object) {
if (!object || object === this._objects[0]) return false;
removeFromArray(this._objects, object);
this._objects.unshift(object);
this._onStackOrderChanged(object);
return true;
}
/**
* Moves an object or the objects of a multiple selection
* to the top of the stack of drawn objects
* @param {fabric.Object} object Object to send
* @returns {boolean} true if change occurred
*/
bringObjectToFront(object) {
if (!object || object === this._objects[this._objects.length - 1]) return false;
removeFromArray(this._objects, object);
this._objects.push(object);
this._onStackOrderChanged(object);
return true;
}
/**
* Moves an object or a selection down in stack of drawn objects
* An optional parameter, `intersecting` allows to move the object in behind
* the first intersecting object. Where intersection is calculated with
* bounding box. If no intersection is found, there will not be change in the
* stack.
* @param {fabric.Object} object Object to send
* @param {boolean} [intersecting] If `true`, send object behind next lower intersecting object
* @returns {boolean} true if change occurred
*/
sendObjectBackwards(object, intersecting) {
if (!object) return false;
const idx = this._objects.indexOf(object);
if (idx !== 0) {
const newIdx = this.findNewLowerIndex(object, idx, intersecting);
removeFromArray(this._objects, object);
this._objects.splice(newIdx, 0, object);
this._onStackOrderChanged(object);
return true;
}
return false;
}
/**
* Moves an object or a selection up in stack of drawn objects
* An optional parameter, intersecting allows to move the object in front
* of the first intersecting object. Where intersection is calculated with
* bounding box. If no intersection is found, there will not be change in the
* stack.
* @param {fabric.Object} object Object to send
* @param {boolean} [intersecting] If `true`, send object in front of next upper intersecting object
* @returns {boolean} true if change occurred
*/
bringObjectForward(object, intersecting) {
if (!object) return false;
const idx = this._objects.indexOf(object);
if (idx !== this._objects.length - 1) {
const newIdx = this.findNewUpperIndex(object, idx, intersecting);
removeFromArray(this._objects, object);
this._objects.splice(newIdx, 0, object);
this._onStackOrderChanged(object);
return true;
}
return false;
}
/**
* Moves an object to specified level in stack of drawn objects
* @param {fabric.Object} object Object to send
* @param {number} index Position to move to
* @returns {boolean} true if change occurred
*/
moveObjectTo(object, index) {
if (object === this._objects[index]) return false;
removeFromArray(this._objects, object);
this._objects.splice(index, 0, object);
this._onStackOrderChanged(object);
return true;
}
findNewLowerIndex(object, idx, intersecting) {
let newIdx;
if (intersecting) {
newIdx = idx;
for (let i = idx - 1; i >= 0; --i) if (object.isOverlapping(this._objects[i])) {
newIdx = i;
break;
}
} else newIdx = idx - 1;
return newIdx;
}
findNewUpperIndex(object, idx, intersecting) {
let newIdx;
if (intersecting) {
newIdx = idx;
for (let i = idx + 1; i < this._objects.length; ++i) if (object.isOverlapping(this._objects[i])) {
newIdx = i;
break;
}
} else newIdx = idx + 1;
return newIdx;
}
/**
* Given a bounding box, return all the objects of the collection that are contained in the bounding box.
* If `includeIntersecting` is true, return also the objects that intersect the bounding box as well.
* This is meant to work with selection. Is not a generic method.
* @param {TBBox} bbox a bounding box in scene coordinates
* @param {{ includeIntersecting?: boolean }} options an object with includeIntersecting
* @returns array of objects contained in the bounding box, ordered from top to bottom stacking wise
*/
collectObjects({ left, top, width, height }, { includeIntersecting = true } = {}) {
const objects = [], tl = new Point(left, top), br = tl.add(new Point(width, height));
for (let i = this._objects.length - 1; i >= 0; i--) {
const object = this._objects[i];
if (object.selectable && object.visible && (includeIntersecting && object.intersectsWithRect(tl, br) || object.isContainedWithinRect(tl, br) || includeIntersecting && object.containsPoint(tl) || includeIntersecting && object.containsPoint(br))) objects.push(object);
}
return objects;
}
}
return Collection;
}
//#endregion
//#region src/CommonMethods.ts
var CommonMethods = class extends Observable {
/**
* Sets object's properties from options, for initialization only
* @protected
* @param {Object} [options] Options object
*/
_setOptions(options = {}) {
for (const prop in options) this.set(prop, options[prop]);
}
/**
* @private
*/
_setObject(obj) {
for (const prop in obj) this._set(prop, obj[prop]);
}
/**
* Sets property to a given value. When changing position/dimension -related properties (left, top, scale, angle, etc.) `set` does not update position of object's borders/controls. If you need to update those, call `setCoords()`.
* @param {String|Object} key Property name or object (if object, iterate over the object properties)
* @param {Object|Function} value Property value (if function, the value is passed into it and its return value is used as a new one)
*/
set(key, value) {
if (typeof key === "object") this._setObject(key);
else this._set(key, value);
return this;
}
_set(key, value) {
this[key] = value;
}
/**
* Toggles specified property from `true` to `false` or from `false` to `true`
* @param {String} property Property to toggle
*/
toggle(property) {
const value = this.get(property);
if (typeof value === "boolean") this.set(property, !value);
return this;
}
/**
* Basic getter
* @param {String} property Property name
* @return {*} value of a property
*/
get(property) {
return this[property];
}
};
//#endregion
//#region src/util/animation/AnimationFrameProvider.ts
function requestAnimFrame(callback) {
return getFabricWindow().requestAnimationFrame(callback);
}
function cancelAnimFrame(handle) {
return getFabricWindow().cancelAnimationFrame(handle);
}
//#endregion
//#region src/util/internals/uid.ts
let id = 0;
const uid = () => id++;
//#endregion
//#region src/util/misc/dom.ts
/**
* Creates canvas element
* @return {CanvasElement} initialized canvas element
*/
const createCanvasElement = () => {
const element = getFabricDocument().createElement("canvas");
if (!element || typeof element.getContext === "undefined") throw new FabricError("Failed to create `canvas` element");
return element;
};
/**
* Creates image element (works on client and node)
* @return {HTMLImageElement} HTML image element
*/
const createImage = () => getFabricDocument().createElement("img");
/**
* Creates a canvas element that is a copy of another and is also painted
* @param {CanvasElement} canvas to copy size and content of
* @return {CanvasElement} initialized canvas element
*/
const copyCanvasElement = (canvas) => {
const newCanvas = createCanvasElementFor(canvas);
newCanvas.getContext("2d")?.drawImage(canvas, 0, 0);
return newCanvas;
};
/**
* Creates a canvas element as big as another
* @param {CanvasElement} canvas to copy size and content of
* @return {CanvasElement} initialized canvas element
*/
const createCanvasElementFor = (canvas) => {
const newCanvas = createCanvasElement();
newCanvas.width = canvas.width;
newCanvas.height = canvas.height;
return newCanvas;
};
/**
* since 2.6.0 moved from canvas instance to utility.
* possibly useless
* @param {CanvasElement} canvasEl to copy size and content of
* @param {String} format 'jpeg' or 'png', in some browsers 'webp' is ok too
* @param {number} quality <= 1 and > 0
* @return {String} data url
*/
const toDataURL = (canvasEl, format, quality) => canvasEl.toDataURL(`image/${format}`, quality);
const isHTMLCanvas = (canvas) => {
return !!canvas && canvas.getContext !== void 0;
};
const toBlob = (canvasEl, format, quality) => new Promise((resolve, _) => {
canvasEl.toBlob(resolve, `image/${format}`, quality);
});
//#endregion
//#region src/util/misc/radiansDegreesConversion.ts
/**
* Transforms degrees to radians.
* @param {TDegree} degrees value in degrees
* @return {TRadian} value in radians
*/
const degreesToRadians = (degrees) => degrees * PiBy180;
/**
* Transforms radians to degrees.
* @param {TRadian} radians value in radians
* @return {TDegree} value in degrees
*/
const radiansToDegrees = (radians) => radians / PiBy180;
//#endregion
//#region src/util/misc/matrix.ts
const isIdentityMatrix = (mat) => mat.every((value, index) => value === iMatrix[index]);
/**
* Apply transform t to point p
* @deprecated use {@link Point#transform}
* @param {Point | XY} p The point to transform
* @param {Array} t The transform
* @param {Boolean} [ignoreOffset] Indicates that the offset should not be applied
* @return {Point} The transformed point
*/
const transformPoint = (p, t, ignoreOffset) => new Point(p).transform(t, ignoreOffset);
/**
* Invert transformation t
* @param {Array} t The transform
* @return {Array} The inverted transform
*/
const invertTransform = (t) => {
const a = 1 / (t[0] * t[3] - t[1] * t[2]), r = [
a * t[3],
-a * t[1],
-a * t[2],
a * t[0],
0,
0
], { x, y } = new Point(t[4], t[5]).transform(r, true);
r[4] = -x;
r[5] = -y;
return r;
};
/**
* Multiply matrix A by matrix B to nest transformations
* @param {TMat2D} a First transformMatrix
* @param {TMat2D} b Second transformMatrix
* @param {Boolean} is2x2 flag to multiply matrices as 2x2 matrices
* @return {TMat2D} The product of the two transform matrices
*/
const multiplyTransformMatrices = (a, b, is2x2) => [
a[0] * b[0] + a[2] * b[1],
a[1] * b[0] + a[3] * b[1],
a[0] * b[2] + a[2] * b[3],
a[1] * b[2] + a[3] * b[3],
is2x2 ? 0 : a[0] * b[4] + a[2] * b[5] + a[4],
is2x2 ? 0 : a[1] * b[4] + a[3] * b[5] + a[5]
];
/**
* Multiplies the matrices array such that a matrix defines the plane for the rest of the matrices **after** it
*
* `multiplyTransformMatrixArray([A, B, C, D])` is equivalent to `A(B(C(D)))`
*
* @param matrices an array of matrices
* @param [is2x2] flag to multiply matrices as 2x2 matrices
* @returns the multiplication product
*/
const multiplyTransformMatrixArray = (matrices, is2x2) => matrices.reduceRight((product, curr) => curr && product ? multiplyTransformMatrices(curr, product, is2x2) : curr || product, void 0) || iMatrix.concat();
const calcPlaneRotation = ([a, b]) => Math.atan2(b, a);
/**
* Decomposes standard 2x3 matrix into transform components
* @param {TMat2D} a transformMatrix
* @return {Object} Components of transform
*/
const qrDecompose = (a) => {
const angle = calcPlaneRotation(a), denom = Math.pow(a[0], 2) + Math.pow(a[1], 2), scaleX = Math.sqrt(denom), scaleY = (a[0] * a[3] - a[2] * a[1]) / scaleX, skewX = Math.atan2(a[0] * a[2] + a[1] * a[3], denom);
return {
angle: radiansToDegrees(angle),
scaleX,
scaleY,
skewX: radiansToDegrees(skewX),
skewY: 0,
translateX: a[4] || 0,
translateY: a[5] || 0
};
};
/**
* Generate a translation matrix
*
* A translation matrix in the form of
* [ 1 0 x ]
* [ 0 1 y ]
* [ 0 0 1 ]
*
* See {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform#translate} for more details
*
* @param {number} x translation on X axis
* @param {number} [y] translation on Y axis
* @returns {TMat2D} matrix
*/
const createTranslateMatrix = (x, y = 0) => [
1,
0,
0,
1,
x,
y
];
/**
* Generate a rotation matrix around around a point (x,y), defaulting to (0,0)
*
* A matrix in the form of
* [cos(a) -sin(a) -x*cos(a)+y*sin(a)+x]
* [sin(a) cos(a) -x*sin(a)-y*cos(a)+y]
* [0 0 1 ]
*
*
* @param {TDegree} angle rotation in degrees
* @param {XY} [pivotPoint] pivot point to rotate around
* @returns {TMat2D} matrix
*/
function createRotateMatrix({ angle = 0 } = {}, { x = 0, y = 0 } = {}) {
const angleRadiant = degreesToRadians(angle), cosValue = cos(angleRadiant), sinValue = sin(angleRadiant);
return [
cosValue,
sinValue,
-sinValue,
cosValue,
x ? x - (cosValue * x - sinValue * y) : 0,
y ? y - (sinValue * x + cosValue * y) : 0
];
}
/**
* Generate a scale matrix around the point (0,0)
*
* A matrix in the form of
* [x 0 0]
* [0 y 0]
* [0 0 1]
*
* {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform#scale}
*
* @param {number} x scale on X axis
* @param {number} [y] scale on Y axis
* @returns {TMat2D} matrix
*/
const createScaleMatrix = (x, y = x) => [
x,
0,
0,
y,
0,
0
];
const angleToSkew = (angle) => Math.tan(degreesToRadians(angle));
/**
* Generate a skew matrix for the X axis
*
* A matrix in the form of
* [1 x 0]
* [0 1 0]
* [0 0 1]
*
* {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform#skewx}
*
* @param {TDegree} skewValue translation on X axis
* @returns {TMat2D} matrix
*/
const createSkewXMatrix = (skewValue) => [
1,
0,
angleToSkew(skewValue),
1,
0,
0
];
/**
* Generate a skew matrix for the Y axis
*
* A matrix in the form of
* [1 0 0]
* [y 1 0]
* [0 0 1]
*
* {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform#skewy}
*
* @param {TDegree} skewValue translation on Y axis
* @returns {TMat2D} matrix
*/
const createSkewYMatrix = (skewValue) => [
1,
angleToSkew(skewValue),
0,
1,
0,
0
];
/**
* Returns a transform matrix starting from an object of the same kind of
* the one returned from qrDecompose, useful also if you want to calculate some
* transformations from an object that is not enlived yet.
* is called DimensionsTransformMatrix because those properties are the one that influence
* the size of the resulting box of the object.
* @param {Object} options
* @param {Number} [options.scaleX]
* @param {Number} [options.scaleY]
* @param {Boolean} [options.flipX]
* @param {Boolean} [options.flipY]
* @param {Number} [options.skewX]
* @param {Number} [options.skewY]
* @return {Number[]} transform matrix
*/
const calcDimensionsMatrix = ({ scaleX = 1, scaleY = 1, flipX = false, flipY = false, skewX = 0, skewY = 0 }) => {
let matrix = createScaleMatrix(flipX ? -scaleX : scaleX, flipY ? -scaleY : scaleY);
if (skewX) matrix = multiplyTransformMatrices(matrix, createSkewXMatrix(skewX), true);
if (skewY) matrix = multiplyTransformMatrices(matrix, createSkewYMatrix(skewY), true);
return matrix;
};
/**
* Returns a transform matrix starting from an object of the same kind of
* the one returned from qrDecompose, useful also if you want to calculate some
* transformations from an object that is not enlived yet
* Before changing this function look at: src/benchmarks/calcTransformMatrix.mjs
* @param {Object} options
* @param {Number} [options.angle]
* @param {Number} [options.scaleX]
* @param {Number} [options.scaleY]
* @param {Boolean} [options.flipX]
* @param {Boolean} [options.flipY]
* @param {Number} [options.skewX]
* @param {Number} [options.skewY]
* @param {Number} [options.translateX]
* @param {Number} [options.translateY]
* @return {Number[]} transform matrix
*/
const composeMatrix = (options) => {
const { translateX = 0, translateY = 0, angle = 0 } = options;
let matrix = createTranslateMatrix(translateX, translateY);
if (angle) matrix = multiplyTransformMatrices(matrix, createRotateMatrix({ angle }));
const scaleMatrix = calcDimensionsMatrix(options);
if (!isIdentityMatrix(scaleMatrix)) matrix = multiplyTransformMatrices(matrix, scaleMatrix);
return matrix;
};
//#endregion
//#region src/util/misc/objectEnlive.ts
/**
* Loads image element from given url and resolve it, or catch.
* @param {String} url URL representing an image
* @param {LoadImageOptions} [options] image loading options
* @returns {Promise<HTMLImageElement>} the loaded image.
*/
const loadImage = (url, { signal, crossOrigin = null } = {}) => new Promise(function(resolve, reject) {
if (signal && signal.aborted) return reject(new SignalAbortedError("loadImage"));
const img = createImage();
let abort;
if (signal) {
abort = function(err) {
img.src = "";
reject(err);
};
signal.addEventListener("abort", abort, { once: true });
}
const done = function() {
img.onload = img.onerror = null;
abort && signal?.removeEventListener("abort", abort);
resolve(img);
};
if (!url) {
done();
return;
}
img.onload = done;
img.onerror = function() {
abort && signal?.removeEventListener("abort", abort);
reject(new FabricError(`Error loading ${img.src}`));
};
crossOrigin && (img.crossOrigin = crossOrigin);
img.src = url;
});
/**
* @TODO type this correctly.
* Creates corresponding fabric instances from their object representations
* @param {Object[]} objects Objects to enliven
* @param {EnlivenObjectOptions} [options]
* @param {(serializedObj: object, instance: FabricObject) => any} [options.reviver] Method for further parsing of object elements,
* called after each fabric object created.
* @param {AbortSignal} [options.signal] handle aborting, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal
* @returns {Promise<FabricObject[]>}
*/
const enlivenObjects = (objects, { signal, reviver = noop } = {}) => new Promise((resolve, reject) => {
const instances = [];
signal && signal.addEventListener("abort", reject, { once: true });
Promise.allSettled(objects.map((obj) => classRegistry.getClass(obj.type).fromObject(obj, { signal }))).then(async (elementsResult) => {
for (const [index, result] of elementsResult.entries()) {
if (result.status === "fulfilled") {
await reviver(objects[index], result.value);
instances.push(result.value);
}
if (result.status === "rejected") {
const fallback = await reviver(objects[index], void 0, result.reason);
if (fallback) instances.push(fallback);
}
}
resolve(instances);
}).catch((error) => {
instances.forEach((instance) => {
instance.dispose && instance.dispose();
});
reject(error);
}).finally(() => {
signal && signal.removeEventListener("abort", reject);
});
});
/**
* Creates corresponding fabric instances residing in an object, e.g. `clipPath`
* @param {Object} object with properties to enlive ( fill, stroke, clipPath, path )
* @param {object} [options]
* @param {AbortSignal} [options.signal] handle aborting, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal
* @returns {Promise<Record<string, FabricObject | TFiller | null>>} the input object with enlived values
*/
const enlivenObjectEnlivables = (serializedObject, { signal } = {}) => new Promise((resolve, reject) => {
const instances = [];
signal && signal.addEventListener("abort", reject, { once: true });
const promises = Object.values(serializedObject).map((value) => {
if (!value) return value;
/**
* clipPath or shadow or gradient or text on a path or a pattern,
* or the backgroundImage or overlayImage of canvas
* If we have a type and there is a classe registered for it, we enlive it.
* If there is no class registered for it we return the value as is
* */
if (value.type && classRegistry.has(value.type)) return enlivenObjects([value], { signal }).then(([enlived]) => {
instances.push(enlived);
return enlived;
});
return value;
});
const keys = Object.keys(serializedObject);
Promise.all(promises).then((enlived) => {
return enlived.reduce((acc, instance, index) => {
acc[keys[index]] = instance;
return acc;
}, {});
}).then(resolve).catch((error) => {
instances.forEach((instance) => {
instance.dispose && instance.dispose();
});
reject(error);
}).finally(() => {
signal && signal.removeEventListener("abort", reject);
});
});
//#endregion
//#region src/util/misc/pick.ts
/**
* Populates an object with properties of another object
* @param {Object} source Source object
* @param {string[]} properties Properties names to include
* @returns object populated with the picked keys
*/
const pick = (source, keys = []) => {
return keys.reduce((o, key) => {
if (key in source) o[key] = source[key];
return o;
}, {});
};
const pickBy = (source, predicate) => {
return Object.keys(source).reduce((o, key) => {
if (predicate(source[key], key, source)) o[key] = source[key];
return o;
}, {});
};
//#endregion
//#region src/util/misc/toFixed.ts
/**
* A wrapper around Number#toFixed, which contrary