From e2d36f2fcc51747a9c011aea5273c893542a2c7a Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Sat, 3 Dec 2022 23:56:08 +0100 Subject: [PATCH 01/19] save the salvageable --- src/EventTypeDefs.ts | 14 +- src/canvas.class.ts | 5 + src/mixins/collection.mixin.ts | 2 +- src/shapes/group.class.ts | 8 +- src/shapes/object.class.ts | 9 + src/static_canvas.class.ts | 3598 ++++++++++++++++---------------- src/util/misc/dom.ts | 5 + 7 files changed, 1826 insertions(+), 1815 deletions(-) diff --git a/src/EventTypeDefs.ts b/src/EventTypeDefs.ts index 9bbbb975934..da7d0fc60b7 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/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/shapes/group.class.ts b/src/shapes/group.class.ts index 7da54cbdf10..c418c210753 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,14 +44,12 @@ export type LayoutResult = { height: number; }; -export type GroupEvents = ObjectEvents & { +export type GroupEvents = ObjectEvents & CollectionEvents & { layout: { context: LayoutContext; result: LayoutResult; diff: Point; }; - 'object:added': { target: FabricObject }; - 'object:removed': { target: FabricObject }; }; export type LayoutStrategy = diff --git a/src/shapes/object.class.ts b/src/shapes/object.class.ts index 89f09226a4c..597045e8147 100644 --- a/src/shapes/object.class.ts +++ b/src/shapes/object.class.ts @@ -596,6 +596,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; /** diff --git a/src/static_canvas.class.ts b/src/static_canvas.class.ts index 7a99dbba6f6..b6063424e91 100644 --- a/src/static_canvas.class.ts +++ b/src/static_canvas.class.ts @@ -1,4 +1,3 @@ -//@ts-nocheck import { config } from './config'; import { VERSION } from './constants'; import { createCollectionMixin } from './mixins/collection.mixin'; @@ -9,1855 +8,1846 @@ import { requestAnimFrame } from './util/animate'; import { removeFromArray } from './util/internals'; import { uid } from './util/internals/uid'; 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 type { TFiller, TMat2D, TCornerPoint, TSize } from './typedefs'; +import type { StaticCanvasEvents } from './EventTypeDefs'; +import { getElementOffset } from './util/dom_misc'; +import { createCanvasElement, isHTMLCanvas } from './util/misc/dom'; +import { fabric } from '../HEADER'; +import { type } from 'os'; +import { invertTransform, transformPoint } from './util/misc/matrix'; + +const CANVAS_INIT_ERROR = 'Could not initialize `canvas` element'; + +export type TCanvasSizeOptions = { + backstoreOnly?: boolean; + cssOnly?: boolean; +} + +/** + * 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 +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 fabric.Image + * @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 fabric.Image + * @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 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: 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. + * @memberOf fabric.StaticCanvas.prototype + */ + 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. + * @memberOf fabric.StaticCanvas.prototype + * @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 fabric.Object + */ + 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; + + 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; + } + + protected _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 }); + } + + protected _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); + } + + constructor(el: string | HTMLCanvasElement, options = {}) { + super(); + this.renderAndResetBound = this.renderAndReset.bind(this); + this.requestRenderAllBound = this.requestRenderAll.bind(this); + this._initStatic(el, options); + } + + /** + * @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; } - insertAt(index: number, ...objects: FabricObject[]) { - super.insertAt(index, ...objects); - objects.length > 0 && this.renderOnAddRemove && this.requestRenderAll(); - return this; - } + if (!backstoreOnly) { + this._setCssDimension(prop as keyof TSize, cssValue); + } + }); + + 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; + var 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 + * @return {fabric.StaticCanvas} instance + * @chainable true + */ + 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 + * @return {fabric.StaticCanvas} instance + * @chainable true + */ + 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.apply(this, 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) { + 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(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); + } + 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) { + const v = this.viewportTransform, + path = this.clipPath; + ctx.save(); + ctx.transform(...v); + // 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(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; + } + 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); + } + ctx.transform(1, 0, 0, 1, fill.offsetX || 0, fill.offsetY || 0); + const m = fill.gradientTransform || fill.patternTransform; + 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, + }; + } + + /** + * Returns coordinates of a center of canvas. + * @return {Point} + */ + getCenterPoint() { + 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(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(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(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(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(object) { + var vpCenter = this.getVpCenter(); + this._centerObject( + object, + new Point(vpCenter.x, object.getCenterPoint().y) + ); + return this; + } - remove(...objects: FabricObject[]) { - const removed = super.remove(...objects); - removed.length > 0 && this.renderOnAddRemove && this.requestRenderAll(); - 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(object) { + var vpCenter = this.getVpCenter(); - 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 }); - } + return this._centerObject( + object, + new Point(object.getCenterPoint().x, vpCenter.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; - } + /** + * Calculate the point in canvas that correspond to the center of actual viewport. + * @return {Point} vpCenter, viewport center + * @chainable + */ + getVpCenter() { + var center = this.getCenterPoint(), + iVpt = invertTransform(this.viewportTransform); + return transformPoint(center, iVpt); + } - 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); - } + /** + * @private + * @param {fabric.Object} object Object to center + * @param {Point} center Center point + * @return {fabric.Canvas} thisArg + * @chainable + */ + _centerObject(object, center) { + object.setXY(center, 'center', 'center'); + object.setCoords(); + this.renderOnAddRemove && this.requestRenderAll(); + return this; + } - 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; - } + /** + * 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) { + return this.toDatalessObject(propertiesToInclude); + } - if (!options.backstoreOnly) { - this._setCssDimension(prop, cssValue); - } - } - if (this._isCurrentlyDrawing) { - this.freeDrawingBrush && - this.freeDrawingBrush._setBrushStyles(this.contextTop); - } - this._initRetinaScaling(); - this.calcOffset(); + /** + * 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) { + return this._toObjectMethod('toObject', propertiesToInclude); + } - if (!options.cssOnly) { - this.requestRenderAll(); - } + /** + * 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(); + } - 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; - } + /** + * 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) { + return this._toObjectMethod('toDatalessObject', propertiesToInclude); + } - if (this.cacheCanvasEl) { - this.cacheCanvasEl[prop] = value; - } + /** + * @private + */ + _toObjectMethod(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), + }; + } - this[prop] = value; + /** + * @private + */ + _toObject(instance, methodName, propertiesToInclude) { + var originalValue; - return this; - }, + if (!this.includeDefaultValues) { + originalValue = instance.includeDefaultValues; + instance.includeDefaultValues = false; + } - /** - * 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; + var object = instance[methodName](propertiesToInclude); + if (!this.includeDefaultValues) { + instance.includeDefaultValues = originalValue; + } + return object; + } - if (this.upperCanvasEl) { - this.upperCanvasEl.style[prop] = value; - } + /** + * @private + */ + __serializeBgOverlay(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; + } - if (this.wrapperEl) { - this.wrapperEl.style[prop] = value; - } + if (overlayColor && overlayColor.toObject) { + if (!overlayColor.excludeFromExport) { + data.overlay = overlayColor.toObject(propertiesToInclude); + } + } else if (overlayColor) { + data.overlay = overlayColor; + } - 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, - }); - }, - - 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; - } + if (bgImage && !bgImage.excludeFromExport) { + data.backgroundImage = this._toObject( + bgImage, + methodName, + propertiesToInclude + ); + } + if (overlayImage && !overlayImage.excludeFromExport) { + data.overlayImage = this._toObject( + overlayImage, + methodName, + propertiesToInclude + ); + } - 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 }); + return data; + } - 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; - } + /* _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, - 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; - } + /** + * 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, 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); - if (overlayColor && overlayColor.toObject) { - if (!overlayColor.excludeFromExport) { - data.overlay = overlayColor.toObject(propertiesToInclude); - } - } else if (overlayColor) { - data.overlay = overlayColor; - } + markup.push(''); - if (bgImage && !bgImage.excludeFromExport) { - data.backgroundImage = this._toObject( - bgImage, - methodName, - propertiesToInclude - ); - } - if (overlayImage && !overlayImage.excludeFromExport) { - data.overlayImage = this._toObject( - overlayImage, - methodName, - propertiesToInclude - ); - } + return markup.join(''); + } - 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); + /** + * @private + */ + _setSVGPreamble(markup, options) { + if (options.suppressPreamble) { + return; + } + markup.push( + '\n', + '\n' + ); + } - markup.push(''); + /** + * @private + */ + _setSVGHeader(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) + + '" '; + } + } - return markup.join(''); - }, + markup.push( + '\n', + 'Created with Fabric.js ', + VERSION, + '\n', + '\n', + this.createSVGFontFacesMarkup(), + this.createSVGRefElementsMarkup(), + this.createSVGClipPathMarkup(options), + '\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) + - '" '; - } - } + createSVGClipPathMarkup(options) { + var clipPath = this.clipPath; + if (clipPath) { + clipPath.clipPathId = 'CLIPPATH_' + uid(); + return ( + '\n' + + this.clipPath.toClipPathSVG(options.reviver) + + '\n' + ); + } + return ''; + } - 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) - : '', - }); - } + /** + * Creates markup containing SVG referenced elements like patterns, gradients etc. + * @return {String} + */ + createSVGRefElementsMarkup() { + 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) + : '', }); - 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); - } - }); - - 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; - } - } - } } + }); + return markup.join(''); + } - 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(''); + /** + * 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() { + 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); + } + }); + + 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; + } } + } + } - if (markup) { - markup = [ - '\t\n', - ].join(''); - } + 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(''); + } - 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 { + if (markup) { + markup = [ + '\t\n', + ].join(''); + } + + return markup; + } + + /** + * @private + */ + _setSVGObjects(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(markup, instance, reviver) { + markup.push(instance.toSVG(reviver)); + } + + /** + * @private + */ + _setSVGBgOverlayImage(markup, property, reviver) { + if ( + this[property] && + !this[property].excludeFromExport && + this[property].toSVG + ) { + markup.push(this[property].toSVG(reviver)); + } + } + + /** + * @private + */ + _setSVGBgOverlayColor(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(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(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(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; + } - 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); - } + /** + * @private + */ + _findNewLowerIndex(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; } - 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 { + } + } else { + newIdx = idx - 1; + } + + 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(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); } - - 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) { + 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(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 + */ + _findNewUpperIndex(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; } - // 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 ( - '#' - ); - }, + } + } else { + newIdx = idx + 1; } - ); - - 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; - } - var ctx = el.getContext('2d'); - if (!ctx) { - return null; - } + return newIdx; + } - switch (methodName) { - case 'setLineDash': - return typeof ctx.setLineDash !== 'undefined'; + /** + * 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(object, index) { + removeFromArray(this._objects, object); + this._objects.splice(index, 0, object); + return this.renderOnAddRemove && this.requestRenderAll(); + } - default: - return null; - } - }, + /** + * 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; + // 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; + } + // 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; + } - 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: fabric.iMatrix.concat(), + backgroundVpt: true, + overlayVpt: true, + enableRetinaScaling: true, + vptCoords: {} + skipOffscreen: true, + clipPath: undefined, +}) + +extend(fabric.StaticCanvas.prototype, fabric.DataURLExporter); + +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); + }; +} + diff --git a/src/util/misc/dom.ts b/src/util/misc/dom.ts index 84157221476..f2a69405c8a 100644 --- a/src/util/misc/dom.ts +++ b/src/util/misc/dom.ts @@ -50,3 +50,8 @@ 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; +} From 16519706d2cbfbb9e91026555292f99685408a51 Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Sun, 4 Dec 2022 10:50:46 +0100 Subject: [PATCH 02/19] some more work --- src/gradient/gradient.class.ts | 23 +++++-- src/pattern.class.ts | 13 +++- src/shapes/object.class.ts | 6 +- src/static_canvas.class.ts | 108 ++++++++++++++++----------------- src/typedefs.ts | 2 + 5 files changed, 88 insertions(+), 64 deletions(-) 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/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/object.class.ts b/src/shapes/object.class.ts index 597045e8147..034c2a0fd9c 100644 --- a/src/shapes/object.class.ts +++ b/src/shapes/object.class.ts @@ -19,6 +19,8 @@ import { pick } from '../util/misc/pick'; import { toFixed } from '../util/misc/toFixed'; import type { Group } from './group.class'; +export type TCachedFabricObject = FabricObject & Required>; + // temporary hack for unfinished migration type TCallSuper = (arg0: string, ...moreArgs: any[]) => any; @@ -835,7 +837,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 @@ -900,7 +902,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/static_canvas.class.ts b/src/static_canvas.class.ts index b6063424e91..6fb1ce3f1eb 100644 --- a/src/static_canvas.class.ts +++ b/src/static_canvas.class.ts @@ -8,13 +8,17 @@ import { requestAnimFrame } from './util/animate'; import { removeFromArray } from './util/internals'; import { uid } from './util/internals/uid'; import { pick } from './util/misc/pick'; -import type { TFiller, TMat2D, TCornerPoint, TSize } from './typedefs'; +import type { TFiller, TMat2D, TCornerPoint, TSize, TValidToObjectMethod } from './typedefs'; import type { StaticCanvasEvents } from './EventTypeDefs'; import { getElementOffset } from './util/dom_misc'; import { createCanvasElement, isHTMLCanvas } from './util/misc/dom'; import { fabric } from '../HEADER'; import { type } from 'os'; import { invertTransform, transformPoint } from './util/misc/matrix'; +import { TCachedFabricObject } from './shapes/object.class'; +import { isFiller } from './util/types'; +import { Gradient } from './gradient'; +import { Pattern } from './pattern.class'; const CANVAS_INIT_ERROR = 'Could not initialize `canvas` element'; @@ -256,6 +260,7 @@ export class StaticCanvas extends createCollectionMixin(CommonMethods).gradientTransform || (fill as Pattern).patternTransform) as TMat2D; + m && ctx.transform(...m); + } ctx.fill(); ctx.restore(); } @@ -921,7 +929,7 @@ export class StaticCanvas extends createCollectionMixin(CommonMethods Date: Sun, 4 Dec 2022 12:23:41 +0100 Subject: [PATCH 03/19] more work --- src/mixins/object.svg_export.ts | 22 ++- src/shapes/object.class.ts | 4 +- src/shapes/text.class.ts | 4 +- src/static_canvas.class.ts | 235 +++++++++++++------------------- src/typedefs.ts | 9 ++ 5 files changed, 125 insertions(+), 149 deletions(-) diff --git a/src/mixins/object.svg_export.ts b/src/mixins/object.svg_export.ts index 72566784f38..a2171544e17 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,14 @@ 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 +199,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 +210,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 +230,7 @@ export class FabricObjectSVGExportMixin { { reviver, additionalTransform = '', - }: { reviver?: SVGReviver; additionalTransform?: string } = {} + }: { reviver?: TSVGReviver; additionalTransform?: string } = {} ) { const commonPieces = [ this.getSvgTransform(true, additionalTransform), @@ -246,7 +254,7 @@ export class FabricObjectSVGExportMixin { additionalTransform, }: { noStyle?: boolean; - reviver?: SVGReviver; + reviver?: TSVGReviver; withShadow?: boolean; additionalTransform?: string; } = {} diff --git a/src/shapes/object.class.ts b/src/shapes/object.class.ts index 034c2a0fd9c..68f7d328fa0 100644 --- a/src/shapes/object.class.ts +++ b/src/shapes/object.class.ts @@ -7,7 +7,7 @@ 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'; @@ -705,7 +705,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 }), 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 6fb1ce3f1eb..55c2683e7c4 100644 --- a/src/static_canvas.class.ts +++ b/src/static_canvas.class.ts @@ -1,3 +1,4 @@ +// @ts-ignore import { config } from './config'; import { VERSION } from './constants'; import { createCollectionMixin } from './mixins/collection.mixin'; @@ -10,15 +11,18 @@ import { uid } from './util/internals/uid'; import { pick } from './util/misc/pick'; import type { TFiller, TMat2D, TCornerPoint, TSize, TValidToObjectMethod } from './typedefs'; import type { StaticCanvasEvents } from './EventTypeDefs'; -import { getElementOffset } from './util/dom_misc'; +import { getElementOffset, getNodeCanvas } from './util/dom_misc'; import { createCanvasElement, isHTMLCanvas } from './util/misc/dom'; import { fabric } from '../HEADER'; -import { type } from 'os'; import { invertTransform, transformPoint } from './util/misc/matrix'; import { TCachedFabricObject } from './shapes/object.class'; -import { isFiller } from './util/types'; +import { isCollection, isFiller, isTextObject } from './util/types'; import { Gradient } from './gradient'; import { Pattern } from './pattern.class'; +import { toFixed } from './util/misc/toFixed'; +import { matrixToSVG } from './util/misc/svgParsing'; +import { Rect } from './shapes/rect.class'; +import { TSVGReviver } from './mixins/object.svg_export'; const CANVAS_INIT_ERROR = 'Could not initialize `canvas` element'; @@ -27,6 +31,20 @@ export type TCanvasSizeOptions = { 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 * @class fabric.StaticCanvas @@ -1195,16 +1213,15 @@ export class StaticCanvas extends createCollectionMixin(CommonMethods\n' + `\n` ); } this._setSVGBgOverlayColor(markup, 'background'); @@ -1224,7 +1241,7 @@ export class StaticCanvas extends createCollectionMixin(CommonMethods `\t\t@font-face {\n\t\t\tfont-family: '${fontFamily}';\n\t\t\tsrc: url('${fontPaths[fontFamily]}');\n\t\t}\n`).join(''); - if (markup) { - markup = [ - '\t\n', - ].join(''); + if (fontListMarkup) { + return `\t\n`; } - - return markup; + return ''; } /** * @private */ - _setSVGObjects(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; + _setSVGObjects(markup: string[], reviver: TSVGReviver) { + this.forEachObject((fabricObject) => { + if (fabricObject.excludeFromExport) { + return; } - this._setSVGObject(markup, instance, reviver); - } + this._setSVGObject(markup, fabricObject, reviver); + }) } /** + * This is its own function because the Canvas ( non static ) requires extra code here * @private */ - _setSVGObject(markup, instance, reviver) { + _setSVGObject(markup: string[], instance: FabricObject, reviver: TSVGReviver) { markup.push(instance.toSVG(reviver)); } /** * @private */ - _setSVGBgOverlayImage(markup, property, reviver) { - if ( - this[property] && - !this[property].excludeFromExport && - this[property].toSVG + _setSVGBgOverlayImage(markup: string[], property: 'overlayImage' | 'backgroundImage', reviver: TSVGReviver) { + const bgOrOverlay = this[property]; + if (bgOrOverlay !== null && + !bgOrOverlay.excludeFromExport && + bgOrOverlay.toSVG ) { - markup.push(this[property].toSVG(reviver)); + markup.push(bgOrOverlay.toSVG(reviver)); } } @@ -1838,15 +1797,15 @@ Object.assign(StaticCanvas.prototype, { clipPath: undefined, }) -extend(fabric.StaticCanvas.prototype, fabric.DataURLExporter); +Object.assign(StaticCanvas.prototype, fabric.DataURLExporter); if (fabric.isLikelyNode) { fabric.StaticCanvas.prototype.createPNGStream = function () { - var impl = getNodeCanvas(this.lowerCanvasEl); + const impl = getNodeCanvas(this.lowerCanvasEl); return impl && impl.createPNGStream(); }; fabric.StaticCanvas.prototype.createJPEGStream = function (opts) { - var impl = getNodeCanvas(this.lowerCanvasEl); + const impl = getNodeCanvas(this.lowerCanvasEl); return impl && impl.createJPEGStream(opts); }; } diff --git a/src/typedefs.ts b/src/typedefs.ts index 19a3114e467..7f5658e4f73 100644 --- a/src/typedefs.ts +++ b/src/typedefs.ts @@ -84,3 +84,12 @@ export type TCornerPoint = { }; export type TValidToObjectMethod = 'toDatalessObject' | 'toObject'; + +export type TCacheCanvasDimensions = { + width: number, + height: number, + zoomX: number, + zoomY: number, + x: number, + y: number, +}; From ce41207458d0107198ac3942dead528011423996 Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Sun, 4 Dec 2022 12:23:55 +0100 Subject: [PATCH 04/19] more work --- src/shapes/test.ts | 0 src/util/types.ts | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 src/shapes/test.ts create mode 100644 src/util/types.ts diff --git a/src/shapes/test.ts b/src/shapes/test.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/util/types.ts b/src/util/types.ts new file mode 100644 index 00000000000..1bb52e8e55e --- /dev/null +++ b/src/util/types.ts @@ -0,0 +1,19 @@ +import type { ActiveSelection } from "../shapes/active_selection.class"; +import type { Group } from "../shapes/group.class"; +import type { FabricObject } from "../shapes/object.class"; +import type { TFiller } from "../typedefs"; +import type { Text } from '../shapes/text.class'; + +export const isFiller = (filler: TFiller | string): filler is TFiller => { + return !!filler && (filler as TFiller).toLive !== undefined; +} + +export const isCollection = (fabricObject: FabricObject): fabricObject is Group | ActiveSelection => { + return !!fabricObject && Array.isArray((fabricObject as Group)._objects); +} + +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'); +} From a041f4125b746d46a8f3b5e5647b123280cbfde1 Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Sun, 4 Dec 2022 12:24:09 +0100 Subject: [PATCH 05/19] more work --- src/shapes/test.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/shapes/test.ts diff --git a/src/shapes/test.ts b/src/shapes/test.ts deleted file mode 100644 index e69de29bb2d..00000000000 From 749de899519e75ca22734096638dc94398fbdd8d Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Sun, 4 Dec 2022 13:24:04 +0100 Subject: [PATCH 06/19] some exta work --- src/EventTypeDefs.ts | 2 +- src/mixins/object.svg_export.ts | 1 - src/shapes/group.class.ts | 13 +- src/shapes/object.class.ts | 20 ++- src/static_canvas.class.ts | 222 +++++++++++++++++++------------- src/typedefs.ts | 12 +- src/util/misc/dom.ts | 7 +- src/util/types.ts | 22 ++-- 8 files changed, 182 insertions(+), 117 deletions(-) diff --git a/src/EventTypeDefs.ts b/src/EventTypeDefs.ts index da7d0fc60b7..ad2317f6667 100644 --- a/src/EventTypeDefs.ts +++ b/src/EventTypeDefs.ts @@ -153,7 +153,7 @@ type CanvasSelectionEvents = { type CollectionEvents = { 'object:added': { target: FabricObject }; 'object:removed': { target: FabricObject }; -} +}; type BeforeSuffix = `${T}:before`; type WithBeforeSuffix = T | BeforeSuffix; diff --git a/src/mixins/object.svg_export.ts b/src/mixins/object.svg_export.ts index a2171544e17..40f7bf4d22d 100644 --- a/src/mixins/object.svg_export.ts +++ b/src/mixins/object.svg_export.ts @@ -29,7 +29,6 @@ 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. diff --git a/src/shapes/group.class.ts b/src/shapes/group.class.ts index c418c210753..923329df319 100644 --- a/src/shapes/group.class.ts +++ b/src/shapes/group.class.ts @@ -44,13 +44,14 @@ export type LayoutResult = { height: number; }; -export type GroupEvents = ObjectEvents & CollectionEvents & { - layout: { - context: LayoutContext; - result: LayoutResult; - diff: Point; +export type GroupEvents = ObjectEvents & + CollectionEvents & { + layout: { + context: LayoutContext; + result: LayoutResult; + diff: Point; + }; }; -}; export type LayoutStrategy = | 'fit-content' diff --git a/src/shapes/object.class.ts b/src/shapes/object.class.ts index 68f7d328fa0..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, TCacheCanvasDimensions } 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,7 +25,17 @@ import { pick } from '../util/misc/pick'; import { toFixed } from '../util/misc/toFixed'; import type { Group } from './group.class'; -export type TCachedFabricObject = FabricObject & Required>; +export type TCachedFabricObject = FabricObject & + Required< + Pick< + FabricObject, + | 'zoomX' + | 'zoomY' + | '_cacheCanvas' + | 'cacheTranslationX' + | 'cacheTranslationY' + > + >; // temporary hack for unfinished migration type TCallSuper = (arg0: string, ...moreArgs: any[]) => any; diff --git a/src/static_canvas.class.ts b/src/static_canvas.class.ts index 55c2683e7c4..d9b9919bd84 100644 --- a/src/static_canvas.class.ts +++ b/src/static_canvas.class.ts @@ -1,4 +1,3 @@ -// @ts-ignore import { config } from './config'; import { VERSION } from './constants'; import { createCollectionMixin } from './mixins/collection.mixin'; @@ -9,7 +8,13 @@ import { requestAnimFrame } from './util/animate'; import { removeFromArray } from './util/internals'; import { uid } from './util/internals/uid'; import { pick } from './util/misc/pick'; -import type { TFiller, TMat2D, TCornerPoint, TSize, TValidToObjectMethod } from './typedefs'; +import type { + TFiller, + TMat2D, + TCornerPoint, + TSize, + TValidToObjectMethod, +} from './typedefs'; import type { StaticCanvasEvents } from './EventTypeDefs'; import { getElementOffset, getNodeCanvas } from './util/dom_misc'; import { createCanvasElement, isHTMLCanvas } from './util/misc/dom'; @@ -29,7 +34,7 @@ const CANVAS_INIT_ERROR = 'Could not initialize `canvas` element'; export type TCanvasSizeOptions = { backstoreOnly?: boolean; cssOnly?: boolean; -} +}; export type TSVGExportOptions = { suppressPreamble?: boolean; @@ -38,12 +43,12 @@ export type TSVGExportOptions = { 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 @@ -58,7 +63,9 @@ export type TSVGExportOptions = { * @fires object:removed */ // eslint-disable-next-line max-len -export class StaticCanvas extends createCollectionMixin(CommonMethods) { +export class StaticCanvas extends createCollectionMixin( + CommonMethods +) { /** * Background color of canvas instance. * @type {(String|TFiller)} @@ -232,6 +239,13 @@ export class StaticCanvas extends createCollectionMixin(CommonMethods 0 && this.renderOnAddRemove && this.requestRenderAll(); @@ -250,7 +264,7 @@ export class StaticCanvas extends createCollectionMixin(CommonMethods, { cssOnly = false, backstoreOnly = false }: TCanvasSizeOptions = {}) { + setDimensions( + dimensions: Partial, + { cssOnly = false, backstoreOnly = false }: TCanvasSizeOptions = {} + ) { Object.entries(dimensions).forEach(([prop, value]) => { let cssValue = `${value}`; @@ -833,7 +848,10 @@ export class StaticCanvas extends createCollectionMixin(CommonMethods).gradientTransform || (fill as Pattern).patternTransform) as TMat2D; + const m = ((fill as Gradient<'linear'>).gradientTransform || + (fill as Pattern).patternTransform) as TMat2D; m && ctx.transform(...m); } ctx.fill(); @@ -994,10 +1016,7 @@ export class StaticCanvas extends createCollectionMixin(CommonMethods\n` - ); + markup.push(`\n`); } this._setSVGBgOverlayColor(markup, 'background'); this._setSVGBgOverlayImage(markup, 'backgroundImage', reviver); @@ -1267,7 +1294,13 @@ export class StaticCanvas extends createCollectionMixin(CommonMethods { - 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) - : '', - }); - } - }).join(''); + 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) : '', + }); + } + }) + .join(''); } /** @@ -1358,7 +1391,7 @@ export class StaticCanvas extends createCollectionMixin(CommonMethods `\t\t@font-face {\n\t\t\tfont-family: '${fontFamily}';\n\t\t\tsrc: url('${fontPaths[fontFamily]}');\n\t\t}\n`).join(''); + 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`; @@ -1392,23 +1430,32 @@ export class StaticCanvas extends createCollectionMixin(CommonMethods { return !!filler && (filler as TFiller).toLive !== undefined; -} +}; -export const isCollection = (fabricObject: FabricObject): fabricObject is Group | ActiveSelection => { +export const isCollection = ( + fabricObject: FabricObject +): fabricObject is Group | ActiveSelection => { return !!fabricObject && Array.isArray((fabricObject as Group)._objects); -} +}; -export const isTextObject = (fabricObject: FabricObject): fabricObject is Text => { +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'); -} +}; From a98d329a1b84a8ce152c27d4d9d121139d947fab Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Sun, 4 Dec 2022 13:39:54 +0100 Subject: [PATCH 07/19] some more work --- src/static_canvas.class.ts | 103 ++++++++++++------------------------- src/util/types.ts | 5 ++ 2 files changed, 39 insertions(+), 69 deletions(-) diff --git a/src/static_canvas.class.ts b/src/static_canvas.class.ts index d9b9919bd84..89c580fb51a 100644 --- a/src/static_canvas.class.ts +++ b/src/static_canvas.class.ts @@ -21,7 +21,7 @@ import { createCanvasElement, isHTMLCanvas } from './util/misc/dom'; import { fabric } from '../HEADER'; import { invertTransform, transformPoint } from './util/misc/matrix'; import { TCachedFabricObject } from './shapes/object.class'; -import { isCollection, isFiller, isTextObject } from './util/types'; +import { isCollection, isFiller, isPattern, isTextObject } from './util/types'; import { Gradient } from './gradient'; import { Pattern } from './pattern.class'; import { toFixed } from './util/misc/toFixed'; @@ -1472,7 +1472,7 @@ export class StaticCanvas extends createCollectionMixin( if (!filler) { return; } - if (isFiller(filler)) { + if (isPattern(filler)) { const repeat = filler.repeat, finalWidth = this.width, finalHeight = this.height, @@ -1485,17 +1485,11 @@ export class StaticCanvas extends createCollectionMixin( finalHeight / 2 })" x="${filler.offsetX - finalWidth / 2}" y="${ filler.offsetY - finalHeight / 2 - }" `, - 'width="', - repeat === 'repeat-y' || repeat === 'no-repeat' + }" width="${repeat === 'repeat-y' || repeat === 'no-repeat' ? filler.source.width - : finalWidth, - '" height="', - repeat === 'repeat-x' || repeat === 'no-repeat' + : finalWidth}" height="${repeat === 'repeat-x' || repeat === 'no-repeat' ? filler.source.height - : finalHeight, - '" fill="url(#SVGID_' + filler.id + ')"', - '>\n' + : finalHeight}" fill="url(#SVGID_' + filler.id + ')">\n` ); } else { markup.push( @@ -1516,18 +1510,13 @@ export class StaticCanvas extends createCollectionMixin( * @return {fabric.Canvas} thisArg * @chainable */ - sendToBack(object) { - if (!object) { - return this; - } - var activeSelection = this._activeObject, - i, - obj, - objs; + sendToBack(object: FabricObject) { + const activeSelection = this._activeObject; + // @TODO: this part should be in canvas. StaticCanvas can't handle active selections if (object === activeSelection && object.type === 'activeSelection') { - objs = activeSelection._objects; - for (i = objs.length; i--; ) { - obj = objs[i]; + const objs = activeSelection._objects; + for (let i = objs.length; i--; ) { + const obj = objs[i]; removeFromArray(this._objects, obj); this._objects.unshift(obj); } @@ -1546,18 +1535,13 @@ export class StaticCanvas extends createCollectionMixin( * @return {fabric.Canvas} thisArg * @chainable */ - bringToFront(object) { - if (!object) { - return this; - } - var activeSelection = this._activeObject, - i, - obj, - objs; + bringToFront(object: FabricObject) { + const activeSelection = this._activeObject; + // @TODO: this part should be in canvas. StaticCanvas can't handle active selections if (object === activeSelection && object.type === 'activeSelection') { - objs = activeSelection._objects; - for (i = 0; i < objs.length; i++) { - obj = objs[i]; + const objs = activeSelection._objects; + for (let i = 0; i < objs.length; i++) { + const obj = objs[i]; removeFromArray(this._objects, obj); this._objects.push(obj); } @@ -1575,42 +1559,31 @@ export class StaticCanvas extends createCollectionMixin( * 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 + * @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, intersecting) { - if (!object) { - return this; - } - var activeSelection = this._activeObject, - i, - obj, - idx, - newIdx, - objs, - objsMoved = 0; - + sendBackwards(object: FabricObject, intersecting: boolean) { + const activeSelection = this._activeObject; if (object === activeSelection && object.type === 'activeSelection') { - objs = activeSelection._objects; - for (i = 0; i < objs.length; i++) { - obj = objs[i]; - idx = this._objects.indexOf(obj); + 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) { - newIdx = idx - 1; removeFromArray(this._objects, obj); - this._objects.splice(newIdx, 0, obj); + this._objects.splice(idx - 1, 0, obj); } objsMoved++; } } else { - idx = this._objects.indexOf(object); + const idx: number = 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._objects.splice(this._findNewLowerIndex(object, idx, intersecting), 0, object); } } this.renderOnAddRemove && this.requestRenderAll(); @@ -1620,29 +1593,21 @@ export class StaticCanvas extends createCollectionMixin( /** * @private */ - _findNewLowerIndex(object, idx, intersecting) { - var newIdx, i; - + _findNewLowerIndex(object: FabricObject, idx: number, intersecting: boolean): number { if (intersecting) { - newIdx = idx; - // traverse down the stack looking for the nearest intersecting object - for (i = idx - 1; i >= 0; --i) { - var isIntersecting = + 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) { - newIdx = i; - break; + return i; } } - } else { - newIdx = idx - 1; } - - return newIdx; + return idx - 1; } /** diff --git a/src/util/types.ts b/src/util/types.ts index 64ff21bba1f..f6f421034ab 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -3,11 +3,16 @@ import type { Group } from '../shapes/group.class'; import type { 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 => { From f10f118e8109d441ff04d21c2db22459641da425 Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Sun, 4 Dec 2022 13:41:21 +0100 Subject: [PATCH 08/19] some more work --- src/mixins/object_geometry.mixin.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/mixins/object_geometry.mixin.ts b/src/mixins/object_geometry.mixin.ts index 0715e0141ac..8fc6134e21c 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, From 915bdd9a098f8911508261dd6a2cf8588dc5d761 Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Sun, 4 Dec 2022 16:30:10 +0100 Subject: [PATCH 09/19] finished converting, now lets see how broken it is --- src/static_canvas.class.ts | 67 +++++++++++++++++++------------------- src/util/types.ts | 6 +++- 2 files changed, 38 insertions(+), 35 deletions(-) diff --git a/src/static_canvas.class.ts b/src/static_canvas.class.ts index 89c580fb51a..6e2caae2cca 100644 --- a/src/static_canvas.class.ts +++ b/src/static_canvas.class.ts @@ -21,7 +21,7 @@ import { createCanvasElement, isHTMLCanvas } from './util/misc/dom'; import { fabric } from '../HEADER'; import { invertTransform, transformPoint } from './util/misc/matrix'; import { TCachedFabricObject } from './shapes/object.class'; -import { isCollection, isFiller, isPattern, isTextObject } from './util/types'; +import { isActiveSelection, isCollection, isFiller, isPattern, isTextObject } from './util/types'; import { Gradient } from './gradient'; import { Pattern } from './pattern.class'; import { toFixed } from './util/misc/toFixed'; @@ -246,6 +246,13 @@ export class StaticCanvas extends createCollectionMixin( */ 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(); @@ -1513,7 +1520,7 @@ export class StaticCanvas extends createCollectionMixin( sendToBack(object: FabricObject) { const activeSelection = this._activeObject; // @TODO: this part should be in canvas. StaticCanvas can't handle active selections - if (object === activeSelection && object.type === 'activeSelection') { + if (object === activeSelection && isActiveSelection(object)) { const objs = activeSelection._objects; for (let i = objs.length; i--; ) { const obj = objs[i]; @@ -1538,7 +1545,7 @@ export class StaticCanvas extends createCollectionMixin( bringToFront(object: FabricObject) { const activeSelection = this._activeObject; // @TODO: this part should be in canvas. StaticCanvas can't handle active selections - if (object === activeSelection && object.type === 'activeSelection') { + if (object === activeSelection && isActiveSelection(object)) { const objs = activeSelection._objects; for (let i = 0; i < objs.length; i++) { const obj = objs[i]; @@ -1566,7 +1573,7 @@ export class StaticCanvas extends createCollectionMixin( */ sendBackwards(object: FabricObject, intersecting: boolean) { const activeSelection = this._activeObject; - if (object === activeSelection && object.type === 'activeSelection') { + if (object === activeSelection && isActiveSelection(object)) { let objsMoved = 0; const objs = activeSelection._objects; for (let i = 0; i < objs.length; i++) { @@ -1582,8 +1589,9 @@ export class StaticCanvas extends createCollectionMixin( 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(this._findNewLowerIndex(object, idx, intersecting), 0, object); + this._objects.splice(newIdx, 0, object); } } this.renderOnAddRemove && this.requestRenderAll(); @@ -1601,7 +1609,6 @@ export class StaticCanvas extends createCollectionMixin( object.intersectsWithObject(this._objects[i]) || object.isContainedWithinObject(this._objects[i]) || this._objects[i].isContainedWithinObject(object); - if (isIntersecting) { return i; } @@ -1621,35 +1628,26 @@ export class StaticCanvas extends createCollectionMixin( * @return {fabric.Canvas} thisArg * @chainable */ - bringForward(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); + 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) { - newIdx = idx + 1; removeFromArray(this._objects, obj); - this._objects.splice(newIdx, 0, obj); + this._objects.splice(idx + 1, 0, obj); } objsMoved++; } } else { - idx = this._objects.indexOf(object); + 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) - newIdx = this._findNewUpperIndex(object, idx, intersecting); + const newIdx = this._findNewUpperIndex(object, idx, intersecting); removeFromArray(this._objects, object); this._objects.splice(newIdx, 0, object); } @@ -1661,15 +1659,15 @@ export class StaticCanvas extends createCollectionMixin( /** * @private */ - _findNewUpperIndex(object, idx, intersecting) { - var newIdx, i, len; + _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 (i = idx + 1, len = this._objects.length; i < len; ++i) { - var isIntersecting = + 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); @@ -1693,7 +1691,7 @@ export class StaticCanvas extends createCollectionMixin( * @return {fabric.Canvas} thisArg * @chainable */ - moveTo(object, index) { + moveTo(object: FabricObject, index: number) { removeFromArray(this._objects, object); this._objects.splice(index, 0, object); return this.renderOnAddRemove && this.requestRenderAll(); @@ -1754,7 +1752,8 @@ export class StaticCanvas extends createCollectionMixin( this._iTextInstances = null; this.contextContainer = null; const canvasElement = this.lowerCanvasEl; - delete this.lowerCanvasEl; + // @ts-ignore + this.lowerCanvasEl = undefined; // restore canvas style and attributes canvasElement.classList.remove('lower-canvas'); canvasElement.removeAttribute('data-fabric'); diff --git a/src/util/types.ts b/src/util/types.ts index f6f421034ab..f2caa7d5612 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -1,6 +1,6 @@ import type { ActiveSelection } from '../shapes/active_selection.class'; import type { Group } from '../shapes/group.class'; -import type { FabricObject } from '../shapes/object.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'; @@ -19,6 +19,10 @@ export const isCollection = ( 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 => { From 0df5c400490753e03482e15907a1699f233e9db6 Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Sun, 4 Dec 2022 16:55:52 +0100 Subject: [PATCH 10/19] ok broken UTs but running --- src/static_canvas.class.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/static_canvas.class.ts b/src/static_canvas.class.ts index 6e2caae2cca..97723d21e8e 100644 --- a/src/static_canvas.class.ts +++ b/src/static_canvas.class.ts @@ -38,15 +38,15 @@ export type TCanvasSizeOptions = { export type TSVGExportOptions = { suppressPreamble?: boolean; - viewBox: { + viewBox?: { x: number; y: number; width: number; height: number; }; - encoding: 'UTF-8'; // test Econding type and see what happens - width: string; - height: string; + encoding?: 'UTF-8'; // test Econding type and see what happens + width?: string; + height?: string; reviver?: TSVGReviver; }; @@ -1249,7 +1249,7 @@ export class StaticCanvas extends createCollectionMixin( * return svg.replace('stroke-dasharray: ; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; ', ''); * }); */ - toSVG(options: TSVGExportOptions, reviver: TSVGReviver) { + toSVG(options: TSVGExportOptions = {}, reviver: TSVGReviver) { options.reviver = reviver; const markup: string[] = []; @@ -1802,16 +1802,14 @@ Object.assign(StaticCanvas.prototype, { svgViewportTransformation: true, skipOffscreen: true, clipPath: undefined, -}); - -Object.assign(StaticCanvas.prototype, fabric.DataURLExporter); +}, fabric.DataURLExporter); if (fabric.isLikelyNode) { - fabric.StaticCanvas.prototype.createPNGStream = function () { + StaticCanvas.prototype.createPNGStream = function () { const impl = getNodeCanvas(this.lowerCanvasEl); return impl && impl.createPNGStream(); }; - fabric.StaticCanvas.prototype.createJPEGStream = function (opts: any) { + StaticCanvas.prototype.createJPEGStream = function (opts: any) { const impl = getNodeCanvas(this.lowerCanvasEl); return impl && impl.createJPEGStream(opts); }; From 93b2c4239a29f0cea55aaf66913ce511e5dc5533 Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Sun, 4 Dec 2022 16:58:38 +0100 Subject: [PATCH 11/19] changelog and prettier --- CHANGELOG.md | 1 + src/mixins/object_geometry.mixin.ts | 2 +- src/static_canvas.class.ts | 70 ++++++++++++++++++----------- src/util/types.ts | 12 +++-- 4 files changed, 55 insertions(+), 30 deletions(-) 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/mixins/object_geometry.mixin.ts b/src/mixins/object_geometry.mixin.ts index 8fc6134e21c..10d149a5156 100644 --- a/src/mixins/object_geometry.mixin.ts +++ b/src/mixins/object_geometry.mixin.ts @@ -294,7 +294,7 @@ export class ObjectGeometry< intersectsWithObject( other: ObjectGeometry, absolute = false, - calculate = false, + calculate = false ): boolean { const intersection = Intersection.intersectPolygonPolygon( this.getCoords(absolute, calculate), diff --git a/src/static_canvas.class.ts b/src/static_canvas.class.ts index 97723d21e8e..e14612e372f 100644 --- a/src/static_canvas.class.ts +++ b/src/static_canvas.class.ts @@ -21,7 +21,13 @@ import { createCanvasElement, isHTMLCanvas } from './util/misc/dom'; import { fabric } from '../HEADER'; import { invertTransform, transformPoint } from './util/misc/matrix'; import { TCachedFabricObject } from './shapes/object.class'; -import { isActiveSelection, isCollection, isFiller, isPattern, isTextObject } from './util/types'; +import { + isActiveSelection, + isCollection, + isFiller, + isPattern, + isTextObject, +} from './util/types'; import { Gradient } from './gradient'; import { Pattern } from './pattern.class'; import { toFixed } from './util/misc/toFixed'; @@ -1492,11 +1498,15 @@ export class StaticCanvas extends createCollectionMixin( finalHeight / 2 })" x="${filler.offsetX - finalWidth / 2}" y="${ filler.offsetY - finalHeight / 2 - }" width="${repeat === 'repeat-y' || repeat === 'no-repeat' - ? filler.source.width - : finalWidth}" height="${repeat === 'repeat-x' || repeat === 'no-repeat' - ? filler.source.height - : finalHeight}" fill="url(#SVGID_' + filler.id + ')">\n` + }" width="${ + repeat === 'repeat-y' || repeat === 'no-repeat' + ? filler.source.width + : finalWidth + }" height="${ + repeat === 'repeat-x' || repeat === 'no-repeat' + ? filler.source.height + : finalHeight + }" fill="url(#SVGID_' + filler.id + ')">\n` ); } else { markup.push( @@ -1601,7 +1611,11 @@ export class StaticCanvas extends createCollectionMixin( /** * @private */ - _findNewLowerIndex(object: FabricObject, idx: number, intersecting: boolean): number { + _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) { @@ -1784,25 +1798,29 @@ export class StaticCanvas extends createCollectionMixin( } } -Object.assign(StaticCanvas.prototype, { - backgroundColor: '', - backgroundImage: null, - overlayColor: '', - overlayImage: null, - includeDefaultValues: true, - stateful: false, - renderOnAddRemove: true, - controlsAboveOverlay: false, - allowTouchScrolling: false, - imageSmoothingEnabled: true, - viewportTransform: fabric.iMatrix.concat(), - backgroundVpt: true, - overlayVpt: true, - enableRetinaScaling: true, - svgViewportTransformation: true, - skipOffscreen: true, - clipPath: undefined, -}, fabric.DataURLExporter); +Object.assign( + StaticCanvas.prototype, + { + backgroundColor: '', + backgroundImage: null, + overlayColor: '', + overlayImage: null, + includeDefaultValues: true, + stateful: false, + renderOnAddRemove: true, + controlsAboveOverlay: false, + allowTouchScrolling: false, + imageSmoothingEnabled: true, + viewportTransform: fabric.iMatrix.concat(), + backgroundVpt: true, + overlayVpt: true, + enableRetinaScaling: true, + svgViewportTransformation: true, + skipOffscreen: true, + clipPath: undefined, + }, + fabric.DataURLExporter +); if (fabric.isLikelyNode) { StaticCanvas.prototype.createPNGStream = function () { diff --git a/src/util/types.ts b/src/util/types.ts index f2caa7d5612..59ff61ad04f 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -10,7 +10,11 @@ export const isFiller = (filler: TFiller | string): filler is TFiller => { }; export const isPattern = (filler: TFiller): filler is Pattern => { - return !!filler && (filler as Pattern).offsetX !== undefined && (filler as Pattern).source !== undefined; + return ( + !!filler && + (filler as Pattern).offsetX !== undefined && + (filler as Pattern).source !== undefined + ); }; export const isCollection = ( @@ -19,9 +23,11 @@ export const isCollection = ( return !!fabricObject && Array.isArray((fabricObject as Group)._objects); }; -export const isActiveSelection = (fabricObject: FabricObject): fabricObject is ActiveSelection => { +export const isActiveSelection = ( + fabricObject: FabricObject +): fabricObject is ActiveSelection => { return !!fabricObject && fabricObject.type === 'activeSelection'; -} +}; export const isTextObject = ( fabricObject: FabricObject From 26a91bf6990024143c2456f7af8981ccfec083bc Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Sun, 4 Dec 2022 17:45:54 +0100 Subject: [PATCH 12/19] some fixes --- src/static_canvas.class.ts | 13 +++++++++---- test/unit/canvas.js | 4 ++-- test/unit/canvas_static.js | 8 +++----- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/static_canvas.class.ts b/src/static_canvas.class.ts index e14612e372f..b7b37385c95 100644 --- a/src/static_canvas.class.ts +++ b/src/static_canvas.class.ts @@ -1029,8 +1029,10 @@ export class StaticCanvas extends createCollectionMixin( * @chainable */ viewportCenterObjectH(object: FabricObject) { - this._centerObject(object, this.getVpCenter()); - return this; + return this._centerObject( + object, + new Point(this.getVpCenter().x, object.getCenterPoint().y) + ); } /** @@ -1040,7 +1042,10 @@ export class StaticCanvas extends createCollectionMixin( * @chainable */ viewportCenterObjectV(object: FabricObject) { - return this._centerObject(object, this.getVpCenter()); + return this._centerObject( + object, + new Point(object.getCenterPoint().x, this.getVpCenter().y) + ); } /** @@ -1485,7 +1490,7 @@ export class StaticCanvas extends createCollectionMixin( if (!filler) { return; } - if (isPattern(filler)) { + if (isFiller(filler)) { const repeat = filler.repeat, finalWidth = this.width, finalHeight = this.height, diff --git a/test/unit/canvas.js b/test/unit/canvas.js index a5d678545b9..fab07775b9d 100644 --- a/test/unit/canvas.js +++ b/test/unit/canvas.js @@ -240,10 +240,10 @@ rect4 = makeRect(); assert.ok(typeof canvas.add === 'function'); - assert.equal(canvas.add(rect1), canvas, 'should be chainable'); assert.strictEqual(canvas.item(0), rect1); canvas.add(rect2, rect3, rect4); + assert.equal(canvas.add(rect1), 4, 'should return the new length of objects array'); assert.equal(canvas.getObjects().length, 4, 'should support multiple arguments'); assert.strictEqual(canvas.item(1), rect2); @@ -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), 1, '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..907c41f9bd2 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,6 @@ canvas.renderOnAddRemove = false; canvas.requestRenderAll = countRenderAll; - assert.equal(canvas.add(rect), canvas, 'should be chainable'); assert.equal(renderAllCount, 0); assert.equal(canvas.item(0), rect); @@ -434,7 +433,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), 1, 'should return the number of removed objects'); assert.strictEqual(canvas.item(0), rect2, 'should be second object'); canvas.remove(rect2, rect3); @@ -460,7 +459,6 @@ canvas.add(rect1, rect2); assert.equal(renderAllCount, 0); - assert.equal(canvas.remove(rect1), canvas, 'should be chainable'); assert.equal(renderAllCount, 0); assert.strictEqual(canvas.item(0), rect2, 'only second object should be left'); }); From c81b40c35b4939e2fea6fbefc607f65ce1181d32 Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Sun, 4 Dec 2022 19:01:54 +0100 Subject: [PATCH 13/19] some fixes --- src/static_canvas.class.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/static_canvas.class.ts b/src/static_canvas.class.ts index b7b37385c95..39e41169e49 100644 --- a/src/static_canvas.class.ts +++ b/src/static_canvas.class.ts @@ -1491,7 +1491,8 @@ export class StaticCanvas extends createCollectionMixin( return; } if (isFiller(filler)) { - const repeat = filler.repeat, + // @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'], @@ -1505,10 +1506,12 @@ export class StaticCanvas extends createCollectionMixin( filler.offsetY - finalHeight / 2 }" width="${ repeat === 'repeat-y' || repeat === 'no-repeat' + // @ts-ignore ? filler.source.width : finalWidth }" height="${ repeat === 'repeat-x' || repeat === 'no-repeat' + // @ts-ignore ? filler.source.height : finalHeight }" fill="url(#SVGID_' + filler.id + ')">\n` From e90c20fec10973dcdf1ddb8da146f48228abdec5 Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Sun, 4 Dec 2022 21:17:36 +0100 Subject: [PATCH 14/19] some fixes --- src/static_canvas.class.ts | 7 ++++--- test/unit/canvas.js | 4 ++-- test/unit/canvas_static.js | 12 ++++++------ 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/static_canvas.class.ts b/src/static_canvas.class.ts index 39e41169e49..27fc0fb1aaf 100644 --- a/src/static_canvas.class.ts +++ b/src/static_canvas.class.ts @@ -1500,7 +1500,7 @@ export class StaticCanvas extends createCollectionMixin( ? matrixToSVG(invertTransform(this.viewportTransform)) : ''; markup.push( - `\n` + : finalHeight + // @ts-ignore + }" fill="url(#SVGID_${filler.id})">\n` ); } else { markup.push( diff --git a/test/unit/canvas.js b/test/unit/canvas.js index fab07775b9d..e003eccdc91 100644 --- a/test/unit/canvas.js +++ b/test/unit/canvas.js @@ -240,10 +240,10 @@ rect4 = makeRect(); assert.ok(typeof canvas.add === 'function'); + 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); - assert.equal(canvas.add(rect1), 4, 'should return the new length of objects array'); assert.equal(canvas.getObjects().length, 4, 'should support multiple arguments'); assert.strictEqual(canvas.item(1), rect2); @@ -275,7 +275,7 @@ canvas.add(rect1, rect2, rect3, rect4); assert.ok(typeof canvas.remove === 'function'); - assert.equal(canvas.remove(rect1), 1, 'should return the number of objects removed'); + 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 907c41f9bd2..1ce4493fcca 100644 --- a/test/unit/canvas_static.js +++ b/test/unit/canvas_static.js @@ -332,7 +332,7 @@ canvas.requestRenderAll = countRenderAll; assert.equal(renderAllCount, 0); - + canvas.add(rect) assert.equal(canvas.item(0), rect); canvas.add(makeRect(), makeRect(), makeRect()); @@ -433,7 +433,7 @@ canvas.renderOnAddRemove = true; assert.ok(typeof canvas.remove === 'function'); assert.equal(renderAllCount, 0); - assert.equal(canvas.remove(rect1), 1, 'should return the number of removed objects'); + 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); @@ -458,7 +458,7 @@ canvas.add(rect1, rect2); assert.equal(renderAllCount, 0); - + 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'); }); @@ -872,7 +872,7 @@ return svg; } - canvas.toSVG(null, reviver); + canvas.toSVG(undefined, reviver); assert.equal(reviverCount, 14); canvas.renderOnAddRemove = true; @@ -911,7 +911,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; @@ -947,7 +947,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; }); From ffdaa888069fc1fa0e35d2e5ad2fdb50dd98f041 Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Sun, 4 Dec 2022 21:27:51 +0100 Subject: [PATCH 15/19] all passin --- src/static_canvas.class.ts | 16 ++++++---------- test/unit/canvas_static.js | 3 ++- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/static_canvas.class.ts b/src/static_canvas.class.ts index 27fc0fb1aaf..368500e2a19 100644 --- a/src/static_canvas.class.ts +++ b/src/static_canvas.class.ts @@ -1472,11 +1472,7 @@ export class StaticCanvas extends createCollectionMixin( reviver: TSVGReviver ) { const bgOrOverlay = this[property]; - if ( - bgOrOverlay !== null && - !bgOrOverlay.excludeFromExport && - bgOrOverlay.toSVG - ) { + if (bgOrOverlay && !bgOrOverlay.excludeFromExport && bgOrOverlay.toSVG) { markup.push(bgOrOverlay.toSVG(reviver)); } } @@ -1506,15 +1502,15 @@ export class StaticCanvas extends createCollectionMixin( filler.offsetY - finalHeight / 2 }" width="${ repeat === 'repeat-y' || repeat === 'no-repeat' - // @ts-ignore - ? filler.source.width + ? // @ts-ignore + filler.source.width : finalWidth }" height="${ repeat === 'repeat-x' || repeat === 'no-repeat' + ? // @ts-ignore + filler.source.height + : finalHeight // @ts-ignore - ? filler.source.height - : finalHeight - // @ts-ignore }" fill="url(#SVGID_${filler.id})">\n` ); } else { diff --git a/test/unit/canvas_static.js b/test/unit/canvas_static.js index 1ce4493fcca..79cf35fe153 100644 --- a/test/unit/canvas_static.js +++ b/test/unit/canvas_static.js @@ -331,8 +331,9 @@ canvas.renderOnAddRemove = false; canvas.requestRenderAll = countRenderAll; - assert.equal(renderAllCount, 0); canvas.add(rect) + assert.equal(renderAllCount, 0); + assert.equal(canvas.item(0), rect); canvas.add(makeRect(), makeRect(), makeRect()); From 276ca50ffaf9c6321f247d49a0ae1e7c3273409d Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Sun, 4 Dec 2022 23:36:19 +0200 Subject: [PATCH 16/19] template string --- src/static_canvas.class.ts | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/static_canvas.class.ts b/src/static_canvas.class.ts index 368500e2a19..32d8278a9f6 100644 --- a/src/static_canvas.class.ts +++ b/src/static_canvas.class.ts @@ -901,10 +901,10 @@ export class StaticCanvas extends createCollectionMixin( ctx: CanvasRenderingContext2D, property: 'background' | 'overlay' ) { - const fill: TFiller | string = this[property + 'Color'], - object: FabricObject = this[property + 'Image'], + const fill = this[`${property}Color`], + object = this[`${property}Image`], v = this.viewportTransform, - needsVpt = this[property + 'Vpt']; + needsVpt = this[`${property}Vpt`]; if (!fill && !object) { return; } @@ -1369,9 +1369,9 @@ export class StaticCanvas extends createCollectionMixin( createSVGRefElementsMarkup(): string { return ['background', 'overlay'] .map((prop) => { - const fill = this[prop + 'Color']; + const fill = this[`${prop}Color`]; if (isFiller(fill)) { - const shouldTransform = this[prop + 'Vpt'], + const shouldTransform = this[`${prop}Vpt`], vpt = this.viewportTransform, object = { width: this.width / (shouldTransform ? vpt[0] : 1), @@ -1482,7 +1482,7 @@ export class StaticCanvas extends createCollectionMixin( * @private */ _setSVGBgOverlayColor(markup: string[], property: 'background' | 'overlay') { - const filler = this[property + 'Color']; + const filler = this[`${property}Color`]; if (!filler) { return; } @@ -1491,7 +1491,7 @@ export class StaticCanvas extends createCollectionMixin( const repeat = filler.repeat || '', finalWidth = this.width, finalHeight = this.height, - shouldInvert = this[property + 'Vpt'], + shouldInvert = this[`${property}Vpt`], additionalTransform = shouldInvert ? matrixToSVG(invertTransform(this.viewportTransform)) : ''; @@ -1792,14 +1792,9 @@ export class StaticCanvas extends createCollectionMixin( * @return {String} string representation of an instance */ toString() { - return ( - '#' - ); + return `#`; } } From 608107438d0b26bd9fd50fa42e6453124e29658d Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Sun, 4 Dec 2022 23:39:14 +0200 Subject: [PATCH 17/19] jsdoc --- src/static_canvas.class.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/static_canvas.class.ts b/src/static_canvas.class.ts index 32d8278a9f6..98281f64fef 100644 --- a/src/static_canvas.class.ts +++ b/src/static_canvas.class.ts @@ -220,7 +220,7 @@ export class StaticCanvas extends createCollectionMixin( * 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 + * @type FabricObject */ clipPath: FabricObject; @@ -979,7 +979,7 @@ export class StaticCanvas extends createCollectionMixin( /** * Centers object horizontally in the canvas - * @param {fabric.Object} object Object to center horizontally + * @param {FabricObject} object Object to center horizontally * @return {fabric.Canvas} thisArg */ centerObjectH(object: FabricObject) { @@ -991,7 +991,7 @@ export class StaticCanvas extends createCollectionMixin( /** * Centers object vertically in the canvas - * @param {fabric.Object} object Object to center vertically + * @param {FabricObject} object Object to center vertically * @return {fabric.Canvas} thisArg * @chainable */ @@ -1004,7 +1004,7 @@ export class StaticCanvas extends createCollectionMixin( /** * Centers object vertically and horizontally in the canvas - * @param {fabric.Object} object Object to center vertically and horizontally + * @param {FabricObject} object Object to center vertically and horizontally * @return {fabric.Canvas} thisArg * @chainable */ @@ -1014,7 +1014,7 @@ export class StaticCanvas extends createCollectionMixin( /** * Centers object vertically and horizontally in the viewport - * @param {fabric.Object} object Object to center vertically and horizontally + * @param {FabricObject} object Object to center vertically and horizontally * @return {fabric.Canvas} thisArg * @chainable */ @@ -1024,7 +1024,7 @@ export class StaticCanvas extends createCollectionMixin( /** * Centers object horizontally in the viewport, object.top is unchanged - * @param {fabric.Object} object Object to center vertically and horizontally + * @param {FabricObject} object Object to center vertically and horizontally * @return {fabric.Canvas} thisArg * @chainable */ @@ -1037,7 +1037,7 @@ export class StaticCanvas extends createCollectionMixin( /** * Centers object Vertically in the viewport, object.top is unchanged - * @param {fabric.Object} object Object to center vertically and horizontally + * @param {FabricObject} object Object to center vertically and horizontally * @return {fabric.Canvas} thisArg * @chainable */ @@ -1062,7 +1062,7 @@ export class StaticCanvas extends createCollectionMixin( /** * @private - * @param {fabric.Object} object Object to center + * @param {FabricObject} object Object to center * @param {Point} center Center point * @return {fabric.Canvas} thisArg * @chainable @@ -1528,7 +1528,7 @@ export class StaticCanvas extends createCollectionMixin( /** * 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 + * @param {FabricObject} object Object to send to back * @return {fabric.Canvas} thisArg * @chainable */ @@ -1553,7 +1553,7 @@ export class StaticCanvas extends createCollectionMixin( /** * 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 + * @param {FabricObject} object Object to send * @return {fabric.Canvas} thisArg * @chainable */ @@ -1642,7 +1642,7 @@ export class StaticCanvas extends createCollectionMixin( * 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 {FabricObject} object Object to send * @param {Boolean} [intersecting] If `true`, send object in front of next upper intersecting object * @return {fabric.Canvas} thisArg * @chainable @@ -1705,7 +1705,7 @@ export class StaticCanvas extends createCollectionMixin( /** * Moves an object to specified level in stack of drawn objects - * @param {fabric.Object} object Object to send + * @param {FabricObject} object Object to send * @param {Number} index Position to move to * @return {fabric.Canvas} thisArg * @chainable From 33390bcaaba17bd3ce256260dc6e2c47d6359702 Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Sun, 4 Dec 2022 23:43:00 +0200 Subject: [PATCH 18/19] imports + more jsdoc --- src/static_canvas.class.ts | 58 +++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 32 deletions(-) diff --git a/src/static_canvas.class.ts b/src/static_canvas.class.ts index 98281f64fef..2c567571def 100644 --- a/src/static_canvas.class.ts +++ b/src/static_canvas.class.ts @@ -1,39 +1,42 @@ +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 { removeFromArray } from './util/internals'; -import { uid } from './util/internals/uid'; -import { pick } from './util/misc/pick'; +import { TCachedFabricObject } from './shapes/object.class'; +import { Rect } from './shapes/rect.class'; import type { + TCornerPoint, TFiller, TMat2D, - TCornerPoint, TSize, TValidToObjectMethod, } from './typedefs'; -import type { StaticCanvasEvents } from './EventTypeDefs'; -import { getElementOffset, getNodeCanvas } from './util/dom_misc'; +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 { fabric } from '../HEADER'; import { invertTransform, transformPoint } from './util/misc/matrix'; -import { TCachedFabricObject } from './shapes/object.class'; +import { pick } from './util/misc/pick'; +import { matrixToSVG } from './util/misc/svgParsing'; +import { toFixed } from './util/misc/toFixed'; import { isActiveSelection, isCollection, isFiller, - isPattern, isTextObject, } from './util/types'; -import { Gradient } from './gradient'; -import { Pattern } from './pattern.class'; -import { toFixed } from './util/misc/toFixed'; -import { matrixToSVG } from './util/misc/svgParsing'; -import { Rect } from './shapes/rect.class'; -import { TSVGReviver } from './mixins/object.svg_export'; const CANVAS_INIT_ERROR = 'Could not initialize `canvas` element'; @@ -58,10 +61,7 @@ export type TSVGExportOptions = { /** * 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 @@ -84,7 +84,7 @@ export class StaticCanvas extends createCollectionMixin( * 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 + * @type FabricObject * @default */ backgroundImage: FabricObject | null; @@ -102,7 +102,7 @@ export class StaticCanvas extends createCollectionMixin( * 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 + * @type FabricObject * @default */ overlayImage: FabricObject | null; @@ -125,7 +125,7 @@ export class StaticCanvas extends createCollectionMixin( /** * 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. + * {@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() ) @@ -199,7 +199,6 @@ export class StaticCanvas extends createCollectionMixin( * 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: TCornerPoint; @@ -209,7 +208,6 @@ export class StaticCanvas extends createCollectionMixin( * 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 */ @@ -662,8 +660,6 @@ export class StaticCanvas extends createCollectionMixin( /** * Pan viewport so as to place point at top left corner of canvas * @param {Point} point to move to - * @return {fabric.StaticCanvas} instance - * @chainable true */ absolutePan(point: Point) { const vpt: TMat2D = [...this.viewportTransform]; @@ -675,8 +671,6 @@ export class StaticCanvas extends createCollectionMixin( /** * Pans viewpoint relatively * @param {Point} point (position vector) to move by - * @return {fabric.StaticCanvas} instance - * @chainable true */ relativePan(point: Point) { return this.absolutePan( @@ -801,7 +795,7 @@ export class StaticCanvas extends createCollectionMixin( cancelRequestedRender() { if (this.nextRenderHandle) { - fabric.util.cancelAnimFrame(this.nextRenderHandle); + cancelAnimFrame(this.nextRenderHandle); this.nextRenderHandle = 0; } } @@ -1784,7 +1778,7 @@ export class StaticCanvas extends createCollectionMixin( // restore canvas size to original size in case retina scaling was applied canvasElement.setAttribute('width', `${this.width}`); canvasElement.setAttribute('height', `${this.height}`); - fabric.util.cleanUpJsdomNode(canvasElement); + cleanUpJsdomNode(canvasElement); } /** @@ -1811,7 +1805,7 @@ Object.assign( controlsAboveOverlay: false, allowTouchScrolling: false, imageSmoothingEnabled: true, - viewportTransform: fabric.iMatrix.concat(), + viewportTransform: iMatrix.concat(), backgroundVpt: true, overlayVpt: true, enableRetinaScaling: true, From 543b773fefc65f68ff1104f8644f72b642f31a66 Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Mon, 5 Dec 2022 10:06:29 +0100 Subject: [PATCH 19/19] added comment --- src/static_canvas.class.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/static_canvas.class.ts b/src/static_canvas.class.ts index 368500e2a19..67bc93e2e59 100644 --- a/src/static_canvas.class.ts +++ b/src/static_canvas.class.ts @@ -525,6 +525,7 @@ export class StaticCanvas extends createCollectionMixin( } }); + // @TODO: move to Canvas if (this._isCurrentlyDrawing) { this.freeDrawingBrush && this.freeDrawingBrush._setBrushStyles(this.contextTop);