Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Smooth Path utils #7140

Merged
merged 9 commits into from
Jun 27, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 16 additions & 33 deletions src/brushes/pencil_brush.class.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)) {
ShaMan123 marked this conversation as resolved.
Show resolved Hide resolved
// do not create 0 width/height paths, as they are
// rendered inconsistently across browsers
// Firefox 4, for example, renders a dot,
Expand Down
6 changes: 3 additions & 3 deletions src/mixins/eraser_brush.mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
69 changes: 69 additions & 0 deletions src/util/path.js
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,18 @@
}
}

/**
*
* @param {string} pathString
* @return {(string|number)[][]} An array of SVG path commands
* @example <caption>Usage</caption>
* 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 = [],
Expand Down Expand Up @@ -729,6 +741,62 @@
return result;
};

/**
*
* Converts points to a smooth SVG path
* @param {Array} points Array of points
* @param {number} [correction] Apply a correction to the path (usually we use `width / 1000`). If undefined it will be inferred using `points`.
* @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;

// if no correction is passed we infer it by calculating the path's width
if (correction === undefined) {
var start = points[0].x;
var minMax = points.reduce(
function (prev, curr) {
return {
min: Math.min(curr.x, prev.min),
max: Math.max(curr.x, prev.max)
};
},
{ min: start, max: start }
);
var width = minMax.max - minMax.min;
correction = width / 1000;
}
ShaMan123 marked this conversation as resolved.
Show resolved Hide resolved

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
Expand Down Expand Up @@ -775,6 +843,7 @@

fabric.util.parsePath = parsePath;
fabric.util.makePathSimpler = makePathSimpler;
fabric.util.getSmoothPathFromPoints = getSmoothPathFromPoints;
fabric.util.getPathSegmentsInfo = getPathSegmentsInfo;
fabric.util.fromArcToBeziers = fromArcToBeziers;
/**
Expand Down
33 changes: 23 additions & 10 deletions test/unit/brushes.js
Original file line number Diff line number Diff line change
@@ -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();
Expand Down Expand Up @@ -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);
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down
19 changes: 19 additions & 0 deletions test/unit/path_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,23 @@
assert.deepEqual(infos[3].length, 20, 'the command 3 a L has length 20');
assert.deepEqual(infos[4].length, 20, 'the command 4 a Z has length 20');
});

QUnit.test('fabric.util.getSmoothPathFromPoints infer correction from points', function (assert) {
var done = assert.async();
assert.ok(typeof fabric.util.getSmoothPathFromPoints === 'function');
var points = [];
var pathData = [
["M", 100.2, 99.8],
["Q", 100, 100, 200, 100],
["Q", 300, 100, 250, 200],
["L", 199.8, 300.2]
];
[['M', 100, 100], ['L', 300, 100], ['L', 200, 300], ['z']].forEach(function (item) {
if (item.length > 2) {
points.push(new fabric.Point(item[1], item[2]));
}
});
assert.deepEqual(fabric.util.getSmoothPathFromPoints(points), pathData, 'path is correct');
done();
});
})();