diff --git a/CHANGELOG.md b/CHANGELOG.md index 7054d42c3c2..321c56035ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [next] +- chore(TS): migrate StatiCanvas to TS [#8485](https://github.com/fabricjs/fabric.js/pull/8485) - chore(): refactor `Object.__uid++` => `uid()` [#8482](https://github.com/fabricjs/fabric.js/pull/8482) - chore(TS): migrate object mixins to TS [#8414](https://github.com/fabricjs/fabric.js/pull/8414) - chore(TS): migrate filters [#8474](https://github.com/fabricjs/fabric.js/pull/8474) diff --git a/src/EventTypeDefs.ts b/src/EventTypeDefs.ts index 9bbbb975934..ad2317f6667 100644 --- a/src/EventTypeDefs.ts +++ b/src/EventTypeDefs.ts @@ -6,6 +6,7 @@ import type { TOriginX, TOriginY, TRadian } from './typedefs'; import type { saveObjectTransform } from './util/misc/objectTransforms'; import type { Canvas } from './__types__'; import type { IText } from './shapes/itext.class'; +import type { StaticCanvas } from './static_canvas.class'; export type ModifierKey = 'altKey' | 'shiftKey' | 'ctrlKey'; @@ -149,6 +150,11 @@ type CanvasSelectionEvents = { }; }; +type CollectionEvents = { + 'object:added': { target: FabricObject }; + 'object:removed': { target: FabricObject }; +}; + type BeforeSuffix = `${T}:before`; type WithBeforeSuffix = T | BeforeSuffix; @@ -181,17 +187,15 @@ export type ObjectEvents = ObjectPointerEvents & }; // tree - added: { target: Group | Canvas }; - removed: { target: Group | Canvas }; + added: { target: Group | Canvas | StaticCanvas }; + removed: { target: Group | Canvas | StaticCanvas }; // erasing 'erasing:end': { path: FabricObject }; }; -export type StaticCanvasEvents = { +export type StaticCanvasEvents = CollectionEvents & { // tree - 'object:added': { target: FabricObject }; - 'object:removed': { target: FabricObject }; 'canvas:cleared': never; // rendering diff --git a/src/canvas.class.ts b/src/canvas.class.ts index 3e1a8f967a7..2bc1501e47b 100644 --- a/src/canvas.class.ts +++ b/src/canvas.class.ts @@ -1418,6 +1418,11 @@ import { saveObjectTransform } from './util/misc/objectTransforms'; // this.discardActiveGroup(); this.discardActiveObject(); this.clearContext(this.contextTop); + if (this._hasITextHandlers) { + this.off('mouse:up', this._mouseUpITextHandler); + this._iTextInstances = null; + this._hasITextHandlers = false; + } return this.callSuper('clear'); }, diff --git a/src/gradient/gradient.class.ts b/src/gradient/gradient.class.ts index 89a619e994a..834b348d48a 100644 --- a/src/gradient/gradient.class.ts +++ b/src/gradient/gradient.class.ts @@ -75,10 +75,25 @@ export class Gradient< */ type: T; + /** + * Defines how the gradient is located in space and spread + * @type GradientCoords + */ coords: GradientCoords; + /** + * Defines how many colors a gradient has and how they are located on the axis + * defined by coords + * @type GradientCoords + */ colorStops: ColorStop[]; + /** + * If true, this object will not be exported during the serialization of a canvas + * @type boolean + */ + excludeFromExport?: boolean; + private id: string | number; constructor({ @@ -130,7 +145,7 @@ export class Gradient< * @param {string[]} [propertiesToInclude] Any properties that you might want to additionally include in the output * @return {object} */ - toObject(propertiesToInclude?: (keyof this)[]) { + toObject(propertiesToInclude?: (keyof this | string)[]) { return { ...pick(this, propertiesToInclude), type: this.type, @@ -276,11 +291,7 @@ export class Gradient< * @param {CanvasRenderingContext2D} ctx Context to render on * @return {CanvasGradient} */ - toLive(ctx: CanvasRenderingContext2D) { - if (!this.type) { - return; - } - + toLive(ctx: CanvasRenderingContext2D): CanvasGradient { const coords = this.coords as GradientCoords<'radial'>; const gradient = this.type === 'linear' diff --git a/src/mixins/collection.mixin.ts b/src/mixins/collection.mixin.ts index 33792aec6b4..3fcf5fa0e74 100644 --- a/src/mixins/collection.mixin.ts +++ b/src/mixins/collection.mixin.ts @@ -26,7 +26,7 @@ export function createCollectionMixin( * @param {...FabricObject[]} objects to add * @returns {number} new array length */ - add(...objects: FabricObject[]) { + add(...objects: FabricObject[]): number { const size = this._objects.push(...objects); objects.forEach((object) => this._onObjectAdded(object)); return size; diff --git a/src/mixins/object.svg_export.ts b/src/mixins/object.svg_export.ts index 72566784f38..40f7bf4d22d 100644 --- a/src/mixins/object.svg_export.ts +++ b/src/mixins/object.svg_export.ts @@ -5,7 +5,7 @@ import { uid } from '../util/internals/uid'; import { matrixToSVG } from '../util/misc/svgParsing'; import { toFixed } from '../util/misc/toFixed'; -type SVGReviver = (markup: string) => string; +export type TSVGReviver = (markup: string) => string; /* _TO_SVG_START_ */ @@ -29,6 +29,13 @@ function getSvgColorString(prop: string, value?: any) { } export class FabricObjectSVGExportMixin { + /** + * When an object is being exported as SVG as a clippath, a reference inside the SVG is needed. + * This reference is a UID in the fabric namespace and is temporary stored here. + * @type {String} + */ + clipPathId?: string; + /** * Returns styles-string for svg-export * @param {Boolean} skipShadow a boolean to skip shadow filter output @@ -191,10 +198,10 @@ export class FabricObjectSVGExportMixin { /** * Returns svg representation of an instance - * @param {SVGReviver} [reviver] Method for further parsing of svg representation. + * @param {TSVGReviver} [reviver] Method for further parsing of svg representation. * @return {String} svg representation of an instance */ - toSVG(reviver?: SVGReviver) { + toSVG(reviver?: TSVGReviver) { return this._createBaseSVGMarkup(this._toSVG(reviver), { reviver, }); @@ -202,10 +209,10 @@ export class FabricObjectSVGExportMixin { /** * Returns svg clipPath representation of an instance - * @param {SVGReviver} [reviver] Method for further parsing of svg representation. + * @param {TSVGReviver} [reviver] Method for further parsing of svg representation. * @return {String} svg representation of an instance */ - toClipPathSVG(reviver?: SVGReviver) { + toClipPathSVG(reviver?: TSVGReviver) { return ( '\t' + this._createBaseClipPathSVGMarkup(this._toSVG(reviver), { @@ -222,7 +229,7 @@ export class FabricObjectSVGExportMixin { { reviver, additionalTransform = '', - }: { reviver?: SVGReviver; additionalTransform?: string } = {} + }: { reviver?: TSVGReviver; additionalTransform?: string } = {} ) { const commonPieces = [ this.getSvgTransform(true, additionalTransform), @@ -246,7 +253,7 @@ export class FabricObjectSVGExportMixin { additionalTransform, }: { noStyle?: boolean; - reviver?: SVGReviver; + reviver?: TSVGReviver; withShadow?: boolean; additionalTransform?: string; } = {} diff --git a/src/mixins/object_geometry.mixin.ts b/src/mixins/object_geometry.mixin.ts index 0715e0141ac..10d149a5156 100644 --- a/src/mixins/object_geometry.mixin.ts +++ b/src/mixins/object_geometry.mixin.ts @@ -293,8 +293,8 @@ export class ObjectGeometry< */ intersectsWithObject( other: ObjectGeometry, - absolute: boolean, - calculate: boolean + absolute = false, + calculate = false ): boolean { const intersection = Intersection.intersectPolygonPolygon( this.getCoords(absolute, calculate), @@ -318,8 +318,8 @@ export class ObjectGeometry< */ isContainedWithinObject( other: ObjectGeometry, - absolute: boolean, - calculate: boolean + absolute = false, + calculate = false ): boolean { const points = this.getCoords(absolute, calculate), otherCoords = absolute ? other.aCoords : other.lineCoords, diff --git a/src/pattern.class.ts b/src/pattern.class.ts index 2460357c5c5..4c3b1d6fd76 100644 --- a/src/pattern.class.ts +++ b/src/pattern.class.ts @@ -75,8 +75,17 @@ export class Pattern { */ patternTransform: TMat2D | null = null; + /** + * The actual pixel source of the pattern + */ source!: CanvasImageSource; + /** + * If true, this object will not be exported during the serialization of a canvas + * @type boolean + */ + excludeFromExport?: boolean; + readonly id: number; /** @@ -122,7 +131,7 @@ export class Pattern { * @param {CanvasRenderingContext2D} ctx Context to create pattern * @return {CanvasPattern} */ - toLive(ctx: CanvasRenderingContext2D) { + toLive(ctx: CanvasRenderingContext2D): CanvasPattern | string { if ( // if the image failed to load, return, and allow rest to continue loading !this.source || @@ -143,7 +152,7 @@ export class Pattern { * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output * @return {object} Object representation of a pattern instance */ - toObject(propertiesToInclude?: (keyof this)[]) { + toObject(propertiesToInclude?: (keyof this | string)[]) { return { ...pick(this, propertiesToInclude), type: 'pattern', diff --git a/src/shapes/group.class.ts b/src/shapes/group.class.ts index 7da54cbdf10..923329df319 100644 --- a/src/shapes/group.class.ts +++ b/src/shapes/group.class.ts @@ -1,10 +1,10 @@ //@ts-nocheck -import { ObjectEvents } from '../EventTypeDefs'; +import type { ObjectEvents, CollectionEvents } from '../EventTypeDefs'; import { fabric } from '../../HEADER'; import { createCollectionMixin } from '../mixins/collection.mixin'; import { resolveOrigin } from '../mixins/object_origin.mixin'; import { Point } from '../point.class'; -import { TClassProperties } from '../typedefs'; +import type { TClassProperties } from '../typedefs'; import { cos } from '../util/misc/cos'; import { invertTransform, @@ -44,15 +44,14 @@ export type LayoutResult = { height: number; }; -export type GroupEvents = ObjectEvents & { - layout: { - context: LayoutContext; - result: LayoutResult; - diff: Point; +export type GroupEvents = ObjectEvents & + CollectionEvents & { + layout: { + context: LayoutContext; + result: LayoutResult; + diff: Point; + }; }; - 'object:added': { target: FabricObject }; - 'object:removed': { target: FabricObject }; -}; export type LayoutStrategy = | 'fit-content' diff --git a/src/shapes/object.class.ts b/src/shapes/object.class.ts index 89f09226a4c..84e5ebfc98b 100644 --- a/src/shapes/object.class.ts +++ b/src/shapes/object.class.ts @@ -7,7 +7,13 @@ import { ObjectEvents } from '../EventTypeDefs'; import { AnimatableObject } from '../mixins/object_animation.mixin'; import { Point } from '../point.class'; import { Shadow } from '../shadow.class'; -import type { TClassProperties, TDegree, TFiller, TSize } from '../typedefs'; +import type { + TClassProperties, + TDegree, + TFiller, + TSize, + TCacheCanvasDimensions, +} from '../typedefs'; import { runningAnimations } from '../util/animation_registry'; import { clone } from '../util/lang_object'; import { capitalize } from '../util/lang_string'; @@ -19,6 +25,18 @@ import { pick } from '../util/misc/pick'; import { toFixed } from '../util/misc/toFixed'; import type { Group } from './group.class'; +export type TCachedFabricObject = FabricObject & + Required< + Pick< + FabricObject, + | 'zoomX' + | 'zoomY' + | '_cacheCanvas' + | 'cacheTranslationX' + | 'cacheTranslationY' + > + >; + // temporary hack for unfinished migration type TCallSuper = (arg0: string, ...moreArgs: any[]) => any; @@ -596,6 +614,15 @@ export class FabricObject< */ ownCaching?: boolean; + /** + * Private. indicates if the object inside a group is on a transformed context or not + * or is part of a larger cache for many object ( a group for example) + * @type boolean + * @default undefined + * @private + */ + _transformDone?: boolean; + callSuper?: TCallSuper; /** @@ -694,7 +721,7 @@ export class FabricObject< * @return {Object}.zoomX zoomX zoom value to unscale the canvas before drawing cache * @return {Object}.zoomY zoomY zoom value to unscale the canvas before drawing cache */ - _getCacheCanvasDimensions() { + _getCacheCanvasDimensions(): TCacheCanvasDimensions { const objectScale = this.getTotalObjectScaling(), // calculate dimensions without skewing dim = this._getTransformedDimensions({ skewX: 0, skewY: 0 }), @@ -826,7 +853,7 @@ export class FabricObject< * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output * @return {Object} Object representation of an instance */ - toObject(propertiesToInclude?: (keyof this)[]): Record { + toObject(propertiesToInclude?: (keyof this | string)[]): Record { const NUM_FRACTION_DIGITS = config.NUM_FRACTION_DIGITS, clipPathData = this.clipPath && !this.clipPath.excludeFromExport @@ -891,7 +918,7 @@ export class FabricObject< * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output * @return {Object} Object representation of an instance */ - toDatalessObject(propertiesToInclude: (keyof this)[]) { + toDatalessObject(propertiesToInclude?: (keyof this | string)[]) { // will be overwritten by subclasses return this.toObject(propertiesToInclude); } diff --git a/src/shapes/text.class.ts b/src/shapes/text.class.ts index 215acc9ae1d..21694d19cbc 100644 --- a/src/shapes/text.class.ts +++ b/src/shapes/text.class.ts @@ -4,7 +4,7 @@ import { cache } from '../cache'; import { DEFAULT_SVG_FONT_SIZE } from '../constants'; import { ObjectEvents } from '../EventTypeDefs'; import { TextStyle, TextStyleMixin } from '../mixins/text_style.mixin'; -import { TClassProperties, TFiller } from '../typedefs'; +import { TClassProperties, TFiller, TCacheCanvasDimensions } from '../typedefs'; import { graphemeSplit } from '../util/lang_string'; import { createCanvasElement } from '../util/misc/dom'; import { @@ -556,7 +556,7 @@ export class Text< * @return {Object}.zoomX zoomX zoom value to unscale the canvas before drawing cache * @return {Object}.zoomY zoomY zoom value to unscale the canvas before drawing cache */ - _getCacheCanvasDimensions(): object { + _getCacheCanvasDimensions(): TCacheCanvasDimensions { const dims = super._getCacheCanvasDimensions(); const fontSize = this.fontSize; dims.width += fontSize * dims.zoomX; diff --git a/src/static_canvas.class.ts b/src/static_canvas.class.ts index 7a99dbba6f6..e9097b70360 100644 --- a/src/static_canvas.class.ts +++ b/src/static_canvas.class.ts @@ -1,1863 +1,1831 @@ -//@ts-nocheck +import { fabric } from '../HEADER'; import { config } from './config'; -import { VERSION } from './constants'; +import { iMatrix, VERSION } from './constants'; +import type { StaticCanvasEvents } from './EventTypeDefs'; +import { Gradient } from './gradient'; import { createCollectionMixin } from './mixins/collection.mixin'; +import { TSVGReviver } from './mixins/object.svg_export'; import { CommonMethods } from './mixins/shared_methods.mixin'; +import { Pattern } from './pattern.class'; import { Point } from './point.class'; import type { FabricObject } from './shapes/fabricObject.class'; -import { requestAnimFrame } from './util/animate'; +import { TCachedFabricObject } from './shapes/object.class'; +import { Rect } from './shapes/rect.class'; +import type { + TCornerPoint, + TFiller, + TMat2D, + TSize, + TValidToObjectMethod, +} from './typedefs'; +import { cancelAnimFrame, requestAnimFrame } from './util/animate'; +import { + cleanUpJsdomNode, + getElementOffset, + getNodeCanvas, +} from './util/dom_misc'; import { removeFromArray } from './util/internals'; import { uid } from './util/internals/uid'; +import { createCanvasElement, isHTMLCanvas } from './util/misc/dom'; +import { invertTransform, transformPoint } from './util/misc/matrix'; import { pick } from './util/misc/pick'; -(function (global) { - // aliases for faster resolution - var fabric = global.fabric, - extend = fabric.util.object.extend, - getElementOffset = fabric.util.getElementOffset, - toFixed = fabric.util.toFixed, - transformPoint = fabric.util.transformPoint, - invertTransform = fabric.util.invertTransform, - getNodeCanvas = fabric.util.getNodeCanvas, - createCanvasElement = fabric.util.createCanvasElement, - CANVAS_INIT_ERROR = new Error('Could not initialize `canvas` element'); - - /** - * Static canvas class - * @class fabric.StaticCanvas - * @mixes fabric.Observable - * @see {@link http://fabricjs.com/static_canvas|StaticCanvas demo} - * @see {@link fabric.StaticCanvas#initialize} for constructor definition - * @fires before:render - * @fires after:render - * @fires canvas:cleared - * @fires object:added - * @fires object:removed - */ - // eslint-disable-next-line max-len - fabric.StaticCanvas = fabric.util.createClass( - class extends createCollectionMixin(CommonMethods) { - add(...objects: FabricObject[]) { - super.add(...objects); - objects.length > 0 && this.renderOnAddRemove && this.requestRenderAll(); - return this; +import { matrixToSVG } from './util/misc/svgParsing'; +import { toFixed } from './util/misc/toFixed'; +import { + isActiveSelection, + isCollection, + isFiller, + isTextObject, +} from './util/types'; + +const CANVAS_INIT_ERROR = 'Could not initialize `canvas` element'; + +export type TCanvasSizeOptions = { + backstoreOnly?: boolean; + cssOnly?: boolean; +}; + +export type TSVGExportOptions = { + suppressPreamble?: boolean; + viewBox?: { + x: number; + y: number; + width: number; + height: number; + }; + encoding?: 'UTF-8'; // test Econding type and see what happens + width?: string; + height?: string; + reviver?: TSVGReviver; +}; + +/** + * Static canvas class + * @see {@link http://fabricjs.com/static_canvas|StaticCanvas demo} + * @fires before:render + * @fires after:render + * @fires canvas:cleared + * @fires object:added + * @fires object:removed + */ +// eslint-disable-next-line max-len +export class StaticCanvas extends createCollectionMixin( + CommonMethods +) { + /** + * Background color of canvas instance. + * @type {(String|TFiller)} + * @default + */ + backgroundColor: TFiller | string; + + /** + * Background image of canvas instance. + * since 2.4.0 image caching is active, please when putting an image as background, add to the + * canvas property a reference to the canvas it is on. Otherwise the image cannot detect the zoom + * vale. As an alternative you can disable image objectCaching + * @type FabricObject + * @default + */ + backgroundImage: FabricObject | null; + + /** + * Overlay color of canvas instance. + * @since 1.3.9 + * @type {(String|TFiller)} + * @default + */ + overlayColor: TFiller | string; + + /** + * Overlay image of canvas instance. + * since 2.4.0 image caching is active, please when putting an image as overlay, add to the + * canvas property a reference to the canvas it is on. Otherwise the image cannot detect the zoom + * vale. As an alternative you can disable image objectCaching + * @type FabricObject + * @default + */ + overlayImage: FabricObject | null; + + /** + * Indicates whether toObject/toDatalessObject should include default values + * if set to false, takes precedence over the object value. + * @type Boolean + * @default + */ + includeDefaultValues: boolean; + + /** + * Indicates whether objects' state should be saved + * @type Boolean + * @deprecated + * @default + */ + stateful: boolean; + + /** + * Indicates whether {@link add}, {@link insertAt} and {@link remove}, + * {@link moveTo}, {@link clear} and many more, should also re-render canvas. + * Disabling this option will not give a performance boost when adding/removing a lot of objects to/from canvas at once + * since the renders are quequed and executed one per frame. + * Disabling is suggested anyway and managing the renders of the app manually is not a big effort ( canvas.requestRenderAll() ) + * Left default to true to do not break documentation and old app, fiddles. + * @type Boolean + * @default + */ + renderOnAddRemove: boolean; + + /** + * Indicates whether object controls (borders/controls) are rendered above overlay image + * @type Boolean + * @default + */ + controlsAboveOverlay: boolean; + + /** + * Indicates whether the browser can be scrolled when using a touchscreen and dragging on the canvas + * @type Boolean + * @default + */ + allowTouchScrolling: boolean; + + /** + * Indicates whether this canvas will use image smoothing, this is on by default in browsers + * @type Boolean + * @default + */ + imageSmoothingEnabled: boolean; + + /** + * The transformation (a Canvas 2D API transform matrix) which focuses the viewport + * @type Array + * @example Default transform + * canvas.viewportTransform = [1, 0, 0, 1, 0, 0]; + * @example Scale by 70% and translate toward bottom-right by 50, without skewing + * canvas.viewportTransform = [0.7, 0, 0, 0.7, 50, 50]; + * @default + */ + viewportTransform: TMat2D; + + /** + * if set to false background image is not affected by viewport transform + * @since 1.6.3 + * @type Boolean + * @todo we should really find a different way to do this + * @default + */ + backgroundVpt: boolean; + + /** + * if set to false overlya image is not affected by viewport transform + * @since 1.6.3 + * @type Boolean + * @todo we should really find a different way to do this + * @default + */ + overlayVpt: boolean; + + /** + * When true, canvas is scaled by devicePixelRatio for better rendering on retina screens + * @type Boolean + * @default + */ + enableRetinaScaling: boolean; + + /** + * Describe canvas element extension over design + * properties are tl,tr,bl,br. + * if canvas is not zoomed/panned those points are the four corner of canvas + * if canvas is viewportTransformed you those points indicate the extension + * of canvas element in plain untrasformed coordinates + * The coordinates get updated with @method calcViewportBoundaries. + */ + vptCoords: TCornerPoint; + + /** + * Based on vptCoords and object.aCoords, skip rendering of objects that + * are not included in current viewport. + * May greatly help in applications with crowded canvas and use of zoom/pan + * If One of the corner of the bounding box of the object is on the canvas + * the objects get rendered. + * @type Boolean + * @default + */ + skipOffscreen: boolean; + + /** + * a fabricObject that, without stroke define a clipping area with their shape. filled in black + * the clipPath object gets used when the canvas has rendered, and the context is placed in the + * top left corner of the canvas. + * clipPath will clip away controls, if you do not want this to happen use controlsAboveOverlay = true + * @type FabricObject + */ + clipPath: FabricObject; + + /** + * A reference to the canvas actual HTMLCanvasElement. + * Can be use to read the raw pixels, but never write or manipulate + * @type HTMLCanvasElement + */ + lowerCanvasEl: HTMLCanvasElement; + + /** + * Width in virtual/logical pixels of the canvas. + * The canvas can be larger than width if retina scaling is active + * @type number + */ + width: number; + + /** + * Height in virtual/logical pixels of the canvas. + * The canvas can be taller than width if retina scaling is active + * @type height + */ + height: number; + + /** + * If true the Canvas is in the process or has been disposed/destroyed. + * No more rendering operation will be executed on this canvas. + * @type boolean + */ + destroyed?: boolean; + + /** + * Started the process of disposing but not done yet. + * WIll likely complete the render cycle already scheduled but stopping adding more. + * @type boolean + */ + disposed?: boolean; + + add(...objects: FabricObject[]) { + const size = super.add(...objects); + objects.length > 0 && this.renderOnAddRemove && this.requestRenderAll(); + return size; + } + + insertAt(index: number, ...objects: FabricObject[]) { + const size = super.insertAt(index, ...objects); + objects.length > 0 && this.renderOnAddRemove && this.requestRenderAll(); + return size; + } + + remove(...objects: FabricObject[]) { + const removed = super.remove(...objects); + removed.length > 0 && this.renderOnAddRemove && this.requestRenderAll(); + return removed; + } + + _onObjectAdded(obj: FabricObject) { + // @ts-ignore; + this.stateful && obj.setupState(); + if (obj.canvas && obj.canvas !== this) { + /* _DEV_MODE_START_ */ + console.warn( + 'fabric.Canvas: trying to add an object that belongs to a different canvas.\n' + + 'Resulting to default behavior: removing object from previous canvas and adding to new canvas' + ); + /* _DEV_MODE_END_ */ + obj.canvas.remove(obj); + } + obj._set('canvas', this); + obj.setCoords(); + this.fire('object:added', { target: obj }); + obj.fire('added', { target: this }); + } + + _onObjectRemoved(obj: FabricObject) { + obj._set('canvas', undefined); + this.fire('object:removed', { target: obj }); + obj.fire('removed', { target: this }); + } + + initialize(el: string | HTMLCanvasElement, options = {}) { + this.renderAndResetBound = this.renderAndReset.bind(this); + this.requestRenderAllBound = this.requestRenderAll.bind(this); + this._initStatic(el, options); + this.calcViewportBoundaries(); + } + + constructor(el: string | HTMLCanvasElement, options = {}) { + super(); + this.renderAndResetBound = this.renderAndReset.bind(this); + this.requestRenderAllBound = this.requestRenderAll.bind(this); + this._initStatic(el, options); + this.calcViewportBoundaries(); + } + + /** + * @private + * @param {HTMLCanvasElement | String} el element to initialize instance on + * @param {Object} [options] Options object + */ + _initStatic(el: string | HTMLCanvasElement, options = {}) { + this._objects = []; + this._createLowerCanvas(el); + this._initOptions(options); + // only initialize retina scaling once + if (!this.interactive) { + this._initRetinaScaling(); + } + this.calcOffset(); + } + + /** + * @private + */ + _isRetinaScaling() { + return config.devicePixelRatio > 1 && this.enableRetinaScaling; + } + + /** + * @private + * @return {Number} retinaScaling if applied, otherwise 1; + */ + getRetinaScaling() { + return this._isRetinaScaling() ? Math.max(1, config.devicePixelRatio) : 1; + } + + /** + * @private + */ + _initRetinaScaling() { + if (!this._isRetinaScaling()) { + return; + } + const scaleRatio = config.devicePixelRatio; + this.__initRetinaScaling( + scaleRatio, + this.lowerCanvasEl, + this.contextContainer + ); + if (this.upperCanvasEl) { + this.__initRetinaScaling(scaleRatio, this.upperCanvasEl, this.contextTop); + } + } + + __initRetinaScaling( + scaleRatio: number, + canvas: HTMLCanvasElement, + context: CanvasRenderingContext2D + ) { + canvas.setAttribute('width', (this.width * scaleRatio).toString()); + canvas.setAttribute('height', (this.height * scaleRatio).toString()); + context.scale(scaleRatio, scaleRatio); + } + + /** + * Calculates canvas element offset relative to the document + * This method is also attached as "resize" event handler of window + * @return {fabric.Canvas} instance + * @chainable + */ + calcOffset() { + this._offset = getElementOffset(this.lowerCanvasEl); + return this; + } + + /** + * @private + */ + _createCanvasElement() { + const element = createCanvasElement(); + if (!element) { + throw new Error(CANVAS_INIT_ERROR); + } + if (typeof element.getContext === 'undefined') { + throw new Error(CANVAS_INIT_ERROR); + } + return element; + } + + /** + * @private + * @param {Object} [options] Options object + */ + _initOptions(options = {}) { + const lowerCanvasEl = this.lowerCanvasEl; + this.set(options); + + this.width = this.width || lowerCanvasEl.width || 0; + this.height = this.height || lowerCanvasEl.height || 0; + + if (!this.lowerCanvasEl.style) { + return; + } + + lowerCanvasEl.width = this.width; + lowerCanvasEl.height = this.height; + + lowerCanvasEl.style.width = this.width + 'px'; + lowerCanvasEl.style.height = this.height + 'px'; + + this.viewportTransform = [...this.viewportTransform]; + } + + /** + * Creates a bottom canvas + * @private + * @param {HTMLElement} [canvasEl] + */ + _createLowerCanvas(canvasEl: HTMLCanvasElement | string) { + // canvasEl === 'HTMLCanvasElement' does not work on jsdom/node + if (isHTMLCanvas(canvasEl)) { + this.lowerCanvasEl = canvasEl; + } else { + this.lowerCanvasEl = + fabric.document.getElementById(canvasEl) || + canvasEl || + this._createCanvasElement(); + } + if (this.lowerCanvasEl.hasAttribute('data-fabric')) { + /* _DEV_MODE_START_ */ + throw new Error( + 'fabric.js: trying to initialize a canvas that has already been initialized' + ); + /* _DEV_MODE_END_ */ + } + this.lowerCanvasEl.classList.add('lower-canvas'); + this.lowerCanvasEl.setAttribute('data-fabric', 'main'); + if (this.interactive) { + this._originalCanvasStyle = this.lowerCanvasEl.style.cssText; + this._applyCanvasStyle(this.lowerCanvasEl); + } + + this.contextContainer = this.lowerCanvasEl.getContext('2d'); + } + + /** + * Returns canvas width (in px) + * @return {Number} + */ + getWidth(): number { + return this.width; + } + + /** + * Returns canvas height (in px) + * @return {Number} + */ + getHeight(): number { + return this.height; + } + + /** + * Sets width of this canvas instance + * @param {Number|String} value Value to set width to + * @param {Object} [options] Options object + * @param {Boolean} [options.backstoreOnly=false] Set the given dimensions only as canvas backstore dimensions + * @param {Boolean} [options.cssOnly=false] Set the given dimensions only as css dimensions + * @deprecated will be removed in 7.0 + */ + setWidth(value: number, options: TCanvasSizeOptions) { + return this.setDimensions({ width: value }, options); + } + + /** + * Sets height of this canvas instance + * @param {Number|String} value Value to set height to + * @param {Object} [options] Options object + * @param {Boolean} [options.backstoreOnly=false] Set the given dimensions only as canvas backstore dimensions + * @param {Boolean} [options.cssOnly=false] Set the given dimensions only as css dimensions + * @deprecated will be removed in 7.0 + */ + setHeight(value: number, options: TCanvasSizeOptions) { + return this.setDimensions({ height: value }, options); + } + + /** + * Sets dimensions (width, height) of this canvas instance. when options.cssOnly flag active you should also supply the unit of measure (px/%/em) + * @param {Object} dimensions Object with width/height properties + * @param {Number|String} [dimensions.width] Width of canvas element + * @param {Number|String} [dimensions.height] Height of canvas element + * @param {Object} [options] Options object + * @param {Boolean} [options.backstoreOnly=false] Set the given dimensions only as canvas backstore dimensions + * @param {Boolean} [options.cssOnly=false] Set the given dimensions only as css dimensions + * @return {fabric.Canvas} thisArg + * @chainable + */ + setDimensions( + dimensions: Partial, + { cssOnly = false, backstoreOnly = false }: TCanvasSizeOptions = {} + ) { + Object.entries(dimensions).forEach(([prop, value]) => { + let cssValue = `${value}`; + + if (!cssOnly) { + this._setBackstoreDimension(prop as keyof TSize, value); + cssValue += 'px'; + this.hasLostContext = true; + } + + if (!backstoreOnly) { + this._setCssDimension(prop as keyof TSize, cssValue); + } + }); + + // @TODO: move to Canvas + if (this._isCurrentlyDrawing) { + this.freeDrawingBrush && + this.freeDrawingBrush._setBrushStyles(this.contextTop); + } + this._initRetinaScaling(); + this.calcOffset(); + + if (!cssOnly) { + this.requestRenderAll(); + } + + return this; + } + + /** + * Helper for setting width/height + * @private + * @param {String} prop property (width|height) + * @param {Number} value value to set property to + * @return {fabric.Canvas} instance + * @todo subclass in canvas and handle upperCanvasEl there. + * @chainable true + */ + _setBackstoreDimension(prop: keyof TSize, value: number) { + this.lowerCanvasEl[prop] = value; + + if (this.upperCanvasEl) { + this.upperCanvasEl[prop] = value; + } + + if (this.cacheCanvasEl) { + this.cacheCanvasEl[prop] = value; + } + + this[prop] = value; + + return this; + } + + /** + * Helper for setting css width/height + * @private + * @param {String} prop property (width|height) + * @param {String} value value to set property to + * @return {fabric.Canvas} instance + * @todo subclass in canvas and handle upperCanvasEl there. + * @chainable true + */ + _setCssDimension(prop: keyof TSize, value: string) { + this.lowerCanvasEl.style[prop] = value; + + if (this.upperCanvasEl) { + this.upperCanvasEl.style[prop] = value; + } + + if (this.wrapperEl) { + this.wrapperEl.style[prop] = value; + } + + return this; + } + + /** + * Returns canvas zoom level + * @return {Number} + */ + getZoom() { + return this.viewportTransform[0]; + } + + /** + * Sets viewport transformation of this canvas instance + * @param {Array} vpt a Canvas 2D API transform matrix + * @return {fabric.Canvas} instance + * @chainable true + */ + setViewportTransform(vpt: TMat2D) { + const activeObject = this._activeObject, + backgroundObject = this.backgroundImage, + overlayObject = this.overlayImage, + len = this._objects.length; + + this.viewportTransform = vpt; + for (let i = 0; i < len; i++) { + const object = this._objects[i]; + object.group || object.setCoords(); + } + if (activeObject) { + activeObject.setCoords(); + } + if (backgroundObject) { + backgroundObject.setCoords(); + } + if (overlayObject) { + overlayObject.setCoords(); + } + this.calcViewportBoundaries(); + this.renderOnAddRemove && this.requestRenderAll(); + return this; + } + + /** + * Sets zoom level of this canvas instance, the zoom centered around point + * meaning that following zoom to point with the same point will have the visual + * effect of the zoom originating from that point. The point won't move. + * It has nothing to do with canvas center or visual center of the viewport. + * @param {Point} point to zoom with respect to + * @param {Number} value to set zoom to, less than 1 zooms out + * @return {fabric.Canvas} instance + * @chainable true + */ + zoomToPoint(point: Point, value: number) { + // TODO: just change the scale, preserve other transformations + const before = point, + vpt: TMat2D = [...this.viewportTransform]; + const newPoint = transformPoint(point, invertTransform(vpt)); + vpt[0] = value; + vpt[3] = value; + const after = transformPoint(newPoint, vpt); + vpt[4] += before.x - after.x; + vpt[5] += before.y - after.y; + return this.setViewportTransform(vpt); + } + + /** + * Sets zoom level of this canvas instance + * @param {Number} value to set zoom to, less than 1 zooms out + * @return {fabric.Canvas} instance + * @chainable true + */ + setZoom(value: number) { + return this.zoomToPoint(new Point(0, 0), value); + } + + /** + * Pan viewport so as to place point at top left corner of canvas + * @param {Point} point to move to + */ + absolutePan(point: Point) { + const vpt: TMat2D = [...this.viewportTransform]; + vpt[4] = -point.x; + vpt[5] = -point.y; + return this.setViewportTransform(vpt); + } + + /** + * Pans viewpoint relatively + * @param {Point} point (position vector) to move by + */ + relativePan(point: Point) { + return this.absolutePan( + new Point( + -point.x - this.viewportTransform[4], + -point.y - this.viewportTransform[5] + ) + ); + } + + /** + * Returns <canvas> element corresponding to this instance + * @return {HTMLCanvasElement} + */ + getElement() { + return this.lowerCanvasEl; + } + + /** + * Clears specified context of canvas element + * @param {CanvasRenderingContext2D} ctx Context to clear + * @return {fabric.Canvas} thisArg + * @chainable + */ + clearContext(ctx: CanvasRenderingContext2D): StaticCanvas { + ctx.clearRect(0, 0, this.width, this.height); + return this; + } + + /** + * Returns context of canvas where objects are drawn + * @return {CanvasRenderingContext2D} + */ + getContext() { + return this.contextContainer; + } + + /** + * Clears all contexts (background, main, top) of an instance + * @return {fabric.Canvas} thisArg + * @chainable + */ + clear(): StaticCanvas { + this.remove(...this.getObjects()); + this.backgroundImage = null; + this.overlayImage = null; + this.backgroundColor = ''; + this.overlayColor = ''; + this.clearContext(this.contextContainer); + this.fire('canvas:cleared'); + this.renderOnAddRemove && this.requestRenderAll(); + return this; + } + + /** + * Renders the canvas + * @return {fabric.Canvas} instance + * @chainable + */ + renderAll(): StaticCanvas { + this.cancelRequestedRender(); + if (this.destroyed) { + return this; + } + this.renderCanvas(this.contextContainer, this._objects); + return this; + } + + /** + * Function created to be instance bound at initialization + * used in requestAnimationFrame rendering + * Let the fabricJS call it. If you call it manually you could have more + * animationFrame stacking on to of each other + * for an imperative rendering, use canvas.renderAll + * @private + * @return {fabric.Canvas} instance + * @chainable + */ + renderAndReset() { + this.nextRenderHandle = 0; + this.renderAll(); + } + + /** + * Append a renderAll request to next animation frame. + * unless one is already in progress, in that case nothing is done + * a boolean flag will avoid appending more. + * @return {fabric.Canvas} instance + * @chainable + */ + requestRenderAll(): StaticCanvas { + if (!this.nextRenderHandle && !this.disposed && !this.destroyed) { + this.nextRenderHandle = requestAnimFrame(this.renderAndResetBound); + } + return this; + } + + /** + * Calculate the position of the 4 corner of canvas with current viewportTransform. + * helps to determinate when an object is in the current rendering viewport using + * object absolute coordinates ( aCoords ) + * @return {Object} points.tl + * @chainable + */ + calcViewportBoundaries(): TCornerPoint { + const width = this.width, + height = this.height, + iVpt = invertTransform(this.viewportTransform), + a = transformPoint({ x: 0, y: 0 }, iVpt), + b = transformPoint({ x: width, y: height }, iVpt), + // we don't support vpt flipping + // but the code is robust enough to mostly work with flipping + min = a.min(b), + max = a.max(b); + return (this.vptCoords = { + tl: min, + tr: new Point(max.x, min.y), + bl: new Point(min.x, max.y), + br: max, + }); + } + + cancelRequestedRender() { + if (this.nextRenderHandle) { + cancelAnimFrame(this.nextRenderHandle); + this.nextRenderHandle = 0; + } + } + + /** + * Renders background, objects, overlay and controls. + * @param {CanvasRenderingContext2D} ctx + * @param {Array} objects to render + * @return {fabric.Canvas} instance + * @chainable + */ + renderCanvas(ctx: CanvasRenderingContext2D, objects: FabricObject[]) { + if (this.destroyed) { + return; + } + + const v = this.viewportTransform, + path = this.clipPath; + this.calcViewportBoundaries(); + this.clearContext(ctx); + ctx.imageSmoothingEnabled = this.imageSmoothingEnabled; + // node-canvas + // @ts-ignore + ctx.patternQuality = 'best'; + this.fire('before:render', { ctx: ctx }); + this._renderBackground(ctx); + + ctx.save(); + //apply viewport transform once for all rendering process + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + this._renderObjects(ctx, objects); + ctx.restore(); + if (!this.controlsAboveOverlay && this.interactive) { + this.drawControls(ctx); + } + if (path) { + path._set('canvas', this); + // needed to setup a couple of variables + path.shouldCache(); + path._transformDone = true; + path.renderCache({ forClipping: true }); + this.drawClipPathOnCanvas(ctx, path as TCachedFabricObject); + } + this._renderOverlay(ctx); + if (this.controlsAboveOverlay && this.interactive) { + this.drawControls(ctx); + } + this.fire('after:render', { ctx: ctx }); + + if (this.__cleanupTask) { + this.__cleanupTask(); + this.__cleanupTask = undefined; + } + } + + /** + * Paint the cached clipPath on the lowerCanvasEl + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + drawClipPathOnCanvas( + ctx: CanvasRenderingContext2D, + clipPath: TCachedFabricObject + ) { + const v = this.viewportTransform; + ctx.save(); + ctx.transform(...v); + // DEBUG: uncomment this line, comment the following + // ctx.globalAlpha = 0.4; + ctx.globalCompositeOperation = 'destination-in'; + clipPath.transform(ctx); + ctx.scale(1 / clipPath.zoomX, 1 / clipPath.zoomY); + ctx.drawImage( + clipPath._cacheCanvas, + -clipPath.cacheTranslationX, + -clipPath.cacheTranslationY + ); + ctx.restore(); + } + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Array} objects to render + */ + _renderObjects(ctx: CanvasRenderingContext2D, objects: FabricObject[]) { + for (let i = 0, len = objects.length; i < len; ++i) { + objects[i] && objects[i].render(ctx); + } + } + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {string} property 'background' or 'overlay' + */ + _renderBackgroundOrOverlay( + ctx: CanvasRenderingContext2D, + property: 'background' | 'overlay' + ) { + const fill = this[`${property}Color`], + object = this[`${property}Image`], + v = this.viewportTransform, + needsVpt = this[`${property}Vpt`]; + if (!fill && !object) { + return; + } + const isAFiller = isFiller(fill); + if (fill) { + ctx.save(); + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(this.width, 0); + ctx.lineTo(this.width, this.height); + ctx.lineTo(0, this.height); + ctx.closePath(); + ctx.fillStyle = isAFiller ? fill.toLive(ctx /* this */) : fill; + if (needsVpt) { + ctx.transform(...v); + } + if (isAFiller) { + ctx.transform(1, 0, 0, 1, fill.offsetX || 0, fill.offsetY || 0); + const m = ((fill as Gradient<'linear'>).gradientTransform || + (fill as Pattern).patternTransform) as TMat2D; + m && ctx.transform(...m); } + ctx.fill(); + ctx.restore(); + } + if (object) { + ctx.save(); + if (needsVpt) { + ctx.transform(...v); + } + object.render(ctx); + ctx.restore(); + } + } + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderBackground(ctx: CanvasRenderingContext2D) { + this._renderBackgroundOrOverlay(ctx, 'background'); + } + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderOverlay(ctx: CanvasRenderingContext2D) { + this._renderBackgroundOrOverlay(ctx, 'overlay'); + } + + /** + * Returns coordinates of a center of canvas. + * Returned value is an object with top and left properties + * @return {Object} object with "top" and "left" number values + * @deprecated migrate to `getCenterPoint` + */ + getCenter() { + return { + top: this.height / 2, + left: this.width / 2, + }; + } - insertAt(index: number, ...objects: FabricObject[]) { - super.insertAt(index, ...objects); - objects.length > 0 && this.renderOnAddRemove && this.requestRenderAll(); - return this; - } + /** + * Returns coordinates of a center of canvas. + * @return {Point} + */ + getCenterPoint() { + return new Point(this.width / 2, this.height / 2); + } - remove(...objects: FabricObject[]) { - const removed = super.remove(...objects); - removed.length > 0 && this.renderOnAddRemove && this.requestRenderAll(); - return this; - } + /** + * Centers object horizontally in the canvas + * @param {FabricObject} object Object to center horizontally + * @return {fabric.Canvas} thisArg + */ + centerObjectH(object: FabricObject) { + return this._centerObject( + object, + new Point(this.getCenterPoint().x, object.getCenterPoint().y) + ); + } - protected _onObjectAdded(obj: FabricObject) { - this.stateful && obj.setupState(); - if (obj.canvas && obj.canvas !== this) { - /* _DEV_MODE_START_ */ - console.warn( - 'fabric.Canvas: trying to add an object that belongs to a different canvas.\n' + - 'Resulting to default behavior: removing object from previous canvas and adding to new canvas' - ); - /* _DEV_MODE_END_ */ - obj.canvas.remove(obj); - } - obj._set('canvas', this); - obj.setCoords(); - this.fire('object:added', { target: obj }); - obj.fire('added', { target: this }); - } + /** + * Centers object vertically in the canvas + * @param {FabricObject} object Object to center vertically + * @return {fabric.Canvas} thisArg + * @chainable + */ + centerObjectV(object: FabricObject) { + return this._centerObject( + object, + new Point(object.getCenterPoint().x, this.getCenterPoint().y) + ); + } - protected _onObjectRemoved(obj: FabricObject) { - obj._set('canvas', undefined); - this.fire('object:removed', { target: obj }); - obj.fire('removed', { target: this }); - } - }, - /** @lends fabric.StaticCanvas.prototype */ { - /** - * Constructor - * @param {HTMLElement | String} el <canvas> element to initialize instance on - * @param {Object} [options] Options object - * @return {Object} thisArg - */ - initialize: function (el, options) { - options || (options = {}); - this.renderAndResetBound = this.renderAndReset.bind(this); - this.requestRenderAllBound = this.requestRenderAll.bind(this); - this._initStatic(el, options); - }, - - /** - * Background color of canvas instance. - * @type {(String|fabric.Pattern)} - * @default - */ - backgroundColor: '', - - /** - * Background image of canvas instance. - * since 2.4.0 image caching is active, please when putting an image as background, add to the - * canvas property a reference to the canvas it is on. Otherwise the image cannot detect the zoom - * vale. As an alternative you can disable image objectCaching - * @type fabric.Image - * @default - */ - backgroundImage: null, - - /** - * Overlay color of canvas instance. - * @since 1.3.9 - * @type {(String|fabric.Pattern)} - * @default - */ - overlayColor: '', - - /** - * Overlay image of canvas instance. - * since 2.4.0 image caching is active, please when putting an image as overlay, add to the - * canvas property a reference to the canvas it is on. Otherwise the image cannot detect the zoom - * vale. As an alternative you can disable image objectCaching - * @type fabric.Image - * @default - */ - overlayImage: null, - - /** - * Indicates whether toObject/toDatalessObject should include default values - * if set to false, takes precedence over the object value. - * @type Boolean - * @default - */ - includeDefaultValues: true, - - /** - * Indicates whether objects' state should be saved - * @type Boolean - * @default - */ - stateful: false, - - /** - * Indicates whether {@link add}, {@link insertAt} and {@link remove}, - * {@link fabric.StaticCanvas.moveTo}, {@link fabric.StaticCanvas.clear} and many more, should also re-render canvas. - * Disabling this option will not give a performance boost when adding/removing a lot of objects to/from canvas at once - * since the renders are quequed and executed one per frame. - * Disabling is suggested anyway and managing the renders of the app manually is not a big effort ( canvas.requestRenderAll() ) - * Left default to true to do not break documentation and old app, fiddles. - * @type Boolean - * @default - */ - renderOnAddRemove: true, - - /** - * Indicates whether object controls (borders/controls) are rendered above overlay image - * @type Boolean - * @default - */ - controlsAboveOverlay: false, - - /** - * Indicates whether the browser can be scrolled when using a touchscreen and dragging on the canvas - * @type Boolean - * @default - */ - allowTouchScrolling: false, - - /** - * Indicates whether this canvas will use image smoothing, this is on by default in browsers - * @type Boolean - * @default - */ - imageSmoothingEnabled: true, - - /** - * The transformation (a Canvas 2D API transform matrix) which focuses the viewport - * @type Array - * @example Default transform - * canvas.viewportTransform = [1, 0, 0, 1, 0, 0]; - * @example Scale by 70% and translate toward bottom-right by 50, without skewing - * canvas.viewportTransform = [0.7, 0, 0, 0.7, 50, 50]; - * @default - */ - viewportTransform: fabric.iMatrix.concat(), - - /** - * if set to false background image is not affected by viewport transform - * @since 1.6.3 - * @type Boolean - * @default - */ - backgroundVpt: true, - - /** - * if set to false overlya image is not affected by viewport transform - * @since 1.6.3 - * @type Boolean - * @default - */ - overlayVpt: true, - - /** - * When true, canvas is scaled by devicePixelRatio for better rendering on retina screens - * @type Boolean - * @default - */ - enableRetinaScaling: true, - - /** - * Describe canvas element extension over design - * properties are tl,tr,bl,br. - * if canvas is not zoomed/panned those points are the four corner of canvas - * if canvas is viewportTransformed you those points indicate the extension - * of canvas element in plain untrasformed coordinates - * The coordinates get updated with @method calcViewportBoundaries. - * @memberOf fabric.StaticCanvas.prototype - */ - vptCoords: {}, - - /** - * Based on vptCoords and object.aCoords, skip rendering of objects that - * are not included in current viewport. - * May greatly help in applications with crowded canvas and use of zoom/pan - * If One of the corner of the bounding box of the object is on the canvas - * the objects get rendered. - * @memberOf fabric.StaticCanvas.prototype - * @type Boolean - * @default - */ - skipOffscreen: true, - - /** - * a fabricObject that, without stroke define a clipping area with their shape. filled in black - * the clipPath object gets used when the canvas has rendered, and the context is placed in the - * top left corner of the canvas. - * clipPath will clip away controls, if you do not want this to happen use controlsAboveOverlay = true - * @type fabric.Object - */ - clipPath: undefined, - - /** - * @private - * @param {HTMLElement | String} el <canvas> element to initialize instance on - * @param {Object} [options] Options object - */ - _initStatic: function (el, options) { - this._objects = []; - this._createLowerCanvas(el); - this._initOptions(options); - // only initialize retina scaling once - if (!this.interactive) { - this._initRetinaScaling(); - } - this.calcOffset(); - }, - - /** - * @private - */ - _isRetinaScaling: function () { - return config.devicePixelRatio > 1 && this.enableRetinaScaling; - }, - - /** - * @private - * @return {Number} retinaScaling if applied, otherwise 1; - */ - getRetinaScaling: function () { - return this._isRetinaScaling() - ? Math.max(1, config.devicePixelRatio) - : 1; - }, - - /** - * @private - */ - _initRetinaScaling: function () { - if (!this._isRetinaScaling()) { - return; - } - var scaleRatio = config.devicePixelRatio; - this.__initRetinaScaling( - scaleRatio, - this.lowerCanvasEl, - this.contextContainer - ); - if (this.upperCanvasEl) { - this.__initRetinaScaling( - scaleRatio, - this.upperCanvasEl, - this.contextTop - ); - } - }, - - __initRetinaScaling: function (scaleRatio, canvas, context) { - canvas.setAttribute('width', this.width * scaleRatio); - canvas.setAttribute('height', this.height * scaleRatio); - context.scale(scaleRatio, scaleRatio); - }, - - /** - * Calculates canvas element offset relative to the document - * This method is also attached as "resize" event handler of window - * @return {fabric.Canvas} instance - * @chainable - */ - calcOffset: function () { - this._offset = getElementOffset(this.lowerCanvasEl); - return this; - }, - - /** - * @private - */ - _createCanvasElement: function () { - var element = createCanvasElement(); - if (!element) { - throw CANVAS_INIT_ERROR; - } - if (!element.style) { - element.style = {}; - } - if (typeof element.getContext === 'undefined') { - throw CANVAS_INIT_ERROR; - } - return element; - }, - - /** - * @private - * @param {Object} [options] Options object - */ - _initOptions: function (options) { - var lowerCanvasEl = this.lowerCanvasEl; - this.set(options); - - this.width = this.width || parseInt(lowerCanvasEl.width, 10) || 0; - this.height = this.height || parseInt(lowerCanvasEl.height, 10) || 0; - - if (!this.lowerCanvasEl.style) { - return; - } + /** + * Centers object vertically and horizontally in the canvas + * @param {FabricObject} object Object to center vertically and horizontally + * @return {fabric.Canvas} thisArg + * @chainable + */ + centerObject(object: FabricObject) { + return this._centerObject(object, this.getCenterPoint()); + } - lowerCanvasEl.width = this.width; - lowerCanvasEl.height = this.height; - - lowerCanvasEl.style.width = this.width + 'px'; - lowerCanvasEl.style.height = this.height + 'px'; - - this.viewportTransform = this.viewportTransform.slice(); - }, - - /** - * Creates a bottom canvas - * @private - * @param {HTMLElement} [canvasEl] - */ - _createLowerCanvas: function (canvasEl) { - // canvasEl === 'HTMLCanvasElement' does not work on jsdom/node - if (canvasEl && canvasEl.getContext) { - this.lowerCanvasEl = canvasEl; - } else { - this.lowerCanvasEl = - fabric.document.getElementById(canvasEl) || - canvasEl || - this._createCanvasElement(); - } - if (this.lowerCanvasEl.hasAttribute('data-fabric')) { - /* _DEV_MODE_START_ */ - throw new Error( - 'fabric.js: trying to initialize a canvas that has already been initialized' - ); - /* _DEV_MODE_END_ */ - } - this.lowerCanvasEl.classList.add('lower-canvas'); - this.lowerCanvasEl.setAttribute('data-fabric', 'main'); - if (this.interactive) { - this._originalCanvasStyle = this.lowerCanvasEl.style.cssText; - this._applyCanvasStyle(this.lowerCanvasEl); - } + /** + * Centers object vertically and horizontally in the viewport + * @param {FabricObject} object Object to center vertically and horizontally + * @return {fabric.Canvas} thisArg + * @chainable + */ + viewportCenterObject(object: FabricObject) { + return this._centerObject(object, this.getVpCenter()); + } - this.contextContainer = this.lowerCanvasEl.getContext('2d'); - }, - - /** - * Returns canvas width (in px) - * @return {Number} - */ - getWidth: function () { - return this.width; - }, - - /** - * Returns canvas height (in px) - * @return {Number} - */ - getHeight: function () { - return this.height; - }, - - /** - * Sets width of this canvas instance - * @param {Number|String} value Value to set width to - * @param {Object} [options] Options object - * @param {Boolean} [options.backstoreOnly=false] Set the given dimensions only as canvas backstore dimensions - * @param {Boolean} [options.cssOnly=false] Set the given dimensions only as css dimensions - * @return {fabric.Canvas} instance - * @chainable true - */ - setWidth: function (value, options) { - return this.setDimensions({ width: value }, options); - }, - - /** - * Sets height of this canvas instance - * @param {Number|String} value Value to set height to - * @param {Object} [options] Options object - * @param {Boolean} [options.backstoreOnly=false] Set the given dimensions only as canvas backstore dimensions - * @param {Boolean} [options.cssOnly=false] Set the given dimensions only as css dimensions - * @return {fabric.Canvas} instance - * @chainable true - */ - setHeight: function (value, options) { - return this.setDimensions({ height: value }, options); - }, - - /** - * Sets dimensions (width, height) of this canvas instance. when options.cssOnly flag active you should also supply the unit of measure (px/%/em) - * @param {Object} dimensions Object with width/height properties - * @param {Number|String} [dimensions.width] Width of canvas element - * @param {Number|String} [dimensions.height] Height of canvas element - * @param {Object} [options] Options object - * @param {Boolean} [options.backstoreOnly=false] Set the given dimensions only as canvas backstore dimensions - * @param {Boolean} [options.cssOnly=false] Set the given dimensions only as css dimensions - * @return {fabric.Canvas} thisArg - * @chainable - */ - setDimensions: function (dimensions, options) { - var cssValue; - - options = options || {}; - - for (var prop in dimensions) { - cssValue = dimensions[prop]; - - if (!options.cssOnly) { - this._setBackstoreDimension(prop, dimensions[prop]); - cssValue += 'px'; - this.hasLostContext = true; - } + /** + * Centers object horizontally in the viewport, object.top is unchanged + * @param {FabricObject} object Object to center vertically and horizontally + * @return {fabric.Canvas} thisArg + * @chainable + */ + viewportCenterObjectH(object: FabricObject) { + return this._centerObject( + object, + new Point(this.getVpCenter().x, object.getCenterPoint().y) + ); + } - if (!options.backstoreOnly) { - this._setCssDimension(prop, cssValue); - } - } - if (this._isCurrentlyDrawing) { - this.freeDrawingBrush && - this.freeDrawingBrush._setBrushStyles(this.contextTop); - } - this._initRetinaScaling(); - this.calcOffset(); + /** + * Centers object Vertically in the viewport, object.top is unchanged + * @param {FabricObject} object Object to center vertically and horizontally + * @return {fabric.Canvas} thisArg + * @chainable + */ + viewportCenterObjectV(object: FabricObject) { + return this._centerObject( + object, + new Point(object.getCenterPoint().x, this.getVpCenter().y) + ); + } - if (!options.cssOnly) { - this.requestRenderAll(); - } + /** + * Calculate the point in canvas that correspond to the center of actual viewport. + * @return {Point} vpCenter, viewport center + * @chainable + */ + getVpCenter(): Point { + return transformPoint( + this.getCenterPoint(), + invertTransform(this.viewportTransform) + ); + } - return this; - }, - - /** - * Helper for setting width/height - * @private - * @param {String} prop property (width|height) - * @param {Number} value value to set property to - * @return {fabric.Canvas} instance - * @chainable true - */ - _setBackstoreDimension: function (prop, value) { - this.lowerCanvasEl[prop] = value; - - if (this.upperCanvasEl) { - this.upperCanvasEl[prop] = value; - } + /** + * @private + * @param {FabricObject} object Object to center + * @param {Point} center Center point + * @return {fabric.Canvas} thisArg + * @chainable + */ + _centerObject(object: FabricObject, center: Point) { + object.setXY(center, 'center', 'center'); + object.setCoords(); + this.renderOnAddRemove && this.requestRenderAll(); + return this; + } - if (this.cacheCanvasEl) { - this.cacheCanvasEl[prop] = value; - } + /** + * Returns dataless JSON representation of canvas + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {String} json string + */ + toDatalessJSON(propertiesToInclude?: string[]) { + return this.toDatalessObject(propertiesToInclude); + } - this[prop] = value; + /** + * Returns object representation of canvas + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toObject(propertiesToInclude?: string[]) { + return this._toObjectMethod('toObject', propertiesToInclude); + } - return this; - }, + /** + * Returns Object representation of canvas + * this alias is provided because if you call JSON.stringify on an instance, + * the toJSON object will be invoked if it exists. + * Having a toJSON method means you can do JSON.stringify(myCanvas) + * @return {Object} JSON compatible object + * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#serialization} + * @see {@link http://jsfiddle.net/fabricjs/pec86/|jsFiddle demo} + * @example JSON without additional properties + * var json = canvas.toJSON(); + * @example JSON with additional properties included + * var json = canvas.toJSON(['lockMovementX', 'lockMovementY', 'lockRotation', 'lockScalingX', 'lockScalingY']); + * @example JSON without default values + * var json = canvas.toJSON(); + */ + toJSON() { + return this.toObject(); + } - /** - * Helper for setting css width/height - * @private - * @param {String} prop property (width|height) - * @param {String} value value to set property to - * @return {fabric.Canvas} instance - * @chainable true - */ - _setCssDimension: function (prop, value) { - this.lowerCanvasEl.style[prop] = value; + /** + * Returns dataless object representation of canvas + * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output + * @return {Object} object representation of an instance + */ + toDatalessObject(propertiesToInclude?: string[]) { + return this._toObjectMethod('toDatalessObject', propertiesToInclude); + } - if (this.upperCanvasEl) { - this.upperCanvasEl.style[prop] = value; - } + /** + * @private + */ + _toObjectMethod( + methodName: TValidToObjectMethod, + propertiesToInclude?: string[] + ) { + const clipPath = this.clipPath; + const clipPathData = + clipPath && !clipPath.excludeFromExport + ? this._toObject(clipPath, methodName, propertiesToInclude) + : null; + return { + version: VERSION, + ...pick(this, propertiesToInclude), + objects: this._objects + .filter((object) => !object.excludeFromExport) + .map((instance) => + this._toObject(instance, methodName, propertiesToInclude) + ), + ...this.__serializeBgOverlay(methodName, propertiesToInclude), + ...(clipPathData ? { clipPath: clipPathData } : null), + }; + } - if (this.wrapperEl) { - this.wrapperEl.style[prop] = value; - } + /** + * @private + */ + _toObject( + instance: FabricObject, + methodName: TValidToObjectMethod, + propertiesToInclude?: string[] + ) { + let originalValue; + + if (!this.includeDefaultValues) { + originalValue = instance.includeDefaultValues; + instance.includeDefaultValues = false; + } - return this; - }, - - /** - * Returns canvas zoom level - * @return {Number} - */ - getZoom: function () { - return this.viewportTransform[0]; - }, - - /** - * Sets viewport transformation of this canvas instance - * @param {Array} vpt a Canvas 2D API transform matrix - * @return {fabric.Canvas} instance - * @chainable true - */ - setViewportTransform: function (vpt) { - var activeObject = this._activeObject, - backgroundObject = this.backgroundImage, - overlayObject = this.overlayImage, - object, - i, - len; - this.viewportTransform = vpt; - for (i = 0, len = this._objects.length; i < len; i++) { - object = this._objects[i]; - object.group || object.setCoords(); - } - if (activeObject) { - activeObject.setCoords(); - } - if (backgroundObject) { - backgroundObject.setCoords(); - } - if (overlayObject) { - overlayObject.setCoords(); - } - this.calcViewportBoundaries(); - this.renderOnAddRemove && this.requestRenderAll(); - return this; - }, - - /** - * Sets zoom level of this canvas instance, the zoom centered around point - * meaning that following zoom to point with the same point will have the visual - * effect of the zoom originating from that point. The point won't move. - * It has nothing to do with canvas center or visual center of the viewport. - * @param {Point} point to zoom with respect to - * @param {Number} value to set zoom to, less than 1 zooms out - * @return {fabric.Canvas} instance - * @chainable true - */ - zoomToPoint: function (point, value) { - // TODO: just change the scale, preserve other transformations - var before = point, - vpt = this.viewportTransform.slice(0); - point = transformPoint(point, invertTransform(this.viewportTransform)); - vpt[0] = value; - vpt[3] = value; - var after = transformPoint(point, vpt); - vpt[4] += before.x - after.x; - vpt[5] += before.y - after.y; - return this.setViewportTransform(vpt); - }, - - /** - * Sets zoom level of this canvas instance - * @param {Number} value to set zoom to, less than 1 zooms out - * @return {fabric.Canvas} instance - * @chainable true - */ - setZoom: function (value) { - this.zoomToPoint(new Point(0, 0), value); - return this; - }, - - /** - * Pan viewport so as to place point at top left corner of canvas - * @param {Point} point to move to - * @return {fabric.Canvas} instance - * @chainable true - */ - absolutePan: function (point) { - var vpt = this.viewportTransform.slice(0); - vpt[4] = -point.x; - vpt[5] = -point.y; - return this.setViewportTransform(vpt); - }, - - /** - * Pans viewpoint relatively - * @param {Point} point (position vector) to move by - * @return {fabric.Canvas} instance - * @chainable true - */ - relativePan: function (point) { - return this.absolutePan( - new Point( - -point.x - this.viewportTransform[4], - -point.y - this.viewportTransform[5] - ) - ); - }, - - /** - * Returns <canvas> element corresponding to this instance - * @return {HTMLCanvasElement} - */ - getElement: function () { - return this.lowerCanvasEl; - }, - - /** - * Clears specified context of canvas element - * @param {CanvasRenderingContext2D} ctx Context to clear - * @return {fabric.Canvas} thisArg - * @chainable - */ - clearContext: function (ctx) { - ctx.clearRect(0, 0, this.width, this.height); - return this; - }, - - /** - * Returns context of canvas where objects are drawn - * @return {CanvasRenderingContext2D} - */ - getContext: function () { - return this.contextContainer; - }, - - /** - * Clears all contexts (background, main, top) of an instance - * @return {fabric.Canvas} thisArg - * @chainable - */ - clear: function () { - this.remove.apply(this, this.getObjects()); - this.backgroundImage = null; - this.overlayImage = null; - this.backgroundColor = ''; - this.overlayColor = ''; - if (this._hasITextHandlers) { - this.off('mouse:up', this._mouseUpITextHandler); - this._iTextInstances = null; - this._hasITextHandlers = false; - } - this.clearContext(this.contextContainer); - this.fire('canvas:cleared'); - this.renderOnAddRemove && this.requestRenderAll(); - return this; - }, - - /** - * Renders the canvas - * @return {fabric.Canvas} instance - * @chainable - */ - renderAll: function () { - this.cancelRequestedRender(); - if (this.destroyed) { - return; - } - this.renderCanvas(this.contextContainer, this._objects); - return this; - }, - - /** - * Function created to be instance bound at initialization - * used in requestAnimationFrame rendering - * Let the fabricJS call it. If you call it manually you could have more - * animationFrame stacking on to of each other - * for an imperative rendering, use canvas.renderAll - * @private - * @return {fabric.Canvas} instance - * @chainable - */ - renderAndReset: function () { - this.nextRenderHandle = 0; - this.renderAll(); - }, - - /** - * Append a renderAll request to next animation frame. - * unless one is already in progress, in that case nothing is done - * a boolean flag will avoid appending more. - * @return {fabric.Canvas} instance - * @chainable - */ - requestRenderAll: function () { - if (!this.nextRenderHandle && !this.disposed && !this.destroyed) { - this.nextRenderHandle = requestAnimFrame(this.renderAndResetBound); - } - return this; - }, - - /** - * Calculate the position of the 4 corner of canvas with current viewportTransform. - * helps to determinate when an object is in the current rendering viewport using - * object absolute coordinates ( aCoords ) - * @return {Object} points.tl - * @chainable - */ - calcViewportBoundaries: function () { - var width = this.width, - height = this.height, - iVpt = invertTransform(this.viewportTransform), - a = transformPoint({ x: 0, y: 0 }, iVpt), - b = transformPoint({ x: width, y: height }, iVpt), - // we don't support vpt flipping - // but the code is robust enough to mostly work with flipping - min = a.min(b), - max = a.max(b); - return (this.vptCoords = { - tl: min, - tr: new Point(max.x, min.y), - bl: new Point(min.x, max.y), - br: max, - }); - }, + const object = instance[methodName](propertiesToInclude); + if (!this.includeDefaultValues) { + instance.includeDefaultValues = !!originalValue; + } + return object; + } - cancelRequestedRender: function () { - if (this.nextRenderHandle) { - fabric.util.cancelAnimFrame(this.nextRenderHandle); - this.nextRenderHandle = 0; - } - }, - - /** - * Renders background, objects, overlay and controls. - * @param {CanvasRenderingContext2D} ctx - * @param {Array} objects to render - * @return {fabric.Canvas} instance - * @chainable - */ - renderCanvas: function (ctx, objects) { - if (this.destroyed) { - return; - } + /** + * @private + */ + __serializeBgOverlay( + methodName: TValidToObjectMethod, + propertiesToInclude?: string[] + ) { + const data: any = {}, + bgImage = this.backgroundImage, + overlayImage = this.overlayImage, + bgColor = this.backgroundColor, + overlayColor = this.overlayColor; + + if (isFiller(bgColor)) { + if (!bgColor.excludeFromExport) { + data.background = bgColor.toObject(propertiesToInclude); + } + } else if (bgColor) { + data.background = bgColor; + } - var v = this.viewportTransform, - path = this.clipPath; - this.calcViewportBoundaries(); - this.clearContext(ctx); - ctx.imageSmoothingEnabled = this.imageSmoothingEnabled; - // node-canvas - ctx.patternQuality = 'best'; - this.fire('before:render', { ctx: ctx }); - this._renderBackground(ctx); - - ctx.save(); - //apply viewport transform once for all rendering process - ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); - this._renderObjects(ctx, objects); - ctx.restore(); - if (!this.controlsAboveOverlay && this.interactive) { - this.drawControls(ctx); - } - if (path) { - path._set('canvas', this); - // needed to setup a couple of variables - path.shouldCache(); - path._transformDone = true; - path.renderCache({ forClipping: true }); - this.drawClipPathOnCanvas(ctx); - } - this._renderOverlay(ctx); - if (this.controlsAboveOverlay && this.interactive) { - this.drawControls(ctx); - } - this.fire('after:render', { ctx: ctx }); + if (isFiller(overlayColor)) { + if (!overlayColor.excludeFromExport) { + data.overlay = overlayColor.toObject(propertiesToInclude); + } + } else if (overlayColor) { + data.overlay = overlayColor; + } - if (this.__cleanupTask) { - this.__cleanupTask(); - this.__cleanupTask = undefined; - } - }, - - /** - * Paint the cached clipPath on the lowerCanvasEl - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - drawClipPathOnCanvas: function (ctx) { - var v = this.viewportTransform, - path = this.clipPath; - ctx.save(); - ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); - // DEBUG: uncomment this line, comment the following - // ctx.globalAlpha = 0.4; - ctx.globalCompositeOperation = 'destination-in'; - path.transform(ctx); - ctx.scale(1 / path.zoomX, 1 / path.zoomY); - ctx.drawImage( - path._cacheCanvas, - -path.cacheTranslationX, - -path.cacheTranslationY - ); - ctx.restore(); - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Array} objects to render - */ - _renderObjects: function (ctx, objects) { - var i, len; - for (i = 0, len = objects.length; i < len; ++i) { - objects[i] && objects[i].render(ctx); - } - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {string} property 'background' or 'overlay' - */ - _renderBackgroundOrOverlay: function (ctx, property) { - var fill = this[property + 'Color'], - object = this[property + 'Image'], - v = this.viewportTransform, - needsVpt = this[property + 'Vpt']; - if (!fill && !object) { - return; - } - if (fill) { - ctx.save(); - ctx.beginPath(); - ctx.moveTo(0, 0); - ctx.lineTo(this.width, 0); - ctx.lineTo(this.width, this.height); - ctx.lineTo(0, this.height); - ctx.closePath(); - ctx.fillStyle = fill.toLive ? fill.toLive(ctx, this) : fill; - if (needsVpt) { - ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); - } - ctx.transform(1, 0, 0, 1, fill.offsetX || 0, fill.offsetY || 0); - var m = fill.gradientTransform || fill.patternTransform; - m && ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); - ctx.fill(); - ctx.restore(); - } - if (object) { - ctx.save(); - if (needsVpt) { - ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); - } - object.render(ctx); - ctx.restore(); - } - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _renderBackground: function (ctx) { - this._renderBackgroundOrOverlay(ctx, 'background'); - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _renderOverlay: function (ctx) { - this._renderBackgroundOrOverlay(ctx, 'overlay'); - }, - - /** - * Returns coordinates of a center of canvas. - * Returned value is an object with top and left properties - * @return {Object} object with "top" and "left" number values - * @deprecated migrate to `getCenterPoint` - */ - getCenter: function () { - return { - top: this.height / 2, - left: this.width / 2, - }; - }, - - /** - * Returns coordinates of a center of canvas. - * @return {Point} - */ - getCenterPoint: function () { - return new Point(this.width / 2, this.height / 2); - }, - - /** - * Centers object horizontally in the canvas - * @param {fabric.Object} object Object to center horizontally - * @return {fabric.Canvas} thisArg - */ - centerObjectH: function (object) { - return this._centerObject( - object, - new Point(this.getCenterPoint().x, object.getCenterPoint().y) - ); - }, - - /** - * Centers object vertically in the canvas - * @param {fabric.Object} object Object to center vertically - * @return {fabric.Canvas} thisArg - * @chainable - */ - centerObjectV: function (object) { - return this._centerObject( - object, - new Point(object.getCenterPoint().x, this.getCenterPoint().y) - ); - }, - - /** - * Centers object vertically and horizontally in the canvas - * @param {fabric.Object} object Object to center vertically and horizontally - * @return {fabric.Canvas} thisArg - * @chainable - */ - centerObject: function (object) { - var center = this.getCenterPoint(); - return this._centerObject(object, center); - }, - - /** - * Centers object vertically and horizontally in the viewport - * @param {fabric.Object} object Object to center vertically and horizontally - * @return {fabric.Canvas} thisArg - * @chainable - */ - viewportCenterObject: function (object) { - var vpCenter = this.getVpCenter(); - return this._centerObject(object, vpCenter); - }, - - /** - * Centers object horizontally in the viewport, object.top is unchanged - * @param {fabric.Object} object Object to center vertically and horizontally - * @return {fabric.Canvas} thisArg - * @chainable - */ - viewportCenterObjectH: function (object) { - var vpCenter = this.getVpCenter(); - this._centerObject( - object, - new Point(vpCenter.x, object.getCenterPoint().y) - ); - return this; - }, - - /** - * Centers object Vertically in the viewport, object.top is unchanged - * @param {fabric.Object} object Object to center vertically and horizontally - * @return {fabric.Canvas} thisArg - * @chainable - */ - viewportCenterObjectV: function (object) { - var vpCenter = this.getVpCenter(); - - return this._centerObject( - object, - new Point(object.getCenterPoint().x, vpCenter.y) - ); - }, - - /** - * Calculate the point in canvas that correspond to the center of actual viewport. - * @return {Point} vpCenter, viewport center - * @chainable - */ - getVpCenter: function () { - var center = this.getCenterPoint(), - iVpt = invertTransform(this.viewportTransform); - return transformPoint(center, iVpt); - }, - - /** - * @private - * @param {fabric.Object} object Object to center - * @param {Point} center Center point - * @return {fabric.Canvas} thisArg - * @chainable - */ - _centerObject: function (object, center) { - object.setXY(center, 'center', 'center'); - object.setCoords(); - this.renderOnAddRemove && this.requestRenderAll(); - return this; - }, - - /** - * Returns dataless JSON representation of canvas - * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output - * @return {String} json string - */ - toDatalessJSON: function (propertiesToInclude) { - return this.toDatalessObject(propertiesToInclude); - }, - - /** - * Returns object representation of canvas - * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output - * @return {Object} object representation of an instance - */ - toObject: function (propertiesToInclude) { - return this._toObjectMethod('toObject', propertiesToInclude); - }, - - /** - * Returns Object representation of canvas - * this alias is provided because if you call JSON.stringify on an instance, - * the toJSON object will be invoked if it exists. - * Having a toJSON method means you can do JSON.stringify(myCanvas) - * @return {Object} JSON compatible object - * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#serialization} - * @see {@link http://jsfiddle.net/fabricjs/pec86/|jsFiddle demo} - * @example JSON without additional properties - * var json = canvas.toJSON(); - * @example JSON with additional properties included - * var json = canvas.toJSON(['lockMovementX', 'lockMovementY', 'lockRotation', 'lockScalingX', 'lockScalingY']); - * @example JSON without default values - * var json = canvas.toJSON(); - */ - toJSON: function () { - return this.toObject(); - }, - - /** - * Returns dataless object representation of canvas - * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output - * @return {Object} object representation of an instance - */ - toDatalessObject: function (propertiesToInclude) { - return this._toObjectMethod('toDatalessObject', propertiesToInclude); - }, - - /** - * @private - */ - _toObjectMethod: function (methodName, propertiesToInclude) { - const clipPath = this.clipPath; - const clipPathData = - clipPath && !clipPath.excludeFromExport - ? this._toObject(clipPath, methodName, propertiesToInclude) - : null; - return { - version: VERSION, - ...pick(this, propertiesToInclude), - objects: this._objects - .filter((object) => !object.excludeFromExport) - .map((instance) => - this._toObject(instance, methodName, propertiesToInclude) - ), - ...this.__serializeBgOverlay(methodName, propertiesToInclude), - ...(clipPathData ? { clipPath: clipPathData } : null), - }; - }, - - /** - * @private - */ - _toObject: function (instance, methodName, propertiesToInclude) { - var originalValue; - - if (!this.includeDefaultValues) { - originalValue = instance.includeDefaultValues; - instance.includeDefaultValues = false; - } + if (bgImage && !bgImage.excludeFromExport) { + data.backgroundImage = this._toObject( + bgImage, + methodName, + propertiesToInclude + ); + } + if (overlayImage && !overlayImage.excludeFromExport) { + data.overlayImage = this._toObject( + overlayImage, + methodName, + propertiesToInclude + ); + } - var object = instance[methodName](propertiesToInclude); - if (!this.includeDefaultValues) { - instance.includeDefaultValues = originalValue; - } - return object; - }, - - /** - * @private - */ - __serializeBgOverlay: function (methodName, propertiesToInclude) { - var data = {}, - bgImage = this.backgroundImage, - overlayImage = this.overlayImage, - bgColor = this.backgroundColor, - overlayColor = this.overlayColor; - - if (bgColor && bgColor.toObject) { - if (!bgColor.excludeFromExport) { - data.background = bgColor.toObject(propertiesToInclude); - } - } else if (bgColor) { - data.background = bgColor; - } + return data; + } - if (overlayColor && overlayColor.toObject) { - if (!overlayColor.excludeFromExport) { - data.overlay = overlayColor.toObject(propertiesToInclude); - } - } else if (overlayColor) { - data.overlay = overlayColor; - } + /* _TO_SVG_START_ */ + /** + * When true, getSvgTransform() will apply the StaticCanvas.viewportTransform to the SVG transformation. When true, + * a zoomed canvas will then produce zoomed SVG output. + * @type Boolean + * @default + */ + svgViewportTransformation: boolean; - if (bgImage && !bgImage.excludeFromExport) { - data.backgroundImage = this._toObject( - bgImage, - methodName, - propertiesToInclude - ); - } - if (overlayImage && !overlayImage.excludeFromExport) { - data.overlayImage = this._toObject( - overlayImage, - methodName, - propertiesToInclude - ); - } + /** + * Returns SVG representation of canvas + * @function + * @param {Object} [options] Options object for SVG output + * @param {Boolean} [options.suppressPreamble=false] If true xml tag is not included + * @param {Object} [options.viewBox] SVG viewbox object + * @param {Number} [options.viewBox.x] x-coordinate of viewbox + * @param {Number} [options.viewBox.y] y-coordinate of viewbox + * @param {Number} [options.viewBox.width] Width of viewbox + * @param {Number} [options.viewBox.height] Height of viewbox + * @param {String} [options.encoding=UTF-8] Encoding of SVG output + * @param {String} [options.width] desired width of svg with or without units + * @param {String} [options.height] desired height of svg with or without units + * @param {Function} [reviver] Method for further parsing of svg elements, called after each fabric object converted into svg representation. + * @return {String} SVG string + * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#serialization} + * @see {@link http://jsfiddle.net/fabricjs/jQ3ZZ/|jsFiddle demo} + * @example Normal SVG output + * var svg = canvas.toSVG(); + * @example SVG output without preamble (without <?xml ../>) + * var svg = canvas.toSVG({suppressPreamble: true}); + * @example SVG output with viewBox attribute + * var svg = canvas.toSVG({ + * viewBox: { + * x: 100, + * y: 100, + * width: 200, + * height: 300 + * } + * }); + * @example SVG output with different encoding (default: UTF-8) + * var svg = canvas.toSVG({encoding: 'ISO-8859-1'}); + * @example Modify SVG output with reviver function + * var svg = canvas.toSVG(null, function(svg) { + * return svg.replace('stroke-dasharray: ; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; ', ''); + * }); + */ + toSVG(options: TSVGExportOptions = {}, reviver: TSVGReviver) { + options.reviver = reviver; + const markup: string[] = []; + + this._setSVGPreamble(markup, options); + this._setSVGHeader(markup, options); + if (this.clipPath) { + markup.push(`\n`); + } + this._setSVGBgOverlayColor(markup, 'background'); + this._setSVGBgOverlayImage(markup, 'backgroundImage', reviver); + this._setSVGObjects(markup, reviver); + if (this.clipPath) { + markup.push('\n'); + } + this._setSVGBgOverlayColor(markup, 'overlay'); + this._setSVGBgOverlayImage(markup, 'overlayImage', reviver); - return data; - }, - - /* _TO_SVG_START_ */ - /** - * When true, getSvgTransform() will apply the StaticCanvas.viewportTransform to the SVG transformation. When true, - * a zoomed canvas will then produce zoomed SVG output. - * @type Boolean - * @default - */ - svgViewportTransformation: true, - - /** - * Returns SVG representation of canvas - * @function - * @param {Object} [options] Options object for SVG output - * @param {Boolean} [options.suppressPreamble=false] If true xml tag is not included - * @param {Object} [options.viewBox] SVG viewbox object - * @param {Number} [options.viewBox.x] x-coordinate of viewbox - * @param {Number} [options.viewBox.y] y-coordinate of viewbox - * @param {Number} [options.viewBox.width] Width of viewbox - * @param {Number} [options.viewBox.height] Height of viewbox - * @param {String} [options.encoding=UTF-8] Encoding of SVG output - * @param {String} [options.width] desired width of svg with or without units - * @param {String} [options.height] desired height of svg with or without units - * @param {Function} [reviver] Method for further parsing of svg elements, called after each fabric object converted into svg representation. - * @return {String} SVG string - * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#serialization} - * @see {@link http://jsfiddle.net/fabricjs/jQ3ZZ/|jsFiddle demo} - * @example Normal SVG output - * var svg = canvas.toSVG(); - * @example SVG output without preamble (without <?xml ../>) - * var svg = canvas.toSVG({suppressPreamble: true}); - * @example SVG output with viewBox attribute - * var svg = canvas.toSVG({ - * viewBox: { - * x: 100, - * y: 100, - * width: 200, - * height: 300 - * } - * }); - * @example SVG output with different encoding (default: UTF-8) - * var svg = canvas.toSVG({encoding: 'ISO-8859-1'}); - * @example Modify SVG output with reviver function - * var svg = canvas.toSVG(null, function(svg) { - * return svg.replace('stroke-dasharray: ; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; ', ''); - * }); - */ - toSVG: function (options, reviver) { - options || (options = {}); - options.reviver = reviver; - var markup = []; - - this._setSVGPreamble(markup, options); - this._setSVGHeader(markup, options); - if (this.clipPath) { - markup.push( - '\n' - ); - } - this._setSVGBgOverlayColor(markup, 'background'); - this._setSVGBgOverlayImage(markup, 'backgroundImage', reviver); - this._setSVGObjects(markup, reviver); - if (this.clipPath) { - markup.push('\n'); - } - this._setSVGBgOverlayColor(markup, 'overlay'); - this._setSVGBgOverlayImage(markup, 'overlayImage', reviver); + markup.push(''); - markup.push(''); + return markup.join(''); + } - return markup.join(''); - }, + /** + * @private + */ + _setSVGPreamble(markup: string[], options: TSVGExportOptions): void { + if (options.suppressPreamble) { + return; + } + markup.push( + '\n', + '\n' + ); + } - /** - * @private - */ - _setSVGPreamble: function (markup, options) { - if (options.suppressPreamble) { - return; - } - markup.push( - '\n', - '\n' - ); - }, - - /** - * @private - */ - _setSVGHeader: function (markup, options) { - var width = options.width || this.width, - height = options.height || this.height, - vpt, - viewBox = 'viewBox="0 0 ' + this.width + ' ' + this.height + '" ', - NUM_FRACTION_DIGITS = config.NUM_FRACTION_DIGITS; - - if (options.viewBox) { - viewBox = - 'viewBox="' + - options.viewBox.x + - ' ' + - options.viewBox.y + - ' ' + - options.viewBox.width + - ' ' + - options.viewBox.height + - '" '; - } else { - if (this.svgViewportTransformation) { - vpt = this.viewportTransform; - viewBox = - 'viewBox="' + - toFixed(-vpt[4] / vpt[0], NUM_FRACTION_DIGITS) + - ' ' + - toFixed(-vpt[5] / vpt[3], NUM_FRACTION_DIGITS) + - ' ' + - toFixed(this.width / vpt[0], NUM_FRACTION_DIGITS) + - ' ' + - toFixed(this.height / vpt[3], NUM_FRACTION_DIGITS) + - '" '; - } - } + /** + * @private + */ + _setSVGHeader(markup: string[], options: TSVGExportOptions): void { + const width = options.width || `${this.width}`, + height = options.height || `${this.height}`, + NUM_FRACTION_DIGITS = config.NUM_FRACTION_DIGITS, + optViewBox = options.viewBox; + let viewBox: string; + if (optViewBox) { + viewBox = `viewBox="${optViewBox.x} ${optViewBox.y} ${optViewBox.width} ${optViewBox.height}" `; + } else if (this.svgViewportTransformation) { + const vpt = this.viewportTransform; + viewBox = `viewBox="${toFixed( + -vpt[4] / vpt[0], + NUM_FRACTION_DIGITS + )} ${toFixed(-vpt[5] / vpt[3], NUM_FRACTION_DIGITS)} ${toFixed( + this.width / vpt[0], + NUM_FRACTION_DIGITS + )} ${toFixed(this.height / vpt[3], NUM_FRACTION_DIGITS)}" `; + } else { + viewBox = `viewBox="0 0 ${this.width} ${this.height}" `; + } - markup.push( - '\n', - 'Created with Fabric.js ', - VERSION, - '\n', - '\n', - this.createSVGFontFacesMarkup(), - this.createSVGRefElementsMarkup(), - this.createSVGClipPathMarkup(options), - '\n' - ); - }, - - createSVGClipPathMarkup: function (options) { - var clipPath = this.clipPath; - if (clipPath) { - clipPath.clipPathId = 'CLIPPATH_' + uid(); - return ( - '\n' + - this.clipPath.toClipPathSVG(options.reviver) + - '\n' - ); - } - return ''; - }, - - /** - * Creates markup containing SVG referenced elements like patterns, gradients etc. - * @return {String} - */ - createSVGRefElementsMarkup: function () { - var _this = this, - markup = ['background', 'overlay'].map(function (prop) { - var fill = _this[prop + 'Color']; - if (fill && fill.toLive) { - var shouldTransform = _this[prop + 'Vpt'], - vpt = _this.viewportTransform, - object = { - width: _this.width / (shouldTransform ? vpt[0] : 1), - height: _this.height / (shouldTransform ? vpt[3] : 1), - }; - return fill.toSVG(object, { - additionalTransform: shouldTransform - ? fabric.util.matrixToSVG(vpt) - : '', - }); - } + markup.push( + '\n', + 'Created with Fabric.js ', + VERSION, + '\n', + '\n', + this.createSVGFontFacesMarkup(), + this.createSVGRefElementsMarkup(), + this.createSVGClipPathMarkup(options), + '\n' + ); + } + + createSVGClipPathMarkup(options: TSVGExportOptions): string { + const clipPath = this.clipPath; + if (clipPath) { + clipPath.clipPathId = `CLIPPATH_${uid()}`; + return ( + '\n' + + this.clipPath.toClipPathSVG(options.reviver) + + '\n' + ); + } + return ''; + } + + /** + * Creates markup containing SVG referenced elements like patterns, gradients etc. + * @return {String} + */ + createSVGRefElementsMarkup(): string { + return ['background', 'overlay'] + .map((prop) => { + const fill = this[`${prop}Color`]; + if (isFiller(fill)) { + const shouldTransform = this[`${prop}Vpt`], + vpt = this.viewportTransform, + object = { + width: this.width / (shouldTransform ? vpt[0] : 1), + height: this.height / (shouldTransform ? vpt[3] : 1), + }; + return fill.toSVG(object as Rect, { + additionalTransform: shouldTransform ? matrixToSVG(vpt) : '', }); - return markup.join(''); - }, - - /** - * Creates markup containing SVG font faces, - * font URLs for font faces must be collected by developers - * and are not extracted from the DOM by fabricjs - * @param {Array} objects Array of fabric objects - * @return {String} - */ - createSVGFontFacesMarkup: function () { - var markup = '', - fontList = {}, - obj, - fontFamily, - style, - row, - rowIndex, - _char, - charIndex, - i, - len, - fontPaths = config.fontPaths, - objects = []; - - this._objects.forEach(function add(object) { - objects.push(object); - if (object._objects) { - object._objects.forEach(add); + } + }) + .join(''); + } + + /** + * Creates markup containing SVG font faces, + * font URLs for font faces must be collected by developers + * and are not extracted from the DOM by fabricjs + * @param {Array} objects Array of fabric objects + * @return {String} + */ + createSVGFontFacesMarkup(): string { + const objects: FabricObject[] = [], + fontList: Record = {}, + fontPaths = config.fontPaths; + + this._objects.forEach(function add(object) { + objects.push(object); + if (isCollection(object)) { + object._objects.forEach(add); + } + }); + + objects.forEach((obj) => { + if (!isTextObject(obj)) { + return; + } + let fontFamily = obj.fontFamily; + if (fontList[fontFamily] || !fontPaths[fontFamily]) { + return; + } + fontList[fontFamily] = true; + if (!obj.styles) { + return; + } + Object.values(obj.styles).forEach((styleRow) => { + Object.values(styleRow).forEach((textCharStyle) => { + fontFamily = textCharStyle.fontFamily; + if (!fontList[fontFamily] && fontPaths[fontFamily]) { + fontList[fontFamily] = true; } }); + }); + }); + + const fontListMarkup = Object.keys(fontList) + .map( + (fontFamily) => + `\t\t@font-face {\n\t\t\tfont-family: '${fontFamily}';\n\t\t\tsrc: url('${fontPaths[fontFamily]}');\n\t\t}\n` + ) + .join(''); + + if (fontListMarkup) { + return `\t\n`; + } + return ''; + } - for (i = 0, len = objects.length; i < len; i++) { - obj = objects[i]; - fontFamily = obj.fontFamily; - if ( - obj.type.indexOf('text') === -1 || - fontList[fontFamily] || - !fontPaths[fontFamily] - ) { - continue; - } - fontList[fontFamily] = true; - if (!obj.styles) { - continue; - } - style = obj.styles; - for (rowIndex in style) { - row = style[rowIndex]; - for (charIndex in row) { - _char = row[charIndex]; - fontFamily = _char.fontFamily; - if (!fontList[fontFamily] && fontPaths[fontFamily]) { - fontList[fontFamily] = true; - } - } - } - } + /** + * @private + */ + _setSVGObjects(markup: string[], reviver: TSVGReviver) { + this.forEachObject((fabricObject) => { + if (fabricObject.excludeFromExport) { + return; + } + this._setSVGObject(markup, fabricObject, reviver); + }); + } - for (var j in fontList) { - markup += [ - '\t\t@font-face {\n', - "\t\t\tfont-family: '", - j, - "';\n", - "\t\t\tsrc: url('", - fontPaths[j], - "');\n", - '\t\t}\n', - ].join(''); - } + /** + * This is its own function because the Canvas ( non static ) requires extra code here + * @private + */ + _setSVGObject( + markup: string[], + instance: FabricObject, + reviver: TSVGReviver + ) { + markup.push(instance.toSVG(reviver)); + } - if (markup) { - markup = [ - '\t\n', - ].join(''); - } + /** + * @private + */ + _setSVGBgOverlayImage( + markup: string[], + property: 'overlayImage' | 'backgroundImage', + reviver: TSVGReviver + ) { + const bgOrOverlay = this[property]; + if (bgOrOverlay && !bgOrOverlay.excludeFromExport && bgOrOverlay.toSVG) { + markup.push(bgOrOverlay.toSVG(reviver)); + } + } - return markup; - }, - - /** - * @private - */ - _setSVGObjects: function (markup, reviver) { - var instance, - i, - len, - objects = this._objects; - for (i = 0, len = objects.length; i < len; i++) { - instance = objects[i]; - if (instance.excludeFromExport) { - continue; - } - this._setSVGObject(markup, instance, reviver); - } - }, - - /** - * @private - */ - _setSVGObject: function (markup, instance, reviver) { - markup.push(instance.toSVG(reviver)); - }, - - /** - * @private - */ - _setSVGBgOverlayImage: function (markup, property, reviver) { - if ( - this[property] && - !this[property].excludeFromExport && - this[property].toSVG - ) { - markup.push(this[property].toSVG(reviver)); - } - }, - - /** - * @private - */ - _setSVGBgOverlayColor: function (markup, property) { - var filler = this[property + 'Color'], - vpt = this.viewportTransform, - finalWidth = this.width, - finalHeight = this.height; - if (!filler) { - return; - } - if (filler.toLive) { - var repeat = filler.repeat, - iVpt = fabric.util.invertTransform(vpt), - shouldInvert = this[property + 'Vpt'], - additionalTransform = shouldInvert - ? fabric.util.matrixToSVG(iVpt) - : ''; - markup.push( - '\n' - ); - } else { - markup.push( - '\n' - ); - } - }, - /* _TO_SVG_END_ */ - - /** - * 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 - * @return {fabric.Canvas} thisArg - * @chainable - */ - sendToBack: function (object) { - if (!object) { - return this; - } - var activeSelection = this._activeObject, - i, - obj, - objs; - if (object === activeSelection && object.type === 'activeSelection') { - objs = activeSelection._objects; - for (i = objs.length; i--; ) { - obj = objs[i]; - removeFromArray(this._objects, obj); - this._objects.unshift(obj); - } - } else { - removeFromArray(this._objects, object); - this._objects.unshift(object); - } - this.renderOnAddRemove && this.requestRenderAll(); - return this; - }, - - /** - * 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 - * @return {fabric.Canvas} thisArg - * @chainable - */ - bringToFront: function (object) { - if (!object) { - return this; - } - var activeSelection = this._activeObject, - i, - obj, - objs; - if (object === activeSelection && object.type === 'activeSelection') { - objs = activeSelection._objects; - for (i = 0; i < objs.length; i++) { - obj = objs[i]; - removeFromArray(this._objects, obj); - this._objects.push(obj); - } - } else { - removeFromArray(this._objects, object); - this._objects.push(object); - } - this.renderOnAddRemove && this.requestRenderAll(); - return this; - }, - - /** - * 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 - * @return {fabric.Canvas} thisArg - * @chainable - */ - sendBackwards: function (object, intersecting) { - if (!object) { - return this; - } - var activeSelection = this._activeObject, - i, - obj, - idx, - newIdx, - objs, - objsMoved = 0; - - if (object === activeSelection && object.type === 'activeSelection') { - objs = activeSelection._objects; - for (i = 0; i < objs.length; i++) { - obj = objs[i]; - idx = this._objects.indexOf(obj); - if (idx > 0 + objsMoved) { - newIdx = idx - 1; - removeFromArray(this._objects, obj); - this._objects.splice(newIdx, 0, obj); - } - objsMoved++; - } - } else { - idx = this._objects.indexOf(object); - if (idx !== 0) { - // if object is not on the bottom of stack - newIdx = this._findNewLowerIndex(object, idx, intersecting); - removeFromArray(this._objects, object); - this._objects.splice(newIdx, 0, object); - } - } - this.renderOnAddRemove && this.requestRenderAll(); - return this; - }, - - /** - * @private - */ - _findNewLowerIndex: function (object, idx, intersecting) { - var newIdx, i; - - if (intersecting) { - newIdx = idx; - - // traverse down the stack looking for the nearest intersecting object - for (i = idx - 1; i >= 0; --i) { - var isIntersecting = - object.intersectsWithObject(this._objects[i]) || - object.isContainedWithinObject(this._objects[i]) || - this._objects[i].isContainedWithinObject(object); - - if (isIntersecting) { - newIdx = i; - break; - } - } - } else { - newIdx = idx - 1; - } + /** + * @TODO this seems to handle patterns but fail at gradients. + * @private + */ + _setSVGBgOverlayColor(markup: string[], property: 'background' | 'overlay') { + const filler = this[`${property}Color`]; + if (!filler) { + return; + } + if (isFiller(filler)) { + // @ts-ignore TS is so stubbordn that i can't even check if a property exists. + const repeat = filler.repeat || '', + finalWidth = this.width, + finalHeight = this.height, + shouldInvert = this[`${property}Vpt`], + additionalTransform = shouldInvert + ? matrixToSVG(invertTransform(this.viewportTransform)) + : ''; + markup.push( + `\n` + ); + } else { + markup.push( + '\n' + ); + } + } + /* _TO_SVG_END_ */ - return newIdx; - }, - - /** - * 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 - * @return {fabric.Canvas} thisArg - * @chainable - */ - bringForward: function (object, intersecting) { - if (!object) { - return this; - } - var activeSelection = this._activeObject, - i, - obj, - idx, - newIdx, - objs, - objsMoved = 0; - - if (object === activeSelection && object.type === 'activeSelection') { - objs = activeSelection._objects; - for (i = objs.length; i--; ) { - obj = objs[i]; - idx = this._objects.indexOf(obj); - if (idx < this._objects.length - 1 - objsMoved) { - newIdx = idx + 1; - removeFromArray(this._objects, obj); - this._objects.splice(newIdx, 0, obj); - } - objsMoved++; - } - } else { - idx = this._objects.indexOf(object); - if (idx !== this._objects.length - 1) { - // if object is not on top of stack (last item in an array) - newIdx = this._findNewUpperIndex(object, idx, intersecting); - removeFromArray(this._objects, object); - this._objects.splice(newIdx, 0, object); - } - } - this.renderOnAddRemove && this.requestRenderAll(); - return this; - }, - - /** - * @private - */ - _findNewUpperIndex: function (object, idx, intersecting) { - var newIdx, i, len; - - if (intersecting) { - newIdx = idx; - - // traverse up the stack looking for the nearest intersecting object - for (i = idx + 1, len = this._objects.length; i < len; ++i) { - var isIntersecting = - object.intersectsWithObject(this._objects[i]) || - object.isContainedWithinObject(this._objects[i]) || - this._objects[i].isContainedWithinObject(object); - - if (isIntersecting) { - newIdx = i; - break; - } - } - } else { - newIdx = idx + 1; - } + /** + * Moves an object or the objects of a multiple selection + * to the bottom of the stack of drawn objects + * @param {FabricObject} object Object to send to back + * @return {fabric.Canvas} thisArg + * @chainable + */ + sendToBack(object: FabricObject) { + const activeSelection = this._activeObject; + // @TODO: this part should be in canvas. StaticCanvas can't handle active selections + if (object === activeSelection && isActiveSelection(object)) { + const objs = activeSelection._objects; + for (let i = objs.length; i--; ) { + const obj = objs[i]; + removeFromArray(this._objects, obj); + this._objects.unshift(obj); + } + } else { + removeFromArray(this._objects, object); + this._objects.unshift(object); + } + this.renderOnAddRemove && this.requestRenderAll(); + return this; + } - return newIdx; - }, - - /** - * 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 - * @return {fabric.Canvas} thisArg - * @chainable - */ - moveTo: function (object, index) { + /** + * Moves an object or the objects of a multiple selection + * to the top of the stack of drawn objects + * @param {FabricObject} object Object to send + * @return {fabric.Canvas} thisArg + * @chainable + */ + bringToFront(object: FabricObject) { + const activeSelection = this._activeObject; + // @TODO: this part should be in canvas. StaticCanvas can't handle active selections + if (object === activeSelection && isActiveSelection(object)) { + const objs = activeSelection._objects; + for (let i = 0; i < objs.length; i++) { + const obj = objs[i]; + removeFromArray(this._objects, obj); + this._objects.push(obj); + } + } else { + removeFromArray(this._objects, object); + this._objects.push(object); + } + this.renderOnAddRemove && this.requestRenderAll(); + return this; + } + + /** + * 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 {FabricObject} object Object to send + * @param {boolean} [intersecting] If `true`, send object behind next lower intersecting object + * @return {fabric.Canvas} thisArg + * @chainable + */ + sendBackwards(object: FabricObject, intersecting: boolean) { + const activeSelection = this._activeObject; + if (object === activeSelection && isActiveSelection(object)) { + let objsMoved = 0; + const objs = activeSelection._objects; + for (let i = 0; i < objs.length; i++) { + const obj = objs[i]; + const idx = this._objects.indexOf(obj); + if (idx > 0 + objsMoved) { + removeFromArray(this._objects, obj); + this._objects.splice(idx - 1, 0, obj); + } + objsMoved++; + } + } else { + const idx: number = 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(index, 0, object); - return this.renderOnAddRemove && this.requestRenderAll(); - }, - - /** - * Waits until rendering has settled to destroy the canvas - * @returns {Promise} a promise resolving to `true` once the canvas has been destroyed or to `false` if the canvas has was already destroyed - * @throws if aborted by a consequent call - */ - dispose: function () { - this.disposed = true; - return new Promise((resolve, reject) => { - const task = () => { - this.destroy(); - resolve(true); - }; - task.kill = reject; - if (this.__cleanupTask) { - this.__cleanupTask.kill('aborted'); - } + this._objects.splice(newIdx, 0, object); + } + } + this.renderOnAddRemove && this.requestRenderAll(); + return this; + } - if (this.destroyed) { - resolve(false); - } else if (this.nextRenderHandle) { - this.__cleanupTask = task; - } else { - task(); - } - }); - }, - - /** - * Clears the canvas element, disposes objects and frees resources - * - * **CAUTION**: - * - * This method is **UNSAFE**. - * You may encounter a race condition using it if there's a requested render. - * Call this method only if you are sure rendering has settled. - * Consider using {@link dispose} as it is **SAFE** - * - * @private - */ - destroy: function () { - this.destroyed = true; - this.cancelRequestedRender(); - this.forEachObject((object) => object.dispose()); - this._objects = []; - if (this.backgroundImage && this.backgroundImage.dispose) { - this.backgroundImage.dispose(); - } - this.backgroundImage = null; - if (this.overlayImage && this.overlayImage.dispose) { - this.overlayImage.dispose(); - } - this.overlayImage = null; - this._iTextInstances = null; - this.contextContainer = null; - // restore canvas style and attributes - this.lowerCanvasEl.classList.remove('lower-canvas'); - this.lowerCanvasEl.removeAttribute('data-fabric'); - if (this.interactive) { - this.lowerCanvasEl.style.cssText = this._originalCanvasStyle; - delete this._originalCanvasStyle; + /** + * @private + */ + _findNewLowerIndex( + object: FabricObject, + idx: number, + intersecting: boolean + ): number { + if (intersecting) { + // traverse down the stack looking for the nearest intersecting object + for (let i = idx - 1; i >= 0; --i) { + const isIntersecting = + object.intersectsWithObject(this._objects[i]) || + object.isContainedWithinObject(this._objects[i]) || + this._objects[i].isContainedWithinObject(object); + if (isIntersecting) { + return i; } - // restore canvas size to original size in case retina scaling was applied - this.lowerCanvasEl.setAttribute('width', this.width); - this.lowerCanvasEl.setAttribute('height', this.height); - fabric.util.cleanUpJsdomNode(this.lowerCanvasEl); - this.lowerCanvasEl = undefined; - }, - - /** - * Returns a string representation of an instance - * @return {String} string representation of an instance - */ - toString: function () { - return ( - '#' - ); - }, + } } - ); - - extend(fabric.StaticCanvas.prototype, fabric.DataURLExporter); - - extend( - fabric.StaticCanvas, - /** @lends fabric.StaticCanvas */ { - /** - * @static - * @type String - * @default - */ - EMPTY_JSON: '{"objects": [], "background": "white"}', - - /** - * Provides a way to check support of some of the canvas methods - * (either those of HTMLCanvasElement itself, or rendering context) - * - * @param {String} methodName Method to check support for; - * Could be one of "setLineDash" - * @return {Boolean | null} `true` if method is supported (or at least exists), - * `null` if canvas element or context can not be initialized - */ - supports: function (methodName) { - var el = createCanvasElement(); - - if (!el || !el.getContext) { - return null; - } + return idx - 1; + } + + /** + * 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 {FabricObject} object Object to send + * @param {Boolean} [intersecting] If `true`, send object in front of next upper intersecting object + * @return {fabric.Canvas} thisArg + * @chainable + */ + bringForward(object: FabricObject, intersecting: boolean) { + const activeSelection = this._activeObject; + let objsMoved = 0; + + if (object === activeSelection && isActiveSelection(object)) { + const objs = activeSelection._objects; + for (let i = objs.length; i--; ) { + const obj = objs[i]; + const idx = this._objects.indexOf(obj); + if (idx < this._objects.length - 1 - objsMoved) { + removeFromArray(this._objects, obj); + this._objects.splice(idx + 1, 0, obj); + } + objsMoved++; + } + } else { + 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.renderOnAddRemove && this.requestRenderAll(); + return this; + } - var ctx = el.getContext('2d'); - if (!ctx) { - return null; + /** + * @private + */ + _findNewUpperIndex(object: FabricObject, idx: number, intersecting: boolean) { + let newIdx; + + if (intersecting) { + newIdx = idx; + const len = this._objects.length; + // traverse up the stack looking for the nearest intersecting object + for (let i = idx + 1; i < len; ++i) { + const isIntersecting = + object.intersectsWithObject(this._objects[i]) || + object.isContainedWithinObject(this._objects[i]) || + this._objects[i].isContainedWithinObject(object); + + if (isIntersecting) { + newIdx = i; + break; } + } + } else { + newIdx = idx + 1; + } - switch (methodName) { - case 'setLineDash': - return typeof ctx.setLineDash !== 'undefined'; + return newIdx; + } - default: - return null; - } - }, + /** + * Moves an object to specified level in stack of drawn objects + * @param {FabricObject} object Object to send + * @param {Number} index Position to move to + * @return {fabric.Canvas} thisArg + * @chainable + */ + moveTo(object: FabricObject, index: number) { + removeFromArray(this._objects, object); + this._objects.splice(index, 0, object); + return this.renderOnAddRemove && this.requestRenderAll(); + } + + /** + * Waits until rendering has settled to destroy the canvas + * @returns {Promise} a promise resolving to `true` once the canvas has been destroyed or to `false` if the canvas has was already destroyed + * @throws if aborted by a consequent call + */ + dispose() { + this.disposed = true; + return new Promise((resolve, reject) => { + const task = () => { + this.destroy(); + resolve(true); + }; + task.kill = reject; + if (this.__cleanupTask) { + this.__cleanupTask.kill('aborted'); + } + + if (this.destroyed) { + resolve(false); + } else if (this.nextRenderHandle) { + this.__cleanupTask = task; + } else { + task(); + } + }); + } + + /** + * Clears the canvas element, disposes objects and frees resources + * + * **CAUTION**: + * + * This method is **UNSAFE**. + * You may encounter a race condition using it if there's a requested render. + * Call this method only if you are sure rendering has settled. + * Consider using {@link dispose} as it is **SAFE** + * + * @private + */ + destroy() { + this.destroyed = true; + this.cancelRequestedRender(); + this.forEachObject((object) => object.dispose()); + this._objects = []; + if (this.backgroundImage && this.backgroundImage.dispose) { + this.backgroundImage.dispose(); + } + this.backgroundImage = null; + if (this.overlayImage && this.overlayImage.dispose) { + this.overlayImage.dispose(); } - ); + this.overlayImage = null; + this._iTextInstances = null; + this.contextContainer = null; + const canvasElement = this.lowerCanvasEl; + // @ts-ignore + this.lowerCanvasEl = undefined; + // restore canvas style and attributes + canvasElement.classList.remove('lower-canvas'); + canvasElement.removeAttribute('data-fabric'); + // needs to be moved into Canvas class + if (this.interactive) { + canvasElement.style.cssText = this._originalCanvasStyle; + delete this._originalCanvasStyle; + } + // restore canvas size to original size in case retina scaling was applied + canvasElement.setAttribute('width', `${this.width}`); + canvasElement.setAttribute('height', `${this.height}`); + cleanUpJsdomNode(canvasElement); + } - if (fabric.isLikelyNode) { - fabric.StaticCanvas.prototype.createPNGStream = function () { - var impl = getNodeCanvas(this.lowerCanvasEl); - return impl && impl.createPNGStream(); - }; - fabric.StaticCanvas.prototype.createJPEGStream = function (opts) { - var impl = getNodeCanvas(this.lowerCanvasEl); - return impl && impl.createJPEGStream(opts); - }; + /** + * Returns a string representation of an instance + * @return {String} string representation of an instance + */ + toString() { + return `#`; } -})(typeof exports !== 'undefined' ? exports : window); +} + +Object.assign( + StaticCanvas.prototype, + { + backgroundColor: '', + backgroundImage: null, + overlayColor: '', + overlayImage: null, + includeDefaultValues: true, + stateful: false, + renderOnAddRemove: true, + controlsAboveOverlay: false, + allowTouchScrolling: false, + imageSmoothingEnabled: true, + viewportTransform: iMatrix.concat(), + backgroundVpt: true, + overlayVpt: true, + enableRetinaScaling: true, + svgViewportTransformation: true, + skipOffscreen: true, + clipPath: undefined, + }, + fabric.DataURLExporter +); + +if (fabric.isLikelyNode) { + StaticCanvas.prototype.createPNGStream = function () { + const impl = getNodeCanvas(this.lowerCanvasEl); + return impl && impl.createPNGStream(); + }; + StaticCanvas.prototype.createJPEGStream = function (opts: any) { + const impl = getNodeCanvas(this.lowerCanvasEl); + return impl && impl.createJPEGStream(opts); + }; +} + +fabric.StaticCanvas = StaticCanvas; diff --git a/src/typedefs.ts b/src/typedefs.ts index 26d6f8eeac2..b682ebe7773 100644 --- a/src/typedefs.ts +++ b/src/typedefs.ts @@ -82,3 +82,14 @@ export type TCornerPoint = { bl: Point; br: Point; }; + +export type TValidToObjectMethod = 'toDatalessObject' | 'toObject'; + +export type TCacheCanvasDimensions = { + width: number; + height: number; + zoomX: number; + zoomY: number; + x: number; + y: number; +}; diff --git a/src/util/misc/dom.ts b/src/util/misc/dom.ts index 84157221476..537abe41228 100644 --- a/src/util/misc/dom.ts +++ b/src/util/misc/dom.ts @@ -50,3 +50,9 @@ export const toDataURL = ( format: ImageFormat, quality: number ) => canvasEl.toDataURL(`image/${format}`, quality); + +export const isHTMLCanvas = ( + canvas: HTMLCanvasElement | string +): canvas is HTMLCanvasElement => { + return !!canvas && (canvas as HTMLCanvasElement).getContext !== undefined; +}; diff --git a/src/util/types.ts b/src/util/types.ts new file mode 100644 index 00000000000..59ff61ad04f --- /dev/null +++ b/src/util/types.ts @@ -0,0 +1,38 @@ +import type { ActiveSelection } from '../shapes/active_selection.class'; +import type { Group } from '../shapes/group.class'; +import { FabricObject } from '../shapes/object.class'; +import type { TFiller } from '../typedefs'; +import type { Text } from '../shapes/text.class'; +import type { Pattern } from '../pattern.class'; + +export const isFiller = (filler: TFiller | string): filler is TFiller => { + return !!filler && (filler as TFiller).toLive !== undefined; +}; + +export const isPattern = (filler: TFiller): filler is Pattern => { + return ( + !!filler && + (filler as Pattern).offsetX !== undefined && + (filler as Pattern).source !== undefined + ); +}; + +export const isCollection = ( + fabricObject: FabricObject +): fabricObject is Group | ActiveSelection => { + return !!fabricObject && Array.isArray((fabricObject as Group)._objects); +}; + +export const isActiveSelection = ( + fabricObject: FabricObject +): fabricObject is ActiveSelection => { + return !!fabricObject && fabricObject.type === 'activeSelection'; +}; + +export const isTextObject = ( + fabricObject: FabricObject +): fabricObject is Text => { + // we could use instanceof but that would mean pulling in Text code for a simple check + // @todo discuss what to do and how to do + return !!fabricObject && fabricObject.type.includes('text'); +}; diff --git a/test/unit/canvas.js b/test/unit/canvas.js index a5d678545b9..e003eccdc91 100644 --- a/test/unit/canvas.js +++ b/test/unit/canvas.js @@ -240,7 +240,7 @@ rect4 = makeRect(); assert.ok(typeof canvas.add === 'function'); - assert.equal(canvas.add(rect1), canvas, 'should be chainable'); + assert.equal(canvas.add(rect1), 1, 'should return the new length of objects array'); assert.strictEqual(canvas.item(0), rect1); canvas.add(rect2, rect3, rect4); @@ -275,7 +275,7 @@ canvas.add(rect1, rect2, rect3, rect4); assert.ok(typeof canvas.remove === 'function'); - assert.equal(canvas.remove(rect1), canvas, 'should be chainable'); + assert.equal(canvas.remove(rect1)[0], rect1, 'should return the number of objects removed'); assert.strictEqual(canvas.item(0), rect2, 'should be second object'); canvas.remove(rect2, rect3); diff --git a/test/unit/canvas_static.js b/test/unit/canvas_static.js index 12e595ab6bb..79cf35fe153 100644 --- a/test/unit/canvas_static.js +++ b/test/unit/canvas_static.js @@ -266,11 +266,11 @@ canvas.renderOnAddRemove = true; canvas.requestRenderAll = countRenderAll; assert.ok(typeof canvas.add === 'function'); - assert.equal(canvas.add(rect1), canvas, 'should be chainable'); + assert.equal(canvas.add(rect1), 1, 'should return the length of objects array'); assert.strictEqual(canvas.item(0), rect1); assert.equal(renderAllCount, 1); - canvas.add(rect2, rect3, rect4); + assert.equal(canvas.add(rect2, rect3, rect4), 4, 'should return the length of objects array'); assert.equal(canvas.getObjects().length, 4, 'should support multiple arguments'); assert.equal(renderAllCount, 2); @@ -331,7 +331,7 @@ canvas.renderOnAddRemove = false; canvas.requestRenderAll = countRenderAll; - assert.equal(canvas.add(rect), canvas, 'should be chainable'); + canvas.add(rect) assert.equal(renderAllCount, 0); assert.equal(canvas.item(0), rect); @@ -434,7 +434,7 @@ canvas.renderOnAddRemove = true; assert.ok(typeof canvas.remove === 'function'); assert.equal(renderAllCount, 0); - assert.equal(canvas.remove(rect1), canvas, 'should be chainable'); + assert.equal(canvas.remove(rect1)[0], rect1, 'should return the number of removed objects'); assert.strictEqual(canvas.item(0), rect2, 'should be second object'); canvas.remove(rect2, rect3); @@ -459,8 +459,7 @@ canvas.add(rect1, rect2); assert.equal(renderAllCount, 0); - - assert.equal(canvas.remove(rect1), canvas, 'should be chainable'); + assert.equal(canvas.remove(rect1)[0], rect1, 'will return an array with removed objects'); assert.equal(renderAllCount, 0); assert.strictEqual(canvas.item(0), rect2, 'only second object should be left'); }); @@ -874,7 +873,7 @@ return svg; } - canvas.toSVG(null, reviver); + canvas.toSVG(undefined, reviver); assert.equal(reviverCount, 14); canvas.renderOnAddRemove = true; @@ -913,7 +912,7 @@ return svg; } - canvas.toSVG(null, reviver); + canvas.toSVG(undefined, reviver); assert.equal(reviverCount, len + 2, 'reviver should include background and overlay image'); canvas.backgroundImage = null; canvas.overlayImage = null; @@ -949,7 +948,7 @@ return svg; } - canvas.toSVG(null, reviver); + canvas.toSVG(undefined, reviver); assert.equal(reviverCount, len - 2, 'reviver should not include objects with excludeFromExport'); canvas.renderOnAddRemove = true; });