UNPKG

fabric

Version:

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

418 lines (391 loc) 12.3 kB
import { Color } from '../color/Color'; import { iMatrix } from '../constants'; import { parseTransformAttribute } from '../parser/parseTransformAttribute'; import type { FabricObject } from '../shapes/Object/FabricObject'; import type { TMat2D } from '../typedefs'; import { uid } from '../util/internals/uid'; import { pick } from '../util/misc/pick'; import { matrixToSVG } from '../util/misc/svgExport'; import { linearDefaultCoords, radialDefaultCoords } from './constants'; import { parseColorStops } from './parser/parseColorStops'; import { parseCoords } from './parser/parseCoords'; import { parseType, parseGradientUnits } from './parser/misc'; import type { ColorStop, GradientCoords, GradientOptions, GradientType, GradientUnits, SVGOptions, } from './typedefs'; import { classRegistry } from '../ClassRegistry'; import { isPath } from '../util/typeAssertions'; /** * Gradient class * @class Gradient * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#gradients} */ 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; /** * A transform matrix to apply to the gradient before painting. * Imported from svg gradients, is not applied with the current transform in the center. * Before this transform is applied, the origin point is at the top left corner of the object * plus the addition of offsetY and offsetX. * @type Number[] * @default null */ declare gradientTransform?: TMat2D; /** * coordinates units for coords. * If `pixels`, the number of coords are in the same unit of width / height. * If set as `percentage` the coords are still a number, but 1 means 100% of width * for the X and 100% of the height for the y. It can be bigger than 1 and negative. * allowed values pixels or percentage. * @type GradientUnits * @default 'pixels' */ declare gradientUnits: GradientUnits; /** * Gradient type linear or radial * @type GradientType * @default 'linear' */ declare type: T; /** * Defines how the gradient is located in space and spread * @type GradientCoords */ declare coords: GradientCoords<T>; /** * Defines how many colors a gradient has and how they are located on the axis * defined by coords * @type GradientCoords */ declare colorStops: ColorStop[]; /** * 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 | string */ declare readonly id: string | number; static type = 'Gradient'; constructor(options: GradientOptions<T>) { const { type = 'linear' as T, gradientUnits = 'pixels', coords = {}, colorStops = [], offsetX = 0, offsetY = 0, gradientTransform, id, } = options || {}; Object.assign(this, { type, gradientUnits, coords: { ...(type === 'radial' ? radialDefaultCoords : linearDefaultCoords), ...coords, }, colorStops, offsetX, offsetY, gradientTransform, id: id ? `${id}_${uid()}` : uid(), }); } /** * Adds another colorStop * @param {Record<string, string>} colorStop Object with offset and color * @return {Gradient} thisArg */ addColorStop(colorStops: Record<string, string>) { for (const position in colorStops) { const color = new Color(colorStops[position]); this.colorStops.push({ offset: parseFloat(position), color: color.toRgb(), opacity: color.getAlpha(), }); } return this; } /** * Returns object representation of a gradient * @param {string[]} [propertiesToInclude] Any properties that you might want to additionally include in the output * @return {object} */ toObject(propertiesToInclude?: (keyof this | string)[]) { return { ...pick(this, propertiesToInclude as (keyof this)[]), type: this.type, coords: { ...this.coords }, colorStops: this.colorStops.map((colorStop) => ({ ...colorStop })), offsetX: this.offsetX, offsetY: this.offsetY, gradientUnits: this.gradientUnits, gradientTransform: this.gradientTransform ? [...this.gradientTransform] : undefined, }; } /* _TO_SVG_START_ */ /** * Returns SVG representation of an gradient * @param {FabricObject} object Object to create a gradient for * @return {String} SVG representation of an gradient (linear/radial) */ toSVG( object: FabricObject, { additionalTransform: preTransform, }: { additionalTransform?: string } = {}, ) { const markup = [], transform = ( this.gradientTransform ? this.gradientTransform.concat() : iMatrix.concat() ) as TMat2D, gradientUnits = this.gradientUnits === 'pixels' ? 'userSpaceOnUse' : 'objectBoundingBox'; // colorStops must be sorted ascending, and guarded against deep mutations const colorStops = this.colorStops .map((colorStop) => ({ ...colorStop })) .sort((a, b) => { return a.offset - b.offset; }); let offsetX = -this.offsetX, offsetY = -this.offsetY; if (gradientUnits === 'objectBoundingBox') { offsetX /= object.width; offsetY /= object.height; } else { offsetX += object.width / 2; offsetY += object.height / 2; } // todo what about polygon/polyline? if (isPath(object) && this.gradientUnits !== 'percentage') { offsetX -= object.pathOffset.x; offsetY -= object.pathOffset.y; } transform[4] -= offsetX; transform[5] -= offsetY; const commonAttributes = [ `id="SVGID_${this.id}"`, `gradientUnits="${gradientUnits}"`, `gradientTransform="${ preTransform ? preTransform + ' ' : '' }${matrixToSVG(transform)}"`, '', ].join(' '); if (this.type === 'linear') { const { x1, y1, x2, y2 } = this.coords; markup.push( '<linearGradient ', commonAttributes, ' x1="', x1, '" y1="', y1, '" x2="', x2, '" y2="', y2, '">\n', ); } else if (this.type === 'radial') { const { x1, y1, x2, y2, r1, r2 } = this .coords as GradientCoords<'radial'>; const needsSwap = r1 > r2; // svg radial gradient has just 1 radius. the biggest. markup.push( '<radialGradient ', commonAttributes, ' cx="', needsSwap ? x1 : x2, '" cy="', needsSwap ? y1 : y2, '" r="', needsSwap ? r1 : r2, '" fx="', needsSwap ? x2 : x1, '" fy="', needsSwap ? y2 : y1, '">\n', ); if (needsSwap) { // svg goes from internal to external radius. if radius are inverted, swap color stops. colorStops.reverse(); // mutates array colorStops.forEach((colorStop) => { colorStop.offset = 1 - colorStop.offset; }); } const minRadius = Math.min(r1, r2); if (minRadius > 0) { // i have to shift all colorStops and add new one in 0. const maxRadius = Math.max(r1, r2), percentageShift = minRadius / maxRadius; colorStops.forEach((colorStop) => { colorStop.offset += percentageShift * (1 - colorStop.offset); }); } } colorStops.forEach(({ color, offset, opacity }) => { markup.push( '<stop ', 'offset="', offset * 100 + '%', '" style="stop-color:', color, typeof opacity !== 'undefined' ? ';stop-opacity: ' + opacity : ';', '"/>\n', ); }); markup.push( this.type === 'linear' ? '</linearGradient>' : '</radialGradient>', '\n', ); return markup.join(''); } /* _TO_SVG_END_ */ /** * Returns an instance of CanvasGradient * @param {CanvasRenderingContext2D} ctx Context to render on * @return {CanvasGradient} */ toLive(ctx: CanvasRenderingContext2D): CanvasGradient { const { x1, y1, x2, y2, r1, r2 } = this.coords as GradientCoords<'radial'>; const gradient = this.type === 'linear' ? ctx.createLinearGradient(x1, y1, x2, y2) : ctx.createRadialGradient(x1, y1, r1, x2, y2, r2); this.colorStops.forEach(({ color, opacity, offset }) => { gradient.addColorStop( offset, typeof opacity !== 'undefined' ? new Color(color).setAlpha(opacity).toRgba() : color, ); }); return gradient; } static async fromObject( options: GradientOptions<'linear'>, ): Promise<Gradient<'radial'>>; static async fromObject( options: GradientOptions<'radial'>, ): Promise<Gradient<'radial'>>; static async fromObject( options: GradientOptions<'linear'> | GradientOptions<'radial'>, ) { const { colorStops, gradientTransform } = options; return new this({ ...options, colorStops: colorStops ? colorStops.map((colorStop) => ({ ...colorStop })) : undefined, gradientTransform: gradientTransform ? [...gradientTransform] : undefined, }); } /* _FROM_SVG_START_ */ /** * Returns {@link Gradient} instance from an SVG element * @static * @memberOf Gradient * @param {SVGGradientElement} el SVG gradient element * @param {FabricObject} instance * @param {String} opacity A fill-opacity or stroke-opacity attribute to multiply to each stop's opacity. * @param {SVGOptions} svgOptions an object containing the size of the SVG in order to parse correctly gradients * that uses gradientUnits as 'userSpaceOnUse' and percentages. * @return {Gradient} Gradient instance * @see http://www.w3.org/TR/SVG/pservers.html#LinearGradientElement * @see http://www.w3.org/TR/SVG/pservers.html#RadialGradientElement * * @example * * <linearGradient id="linearGrad1"> * <stop offset="0%" stop-color="white"/> * <stop offset="100%" stop-color="black"/> * </linearGradient> * * OR * * <linearGradient id="linearGrad2"> * <stop offset="0" style="stop-color:rgb(255,255,255)"/> * <stop offset="1" style="stop-color:rgb(0,0,0)"/> * </linearGradient> * * OR * * <radialGradient id="radialGrad1"> * <stop offset="0%" stop-color="white" stop-opacity="1" /> * <stop offset="50%" stop-color="black" stop-opacity="0.5" /> * <stop offset="100%" stop-color="white" stop-opacity="1" /> * </radialGradient> * * OR * * <radialGradient id="radialGrad2"> * <stop offset="0" stop-color="rgb(255,255,255)" /> * <stop offset="0.5" stop-color="rgb(0,0,0)" /> * <stop offset="1" stop-color="rgb(255,255,255)" /> * </radialGradient> * */ static fromElement( el: SVGGradientElement, instance: FabricObject, svgOptions: SVGOptions, ): Gradient<GradientType> { const gradientUnits = parseGradientUnits(el); const center = instance._findCenterFromElement(); return new this({ id: el.getAttribute('id') || undefined, type: parseType(el), coords: parseCoords(el, { width: svgOptions.viewBoxWidth || svgOptions.width, height: svgOptions.viewBoxHeight || svgOptions.height, }), colorStops: parseColorStops(el, svgOptions.opacity), gradientUnits, gradientTransform: parseTransformAttribute( el.getAttribute('gradientTransform') || '', ), ...(gradientUnits === 'pixels' ? { offsetX: instance.width / 2 - center.x, offsetY: instance.height / 2 - center.y, } : { offsetX: 0, offsetY: 0, }), }); } /* _FROM_SVG_END_ */ } classRegistry.setClass(Gradient, 'gradient'); classRegistry.setClass(Gradient, 'linear'); classRegistry.setClass(Gradient, 'radial');