diff --git a/src/gradient/constants.ts b/src/gradient/constants.ts new file mode 100644 index 00000000000..56d1b4c2f6d --- /dev/null +++ b/src/gradient/constants.ts @@ -0,0 +1,13 @@ + +export const linearDefaultCoords = { + x1: 0, + y1: 0, + x2: 0, + y2: 0, +} + +export const radialDefaultCoords = { + ...linearDefaultCoords, + r1: 0, + r2: 0, +} \ No newline at end of file diff --git a/src/gradient/gradient.class.ts b/src/gradient/gradient.class.ts index 3f9b4d29ecc..7d1988332c0 100644 --- a/src/gradient/gradient.class.ts +++ b/src/gradient/gradient.class.ts @@ -1,493 +1,334 @@ -//@ts-nocheck -import { Color } from "../color"; +import { fabric } from "../../HEADER"; +import { Color } from "../color"; +import { iMatrix } from "../constants"; +import { parseTransformAttribute } from "../parser/parseTransformAttribute"; +import { matrixToSVG, populateWithProperties } from "../util"; +import { linearDefaultCoords, radialDefaultCoords } from "./constants"; +import { parseColorStops, parseCoords, parseGradientUnits, parseType } from "./parser"; +import { ColorStop, GradientCoords, GradientOptions, GradientType, GradientUnits, SVGOptions } from "./typedefs"; + +/** + * @todo remove this transient junk + */ +type FabricObject = any; + +/** + * Gradient class + * @class Gradient + * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#gradients} + */ +export class Gradient { -(function(global) { - var fabric = global.fabric; - /* _FROM_SVG_START_ */ - function getColorStop(el, multiplier) { - var style = el.getAttribute('style'), - offset = el.getAttribute('offset') || 0, - color, colorAlpha, opacity, i; + /** + * Horizontal offset for aligning gradients coming from SVG when outside pathgroups + * @type Number + * @default 0 + */ + offsetX = 0 - // convert percents to absolute values - offset = parseFloat(offset) / (/%$/.test(offset) ? 100 : 1); - offset = offset < 0 ? 0 : offset > 1 ? 1 : offset; - if (style) { - var keyValuePairs = style.split(/\s*;\s*/); + /** + * Vertical offset for aligning gradients coming from SVG when outside pathgroups + * @type Number + * @default 0 + */ + offsetY = 0 - if (keyValuePairs[keyValuePairs.length - 1] === '') { - keyValuePairs.pop(); - } + /** + * 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 + */ + gradientTransform: number[] | null = null - for (i = keyValuePairs.length; i--; ) { + /** + * 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' + */ + gradientUnits: GradientUnits - var split = keyValuePairs[i].split(/\s*:\s*/), - key = split[0].trim(), - value = split[1].trim(); + /** + * Gradient type linear or radial + * @type GradientType + * @default 'linear' + */ + type: T + + coords: GradientCoords + + colorStops: ColorStop[] + + private id: string | number + + constructor({ + type = 'linear' as T, + gradientUnits = 'pixels', + coords, + colorStops = [], + offsetX = 0, + offsetY = 0, + gradientTransform, + id + }: GradientOptions) { + const uid = fabric.Object.__uid++; + this.id = id ? `${id}_${uid}` : uid; + this.type = type; + this.gradientUnits = gradientUnits; + this.gradientTransform = gradientTransform || null; + this.offsetX = offsetX; + this.offsetY = offsetY; + this.coords = { + ...this.type === 'radial' ? + radialDefaultCoords : + linearDefaultCoords, + ...coords + } as GradientCoords; + this.colorStops = colorStops.slice(); + } - if (key === 'stop-color') { - color = value; - } - else if (key === 'stop-opacity') { - opacity = value; - } - } - } + // isType(type: S): this is Gradient { + // return (this.type as GradientType) === type; + // } - if (!color) { - color = el.getAttribute('stop-color') || 'rgb(0,0,0)'; - } - if (!opacity) { - opacity = el.getAttribute('stop-opacity'); + /** + * Adds another colorStop + * @param {Record} colorStop Object with offset and color + * @return {Gradient} thisArg + */ + addColorStop(colorStops: Record) { + for (const position in colorStops) { + const color = new Color(colorStops[position]); + this.colorStops.push({ + offset: parseFloat(position), + color: color.toRgb(), + opacity: color.getAlpha() + }); } - - color = new Color(color); - colorAlpha = color.getAlpha(); - opacity = isNaN(parseFloat(opacity)) ? 1 : parseFloat(opacity); - opacity *= colorAlpha * multiplier; - - return { - offset: offset, - color: color.toRgb(), - opacity: opacity - }; + return this; } - function getLinearCoords(el) { - return { - x1: el.getAttribute('x1') || 0, - y1: el.getAttribute('y1') || 0, - x2: el.getAttribute('x2') || '100%', - y2: el.getAttribute('y2') || 0 + /** + * 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?: string[]) { + const object = { + type: this.type, + coords: this.coords, + colorStops: this.colorStops, + offsetX: this.offsetX, + offsetY: this.offsetY, + gradientUnits: this.gradientUnits, + gradientTransform: this.gradientTransform ? this.gradientTransform.concat() : this.gradientTransform }; - } + populateWithProperties(this, object, propertiesToInclude); - function getRadialCoords(el) { - return { - x1: el.getAttribute('fx') || el.getAttribute('cx') || '50%', - y1: el.getAttribute('fy') || el.getAttribute('cy') || '50%', - r1: 0, - x2: el.getAttribute('cx') || '50%', - y2: el.getAttribute('cy') || '50%', - r2: el.getAttribute('r') || '50%' - }; + return object; } - /* _FROM_SVG_END_ */ + /* _TO_SVG_START_ */ /** - * Gradient class - * @class fabric.Gradient - * @tutorial {@link http://fabricjs.com/fabric-intro-part-2#gradients} - * @see {@link fabric.Gradient#initialize} for constructor definition + * Returns SVG representation of an gradient + * @param {fabric.Object} object Object to create a gradient for + * @return {String} SVG representation of an gradient (linear/radial) */ - fabric.Gradient = fabric.util.createClass(/** @lends fabric.Gradient.prototype */ { - - /** - * Horizontal offset for aligning gradients coming from SVG when outside pathgroups - * @type Number - * @default 0 - */ - offsetX: 0, - - /** - * Vertical offset for aligning gradients coming from SVG when outside pathgroups - * @type Number - * @default 0 - */ - offsetY: 0, - - /** - * 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 - */ - gradientTransform: null, - - /** - * 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 String - * @default 'pixels' - */ - gradientUnits: 'pixels', - - /** - * Gradient type linear or radial - * @type String - * @default 'pixels' - */ - type: 'linear', - - /** - * Constructor - * @param {Object} options Options object with type, coords, gradientUnits and colorStops - * @param {Object} [options.type] gradient type linear or radial - * @param {Object} [options.gradientUnits] gradient units - * @param {Object} [options.offsetX] SVG import compatibility - * @param {Object} [options.offsetY] SVG import compatibility - * @param {Object[]} options.colorStops contains the colorstops. - * @param {Object} options.coords contains the coords of the gradient - * @param {Number} [options.coords.x1] X coordiante of the first point for linear or of the focal point for radial - * @param {Number} [options.coords.y1] Y coordiante of the first point for linear or of the focal point for radial - * @param {Number} [options.coords.x2] X coordiante of the second point for linear or of the center point for radial - * @param {Number} [options.coords.y2] Y coordiante of the second point for linear or of the center point for radial - * @param {Number} [options.coords.r1] only for radial gradient, radius of the inner circle - * @param {Number} [options.coords.r2] only for radial gradient, radius of the external circle - * @return {fabric.Gradient} thisArg - */ - initialize: function(options) { - options || (options = { }); - options.coords || (options.coords = { }); - - var coords, _this = this; - - // sets everything, then coords and colorstops get sets again - Object.keys(options).forEach(function(option) { - _this[option] = options[option]; - }); - - if (this.id) { - this.id += '_' + fabric.Object.__uid++; - } - else { - this.id = fabric.Object.__uid++; - } - - coords = { - x1: options.coords.x1 || 0, - y1: options.coords.y1 || 0, - x2: options.coords.x2 || 0, - y2: options.coords.y2 || 0 - }; - - if (this.type === 'radial') { - coords.r1 = options.coords.r1 || 0; - coords.r2 = options.coords.r2 || 0; - } - - this.coords = coords; - this.colorStops = options.colorStops.slice(); - }, + toSVG(object: FabricObject, { additionalTransform: preTransform }: { additionalTransform?: string } = {}) { + const markup = [], + transform = this.gradientTransform ? this.gradientTransform.concat() : iMatrix.concat(), + gradientUnits = this.gradientUnits === 'pixels' ? 'userSpaceOnUse' : 'objectBoundingBox'; + // colorStops must be sorted ascending + const colorStops = this.colorStops.concat().sort((a, b) => { + return a.offset - b.offset; + }); - /** - * Adds another colorStop - * @param {Object} colorStop Object with offset and color - * @return {fabric.Gradient} thisArg - */ - addColorStop: function(colorStops) { - for (var position in colorStops) { - var color = new Color(colorStops[position]); - this.colorStops.push({ - offset: parseFloat(position), - color: color.toRgb(), - opacity: color.getAlpha() + 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; + } + if (object.type === 'path' && 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( + '\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( + '\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; }); } - return this; - }, - - /** - * Returns object representation of a gradient - * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output - * @return {Object} - */ - toObject: function(propertiesToInclude) { - var object = { - type: this.type, - coords: this.coords, - colorStops: this.colorStops, - offsetX: this.offsetX, - offsetY: this.offsetY, - gradientUnits: this.gradientUnits, - gradientTransform: this.gradientTransform ? this.gradientTransform.concat() : this.gradientTransform - }; - fabric.util.populateWithProperties(this, object, propertiesToInclude); - - return object; - }, - - /* _TO_SVG_START_ */ - /** - * Returns SVG representation of an gradient - * @param {Object} object Object to create a gradient for - * @return {String} SVG representation of an gradient (linear/radial) - */ - toSVG: function(object, options) { - var coords = this.coords, i, len, options = options || {}, - markup, commonAttributes, colorStops = this.colorStops, - needsSwap = coords.r1 > coords.r2, - transform = this.gradientTransform ? this.gradientTransform.concat() : fabric.iMatrix.concat(), - offsetX = -this.offsetX, offsetY = -this.offsetY, - withViewport = !!options.additionalTransform, - gradientUnits = this.gradientUnits === 'pixels' ? 'userSpaceOnUse' : 'objectBoundingBox'; - // colorStops must be sorted ascending - colorStops.sort(function(a, b) { - return a.offset - b.offset; - }); - - if (gradientUnits === 'objectBoundingBox') { - offsetX /= object.width; - offsetY /= object.height; - } - else { - offsetX += object.width / 2; - offsetY += object.height / 2; - } - if (object.type === 'path' && this.gradientUnits !== 'percentage') { - offsetX -= object.pathOffset.x; - offsetY -= object.pathOffset.y; - } - - - transform[4] -= offsetX; - transform[5] -= offsetY; - - commonAttributes = 'id="SVGID_' + this.id + - '" gradientUnits="' + gradientUnits + '"'; - commonAttributes += ' gradientTransform="' + (withViewport ? - options.additionalTransform + ' ' : '') + fabric.util.matrixToSVG(transform) + '" '; - - if (this.type === 'linear') { - markup = [ - '\n' - ]; - } - else if (this.type === 'radial') { - // svg radial gradient has just 1 radius. the biggest. - markup = [ - '\n' - ]; - } - - if (this.type === 'radial') { - if (needsSwap) { - // svg goes from internal to external radius. if radius are inverted, swap color stops. - colorStops = colorStops.concat(); - colorStops.reverse(); - for (i = 0, len = colorStops.length; i < len; i++) { - colorStops[i].offset = 1 - colorStops[i].offset; - } - } - var minRadius = Math.min(coords.r1, coords.r2); - if (minRadius > 0) { - // i have to shift all colorStops and add new one in 0. - var maxRadius = Math.max(coords.r1, coords.r2), - percentageShift = minRadius / maxRadius; - for (i = 0, len = colorStops.length; i < len; i++) { - colorStops[i].offset += percentageShift * (1 - colorStops[i].offset); - } - } - } - - for (i = 0, len = colorStops.length; i < len; i++) { - var colorStop = colorStops[i]; - markup.push( - '\n' - ); - } - - markup.push((this.type === 'linear' ? '\n' : '\n')); - - return markup.join(''); - }, - /* _TO_SVG_END_ */ - - /** - * Returns an instance of CanvasGradient - * @param {CanvasRenderingContext2D} ctx Context to render on - * @return {CanvasGradient} - */ - toLive: function(ctx) { - var gradient, coords = this.coords, i, len; - - if (!this.type) { - return; - } - - if (this.type === 'linear') { - gradient = ctx.createLinearGradient( - coords.x1, coords.y1, coords.x2, coords.y2); - } - else if (this.type === 'radial') { - gradient = ctx.createRadialGradient( - coords.x1, coords.y1, coords.r1, coords.x2, coords.y2, coords.r2); - } - - for (i = 0, len = this.colorStops.length; i < len; i++) { - var color = this.colorStops[i].color, - opacity = this.colorStops[i].opacity, - offset = this.colorStops[i].offset; - - if (typeof opacity !== 'undefined') { - color = new Color(color).setAlpha(opacity).toRgba(); - } - gradient.addColorStop(offset, color); + 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); + }); } - - return gradient; } - }); - - fabric.util.object.extend(fabric.Gradient, { - - /* _FROM_SVG_START_ */ - /** - * Returns {@link fabric.Gradient} instance from an SVG element - * @static - * @memberOf fabric.Gradient - * @param {SVGGradientElement} el SVG gradient element - * @param {fabric.Object} instance - * @param {String} opacityAttr A fill-opacity or stroke-opacity attribute to multiply to each stop's opacity. - * @param {Object} svgOptions an object containing the size of the SVG in order to parse correctly gradients - * that uses gradientUnits as 'userSpaceOnUse' and percentages. - * @param {Object.number} viewBoxWidth width part of the viewBox attribute on svg - * @param {Object.number} viewBoxHeight height part of the viewBox attribute on svg - * @param {Object.number} width width part of the svg tag if viewBox is not specified - * @param {Object.number} height height part of the svg tag if viewBox is not specified - * @return {fabric.Gradient} Gradient instance - * @see http://www.w3.org/TR/SVG/pservers.html#LinearGradientElement - * @see http://www.w3.org/TR/SVG/pservers.html#RadialGradientElement - */ - fromElement: function(el, instance, opacityAttr, svgOptions) { - /** - * @example: - * - * - * - * - * - * - * OR - * - * - * - * - * - * - * OR - * - * - * - * - * - * - * - * OR - * - * - * - * - * - * - * - */ - var multiplier = parseFloat(opacityAttr) / (/%$/.test(opacityAttr) ? 100 : 1); - multiplier = multiplier < 0 ? 0 : multiplier > 1 ? 1 : multiplier; - if (isNaN(multiplier)) { - multiplier = 1; - } - - var colorStopEls = el.getElementsByTagName('stop'), - type, - gradientUnits = el.getAttribute('gradientUnits') === 'userSpaceOnUse' ? - 'pixels' : 'percentage', - gradientTransform = el.getAttribute('gradientTransform') || '', - colorStops = [], - coords, i, offsetX = 0, offsetY = 0, - transformMatrix; - if (el.nodeName === 'linearGradient' || el.nodeName === 'LINEARGRADIENT') { - type = 'linear'; - coords = getLinearCoords(el); - } - else { - type = 'radial'; - coords = getRadialCoords(el); - } - - for (i = colorStopEls.length; i--; ) { - colorStops.push(getColorStop(colorStopEls[i], multiplier)); - } + colorStops.forEach(({ color, offset, opacity }) => { + markup.push( + '\n' + ); + }); - transformMatrix = fabric.parseTransformAttribute(gradientTransform); + markup.push(this.type === 'linear' ? '' : '', '\n'); - __convertPercentUnitsToValues(instance, coords, svgOptions, gradientUnits); + return markup.join(''); + } + /* _TO_SVG_END_ */ - if (gradientUnits === 'pixels') { - offsetX = -instance.left; - offsetY = -instance.top; - } + /** + * Returns an instance of CanvasGradient + * @param {CanvasRenderingContext2D} ctx Context to render on + * @return {CanvasGradient} + */ + toLive(ctx: CanvasRenderingContext2D) { + if (!this.type) { + return; + } - var gradient = new fabric.Gradient({ - id: el.getAttribute('id'), - type: type, - coords: coords, - colorStops: colorStops, - gradientUnits: gradientUnits, - gradientTransform: transformMatrix, - offsetX: offsetX, - offsetY: offsetY, - }); + const coords = this.coords as GradientCoords<'radial'>; + const gradient = this.type === 'linear' ? + ctx.createLinearGradient(coords.x1, coords.y1, coords.x2, coords.y2) : + ctx.createRadialGradient(coords.x1, coords.y1, coords.r1, coords.x2, coords.y2, coords.r2); + + this.colorStops.forEach(({ color, opacity, offset }) => { + gradient.addColorStop( + offset, + typeof opacity !== 'undefined' ? + new Color(color).setAlpha(opacity).toRgba() : + color + ); + }); - return gradient; - } - /* _FROM_SVG_END_ */ - }); + return gradient; + } + /* _FROM_SVG_START_ */ /** - * @private + * 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 + * + * + * + * + * + * + * OR + * + * + * + * + * + * + * OR + * + * + * + * + * + * + * + * OR + * + * + * + * + * + * + * */ - function __convertPercentUnitsToValues(instance, options, svgOptions, gradientUnits) { - var propValue, finalValue; - Object.keys(options).forEach(function(prop) { - propValue = options[prop]; - if (propValue === 'Infinity') { - finalValue = 1; - } - else if (propValue === '-Infinity') { - finalValue = 0; - } - else { - finalValue = parseFloat(options[prop], 10); - if (typeof propValue === 'string' && /^(\d+\.\d+)%|(\d+)%$/.test(propValue)) { - finalValue *= 0.01; - if (gradientUnits === 'pixels') { - // then we need to fix those percentages here in svg parsing - if (prop === 'x1' || prop === 'x2' || prop === 'r2') { - finalValue *= svgOptions.viewBoxWidth || svgOptions.width; - } - if (prop === 'y1' || prop === 'y2') { - finalValue *= svgOptions.viewBoxHeight || svgOptions.height; - } - } - } - } - options[prop] = finalValue; + static fromElement(el: SVGGradientElement, instance: FabricObject, svgOptions: SVGOptions): Gradient { + const gradientUnits = parseGradientUnits(el); + return new Gradient({ + 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.left, + offsetY: -instance.top + } : { + offsetX: 0, + offsetY: 0 + }) }); } -})(typeof exports !== 'undefined' ? exports : window); + /* _FROM_SVG_END_ */ +} + +fabric.Gradient = Gradient; diff --git a/src/gradient/parser/index.ts b/src/gradient/parser/index.ts new file mode 100644 index 00000000000..c469a0a9314 --- /dev/null +++ b/src/gradient/parser/index.ts @@ -0,0 +1,3 @@ +export * from './misc'; +export * from './parseColorStops'; +export * from './parseCoords'; diff --git a/src/gradient/parser/misc.ts b/src/gradient/parser/misc.ts new file mode 100644 index 00000000000..4696c3e0888 --- /dev/null +++ b/src/gradient/parser/misc.ts @@ -0,0 +1,13 @@ +import { GradientType, GradientUnits } from "../typedefs"; + +export function parseType(el: SVGGradientElement): GradientType { + return el.nodeName === 'linearGradient' || el.nodeName === 'LINEARGRADIENT' ? + 'linear' : + 'radial' +} + +export function parseGradientUnits(el: SVGGradientElement): GradientUnits { + return el.getAttribute('gradientUnits') === 'userSpaceOnUse' ? + 'pixels' : + 'percentage'; +} \ No newline at end of file diff --git a/src/gradient/parser/parseColorStops.ts b/src/gradient/parser/parseColorStops.ts new file mode 100644 index 00000000000..bff727e3c5d --- /dev/null +++ b/src/gradient/parser/parseColorStops.ts @@ -0,0 +1,46 @@ +import { Color } from "../../color"; +import { parsePercent } from "../../parser/percent"; +import { ifNaN } from "../../util/internals"; + +const RE_KEY_VALUE_PAIRS = /\s*;\s*/; +const RE_KEY_VALUE = /\s*:\s*/; + +function parseColorStop(el: SVGStopElement, multiplier: number) { + let colorValue, opacity; + const style = el.getAttribute('style'); + if (style) { + const keyValuePairs = style.split(RE_KEY_VALUE_PAIRS); + + if (keyValuePairs[keyValuePairs.length - 1] === '') { + keyValuePairs.pop(); + } + + for (let i = keyValuePairs.length; i--;) { + const [key, value] = keyValuePairs[i].split(RE_KEY_VALUE).map(s => s.trim()); + if (key === 'stop-color') { + colorValue = value; + } + else if (key === 'stop-opacity') { + opacity = value; + } + } + } + + const color = new Color(colorValue || el.getAttribute('stop-color') || 'rgb(0,0,0)'); + + return { + offset: parsePercent(el.getAttribute('offset'), 0), + color: color.toRgb(), + opacity: ifNaN(parseFloat(opacity || el.getAttribute('stop-opacity') || ''), 1) * color.getAlpha() * multiplier + }; +} + +export function parseColorStops(el: SVGGradientElement, opacityAttr: string | null) { + const colorStops = [], + colorStopEls = el.getElementsByTagName('stop'), + multiplier = parsePercent(opacityAttr, 1); + for (let i = colorStopEls.length; i--;) { + colorStops.push(parseColorStop(colorStopEls[i], multiplier)); + } + return colorStops; +} diff --git a/src/gradient/parser/parseCoords.ts b/src/gradient/parser/parseCoords.ts new file mode 100644 index 00000000000..3258b1b59fc --- /dev/null +++ b/src/gradient/parser/parseCoords.ts @@ -0,0 +1,71 @@ +import { isPercent } from "../../parser/percent"; +import { TSize } from "../../typedefs"; +import { GradientCoords, GradientType, GradientUnits } from "../typedefs"; +import { parseGradientUnits, parseType } from "./misc"; + +function convertPercentUnitsToValues>( + valuesToConvert: Record, + { width, height, gradientUnits }: TSize & { gradientUnits: GradientUnits } +) { + let finalValue; + return (Object.keys(valuesToConvert) as K[]).reduce((acc, prop) => { + const propValue = valuesToConvert[prop]; + if (propValue === 'Infinity') { + finalValue = 1; + } + else if (propValue === '-Infinity') { + finalValue = 0; + } + else { + finalValue = typeof propValue === 'string' ? parseFloat(propValue) : propValue; + if (typeof propValue === 'string' && isPercent(propValue)) { + finalValue *= 0.01; + if (gradientUnits === 'pixels') { + // then we need to fix those percentages here in svg parsing + if (prop === 'x1' || prop === 'x2' || prop === 'r2') { + finalValue *= width; + } + if (prop === 'y1' || prop === 'y2') { + finalValue *= height; + } + } + } + } + acc[prop] = finalValue; + return acc; + }, {} as Record); +} + +function getValue(el: SVGGradientElement, key: string) { + return el.getAttribute(key); +} + +export function parseLinearCoords(el: SVGGradientElement) { + return { + x1: getValue(el, 'x1') || 0, + y1: getValue(el, 'y1') || 0, + x2: getValue(el, 'x2') || '100%', + y2: getValue(el, 'y2') || 0 + }; +} + +export function parseRadialCoords(el: SVGGradientElement) { + return { + x1: getValue(el, 'fx') || getValue(el, 'cx') || '50%', + y1: getValue(el, 'fy') || getValue(el, 'cy') || '50%', + r1: 0, + x2: getValue(el, 'cx') || '50%', + y2: getValue(el, 'cy') || '50%', + r2: getValue(el, 'r') || '50%' + }; +} + +export function parseCoords(el: SVGGradientElement, size: TSize) { + return convertPercentUnitsToValues( + parseType(el) === 'linear' ? parseLinearCoords(el) : parseRadialCoords(el), + { + ...size, + gradientUnits: parseGradientUnits(el) + } + ); +} diff --git a/src/gradient/typedefs.ts b/src/gradient/typedefs.ts new file mode 100644 index 00000000000..ece00301892 --- /dev/null +++ b/src/gradient/typedefs.ts @@ -0,0 +1,104 @@ +import { Percent } from "../typedefs"; + +export type GradientUnits = 'pixels' | 'percentage'; + +export type GradientType = 'linear' | 'radial'; + +export type GradientCoordValue = number | Percent | string; + +export type ColorStop = { + color: string; + offset: number; + opacity?: number; +}; + +export type LinearGradientCoords = { + /** + * X coordiante of the first point + */ + x1: T; + /** + * Y coordiante of the first point + */ + y1: T; + /** + * X coordiante of the second point + */ + x2: T; + /** + * Y coordiante of the second point + */ + y2: T; +}; + +export type RadialGradientCoords = { + /** + * X coordiante of the first focal point + */ + x1: T; + /** + * Y coordiante of the first focal point + */ + y1: T; + /** + * X coordiante of the second focal point + */ + x2: T; + /** + * Y coordiante of the second focal point + */ + y2: T; + /** + * radius of the inner circle + */ + r1: T; + /** + * radius of the outer circle + */ + r2: T; +}; + +export type GradientCoords = T extends 'linear' ? + LinearGradientCoords : + RadialGradientCoords; + +export type GradientOptions = { + type?: T; + gradientUnits?: GradientUnits; + colorStops?: ColorStop[]; + coords: Partial>; + /** + * @todo rename? + */ + gradientTransform?: number[]; + id?: string; + /** + * SVG import compatibility + */ + offsetX?: number; + /** + * SVG import compatibility + */ + offsetY?: number; +}; + +export type SVGOptions = { + /** + * width part of the viewBox attribute on svg + */ + viewBoxWidth: number; + /** + * height part of the viewBox attribute on svg + */ + viewBoxHeight: number; + /** + * width part of the svg tag if viewBox is not specified + */ + width: number; + /** + * height part of the svg tag if viewBox is not specified + */ + height: number; + + opacity: string | null; +}; \ No newline at end of file diff --git a/src/parser/elements_parser.ts b/src/parser/elements_parser.ts index da56697c775..0a0ba960fd1 100644 --- a/src/parser/elements_parser.ts +++ b/src/parser/elements_parser.ts @@ -79,7 +79,7 @@ import { capitalize, invertTransform, multiplyTransformMatrices, qrDecompose } f const gradientDef = this.extractPropertyDefinition(obj, property, 'gradientDefs'); if (gradientDef) { const opacityAttr = el.getAttribute(property + '-opacity'); - const gradient = fabric.Gradient.fromElement(gradientDef, obj, opacityAttr, this.options); + const gradient = fabric.Gradient.fromElement(gradientDef, obj, { ...this.options, opacity: opacityAttr }); obj.set(property, gradient); } }; diff --git a/src/parser/percent.ts b/src/parser/percent.ts new file mode 100644 index 00000000000..e51e0256284 --- /dev/null +++ b/src/parser/percent.ts @@ -0,0 +1,23 @@ +import { ifNaN } from "../util/internals"; +import { capValue } from "../util/misc/capValue"; + +const RE_PERCENT = /^(\d+\.\d+)%|(\d+)%$/; + +export function isPercent(value: string | null) { + return value && RE_PERCENT.test(value); +} + +/** + * + * @param value + * @param valueIfNaN + * @returns ∈ [0, 1] + */ +export function parsePercent(value: string | number | null | undefined, valueIfNaN?: number) { + const parsed = typeof value === 'number' ? + value : + typeof value === 'string' ? + parseFloat(value) / (isPercent(value) ? 100 : 1) : + NaN; + return capValue(0, ifNaN(parsed, valueIfNaN), 1) +} diff --git a/src/typedefs.ts b/src/typedefs.ts index a9712952819..e0dbf689a54 100644 --- a/src/typedefs.ts +++ b/src/typedefs.ts @@ -1,4 +1,5 @@ -//@ts-nocheck +// https://www.typescriptlang.org/docs/handbook/utility-types.html + interface NominalTag { 'nominalTag': T; } @@ -11,6 +12,13 @@ const enum Radian { } export type TDegree = Nominal; export type TRadian = Nominal; +export type TSize = { + width: number; + height: number; +} + +export type Percent = `${number}%`; + export const enum StrokeLineJoin { miter = 'miter', bevel = 'bevel', diff --git a/src/util/internals/ifNaN.ts b/src/util/internals/ifNaN.ts new file mode 100644 index 00000000000..70c2d109396 --- /dev/null +++ b/src/util/internals/ifNaN.ts @@ -0,0 +1,10 @@ + +/** + * + * @param value value to check if NaN + * @param [valueIfNaN] + * @returns `fallback` is `value is NaN + */ +export const ifNaN = (value: number, valueIfNaN?: number) => { + return isNaN(value) && typeof valueIfNaN === 'number' ? valueIfNaN : value; +} \ No newline at end of file diff --git a/src/util/internals/index.ts b/src/util/internals/index.ts index 6a78faf68b1..58829c2edd5 100644 --- a/src/util/internals/index.ts +++ b/src/util/internals/index.ts @@ -1,2 +1,4 @@ -export { removeFromArray } from './removeFromArray'; export { getRandomInt } from './getRandomInt'; +export { ifNaN } from './ifNaN'; +export { removeFromArray } from './removeFromArray'; + diff --git a/src/util/internals/removeFromArray.ts b/src/util/internals/removeFromArray.ts index dfd72915f4c..d2fdda2db15 100644 --- a/src/util/internals/removeFromArray.ts +++ b/src/util/internals/removeFromArray.ts @@ -7,8 +7,7 @@ * @param {*} value * @return {Array} original array */ - -export const removeFromArray = (array: T[], value: T): T[] => { +export const removeFromArray = (array: T[], value: T): T[] => { const idx = array.indexOf(value); if (idx !== -1) { array.splice(idx, 1); diff --git a/test/unit/gradient.js b/test/unit/gradient.js index 7fd6fcaa546..8a02c462532 100644 --- a/test/unit/gradient.js +++ b/test/unit/gradient.js @@ -215,7 +215,7 @@ element.appendChild(stop2); var object = new fabric.Object({ width: 100, height: 100 }); - var gradient = fabric.Gradient.fromElement(element, object, ''); + var gradient = fabric.Gradient.fromElement(element, object, { opacity: '' }); assert.ok(gradient instanceof fabric.Gradient); assert.equal(gradient.type, 'linear'); @@ -258,7 +258,7 @@ element.appendChild(stop2); var object = new fabric.Object({ width: 200, height: 200 }); - var gradient = fabric.Gradient.fromElement(element, object, ''); + var gradient = fabric.Gradient.fromElement(element, object, { opacity: '' }); assert.ok(gradient instanceof fabric.Gradient); @@ -293,7 +293,8 @@ element.appendChild(stop2); var object = new fabric.Object({left: 10, top: 15, width: 200, height: 200 }); - var gradient = fabric.Gradient.fromElement(element, object, '', { + var gradient = fabric.Gradient.fromElement(element, object, { + opacity: '', viewBoxWidth: 400, viewBoxHeight: 300, }); @@ -331,7 +332,7 @@ element.appendChild(stop2); var object = new fabric.Object({ width: 100, height: 300, top: 20, left: 30 }); - var gradient = fabric.Gradient.fromElement(element, object, ''); + var gradient = fabric.Gradient.fromElement(element, object, { opacity: '' }); assert.ok(gradient instanceof fabric.Gradient); @@ -367,7 +368,7 @@ element.appendChild(stop2); var object = new fabric.Object({ width: 100, height: 100 }); - var gradient = fabric.Gradient.fromElement(element, object, ''); + var gradient = fabric.Gradient.fromElement(element, object, { opacity: '' }); assert.ok(gradient instanceof fabric.Gradient); @@ -387,13 +388,13 @@ element.setAttributeNS(namespace, 'y2', 'Infinity'); var object = new fabric.Object({ width: 200, height: 200 }); - var gradient = fabric.Gradient.fromElement(element, object, ''); + var gradient = fabric.Gradient.fromElement(element, object, { opacity: '' }); assert.equal(gradient.coords.x1, 0.3); assert.equal(gradient.coords.y1, 0.1); assert.equal(gradient.coords.x2, 0.2); assert.equal(gradient.coords.y2, 1); object = new fabric.Object({ width: 200, height: 200, top: 50, left: 10 }); - gradient = fabric.Gradient.fromElement(element, object, ''); + gradient = fabric.Gradient.fromElement(element, object, { opacity: '' }); assert.equal(gradient.coords.x1, 0.3, 'top and left do not change the output'); assert.equal(gradient.coords.y1, 0.1, 'top and left do not change the output'); assert.equal(gradient.coords.x2, 0.2, 'top and left do not change the output'); @@ -413,7 +414,7 @@ element.setAttributeNS(namespace, 'r', '100%'); var object = new fabric.Object({ width: 200, height: 200 }); - var gradient = fabric.Gradient.fromElement(element, object, ''); + var gradient = fabric.Gradient.fromElement(element, object, { opacity: '' }); assert.equal(gradient.coords.x1, 0.3, 'should not change with width height'); assert.equal(gradient.coords.y1, 0.2, 'should not change with width height'); assert.equal(gradient.coords.x2, 0.1, 'should not change with width height'); @@ -422,7 +423,7 @@ assert.equal(gradient.coords.r2, 1, 'should not change with width height'); object = new fabric.Object({ width: 200, height: 200, top: 10, left: 10 }); - gradient = fabric.Gradient.fromElement(element, object, ''); + gradient = fabric.Gradient.fromElement(element, object, { opacity: '' }); assert.equal(gradient.coords.x1, 0.3, 'should not change with top left'); assert.equal(gradient.coords.y1, 0.2, 'should not change with top left'); assert.equal(gradient.coords.x2, 0.1, 'should not change with top left'); @@ -445,7 +446,7 @@ element.setAttributeNS(namespace, 'gradientUnits', 'userSpaceOnUse'); var object = new fabric.Object({ width: 200, height: 200 }); - var gradient = fabric.Gradient.fromElement(element, object, ''); + var gradient = fabric.Gradient.fromElement(element, object, { opacity: '' }); assert.equal(gradient.coords.x1, 30, 'should not change with width height'); assert.equal(gradient.coords.y1, 20, 'should not change with width height'); assert.equal(gradient.coords.x2, 15, 'should not change with width height'); @@ -454,7 +455,7 @@ assert.equal(gradient.coords.r2, 100, 'should not change with width height'); object = new fabric.Object({ width: 200, height: 200, top: 50, left: 60 }); - gradient = fabric.Gradient.fromElement(element, object, ''); + gradient = fabric.Gradient.fromElement(element, object, { opacity: '' }); assert.equal(gradient.coords.x1, 30, 'should not change with top left'); assert.equal(gradient.coords.y1, 20, 'should not change with top left'); assert.equal(gradient.coords.x2, 15, 'should not change with top left'); @@ -476,14 +477,14 @@ element.setAttributeNS(namespace, 'gradientUnits', 'userSpaceOnUse'); var object = new fabric.Object({ width: 200, height: 200 }); - var gradient = fabric.Gradient.fromElement(element, object, ''); + var gradient = fabric.Gradient.fromElement(element, object, { opacity: '' }); assert.equal(gradient.coords.x1, 30, 'should not change with width height'); assert.equal(gradient.coords.y1, 20, 'should not change with width height'); assert.equal(gradient.coords.x2, 15, 'should not change with width height'); assert.equal(gradient.coords.y2, 18, 'should not change with width height'); object = new fabric.Object({ width: 200, height: 200, top: 40, left: 40 }); - gradient = fabric.Gradient.fromElement(element, object, ''); + gradient = fabric.Gradient.fromElement(element, object, { opacity: '' }); assert.equal(gradient.coords.x1, 30, 'should not change with top left'); assert.equal(gradient.coords.y1, 20, 'should not change with top left'); assert.equal(gradient.coords.x2, 15, 'should not change with top left'); @@ -508,7 +509,7 @@ element.appendChild(stop2); var object = new fabric.Object({ width: 100, height: 100 }); - var gradient = fabric.Gradient.fromElement(element, object, '', {}); + var gradient = fabric.Gradient.fromElement(element, object, {}); assert.ok(gradient instanceof fabric.Gradient); @@ -544,7 +545,7 @@ element.appendChild(stop2); element.setAttributeNS(namespace, 'gradientTransform', 'matrix(3.321 -0.6998 0.4077 1.9347 -440.9168 -408.0598)'); var object = new fabric.Object({ width: 100, height: 100 }); - var gradient = fabric.Gradient.fromElement(element, object, '', {}); + var gradient = fabric.Gradient.fromElement(element, object, {}); assert.deepEqual(gradient.gradientTransform, [3.321, -0.6998, 0.4077, 1.9347, -440.9168, -408.0598]); }); @@ -581,7 +582,7 @@ element.appendChild(stop4); var object = new fabric.Object({ width: 100, height: 100 }); - var gradient = fabric.Gradient.fromElement(element, object, ''); + var gradient = fabric.Gradient.fromElement(element, object, { opacity: '' }); assert.ok(gradient instanceof fabric.Gradient); @@ -639,7 +640,7 @@ element.appendChild(stop4); var object = new fabric.Object({ width: 100, height: 100 }); - var gradient = fabric.Gradient.fromElement(element, object, ''); + var gradient = fabric.Gradient.fromElement(element, object, { opacity: '' }); assert.ok(gradient instanceof fabric.Gradient); diff --git a/test/visual/golden/text7.png b/test/visual/golden/text7.png index 10198be0f1d..b0c7c2b7ec2 100644 Binary files a/test/visual/golden/text7.png and b/test/visual/golden/text7.png differ diff --git a/test/visual/text.js b/test/visual/text.js index f7a1e417b7f..8ce840708aa 100644 --- a/test/visual/text.js +++ b/test/visual/text.js @@ -229,10 +229,10 @@ color: 'blue' }] }); - var text = new fabric.Text('PERCENTAGE GRADIENT\nPERCENTAGE GRADIENT\nPERCENTAGE GRADIENT', { + var text = new fabric.Text('PERCENT GRADIENT\nPERCENT GRADIENT\nPERCENT GRADIENT', { left: 0, top: 0, - fontSize: 16, + fontSize: 32, fill: gradient, }); canvas.add(text); @@ -243,8 +243,10 @@ tests.push({ test: 'Text percentage gradient', code: text7, + width: 350, + height: 150, golden: 'text7.png', - percentage: 0.06, + percentage: 0.04, }); function text8(canvas, callback) {