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

feat(fabric.util): Add a function to work with path measurements #6525

Merged
merged 8 commits into from
Aug 23, 2020
Merged
Show file tree
Hide file tree
Changes from all 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
184 changes: 169 additions & 15 deletions src/util/path.js
Original file line number Diff line number Diff line change
Expand Up @@ -396,39 +396,192 @@
return destinationPath;
};

/**
* Calc length from point x1,y1 to x2,y2
* @param {Number} x1 starting point x
* @param {Number} y1 starting point y
* @param {Number} x2 starting point x
* @param {Number} y2 starting point y
* @return {Number} length of segment
*/
function calcLineLength(x1, y1, x2, y2) {
return Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
}

// functions for the Cubic beizer
// taken from: https://github.com/konvajs/konva/blob/7.0.5/src/shapes/Path.ts#L350
function CB1(t) {
return t * t * t;
}
function CB2(t) {
return 3 * t * t * (1 - t);
}
function CB3(t) {
return 3 * t * (1 - t) * (1 - t);
}
function CB4(t) {
return (1 - t) * (1 - t) * (1 - t);
}

function getPointOnCubicBezierIterator(p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y) {
return function(pct) {
var c1 = CB1(pct), c2 = CB2(pct), c3 = CB3(pct), c4 = CB4(pct);
return {
x: p4x * c1 + p3x * c2 + p2x * c3 + p1x * c4,
y: p4y * c1 + p3y * c2 + p2y * c3 + p1y * c4
};
};
}

function QB1(t) {
return t * t;
}

function QB2(t) {
return 2 * t * (1 - t);
}

function QB3(t) {
return (1 - t) * (1 - t);
}

function getPointOnQuadraticBezierIterator(p1x, p1y, p2x, p2y, p3x, p3y) {
return function(pct) {
var c1 = QB1(pct), c2 = QB2(pct), c3 = QB3(pct);
return {
x: p3x * c1 + p2x * c2 + p1x * c3,
y: p3y * c1 + p2y * c2 + p1y * c3
};
};
}

function pathIterator(iterator, x1, y1) {
var tempP = { x: x1, y: y1 }, p, tmpLen = 0, perc;
for (perc = 0.01; perc <= 1; perc += 0.01) {
p = iterator(perc);
tmpLen += calcLineLength(tempP.x, tempP.y, p.x, p.y);
tempP = p;
}
return tmpLen;
}

//measures the length of a pre-simplified path
function measurePath(path) {
function getPathSegmentsInfo(path) {
var totalLength = 0, len = path.length, current,
x1 = 0, y1 = 0, x2 = 0, y2 = 0;
//x1 and y1 are the coords of the previous point on the path
//x2 and y2 are the coords of the current point
//x2 and y2 are the coords of segment start
//x1 and y1 are the coords of the current point
x1 = 0, y1 = 0, x2 = 0, y2 = 0, info = [], iterator, tempInfo;
for (var i = 0; i < len; i++) {
current = path[i];
tempInfo = {
x: x1,
y: y1,
command: current[0],
};
switch (current[0]) { //first letter
case 'L':
x2 = current[1];
y2 = current[2];
totalLength += calcLineLength(x1, y1, x2, y2);
x1 = current[1];
y1 = current[2];
break;
case 'M':
tempInfo.length = 0;
x2 = x1 = current[1];
y2 = y1 = current[2];
break;
case 'L':
tempInfo.length = calcLineLength(x1, y1, current[1], current[2]);
x1 = current[1];
y1 = current[2];
break;
case 'C':
//todo
iterator = getPointOnCubicBezierIterator(
x1,
y1,
current[1],
current[2],
current[3],
current[4],
current[5],
current[6]
);
tempInfo.length = pathIterator(iterator, x1, y1);
x1 = current[5];
y1 = current[6];
break;
case 'Q':
//todo
iterator = getPointOnQuadraticBezierIterator(
x1,
y1,
current[1],
current[2],
current[3],
current[4]
);
tempInfo.length = pathIterator(iterator, x1, y1);
x1 = current[3];
y1 = current[4];
break;
case 'Z':
case 'z':
// we add those in order to ease calculations later
tempInfo.destX = x2;
tempInfo.destY = y2;
tempInfo.length = calcLineLength(x1, y1, x2, y2);
x1 = x2;
y1 = y2;
break;
}
totalLength += tempInfo.length;
info.push(tempInfo);
}
info.push({ length: totalLength, x: x1, y: y1 });
return info;
}

function getPointOnPath(path, perc, infos) {
if (!infos) {
infos = getPathSegmentsInfo(path);
}
var distance = infos[infos.length - 1] * perc, i = 0;
while ((distance - infos[i] > 0) && i < infos.length) {
distance -= infos[i];
i++;
}
var segInfo = infos[i], segPercent = distance / segInfo.length,
command = segInfo.length, segment = path[i];
switch (command) {
case 'Z':
case 'z':
return new fabric.Point(segInfo.x, segInfo.y).lerp(
new fabric.Point(segInfo.destX, segInfo.destY),
segPercent
);
break;
case 'L':
return new fabric.Point(segInfo.x, segInfo.y).lerp(
new fabric.Point(segment[1], segment[2]),
segPercent
);
break;
case 'C':
return getPointOnCubicBezierIterator(
segInfo.x,
segInfo.y,
segment[1],
segment[2],
segment[3],
segment[4],
segment[5],
segment[6]
)(segPercent);
break;
case 'Q':
return getPointOnQuadraticBezierIterator(
segInfo.x,
segInfo.y,
segment[1],
segment[2],
segment[3],
segment[4]
)(segPercent);
break;
}
return totalLength;
}

function parsePath(pathString) {
Expand Down Expand Up @@ -528,9 +681,10 @@

fabric.util.parsePath = parsePath;
fabric.util.makePathSimpler = makePathSimpler;
fabric.util.measurePath = measurePath;
fabric.util.getPathSegmentsInfo = getPathSegmentsInfo;
fabric.util.fromArcToBeizers = fromArcToBeizers;
fabric.util.getBoundsOfCurve = getBoundsOfCurve;
fabric.util.getPointOnPath = getPointOnPath;
// kept because we do not want to make breaking changes.
// but useless and deprecated.
fabric.util.getBoundsOfArc = getBoundsOfArc;
Expand Down
32 changes: 30 additions & 2 deletions test/unit/path_utils.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
(function() {
QUnit.module('fabric.util - path.js');
// eslint-disable-next-line max-len
var path = 'M 2 5 l 2 -2 L 4 4 h 3 H 9 C 8 3 10 3 10 3 c 1 -1 2 0 1 1 S 8 5 9 7 v 1 s 2 -1 1 2 Q 9 10 10 11 T 12 11 t -1 -1 v 2 T 10 12 S 9 12 7 11 c 0 -1 0 -1 -2 -2 z m 0 2 l 1 0 l 0 1 l -1 0 z M 1 1 a 1 1 1 0 30 2 2 A 2 2 1 0 30 6 6';
var path = 'M 2 5 l 2 -2 L 4 4 h 3 H 9 C 8 3 10 3 10 3 c 1 -1 2 0 1 1 S 8 5 9 7 v 1 s 2 -1 1 2 Q 9 10 10 11 T 12 11 t -1 -1 v 2 T 10 12 S 9 12 7 11 c 0 -1 0 -1 -2 -2 z m 0 2 l 1 0 l 0 1 l -1 0 z M 1 1 a 1 1 30 1 0 2 2 A 2 2 30 1 0 6 6';
// eslint-disable-next-line
var expectedParse = [['M',2,5],['l',2,-2],['L',4,4],['h',3],['H',9],['C',8,3,10,3,10,3],['c',1,-1,2,0,1,1],['S',8,5,9,7],['v',1],['s',2,-1,1,2],['Q',9,10,10,11],['T',12,11],['t',-1,-1],['v',2],['T',10,12],['S',9,12,7,11],['c',0,-1,0,-1,-2,-2],['z'],['m',0,2],['l',1,0],['l',0,1],['l',-1,0],['z'],['M', 1, 1], ['a', 1, 1, 1, 0, 30, 2, 2],['A', 2,2,1,0,30,6,6]];
var expectedParse = [['M',2,5],['l',2,-2],['L',4,4],['h',3],['H',9],['C',8,3,10,3,10,3],['c',1,-1,2,0,1,1],['S',8,5,9,7],['v',1],['s',2,-1,1,2],['Q',9,10,10,11],['T',12,11],['t',-1,-1],['v',2],['T',10,12],['S',9,12,7,11],['c',0,-1,0,-1,-2,-2],['z'],['m',0,2],['l',1,0],['l',0,1],['l',-1,0],['z'],['M', 1, 1], ['a', 1, 1, 30, 1, 0, 2, 2],['A', 2,2,30,1,0,6,6]];
// eslint-disable-next-line
var expectedSimplified = [['M', 2, 5], ['L', 4, 3], ['L', 4, 4], ['L', 7, 4], ['L', 9, 4], ['C', 8, 3, 10, 3, 10, 3], ['C', 11, 2, 12, 3, 11, 4], ['C', 10, 5, 8, 5, 9, 7], ['L', 9, 8], ['C', 9, 8, 11, 7, 10, 10], ['Q', 9, 10, 10, 11], ['Q', 11, 12, 12, 11], ['Q', 13, 10, 11, 10], ['L', 11, 12], ['Q', 11, 12, 10, 12], ['C', 10, 12, 9, 12, 7, 11], ['C', 7, 10, 7, 10, 5, 9], ['z'], ['M', 2, 7], ['L', 3, 7], ['L', 3, 8], ['L', 2, 8], ['z'], ['M', 1, 1], ['C', 1.5522847498307932, 0.4477152501692063, 2.4477152501692068, 0.44771525016920666, 3, 1], ['C', 3.5522847498307932, 1.5522847498307937, 3.5522847498307932, 2.4477152501692063, 3, 3], ['C', 3.82842712474619, 2.1715728752538093, 5.17157287525381, 2.1715728752538097, 6, 3], ['C', 6.82842712474619, 3.82842712474619, 6.828427124746191, 5.17157287525381, 6, 6]];
QUnit.test('fabric.util.parsePath', function(assert) {
Expand All @@ -22,4 +22,32 @@
assert.deepEqual(command, expectedSimplified[index], 'should contain a subset of equivalent commands ' + index);
});
});
QUnit.test('fabric.util.getPathSegmentsInfo', function(assert) {
assert.ok(typeof fabric.util.getPathSegmentsInfo === 'function');
var parsed = fabric.util.makePathSimpler(fabric.util.parsePath(path));
var infos = fabric.util.getPathSegmentsInfo(parsed);
assert.deepEqual(infos[0].length, 0, 'the command 0 a M has a length 0');
assert.deepEqual(infos[1].length, 2.8284271247461903, 'the command 1 a L has a length 2.82');
assert.deepEqual(infos[2].length, 1, 'the command 2 a L with one step on Y has a length 1');
assert.deepEqual(infos[3].length, 3, 'the command 3 a L with 3 step on X has a length 3');
assert.deepEqual(infos[4].length, 2, 'the command 4 a L with 2 step on X has a length 0');
assert.deepEqual(infos[5].length, 2.061820497903685, 'the command 5 a C has a approximated lenght of 2.061');
assert.deepEqual(infos[6].length, 2.786311794934689, 'the command 6 a C has a approximated lenght of 2.786');
assert.deepEqual(infos[7].length, 4.123555017527272, 'the command 7 a C has a approximated lenght of 4.123');
assert.deepEqual(infos[8].length, 1, 'the command 8 a L with 1 step on the Y has an exact lenght of 1');
assert.deepEqual(infos[9].length, 3.1338167707969693, 'the command 9 a C has a approximated lenght of 3.183');
assert.deepEqual(infos[10].length, 1.512191042774622, 'the command 10 a Q has a approximated lenght of 1.512');
assert.deepEqual(infos[11].length, 2.2674203737413428, 'the command 11 a Q has a approximated lenght of 2.267');
});

QUnit.test('fabric.util.getPathSegmentsInfo test Z command', function(assert) {
assert.ok(typeof fabric.util.getPathSegmentsInfo === 'function');
var parsed = fabric.util.makePathSimpler(fabric.util.parsePath('M 0 0 h 20, v 20 L 0, 20 Z'));
var infos = fabric.util.getPathSegmentsInfo(parsed);
assert.deepEqual(infos[0].length, 0, 'the command 0 a M has a length 0');
assert.deepEqual(infos[1].length, 20, 'the command 1 a L has length 20');
assert.deepEqual(infos[2].length, 20, 'the command 2 a L has length 20');
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');
});
})();