diff --git a/scripts/transform_files.js b/scripts/transform_files.js index eb62528df65..b82e793b27d 100644 --- a/scripts/transform_files.js +++ b/scripts/transform_files.js @@ -121,7 +121,7 @@ function findClassBase(raw, regex) { const rawObject = findObject(raw, '{', '}', result.index); const NS = namespace.slice(0, namespace.lastIndexOf('.')); const { fabric } = require(wd); - const klass = fabric.util.resolveNamespace(NS === 'fabric' ? null : NS)[name]; + // const klass = fabric.util.resolveNamespace(NS === 'fabric' ? null : NS)[name]; return { name, namespace, @@ -133,7 +133,7 @@ function findClassBase(raw, regex) { value: match }, ...rawObject, - klass + // klass }; } diff --git a/src/__types__.ts b/src/__types__.ts index bb693c3bc60..77f9b5f7a5b 100644 --- a/src/__types__.ts +++ b/src/__types__.ts @@ -3,4 +3,5 @@ */ export type Shadow = any; export type Canvas = any; -export type Rect = any; \ No newline at end of file +export type Rect = any; +export type TObject = any; \ No newline at end of file diff --git a/src/pattern.class.ts b/src/pattern.class.ts index 621b6df817b..5022a683720 100644 --- a/src/pattern.class.ts +++ b/src/pattern.class.ts @@ -1,196 +1,188 @@ //@ts-nocheck - -import { pick } from "./util/misc/pick"; +import { fabric } from "../HEADER"; import { config } from "./config"; +import { TCrossOrigin, TMat2D, TSize } from "./typedefs"; +import { ifNaN } from "./util/internals"; +import { loadImage } from "./util/misc/objectEnlive"; +import { pick } from "./util/misc/pick"; +import { toFixed } from "./util/misc/toFixed"; +export type TPatternRepeat = 'repeat' | 'repeat-x' | 'repeat-y' | 'no-repeat'; -(function(global) { - var fabric = global.fabric, toFixed = fabric.util.toFixed; +type TExportedKeys = 'crossOrigin' | 'offsetX' | 'offsetY' | 'patternTransform' | 'repeat' | 'source'; +export type TPatternOptions = Partial>; + +export type TPatternSerialized = TPatternOptions & { + source: string; +}; + +export type TPatternHydrationOptions = { /** - * Pattern class - * @class fabric.Pattern - * @see {@link http://fabricjs.com/patterns|Pattern demo} - * @see {@link http://fabricjs.com/dynamic-patterns|DynamicPattern demo} - * @see {@link fabric.Pattern#initialize} for constructor definition + * handle aborting + * @see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal */ + signal: AbortSignal +} +type TImageSource = { source: HTMLImageElement }; +type TCanvasSource = { source: HTMLCanvasElement }; - fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ { - - /** - * Repeat property of a pattern (one of repeat, repeat-x, repeat-y or no-repeat) - * @type String - * @default - */ - repeat: 'repeat', - - /** - * Pattern horizontal offset from object's left/top corner - * @type Number - * @default - */ - offsetX: 0, - - /** - * Pattern vertical offset from object's left/top corner - * @type Number - * @default - */ - offsetY: 0, - - /** - * crossOrigin value (one of "", "anonymous", "use-credentials") - * @see https://developer.mozilla.org/en-US/docs/HTML/CORS_settings_attributes - * @type String - * @default - */ - crossOrigin: '', - - /** - * transform matrix to change the pattern, imported from svgs. - * @type Array - * @default - */ - patternTransform: null, - - type: 'pattern', - - /** - * Constructor - * @param {Object} [options] Options object - * @param {option.source} [source] the pattern source, eventually empty or a drawable - * @return {fabric.Pattern} thisArg - */ - initialize: function(options) { - options || (options = { }); - this.id = fabric.Object.__uid++; - this.setOptions(options); - }, - - /** - * Returns object representation of a 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: function(propertiesToInclude) { - var NUM_FRACTION_DIGITS = config.NUM_FRACTION_DIGITS, - source, object; - - // element - if (typeof this.source.src === 'string') { - source = this.source.src; - } - // element - else if (typeof this.source === 'object' && this.source.toDataURL) { - source = this.source.toDataURL(); - } - - return { - ...pick(this, propertiesToInclude), - type: 'pattern', - source: source, - repeat: this.repeat, - crossOrigin: this.crossOrigin, - offsetX: toFixed(this.offsetX, NUM_FRACTION_DIGITS), - offsetY: toFixed(this.offsetY, NUM_FRACTION_DIGITS), - patternTransform: this.patternTransform ? this.patternTransform.concat() : null - }; - }, - - /* _TO_SVG_START_ */ - /** - * Returns SVG representation of a pattern - * @param {fabric.Object} object - * @return {String} SVG representation of a pattern - */ - toSVG: function(object) { - var patternSource = typeof this.source === 'function' ? this.source() : this.source, - patternWidth = patternSource.width / object.width, - patternHeight = patternSource.height / object.height, - patternOffsetX = this.offsetX / object.width, - patternOffsetY = this.offsetY / object.height, - patternImgSrc = ''; - if (this.repeat === 'repeat-x' || this.repeat === 'no-repeat') { - patternHeight = 1; - if (patternOffsetY) { - patternHeight += Math.abs(patternOffsetY); - } - } - if (this.repeat === 'repeat-y' || this.repeat === 'no-repeat') { - patternWidth = 1; - if (patternOffsetX) { - patternWidth += Math.abs(patternOffsetX); - } - - } - if (patternSource.src) { - patternImgSrc = patternSource.src; - } - else if (patternSource.toDataURL) { - patternImgSrc = patternSource.toDataURL(); - } - - return '\n' + - '\n' + - '\n'; - }, - /* _TO_SVG_END_ */ - - setOptions: function(options) { - for (var prop in options) { - this[prop] = options[prop]; - } - }, - - /** - * Returns an instance of CanvasPattern - * @param {CanvasRenderingContext2D} ctx Context to create pattern - * @return {CanvasPattern} - */ - toLive: function(ctx) { - var source = this.source; - // if the image failed to load, return, and allow rest to continue loading - if (!source) { - return ''; - } +/** + * @see {@link http://fabricjs.com/patterns demo} + * @see {@link http://fabricjs.com/dynamic-patterns demo} + */ +export class Pattern { + + type = 'pattern' + + /** + * @type TPatternRepeat + * @defaults + */ + repeat: TPatternRepeat = 'repeat' + + /** + * Pattern horizontal offset from object's left/top corner + * @type Number + * @default + */ + offsetX = 0 + + /** + * Pattern vertical offset from object's left/top corner + * @type Number + * @default + */ + offsetY = 0 + + /** + * @type TCrossOrigin + * @default + */ + crossOrigin: TCrossOrigin = '' + + /** + * transform matrix to change the pattern, imported from svgs. + * @type Array + * @default + */ + patternTransform: TMat2D | null = null + + source!: CanvasImageSource + + readonly id: number; + /** + * Constructor + * @param {Object} [options] Options object + * @param {option.source} [source] the pattern source, eventually empty or a drawable + * @return {fabric.Pattern} thisArg + */ + constructor(options: TPatternOptions = {}) { + this.id = fabric.Object.__uid++; + this.setOptions(options); + } + + setOptions(options: Record) { + for (const prop in options) { + this[prop] = options[prop]; + } + } + + /** + * @returns true if {@link source} is an element + */ + isImageSource(): this is TImageSource { + return typeof this.source.src === 'string'; + } + + /** + * @returns true if {@link source} is a element + */ + isCanvasSource(): this is TCanvasSource { + return typeof this.source === 'object' && this.source.toDataURL; + } + + sourceToString() { + return this.isImageSource() ? + this.source.src : + this.isCanvasSource() ? + this.source.toDataURL() : + ''; + } + + + /** + * Returns an instance of CanvasPattern + * @param {CanvasRenderingContext2D} ctx Context to create pattern + * @return {CanvasPattern} + */ + toLive(ctx: CanvasRenderingContext2D) { + if ( + // if the image failed to load, return, and allow rest to continue loading + !this.source // if an image - if (typeof source.src !== 'undefined') { - if (!source.complete) { - return ''; - } - if (source.naturalWidth === 0 || source.naturalHeight === 0) { - return ''; - } - } - return ctx.createPattern(source, this.repeat); + || (this.isImageSource() + && (!this.source.complete || this.source.naturalWidth === 0 || this.source.naturalHeight === 0)) + ) { + return ''; } - }); + return ctx.createPattern(this.source, this.repeat); + } + + /** + * Returns object representation of a 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)[]) { + return { + ...pick(this, propertiesToInclude), + type: 'pattern', + source: this.sourceToString(), + repeat: this.repeat, + crossOrigin: this.crossOrigin, + offsetX: toFixed(this.offsetX, config.NUM_FRACTION_DIGITS), + offsetY: toFixed(this.offsetY, config.NUM_FRACTION_DIGITS), + patternTransform: this.patternTransform ? this.patternTransform.concat() : null + }; + } + + /* _TO_SVG_START_ */ /** - * - * @param {object} object - * @param {object} [options] - * @param {AbortSignal} [options.signal] handle aborting, see https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal - * @returns + * Returns SVG representation of a pattern */ - fabric.Pattern.fromObject = function(object, options) { - var patternOptions = Object.assign({}, object), - imageOptions = Object.assign({}, options, { crossOrigin: object.crossOrigin }); - return fabric.util.loadImage(object.source, imageOptions) - .then(function(img) { - patternOptions.source = img; - return new fabric.Pattern(patternOptions); - }); - }; -})(typeof exports !== 'undefined' ? exports : window); + toSVG({ width, height }: TSize) { + const patternSource = this.source, + patternOffsetX = ifNaN(this.offsetX / width, 0), + patternOffsetY = ifNaN(this.offsetY / height, 0), + patternWidth = this.repeat === 'repeat-y' || this.repeat === 'no-repeat' ? + 1 + Math.abs(patternOffsetX || 0) : + ifNaN(patternSource.width / width, 0), + patternHeight = this.repeat === 'repeat-x' || this.repeat === 'no-repeat' ? + 1 + Math.abs(patternOffsetY || 0) : + ifNaN(patternSource.height / height, 0); + + return [ + ``, + ``, + ``, + '' + ].join('\n'); + } + /* _TO_SVG_END_ */ + + static async fromObject({ source, ...serialized }: TPatternSerialized, options: TPatternHydrationOptions) { + const img = await loadImage(source, { + ...options, + crossOrigin: serialized.crossOrigin + }) + return new Pattern({ ...serialized, source: img }); + } +} + + +fabric.Pattern = Pattern; diff --git a/src/typedefs.ts b/src/typedefs.ts index e6b493ba53a..d9d9c292c39 100644 --- a/src/typedefs.ts +++ b/src/typedefs.ts @@ -47,7 +47,7 @@ export const enum SupportedSVGUnit { } export type TMat2D = [number, number, number, number, number, number]; - + export type ModifierKey = 'altKey' | 'shiftKey' | 'ctrlKey'; /** @@ -64,3 +64,9 @@ export type TransformEvent = TEvent & T & { target: any } } + +/** + * An invalid keyword and an empty string will be handled as the `anonymous` keyword. + * @see https://developer.mozilla.org/en-US/docs/HTML/CORS_settings_attributes + */ +export type TCrossOrigin = '' | 'anonymous' | 'use-credentials' | null; diff --git a/src/util/misc/dom.ts b/src/util/misc/dom.ts index b944733458d..236fa2bd3bd 100644 --- a/src/util/misc/dom.ts +++ b/src/util/misc/dom.ts @@ -14,7 +14,7 @@ export const createCanvasElement = (): HTMLCanvasElement => fabric.document.crea * @memberOf fabric.util * @return {HTMLImageElement} HTML image element */ -export const createImage = () =>fabric.document.createElement('img'); +export const createImage = (): HTMLImageElement => fabric.document.createElement('img'); /** * Creates a canvas element that is a copy of another and is also painted diff --git a/src/util/misc/objectEnlive.ts b/src/util/misc/objectEnlive.ts index 92d120bf0f9..a19d8958f69 100644 --- a/src/util/misc/objectEnlive.ts +++ b/src/util/misc/objectEnlive.ts @@ -1,6 +1,9 @@ import { fabric } from '../../../HEADER'; -import { capitalize, camelize } from '../lang_string'; import { noop } from '../../constants'; +import { TCrossOrigin } from '../../typedefs'; +import { TObject } from '../../__types__'; +import { camelize, capitalize } from '../lang_string'; +import { createImage } from './dom'; /** * Returns klass "Class" object of given namespace @@ -13,7 +16,7 @@ export const getKlass = (type: string, namespace = fabric): any => namespace[cap type LoadImageOptions = { signal?: AbortSignal; - crossOrigin?: 'anonymous' | 'use-credentials' | null; + crossOrigin?: TCrossOrigin; } /** @@ -26,11 +29,11 @@ type LoadImageOptions = { * @param {Promise} img the loaded image. */ export const loadImage = (url: string, { signal, crossOrigin = null }: LoadImageOptions = {}) => - new Promise(function (resolve, reject) { + new Promise(function (resolve, reject) { if (signal && signal.aborted) { return reject(new Error('`options.signal` is in `aborted` state')); } - const img = fabric.util.createImage(); + const img = createImage(); let abort: EventListenerOrEventListenerObject; if (signal) { abort = function (err: Event) { @@ -39,7 +42,7 @@ export const loadImage = (url: string, { signal, crossOrigin = null }: LoadImage }; signal.addEventListener('abort', abort, { once: true }); } - const done = function() { + const done = function () { img.onload = img.onerror = null; abort && signal?.removeEventListener('abort', abort); resolve(img); @@ -83,31 +86,31 @@ export const enlivenObjects = ( reviver = noop, namespace = fabric, }: EnlivenObjectOptions = {}, -) => new Promise((resolve, reject) => { - const instances: any[] = []; +) => new Promise((resolve, reject) => { + const instances: TObject[] = []; signal && signal.addEventListener('abort', reject, { once: true }); Promise.all(objects.map((obj) => getKlass(obj.type, namespace).fromObject(obj, { signal, reviver, namespace, - }).then((fabricInstance: any) => { + }).then((fabricInstance: TObject) => { reviver(obj, fabricInstance); instances.push(fabricInstance); return fabricInstance; }) )) - .then(resolve) - .catch((error) => { - // cleanup - instances.forEach(function (instance) { - instance.dispose && instance.dispose(); + .then(resolve) + .catch((error) => { + // cleanup + instances.forEach(function (instance) { + instance.dispose && instance.dispose(); + }); + reject(error); + }) + .finally(() => { + signal && signal.removeEventListener('abort', reject); }); - reject(error); - }) - .finally(() => { - signal && signal.removeEventListener('abort', reject); - }); }); /** @@ -134,7 +137,7 @@ export const enlivenObjectEnlivables = (serializedObject: any, { signal }: { sig } // clipPath if (value.type) { - return fabric.util.enlivenObjects([value], { signal }).then(([enlived]: any[]) => { + return enlivenObjects([value], { signal }).then(([enlived]) => { instances.push(enlived); return enlived; }); @@ -155,15 +158,15 @@ export const enlivenObjectEnlivables = (serializedObject: any, { signal }: { sig return acc; }, {}); }) - .then(resolve) - .catch(function (error) { - // cleanup - instances.forEach((instance) => { - instance.dispose && instance.dispose(); + .then(resolve) + .catch(function (error) { + // cleanup + instances.forEach((instance) => { + instance.dispose && instance.dispose(); + }); + reject(error); + }) + .finally(function () { + signal && signal.removeEventListener('abort', reject); }); - reject(error); - }) - .finally(function () { - signal && signal.removeEventListener('abort', reject); - }); }); diff --git a/test/unit/pattern.js b/test/unit/pattern.js index a424eca0031..00389ac86ae 100644 --- a/test/unit/pattern.js +++ b/test/unit/pattern.js @@ -11,10 +11,10 @@ var img = fabric.document.createElement('img'); setSrc(img, IMG_SRC); - function createPattern(callback) { + function createPattern() { return new fabric.Pattern({ source: img - }, callback); + }); } QUnit.test('constructor', function(assert) {