diff options
| author | littleblack111 <littleblack11111@gmail.com> | 2024-01-04 11:07:54 +0800 |
|---|---|---|
| committer | littleblack111 <littleblack11111@gmail.com> | 2024-01-04 11:07:54 +0800 |
| commit | 275f9dc0bdf3e58aa36b800f55f8648fbc55d8af (patch) | |
| tree | 81c7cf120fb703cfd5e3162f2366033d9fe5c7f8 /node_modules/svgo/plugins/convertPathData.js | |
| parent | 2b4ab36917baef56c7ce1602966f941be15af785 (diff) | |
upgrade
Diffstat (limited to 'node_modules/svgo/plugins/convertPathData.js')
| -rw-r--r-- | node_modules/svgo/plugins/convertPathData.js | 323 |
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 = ''; |
