summaryrefslogtreecommitdiff
path: root/node_modules/svgo/plugins/convertPathData.js
diff options
context:
space:
mode:
Diffstat (limited to 'node_modules/svgo/plugins/convertPathData.js')
-rw-r--r--node_modules/svgo/plugins/convertPathData.js323
1 files changed, 192 insertions, 131 deletions
diff --git a/node_modules/svgo/plugins/convertPathData.js b/node_modules/svgo/plugins/convertPathData.js
index 6229d9f7..de7b357f 100644
--- a/node_modules/svgo/plugins/convertPathData.js
+++ b/node_modules/svgo/plugins/convertPathData.js
@@ -9,31 +9,21 @@ const { visit } = require('../lib/xast.js');
const { pathElems } = require('./_collections.js');
const { path2js, js2path } = require('./_path.js');
const { applyTransforms } = require('./applyTransforms.js');
-const { cleanupOutData } = require('../lib/svgo/tools');
+const { cleanupOutData, toFixed } = require('../lib/svgo/tools');
exports.name = 'convertPathData';
exports.description =
'optimizes path data: writes in shorter form, applies transformations';
-/**
- * @type {(data: number[]) => number[]}
- */
+/** @type {(data: number[]) => number[]} */
let roundData;
-/**
- * @type {number | false}
- */
+/** @type {number | false} */
let precision;
-/**
- * @type {number}
- */
+/** @type {number} */
let error;
-/**
- * @type {number}
- */
+/** @type {number} */
let arcThreshold;
-/**
- * @type {number}
- */
+/** @type {number} */
let arcTolerance;
/**
@@ -45,11 +35,13 @@ let arcTolerance;
* tolerance: number,
* },
* straightCurves: boolean,
+ * convertToQ: boolean,
* lineShorthands: boolean,
* convertToZ: boolean,
* curveSmoothShorthands: boolean,
* floatPrecision: number | false,
* transformPrecision: number,
+ * smartArcRounding: boolean,
* removeUseless: boolean,
* collapseRepeated: boolean,
* utilizeAbsolute: boolean,
@@ -95,11 +87,13 @@ exports.fn = (root, params) => {
tolerance: 0.5, // percentage of radius
},
straightCurves = true,
+ convertToQ = true,
lineShorthands = true,
convertToZ = true,
curveSmoothShorthands = true,
floatPrecision = 3,
transformPrecision = 5,
+ smartArcRounding = true,
removeUseless = true,
collapseRepeated = true,
utilizeAbsolute = true,
@@ -117,11 +111,13 @@ exports.fn = (root, params) => {
applyTransformsStroked,
makeArcs,
straightCurves,
+ convertToQ,
lineShorthands,
convertToZ,
curveSmoothShorthands,
floatPrecision,
transformPrecision,
+ smartArcRounding,
removeUseless,
collapseRepeated,
utilizeAbsolute,
@@ -139,7 +135,7 @@ exports.fn = (root, params) => {
applyTransforms(root, {
transformPrecision,
applyTransformsStroked,
- })
+ }),
);
}
@@ -147,14 +143,15 @@ exports.fn = (root, params) => {
return {
element: {
enter: (node) => {
- if (pathElems.includes(node.name) && node.attributes.d != null) {
+ if (pathElems.has(node.name) && node.attributes.d != null) {
const computedStyle = computeStyle(stylesheet, node);
precision = floatPrecision;
error =
precision !== false
? +Math.pow(0.1, precision).toFixed(precision)
: 1e-2;
- roundData = precision > 0 && precision < 20 ? strongRound : round;
+ roundData =
+ precision && precision > 0 && precision < 20 ? strongRound : round;
if (makeArcs) {
arcThreshold = makeArcs.threshold;
arcTolerance = makeArcs.tolerance;
@@ -387,14 +384,20 @@ const convertToRelative = (pathData) => {
function filters(
path,
params,
- { isSafeToUseZ, maybeHasStrokeAndLinecap, hasMarkerMid }
+ { isSafeToUseZ, maybeHasStrokeAndLinecap, hasMarkerMid },
) {
- var stringify = data2Path.bind(null, params),
- relSubpoint = [0, 0],
- pathBase = [0, 0],
- prev = {};
+ const stringify = data2Path.bind(null, params);
+ const relSubpoint = [0, 0];
+ const pathBase = [0, 0];
+ /** @type {any} */
+ let prev = {};
+ /** @type {Point | undefined} */
+ let prevQControlPoint;
path = path.filter(function (item, index, path) {
+ const qControlPoint = prevQControlPoint;
+ prevQControlPoint = undefined;
+
let command = item.command;
let data = item.args;
let next = path[index + 1];
@@ -406,9 +409,8 @@ function filters(
if (command === 's') {
sdata = [0, 0].concat(data);
- // @ts-ignore
- var pdata = prev.args,
- n = pdata.length;
+ const pdata = prev.args;
+ const n = pdata.length;
// (-x, -y) of the prev tangent point relative to the current point
sdata[0] = pdata[n - 2] - pdata[n - 4];
@@ -455,16 +457,11 @@ function filters(
nextLonghand;
if (
- // @ts-ignore
(prev.command == 'c' &&
- // @ts-ignore
isConvex(prev.args) &&
- // @ts-ignore
isArcPrev(prev.args, circle)) ||
- // @ts-ignore
(prev.command == 'a' && prev.sdata && isArcPrev(prev.sdata, circle))
) {
- // @ts-ignore
arcCurves.unshift(prev);
// @ts-ignore
arc.base = prev.base;
@@ -472,7 +469,6 @@ function filters(
arc.args[5] = arc.coords[0] - arc.base[0];
// @ts-ignore
arc.args[6] = arc.coords[1] - arc.base[1];
- // @ts-ignore
var prevData = prev.command == 'a' ? prev.sdata : prev.args;
var prevAngle = findArcAngle(prevData, {
center: [
@@ -489,14 +485,14 @@ function filters(
// check if next curves are fitting the arc
for (
var j = index;
- (next = path[++j]) && ~'cs'.indexOf(next.command);
+ (next = path[++j]) && (next.command === 'c' || next.command === 's');
) {
var nextData = next.args;
if (next.command == 's') {
nextLonghand = makeLonghand(
{ command: 's', args: next.args.slice() },
- path[j - 1].args
+ path[j - 1].args,
);
nextData = nextLonghand.args;
nextLonghand.args = nextData.slice(0, 2);
@@ -565,7 +561,6 @@ function filters(
relSubpoint[0] += prevArc.args[5] - prev.args[prev.args.length - 2];
// @ts-ignore
relSubpoint[1] += prevArc.args[6] - prev.args[prev.args.length - 1];
- // @ts-ignore
prev.command = 'a';
// @ts-ignore
prev.args = prevArc.args;
@@ -579,11 +574,7 @@ function filters(
item.sdata = sdata.slice(); // preserve curve data for future checks
} else if (arcCurves.length - 1 - hasPrev > 0) {
// filter out consumed next items
- path.splice.apply(
- path,
- // @ts-ignore
- [index + 1, arcCurves.length - 1 - hasPrev].concat(output)
- );
+ path.splice(index + 1, arcCurves.length - 1 - hasPrev, ...output);
}
if (!arc) return false;
command = 'a';
@@ -637,6 +628,24 @@ function filters(
}
}
+ // round arc radius more accurately
+ // eg m 0 0 a 1234.567 1234.567 0 0 1 10 0 -> m 0 0 a 1235 1235 0 0 1 10 0
+ const sagitta = command === 'a' ? calculateSagitta(data) : undefined;
+ if (params.smartArcRounding && sagitta !== undefined && precision) {
+ for (let precisionNew = precision; precisionNew >= 0; precisionNew--) {
+ const radius = toFixed(data[0], precisionNew);
+ const sagittaNew = /** @type {number} */ (
+ calculateSagitta([radius, radius, ...data.slice(2)])
+ );
+ if (Math.abs(sagitta - sagittaNew) < error) {
+ data[0] = radius;
+ data[1] = radius;
+ } else {
+ break;
+ }
+ }
+ }
+
// convert straight curves into lines segments
if (params.straightCurves) {
if (
@@ -652,19 +661,60 @@ function filters(
data = data.slice(-2);
} else if (
command === 't' &&
- // @ts-ignore
prev.command !== 'q' &&
- // @ts-ignore
prev.command !== 't'
) {
command = 'l';
data = data.slice(-2);
- } else if (command === 'a' && (data[0] === 0 || data[1] === 0)) {
+ } else if (
+ command === 'a' &&
+ (data[0] === 0 ||
+ data[1] === 0 ||
+ (sagitta !== undefined && sagitta < error))
+ ) {
command = 'l';
data = data.slice(-2);
}
}
+ // degree-lower c to q when possible
+ // m 0 12 C 4 4 8 4 12 12 → M 0 12 Q 6 0 12 12
+ if (params.convertToQ && command == 'c') {
+ const x1 =
+ // @ts-ignore
+ 0.75 * (item.base[0] + data[0]) - 0.25 * item.base[0];
+ const x2 =
+ // @ts-ignore
+ 0.75 * (item.base[0] + data[2]) - 0.25 * (item.base[0] + data[4]);
+ if (Math.abs(x1 - x2) < error * 2) {
+ const y1 =
+ // @ts-ignore
+ 0.75 * (item.base[1] + data[1]) - 0.25 * item.base[1];
+ const y2 =
+ // @ts-ignore
+ 0.75 * (item.base[1] + data[3]) - 0.25 * (item.base[1] + data[5]);
+ if (Math.abs(y1 - y2) < error * 2) {
+ const newData = data.slice();
+ newData.splice(
+ 0,
+ 4,
+ // @ts-ignore
+ x1 + x2 - item.base[0],
+ // @ts-ignore
+ y1 + y2 - item.base[1],
+ );
+ roundData(newData);
+ const originalLength = cleanupOutData(data, params).length,
+ newLength = cleanupOutData(newData, params).length;
+ if (newLength < originalLength) {
+ command = 'q';
+ data = newData;
+ if (next && next.command == 's') makeLonghand(next, data); // fix up next curve
+ }
+ }
+ }
+ }
+
// horizontal and vertical line shorthands
// l 50 0 → h 50
// l 0 50 → v 50
@@ -678,64 +728,36 @@ function filters(
}
}
- // convert going home to z
- // m 0 0 h 5 v 5 l -5 -5 -> m 0 0 h 5 v 5 z
- if (
- params.convertToZ &&
- (isSafeToUseZ || next?.command === 'Z' || next?.command === 'z') &&
- (command === 'l' || command === 'h' || command === 'v')
- ) {
- if (
- // @ts-ignore
- Math.abs(pathBase[0] - item.coords[0]) < error &&
- // @ts-ignore
- Math.abs(pathBase[1] - item.coords[1]) < error
- ) {
- command = 'z';
- data = [];
- }
- }
-
// collapse repeated commands
// h 20 h 30 -> h 50
if (
params.collapseRepeated &&
hasMarkerMid === false &&
(command === 'm' || command === 'h' || command === 'v') &&
- // @ts-ignore
prev.command &&
- // @ts-ignore
command == prev.command.toLowerCase() &&
((command != 'h' && command != 'v') ||
- // @ts-ignore
prev.args[0] >= 0 == data[0] >= 0)
) {
- // @ts-ignore
prev.args[0] += data[0];
if (command != 'h' && command != 'v') {
- // @ts-ignore
prev.args[1] += data[1];
}
// @ts-ignore
prev.coords = item.coords;
- // @ts-ignore
path[index] = prev;
return false;
}
// convert curves into smooth shorthands
- // @ts-ignore
if (params.curveSmoothShorthands && prev.command) {
// curveto
if (command === 'c') {
// c + c → c + s
if (
- // @ts-ignore
prev.command === 'c' &&
- // @ts-ignore
- data[0] === -(prev.args[2] - prev.args[4]) &&
- // @ts-ignore
- data[1] === -(prev.args[3] - prev.args[5])
+ Math.abs(data[0] - -(prev.args[2] - prev.args[4])) < error &&
+ Math.abs(data[1] - -(prev.args[3] - prev.args[5])) < error
) {
command = 's';
data = data.slice(2);
@@ -743,12 +765,9 @@ function filters(
// s + c → s + s
else if (
- // @ts-ignore
prev.command === 's' &&
- // @ts-ignore
- data[0] === -(prev.args[0] - prev.args[2]) &&
- // @ts-ignore
- data[1] === -(prev.args[1] - prev.args[3])
+ Math.abs(data[0] - -(prev.args[0] - prev.args[2])) < error &&
+ Math.abs(data[1] - -(prev.args[1] - prev.args[3])) < error
) {
command = 's';
data = data.slice(2);
@@ -756,12 +775,10 @@ function filters(
// [^cs] + c → [^cs] + s
else if (
- // @ts-ignore
prev.command !== 'c' &&
- // @ts-ignore
prev.command !== 's' &&
- data[0] === 0 &&
- data[1] === 0
+ Math.abs(data[0]) < error &&
+ Math.abs(data[1]) < error
) {
command = 's';
data = data.slice(2);
@@ -772,28 +789,36 @@ function filters(
else if (command === 'q') {
// q + q → q + t
if (
- // @ts-ignore
prev.command === 'q' &&
- // @ts-ignore
- data[0] === prev.args[2] - prev.args[0] &&
- // @ts-ignore
- data[1] === prev.args[3] - prev.args[1]
+ Math.abs(data[0] - (prev.args[2] - prev.args[0])) < error &&
+ Math.abs(data[1] - (prev.args[3] - prev.args[1])) < error
) {
command = 't';
data = data.slice(2);
}
// t + q → t + t
- else if (
- // @ts-ignore
- prev.command === 't' &&
- // @ts-ignore
- data[2] === prev.args[0] &&
- // @ts-ignore
- data[3] === prev.args[1]
- ) {
- command = 't';
- data = data.slice(2);
+ else if (prev.command === 't') {
+ const predictedControlPoint = reflectPoint(
+ // @ts-ignore
+ qControlPoint,
+ // @ts-ignore
+ item.base,
+ );
+ const realControlPoint = [
+ // @ts-ignore
+ data[0] + item.base[0],
+ // @ts-ignore
+ data[1] + item.base[1],
+ ];
+ if (
+ Math.abs(predictedControlPoint[0] - realControlPoint[0]) <
+ error &&
+ Math.abs(predictedControlPoint[1] - realControlPoint[1]) < error
+ ) {
+ command = 't';
+ data = data.slice(2);
+ }
}
}
}
@@ -813,26 +838,41 @@ function filters(
return i === 0;
})
) {
- // @ts-ignore
path[index] = prev;
return false;
}
// a 25,25 -30 0,1 0,0
if (command === 'a' && data[5] === 0 && data[6] === 0) {
- // @ts-ignore
path[index] = prev;
return false;
}
}
+ // convert going home to z
+ // m 0 0 h 5 v 5 l -5 -5 -> m 0 0 h 5 v 5 z
+ if (
+ params.convertToZ &&
+ (isSafeToUseZ || next?.command === 'Z' || next?.command === 'z') &&
+ (command === 'l' || command === 'h' || command === 'v')
+ ) {
+ if (
+ // @ts-ignore
+ Math.abs(pathBase[0] - item.coords[0]) < error &&
+ // @ts-ignore
+ Math.abs(pathBase[1] - item.coords[1]) < error
+ ) {
+ command = 'z';
+ data = [];
+ }
+ }
+
item.command = command;
item.args = data;
} else {
// z resets coordinates
relSubpoint[0] = pathBase[0];
relSubpoint[1] = pathBase[1];
- // @ts-ignore
if (prev.command === 'Z' || prev.command === 'z') return false;
}
if (
@@ -846,6 +886,18 @@ function filters(
)
return false;
+ if (command === 'q') {
+ // @ts-ignore
+ prevQControlPoint = [data[0] + item.base[0], data[1] + item.base[1]];
+ } else if (command === 't') {
+ if (qControlPoint) {
+ // @ts-ignore
+ prevQControlPoint = reflectPoint(qControlPoint, item.base);
+ } else {
+ // @ts-ignore
+ prevQControlPoint = item.coords;
+ }
+ }
prev = item;
return true;
});
@@ -870,7 +922,8 @@ function convertToMixed(path, params) {
var command = item.command,
data = item.args,
- adata = data.slice();
+ adata = data.slice(),
+ rdata = data.slice();
if (
command === 'm' ||
@@ -898,9 +951,10 @@ function convertToMixed(path, params) {
}
roundData(adata);
+ roundData(rdata);
var absoluteDataStr = cleanupOutData(adata, params),
- relativeDataStr = cleanupOutData(data, params);
+ relativeDataStr = cleanupOutData(rdata, params);
// Convert to absolute coordinates if it's shorter or forceAbsolutePath is true.
// v-20 -> V0
@@ -915,8 +969,9 @@ function convertToMixed(path, params) {
prev.command.charCodeAt(0) > 96 &&
absoluteDataStr.length == relativeDataStr.length - 1 &&
(data[0] < 0 ||
- // @ts-ignore
- (/^0\./.test(data[0]) && prev.args[prev.args.length - 1] % 1))
+ (Math.floor(data[0]) === 0 &&
+ !Number.isInteger(data[0]) &&
+ prev.args[prev.args.length - 1] % 1))
))
) {
// @ts-ignore
@@ -925,7 +980,6 @@ function convertToMixed(path, params) {
}
prev = item;
-
return true;
});
@@ -992,16 +1046,6 @@ function getIntersection(coords) {
}
/**
- * Does the same as `Number.prototype.toFixed` but without casting
- * the return value to a string.
- * @type {(num: number, precision: number) => number}
- */
-function toFixed(num, precision) {
- const pow = 10 ** precision;
- return Math.round(num * pow) / pow;
-}
-
-/**
* Decrease accuracy of floating-point numbers
* in path data keeping a specified number of decimals.
* Smart rounds values like 2.3491 to 2.35 instead of 2.349.
@@ -1042,7 +1086,6 @@ function round(data) {
*
* @type {(data: number[]) => boolean}
*/
-
function isCurveStraightLine(data) {
// Get line equation a·x + b·y + c = 0 coefficients a, b (c = 0) by start and end points.
var i = data.length - 2,
@@ -1062,11 +1105,25 @@ function isCurveStraightLine(data) {
}
/**
+ * Calculates the sagitta of an arc if possible.
+ *
+ * @type {(data: number[]) => number | undefined}
+ * @see https://wikipedia.org/wiki/Sagitta_(geometry)#Formulas
+ */
+function calculateSagitta(data) {
+ if (data[3] === 1) return undefined;
+ const [rx, ry] = data;
+ if (Math.abs(rx - ry) > error) return undefined;
+ const chord = Math.sqrt(data[5] ** 2 + data[6] ** 2);
+ if (chord > rx * 2) return undefined;
+ return rx - Math.sqrt(rx ** 2 - 0.25 * chord ** 2);
+}
+
+/**
* Converts next curve from shorthand to full form using the current curve data.
*
* @type {(item: PathDataItem, data: number[]) => PathDataItem}
*/
-
function makeLonghand(item, data) {
switch (item.command) {
case 's':
@@ -1078,7 +1135,7 @@ function makeLonghand(item, data) {
}
item.args.unshift(
data[data.length - 2] - data[data.length - 4],
- data[data.length - 1] - data[data.length - 3]
+ data[data.length - 1] - data[data.length - 3],
);
return item;
}
@@ -1088,9 +1145,19 @@ function makeLonghand(item, data) {
*
* @type {(point1: Point, point2: Point) => number}
*/
-
function getDistance(point1, point2) {
- return Math.hypot(point1[0] - point2[0], point1[1] - point2[1]);
+ return Math.sqrt((point1[0] - point2[0]) ** 2 + (point1[1] - point2[1]) ** 2);
+}
+
+/**
+ * Reflects point across another point.
+ *
+ * @param {Point} controlPoint
+ * @param {Point} base
+ * @returns {Point}
+ */
+function reflectPoint(controlPoint, base) {
+ return [2 * base[0] - controlPoint[0], 2 * base[1] - controlPoint[1]];
}
/**
@@ -1100,7 +1167,6 @@ function getDistance(point1, point2) {
*
* @type {(curve: number[], t: number) => Point}
*/
-
function getCubicBezierPoint(curve, t) {
var sqrT = t * t,
cubT = sqrT * t,
@@ -1118,7 +1184,6 @@ function getCubicBezierPoint(curve, t) {
*
* @type {(curve: number[]) => undefined | Circle}
*/
-
function findCircle(curve) {
var midPoint = getCubicBezierPoint(curve, 1 / 2),
m1 = [midPoint[0] / 2, midPoint[1] / 2],
@@ -1145,7 +1210,7 @@ function findCircle(curve) {
return (
Math.abs(
// @ts-ignore
- getDistance(getCubicBezierPoint(curve, point), center) - radius
+ getDistance(getCubicBezierPoint(curve, point), center) - radius,
) <= tolerance
);
})
@@ -1159,18 +1224,17 @@ function findCircle(curve) {
*
* @type {(curve: number[], circle: Circle) => boolean}
*/
-
function isArc(curve, circle) {
var tolerance = Math.min(
arcThreshold * error,
- (arcTolerance * circle.radius) / 100
+ (arcTolerance * circle.radius) / 100,
);
return [0, 1 / 4, 1 / 2, 3 / 4, 1].every(function (point) {
return (
Math.abs(
getDistance(getCubicBezierPoint(curve, point), circle.center) -
- circle.radius
+ circle.radius,
) <= tolerance
);
});
@@ -1181,7 +1245,6 @@ function isArc(curve, circle) {
*
* @type {(curve: number[], circle: Circle) => boolean}
*/
-
function isArcPrev(curve, circle) {
return isArc(curve, {
center: [circle.center[0] + curve[4], circle.center[1] + curve[5]],
@@ -1194,7 +1257,6 @@ function isArcPrev(curve, circle) {
* @type {(curve: number[], relCircle: Circle) => number}
*/
-
function findArcAngle(curve, relCircle) {
var x1 = -relCircle.center[0],
y1 = -relCircle.center[1],
@@ -1202,7 +1264,7 @@ function findArcAngle(curve, relCircle) {
y2 = curve[5] - relCircle.center[1];
return Math.acos(
- (x1 * x2 + y1 * y2) / Math.sqrt((x1 * x1 + y1 * y1) * (x2 * x2 + y2 * y2))
+ (x1 * x2 + y1 * y2) / Math.sqrt((x1 * x1 + y1 * y1) * (x2 * x2 + y2 * y2)),
);
}
@@ -1211,7 +1273,6 @@ function findArcAngle(curve, relCircle) {
*
* @type {(params: InternalParams, pathData: PathDataItem[]) => string}
*/
-
function data2Path(params, pathData) {
return pathData.reduce(function (pathString, item) {
var strData = '';