UNPKG

fabric

Version:

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

1,565 lines (1,461 loc) 1.01 MB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.fabric = {})); })(this, (function (exports) { 'use strict'; function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread2(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } function _objectWithoutProperties(e, t) { if (null == e) return {}; var o, r, i = _objectWithoutPropertiesLoose(e, t); if (Object.getOwnPropertySymbols) { var n = Object.getOwnPropertySymbols(e); for (r = 0; r < n.length; r++) o = n[r], t.indexOf(o) >= 0 || {}.propertyIsEnumerable.call(e, o) && (i[o] = e[o]); } return i; } function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (e.indexOf(n) >= 0) continue; t[n] = r[n]; } return t; } function _taggedTemplateLiteral(e, t) { return t || (t = e.slice(0)), Object.freeze(Object.defineProperties(e, { raw: { value: Object.freeze(t) } })); } function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; } class BaseConfiguration { constructor() { /** * 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 */ _defineProperty(this, "browserShadowBlurConstant", 1); /** * Pixel per Inch as a default value set to 96. Can be changed for more realistic conversion. */ _defineProperty(this, "DPI", 96); /** * Device Pixel Ratio * @see https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/HTML-canvas-guide/SettingUptheCanvas/SettingUptheCanvas.html */ _defineProperty(this, "devicePixelRatio", typeof window !== 'undefined' ? window.devicePixelRatio : 1); // eslint-disable-line no-restricted-globals /** * Pixel limit for cache canvases. 1Mpx , 4Mpx should be fine. * @since 1.7.14 * @type Number * @default */ _defineProperty(this, "perfLimitSizeTotal", 2097152); /** * Pixel limit for cache canvases width or height. IE fixes the maximum at 5000 * @since 1.7.14 * @type Number * @default */ _defineProperty(this, "maxCacheSideLimit", 4096); /** * Lowest pixel limit for cache canvases, set at 256PX * @since 1.7.14 * @type Number * @default */ _defineProperty(this, "minCacheSideLimit", 256); /** * When 'true', style information is not retained when copy/pasting text, making * pasted text use destination style. * Defaults to 'false'. * @type Boolean * @default * @deprecated */ _defineProperty(this, "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 * @default */ _defineProperty(this, "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 * @default */ _defineProperty(this, "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 */ _defineProperty(this, "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 */ _defineProperty(this, "cachesBoundsOfCurve", false); /** * Map of font files * Map<fontFamily, pathToFile> of font files */ _defineProperty(this, "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. */ _defineProperty(this, "NUM_FRACTION_DIGITS", 4); } } class Configuration extends BaseConfiguration { constructor(config) { super(); this.configure(config); } configure() { let config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; Object.assign(this, config); } /** * Map<fontFamily, pathToFile> of font files */ addFonts() { let paths = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; this.fontPaths = _objectSpread2(_objectSpread2({}, this.fontPaths), paths); } removeFonts() { let fontFamilys = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; fontFamilys.forEach(fontFamily => { delete this.fontPaths[fontFamily]; }); } clearFonts() { this.fontPaths = {}; } restoreDefaults(keys) { const defaults = new BaseConfiguration(); const config = (keys === null || keys === void 0 ? void 0 : keys.reduce((acc, key) => { acc[key] = defaults[key]; return acc; }, {})) || defaults; this.configure(config); } } const config = new Configuration(); const log = function (severity) { for (var _len = arguments.length, optionalParams = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { optionalParams[_key - 1] = arguments[_key]; } return ( // eslint-disable-next-line no-restricted-syntax console[severity]('fabric', ...optionalParams) ); }; class FabricError extends Error { constructor(message, options) { super("fabric: ".concat(message), options); } } class SignalAbortedError extends FabricError { constructor(context) { super("".concat(context, " 'options.signal' is in 'aborted' state")); } } class GLProbe {} /** * Lazy initialize WebGL constants */ class WebGLProbe 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 ".concat(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 ".concat(this.maxTextureSize)); } } isSupported(textureSize) { return !!this.maxTextureSize && this.maxTextureSize >= textureSize; } } /* eslint-disable no-restricted-globals */ const copyPasteData = {}; const getEnv$1 = () => { return { document, window, isTouchSupported: 'ontouchstart' in window || 'ontouchstart' in document || window && window.navigator && window.navigator.maxTouchPoints > 0, WebGLProbe: new WebGLProbe(), dispose() { // noop }, copyPasteData }; }; /** * 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$1()); const getFabricDocument = () => getEnv().document; const getFabricWindow = () => getEnv().window; /** * @returns the config value if defined, fallbacks to the environment value */ const getDevicePixelRatio = () => { var _config$devicePixelRa; return Math.max((_config$devicePixelRa = config.devicePixelRatio) !== null && _config$devicePixelRa !== void 0 ? _config$devicePixelRa : getFabricWindow().devicePixelRatio, 1); }; class Cache { constructor() { /** * Cache of widths of chars in text rendering. */ _defineProperty(this, "charWidthsCache", {}); /** * 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 */ _defineProperty(this, "boundsOfCurveCache", {}); } /** * @return {Object} reference to cache */ getFontCache(_ref) { let { fontFamily, fontStyle, fontWeight } = _ref; fontFamily = fontFamily.toLowerCase(); if (!this.charWidthsCache[fontFamily]) { this.charWidthsCache[fontFamily] = {}; } const fontCache = this.charWidthsCache[fontFamily]; const cacheKey = "".concat(fontStyle.toLowerCase(), "_").concat((fontWeight + '').toLowerCase()); if (!fontCache[cacheKey]) { fontCache[cacheKey] = {}; } return fontCache[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) { fontFamily = (fontFamily || '').toLowerCase(); if (!fontFamily) { this.charWidthsCache = {}; } else if (this.charWidthsCache[fontFamily]) { delete this.charWidthsCache[fontFamily]; } } /** * 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); // we are not returning a point on purpose, to avoid circular dependencies // this is an internal utility return [Math.floor(roughWidth), Math.floor(perfLimitSizeTotal / roughWidth)]; } } const cache = new Cache(); var version = "6.7.0"; // use this syntax so babel plugin see this import here const VERSION = version; // eslint-disable-next-line @typescript-eslint/no-empty-function function noop() {} const halfPI = Math.PI / 2; const twoMathPi = Math.PI * 2; const PiBy180 = Math.PI / 180; const iMatrix = Object.freeze([1, 0, 0, 1, 0, 0]); const DEFAULT_SVG_FONT_SIZE = 16; const ALIASING_LIMIT = 2; /* "magic number" for bezier approximations of arcs (http://itc.ktu.lt/itc354/Riskus354.pdf) */ const kRect = 1 - 0.5522847498; const CENTER = 'center'; const LEFT = 'left'; const TOP = 'top'; 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'; /* * This Map connects the objects type value with their * class implementation. It used from any object to understand which are * the classes to enlive when requesting a object.type = 'path' for example. * Objects uses it for clipPath, Canvas uses it for everything. * This is necessary for generic code to run and enlive instances from serialized representation. * You can customize which classes get enlived from SVG parsing using this classRegistry. * The Registry start empty and gets filled in depending which files you import. * If you want to be able to parse arbitrary SVGs or JSON representation of canvases, coming from * different sources you will need to import all fabric because you may need all classes. */ const JSON$1 = 'json'; const SVG = 'svg'; class ClassRegistry { constructor() { this[JSON$1] = new Map(); this[SVG] = 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 ".concat(classType)); } return constructor; } setClass(classConstructor, classType) { if (classType) { this[JSON$1].set(classType, classConstructor); } else { this[JSON$1].set(classConstructor.type, classConstructor); // legacy // @TODO: needs to be removed in fabric 7 or 8 this[JSON$1].set(classConstructor.type.toLowerCase(), classConstructor); } } getSVGClass(SVGTagName) { return this[SVG].get(SVGTagName); } setSVGClass(classConstructor, SVGTagName) { this[SVG].set(SVGTagName !== null && SVGTagName !== void 0 ? SVGTagName : classConstructor.type.toLowerCase(), classConstructor); } } const classRegistry = new ClassRegistry(); /** * Array holding all running animations */ class AnimationRegistry 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 => { var _animation$target; return animation.target === canvas || typeof animation.target === 'object' && ((_animation$target = animation.target) === null || _animation$target === void 0 ? void 0 : _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(); /** * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#events} * @see {@link http://fabricjs.com/events|Events demo} */ class Observable { constructor() { _defineProperty(this, "__eventListeners", {}); } /** * Observes specified event * @alias on * @param {string} eventName Event name (eg. 'after:render') * @param {EventRegistryObject} handlers key/value pairs (eg. {'after:render': handler, 'selection:cleared': handler}) * @param {Function} handler Function that receives a notification when an event of the specified type occurs * @return {Function} disposer */ on(arg0, handler) { if (!this.__eventListeners) { this.__eventListeners = {}; } if (typeof arg0 === 'object') { // one object with key/value pairs was passed Object.entries(arg0).forEach(_ref => { let [eventName, handler] = _ref; 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 { // noop return () => false; } } /** * Observes specified event **once** * @alias once * @param {string} eventName Event name (eg. 'after:render') * @param {EventRegistryObject} handlers key/value pairs (eg. {'after:render': handler, 'selection:cleared': handler}) * @param {Function} handler Function that receives a notification when an event of the specified type occurs * @return {Function} disposer */ once(arg0, handler) { if (typeof arg0 === 'object') { // one object with key/value pairs was passed const disposers = []; Object.entries(arg0).forEach(_ref2 => { let [eventName, handler] = _ref2; disposers.push(this.once(eventName, handler)); }); return () => disposers.forEach(d => d()); } else if (handler) { const disposer = this.on(arg0, function onceHandler() { for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } handler.call(this, ...args); disposer(); }); return disposer; } else { // noop 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] = []; } } /** * Unsubscribe all event listeners for eventname. * Do not use this pattern. You could kill internal fabricJS events. * We know we should have protected events for internal flows, but we don't have yet * @deprecated * @param {string} eventName event name (eg. 'after:render') */ /** * unsubscribe an event listener * @param {string} eventName event name (eg. 'after:render') * @param {TEventCallback} handler event listener to unsubscribe */ /** * unsubscribe event listeners * @param handlers handlers key/value pairs (eg. {'after:render': handler, 'selection:cleared': handler}) */ /** * unsubscribe all event listeners */ off(arg0, handler) { if (!this.__eventListeners) { return; } // remove all key/value pairs (event name -> event handler) if (typeof arg0 === 'undefined') { for (const eventName in this.__eventListeners) { this._removeEventListener(eventName); } } // one object with key/value pairs was passed else if (typeof arg0 === 'object') { Object.entries(arg0).forEach(_ref3 => { let [eventName, handler] = _ref3; 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) { var _this$__eventListener; if (!this.__eventListeners) { return; } const listenersForEvent = (_this$__eventListener = this.__eventListeners[eventName]) === null || _this$__eventListener === void 0 ? void 0 : _this$__eventListener.concat(); if (listenersForEvent) { for (let i = 0; i < listenersForEvent.length; i++) { listenersForEvent[i].call(this, options || {}); } } } } /** * 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; }; /** * 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; } const angleSlice = Math.abs(angle) / halfPI; switch (angleSlice) { case 1: case 3: return 0; case 2: return -1; } return Math.cos(angle); }; /** * 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); }; /** * Adaptation of work of Kevin Lindsey(kevin@kevlindev.com) */ class Point { constructor() { let arg0 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; let y = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 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 another one * @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 * @chainable * @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 * @chainable * @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 * @chainable * @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 * @chainable * @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 * @chainable * @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 * @chainable * @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) { let t = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0.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 "".concat(this.x, ",").concat(this.y); } /** * Sets x/y of this point * @param {Number} x * @param {Number} y * @chainable */ setXY(x, y) { this.x = x; this.y = y; return this; } /** * Sets x of this point * @param {Number} x * @chainable */ setX(x) { this.x = x; return this; } /** * Sets y of this point * @param {Number} y * @chainable */ setY(y) { this.y = y; return this; } /** * Sets x/y of this point from another point * @param {XY} that * @chainable */ 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` * @static * @memberOf fabric.util * @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) { let origin = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ZERO; // TODO benchmark and verify the add and subtract how much cost // and then in case early return if no origin is passed const sinus = sin(radians), cosinus = cos(radians); const p = this.subtract(origin); const rotated = new Point(p.x * cosinus - p.y * sinus, p.x * sinus + p.y * cosinus); return rotated.add(origin); } /** * Apply transform t to point p * @static * @memberOf fabric.util * @param {TMat2D} t The transform * @param {Boolean} [ignoreOffset] Indicates that the offset should not be applied * @return {Point} The transformed point */ transform(t) { let ignoreOffset = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 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); const isCollection = fabricObject => { return !!fabricObject && Array.isArray(fabricObject._objects); }; function createCollectionMixin(Base) { class Collection extends Base { constructor() { super(...arguments); /** * @type {FabricObject[]} * @TODO needs to end up in the constructor too */ _defineProperty(this, "_objects", []); } // eslint-disable-next-line @typescript-eslint/no-unused-vars _onObjectAdded(object) { // subclasses should override this method } // eslint-disable-next-line @typescript-eslint/no-unused-vars _onObjectRemoved(object) { // subclasses should override this method } // eslint-disable-next-line @typescript-eslint/no-unused-vars _onStackOrderChanged(object) { // subclasses should override this method } /** * Adds objects to collection * Objects should be instances of (or inherit from) FabricObject * @param {...FabricObject[]} objects to add * @returns {number} new array length */ add() { for (var _len = arguments.length, objects = new Array(_len), _key = 0; _key < _len; _key++) { objects[_key] = arguments[_key]; } 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) { for (var _len2 = arguments.length, objects = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { objects[_key2 - 1] = arguments[_key2]; } 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() { const array = this._objects, removed = []; for (var _len3 = arguments.length, objects = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { objects[_key3] = arguments[_key3]; } objects.forEach(object => { const index = array.indexOf(object); // only call onObjectRemoved if an object was actually removed 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() { for (var _len4 = arguments.length, types = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) { types[_key4] = arguments[_key4]; } 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) { // if object is not on the bottom of stack 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) { // if object is not on top of stack (last item in an array) 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; // traverse down the stack looking for the nearest intersecting object 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; // traverse up the stack looking for the nearest intersecting object 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(_ref) { let { left, top, width, height } = _ref; let { includeIntersecting = true } = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; const objects = [], tl = new Point(left, top), br = tl.add(new Point(width, height)); // we iterate reverse order to collect top first in case of click. 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; } } // https://github.com/microsoft/TypeScript/issues/32080 return Collection; } class CommonMethods extends Observable { /** * Sets object's properties from options, for initialization only * @protected * @param {Object} [options] Options object */ _setOptions() { let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 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]; } } function requestAnimFrame(callback) { return getFabricWindow().requestAnimationFrame(callback); } function cancelAnimFrame(handle) { return getFabricWindow().cancelAnimationFrame(handle); } let id = 0; const uid = () => id++; /** * 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 => { var _newCanvas$getContext; const newCanvas = createCanvasElementFor(canva