source: trip-planner-front/node_modules/svgo/plugins/convertTransform.js@ 1ad8e64

Last change on this file since 1ad8e64 was 6a3a178, checked in by Ema <ema_spirova@…>, 3 years ago

initial commit

  • Property mode set to 100644
File size: 11.4 KB
Line 
1'use strict';
2
3/**
4 * @typedef {import('../lib/types').XastElement} XastElement
5 */
6
7const { cleanupOutData } = require('../lib/svgo/tools.js');
8const {
9 transform2js,
10 transformsMultiply,
11 matrixToTransform,
12} = require('./_transforms.js');
13
14exports.type = 'visitor';
15exports.name = 'convertTransform';
16exports.active = true;
17exports.description = 'collapses multiple transformations and optimizes it';
18
19/**
20 * Convert matrices to the short aliases,
21 * convert long translate, scale or rotate transform notations to the shorts ones,
22 * convert transforms to the matrices and multiply them all into one,
23 * remove useless transforms.
24 *
25 * @see https://www.w3.org/TR/SVG11/coords.html#TransformMatrixDefined
26 *
27 * @author Kir Belevich
28 *
29 * @type {import('../lib/types').Plugin<{
30 * convertToShorts?: boolean,
31 * degPrecision?: number,
32 * floatPrecision?: number,
33 * transformPrecision?: number,
34 * matrixToTransform?: boolean,
35 * shortTranslate?: boolean,
36 * shortScale?: boolean,
37 * shortRotate?: boolean,
38 * removeUseless?: boolean,
39 * collapseIntoOne?: boolean,
40 * leadingZero?: boolean,
41 * negativeExtraSpace?: boolean,
42 * }>}
43 */
44exports.fn = (_root, params) => {
45 const {
46 convertToShorts = true,
47 // degPrecision = 3, // transformPrecision (or matrix precision) - 2 by default
48 degPrecision,
49 floatPrecision = 3,
50 transformPrecision = 5,
51 matrixToTransform = true,
52 shortTranslate = true,
53 shortScale = true,
54 shortRotate = true,
55 removeUseless = true,
56 collapseIntoOne = true,
57 leadingZero = true,
58 negativeExtraSpace = false,
59 } = params;
60 const newParams = {
61 convertToShorts,
62 degPrecision,
63 floatPrecision,
64 transformPrecision,
65 matrixToTransform,
66 shortTranslate,
67 shortScale,
68 shortRotate,
69 removeUseless,
70 collapseIntoOne,
71 leadingZero,
72 negativeExtraSpace,
73 };
74 return {
75 element: {
76 enter: (node) => {
77 // transform
78 if (node.attributes.transform != null) {
79 convertTransform(node, 'transform', newParams);
80 }
81 // gradientTransform
82 if (node.attributes.gradientTransform != null) {
83 convertTransform(node, 'gradientTransform', newParams);
84 }
85 // patternTransform
86 if (node.attributes.patternTransform != null) {
87 convertTransform(node, 'patternTransform', newParams);
88 }
89 },
90 },
91 };
92};
93
94/**
95 * @typedef {{
96 * convertToShorts: boolean,
97 * degPrecision?: number,
98 * floatPrecision: number,
99 * transformPrecision: number,
100 * matrixToTransform: boolean,
101 * shortTranslate: boolean,
102 * shortScale: boolean,
103 * shortRotate: boolean,
104 * removeUseless: boolean,
105 * collapseIntoOne: boolean,
106 * leadingZero: boolean,
107 * negativeExtraSpace: boolean,
108 * }} TransformParams
109 */
110
111/**
112 * @typedef {{ name: string, data: Array<number> }} TransformItem
113 */
114
115/**
116 * Main function.
117 *
118 * @type {(item: XastElement, attrName: string, params: TransformParams) => void}
119 */
120const convertTransform = (item, attrName, params) => {
121 let data = transform2js(item.attributes[attrName]);
122 params = definePrecision(data, params);
123
124 if (params.collapseIntoOne && data.length > 1) {
125 data = [transformsMultiply(data)];
126 }
127
128 if (params.convertToShorts) {
129 data = convertToShorts(data, params);
130 } else {
131 data.forEach((item) => roundTransform(item, params));
132 }
133
134 if (params.removeUseless) {
135 data = removeUseless(data);
136 }
137
138 if (data.length) {
139 item.attributes[attrName] = js2transform(data, params);
140 } else {
141 delete item.attributes[attrName];
142 }
143};
144
145/**
146 * Defines precision to work with certain parts.
147 * transformPrecision - for scale and four first matrix parameters (needs a better precision due to multiplying),
148 * floatPrecision - for translate including two last matrix and rotate parameters,
149 * degPrecision - for rotate and skew. By default it's equal to (rougly)
150 * transformPrecision - 2 or floatPrecision whichever is lower. Can be set in params.
151 *
152 * @type {(data: Array<TransformItem>, params: TransformParams) => TransformParams}
153 *
154 * clone params so it don't affect other elements transformations.
155 */
156const definePrecision = (data, { ...newParams }) => {
157 const matrixData = [];
158 for (const item of data) {
159 if (item.name == 'matrix') {
160 matrixData.push(...item.data.slice(0, 4));
161 }
162 }
163 let significantDigits = newParams.transformPrecision;
164 // Limit transform precision with matrix one. Calculating with larger precision doesn't add any value.
165 if (matrixData.length) {
166 newParams.transformPrecision = Math.min(
167 newParams.transformPrecision,
168 Math.max.apply(Math, matrixData.map(floatDigits)) ||
169 newParams.transformPrecision
170 );
171 significantDigits = Math.max.apply(
172 Math,
173 matrixData.map(
174 (n) => n.toString().replace(/\D+/g, '').length // Number of digits in a number. 123.45 → 5
175 )
176 );
177 }
178 // No sense in angle precision more then number of significant digits in matrix.
179 if (newParams.degPrecision == null) {
180 newParams.degPrecision = Math.max(
181 0,
182 Math.min(newParams.floatPrecision, significantDigits - 2)
183 );
184 }
185 return newParams;
186};
187
188/**
189 * @type {(data: Array<number>, params: TransformParams) => Array<number>}
190 */
191const degRound = (data, params) => {
192 if (
193 params.degPrecision != null &&
194 params.degPrecision >= 1 &&
195 params.floatPrecision < 20
196 ) {
197 return smartRound(params.degPrecision, data);
198 } else {
199 return round(data);
200 }
201};
202/**
203 * @type {(data: Array<number>, params: TransformParams) => Array<number>}
204 */
205const floatRound = (data, params) => {
206 if (params.floatPrecision >= 1 && params.floatPrecision < 20) {
207 return smartRound(params.floatPrecision, data);
208 } else {
209 return round(data);
210 }
211};
212
213/**
214 * @type {(data: Array<number>, params: TransformParams) => Array<number>}
215 */
216const transformRound = (data, params) => {
217 if (params.transformPrecision >= 1 && params.floatPrecision < 20) {
218 return smartRound(params.transformPrecision, data);
219 } else {
220 return round(data);
221 }
222};
223
224/**
225 * Returns number of digits after the point. 0.125 → 3
226 *
227 * @type {(n: number) => number}
228 */
229const floatDigits = (n) => {
230 const str = n.toString();
231 return str.slice(str.indexOf('.')).length - 1;
232};
233
234/**
235 * Convert transforms to the shorthand alternatives.
236 *
237 * @type {(transforms: Array<TransformItem>, params: TransformParams) => Array<TransformItem>}
238 */
239const convertToShorts = (transforms, params) => {
240 for (var i = 0; i < transforms.length; i++) {
241 var transform = transforms[i];
242
243 // convert matrix to the short aliases
244 if (params.matrixToTransform && transform.name === 'matrix') {
245 var decomposed = matrixToTransform(transform, params);
246 if (
247 js2transform(decomposed, params).length <=
248 js2transform([transform], params).length
249 ) {
250 transforms.splice(i, 1, ...decomposed);
251 }
252 transform = transforms[i];
253 }
254
255 // fixed-point numbers
256 // 12.754997 → 12.755
257 roundTransform(transform, params);
258
259 // convert long translate transform notation to the shorts one
260 // translate(10 0) → translate(10)
261 if (
262 params.shortTranslate &&
263 transform.name === 'translate' &&
264 transform.data.length === 2 &&
265 !transform.data[1]
266 ) {
267 transform.data.pop();
268 }
269
270 // convert long scale transform notation to the shorts one
271 // scale(2 2) → scale(2)
272 if (
273 params.shortScale &&
274 transform.name === 'scale' &&
275 transform.data.length === 2 &&
276 transform.data[0] === transform.data[1]
277 ) {
278 transform.data.pop();
279 }
280
281 // convert long rotate transform notation to the short one
282 // translate(cx cy) rotate(a) translate(-cx -cy) → rotate(a cx cy)
283 if (
284 params.shortRotate &&
285 transforms[i - 2] &&
286 transforms[i - 2].name === 'translate' &&
287 transforms[i - 1].name === 'rotate' &&
288 transforms[i].name === 'translate' &&
289 transforms[i - 2].data[0] === -transforms[i].data[0] &&
290 transforms[i - 2].data[1] === -transforms[i].data[1]
291 ) {
292 transforms.splice(i - 2, 3, {
293 name: 'rotate',
294 data: [
295 transforms[i - 1].data[0],
296 transforms[i - 2].data[0],
297 transforms[i - 2].data[1],
298 ],
299 });
300
301 // splice compensation
302 i -= 2;
303 }
304 }
305
306 return transforms;
307};
308
309/**
310 * Remove useless transforms.
311 *
312 * @type {(trasforms: Array<TransformItem>) => Array<TransformItem>}
313 */
314const removeUseless = (transforms) => {
315 return transforms.filter((transform) => {
316 // translate(0), rotate(0[, cx, cy]), skewX(0), skewY(0)
317 if (
318 (['translate', 'rotate', 'skewX', 'skewY'].indexOf(transform.name) > -1 &&
319 (transform.data.length == 1 || transform.name == 'rotate') &&
320 !transform.data[0]) ||
321 // translate(0, 0)
322 (transform.name == 'translate' &&
323 !transform.data[0] &&
324 !transform.data[1]) ||
325 // scale(1)
326 (transform.name == 'scale' &&
327 transform.data[0] == 1 &&
328 (transform.data.length < 2 || transform.data[1] == 1)) ||
329 // matrix(1 0 0 1 0 0)
330 (transform.name == 'matrix' &&
331 transform.data[0] == 1 &&
332 transform.data[3] == 1 &&
333 !(
334 transform.data[1] ||
335 transform.data[2] ||
336 transform.data[4] ||
337 transform.data[5]
338 ))
339 ) {
340 return false;
341 }
342
343 return true;
344 });
345};
346
347/**
348 * Convert transforms JS representation to string.
349 *
350 * @type {(transformJS: Array<TransformItem>, params: TransformParams) => string}
351 */
352const js2transform = (transformJS, params) => {
353 var transformString = '';
354
355 // collect output value string
356 transformJS.forEach((transform) => {
357 roundTransform(transform, params);
358 transformString +=
359 (transformString && ' ') +
360 transform.name +
361 '(' +
362 cleanupOutData(transform.data, params) +
363 ')';
364 });
365
366 return transformString;
367};
368
369/**
370 * @type {(transform: TransformItem, params: TransformParams) => TransformItem}
371 */
372const roundTransform = (transform, params) => {
373 switch (transform.name) {
374 case 'translate':
375 transform.data = floatRound(transform.data, params);
376 break;
377 case 'rotate':
378 transform.data = [
379 ...degRound(transform.data.slice(0, 1), params),
380 ...floatRound(transform.data.slice(1), params),
381 ];
382 break;
383 case 'skewX':
384 case 'skewY':
385 transform.data = degRound(transform.data, params);
386 break;
387 case 'scale':
388 transform.data = transformRound(transform.data, params);
389 break;
390 case 'matrix':
391 transform.data = [
392 ...transformRound(transform.data.slice(0, 4), params),
393 ...floatRound(transform.data.slice(4), params),
394 ];
395 break;
396 }
397 return transform;
398};
399
400/**
401 * Rounds numbers in array.
402 *
403 * @type {(data: Array<number>) => Array<number>}
404 */
405const round = (data) => {
406 return data.map(Math.round);
407};
408
409/**
410 * Decrease accuracy of floating-point numbers
411 * in transforms keeping a specified number of decimals.
412 * Smart rounds values like 2.349 to 2.35.
413 *
414 * @type {(precision: number, data: Array<number>) => Array<number>}
415 */
416const smartRound = (precision, data) => {
417 for (
418 var i = data.length,
419 tolerance = +Math.pow(0.1, precision).toFixed(precision);
420 i--;
421
422 ) {
423 if (Number(data[i].toFixed(precision)) !== data[i]) {
424 var rounded = +data[i].toFixed(precision - 1);
425 data[i] =
426 +Math.abs(rounded - data[i]).toFixed(precision + 1) >= tolerance
427 ? +data[i].toFixed(precision)
428 : rounded;
429 }
430 }
431 return data;
432};
Note: See TracBrowser for help on using the repository browser.