From cda061132663459478e30f1c700c7eff6e49a2c0 Mon Sep 17 00:00:00 2001 From: Andrea Bogazzi Date: Wed, 2 Sep 2015 21:28:57 +0200 Subject: [PATCH] Add support for skew objects. --- src/canvas.class.js | 186 +++++++++++++++++++---- src/mixins/canvas_events.mixin.js | 28 +++- src/mixins/object.svg_export.js | 8 +- src/mixins/object_geometry.mixin.js | 36 ++--- src/mixins/object_interactivity.mixin.js | 37 ++++- src/mixins/textbox_behavior.mixin.js | 4 +- src/shapes/object.class.js | 37 ++++- src/util/misc.js | 54 ++++++- test/unit/canvas.js | 4 +- test/unit/canvas_static.js | 8 +- test/unit/circle.js | 8 +- test/unit/ellipse.js | 2 + test/unit/group.js | 2 + test/unit/image.js | 2 + test/unit/itext.js | 2 + test/unit/line.js | 2 + test/unit/object.js | 20 ++- test/unit/object_origin.js | 3 +- test/unit/path.js | 2 + test/unit/path_group.js | 2 + test/unit/polygon.js | 2 + test/unit/polyline.js | 2 + test/unit/rect.js | 6 +- test/unit/text.js | 8 +- 24 files changed, 367 insertions(+), 98 deletions(-) diff --git a/src/canvas.class.js b/src/canvas.class.js index 60d32413..6550c465 100644 --- a/src/canvas.class.js +++ b/src/canvas.class.js @@ -213,17 +213,19 @@ * @private * @param {Event} e Event object fired on mousemove */ - _resetCurrentTransform: function(e) { + _resetCurrentTransform: function() { var t = this._currentTransform; t.target.set({ scaleX: t.original.scaleX, scaleY: t.original.scaleY, + skewX: t.original.skewX, + skewY: t.original.skewY, left: t.original.left, top: t.original.top }); - if (this._shouldCenterTransform(e, t.target)) { + if (this._shouldCenterTransform(t.target)) { if (t.action === 'rotate') { this._setOriginToCenter(t.target); } @@ -348,10 +350,9 @@ /** * @private - * @param {Event} e Event object * @param {fabric.Object} target */ - _shouldCenterTransform: function (e, target) { + _shouldCenterTransform: function (target) { if (!target) { return; } @@ -366,7 +367,7 @@ centerTransform = this.centeredRotation || target.centeredRotation; } - return centerTransform ? !e.altKey : e.altKey; + return centerTransform ? !t.altKey : t.altKey; }, /** @@ -398,18 +399,23 @@ /** * @private */ - _getActionFromCorner: function(target, corner) { - var action = 'drag'; - if (corner) { - action = (corner === 'ml' || corner === 'mr') - ? 'scaleX' - : (corner === 'mt' || corner === 'mb') - ? 'scaleY' - : corner === 'mtr' - ? 'rotate' - : 'scale'; + _getActionFromCorner: function(target, corner, e) { + if (!corner) { + return 'drag'; + } + + switch (corner) { + case 'mtr': + return 'rotate'; + case 'ml': + case 'mr': + return e.shiftKey ? 'skewY' : 'scaleX'; + case 'mt': + case 'mb': + return e.shiftKey ? 'skewX' : 'scaleY'; + default: + return 'scale'; } - return action; }, /** @@ -424,26 +430,33 @@ var pointer = this.getPointer(e), corner = target._findTargetCorner(this.getPointer(e, true)), - action = this._getActionFromCorner(target, corner), + action = this._getActionFromCorner(target, corner, e), origin = this._getOriginFromCorner(target, corner); this._currentTransform = { target: target, action: action, + corner: corner, scaleX: target.scaleX, scaleY: target.scaleY, + skewX: target.skewX, + skewY: target.skewY, offsetX: pointer.x - target.left, offsetY: pointer.y - target.top, originX: origin.x, originY: origin.y, ex: pointer.x, ey: pointer.y, + lastX: pointer.x, + lastY: pointer.y, left: target.left, top: target.top, theta: degreesToRadians(target.angle), width: target.width * target.scaleX, mouseXSign: 1, - mouseYSign: 1 + mouseYSign: 1, + shiftKey: e.shiftKey, + altKey: e.altKey }; this._currentTransform.original = { @@ -451,11 +464,13 @@ top: target.top, scaleX: target.scaleX, scaleY: target.scaleY, + skewX: target.skewX, + skewY: target.skewY, originX: origin.x, originY: origin.y }; - this._resetCurrentTransform(e); + this._resetCurrentTransform(); }, /** @@ -475,6 +490,114 @@ } }, + /** + * Check if we are increasing a positive skew or lower it, + * checking mouse direction and pressed corner. + * @private + */ + _changeSkewTransformOrigin: function(mouseMove, t, by) { + var property = 'originX', origins = { 0: 'center' }, + skew = t.target.skewX, originA = 'left', originB = 'right', + corner = t.corner === 'mt' || t.corner === 'ml' ? 1 : -1, + flipSign = 1; + + mouseMove = mouseMove > 0 ? 1 : -1; + if (by === 'y') { + skew = t.target.skewY; + originA = 'top'; + originB = 'bottom'; + property = 'originY'; + } + origins[-1] = originA; + origins[1] = originB; + + t.target.flipX && (flipSign *= -1); + t.target.flipY && (flipSign *= -1); + + if (skew === 0) { + t.skewSign = -corner * mouseMove * flipSign; + t[property] = origins[-mouseMove]; + } + else { + skew = skew > 0 ? 1 : -1; + t.skewSign = skew; + t[property] = origins[skew * corner * flipSign]; + } + }, + + /** + * Skew object by mouse events + * @private + * @param {Number} x pointer's x coordinate + * @param {Number} y pointer's y coordinate + * @param {String} by Either 'x' or 'y' + */ + _skewObject: function (x, y, by) { + var t = this._currentTransform, + target = t.target, + lockSkewingX = target.get('lockSkewingX'), + lockSkewingY = target.get('lockSkewingY'); + + if ((lockSkewingX && by === 'x') || (lockSkewingY && by === 'y')) { + return; + } + + // Get the constraint point + var center = target.getCenterPoint(), + actualMouseByCenter = target.toLocalPoint(new fabric.Point(x, y), 'center', 'center')[by], + lastMouseByCenter = target.toLocalPoint(new fabric.Point(t.lastX, t.lastY), 'center', 'center')[by], + actualMouseByOrigin, constraintPosition, dim = target._getTransformedDimensions(); + + this._changeSkewTransformOrigin(actualMouseByCenter - lastMouseByCenter, t, by); + actualMouseByOrigin = target.toLocalPoint(new fabric.Point(x, y), t.originX, t.originY)[by], + + constraintPosition = target.translateToOriginPoint(center, t.originX, t.originY); + // Actually skew the object + this._setObjectSkew(actualMouseByOrigin, t, by, dim); + t.lastX = x; + t.lastY = y; + // Make sure the constraints apply + target.setPositionByOrigin(constraintPosition, t.originX, t.originY); + }, + + _setObjectSkew: function(localMouse, transform, by, _dim) { + var target = transform.target, newValue, + skewSign = transform.skewSign, newDim, dimNoSkew, + otherBy, _otherBy, _by, newDimMouse, skewX, skewY; + + if (by === 'x') { + otherBy = 'y'; + _otherBy = 'Y'; + _by = 'X'; + skewX = 0; + skewY = target.skewY; + } + else { + otherBy = 'x'; + _otherBy = 'X'; + _by = 'Y'; + skewX = target.skewX; + skewY = 0; + } + + dimNoSkew = target._getTransformedDimensions(skewX, skewY); + newDimMouse = 2 * Math.abs(localMouse) - dimNoSkew[by]; + if (newDimMouse <= 2) { + newValue = 0; + } + else { + newValue = skewSign * Math.atan((newDimMouse / target['scale' + _by]) / + (dimNoSkew[otherBy] / target['scale' + _otherBy])); + newValue = fabric.util.radiansToDegrees(newValue); + } + target.set('skew' + _by, newValue); + if (target['skew' + _otherBy] !== 0) { + newDim = target._getTransformedDimensions(); + newValue = (_dim[otherBy] / newDim[otherBy]) * target['scale' + _otherBy]; + target.set('scale' + _otherBy, newValue); + } + }, + /** * Scales object by invoking its scaleX/scaleY methods * @private @@ -496,12 +619,13 @@ // Get the constraint point var constraintPosition = target.translateToOriginPoint(target.getCenterPoint(), t.originX, t.originY), - localMouse = target.toLocalPoint(new fabric.Point(x, y), t.originX, t.originY); + localMouse = target.toLocalPoint(new fabric.Point(x, y), t.originX, t.originY), + dim = target._getTransformedDimensions(); this._setLocalMouse(localMouse, t); // Actually scale the object - this._setObjectScale(localMouse, t, lockScalingX, lockScalingY, by, lockScalingFlip); + this._setObjectScale(localMouse, t, lockScalingX, lockScalingY, by, lockScalingFlip, dim); // Make sure the constraints apply target.setPositionByOrigin(constraintPosition, t.originX, t.originY); @@ -510,12 +634,11 @@ /** * @private */ - _setObjectScale: function(localMouse, transform, lockScalingX, lockScalingY, by, lockScalingFlip) { - var target = transform.target, forbidScalingX = false, forbidScalingY = false, - dim = target._getNonTransformedDimensions(); + _setObjectScale: function(localMouse, transform, lockScalingX, lockScalingY, by, lockScalingFlip, _dim) { + var target = transform.target, forbidScalingX = false, forbidScalingY = false; - transform.newScaleX = localMouse.x / dim.x; - transform.newScaleY = localMouse.y / dim.y; + transform.newScaleX = localMouse.x * target.scaleX / _dim.x; + transform.newScaleY = localMouse.y * target.scaleY / _dim.y; if (lockScalingFlip && transform.newScaleX <= 0 && transform.newScaleX < target.scaleX) { forbidScalingX = true; @@ -526,7 +649,7 @@ } if (by === 'equally' && !lockScalingX && !lockScalingY) { - forbidScalingX || forbidScalingY || this._scaleObjectEqually(localMouse, target, transform); + forbidScalingX || forbidScalingY || this._scaleObjectEqually(localMouse, target, transform, _dim); } else if (!by) { forbidScalingX || lockScalingX || target.set('scaleX', transform.newScaleX); @@ -546,12 +669,11 @@ /** * @private */ - _scaleObjectEqually: function(localMouse, target, transform) { + _scaleObjectEqually: function(localMouse, target, transform, _dim) { var dist = localMouse.y + localMouse.x, - dim = target._getNonTransformedDimensions(), - lastDist = dim.y * transform.original.scaleY + - dim.x * transform.original.scaleX; + lastDist = _dim.y * transform.original.scaleY / target.scaleY + + _dim.x * transform.original.scaleX / target.scaleX; // We use transform.scaleX/Y instead of target.scaleX/Y // because the object may have a min scale and we'll loose the proportions @@ -680,6 +802,8 @@ _resetObjectTransform: function (target) { target.scaleX = 1; target.scaleY = 1; + target.skewX = 0; + target.skewY = 0; target.setAngle(0); }, diff --git a/src/mixins/canvas_events.mixin.js b/src/mixins/canvas_events.mixin.js index e1bbc4c1..5e221e10 100644 --- a/src/mixins/canvas_events.mixin.js +++ b/src/mixins/canvas_events.mixin.js @@ -437,7 +437,7 @@ target = this.getActiveGroup(); } - if (target && target.selectable && !shouldGroup) { + if (target && target.selectable && (target.__corner || !shouldGroup)) { this._beforeTransform(e, target); this._setupCurrentTransform(e, target); } @@ -617,6 +617,14 @@ this._scaleObject(x, y, 'y'); this._fire('scaling', target, e); } + else if (action === 'skewX') { + this._skewObject(x, y, 'x'); + this._fire('skewing', target, e); + } + else if (action === 'skewY') { + this._skewObject(x, y, 'y'); + this._fire('skewing', target, e); + } else { this._translateObject(x, y); this._fire('moving', target, e); @@ -637,14 +645,14 @@ */ _beforeScaleTransform: function(e, transform) { if (transform.action === 'scale' || transform.action === 'scaleX' || transform.action === 'scaleY') { - var centerTransform = this._shouldCenterTransform(e, transform.target); + var centerTransform = this._shouldCenterTransform(transform.target); // Switch from a normal resize to center-based if ((centerTransform && (transform.originX !== 'center' || transform.originY !== 'center')) || // Switch from center-based resize to normal one (!centerTransform && transform.originX === 'center' && transform.originY === 'center') ) { - this._resetCurrentTransform(e); + this._resetCurrentTransform(); transform.reset = true; } } @@ -663,7 +671,7 @@ else { // Switch from a normal resize to proportional if (!transform.reset && transform.currentAction === 'scale') { - this._resetCurrentTransform(e, transform.target); + this._resetCurrentTransform(); } transform.currentAction = 'scaleEqually'; @@ -693,7 +701,7 @@ this.setCursor(target.hoverCursor || this.hoverCursor); } else { - this._setCornerCursor(corner, target); + this._setCornerCursor(corner, target, e); } } return true; @@ -702,9 +710,9 @@ /** * @private */ - _setCornerCursor: function(corner, target) { + _setCornerCursor: function(corner, target, e) { if (corner in cursorOffset) { - this.setCursor(this._getRotatedCornerCursor(corner, target)); + this.setCursor(this._getRotatedCornerCursor(corner, target, e)); } else if (corner === 'mtr' && target.hasRotatingPoint) { this.setCursor(this.rotationCursor); @@ -718,13 +726,17 @@ /** * @private */ - _getRotatedCornerCursor: function(corner, target) { + _getRotatedCornerCursor: function(corner, target, e) { var n = Math.round((target.getAngle() % 360) / 45); if (n < 0) { n += 8; // full circle ahead } n += cursorOffset[corner]; + if (e.shiftKey && cursorOffset[corner] % 2 === 0) { + //if we are holding shift and we are on a mx corner... + n += 2; + } // normalize n to be from 0 to 7 n %= 8; diff --git a/src/mixins/object.svg_export.js b/src/mixins/object.svg_export.js index 786a8081..acb86998 100644 --- a/src/mixins/object.svg_export.js +++ b/src/mixins/object.svg_export.js @@ -58,6 +58,8 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot } var toFixed = fabric.util.toFixed, angle = this.getAngle(), + skewX = (this.getSkewX() % 360), + skewY = (this.getSkewY() % 360), vpt = !this.canvas || this.canvas.svgViewportTransformation ? this.getViewportTransform() : [1, 0, 0, 1, 0, 0], center = fabric.util.transformPoint(this.getCenterPoint(), vpt), @@ -81,6 +83,10 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot toFixed(this.scaleY * vpt[3], NUM_FRACTION_DIGITS) + ')'), + skewXPart = skewX !== 0 ? ' skewX(' + toFixed(skewX, NUM_FRACTION_DIGITS) + ')' : '', + + skewYPart = skewY !== 0 ? ' skewY(' + toFixed(skewY, NUM_FRACTION_DIGITS) + ')' : '', + addTranslateX = this.type === 'path-group' ? this.width * vpt[0] : 0, flipXPart = this.flipX ? ' matrix(-1 0 0 1 ' + addTranslateX + ' 0) ' : '', @@ -90,7 +96,7 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot flipYPart = this.flipY ? ' matrix(1 0 0 -1 0 ' + addTranslateY + ')' : ''; return [ - translatePart, anglePart, scalePart, flipXPart, flipYPart + translatePart, anglePart, scalePart, flipXPart, flipYPart, skewXPart, skewYPart ].join(''); }, diff --git a/src/mixins/object_geometry.mixin.js b/src/mixins/object_geometry.mixin.js index 88b0534c..a2609690 100644 --- a/src/mixins/object_geometry.mixin.js +++ b/src/mixins/object_geometry.mixin.js @@ -192,23 +192,12 @@ */ getBoundingRect: function() { this.oCoords || this.setCoords(); - - var xCoords = [this.oCoords.tl.x, this.oCoords.tr.x, this.oCoords.br.x, this.oCoords.bl.x], - minX = fabric.util.array.min(xCoords), - maxX = fabric.util.array.max(xCoords), - width = Math.abs(minX - maxX), - - yCoords = [this.oCoords.tl.y, this.oCoords.tr.y, this.oCoords.br.y, this.oCoords.bl.y], - minY = fabric.util.array.min(yCoords), - maxY = fabric.util.array.max(yCoords), - height = Math.abs(minY - maxY); - - return { - left: minX, - top: minY, - width: width, - height: height - }; + return fabric.util.makeBoundingBoxFromPoints([ + this.oCoords.tl, + this.oCoords.tr, + this.oCoords.br, + this.oCoords.bl + ]); }, /** @@ -217,7 +206,7 @@ */ getWidth: function() { //needs to be changed - return this.width * this.scaleX; + return this._getTransformedDimensions().x; }, /** @@ -226,7 +215,7 @@ */ getHeight: function() { //needs to be changed - return this.height * this.scaleY; + return this._getTransformedDimensions().y; }, /** @@ -357,9 +346,12 @@ return this; }, - _calcDimensionsTransformMatrix: function() { - // introduce skew matrix here later - return [this.scaleX, 0, 0, this.scaleY, 0, 0]; + _calcDimensionsTransformMatrix: function(skewX, skewY) { + var skewMatrixX = [1, 0, Math.tan(degreesToRadians(skewX)), 1, 0, 0], + skewMatrixY = [1, Math.tan(degreesToRadians(skewY)), 0, 1, 0, 0], + scaleMatrix = [this.scaleX, 0, 0, this.scaleY, 0, 0], + m = fabric.util.multiplyTransformMatrices(scaleMatrix, skewMatrixX, true); + return fabric.util.multiplyTransformMatrices(m, skewMatrixY, true); } }); })(); diff --git a/src/mixins/object_interactivity.mixin.js b/src/mixins/object_interactivity.mixin.js index 15c30914..5f6be6ad 100644 --- a/src/mixins/object_interactivity.mixin.js +++ b/src/mixins/object_interactivity.mixin.js @@ -138,12 +138,39 @@ /* * @private */ - _getTransformedDimensions: function(dimensions) { - if (!dimensions) { - dimensions = this._getNonTransformedDimensions(); + _getTransformedDimensions: function(skewX, skewY) { + if (typeof skewX === 'undefined') { + skewX = this.skewX; } - var transformMatrix = this._calcDimensionsTransformMatrix(); - return fabric.util.transformPoint(dimensions, transformMatrix, true); + if (typeof skewY === 'undefined') { + skewY = this.skewY; + } + var dimensions = this._getNonTransformedDimensions(), + dimX = dimensions.x /2, dimY = dimensions.y / 2, + points = [ + { + x: -dimX, + y: -dimY + }, + { + x: dimX, + y: -dimY + }, + { + x: -dimX, + y: dimY + }, + { + x: dimX, + y: dimY + }], + i, transformMatrix = this._calcDimensionsTransformMatrix(skewX, skewY), + bbox; + for (i = 0; i < points.length; i++) { + points[i] = fabric.util.transformPoint(points[i], transformMatrix); + } + bbox = fabric.util.makeBoundingBoxFromPoints(points); + return { x: bbox.width, y: bbox.height }; }, /* diff --git a/src/mixins/textbox_behavior.mixin.js b/src/mixins/textbox_behavior.mixin.js index fb0905a9..2b9d934b 100644 --- a/src/mixins/textbox_behavior.mixin.js +++ b/src/mixins/textbox_behavior.mixin.js @@ -7,7 +7,7 @@ var setObjectScaleOverridden = fabric.Canvas.prototype._setObjectScale; fabric.Canvas.prototype._setObjectScale = function(localMouse, transform, - lockScalingX, lockScalingY, by, lockScalingFlip) { + lockScalingX, lockScalingY, by, lockScalingFlip, _dim) { var t = transform.target; if (t instanceof fabric.Textbox) { @@ -18,7 +18,7 @@ } else { setObjectScaleOverridden.call(fabric.Canvas.prototype, localMouse, transform, - lockScalingX, lockScalingY, by, lockScalingFlip); + lockScalingX, lockScalingY, by, lockScalingFlip, _dim); } }; diff --git a/src/shapes/object.class.js b/src/shapes/object.class.js index 7d538f2c..08a7d557 100644 --- a/src/shapes/object.class.js +++ b/src/shapes/object.class.js @@ -27,6 +27,7 @@ * @fires rotating * @fires scaling * @fires moving + * @fires skewing * * @fires mousedown * @fires mouseup @@ -382,6 +383,20 @@ */ angle: 0, + /** + * Angle of skew on x axes of an object (in degrees) + * @type Number + * @default + */ + skewX: 0, + + /** + * Angle of skew on y axes of an object (in degrees) + * @type Number + * @default + */ + skewY: 0, + /** * Size of object's controlling corners (in pixels) * @type Number @@ -662,6 +677,20 @@ */ lockUniScaling: false, + /** + * When `true`, object horizontal skewing is locked + * @type Boolean + * @default + */ + lockSkewingX: false, + + /** + * When `true`, object vertical skewing is locked + * @type Boolean + * @default + */ + lockSkewingY: false, + /** * When `true`, object cannot be flipped by scaling into negative values * @type Boolean @@ -679,7 +708,7 @@ 'top left width height scaleX scaleY flipX flipY originX originY transformMatrix ' + 'stroke strokeWidth strokeDashArray strokeLineCap strokeLineJoin strokeMiterLimit ' + 'angle opacity fill fillRule globalCompositeOperation shadow clipTo visible backgroundColor ' + - 'alignX alignY meetOrSlice' + 'alignX alignY meetOrSlice skewX skewY' ).split(' '), /** @@ -762,6 +791,8 @@ this.scaleX * (this.flipX ? -1 : 1), this.scaleY * (this.flipY ? -1 : 1) ); + ctx.transform(1, 0, Math.tan(degreesToRadians(this.skewX)), 1, 0, 0); + ctx.transform(1, Math.tan(degreesToRadians(this.skewY)), 0, 1, 0, 0); }, /** @@ -799,7 +830,9 @@ backgroundColor: this.backgroundColor, fillRule: this.fillRule, globalCompositeOperation: this.globalCompositeOperation, - transformMatrix: this.transformMatrix ? this.transformMatrix.concat() : this.transformMatrix + transformMatrix: this.transformMatrix ? this.transformMatrix.concat() : this.transformMatrix, + skewX: toFixed(this.skewX, NUM_FRACTION_DIGITS), + skewY: toFixed(this.skewY, NUM_FRACTION_DIGITS) }; if (!this.includeDefaultValues) { diff --git a/src/util/misc.js b/src/util/misc.js index d5c4a23b..d8eb0ec2 100644 --- a/src/util/misc.js +++ b/src/util/misc.js @@ -2,6 +2,8 @@ var sqrt = Math.sqrt, atan2 = Math.atan2, + atan = Math.atan, + pow = Math.pow, PiBy180 = Math.PI / 180; /** @@ -100,6 +102,29 @@ ); }, + /** + * Returns coordinates of points's bounding rectangle (left, top, width, height) + * @param {Array} points 4 points array + * @return {Object} Object with left, top, width, height properties + */ + makeBoundingBoxFromPoints: function(points) { + 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), + width = Math.abs(minX - maxX), + yPoints = [points[0].y, points[1].y, points[2].y, points[3].y], + minY = fabric.util.array.min(yPoints), + maxY = fabric.util.array.max(yPoints), + height = Math.abs(minY - maxY); + + return { + left: minX, + top: minY, + width: width, + height: height + }; + }, + /** * Invert transformation t * @static @@ -457,20 +482,43 @@ * @memberOf fabric.util * @param {Array} a First transformMatrix * @param {Array} b Second transformMatrix + * @param {Boolean} is2x2 flag to multiply matrices as 2x2 matrices * @return {Array} The product of the two transform matrices */ - multiplyTransformMatrices: function(a, b) { + multiplyTransformMatrices: function(a, b, is2x2) { // Matrix multiply a * b return [ a[0] * b[0] + a[2] * b[1], a[1] * b[0] + a[3] * b[1], a[0] * b[2] + a[2] * b[3], a[1] * b[2] + a[3] * b[3], - a[0] * b[4] + a[2] * b[5] + a[4], - a[1] * b[4] + a[3] * b[5] + a[5] + is2x2 ? 0 : a[0] * b[4] + a[2] * b[5] + a[4], + is2x2 ? 0 : a[1] * b[4] + a[3] * b[5] + a[5] ]; }, + /** + * Decompone standard 2x2 matrix into transform componentes + * @static + * @memberOf fabric.util + * @param {Array} a transformMatrix + * @return {Object} Components of transform + */ + qrDecompone: function(a) { + var angle = atan(a[0] / a[1]), + denom = pow(a[0]) + pow(a[1]), + scaleX = sqrt(denom), + scaleY = (a[0] * a[3] - a[2] * a [1]) / scaleX, + skewX = atan((a[0] * a[2] + a[1] * a [3]) / denom); + return { + angle: angle / PiBy180, + scaleX: scaleX, + scaleY: scaleY, + skewX: skewX / PiBy180, + skewY: 0 + }; + }, + /** * Returns string representation of function body * @param {Function} fn Function to get body of diff --git a/test/unit/canvas.js b/test/unit/canvas.js index 44a6c325..a807b7fb 100644 --- a/test/unit/canvas.js +++ b/test/unit/canvas.js @@ -41,12 +41,12 @@ var PATH_DATALESS_JSON = '{"objects":[{"type":"path","originX":"left","originY":"top","left":100,"top":100,"width":200,"height":200,"fill":"rgb(0,0,0)",'+ '"stroke":null,"strokeWidth":1,"strokeDashArray":null,"strokeLineCap":"butt","strokeLineJoin":"miter","strokeMiterLimit":10,'+ '"scaleX":1,"scaleY":1,"angle":0,"flipX":false,"flipY":false,"opacity":1,'+ - '"shadow":null,"visible":true,"clipTo":null,"backgroundColor":"","fillRule":"nonzero","globalCompositeOperation":"source-over","transformMatrix":null,"path":"http://example.com/","pathOffset":{"x":200,"y":200}}],"background":""}'; + '"shadow":null,"visible":true,"clipTo":null,"backgroundColor":"","fillRule":"nonzero","globalCompositeOperation":"source-over","transformMatrix":null,"skewX":0,"skewY":0,"path":"http://example.com/","pathOffset":{"x":200,"y":200}}],"background":""}'; var RECT_JSON = '{"objects":[{"type":"rect","originX":"left","originY":"top","left":0,"top":0,"width":10,"height":10,"fill":"rgb(0,0,0)",'+ '"stroke":null,"strokeWidth":1,"strokeDashArray":null,"strokeLineCap":"butt","strokeLineJoin":"miter","strokeMiterLimit":10,"scaleX":1,"scaleY":1,"angle":0,"flipX":false,"flipY":false,"opacity":1,'+ '"shadow":null,'+ - '"visible":true,"clipTo":null,"backgroundColor":"","fillRule":"nonzero","globalCompositeOperation":"source-over","transformMatrix":null,"rx":0,"ry":0}],"background":"#ff5555","overlay":"rgba(0,0,0,0.2)"}'; + '"visible":true,"clipTo":null,"backgroundColor":"","fillRule":"nonzero","globalCompositeOperation":"source-over","transformMatrix":null,"skewX":0,"skewY":0,"rx":0,"ry":0}],"background":"#ff5555","overlay":"rgba(0,0,0,0.2)"}'; var el = fabric.document.createElement('canvas'); el.width = 600; el.height = 600; diff --git a/test/unit/canvas_static.js b/test/unit/canvas_static.js index 8e6716c6..3d089d7b 100644 --- a/test/unit/canvas_static.js +++ b/test/unit/canvas_static.js @@ -28,17 +28,17 @@ var PATH_DATALESS_JSON = '{"objects":[{"type":"path","originX":"left","originY":"top","left":100,"top":100,"width":200,"height":200,"fill":"rgb(0,0,0)",'+ '"stroke":null,"strokeWidth":1,"strokeDashArray":null,"strokeLineCap":"butt","strokeLineJoin":"miter","strokeMiterLimit":10,'+ '"scaleX":1,"scaleY":1,"angle":0,"flipX":false,"flipY":false,"opacity":1,'+ - '"shadow":null,"visible":true,"clipTo":null,"backgroundColor":"","fillRule":"nonzero","globalCompositeOperation":"source-over","transformMatrix":null,"path":"http://example.com/","pathOffset":{"x":200,"y":200}}],"background":""}'; + '"shadow":null,"visible":true,"clipTo":null,"backgroundColor":"","fillRule":"nonzero","globalCompositeOperation":"source-over","transformMatrix":null,"skewX":0,"skewY":0,"path":"http://example.com/","pathOffset":{"x":200,"y":200}}],"background":""}'; var RECT_JSON = '{"objects":[{"type":"rect","originX":"left","originY":"top","left":0,"top":0,"width":10,"height":10,"fill":"rgb(0,0,0)",'+ '"stroke":null,"strokeWidth":1,"strokeDashArray":null,"strokeLineCap":"butt","strokeLineJoin":"miter","strokeMiterLimit":10,'+ '"scaleX":1,"scaleY":1,"angle":0,"flipX":false,"flipY":false,"opacity":1,'+ - '"shadow":null,"visible":true,"clipTo":null,"backgroundColor":"","fillRule":"nonzero","globalCompositeOperation":"source-over","transformMatrix":null,"rx":0,"ry":0}],"background":"#ff5555","overlay":"rgba(0,0,0,0.2)"}'; + '"shadow":null,"visible":true,"clipTo":null,"backgroundColor":"","fillRule":"nonzero","globalCompositeOperation":"source-over","transformMatrix":null,"skewX":0,"skewY":0,"rx":0,"ry":0}],"background":"#ff5555","overlay":"rgba(0,0,0,0.2)"}'; var RECT_JSON_WITH_PADDING = '{"objects":[{"type":"rect","originX":"left","originY":"top","left":0,"top":0,"width":10,"height":20,"fill":"rgb(0,0,0)",'+ '"stroke":null,"strokeWidth":1,"strokeDashArray":null,"strokeLineCap":"butt","strokeLineJoin":"miter","strokeMiterLimit":10,'+ '"scaleX":1,"scaleY":1,"angle":0,"flipX":false,"flipY":false,"opacity":1,'+ - '"shadow":null,"visible":true,"clipTo":null,"backgroundColor":"","fillRule":"nonzero","globalCompositeOperation":"source-over","transformMatrix":null,"padding":123,"foo":"bar","rx":0,"ry":0}],"background":""}'; + '"shadow":null,"visible":true,"clipTo":null,"backgroundColor":"","fillRule":"nonzero","globalCompositeOperation":"source-over","transformMatrix":null,"skewX":0,"skewY":0,"padding":123,"foo":"bar","rx":0,"ry":0}],"background":""}'; function getAbsolutePath(path) { var isAbsolute = /^https?:/.test(path); @@ -85,6 +85,8 @@ 'globalCompositeOperation': 'source-over', 'transformMatrix': null, 'crossOrigin': '', + 'skewX': 0, + 'skewY': 0, 'alignX': 'none', 'alignY': 'none', 'meetOrSlice': 'meet' diff --git a/test/unit/circle.js b/test/unit/circle.js index a2dc5d09..5ed483be 100644 --- a/test/unit/circle.js +++ b/test/unit/circle.js @@ -39,7 +39,7 @@ }); test('setRadius', function() { - var circle = new fabric.Circle({ radius: 10 }); + var circle = new fabric.Circle({radius: 10, strokeWidth: 0}); ok(typeof circle.setRadius == 'function'); @@ -59,7 +59,7 @@ }); test('set radius', function() { - var circle = new fabric.Circle(); + var circle = new fabric.Circle({strokeWidth: 0}); circle.set('radius', 20); @@ -108,6 +108,8 @@ 'radius': 0, 'startAngle': 0, 'endAngle': 2 * Math.PI, + 'skewX': 0, + 'skewY': 0, 'transformMatrix': null }; ok(typeof circle.toObject == 'function'); @@ -218,7 +220,7 @@ }); test('cloning and radius, width, height', function() { - var circle = new fabric.Circle({ radius: 10 }); + var circle = new fabric.Circle({ radius: 10, strokeWidth: 0}); circle.scale(2); var clone = circle.clone(); diff --git a/test/unit/ellipse.js b/test/unit/ellipse.js index fc9fd388..098f38de 100644 --- a/test/unit/ellipse.js +++ b/test/unit/ellipse.js @@ -42,6 +42,8 @@ 'flipX': false, 'flipY': false, 'opacity': 1, + 'skewX': 0, + 'skewY': 0, 'rx': 0, 'ry': 0, 'shadow': null, diff --git a/test/unit/group.js b/test/unit/group.js index 51818176..7cac351f 100644 --- a/test/unit/group.js +++ b/test/unit/group.js @@ -191,6 +191,8 @@ 'fillRule': 'nonzero', 'globalCompositeOperation': 'source-over', 'transformMatrix': null, + 'skewX': 0, + 'skewY': 0, 'objects': clone.objects }; diff --git a/test/unit/image.js b/test/unit/image.js index 592043b1..f38f7df0 100644 --- a/test/unit/image.js +++ b/test/unit/image.js @@ -43,6 +43,8 @@ 'filters': [], 'fillRule': 'nonzero', 'globalCompositeOperation': 'source-over', + 'skewX': 0, + 'skewY': 0, 'transformMatrix': null, 'crossOrigin': '', 'alignX': 'none', diff --git a/test/unit/itext.js b/test/unit/itext.js index 237b0074..08722538 100644 --- a/test/unit/itext.js +++ b/test/unit/itext.js @@ -43,6 +43,8 @@ 'textBackgroundColor': '', 'fillRule': 'nonzero', 'globalCompositeOperation': 'source-over', + 'skewX': 0, + 'skewY': 0, 'transformMatrix': null, styles: { } }; diff --git a/test/unit/line.js b/test/unit/line.js index 47aae9aa..ff5e5e5e 100644 --- a/test/unit/line.js +++ b/test/unit/line.js @@ -31,6 +31,8 @@ 'backgroundColor': '', 'fillRule': 'nonzero', 'globalCompositeOperation': 'source-over', + 'skewX': 0, + 'skewY': 0, 'transformMatrix': null }; diff --git a/test/unit/object.js b/test/unit/object.js index 492008d0..3d9bb749 100644 --- a/test/unit/object.js +++ b/test/unit/object.js @@ -154,13 +154,13 @@ '"stroke":null,"strokeWidth":1,"strokeDashArray":null,"strokeLineCap":"butt","strokeLineJoin":"miter","strokeMiterLimit":10,'+ '"scaleX":1,"scaleY":1,"angle":0,"flipX":false,"flipY":false,"opacity":1,'+ '"shadow":null,"visible":true,"clipTo":null,"backgroundColor":"","fillRule":"nonzero","globalCompositeOperation":"source-over",'+ - '"transformMatrix":null}'; + '"transformMatrix":null,"skewX":0,"skewY":0}'; var augmentedJSON = '{"type":"object","originX":"left","originY":"top","left":0,"top":0,"width":122,"height":0,"fill":"rgb(0,0,0)",'+ '"stroke":null,"strokeWidth":1,"strokeDashArray":[5,2],"strokeLineCap":"round","strokeLineJoin":"bevil","strokeMiterLimit":5,'+ '"scaleX":1.3,"scaleY":1,"angle":0,"flipX":false,"flipY":true,"opacity":0.88,'+ '"shadow":null,"visible":true,"clipTo":null,"backgroundColor":"","fillRule":"nonzero","globalCompositeOperation":"source-over",'+ - '"transformMatrix":null}'; + '"transformMatrix":null,"skewX":0,"skewY":0}'; var cObj = new fabric.Object(); ok(typeof cObj.toJSON == 'function'); @@ -206,6 +206,8 @@ 'clipTo': null, 'fillRule': 'nonzero', 'globalCompositeOperation': 'source-over', + 'skewX': 0, + 'skewY': 0, 'transformMatrix': null }; @@ -236,7 +238,9 @@ 'clipTo': null, 'fillRule': 'nonzero', 'globalCompositeOperation': 'source-over', - 'transformMatrix': null + 'transformMatrix': null, + 'skewX': 0, + 'skewY': 0 }; var cObj = new fabric.Object(); @@ -408,17 +412,17 @@ test('getBoundingRectWithStroke', function() { }); test('getWidth', function() { - var cObj = new fabric.Object({ strokeWidth: 0 }); + var cObj = new fabric.Object(); ok(typeof cObj.getWidth == 'function'); - equal(cObj.getWidth(), 0); + equal(cObj.getWidth(), 0 + cObj.strokeWidth); cObj.set('width', 123); - equal(cObj.getWidth(), 123); + equal(cObj.getWidth(), 123 + cObj.strokeWidth); cObj.set('scaleX', 2); - equal(cObj.getWidth(), 246); + equal(cObj.getWidth(), 246 + cObj.strokeWidth * 2); }); test('getHeight', function() { - var cObj = new fabric.Object(); + var cObj = new fabric.Object({strokeWidth: 0}); ok(typeof cObj.getHeight == 'function'); equal(cObj.getHeight(), 0); cObj.set('height', 123); diff --git a/test/unit/object_origin.js b/test/unit/object_origin.js index 7cc1a907..3f79465a 100644 --- a/test/unit/object_origin.js +++ b/test/unit/object_origin.js @@ -237,6 +237,7 @@ var rect = new fabric.Rect(rectOptions), p; + rect.strokeWidth = 0; rect.originX = 'left'; rect.originY = 'top'; p = rect.adjustPosition('left'); @@ -294,7 +295,7 @@ p; rect.angle = 35; - + rect.strokeWidth = 0; rect.originX = 'left'; rect.originY = 'top'; p = rect.adjustPosition('left'); diff --git a/test/unit/path.js b/test/unit/path.js index 5e0ce502..688026bf 100644 --- a/test/unit/path.js +++ b/test/unit/path.js @@ -29,6 +29,8 @@ 'clipTo': null, 'fillRule': 'nonzero', 'globalCompositeOperation': 'source-over', + 'skewX': 0, + 'skewY': 0, 'transformMatrix': null }; diff --git a/test/unit/path_group.js b/test/unit/path_group.js index 1234e5b3..03ff3afd 100644 --- a/test/unit/path_group.js +++ b/test/unit/path_group.js @@ -28,6 +28,8 @@ 'fillRule': 'nonzero', 'globalCompositeOperation': 'source-over', 'transformMatrix': null, + 'skewX': 0, + 'skewY': 0, 'paths': getPathObjects() }; diff --git a/test/unit/polygon.js b/test/unit/polygon.js index fb59c36b..3dc4c264 100644 --- a/test/unit/polygon.js +++ b/test/unit/polygon.js @@ -35,6 +35,8 @@ 'clipTo': null, 'fillRule': 'nonzero', 'globalCompositeOperation': 'source-over', + 'skewX': 0, + 'skewY': 0, 'transformMatrix': null }; diff --git a/test/unit/polyline.js b/test/unit/polyline.js index 6e263efb..8527fe64 100644 --- a/test/unit/polyline.js +++ b/test/unit/polyline.js @@ -35,6 +35,8 @@ 'clipTo': null, 'fillRule': 'nonzero', 'globalCompositeOperation': 'source-over', + 'skewX': 0, + 'skewY': 0, 'transformMatrix': null }; diff --git a/test/unit/rect.js b/test/unit/rect.js index 3771b530..eed2f7ce 100644 --- a/test/unit/rect.js +++ b/test/unit/rect.js @@ -28,10 +28,10 @@ 'fillRule': 'nonzero', 'globalCompositeOperation': 'source-over', 'transformMatrix': null, - 'skewX': 0, - 'skewY': 0, 'rx': 0, 'ry': 0, + 'skewX': 0, + 'skewY': 0, }; QUnit.module('fabric.Rect'); @@ -104,7 +104,6 @@ elRectWithAttrs.setAttribute('stroke-linecap', 'round'); elRectWithAttrs.setAttribute('stroke-linejoin', 'bevil'); elRectWithAttrs.setAttribute('stroke-miterlimit', 5); - elRectWithAttrs.setAttribute('skewX', 30); //elRectWithAttrs.setAttribute('transform', 'translate(-10,-20) scale(2) rotate(45) translate(5,10)'); var rectWithAttrs = fabric.Rect.fromElement(elRectWithAttrs); @@ -123,7 +122,6 @@ strokeLineCap: 'round', strokeLineJoin: 'bevil', strokeMiterLimit: 5, - skewX: 30, rx: 11, ry: 12 }); diff --git a/test/unit/text.js b/test/unit/text.js index f9311099..c62e3ce8 100644 --- a/test/unit/text.js +++ b/test/unit/text.js @@ -44,6 +44,8 @@ 'textBackgroundColor': '', 'fillRule': 'nonzero', 'globalCompositeOperation': 'source-over', + 'skewX': 0, + 'skewY': 0, 'transformMatrix': null }; @@ -154,8 +156,8 @@ // text.width = CHAR_WIDTH; var expectedObject = fabric.util.object.extend(fabric.util.object.clone(REFERENCE_TEXT_OBJECT), { - left: 4, - top: -3.61, + left: 4.5, + top: -4.11, width: 8, height: 20.97, fontSize: 16, @@ -196,7 +198,7 @@ var expectedObject = fabric.util.object.extend(fabric.util.object.clone(REFERENCE_TEXT_OBJECT), { /* left varies slightly due to node-canvas rendering */ left: fabric.util.toFixed(textWithAttrs.left + '', 2), - top: -7.72, + top: -9.22, width: CHAR_WIDTH, height: 161.23, fill: 'rgb(255,255,255)',