svg-path-commander 0.1.7 → 0.1.10-alpha2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/README.md +1 -13
  2. package/dist/svg-path-commander.esm.js +2587 -1819
  3. package/dist/svg-path-commander.esm.min.js +2 -2
  4. package/dist/svg-path-commander.js +2624 -1843
  5. package/dist/svg-path-commander.min.js +2 -2
  6. package/package.json +4 -4
  7. package/src/convert/pathToAbsolute.js +22 -14
  8. package/src/convert/pathToCurve.js +24 -20
  9. package/src/convert/pathToRelative.js +28 -19
  10. package/src/convert/pathToString.js +13 -2
  11. package/src/index.js +1 -4
  12. package/src/math/distanceSquareRoot.js +8 -0
  13. package/src/math/epsilon.js +8 -1
  14. package/src/math/midPoint.js +10 -4
  15. package/src/math/polygonArea.js +9 -2
  16. package/src/math/polygonLength.js +13 -7
  17. package/src/math/rotateVector.js +13 -0
  18. package/src/options/options.js +5 -1
  19. package/src/parser/finalizeSegment.js +31 -0
  20. package/src/parser/invalidPathValue.js +2 -0
  21. package/src/parser/isArcCommand.js +10 -0
  22. package/src/parser/isDigit.js +9 -0
  23. package/src/{util → parser}/isDigitStart.js +7 -0
  24. package/src/{util → parser}/isPathCommand.js +7 -1
  25. package/src/parser/isSpace.js +16 -0
  26. package/src/parser/paramsCount.js +8 -0
  27. package/src/parser/parsePathString.js +40 -0
  28. package/src/parser/scanFlag.js +26 -0
  29. package/src/parser/scanParam.js +97 -0
  30. package/src/parser/scanSegment.js +70 -0
  31. package/src/parser/skipSpaces.js +15 -0
  32. package/src/parser/svgPathArray.js +23 -0
  33. package/src/process/arcToCubic.js +30 -14
  34. package/src/process/clonePath.js +9 -2
  35. package/src/process/fixArc.js +21 -0
  36. package/src/{util → process}/getSVGMatrix.js +16 -14
  37. package/src/process/lineToCubic.js +11 -2
  38. package/src/process/normalizePath.js +20 -13
  39. package/src/process/normalizeSegment.js +10 -2
  40. package/src/process/optimizePath.js +16 -5
  41. package/src/{util → process}/projection2d.js +14 -5
  42. package/src/process/quadToCubic.js +11 -0
  43. package/src/process/reverseCurve.js +11 -5
  44. package/src/process/reversePath.js +19 -12
  45. package/src/process/roundPath.js +24 -18
  46. package/src/process/segmentToCubic.js +18 -7
  47. package/src/process/shorthandToCubic.js +11 -1
  48. package/src/process/shorthandToQuad.js +11 -1
  49. package/src/process/splitCubic.js +9 -3
  50. package/src/process/splitPath.js +12 -3
  51. package/src/{util → process}/transformEllipse.js +9 -1
  52. package/src/process/transformPath.js +34 -22
  53. package/src/svg-path-commander.js +193 -0
  54. package/src/util/createPath.js +16 -7
  55. package/src/util/getCubicSize.js +14 -2
  56. package/src/util/getDrawDirection.js +11 -4
  57. package/src/util/getPathArea.js +30 -8
  58. package/src/util/getPathBBox.js +13 -7
  59. package/src/util/getPathLength.js +14 -8
  60. package/src/util/getPointAtLength.js +12 -7
  61. package/src/util/getPointAtSegLength.js +15 -1
  62. package/src/util/getSegArcLength.js +8 -5
  63. package/src/util/getSegCubicLength.js +14 -1
  64. package/src/util/getSegLineLength.js +10 -2
  65. package/src/util/getSegQuadLength.js +13 -1
  66. package/src/util/isAbsoluteArray.js +11 -3
  67. package/src/util/isCurveArray.js +11 -3
  68. package/src/util/isNormalizedArray.js +14 -5
  69. package/src/util/isPathArray.js +11 -5
  70. package/src/util/isRelativeArray.js +11 -4
  71. package/src/util/isValidPath.js +26 -0
  72. package/src/util/shapeToPath.js +181 -0
  73. package/src/util/util.js +36 -29
  74. package/src/util/version.js +9 -0
  75. package/types/index.d.ts +834 -2
  76. package/types/types.d.ts +83 -0
  77. package/src/class/SVGPathCommander.js +0 -107
  78. package/src/process/finalizeSegment.js +0 -26
  79. package/src/process/parsePathString.js +0 -36
  80. package/src/process/scanFlag.js +0 -20
  81. package/src/process/scanParam.js +0 -93
  82. package/src/process/scanSegment.js +0 -76
  83. package/src/process/skipSpaces.js +0 -7
  84. package/src/util/DOMMatrix.js +0 -5
  85. package/src/util/fixArc.js +0 -16
  86. package/src/util/invalidPathValue.js +0 -1
  87. package/src/util/isArcCommand.js +0 -4
  88. package/src/util/isDigit.js +0 -3
  89. package/src/util/isSpace.js +0 -9
  90. package/src/util/paramsCount.js +0 -3
  91. package/src/util/rotateVector.js +0 -5
  92. package/src/util/svgPathArray.js +0 -11
  93. package/types/class/SVGPathCommander.d.ts +0 -7
  94. package/types/convert/pathToAbsolute.d.ts +0 -1
  95. package/types/convert/pathToCurve.d.ts +0 -1
  96. package/types/convert/pathToRelative.d.ts +0 -1
  97. package/types/convert/pathToString.d.ts +0 -1
  98. package/types/math/distanceSquareRoot.d.ts +0 -1
  99. package/types/math/epsilon.d.ts +0 -2
  100. package/types/math/midPoint.d.ts +0 -1
  101. package/types/math/polygonArea.d.ts +0 -1
  102. package/types/math/polygonLength.d.ts +0 -1
  103. package/types/options/options.d.ts +0 -6
  104. package/types/process/arcToCubic.d.ts +0 -1
  105. package/types/process/clonePath.d.ts +0 -1
  106. package/types/process/finalizeSegment.d.ts +0 -1
  107. package/types/process/lineToCubic.d.ts +0 -1
  108. package/types/process/normalizePath.d.ts +0 -1
  109. package/types/process/normalizeSegment.d.ts +0 -1
  110. package/types/process/optimizePath.d.ts +0 -1
  111. package/types/process/parsePathString.d.ts +0 -1
  112. package/types/process/quadToCubic.d.ts +0 -1
  113. package/types/process/reverseCurve.d.ts +0 -1
  114. package/types/process/reversePath.d.ts +0 -1
  115. package/types/process/roundPath.d.ts +0 -1
  116. package/types/process/scanFlag.d.ts +0 -1
  117. package/types/process/scanParam.d.ts +0 -1
  118. package/types/process/scanSegment.d.ts +0 -1
  119. package/types/process/segmentToCubic.d.ts +0 -1
  120. package/types/process/shorthandToCubic.d.ts +0 -4
  121. package/types/process/shorthandToQuad.d.ts +0 -4
  122. package/types/process/skipSpaces.d.ts +0 -1
  123. package/types/process/splitCubic.d.ts +0 -1
  124. package/types/process/splitPath.d.ts +0 -1
  125. package/types/process/transformPath.d.ts +0 -1
  126. package/types/util/DOMMatrix.d.ts +0 -2
  127. package/types/util/createPath.d.ts +0 -1
  128. package/types/util/fixArc.d.ts +0 -1
  129. package/types/util/getCubicSize.d.ts +0 -10
  130. package/types/util/getDrawDirection.d.ts +0 -1
  131. package/types/util/getPathArea.d.ts +0 -1
  132. package/types/util/getPathBBox.d.ts +0 -19
  133. package/types/util/getPathLength.d.ts +0 -1
  134. package/types/util/getPointAtLength.d.ts +0 -1
  135. package/types/util/getPointAtSegLength.d.ts +0 -4
  136. package/types/util/getSVGMatrix.d.ts +0 -1
  137. package/types/util/getSegArcLength.d.ts +0 -1
  138. package/types/util/getSegCubicLength.d.ts +0 -1
  139. package/types/util/getSegLineLength.d.ts +0 -1
  140. package/types/util/getSegQuadLength.d.ts +0 -1
  141. package/types/util/invalidPathValue.d.ts +0 -2
  142. package/types/util/isAbsoluteArray.d.ts +0 -1
  143. package/types/util/isArcCommand.d.ts +0 -1
  144. package/types/util/isCurveArray.d.ts +0 -1
  145. package/types/util/isDigit.d.ts +0 -1
  146. package/types/util/isDigitStart.d.ts +0 -1
  147. package/types/util/isNormalizedArray.d.ts +0 -1
  148. package/types/util/isPathArray.d.ts +0 -1
  149. package/types/util/isPathCommand.d.ts +0 -1
  150. package/types/util/isRelativeArray.d.ts +0 -1
  151. package/types/util/isSpace.d.ts +0 -1
  152. package/types/util/paramsCount.d.ts +0 -14
  153. package/types/util/projection2d.d.ts +0 -1
  154. package/types/util/rotateVector.d.ts +0 -4
  155. package/types/util/svgPathArray.d.ts +0 -12
  156. package/types/util/transformEllipse.d.ts +0 -5
  157. package/types/util/util.d.ts +0 -55
@@ -1,1626 +1,2331 @@
1
1
  /*!
2
- * SVGPathCommander v0.1.7 (http://thednp.github.io/svg-path-commander)
2
+ * SVGPathCommander v0.1.10alpha2 (http://thednp.github.io/svg-path-commander)
3
3
  * Copyright 2021 © thednp
4
4
  * Licensed under MIT (https://github.com/thednp/svg-path-commander/blob/master/LICENSE)
5
5
  */
6
+ /**
7
+ * SVGPathCommander default options
8
+ *
9
+ */
6
10
  const SVGPCO = {
7
- origin: null,
11
+ origin: [0, 0],
8
12
  decimals: 4,
9
13
  round: 1,
10
14
  };
11
15
 
12
16
  /**
13
- * DOMMatrix shim - CSSMatrix
14
- *
15
- * Creates and returns a new `DOMMatrix` compatible *Object*
16
- * with equivalent instance methods.
17
- *
18
- * https://developer.mozilla.org/en-US/docs/Web/API/DOMMatrix
19
- * https://github.com/thednp/DOMMatrix/
17
+ * @type {Object.<string, number>}
18
+ */
19
+ const paramsCount = {
20
+ a: 7, c: 6, h: 1, l: 2, m: 2, r: 4, q: 4, s: 4, t: 2, v: 1, z: 0,
21
+ };
22
+
23
+ /**
24
+ * Breaks the parsing of a pathString once a segment is finalized.
20
25
  *
21
- * @param {String} String valid CSS transform in `matrix()`/`matrix3d()` format
22
- * @param {Array} Array expected to be *Float64Array* or *Float32Array* in the column major order.
23
- * @param {[a,b,c,d,e,f]} Arguments representing the 6 elements of a 2d matrix
24
- * @param {[m11,m21,m31,m41..]} Arguments representing the 16 elements of a 3d matrix
26
+ * @param {SVGPC.parserPathArray} path the `parserPathArray` instance
25
27
  */
28
+ function finalizeSegment(path) {
29
+ let pathCommand = path.pathValue[path.segmentStart];
30
+ let pathComLK = pathCommand.toLowerCase();
31
+ let params = path.data;
26
32
 
27
- class CSSMatrix {
28
- constructor(...args) {
29
- this.setIdentity();
30
- return args && args.length && this.setMatrixValue(args);
33
+ // Process duplicated commands (without comand name)
34
+ if (pathComLK === 'm' && params.length > 2) {
35
+ path.segments.push([pathCommand, params[0], params[1]]);
36
+ params = params.slice(2);
37
+ pathComLK = 'l';
38
+ pathCommand = (pathCommand === 'm') ? 'l' : 'L';
31
39
  }
32
40
 
33
- /**
34
- * A `Boolean` whose value is `true` if the matrix is the identity matrix. The identity
35
- * matrix is one in which every value is 0 except those on the main diagonal from top-left
36
- * to bottom-right corner (in other words, where the offsets in each direction are equal).
37
- *
38
- * @return {Boolean} `Boolean` the current property value
39
- */
40
- get isIdentity() {
41
- const m = this;
42
- return (m.m11 === 1 && m.m12 === 0 && m.m13 === 0 && m.m14 === 0
43
- && m.m21 === 0 && m.m22 === 1 && m.m23 === 0 && m.m24 === 0
44
- && m.m31 === 0 && m.m32 === 0 && m.m33 === 1 && m.m34 === 0
45
- && m.m41 === 0 && m.m42 === 0 && m.m43 === 0 && m.m44 === 1);
41
+ if (pathComLK === 'r') {
42
+ path.segments.push([pathCommand].concat(params));
43
+ } else {
44
+ while (params.length >= paramsCount[pathComLK]) {
45
+ path.segments.push([pathCommand].concat(params.splice(0, paramsCount[pathComLK])));
46
+ if (!paramsCount[pathComLK]) {
47
+ break;
48
+ }
49
+ }
46
50
  }
51
+ }
47
52
 
48
- /**
49
- * Sets a new `Boolean` flag value for `this.isIdentity` matrix property.
50
- *
51
- * @param {Boolean} value sets a new `Boolean` flag for this property
52
- */
53
- set isIdentity(value) {
54
- this.isIdentity = value;
55
- }
53
+ const invalidPathValue = 'Invalid path value';
56
54
 
57
- /**
58
- * A `Boolean` flag whose value is `true` if the matrix was initialized as a 2D matrix
59
- * and `false` if the matrix is 3D.
60
- *
61
- * @return {Boolean} `Boolean` the current property value
62
- */
63
- get is2D() {
64
- const m = this;
65
- return (m.m31 === 0 && m.m32 === 0 && m.m33 === 1 && m.m34 === 0 && m.m43 === 0 && m.m44 === 1);
66
- }
55
+ /**
56
+ * Validates an A (arc-to) specific path command value.
57
+ * Usually a `large-arc-flag` or `sweep-flag`.
58
+ *
59
+ * @param {SVGPC.parserPathArray} path the `parserPathArray` instance
60
+ */
61
+ function scanFlag(path) {
62
+ const { index } = path;
63
+ const ch = path.pathValue.charCodeAt(index);
67
64
 
68
- /**
69
- * Sets a new `Boolean` flag value for `this.is2D` matrix property.
70
- *
71
- * @param {Boolean} value sets a new `Boolean` flag for this property
72
- */
73
- set is2D(value) {
74
- this.is2D = value;
65
+ if (ch === 0x30/* 0 */) {
66
+ path.param = 0;
67
+ path.index += 1;
68
+ return;
75
69
  }
76
- }
77
70
 
78
- // export proto for custom compile via Buble
79
- const CSSMatrixProto = CSSMatrix.prototype;
71
+ if (ch === 0x31/* 1 */) {
72
+ path.param = 1;
73
+ path.index += 1;
74
+ return;
75
+ }
80
76
 
81
- // Transform Functions
82
- // https://www.w3.org/TR/css-transforms-1/#transform-functions
77
+ path.err = `${invalidPathValue}: invalid Arc flag ${ch}, expecting 0 or 1 at index ${index}`;
78
+ }
83
79
 
84
80
  /**
85
- * Creates a new `CSSMatrix` for the translation matrix and returns it.
86
- * This method is equivalent to the CSS `translate3d()` function.
87
- *
88
- * https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/translate3d
81
+ * Checks if a character is a digit.
89
82
  *
90
- * @param {Number} x the `x-axis` position.
91
- * @param {Number} y the `y-axis` position.
92
- * @param {Number} z the `z-axis` position.
83
+ * @param {string} code the character to check
84
+ * @returns {boolean} check result
93
85
  */
94
- function Translate(x, y, z) {
95
- const m = new CSSMatrix();
96
- m.m41 = x;
97
- m.e = x;
98
- m.m42 = y;
99
- m.f = y;
100
- m.m43 = z;
101
- return m;
86
+ function isDigit(code) {
87
+ return (code >= 48 && code <= 57); // 0..9
102
88
  }
103
89
 
104
90
  /**
105
- * Creates a new `CSSMatrix` for the rotation matrix and returns it.
106
- *
107
- * http://en.wikipedia.org/wiki/Rotation_matrix
91
+ * Validates every character of the path string,
92
+ * every path command, negative numbers or floating point numbers.
108
93
  *
109
- * @param {Number} rx the `x-axis` rotation.
110
- * @param {Number} ry the `y-axis` rotation.
111
- * @param {Number} rz the `z-axis` rotation.
94
+ * @param {SVGPC.parserPathArray} path the `parserPathArray` instance
112
95
  */
96
+ function scanParam(path) {
97
+ const { max, pathValue, index: start } = path;
98
+ let index = start;
99
+ let zeroFirst = false;
100
+ let hasCeiling = false;
101
+ let hasDecimal = false;
102
+ let hasDot = false;
103
+ let ch;
113
104
 
114
- function Rotate(rx, ry, rz) {
115
- const m = new CSSMatrix();
105
+ if (index >= max) {
106
+ // path.err = 'SvgPath: missed param (at pos ' + index + ')';
107
+ path.err = `${invalidPathValue} at ${index}: missing param ${pathValue[index]}`;
108
+ return;
109
+ }
110
+ ch = pathValue.charCodeAt(index);
116
111
 
117
- const radX = (rx * Math.PI) / 180;
118
- const radY = (ry * Math.PI) / 180;
119
- const radZ = (rz * Math.PI) / 180;
112
+ if (ch === 0x2B/* + */ || ch === 0x2D/* - */) {
113
+ index += 1;
114
+ ch = (index < max) ? pathValue.charCodeAt(index) : 0;
115
+ }
120
116
 
121
- // minus sin() because of right-handed system
122
- const cosx = Math.cos(radX);
123
- const sinx = -Math.sin(radX);
124
- const cosy = Math.cos(radY);
125
- const siny = -Math.sin(radY);
126
- const cosz = Math.cos(radZ);
127
- const sinz = -Math.sin(radZ);
117
+ // This logic is shamelessly borrowed from Esprima
118
+ // https://github.com/ariya/esprimas
119
+ if (!isDigit(ch) && ch !== 0x2E/* . */) {
120
+ // path.err = 'SvgPath: param should start with 0..9 or `.` (at pos ' + index + ')';
121
+ path.err = `${invalidPathValue} at index ${index}: ${pathValue[index]} is not a number`;
122
+ return;
123
+ }
128
124
 
129
- const cycz = cosy * cosz;
130
- const cysz = -cosy * sinz;
125
+ if (ch !== 0x2E/* . */) {
126
+ zeroFirst = (ch === 0x30/* 0 */);
127
+ index += 1;
131
128
 
132
- m.m11 = cycz;
133
- m.a = cycz;
129
+ ch = (index < max) ? pathValue.charCodeAt(index) : 0;
134
130
 
135
- m.m12 = cysz;
136
- m.b = cysz;
131
+ if (zeroFirst && index < max) {
132
+ // decimal number starts with '0' such as '09' is illegal.
133
+ if (ch && isDigit(ch)) {
134
+ // path.err = 'SvgPath: numbers started with `0` such as `09`
135
+ // are illegal (at pos ' + start + ')';
136
+ path.err = `${invalidPathValue} at index ${start}: ${pathValue[start]} illegal number`;
137
+ return;
138
+ }
139
+ }
137
140
 
138
- m.m13 = siny;
141
+ while (index < max && isDigit(pathValue.charCodeAt(index))) {
142
+ index += 1;
143
+ hasCeiling = true;
144
+ }
145
+ ch = (index < max) ? pathValue.charCodeAt(index) : 0;
146
+ }
139
147
 
140
- const sxsy = sinx * siny * cosz + cosx * sinz;
141
- m.m21 = sxsy;
142
- m.c = sxsy;
148
+ if (ch === 0x2E/* . */) {
149
+ hasDot = true;
150
+ index += 1;
151
+ while (isDigit(pathValue.charCodeAt(index))) {
152
+ index += 1;
153
+ hasDecimal = true;
154
+ }
155
+ ch = (index < max) ? pathValue.charCodeAt(index) : 0;
156
+ }
143
157
 
144
- const cxcz = cosx * cosz - sinx * siny * sinz;
145
- m.m22 = cxcz;
146
- m.d = cxcz;
158
+ if (ch === 0x65/* e */ || ch === 0x45/* E */) {
159
+ if (hasDot && !hasCeiling && !hasDecimal) {
160
+ path.err = `${invalidPathValue} at index ${index}: ${pathValue[index]} invalid float exponent`;
161
+ return;
162
+ }
147
163
 
148
- m.m23 = -sinx * cosy;
164
+ index += 1;
149
165
 
150
- m.m31 = sinx * sinz - cosx * siny * cosz;
151
- m.m32 = sinx * cosz + cosx * siny * sinz;
152
- m.m33 = cosx * cosy;
166
+ ch = (index < max) ? pathValue.charCodeAt(index) : 0;
167
+ if (ch === 0x2B/* + */ || ch === 0x2D/* - */) {
168
+ index += 1;
169
+ }
170
+ if (index < max && isDigit(pathValue.charCodeAt(index))) {
171
+ while (index < max && isDigit(pathValue.charCodeAt(index))) {
172
+ index += 1;
173
+ }
174
+ } else {
175
+ // path.err = 'SvgPath: invalid float exponent (at pos ' + index + ')';
176
+ path.err = `${invalidPathValue} at index ${index}: ${pathValue[index]} invalid float exponent`;
177
+ return;
178
+ }
179
+ }
153
180
 
154
- return m;
181
+ path.index = index;
182
+ path.param = +path.pathValue.slice(start, index);
155
183
  }
156
184
 
157
185
  /**
158
- * Creates a new `CSSMatrix` for the rotation matrix and returns it.
159
- * This method is equivalent to the CSS `rotate3d()` function.
186
+ * Checks if the character is a space.
160
187
  *
161
- * https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/rotate3d
188
+ * @param {string} code the character to check
189
+ * @returns {boolean} check result
190
+ */
191
+ function isSpace(code) {
192
+ const specialSpaces = [
193
+ 0x1680, 0x180E, 0x2000, 0x2001, 0x2002, 0x2003, 0x2004, 0x2005, 0x2006,
194
+ 0x2007, 0x2008, 0x2009, 0x200A, 0x202F, 0x205F, 0x3000, 0xFEFF];
195
+ // Line terminators
196
+ return (code === 0x0A) || (code === 0x0D) || (code === 0x2028) || (code === 0x2029)
197
+ // White spaces
198
+ || (code === 0x20) || (code === 0x09) || (code === 0x0B) || (code === 0x0C) || (code === 0xA0)
199
+ || (code >= 0x1680 && specialSpaces.indexOf(code) >= 0);
200
+ }
201
+
202
+ /**
203
+ * Points the parser to the next character in the
204
+ * path string every time it encounters any kind of
205
+ * space character.
162
206
  *
163
- * @param {Number} x the `x-axis` vector length.
164
- * @param {Number} y the `y-axis` vector length.
165
- * @param {Number} z the `z-axis` vector length.
166
- * @param {Number} angle the value in degrees of the rotation.
207
+ * @param {SVGPC.parserPathArray} path the `parserPathArray` instance
167
208
  */
168
- function RotateAxisAngle(x, y, z, angle) {
169
- const m = new CSSMatrix();
170
- const radA = (angle * Math.PI) / 360;
171
- const sinA = Math.sin(radA);
172
- const cosA = Math.cos(radA);
173
- const sinA2 = sinA * sinA;
174
- const length = Math.sqrt(x * x + y * y + z * z);
175
- let X = 0;
176
- let Y = 0;
177
- let Z = 1;
209
+ function skipSpaces(path) {
210
+ const { pathValue, max } = path;
211
+ while (path.index < max && isSpace(pathValue.charCodeAt(path.index))) {
212
+ path.index += 1;
213
+ }
214
+ }
178
215
 
179
- // bad vector length, use something reasonable
180
- if (length !== 0) {
181
- X = x / length;
182
- Y = y / length;
183
- Z = z / length;
216
+ /**
217
+ * Checks if the character is a path command.
218
+ *
219
+ * @param {any} code the character to check
220
+ * @returns {boolean} check result
221
+ */
222
+ function isPathCommand(code) {
223
+ // eslint-disable-next-line no-bitwise -- Impossible to satisfy
224
+ switch (code | 0x20) {
225
+ case 0x6D/* m */:
226
+ case 0x7A/* z */:
227
+ case 0x6C/* l */:
228
+ case 0x68/* h */:
229
+ case 0x76/* v */:
230
+ case 0x63/* c */:
231
+ case 0x73/* s */:
232
+ case 0x71/* q */:
233
+ case 0x74/* t */:
234
+ case 0x61/* a */:
235
+ case 0x72/* r */:
236
+ return true;
237
+ default:
238
+ return false;
184
239
  }
240
+ }
185
241
 
186
- const x2 = X * X;
187
- const y2 = Y * Y;
188
- const z2 = Z * Z;
242
+ /**
243
+ * Checks if the character is or belongs to a number.
244
+ * [0-9]|+|-|.
245
+ *
246
+ * @param {string} code the character to check
247
+ * @returns {boolean} check result
248
+ */
249
+ function isDigitStart(code) {
250
+ return (code >= 48 && code <= 57) /* 0..9 */
251
+ || code === 0x2B /* + */
252
+ || code === 0x2D /* - */
253
+ || code === 0x2E; /* . */
254
+ }
189
255
 
190
- const m11 = 1 - 2 * (y2 + z2) * sinA2;
191
- m.m11 = m11;
192
- m.a = m11;
256
+ /**
257
+ * Checks if the character is an A (arc-to) path command.
258
+ *
259
+ * @param {string} code the character to check
260
+ * @returns {boolean} check result
261
+ */
262
+ function isArcCommand(code) {
263
+ // eslint-disable-next-line no-bitwise -- Impossible to satisfy
264
+ return (code | 0x20) === 0x61;
265
+ }
193
266
 
194
- const m12 = 2 * (x * y * sinA2 + z * sinA * cosA);
195
- m.m12 = m12;
196
- m.b = m12;
267
+ /**
268
+ * Scans every character in the path string to determine
269
+ * where a segment starts and where it ends.
270
+ *
271
+ * @param {SVGPC.parserPathArray} path the `parserPathArray` instance
272
+ */
273
+ function scanSegment(path) {
274
+ const { max, pathValue, index } = path;
275
+ const cmdCode = pathValue.charCodeAt(index);
276
+ const reqParams = paramsCount[pathValue[index].toLowerCase()];
197
277
 
198
- m.m13 = 2 * (x * z * sinA2 - y * sinA * cosA);
278
+ path.segmentStart = index;
199
279
 
200
- const m21 = 2 * (y * x * sinA2 - z * sinA * cosA);
201
- m.m21 = m21;
202
- m.c = m21;
280
+ if (!isPathCommand(cmdCode)) {
281
+ path.err = `${invalidPathValue}: ${pathValue[index]} not a path command`;
282
+ return;
283
+ }
203
284
 
204
- const m22 = 1 - 2 * (z2 + x2) * sinA2;
205
- m.m22 = m22;
206
- m.d = m22;
285
+ path.index += 1;
286
+ skipSpaces(path);
207
287
 
208
- m.m23 = 2 * (y * z * sinA2 + x * sinA * cosA);
209
- m.m31 = 2 * (z * x * sinA2 + y * sinA * cosA);
210
- m.m32 = 2 * (z * y * sinA2 - x * sinA * cosA);
211
- m.m33 = 1 - 2 * (x2 + y2) * sinA2;
288
+ path.data = [];
212
289
 
213
- m.m14 = 0;
214
- m.m24 = 0;
215
- m.m34 = 0;
290
+ if (!reqParams) {
291
+ // Z
292
+ finalizeSegment(path);
293
+ return;
294
+ }
216
295
 
217
- m.m41 = 0;
218
- m.e = 0;
219
- m.m42 = 0;
220
- m.f = 0;
221
- m.m43 = 0;
296
+ for (;;) {
297
+ for (let i = reqParams; i > 0; i -= 1) {
298
+ if (isArcCommand(cmdCode) && (i === 3 || i === 4)) scanFlag(path);
299
+ else scanParam(path);
222
300
 
223
- m.m44 = 1;
301
+ if (path.err.length) {
302
+ return;
303
+ }
304
+ path.data.push(path.param);
224
305
 
225
- return m;
306
+ skipSpaces(path);
307
+
308
+ // after ',' param is mandatory
309
+ if (path.index < max && pathValue.charCodeAt(path.index) === 0x2C/* , */) {
310
+ path.index += 1;
311
+ skipSpaces(path);
312
+ }
313
+ }
314
+
315
+ if (path.index >= path.max) {
316
+ break;
317
+ }
318
+
319
+ // Stop on next segment
320
+ if (!isDigitStart(pathValue.charCodeAt(path.index))) {
321
+ break;
322
+ }
323
+ }
324
+
325
+ finalizeSegment(path);
226
326
  }
227
327
 
328
+ // @ts-nocheck
228
329
  /**
229
- * Creates a new `CSSMatrix` for the scale matrix and returns it.
230
- * This method is equivalent to the CSS `scale3d()` function.
231
- *
232
- * https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/scale3d
330
+ * Returns a clone of an existing `pathArray`.
233
331
  *
234
- * @param {Number} x the `x-axis` scale.
235
- * @param {Number} y the `y-axis` scale.
236
- * @param {Number} z the `z-axis` scale.
332
+ * @param {SVGPC.pathArray | string[]} path the source `pathArray`
333
+ * @returns {SVGPC.pathArray} the cloned `pathArray`
237
334
  */
238
- function Scale(x, y, z) {
239
- const m = new CSSMatrix();
240
- m.m11 = x;
241
- m.a = x;
242
-
243
- m.m22 = y;
244
- m.d = y;
245
-
246
- m.m33 = z;
247
- return m;
335
+ function clonePath(path) {
336
+ return path.map((x) => {
337
+ if (Array.isArray(x)) {
338
+ return clonePath(x);
339
+ }
340
+ return !Number.isNaN(+x) ? +x : x;
341
+ });
248
342
  }
249
343
 
250
344
  /**
251
- * Creates a new `CSSMatrix` for the shear of the `x-axis` rotation matrix and
252
- * returns it. This method is equivalent to the CSS `skewX()` function.
253
- *
254
- * https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/skewX
345
+ * The `parserPathArray` used by the parser.
255
346
  *
256
- * @param {Number} angle the angle in degrees.
347
+ * @param {string} pathString
257
348
  */
258
- function SkewX(angle) {
259
- const radA = (angle * Math.PI) / 180;
260
- const m = new CSSMatrix();
261
- const t = Math.tan(radA);
262
- m.m21 = t;
263
- m.c = t;
264
- return m;
349
+ function SVGPathArray(pathString) {
350
+ /** @type {[string, ...number[]][]} */
351
+ this.segments = [];
352
+ /** @type {string} */
353
+ this.pathValue = pathString;
354
+ /** @type {number} */
355
+ this.max = pathString.length;
356
+ /** @type {number} */
357
+ this.index = 0;
358
+ /** @type {number} */
359
+ this.param = 0.0;
360
+ /** @type {number} */
361
+ this.segmentStart = 0;
362
+ /** @type {any} */
363
+ this.data = [];
364
+ /** @type {string} */
365
+ this.err = '';
265
366
  }
266
367
 
267
368
  /**
268
- * Creates a new `CSSMatrix` for the shear of the `y-axis` rotation matrix and
269
- * returns it. This method is equivalent to the CSS `skewY()` function.
270
- *
271
- * https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/skewY
369
+ * Iterates an array to check if it's an actual `pathArray`.
272
370
  *
273
- * @param {Number} angle the angle in degrees.
371
+ * @param {string | SVGPC.pathArray} path the `pathArray` to be checked
372
+ * @returns {boolean} iteration result
274
373
  */
275
- function SkewY(angle) {
276
- const radA = (angle * Math.PI) / 180;
277
- const m = new CSSMatrix();
278
- const t = Math.tan(radA);
279
- m.m12 = t;
280
- m.b = t;
281
- return m;
374
+ function isPathArray(path) {
375
+ return Array.isArray(path) && path.every((seg) => {
376
+ const lk = seg[0].toLowerCase();
377
+ return paramsCount[lk] === seg.length - 1 && /[achlmqstvz]/gi.test(lk);
378
+ });
282
379
  }
283
380
 
284
381
  /**
285
- * Creates a new `CSSMatrix` resulted from the multiplication of two matrixes
286
- * and returns it. Both matrixes are not changed.
382
+ * Parses a path string value and returns an array
383
+ * of segments we like to call `pathArray`.
287
384
  *
288
- * @param {CSSMatrix} m1 the first matrix.
289
- * @param {CSSMatrix} m2 the second matrix.
385
+ * @param {string | SVGPC.pathArray} pathInput the string to be parsed
386
+ * @returns {SVGPC.pathArray} the resulted `pathArray`
290
387
  */
291
- function Multiply(m1, m2) {
292
- const m11 = m2.m11 * m1.m11 + m2.m12 * m1.m21 + m2.m13 * m1.m31 + m2.m14 * m1.m41;
293
- const m12 = m2.m11 * m1.m12 + m2.m12 * m1.m22 + m2.m13 * m1.m32 + m2.m14 * m1.m42;
294
- const m13 = m2.m11 * m1.m13 + m2.m12 * m1.m23 + m2.m13 * m1.m33 + m2.m14 * m1.m43;
295
- const m14 = m2.m11 * m1.m14 + m2.m12 * m1.m24 + m2.m13 * m1.m34 + m2.m14 * m1.m44;
296
-
297
- const m21 = m2.m21 * m1.m11 + m2.m22 * m1.m21 + m2.m23 * m1.m31 + m2.m24 * m1.m41;
298
- const m22 = m2.m21 * m1.m12 + m2.m22 * m1.m22 + m2.m23 * m1.m32 + m2.m24 * m1.m42;
299
- const m23 = m2.m21 * m1.m13 + m2.m22 * m1.m23 + m2.m23 * m1.m33 + m2.m24 * m1.m43;
300
- const m24 = m2.m21 * m1.m14 + m2.m22 * m1.m24 + m2.m23 * m1.m34 + m2.m24 * m1.m44;
388
+ function parsePathString(pathInput) {
389
+ if (isPathArray(pathInput)) {
390
+ return clonePath(pathInput);
391
+ }
301
392
 
302
- const m31 = m2.m31 * m1.m11 + m2.m32 * m1.m21 + m2.m33 * m1.m31 + m2.m34 * m1.m41;
303
- const m32 = m2.m31 * m1.m12 + m2.m32 * m1.m22 + m2.m33 * m1.m32 + m2.m34 * m1.m42;
304
- const m33 = m2.m31 * m1.m13 + m2.m32 * m1.m23 + m2.m33 * m1.m33 + m2.m34 * m1.m43;
305
- const m34 = m2.m31 * m1.m14 + m2.m32 * m1.m24 + m2.m33 * m1.m34 + m2.m34 * m1.m44;
393
+ const path = new SVGPathArray(pathInput);
306
394
 
307
- const m41 = m2.m41 * m1.m11 + m2.m42 * m1.m21 + m2.m43 * m1.m31 + m2.m44 * m1.m41;
308
- const m42 = m2.m41 * m1.m12 + m2.m42 * m1.m22 + m2.m43 * m1.m32 + m2.m44 * m1.m42;
309
- const m43 = m2.m41 * m1.m13 + m2.m42 * m1.m23 + m2.m43 * m1.m33 + m2.m44 * m1.m43;
310
- const m44 = m2.m41 * m1.m14 + m2.m42 * m1.m24 + m2.m43 * m1.m34 + m2.m44 * m1.m44;
395
+ skipSpaces(path);
311
396
 
312
- return new CSSMatrix(
313
- [m11, m21, m31, m41,
314
- m12, m22, m32, m42,
315
- m13, m23, m33, m43,
316
- m14, m24, m34, m44],
317
- );
318
- }
397
+ while (path.index < path.max && !path.err.length) {
398
+ scanSegment(path);
399
+ }
319
400
 
320
- /**
321
- * Returns a new *Float32Array* containing all 16 elements which comprise the matrix.
322
- * The elements are stored into the array as single-precision floating-point numbers
323
- * in column-major (colexographical access access or "colex") order.
324
- *
325
- * @return {Float32Array} matrix elements (m11, m21, m31, m41, ..)
326
- */
327
- // toFloat32Array(){
328
- // return Float32Array.from(this.toArray());
329
- // }
401
+ if (path.err.length) {
402
+ path.segments = [];
403
+ } else if (path.segments.length) {
404
+ if ('mM'.indexOf(path.segments[0][0]) < 0) {
405
+ path.err = `${invalidPathValue}: missing M/m`;
406
+ path.segments = [];
407
+ } else {
408
+ path.segments[0][0] = 'M';
409
+ }
410
+ }
330
411
 
331
- /**
332
- * Returns a new Float64Array containing all 16 elements which comprise the matrix.
333
- * The elements are stored into the array as double-precision floating-point numbers
334
- * in column-major (colexographical access access or "colex") order.
335
- *
336
- * @return {Float64Array} matrix elements (m11, m21, m31, m41, ..)
337
- */
338
- // toFloat64Array(){
339
- // return Float64Array.from(this.toArray());
340
- // }
412
+ return path.segments;
413
+ }
341
414
 
342
415
  /**
343
- * Creates a new mutable `CSSMatrix` object given an existing matrix or a
344
- * `DOMMatrix` *Object* which provides the values for its properties.
416
+ * Iterates an array to check if it's a `pathArray`
417
+ * with all absolute values.
345
418
  *
346
- * @param {CSSMatrix} CSSMatrix the source `CSSMatrix` initialization to feed values from
419
+ * @param {string | SVGPC.pathArray} path the `pathArray` to be checked
420
+ * @returns {boolean} iteration result
347
421
  */
348
- function fromMatrix(m) {
349
- return new CSSMatrix(
350
- // DOMMatrix elements order
351
- [m.m11, m.m21, m.m31, m.m41,
352
- m.m12, m.m22, m.m32, m.m42,
353
- m.m13, m.m23, m.m33, m.m43,
354
- m.m14, m.m24, m.m34, m.m44],
355
- );
422
+ function isAbsoluteArray(path) {
423
+ return Array.isArray(path) && isPathArray(path)
424
+ && path.every((x) => x[0] === x[0].toUpperCase());
356
425
  }
357
426
 
358
427
  /**
359
- * Feed a CSSMatrix object with the values of a 6/16 values array and returns it.
428
+ * Parses a path string value or object and returns an array
429
+ * of segments, all converted to absolute values.
360
430
  *
361
- * @param {Array} array The source `Array` to feed values from.
362
- * @return {CSSMatrix} a The source array to feed values from.
431
+ * @param {string | SVGPC.pathArray} pathInput the path string | object
432
+ * @returns {SVGPC.pathArray} the resulted `pathArray` with absolute values
363
433
  */
364
- function feedFromArray(m, array) {
365
- const a = Array.from(array);
366
- if (a.length === 16) {
367
- const [m11, m21, m31, m41,
368
- m12, m22, m32, m42,
369
- m13, m23, m33, m43,
370
- m14, m24, m34, m44] = a;
371
-
372
- m.m11 = m11;
373
- m.a = m11;
374
-
375
- m.m21 = m21;
376
- m.c = m21;
377
-
378
- m.m31 = m31;
379
-
380
- m.m41 = m41;
381
- m.e = m41;
382
-
383
- m.m12 = m12;
384
- m.b = m12;
385
-
386
- m.m22 = m22;
387
- m.d = m22;
388
-
389
- m.m32 = m32;
390
-
391
- m.m42 = m42;
392
- m.f = m42;
434
+ function pathToAbsolute(pathInput) {
435
+ if (Array.isArray(pathInput) && isAbsoluteArray(pathInput)) {
436
+ return clonePath(pathInput);
437
+ }
393
438
 
394
- m.m13 = m13;
395
- m.m23 = m23;
396
- m.m33 = m33;
397
- m.m43 = m43;
398
- m.m14 = m14;
399
- m.m24 = m24;
400
- m.m34 = m34;
401
- m.m44 = m44;
402
- } else if (a.length === 6) {
403
- const [m11, m12, m21, m22, m14, m24] = a;
439
+ const path = parsePathString(pathInput);
440
+ const ii = path.length;
441
+ /** @type {SVGPC.pathArray} */
442
+ const resultArray = [];
443
+ let x = 0;
444
+ let y = 0;
445
+ let mx = 0;
446
+ let my = 0;
447
+ let start = 0;
404
448
 
405
- m.m11 = m11;
406
- m.a = m11;
449
+ if (path[0][0] === 'M') {
450
+ x = +path[0][1];
451
+ y = +path[0][2];
452
+ mx = x;
453
+ my = y;
454
+ start += 1;
455
+ resultArray.push(['M', x, y]);
456
+ }
407
457
 
408
- m.m12 = m12;
409
- m.b = m12;
458
+ for (let i = start; i < ii; i += 1) {
459
+ const segment = path[i];
460
+ const [pathCommand] = segment;
461
+ const absCommand = pathCommand.toUpperCase();
462
+ const absoluteSegment = [];
463
+ let newSeg = [];
410
464
 
411
- m.m21 = m21;
412
- m.c = m21;
465
+ if (pathCommand !== absCommand) {
466
+ absoluteSegment[0] = absCommand;
413
467
 
414
- m.m22 = m22;
415
- m.d = m22;
468
+ switch (absCommand) {
469
+ case 'A':
470
+ newSeg = segment.slice(1, -2).concat([+segment[6] + x, +segment[7] + y]);
471
+ for (let j = 0; j < newSeg.length; j += 1) {
472
+ absoluteSegment.push(newSeg[j]);
473
+ }
474
+ break;
475
+ case 'V':
476
+ absoluteSegment[1] = +segment[1] + y;
477
+ break;
478
+ case 'H':
479
+ absoluteSegment[1] = +segment[1] + x;
480
+ break;
481
+ default:
482
+ if (absCommand === 'M') {
483
+ mx = +segment[1] + x;
484
+ my = +segment[2] + y;
485
+ }
486
+ // for is here to stay for eslint
487
+ for (let j = 1; j < segment.length; j += 1) {
488
+ absoluteSegment.push(+segment[j] + (j % 2 ? x : y));
489
+ }
490
+ }
491
+ } else {
492
+ for (let j = 0; j < segment.length; j += 1) {
493
+ absoluteSegment.push(segment[j]);
494
+ }
495
+ }
496
+ // @ts-ignore
497
+ resultArray.push(absoluteSegment);
416
498
 
417
- m.m14 = m14;
418
- m.e = m14;
499
+ const segLength = absoluteSegment.length;
500
+ switch (absCommand) {
501
+ case 'Z':
502
+ x = mx;
503
+ y = my;
504
+ break;
505
+ case 'H':
506
+ x = +absoluteSegment[1];
507
+ break;
508
+ case 'V':
509
+ y = +absoluteSegment[1];
510
+ break;
511
+ default:
512
+ x = +absoluteSegment[segLength - 2];
513
+ y = +absoluteSegment[segLength - 1];
419
514
 
420
- m.m24 = m24;
421
- m.f = m24;
422
- } else {
423
- throw new TypeError('CSSMatrix: expecting a 6/16 values Array');
515
+ if (absCommand === 'M') {
516
+ mx = x;
517
+ my = y;
518
+ }
519
+ }
424
520
  }
425
- return m;
426
- }
427
521
 
428
- /**
429
- * Creates a new mutable `CSSMatrix` object given an array float values.
430
- *
431
- * If the array has six values, the result is a 2D matrix; if the array has 16 values,
432
- * the result is a 3D matrix. Otherwise, a TypeError exception is thrown.
433
- *
434
- * @param {Array} array The source `Array` to feed values from.
435
- * @return {CSSMatrix} a The source array to feed values from.
436
- */
437
- function fromArray(a) {
438
- return feedFromArray(new CSSMatrix(), a);
522
+ return resultArray;
439
523
  }
440
524
 
441
525
  /**
442
- * Each create a new mutable `CSSMatrix` object given an array of single/double-precision
443
- * (32/64 bit) floating-point values.
444
- *
445
- * If the array has six values, the result is a 2D matrix; if the array has 16 values,
446
- * the result is a 3D matrix. Otherwise, a TypeError exception is thrown.
526
+ * Iterates an array to check if it's a `pathArray`
527
+ * with relative values.
447
528
  *
448
- * @param {Float32Array|Float64Array} array The source float array to feed values from.
449
- * @return {CSSMatrix} a The source array to feed values from.
529
+ * @param {string | SVGPC.pathArray} path the `pathArray` to be checked
530
+ * @returns {boolean} iteration result
450
531
  */
451
- // more of an alias for now, will update later if it's the case
452
- // function fromFloat32Array(a){
453
- // return feedFromArray(new CSSMatrix(), a);
454
- // }
455
- // function fromFloat64Array(a){ // more of an alias
456
- // return feedFromArray(new CSSMatrix(), a);
457
- // }
532
+ function isRelativeArray(path) {
533
+ return Array.isArray(path) && isPathArray(path)
534
+ && path.slice(1).every((seg) => seg[0] === seg[0].toLowerCase());
535
+ }
458
536
 
459
537
  /**
460
- * The `setMatrixValue` method replaces the existing matrix with one computed
461
- * in the browser. EG: `matrix(1,0.25,-0.25,1,0,0)`
462
- *
463
- * The method accepts *Float64Array* / *Float32Array* / any *Array* values, the result of
464
- * `DOMMatrix` / `CSSMatrix` instance method calls `toFloat64Array()` / `toFloat32Array()`.
538
+ * Parses a path string value or object and returns an array
539
+ * of segments, all converted to relative values.
465
540
  *
466
- * This method expects valid *matrix()* / *matrix3d()* string values, other
467
- * transform functions like *translate()* are not supported.
468
- *
469
- * @param {String} source the *String* resulted from `getComputedStyle()`.
470
- * @param {Array} source the *Array* resulted from `toFloat64Array()`.
541
+ * @param {string | SVGPC.pathArray} pathInput the path string | object
542
+ * @returns {SVGPC.pathArray} the resulted `pathArray` with relative values
471
543
  */
472
- CSSMatrixProto.setMatrixValue = function setMatrixValue(source) {
473
- const m = this;
474
-
475
- if (!source || !source.length) { // no parameters or source
476
- return m;
477
- } if (source.length && typeof source[0] === 'string' && source[0].length) { // CSS transform String source
478
- const string = String(source[0]).trim();
479
- let type = '';
480
- let values = [];
481
-
482
- if (string === 'none') return m;
544
+ function pathToRelative(pathInput) {
545
+ if (Array.isArray(pathInput) && isRelativeArray(pathInput)) {
546
+ return clonePath(pathInput);
547
+ }
483
548
 
484
- type = string.slice(0, string.indexOf('('));
485
- values = string.slice((type === 'matrix' ? 7 : 9), -1).split(',')
486
- .map((n) => (Math.abs(n) < 1e-6 ? 0 : +n));
549
+ const path = parsePathString(pathInput);
550
+ const ii = path.length;
551
+ /** @type {SVGPC.pathArray} */
552
+ const resultArray = [];
553
+ let x = 0;
554
+ let y = 0;
555
+ let mx = 0;
556
+ let my = 0;
557
+ let start = 0;
487
558
 
488
- if ([6, 16].indexOf(values.length) > -1) {
489
- feedFromArray(m, values);
490
- } else {
491
- throw new TypeError('CSSMatrix: expecting valid CSS matrix() / matrix3d() syntax');
492
- }
493
- } else if (source[0] instanceof CSSMatrix) { // CSSMatrix instance
494
- feedFromArray(m, source[0].toArray());
495
- } else if (Array.isArray(source[0])) { // Float32Array,Float64Array source
496
- feedFromArray(m, source[0]);
497
- } else if (Array.isArray(source)) { // Arguments list come here
498
- feedFromArray(m, source);
559
+ if (path[0][0] === 'M') {
560
+ x = +path[0][1];
561
+ y = +path[0][2];
562
+ mx = x;
563
+ my = y;
564
+ start += 1;
565
+ resultArray.push(['M', x, y]);
499
566
  }
500
- return m;
501
- };
502
567
 
503
- /**
504
- * Creates and returns a string representation of the matrix in `CSS` matrix syntax,
505
- * using the appropriate `CSS` matrix notation.
506
- *
507
- * The 16 items in the array 3D matrix array are *transposed* in row-major order.
508
- *
509
- * @matrix3d *matrix3d(m11, m12, m13, m14, m21, ...)*
510
- * @matrix *matrix(a, b, c, d, e, f)*
511
- *
512
- * @return {String} `String` representation of the matrix
513
- */
514
- CSSMatrixProto.toString = function toString() {
515
- const m = this;
516
- const type = m.is2D ? 'matrix' : 'matrix3d';
517
-
518
- return `${type}(${m.toArray(1).join(',')})`;
519
- };
568
+ for (let i = start; i < ii; i += 1) {
569
+ const segment = path[i];
570
+ const [pathCommand] = segment;
571
+ const relativeCommand = pathCommand.toLowerCase();
572
+ const relativeSegment = []; // this a test to please TS
573
+ let newSeg = [];
520
574
 
521
- /**
522
- * Returns an *Array* containing all 16 elements which comprise the matrix.
523
- * The method can return either the elements in default column major order or
524
- * row major order (what we call the *transposed* matrix, used by `toString`).
525
- *
526
- * Other methods make use of this method to feed their output values from this matrix.
527
- *
528
- * @param {Boolean} transposed changes the order of elements in the output
529
- * @return {Array} an *Array* representation of the matrix
530
- */
531
- CSSMatrixProto.toArray = function toArray(transposed) {
532
- const m = this;
533
- let result;
575
+ if (pathCommand !== relativeCommand) {
576
+ relativeSegment[0] = relativeCommand;
577
+ switch (relativeCommand) {
578
+ case 'a':
579
+ newSeg = segment.slice(1, -2).concat([+segment[6] - x, +segment[7] - y]);
534
580
 
535
- if (m.is2D) {
536
- result = [m.a, m.b, m.c, m.d, m.e, m.f];
537
- } else if (transposed) {
538
- result = [m.m11, m.m12, m.m13, m.m14, // transposed is used by toString
539
- m.m21, m.m22, m.m23, m.m24,
540
- m.m31, m.m32, m.m33, m.m34,
541
- m.m41, m.m42, m.m43, m.m44];
542
- } else {
543
- result = [m.m11, m.m21, m.m31, m.m41, // used by constructor
544
- m.m12, m.m22, m.m32, m.m42,
545
- m.m13, m.m23, m.m33, m.m43,
546
- m.m14, m.m24, m.m34, m.m44];
547
- }
548
- return result;
549
- };
581
+ for (let j = 0; j < newSeg.length; j += 1) {
582
+ relativeSegment.push(newSeg[j]);
583
+ }
584
+ break;
585
+ case 'v':
586
+ relativeSegment[1] = +segment[1] - y;
587
+ break;
588
+ default:
589
+ // for is here to stay for eslint
590
+ for (let j = 1; j < segment.length; j += 1) {
591
+ relativeSegment.push(+segment[j] - (j % 2 ? x : y));
592
+ }
550
593
 
551
- /**
552
- * The Multiply method returns a new CSSMatrix which is the result of this
553
- * matrix multiplied by the passed matrix, with the passed matrix to the right.
554
- * This matrix is not modified.
555
- *
556
- * @param {CSSMatrix} m2 CSSMatrix
557
- * @return {CSSMatrix} The result matrix.
558
- */
559
- CSSMatrixProto.multiply = function multiply(m2) {
560
- return Multiply(this, m2);
561
- };
594
+ if (relativeCommand === 'm') {
595
+ mx = +segment[1];
596
+ my = +segment[2];
597
+ }
598
+ }
599
+ } else {
600
+ if (pathCommand === 'm') {
601
+ mx = +segment[1] + x;
602
+ my = +segment[2] + y;
603
+ }
604
+ for (let j = 0; j < segment.length; j += 1) {
605
+ relativeSegment.push(segment[j]);
606
+ }
607
+ }
608
+ // @ts-ignore
609
+ resultArray.push(relativeSegment);
610
+
611
+ const segLength = relativeSegment.length;
612
+ switch (relativeSegment[0]) {
613
+ case 'z':
614
+ x = mx;
615
+ y = my;
616
+ break;
617
+ case 'h':
618
+ x += +relativeSegment[segLength - 1];
619
+ break;
620
+ case 'v':
621
+ y += +relativeSegment[segLength - 1];
622
+ break;
623
+ default:
624
+ x += +resultArray[i][segLength - 2];
625
+ y += +resultArray[i][segLength - 1];
626
+ }
627
+ }
628
+
629
+ return resultArray;
630
+ }
562
631
 
563
632
  /**
633
+ * Rounds the values of a `pathArray` instance to
634
+ * a specified amount of decimals and returns it.
564
635
  *
565
- * These methods will be implemented later into an extended version to provide
566
- * additional functionality.
636
+ * @param {SVGPC.pathArray} path the source `pathArray`
637
+ * @param {null | number} round the amount of decimals to round numbers to
638
+ * @returns {SVGPC.pathArray} the resulted `pathArray` with rounded values
567
639
  */
568
- // inverse = function(){}
569
- // determinant = function(){}
570
- // transpose = function(){}
640
+ function roundPath(path, round) {
641
+ const { round: defaultRound, decimals: defaultDecimals } = SVGPCO;
642
+ const decimalsOption = round && !Number.isNaN(+round) ? +round
643
+ : defaultRound && defaultDecimals;
644
+
645
+ if (!decimalsOption) return clonePath(path);
646
+
647
+ // @ts-ignore
648
+ return path.map((seg) => seg.map((c) => {
649
+ const nr = +c;
650
+ const dc = 10 ** decimalsOption;
651
+ if (!Number.isNaN(nr)) {
652
+ return nr % 1 === 0 ? nr : Math.round(nr * dc) / dc;
653
+ }
654
+ return c;
655
+ }));
656
+ }
571
657
 
572
658
  /**
573
- * The translate method returns a new matrix which is this matrix post
574
- * multiplied by a translation matrix containing the passed values. If the z
575
- * component is undefined, a 0 value is used in its place. This matrix is not
576
- * modified.
659
+ * Returns a valid `d` attribute string value created
660
+ * by rounding values and concatenating the `pathArray` segments.
577
661
  *
578
- * @param {number} x X component of the translation value.
579
- * @param {number} y Y component of the translation value.
580
- * @param {number=} z Z component of the translation value.
581
- * @return {CSSMatrix} The result matrix
662
+ * @param {SVGPC.pathArray} path the `pathArray` object
663
+ * @param {number | null} round amount of decimals to round values to
664
+ * @returns {string} the concatenated path string
582
665
  */
583
-
584
- CSSMatrixProto.translate = function translate(x, y, z) {
585
- const X = x;
586
- let Y = y;
587
- let Z = z;
588
- if (Z == null) Z = 0;
589
- if (Y == null) Y = 0;
590
- return Multiply(this, Translate(X, Y, Z));
591
- };
666
+ function pathToString(path, round) {
667
+ return roundPath(path, round)
668
+ .map((x) => x[0].concat(x.slice(1).join(' '))).join('');
669
+ }
592
670
 
593
671
  /**
594
- * The scale method returns a new matrix which is this matrix post multiplied by
595
- * a scale matrix containing the passed values. If the z component is undefined,
596
- * a 1 value is used in its place. If the y component is undefined, the x
597
- * component value is used in its place. This matrix is not modified.
672
+ * Returns the missing control point from an
673
+ * T (shorthand quadratic bezier) segment.
598
674
  *
599
- * @param {number} x The X component of the scale value.
600
- * @param {number=} y The Y component of the scale value.
601
- * @param {number=} z The Z component of the scale value.
602
- * @return {CSSMatrix} The result matrix
675
+ * @param {Number} x1 curve start x
676
+ * @param {Number} y1 curve start y
677
+ * @param {Number} qx control point x
678
+ * @param {Number} qy control point y
679
+ * @param {String} prevCommand the previous path command
680
+ * @returns {Object} the missing control point
603
681
  */
604
- CSSMatrixProto.scale = function scale(x, y, z) {
605
- const X = x;
606
- let Y = y;
607
- let Z = z;
608
- if (Y == null) Y = x;
609
- if (Z == null) Z = x;
610
-
611
- return Multiply(this, Scale(X, Y, Z));
612
- };
682
+ function shorthandToQuad(x1, y1, qx, qy, prevCommand) {
683
+ return 'QT'.indexOf(prevCommand) > -1
684
+ ? { qx: x1 * 2 - qx, qy: y1 * 2 - qy }
685
+ : { qx: x1, qy: y1 };
686
+ }
613
687
 
614
688
  /**
615
- * The rotate method returns a new matrix which is this matrix post multiplied
616
- * by each of 3 rotation matrices about the major axes, first X, then Y, then Z.
617
- * If the y and z components are undefined, the x value is used to rotate the
618
- * object about the z axis, as though the vector (0,0,x) were passed. All
619
- * rotation values are in degrees. This matrix is not modified.
689
+ * Returns the missing control point from an
690
+ * S (shorthand cubic bezier) segment.
620
691
  *
621
- * @param {number} rx The X component of the rotation, or Z if Y and Z are null.
622
- * @param {number=} ry The (optional) Y component of the rotation value.
623
- * @param {number=} rz The (optional) Z component of the rotation value.
624
- * @return {CSSMatrix} The result matrix
692
+ * @param {Number} x1 curve start x
693
+ * @param {Number} y1 curve start y
694
+ * @param {Number} x2 curve end x
695
+ * @param {Number} y2 curve end y
696
+ * @param {String} prevCommand the previous path command
697
+ * @returns {Object} the missing control point
625
698
  */
626
- CSSMatrixProto.rotate = function rotate(rx, ry, rz) {
627
- let RX = rx;
628
- let RY = ry;
629
- let RZ = rz;
630
- if (RY == null) RY = 0;
631
- if (RZ == null) { RZ = RX; RX = 0; }
632
- return Multiply(this, Rotate(RX, RY, RZ));
633
- };
699
+ function shorthandToCubic(x1, y1, x2, y2, prevCommand) {
700
+ return 'CS'.indexOf(prevCommand) > -1
701
+ ? { x1: x1 * 2 - x2, y1: y1 * 2 - y2 }
702
+ : { x1, y1 };
703
+ }
634
704
 
635
705
  /**
636
- * The rotateAxisAngle method returns a new matrix which is this matrix post
637
- * multiplied by a rotation matrix with the given axis and `angle`. The right-hand
638
- * rule is used to determine the direction of rotation. All rotation values are
639
- * in degrees. This matrix is not modified.
706
+ * Normalizes a single segment of a `pathArray` object.
640
707
  *
641
- * @param {number} x The X component of the axis vector.
642
- * @param {number} y The Y component of the axis vector.
643
- * @param {number} z The Z component of the axis vector.
644
- * @param {number} angle The angle of rotation about the axis vector, in degrees.
645
- * @return {CSSMatrix} The `CSSMatrix` result
708
+ * @param {SVGPC.pathSegment} segment the segment object
709
+ * @param {Object} params the coordinates of the previous segment
710
+ * @param {String} prevCommand the path command of the previous segment
711
+ * @returns {SVGPC.pathSegment} the normalized segment
646
712
  */
713
+ function normalizeSegment(segment, params, prevCommand) {
714
+ const [pathCommand] = segment;
715
+ const xy = segment.slice(1);
716
+ let result = segment;
647
717
 
648
- CSSMatrixProto.rotateAxisAngle = function rotateAxisAngle(x, y, z, angle) {
649
- if (arguments.length !== 4) {
650
- throw new TypeError('CSSMatrix: expecting 4 values');
718
+ if ('TQ'.indexOf(segment[0]) < 0) {
719
+ // optional but good to be cautious
720
+ params.qx = null;
721
+ params.qy = null;
651
722
  }
652
- return Multiply(this, RotateAxisAngle(x, y, z, angle));
653
- };
723
+
724
+ if (pathCommand === 'H') {
725
+ result = ['L', segment[1], params.y1];
726
+ } else if (pathCommand === 'V') {
727
+ result = ['L', params.x1, segment[1]];
728
+ } else if (pathCommand === 'S') {
729
+ const { x1, y1 } = shorthandToCubic(params.x1, params.y1, params.x2, params.y2, prevCommand);
730
+ params.x1 = x1;
731
+ params.y1 = y1;
732
+ result = ['C', x1, y1].concat(xy);
733
+ } else if (pathCommand === 'T') {
734
+ const { qx, qy } = shorthandToQuad(params.x1, params.y1, params.qx, params.qy, prevCommand);
735
+ params.qx = qx;
736
+ params.qy = qy;
737
+ result = ['Q', qx, qy].concat(xy);
738
+ } else if (pathCommand === 'Q') {
739
+ const [nqx, nqy] = xy;
740
+ params.qx = nqx;
741
+ params.qy = nqy;
742
+ }
743
+ return result;
744
+ }
654
745
 
655
746
  /**
656
- * Specifies a skew transformation along the `x-axis` by the given angle.
657
- * This matrix is not modified.
747
+ * Iterates an array to check if it's a `pathArray`
748
+ * with all segments are in non-shorthand notation
749
+ * with absolute values.
658
750
  *
659
- * @param {number} angle The angle amount in degrees to skew.
660
- * @return {CSSMatrix} The `CSSMatrix` result
751
+ * @param {string | SVGPC.pathArray} path the `pathArray` to be checked
752
+ * @returns {boolean} iteration result
661
753
  */
662
-
663
- CSSMatrixProto.skewX = function skewX(angle) {
664
- return Multiply(this, SkewX(angle));
665
- };
754
+ function isNormalizedArray(path) {
755
+ return Array.isArray(path) && isPathArray(path) && path.every((seg) => {
756
+ const lk = seg[0].toLowerCase();
757
+ return paramsCount[lk] === seg.length - 1 && ('ACLMQZ').includes(seg[0]); // achlmqstvz
758
+ });
759
+ }
666
760
 
667
761
  /**
668
- * Specifies a skew transformation along the `y-axis` by the given angle.
669
- * This matrix is not modified.
762
+ * Normalizes a `path` object for further processing:
763
+ * * convert segments to absolute values
764
+ * * convert shorthand path commands to their non-shorthand notation
670
765
  *
671
- * @param {number} angle The angle amount in degrees to skew.
672
- * @return {CSSMatrix} The `CSSMatrix` result
766
+ * @param {string | SVGPC.pathArray} pathInput the string to be parsed or 'pathArray'
767
+ * @returns {SVGPC.pathArray} the normalized `pathArray`
673
768
  */
769
+ function normalizePath(pathInput) { // path|pathString
770
+ if (Array.isArray(pathInput) && isNormalizedArray(pathInput)) {
771
+ return clonePath(pathInput);
772
+ }
674
773
 
675
- CSSMatrixProto.skewY = function skewY(angle) {
676
- return Multiply(this, SkewY(angle));
677
- };
774
+ const path = pathToAbsolute(pathInput);
775
+ const params = {
776
+ x1: 0, y1: 0, x2: 0, y2: 0, x: 0, y: 0, qx: null, qy: null,
777
+ };
778
+ const allPathCommands = [];
779
+ const ii = path.length;
780
+ let prevCommand = '';
781
+ let segment;
782
+ let seglen;
678
783
 
679
- /**
680
- * Set the current `CSSMatrix` instance to the identity form and returns it.
681
- *
682
- * @return {CSSMatrix} this `CSSMatrix` instance
683
- */
684
- CSSMatrixProto.setIdentity = function setIdentity() {
685
- const identity = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
686
- return feedFromArray(this, identity);
687
- };
784
+ for (let i = 0; i < ii; i += 1) {
785
+ // save current path command
786
+ const [pathCommand] = path[i];
688
787
 
689
- /**
690
- * Transforms the specified point using the matrix, returning a new
691
- * *Object* containing the transformed point.
692
- * Neither the matrix nor the original point are altered.
693
- *
694
- * The method is equivalent with `transformPoint()` method
695
- * of the `DOMMatrix` constructor.
696
- *
697
- * JavaScript implementation by thednp
698
- *
699
- * @param {Point} point the *Object* with `x`, `y`, `z` and `w` components
700
- * @return {Point} a new `{x,y,z,w}` *Object*
701
- */
702
- CSSMatrixProto.transformPoint = function transformPoint(v) {
703
- const M = this;
704
- let m = Translate(v.x, v.y, v.z);
788
+ // Save current path command
789
+ allPathCommands[i] = pathCommand;
790
+ // Get previous path command
791
+ if (i) prevCommand = allPathCommands[i - 1];
792
+ // Previous path command is inputted to processSegment
793
+ path[i] = normalizeSegment(path[i], params, prevCommand);
705
794
 
706
- m.m44 = v.w || 1;
707
- m = M.multiply(m);
795
+ segment = path[i];
796
+ seglen = segment.length;
708
797
 
709
- return {
710
- x: m.m41,
711
- y: m.m42,
712
- z: m.m43,
713
- w: m.m44,
714
- };
715
- };
798
+ params.x1 = +segment[seglen - 2];
799
+ params.y1 = +segment[seglen - 1];
800
+ params.x2 = +(segment[seglen - 4]) || params.x1;
801
+ params.y2 = +(segment[seglen - 3]) || params.y1;
802
+ }
803
+ return path;
804
+ }
716
805
 
717
806
  /**
718
- * Transforms the specified vector using the matrix, returning a new
719
- * {x,y,z,w} *Object* comprising the transformed vector.
720
- * Neither the matrix nor the original vector are altered.
807
+ * Reverses all segments and their values of a `pathArray`
808
+ * and returns a new instance.
721
809
  *
722
- * @param {Tuple} tupple an object with x, y, z and w components
723
- * @return {Tuple} the passed tuple
810
+ * @param {SVGPC.pathArray} pathInput the source `pathArray`
811
+ * @returns {SVGPC.pathArray} the reversed `pathArray`
724
812
  */
725
- CSSMatrixProto.transform = function transform(t) {
726
- const m = this;
727
- const x = m.m11 * t.x + m.m12 * t.y + m.m13 * t.z + m.m14 * t.w;
728
- const y = m.m21 * t.x + m.m22 * t.y + m.m23 * t.z + m.m24 * t.w;
729
- const z = m.m31 * t.x + m.m32 * t.y + m.m33 * t.z + m.m34 * t.w;
730
- const w = m.m41 * t.x + m.m42 * t.y + m.m43 * t.z + m.m44 * t.w;
731
-
732
- return {
733
- x: x / w,
734
- y: y / w,
735
- z: z / w,
736
- w,
737
- };
738
- };
739
-
740
- // Add Transform Functions to CSSMatrix object
741
- CSSMatrix.Translate = Translate;
742
- CSSMatrix.Rotate = Rotate;
743
- CSSMatrix.RotateAxisAngle = RotateAxisAngle;
744
- CSSMatrix.Scale = Scale;
745
- CSSMatrix.SkewX = SkewX;
746
- CSSMatrix.SkewY = SkewY;
747
- CSSMatrix.Multiply = Multiply;
748
- CSSMatrix.fromMatrix = fromMatrix;
749
- CSSMatrix.fromArray = fromArray;
750
- CSSMatrix.feedFromArray = feedFromArray;
813
+ function reversePath(pathInput) {
814
+ const absolutePath = pathToAbsolute(pathInput);
815
+ const isClosed = absolutePath.slice(-1)[0][0] === 'Z';
816
+ let reversedPath = [];
817
+ let segLength = 0;
751
818
 
752
- const CSS3Matrix = typeof DOMMatrix !== 'undefined' ? DOMMatrix : CSSMatrix;
819
+ reversedPath = normalizePath(absolutePath).map((segment, i) => {
820
+ segLength = segment.length;
821
+ return {
822
+ seg: absolutePath[i], // absolute
823
+ n: segment, // normalized
824
+ c: absolutePath[i][0], // pathCommand
825
+ x: segment[segLength - 2], // x
826
+ y: segment[segLength - 1], // y
827
+ };
828
+ }).map((seg, i, path) => {
829
+ const segment = seg.seg;
830
+ const data = seg.n;
831
+ const prevSeg = i && path[i - 1];
832
+ const nextSeg = path[i + 1] && path[i + 1];
833
+ const pathCommand = seg.c;
834
+ const pLen = path.length;
835
+ const x = i ? path[i - 1].x : path[pLen - 1].x;
836
+ const y = i ? path[i - 1].y : path[pLen - 1].y;
837
+ let result = [];
753
838
 
754
- function clonePath(pathArray) {
755
- return pathArray.map((x) => {
756
- if (Array.isArray(x)) {
757
- return clonePath(x);
839
+ switch (pathCommand) {
840
+ case 'M':
841
+ result = isClosed ? ['Z'] : [pathCommand, x, y];
842
+ break;
843
+ case 'A':
844
+ result = segment.slice(0, -3).concat([(segment[5] === 1 ? 0 : 1), x, y]);
845
+ break;
846
+ case 'C':
847
+ if (nextSeg && nextSeg.c === 'S') {
848
+ result = ['S', segment[1], segment[2], x, y];
849
+ } else {
850
+ result = [pathCommand, segment[3], segment[4], segment[1], segment[2], x, y];
851
+ }
852
+ break;
853
+ case 'S':
854
+ if ((prevSeg && 'CS'.indexOf(prevSeg.c) > -1) && (!nextSeg || (nextSeg && nextSeg.c !== 'S'))) {
855
+ result = ['C', data[3], data[4], data[1], data[2], x, y];
856
+ } else {
857
+ result = [pathCommand, data[1], data[2], x, y];
858
+ }
859
+ break;
860
+ case 'Q':
861
+ if (nextSeg && nextSeg.c === 'T') {
862
+ result = ['T', x, y];
863
+ } else {
864
+ result = segment.slice(0, -2).concat([x, y]);
865
+ }
866
+ break;
867
+ case 'T':
868
+ if ((prevSeg && 'QT'.indexOf(prevSeg.c) > -1) && (!nextSeg || (nextSeg && nextSeg.c !== 'T'))) {
869
+ result = ['Q', data[1], data[2], x, y];
870
+ } else {
871
+ result = [pathCommand, x, y];
872
+ }
873
+ break;
874
+ case 'Z':
875
+ result = ['M', x, y];
876
+ break;
877
+ case 'H':
878
+ result = [pathCommand, x];
879
+ break;
880
+ case 'V':
881
+ result = [pathCommand, y];
882
+ break;
883
+ default:
884
+ result = segment.slice(0, -2).concat([x, y]);
758
885
  }
759
- return !Number.isNaN(+x) ? +x : x;
886
+
887
+ return result;
760
888
  });
889
+ // @ts-ignore
890
+ return isClosed ? reversedPath.reverse()
891
+ : [reversedPath[0]].concat(reversedPath.slice(1).reverse());
761
892
  }
762
893
 
763
- function roundPath(pathArray, round) {
764
- const decimalsOption = !Number.isNaN(+round) ? +round : SVGPCO.round && SVGPCO.decimals;
765
- let result;
766
-
767
- if (decimalsOption) {
768
- result = pathArray.map((seg) => seg.map((c) => {
769
- const nr = +c;
770
- const dc = 10 ** decimalsOption;
771
- if (nr) {
772
- return nr % 1 === 0 ? nr : Math.round(nr * dc) / dc;
773
- }
774
- return c;
775
- }));
776
- } else {
777
- result = clonePath(pathArray);
778
- }
779
- return result;
894
+ /**
895
+ * Split a path into an `Array` of sub-path strings.
896
+ *
897
+ * In the process, values are converted to absolute
898
+ * for visual consistency.
899
+ *
900
+ * @param {SVGPC.pathArray | string} pathInput the cubic-bezier parameters
901
+ * @return {string[]} an array with all sub-path strings
902
+ */
903
+ function splitPath(pathInput) {
904
+ return pathToString(pathToAbsolute(pathInput), 0)
905
+ .replace(/(m|M)/g, '|$1')
906
+ .split('|')
907
+ .map((s) => s.trim())
908
+ .filter((s) => s);
780
909
  }
781
910
 
782
- function fixArc(pathArray, allPathCommands, i) {
783
- if (pathArray[i].length > 7) {
784
- pathArray[i].shift();
785
- const pi = pathArray[i];
786
- // const ni = i + 1;
787
- let ni = i;
788
- while (pi.length) {
789
- // if created multiple C:s, their original seg is saved
790
- allPathCommands[i] = 'A';
791
- pathArray.splice(ni += 1, 0, ['C'].concat(pi.splice(0, 6)));
792
- // pathArray.splice(i += 1, 0, ['C'].concat(pi.splice(0, 6)));
793
- // pathArray.splice(i++, 0, ['C'].concat(pi.splice(0, 6)));
911
+ /**
912
+ * Optimizes a `pathArray` object:
913
+ * * convert segments to absolute and relative values
914
+ * * create a new `pathArray` with elements with shortest segments
915
+ * from absolute and relative `pathArray`s
916
+ *
917
+ * @param {string | SVGPC.pathArray} pathInput a string or `pathArray`
918
+ * @param {number | null} round the amount of decimals to round values to
919
+ * @returns {SVGPC.pathArray} the optimized `pathArray`
920
+ */
921
+ function optimizePath(pathInput, round) {
922
+ const absolutePath = roundPath(pathToAbsolute(pathInput), round);
923
+ const relativePath = roundPath(pathToRelative(pathInput), round);
924
+ return absolutePath.map((x, i) => {
925
+ if (i) {
926
+ return x.join('').length < relativePath[i].join('').length ? x : relativePath[i];
794
927
  }
795
- pathArray.splice(i, 1);
796
- }
797
- }
798
-
799
- var paramsCount = {
800
- a: 7, c: 6, h: 1, l: 2, m: 2, r: 4, q: 4, s: 4, t: 2, v: 1, z: 0,
801
- };
802
-
803
- function isPathArray(pathArray) {
804
- return Array.isArray(pathArray) && pathArray.every((seg) => {
805
- const pathCommand = seg[0].toLowerCase();
806
- return paramsCount[pathCommand] === seg.length - 1 && /[achlmrqstvz]/g.test(pathCommand);
928
+ return x;
807
929
  });
808
930
  }
809
931
 
810
- function isCurveArray(pathArray) {
811
- return isPathArray(pathArray) && pathArray.slice(1).every((seg) => seg[0] === 'C');
932
+ /**
933
+ * A global namespace for epsilon.
934
+ *
935
+ * @type {Number}
936
+ */
937
+ const epsilon = 1e-9;
938
+
939
+ /**
940
+ * Returns an {x,y} vector rotated by a given
941
+ * angle in radian.
942
+ *
943
+ * @param {Number} x the initial vector x
944
+ * @param {Number} y the initial vector y
945
+ * @returns {{x: number, y: number}} the rotated vector
946
+ */
947
+ function rotateVector(x, y, rad) {
948
+ const X = x * Math.cos(rad) - y * Math.sin(rad);
949
+ const Y = x * Math.sin(rad) + y * Math.cos(rad);
950
+ return { x: X, y: Y };
812
951
  }
813
952
 
814
- function finalizeSegment(state) {
815
- let pathCommand = state.pathValue[state.segmentStart];
816
- let pathComLK = pathCommand.toLowerCase();
817
- let params = state.data;
953
+ /**
954
+ * Converts A (arc-to) segments to C (cubic-bezier-to).
955
+ *
956
+ * For more information of where this math came from visit:
957
+ * http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
958
+ *
959
+ * @param {number} x1 the starting x position
960
+ * @param {number} y1 the starting y position
961
+ * @param {number} rx x-radius of the arc
962
+ * @param {number} ry y-radius of the arc
963
+ * @param {number} angle x-axis-rotation of the arc
964
+ * @param {number} LAF large-arc-flag of the arc
965
+ * @param {number} SF sweep-flag of the arc
966
+ * @param {number} x2 the ending x position
967
+ * @param {number} y2 the ending y position
968
+ * @param {number[] | null} recursive the parameters needed to split arc into 2 segments
969
+ * @return {number[]} the resulting cubic-bezier segment(s)
970
+ */
971
+ function arcToCubic(x1, y1, rx, ry, angle, LAF, SF, x2, y2, recursive) {
972
+ const d120 = (Math.PI * 120) / 180;
973
+ const rad = (Math.PI / 180) * (angle || 0);
974
+ /** @type {number[]} */
975
+ let res = [];
976
+ let X1 = x1;
977
+ let X2 = x2;
978
+ let Y1 = y1;
979
+ let Y2 = y2;
980
+ let RX = rx;
981
+ let RY = ry;
982
+ let xy;
983
+ let f1;
984
+ let f2;
985
+ let cx;
986
+ let cy;
818
987
 
819
- // Process duplicated commands (without comand name)
820
- if (pathComLK === 'm' && params.length > 2) {
821
- state.segments.push([pathCommand, params[0], params[1]]);
822
- params = params.slice(2);
823
- pathComLK = 'l';
824
- pathCommand = (pathCommand === 'm') ? 'l' : 'L';
825
- }
988
+ if (!recursive) {
989
+ xy = rotateVector(X1, Y1, -rad);
990
+ X1 = xy.x;
991
+ Y1 = xy.y;
992
+ xy = rotateVector(X2, Y2, -rad);
993
+ X2 = xy.x;
994
+ Y2 = xy.y;
826
995
 
827
- if (pathComLK === 'r') {
828
- state.segments.push([pathCommand].concat(params));
829
- } else {
830
- while (params.length >= paramsCount[pathComLK]) {
831
- state.segments.push([pathCommand].concat(params.splice(0, paramsCount[pathComLK])));
832
- if (!paramsCount[pathComLK]) {
833
- break;
834
- }
996
+ const x = (X1 - X2) / 2;
997
+ const y = (Y1 - Y2) / 2;
998
+ let h = (x ** 2) / (RX ** 2) + (y ** 2) / (RY ** 2);
999
+ if (h > 1) {
1000
+ h = Math.sqrt(h);
1001
+ RX *= h;
1002
+ RY *= h;
835
1003
  }
836
- }
837
- }
1004
+ const rx2 = RX ** 2;
1005
+ const ry2 = RY ** 2;
1006
+ const k = (LAF === SF ? -1 : 1)
1007
+ * Math.sqrt(Math.abs((rx2 * ry2 - rx2 * y * y - ry2 * x * x)
1008
+ / (rx2 * y * y + ry2 * x * x)));
1009
+
1010
+ cx = ((k * RX * y) / RY) + ((X1 + X2) / 2);
1011
+ cy = ((k * -RY * x) / RX) + ((Y1 + Y2) / 2);
838
1012
 
839
- var invalidPathValue = 'Invalid path value';
1013
+ // eslint-disable-next-line no-bitwise -- Impossible to satisfy no-bitwise
1014
+ f1 = Math.asin((((Y1 - cy) / RY) * 10 ** 9 >> 0) / (10 ** 9));
1015
+ // eslint-disable-next-line no-bitwise -- Impossible to satisfy no-bitwise
1016
+ f2 = Math.asin((((Y2 - cy) / RY) * 10 ** 9 >> 0) / (10 ** 9));
840
1017
 
841
- function scanFlag(state) {
842
- const ch = state.pathValue.charCodeAt(state.index);
1018
+ f1 = X1 < cx ? Math.PI - f1 : f1;
1019
+ f2 = X2 < cx ? Math.PI - f2 : f2;
843
1020
 
844
- if (ch === 0x30/* 0 */) {
845
- state.param = 0;
846
- state.index += 1;
847
- return;
848
- }
1021
+ if (f1 < 0) { f1 = Math.PI * 2 + f1; }
1022
+ if (f2 < 0) { f2 = Math.PI * 2 + f2; }
849
1023
 
850
- if (ch === 0x31/* 1 */) {
851
- state.param = 1;
852
- state.index += 1;
853
- return;
1024
+ if (SF && f1 > f2) {
1025
+ f1 -= Math.PI * 2;
1026
+ }
1027
+ if (!SF && f2 > f1) {
1028
+ f2 -= Math.PI * 2;
1029
+ }
1030
+ } else {
1031
+ const [r1, r2, r3, r4] = recursive;
1032
+ f1 = r1;
1033
+ f2 = r2;
1034
+ cx = r3;
1035
+ cy = r4;
854
1036
  }
855
1037
 
856
- // state.err = 'SvgPath: arc flag can be 0 or 1 only (at pos ' + state.index + ')';
857
- state.err = `${invalidPathValue}: invalid Arc flag ${ch}`;
858
- }
859
-
860
- function isDigit(code) {
861
- return (code >= 48 && code <= 57); // 0..9
862
- }
1038
+ let df = f2 - f1;
863
1039
 
864
- function scanParam(state) {
865
- const start = state.index;
866
- const { max } = state;
867
- let index = start;
868
- let zeroFirst = false;
869
- let hasCeiling = false;
870
- let hasDecimal = false;
871
- let hasDot = false;
872
- let ch;
1040
+ if (Math.abs(df) > d120) {
1041
+ const f2old = f2;
1042
+ const x2old = X2;
1043
+ const y2old = Y2;
873
1044
 
874
- if (index >= max) {
875
- // state.err = 'SvgPath: missed param (at pos ' + index + ')';
876
- state.err = `${invalidPathValue}: missing param ${state.pathValue[index]}`;
877
- return;
1045
+ f2 = f1 + d120 * (SF && f2 > f1 ? 1 : -1);
1046
+ X2 = cx + RX * Math.cos(f2);
1047
+ Y2 = cy + RY * Math.sin(f2);
1048
+ res = arcToCubic(X2, Y2, RX, RY, angle, 0, SF, x2old, y2old, [f2, f2old, cx, cy]);
878
1049
  }
879
- ch = state.pathValue.charCodeAt(index);
880
1050
 
881
- if (ch === 0x2B/* + */ || ch === 0x2D/* - */) {
882
- index += 1;
883
- ch = (index < max) ? state.pathValue.charCodeAt(index) : 0;
884
- }
1051
+ df = f2 - f1;
1052
+ const c1 = Math.cos(f1);
1053
+ const s1 = Math.sin(f1);
1054
+ const c2 = Math.cos(f2);
1055
+ const s2 = Math.sin(f2);
1056
+ const t = Math.tan(df / 4);
1057
+ const hx = (4 / 3) * RX * t;
1058
+ const hy = (4 / 3) * RY * t;
1059
+ const m1 = [X1, Y1];
1060
+ const m2 = [X1 + hx * s1, Y1 - hy * c1];
1061
+ const m3 = [X2 + hx * s2, Y2 - hy * c2];
1062
+ const m4 = [X2, Y2];
1063
+ m2[0] = 2 * m1[0] - m2[0];
1064
+ m2[1] = 2 * m1[1] - m2[1];
885
1065
 
886
- // This logic is shamelessly borrowed from Esprima
887
- // https://github.com/ariya/esprimas
888
- if (!isDigit(ch) && ch !== 0x2E/* . */) {
889
- // state.err = 'SvgPath: param should start with 0..9 or `.` (at pos ' + index + ')';
890
- state.err = `${invalidPathValue} at index ${index}: ${state.pathValue[index]} is not a number`;
891
- return;
1066
+ if (recursive) {
1067
+ return [m2, m3, m4].concat(res).map((n) => +n);
892
1068
  }
1069
+ res = [m2, m3, m4].concat(res).join().split(',').map((n) => +n);
1070
+ return res.map((rz, i) => {
1071
+ if (i % 2) {
1072
+ return rotateVector(res[i - 1], rz, rad).y;
1073
+ }
1074
+ return rotateVector(rz, res[i + 1], rad).x;
1075
+ });
1076
+ }
893
1077
 
894
- if (ch !== 0x2E/* . */) {
895
- zeroFirst = (ch === 0x30/* 0 */);
896
- index += 1;
1078
+ /**
1079
+ * Converts a Q (quadratic-bezier) segment to C (cubic-bezier).
1080
+ *
1081
+ * @param {Number} x1 curve start x
1082
+ * @param {Number} y1 curve start y
1083
+ * @param {Number} qx control point x
1084
+ * @param {Number} qy control point y
1085
+ * @param {Number} x2 curve end x
1086
+ * @param {Number} y2 curve end y
1087
+ * @returns {Number[]} the cubic-bezier segment
1088
+ */
1089
+ function quadToCubic(x1, y1, qx, qy, x2, y2) {
1090
+ const r13 = 1 / 3;
1091
+ const r23 = 2 / 3;
1092
+ return [
1093
+ r13 * x1 + r23 * qx, // cpx1
1094
+ r13 * y1 + r23 * qy, // cpy1
1095
+ r13 * x2 + r23 * qx, // cpx2
1096
+ r13 * y2 + r23 * qy, // cpy2
1097
+ x2, y2, // x,y
1098
+ ];
1099
+ }
897
1100
 
898
- ch = (index < max) ? state.pathValue.charCodeAt(index) : 0;
1101
+ /**
1102
+ * Returns the {x,y} coordinates of a point at a
1103
+ * given length of a cubic-bezier segment.
1104
+ *
1105
+ * @param {Number} p1x the starting point X
1106
+ * @param {Number} p1y the starting point Y
1107
+ * @param {Number} c1x the first control point X
1108
+ * @param {Number} c1y the first control point Y
1109
+ * @param {Number} c2x the second control point X
1110
+ * @param {Number} c2y the second control point Y
1111
+ * @param {Number} px2 the ending point X
1112
+ * @param {Number} py2 the ending point Y
1113
+ * @param {Number} t a [0-1] ratio
1114
+ * @returns {{x: number, y: number}} the requested {x,y} coordinates
1115
+ */
1116
+ function getPointAtSegLength(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t) {
1117
+ const t1 = 1 - t;
1118
+ return {
1119
+ x: (t1 ** 3) * p1x
1120
+ + t1 * t1 * 3 * t * c1x
1121
+ + t1 * 3 * t * t * c2x
1122
+ + (t ** 3) * p2x,
1123
+ y: (t1 ** 3) * p1y
1124
+ + t1 * t1 * 3 * t * c1y
1125
+ + t1 * 3 * t * t * c2y
1126
+ + (t ** 3) * p2y,
1127
+ };
1128
+ }
899
1129
 
900
- if (zeroFirst && index < max) {
901
- // decimal number starts with '0' such as '09' is illegal.
902
- if (ch && isDigit(ch)) {
903
- // state.err = 'SvgPath: numbers started with `0` such as `09`
904
- // are illegal (at pos ' + start + ')';
905
- state.err = `${invalidPathValue}: ${state.pathValue[start]} illegal number`;
906
- return;
907
- }
908
- }
1130
+ /**
1131
+ * Returns the coordinates of a specified distance
1132
+ * ratio between two points.
1133
+ *
1134
+ * @param {Number[]} a the first point coordinates
1135
+ * @param {Number[]} b the second point coordinates
1136
+ * @param {Number} t the ratio
1137
+ * @returns {Number[]} the midpoint coordinates
1138
+ */
1139
+ function midPoint(a, b, t) {
1140
+ const [ax, ay] = a; const [bx, by] = b;
1141
+ return [ax + (bx - ax) * t, ay + (by - ay) * t];
1142
+ }
909
1143
 
910
- while (index < max && isDigit(state.pathValue.charCodeAt(index))) {
911
- index += 1;
912
- hasCeiling = true;
913
- }
914
- ch = (index < max) ? state.pathValue.charCodeAt(index) : 0;
915
- }
1144
+ /**
1145
+ * Converts an L (line-to) segment to C (cubic-bezier).
1146
+ *
1147
+ * @param {Number} x1 line start x
1148
+ * @param {Number} y1 line start y
1149
+ * @param {Number} x2 line end x
1150
+ * @param {Number} y2 line end y
1151
+ * @returns {Number[]} the cubic-bezier segment
1152
+ */
1153
+ function lineToCubic(x1, y1, x2, y2) {
1154
+ const t = 0.5;
1155
+ const p0 = [x1, y1];
1156
+ const p1 = [x2, y2];
1157
+ const p2 = midPoint(p0, p1, t);
1158
+ const p3 = midPoint(p1, p2, t);
1159
+ const p4 = midPoint(p2, p3, t);
1160
+ const p5 = midPoint(p3, p4, t);
1161
+ const p6 = midPoint(p4, p5, t);
1162
+ const cp1 = getPointAtSegLength.apply(0, p0.concat(p2, p4, p6, t));
1163
+ const cp2 = getPointAtSegLength.apply(0, p6.concat(p5, p3, p1, 0));
916
1164
 
917
- if (ch === 0x2E/* . */) {
918
- hasDot = true;
919
- index += 1;
920
- while (isDigit(state.pathValue.charCodeAt(index))) {
921
- index += 1;
922
- hasDecimal = true;
923
- }
924
- ch = (index < max) ? state.pathValue.charCodeAt(index) : 0;
925
- }
1165
+ return [cp1.x, cp1.y, cp2.x, cp2.y, x2, y2];
1166
+ }
926
1167
 
927
- if (ch === 0x65/* e */ || ch === 0x45/* E */) {
928
- if (hasDot && !hasCeiling && !hasDecimal) {
929
- // state.err = 'SvgPath: invalid float exponent (at pos ' + index + ')';
930
- state.err = `${invalidPathValue}: ${state.pathValue[index]} invalid float exponent`;
931
- return;
932
- }
1168
+ /**
1169
+ * Converts any segment to C (cubic-bezier).
1170
+ *
1171
+ * @param {SVGPC.pathSegment} segment the source segment
1172
+ * @param {SVGPC.parserParams} params the source segment parameters
1173
+ * @returns {SVGPC.pathSegment} the cubic-bezier segment
1174
+ */
1175
+ function segmentToCubic(segment, params) {
1176
+ if ('TQ'.indexOf(segment[0]) < 0) {
1177
+ params.qx = null;
1178
+ params.qy = null;
1179
+ }
933
1180
 
934
- index += 1;
1181
+ const [s1, s2] = segment.slice(1);
935
1182
 
936
- ch = (index < max) ? state.pathValue.charCodeAt(index) : 0;
937
- if (ch === 0x2B/* + */ || ch === 0x2D/* - */) {
938
- index += 1;
939
- }
940
- if (index < max && isDigit(state.pathValue.charCodeAt(index))) {
941
- while (index < max && isDigit(state.pathValue.charCodeAt(index))) {
942
- index += 1;
943
- }
944
- } else {
945
- // state.err = 'SvgPath: invalid float exponent (at pos ' + index + ')';
946
- state.err = `${invalidPathValue}: ${state.pathValue[index]} invalid float exponent`;
947
- return;
948
- }
1183
+ switch (segment[0]) {
1184
+ case 'M':
1185
+ params.x = +s1;
1186
+ params.y = +s2;
1187
+ return segment;
1188
+ case 'A':
1189
+ // @ts-ignore
1190
+ return ['C'].concat(arcToCubic.apply(0, [params.x1, params.y1].concat(segment.slice(1))));
1191
+ case 'Q':
1192
+ params.qx = +s1;
1193
+ params.qy = +s2;
1194
+ // @ts-ignore
1195
+ return ['C'].concat(quadToCubic.apply(0, [params.x1, params.y1].concat(segment.slice(1))));
1196
+ case 'L':
1197
+ // @ts-ignore
1198
+ return ['C'].concat(lineToCubic(params.x1, params.y1, segment[1], segment[2]));
1199
+ case 'Z':
1200
+ // @ts-ignore
1201
+ return ['C'].concat(lineToCubic(params.x1, params.y1, params.x, params.y));
949
1202
  }
950
-
951
- state.index = index;
952
- state.param = +state.pathValue.slice(start, index);
1203
+ return segment;
953
1204
  }
954
1205
 
955
- function isSpace(ch) {
956
- const specialSpaces = [
957
- 0x1680, 0x180E, 0x2000, 0x2001, 0x2002, 0x2003, 0x2004, 0x2005, 0x2006,
958
- 0x2007, 0x2008, 0x2009, 0x200A, 0x202F, 0x205F, 0x3000, 0xFEFF];
959
- return (ch === 0x0A) || (ch === 0x0D) || (ch === 0x2028) || (ch === 0x2029) // Line terminators
960
- // White spaces
961
- || (ch === 0x20) || (ch === 0x09) || (ch === 0x0B) || (ch === 0x0C) || (ch === 0xA0)
962
- || (ch >= 0x1680 && specialSpaces.indexOf(ch) >= 0);
963
- }
1206
+ /**
1207
+ * Splits an extended A (arc-to) segment into two cubic-bezier segments.
1208
+ *
1209
+ * @param {SVGPC.pathArray} path the `pathArray` this segment belongs to
1210
+ * @param {string[]} allPathCommands all previous path commands
1211
+ * @param {Number} i the index of the segment
1212
+ */
964
1213
 
965
- function skipSpaces(state) {
966
- while (state.index < state.max && isSpace(state.pathValue.charCodeAt(state.index))) {
967
- state.index += 1;
1214
+ function fixArc(path, allPathCommands, i) {
1215
+ if (path[i].length > 7) {
1216
+ path[i].shift();
1217
+ const segment = path[i];
1218
+ let ni = i; // ESLint
1219
+ while (segment.length) {
1220
+ // if created multiple C:s, their original seg is saved
1221
+ allPathCommands[i] = 'A';
1222
+ path.splice(ni += 1, 0, ['C'].concat(segment.splice(0, 6)));
1223
+ }
1224
+ path.splice(i, 1);
968
1225
  }
969
1226
  }
970
1227
 
971
- function isPathCommand(code) {
972
- // eslint-disable no-bitwise
973
- switch (code | 0x20) {
974
- case 0x6D/* m */:
975
- case 0x7A/* z */:
976
- case 0x6C/* l */:
977
- case 0x68/* h */:
978
- case 0x76/* v */:
979
- case 0x63/* c */:
980
- case 0x73/* s */:
981
- case 0x71/* q */:
982
- case 0x74/* t */:
983
- case 0x61/* a */:
984
- case 0x72/* r */:
985
- return true;
986
- default:
987
- return false;
988
- }
989
- }
1228
+ var version$1 = "0.0.16alpha4";
990
1229
 
991
- function isDigitStart(code) {
992
- return (code >= 48 && code <= 57) /* 0..9 */
993
- || code === 0x2B /* + */
994
- || code === 0x2D /* - */
995
- || code === 0x2E; /* . */
996
- }
997
-
998
- function isArcCommand(code) {
999
- // eslint disable no-bitwise
1000
- return (code | 0x20) === 0x61;
1001
- }
1230
+ // @ts-ignore
1002
1231
 
1003
- function scanSegment(state) {
1004
- const { max } = state;
1005
- const cmdCode = state.pathValue.charCodeAt(state.index);
1006
- const reqParams = paramsCount[state.pathValue[state.index].toLowerCase()];
1007
- // let hasComma;
1232
+ /**
1233
+ * A global namespace for library version.
1234
+ * @type {string}
1235
+ */
1236
+ const DMVersion = version$1;
1008
1237
 
1009
- state.segmentStart = state.index;
1238
+ // DOMMatrix Static methods
1239
+ // * `fromFloat64Array` and `fromFloat32Array` methods are not supported;
1240
+ // * `fromArray` a more simple implementation, should also accept float[32/64]Array;
1241
+ // * `fromMatrix` load values from another CSSMatrix/DOMMatrix instance;
1242
+ // * `fromString` parses and loads values from any valid CSS transform string.
1010
1243
 
1011
- if (!isPathCommand(cmdCode)) {
1012
- // state.err = 'SvgPath: bad command '
1013
- // + state.pathValue[state.index]
1014
- // + ' (at pos ' + state.index + ')';
1015
- state.err = `${invalidPathValue}: ${state.pathValue[state.index]} not a path command`;
1016
- return;
1244
+ /**
1245
+ * Creates a new mutable `CSSMatrix` object given an array of floating point values.
1246
+ *
1247
+ * This static method invalidates arrays that contain non-number elements.
1248
+ *
1249
+ * If the array has six values, the result is a 2D matrix; if the array has 16 values,
1250
+ * the result is a 3D matrix. Otherwise, a TypeError exception is thrown.
1251
+ *
1252
+ * @param {number[]} array an `Array` to feed values from.
1253
+ * @return {CSSMatrix} the resulted matrix.
1254
+ */
1255
+ function fromArray(array) {
1256
+ if (!array.every((n) => !Number.isNaN(n))) {
1257
+ throw TypeError(`CSSMatrix: "${array}" must only have numbers.`);
1017
1258
  }
1259
+ const m = new CSSMatrix();
1260
+ const a = Array.from(array);
1018
1261
 
1019
- state.index += 1;
1020
- skipSpaces(state);
1262
+ if (a.length === 16) {
1263
+ const [m11, m12, m13, m14,
1264
+ m21, m22, m23, m24,
1265
+ m31, m32, m33, m34,
1266
+ m41, m42, m43, m44] = a;
1021
1267
 
1022
- state.data = [];
1268
+ m.m11 = m11;
1269
+ m.a = m11;
1023
1270
 
1024
- if (!reqParams) {
1025
- // Z
1026
- finalizeSegment(state);
1027
- return;
1028
- }
1271
+ m.m21 = m21;
1272
+ m.c = m21;
1029
1273
 
1030
- // hasComma = false;
1274
+ m.m31 = m31;
1031
1275
 
1032
- for (;;) {
1033
- for (let i = reqParams; i > 0; i -= 1) {
1034
- if (isArcCommand(cmdCode) && (i === 3 || i === 4)) scanFlag(state);
1035
- else scanParam(state);
1276
+ m.m41 = m41;
1277
+ m.e = m41;
1036
1278
 
1037
- if (state.err.length) {
1038
- return;
1039
- }
1040
- state.data.push(state.param);
1279
+ m.m12 = m12;
1280
+ m.b = m12;
1041
1281
 
1042
- skipSpaces(state);
1043
- // hasComma = false;
1282
+ m.m22 = m22;
1283
+ m.d = m22;
1044
1284
 
1045
- if (state.index < max && state.pathValue.charCodeAt(state.index) === 0x2C/* , */) {
1046
- state.index += 1;
1047
- skipSpaces(state);
1048
- // hasComma = true;
1049
- }
1050
- }
1285
+ m.m32 = m32;
1286
+
1287
+ m.m42 = m42;
1288
+ m.f = m42;
1051
1289
 
1052
- // after ',' param is mandatory
1053
- // if (hasComma) {
1054
- // continue;
1055
- // }
1290
+ m.m13 = m13;
1291
+ m.m23 = m23;
1292
+ m.m33 = m33;
1293
+ m.m43 = m43;
1294
+ m.m14 = m14;
1295
+ m.m24 = m24;
1296
+ m.m34 = m34;
1297
+ m.m44 = m44;
1298
+ } else if (a.length === 6) {
1299
+ const [m11, m12, m21, m22, m41, m42] = a;
1056
1300
 
1057
- if (state.index >= state.max) {
1058
- break;
1059
- }
1301
+ m.m11 = m11;
1302
+ m.a = m11;
1060
1303
 
1061
- // Stop on next segment
1062
- if (!isDigitStart(state.pathValue.charCodeAt(state.index))) {
1063
- break;
1064
- }
1065
- }
1304
+ m.m12 = m12;
1305
+ m.b = m12;
1066
1306
 
1067
- finalizeSegment(state);
1068
- }
1307
+ m.m21 = m21;
1308
+ m.c = m21;
1069
1309
 
1070
- function SVGPathArray(pathString) {
1071
- this.segments = [];
1072
- this.pathValue = pathString;
1073
- this.max = pathString.length;
1074
- this.index = 0;
1075
- this.param = 0.0;
1076
- this.segmentStart = 0;
1077
- this.data = [];
1078
- this.err = '';
1079
- // return this;
1080
- }
1310
+ m.m22 = m22;
1311
+ m.d = m22;
1081
1312
 
1082
- // Returns array of segments:
1083
- function parsePathString(pathString, round) {
1084
- if (isPathArray(pathString)) {
1085
- return clonePath(pathString);
1086
- }
1313
+ m.m41 = m41;
1314
+ m.e = m41;
1087
1315
 
1088
- const state = new SVGPathArray(pathString);
1316
+ m.m42 = m42;
1317
+ m.f = m42;
1318
+ } else {
1319
+ throw new TypeError('CSSMatrix: expecting an Array of 6/16 values.');
1320
+ }
1321
+ return m;
1322
+ }
1089
1323
 
1090
- skipSpaces(state);
1324
+ /**
1325
+ * Creates a new mutable `CSSMatrix` instance given an existing matrix or a
1326
+ * `DOMMatrix` instance which provides the values for its properties.
1327
+ *
1328
+ * @param {CSSMatrix | DOMMatrix | DMNS.jsonMatrix} m the source matrix to feed values from.
1329
+ * @return {CSSMatrix} the resulted matrix.
1330
+ */
1331
+ function fromMatrix(m) {
1332
+ const keys = [
1333
+ 'm11', 'm12', 'm13', 'm14',
1334
+ 'm21', 'm22', 'm23', 'm24',
1335
+ 'm31', 'm32', 'm33', 'm34',
1336
+ 'm41', 'm42', 'm43', 'm44'];
1337
+ if ([CSSMatrix, DOMMatrix].some((x) => m instanceof x)
1338
+ || (typeof m === 'object' && keys.every((k) => k in m))) {
1339
+ return fromArray(
1340
+ [m.m11, m.m12, m.m13, m.m14,
1341
+ m.m21, m.m22, m.m23, m.m24,
1342
+ m.m31, m.m32, m.m33, m.m34,
1343
+ m.m41, m.m42, m.m43, m.m44],
1344
+ );
1345
+ }
1346
+ throw TypeError(`CSSMatrix: "${m}" is not a DOMMatrix / CSSMatrix compatible object.`);
1347
+ }
1091
1348
 
1092
- while (state.index < state.max && !state.err.length) {
1093
- scanSegment(state);
1349
+ /**
1350
+ * Creates a new mutable `CSSMatrix` instance given any valid CSS transform string.
1351
+ *
1352
+ * * `matrix(a, b, c, d, e, f)` - valid matrix() transform function
1353
+ * * `matrix3d(m11, m12, m13, ...m44)` - valid matrix3d() transform function
1354
+ * * `translate(tx, ty) rotateX(alpha)` - any valid transform function(s)
1355
+ *
1356
+ * @copyright thednp © 2021
1357
+ *
1358
+ * @param {string} source valid CSS transform string syntax.
1359
+ * @return {CSSMatrix} the resulted matrix.
1360
+ */
1361
+ function fromString(source) {
1362
+ if (typeof source !== 'string') {
1363
+ throw TypeError(`CSSMatrix: "${source}" is not a string.`);
1094
1364
  }
1365
+ const str = String(source).replace(/\s/g, '');
1366
+ let m = new CSSMatrix();
1367
+ let is2D = true;
1368
+ const tramsformObject = str.split(')').filter((f) => f).map((fn) => {
1369
+ const [prop, value] = fn.split('(');
1370
+ const components = value.split(',')
1371
+ .map((n) => (n.includes('rad') ? parseFloat(n) * (180 / Math.PI) : parseFloat(n)));
1372
+ const [x, y, z, a] = components;
1373
+
1374
+ // don't add perspective if is2D
1375
+ if (is2D && (prop === 'matrix3d' // only modify is2D once
1376
+ || (prop === 'rotate3d' && [x, y].every((n) => !Number.isNaN(+n) && n !== 0) && a)
1377
+ || (['rotateX', 'rotateY'].includes(prop) && x)
1378
+ || (prop === 'translate3d' && [x, y, z].every((n) => !Number.isNaN(+n)) && z)
1379
+ || (prop === 'scale3d' && [x, y, z].every((n) => !Number.isNaN(+n) && n !== x))
1380
+ )) {
1381
+ is2D = false;
1382
+ }
1383
+ return { prop, components };
1384
+ });
1095
1385
 
1096
- if (state.err.length) {
1097
- state.segments = [];
1098
- } else if (state.segments.length) {
1099
- if ('mM'.indexOf(state.segments[0][0]) < 0) {
1100
- // state.err = 'Path string should start with `M` or `m`';
1101
- state.err = `${invalidPathValue}: missing M/m`;
1102
- state.segments = [];
1103
- } else {
1104
- state.segments[0][0] = 'M';
1386
+ tramsformObject.forEach((tf) => {
1387
+ const { prop, components } = tf;
1388
+ const [x, y, z, a] = components;
1389
+ const xyz = [x, y, z];
1390
+ const xyza = [x, y, z, a];
1391
+
1392
+ if (prop === 'perspective' && !is2D) {
1393
+ m.m34 = -1 / x;
1394
+ } else if (prop.includes('matrix')) {
1395
+ const values = components.map((n) => (Math.abs(n) < 1e-6 ? 0 : n));
1396
+ if ([6, 16].includes(values.length)) {
1397
+ m = m.multiply(fromArray(values));
1398
+ }
1399
+ } else if (['translate', 'translate3d'].some((p) => prop === p) && x) {
1400
+ m = m.translate(x, y || 0, z || 0);
1401
+ } else if (prop === 'rotate3d' && xyza.every((n) => !Number.isNaN(+n)) && a) {
1402
+ m = m.rotateAxisAngle(x, y, z, a);
1403
+ } else if (prop === 'scale3d' && xyz.every((n) => !Number.isNaN(+n)) && xyz.some((n) => n !== 1)) {
1404
+ m = m.scale(x, y, z);
1405
+ } else if (prop === 'rotate' && x) {
1406
+ m = m.rotate(0, 0, x);
1407
+ } else if (prop === 'scale' && !Number.isNaN(x) && x !== 1) {
1408
+ const nosy = Number.isNaN(+y);
1409
+ const sy = nosy ? x : y;
1410
+ m = m.scale(x, sy, 1);
1411
+ } else if (prop === 'skew' && (x || y)) {
1412
+ m = x ? m.skewX(x) : m;
1413
+ m = y ? m.skewY(y) : m;
1414
+ } else if (/[XYZ]/.test(prop) && x) {
1415
+ if (prop.includes('skew')) {
1416
+ // @ts-ignore unfortunately
1417
+ m = m[prop](x);
1418
+ } else {
1419
+ const fn = prop.replace(/[XYZ]/, '');
1420
+ const axis = prop.replace(fn, '');
1421
+ const idx = ['X', 'Y', 'Z'].indexOf(axis);
1422
+ const axeValues = [
1423
+ idx === 0 ? x : 0,
1424
+ idx === 1 ? x : 0,
1425
+ idx === 2 ? x : 0];
1426
+ // @ts-ignore unfortunately
1427
+ m = m[fn](...axeValues);
1428
+ }
1105
1429
  }
1106
- }
1430
+ });
1107
1431
 
1108
- return roundPath(state.segments, round);
1432
+ return m;
1109
1433
  }
1110
1434
 
1111
- function isAbsoluteArray(pathInput) {
1112
- return isPathArray(pathInput) && pathInput.every((x) => x[0] === x[0].toUpperCase());
1435
+ // Transform Functions
1436
+ // https://www.w3.org/TR/css-transforms-1/#transform-functions
1437
+
1438
+ /**
1439
+ * Creates a new `CSSMatrix` for the translation matrix and returns it.
1440
+ * This method is equivalent to the CSS `translate3d()` function.
1441
+ *
1442
+ * https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/translate3d
1443
+ *
1444
+ * @param {number} x the `x-axis` position.
1445
+ * @param {number} y the `y-axis` position.
1446
+ * @param {number} z the `z-axis` position.
1447
+ * @return {CSSMatrix} the resulted matrix.
1448
+ */
1449
+ function Translate(x, y, z) {
1450
+ const m = new CSSMatrix();
1451
+ m.m41 = x;
1452
+ m.e = x;
1453
+ m.m42 = y;
1454
+ m.f = y;
1455
+ m.m43 = z;
1456
+ return m;
1113
1457
  }
1114
1458
 
1115
- function pathToAbsolute(pathInput, round) {
1116
- if (isAbsoluteArray(pathInput)) {
1117
- return clonePath(pathInput);
1118
- }
1459
+ /**
1460
+ * Creates a new `CSSMatrix` for the rotation matrix and returns it.
1461
+ *
1462
+ * http://en.wikipedia.org/wiki/Rotation_matrix
1463
+ *
1464
+ * @param {number} rx the `x-axis` rotation.
1465
+ * @param {number} ry the `y-axis` rotation.
1466
+ * @param {number} rz the `z-axis` rotation.
1467
+ * @return {CSSMatrix} the resulted matrix.
1468
+ */
1469
+ function Rotate(rx, ry, rz) {
1470
+ const m = new CSSMatrix();
1471
+ const degToRad = Math.PI / 180;
1472
+ const radX = rx * degToRad;
1473
+ const radY = ry * degToRad;
1474
+ const radZ = rz * degToRad;
1119
1475
 
1120
- const pathArray = parsePathString(pathInput, round);
1121
- const ii = pathArray.length;
1122
- const resultArray = [];
1123
- let x = 0;
1124
- let y = 0;
1125
- let mx = 0;
1126
- let my = 0;
1127
- let start = 0;
1476
+ // minus sin() because of right-handed system
1477
+ const cosx = Math.cos(radX);
1478
+ const sinx = -Math.sin(radX);
1479
+ const cosy = Math.cos(radY);
1480
+ const siny = -Math.sin(radY);
1481
+ const cosz = Math.cos(radZ);
1482
+ const sinz = -Math.sin(radZ);
1128
1483
 
1129
- if (pathArray[0][0] === 'M') {
1130
- x = +pathArray[0][1];
1131
- y = +pathArray[0][2];
1132
- mx = x;
1133
- my = y;
1134
- start += 1;
1135
- resultArray.push(['M', x, y]);
1136
- }
1484
+ const m11 = cosy * cosz;
1485
+ const m12 = -cosy * sinz;
1137
1486
 
1138
- for (let i = start; i < ii; i += 1) {
1139
- const segment = pathArray[i];
1140
- const [pathCommand] = segment;
1141
- const absCommand = pathCommand.toUpperCase();
1142
- const absoluteSegment = [];
1143
- let newSeg = [];
1144
- resultArray.push(absoluteSegment);
1487
+ m.m11 = m11;
1488
+ m.a = m11;
1145
1489
 
1146
- if (pathCommand !== absCommand) {
1147
- absoluteSegment[0] = absCommand;
1490
+ m.m12 = m12;
1491
+ m.b = m12;
1148
1492
 
1149
- switch (absCommand) {
1150
- case 'A':
1151
- newSeg = segment.slice(1, -2).concat([+segment[6] + x, +segment[7] + y]);
1152
- for (let j = 0; j < newSeg.length; j += 1) {
1153
- absoluteSegment.push(newSeg[j]);
1154
- }
1155
- break;
1156
- case 'V':
1157
- absoluteSegment[1] = +segment[1] + y;
1158
- break;
1159
- case 'H':
1160
- absoluteSegment[1] = +segment[1] + x;
1161
- break;
1162
- default:
1163
- if (absCommand === 'M') {
1164
- mx = +segment[1] + x;
1165
- my = +segment[2] + y;
1166
- }
1167
- // for is here to stay for eslint
1168
- for (let j = 1; j < segment.length; j += 1) {
1169
- absoluteSegment.push(+segment[j] + (j % 2 ? x : y));
1170
- }
1171
- }
1172
- } else {
1173
- for (let j = 0; j < segment.length; j += 1) {
1174
- absoluteSegment.push(segment[j]);
1175
- }
1176
- }
1493
+ m.m13 = siny;
1177
1494
 
1178
- const segLength = absoluteSegment.length;
1179
- switch (absCommand) {
1180
- case 'Z':
1181
- x = mx;
1182
- y = my;
1183
- break;
1184
- case 'H':
1185
- x = +absoluteSegment[1];
1186
- break;
1187
- case 'V':
1188
- y = +absoluteSegment[1];
1189
- break;
1190
- default:
1191
- x = +absoluteSegment[segLength - 2];
1192
- y = +absoluteSegment[segLength - 1];
1495
+ const m21 = sinx * siny * cosz + cosx * sinz;
1496
+ m.m21 = m21;
1497
+ m.c = m21;
1193
1498
 
1194
- if (absCommand === 'M') {
1195
- mx = x;
1196
- my = y;
1197
- }
1198
- }
1199
- }
1499
+ const m22 = cosx * cosz - sinx * siny * sinz;
1500
+ m.m22 = m22;
1501
+ m.d = m22;
1200
1502
 
1201
- return roundPath(resultArray, round);
1202
- }
1503
+ m.m23 = -sinx * cosy;
1203
1504
 
1204
- // returns {qx,qy} for shorthand quadratic bezier segments
1205
- function shorthandToQuad(x1, y1, qx, qy, prevCommand) {
1206
- return 'QT'.indexOf(prevCommand) > -1
1207
- ? { qx: x1 * 2 - qx, qy: y1 * 2 - qy }
1208
- : { qx: x1, qy: y1 };
1209
- }
1505
+ m.m31 = sinx * sinz - cosx * siny * cosz;
1506
+ m.m32 = sinx * cosz + cosx * siny * sinz;
1507
+ m.m33 = cosx * cosy;
1210
1508
 
1211
- // returns {x1,x2} for shorthand cubic bezier segments
1212
- function shorthandToCubic(x1, y1, x2, y2, prevCommand) {
1213
- return 'CS'.indexOf(prevCommand) > -1
1214
- ? { x1: x1 * 2 - x2, y1: y1 * 2 - y2 }
1215
- : { x1, y1 };
1509
+ return m;
1216
1510
  }
1217
1511
 
1218
- function normalizeSegment(segment, params, prevCommand) {
1219
- const [pathCommand] = segment;
1220
- const xy = segment.slice(1);
1221
- let result = segment;
1512
+ /**
1513
+ * Creates a new `CSSMatrix` for the rotation matrix and returns it.
1514
+ * This method is equivalent to the CSS `rotate3d()` function.
1515
+ *
1516
+ * https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/rotate3d
1517
+ *
1518
+ * @param {number} x the `x-axis` vector length.
1519
+ * @param {number} y the `y-axis` vector length.
1520
+ * @param {number} z the `z-axis` vector length.
1521
+ * @param {number} alpha the value in degrees of the rotation.
1522
+ * @return {CSSMatrix} the resulted matrix.
1523
+ */
1524
+ function RotateAxisAngle(x, y, z, alpha) {
1525
+ const m = new CSSMatrix();
1526
+ const angle = alpha * (Math.PI / 360);
1527
+ const sinA = Math.sin(angle);
1528
+ const cosA = Math.cos(angle);
1529
+ const sinA2 = sinA * sinA;
1530
+ const length = Math.sqrt(x * x + y * y + z * z);
1531
+ let X = x;
1532
+ let Y = y;
1533
+ let Z = z;
1222
1534
 
1223
- if ('TQ'.indexOf(segment[0]) < 0) {
1224
- // optional but good to be cautious
1225
- params.qx = null;
1226
- params.qy = null;
1535
+ if (length === 0) {
1536
+ // bad vector length, use something reasonable
1537
+ X = 0;
1538
+ Y = 0;
1539
+ Z = 1;
1540
+ } else {
1541
+ X /= length;
1542
+ Y /= length;
1543
+ Z /= length;
1227
1544
  }
1228
1545
 
1229
- if (pathCommand === 'H') {
1230
- result = ['L', segment[1], params.y1];
1231
- } else if (pathCommand === 'V') {
1232
- result = ['L', params.x1, segment[1]];
1233
- } else if (pathCommand === 'S') {
1234
- const { x1, y1 } = shorthandToCubic(params.x1, params.y1, params.x2, params.y2, prevCommand);
1235
- params.x1 = x1;
1236
- params.y1 = y1;
1237
- result = ['C', x1, y1].concat(xy);
1238
- } else if (pathCommand === 'T') {
1239
- const { qx, qy } = shorthandToQuad(params.x1, params.y1, params.qx, params.qy, prevCommand);
1240
- params.qx = qx;
1241
- params.qy = qy;
1242
- result = ['Q', qx, qy].concat(xy);
1243
- } else if (pathCommand === 'Q') {
1244
- const [nqx, nqy] = xy;
1245
- params.qx = nqx;
1246
- params.qy = nqy;
1247
- }
1248
- return result;
1249
- }
1546
+ const x2 = X * X;
1547
+ const y2 = Y * Y;
1548
+ const z2 = Z * Z;
1250
1549
 
1251
- function isNormalizedArray(pathArray) {
1252
- return Array.isArray(pathArray) && pathArray.every((seg) => {
1253
- const pathCommand = seg[0].toLowerCase();
1254
- return paramsCount[pathCommand] === seg.length - 1 && /[ACLMQZ]/.test(seg[0]); // achlmrqstvz
1255
- });
1256
- }
1550
+ const m11 = 1 - 2 * (y2 + z2) * sinA2;
1551
+ m.m11 = m11;
1552
+ m.a = m11;
1257
1553
 
1258
- function normalizePath(pathInput, round) { // pathArray|pathString
1259
- if (isNormalizedArray(pathInput)) {
1260
- return clonePath(pathInput);
1261
- }
1554
+ const m12 = 2 * (X * Y * sinA2 + Z * sinA * cosA);
1555
+ m.m12 = m12;
1556
+ m.b = m12;
1262
1557
 
1263
- const pathArray = pathToAbsolute(pathInput, round);
1264
- const params = {
1265
- x1: 0, y1: 0, x2: 0, y2: 0, x: 0, y: 0, qx: null, qy: null,
1266
- };
1267
- const allPathCommands = [];
1268
- const ii = pathArray.length;
1269
- let prevCommand = '';
1270
- let segment;
1271
- let seglen;
1558
+ m.m13 = 2 * (X * Z * sinA2 - Y * sinA * cosA);
1272
1559
 
1273
- for (let i = 0; i < ii; i += 1) {
1274
- // save current path command
1275
- const [pathCommand] = pathArray[i];
1560
+ const m21 = 2 * (Y * X * sinA2 - Z * sinA * cosA);
1561
+ m.m21 = m21;
1562
+ m.c = m21;
1276
1563
 
1277
- // Save current path command
1278
- allPathCommands[i] = pathCommand;
1279
- // Get previous path command
1280
- if (i) prevCommand = allPathCommands[i - 1];
1281
- // Previous path command is inputted to processSegment
1282
- pathArray[i] = normalizeSegment(pathArray[i], params, prevCommand);
1564
+ const m22 = 1 - 2 * (z2 + x2) * sinA2;
1565
+ m.m22 = m22;
1566
+ m.d = m22;
1283
1567
 
1284
- segment = pathArray[i];
1285
- seglen = segment.length;
1568
+ m.m23 = 2 * (Y * Z * sinA2 + X * sinA * cosA);
1569
+ m.m31 = 2 * (Z * X * sinA2 + Y * sinA * cosA);
1570
+ m.m32 = 2 * (Z * Y * sinA2 - X * sinA * cosA);
1571
+ m.m33 = 1 - 2 * (x2 + y2) * sinA2;
1286
1572
 
1287
- params.x1 = +segment[seglen - 2];
1288
- params.y1 = +segment[seglen - 1];
1289
- params.x2 = +(segment[seglen - 4]) || params.x1;
1290
- params.y2 = +(segment[seglen - 3]) || params.y1;
1291
- }
1292
- return roundPath(pathArray, round);
1573
+ return m;
1293
1574
  }
1294
1575
 
1295
- function rotateVector(x, y, rad) {
1296
- const X = x * Math.cos(rad) - y * Math.sin(rad);
1297
- const Y = x * Math.sin(rad) + y * Math.cos(rad);
1298
- return { x: X, y: Y };
1576
+ /**
1577
+ * Creates a new `CSSMatrix` for the scale matrix and returns it.
1578
+ * This method is equivalent to the CSS `scale3d()` function.
1579
+ *
1580
+ * https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/scale3d
1581
+ *
1582
+ * @param {number} x the `x-axis` scale.
1583
+ * @param {number} y the `y-axis` scale.
1584
+ * @param {number} z the `z-axis` scale.
1585
+ * @return {CSSMatrix} the resulted matrix.
1586
+ */
1587
+ function Scale(x, y, z) {
1588
+ const m = new CSSMatrix();
1589
+ m.m11 = x;
1590
+ m.a = x;
1591
+
1592
+ m.m22 = y;
1593
+ m.d = y;
1594
+
1595
+ m.m33 = z;
1596
+ return m;
1299
1597
  }
1300
1598
 
1301
- // for more information of where this math came from visit:
1302
- // http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
1303
- // LAF = largeArcFlag, SF = sweepFlag
1599
+ /**
1600
+ * Creates a new `CSSMatrix` for the shear of the `x-axis` rotation matrix and
1601
+ * returns it. This method is equivalent to the CSS `skewX()` function.
1602
+ *
1603
+ * https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/skewX
1604
+ *
1605
+ * @param {number} angle the angle in degrees.
1606
+ * @return {CSSMatrix} the resulted matrix.
1607
+ */
1608
+ function SkewX(angle) {
1609
+ const m = new CSSMatrix();
1610
+ const radA = (angle * Math.PI) / 180;
1611
+ const t = Math.tan(radA);
1612
+ m.m21 = t;
1613
+ m.c = t;
1614
+ return m;
1615
+ }
1304
1616
 
1305
- function arcToCubic(x1, y1, rx, ry, angle, LAF, SF, x2, y2, recursive) {
1306
- const d120 = (Math.PI * 120) / 180;
1307
- const rad = (Math.PI / 180) * (angle || 0);
1308
- let res = [];
1309
- let X1 = x1;
1310
- let X2 = x2;
1311
- let Y1 = y1;
1312
- let Y2 = y2;
1313
- let RX = rx;
1314
- let RY = ry;
1315
- let xy;
1316
- let f1;
1317
- let f2;
1318
- let cx;
1319
- let cy;
1617
+ /**
1618
+ * Creates a new `CSSMatrix` for the shear of the `y-axis` rotation matrix and
1619
+ * returns it. This method is equivalent to the CSS `skewY()` function.
1620
+ *
1621
+ * https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/skewY
1622
+ *
1623
+ * @param {number} angle the angle in degrees.
1624
+ * @return {CSSMatrix} the resulted matrix.
1625
+ */
1626
+ function SkewY(angle) {
1627
+ const m = new CSSMatrix();
1628
+ const radA = (angle * Math.PI) / 180;
1629
+ const t = Math.tan(radA);
1630
+ m.m12 = t;
1631
+ m.b = t;
1632
+ return m;
1633
+ }
1320
1634
 
1321
- if (!recursive) {
1322
- xy = rotateVector(X1, Y1, -rad);
1323
- X1 = xy.x;
1324
- Y1 = xy.y;
1325
- xy = rotateVector(X2, Y2, -rad);
1326
- X2 = xy.x;
1327
- Y2 = xy.y;
1635
+ /**
1636
+ * Creates a new `CSSMatrix` resulted from the multiplication of two matrixes
1637
+ * and returns it. Both matrixes are not changed.
1638
+ *
1639
+ * @param {CSSMatrix} m1 the first matrix.
1640
+ * @param {CSSMatrix} m2 the second matrix.
1641
+ * @return {CSSMatrix} the resulted matrix.
1642
+ */
1643
+ function Multiply(m1, m2) {
1644
+ const m11 = m2.m11 * m1.m11 + m2.m12 * m1.m21 + m2.m13 * m1.m31 + m2.m14 * m1.m41;
1645
+ const m12 = m2.m11 * m1.m12 + m2.m12 * m1.m22 + m2.m13 * m1.m32 + m2.m14 * m1.m42;
1646
+ const m13 = m2.m11 * m1.m13 + m2.m12 * m1.m23 + m2.m13 * m1.m33 + m2.m14 * m1.m43;
1647
+ const m14 = m2.m11 * m1.m14 + m2.m12 * m1.m24 + m2.m13 * m1.m34 + m2.m14 * m1.m44;
1328
1648
 
1329
- const x = (X1 - X2) / 2;
1330
- const y = (Y1 - Y2) / 2;
1331
- let h = (x * x) / (RX * RY) + (y ** 2) / (RY ** 2);
1332
- if (h > 1) {
1333
- h = Math.sqrt(h);
1334
- RX *= h;
1335
- RY *= h;
1336
- }
1337
- const rx2 = RX ** 2;
1338
- const ry2 = RY ** 2;
1339
- const k = (LAF === SF ? -1 : 1)
1340
- * Math.sqrt(Math.abs((rx2 * ry2 - rx2 * y * y - ry2 * x * x)
1341
- / (rx2 * y * y + ry2 * x * x)));
1649
+ const m21 = m2.m21 * m1.m11 + m2.m22 * m1.m21 + m2.m23 * m1.m31 + m2.m24 * m1.m41;
1650
+ const m22 = m2.m21 * m1.m12 + m2.m22 * m1.m22 + m2.m23 * m1.m32 + m2.m24 * m1.m42;
1651
+ const m23 = m2.m21 * m1.m13 + m2.m22 * m1.m23 + m2.m23 * m1.m33 + m2.m24 * m1.m43;
1652
+ const m24 = m2.m21 * m1.m14 + m2.m22 * m1.m24 + m2.m23 * m1.m34 + m2.m24 * m1.m44;
1342
1653
 
1343
- cx = ((k * RX * y) / RY) + ((X1 + X2) / 2);
1344
- cy = ((k * -RY * x) / RX) + ((Y1 + Y2) / 2);
1654
+ const m31 = m2.m31 * m1.m11 + m2.m32 * m1.m21 + m2.m33 * m1.m31 + m2.m34 * m1.m41;
1655
+ const m32 = m2.m31 * m1.m12 + m2.m32 * m1.m22 + m2.m33 * m1.m32 + m2.m34 * m1.m42;
1656
+ const m33 = m2.m31 * m1.m13 + m2.m32 * m1.m23 + m2.m33 * m1.m33 + m2.m34 * m1.m43;
1657
+ const m34 = m2.m31 * m1.m14 + m2.m32 * m1.m24 + m2.m33 * m1.m34 + m2.m34 * m1.m44;
1345
1658
 
1346
- // f1 = Math.asin(((Y1 - cy) / RY).toFixed(9)); // keep toFIxed(9)!
1347
- // f2 = Math.asin(((Y2 - cy) / RY).toFixed(9));
1348
- f1 = Math.asin((((Y1 - cy) / RY) * 10 ** 9 >> 0) / (10 ** 9));
1349
- f2 = Math.asin((((Y2 - cy) / RY) * 10 ** 9 >> 0) / (10 ** 9));
1659
+ const m41 = m2.m41 * m1.m11 + m2.m42 * m1.m21 + m2.m43 * m1.m31 + m2.m44 * m1.m41;
1660
+ const m42 = m2.m41 * m1.m12 + m2.m42 * m1.m22 + m2.m43 * m1.m32 + m2.m44 * m1.m42;
1661
+ const m43 = m2.m41 * m1.m13 + m2.m42 * m1.m23 + m2.m43 * m1.m33 + m2.m44 * m1.m43;
1662
+ const m44 = m2.m41 * m1.m14 + m2.m42 * m1.m24 + m2.m43 * m1.m34 + m2.m44 * m1.m44;
1350
1663
 
1351
- f1 = X1 < cx ? Math.PI - f1 : f1;
1352
- f2 = X2 < cx ? Math.PI - f2 : f2;
1664
+ return fromArray(
1665
+ [m11, m12, m13, m14,
1666
+ m21, m22, m23, m24,
1667
+ m31, m32, m33, m34,
1668
+ m41, m42, m43, m44],
1669
+ );
1670
+ }
1353
1671
 
1354
- if (f1 < 0) f1 = Math.PI * 2 + f1;
1355
- if (f2 < 0) f2 = Math.PI * 2 + f2;
1672
+ /**
1673
+ * Creates and returns a new `DOMMatrix` compatible *Object*
1674
+ * with equivalent instance methods.
1675
+ *
1676
+ * https://developer.mozilla.org/en-US/docs/Web/API/DOMMatrix
1677
+ * https://github.com/thednp/DOMMatrix/
1678
+ */
1356
1679
 
1357
- if (SF && f1 > f2) {
1358
- f1 -= Math.PI * 2;
1359
- }
1360
- if (!SF && f2 > f1) {
1361
- f2 -= Math.PI * 2;
1680
+ class CSSMatrix {
1681
+ /**
1682
+ * @constructor
1683
+ * @param {any} args accepts all parameter configurations:
1684
+ *
1685
+ * * valid CSS transform string,
1686
+ * * CSSMatrix/DOMMatrix instance,
1687
+ * * a 6/16 elements *Array*.
1688
+ */
1689
+ constructor(...args) {
1690
+ const m = this;
1691
+ // array 6
1692
+ m.a = 1; m.b = 0;
1693
+ m.c = 0; m.d = 1;
1694
+ m.e = 0; m.f = 0;
1695
+ // array 16
1696
+ m.m11 = 1; m.m12 = 0; m.m13 = 0; m.m14 = 0;
1697
+ m.m21 = 0; m.m22 = 1; m.m23 = 0; m.m24 = 0;
1698
+ m.m31 = 0; m.m32 = 0; m.m33 = 1; m.m34 = 0;
1699
+ m.m41 = 0; m.m42 = 0; m.m43 = 0; m.m44 = 1;
1700
+
1701
+ if (args && args.length) {
1702
+ let ARGS = args;
1703
+
1704
+ if (args instanceof Array) {
1705
+ if ((args[0] instanceof Array && [16, 6].includes(args[0].length))
1706
+ || typeof args[0] === 'string'
1707
+ || [CSSMatrix, DOMMatrix].some((x) => args[0] instanceof x)) {
1708
+ [ARGS] = args;
1709
+ }
1710
+ }
1711
+ return m.setMatrixValue(ARGS);
1362
1712
  }
1363
- } else {
1364
- const [r1, r2, r3, r4] = recursive;
1365
- f1 = r1;
1366
- f2 = r2;
1367
- cx = r3;
1368
- cy = r4;
1713
+ return m;
1369
1714
  }
1370
1715
 
1371
- let df = f2 - f1;
1372
-
1373
- if (Math.abs(df) > d120) {
1374
- const f2old = f2; const x2old = X2; const
1375
- y2old = Y2;
1716
+ /**
1717
+ * Sets a new `Boolean` flag value for `this.isIdentity` matrix property.
1718
+ *
1719
+ * @param {Boolean} value sets a new flag for this property
1720
+ */
1721
+ set isIdentity(value) {
1722
+ this.isIdentity = value;
1723
+ }
1376
1724
 
1377
- f2 = f1 + d120 * (SF && f2 > f1 ? 1 : -1);
1378
- X2 = cx + RX * Math.cos(f2);
1379
- Y2 = cy + RY * Math.sin(f2);
1380
- res = arcToCubic(X2, Y2, RX, RY, angle, 0, SF, x2old, y2old, [f2, f2old, cx, cy]);
1725
+ /**
1726
+ * A `Boolean` whose value is `true` if the matrix is the identity matrix. The identity
1727
+ * matrix is one in which every value is 0 except those on the main diagonal from top-left
1728
+ * to bottom-right corner (in other words, where the offsets in each direction are equal).
1729
+ *
1730
+ * @return {Boolean} the current property value
1731
+ */
1732
+ get isIdentity() {
1733
+ const m = this;
1734
+ return (m.m11 === 1 && m.m12 === 0 && m.m13 === 0 && m.m14 === 0
1735
+ && m.m21 === 0 && m.m22 === 1 && m.m23 === 0 && m.m24 === 0
1736
+ && m.m31 === 0 && m.m32 === 0 && m.m33 === 1 && m.m34 === 0
1737
+ && m.m41 === 0 && m.m42 === 0 && m.m43 === 0 && m.m44 === 1);
1381
1738
  }
1382
1739
 
1383
- df = f2 - f1;
1384
- const c1 = Math.cos(f1);
1385
- const s1 = Math.sin(f1);
1386
- const c2 = Math.cos(f2);
1387
- const s2 = Math.sin(f2);
1388
- const t = Math.tan(df / 4);
1389
- const hx = (4 / 3) * RX * t;
1390
- const hy = (4 / 3) * RY * t;
1391
- const m1 = [X1, Y1];
1392
- const m2 = [X1 + hx * s1, Y1 - hy * c1];
1393
- const m3 = [X2 + hx * s2, Y2 - hy * c2];
1394
- const m4 = [X2, Y2];
1395
- m2[0] = 2 * m1[0] - m2[0];
1396
- m2[1] = 2 * m1[1] - m2[1];
1397
-
1398
- if (recursive) {
1399
- return [m2, m3, m4].concat(res);
1740
+ /**
1741
+ * A `Boolean` flag whose value is `true` if the matrix was initialized as a 2D matrix
1742
+ * and `false` if the matrix is 3D.
1743
+ *
1744
+ * @return {Boolean} the current property value
1745
+ */
1746
+ get is2D() {
1747
+ const m = this;
1748
+ return (m.m31 === 0 && m.m32 === 0 && m.m33 === 1 && m.m34 === 0 && m.m43 === 0 && m.m44 === 1);
1400
1749
  }
1401
- res = [m2, m3, m4].concat(res).join().split(',');
1402
- return res.map((rz, i) => {
1403
- if (i % 2) {
1404
- return rotateVector(res[i - 1], rz, rad).y;
1405
- }
1406
- return rotateVector(rz, res[i + 1], rad).x;
1407
- });
1408
- }
1409
-
1410
- function quadToCubic(x1, y1, qx, qy, x2, y2) {
1411
- const r13 = 1 / 3;
1412
- const r23 = 2 / 3;
1413
- return [
1414
- r13 * x1 + r23 * qx, // cpx1
1415
- r13 * y1 + r23 * qy, // cpy1
1416
- r13 * x2 + r23 * qx, // cpx2
1417
- r13 * y2 + r23 * qy, // cpy2
1418
- x2, y2, // x,y
1419
- ];
1420
- }
1421
-
1422
- // t = [0-1]
1423
- function getPointAtSegLength(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t) {
1424
- const t1 = 1 - t;
1425
- return {
1426
- x: (t1 ** 3) * p1x
1427
- + t1 * t1 * 3 * t * c1x
1428
- + t1 * 3 * t * t * c2x
1429
- + (t ** 3) * p2x,
1430
- y: (t1 ** 3) * p1y
1431
- + t1 * t1 * 3 * t * c1y
1432
- + t1 * 3 * t * t * c2y
1433
- + (t ** 3) * p2y,
1434
- };
1435
- }
1436
-
1437
- function midPoint(a, b, t) {
1438
- const ax = a[0];
1439
- const ay = a[1];
1440
- const bx = b[0];
1441
- const by = b[1];
1442
- return [ax + (bx - ax) * t, ay + (by - ay) * t];
1443
- }
1444
-
1445
- function lineToCubic(x1, y1, x2, y2) {
1446
- const t = 0.5;
1447
- const p0 = [x1, y1];
1448
- const p1 = [x2, y2];
1449
- const p2 = midPoint(p0, p1, t);
1450
- const p3 = midPoint(p1, p2, t);
1451
- const p4 = midPoint(p2, p3, t);
1452
- const p5 = midPoint(p3, p4, t);
1453
- const p6 = midPoint(p4, p5, t);
1454
- const cp1 = getPointAtSegLength.apply(0, p0.concat(p2, p4, p6, t));
1455
- const cp2 = getPointAtSegLength.apply(0, p6.concat(p5, p3, p1, 0));
1456
-
1457
- return [cp1.x, cp1.y, cp2.x, cp2.y, x2, y2];
1458
- }
1459
1750
 
1460
- function segmentToCubic(segment, params) {
1461
- if ('TQ'.indexOf(segment[0]) < 0) {
1462
- params.qx = null;
1463
- params.qy = null;
1751
+ /**
1752
+ * Sets a new `Boolean` flag value for `this.is2D` matrix property.
1753
+ *
1754
+ * @param {Boolean} value sets a new flag for this property
1755
+ */
1756
+ set is2D(value) {
1757
+ this.is2D = value;
1464
1758
  }
1465
1759
 
1466
- const [s1, s2] = segment.slice(1);
1760
+ /**
1761
+ * The `setMatrixValue` method replaces the existing matrix with one computed
1762
+ * in the browser. EG: `matrix(1,0.25,-0.25,1,0,0)`
1763
+ *
1764
+ * The method accepts any *Array* values, the result of
1765
+ * `DOMMatrix` instance method `toFloat64Array()` / `toFloat32Array()` calls
1766
+ * or `CSSMatrix` instance method `toArray()`.
1767
+ *
1768
+ * This method expects valid *matrix()* / *matrix3d()* string values, as well
1769
+ * as other transform functions like *translateX(10px)*.
1770
+ *
1771
+ * @param {string | number[] | CSSMatrix | DOMMatrix} source
1772
+ * @return {CSSMatrix} the matrix instance
1773
+ */
1774
+ setMatrixValue(source) {
1775
+ const m = this;
1467
1776
 
1468
- switch (segment[0]) {
1469
- case 'M':
1470
- params.x = s1;
1471
- params.y = s2;
1472
- return segment;
1473
- case 'A':
1474
- return ['C'].concat(arcToCubic.apply(0, [params.x1, params.y1].concat(segment.slice(1))));
1475
- case 'Q':
1476
- params.qx = s1;
1477
- params.qy = s2;
1478
- return ['C'].concat(quadToCubic.apply(0, [params.x1, params.y1].concat(segment.slice(1))));
1479
- case 'L':
1480
- return ['C'].concat(lineToCubic(params.x1, params.y1, segment[1], segment[2]));
1481
- case 'Z':
1482
- return ['C'].concat(lineToCubic(params.x1, params.y1, params.x, params.y));
1777
+ // new CSSMatrix(CSSMatrix | DOMMatrix)
1778
+ if ([DOMMatrix, CSSMatrix].some((x) => source instanceof x)) {
1779
+ // @ts-ignore
1780
+ return fromMatrix(source);
1781
+ // CSS transform string source
1782
+ } if (typeof source === 'string' && source.length && source !== 'none') {
1783
+ return fromString(source);
1784
+ // [Arguments list | Array] come here
1785
+ } if (Array.isArray(source)) {
1786
+ return fromArray(source);
1787
+ }
1788
+ return m;
1483
1789
  }
1484
- return segment;
1485
- }
1486
1790
 
1487
- function pathToCurve(pathInput, round) { // pathArray|pathString
1488
- if (isCurveArray(pathInput)) {
1489
- return clonePath(pathInput);
1791
+ /**
1792
+ * Creates and returns a string representation of the matrix in `CSS` matrix syntax,
1793
+ * using the appropriate `CSS` matrix notation.
1794
+ *
1795
+ * matrix3d *matrix3d(m11, m12, m13, m14, m21, ...)*
1796
+ * matrix *matrix(a, b, c, d, e, f)*
1797
+ *
1798
+ * @return {string} a string representation of the matrix
1799
+ */
1800
+ toString() {
1801
+ const m = this;
1802
+ const values = m.toArray().join(',');
1803
+ const type = m.is2D ? 'matrix' : 'matrix3d';
1804
+ return `${type}(${values})`;
1490
1805
  }
1491
1806
 
1492
- const pathArray = normalizePath(pathInput, round);
1493
- const params = {
1494
- x1: 0, y1: 0, x2: 0, y2: 0, x: 0, y: 0, qx: null, qy: null,
1495
- };
1496
- const allPathCommands = [];
1497
- let pathCommand = '';
1498
- let ii = pathArray.length;
1499
- let segment;
1500
- let seglen;
1501
-
1502
- for (let i = 0; i < ii; i += 1) {
1503
- if (pathArray[i]) [pathCommand] = pathArray[i];
1807
+ /**
1808
+ * Returns an *Array* containing all 16 elements which comprise the matrix.
1809
+ * The method can return either the elements.
1810
+ *
1811
+ * Other methods make use of this method to feed their output values from this matrix.
1812
+ *
1813
+ * @return {number[]} an *Array* representation of the matrix
1814
+ */
1815
+ toArray() {
1816
+ const m = this;
1817
+ const pow6 = (10 ** 6);
1818
+ let result;
1504
1819
 
1505
- allPathCommands[i] = pathCommand;
1506
- pathArray[i] = segmentToCubic(pathArray[i], params);
1820
+ if (m.is2D) {
1821
+ result = [m.a, m.b, m.c, m.d, m.e, m.f];
1822
+ } else {
1823
+ result = [m.m11, m.m12, m.m13, m.m14,
1824
+ m.m21, m.m22, m.m23, m.m24,
1825
+ m.m31, m.m32, m.m33, m.m34,
1826
+ m.m41, m.m42, m.m43, m.m44];
1827
+ }
1828
+ // clean up the numbers
1829
+ // eslint-disable-next-line -- no-bitwise
1830
+ return result.map((n) => (Math.abs(n) < 1e-6 ? 0 : ((n * pow6) >> 0) / pow6));
1831
+ }
1507
1832
 
1508
- fixArc(pathArray, allPathCommands, i);
1509
- ii = pathArray.length; // solves curveArrays ending in Z
1833
+ /**
1834
+ * Returns a JSON representation of the `CSSMatrix` instance, a standard *Object*
1835
+ * that includes `{a,b,c,d,e,f}` and `{m11,m12,m13,..m44}` properties and
1836
+ * excludes `is2D` & `isIdentity` properties.
1837
+ *
1838
+ * The result can also be used as a second parameter for the `fromMatrix` static method
1839
+ * to load values into a matrix instance.
1840
+ *
1841
+ * @return {DMNS.jsonMatrix} an *Object* with all matrix values.
1842
+ */
1843
+ toJSON() {
1844
+ return JSON.parse(JSON.stringify(this));
1845
+ }
1510
1846
 
1511
- segment = pathArray[i];
1512
- seglen = segment.length;
1847
+ /**
1848
+ * The Multiply method returns a new CSSMatrix which is the result of this
1849
+ * matrix multiplied by the passed matrix, with the passed matrix to the right.
1850
+ * This matrix is not modified.
1851
+ *
1852
+ * @param {CSSMatrix | DOMMatrix | DMNS.jsonMatrix} m2 CSSMatrix
1853
+ * @return {CSSMatrix} The resulted matrix.
1854
+ */
1855
+ multiply(m2) {
1856
+ // @ts-ignore - we only access [m11, m12, ... m44] values
1857
+ return Multiply(this, m2);
1858
+ }
1513
1859
 
1514
- params.x1 = +segment[seglen - 2];
1515
- params.y1 = +segment[seglen - 1];
1516
- params.x2 = +(segment[seglen - 4]) || params.x1;
1517
- params.y2 = +(segment[seglen - 3]) || params.y1;
1860
+ /**
1861
+ * The translate method returns a new matrix which is this matrix post
1862
+ * multiplied by a translation matrix containing the passed values. If the z
1863
+ * component is undefined, a 0 value is used in its place. This matrix is not
1864
+ * modified.
1865
+ *
1866
+ * @param {number} x X component of the translation value.
1867
+ * @param {number | null} y Y component of the translation value.
1868
+ * @param {number | null} z Z component of the translation value.
1869
+ * @return {CSSMatrix} The resulted matrix
1870
+ */
1871
+ translate(x, y, z) {
1872
+ const X = x;
1873
+ let Y = y;
1874
+ let Z = z;
1875
+ if (Z == null) Z = 0;
1876
+ if (Y == null) Y = 0;
1877
+ return Multiply(this, Translate(X, Y, Z));
1518
1878
  }
1519
- return roundPath(pathArray, round);
1520
- }
1521
1879
 
1522
- // https://github.com/paperjs/paper.js/blob/develop/src/path/Path.js
1880
+ /**
1881
+ * The scale method returns a new matrix which is this matrix post multiplied by
1882
+ * a scale matrix containing the passed values. If the z component is undefined,
1883
+ * a 1 value is used in its place. If the y component is undefined, the x
1884
+ * component value is used in its place. This matrix is not modified.
1885
+ *
1886
+ * @param {number} x The X component of the scale value.
1887
+ * @param {number | null} y The Y component of the scale value.
1888
+ * @param {number | null} z The Z component of the scale value.
1889
+ * @return {CSSMatrix} The resulted matrix
1890
+ */
1891
+ scale(x, y, z) {
1892
+ const X = x;
1893
+ let Y = y;
1894
+ let Z = z;
1895
+ if (Y == null) Y = x;
1896
+ if (Z == null) Z = x;
1897
+
1898
+ return Multiply(this, Scale(X, Y, Z));
1899
+ }
1523
1900
 
1524
- function getCubicSegArea(x0, y0, x1, y1, x2, y2, x3, y3) {
1525
- // http://objectmix.com/graphics/133553-area-closed-bezier-curve.html
1526
- return (3 * ((y3 - y0) * (x1 + x2) - (x3 - x0) * (y1 + y2)
1527
- + (y1 * (x0 - x2)) - (x1 * (y0 - y2))
1528
- + (y3 * (x2 + x0 / 3)) - (x3 * (y2 + y0 / 3)))) / 20;
1529
- }
1901
+ /**
1902
+ * The rotate method returns a new matrix which is this matrix post multiplied
1903
+ * by each of 3 rotation matrices about the major axes, first X, then Y, then Z.
1904
+ * If the y and z components are undefined, the x value is used to rotate the
1905
+ * object about the z axis, as though the vector (0,0,x) were passed. All
1906
+ * rotation values are in degrees. This matrix is not modified.
1907
+ *
1908
+ * @param {number} rx The X component of the rotation, or Z if Y and Z are null.
1909
+ * @param {number | null} ry The (optional) Y component of the rotation value.
1910
+ * @param {number | null} rz The (optional) Z component of the rotation value.
1911
+ * @return {CSSMatrix} The resulted matrix
1912
+ */
1913
+ rotate(rx, ry, rz) {
1914
+ let RX = rx;
1915
+ let RY = ry;
1916
+ let RZ = rz;
1917
+ if (RY == null) RY = 0;
1918
+ if (RZ == null) { RZ = RX; RX = 0; }
1919
+ return Multiply(this, Rotate(RX, RY, RZ));
1920
+ }
1530
1921
 
1531
- function getPathArea(pathArray, round) {
1532
- let x = 0; let y = 0; let mx = 0; let my = 0; let
1533
- len = 0;
1534
- return pathToCurve(pathArray, round).map((seg) => {
1535
- switch (seg[0]) {
1536
- case 'M':
1537
- case 'Z':
1538
- mx = seg[0] === 'M' ? seg[1] : mx;
1539
- my = seg[0] === 'M' ? seg[2] : my;
1540
- x = mx;
1541
- y = my;
1542
- return 0;
1543
- default:
1544
- len = getCubicSegArea.apply(0, [x, y].concat(seg.slice(1)));
1545
- [x, y] = seg.slice(-2);
1546
- return len;
1922
+ /**
1923
+ * The rotateAxisAngle method returns a new matrix which is this matrix post
1924
+ * multiplied by a rotation matrix with the given axis and `angle`. The right-hand
1925
+ * rule is used to determine the direction of rotation. All rotation values are
1926
+ * in degrees. This matrix is not modified.
1927
+ *
1928
+ * @param {number} x The X component of the axis vector.
1929
+ * @param {number} y The Y component of the axis vector.
1930
+ * @param {number} z The Z component of the axis vector.
1931
+ * @param {number} angle The angle of rotation about the axis vector, in degrees.
1932
+ * @return {CSSMatrix} The resulted matrix
1933
+ */
1934
+ rotateAxisAngle(x, y, z, angle) {
1935
+ if ([x, y, z, angle].some((n) => Number.isNaN(n))) {
1936
+ throw new TypeError('CSSMatrix: expecting 4 values');
1547
1937
  }
1548
- }).reduce((a, b) => a + b, 0);
1549
- }
1938
+ return Multiply(this, RotateAxisAngle(x, y, z, angle));
1939
+ }
1550
1940
 
1551
- function base3(p1, p2, p3, p4, t) {
1552
- const t1 = -3 * p1 + 9 * p2 - 9 * p3 + 3 * p4;
1553
- const t2 = t * t1 + 6 * p1 - 12 * p2 + 6 * p3;
1554
- return t * t2 - 3 * p1 + 3 * p2;
1555
- }
1941
+ /**
1942
+ * Specifies a skew transformation along the `x-axis` by the given angle.
1943
+ * This matrix is not modified.
1944
+ *
1945
+ * @param {number} angle The angle amount in degrees to skew.
1946
+ * @return {CSSMatrix} The resulted matrix
1947
+ */
1948
+ skewX(angle) {
1949
+ return Multiply(this, SkewX(angle));
1950
+ }
1556
1951
 
1557
- // returns the cubic bezier segment length
1558
- function getSegCubicLength(x1, y1, x2, y2, x3, y3, x4, y4, z) {
1559
- let Z;
1560
- if (z === null || Number.isNaN(+z)) Z = 1;
1952
+ /**
1953
+ * Specifies a skew transformation along the `y-axis` by the given angle.
1954
+ * This matrix is not modified.
1955
+ *
1956
+ * @param {number} angle The angle amount in degrees to skew.
1957
+ * @return {CSSMatrix} The resulted matrix
1958
+ */
1959
+ skewY(angle) {
1960
+ return Multiply(this, SkewY(angle));
1961
+ }
1561
1962
 
1562
- // Z = Z > 1 ? 1 : Z < 0 ? 0 : Z;
1563
- if (Z > 1) Z = 1;
1564
- if (Z < 0) Z = 0;
1963
+ /**
1964
+ * Transforms a specified point using the matrix, returning a new
1965
+ * Tuple *Object* comprising of the transformed point.
1966
+ * Neither the matrix nor the original point are altered.
1967
+ *
1968
+ * The method is equivalent with `transformPoint()` method
1969
+ * of the `DOMMatrix` constructor.
1970
+ *
1971
+ * @copyright thednp © 2021
1972
+ *
1973
+ * @param {DMNS.PointTuple | DOMPoint} v Tuple or DOMPoint
1974
+ * @return {DMNS.PointTuple} the resulting Tuple
1975
+ */
1976
+ transformPoint(v) {
1977
+ const M = this;
1978
+ let m = Translate(v.x, v.y, v.z);
1565
1979
 
1566
- const z2 = Z / 2; let ct = 0; let xbase = 0; let ybase = 0; let sum = 0;
1567
- const Tvalues = [-0.1252, 0.1252, -0.3678, 0.3678,
1568
- -0.5873, 0.5873, -0.7699, 0.7699,
1569
- -0.9041, 0.9041, -0.9816, 0.9816];
1570
- const Cvalues = [0.2491, 0.2491, 0.2335, 0.2335,
1571
- 0.2032, 0.2032, 0.1601, 0.1601,
1572
- 0.1069, 0.1069, 0.0472, 0.0472];
1980
+ m.m44 = v.w || 1;
1981
+ m = M.multiply(m);
1573
1982
 
1574
- Tvalues.forEach((T, i) => {
1575
- ct = z2 * T + z2;
1576
- xbase = base3(x1, x2, x3, x4, ct);
1577
- ybase = base3(y1, y2, y3, y4, ct);
1578
- sum += Cvalues[i] * Math.sqrt(xbase * xbase + ybase * ybase);
1579
- });
1580
- return z2 * sum;
1581
- }
1983
+ return {
1984
+ x: m.m41,
1985
+ y: m.m42,
1986
+ z: m.m43,
1987
+ w: m.m44,
1988
+ };
1989
+ }
1582
1990
 
1583
- // calculates the shape total length
1584
- // equivalent to shape.getTotalLength()
1585
- // pathToCurve version
1586
- function getPathLength(pathArray, round) {
1587
- let totalLength = 0;
1588
- pathToCurve(pathArray, round).forEach((s, i, curveArray) => {
1589
- totalLength += s[0] !== 'M' ? getSegCubicLength.apply(0, curveArray[i - 1].slice(-2).concat(s.slice(1))) : 0;
1590
- });
1591
- return totalLength;
1592
- }
1991
+ /**
1992
+ * Transforms a specified vector using the matrix, returning a new
1993
+ * {x,y,z,w} Tuple *Object* comprising the transformed vector.
1994
+ * Neither the matrix nor the original vector are altered.
1995
+ *
1996
+ * @param {DMNS.PointTuple} t Tuple with `{x,y,z,w}` components
1997
+ * @return {DMNS.PointTuple} the resulting Tuple
1998
+ */
1999
+ transform(t) {
2000
+ const m = this;
2001
+ const x = m.m11 * t.x + m.m12 * t.y + m.m13 * t.z + m.m14 * t.w;
2002
+ const y = m.m21 * t.x + m.m22 * t.y + m.m23 * t.z + m.m24 * t.w;
2003
+ const z = m.m31 * t.x + m.m32 * t.y + m.m33 * t.z + m.m34 * t.w;
2004
+ const w = m.m41 * t.x + m.m42 * t.y + m.m43 * t.z + m.m44 * t.w;
1593
2005
 
1594
- function getDrawDirection(pathArray, round) {
1595
- return getPathArea(pathToCurve(pathArray, round)) >= 0;
2006
+ return {
2007
+ x: x / w,
2008
+ y: y / w,
2009
+ z: z / w,
2010
+ w,
2011
+ };
2012
+ }
1596
2013
  }
1597
2014
 
1598
- // calculates the shape total length
1599
- // almost equivalent to shape.getTotalLength()
1600
- function getPointAtLength(pathArray, length) {
1601
- let totalLength = 0;
1602
- let segLen;
1603
- let data;
1604
- let result;
2015
+ // Add Transform Functions to CSSMatrix object
2016
+ CSSMatrix.Translate = Translate;
2017
+ CSSMatrix.Rotate = Rotate;
2018
+ CSSMatrix.RotateAxisAngle = RotateAxisAngle;
2019
+ CSSMatrix.Scale = Scale;
2020
+ CSSMatrix.SkewX = SkewX;
2021
+ CSSMatrix.SkewY = SkewY;
2022
+ CSSMatrix.Multiply = Multiply;
2023
+ CSSMatrix.fromArray = fromArray;
2024
+ CSSMatrix.fromMatrix = fromMatrix;
2025
+ CSSMatrix.fromString = fromString;
2026
+ CSSMatrix.Version = DMVersion;
1605
2027
 
1606
- return pathToCurve(pathArray, 9).map((seg, i, curveArray) => { // process data
1607
- data = i ? curveArray[i - 1].slice(-2).concat(seg.slice(1)) : seg.slice(1);
1608
- segLen = i ? getSegCubicLength.apply(0, data) : 0;
1609
- totalLength += segLen;
2028
+ /**
2029
+ * Returns a transformation matrix to apply to `<path>` elements.
2030
+ *
2031
+ * @param {SVGPC.transformObject} transform the `transformObject`
2032
+ * @returns {CSSMatrix} a new transformation matrix
2033
+ */
2034
+ function getSVGMatrix(transform) {
2035
+ let matrix = new CSSMatrix();
2036
+ const { origin } = transform;
2037
+ const originX = +origin[0];
2038
+ const originY = +origin[1];
2039
+ const { translate } = transform;
2040
+ const { rotate } = transform;
2041
+ const { skew } = transform;
2042
+ const { scale } = transform;
1610
2043
 
1611
- if (i === 0) {
1612
- result = { x: data[0], y: data[1] };
1613
- } else if (totalLength > length && length > totalLength - segLen) {
1614
- result = getPointAtSegLength.apply(0, data.concat(1 - (totalLength - length) / segLen));
1615
- } else {
1616
- result = null;
2044
+ // set translate
2045
+ if ((Array.isArray(translate) && translate.some((x) => +x !== 0)) || !Number.isNaN(translate)) {
2046
+ matrix = Array.isArray(translate)
2047
+ ? matrix.translate(+translate[0] || 0, +translate[1] || 0, +translate[2] || 0)
2048
+ : matrix.translate(+translate || 0);
2049
+ }
2050
+
2051
+ if (rotate || skew || scale) {
2052
+ // set SVG transform-origin, always defined
2053
+ // matrix = matrix.translate(+originX,+originY,+originZ)
2054
+ matrix = matrix.translate(+originX, +originY);
2055
+
2056
+ // set rotation
2057
+ if (rotate) {
2058
+ matrix = Array.isArray(rotate) && rotate.some((x) => +x !== 0)
2059
+ ? matrix.rotate(+rotate[0] || 0, +rotate[1] || 0, +rotate[2] || 0)
2060
+ : matrix.rotate(+rotate || 0);
2061
+ }
2062
+ // set skew(s)
2063
+ if (Array.isArray(skew) && skew.some((x) => +x !== 0)) {
2064
+ if (Array.isArray(skew)) {
2065
+ matrix = skew[0] ? matrix.skewX(+skew[0] || 0) : matrix;
2066
+ matrix = skew[1] ? matrix.skewY(+skew[1] || 0) : matrix;
2067
+ } else {
2068
+ matrix = matrix.skewX(+skew || 0);
2069
+ }
1617
2070
  }
2071
+ // set scale
2072
+ if (!Number.isNaN(scale) || (Array.isArray(scale) && scale.some((x) => +x !== 1))) {
2073
+ matrix = Array.isArray(scale)
2074
+ ? (matrix.scale(+scale[0] || 1, +scale[1] || 1, +scale[2] || 1))
2075
+ : matrix.scale(+scale || 1);
2076
+ }
2077
+ // set SVG transform-origin
2078
+ // matrix = matrix.translate(-originX,-originY,-originZ)
2079
+ matrix = matrix.translate(-originX, -originY);
2080
+ }
2081
+ return matrix;
2082
+ }
1618
2083
 
1619
- return result;
1620
- }).filter((x) => x).slice(-1)[0]; // isolate last segment
2084
+ /**
2085
+ * Apply a 2D transformation matrix to an ellipse.
2086
+ *
2087
+ * @param {number[]} m the 2D transformation matrix
2088
+ * @param {number} rx ellipse radius X
2089
+ * @param {number} ry ellipse radius Y
2090
+ * @param {number} ax ellipse rotation angle
2091
+ */
2092
+ function transformEllipse(m, rx, ry, ax) {
2093
+ // We consider the current ellipse as image of the unit circle
2094
+ // by first scale(rx,ry) and then rotate(ax) ...
2095
+ // So we apply ma = m x rotate(ax) x scale(rx,ry) to the unit circle.
2096
+ const c = Math.cos((ax * Math.PI) / 180);
2097
+ const s = Math.sin((ax * Math.PI) / 180);
2098
+ const ma = [
2099
+ rx * (m[0] * c + m[2] * s),
2100
+ rx * (m[1] * c + m[3] * s),
2101
+ ry * (-m[0] * s + m[2] * c),
2102
+ ry * (-m[1] * s + m[3] * c),
2103
+ ];
2104
+
2105
+ // ma * transpose(ma) = [ J L ]
2106
+ // [ L K ]
2107
+ // L is calculated later (if the image is not a circle)
2108
+ const J = ma[0] * ma[0] + ma[2] * ma[2];
2109
+ const K = ma[1] * ma[1] + ma[3] * ma[3];
2110
+
2111
+ // the discriminant of the characteristic polynomial of ma * transpose(ma)
2112
+ let D = ((ma[0] - ma[3]) * (ma[0] - ma[3]) + (ma[2] + ma[1]) * (ma[2] + ma[1]))
2113
+ * ((ma[0] + ma[3]) * (ma[0] + ma[3]) + (ma[2] - ma[1]) * (ma[2] - ma[1]));
2114
+
2115
+ // the "mean eigenvalue"
2116
+ const JK = (J + K) / 2;
2117
+
2118
+ // check if the image is (almost) a circle
2119
+ if (D < epsilon * JK) {
2120
+ // if it is
2121
+ const rxy = Math.sqrt(JK);
2122
+
2123
+ return { rx: rxy, ry: rxy, ax: 0 };
2124
+ }
2125
+
2126
+ // if it is not a circle
2127
+ const L = ma[0] * ma[1] + ma[2] * ma[3];
2128
+
2129
+ D = Math.sqrt(D);
2130
+
2131
+ // {l1,l2} = the two eigen values of ma * transpose(ma)
2132
+ const l1 = JK + D / 2;
2133
+ const l2 = JK - D / 2;
2134
+ // the x - axis - rotation angle is the argument of the l1 - eigenvector
2135
+ let AX = (Math.abs(L) < epsilon && Math.abs(l1 - K) < epsilon) ? 90
2136
+ : Math.atan(Math.abs(L) > Math.abs(l1 - K) ? (l1 - J) / L
2137
+ : ((L / (l1 - K))) * 180) / Math.PI;
2138
+ let RX;
2139
+ let RY;
2140
+
2141
+ // if ax > 0 => rx = sqrt(l1), ry = sqrt(l2), else exchange axes and ax += 90
2142
+ if (AX >= 0) {
2143
+ // if ax in [0,90]
2144
+ RX = Math.sqrt(l1);
2145
+ RY = Math.sqrt(l2);
2146
+ } else {
2147
+ // if ax in ]-90,0[ => exchange axes
2148
+ AX += 90;
2149
+ RX = Math.sqrt(l2);
2150
+ RY = Math.sqrt(l1);
2151
+ }
2152
+
2153
+ return { rx: RX, ry: RY, ax: AX };
2154
+ }
2155
+
2156
+ /**
2157
+ * Returns the [x,y] projected coordinates for a given an [x,y] point
2158
+ * and an [x,y,z] perspective origin point.
2159
+ *
2160
+ * Equation found here =>
2161
+ * http://en.wikipedia.org/wiki/3D_projection#Diagram
2162
+ * Details =>
2163
+ * https://stackoverflow.com/questions/23792505/predicted-rendering-of-css-3d-transformed-pixel
2164
+ *
2165
+ * @param {SVGPC.CSSMatrix} m the transformation matrix
2166
+ * @param {Number[]} point2D the initial [x,y] coordinates
2167
+ * @param {number[]} origin the initial [x,y] coordinates
2168
+ * @returns {Number[]} the projected [x,y] coordinates
2169
+ */
2170
+ function projection2d(m, point2D, origin) {
2171
+ const point3D = m.transformPoint({
2172
+ x: point2D[0], y: point2D[1], z: 0, w: 1,
2173
+ });
2174
+ const originX = origin[0] || 0;
2175
+ const originY = origin[1] || 0;
2176
+ const originZ = origin[2] || 0;
2177
+ const relativePositionX = point3D.x - originX;
2178
+ const relativePositionY = point3D.y - originY;
2179
+ const relativePositionZ = point3D.z - originZ;
2180
+
2181
+ return [
2182
+ relativePositionX * (Math.abs(originZ) / Math.abs(relativePositionZ)) + originX,
2183
+ relativePositionY * (Math.abs(originZ) / Math.abs(relativePositionZ)) + originY,
2184
+ ];
2185
+ }
2186
+
2187
+ /**
2188
+ * Apply a 2D / 3D transformation to a `pathArray` instance.
2189
+ *
2190
+ * Since *SVGElement* doesn't support 3D transformation, this function
2191
+ * creates a 2D projection of the <path> element.
2192
+ *
2193
+ * @param {SVGPC.pathArray} path the `pathArray` to apply transformation
2194
+ * @param {any} transform the transform functions `Object`
2195
+ * @returns {SVGPC.pathArray} the resulted `pathArray`
2196
+ */
2197
+ function transformPath(path, transform) {
2198
+ let x = 0; let y = 0; let i; let j; let ii; let jj; let lx; let ly; let te;
2199
+ const absolutePath = pathToAbsolute(path);
2200
+ const normalizedPath = normalizePath(absolutePath);
2201
+ const matrixInstance = getSVGMatrix(transform);
2202
+ const transformProps = Object.keys(transform);
2203
+ const { origin } = transform;
2204
+ const {
2205
+ a, b, c, d, e, f,
2206
+ } = matrixInstance;
2207
+ const matrix2d = [a, b, c, d, e, f];
2208
+ const params = {
2209
+ x1: 0, y1: 0, x2: 0, y2: 0, x: 0, y: 0, qx: null, qy: null,
2210
+ };
2211
+ let segment = [];
2212
+ let seglen = 0;
2213
+ let pathCommand = '';
2214
+ /** @type {any} */
2215
+ let transformedPath = [];
2216
+ const allPathCommands = []; // needed for arc to curve transformation
2217
+ let result = [];
2218
+
2219
+ if (!matrixInstance.isIdentity) {
2220
+ for (i = 0, ii = absolutePath.length; i < ii; i += 1) {
2221
+ segment = absolutePath[i];
2222
+
2223
+ if (absolutePath[i]) [pathCommand] = segment;
2224
+
2225
+ // REPLACE Arc path commands with Cubic Beziers
2226
+ // we don't have any scripting know-how on 3d ellipse transformation
2227
+ /// ////////////////////////////////////////
2228
+ allPathCommands[i] = pathCommand;
2229
+
2230
+ // Arcs don't work very well with 3D transformations or skews
2231
+ if (pathCommand === 'A' && (!matrixInstance.is2D || !['skewX', 'skewY'].find((p) => transformProps.includes(p)))) {
2232
+ segment = segmentToCubic(normalizedPath[i], params);
2233
+
2234
+ absolutePath[i] = segmentToCubic(normalizedPath[i], params);
2235
+ fixArc(absolutePath, allPathCommands, i);
2236
+
2237
+ normalizedPath[i] = segmentToCubic(normalizedPath[i], params);
2238
+ fixArc(normalizedPath, allPathCommands, i);
2239
+ ii = Math.max(absolutePath.length, normalizedPath.length);
2240
+ }
2241
+ /// ////////////////////////////////////////
2242
+
2243
+ segment = normalizedPath[i];
2244
+ seglen = segment.length;
2245
+
2246
+ params.x1 = +segment[seglen - 2];
2247
+ params.y1 = +segment[seglen - 1];
2248
+ params.x2 = +(segment[seglen - 4]) || params.x1;
2249
+ params.y2 = +(segment[seglen - 3]) || params.y1;
2250
+ // @ts-ignore
2251
+ result = { s: absolutePath[i], c: absolutePath[i][0] };
2252
+
2253
+ if (pathCommand !== 'Z') {
2254
+ // @ts-ignore
2255
+ result.x = params.x1;
2256
+ // @ts-ignore
2257
+ result.y = params.y1;
2258
+ }
2259
+ // @ts-ignore
2260
+ transformedPath = transformedPath.concat(result);
2261
+ }
2262
+ // @ts-ignore
2263
+ return transformedPath.map((seg) => {
2264
+ pathCommand = seg.c;
2265
+ segment = seg.s;
2266
+ switch (pathCommand) {
2267
+ case 'A': // only apply to 2D transformations
2268
+ te = transformEllipse(matrix2d, segment[1], segment[2], segment[3]);
2269
+
2270
+ if (matrix2d[0] * matrix2d[3] - matrix2d[1] * matrix2d[2] < 0) {
2271
+ segment[5] = +segment[5] ? 0 : 1;
2272
+ }
2273
+
2274
+ [lx, ly] = projection2d(matrixInstance, [segment[6], segment[7]], origin);
2275
+
2276
+ if ((x === lx && y === ly) || (te.rx < epsilon * te.ry) || (te.ry < epsilon * te.rx)) {
2277
+ segment = ['L', lx, ly];
2278
+ } else {
2279
+ segment = [pathCommand, te.rx, te.ry, te.ax, segment[4], segment[5], lx, ly];
2280
+ }
2281
+
2282
+ x = lx; y = ly;
2283
+ return segment;
2284
+
2285
+ case 'L':
2286
+ case 'H':
2287
+ case 'V':
2288
+
2289
+ [lx, ly] = projection2d(matrixInstance, [seg.x, seg.y], origin);
2290
+
2291
+ if (x !== lx && y !== ly) {
2292
+ segment = ['L', lx, ly];
2293
+ } else if (y === ly) {
2294
+ segment = ['H', lx];
2295
+ } else if (x === lx) {
2296
+ segment = ['V', ly];
2297
+ }
2298
+
2299
+ x = lx; y = ly; // now update x and y
2300
+
2301
+ return segment;
2302
+ default:
2303
+ for (j = 1, jj = segment.length; j < jj; j += 2) {
2304
+ // compute line coordinates without altering previous coordinates
2305
+ [x, y] = projection2d(matrixInstance, [segment[j], segment[j + 1]], origin);
2306
+ segment[j] = x;
2307
+ segment[j + 1] = y;
2308
+ }
2309
+ return segment;
2310
+ }
2311
+ });
2312
+ }
2313
+ return clonePath(absolutePath);
1621
2314
  }
1622
2315
 
1623
- // returns the cubic bezier segment length
2316
+ /**
2317
+ * Returns the cubic-bezier segment length.
2318
+ *
2319
+ * @param {number} p1x the starting point X
2320
+ * @param {number} p1y the starting point Y
2321
+ * @param {number} c1x the first control point X
2322
+ * @param {number} c1y the first control point Y
2323
+ * @param {number} c2x the second control point X
2324
+ * @param {number} c2y the second control point Y
2325
+ * @param {number} p2x the ending point X
2326
+ * @param {number} p2y the ending point Y
2327
+ * @returns {SVGPC.segmentLimits} the length of the cubic-bezier segment
2328
+ */
1624
2329
  function getCubicSize(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y) {
1625
2330
  let a = (c2x - 2 * c1x + p1x) - (p2x - 2 * c2x + c1x);
1626
2331
  let b = 2 * (c1x - p1x) - 2 * (c2x - c1x);
@@ -1669,21 +2374,79 @@ function getCubicSize(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y) {
1669
2374
  };
1670
2375
  }
1671
2376
 
1672
- function getPathBBox(pathArray, round) {
1673
- if (!pathArray) {
1674
- return {
1675
- x: 0, y: 0, width: 0, height: 0, x2: 0, y2: 0,
1676
- };
1677
- }
1678
- const pathCurve = pathToCurve(pathArray, round);
2377
+ /**
2378
+ * Iterates an array to check if it's a `pathArray`
2379
+ * with all C (cubic bezier) segments.
2380
+ *
2381
+ * @param {string | SVGPC.pathArray} path the `Array` to be checked
2382
+ * @returns {boolean} iteration result
2383
+ */
2384
+ function isCurveArray(path) {
2385
+ return Array.isArray(path) && isPathArray(path)
2386
+ && path.slice(1).every((seg) => seg[0] === 'C');
2387
+ }
1679
2388
 
1680
- let x = 0;
1681
- let y = 0;
1682
- let X = [];
1683
- let Y = [];
2389
+ /**
2390
+ * Parses a path string value or 'pathArray' and returns a new one
2391
+ * in which all segments are converted to cubic-bezier.
2392
+ *
2393
+ * @param {string | SVGPC.pathArray} pathInput the string to be parsed or object
2394
+ * @returns {SVGPC.pathArray} the resulted `pathArray` converted to cubic-bezier
2395
+ */
2396
+ function pathToCurve(pathInput) {
2397
+ if (Array.isArray(pathInput) && isCurveArray(pathInput)) {
2398
+ return clonePath(pathInput);
2399
+ }
1684
2400
 
1685
- pathCurve.forEach((segment) => {
1686
- const [s1, s2] = segment.slice(-2);
2401
+ const path = normalizePath(pathInput);
2402
+ const params = {
2403
+ x1: 0, y1: 0, x2: 0, y2: 0, x: 0, y: 0, qx: null, qy: null,
2404
+ };
2405
+ const allPathCommands = [];
2406
+ let pathCommand = '';
2407
+ let ii = path.length;
2408
+
2409
+ for (let i = 0; i < ii; i += 1) {
2410
+ const segment = path[i];
2411
+ const seglen = segment.length;
2412
+ if (segment) [pathCommand] = segment;
2413
+
2414
+ allPathCommands[i] = pathCommand;
2415
+ path[i] = segmentToCubic(segment, params);
2416
+
2417
+ fixArc(path, allPathCommands, i);
2418
+ ii = path.length; // solves curveArrays ending in Z
2419
+
2420
+ params.x1 = +segment[seglen - 2];
2421
+ params.y1 = +segment[seglen - 1];
2422
+ params.x2 = +(segment[seglen - 4]) || params.x1;
2423
+ params.y2 = +(segment[seglen - 3]) || params.y1;
2424
+ }
2425
+
2426
+ return path;
2427
+ }
2428
+
2429
+ /**
2430
+ * Returns the bounding box of a shape.
2431
+ *
2432
+ * @param {SVGPC.pathArray} path the shape `pathArray`
2433
+ * @returns {SVGPC.pathBBox} the length of the cubic-bezier segment
2434
+ */
2435
+ function getPathBBox(path) {
2436
+ if (!path) {
2437
+ return {
2438
+ x: 0, y: 0, width: 0, height: 0, x2: 0, y2: 0,
2439
+ };
2440
+ }
2441
+ const pathCurve = pathToCurve(path);
2442
+
2443
+ let x = 0;
2444
+ let y = 0;
2445
+ let X = [];
2446
+ let Y = [];
2447
+
2448
+ pathCurve.forEach((segment) => {
2449
+ const [s1, s2] = segment.slice(-2);
1687
2450
  if (segment[0] === 'M') {
1688
2451
  x = s1;
1689
2452
  y = s2;
@@ -1706,491 +2469,413 @@ function getPathBBox(pathArray, round) {
1706
2469
  const height = yBot - yTop;
1707
2470
 
1708
2471
  return {
2472
+ width,
2473
+ height,
1709
2474
  x: xTop,
1710
2475
  y: yTop,
1711
2476
  x2: xBot,
1712
2477
  y2: yBot,
1713
- width,
1714
- height,
1715
2478
  cx: xTop + width / 2,
1716
2479
  cy: yTop + height / 2,
1717
2480
  };
1718
2481
  }
1719
2482
 
1720
- function isRelativeArray(pathInput) {
1721
- return isPathArray(pathInput)
1722
- && pathInput.slice(1).every((seg) => seg[0] === seg[0].toLowerCase());
1723
- }
1724
-
1725
- function pathToString(pathArray) {
1726
- return pathArray.map((x) => x[0].concat(x.slice(1).join(' '))).join('');
1727
- }
1728
-
1729
- function splitPath(pathInput) {
1730
- return pathToString(pathToAbsolute(pathInput, 0))
1731
- .replace(/(m|M)/g, '|$1')
1732
- .split('|')
1733
- .map((s) => s.trim())
1734
- .filter((s) => s);
2483
+ /**
2484
+ * Returns the area of a single segment shape.
2485
+ *
2486
+ * http://objectmix.com/graphics/133553-area-closed-bezier-curve.html
2487
+ *
2488
+ * @param {number} x0 the starting point X
2489
+ * @param {number} y0 the starting point Y
2490
+ * @param {number} x1 the first control point X
2491
+ * @param {number} y1 the first control point Y
2492
+ * @param {number} x2 the second control point X
2493
+ * @param {number} y2 the second control point Y
2494
+ * @param {number} x3 the ending point X
2495
+ * @param {number} y3 the ending point Y
2496
+ * @returns {number} the area of the cubic-bezier segment
2497
+ */
2498
+ function getCubicSegArea(x0, y0, x1, y1, x2, y2, x3, y3) {
2499
+ return (3 * ((y3 - y0) * (x1 + x2) - (x3 - x0) * (y1 + y2)
2500
+ + (y1 * (x0 - x2)) - (x1 * (y0 - y2))
2501
+ + (y3 * (x2 + x0 / 3)) - (x3 * (y2 + y0 / 3)))) / 20;
1735
2502
  }
1736
2503
 
1737
- function pathToRelative(pathInput, round) {
1738
- if (isRelativeArray(pathInput)) {
1739
- return clonePath(pathInput);
1740
- }
1741
-
1742
- const pathArray = parsePathString(pathInput, round);
1743
- const ii = pathArray.length;
1744
- const resultArray = [];
1745
- let x = 0;
1746
- let y = 0;
1747
- let mx = 0;
1748
- let my = 0;
1749
- let start = 0;
1750
-
1751
- if (pathArray[0][0] === 'M') {
1752
- x = +pathArray[0][1];
1753
- y = +pathArray[0][2];
1754
- mx = x;
1755
- my = y;
1756
- start += 1;
1757
- resultArray.push(['M', x, y]);
1758
- }
1759
-
1760
- for (let i = start; i < ii; i += 1) {
1761
- const segment = pathArray[i];
1762
- const [pathCommand] = segment;
1763
- const relativeCommand = pathCommand.toLowerCase();
1764
- const relativeSegment = [];
1765
- let newSeg = [];
1766
- resultArray.push(relativeSegment);
1767
-
1768
- if (pathCommand !== relativeCommand) {
1769
- relativeSegment[0] = relativeCommand;
1770
- switch (relativeCommand) {
1771
- case 'a':
1772
- newSeg = segment.slice(1, -2).concat([+segment[6] - x, +segment[7] - y]);
1773
-
1774
- for (let j = 0; j < newSeg.length; j += 1) {
1775
- relativeSegment.push(newSeg[j]);
1776
- }
1777
- break;
1778
- case 'v':
1779
- relativeSegment[1] = +segment[1] - y;
1780
- break;
1781
- default:
1782
- // for is here to stay for eslint
1783
- for (let j = 1; j < segment.length; j += 1) {
1784
- relativeSegment.push(+segment[j] - (j % 2 ? x : y));
1785
- }
1786
-
1787
- if (relativeCommand === 'm') {
1788
- mx = +segment[1];
1789
- my = +segment[2];
1790
- }
1791
- }
1792
- } else {
1793
- if (pathCommand === 'm') {
1794
- mx = +segment[1] + x;
1795
- my = +segment[2] + y;
1796
- }
1797
- for (let j = 0; j < segment.length; j += 1) {
1798
- relativeSegment.push(segment[j]);
1799
- }
1800
- }
1801
-
1802
- const segLength = relativeSegment.length;
1803
- switch (relativeSegment[0]) {
1804
- case 'z':
2504
+ /**
2505
+ * Returns the area of a shape.
2506
+ * @author Jürg Lehni & Jonathan Puckey
2507
+ *
2508
+ * => https://github.com/paperjs/paper.js/blob/develop/src/path/Path.js
2509
+ *
2510
+ * @param {SVGPC.pathArray} path the shape `pathArray`
2511
+ * @returns {SVGPC.pathBBox} the length of the cubic-bezier segment
2512
+ */
2513
+ function getPathArea(path) {
2514
+ let x = 0; let y = 0;
2515
+ let mx = 0; let my = 0;
2516
+ let len = 0;
2517
+ return pathToCurve(path).map((seg) => {
2518
+ switch (seg[0]) {
2519
+ case 'M':
2520
+ case 'Z':
2521
+ mx = seg[0] === 'M' ? seg[1] : mx;
2522
+ my = seg[0] === 'M' ? seg[2] : my;
1805
2523
  x = mx;
1806
2524
  y = my;
1807
- break;
1808
- case 'h':
1809
- x += relativeSegment[segLength - 1];
1810
- break;
1811
- case 'v':
1812
- y += relativeSegment[segLength - 1];
1813
- break;
2525
+ return 0;
1814
2526
  default:
1815
- x += resultArray[i][segLength - 2];
1816
- y += resultArray[i][segLength - 1];
1817
- }
1818
- }
1819
- return roundPath(resultArray, round);
1820
- }
1821
-
1822
- function optimizePath(pathArray, round) {
1823
- const absolutePath = pathToAbsolute(pathArray, round);
1824
- const relativePath = pathToRelative(pathArray, round);
1825
- return absolutePath.map((x, i) => {
1826
- if (i) {
1827
- return x.join('').length < relativePath[i].join('').length ? x : relativePath[i];
2527
+ len = getCubicSegArea.apply(0, [x, y].concat(seg.slice(1)));
2528
+ [x, y] = seg.slice(-2);
2529
+ return len;
1828
2530
  }
1829
- return x;
1830
- });
2531
+ }).reduce((a, b) => a + b, 0);
1831
2532
  }
1832
2533
 
1833
- // reverse CURVE based pathArray segments only
1834
- function reverseCurve(pathArray) {
1835
- const rotatedCurve = pathArray.slice(1)
1836
- .map((x, i, curveOnly) => (!i
1837
- ? pathArray[0].slice(1).concat(x.slice(1))
1838
- : curveOnly[i - 1].slice(-2).concat(x.slice(1))))
1839
- .map((x) => x.map((y, i) => x[x.length - i - 2 * (1 - (i % 2))]))
1840
- .reverse();
1841
-
1842
- return [['M'].concat(rotatedCurve[0]
1843
- .slice(0, 2))]
1844
- .concat(rotatedCurve.map((x) => ['C'].concat(x.slice(2))));
2534
+ function base3(p1, p2, p3, p4, t) {
2535
+ const t1 = -3 * p1 + 9 * p2 - 9 * p3 + 3 * p4;
2536
+ const t2 = t * t1 + 6 * p1 - 12 * p2 + 6 * p3;
2537
+ return t * t2 - 3 * p1 + 3 * p2;
1845
2538
  }
1846
2539
 
1847
- function reversePath(pathString, round) { // pathArray | pathString
1848
- const absolutePath = pathToAbsolute(pathString, round);
1849
- const isClosed = absolutePath.slice(-1)[0][0] === 'Z';
1850
- let reversedPath = [];
1851
- let segLength = 0;
2540
+ /**
2541
+ * Returns the C (cubic-bezier) segment length.
2542
+ *
2543
+ * @param {Number} x1 the starting point X
2544
+ * @param {Number} y1 the starting point Y
2545
+ * @param {Number} x2 the first control point X
2546
+ * @param {Number} y2 the first control point Y
2547
+ * @param {Number} x3 the second control point X
2548
+ * @param {Number} y3 the second control point Y
2549
+ * @param {Number} x4 the ending point X
2550
+ * @param {Number} y4 the ending point Y
2551
+ * @param {Number} z a [0-1] ratio
2552
+ * @returns {Number} the cubic-bezier segment length
2553
+ */
2554
+ function getSegCubicLength(x1, y1, x2, y2, x3, y3, x4, y4, z) {
2555
+ let Z;
2556
+ if (z === null || Number.isNaN(+z)) Z = 1;
1852
2557
 
1853
- reversedPath = normalizePath(absolutePath, round).map((segment, i) => {
1854
- segLength = segment.length;
1855
- return {
1856
- seg: absolutePath[i], // absolute
1857
- n: segment, // normalized
1858
- c: absolutePath[i][0], // pathCommand
1859
- x: segment[segLength - 2], // x
1860
- y: segment[segLength - 1], // y
1861
- };
1862
- }).map((seg, i, pathArray) => {
1863
- const segment = seg.seg;
1864
- const data = seg.n;
1865
- const prevSeg = i && pathArray[i - 1];
1866
- const nextSeg = pathArray[i + 1] && pathArray[i + 1];
1867
- const pathCommand = seg.c;
1868
- const pLen = pathArray.length;
1869
- const x = i ? pathArray[i - 1].x : pathArray[pLen - 1].x;
1870
- const y = i ? pathArray[i - 1].y : pathArray[pLen - 1].y;
1871
- let result = [];
2558
+ // Z = Z > 1 ? 1 : Z < 0 ? 0 : Z;
2559
+ if (Z > 1) Z = 1;
2560
+ if (Z < 0) Z = 0;
1872
2561
 
1873
- switch (pathCommand) {
1874
- case 'M':
1875
- result = isClosed ? ['Z'] : [pathCommand, x, y];
1876
- break;
1877
- case 'A':
1878
- result = segment.slice(0, -3).concat([(segment[5] === 1 ? 0 : 1), x, y]);
1879
- break;
1880
- case 'C':
1881
- if (nextSeg && nextSeg.c === 'S') {
1882
- result = ['S', segment[1], segment[2], x, y];
1883
- } else {
1884
- result = [pathCommand, segment[3], segment[4], segment[1], segment[2], x, y];
1885
- }
1886
- break;
1887
- case 'S':
1888
- if ((prevSeg && 'CS'.indexOf(prevSeg.c) > -1) && (!nextSeg || (nextSeg && nextSeg.c !== 'S'))) {
1889
- result = ['C', data[3], data[4], data[1], data[2], x, y];
1890
- } else {
1891
- result = [pathCommand, data[1], data[2], x, y];
1892
- }
1893
- break;
1894
- case 'Q':
1895
- if (nextSeg && nextSeg.c === 'T') {
1896
- result = ['T', x, y];
1897
- } else {
1898
- result = segment.slice(0, -2).concat([x, y]);
1899
- }
1900
- break;
1901
- case 'T':
1902
- if ((prevSeg && 'QT'.indexOf(prevSeg.c) > -1) && (!nextSeg || (nextSeg && nextSeg.c !== 'T'))) {
1903
- result = ['Q', data[1], data[2], x, y];
1904
- } else {
1905
- result = [pathCommand, x, y];
1906
- }
1907
- break;
1908
- case 'Z':
1909
- result = ['M', x, y];
1910
- break;
1911
- case 'H':
1912
- result = [pathCommand, x];
1913
- break;
1914
- case 'V':
1915
- result = [pathCommand, y];
1916
- break;
1917
- default:
1918
- result = segment.slice(0, -2).concat([x, y]);
1919
- }
2562
+ const z2 = Z / 2; let ct = 0; let xbase = 0; let ybase = 0; let sum = 0;
2563
+ const Tvalues = [-0.1252, 0.1252, -0.3678, 0.3678,
2564
+ -0.5873, 0.5873, -0.7699, 0.7699,
2565
+ -0.9041, 0.9041, -0.9816, 0.9816];
2566
+ const Cvalues = [0.2491, 0.2491, 0.2335, 0.2335,
2567
+ 0.2032, 0.2032, 0.1601, 0.1601,
2568
+ 0.1069, 0.1069, 0.0472, 0.0472];
1920
2569
 
1921
- return result;
2570
+ Tvalues.forEach((T, i) => {
2571
+ ct = z2 * T + z2;
2572
+ xbase = base3(x1, x2, x3, x4, ct);
2573
+ ybase = base3(y1, y2, y3, y4, ct);
2574
+ sum += Cvalues[i] * Math.sqrt(xbase * xbase + ybase * ybase);
1922
2575
  });
1923
-
1924
- return isClosed ? reversedPath.reverse()
1925
- : [reversedPath[0]].concat(reversedPath.slice(1).reverse());
2576
+ return z2 * sum;
1926
2577
  }
1927
2578
 
1928
- var epsilon = 1e-9;
1929
-
1930
- function getSVGMatrix(transformObject) {
1931
- let matrix = new CSS3Matrix();
1932
- const { origin } = transformObject;
1933
- const originX = +origin[0];
1934
- const originY = +origin[1];
1935
- // originZ = +origin[2] || originX, // maybe later. maybe not required
1936
- // perspective = transformObject.perspective,
1937
- const { translate } = transformObject;
1938
- const { rotate } = transformObject;
1939
- const { skew } = transformObject;
1940
- const { scale } = transformObject;
2579
+ /**
2580
+ * Returns the shape total length,
2581
+ * or the equivalent to `shape.getTotalLength()`
2582
+ * pathToCurve version
2583
+ *
2584
+ * @param {SVGPC.pathArray} path the ending point Y
2585
+ * @returns {Number} the shape total length
2586
+ */
2587
+ function getPathLength(path) {
2588
+ let totalLength = 0;
2589
+ pathToCurve(path).forEach((s, i, curveArray) => {
2590
+ totalLength += s[0] === 'M' ? 0
2591
+ : getSegCubicLength.apply(0, curveArray[i - 1].slice(-2).concat(s.slice(1)));
2592
+ });
2593
+ return totalLength;
2594
+ }
1941
2595
 
1942
- // !isNaN(perspective) && perspective && (matrix.m34 = -1/perspective)
2596
+ /**
2597
+ * Check if a path is drawn clockwise and returns true if so,
2598
+ * false otherwise.
2599
+ *
2600
+ * @param {string | SVGPC.pathArray} path the path string or `pathArray`
2601
+ * @returns {boolean} true when clockwise or false if not
2602
+ */
2603
+ function getDrawDirection(path) {
2604
+ return getPathArea(pathToCurve(path)) >= 0;
2605
+ }
1943
2606
 
1944
- // set translate
1945
- if (!Number.isNaN(translate) || (Array.isArray(translate) && translate.some((x) => +x !== 0))) {
1946
- matrix = Array.isArray(translate)
1947
- ? matrix.translate(+translate[0] || 0, +translate[1] || 0, +translate[2] || 0)
1948
- : matrix.translate(+translate || 0, 0, 0);
1949
- }
2607
+ /**
2608
+ * Returns [x,y] coordinates of a point at a given length of a shape.
2609
+ *
2610
+ * @param {string | SVGPC.pathArray} path the `pathArray` to look into
2611
+ * @param {Number} length the length of the shape to look at
2612
+ * @returns {Number[]} the requested [x,y] coordinates
2613
+ */
2614
+ function getPointAtLength(path, length) {
2615
+ let totalLength = 0;
2616
+ let segLen;
2617
+ let data;
2618
+ let result;
1950
2619
 
1951
- if (rotate || skew || scale) {
1952
- // set SVG transform-origin, always defined
1953
- // matrix = matrix.translate(+originX,+originY,+originZ)
1954
- matrix = matrix.translate(+originX, +originY);
2620
+ return pathToCurve(path).map((seg, i, curveArray) => {
2621
+ data = i ? curveArray[i - 1].slice(-2).concat(seg.slice(1)) : seg.slice(1);
2622
+ segLen = i ? getSegCubicLength.apply(0, data) : 0;
2623
+ totalLength += segLen;
1955
2624
 
1956
- // set rotation
1957
- if (rotate) {
1958
- matrix = Array.isArray(rotate) && rotate.some((x) => +x !== 0)
1959
- ? matrix.rotate(+rotate[0] || 0, +rotate[1] || 0, +rotate[2] || 0)
1960
- : matrix.rotate(+rotate || 0);
1961
- }
1962
- // set skew(s)
1963
- if (Array.isArray(skew) && skew.some((x) => +x !== 0)) {
1964
- if (Array.isArray(skew)) {
1965
- matrix = skew[0] ? matrix.skewX(+skew[0] || 0) : matrix;
1966
- matrix = skew[1] ? matrix.skewY(+skew[1] || 0) : matrix;
1967
- } else {
1968
- matrix = matrix.skewX(+skew || 0);
1969
- }
1970
- }
1971
- // set scale
1972
- if (!Number.isNaN(scale) || (Array.isArray(scale) && scale.some((x) => +x !== 1))) {
1973
- matrix = Array.isArray(scale)
1974
- ? (matrix.scale(+scale[0] || 1, +scale[1] || 1, +scale[2] || 1))
1975
- : matrix.scale(+scale || 1);
2625
+ if (i === 0) {
2626
+ result = { x: data[0], y: data[1] };
2627
+ } else if (totalLength > length && length > totalLength - segLen) {
2628
+ result = getPointAtSegLength.apply(0, data.concat(1 - (totalLength - length) / segLen));
2629
+ } else {
2630
+ result = null;
1976
2631
  }
1977
- // set SVG transform-origin
1978
- // matrix = matrix.translate(-originX,-originY,-originZ)
1979
- matrix = matrix.translate(-originX, -originY);
1980
- }
1981
- return matrix;
1982
- }
1983
2632
 
1984
- function transformEllipse(m, rx, ry, ax) {
1985
- // We consider the current ellipse as image of the unit circle
1986
- // by first scale(rx,ry) and then rotate(ax) ...
1987
- // So we apply ma = m x rotate(ax) x scale(rx,ry) to the unit circle.
1988
- const c = Math.cos((ax * Math.PI) / 180);
1989
- const s = Math.sin((ax * Math.PI) / 180);
1990
- const ma = [
1991
- rx * (m[0] * c + m[2] * s),
1992
- rx * (m[1] * c + m[3] * s),
1993
- ry * (-m[0] * s + m[2] * c),
1994
- ry * (-m[1] * s + m[3] * c),
1995
- ];
1996
-
1997
- // ma * transpose(ma) = [ J L ]
1998
- // [ L K ]
1999
- // L is calculated later (if the image is not a circle)
2000
- const J = ma[0] * ma[0] + ma[2] * ma[2];
2001
- const K = ma[1] * ma[1] + ma[3] * ma[3];
2633
+ return result;
2634
+ }).filter((x) => x).slice(-1)[0]; // isolate last segment
2635
+ }
2002
2636
 
2003
- // the discriminant of the characteristic polynomial of ma * transpose(ma)
2004
- let D = ((ma[0] - ma[3]) * (ma[0] - ma[3]) + (ma[2] + ma[1]) * (ma[2] + ma[1]))
2005
- * ((ma[0] + ma[3]) * (ma[0] + ma[3]) + (ma[2] - ma[1]) * (ma[2] - ma[1]));
2637
+ /**
2638
+ * Parses a path string value to determine its validity
2639
+ * then returns true if it's valid or false otherwise.
2640
+ *
2641
+ * @param {string} pathString the path string to be parsed
2642
+ * @returns {boolean} the path string validity
2643
+ */
2644
+ function isValidPath(pathString) {
2645
+ if (typeof pathString !== 'string') {
2646
+ return false;
2647
+ }
2006
2648
 
2007
- // the "mean eigenvalue"
2008
- const JK = (J + K) / 2;
2649
+ const path = new SVGPathArray(pathString);
2009
2650
 
2010
- // check if the image is (almost) a circle
2011
- if (D < epsilon * JK) {
2012
- // if it is
2013
- const rxy = Math.sqrt(JK);
2651
+ skipSpaces(path);
2014
2652
 
2015
- return { rx: rxy, ry: rxy, ax: 0 };
2653
+ while (path.index < path.max && !path.err.length) {
2654
+ scanSegment(path);
2016
2655
  }
2017
2656
 
2018
- // if it is not a circle
2019
- const L = ma[0] * ma[1] + ma[2] * ma[3];
2657
+ return !path.err.length && 'mM'.includes(path.segments[0][0]);
2658
+ }
2020
2659
 
2021
- D = Math.sqrt(D);
2660
+ /**
2661
+ * Supported shapes and their specific parameters.
2662
+ */
2663
+ const shapeParams = {
2664
+ circle: ['cx', 'cy', 'r'],
2665
+ ellipse: ['cx', 'cy', 'rx', 'ry'],
2666
+ rect: ['width', 'height', 'x', 'y', 'rx', 'ry'],
2667
+ polygon: ['points'],
2668
+ polyline: ['points'],
2669
+ glyph: [],
2670
+ };
2022
2671
 
2023
- // {l1,l2} = the two eigen values of ma * transpose(ma)
2024
- const l1 = JK + D / 2;
2025
- const l2 = JK - D / 2;
2026
- // the x - axis - rotation angle is the argument of the l1 - eigenvector
2027
- let AX = (Math.abs(L) < epsilon && Math.abs(l1 - K) < epsilon) ? 90
2028
- : Math.atan(Math.abs(L) > Math.abs(l1 - K) ? (l1 - J) / L
2029
- : ((L / (l1 - K))) * 180) / Math.PI;
2030
- let RX;
2031
- let RY;
2672
+ /**
2673
+ * Returns a new `pathArray` from line attributes.
2674
+ *
2675
+ * @param {SVGPC.lineAttr} attr shape configuration
2676
+ * @return {SVGPC.pathArray} a new line `pathArray`
2677
+ */
2678
+ function getLinePath(attr) {
2679
+ const {
2680
+ x1, y1, x2, y2,
2681
+ } = attr;
2682
+ return [['M', +x1, +y1], ['L', +x2, +y2]];
2683
+ }
2032
2684
 
2033
- // if ax > 0 => rx = sqrt(l1), ry = sqrt(l2), else exchange axes and ax += 90
2034
- if (AX >= 0) {
2035
- // if ax in [0,90]
2036
- RX = Math.sqrt(l1);
2037
- RY = Math.sqrt(l2);
2038
- } else {
2039
- // if ax in ]-90,0[ => exchange axes
2040
- AX += 90;
2041
- RX = Math.sqrt(l2);
2042
- RY = Math.sqrt(l1);
2685
+ /**
2686
+ * Returns a new `pathArray` like from polyline/polygon attributes.
2687
+ *
2688
+ * @param {SVGPC.polyAttr} attr shape configuration
2689
+ * @return {SVGPC.pathArray} a new polygon/polyline `pathArray`
2690
+ */
2691
+ function getPolyPath(attr) {
2692
+ /** @type {SVGPC.pathArray} */
2693
+ const pathArray = [];
2694
+ const points = attr.points.split(/[\s|,]/).map(Number);
2695
+
2696
+ let index = 0;
2697
+ while (index < points.length) {
2698
+ pathArray.push([(index ? 'L' : 'M'), (points[index]), (points[index + 1])]);
2699
+ index += 2;
2043
2700
  }
2044
2701
 
2045
- return { rx: RX, ry: RY, ax: AX };
2702
+ return attr.type === 'polygon' ? pathArray.concat([['z']]) : pathArray;
2046
2703
  }
2047
2704
 
2048
- // Given an xyz point and an xyz perspective origin point,
2049
- // this will return the xy projected location
2050
- // Using the equation found here: http://en.wikipedia.org/wiki/3D_projection#Diagram
2051
- // https://stackoverflow.com/questions/23792505/predicted-rendering-of-css-3d-transformed-pixel
2052
-
2053
- function projection2d(m, point2D, origin) {
2054
- const point3D = m.transformPoint({
2055
- x: point2D[0], y: point2D[1], z: 0, w: 1,
2056
- });
2057
- const originX = origin[0] || 0;
2058
- const originY = origin[1] || 0;
2059
- const originZ = origin[2] || 0;
2060
- const relativePositionX = point3D.x - originX;
2061
- const relativePositionY = point3D.y - originY;
2062
- const relativePositionZ = point3D.z - originZ;
2705
+ /**
2706
+ * Returns a new `pathArray` from circle attributes.
2707
+ *
2708
+ * @param {SVGPC.circleAttr} attr shape configuration
2709
+ * @return {SVGPC.pathArray} a circle `pathArray`
2710
+ */
2711
+ function getCirclePath(attr) {
2712
+ const {
2713
+ cx, cy, r,
2714
+ } = attr;
2063
2715
 
2064
2716
  return [
2065
- relativePositionX * (Math.abs(originZ) / Math.abs(relativePositionZ)) + originX,
2066
- relativePositionY * (Math.abs(originZ) / Math.abs(relativePositionZ)) + originY,
2717
+ ['M', (cx - r), cy],
2718
+ ['a', r, r, 0, 1, 0, (2 * r), 0],
2719
+ ['a', r, r, 0, 1, 0, (-2 * r), 0],
2067
2720
  ];
2068
2721
  }
2069
2722
 
2070
- function transformPath(pathArray, transformObject, round) {
2071
- let x; let y; let i; let j; let ii; let jj; let lx; let ly; let te;
2072
- const absolutePath = pathToAbsolute(pathArray);
2073
- const normalizedPath = normalizePath(absolutePath);
2074
- const matrixInstance = getSVGMatrix(transformObject);
2075
- const transformProps = Object.keys(transformObject);
2076
- const { origin } = transformObject;
2723
+ /**
2724
+ * Returns a new `pathArray` from ellipse attributes.
2725
+ *
2726
+ * @param {SVGPC.ellipseAttr} attr shape configuration
2727
+ * @return {SVGPC.pathArray} an ellipse `pathArray`
2728
+ */
2729
+ function getEllipsePath(attr) {
2077
2730
  const {
2078
- a, b, c, d, e, f,
2079
- } = matrixInstance;
2080
- const matrix2d = [a, b, c, d, e, f];
2081
- const params = {
2082
- x1: 0, y1: 0, x2: 0, y2: 0, x: 0, y: 0,
2083
- };
2084
- let segment = [];
2085
- let seglen = 0;
2086
- let pathCommand = '';
2087
- let transformedPath = [];
2088
- const allPathCommands = []; // needed for arc to curve transformation
2089
- let result = [];
2090
-
2091
- if (!matrixInstance.isIdentity) {
2092
- for (i = 0, ii = absolutePath.length; i < ii; i += 1) {
2093
- segment = absolutePath[i];
2731
+ cx, cy, rx, ry,
2732
+ } = attr;
2094
2733
 
2095
- if (absolutePath[i]) [pathCommand] = segment;
2734
+ return [
2735
+ ['M', (cx - rx), cy],
2736
+ ['a', rx, ry, 0, 1, 0, (2 * rx), 0],
2737
+ ['a', rx, ry, 0, 1, 0, (-2 * rx), 0],
2738
+ ];
2739
+ }
2096
2740
 
2097
- // REPLACE Arc path commands with Cubic Beziers
2098
- // we don't have any scripting know-how on 3d ellipse transformation
2099
- /// ////////////////////////////////////////
2100
- allPathCommands[i] = pathCommand;
2741
+ /**
2742
+ * Returns a new `pathArray` like from rect attributes.
2743
+ *
2744
+ * @param {SVGPC.rectAttr} attr object with properties above
2745
+ * @return {SVGPC.pathArray} a new `pathArray` from `<rect>` attributes
2746
+ */
2747
+ function getRectanglePath(attr) {
2748
+ const x = +attr.x || 0;
2749
+ const y = +attr.y || 0;
2750
+ const w = +attr.width;
2751
+ const h = +attr.height;
2752
+ let rx = +attr.rx;
2753
+ let ry = +attr.ry;
2754
+
2755
+ // Validity checks from http://www.w3.org/TR/SVG/shapes.html#RectElement:
2756
+ if (rx || ry) {
2757
+ rx = !rx ? ry : rx;
2758
+ ry = !ry ? rx : ry;
2759
+
2760
+ if (rx * 2 > w) rx -= (rx * 2 - w) / 2;
2761
+ if (ry * 2 > h) ry -= (ry * 2 - h) / 2;
2762
+
2763
+ return [
2764
+ ['M', x + rx, y],
2765
+ ['h', w - rx * 2],
2766
+ ['s', rx, 0, rx, ry],
2767
+ ['v', h - ry * 2],
2768
+ ['s', 0, ry, -rx, ry],
2769
+ ['h', -w + rx * 2],
2770
+ ['s', -rx, 0, -rx, -ry],
2771
+ ['v', -h + ry * 2],
2772
+ ['s', 0, -ry, rx, -ry],
2773
+ ];
2774
+ }
2101
2775
 
2102
- // Arcs don't work very well with 3D transformations or skews
2103
- if (pathCommand === 'A' && (!matrixInstance.is2D || !['skewX', 'skewY'].find((p) => transformProps.includes(p)))) {
2104
- segment = segmentToCubic(normalizedPath[i], params);
2776
+ return [
2777
+ ['M', x, y],
2778
+ ['h', w],
2779
+ ['v', h],
2780
+ ['H', x],
2781
+ ['Z'],
2782
+ ];
2783
+ }
2105
2784
 
2106
- absolutePath[i] = segmentToCubic(normalizedPath[i], params);
2107
- fixArc(absolutePath, allPathCommands, i);
2785
+ /**
2786
+ * Returns a new `<path>` element created from attributes of a `<line>`, `<polyline>`,
2787
+ * `<polygon>`, `<rect>`, `<ellipse>`, `<circle>` or `<glyph>`. If `replace` parameter
2788
+ * is `true`, it will replace the target.
2789
+ *
2790
+ * The newly created `<path>` element keeps all non-specific
2791
+ * attributes like `class`, `fill`, etc.
2792
+ *
2793
+ * @param {SVGPC.shapeTypes} element target shape
2794
+ * @param {boolean} replace option to replace target
2795
+ * @return {?SVGPathElement} the newly created `<path>` element
2796
+ */
2797
+ function shapeToPath(element, replace) {
2798
+ const supportedShapes = Object.keys(shapeParams).concat(['glyph']);
2108
2799
 
2109
- normalizedPath[i] = segmentToCubic(normalizedPath[i], params);
2110
- fixArc(normalizedPath, allPathCommands, i);
2111
- ii = Math.max(absolutePath.length, normalizedPath.length);
2112
- }
2113
- /// ////////////////////////////////////////
2800
+ if (!supportedShapes.some((s) => element.tagName === s)) {
2801
+ throw TypeError(`shapeToPath: ${element} is not SVGElement`);
2802
+ }
2114
2803
 
2115
- segment = normalizedPath[i];
2116
- seglen = segment.length;
2804
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
2805
+ const type = element.tagName;
2806
+ /** @type {string[]} */
2807
+ const shapeAttrs = shapeParams[type];
2117
2808
 
2118
- params.x1 = +segment[seglen - 2];
2119
- params.y1 = +segment[seglen - 1];
2120
- params.x2 = +(segment[seglen - 4]) || params.x1;
2121
- params.y2 = +(segment[seglen - 3]) || params.y1;
2809
+ // set config
2810
+ const config = {};
2811
+ config.type = type;
2812
+ shapeAttrs.forEach((p) => { config[p] = element.getAttribute(p); });
2122
2813
 
2123
- result = { s: absolutePath[i], c: absolutePath[i][0] };
2814
+ // set no-specific shape attributes: fill, stroke, etc
2815
+ Object.values(element.attributes).forEach(({ name, value }) => {
2816
+ if (!shapeAttrs.includes(name)) path.setAttribute(name, value);
2817
+ });
2124
2818
 
2125
- if (pathCommand !== 'Z') {
2126
- result.x = params.x1;
2127
- result.y = params.y1;
2128
- }
2129
- transformedPath = transformedPath.concat(result);
2819
+ // set d
2820
+ let description;
2821
+ if (type === 'circle') description = pathToString(getCirclePath(config));
2822
+ else if (type === 'ellipse') description = pathToString(getEllipsePath(config));
2823
+ else if (['polyline', 'polygon'].includes(type)) description = pathToString(getPolyPath(config));
2824
+ else if (type === 'rect') description = pathToString(getRectanglePath(config));
2825
+ else if (type === 'line') description = pathToString(getLinePath(config));
2826
+ else if (type === 'glyph') description = element.getAttribute('d');
2827
+
2828
+ // replace target element
2829
+ if (description) {
2830
+ path.setAttribute('d', description);
2831
+ if (replace) {
2832
+ element.before(path, element);
2833
+ element.remove();
2130
2834
  }
2835
+ return path;
2836
+ }
2837
+ return null;
2838
+ }
2131
2839
 
2132
- transformedPath = transformedPath.map((seg) => {
2133
- pathCommand = seg.c;
2134
- segment = seg.s;
2135
- switch (pathCommand) {
2136
- case 'A': // only apply to 2D transformations
2137
- te = transformEllipse(matrix2d, segment[1], segment[2], segment[3]);
2138
-
2139
- if (matrix2d[0] * matrix2d[3] - matrix2d[1] * matrix2d[2] < 0) {
2140
- segment[5] = +segment[5] ? 0 : 1;
2141
- }
2142
-
2143
- [lx, ly] = projection2d(matrixInstance, [segment[6], segment[7]], origin);
2144
-
2145
- if ((x === lx && y === ly) || (te.rx < epsilon * te.ry) || (te.ry < epsilon * te.rx)) {
2146
- segment = ['L', lx, ly];
2147
- } else {
2148
- segment = [pathCommand, te.rx, te.ry, te.ax, segment[4], segment[5], lx, ly];
2149
- }
2150
-
2151
- x = lx; y = ly;
2152
- return segment;
2153
-
2154
- case 'L':
2155
- case 'H':
2156
- case 'V':
2840
+ /**
2841
+ * Reverses all segments and their values from a `pathArray`
2842
+ * which consists of only C (cubic-bezier) path commands.
2843
+ *
2844
+ * @param {SVGPC.pathArray} path the source `pathArray`
2845
+ * @returns {SVGPC.pathArray} the reversed `pathArray`
2846
+ */
2847
+ function reverseCurve(path) {
2848
+ const rotatedCurve = path.slice(1)
2849
+ .map((x, i, curveOnly) => (!i
2850
+ ? path[0].slice(1).concat(x.slice(1))
2851
+ : curveOnly[i - 1].slice(-2).concat(x.slice(1))))
2852
+ .map((x) => x.map((_, i) => x[x.length - i - 2 * (1 - (i % 2))]))
2853
+ .reverse();
2157
2854
 
2158
- [lx, ly] = projection2d(matrixInstance, [seg.x, seg.y], origin);
2855
+ return [['M'].concat(rotatedCurve[0]
2856
+ .slice(0, 2))]
2857
+ .concat(rotatedCurve.map((x) => ['C'].concat(x.slice(2))));
2858
+ }
2159
2859
 
2160
- if (x !== lx && y !== ly) {
2161
- segment = ['L', lx, ly];
2162
- } else if (y === ly) {
2163
- segment = ['H', lx];
2164
- } else if (x === lx) {
2165
- segment = ['V', ly];
2166
- }
2860
+ var version = "0.1.10alpha2";
2167
2861
 
2168
- x = lx; y = ly; // now update x and y
2862
+ // @ts-ignore
2169
2863
 
2170
- return segment;
2171
- default:
2172
- for (j = 1, jj = segment.length; j < jj; j += 2) {
2173
- // compute line coordinates without altering previous coordinates
2174
- [x, y] = projection2d(matrixInstance, [segment[j], segment[j + 1]], origin);
2175
- segment[j] = x;
2176
- segment[j + 1] = y;
2177
- }
2178
- return segment;
2179
- }
2180
- });
2181
- return roundPath(transformedPath, round);
2182
- }
2183
- return clonePath(absolutePath);
2184
- }
2864
+ /**
2865
+ * A global namespace for library version.
2866
+ * @type {string}
2867
+ */
2868
+ const Version = version;
2185
2869
 
2186
- var util = {
2187
- CSSMatrix: CSS3Matrix,
2870
+ const Util = {
2871
+ CSSMatrix,
2188
2872
  parsePathString,
2189
2873
  isPathArray,
2190
2874
  isCurveArray,
2191
2875
  isAbsoluteArray,
2192
2876
  isRelativeArray,
2193
2877
  isNormalizedArray,
2878
+ isValidPath,
2194
2879
  pathToAbsolute,
2195
2880
  pathToRelative,
2196
2881
  pathToCurve,
@@ -2209,101 +2894,184 @@ var util = {
2209
2894
  normalizePath,
2210
2895
  transformPath,
2211
2896
  getSVGMatrix,
2897
+ shapeToPath,
2212
2898
  options: SVGPCO,
2899
+ Version,
2213
2900
  };
2214
2901
 
2902
+ // import isPathArray from './util/isPathArray';
2903
+
2904
+ /**
2905
+ * Creates a new SVGPathCommander instance.
2906
+ *
2907
+ * @author thednp <https://github.com/thednp/svg-path-commander>
2908
+ * @class
2909
+ */
2215
2910
  class SVGPathCommander {
2216
- constructor(pathValue, ops) {
2217
- const options = ops || {};
2218
- // check for either true or > 0
2219
- const roundOption = +options.round === 0 || options.round === false ? 0 : SVGPCO.round;
2220
- const decimalsOption = roundOption && (options.decimals || SVGPCO.decimals);
2221
- const originOption = options.origin;
2911
+ /**
2912
+ * @constructor
2913
+ * @param {string} pathValue the path string
2914
+ * @param {any} config instance options
2915
+ */
2916
+ constructor(pathValue, config) {
2917
+ const options = config || {};
2918
+
2919
+ let { round } = SVGPCO;
2920
+ const { round: roundOption } = options;
2921
+ if ((roundOption && +roundOption === 0) || roundOption === false) {
2922
+ round = 0;
2923
+ }
2924
+
2925
+ const { decimals } = round && (options || SVGPCO);
2222
2926
 
2223
2927
  // set instance options
2224
- this.round = roundOption === 0 ? 0 : decimalsOption; // ZERO will disable rounding numbers
2225
- this.origin = originOption && !Number.isNaN(originOption.x) && !Number.isNaN(originOption.y)
2226
- ? originOption : null;
2928
+ this.round = round === 0 ? 0 : decimals;
2929
+ // ZERO | FALSE will disable rounding numbers
2227
2930
 
2228
- const path = parsePathString(pathValue, this.round);
2229
- this.segments = clonePath(path);
2931
+ /** @type {SVGPC.pathArray} */
2932
+ this.segments = parsePathString(pathValue);
2933
+
2934
+ /** * @type {string} */
2230
2935
  this.pathValue = pathValue;
2936
+
2231
2937
  return this;
2232
2938
  }
2233
- }
2234
2939
 
2235
- const SVGPCProto = SVGPathCommander.prototype;
2940
+ /**
2941
+ * Convert path to absolute values
2942
+ * @public
2943
+ */
2944
+ toAbsolute() {
2945
+ const { segments } = this;
2946
+ this.segments = pathToAbsolute(segments);
2947
+ return this;
2948
+ }
2236
2949
 
2237
- SVGPCProto.toAbsolute = function toAbsolute() {
2238
- const path = pathToAbsolute(this.segments, this.round);
2239
- this.segments = clonePath(path);
2240
- return this;
2241
- };
2242
- SVGPCProto.toRelative = function toRelative() {
2243
- const path = pathToRelative(this.segments, this.round);
2244
- this.segments = clonePath(path);
2245
- return this;
2246
- };
2247
- SVGPCProto.reverse = function reverse(onlySubpath) {
2248
- this.toAbsolute();
2249
-
2250
- const subPath = splitPath(this.pathValue).length > 1 && splitPath(this.toString());
2251
- const absoluteMultiPath = subPath && clonePath(subPath)
2252
- .map((x, i) => {
2253
- if (onlySubpath) {
2254
- return i ? reversePath(x) : parsePathString(x);
2255
- }
2256
- return reversePath(x);
2257
- });
2950
+ /**
2951
+ * Convert path to relative values
2952
+ * @public
2953
+ */
2954
+ toRelative() {
2955
+ const { segments } = this;
2956
+ this.segments = pathToRelative(segments);
2957
+ return this;
2958
+ }
2258
2959
 
2259
- let path = [];
2260
- if (subPath) {
2261
- path = absoluteMultiPath.flat(1);
2262
- } else {
2263
- path = onlySubpath ? this.segments : reversePath(this.segments, this.round);
2960
+ /**
2961
+ * Reverse the order of the segments and their values.
2962
+ * @param {boolean | number} onlySubpath option to reverse all sub-paths except first
2963
+ * @public
2964
+ */
2965
+ reverse(onlySubpath) {
2966
+ this.toAbsolute();
2967
+
2968
+ const { segments } = this;
2969
+ const split = splitPath(this.toString());
2970
+ const subPath = split.length > 1 ? split : 0;
2971
+
2972
+ const absoluteMultiPath = subPath && clonePath(subPath)
2973
+ .map((x, i) => {
2974
+ if (onlySubpath) {
2975
+ // @ts-ignore
2976
+ return i ? reversePath(x) : parsePathString(x);
2977
+ }
2978
+ // @ts-ignore
2979
+ return reversePath(x);
2980
+ });
2981
+
2982
+ let path = [];
2983
+ if (subPath) {
2984
+ // @ts-ignore
2985
+ path = absoluteMultiPath.flat(1);
2986
+ } else {
2987
+ path = onlySubpath ? segments : reversePath(segments);
2988
+ }
2989
+
2990
+ this.segments = clonePath(path);
2991
+ return this;
2264
2992
  }
2265
2993
 
2266
- this.segments = clonePath(path);
2267
- return this;
2268
- };
2269
- SVGPCProto.normalize = function normalize() {
2270
- const path = normalizePath(this.segments, this.round);
2271
- this.segments = clonePath(path);
2272
- return this;
2273
- };
2274
- SVGPCProto.optimize = function optimize() {
2275
- const path = optimizePath(this.segments, this.round);
2276
- this.segments = clonePath(path);
2277
- return this;
2278
- };
2279
- SVGPCProto.transform = function transform(transformInput) {
2280
- const transformObject = transformInput || {};
2281
- if (!transformObject.origin) {
2282
- const BBox = getPathBBox(this.segments);
2283
- transformObject.origin = [BBox.cx, BBox.cy, BBox.cx];
2994
+ /**
2995
+ * Normalize path in 2 steps:
2996
+ * * convert `pathArray`(s) to absolute values
2997
+ * * convert shorthand notation to standard notation
2998
+ * @public
2999
+ */
3000
+ normalize() {
3001
+ const { segments } = this;
3002
+ this.segments = normalizePath(segments);
3003
+ return this;
2284
3004
  }
2285
3005
 
2286
- const path = transformPath(
2287
- this.segments, // the pathArray
2288
- transformObject, // transform functions object, now includes the transform origin
2289
- this.round, // decimals option
2290
- );
3006
+ /**
3007
+ * Optimize `pathArray` values:
3008
+ * * convert segments to absolute and/or relative values
3009
+ * * select segments with shortest resulted string
3010
+ * * round values to the specified `decimals` option value
3011
+ * @public
3012
+ */
3013
+ optimize() {
3014
+ const { segments } = this;
2291
3015
 
2292
- this.segments = clonePath(path);
2293
- return this;
2294
- };
2295
- SVGPCProto.flipX = function flipX() {
2296
- this.transform({ rotate: [180, 0, 0] });
2297
- return this;
2298
- };
2299
- SVGPCProto.flipY = function flipY() {
2300
- this.transform({ rotate: [0, 180, 0] });
2301
- return this;
2302
- };
2303
- SVGPCProto.toString = function toString() {
2304
- return pathToString(this.segments);
2305
- };
3016
+ this.segments = optimizePath(segments, this.round);
3017
+ return this;
3018
+ }
3019
+
3020
+ /**
3021
+ * Transform path using values from an `Object` defined as `transformObject`.
3022
+ * @see SVGPC.transformObject for a quick refference
3023
+ *
3024
+ * @param {Object.<string, (number | number[])>} source a `transformObject`as described above
3025
+ * @public
3026
+ */
3027
+ transform(source) {
3028
+ if (!source || typeof source !== 'object' || (typeof source === 'object'
3029
+ && !['translate', 'rotate', 'skew', 'scale'].some((x) => x in source))) return this;
3030
+
3031
+ const transform = source || {};
3032
+ const { segments } = this;
3033
+
3034
+ // if origin is not specified
3035
+ // it's important that we have one
3036
+ if (!transform.origin) {
3037
+ const BBox = getPathBBox(segments);
3038
+ transform.origin = [BBox.cx, BBox.cy, BBox.cx];
3039
+ }
3040
+
3041
+ this.segments = transformPath(segments, transform);
3042
+ return this;
3043
+ }
3044
+
3045
+ /**
3046
+ * Rotate path 180deg horizontally
3047
+ * @public
3048
+ */
3049
+ flipX() {
3050
+ this.transform({ rotate: [180, 0, 0] });
3051
+ return this;
3052
+ }
3053
+
3054
+ /**
3055
+ * Rotate path 180deg vertically
3056
+ * @public
3057
+ */
3058
+ flipY() {
3059
+ this.transform({ rotate: [0, 180, 0] });
3060
+ return this;
3061
+ }
3062
+
3063
+ /**
3064
+ * Export the current path to be used
3065
+ * for the `d` (description) attribute.
3066
+ * @public
3067
+ * @return {String} the path string
3068
+ */
3069
+ toString() {
3070
+ return pathToString(this.segments, this.round);
3071
+ }
3072
+ }
2306
3073
 
2307
- Object.keys(util).forEach((x) => { SVGPathCommander[x] = util[x]; });
3074
+ // @ts-ignore
3075
+ Object.keys(Util).forEach((x) => { SVGPathCommander[x] = Util[x]; });
2308
3076
 
2309
3077
  export { SVGPathCommander as default };