UNPKG

fabric

Version:

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

1 lines 87.6 kB
{"version":3,"file":"StaticCanvas.mjs","sources":["../../../src/canvas/StaticCanvas.ts"],"sourcesContent":["import { config } from '../config';\nimport { CENTER, VERSION } from '../constants';\nimport type { CanvasEvents, StaticCanvasEvents } from '../EventTypeDefs';\nimport type { Gradient } from '../gradient/Gradient';\nimport { createCollectionMixin, isCollection } from '../Collection';\nimport { CommonMethods } from '../CommonMethods';\nimport type { Pattern } from '../Pattern';\nimport { Point } from '../Point';\nimport type { TCachedFabricObject } from '../shapes/Object/Object';\nimport type {\n Abortable,\n Constructor,\n TCornerPoint,\n TDataUrlOptions,\n TFiller,\n TMat2D,\n TSize,\n TSVGReviver,\n TToCanvasElementOptions,\n TValidToObjectMethod,\n TOptions,\n} from '../typedefs';\nimport {\n cancelAnimFrame,\n requestAnimFrame,\n} from '../util/animation/AnimationFrameProvider';\nimport { runningAnimations } from '../util/animation/AnimationRegistry';\nimport { uid } from '../util/internals/uid';\nimport { createCanvasElementFor, toBlob, toDataURL } from '../util/misc/dom';\nimport { invertTransform, transformPoint } from '../util/misc/matrix';\nimport type { EnlivenObjectOptions } from '../util/misc/objectEnlive';\nimport {\n enlivenObjectEnlivables,\n enlivenObjects,\n} from '../util/misc/objectEnlive';\nimport { pick } from '../util/misc/pick';\nimport { matrixToSVG } from '../util/misc/svgExport';\nimport { toFixed } from '../util/misc/toFixed';\nimport { isFiller, isPattern, isTextObject } from '../util/typeAssertions';\nimport { StaticCanvasDOMManager } from './DOMManagers/StaticCanvasDOMManager';\nimport type { CSSDimensions } from './DOMManagers/util';\nimport type { FabricObject } from '../shapes/Object/FabricObject';\nimport type { StaticCanvasOptions } from './StaticCanvasOptions';\nimport { staticCanvasDefaults } from './StaticCanvasOptions';\nimport { log, FabricError } from '../util/internals/console';\nimport { getDevicePixelRatio } from '../env';\n\n/**\n * Having both options in TCanvasSizeOptions set to true transform the call in a calcOffset\n * Better try to restrict with types to avoid confusion.\n */\nexport type TCanvasSizeOptions =\n | {\n backstoreOnly?: true;\n cssOnly?: false;\n }\n | {\n backstoreOnly?: false;\n cssOnly?: true;\n };\n\nexport type TSVGExportOptions = {\n suppressPreamble?: boolean;\n viewBox?: {\n x: number;\n y: number;\n width: number;\n height: number;\n };\n encoding?: 'UTF-8'; // test Encoding type and see what happens\n width?: string;\n height?: string;\n reviver?: TSVGReviver;\n};\n\n/**\n * Static canvas class\n * @see {@link http://fabricjs.com/static_canvas|StaticCanvas demo}\n * @fires before:render\n * @fires after:render\n * @fires canvas:cleared\n * @fires object:added\n * @fires object:removed\n */\n// TODO: fix `EventSpec` inheritance https://github.com/microsoft/TypeScript/issues/26154#issuecomment-1366616260\nexport class StaticCanvas<\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n EventSpec extends StaticCanvasEvents = StaticCanvasEvents,\n >\n extends createCollectionMixin(CommonMethods<CanvasEvents>)\n implements StaticCanvasOptions\n{\n declare width: number;\n declare height: number;\n\n // background\n declare backgroundVpt: boolean;\n declare backgroundColor: TFiller | string;\n declare backgroundImage?: FabricObject;\n // overlay\n declare overlayVpt: boolean;\n declare overlayColor: TFiller | string;\n declare overlayImage?: FabricObject;\n\n declare clipPath?: FabricObject;\n\n declare includeDefaultValues: boolean;\n\n // rendering config\n declare renderOnAddRemove: boolean;\n declare skipOffscreen: boolean;\n declare enableRetinaScaling: boolean;\n declare imageSmoothingEnabled: boolean;\n\n /**\n * @todo move to Canvas\n */\n declare controlsAboveOverlay: boolean;\n\n /**\n * @todo move to Canvas\n */\n declare allowTouchScrolling: boolean;\n\n declare viewportTransform: TMat2D;\n\n /**\n * The viewport bounding box in scene plane coordinates, see {@link calcViewportBoundaries}\n */\n declare vptCoords: TCornerPoint;\n\n /**\n * A reference to the canvas actual HTMLCanvasElement.\n * Can be use to read the raw pixels, but never write or manipulate\n * @type HTMLCanvasElement\n */\n get lowerCanvasEl() {\n return this.elements.lower?.el;\n }\n\n get contextContainer() {\n return this.elements.lower?.ctx;\n }\n\n /**\n * If true the Canvas is in the process or has been disposed/destroyed.\n * No more rendering operation will be executed on this canvas.\n * @type boolean\n */\n declare destroyed?: boolean;\n\n /**\n * Started the process of disposing but not done yet.\n * WIll likely complete the render cycle already scheduled but stopping adding more.\n * @type boolean\n */\n declare disposed?: boolean;\n\n declare _offset: { left: number; top: number };\n protected declare hasLostContext: boolean;\n protected declare nextRenderHandle: number;\n\n declare elements: StaticCanvasDOMManager;\n\n /**\n * When true control drawing is skipped.\n * This boolean is used to avoid toDataURL to export controls.\n * Usage of this boolean to build up other flows and features is not supported\n * @type Boolean\n * @default false\n */\n protected declare skipControlsDrawing: boolean;\n\n static ownDefaults = staticCanvasDefaults;\n\n // reference to\n protected declare __cleanupTask?: {\n (): void;\n kill: (reason?: any) => void;\n };\n\n static getDefaults(): Record<string, any> {\n return StaticCanvas.ownDefaults;\n }\n\n constructor(\n el?: string | HTMLCanvasElement,\n options: TOptions<StaticCanvasOptions> = {},\n ) {\n super();\n Object.assign(\n this,\n (this.constructor as typeof StaticCanvas).getDefaults(),\n );\n this.set(options);\n this.initElements(el);\n this._setDimensionsImpl({\n width: this.width || this.elements.lower.el.width || 0,\n height: this.height || this.elements.lower.el.height || 0,\n });\n this.skipControlsDrawing = false;\n this.viewportTransform = [...this.viewportTransform];\n this.calcViewportBoundaries();\n }\n\n protected initElements(el?: string | HTMLCanvasElement) {\n this.elements = new StaticCanvasDOMManager(el);\n }\n\n add(...objects: FabricObject[]) {\n const size = super.add(...objects);\n objects.length > 0 && this.renderOnAddRemove && this.requestRenderAll();\n return size;\n }\n\n insertAt(index: number, ...objects: FabricObject[]) {\n const size = super.insertAt(index, ...objects);\n objects.length > 0 && this.renderOnAddRemove && this.requestRenderAll();\n return size;\n }\n\n remove(...objects: FabricObject[]) {\n const removed = super.remove(...objects);\n removed.length > 0 && this.renderOnAddRemove && this.requestRenderAll();\n return removed;\n }\n\n _onObjectAdded(obj: FabricObject) {\n if (obj.canvas && (obj.canvas as StaticCanvas) !== this) {\n log(\n 'warn',\n 'Canvas is trying to add an object that belongs to a different canvas.\\n' +\n 'Resulting to default behavior: removing object from previous canvas and adding to new canvas',\n );\n obj.canvas.remove(obj);\n }\n obj._set('canvas', this);\n obj.setCoords();\n this.fire('object:added', { target: obj });\n obj.fire('added', { target: this });\n }\n\n _onObjectRemoved(obj: FabricObject) {\n obj._set('canvas', undefined);\n this.fire('object:removed', { target: obj });\n obj.fire('removed', { target: this });\n }\n\n _onStackOrderChanged() {\n this.renderOnAddRemove && this.requestRenderAll();\n }\n\n /**\n * @private\n * @see https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/HTML-canvas-guide/SettingUptheCanvas/SettingUptheCanvas.html\n * @return {Number} retinaScaling if applied, otherwise 1;\n */\n getRetinaScaling() {\n return this.enableRetinaScaling ? getDevicePixelRatio() : 1;\n }\n\n /**\n * Calculates canvas element offset relative to the document\n * This method is also attached as \"resize\" event handler of window\n */\n calcOffset() {\n return (this._offset = this.elements.calcOffset());\n }\n\n /**\n * Returns canvas width (in px)\n * @return {Number}\n */\n getWidth(): number {\n return this.width;\n }\n\n /**\n * Returns canvas height (in px)\n * @return {Number}\n */\n getHeight(): number {\n return this.height;\n }\n\n /**\n * Sets width of this canvas instance\n * @param {Number|String} value Value to set width to\n * @param {Object} [options] Options object\n * @param {Boolean} [options.backstoreOnly=false] Set the given dimensions only as canvas backstore dimensions\n * @param {Boolean} [options.cssOnly=false] Set the given dimensions only as css dimensions\n * @deprecated will be removed in 7.0\n */\n setWidth(\n value: TSize['width'],\n options?: { backstoreOnly?: true; cssOnly?: false },\n ): void;\n setWidth(\n value: CSSDimensions['width'],\n options?: { cssOnly?: true; backstoreOnly?: false },\n ): void;\n setWidth(value: number, options?: never) {\n return this.setDimensions({ width: value }, options);\n }\n\n /**s\n * Sets height of this canvas instance\n * @param {Number|String} value Value to set height to\n * @param {Object} [options] Options object\n * @param {Boolean} [options.backstoreOnly=false] Set the given dimensions only as canvas backstore dimensions\n * @param {Boolean} [options.cssOnly=false] Set the given dimensions only as css dimensions\n * @deprecated will be removed in 7.0\n */\n setHeight(\n value: TSize['height'],\n options?: { backstoreOnly?: true; cssOnly?: false },\n ): void;\n setHeight(\n value: CSSDimensions['height'],\n options?: { cssOnly?: true; backstoreOnly?: false },\n ): void;\n setHeight(value: CSSDimensions['height'], options?: never) {\n return this.setDimensions({ height: value }, options);\n }\n\n /**\n * Internal use only\n * @protected\n */\n protected _setDimensionsImpl(\n dimensions: Partial<TSize | CSSDimensions>,\n { cssOnly = false, backstoreOnly = false }: TCanvasSizeOptions = {},\n ) {\n if (!cssOnly) {\n const size = {\n width: this.width,\n height: this.height,\n ...(dimensions as Partial<TSize>),\n };\n this.elements.setDimensions(size, this.getRetinaScaling());\n this.hasLostContext = true;\n this.width = size.width;\n this.height = size.height;\n }\n if (!backstoreOnly) {\n this.elements.setCSSDimensions(dimensions);\n }\n\n this.calcOffset();\n }\n\n /**\n * Sets dimensions (width, height) of this canvas instance. when options.cssOnly flag active you should also supply the unit of measure (px/%/em)\n * @param {Object} dimensions Object with width/height properties\n * @param {Number|String} [dimensions.width] Width of canvas element\n * @param {Number|String} [dimensions.height] Height of canvas element\n * @param {Object} [options] Options object\n * @param {Boolean} [options.backstoreOnly=false] Set the given dimensions only as canvas backstore dimensions\n * @param {Boolean} [options.cssOnly=false] Set the given dimensions only as css dimensions\n */\n setDimensions(\n dimensions: Partial<CSSDimensions>,\n options?: { cssOnly?: true; backstoreOnly?: false },\n ): void;\n setDimensions(\n dimensions: Partial<TSize>,\n options?: { backstoreOnly?: true; cssOnly?: false },\n ): void;\n setDimensions(dimensions: Partial<TSize>, options?: never): void;\n setDimensions(\n dimensions: Partial<TSize | CSSDimensions>,\n options?: TCanvasSizeOptions,\n ) {\n this._setDimensionsImpl(dimensions, options);\n if (!options || !options.cssOnly) {\n this.requestRenderAll();\n }\n }\n\n /**\n * Returns canvas zoom level\n * @return {Number}\n */\n getZoom() {\n return this.viewportTransform[0];\n }\n\n /**\n * Sets viewport transformation of this canvas instance\n * @param {Array} vpt a Canvas 2D API transform matrix\n */\n setViewportTransform(vpt: TMat2D) {\n this.viewportTransform = vpt;\n this.calcViewportBoundaries();\n this.renderOnAddRemove && this.requestRenderAll();\n }\n\n /**\n * Sets zoom level of this canvas instance, the zoom centered around point\n * meaning that following zoom to point with the same point will have the visual\n * effect of the zoom originating from that point. The point won't move.\n * It has nothing to do with canvas center or visual center of the viewport.\n * @param {Point} point to zoom with respect to\n * @param {Number} value to set zoom to, less than 1 zooms out\n */\n zoomToPoint(point: Point, value: number) {\n // TODO: just change the scale, preserve other transformations\n const before = point,\n vpt: TMat2D = [...this.viewportTransform];\n const newPoint = transformPoint(point, invertTransform(vpt));\n vpt[0] = value;\n vpt[3] = value;\n const after = transformPoint(newPoint, vpt);\n vpt[4] += before.x - after.x;\n vpt[5] += before.y - after.y;\n this.setViewportTransform(vpt);\n }\n\n /**\n * Sets zoom level of this canvas instance\n * @param {Number} value to set zoom to, less than 1 zooms out\n */\n setZoom(value: number) {\n this.zoomToPoint(new Point(0, 0), value);\n }\n\n /**\n * Pan viewport so as to place point at top left corner of canvas\n * @param {Point} point to move to\n */\n absolutePan(point: Point) {\n const vpt: TMat2D = [...this.viewportTransform];\n vpt[4] = -point.x;\n vpt[5] = -point.y;\n return this.setViewportTransform(vpt);\n }\n\n /**\n * Pans viewpoint relatively\n * @param {Point} point (position vector) to move by\n */\n relativePan(point: Point) {\n return this.absolutePan(\n new Point(\n -point.x - this.viewportTransform[4],\n -point.y - this.viewportTransform[5],\n ),\n );\n }\n\n /**\n * Returns &lt;canvas> element corresponding to this instance\n * @return {HTMLCanvasElement}\n */\n getElement(): HTMLCanvasElement {\n return this.elements.lower.el;\n }\n\n /**\n * Clears specified context of canvas element\n * @param {CanvasRenderingContext2D} ctx Context to clear\n */\n clearContext(ctx: CanvasRenderingContext2D) {\n ctx.clearRect(0, 0, this.width, this.height);\n }\n\n /**\n * Returns context of canvas where objects are drawn\n * @return {CanvasRenderingContext2D}\n */\n getContext(): CanvasRenderingContext2D {\n return this.elements.lower.ctx;\n }\n\n /**\n * Clears all contexts (background, main, top) of an instance\n */\n clear() {\n this.remove(...this.getObjects());\n this.backgroundImage = undefined;\n this.overlayImage = undefined;\n this.backgroundColor = '';\n this.overlayColor = '';\n this.clearContext(this.getContext());\n this.fire('canvas:cleared');\n this.renderOnAddRemove && this.requestRenderAll();\n }\n\n /**\n * Renders the canvas\n */\n renderAll() {\n this.cancelRequestedRender();\n if (this.destroyed) {\n return;\n }\n this.renderCanvas(this.getContext(), this._objects);\n }\n\n /**\n * Function created to be instance bound at initialization\n * used in requestAnimationFrame rendering\n * Let the fabricJS call it. If you call it manually you could have more\n * animationFrame stacking on to of each other\n * for an imperative rendering, use canvas.renderAll\n * @private\n */\n renderAndReset() {\n this.nextRenderHandle = 0;\n this.renderAll();\n }\n\n /**\n * Append a renderAll request to next animation frame.\n * unless one is already in progress, in that case nothing is done\n * a boolean flag will avoid appending more.\n */\n requestRenderAll() {\n if (!this.nextRenderHandle && !this.disposed && !this.destroyed) {\n this.nextRenderHandle = requestAnimFrame(() => this.renderAndReset());\n }\n }\n\n /**\n * Calculate the position of the 4 corner of canvas with current viewportTransform.\n * helps to determinate when an object is in the current rendering viewport\n */\n calcViewportBoundaries(): TCornerPoint {\n const width = this.width,\n height = this.height,\n iVpt = invertTransform(this.viewportTransform),\n a = transformPoint({ x: 0, y: 0 }, iVpt),\n b = transformPoint({ x: width, y: height }, iVpt),\n // we don't support vpt flipping\n // but the code is robust enough to mostly work with flipping\n min = a.min(b),\n max = a.max(b);\n return (this.vptCoords = {\n tl: min,\n tr: new Point(max.x, min.y),\n bl: new Point(min.x, max.y),\n br: max,\n });\n }\n\n cancelRequestedRender() {\n if (this.nextRenderHandle) {\n cancelAnimFrame(this.nextRenderHandle);\n this.nextRenderHandle = 0;\n }\n }\n\n drawControls(_ctx: CanvasRenderingContext2D) {\n // Static canvas has no controls\n }\n\n /**\n * Renders background, objects, overlay and controls.\n * @param {CanvasRenderingContext2D} ctx\n * @param {Array} objects to render\n */\n renderCanvas(ctx: CanvasRenderingContext2D, objects: FabricObject[]) {\n if (this.destroyed) {\n return;\n }\n\n const v = this.viewportTransform,\n path = this.clipPath;\n this.calcViewportBoundaries();\n this.clearContext(ctx);\n ctx.imageSmoothingEnabled = this.imageSmoothingEnabled;\n // @ts-expect-error node-canvas stuff\n ctx.patternQuality = 'best';\n this.fire('before:render', { ctx });\n this._renderBackground(ctx);\n\n ctx.save();\n //apply viewport transform once for all rendering process\n ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]);\n this._renderObjects(ctx, objects);\n ctx.restore();\n if (!this.controlsAboveOverlay && !this.skipControlsDrawing) {\n this.drawControls(ctx);\n }\n if (path) {\n path._set('canvas', this);\n // needed to setup a couple of variables\n // todo migrate to the newer one\n path.shouldCache();\n path._transformDone = true;\n (path as TCachedFabricObject).renderCache({ forClipping: true });\n this.drawClipPathOnCanvas(ctx, path as TCachedFabricObject);\n }\n this._renderOverlay(ctx);\n if (this.controlsAboveOverlay && !this.skipControlsDrawing) {\n this.drawControls(ctx);\n }\n this.fire('after:render', { ctx });\n\n if (this.__cleanupTask) {\n this.__cleanupTask();\n this.__cleanupTask = undefined;\n }\n }\n\n /**\n * Paint the cached clipPath on the lowerCanvasEl\n * @param {CanvasRenderingContext2D} ctx Context to render on\n */\n drawClipPathOnCanvas(\n ctx: CanvasRenderingContext2D,\n clipPath: TCachedFabricObject,\n ) {\n const v = this.viewportTransform;\n ctx.save();\n ctx.transform(...v);\n // DEBUG: uncomment this line, comment the following\n // ctx.globalAlpha = 0.4;\n ctx.globalCompositeOperation = 'destination-in';\n clipPath.transform(ctx);\n ctx.scale(1 / clipPath.zoomX, 1 / clipPath.zoomY);\n ctx.drawImage(\n clipPath._cacheCanvas,\n -clipPath.cacheTranslationX,\n -clipPath.cacheTranslationY,\n );\n ctx.restore();\n }\n\n /**\n * @private\n * @param {CanvasRenderingContext2D} ctx Context to render on\n * @param {Array} objects to render\n */\n _renderObjects(ctx: CanvasRenderingContext2D, objects: FabricObject[]) {\n for (let i = 0, len = objects.length; i < len; ++i) {\n objects[i] && objects[i].render(ctx);\n }\n }\n\n /**\n * @private\n * @param {CanvasRenderingContext2D} ctx Context to render on\n * @param {string} property 'background' or 'overlay'\n */\n _renderBackgroundOrOverlay(\n ctx: CanvasRenderingContext2D,\n property: 'background' | 'overlay',\n ) {\n const fill = this[`${property}Color`],\n object = this[`${property}Image`],\n v = this.viewportTransform,\n needsVpt = this[`${property}Vpt`];\n if (!fill && !object) {\n return;\n }\n const isAFiller = isFiller(fill);\n if (fill) {\n ctx.save();\n ctx.beginPath();\n ctx.moveTo(0, 0);\n ctx.lineTo(this.width, 0);\n ctx.lineTo(this.width, this.height);\n ctx.lineTo(0, this.height);\n ctx.closePath();\n ctx.fillStyle = isAFiller ? fill.toLive(ctx /* this */)! : fill;\n if (needsVpt) {\n ctx.transform(...v);\n }\n if (isAFiller) {\n ctx.transform(1, 0, 0, 1, fill.offsetX || 0, fill.offsetY || 0);\n const m = ((fill as Gradient<'linear'>).gradientTransform ||\n (fill as Pattern).patternTransform) as TMat2D;\n m && ctx.transform(...m);\n }\n ctx.fill();\n ctx.restore();\n }\n if (object) {\n ctx.save();\n const { skipOffscreen } = this;\n // if the object doesn't move with the viewport,\n // the offscreen concept does not apply;\n this.skipOffscreen = needsVpt;\n if (needsVpt) {\n ctx.transform(...v);\n }\n object.render(ctx);\n this.skipOffscreen = skipOffscreen;\n ctx.restore();\n }\n }\n\n /**\n * @private\n * @param {CanvasRenderingContext2D} ctx Context to render on\n */\n _renderBackground(ctx: CanvasRenderingContext2D) {\n this._renderBackgroundOrOverlay(ctx, 'background');\n }\n\n /**\n * @private\n * @param {CanvasRenderingContext2D} ctx Context to render on\n */\n _renderOverlay(ctx: CanvasRenderingContext2D) {\n this._renderBackgroundOrOverlay(ctx, 'overlay');\n }\n\n /**\n * Returns coordinates of a center of canvas.\n * Returned value is an object with top and left properties\n * @return {Object} object with \"top\" and \"left\" number values\n * @deprecated migrate to `getCenterPoint`\n */\n getCenter() {\n return {\n top: this.height / 2,\n left: this.width / 2,\n };\n }\n\n /**\n * Returns coordinates of a center of canvas.\n * @return {Point}\n */\n getCenterPoint() {\n return new Point(this.width / 2, this.height / 2);\n }\n\n /**\n * Centers object horizontally in the canvas\n */\n centerObjectH(object: FabricObject) {\n return this._centerObject(\n object,\n new Point(this.getCenterPoint().x, object.getCenterPoint().y),\n );\n }\n\n /**\n * Centers object vertically in the canvas\n * @param {FabricObject} object Object to center vertically\n */\n centerObjectV(object: FabricObject) {\n return this._centerObject(\n object,\n new Point(object.getCenterPoint().x, this.getCenterPoint().y),\n );\n }\n\n /**\n * Centers object vertically and horizontally in the canvas\n * @param {FabricObject} object Object to center vertically and horizontally\n */\n centerObject(object: FabricObject) {\n return this._centerObject(object, this.getCenterPoint());\n }\n\n /**\n * Centers object vertically and horizontally in the viewport\n * @param {FabricObject} object Object to center vertically and horizontally\n */\n viewportCenterObject(object: FabricObject) {\n return this._centerObject(object, this.getVpCenter());\n }\n\n /**\n * Centers object horizontally in the viewport, object.top is unchanged\n * @param {FabricObject} object Object to center vertically and horizontally\n */\n viewportCenterObjectH(object: FabricObject) {\n return this._centerObject(\n object,\n new Point(this.getVpCenter().x, object.getCenterPoint().y),\n );\n }\n\n /**\n * Centers object Vertically in the viewport, object.top is unchanged\n * @param {FabricObject} object Object to center vertically and horizontally\n */\n viewportCenterObjectV(object: FabricObject) {\n return this._centerObject(\n object,\n new Point(object.getCenterPoint().x, this.getVpCenter().y),\n );\n }\n\n /**\n * Calculate the point in canvas that correspond to the center of actual viewport.\n * @return {Point} vpCenter, viewport center\n */\n getVpCenter(): Point {\n return transformPoint(\n this.getCenterPoint(),\n invertTransform(this.viewportTransform),\n );\n }\n\n /**\n * @private\n * @param {FabricObject} object Object to center\n * @param {Point} center Center point\n */\n _centerObject(object: FabricObject, center: Point) {\n object.setXY(center, CENTER, CENTER);\n object.setCoords();\n this.renderOnAddRemove && this.requestRenderAll();\n }\n\n /**\n * Returns dataless JSON representation of canvas\n * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output\n * @return {String} json string\n */\n toDatalessJSON(propertiesToInclude?: string[]) {\n return this.toDatalessObject(propertiesToInclude);\n }\n\n /**\n * Returns object representation of canvas\n * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output\n * @return {Object} object representation of an instance\n */\n toObject(propertiesToInclude?: string[]) {\n return this._toObjectMethod('toObject', propertiesToInclude);\n }\n\n /**\n * Returns Object representation of canvas\n * this alias is provided because if you call JSON.stringify on an instance,\n * the toJSON object will be invoked if it exists.\n * Having a toJSON method means you can do JSON.stringify(myCanvas)\n * @return {Object} JSON compatible object\n * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#serialization}\n * @see {@link http://jsfiddle.net/fabricjs/pec86/|jsFiddle demo}\n * @example <caption>JSON without additional properties</caption>\n * var json = canvas.toJSON();\n * @example <caption>JSON with additional properties included</caption>\n * var json = canvas.toJSON(['lockMovementX', 'lockMovementY', 'lockRotation', 'lockScalingX', 'lockScalingY']);\n * @example <caption>JSON without default values</caption>\n * var json = canvas.toJSON();\n */\n toJSON() {\n return this.toObject();\n }\n\n /**\n * Returns dataless object representation of canvas\n * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output\n * @return {Object} object representation of an instance\n */\n toDatalessObject(propertiesToInclude?: string[]) {\n return this._toObjectMethod('toDatalessObject', propertiesToInclude);\n }\n\n /**\n * @private\n */\n _toObjectMethod(\n methodName: TValidToObjectMethod,\n propertiesToInclude?: string[],\n ) {\n const clipPath = this.clipPath;\n const clipPathData =\n clipPath && !clipPath.excludeFromExport\n ? this._toObject(clipPath, methodName, propertiesToInclude)\n : null;\n return {\n version: VERSION,\n ...pick(this, propertiesToInclude as (keyof this)[]),\n objects: this._objects\n .filter((object) => !object.excludeFromExport)\n .map((instance) =>\n this._toObject(instance, methodName, propertiesToInclude),\n ),\n ...this.__serializeBgOverlay(methodName, propertiesToInclude),\n ...(clipPathData ? { clipPath: clipPathData } : null),\n };\n }\n\n /**\n * @private\n */\n protected _toObject(\n instance: FabricObject,\n methodName: TValidToObjectMethod,\n propertiesToInclude?: string[],\n ) {\n let originalValue;\n\n if (!this.includeDefaultValues) {\n originalValue = instance.includeDefaultValues;\n instance.includeDefaultValues = false;\n }\n\n const object = instance[methodName](propertiesToInclude);\n if (!this.includeDefaultValues) {\n instance.includeDefaultValues = !!originalValue;\n }\n return object;\n }\n\n /**\n * @private\n */\n __serializeBgOverlay(\n methodName: TValidToObjectMethod,\n propertiesToInclude?: string[],\n ) {\n const data: any = {},\n bgImage = this.backgroundImage,\n overlayImage = this.overlayImage,\n bgColor = this.backgroundColor,\n overlayColor = this.overlayColor;\n\n if (isFiller(bgColor)) {\n if (!bgColor.excludeFromExport) {\n data.background = bgColor.toObject(propertiesToInclude);\n }\n } else if (bgColor) {\n data.background = bgColor;\n }\n\n if (isFiller(overlayColor)) {\n if (!overlayColor.excludeFromExport) {\n data.overlay = overlayColor.toObject(propertiesToInclude);\n }\n } else if (overlayColor) {\n data.overlay = overlayColor;\n }\n\n if (bgImage && !bgImage.excludeFromExport) {\n data.backgroundImage = this._toObject(\n bgImage,\n methodName,\n propertiesToInclude,\n );\n }\n if (overlayImage && !overlayImage.excludeFromExport) {\n data.overlayImage = this._toObject(\n overlayImage,\n methodName,\n propertiesToInclude,\n );\n }\n\n return data;\n }\n\n /* _TO_SVG_START_ */\n\n declare svgViewportTransformation: boolean;\n\n /**\n * Returns SVG representation of canvas\n * @function\n * @param {Object} [options] Options object for SVG output\n * @param {Boolean} [options.suppressPreamble=false] If true xml tag is not included\n * @param {Object} [options.viewBox] SVG viewbox object\n * @param {Number} [options.viewBox.x] x-coordinate of viewbox\n * @param {Number} [options.viewBox.y] y-coordinate of viewbox\n * @param {Number} [options.viewBox.width] Width of viewbox\n * @param {Number} [options.viewBox.height] Height of viewbox\n * @param {String} [options.encoding=UTF-8] Encoding of SVG output\n * @param {String} [options.width] desired width of svg with or without units\n * @param {String} [options.height] desired height of svg with or without units\n * @param {Function} [reviver] Method for further parsing of svg elements, called after each fabric object converted into svg representation.\n * @return {String} SVG string\n * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#serialization}\n * @see {@link http://jsfiddle.net/fabricjs/jQ3ZZ/|jsFiddle demo}\n * @example <caption>Normal SVG output</caption>\n * var svg = canvas.toSVG();\n * @example <caption>SVG output without preamble (without &lt;?xml ../>)</caption>\n * var svg = canvas.toSVG({suppressPreamble: true});\n * @example <caption>SVG output with viewBox attribute</caption>\n * var svg = canvas.toSVG({\n * viewBox: {\n * x: 100,\n * y: 100,\n * width: 200,\n * height: 300\n * }\n * });\n * @example <caption>SVG output with different encoding (default: UTF-8)</caption>\n * var svg = canvas.toSVG({encoding: 'ISO-8859-1'});\n * @example <caption>Modify SVG output with reviver function</caption>\n * var svg = canvas.toSVG(null, function(svg) {\n * return svg.replace('stroke-dasharray: ; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; ', '');\n * });\n */\n toSVG(options: TSVGExportOptions = {}, reviver?: TSVGReviver) {\n options.reviver = reviver;\n const markup: string[] = [];\n\n this._setSVGPreamble(markup, options);\n this._setSVGHeader(markup, options);\n if (this.clipPath) {\n markup.push(`<g clip-path=\"url(#${this.clipPath.clipPathId})\" >\\n`);\n }\n this._setSVGBgOverlayColor(markup, 'background');\n this._setSVGBgOverlayImage(markup, 'backgroundImage', reviver);\n this._setSVGObjects(markup, reviver);\n if (this.clipPath) {\n markup.push('</g>\\n');\n }\n this._setSVGBgOverlayColor(markup, 'overlay');\n this._setSVGBgOverlayImage(markup, 'overlayImage', reviver);\n\n markup.push('</svg>');\n\n return markup.join('');\n }\n\n /**\n * @private\n */\n _setSVGPreamble(markup: string[], options: TSVGExportOptions): void {\n if (options.suppressPreamble) {\n return;\n }\n markup.push(\n '<?xml version=\"1.0\" encoding=\"',\n options.encoding || 'UTF-8',\n '\" standalone=\"no\" ?>\\n',\n '<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" ',\n '\"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\\n',\n );\n }\n\n /**\n * @private\n */\n _setSVGHeader(markup: string[], options: TSVGExportOptions): void {\n const width = options.width || `${this.width}`,\n height = options.height || `${this.height}`,\n NUM_FRACTION_DIGITS = config.NUM_FRACTION_DIGITS,\n optViewBox = options.viewBox;\n let viewBox: string;\n if (optViewBox) {\n viewBox = `viewBox=\"${optViewBox.x} ${optViewBox.y} ${optViewBox.width} ${optViewBox.height}\" `;\n } else if (this.svgViewportTransformation) {\n const vpt = this.viewportTransform;\n viewBox = `viewBox=\"${toFixed(\n -vpt[4] / vpt[0],\n NUM_FRACTION_DIGITS,\n )} ${toFixed(-vpt[5] / vpt[3], NUM_FRACTION_DIGITS)} ${toFixed(\n this.width / vpt[0],\n NUM_FRACTION_DIGITS,\n )} ${toFixed(this.height / vpt[3], NUM_FRACTION_DIGITS)}\" `;\n } else {\n viewBox = `viewBox=\"0 0 ${this.width} ${this.height}\" `;\n }\n\n markup.push(\n '<svg ',\n 'xmlns=\"http://www.w3.org/2000/svg\" ',\n 'xmlns:xlink=\"http://www.w3.org/1999/xlink\" ',\n 'version=\"1.1\" ',\n 'width=\"',\n width,\n '\" ',\n 'height=\"',\n height,\n '\" ',\n viewBox,\n 'xml:space=\"preserve\">\\n',\n '<desc>Created with Fabric.js ',\n VERSION,\n '</desc>\\n',\n '<defs>\\n',\n this.createSVGFontFacesMarkup(),\n this.createSVGRefElementsMarkup(),\n this.createSVGClipPathMarkup(options),\n '</defs>\\n',\n );\n }\n\n createSVGClipPathMarkup(options: TSVGExportOptions): string {\n const clipPath = this.clipPath;\n if (clipPath) {\n clipPath.clipPathId = `CLIPPATH_${uid()}`;\n return `<clipPath id=\"${clipPath.clipPathId}\" >\\n${clipPath.toClipPathSVG(\n options.reviver,\n )}</clipPath>\\n`;\n }\n return '';\n }\n\n /**\n * Creates markup containing SVG referenced elements like patterns, gradients etc.\n * @return {String}\n */\n createSVGRefElementsMarkup(): string {\n return (['background', 'overlay'] as const)\n .map((prop) => {\n const fill = this[`${prop}Color`];\n if (isFiller(fill)) {\n const shouldTransform = this[`${prop}Vpt`],\n vpt = this.viewportTransform,\n object = {\n // otherwise circular dependency\n isType: () => false,\n width: this.width / (shouldTransform ? vpt[0] : 1),\n height: this.height / (shouldTransform ? vpt[3] : 1),\n };\n return fill.toSVG(object as FabricObject, {\n additionalTransform: shouldTransform ? matrixToSVG(vpt) : '',\n });\n }\n })\n .join('');\n }\n\n /**\n * Creates markup containing SVG font faces,\n * font URLs for font faces must be collected by developers\n * and are not extracted from the DOM by fabricjs\n * @param {Array} objects Array of fabric objects\n * @return {String}\n */\n createSVGFontFacesMarkup(): string {\n const objects: FabricObject[] = [],\n fontList: Record<string, boolean> = {},\n fontPaths = config.fontPaths;\n\n this._objects.forEach(function add(object) {\n objects.push(object);\n if (isCollection(object)) {\n object._objects.forEach(add);\n }\n });\n\n objects.forEach((obj) => {\n if (!isTextObject(obj)) {\n return;\n }\n const { styles, fontFamily } = obj;\n if (fontList[fontFamily] || !fontPaths[fontFamily]) {\n return;\n }\n fontList[fontFamily] = true;\n if (!styles) {\n return;\n }\n Object.values(styles).forEach((styleRow) => {\n Object.values(styleRow).forEach(({ fontFamily = '' }) => {\n if (!fontList[fontFamily] && fontPaths[fontFamily]) {\n fontList[fontFamily] = true;\n }\n });\n });\n });\n\n const fontListMarkup = Object.keys(fontList)\n .map(\n (fontFamily) =>\n `\\t\\t@font-face {\\n\\t\\t\\tfont-family: '${fontFamily}';\\n\\t\\t\\tsrc: url('${fontPaths[fontFamily]}');\\n\\t\\t}\\n`,\n )\n .join('');\n\n if (fontListMarkup) {\n return `\\t<style type=\"text/css\"><![CDATA[\\n${fontListMarkup}]]></style>\\n`;\n }\n return '';\n }\n\n /**\n * @private\n */\n _setSVGObjects(markup: string[], reviver?: TSVGReviver) {\n this.forEachObject((fabricObject) => {\n if (fabricObject.excludeFromExport) {\n return;\n }\n this._setSVGObject(markup, fabricObject, reviver);\n });\n }\n\n /**\n * This is its own function because the Canvas ( non static ) requires extra code here\n * @private\n */\n _setSVGObject(\n markup: string[],\n instance: FabricObject,\n reviver?: TSVGReviver,\n ) {\n markup.push(instance.toSVG(reviver));\n }\n\n /**\n * @private\n */\n _setSVGBgOverlayImage(\n markup: string[],\n property: 'overlayImage' | 'backgroundImage',\n reviver?: TSVGReviver,\n ) {\n const bgOrOverlay = this[property];\n if (bgOrOverlay && !bgOrOverlay.excludeFromExport && bgOrOverlay.toSVG) {\n markup.push(bgOrOverlay.toSVG(reviver));\n }\n }\n\n /**\n * @TODO this seems to handle patterns but fail at gradients.\n * @private\n */\n _setSVGBgOverlayColor(markup: string[], property: 'background' | 'overlay') {\n const filler = this[`${property}Color`];\n if (!filler) {\n return;\n }\n if (isFiller(filler)) {\n const repeat = (filler as Pattern).repeat || '',\n finalWidth = this.width,\n finalHeight = this.height,\n shouldInvert = this[`${property}Vpt`],\n additionalTransform = shouldInvert\n ? matrixToSVG(invertTransform(this.viewportTransform))\n : '';\n markup.push(\n `<rect transform=\"${additionalTransform} translate(${finalWidth / 2},${\n finalHeight / 2\n })\" x=\"${filler.offsetX - finalWidth / 2}\" y=\"${\n filler.offsetY - finalHeight / 2\n }\" width=\"${\n (repeat === 'repeat-y' || repeat === 'no-repeat') && isPattern(filler)\n ? (filler.source as HTMLImageElement).width\n : finalWidth\n }\" height=\"${\n (repeat === 'repeat-x' || repeat === 'no-repeat') && isPattern(filler)\n ? (filler.source as HTMLImageElement).height\n : finalHeight\n }\" fill=\"url(#SVGID_${filler.id})\"></rect>\\n`,\n );\n } else {\n markup.push(\n '<rect x=\"0\" y=\"0\" width=\"100%\" height=\"100%\" ',\n 'fill=\"',\n filler,\n '\"',\n '></rect>\\n',\n );\n }\n }\n /* _TO_SVG_END_ */\n\n /**\n * Populates canvas with data from the specified JSON.\n * JSON format must conform to the one of {@link fabric.Canvas#toJSON}\n *\n * **IMPORTANT**: It is recommended to abort loading tasks before calling this method to prevent race conditions and unnecessary networking\n *\n * @param {String|Object} json JSON string or object\n * @param {Function} [reviver] Method for further parsing of JSON elements, called after each fabric object created.\n * @param {Object} [options] options\n * @param {AbortSignal} [options.signal] see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal\n * @return {Promise<Canvas | StaticCanvas>} instance\n * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#deserialization}\n * @see {@link http://jsfiddle.net/fabricjs/fmgXt/|jsFiddle demo}\n * @example <caption>loadFromJSON</caption>\n * canvas.loadFromJSON(json).then((canvas) => canvas.requestRenderAll());\n * @example <caption>loadFromJSON with reviver</caption>\n * canvas.loadFromJSON(json, function(o, object) {\n * // `o` = json object\n * // `object` = fabric.Object instance\n * // ... do some stuff ...\n * }).then((canvas) => {\n * ... canvas is restored, add your code.\n * });\n *\n */\n loadFromJSON(\n json: string | Record<string, any>,\n reviver?: EnlivenObjectOptions['reviver'],\n { signal }: Abortable = {},\n ): Promise<this> {\n if (!json) {\n return Promise.reject(new FabricError('`json` is undefined'));\n }\n\n // parse json if it wasn't already\n const serialized = typeof json === 'string' ? JSON.parse(json) : json;\n const {\n objects = [],\n backgroundImage,\n background,\n overlayImage,\n overlay,\n clipPath,\n } = serialized;\n const renderOnAddRemove = this.renderOnAddRemove;\n this.renderOnAddRemove = false;\n\n return Promise.all([\n enlivenObjects<FabricObject>(objects, {\n reviver,\n signal,\n }),\n enlivenObjectEnlivables(\n {\n backgroundImage,\n backgroundColor: background,\n overlayImage,\n overlayColor: overlay,\n clipPath,\n },\n { signal },\n ),\n ]).then(([enlived, enlivedMap]) => {\n this.clear();\n this.add(...enlived);\n this.set(serialized);\n this.set(enlivedMap);\n this.renderOnAddRemove = renderOnAddRemove;\n return this;\n });\n }\n\n /**\n * Clones canvas instance\n * @param {string[]} [properties] Array of properties to include in the cloned canvas and children\n */\n clone(properties: string[]) {\n const data = this.toObject(properties);\n const canvas = this.cloneWithoutData();\n return canvas.loadFromJSON(data);\n }\n\n /**\n * Clones canvas instance without cloning existing data.\n * This essentially copies canvas dimensions since loadFromJSON does not affect canvas size.\n */\n cloneWithoutData() {\n const el = createCanvasElementFor(this);\n return new (this.constructor as Constructor<this>)(el);\n }\n\n /**\n * Exports canvas element to a dataurl image. Note that when multiplier is used, cropping is scaled appropriately\n * @param {Object} [options] Options object\n * @param {String} [options.format=png] The format of the output image. Either \"jpeg\" or \"png\"\n * @param {Number} [options.quality=1] Quality level (0..1). Only used for jpeg.\n * @param {Number} [options.multiplier=1] Multiplier to scale by, to have consistent\n * @param {Number} [options.left] Cropping left offset. Introduced in v1.2.14\n * @param {Number} [options.top] Cropping top offset. Introduced in v1.2.14\n * @param {Number} [options.width] Cropping width. Introduced in v1.2.14\n * @param {Number} [options.height] Cropping height. Introduced in v1.2.14\n * @param {Boolean} [options.enableRetinaScaling] Enable retina scaling for clone image. Introduce in 2.0.0\n * @param {(object: fabric.Object) => boolean} [options.filter] Function to filter objects.\n * @return {String} Returns a data: URL containing a representation of the object in the format specified by options.format\n * @see {@link https://jsfiddle.net/xsjua1rd/ demo}\n * @example <caption>Generate jpeg dataURL with lower quality</caption>\n * var dataURL = canvas.toDataURL({\n * format: 'jpeg',\n * quality: 0.8\n * });\n * @example <caption>Generate cropped png dataURL (clipping of canvas)</caption>\n * var dataURL = canvas.toDataURL({\n * format: 'png',\n * left: 100,\n * top: 100,\n * width: 200,\n * height: 200\n * });\n * @example <caption>Generate double scaled png dataURL</caption>\n * var dataURL = canvas.toDataURL({\n * format: 'png',\n * multiplier: 2\n * });\n * @example <caption>Generate dataURL with objects that overlap a specified object</caption>\n * var myObject;\n * var dataURL = canvas.toDataURL({\n * filter: (object) => object.isContainedWithinObject(myObject) || object.intersectsWithObject(myObject)\n * });\n */\n toDataURL(options = {} as TDataUrlOptions): string {\n const {\n format = 'png',\n quality = 1,\n multiplier = 1,\n enableRetinaScaling = false,\n } = options;\n const finalMultiplier =\n multiplier * (enableRetinaScaling ? this.getRetinaScaling() : 1);\n\n return toDataURL(\n this.toCanvasElement(finalMultiplier, options),\n format,\n quality,\n );\n }\n toBlob(options = {} as TDataUrlOptions): Promise<Blob | null> {\n const {\n format = 'png',\n quality = 1,\n multiplier = 1,\n enableRetinaScaling = false,\n } = options;\n const finalMultiplier =\n multiplier * (enableRetinaScaling ? this.getRetinaScaling() : 1);\n\n return toBlob(\n this.toCanvasElement(finalMultiplier, options),\n format,\n quality,\n );\n }\n\n /**\n * Create a new HTMLCanvas element painted with the current canvas content.\n * No need to resize the actual one or repaint it.\n * Will transfer object ownership to a new canvas, paint it, and set everything back.\n * This is an intermediary step used to get to a dataUrl but also it is useful to\n * create quick image copies of a canvas without passing for the dataUrl string\n * @param {Number} [multiplier] a zoom factor.\n * @param {Object} [options] Cropping informations\n * @param {Number} [options.left] Cropping left offset.\n * @param {Number} [options.top] Cropping top offset.\n * @param {Number} [options.width] Cropping width.\n * @param {Number} [options.height] Cropping height.\n * @param {(object: fabric.Object) => boolean} [options.filter] Function to filter objects.\n */\n toCanvasElement(\n multiplier = 1,\n { width, height, left, top, filter } = {} as TToCanvasElementOptions,\n ): HTMLCanvasElement {\n const scaledWidth = (width || this.width) * multiplier,\n scaledHeight = (height || this.height) * multiplier,\n zoom = this.getZoom(),\n originalWidth = this.width,\n originalHeight = this.height,\n originalSkipControlsDrawing = this.skipControlsDrawing,\n newZoom = zoom * multiplier,\n vp = this.viewportTransform,\n translateX = (vp[4] - (left || 0)) * multiplier,\n translateY = (vp[5] - (top || 0)) * multiplier,\n newVp = [newZoom, 0, 0, newZoom, translateX, translateY] as TMat2D,\n originalRetina = this.enableRetinaScaling,\n canvasEl = createCanvasElementFor({\n width: scaledWidth,\n height: scaledHeight,\n }),\n objectsToRender = filter\n ? this._objects.filter((obj) => filter(obj))\n : this._objects;\n this.enableRetinaScaling = false;\n this.viewportTransform = newVp;\n this.width = scaledWidth;\n this.height = scaledHeight;\n this.skipControlsDrawing = true;\n this.calcViewportBoundaries();\n this.renderCanvas(canvasEl.getContext('2d')!, objectsToRender);\n this.viewportTransform = vp;\n this.width = originalWidth;\n this.height = originalHeight;\n this.calcViewportBoundaries();\n this.enableRetinaScaling = originalRetina;\n this.skipControlsDrawing = originalSkipControlsDrawing;\n return canvasEl;\n }\n\n /**\n * Waits until rendering has settled to destroy the canvas\n * @returns {Promise<boolean>} a promise resolving to `true` once the canvas has been destroyed or to `false` if the canvas has was already destroyed\n * @throws if aborted by a consequent call\n */\n dispose() {\n !this.disposed &&\n this.elements.cleanupDOM({ width: this.width, height: this.height });\n runningAnimations.cancelByCanvas(this);\n this.disposed = true;\n return new Promise<boolean>((resolve, reject) => {\n const task = () => {\n this.destroy();\n resolve(true);\n };\n task.kill = reject;\n if (this.__cleanupTask) {\n this.__cleanupTask.kill('aborted');\n }\n\n if (this.destroyed) {\n resolve(false);\n } else if (this.nextRenderHandle) {\n this.__cleanupTask = task;\n } else {\n task();\n }\n });\n }\n\n /**\n * Clears the canvas element, disposes objects and frees resources.\n *\n * Invoked as part of the **async** operation of {@link dispose}.\n *\n * **CAUTION**:\n *\n * This method is **UNSAFE**.\n * You may encounter a race condition using it if there's a requested render.\n * Call this method only if you are sure rendering has settled.\n * Consider using {@link dispose} as it is **SAFE**\n *\n * @private\n */\n destroy() {\n this.destroyed = true;\n this.cancelRequestedRender();\n this.forEachObject((object) => object.dispose());\n this._objects = [];\n if (this.backgroundImage) {\n this.backgroundImage.dispose();\n }\n this.backgroundImage = undefined;\n if (this.overlayImage) {\n this.overlayImage.dispose();\n }\n this.overlayImage = undefined;\n this.elements.dispose();\n }\n\n /**\n * Returns a string representation of an instance\n * @return {String} string representation of an instance\n */\n toString() {\n return `#<Canvas (${this.complexity()}): { objects: ${\n this._objects.length\n } }>`;\n }\n}\n"],"names":["StaticCanvas","createCollectionMixin","CommonMethods","lowerCanvasEl","_this$elements$lower","elements","lower","el","contextContainer","_this$elements$lower2","ctx","getDefaults","ownDefaults","constructor","options","arguments","length","undefined","Object","assign","set","initElements","_setDimensionsImpl","width","height","skipControlsDrawing","viewportTransform","calcViewportBoundaries","StaticCanvas