diff --git a/src/elements_parser.js b/src/elements_parser.js index 0bb377f0b9c..cccb2d783cb 100644 --- a/src/elements_parser.js +++ b/src/elements_parser.js @@ -73,7 +73,7 @@ fabric.ElementsParser = function(elements, callback, options, reviver, parsingOp var gradientDef = this.extractPropertyDefinition(obj, property, 'gradientDefs'); if (gradientDef) { var opacityAttr = el.getAttribute(property + '-opacity'); - var gradient = fabric.Gradient.fromElement(gradientDef, obj, opacityAttr); + var gradient = fabric.Gradient.fromElement(gradientDef, obj, opacityAttr, this.options); obj.set(property, gradient); } }; diff --git a/src/gradient.class.js b/src/gradient.class.js index b3313b53e67..a909d23c818 100644 --- a/src/gradient.class.js +++ b/src/gradient.class.js @@ -95,18 +95,67 @@ */ 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 Array[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. + * @type String pixels || percentage + * @default 'pixels' + */ + gradientUnits: 'pixels', + + /** + * Gradient type + * @type String linear || radial + * @default 'pixels' + */ + type: 'linear', + /** * Constructor - * @param {Object} [options] Options object with type, coords, gradientUnits and colorStops + * @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 {Array[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 = { }; + var coords, _this = this; - this.id = fabric.Object.__uid++; - this.type = options.type || 'linear'; + // 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, @@ -119,13 +168,9 @@ coords.r1 = options.coords.r1 || 0; coords.r2 = options.coords.r2 || 0; } + this.coords = coords; this.colorStops = options.colorStops.slice(); - if (options.gradientTransform) { - this.gradientTransform = options.gradientTransform; - } - this.offsetX = options.offsetX || this.offsetX; - this.offsetY = options.offsetY || this.offsetY; }, /** @@ -157,6 +202,7 @@ 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); @@ -175,23 +221,33 @@ markup, commonAttributes, colorStops = clone(this.colorStops, true), needsSwap = coords.r1 > coords.r2, transform = this.gradientTransform ? this.gradientTransform.concat() : fabric.iMatrix.concat(), - offsetX = object.width / 2 - this.offsetX, offsetY = object.height / 2 - this.offsetY, - withViewport = !!options.additionalTransform; + 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') { offsetX -= object.pathOffset.x; offsetY -= object.pathOffset.y; } + transform[4] -= offsetX; transform[5] -= offsetY; commonAttributes = 'id="SVGID_' + this.id + - '" gradientUnits="userSpaceOnUse"'; + '" gradientUnits="' + gradientUnits + '"'; commonAttributes += ' gradientTransform="' + (withViewport ? options.additionalTransform + ' ' : '') + fabric.util.matrixToSVG(transform) + '" '; @@ -303,11 +359,17 @@ * @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 graidents + * 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) { + fromElement: function(el, instance, opacityAttr, svgOptions) { /** * @example: * @@ -349,22 +411,18 @@ var colorStopEls = el.getElementsByTagName('stop'), type, - gradientUnits = el.getAttribute('gradientUnits') || 'objectBoundingBox', - gradientTransform = el.getAttribute('gradientTransform'), + gradientUnits = el.getAttribute('gradientUnits') === 'userSpaceOnUse' ? + 'pixels' : 'percentage', + gradientTransform = el.getAttribute('gradientTransform') || '', colorStops = [], - coords, ellipseMatrix, i; - + coords, i, offsetX = 0, offsetY = 0, + transformMatrix; if (el.nodeName === 'linearGradient' || el.nodeName === 'LINEARGRADIENT') { type = 'linear'; + coords = getLinearCoords(el); } else { type = 'radial'; - } - - if (type === 'linear') { - coords = getLinearCoords(el); - } - else if (type === 'radial') { coords = getRadialCoords(el); } @@ -372,34 +430,46 @@ colorStops.push(getColorStop(colorStopEls[i], multiplier)); } - ellipseMatrix = _convertPercentUnitsToValues(instance, coords, gradientUnits); + transformMatrix = fabric.parseTransformAttribute(gradientTransform); + + __convertPercentUnitsToValues(instance, coords, svgOptions, gradientUnits); + + if (gradientUnits === 'pixels') { + offsetX = -instance.left; + offsetY = -instance.top; + } var gradient = new fabric.Gradient({ + id: el.getAttribute('id'), type: type, coords: coords, colorStops: colorStops, - offsetX: -instance.left, - offsetY: -instance.top + gradientUnits: gradientUnits, + gradientTransform: transformMatrix, + offsetX: offsetX, + offsetY: offsetY, }); - if (gradientTransform || ellipseMatrix !== '') { - gradient.gradientTransform = fabric.parseTransformAttribute((gradientTransform || '') + ellipseMatrix); - } - return gradient; }, /* _FROM_SVG_END_ */ /** * Returns {@link fabric.Gradient} instance from its object representation + * this function is uniquely used by Object.setGradient and is deprecated with it. * @static + * @deprecated since 3.4.0 * @memberOf fabric.Gradient * @param {Object} obj * @param {Object} [options] Options object */ forObject: function(obj, options) { options || (options = { }); - _convertPercentUnitsToValues(obj, options.coords, 'userSpaceOnUse'); + __convertPercentUnitsToValues(obj, options.coords, options.gradientUnits, { + // those values are to avoid errors. this function is uniquely used by + viewBoxWidth: 100, + viewBoxHeight: 100, + }); return new fabric.Gradient(options); } }); @@ -407,46 +477,32 @@ /** * @private */ - function _convertPercentUnitsToValues(object, options, gradientUnits) { - var propValue, addFactor = 0, multFactor = 1, ellipseMatrix = ''; - for (var prop in options) { - if (options[prop] === 'Infinity') { - options[prop] = 1; - } - else if (options[prop] === '-Infinity') { - options[prop] = 0; + function __convertPercentUnitsToValues(instance, options, svgOptions, gradientUnits) { + var propValue, finalValue; + Object.keys(options).forEach(function(prop) { + propValue = options[prop]; + if (propValue === 'Infinity') { + finalValue = 1; } - propValue = parseFloat(options[prop], 10); - if (typeof options[prop] === 'string' && /^(\d+\.\d+)%|(\d+)%$/.test(options[prop])) { - multFactor = 0.01; + else if (propValue === '-Infinity') { + finalValue = 0; } else { - multFactor = 1; - } - if (prop === 'x1' || prop === 'x2' || prop === 'r2') { - multFactor *= gradientUnits === 'objectBoundingBox' ? object.width : 1; - addFactor = gradientUnits === 'objectBoundingBox' ? object.left || 0 : 0; - } - else if (prop === 'y1' || prop === 'y2') { - multFactor *= gradientUnits === 'objectBoundingBox' ? object.height : 1; - addFactor = gradientUnits === 'objectBoundingBox' ? object.top || 0 : 0; - } - options[prop] = propValue * multFactor + addFactor; - } - if (object.type === 'ellipse' && - options.r2 !== null && - gradientUnits === 'objectBoundingBox' && - object.rx !== object.ry) { - - var scaleFactor = object.ry / object.rx; - ellipseMatrix = ' scale(1, ' + scaleFactor + ')'; - if (options.y1) { - options.y1 /= scaleFactor; - } - if (options.y2) { - options.y2 /= scaleFactor; + 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; + } + } + } } - } - return ellipseMatrix; + options[prop] = finalValue; + }); } })(); diff --git a/src/mixins/object_geometry.mixin.js b/src/mixins/object_geometry.mixin.js index 90aa87ee75b..f35b337b801 100644 --- a/src/mixins/object_geometry.mixin.js +++ b/src/mixins/object_geometry.mixin.js @@ -478,11 +478,7 @@ * @return {Array} rotation matrix for the object */ _calcRotateMatrix: function() { - if (this.angle) { - var theta = degreesToRadians(this.angle), cos = fabric.util.cos(theta), sin = fabric.util.sin(theta); - return [cos, sin, -sin, cos, 0, 0]; - } - return fabric.iMatrix.concat(); + return fabric.util.calcRotateMatrix(this); }, /** @@ -527,41 +523,41 @@ return matrix; }, + /** + * calculate transform matrix that represents the current transformations from the + * object's properties, this matrix does not include the group transformation + * @return {Array} transform matrix for the object + */ calcOwnMatrix: function() { var key = this.transformMatrixKey(true), cache = this.ownMatrixCache || (this.ownMatrixCache = {}); if (cache.key === key) { return cache.value; } - var matrix = this._calcTranslateMatrix(), - rotateMatrix, - dimensionMatrix = this._calcDimensionsTransformMatrix(this.skewX, this.skewY, true); - if (this.angle) { - rotateMatrix = this._calcRotateMatrix(); - matrix = multiplyMatrices(matrix, rotateMatrix); - } - matrix = multiplyMatrices(matrix, dimensionMatrix); + var tMatrix = this._calcTranslateMatrix(); + this.translateX = tMatrix[4]; + this.translateY = tMatrix[5]; cache.key = key; - cache.value = matrix; - return matrix; + cache.value = fabric.util.composeMatrix(this); + return cache.value; }, + /* + * Calculate object dimensions from its properties + * @private + * @deprecated since 3.4.0, please use fabric.util._calcDimensionsTransformMatrix + * not including or including flipX, flipY to emulate the flipping boolean + * @return {Object} .x width dimension + * @return {Object} .y height dimension + */ _calcDimensionsTransformMatrix: function(skewX, skewY, flipping) { - var skewMatrix, - scaleX = this.scaleX * (flipping && this.flipX ? -1 : 1), - scaleY = this.scaleY * (flipping && this.flipY ? -1 : 1), - scaleMatrix = [scaleX, 0, 0, scaleY, 0, 0]; - if (skewX) { - skewMatrix = [1, 0, Math.tan(degreesToRadians(skewX)), 1]; - scaleMatrix = multiplyMatrices(scaleMatrix, skewMatrix, true); - } - if (skewY) { - skewMatrix = [1, Math.tan(degreesToRadians(skewY)), 0, 1]; - scaleMatrix = multiplyMatrices(scaleMatrix, skewMatrix, true); - } - return scaleMatrix; + return fabric.util.calcDimensionsMatrix({ + skewX: skewX, + skewY: skewY, + scaleX: this.scaleX * (flipping && this.flipX ? -1 : 1), + scaleY: this.scaleY * (flipping && this.flipY ? -1 : 1) + }); }, - /* * Calculate object dimensions from its properties * @private @@ -625,12 +621,13 @@ x: dimX, y: dimY }], - i, transformMatrix = this._calcDimensionsTransformMatrix(skewX, skewY, false), - bbox; - for (i = 0; i < points.length; i++) { - points[i] = fabric.util.transformPoint(points[i], transformMatrix); - } - bbox = fabric.util.makeBoundingBoxFromPoints(points); + transformMatrix = fabric.util.calcDimensionsMatrix({ + scaleX: this.scaleX, + scaleY: this.scaleY, + skewX: this.skewX, + skewY: this.skewY, + }), + bbox = fabric.util.makeBoundingBoxFromPoints(points, transformMatrix); return this._finalizeDimensions(bbox.width, bbox.height); }, diff --git a/src/mixins/object_interactivity.mixin.js b/src/mixins/object_interactivity.mixin.js index e1229d49349..1b3a2e847da 100644 --- a/src/mixins/object_interactivity.mixin.js +++ b/src/mixins/object_interactivity.mixin.js @@ -195,7 +195,11 @@ drawBordersInGroup: function(ctx, options, styleOverride) { styleOverride = styleOverride || {}; var p = this._getNonTransformedDimensions(), - matrix = fabric.util.customTransformMatrix(options.scaleX, options.scaleY, options.skewX), + matrix = fabric.util.composeMatrix({ + scaleX: options.scaleX, + scaleY: options.scaleY, + skewX: options.skewX + }), wh = fabric.util.transformPoint(p, matrix), strokeWidth = 1 / this.borderScaleFactor, width = wh.x + strokeWidth, diff --git a/src/parser.js b/src/parser.js index 6e27c7683fa..f8428bef845 100644 --- a/src/parser.js +++ b/src/parser.js @@ -559,12 +559,14 @@ parsedDim.height = parseUnit(heightAttr); return parsedDim; } - minX = -parseFloat(viewBoxAttr[1]); minY = -parseFloat(viewBoxAttr[2]); viewBoxWidth = parseFloat(viewBoxAttr[3]); viewBoxHeight = parseFloat(viewBoxAttr[4]); - + parsedDim.minX = minX; + parsedDim.minY = minY; + parsedDim.viewBoxWidth = viewBoxWidth; + parsedDim.viewBoxHeight = viewBoxHeight; if (!missingDimAttr) { parsedDim.width = parseUnit(widthAttr); parsedDim.height = parseUnit(heightAttr); @@ -723,7 +725,7 @@ recursivelyParseGradientsXlink(doc, referencedGradient); } gradientsAttrs.forEach(function(attr) { - if (!gradient.hasAttribute(attr)) { + if (referencedGradient && !gradient.hasAttribute(attr) && referencedGradient.hasAttribute(attr)) { gradient.setAttribute(attr, referencedGradient.getAttribute(attr)); } }); diff --git a/src/shapes/object.class.js b/src/shapes/object.class.js index 7d98c72e36a..fd449dab63f 100644 --- a/src/shapes/object.class.js +++ b/src/shapes/object.class.js @@ -1474,7 +1474,13 @@ var t = filler.gradientTransform || filler.patternTransform; var offsetX = -this.width / 2 + filler.offsetX || 0, offsetY = -this.height / 2 + filler.offsetY || 0; - ctx.translate(offsetX, offsetY); + + if (filler.gradientUnits === 'percentage') { + ctx.transform(this.width, 0, 0, this.height, offsetX, offsetY); + } + else { + ctx.transform(1, 0, 0, 1, offsetX, offsetY); + } if (t) { ctx.transform(t[0], t[1], t[2], t[3], t[4], t[5]); } @@ -1527,6 +1533,10 @@ ctx.restore(); }, + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + */ _renderStroke: function(ctx) { if (!this.stroke || this.strokeWidth === 0) { return; @@ -1541,11 +1551,56 @@ ctx.scale(1 / this.scaleX, 1 / this.scaleY); } this._setLineDash(ctx, this.strokeDashArray, this._renderDashedStroke); - this._applyPatternGradientTransform(ctx, this.stroke); + if (this.stroke.toLive && this.stroke.gradientUnits === 'percentage') { + // need to transform gradient in a pattern. + // this is a slow process. If you are hitting this codepath, and the object + // is not using caching, you should consider switching it on. + // we need a canvas as big as the current object caching canvas. + this._applyPatternForTransformedGradient(ctx, this.stroke); + } + else { + this._applyPatternGradientTransform(ctx, this.stroke); + } ctx.stroke(); ctx.restore(); }, + /** + * This function try to patch the missing gradientTransform on canvas gradients. + * transforming a context to transform the gradient, is going to transform the stroke too. + * we want to transform the gradient but not the stroke operation, so we create + * a transformed gradient on a pattern and then we use the pattern instead of the gradient. + * this method has drwabacks: is slow, is in low resolution, needs a patch for when the size + * is limited. + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {fabric.Gradient} filler a fabric gradient instance + */ + _applyPatternForTransformedGradient: function(ctx, filler) { + var dims = this._limitCacheSize(this._getCacheCanvasDimensions()), + pCanvas = fabric.util.createCanvasElement(), pCtx, retinaScaling = this.canvas.getRetinaScaling(), + width = dims.x / this.scaleX / retinaScaling, height = dims.y / this.scaleY / retinaScaling; + pCanvas.width = width; + pCanvas.height = height; + pCtx = pCanvas.getContext('2d'); + pCtx.beginPath(); pCtx.moveTo(0, 0); pCtx.lineTo(width, 0); pCtx.lineTo(width, height); + pCtx.lineTo(0, height); pCtx.closePath(); + pCtx.translate(width / 2, height / 2); + pCtx.scale( + dims.zoomX / this.scaleX / retinaScaling, + dims.zoomY / this.scaleY / retinaScaling + ); + this._applyPatternGradientTransform(pCtx, filler); + pCtx.fillStyle = filler.toLive(ctx); + pCtx.fill(); + ctx.translate(-this.width / 2 - this.strokeWidth / 2, -this.height / 2 - this.strokeWidth / 2); + ctx.scale( + retinaScaling * this.scaleX / dims.zoomX, + retinaScaling * this.scaleY / dims.zoomY + ); + ctx.strokeStyle = pCtx.createPattern(pCanvas, 'no-repeat'); + }, + /** * This function is an helper for svg import. it returns the center of the object in the svg * untransformed coordinates @@ -1766,6 +1821,7 @@ /** * Sets gradient (fill or stroke) of an object + * percentages for x1,x2,y1,y2,r1,r2 together with gradientUnits 'pixels', are not supported. * Backwards incompatibility note: This method was named "setGradientFill" until v1.1.0 * @param {String} property Property name 'stroke' or 'fill' * @param {Object} [options] Options object @@ -1780,6 +1836,7 @@ * @param {Object} [options.gradientTransform] transformMatrix for gradient * @return {fabric.Object} thisArg * @chainable + * @deprecated since 3.4.0 * @see {@link http://jsfiddle.net/fabricjs/58y8b/|jsFiddle demo} * @example Set linear gradient * object.setGradient('fill', { @@ -1824,7 +1881,7 @@ x2: options.x2, y2: options.y2 }; - + gradient.gradientUnits = options.gradientUnits || 'pixels'; if (options.r1 || options.r2) { gradient.coords.r1 = options.r1; gradient.coords.r2 = options.r2; diff --git a/src/util/misc.js b/src/util/misc.js index 8e9a85381b0..097d4080eb3 100644 --- a/src/util/misc.js +++ b/src/util/misc.js @@ -3,7 +3,6 @@ var sqrt = Math.sqrt, atan2 = Math.atan2, pow = Math.pow, - abs = Math.abs, PiBy180 = Math.PI / 180, PiBy2 = Math.PI / 2; @@ -165,9 +164,15 @@ /** * Returns coordinates of points's bounding rectangle (left, top, width, height) * @param {Array} points 4 points array + * @param {Array} [transform] an array of 6 numbers representing a 2x3 transform matrix * @return {Object} Object with left, top, width, height properties */ - makeBoundingBoxFromPoints: function(points) { + makeBoundingBoxFromPoints: function(points, transform) { + if (transform) { + for (var i = 0; i < points.length; i++) { + points[i] = fabric.util.transformPoint(points[i], transform); + } + } var xPoints = [points[0].x, points[1].x, points[2].x, points[3].x], minX = fabric.util.array.min(xPoints), maxX = fabric.util.array.max(xPoints), @@ -658,7 +663,7 @@ }, /** - * Decomposes standard 2x2 matrix into transform componentes + * Decomposes standard 2x3 matrix into transform components * @static * @memberOf fabric.util * @param {Array} a transformMatrix @@ -681,10 +686,113 @@ }; }, + /** + * Returns a transform matrix starting from an object of the same kind of + * the one returned from qrDecompose, useful also if you want to calculate some + * transformations from an object that is not enlived yet + * @static + * @memberOf fabric.util + * @param {Object} options + * @param {Number} [options.angle] angle in degrees + * @return {Array[Number]} transform matrix + */ + calcRotateMatrix: function(options) { + if (!options.angle) { + return fabric.iMatrix.concat(); + } + var theta = fabric.util.degreesToRadians(options.angle), + cos = fabric.util.cos(theta), + sin = fabric.util.sin(theta); + return [cos, sin, -sin, cos, 0, 0]; + }, + + /** + * Returns a transform matrix starting from an object of the same kind of + * the one returned from qrDecompose, useful also if you want to calculate some + * transformations from an object that is not enlived yet. + * is called DimensionsTransformMatrix because those properties are the one that influence + * the size of the resulting box of the object. + * @static + * @memberOf fabric.util + * @param {Object} options + * @param {Number} [options.scaleX] + * @param {Number} [options.scaleY] + * @param {Boolean} [options.flipX] + * @param {Boolean} [options.flipY] + * @param {Number} [options.skewX] + * @param {Number} [options.skewX] + * @return {Array[Number]} transform matrix + */ + calcDimensionsMatrix: function(options) { + var scaleX = typeof options.scaleX === 'undefined' ? 1 : options.scaleX, + scaleY = typeof options.scaleY === 'undefined' ? 1 : options.scaleY, + scaleMatrix = [ + options.flipX ? -scaleX : scaleX, + 0, + 0, + options.flipY ? -scaleY : scaleY, + 0, + 0], + multiply = fabric.util.multiplyTransformMatrices, + degreesToRadians = fabric.util.degreesToRadians; + if (options.skewX) { + scaleMatrix = multiply( + scaleMatrix, + [1, 0, Math.tan(degreesToRadians(options.skewX)), 1], + true); + } + if (options.skewY) { + scaleMatrix = multiply( + scaleMatrix, + [1, Math.tan(degreesToRadians(options.skewY)), 0, 1], + true); + } + return scaleMatrix; + }, + + /** + * Returns a transform matrix starting from an object of the same kind of + * the one returned from qrDecompose, useful also if you want to calculate some + * transformations from an object that is not enlived yet + * @static + * @memberOf fabric.util + * @param {Object} options + * @param {Number} [options.angle] + * @param {Number} [options.scaleX] + * @param {Number} [options.scaleY] + * @param {Boolean} [options.flipX] + * @param {Boolean} [options.flipY] + * @param {Number} [options.skewX] + * @param {Number} [options.skewX] + * @param {Number} [options.translateX] + * @param {Number} [options.translateY] + * @return {Array[Number]} transform matrix + */ + composeMatrix: function(options) { + var matrix = [1, 0, 0, 1, options.translateX || 0, options.translateY || 0], + multiply = fabric.util.multiplyTransformMatrices; + if (options.angle) { + matrix = multiply(matrix, fabric.util.calcRotateMatrix(options)); + } + if (options.scaleX || options.scaleY || options.skewX || options.skewY || options.flipX || options.flipY) { + matrix = multiply(matrix, fabric.util.calcDimensionsMatrix(options)); + } + return matrix; + }, + + /** + * Returns a transform matrix that has the same effect of scaleX, scaleY and skewX. + * Is deprecated for composeMatrix. Please do not use it. + * @static + * @deprecated since 3.4.0 + * @memberOf fabric.util + * @param {Number} scaleX + * @param {Number} scaleY + * @param {Number} skewX + * @return {Array[Number]} transform matrix + */ customTransformMatrix: function(scaleX, scaleY, skewX) { - var skewMatrixX = [1, 0, abs(Math.tan(skewX * PiBy180)), 1], - scaleMatrix = [abs(scaleX), 0, 0, abs(scaleY)]; - return fabric.util.multiplyTransformMatrices(scaleMatrix, skewMatrixX, true); + return fabric.util.composeMatrix({ scaleX: scaleX, scaleY: scaleY, skewX: skewX }); }, /** diff --git a/test/lib/visualTestLoop.js b/test/lib/visualTestLoop.js index dd82456fd32..89d4528d71e 100644 --- a/test/lib/visualTestLoop.js +++ b/test/lib/visualTestLoop.js @@ -49,6 +49,7 @@ var finalName = '/assets/' + filename + '.svg'; return fabric.isLikelyNode ? localPath('/../visual', finalName) : getAbsolutePath('/test/visual' + finalName); } + exports.getAssetName = getAssetName; function getGoldeName(filename) { var finalName = '/golden/' + filename; diff --git a/test/node_test_setup.js b/test/node_test_setup.js index c5c79803a34..36b8200b959 100644 --- a/test/node_test_setup.js +++ b/test/node_test_setup.js @@ -9,6 +9,7 @@ global.visualCallback = { global.visualTestLoop = require('./lib/visualTestLoop').visualTestLoop; global.getFixture = require('./lib/visualTestLoop').getFixture; global.getAsset = require('./lib/visualTestLoop').getAsset; +global.getAssetName = require('./lib/visualTestLoop').getAssetName; global.imageDataToChalk = function(imageData) { // actually this does not work on travis-ci, so commenting it out return ''; diff --git a/test/unit/gradient.js b/test/unit/gradient.js index 7c32f1b18fe..dc7d26bce96 100644 --- a/test/unit/gradient.js +++ b/test/unit/gradient.js @@ -2,9 +2,10 @@ QUnit.module('fabric.Gradient'); - function createLinearGradient() { + function createLinearGradient(units) { return new fabric.Gradient({ type: 'linear', + gradientUnits: units || 'pixels', coords: { x1: 0, y1: 10, @@ -18,9 +19,10 @@ }); } - function createRadialGradient() { + function createRadialGradient(units) { return new fabric.Gradient({ type: 'radial', + gradientUnits: units || 'pixels', coords: { x1: 0, y1: 10, @@ -76,7 +78,8 @@ var SVG_RADIAL = '\n\n\n\n'; var SVG_INTERNALRADIUS = '\n\n\n\n'; var SVG_SWAPPED = '\n\n\n\n'; - + var SVG_LINEAR_PERCENTAGE = '\n\n\n\n'; + var SVG_RADIAL_PERCENTAGE = '\n\n\n\n'; QUnit.test('constructor linearGradient', function(assert) { assert.ok(fabric.Gradient); @@ -143,7 +146,7 @@ assert.equal(object.coords.x2, gradient.coords.x2); assert.equal(object.coords.y1, gradient.coords.y1); assert.equal(object.coords.y2, gradient.coords.y2); - + assert.equal(object.gradientUnits, gradient.gradientUnits); assert.equal(object.type, gradient.type); assert.deepEqual(object.gradientTransform, gradient.gradientTransform); assert.equal(object.colorStops, gradient.colorStops); @@ -212,11 +215,12 @@ var gradient = fabric.Gradient.fromElement(element, object, ''); assert.ok(gradient instanceof fabric.Gradient); - + assert.equal(gradient.type, 'linear'); assert.equal(gradient.coords.x1, 0); assert.equal(gradient.coords.y1, 0); - assert.equal(gradient.coords.x2, 100); + assert.equal(gradient.coords.x2, 1); assert.equal(gradient.coords.y2, 0); + assert.equal(gradient.gradientUnits, 'percentage'); assert.equal(gradient.colorStops[0].offset, 1); assert.equal(gradient.colorStops[1].offset, 0); @@ -254,10 +258,11 @@ assert.ok(gradient instanceof fabric.Gradient); - assert.equal(gradient.coords.x1, 20); - assert.equal(gradient.coords.y1, 0.4); - assert.equal(gradient.coords.x2, 40000); - assert.equal(gradient.coords.y2, 40); + assert.equal(gradient.coords.x1, 0.1); + assert.equal(gradient.coords.y1, 0.002); + assert.equal(gradient.coords.x2, 200); + assert.equal(gradient.coords.y2, 0.2); + assert.equal(gradient.gradientUnits, 'percentage'); }); QUnit.test('fromElement linearGradient with floats percentage - userSpaceOnUse', function(assert) { @@ -282,15 +287,20 @@ element.appendChild(stop1); element.appendChild(stop2); - var object = new fabric.Object({ width: 200, height: 200 }); - var gradient = fabric.Gradient.fromElement(element, object, ''); + var object = new fabric.Object({left: 10, top: 15, width: 200, height: 200 }); + var gradient = fabric.Gradient.fromElement(element, object, '', { + viewBoxWidth: 400, + viewBoxHeight: 300, + }); assert.ok(gradient instanceof fabric.Gradient); - - assert.equal(gradient.coords.x1, 0.1); - assert.equal(gradient.coords.y1, 0.002); + assert.equal(gradient.gradientUnits, 'pixels'); + assert.equal(gradient.offsetX, -10); + assert.equal(gradient.offsetY, -15); + assert.equal(gradient.coords.x1, 40); + assert.equal(gradient.coords.y1, 0.6); assert.equal(gradient.coords.x2, 200); - assert.equal(gradient.coords.y2, 0.2); + assert.equal(gradient.coords.y2, 60); }); QUnit.test('fromElement linearGradient with Infinity', function(assert) { @@ -314,14 +324,14 @@ element.appendChild(stop1); element.appendChild(stop2); - var object = new fabric.Object({ width: 100, height: 100, top: 0, left: 0 }); + var object = new fabric.Object({ width: 100, height: 300, top: 20, left: 30 }); var gradient = fabric.Gradient.fromElement(element, object, ''); assert.ok(gradient instanceof fabric.Gradient); assert.equal(gradient.coords.x1, 0); - assert.equal(gradient.coords.y1, 100); - assert.equal(gradient.coords.x2, 100); + assert.equal(gradient.coords.y1, 1); + assert.equal(gradient.coords.x2, 1); assert.equal(gradient.coords.y2, 0); assert.equal(gradient.colorStops[0].offset, 1); @@ -370,16 +380,16 @@ var object = new fabric.Object({ width: 200, height: 200 }); var gradient = fabric.Gradient.fromElement(element, object, ''); - assert.equal(gradient.coords.x1, 60); - assert.equal(gradient.coords.y1, 20); - assert.equal(gradient.coords.x2, 40); - assert.equal(gradient.coords.y2, 200); + 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, ''); - assert.equal(gradient.coords.x1, 70); - assert.equal(gradient.coords.y1, 70); - assert.equal(gradient.coords.x2, 50); - assert.equal(gradient.coords.y2, 250); + 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'); + assert.equal(gradient.coords.y2, 1, 'top and left do not change the output'); }); QUnit.test('fromElement with x1,x2,y1,2 radial', function(assert) { @@ -395,21 +405,21 @@ var object = new fabric.Object({ width: 200, height: 200 }); var gradient = fabric.Gradient.fromElement(element, object, ''); - assert.equal(gradient.coords.x1, 60, 'should change with width height'); - assert.equal(gradient.coords.y1, 40, 'should change with width height'); - assert.equal(gradient.coords.x2, 20, 'should change with width height'); - assert.equal(gradient.coords.y2, 200, 'should change with width height'); - assert.equal(gradient.coords.r1, 0, 'should change with width height'); - assert.equal(gradient.coords.r2, 200, 'should change with width height'); + 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'); + assert.equal(gradient.coords.y2, 1, 'should not change with width height'); + assert.equal(gradient.coords.r1, 0, 'should not change with width height'); + 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, ''); - assert.equal(gradient.coords.x1, 70, 'should change with top left'); - assert.equal(gradient.coords.y1, 50, 'should change with top left'); - assert.equal(gradient.coords.x2, 30, 'should change with top left'); - assert.equal(gradient.coords.y2, 210, 'should change with top left'); - assert.equal(gradient.coords.r1, 10, 'should change with top left'); - assert.equal(gradient.coords.r2, 210, 'should change with top left'); + 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'); + assert.equal(gradient.coords.y2, 1, 'should not change with top left'); + assert.equal(gradient.coords.r1, 0, 'should not change with top left'); + assert.equal(gradient.coords.r2, 1, 'should not change with top left'); }); QUnit.test('fromElement with x1,x2,y1,2 radial userSpaceOnUse', function(assert) { @@ -469,7 +479,7 @@ assert.equal(gradient.coords.y2, 18, 'should not change with top left'); }); - QUnit.test('fromElement radialGradient', function(assert) { + QUnit.test('fromElement radialGradient defaults', function(assert) { assert.ok(typeof fabric.Gradient.fromElement === 'function'); var element = fabric.document.createElement('radialGradient'); @@ -486,14 +496,16 @@ 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); - assert.equal(gradient.coords.x1, 50); - assert.equal(gradient.coords.y1, 50); - assert.equal(gradient.coords.x2, 50); - assert.equal(gradient.coords.y2, 50); + assert.equal(gradient.coords.x1, 0.5); + assert.equal(gradient.coords.y1, 0.5); + assert.equal(gradient.coords.x2, 0.5); + assert.equal(gradient.coords.y2, 0.5); + assert.equal(gradient.coords.r1, 0); + assert.equal(gradient.coords.r2, 0.5); assert.equal(gradient.colorStops[0].offset, 1); assert.equal(gradient.colorStops[1].offset, 0); @@ -519,20 +531,7 @@ element.appendChild(stop2); element.setAttribute('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, ''); - - assert.ok(gradient instanceof fabric.Gradient); - - assert.equal(gradient.coords.x1, 50); - assert.equal(gradient.coords.y1, 50); - assert.equal(gradient.coords.x2, 50); - assert.equal(gradient.coords.y2, 50); - - assert.equal(gradient.colorStops[0].offset, 1); - assert.equal(gradient.colorStops[1].offset, 0); - - assert.equal(gradient.colorStops[0].color, 'rgb(0,0,0)'); - assert.equal(gradient.colorStops[1].color, 'rgb(255,255,255)'); + var gradient = fabric.Gradient.fromElement(element, object, '', {}); assert.deepEqual(gradient.gradientTransform, [3.321, -0.6998, 0.4077, 1.9347, -440.9168, -408.0598]); }); @@ -574,7 +573,7 @@ assert.equal(gradient.coords.x1, 0); assert.equal(gradient.coords.y1, 0); - assert.equal(gradient.coords.x2, 100); + assert.equal(gradient.coords.x2, 1); assert.equal(gradient.coords.y2, 0); assert.equal(gradient.colorStops[0].offset, 1); @@ -629,11 +628,6 @@ assert.ok(gradient instanceof fabric.Gradient); - assert.equal(gradient.coords.x1, 50); - assert.equal(gradient.coords.y1, 50); - assert.equal(gradient.coords.x2, 50); - assert.equal(gradient.coords.y2, 50); - assert.equal(gradient.colorStops[0].offset, 1); assert.equal(gradient.colorStops[1].offset, 0.75); assert.equal(gradient.colorStops[2].offset, 0.5); @@ -770,11 +764,25 @@ assert.equal(gradient.toSVG(obj), SVG_INTERNALRADIUS); }); - QUnit.test('toSVG radial with r1 > 0', function(assert) { + QUnit.test('toSVG radial with r1 > 0 swapped', function(assert) { fabric.Object.__uid = 0; var gradient = createRadialGradientSwapped(); var obj = new fabric.Object({ width: 100, height: 100 }); assert.equal(gradient.toSVG(obj), SVG_SWAPPED); }); + QUnit.test('toSVG linear objectBoundingBox', function(assert) { + fabric.Object.__uid = 0; + var gradient = createLinearGradient('percentage'); + var obj = new fabric.Object({ width: 100, height: 100 }); + assert.equal(gradient.toSVG(obj), SVG_LINEAR_PERCENTAGE); + }); + + QUnit.test('toSVG radial objectBoundingBox', function(assert) { + fabric.Object.__uid = 0; + var gradient = createRadialGradient('percentage'); + var obj = new fabric.Object({ width: 100, height: 100 }); + assert.equal(gradient.toSVG(obj), SVG_RADIAL_PERCENTAGE); + }); + })(); diff --git a/test/unit/object_geometry.js b/test/unit/object_geometry.js index eb61b2f6d86..cd3437a7bb6 100644 --- a/test/unit/object_geometry.js +++ b/test/unit/object_geometry.js @@ -435,13 +435,73 @@ }); QUnit.test('_calcDimensionsTransformMatrix', function(assert) { - var cObj = new fabric.Object({ width: 10, height: 15, strokeWidth: 0 }); + var cObj = new fabric.Object({ width: 10, height: 15, strokeWidth: 0, scaleX: 2, scaleY: 3, skewY: 10 }); + assert.ok(typeof cObj._calcDimensionsTransformMatrix === 'function', '_calcDimensionsTransformMatrix should exist'); + var matrix = cObj._calcDimensionsTransformMatrix(); + var expected = [ + 2, + 0, + 0, + 3, + 0, + 0 + ]; + assert.deepEqual(matrix, expected, 'dimensions matrix is equal'); + }); + + QUnit.test('_calcDimensionsTransformMatrix with flipping', function(assert) { + var cObj = new fabric.Object({ width: 10, height: 15, strokeWidth: 0, scaleX: 2, scaleY: 3, skewY: 10, flipX: true }); assert.ok(typeof cObj._calcDimensionsTransformMatrix === 'function', '_calcDimensionsTransformMatrix should exist'); + var matrix = cObj._calcDimensionsTransformMatrix(0, 0, false); + var expected = [ + 2, + 0, + 0, + 3, + 0, + 0 + ]; + assert.deepEqual(matrix, expected, 'dimensions matrix with flipping = false is equal'); + var matrix2 = cObj._calcDimensionsTransformMatrix(0, 0, true); + var expected = [ + -2, + 0, + 0, + 3, + 0, + 0 + ]; + assert.deepEqual(matrix2, expected, 'dimensions matrix with flipping = true is equal'); }); QUnit.test('_calcRotateMatrix', function(assert) { - var cObj = new fabric.Object({ width: 10, height: 15, strokeWidth: 0 }); + var cObj = new fabric.Object({ width: 10, height: 15, strokeWidth: 0, angle: 90 }); assert.ok(typeof cObj._calcRotateMatrix === 'function', '_calcRotateMatrix should exist'); + var matrix = cObj._calcRotateMatrix(); + var expected = [ + 0, + 1, + -1, + 0, + 0, + 0 + ]; + assert.deepEqual(matrix, expected, 'rotate matrix is equal'); + }); + + QUnit.test('_calcTranslateMatrix', function(assert) { + var cObj = new fabric.Object({ top: 5, width: 10, height: 15, strokeWidth: 0, angle: 90 }); + assert.ok(typeof cObj._calcTranslateMatrix === 'function', '_calcTranslateMatrix should exist'); + var matrix = cObj._calcTranslateMatrix(); + var expected = [ + 1, + 0, + 0, + 1, + -7.5, + 10 + ]; + assert.deepEqual(matrix, expected, 'translate matrix is equal'); }); QUnit.test('scaleToWidth', function(assert) { diff --git a/test/unit/util.js b/test/unit/util.js index 678fbeaa520..c53fdff870e 100644 --- a/test/unit/util.js +++ b/test/unit/util.js @@ -917,6 +917,27 @@ assert.equal(options.translateY, 200, 'imatrix has translateY 200'); }); + QUnit.test('composeMatrix with defaults', function(assert) { + assert.ok(typeof fabric.util.composeMatrix === 'function'); + var matrix = fabric.util.composeMatrix({ + scaleX: 2, + scaleY: 3, + skewX: 28, + angle: 11, + translateX: 100, + translateY: 200, + }).map(function(val) { + return fabric.util.toFixed(val, 2); + }); + assert.deepEqual(matrix, [1.96, 0.38, 0.47, 3.15, 100, 200], 'default is identity matrix'); + }); + + QUnit.test('composeMatrix with options', function(assert) { + assert.ok(typeof fabric.util.composeMatrix === 'function'); + var matrix = fabric.util.composeMatrix({}); + assert.deepEqual(matrix, fabric.iMatrix, 'default is identity matrix'); + }); + QUnit.test('drawArc', function(assert) { assert.ok(typeof fabric.util.drawArc === 'function'); var canvas = this.canvas = new fabric.StaticCanvas(null, {enableRetinaScaling: false, width: 600, height: 600}); diff --git a/test/visual/assets/svg_linear_9.svg b/test/visual/assets/svg_linear_9.svg new file mode 100644 index 00000000000..9cc808c5958 --- /dev/null +++ b/test/visual/assets/svg_linear_9.svg @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/visual/golden/multipleGradients.png b/test/visual/golden/multipleGradients.png new file mode 100644 index 00000000000..91502cbf7a4 Binary files /dev/null and b/test/visual/golden/multipleGradients.png differ diff --git a/test/visual/golden/svg_linear_9.png b/test/visual/golden/svg_linear_9.png new file mode 100644 index 00000000000..8a9e68a026a Binary files /dev/null and b/test/visual/golden/svg_linear_9.png differ diff --git a/test/visual/svg_import.js b/test/visual/svg_import.js index 5d87a1e0658..18b8dcef99b 100644 --- a/test/visual/svg_import.js +++ b/test/visual/svg_import.js @@ -58,6 +58,7 @@ 'svg_linear_6', 'svg_linear_7', 'svg_linear_8', + 'svg_linear_9', 'svg_radial_1', 'svg_radial_2', 'svg_radial_3', @@ -76,7 +77,7 @@ 'clippath-7', 'clippath-9', 'vector-effect', - 'svg-with-no-dim-rect' + 'svg-with-no-dim-rect', //'clippath-8', ].map(createTestFromSVG); diff --git a/test/visual/z_svg_export.js b/test/visual/z_svg_export.js index 6a6506c8d48..cb9925c097a 100644 --- a/test/visual/z_svg_export.js +++ b/test/visual/z_svg_export.js @@ -2,11 +2,14 @@ fabric.enableGLFiltering = false; fabric.isWebglSupported = false; var visualTestLoop; + var getAssetName; if (fabric.isLikelyNode) { visualTestLoop = global.visualTestLoop; + getAssetName = global.getAssetName; } else { visualTestLoop = window.visualTestLoop; + getAssetName = window.getAssetName; } function svgToDataURL(svgStr) { @@ -396,5 +399,22 @@ width: 210, height: 230, }); + + function multipleGradients(canvas, callback) { + fabric.loadSVGFromURL(getAssetName('svg_linear_9'), function(objects) { + var group = fabric.util.groupSVGElements(objects); + canvas.add(group); + toSVGCanvas(canvas, callback); + }); + } + + tests.push({ + test: 'Multiple gradients import', + code: multipleGradients, + golden: 'multipleGradients.png', + percentage: 0.06, + width: 760, + height: 760, + }); tests.forEach(visualTestLoop(QUnit)); })();