convertTransform.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. 'use strict';
  2. exports.type = 'perItem';
  3. exports.active = true;
  4. exports.description = 'collapses multiple transformations and optimizes it';
  5. exports.params = {
  6. convertToShorts: true,
  7. // degPrecision: 3, // transformPrecision (or matrix precision) - 2 by default
  8. floatPrecision: 3,
  9. transformPrecision: 5,
  10. matrixToTransform: true,
  11. shortTranslate: true,
  12. shortScale: true,
  13. shortRotate: true,
  14. removeUseless: true,
  15. collapseIntoOne: true,
  16. leadingZero: true,
  17. negativeExtraSpace: false
  18. };
  19. var cleanupOutData = require('../lib/svgo/tools').cleanupOutData,
  20. EXTEND = require('whet.extend'),
  21. transform2js = require('./_transforms.js').transform2js,
  22. transformsMultiply = require('./_transforms.js').transformsMultiply,
  23. matrixToTransform = require('./_transforms.js').matrixToTransform,
  24. degRound,
  25. floatRound,
  26. transformRound;
  27. /**
  28. * Convert matrices to the short aliases,
  29. * convert long translate, scale or rotate transform notations to the shorts ones,
  30. * convert transforms to the matrices and multiply them all into one,
  31. * remove useless transforms.
  32. *
  33. * @see http://www.w3.org/TR/SVG/coords.html#TransformMatrixDefined
  34. *
  35. * @param {Object} item current iteration item
  36. * @param {Object} params plugin params
  37. * @return {Boolean} if false, item will be filtered out
  38. *
  39. * @author Kir Belevich
  40. */
  41. exports.fn = function(item, params) {
  42. if (item.elem) {
  43. // transform
  44. if (item.hasAttr('transform')) {
  45. convertTransform(item, 'transform', params);
  46. }
  47. // gradientTransform
  48. if (item.hasAttr('gradientTransform')) {
  49. convertTransform(item, 'gradientTransform', params);
  50. }
  51. // patternTransform
  52. if (item.hasAttr('patternTransform')) {
  53. convertTransform(item, 'patternTransform', params);
  54. }
  55. }
  56. };
  57. /**
  58. * Main function.
  59. *
  60. * @param {Object} item input item
  61. * @param {String} attrName attribute name
  62. * @param {Object} params plugin params
  63. */
  64. function convertTransform(item, attrName, params) {
  65. var data = transform2js(item.attr(attrName).value);
  66. params = definePrecision(data, params);
  67. if (params.collapseIntoOne && data.length > 1) {
  68. data = [transformsMultiply(data)];
  69. }
  70. if (params.convertToShorts) {
  71. data = convertToShorts(data, params);
  72. } else {
  73. data.forEach(roundTransform);
  74. }
  75. if (params.removeUseless) {
  76. data = removeUseless(data);
  77. }
  78. if (data.length) {
  79. item.attr(attrName).value = js2transform(data, params);
  80. } else {
  81. item.removeAttr(attrName);
  82. }
  83. }
  84. /**
  85. * Defines precision to work with certain parts.
  86. * transformPrecision - for scale and four first matrix parameters (needs a better precision due to multiplying),
  87. * floatPrecision - for translate including two last matrix and rotate parameters,
  88. * degPrecision - for rotate and skew. By default it's equal to (rougly)
  89. * transformPrecision - 2 or floatPrecision whichever is lower. Can be set in params.
  90. *
  91. * @param {Array} transforms input array
  92. * @param {Object} params plugin params
  93. * @return {Array} output array
  94. */
  95. function definePrecision(data, params) {
  96. /* jshint validthis: true */
  97. var matrixData = data.reduce(getMatrixData, []),
  98. significantDigits = params.transformPrecision;
  99. // Clone params so it don't affect other elements transformations.
  100. params = EXTEND({}, params);
  101. // Limit transform precision with matrix one. Calculating with larger precision doesn't add any value.
  102. if (matrixData.length) {
  103. params.transformPrecision = Math.min(params.transformPrecision,
  104. Math.max.apply(Math, matrixData.map(floatDigits)) || params.transformPrecision);
  105. significantDigits = Math.max.apply(Math, matrixData.map(function(n) {
  106. return String(n).replace(/\D+/g, '').length; // Number of digits in a number. 123.45 → 5
  107. }));
  108. }
  109. // No sense in angle precision more then number of significant digits in matrix.
  110. if (!('degPrecision' in params)) {
  111. params.degPrecision = Math.max(0, Math.min(params.floatPrecision, significantDigits - 2));
  112. }
  113. floatRound = params.floatPrecision >= 1 && params.floatPrecision < 20 ?
  114. smartRound.bind(this, params.floatPrecision) :
  115. round;
  116. degRound = params.degPrecision >= 1 && params.floatPrecision < 20 ?
  117. smartRound.bind(this, params.degPrecision) :
  118. round;
  119. transformRound = params.transformPrecision >= 1 && params.floatPrecision < 20 ?
  120. smartRound.bind(this, params.transformPrecision) :
  121. round;
  122. return params;
  123. }
  124. /**
  125. * Gathers four first matrix parameters.
  126. *
  127. * @param {Array} a array of data
  128. * @param {Object} transform
  129. * @return {Array} output array
  130. */
  131. function getMatrixData(a, b) {
  132. return b.name == 'matrix' ? a.concat(b.data.slice(0, 4)) : a;
  133. }
  134. /**
  135. * Returns number of digits after the point. 0.125 → 3
  136. */
  137. function floatDigits(n) {
  138. return (n = String(n)).slice(n.indexOf('.')).length - 1;
  139. }
  140. /**
  141. * Convert transforms to the shorthand alternatives.
  142. *
  143. * @param {Array} transforms input array
  144. * @param {Object} params plugin params
  145. * @return {Array} output array
  146. */
  147. function convertToShorts(transforms, params) {
  148. for(var i = 0; i < transforms.length; i++) {
  149. var transform = transforms[i];
  150. // convert matrix to the short aliases
  151. if (
  152. params.matrixToTransform &&
  153. transform.name === 'matrix'
  154. ) {
  155. var decomposed = matrixToTransform(transform, params);
  156. if (decomposed != transform &&
  157. js2transform(decomposed, params).length <= js2transform([transform], params).length) {
  158. transforms.splice.apply(transforms, [i, 1].concat(decomposed));
  159. }
  160. transform = transforms[i];
  161. }
  162. // fixed-point numbers
  163. // 12.754997 → 12.755
  164. roundTransform(transform);
  165. // convert long translate transform notation to the shorts one
  166. // translate(10 0) → translate(10)
  167. if (
  168. params.shortTranslate &&
  169. transform.name === 'translate' &&
  170. transform.data.length === 2 &&
  171. !transform.data[1]
  172. ) {
  173. transform.data.pop();
  174. }
  175. // convert long scale transform notation to the shorts one
  176. // scale(2 2) → scale(2)
  177. if (
  178. params.shortScale &&
  179. transform.name === 'scale' &&
  180. transform.data.length === 2 &&
  181. transform.data[0] === transform.data[1]
  182. ) {
  183. transform.data.pop();
  184. }
  185. // convert long rotate transform notation to the short one
  186. // translate(cx cy) rotate(a) translate(-cx -cy) → rotate(a cx cy)
  187. if (
  188. params.shortRotate &&
  189. transforms[i - 2] &&
  190. transforms[i - 2].name === 'translate' &&
  191. transforms[i - 1].name === 'rotate' &&
  192. transforms[i].name === 'translate' &&
  193. transforms[i - 2].data[0] === -transforms[i].data[0] &&
  194. transforms[i - 2].data[1] === -transforms[i].data[1]
  195. ) {
  196. transforms.splice(i - 2, 3, {
  197. name: 'rotate',
  198. data: [
  199. transforms[i - 1].data[0],
  200. transforms[i - 2].data[0],
  201. transforms[i - 2].data[1]
  202. ]
  203. });
  204. // splice compensation
  205. i -= 2;
  206. transform = transforms[i];
  207. }
  208. }
  209. return transforms;
  210. }
  211. /**
  212. * Remove useless transforms.
  213. *
  214. * @param {Array} transforms input array
  215. * @return {Array} output array
  216. */
  217. function removeUseless(transforms) {
  218. return transforms.filter(function(transform) {
  219. // translate(0), rotate(0[, cx, cy]), skewX(0), skewY(0)
  220. if (
  221. ['translate', 'rotate', 'skewX', 'skewY'].indexOf(transform.name) > -1 &&
  222. (transform.data.length == 1 || transform.name == 'rotate') &&
  223. !transform.data[0] ||
  224. // translate(0, 0)
  225. transform.name == 'translate' &&
  226. !transform.data[0] &&
  227. !transform.data[1] ||
  228. // scale(1)
  229. transform.name == 'scale' &&
  230. transform.data[0] == 1 &&
  231. (transform.data.length < 2 || transform.data[1] == 1) ||
  232. // matrix(1 0 0 1 0 0)
  233. transform.name == 'matrix' &&
  234. transform.data[0] == 1 &&
  235. transform.data[3] == 1 &&
  236. !(transform.data[1] || transform.data[2] || transform.data[4] || transform.data[5])
  237. ) {
  238. return false;
  239. }
  240. return true;
  241. });
  242. }
  243. /**
  244. * Convert transforms JS representation to string.
  245. *
  246. * @param {Array} transformJS JS representation array
  247. * @param {Object} params plugin params
  248. * @return {String} output string
  249. */
  250. function js2transform(transformJS, params) {
  251. var transformString = '';
  252. // collect output value string
  253. transformJS.forEach(function(transform) {
  254. roundTransform(transform);
  255. transformString += (transformString && ' ') + transform.name + '(' + cleanupOutData(transform.data, params) + ')';
  256. });
  257. return transformString;
  258. }
  259. function roundTransform(transform) {
  260. switch (transform.name) {
  261. case 'translate':
  262. transform.data = floatRound(transform.data);
  263. break;
  264. case 'rotate':
  265. transform.data = degRound(transform.data.slice(0, 1)).concat(floatRound(transform.data.slice(1)));
  266. break;
  267. case 'skewX':
  268. case 'skewY':
  269. transform.data = degRound(transform.data);
  270. break;
  271. case 'scale':
  272. transform.data = transformRound(transform.data);
  273. break;
  274. case 'matrix':
  275. transform.data = transformRound(transform.data.slice(0, 4)).concat(floatRound(transform.data.slice(4)));
  276. break;
  277. }
  278. return transform;
  279. }
  280. /**
  281. * Rounds numbers in array.
  282. *
  283. * @param {Array} data input data array
  284. * @return {Array} output data array
  285. */
  286. function round(data) {
  287. return data.map(Math.round);
  288. }
  289. /**
  290. * Decrease accuracy of floating-point numbers
  291. * in transforms keeping a specified number of decimals.
  292. * Smart rounds values like 2.349 to 2.35.
  293. *
  294. * @param {Number} fixed number of decimals
  295. * @param {Array} data input data array
  296. * @return {Array} output data array
  297. */
  298. function smartRound(precision, data) {
  299. for (var i = data.length, tolerance = +Math.pow(.1, precision).toFixed(precision); i--;) {
  300. if (data[i].toFixed(precision) != data[i]) {
  301. var rounded = +data[i].toFixed(precision - 1);
  302. data[i] = +Math.abs(rounded - data[i]).toFixed(precision + 1) >= tolerance ?
  303. +data[i].toFixed(precision) :
  304. rounded;
  305. }
  306. }
  307. return data;
  308. }