diff --git a/CHANGELOG.md b/CHANGELOG.md index 864920321f3..8395d3bd761 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [next] +- refactor(): create Filler base class for Pattern/Gradient [#8947](https://github.com/fabricjs/fabric.js/pull/8947) - ci(): automate PR changelog [#8938](https://github.com/fabricjs/fabric.js/pull/8938) - chore(): move canvas click handler to TextManager [#8939](https://github.com/fabricjs/fabric.js/pull/8939) diff --git a/src/Pattern/Pattern.ts b/src/Pattern/Pattern.ts index 98bd8c611cd..88ba268b170 100644 --- a/src/Pattern/Pattern.ts +++ b/src/Pattern/Pattern.ts @@ -3,7 +3,6 @@ import type { Abortable, TCrossOrigin, TMat2D, TSize } from '../typedefs'; import { ifNaN } from '../util/internals'; import { uid } from '../util/internals/uid'; import { loadImage } from '../util/misc/objectEnlive'; -import { pick } from '../util/misc/pick'; import { toFixed } from '../util/misc/toFixed'; import { classRegistry } from '../ClassRegistry'; import type { @@ -11,15 +10,15 @@ import type { PatternOptions, SerializedPatternOptions, } from './types'; +import { Filler } from '../fillers/Filler'; /** * @see {@link http://fabricjs.com/patterns demo} * @see {@link http://fabricjs.com/dynamic-patterns demo} */ -export class Pattern { +export class Pattern extends Filler { /** - * Legacy identifier of the class. Prefer using this.constructor.name 'Pattern' - * or utils like isPattern + * Legacy identifier of the class. Prefer using this.constructor.name or `instanceof` * Will be removed in fabric 7 or 8. * @TODO add sustainable warning message * @type string @@ -39,20 +38,6 @@ export class Pattern { */ repeat: PatternRepeat = '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 @@ -90,6 +75,7 @@ export class Pattern { * @param {option.source} [source] the pattern source, eventually empty or a drawable */ constructor(options: PatternOptions = {}) { + super(); this.id = uid(); Object.assign(this, options); } @@ -144,10 +130,10 @@ 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: string[] = []): Record { + toObject(propertiesToInclude?: T[]) { const { repeat, crossOrigin } = this; return { - ...pick(this, propertiesToInclude as (keyof this)[]), + ...super.toObject(propertiesToInclude), type: 'pattern', source: this.sourceToString(), repeat, diff --git a/src/canvas/StaticCanvas.ts b/src/canvas/StaticCanvas.ts index 501e1a64c17..da06469d472 100644 --- a/src/canvas/StaticCanvas.ts +++ b/src/canvas/StaticCanvas.ts @@ -5,7 +5,7 @@ import type { CanvasEvents, StaticCanvasEvents } from '../EventTypeDefs'; import type { Gradient } from '../gradient/Gradient'; import { createCollectionMixin } from '../Collection'; import { CommonMethods } from '../CommonMethods'; -import type { Pattern } from '../Pattern'; +import { Pattern } from '../Pattern'; import { Point } from '../Point'; import type { BaseFabricObject as FabricObject } from '../EventTypeDefs'; import type { TCachedFabricObject } from '../shapes/Object/Object'; @@ -15,13 +15,13 @@ import type { Constructor, TCornerPoint, TDataUrlOptions, - TFiller, TMat2D, TSize, TSVGReviver, TToCanvasElementOptions, TValidToObjectMethod, } from '../typedefs'; +import type { TFiller } from '../fillers/typedefs'; import { cancelAnimFrame, requestAnimFrame, @@ -38,12 +38,8 @@ import { import { pick } from '../util/misc/pick'; import { matrixToSVG } from '../util/misc/svgParsing'; import { toFixed } from '../util/misc/toFixed'; -import { - isCollection, - isFiller, - isPattern, - isTextObject, -} from '../util/typeAssertions'; +import { isCollection, isTextObject } from '../util/typeAssertions'; +import { isFiller } from '../fillers/Filler'; export type TCanvasSizeOptions = { backstoreOnly?: boolean; @@ -1405,20 +1401,27 @@ export class StaticCanvas< additionalTransform = shouldInvert ? matrixToSVG(invertTransform(this.viewportTransform)) : ''; + const { width = finalWidth, height = finalHeight } = + filler instanceof Pattern + ? { + width: + repeat === 'repeat-y' || repeat === 'no-repeat' + ? filler.source.width + : undefined, + height: + repeat === 'repeat-x' || repeat === 'no-repeat' + ? filler.source.height + : undefined, + } + : {}; markup.push( `\n` + }" width="${width}" height="${height}" fill="url(#SVGID_${ + filler.id + })">\n` ); } else { markup.push( diff --git a/src/fillers/Filler.ts b/src/fillers/Filler.ts new file mode 100644 index 00000000000..8e47b34daba --- /dev/null +++ b/src/fillers/Filler.ts @@ -0,0 +1,61 @@ +import { config } from '../config'; +import type { Point } from '../Point'; +import type { TSize } from '../typedefs'; +import type { TFiller } from './typedefs'; +import { pick } from '../util/misc/pick'; +import { toFixed } from '../util/misc/toFixed'; + +export type TFillerAction = 'stroke' | 'fill'; + +export type TFillerRenderingOptions = { + action: TFillerAction; + size: TSize; + offset: Point; + noTransform?: boolean; +}; + +export const isFiller = ( + filler: TFiller | string | null +): filler is TFiller => { + return !!filler && filler instanceof Filler; +}; + +export abstract class Filler { + /** + * horizontal offset from object's left/top corner + * @type Number + * @default + */ + offsetX = 0; + + /** + * vertical offset from object's left/top corner + * @type Number + * @default + */ + offsetY = 0; + + protected abstract toLive( + ctx: CanvasRenderingContext2D, + options: TFillerRenderingOptions + ): T | null; + + protected prepare( + ctx: CanvasRenderingContext2D, + options: TFillerRenderingOptions + ): Point | void { + ctx[`${options.action}Style`] = this.toLive(ctx, options) || ''; + } + + toObject(propertiesToInclude?: T[]) { + return { + ...pick(this, propertiesToInclude), + offsetX: toFixed(this.offsetX, config.NUM_FRACTION_DIGITS), + offsetY: toFixed(this.offsetY, config.NUM_FRACTION_DIGITS), + }; + } + + toJSON() { + return this.toObject(); + } +} diff --git a/src/fillers/typedefs.ts b/src/fillers/typedefs.ts new file mode 100644 index 00000000000..1f269bd0bb6 --- /dev/null +++ b/src/fillers/typedefs.ts @@ -0,0 +1,4 @@ +import type { Gradient } from '../gradient/Gradient'; +import type { Pattern } from '../Pattern'; + +export type TFiller = Gradient<'linear'> | Gradient<'radial'> | Pattern; diff --git a/src/gradient/Gradient.ts b/src/gradient/Gradient.ts index 5df624a55bc..e83c96a9f08 100644 --- a/src/gradient/Gradient.ts +++ b/src/gradient/Gradient.ts @@ -6,7 +6,6 @@ import type { FabricObject } from '../shapes/Object/FabricObject'; import { FabricObject as BaseFabricObject } from '../shapes/Object/Object'; import type { TMat2D } from '../typedefs'; import { uid } from '../util/internals/uid'; -import { pick } from '../util/misc/pick'; import { matrixToSVG } from '../util/misc/svgParsing'; import { linearDefaultCoords, radialDefaultCoords } from './constants'; import { @@ -24,6 +23,7 @@ import type { SVGOptions, } from './typedefs'; import { classRegistry } from '../ClassRegistry'; +import { Filler } from '../fillers/Filler'; /** * Gradient class @@ -33,21 +33,7 @@ import { classRegistry } from '../ClassRegistry'; export class Gradient< S, T extends GradientType = S extends GradientType ? S : 'linear' -> { - /** - * Horizontal offset for aligning gradients coming from SVG when outside pathgroups - * @type Number - * @default 0 - */ - declare offsetX: number; - - /** - * Vertical offset for aligning gradients coming from SVG when outside pathgroups - * @type Number - * @default 0 - */ - declare offsetY: number; - +> extends Filler { /** * A transform matrix to apply to the gradient before painting. * Imported from svg gradients, is not applied with the current transform in the center. @@ -111,6 +97,7 @@ export class Gradient< gradientTransform = null, id, }: GradientOptions) { + super(); this.id = id ? `${id}_${uid()}` : uid(); this.type = type; this.gradientUnits = gradientUnits; @@ -150,9 +137,9 @@ export class Gradient< * @param {string[]} [propertiesToInclude] Any properties that you might want to additionally include in the output * @return {object} */ - toObject(propertiesToInclude?: (keyof this | string)[]) { + toObject(propertiesToInclude?: T[]) { return { - ...pick(this, propertiesToInclude), + ...super.toObject(propertiesToInclude), type: this.type, coords: this.coords, colorStops: this.colorStops, diff --git a/src/shapes/IText/IText.ts b/src/shapes/IText/IText.ts index 438da1d74e8..1e77cc5b494 100644 --- a/src/shapes/IText/IText.ts +++ b/src/shapes/IText/IText.ts @@ -7,7 +7,8 @@ import { keysMap, keysMapRtl, } from './constants'; -import type { AssertKeys, TFiller } from '../../typedefs'; +import type { AssertKeys } from '../../typedefs'; +import type { TFiller } from '../../fillers/typedefs'; import { classRegistry } from '../../ClassRegistry'; import type { SerializedTextProps, TextProps } from '../Text/Text'; diff --git a/src/shapes/Line.ts b/src/shapes/Line.ts index ce34d94cb8f..f1c48f9ffd6 100644 --- a/src/shapes/Line.ts +++ b/src/shapes/Line.ts @@ -4,7 +4,7 @@ import type { TClassProperties } from '../typedefs'; import { classRegistry } from '../ClassRegistry'; import { FabricObject, cacheProperties } from './Object/FabricObject'; import { Point } from '../Point'; -import { isFiller } from '../util/typeAssertions'; +import { isFiller } from '../fillers/Filler'; import type { FabricObjectProps, SerializedObjectProps, diff --git a/src/shapes/Object/Object.ts b/src/shapes/Object/Object.ts index ad177833c45..eec2a8e7f29 100644 --- a/src/shapes/Object/Object.ts +++ b/src/shapes/Object/Object.ts @@ -7,11 +7,11 @@ import { Point } from '../../Point'; import { Shadow } from '../../Shadow'; import type { TDegree, - TFiller, TSize, TCacheCanvasDimensions, Abortable, } from '../../typedefs'; +import type { TFiller } from '../../fillers/typedefs'; import { classRegistry } from '../../ClassRegistry'; import { runningAnimations } from '../../util/animation/AnimationRegistry'; import { cloneDeep } from '../../util/internals/cloneDeep'; @@ -28,11 +28,8 @@ import { pick, pickBy } from '../../util/misc/pick'; import { toFixed } from '../../util/misc/toFixed'; import type { Group } from '../Group'; import { StaticCanvas } from '../../canvas/StaticCanvas'; -import { - isFiller, - isSerializableFiller, - isTextObject, -} from '../../util/typeAssertions'; +import { isTextObject } from '../../util/typeAssertions'; +import { isFiller } from '../../fillers/Filler'; import type { Image } from '../Image'; import { cacheProperties, @@ -516,12 +513,8 @@ export class FabricObject< top: toFixed(this.top, NUM_FRACTION_DIGITS), width: toFixed(this.width, NUM_FRACTION_DIGITS), height: toFixed(this.height, NUM_FRACTION_DIGITS), - fill: isSerializableFiller(this.fill) - ? this.fill.toObject() - : this.fill, - stroke: isSerializableFiller(this.stroke) - ? this.stroke.toObject() - : this.stroke, + fill: isFiller(this.fill) ? this.fill.toObject() : this.fill, + stroke: isFiller(this.stroke) ? this.stroke.toObject() : this.stroke, strokeWidth: toFixed(this.strokeWidth, NUM_FRACTION_DIGITS), strokeDashArray: this.strokeDashArray ? this.strokeDashArray.concat() diff --git a/src/shapes/Object/types/FillStrokeProps.ts b/src/shapes/Object/types/FillStrokeProps.ts index 44c8d04657e..59c1c11c141 100644 --- a/src/shapes/Object/types/FillStrokeProps.ts +++ b/src/shapes/Object/types/FillStrokeProps.ts @@ -1,4 +1,4 @@ -import type { TFiller } from '../../../typedefs'; +import type { TFiller } from '../../../fillers/typedefs'; export interface FillStrokeProps { /** diff --git a/src/shapes/Object/types/ObjectProps.ts b/src/shapes/Object/types/ObjectProps.ts index 2b32bb6a7be..7d63aff7506 100644 --- a/src/shapes/Object/types/ObjectProps.ts +++ b/src/shapes/Object/types/ObjectProps.ts @@ -1,7 +1,7 @@ import type { Shadow } from '../../../Shadow'; import type { Canvas } from '../../../canvas/Canvas'; import type { StaticCanvas } from '../../../canvas/StaticCanvas'; -import type { TFiller } from '../../../typedefs'; +import type { TFiller } from '../../../fillers/typedefs'; import type { FabricObject } from '../Object'; import type { ClipPathProps, diff --git a/src/shapes/Text/Text.ts b/src/shapes/Text/Text.ts index ddb1e848a9a..8e2aaa31173 100644 --- a/src/shapes/Text/Text.ts +++ b/src/shapes/Text/Text.ts @@ -7,11 +7,8 @@ import { StyledText } from './StyledText'; import { SHARED_ATTRIBUTES } from '../../parser/attributes'; import { parseAttributes } from '../../parser/parseAttributes'; import type { Point } from '../../Point'; -import type { - TCacheCanvasDimensions, - TClassProperties, - TFiller, -} from '../../typedefs'; +import type { TCacheCanvasDimensions, TClassProperties } from '../../typedefs'; +import type { TFiller } from '../../fillers/typedefs'; import { classRegistry } from '../../ClassRegistry'; import { graphemeSplit } from '../../util/lang_string'; import { createCanvasElement } from '../../util/misc/dom'; diff --git a/src/typedefs.ts b/src/typedefs.ts index 20537ee8e08..0b10ca3d31c 100644 --- a/src/typedefs.ts +++ b/src/typedefs.ts @@ -1,7 +1,5 @@ // https://www.typescriptlang.org/docs/handbook/utility-types.html import type { BaseFabricObject } from './EventTypeDefs'; -import type { Gradient } from './gradient/Gradient'; -import type { Pattern } from './Pattern'; import type { XY, Point } from './Point'; interface NominalTag { @@ -29,8 +27,6 @@ export type TAxis = 'x' | 'y'; export type TAxisKey = `${T}${Capitalize}`; -export type TFiller = Gradient<'linear'> | Gradient<'radial'> | Pattern; - export type TSize = { width: number; height: number; diff --git a/src/util/misc/objectEnlive.ts b/src/util/misc/objectEnlive.ts index 15d78c567b9..d6e1397bb14 100644 --- a/src/util/misc/objectEnlive.ts +++ b/src/util/misc/objectEnlive.ts @@ -1,7 +1,8 @@ import { noop } from '../../constants'; import type { Pattern } from '../../Pattern'; import type { FabricObject } from '../../shapes/Object/FabricObject'; -import type { Abortable, TCrossOrigin, TFiller } from '../../typedefs'; +import type { Abortable, TCrossOrigin } from '../../typedefs'; +import type { TFiller } from '../../fillers/typedefs'; import { createImage } from './dom'; import { classRegistry } from '../../ClassRegistry'; diff --git a/src/util/typeAssertions.ts b/src/util/typeAssertions.ts index 2778be1354c..c5444f52b32 100644 --- a/src/util/typeAssertions.ts +++ b/src/util/typeAssertions.ts @@ -5,32 +5,10 @@ import type { TCachedFabricObject, } from '../shapes/Object/Object'; import type { FabricObjectWithDragSupport } from '../shapes/Object/InteractiveObject'; -import type { TFiller } from '../typedefs'; import type { Text } from '../shapes/Text/Text'; -import type { Pattern } from '../Pattern'; import type { IText } from '../shapes/IText/IText'; import type { Textbox } from '../shapes/Textbox'; -export const isFiller = ( - filler: TFiller | string | null -): filler is TFiller => { - return !!filler && (filler as TFiller).toLive !== undefined; -}; - -export const isSerializableFiller = ( - filler: TFiller | string | null -): filler is TFiller => { - return !!filler && typeof (filler as TFiller).toObject === 'function'; -}; - -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 => {