diff --git a/src/shapes/line.class.js b/src/shapes/line.class.js index 121f2803..50708a31 100644 --- a/src/shapes/line.class.js +++ b/src/shapes/line.class.js @@ -90,11 +90,11 @@ this.left = 'left' in options ? options.left - : (Math.min(this.x1, this.x2) + this.width / 2); + : this._getLeftToOriginX(); this.top = 'top' in options ? options.top - : (Math.min(this.y1, this.y2) + this.height / 2); + : this._getTopToOriginY(); }, /** @@ -110,6 +110,77 @@ return this; }, + /** + * @private + * @return {Number} leftToOriginX Distance from left edge of canvas to originX of Line. + */ + _getLeftToOriginX: makeEdgeToOriginGetter( + { // property names + origin: 'originX', + axis1: 'x1', + axis2: 'x2', + dimension: 'width', + }, + { // possible values of origin + nearest: 'left', + center: 'center', + farthest: 'right', + } + ), + + /** + * @private + * @return {Number} topToOriginY Distance from top edge of canvas to originY of Line. + */ + _getTopToOriginY: makeEdgeToOriginGetter( + { // property names + origin: 'originY', + axis1: 'y1', + axis2: 'y2', + dimension: 'height', + }, + { // possible values of origin + nearest: 'top', + center: 'center', + farthest: 'bottom', + } + ), + + /** + * @private + * @return {Number} centerToCenterX Distance from center of path group to horizontal center of Line. + */ + _getCenterToCenterX: makeCenterToCenterGetter( + { // property names + origin: 'originX', + coordinate: 'left', + dimension: 'width', + }, + { // possible values of origin + nearest: 'left', + center: 'center', + farthest: 'right', + } + ), + + /** + * @private + * @return {Number} centerToOriginY Distance from center of path group to vertical center of Line. + */ + _getCenterToCenterY: makeCenterToCenterGetter( + { // property names + origin: 'originY', + coordinate: 'top', + dimension: 'height', + }, + { // possible values of origin + nearest: 'top', + center: 'center', + farthest: 'bottom', + } + ), + + /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on @@ -119,7 +190,10 @@ var isInPathGroup = this.group && this.group.type === 'path-group'; if (isInPathGroup && !this.transformMatrix) { - ctx.translate(-this.group.width/2 + this.left, -this.group.height / 2 + this.top); + ctx.translate( + this._getCenterToCenterX(), + this._getCenterToCenterY() + ); } if (!this.strokeDashArray || this.strokeDashArray && supportsLineDash) { @@ -253,4 +327,62 @@ return new fabric.Line(points, object); }; + /** + * Produces a function that calculates distance from canvas edge to Line origin. + */ + function makeEdgeToOriginGetter(propertyNames, originValues) { + var origin = propertyNames.origin, + axis1 = propertyNames.axis1, + axis2 = propertyNames.axis2, + dimension = propertyNames.dimension, + nearest = originValues.nearest, + center = originValues.center, + farthest = originValues.farthest; + + return function() { + switch (this.get(origin)) { + case nearest: + return Math.min(this.get(axis1), this.get(axis2)); + case center: + return Math.min(this.get(axis1), this.get(axis2)) + (0.5 * this.get(dimension)); + case farthest: + return Math.max(this.get(axis1), this.get(axis2)); + } + }; + + } + + /** + * Produces a function that calculates distance from path group center to center of Line dimension. + * + * The context starts off at center-center of path-group, while line coords + * are relative to left-top of canvas. Additionally, rendering assumes contex + * will start at center-center of line. + * To get coords of line's center-center relative to center-center of + * path-group we subtract the distance from center of PG to it's edge. + * To get the center of line, we add the distance from it's origin to it's + * center. + * + */ + function makeCenterToCenterGetter(propertyNames, originValues) { + var origin = propertyNames.origin, + coordinate = propertyNames.coordinate, + dimension = propertyNames.dimension, + nearest = originValues.nearest, + center = originValues.center, + farthest = originValues.farthest; + + return function() { + pathGroupCenterToEdge = (-0.5 * this.group.get(dimension)) + switch (this.get(origin)) { + case nearest: + return pathGroupCenterToEdge + this.get(coordinate) + (0.5 * this.get(dimension)); + case center: + return pathGroupCenterToEdge + this.get(coordinate); + case farthest: + return pathGroupCenterToEdge + this.get(coordinate) - (0.5 * this.get(dimension)); + } + }; + } + })(typeof exports !== 'undefined' ? exports : this); diff --git a/test/unit/line.js b/test/unit/line.js index 1c3102e5..3d3d1a19 100644 --- a/test/unit/line.js +++ b/test/unit/line.js @@ -4,8 +4,8 @@ 'type': 'line', 'originX': 'left', 'originY': 'top', - 'left': 12, - 'top': 13, + 'left': 11, + 'top': 12, 'width': 2, 'height': 2, 'fill': 'rgb(0,0,0)', @@ -164,4 +164,618 @@ // equal(200, line.height); // }); + var lineCoordsCases = [ + { description: 'default to 0 left and 0 top', + givenLineArgs: {}, + expectedCoords: { + left: 0, + top: 0, + } + }, + { description: 'origin defaults to left-top', + givenLineArgs: { + points: [0, 0, 11, 22], + }, + expectedCoords: { + left: 0, + top: 0, + } + }, + { description: 'equal smallest points when origin is left-top and line not offset', + givenLineArgs: { + points: [0, 0, 12.3, 34.5], + options: { + originX: 'left', + originY: 'top', + }, + }, + expectedCoords: { + left: 0, + top: 0, + } + }, + { description: 'include offsets for left-top origin', + givenLineArgs: { + points: [0+33, 0+44, 11+33, 22+44], + options: { + originX: 'left', + originY: 'top', + }, + }, + expectedCoords: { + left: 33, + top: 44, + } + }, + { description: 'equal half-dimensions when origin is center and line not offset', + givenLineArgs: { + points: [0, 0, 12.3, 34.5], + options: { + originX: 'center', + originY: 'center', + }, + }, + expectedCoords: { + left: 0.5 * 12.3, + top: 0.5 * 34.5, + } + }, + { description: 'include offsets for center-center origin', + givenLineArgs: { + points: [0+9.87, 0-4.32, 12.3+9.87, 34.5-4.32], + options: { + originX: 'center', + originY: 'center', + }, + }, + expectedCoords: { + left: (0.5 * 12.3) + 9.87, + top: (0.5 * 34.5) - 4.32, + } + }, + { description: 'equal full dimensions when origin is right-bottom and line not offset', + givenLineArgs: { + points: [0, 0, 55, 18], + options: { + originX: 'right', + originY: 'bottom', + }, + }, + expectedCoords: { + left: 55, + top: 18, + } + }, + { description: 'include offsets for right-bottom origin', + givenLineArgs: { + points: [0-3.14, 0-1.41, 55-3.14, 18-1.41], + options: { + originX: 'right', + originY: 'bottom', + }, + }, + expectedCoords: { + left: 55 - 3.14, + top: 18 - 1.41, + } + }, + { description: 'arent changed by rotation for left-top origin', + givenLineArgs: { + points: [1, 2, 30, 40], + options: { + originX: 'left', + originY: 'top', + angle: 67, + } + }, + expectedCoords: { + left: 1, + top: 2, + } + }, + { description: 'arent changed by rotation for right-bottom origin', + givenLineArgs: { + points: [1, 2, 30, 40], + options: { + originX: 'right', + originY: 'bottom', + angle: 67, + } + }, + expectedCoords: { + left: 30, + top: 40, + } + }, + { description: 'arent changed by scaling for left-top origin', + givenLineArgs: { + points: [1, 2, 30, 40], + options: { + originX: 'left', + originY: 'top', + scale: 2.1, + } + }, + expectedCoords: { + left: 1, + top: 2, + } + }, + { description: 'arent changed by scaling for right-bottom origin', + givenLineArgs: { + points: [1, 2, 30, 40], + options: { + originX: 'right', + originY: 'bottom', + scale: 1.2, + } + }, + expectedCoords: { + left: 30, + top: 40, + } + }, + { description: 'arent changed by strokeWidth for left-top origin', + givenLineArgs: { + points: [31, 41, 59, 26], + options: { + originX: 'left', + originY: 'top', + stroke: 'black', + strokeWidth: '53' + } + }, + expectedCoords: { + left: 31, + top: 26, + } + }, + { description: 'arent changed by strokeWidth for center-center origin', + givenLineArgs: { + points: [0+31, 15+26, 28+31, 0+26], + options: { + originX: 'center', + originY: 'center', + stroke: 'black', + strokeWidth: '53' + } + }, + expectedCoords: { + left: (0.5 * 28) + 31, + top: (0.5 * 15) + 26, + } + }, + { description: 'arent changed by strokeWidth for right-bottom origin', + givenLineArgs: { + points: [1, 2, 30, 40], + options: { + originX: 'right', + originY: 'bottom', + stroke: 'black', + strokeWidth: '53' + } + }, + expectedCoords: { + left: 30, + top: 40, + } + }, + { description: 'left and top options override points', + givenLineArgs: { + points: [12, 34, 56, 78], + options: { + left: 98, + top: 76, + } + }, + expectedCoords: { + left: 98, + top: 76, + } + }, + { description: '0 left and 0 top options override points', + givenLineArgs: { + points: [12, 34, 56, 78], + options: { + left: 0, + top: 0, + } + }, + expectedCoords: { + left: 0, + top: 0, + } + }, + { description: 'equal x2 and y2 for left-top origin when x1 and y1 are largest and line not offset', + givenLineArgs: { + points: [100, 200, 30, 40], + options: { + originX: 'left', + originY: 'top', + } + }, + expectedCoords: { + left: 30, + top: 40, + } + }, + { description: 'equal half-dimensions for center-center origin when x1 and y1 are largest and line not offset', + givenLineArgs: { + points: [100, 200, 0, 0], + options: { + originX: 'center', + originY: 'center', + } + }, + expectedCoords: { + left: 0.5 * 100, + top: 0.5 * 200, + } + }, + { description: 'equal x1 and y1 for right-bottom origin when x1 and y1 are largest and line not offset', + givenLineArgs: { + points: [100, 200, 0, 0], + options: { + originX: 'right', + originY: 'bottom', + } + }, + expectedCoords: { + left: 100, + top: 200, + } + }, + ]; + + lineCoordsCases.forEach(function (c_) { + test('stroke-less line coords ' + c_.description, function() { + var points = c_.givenLineArgs.points; + var options = c_.givenLineArgs.options; + + var givenLine = new fabric.Line( + points, + options + ); + + equal(givenLine.left, c_.expectedCoords.left); + equal(givenLine.top, c_.expectedCoords.top); + }); + }); + + var getLeftToOriginXCases = [ + { description: 'is x1 for left origin and x1 lesser than x2', + givenOrigin: 'left', + givenPoints: [0, 0, 1, 0], + expectedLeft: 0, + }, + { description: 'is x2 for left origin and x1 greater than x2', + givenOrigin: 'left', + givenPoints: [1, 0, 0, 0], + expectedLeft: 0, + }, + { description: 'includes positive offset for left origin', + givenOrigin: 'left', + givenPoints: [0+20, 0, 1+20, 0], + expectedLeft: 0+20, + }, + { description: 'includes negative offset for left origin', + givenOrigin: 'left', + givenPoints: [0-11, 0, 1-11, 0], + expectedLeft: 0-11, + }, + { description: 'is half of x1 for center origin and x1 > x2', + givenOrigin: 'center', + givenPoints: [4, 0, 0, 0], + expectedLeft: 0.5 * 4, + }, + { description: 'is half of x2 for center origin and x1 < x2', + givenOrigin: 'center', + givenPoints: [0, 0, 7, 0], + expectedLeft: 0.5 * 7, + }, + { description: 'includes positive offset for center origin', + givenOrigin: 'center', + givenPoints: [0+39, 0, 7+39, 0], + expectedLeft: (0.5 * 7) + 39, + }, + { description: 'includes negative offset for center origin', + givenOrigin: 'center', + givenPoints: [4-13, 0, 0-13, 0], + expectedLeft: (0.5 * 4) - 13, + }, + { description: 'is x1 for right origin and x1 > x2', + givenOrigin: 'right', + givenPoints: [9, 0, 0, 0], + expectedLeft: 9, + }, + { description: 'is x2 for right origin and x1 < x2', + givenOrigin: 'right', + givenPoints: [0, 0, 6, 0], + expectedLeft: 6, + }, + { description: 'includes positive offset for right origin', + givenOrigin: 'right', + givenPoints: [0+47, 0, 6+47, 0], + expectedLeft: 6 + 47, + }, + { description: 'includes negative offset for right origin', + givenOrigin: 'right', + givenPoints: [9-17, 0, 0-17, 0], + expectedLeft: 9 - 17, + }, + ]; + + getLeftToOriginXCases.forEach(function (c_) { + test('Line.getLeftToOriginX() ' + c_.description, function () { + var line = new fabric.Line( + c_.givenPoints, + { originX: c_.givenOrigin } + ); + + equal(line._getLeftToOriginX(), c_.expectedLeft); + }); + }); + + var getTopToOriginYCases = [ + { description: 'is y1 for top origin and y1 lesser than y2', + givenOrigin: 'top', + givenPoints: [0, 0, 0, 1], + expectedTop: 0, + }, + { description: 'is y2 for top origin and y1 greater than y2', + givenOrigin: 'top', + givenPoints: [0, 1, 0, 0], + expectedTop: 0, + }, + { description: 'includes positive offset for top origin', + givenOrigin: 'top', + givenPoints: [0, 0+20, 0, 1+20], + expectedTop: 0+20, + }, + { description: 'includes negative offset for top origin', + givenOrigin: 'top', + givenPoints: [0, 0-11, 0, 1-11], + expectedTop: 0-11, + }, + { description: 'is half of y1 for center origin and y1 > y2', + givenOrigin: 'center', + givenPoints: [0, 4, 0, 0], + expectedTop: 0.5 * 4, + }, + { description: 'is half of y2 for center origin and y1 < y2', + givenOrigin: 'center', + givenPoints: [0, 0, 0, 7], + expectedTop: 0.5 * 7, + }, + { description: 'includes positive offset for center origin', + givenOrigin: 'center', + givenPoints: [0, 0+39, 0, 7+39], + expectedTop: (0.5 * 7) + 39, + }, + { description: 'includes negative offset for center origin', + givenOrigin: 'center', + givenPoints: [0, 4-13, 0, 0-13], + expectedTop: (0.5 * 4) - 13, + }, + { description: 'is y1 for bottom origin and y1 > y2', + givenOrigin: 'bottom', + givenPoints: [0, 9, 0, 0], + expectedTop: 9, + }, + { description: 'is y2 for bottom origin and y1 < y2', + givenOrigin: 'bottom', + givenPoints: [0, 0, 0, 6], + expectedTop: 6, + }, + { description: 'includes positive offset for bottom origin', + givenOrigin: 'bottom', + givenPoints: [0, 0+47, 0, 6+47], + expectedTop: 6 + 47, + }, + { description: 'includes negative offset for bottom origin', + givenOrigin: 'bottom', + givenPoints: [0, 9-17, 0, 0-17], + expectedTop: 9 - 17, + }, + ]; + + getTopToOriginYCases.forEach(function (c_) { + test('Line._getTopToOriginY() ' + c_.description, function () { + var line = new fabric.Line( + c_.givenPoints, + { originY: c_.givenOrigin } + ); + + equal(line._getTopToOriginY(), c_.expectedTop); + }); + }); + + var getCenterToCenterXCases = [ + { description: 'for center origin, is the distance to the left edge of group', + given: { + left: 0, + originX: 'center', + group: { width: 10 } + }, + expectedCenter: (-0.5 * 10), + }, + { description: 'includes negative offset for center origin', + given: { + left: 0-11, + originX: 'center', + group: { width: 20 } + }, + expectedCenter: (-0.5 * 20) - 11, + }, + { description: 'includes positive offset for center origin', + given: { + left: 0+7, + originX: 'center', + group: { width: 30 } + }, + expectedCenter: (-0.5 * 30) + 7, + }, + { description: 'for left origin, is the distance to the left edge of group, offset by half-line-width to the right', + givenWidth: 17, + given: { + left: 0, + originX: 'left', + group: { width: 12 } + }, + expectedCenter: (-0.5 * 12) + (0.5 * 17), + }, + { description: 'includes negative offset for left origin', + givenWidth: 17, + given: { + left: 0-13, + originX: 'left', + group: { width: 22 } + }, + expectedCenter: (-0.5 * 22) + (0.5 * 17) - 13, + }, + { description: 'includes positive offset for left origin', + givenWidth: 17, + given: { + left: 0+19, + originX: 'left', + group: { width: 32 } + }, + expectedCenter: (-0.5 * 32) + (0.5 * 17) + 19, + }, + { description: 'for right origin, is the distance to the left edge of group, offset by half-line-width to the left', + givenWidth: 17, + given: { + left: 0, + originX: 'right', + group: { width: 13 } + }, + expectedCenter: (-0.5 * 13) - (0.5 * 17), + }, + { description: 'includes negative offset for right origin', + givenWidth: 17, + given: { + left: 0-15, + originX: 'right', + group: { width: 23 } + }, + expectedCenter: (-0.5 * 23) - (0.5 * 17) - 15, + }, + { description: 'includes positive offset for right origin', + givenWidth: 17, + given: { + left: 0+21, + originX: 'right', + group: { width: 33 } + }, + expectedCenter: (-0.5 * 33) - (0.5 * 17) + 21, + }, + ]; + + getCenterToCenterXCases.forEach(function (c_) { + test('Line._getCenterToCenterX ' + c_.description, function () { + var line = new fabric.Line( + [0, 0, c_.givenWidth, 0] + ); + for (var prop in c_.given) { + line.set(prop, c_.given[prop]) + } + + equal(line._getCenterToCenterX(), c_.expectedCenter); + }); + }); + + var getCenterToCenterYCases = [ + { description: 'for center origin, is the distance to the top edge of group', + given: { + top: 0, + originY: 'center', + group: { height: 10 } + }, + expectedCenter: (-0.5 * 10), + }, + { description: 'includes negative offset for center origin', + given: { + top: 0-11, + originY: 'center', + group: { height: 20 } + }, + expectedCenter: (-0.5 * 20) - 11, + }, + { description: 'includes positive offset for center origin', + given: { + top: 0+7, + originY: 'center', + group: { height: 30 } + }, + expectedCenter: (-0.5 * 30) + 7, + }, + { description: 'for top origin, is the distance to the top edge of group, offset by half-line-height to the bottom', + givenHeight: 17, + given: { + top: 0, + originY: 'top', + group: { height: 12 } + }, + expectedCenter: (-0.5 * 12) + (0.5 * 17), + }, + { description: 'includes negative offset for top origin', + givenHeight: 17, + given: { + top: 0-13, + originY: 'top', + group: { height: 22 } + }, + expectedCenter: (-0.5 * 22) + (0.5 * 17) - 13, + }, + { description: 'includes positive offset for top origin', + givenHeight: 17, + given: { + top: 0+19, + originY: 'top', + group: { height: 32 } + }, + expectedCenter: (-0.5 * 32) + (0.5 * 17) + 19, + }, + { description: 'for bottom origin, is the distance to the top edge of group, offset by half-line-height to the top', + givenHeight: 17, + given: { + top: 0, + originY: 'bottom', + group: { height: 13 } + }, + expectedCenter: (-0.5 * 13) - (0.5 * 17), + }, + { description: 'includes negative offset for bottom origin', + givenHeight: 17, + given: { + top: 0-15, + originY: 'bottom', + group: { height: 23 } + }, + expectedCenter: (-0.5 * 23) - (0.5 * 17) - 15, + }, + { description: 'includes positive offset for bottom origin', + givenHeight: 17, + given: { + top: 0+21, + originY: 'bottom', + group: { height: 33 } + }, + expectedCenter: (-0.5 * 33) - (0.5 * 17) + 21, + }, + ]; + + getCenterToCenterYCases.forEach(function (c_) { + test('Line._getCenterToCenterY ' + c_.description, function () { + var line = new fabric.Line( + [0, 0, 0, c_.givenHeight] + ); + for (var prop in c_.given) { + line.set(prop, c_.given[prop]) + } + + equal(line._getCenterToCenterY(), c_.expectedCenter); + }); + }); + })();