diff --git a/CHANGELOG.md b/CHANGELOG.md index fe120745a54..f95ddf2530a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - brush doesn't add result to canvas anymore - `path:created`, `before:path:created` events are deprecated, use `interaction:completed` instead - `_render` method is now protected, use `render` instead +- chore(TS): Migrate smaller mixins to classes (dataurl and serialization ) [#8542](https://github.com/fabricjs/fabric.js/pull/8542) - chore(TS): Convert Canvas events mixin and grouping mixin [#8519](https://github.com/fabricjs/fabric.js/pull/8519) - chore(TS): Remove backward compatibility initialize methods [#8525](https://github.com/fabricjs/fabric.js/pull/8525/) - chore(TS): replace getKlass utility with a registry that doesn't require full fabricJS to work [#8500](https://github.com/fabricjs/fabric.js/pull/8500) diff --git a/index.js b/index.js index 629c5f401de..4c5fdf7a750 100644 --- a/index.js +++ b/index.js @@ -11,11 +11,8 @@ import './src/pattern.class'; // optional pattern import './src/shadow.class'; // optional shadow import './src/canvas/static_canvas.class'; import './src/canvas/canvas_events'; // optional interaction -import './src/mixins/canvas_dataurl_exporter.mixin'; -import './src/mixins/canvas_serialization.mixin'; // optional serialization import './src/canvas/canvas_gestures.mixin'; // optional gestures import './src/mixins/canvas_animation.mixin'; // optional animation -import './src/mixins/canvas_straightening.mixin'; // optional animation import './src/shapes/Object/FabricObject'; import './src/mixins/object_stacking.mixin'; // removed in #8461 import './src/mixins/stateful.mixin'; // will die soon diff --git a/src/canvas/canvas.class.ts b/src/canvas/canvas.class.ts index 0bf809f712c..9ffd9b19775 100644 --- a/src/canvas/canvas.class.ts +++ b/src/canvas/canvas.class.ts @@ -37,6 +37,7 @@ import type { Textbox } from '../shapes/textbox.class'; import { pick } from '../util/misc/pick'; import { TSVGReviver } from '../mixins/object.svg_export'; import { sendPointToPlane } from '../util/misc/planeChange'; +import { createCanvasElement } from '../util/misc/dom'; type TDestroyedCanvas = Omit< SelectableCanvas, @@ -1535,6 +1536,7 @@ export class SelectableCanvas< super.destroy(); wrapperEl.removeChild(upperCanvasEl); wrapperEl.removeChild(lowerCanvasEl); + this._iTextInstances = []; this.contextCache = null; this.contextTop = null; cleanUpJsdomNode(upperCanvasEl); diff --git a/src/canvas/canvas_events.ts b/src/canvas/canvas_events.ts index c593918a01c..3174a84abc3 100644 --- a/src/canvas/canvas_events.ts +++ b/src/canvas/canvas_events.ts @@ -14,6 +14,7 @@ import { ActiveSelection } from '../shapes/active_selection.class'; import { Group } from '../shapes/group.class'; import type { FabricObject } from '../shapes/Object/FabricObject'; import { stopEvent } from '../util/dom_event'; +import { createCanvasElement } from '../util/misc/dom'; import { sendPointToPlane } from '../util/misc/planeChange'; import { isActiveSelection, @@ -1640,6 +1641,19 @@ export class Canvas extends SelectableCanvas { // clear selection and current transformation this._groupSelector = null; } + + /** + * Clones canvas instance without cloning existing data. + * This essentially copies canvas dimensions since loadFromJSON does not affect canvas size. + * @returns {StaticCanvas} + */ + cloneWithoutData(): Canvas { + const el = createCanvasElement(); + el.width = this.width; + el.height = this.height; + // this seems wrong. either Canvas or StaticCanvas + return new Canvas(el); + } } // there is an order execution bug if i put this as public property. diff --git a/src/canvas/static_canvas.class.ts b/src/canvas/static_canvas.class.ts index 8812afe1ab6..a8254a0b853 100644 --- a/src/canvas/static_canvas.class.ts +++ b/src/canvas/static_canvas.class.ts @@ -12,11 +12,14 @@ import { Point } from '../point.class'; import type { FabricObject } from '../shapes/Object/FabricObject'; import { TCachedFabricObject } from '../shapes/Object/Object'; import { Rect } from '../shapes/rect.class'; -import type { +import { + ImageFormat, TCornerPoint, + TDataUrlOptions, TFiller, TMat2D, TSize, + TToCanvasElementOptions, TValidToObjectMethod, } from '../typedefs'; import { cancelAnimFrame, requestAnimFrame } from '../util/animate'; @@ -27,8 +30,13 @@ import { } from '../util/dom_misc'; import { removeFromArray } from '../util/internals'; import { uid } from '../util/internals/uid'; -import { createCanvasElement, isHTMLCanvas } from '../util/misc/dom'; +import { createCanvasElement, isHTMLCanvas, toDataURL } from '../util/misc/dom'; import { invertTransform, transformPoint } from '../util/misc/matrix'; +import { + enlivenObjectEnlivables, + EnlivenObjectOptions, + enlivenObjects, +} from '../util/misc/objectEnlive'; import { pick } from '../util/misc/pick'; import { matrixToSVG } from '../util/misc/svgParsing'; import { toFixed } from '../util/misc/toFixed'; @@ -60,6 +68,10 @@ export type TSVGExportOptions = { reviver?: TSVGReviver; }; +type TCanvasHydrationOption = { + signal?: AbortSignal; +}; + /** * Static canvas class * @see {@link http://fabricjs.com/static_canvas|StaticCanvas demo} @@ -273,7 +285,9 @@ export class StaticCanvas< _offset: { left: number; top: number }; protected hasLostContext: boolean; protected nextRenderHandle: number; - protected __cleanupTask: { + + // reference to + protected __cleanupTask?: { (): void; kill: (reason?: any) => void; }; @@ -456,7 +470,7 @@ export class StaticCanvas< } this.lowerCanvasEl.classList.add('lower-canvas'); this.lowerCanvasEl.setAttribute('data-fabric', 'main'); - this.contextContainer = this.lowerCanvasEl.getContext('2d'); + this.contextContainer = this.lowerCanvasEl.getContext('2d')!; } /** @@ -1071,7 +1085,7 @@ export class StaticCanvas< : null; return { version: VERSION, - ...pick(this, propertiesToInclude), + ...pick(this, propertiesToInclude as (keyof this)[]), objects: this._objects .filter((object) => !object.excludeFromExport) .map((instance) => @@ -1304,7 +1318,7 @@ export class StaticCanvas< * @return {String} */ createSVGRefElementsMarkup(): string { - return ['background', 'overlay'] + return (['background', 'overlay'] as const) .map((prop) => { const fill = this[`${prop}Color`]; if (isFiller(fill)) { @@ -1653,6 +1667,217 @@ export class StaticCanvas< return this.renderOnAddRemove && this.requestRenderAll(); } + /** + * Populates canvas with data from the specified JSON. + * JSON format must conform to the one of {@link fabric.Canvas#toJSON} + * + * **IMPORTANT**: It is recommended to abort loading tasks before calling this method to prevent race conditions and unnecessary networking + * + * @param {String|Object} json JSON string or object + * @param {Function} [reviver] Method for further parsing of JSON elements, called after each fabric object created. + * @param {Object} [options] options + * @param {AbortSignal} [options.signal] see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal + * @return {Promise} instance + * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#deserialization} + * @see {@link http://jsfiddle.net/fabricjs/fmgXt/|jsFiddle demo} + * @example loadFromJSON + * canvas.loadFromJSON(json).then((canvas) => canvas.requestRenderAll()); + * @example loadFromJSON with reviver + * canvas.loadFromJSON(json, function(o, object) { + * // `o` = json object + * // `object` = fabric.Object instance + * // ... do some stuff ... + * }).then((canvas) => { + * ... canvas is restored, add your code. + * }); + * + */ + loadFromJSON( + json: string | Record, + reviver?: EnlivenObjectOptions['reviver'], + { signal }: TCanvasHydrationOption = {} + ): Promise { + if (!json) { + return Promise.reject(new Error('fabric.js: `json` is undefined')); + } + + // parse json if it wasn't already + const serialized = typeof json === 'string' ? JSON.parse(json) : json; + const { + objects = [], + backgroundImage, + background, + overlayImage, + overlay, + clipPath, + } = serialized; + const renderOnAddRemove = this.renderOnAddRemove; + this.renderOnAddRemove = false; + + return Promise.all([ + enlivenObjects(objects, { + reviver, + signal, + }), + enlivenObjectEnlivables( + { + backgroundImage, + backgroundColor: background, + overlayImage, + overlayColor: overlay, + clipPath, + }, + { signal } + ), + ]).then(([enlived, enlivedMap]) => { + this.clear(); + this.add(...enlived); + this.set(serialized); + this.set(enlivedMap); + this.renderOnAddRemove = renderOnAddRemove; + return this; + }); + } + + /** + * Clones canvas instance + * @param {string[]} [properties] Array of properties to include in the cloned canvas and children + * @returns {Promise} + */ + clone(properties: string[]): Promise { + const data = this.toObject(properties); + const canvas = this.cloneWithoutData(); + // @ts-ignore + return canvas.loadFromJSON(data); + } + + /** + * Clones canvas instance without cloning existing data. + * This essentially copies canvas dimensions since loadFromJSON does not affect canvas size. + * @returns {StaticCanvas} + */ + cloneWithoutData(): StaticCanvas { + const el = createCanvasElement(); + el.width = this.width; + el.height = this.height; + // this seems wrong. either Canvas or StaticCanvas + return new StaticCanvas(el); + } + + /** + * Exports canvas element to a dataurl image. Note that when multiplier is used, cropping is scaled appropriately + * @param {Object} [options] Options object + * @param {String} [options.format=png] The format of the output image. Either "jpeg" or "png" + * @param {Number} [options.quality=1] Quality level (0..1). Only used for jpeg. + * @param {Number} [options.multiplier=1] Multiplier to scale by, to have consistent + * @param {Number} [options.left] Cropping left offset. Introduced in v1.2.14 + * @param {Number} [options.top] Cropping top offset. Introduced in v1.2.14 + * @param {Number} [options.width] Cropping width. Introduced in v1.2.14 + * @param {Number} [options.height] Cropping height. Introduced in v1.2.14 + * @param {Boolean} [options.enableRetinaScaling] Enable retina scaling for clone image. Introduce in 2.0.0 + * @param {(object: fabric.Object) => boolean} [options.filter] Function to filter objects. + * @return {String} Returns a data: URL containing a representation of the object in the format specified by options.format + * @see {@link https://jsfiddle.net/xsjua1rd/ demo} + * @example Generate jpeg dataURL with lower quality + * var dataURL = canvas.toDataURL({ + * format: 'jpeg', + * quality: 0.8 + * }); + * @example Generate cropped png dataURL (clipping of canvas) + * var dataURL = canvas.toDataURL({ + * format: 'png', + * left: 100, + * top: 100, + * width: 200, + * height: 200 + * }); + * @example Generate double scaled png dataURL + * var dataURL = canvas.toDataURL({ + * format: 'png', + * multiplier: 2 + * }); + * @example Generate dataURL with objects that overlap a specified object + * var myObject; + * var dataURL = canvas.toDataURL({ + * filter: (object) => object.isContainedWithinObject(myObject) || object.intersectsWithObject(myObject) + * }); + */ + toDataURL(options = {} as TDataUrlOptions): string { + const { + format = ImageFormat.png, + quality = 1, + multiplier = 1, + enableRetinaScaling = false, + } = options; + const finalMultiplier = + multiplier * (enableRetinaScaling ? this.getRetinaScaling() : 1); + + return toDataURL( + this.toCanvasElement(finalMultiplier, options), + format, + quality + ); + } + + /** + * Create a new HTMLCanvas element painted with the current canvas content. + * No need to resize the actual one or repaint it. + * Will transfer object ownership to a new canvas, paint it, and set everything back. + * This is an intermediary step used to get to a dataUrl but also it is useful to + * create quick image copies of a canvas without passing for the dataUrl string + * @param {Number} [multiplier] a zoom factor. + * @param {Object} [options] Cropping informations + * @param {Number} [options.left] Cropping left offset. + * @param {Number} [options.top] Cropping top offset. + * @param {Number} [options.width] Cropping width. + * @param {Number} [options.height] Cropping height. + * @param {(object: fabric.Object) => boolean} [options.filter] Function to filter objects. + */ + toCanvasElement( + multiplier = 1, + { width, height, left, top, filter } = {} as TToCanvasElementOptions + ): HTMLCanvasElement { + const scaledWidth = (width || this.width) * multiplier, + scaledHeight = (height || this.height) * multiplier, + zoom = this.getZoom(), + originalWidth = this.width, + originalHeight = this.height, + newZoom = zoom * multiplier, + vp = this.viewportTransform, + translateX = (vp[4] - (left || 0)) * multiplier, + translateY = (vp[5] - (top || 0)) * multiplier, + // @ts-ignore + originalInteractive = this.interactive, + newVp = [newZoom, 0, 0, newZoom, translateX, translateY] as TMat2D, + originalRetina = this.enableRetinaScaling, + canvasEl = createCanvasElement(), + // @ts-ignore + originalContextTop = this.contextTop, + objectsToRender = filter ? this._objects.filter(filter) : this._objects; + canvasEl.width = scaledWidth; + canvasEl.height = scaledHeight; + // @ts-ignore + this.contextTop = null; + this.enableRetinaScaling = false; + // @ts-ignore + this.interactive = false; + this.viewportTransform = newVp; + this.width = scaledWidth; + this.height = scaledHeight; + this.calcViewportBoundaries(); + this.renderCanvas(canvasEl.getContext('2d')!, objectsToRender); + this.viewportTransform = vp; + this.width = originalWidth; + this.height = originalHeight; + this.calcViewportBoundaries(); + // @ts-ignore + this.interactive = originalInteractive; + this.enableRetinaScaling = originalRetina; + // @ts-ignore + this.contextTop = originalContextTop; + return canvasEl; + } + /** * 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 @@ -1705,7 +1930,6 @@ export class StaticCanvas< this.overlayImage.dispose(); } this.overlayImage = null; - this._iTextInstances = null; // @ts-expect-error disposing this.contextContainer = null; const canvasElement = this.lowerCanvasEl; @@ -1733,39 +1957,41 @@ export class StaticCanvas< } } -Object.assign( - StaticCanvas.prototype, - { - backgroundColor: '', - backgroundImage: null, - overlayColor: '', - overlayImage: null, - includeDefaultValues: true, - stateful: false, - renderOnAddRemove: true, - controlsAboveOverlay: false, - allowTouchScrolling: false, - imageSmoothingEnabled: true, - viewportTransform: iMatrix.concat(), - backgroundVpt: true, - overlayVpt: true, - enableRetinaScaling: true, - svgViewportTransformation: true, - skipOffscreen: true, - clipPath: undefined, - }, - fabric.DataURLExporter -); +Object.assign(StaticCanvas.prototype, { + backgroundColor: '', + backgroundImage: null, + overlayColor: '', + overlayImage: null, + includeDefaultValues: true, + stateful: false, + renderOnAddRemove: true, + controlsAboveOverlay: false, + allowTouchScrolling: false, + imageSmoothingEnabled: true, + viewportTransform: iMatrix.concat(), + backgroundVpt: true, + overlayVpt: true, + enableRetinaScaling: true, + svgViewportTransformation: true, + skipOffscreen: true, + clipPath: undefined, +}); if (fabric.isLikelyNode) { - StaticCanvas.prototype.createPNGStream = function () { - const impl = getNodeCanvas(this.lowerCanvasEl); - return impl && impl.createPNGStream(); - }; - StaticCanvas.prototype.createJPEGStream = function (opts: any) { - const impl = getNodeCanvas(this.lowerCanvasEl); - return impl && impl.createJPEGStream(opts); - }; + Object.assign(StaticCanvas.prototype, { + createPNGStream() { + const impl = getNodeCanvas( + (this as unknown as StaticCanvas).lowerCanvasEl + ); + return impl && impl.createPNGStream(); + }, + createJPEGStream(opts: any) { + const impl = getNodeCanvas( + (this as unknown as StaticCanvas).lowerCanvasEl + ); + return impl && impl.createJPEGStream(opts); + }, + }); } fabric.StaticCanvas = StaticCanvas; diff --git a/src/mixins/canvas_animation.mixin.ts b/src/mixins/canvas_animation.mixin.ts index 305e2c0f0b4..182eda76b50 100644 --- a/src/mixins/canvas_animation.mixin.ts +++ b/src/mixins/canvas_animation.mixin.ts @@ -88,7 +88,6 @@ fabric.util.object.extend( */ fxRemove: function (object, callbacks) { callbacks = callbacks || {}; - var empty = function () {}, onComplete = callbacks.onComplete || empty, onChange = callbacks.onChange || empty, @@ -110,5 +109,15 @@ fabric.util.object.extend( }, }); }, + + /** + * @param {fabric.Object} object Object to straighten + * @return {fabric.Canvas} thisArg + */ + fxStraightenObject: function (object: FabricObject) { + return object.fxStraighten({ + onChange: this.requestRenderAllBound, + }); + }, } ); diff --git a/src/mixins/canvas_dataurl_exporter.mixin.ts b/src/mixins/canvas_dataurl_exporter.mixin.ts deleted file mode 100644 index 14ba493d513..00000000000 --- a/src/mixins/canvas_dataurl_exporter.mixin.ts +++ /dev/null @@ -1,112 +0,0 @@ -//@ts-nocheck -(function (global) { - var fabric = global.fabric; - fabric.util.object.extend( - fabric.StaticCanvas.prototype, - /** @lends fabric.StaticCanvas.prototype */ { - /** - * Exports canvas element to a dataurl image. Note that when multiplier is used, cropping is scaled appropriately - * @param {Object} [options] Options object - * @param {String} [options.format=png] The format of the output image. Either "jpeg" or "png" - * @param {Number} [options.quality=1] Quality level (0..1). Only used for jpeg. - * @param {Number} [options.multiplier=1] Multiplier to scale by, to have consistent - * @param {Number} [options.left] Cropping left offset. Introduced in v1.2.14 - * @param {Number} [options.top] Cropping top offset. Introduced in v1.2.14 - * @param {Number} [options.width] Cropping width. Introduced in v1.2.14 - * @param {Number} [options.height] Cropping height. Introduced in v1.2.14 - * @param {Boolean} [options.enableRetinaScaling] Enable retina scaling for clone image. Introduce in 2.0.0 - * @param {(object: fabric.Object) => boolean} [options.filter] Function to filter objects. - * @return {String} Returns a data: URL containing a representation of the object in the format specified by options.format - * @see {@link https://jsfiddle.net/xsjua1rd/ demo} - * @example Generate jpeg dataURL with lower quality - * var dataURL = canvas.toDataURL({ - * format: 'jpeg', - * quality: 0.8 - * }); - * @example Generate cropped png dataURL (clipping of canvas) - * var dataURL = canvas.toDataURL({ - * format: 'png', - * left: 100, - * top: 100, - * width: 200, - * height: 200 - * }); - * @example Generate double scaled png dataURL - * var dataURL = canvas.toDataURL({ - * format: 'png', - * multiplier: 2 - * }); - * @example Generate dataURL with objects that overlap a specified object - * var myObject; - * var dataURL = canvas.toDataURL({ - * filter: (object) => object.isContainedWithinObject(myObject) || object.intersectsWithObject(myObject) - * }); - */ - toDataURL: function (options) { - options || (options = {}); - - var format = options.format || 'png', - quality = options.quality || 1, - multiplier = - (options.multiplier || 1) * - (options.enableRetinaScaling ? this.getRetinaScaling() : 1), - canvasEl = this.toCanvasElement(multiplier, options); - return fabric.util.toDataURL(canvasEl, format, quality); - }, - - /** - * Create a new HTMLCanvas element painted with the current canvas content. - * No need to resize the actual one or repaint it. - * Will transfer object ownership to a new canvas, paint it, and set everything back. - * This is an intermediary step used to get to a dataUrl but also it is useful to - * create quick image copies of a canvas without passing for the dataUrl string - * @param {Number} [multiplier] a zoom factor. - * @param {Object} [options] Cropping informations - * @param {Number} [options.left] Cropping left offset. - * @param {Number} [options.top] Cropping top offset. - * @param {Number} [options.width] Cropping width. - * @param {Number} [options.height] Cropping height. - * @param {(object: fabric.Object) => boolean} [options.filter] Function to filter objects. - */ - toCanvasElement: function (multiplier, options) { - multiplier = multiplier || 1; - options = options || {}; - var scaledWidth = (options.width || this.width) * multiplier, - scaledHeight = (options.height || this.height) * multiplier, - zoom = this.getZoom(), - originalWidth = this.width, - originalHeight = this.height, - newZoom = zoom * multiplier, - vp = this.viewportTransform, - translateX = (vp[4] - (options.left || 0)) * multiplier, - translateY = (vp[5] - (options.top || 0)) * multiplier, - originalInteractive = this.interactive, - newVp = [newZoom, 0, 0, newZoom, translateX, translateY], - originalRetina = this.enableRetinaScaling, - canvasEl = fabric.util.createCanvasElement(), - originalContextTop = this.contextTop, - objectsToRender = options.filter - ? this._objects.filter(options.filter) - : this._objects; - canvasEl.width = scaledWidth; - canvasEl.height = scaledHeight; - this.contextTop = null; - this.enableRetinaScaling = false; - this.interactive = false; - this.viewportTransform = newVp; - this.width = scaledWidth; - this.height = scaledHeight; - this.calcViewportBoundaries(); - this.renderCanvas(canvasEl.getContext('2d'), objectsToRender); - this.viewportTransform = vp; - this.width = originalWidth; - this.height = originalHeight; - this.calcViewportBoundaries(); - this.interactive = originalInteractive; - this.enableRetinaScaling = originalRetina; - this.contextTop = originalContextTop; - return canvasEl; - }, - } - ); -})(typeof exports !== 'undefined' ? exports : window); diff --git a/src/mixins/canvas_serialization.mixin.ts b/src/mixins/canvas_serialization.mixin.ts deleted file mode 100644 index df0f6294702..00000000000 --- a/src/mixins/canvas_serialization.mixin.ts +++ /dev/null @@ -1,133 +0,0 @@ -//@ts-nocheck -(function (global) { - var fabric = global.fabric; - fabric.util.object.extend( - fabric.StaticCanvas.prototype, - /** @lends fabric.StaticCanvas.prototype */ { - /** - * Populates canvas with data from the specified JSON. - * JSON format must conform to the one of {@link fabric.Canvas#toJSON} - * - * **IMPORTANT**: It is recommended to abort loading tasks before calling this method to prevent race conditions and unnecessary networking - * - * @param {String|Object} json JSON string or object - * @param {Function} [reviver] Method for further parsing of JSON elements, called after each fabric object created. - * @param {Object} [options] options - * @param {AbortSignal} [options.signal] see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal - * @return {Promise} instance - * @tutorial {@link http://fabricjs.com/fabric-intro-part-3#deserialization} - * @see {@link http://jsfiddle.net/fabricjs/fmgXt/|jsFiddle demo} - * @example loadFromJSON - * canvas.loadFromJSON(json).then((canvas) => canvas.requestRenderAll()); - * @example loadFromJSON with reviver - * canvas.loadFromJSON(json, function(o, object) { - * // `o` = json object - * // `object` = fabric.Object instance - * // ... do some stuff ... - * }).then((canvas) => { - * ... canvas is restored, add your code. - * }); - * - */ - loadFromJSON: function (json, reviver, options) { - if (!json) { - return Promise.reject(new Error('fabric.js: `json` is undefined')); - } - - // serialize if it wasn't already - var serialized = - typeof json === 'string' ? JSON.parse(json) : Object.assign({}, json); - - var _this = this, - renderOnAddRemove = this.renderOnAddRemove; - this.renderOnAddRemove = false; - - return Promise.all([ - fabric.util.enlivenObjects(serialized.objects || [], { - reviver: reviver, - signal: options && options.signal, - }), - fabric.util.enlivenObjectEnlivables( - { - backgroundImage: serialized.backgroundImage, - backgroundColor: serialized.background, - overlayImage: serialized.overlayImage, - overlayColor: serialized.overlay, - clipPath: serialized.clipPath, - }, - { signal: options && options.signal } - ), - ]).then(function (res) { - var enlived = res[0], - enlivedMap = res[1]; - _this.clear(); - _this.__setupCanvas(serialized, enlived); - _this.renderOnAddRemove = renderOnAddRemove; - _this.set(enlivedMap); - return _this; - }); - }, - - /** - * @private - * @param {Object} serialized Object with background and overlay information - * @param {Array} enlivenedObjects canvas objects - */ - __setupCanvas: function (serialized, enlivenedObjects) { - enlivenedObjects.forEach((obj, index) => { - // we splice the array just in case some custom classes restored from JSON - // will add more object to canvas at canvas init. - this.insertAt(index, obj); - }); - // remove parts i cannot set as options - delete serialized.objects; - delete serialized.backgroundImage; - delete serialized.overlayImage; - delete serialized.background; - delete serialized.overlay; - // this._initOptions does too many things to just - // call it. Normally loading an Object from JSON - // create the Object instance. Here the Canvas is - // already an instance and we are just loading things over it - this._setOptions(serialized); - }, - - /** - * Clones canvas instance - * @param {Array} [properties] Array of properties to include in the cloned canvas and children - * @returns {Promise} - */ - clone: function (properties) { - var data = JSON.stringify(this.toJSON(properties)); - return this.cloneWithoutData().then(function (clone) { - return clone.loadFromJSON(data); - }); - }, - - /** - * Clones canvas instance without cloning existing data. - * This essentially copies canvas dimensions, clipping properties, etc. - * but leaves data empty (so that you can populate it with your own) - * @returns {Promise} - */ - cloneWithoutData: function () { - var el = fabric.util.createCanvasElement(); - - el.width = this.width; - el.height = this.height; - // this seems wrong. either Canvas or StaticCanvas - var clone = new fabric.Canvas(el); - var data = {}; - if (this.backgroundImage) { - data.backgroundImage = this.backgroundImage.toObject(); - } - if (this.backgroundColor) { - data.background = this.backgroundColor.toObject - ? this.backgroundColor.toObject() - : this.backgroundColor; - } - return clone.loadFromJSON(data); - }, - } - ); -})(typeof exports !== 'undefined' ? exports : window); diff --git a/src/mixins/canvas_straightening.mixin.ts b/src/mixins/canvas_straightening.mixin.ts deleted file mode 100644 index 2114664cff5..00000000000 --- a/src/mixins/canvas_straightening.mixin.ts +++ /dev/null @@ -1,31 +0,0 @@ -// @ts-nocheck -import { fabric } from '../../HEADER'; -import type { FabricObject } from '../shapes/Object/FabricObject'; - -fabric.util.object.extend( - fabric.StaticCanvas.prototype, - /** @lends fabric.StaticCanvas.prototype */ { - /** - * Straightens object, then rerenders canvas - * @param {fabric.Object} object Object to straighten - * @return {fabric.Canvas} thisArg - * @chainable - */ - straightenObject: function (object: FabricObject) { - object.straighten(); - this.requestRenderAll(); - return this; - }, - - /** - * Same as {@link fabric.Canvas.prototype.straightenObject}, but animated - * @param {fabric.Object} object Object to straighten - * @return {fabric.Canvas} thisArg - */ - fxStraightenObject: function (object: FabricObject) { - return object.fxStraighten({ - onChange: this.requestRenderAllBound, - }); - }, - } -); diff --git a/src/typedefs.ts b/src/typedefs.ts index ba27c9aed13..83989a543c5 100644 --- a/src/typedefs.ts +++ b/src/typedefs.ts @@ -2,6 +2,7 @@ import type { Gradient } from './gradient/gradient.class'; import type { Pattern } from './pattern.class'; import type { Point } from './point.class'; +import type { FabricObject } from './shapes/Object/FabricObject'; interface NominalTag { nominalTag?: T; @@ -96,3 +97,18 @@ export type TCacheCanvasDimensions = { x: number; y: number; }; + +export type TToCanvasElementOptions = { + left?: number; + top?: number; + width?: number; + height?: number; + filter?: (object: FabricObject) => boolean; +}; + +export type TDataUrlOptions = TToCanvasElementOptions & { + multiplier: number; + format?: ImageFormat; + quality?: number; + enableRetinaScaling?: boolean; +}; diff --git a/src/util/misc/objectEnlive.ts b/src/util/misc/objectEnlive.ts index 5286767002e..f02cdbaa5c2 100644 --- a/src/util/misc/objectEnlive.ts +++ b/src/util/misc/objectEnlive.ts @@ -59,7 +59,7 @@ export const loadImage = ( img.src = url; }); -type EnlivenObjectOptions = { +export type EnlivenObjectOptions = { /** * handle aborting, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal */ diff --git a/test/unit/canvas.js b/test/unit/canvas.js index bdc25ef1e86..e569d5c2d58 100644 --- a/test/unit/canvas.js +++ b/test/unit/canvas.js @@ -1334,22 +1334,6 @@ assert.equal(rect.getCenterPoint().x, upperCanvasEl.width / 2, 'object\'s "left" property should correspond to canvas element\'s center'); }); - QUnit.test('straightenObject', function(assert) { - assert.ok(typeof canvas.straightenObject === 'function'); - var rect = makeRect({ angle: 10 }); - canvas.add(rect); - canvas.straightenObject(rect); - assert.equal(rect.get('angle'), 0, 'angle should be coerced to 0 (from 10)'); - - rect.rotate('60'); - canvas.straightenObject(rect); - assert.equal(rect.get('angle'), 90, 'angle should be coerced to 90 (from 60)'); - - rect.rotate('100'); - canvas.straightenObject(rect); - assert.equal(rect.get('angle'), 90, 'angle should be coerced to 90 (from 100)'); - }); - QUnit.test('toJSON', function(assert) { assert.ok(typeof canvas.toJSON === 'function'); assert.equal(JSON.stringify(canvas.toJSON()), EMPTY_JSON); @@ -2126,22 +2110,19 @@ }); QUnit.test('cloneWithoutData', function(assert) { - var done = assert.async(); assert.ok(typeof canvas.cloneWithoutData === 'function'); canvas.add(new fabric.Rect({ width: 100, height: 110, top: 120, left: 130, fill: 'rgba(0,1,2,0.3)' })); - canvas.cloneWithoutData().then(function(clone) { + const clone = canvas.cloneWithoutData(); - assert.ok(clone instanceof fabric.Canvas); + assert.ok(clone instanceof fabric.Canvas); - assert.equal(JSON.stringify(clone), EMPTY_JSON, 'data on cloned canvas should be empty'); + assert.equal(JSON.stringify(clone), EMPTY_JSON, 'data on cloned canvas should be empty'); - assert.equal(canvas.getWidth(), clone.getWidth()); - assert.equal(canvas.getHeight(), clone.getHeight()); - clone.renderAll(); - done(); - }); + assert.equal(canvas.getWidth(), clone.getWidth()); + assert.equal(canvas.getHeight(), clone.getHeight()); + clone.renderAll(); }); QUnit.test('getSetWidth', function(assert) { diff --git a/test/unit/canvas_static.js b/test/unit/canvas_static.js index 966e1486cf4..c780c0ef6db 100644 --- a/test/unit/canvas_static.js +++ b/test/unit/canvas_static.js @@ -794,22 +794,6 @@ canvas.viewportTransform = [1, 0, 0, 1, 0, 0]; }); - QUnit.test('straightenObject', function(assert) { - assert.ok(typeof canvas.straightenObject === 'function'); - var rect = makeRect({ angle: 10 }); - canvas.add(rect); - canvas.straightenObject(rect); - assert.equal(rect.get('angle'), 0, 'angle should be coerced to 0 (from 10)'); - - rect.rotate('60'); - canvas.straightenObject(rect); - assert.equal(rect.get('angle'), 90, 'angle should be coerced to 90 (from 60)'); - - rect.rotate('100'); - canvas.straightenObject(rect); - assert.equal(rect.get('angle'), 90, 'angle should be coerced to 90 (from 100)'); - }); - QUnit.test('toSVG', function(assert) { assert.ok(typeof canvas.toSVG === 'function'); canvas.clear(); @@ -1594,7 +1578,10 @@ var rect = new fabric.Rect(); canvas2.add(rect); canvas2.clone().then(function(cloned) { - assert.ok(cloned instanceof fabric.Canvas, 'is cloned in a Canvas, sad but true'); + assert.ok(cloned instanceof fabric.StaticCanvas, 'is cloned in a StaticCanvas'); + // check regression + assert.notOk(cloned instanceof fabric.Canvas, 'is not cloned in a Canvas'); + var clonedRect = cloned.getObjects()[0]; assert.equal(clonedRect.type, 'rect', 'the rect has been cloned too'); assert.equal(clonedRect.width, rect.width, 'the rect has been cloned too with properties'); @@ -1604,20 +1591,17 @@ }); QUnit.test('cloneWithoutData', function(assert) { - var done = assert.async(); var canvas2 = new fabric.StaticCanvas(null, { renderOnAddRemove: false, width: 10, height: 10 }); assert.ok(typeof canvas.clone === 'function'); var rect = new fabric.Rect(); canvas2.add(rect); canvas2.backgroundColor = 'red'; - canvas2.cloneWithoutData().then(function(cloned) { - assert.ok(cloned instanceof fabric.Canvas, 'is cloned in a Canvas, sad but true'); - var clonedObjects = cloned.getObjects(); - assert.equal(clonedObjects.length, 0, 'no cloend objects'); - assert.equal(cloned.width, canvas2.width, 'the canvas has been cloned with properties'); - assert.equal(cloned.backgroundColor, 'red', 'background color has been cloned'); - done(); - }); + const cloned = canvas2.cloneWithoutData() + assert.ok(cloned instanceof fabric.StaticCanvas, 'is cloned in a StaticCanvas'); + var clonedObjects = cloned.getObjects(); + assert.equal(clonedObjects.length, 0, 'no cloend objects'); + assert.equal(cloned.width, canvas2.width, 'the canvas has been cloned with properties'); + assert.notEqual(cloned.backgroundColor, 'red', 'background color has not been cloned'); }); QUnit.test('getSetWidth', function(assert) {