diff --git a/src/brushes/pencil_brush.class.js b/src/brushes/pencil_brush.class.js index e9c3830b19c..68c7c7f052a 100644 --- a/src/brushes/pencil_brush.class.js +++ b/src/brushes/pencil_brush.class.js @@ -179,43 +179,26 @@ /** * Converts points to SVG path * @param {Array} points Array of points - * @return {String} SVG path + * @return {(string|number)[][]} SVG path commands */ - convertPointsToSVGPath: function(points) { - var path = [], i, width = this.width / 1000, - p1 = new fabric.Point(points[0].x, points[0].y), - p2 = new fabric.Point(points[1].x, points[1].y), - len = points.length, multSignX = 1, multSignY = 0, manyPoints = len > 2; + convertPointsToSVGPath: function (points) { + var correction = this.width / 1000; + return fabric.util.getSmoothPathFromPoints(points, correction); + }, - if (manyPoints) { - multSignX = points[2].x < p2.x ? -1 : points[2].x === p2.x ? 0 : 1; - multSignY = points[2].y < p2.y ? -1 : points[2].y === p2.y ? 0 : 1; - } - path.push('M ', p1.x - multSignX * width, ' ', p1.y - multSignY * width, ' '); - for (i = 1; i < len; i++) { - if (!p1.eq(p2)) { - var midPoint = p1.midPointFrom(p2); - // p1 is our bezier control point - // midpoint is our endpoint - // start point is p(i-1) value. - path.push('Q ', p1.x, ' ', p1.y, ' ', midPoint.x, ' ', midPoint.y, ' '); - } - p1 = points[i]; - if ((i + 1) < points.length) { - p2 = points[i + 1]; - } - } - if (manyPoints) { - multSignX = p1.x > points[i - 2].x ? 1 : p1.x === points[i - 2].x ? 0 : -1; - multSignY = p1.y > points[i - 2].y ? 1 : p1.y === points[i - 2].y ? 0 : -1; - } - path.push('L ', p1.x + multSignX * width, ' ', p1.y + multSignY * width); - return path; + /** + * @private + * @param {(string|number)[][]} pathData SVG path commands + * @returns {boolean} + */ + _isEmptySVGPath: function (pathData) { + var pathString = pathData.map(function (segment) { return segment.join(' '); }).join(' '); + return pathString === 'M 0 0 Q 0 0 0 0 L 0 0'; }, /** * Creates fabric.Path object to add on canvas - * @param {String} pathData Path data + * @param {(string|number)[][]} pathData Path data * @return {fabric.Path} Path to add on canvas */ createPath: function(pathData) { @@ -272,8 +255,8 @@ if (this.decimate) { this._points = this.decimatePoints(this._points, this.decimate); } - var pathData = this.convertPointsToSVGPath(this._points).join(''); - if (pathData === 'M 0 0 Q 0 0 0 0 L 0 0') { + var pathData = this.convertPointsToSVGPath(this._points); + if (this._isEmptySVGPath(pathData)) { // do not create 0 width/height paths, as they are // rendered inconsistently across browsers // Firefox 4, for example, renders a dot, diff --git a/src/mixins/eraser_brush.mixin.js b/src/mixins/eraser_brush.mixin.js index 7e4568571e7..52a977e03f8 100644 --- a/src/mixins/eraser_brush.mixin.js +++ b/src/mixins/eraser_brush.mixin.js @@ -674,9 +674,9 @@ this._isErasing = false; var pathData = this._points && this._points.length > 1 ? - this.convertPointsToSVGPath(this._points).join('') : - 'M 0 0 Q 0 0 0 0 L 0 0'; - if (pathData === 'M 0 0 Q 0 0 0 0 L 0 0') { + this.convertPointsToSVGPath(this._points) : + null; + if (!pathData || this._isEmptySVGPath(pathData)) { canvas.fire('erasing:end'); // do not create 0 width/height paths, as they are // rendered inconsistently across browsers diff --git a/src/util/path.js b/src/util/path.js index b72378ea05f..b6479352b4c 100644 --- a/src/util/path.js +++ b/src/util/path.js @@ -661,6 +661,18 @@ } } + /** + * + * @param {string} pathString + * @return {(string|number)[][]} An array of SVG path commands + * @example Usage + * parsePath('M 3 4 Q 3 5 2 1 4 0 Q 9 12 2 1 4 0') === [ + * ['M', 3, 4], + * ['Q', 3, 5, 2, 1, 4, 0], + * ['Q', 9, 12, 2, 1, 4, 0], + * ]; + * + */ function parsePath(pathString) { var result = [], coords = [], @@ -729,6 +741,46 @@ return result; }; + /** + * + * Converts points to a smooth SVG path + * @param {{ x: number,y: number }[]} points Array of points + * @param {number} [correction] Apply a correction to the path (usually we use `width / 1000`). If value is undefined 0 is used as the correction value. + * @return {(string|number)[][]} An array of SVG path commands + */ + function getSmoothPathFromPoints(points, correction) { + var path = [], i, + p1 = new fabric.Point(points[0].x, points[0].y), + p2 = new fabric.Point(points[1].x, points[1].y), + len = points.length, multSignX = 1, multSignY = 0, manyPoints = len > 2; + correction = correction || 0; + + if (manyPoints) { + multSignX = points[2].x < p2.x ? -1 : points[2].x === p2.x ? 0 : 1; + multSignY = points[2].y < p2.y ? -1 : points[2].y === p2.y ? 0 : 1; + } + path.push(['M', p1.x - multSignX * correction, p1.y - multSignY * correction]); + for (i = 1; i < len; i++) { + if (!p1.eq(p2)) { + var midPoint = p1.midPointFrom(p2); + // p1 is our bezier control point + // midpoint is our endpoint + // start point is p(i-1) value. + path.push(['Q', p1.x, p1.y, midPoint.x, midPoint.y]); + } + p1 = points[i]; + if ((i + 1) < points.length) { + p2 = points[i + 1]; + } + } + if (manyPoints) { + multSignX = p1.x > points[i - 2].x ? 1 : p1.x === points[i - 2].x ? 0 : -1; + multSignY = p1.y > points[i - 2].y ? 1 : p1.y === points[i - 2].y ? 0 : -1; + } + path.push(['L', p1.x + multSignX * correction, p1.y + multSignY * correction]); + return path; + } + /** * Calculate bounding box of a elliptic-arc * @deprecated @@ -775,6 +827,7 @@ fabric.util.parsePath = parsePath; fabric.util.makePathSimpler = makePathSimpler; + fabric.util.getSmoothPathFromPoints = getSmoothPathFromPoints; fabric.util.getPathSegmentsInfo = getPathSegmentsInfo; fabric.util.fromArcToBeziers = fromArcToBeziers; /** diff --git a/test/unit/brushes.js b/test/unit/brushes.js index f8cd02199f2..a018089ad69 100644 --- a/test/unit/brushes.js +++ b/test/unit/brushes.js @@ -1,5 +1,6 @@ (function() { var canvas = new fabric.Canvas(); + var parsePath = fabric.util.parsePath; QUnit.module('fabric.BaseBrush', function(hooks) { hooks.afterEach(function() { canvas.cancelRequestedRender(); @@ -40,8 +41,8 @@ var brush = new fabric.PencilBrush(canvas); var pointer = canvas.getPointer({ clientX: 10, clientY: 10}); brush.onMouseDown(pointer, { e: {} }); - var pathData = brush.convertPointsToSVGPath(brush._points).join(''); - assert.equal(pathData, 'M 9.999 10 L 10.001 10', 'path data create a small line that looks like a point'); + var pathData = brush.convertPointsToSVGPath(brush._points); + assert.deepEqual(pathData, parsePath('M 9.999 10 L 10.001 10'), 'path data create a small line that looks like a point'); }); QUnit.test('fabric pencil brush multiple points', function(assert) { var brush = new fabric.PencilBrush(canvas); @@ -51,8 +52,8 @@ brush.onMouseMove(pointer, { e: {} }); brush.onMouseMove(pointer, { e: {} }); brush.onMouseMove(pointer, { e: {} }); - var pathData = brush.convertPointsToSVGPath(brush._points).join(''); - assert.equal(pathData, 'M 9.999 10 L 10.001 10', 'path data create a small line that looks like a point'); + var pathData = brush.convertPointsToSVGPath(brush._points); + assert.deepEqual(pathData, parsePath('M 9.999 10 L 10.001 10'), 'path data create a small line that looks like a point'); assert.equal(brush._points.length, 2, 'concident points are discarded'); }); QUnit.test('fabric pencil brush multiple points not discarded', function(assert) { @@ -65,8 +66,12 @@ brush.onMouseMove(pointer3, { e: {} }); brush.onMouseMove(pointer2, { e: {} }); brush.onMouseMove(pointer3, { e: {} }); - var pathData = brush.convertPointsToSVGPath(brush._points).join(''); - assert.equal(pathData, 'M 9.999 9.999 Q 10 10 12.5 12.5 Q 15 15 17.5 17.5 Q 20 20 17.5 17.5 Q 15 15 17.5 17.5 L 20.001 20.001', 'path data create a complex path'); + var pathData = brush.convertPointsToSVGPath(brush._points); + assert.deepEqual( + pathData, + parsePath('M 9.999 9.999 Q 10 10 12.5 12.5 Q 15 15 17.5 17.5 Q 20 20 17.5 17.5 Q 15 15 17.5 17.5 L 20.001 20.001'), + 'path data create a complex path' + ); assert.equal(brush._points.length, 6, 'concident points are discarded'); }); QUnit.test('fabric pencil brush multiple points outside canvas', function(assert) { @@ -81,8 +86,12 @@ brush.onMouseMove(pointer3, { e: {} }); brush.onMouseMove(pointer4, { e: {} }); brush.onMouseMove(pointer5, { e: {} }); - var pathData = brush.convertPointsToSVGPath(brush._points).join(''); - assert.equal(pathData, 'M 9.999 9.999 Q 10 10 12.5 55 Q 15 100 17.5 130 Q 20 160 170 130 Q 320 100 210 100 L 99.999 100', 'path data create a path that goes beyond canvas'); + var pathData = brush.convertPointsToSVGPath(brush._points); + assert.deepEqual( + pathData, + parsePath('M 9.999 9.999 Q 10 10 12.5 55 Q 15 100 17.5 130 Q 20 160 170 130 Q 320 100 210 100 L 99.999 100'), + 'path data create a path that goes beyond canvas' + ); assert.equal(brush._points.length, 6, 'all points are available'); }); QUnit.test('fabric pencil brush multiple points outside canvas, limitedToCanvasSize true', function(assert) { @@ -98,8 +107,12 @@ brush.onMouseMove(pointer3, { e: {} }); brush.onMouseMove(pointer4, { e: {} }); brush.onMouseMove(pointer5, { e: {} }); - var pathData = brush.convertPointsToSVGPath(brush._points).join(''); - assert.equal(pathData, 'M 9.999 9.999 Q 10 10 12.5 55 Q 15 100 57.5 100 L 100.001 100', 'path data create a path that does not go beyond canvas'); + var pathData = brush.convertPointsToSVGPath(brush._points); + assert.deepEqual( + pathData, + parsePath('M 9.999 9.999 Q 10 10 12.5 55 Q 15 100 57.5 100 L 100.001 100'), + 'path data create a path that does not go beyond canvas' + ); assert.equal(brush._points.length, 4, '2 points have been discarded'); }); QUnit.test('fabric pencil brush multiple points not discarded', function(assert) {