Skip to content

Commit

Permalink
feat(fabric.util): Add a function to work with path measurements (fab…
Browse files Browse the repository at this point in the history
  • Loading branch information
asturur authored and shanicerae committed Jan 16, 2021
1 parent b645654 commit 9781bb3
Show file tree
Hide file tree
Showing 2 changed files with 199 additions and 17 deletions.
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');
});
})();

0 comments on commit 9781bb3

Please sign in to comment.