UNPKG

fabric

Version:

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

1,611 lines (1,610 loc) 786 kB
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); //#region \0rolldown/runtime.js var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; 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; }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) { key = keys[i]; if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: ((k) => from[k]).bind(null, key), enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod)); //#endregion let jsdom = require("jsdom"); let jsdom_lib_jsdom_living_generated_utils_js = require("jsdom/lib/jsdom/living/generated/utils.js"); jsdom_lib_jsdom_living_generated_utils_js = __toESM(jsdom_lib_jsdom_living_generated_utils_js); //#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 } = jsdom_lib_jsdom_living_generated_utils_js.default; const copyPasteData = {}; const { window: JSDOMWindow } = new jsdom.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.pu