UNPKG

fabric

Version:

Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.

227 lines (204 loc) 6.18 kB
import { config } from '../config'; import type { Abortable, TCrossOrigin, TMat2D, TSize } from '../typedefs'; import { ifNaN } from '../util/internals/ifNaN'; 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 { PatternRepeat, PatternOptions, SerializedPatternOptions, } from './types'; import { log } from '../util/internals/console'; /** * @see {@link http://fabricjs.com/patterns demo} * @see {@link http://fabricjs.com/dynamic-patterns demo} */ export class Pattern { static type = 'Pattern'; /** * Legacy identifier of the class. Prefer using this.constructor.type 'Pattern' * or utils like isPattern, or instance of to indentify a pattern in your code. * Will be removed in future versiones * @TODO add sustainable warning message * @type string * @deprecated */ get type() { return 'pattern'; } set type(value) { log('warn', 'Setting type has no effect', value); } /** * @type PatternRepeat * @defaults */ 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 */ crossOrigin: TCrossOrigin = ''; /** * transform matrix to change the pattern, imported from svgs. * @todo verify if using the identity matrix as default makes the rest of the code more easy * @type Array * @default */ declare patternTransform?: TMat2D; /** * The actual pixel source of the pattern */ declare source: CanvasImageSource; /** * If true, this object will not be exported during the serialization of a canvas * @type boolean */ declare excludeFromExport?: boolean; /** * ID used for SVG export functionalities * @type number */ declare readonly id: number; /** * Constructor * @param {Object} [options] Options object * @param {option.source} [source] the pattern source, eventually empty or a drawable */ constructor(options: PatternOptions) { this.id = uid(); Object.assign(this, options); } /** * @returns true if {@link source} is an <img> element */ isImageSource(): this is { source: HTMLImageElement } { return ( !!this.source && typeof (this.source as HTMLImageElement).src === 'string' ); } /** * @returns true if {@link source} is a <canvas> element */ isCanvasSource(): this is { source: HTMLCanvasElement } { return !!this.source && !!(this.source as HTMLCanvasElement).toDataURL; } sourceToString(): string { 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): CanvasPattern | null { if ( // if the image failed to load, return, and allow rest to continue loading !this.source || // if an image (this.isImageSource() && (!this.source.complete || this.source.naturalWidth === 0 || this.source.naturalHeight === 0)) ) { return null; } 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: string[] = []): Record<string, any> { const { repeat, crossOrigin } = this; return { ...pick(this, propertiesToInclude as (keyof this)[]), type: 'pattern', source: this.sourceToString(), repeat, crossOrigin, offsetX: toFixed(this.offsetX, config.NUM_FRACTION_DIGITS), offsetY: toFixed(this.offsetY, config.NUM_FRACTION_DIGITS), patternTransform: this.patternTransform ? [...this.patternTransform] : null, }; } /* _TO_SVG_START_ */ /** * Returns SVG representation of a pattern */ toSVG({ width, height }: TSize): string { const { source: patternSource, repeat, id } = this, patternOffsetX = ifNaN(this.offsetX / width, 0), patternOffsetY = ifNaN(this.offsetY / height, 0), patternWidth = repeat === 'repeat-y' || repeat === 'no-repeat' ? 1 + Math.abs(patternOffsetX || 0) : ifNaN( ((patternSource as HTMLImageElement).width as number) / width, 0, ), patternHeight = repeat === 'repeat-x' || repeat === 'no-repeat' ? 1 + Math.abs(patternOffsetY || 0) : ifNaN( ((patternSource as HTMLImageElement).height as number) / height, 0, ); return [ `<pattern id="SVGID_${id}" x="${patternOffsetX}" y="${patternOffsetY}" width="${patternWidth}" height="${patternHeight}">`, `<image x="0" y="0" width="${ (patternSource as HTMLImageElement).width }" height="${ (patternSource as HTMLImageElement).height }" xlink:href="${this.sourceToString()}"></image>`, `</pattern>`, '', ].join('\n'); } /* _TO_SVG_END_ */ static async fromObject( { type, source, patternTransform, ...otherOptions }: SerializedPatternOptions, options?: Abortable, ): Promise<Pattern> { const img = await loadImage(source, { ...options, crossOrigin: otherOptions.crossOrigin, }); return new this({ ...otherOptions, patternTransform: patternTransform && (patternTransform.slice(0) as TMat2D), source: img, }); } } classRegistry.setClass(Pattern); // kept for compatibility reason classRegistry.setClass(Pattern, 'pattern');