schnee effeckt und fehler Korektur

This commit is contained in:
2023-08-14 17:52:24 +02:00
parent 4a843d4936
commit 79af4e9907
6813 changed files with 343821 additions and 356128 deletions

View File

@@ -1,103 +1,146 @@
'use strict';
exports.type = 'perItem';
/**
* @typedef {import('../lib/types').XastElement} XastElement
*/
const { cleanupOutData } = require('../lib/svgo/tools.js');
const {
transform2js,
transformsMultiply,
matrixToTransform,
} = require('./_transforms.js');
exports.type = 'visitor';
exports.name = 'convertTransform';
exports.active = true;
exports.description = 'collapses multiple transformations and optimizes it';
exports.params = {
convertToShorts: true,
// degPrecision: 3, // transformPrecision (or matrix precision) - 2 by default
floatPrecision: 3,
transformPrecision: 5,
matrixToTransform: true,
shortTranslate: true,
shortScale: true,
shortRotate: true,
removeUseless: true,
collapseIntoOne: true,
leadingZero: true,
negativeExtraSpace: false
};
var cleanupOutData = require('../lib/svgo/tools').cleanupOutData,
EXTEND = require('whet.extend'),
transform2js = require('./_transforms.js').transform2js,
transformsMultiply = require('./_transforms.js').transformsMultiply,
matrixToTransform = require('./_transforms.js').matrixToTransform,
degRound,
floatRound,
transformRound;
/**
* Convert matrices to the short aliases,
* convert long translate, scale or rotate transform notations to the shorts ones,
* convert transforms to the matrices and multiply them all into one,
* remove useless transforms.
*
* @see http://www.w3.org/TR/SVG/coords.html#TransformMatrixDefined
*
* @param {Object} item current iteration item
* @param {Object} params plugin params
* @return {Boolean} if false, item will be filtered out
* @see https://www.w3.org/TR/SVG11/coords.html#TransformMatrixDefined
*
* @author Kir Belevich
*
* @type {import('../lib/types').Plugin<{
* convertToShorts?: boolean,
* degPrecision?: number,
* floatPrecision?: number,
* transformPrecision?: number,
* matrixToTransform?: boolean,
* shortTranslate?: boolean,
* shortScale?: boolean,
* shortRotate?: boolean,
* removeUseless?: boolean,
* collapseIntoOne?: boolean,
* leadingZero?: boolean,
* negativeExtraSpace?: boolean,
* }>}
*/
exports.fn = function(item, params) {
if (item.elem) {
exports.fn = (_root, params) => {
const {
convertToShorts = true,
// degPrecision = 3, // transformPrecision (or matrix precision) - 2 by default
degPrecision,
floatPrecision = 3,
transformPrecision = 5,
matrixToTransform = true,
shortTranslate = true,
shortScale = true,
shortRotate = true,
removeUseless = true,
collapseIntoOne = true,
leadingZero = true,
negativeExtraSpace = false,
} = params;
const newParams = {
convertToShorts,
degPrecision,
floatPrecision,
transformPrecision,
matrixToTransform,
shortTranslate,
shortScale,
shortRotate,
removeUseless,
collapseIntoOne,
leadingZero,
negativeExtraSpace,
};
return {
element: {
enter: (node) => {
// transform
if (item.hasAttr('transform')) {
convertTransform(item, 'transform', params);
if (node.attributes.transform != null) {
convertTransform(node, 'transform', newParams);
}
// gradientTransform
if (item.hasAttr('gradientTransform')) {
convertTransform(item, 'gradientTransform', params);
if (node.attributes.gradientTransform != null) {
convertTransform(node, 'gradientTransform', newParams);
}
// patternTransform
if (item.hasAttr('patternTransform')) {
convertTransform(item, 'patternTransform', params);
if (node.attributes.patternTransform != null) {
convertTransform(node, 'patternTransform', newParams);
}
}
},
},
};
};
/**
* @typedef {{
* convertToShorts: boolean,
* degPrecision?: number,
* floatPrecision: number,
* transformPrecision: number,
* matrixToTransform: boolean,
* shortTranslate: boolean,
* shortScale: boolean,
* shortRotate: boolean,
* removeUseless: boolean,
* collapseIntoOne: boolean,
* leadingZero: boolean,
* negativeExtraSpace: boolean,
* }} TransformParams
*/
/**
* @typedef {{ name: string, data: Array<number> }} TransformItem
*/
/**
* Main function.
*
* @param {Object} item input item
* @param {String} attrName attribute name
* @param {Object} params plugin params
* @type {(item: XastElement, attrName: string, params: TransformParams) => void}
*/
function convertTransform(item, attrName, params) {
var data = transform2js(item.attr(attrName).value);
params = definePrecision(data, params);
const convertTransform = (item, attrName, params) => {
let data = transform2js(item.attributes[attrName]);
params = definePrecision(data, params);
if (params.collapseIntoOne && data.length > 1) {
data = [transformsMultiply(data)];
}
if (params.collapseIntoOne && data.length > 1) {
data = [transformsMultiply(data)];
}
if (params.convertToShorts) {
data = convertToShorts(data, params);
} else {
data.forEach(roundTransform);
}
if (params.convertToShorts) {
data = convertToShorts(data, params);
} else {
data.forEach((item) => roundTransform(item, params));
}
if (params.removeUseless) {
data = removeUseless(data);
}
if (params.removeUseless) {
data = removeUseless(data);
}
if (data.length) {
item.attr(attrName).value = js2transform(data, params);
} else {
item.removeAttr(attrName);
}
}
if (data.length) {
item.attributes[attrName] = js2transform(data, params);
} else {
delete item.attributes[attrName];
}
};
/**
* Defines precision to work with certain parts.
@@ -106,259 +149,284 @@ function convertTransform(item, attrName, params) {
* degPrecision - for rotate and skew. By default it's equal to (rougly)
* transformPrecision - 2 or floatPrecision whichever is lower. Can be set in params.
*
* @param {Array} transforms input array
* @param {Object} params plugin params
* @return {Array} output array
* @type {(data: Array<TransformItem>, params: TransformParams) => TransformParams}
*
* clone params so it don't affect other elements transformations.
*/
function definePrecision(data, params) {
/* jshint validthis: true */
var matrixData = data.reduce(getMatrixData, []),
significantDigits = params.transformPrecision;
// Clone params so it don't affect other elements transformations.
params = EXTEND({}, params);
// Limit transform precision with matrix one. Calculating with larger precision doesn't add any value.
if (matrixData.length) {
params.transformPrecision = Math.min(params.transformPrecision,
Math.max.apply(Math, matrixData.map(floatDigits)) || params.transformPrecision);
significantDigits = Math.max.apply(Math, matrixData.map(function(n) {
return String(n).replace(/\D+/g, '').length; // Number of digits in a number. 123.45 → 5
}));
const definePrecision = (data, { ...newParams }) => {
const matrixData = [];
for (const item of data) {
if (item.name == 'matrix') {
matrixData.push(...item.data.slice(0, 4));
}
// No sense in angle precision more then number of significant digits in matrix.
if (!('degPrecision' in params)) {
params.degPrecision = Math.max(0, Math.min(params.floatPrecision, significantDigits - 2));
}
floatRound = params.floatPrecision >= 1 && params.floatPrecision < 20 ?
smartRound.bind(this, params.floatPrecision) :
round;
degRound = params.degPrecision >= 1 && params.floatPrecision < 20 ?
smartRound.bind(this, params.degPrecision) :
round;
transformRound = params.transformPrecision >= 1 && params.floatPrecision < 20 ?
smartRound.bind(this, params.transformPrecision) :
round;
return params;
}
}
let significantDigits = newParams.transformPrecision;
// Limit transform precision with matrix one. Calculating with larger precision doesn't add any value.
if (matrixData.length) {
newParams.transformPrecision = Math.min(
newParams.transformPrecision,
Math.max.apply(Math, matrixData.map(floatDigits)) ||
newParams.transformPrecision
);
significantDigits = Math.max.apply(
Math,
matrixData.map(
(n) => n.toString().replace(/\D+/g, '').length // Number of digits in a number. 123.45 → 5
)
);
}
// No sense in angle precision more then number of significant digits in matrix.
if (newParams.degPrecision == null) {
newParams.degPrecision = Math.max(
0,
Math.min(newParams.floatPrecision, significantDigits - 2)
);
}
return newParams;
};
/**
* Gathers four first matrix parameters.
*
* @param {Array} a array of data
* @param {Object} transform
* @return {Array} output array
* @type {(data: Array<number>, params: TransformParams) => Array<number>}
*/
function getMatrixData(a, b) {
return b.name == 'matrix' ? a.concat(b.data.slice(0, 4)) : a;
}
const degRound = (data, params) => {
if (
params.degPrecision != null &&
params.degPrecision >= 1 &&
params.floatPrecision < 20
) {
return smartRound(params.degPrecision, data);
} else {
return round(data);
}
};
/**
* @type {(data: Array<number>, params: TransformParams) => Array<number>}
*/
const floatRound = (data, params) => {
if (params.floatPrecision >= 1 && params.floatPrecision < 20) {
return smartRound(params.floatPrecision, data);
} else {
return round(data);
}
};
/**
* @type {(data: Array<number>, params: TransformParams) => Array<number>}
*/
const transformRound = (data, params) => {
if (params.transformPrecision >= 1 && params.floatPrecision < 20) {
return smartRound(params.transformPrecision, data);
} else {
return round(data);
}
};
/**
* Returns number of digits after the point. 0.125 → 3
*
* @type {(n: number) => number}
*/
function floatDigits(n) {
return (n = String(n)).slice(n.indexOf('.')).length - 1;
}
const floatDigits = (n) => {
const str = n.toString();
return str.slice(str.indexOf('.')).length - 1;
};
/**
* Convert transforms to the shorthand alternatives.
*
* @param {Array} transforms input array
* @param {Object} params plugin params
* @return {Array} output array
* @type {(transforms: Array<TransformItem>, params: TransformParams) => Array<TransformItem>}
*/
function convertToShorts(transforms, params) {
for(var i = 0; i < transforms.length; i++) {
var transform = transforms[i];
// convert matrix to the short aliases
if (
params.matrixToTransform &&
transform.name === 'matrix'
) {
var decomposed = matrixToTransform(transform, params);
if (decomposed != transform &&
js2transform(decomposed, params).length <= js2transform([transform], params).length) {
transforms.splice.apply(transforms, [i, 1].concat(decomposed));
}
transform = transforms[i];
}
// fixed-point numbers
// 12.754997 → 12.755
roundTransform(transform);
// convert long translate transform notation to the shorts one
// translate(10 0) → translate(10)
if (
params.shortTranslate &&
transform.name === 'translate' &&
transform.data.length === 2 &&
!transform.data[1]
) {
transform.data.pop();
}
// convert long scale transform notation to the shorts one
// scale(2 2) → scale(2)
if (
params.shortScale &&
transform.name === 'scale' &&
transform.data.length === 2 &&
transform.data[0] === transform.data[1]
) {
transform.data.pop();
}
// convert long rotate transform notation to the short one
// translate(cx cy) rotate(a) translate(-cx -cy) → rotate(a cx cy)
if (
params.shortRotate &&
transforms[i - 2] &&
transforms[i - 2].name === 'translate' &&
transforms[i - 1].name === 'rotate' &&
transforms[i].name === 'translate' &&
transforms[i - 2].data[0] === -transforms[i].data[0] &&
transforms[i - 2].data[1] === -transforms[i].data[1]
) {
transforms.splice(i - 2, 3, {
name: 'rotate',
data: [
transforms[i - 1].data[0],
transforms[i - 2].data[0],
transforms[i - 2].data[1]
]
});
// splice compensation
i -= 2;
transform = transforms[i];
}
const convertToShorts = (transforms, params) => {
for (var i = 0; i < transforms.length; i++) {
var transform = transforms[i];
// convert matrix to the short aliases
if (params.matrixToTransform && transform.name === 'matrix') {
var decomposed = matrixToTransform(transform, params);
if (
js2transform(decomposed, params).length <=
js2transform([transform], params).length
) {
transforms.splice(i, 1, ...decomposed);
}
transform = transforms[i];
}
return transforms;
// fixed-point numbers
// 12.754997 → 12.755
roundTransform(transform, params);
}
// convert long translate transform notation to the shorts one
// translate(10 0) → translate(10)
if (
params.shortTranslate &&
transform.name === 'translate' &&
transform.data.length === 2 &&
!transform.data[1]
) {
transform.data.pop();
}
// convert long scale transform notation to the shorts one
// scale(2 2) → scale(2)
if (
params.shortScale &&
transform.name === 'scale' &&
transform.data.length === 2 &&
transform.data[0] === transform.data[1]
) {
transform.data.pop();
}
// convert long rotate transform notation to the short one
// translate(cx cy) rotate(a) translate(-cx -cy) → rotate(a cx cy)
if (
params.shortRotate &&
transforms[i - 2] &&
transforms[i - 2].name === 'translate' &&
transforms[i - 1].name === 'rotate' &&
transforms[i].name === 'translate' &&
transforms[i - 2].data[0] === -transforms[i].data[0] &&
transforms[i - 2].data[1] === -transforms[i].data[1]
) {
transforms.splice(i - 2, 3, {
name: 'rotate',
data: [
transforms[i - 1].data[0],
transforms[i - 2].data[0],
transforms[i - 2].data[1],
],
});
// splice compensation
i -= 2;
}
}
return transforms;
};
/**
* Remove useless transforms.
*
* @param {Array} transforms input array
* @return {Array} output array
* @type {(trasforms: Array<TransformItem>) => Array<TransformItem>}
*/
function removeUseless(transforms) {
const removeUseless = (transforms) => {
return transforms.filter((transform) => {
// translate(0), rotate(0[, cx, cy]), skewX(0), skewY(0)
if (
(['translate', 'rotate', 'skewX', 'skewY'].indexOf(transform.name) > -1 &&
(transform.data.length == 1 || transform.name == 'rotate') &&
!transform.data[0]) ||
// translate(0, 0)
(transform.name == 'translate' &&
!transform.data[0] &&
!transform.data[1]) ||
// scale(1)
(transform.name == 'scale' &&
transform.data[0] == 1 &&
(transform.data.length < 2 || transform.data[1] == 1)) ||
// matrix(1 0 0 1 0 0)
(transform.name == 'matrix' &&
transform.data[0] == 1 &&
transform.data[3] == 1 &&
!(
transform.data[1] ||
transform.data[2] ||
transform.data[4] ||
transform.data[5]
))
) {
return false;
}
return transforms.filter(function(transform) {
// translate(0), rotate(0[, cx, cy]), skewX(0), skewY(0)
if (
['translate', 'rotate', 'skewX', 'skewY'].indexOf(transform.name) > -1 &&
(transform.data.length == 1 || transform.name == 'rotate') &&
!transform.data[0] ||
// translate(0, 0)
transform.name == 'translate' &&
!transform.data[0] &&
!transform.data[1] ||
// scale(1)
transform.name == 'scale' &&
transform.data[0] == 1 &&
(transform.data.length < 2 || transform.data[1] == 1) ||
// matrix(1 0 0 1 0 0)
transform.name == 'matrix' &&
transform.data[0] == 1 &&
transform.data[3] == 1 &&
!(transform.data[1] || transform.data[2] || transform.data[4] || transform.data[5])
) {
return false;
}
return true;
});
}
return true;
});
};
/**
* Convert transforms JS representation to string.
*
* @param {Array} transformJS JS representation array
* @param {Object} params plugin params
* @return {String} output string
* @type {(transformJS: Array<TransformItem>, params: TransformParams) => string}
*/
function js2transform(transformJS, params) {
const js2transform = (transformJS, params) => {
var transformString = '';
var transformString = '';
// collect output value string
transformJS.forEach((transform) => {
roundTransform(transform, params);
transformString +=
(transformString && ' ') +
transform.name +
'(' +
cleanupOutData(transform.data, params) +
')';
});
// collect output value string
transformJS.forEach(function(transform) {
roundTransform(transform);
transformString += (transformString && ' ') + transform.name + '(' + cleanupOutData(transform.data, params) + ')';
});
return transformString;
};
return transformString;
}
function roundTransform(transform) {
switch (transform.name) {
case 'translate':
transform.data = floatRound(transform.data);
break;
case 'rotate':
transform.data = degRound(transform.data.slice(0, 1)).concat(floatRound(transform.data.slice(1)));
break;
case 'skewX':
case 'skewY':
transform.data = degRound(transform.data);
break;
case 'scale':
transform.data = transformRound(transform.data);
break;
case 'matrix':
transform.data = transformRound(transform.data.slice(0, 4)).concat(floatRound(transform.data.slice(4)));
break;
}
return transform;
}
/**
* @type {(transform: TransformItem, params: TransformParams) => TransformItem}
*/
const roundTransform = (transform, params) => {
switch (transform.name) {
case 'translate':
transform.data = floatRound(transform.data, params);
break;
case 'rotate':
transform.data = [
...degRound(transform.data.slice(0, 1), params),
...floatRound(transform.data.slice(1), params),
];
break;
case 'skewX':
case 'skewY':
transform.data = degRound(transform.data, params);
break;
case 'scale':
transform.data = transformRound(transform.data, params);
break;
case 'matrix':
transform.data = [
...transformRound(transform.data.slice(0, 4), params),
...floatRound(transform.data.slice(4), params),
];
break;
}
return transform;
};
/**
* Rounds numbers in array.
*
* @param {Array} data input data array
* @return {Array} output data array
* @type {(data: Array<number>) => Array<number>}
*/
function round(data) {
return data.map(Math.round);
}
const round = (data) => {
return data.map(Math.round);
};
/**
* Decrease accuracy of floating-point numbers
* in transforms keeping a specified number of decimals.
* Smart rounds values like 2.349 to 2.35.
*
* @param {Number} fixed number of decimals
* @param {Array} data input data array
* @return {Array} output data array
* @type {(precision: number, data: Array<number>) => Array<number>}
*/
function smartRound(precision, data) {
for (var i = data.length, tolerance = +Math.pow(.1, precision).toFixed(precision); i--;) {
if (data[i].toFixed(precision) != data[i]) {
var rounded = +data[i].toFixed(precision - 1);
data[i] = +Math.abs(rounded - data[i]).toFixed(precision + 1) >= tolerance ?
+data[i].toFixed(precision) :
rounded;
}
const smartRound = (precision, data) => {
for (
var i = data.length,
tolerance = +Math.pow(0.1, precision).toFixed(precision);
i--;
) {
if (Number(data[i].toFixed(precision)) !== data[i]) {
var rounded = +data[i].toFixed(precision - 1);
data[i] =
+Math.abs(rounded - data[i]).toFixed(precision + 1) >= tolerance
? +data[i].toFixed(precision)
: rounded;
}
return data;
}
}
return data;
};