1 | // Transform SVG PathData
|
---|
2 | // http://www.w3.org/TR/SVG/paths.html#PathDataBNF
|
---|
3 |
|
---|
4 | import { a2c, annotateArcCommand, arcAt, assertNumbers, bezierAt, bezierRoot,
|
---|
5 | intersectionUnitCircleLine } from "./mathUtils";
|
---|
6 | import { SVGPathData } from "./SVGPathData";
|
---|
7 | import { SVGCommand, TransformFunction } from "./types";
|
---|
8 |
|
---|
9 | export namespace SVGPathDataTransformer {
|
---|
10 | // Predefined transforming functions
|
---|
11 | // Rounds commands values
|
---|
12 | export function ROUND(roundVal = 1e13) {
|
---|
13 | assertNumbers(roundVal);
|
---|
14 | function rf(val: number) { return Math.round(val * roundVal) / roundVal; }
|
---|
15 | return function round(command: any) {
|
---|
16 | if ("undefined" !== typeof command.x1) {
|
---|
17 | command.x1 = rf(command.x1);
|
---|
18 | }
|
---|
19 | if ("undefined" !== typeof command.y1) {
|
---|
20 | command.y1 = rf(command.y1);
|
---|
21 | }
|
---|
22 |
|
---|
23 | if ("undefined" !== typeof command.x2) {
|
---|
24 | command.x2 = rf(command.x2);
|
---|
25 | }
|
---|
26 | if ("undefined" !== typeof command.y2) {
|
---|
27 | command.y2 = rf(command.y2);
|
---|
28 | }
|
---|
29 |
|
---|
30 | if ("undefined" !== typeof command.x) {
|
---|
31 | command.x = rf(command.x);
|
---|
32 | }
|
---|
33 | if ("undefined" !== typeof command.y) {
|
---|
34 | command.y = rf(command.y);
|
---|
35 | }
|
---|
36 |
|
---|
37 | if ("undefined" !== typeof command.rX) {
|
---|
38 | command.rX = rf(command.rX);
|
---|
39 | }
|
---|
40 | if ("undefined" !== typeof command.rY) {
|
---|
41 | command.rY = rf(command.rY);
|
---|
42 | }
|
---|
43 |
|
---|
44 | return command;
|
---|
45 | };
|
---|
46 | }
|
---|
47 | // Relative to absolute commands
|
---|
48 | export function TO_ABS() {
|
---|
49 | return INFO((command, prevX, prevY) => {
|
---|
50 | if (command.relative) {
|
---|
51 | // x1/y1 values
|
---|
52 | if ("undefined" !== typeof command.x1) {
|
---|
53 | command.x1 += prevX;
|
---|
54 | }
|
---|
55 | if ("undefined" !== typeof command.y1) {
|
---|
56 | command.y1 += prevY;
|
---|
57 | }
|
---|
58 | // x2/y2 values
|
---|
59 | if ("undefined" !== typeof command.x2) {
|
---|
60 | command.x2 += prevX;
|
---|
61 | }
|
---|
62 | if ("undefined" !== typeof command.y2) {
|
---|
63 | command.y2 += prevY;
|
---|
64 | }
|
---|
65 | // Finally x/y values
|
---|
66 | if ("undefined" !== typeof command.x) {
|
---|
67 | command.x += prevX;
|
---|
68 | }
|
---|
69 | if ("undefined" !== typeof command.y) {
|
---|
70 | command.y += prevY;
|
---|
71 | }
|
---|
72 | command.relative = false;
|
---|
73 | }
|
---|
74 | return command;
|
---|
75 | });
|
---|
76 | }
|
---|
77 | // Absolute to relative commands
|
---|
78 | export function TO_REL() {
|
---|
79 | return INFO((command, prevX, prevY) => {
|
---|
80 | if (!command.relative) {
|
---|
81 | // x1/y1 values
|
---|
82 | if ("undefined" !== typeof command.x1) {
|
---|
83 | command.x1 -= prevX;
|
---|
84 | }
|
---|
85 | if ("undefined" !== typeof command.y1) {
|
---|
86 | command.y1 -= prevY;
|
---|
87 | }
|
---|
88 | // x2/y2 values
|
---|
89 | if ("undefined" !== typeof command.x2) {
|
---|
90 | command.x2 -= prevX;
|
---|
91 | }
|
---|
92 | if ("undefined" !== typeof command.y2) {
|
---|
93 | command.y2 -= prevY;
|
---|
94 | }
|
---|
95 | // Finally x/y values
|
---|
96 | if ("undefined" !== typeof command.x) {
|
---|
97 | command.x -= prevX;
|
---|
98 | }
|
---|
99 | if ("undefined" !== typeof command.y) {
|
---|
100 | command.y -= prevY;
|
---|
101 | }
|
---|
102 | command.relative = true;
|
---|
103 | }
|
---|
104 | return command;
|
---|
105 | });
|
---|
106 | }
|
---|
107 | // Convert H, V, Z and A with rX = 0 to L
|
---|
108 | export function NORMALIZE_HVZ(normalizeZ = true, normalizeH = true, normalizeV = true) {
|
---|
109 | return INFO((command, prevX, prevY, pathStartX, pathStartY) => {
|
---|
110 | if (isNaN(pathStartX) && !(command.type & SVGPathData.MOVE_TO)) {
|
---|
111 | throw new Error("path must start with moveto");
|
---|
112 | }
|
---|
113 | if (normalizeH && command.type & SVGPathData.HORIZ_LINE_TO) {
|
---|
114 | command.type = SVGPathData.LINE_TO;
|
---|
115 | command.y = command.relative ? 0 : prevY;
|
---|
116 | }
|
---|
117 | if (normalizeV && command.type & SVGPathData.VERT_LINE_TO) {
|
---|
118 | command.type = SVGPathData.LINE_TO;
|
---|
119 | command.x = command.relative ? 0 : prevX;
|
---|
120 | }
|
---|
121 | if (normalizeZ && command.type & SVGPathData.CLOSE_PATH) {
|
---|
122 | command.type = SVGPathData.LINE_TO;
|
---|
123 | command.x = command.relative ? pathStartX - prevX : pathStartX;
|
---|
124 | command.y = command.relative ? pathStartY - prevY : pathStartY;
|
---|
125 | }
|
---|
126 | if (command.type & SVGPathData.ARC && (0 === command.rX || 0 === command.rY)) {
|
---|
127 | command.type = SVGPathData.LINE_TO;
|
---|
128 | delete command.rX;
|
---|
129 | delete command.rY;
|
---|
130 | delete command.xRot;
|
---|
131 | delete command.lArcFlag;
|
---|
132 | delete command.sweepFlag;
|
---|
133 | }
|
---|
134 | return command;
|
---|
135 | });
|
---|
136 | }
|
---|
137 | /*
|
---|
138 | * Transforms smooth curves and quads to normal curves and quads (SsTt to CcQq)
|
---|
139 | */
|
---|
140 | export function NORMALIZE_ST() {
|
---|
141 | let prevCurveC2X = NaN;
|
---|
142 | let prevCurveC2Y = NaN;
|
---|
143 | let prevQuadCX = NaN;
|
---|
144 | let prevQuadCY = NaN;
|
---|
145 |
|
---|
146 | return INFO((command, prevX, prevY) => {
|
---|
147 | if (command.type & SVGPathData.SMOOTH_CURVE_TO) {
|
---|
148 | command.type = SVGPathData.CURVE_TO;
|
---|
149 | prevCurveC2X = isNaN(prevCurveC2X) ? prevX : prevCurveC2X;
|
---|
150 | prevCurveC2Y = isNaN(prevCurveC2Y) ? prevY : prevCurveC2Y;
|
---|
151 | command.x1 = command.relative ? prevX - prevCurveC2X : 2 * prevX - prevCurveC2X;
|
---|
152 | command.y1 = command.relative ? prevY - prevCurveC2Y : 2 * prevY - prevCurveC2Y;
|
---|
153 | }
|
---|
154 | if (command.type & SVGPathData.CURVE_TO) {
|
---|
155 | prevCurveC2X = command.relative ? prevX + command.x2 : command.x2;
|
---|
156 | prevCurveC2Y = command.relative ? prevY + command.y2 : command.y2;
|
---|
157 | } else {
|
---|
158 | prevCurveC2X = NaN;
|
---|
159 | prevCurveC2Y = NaN;
|
---|
160 | }
|
---|
161 | if (command.type & SVGPathData.SMOOTH_QUAD_TO) {
|
---|
162 | command.type = SVGPathData.QUAD_TO;
|
---|
163 | prevQuadCX = isNaN(prevQuadCX) ? prevX : prevQuadCX;
|
---|
164 | prevQuadCY = isNaN(prevQuadCY) ? prevY : prevQuadCY;
|
---|
165 | command.x1 = command.relative ? prevX - prevQuadCX : 2 * prevX - prevQuadCX;
|
---|
166 | command.y1 = command.relative ? prevY - prevQuadCY : 2 * prevY - prevQuadCY;
|
---|
167 | }
|
---|
168 | if (command.type & SVGPathData.QUAD_TO) {
|
---|
169 | prevQuadCX = command.relative ? prevX + command.x1 : command.x1;
|
---|
170 | prevQuadCY = command.relative ? prevY + command.y1 : command.y1;
|
---|
171 | } else {
|
---|
172 | prevQuadCX = NaN;
|
---|
173 | prevQuadCY = NaN;
|
---|
174 | }
|
---|
175 |
|
---|
176 | return command;
|
---|
177 | });
|
---|
178 | }
|
---|
179 | /*
|
---|
180 | * A quadratic bézier curve can be represented by a cubic bézier curve which has
|
---|
181 | * the same end points as the quadratic and both control points in place of the
|
---|
182 | * quadratic"s one.
|
---|
183 | *
|
---|
184 | * This transformer replaces QqTt commands with Cc commands respectively.
|
---|
185 | * This is useful for reading path data into a system which only has a
|
---|
186 | * representation for cubic curves.
|
---|
187 | */
|
---|
188 | export function QT_TO_C() {
|
---|
189 | let prevQuadX1 = NaN;
|
---|
190 | let prevQuadY1 = NaN;
|
---|
191 |
|
---|
192 | return INFO((command, prevX, prevY) => {
|
---|
193 | if (command.type & SVGPathData.SMOOTH_QUAD_TO) {
|
---|
194 | command.type = SVGPathData.QUAD_TO;
|
---|
195 | prevQuadX1 = isNaN(prevQuadX1) ? prevX : prevQuadX1;
|
---|
196 | prevQuadY1 = isNaN(prevQuadY1) ? prevY : prevQuadY1;
|
---|
197 | command.x1 = command.relative ? prevX - prevQuadX1 : 2 * prevX - prevQuadX1;
|
---|
198 | command.y1 = command.relative ? prevY - prevQuadY1 : 2 * prevY - prevQuadY1;
|
---|
199 | }
|
---|
200 | if (command.type & SVGPathData.QUAD_TO) {
|
---|
201 | prevQuadX1 = command.relative ? prevX + command.x1 : command.x1;
|
---|
202 | prevQuadY1 = command.relative ? prevY + command.y1 : command.y1;
|
---|
203 | const x1 = command.x1;
|
---|
204 | const y1 = command.y1;
|
---|
205 |
|
---|
206 | command.type = SVGPathData.CURVE_TO;
|
---|
207 | command.x1 = ((command.relative ? 0 : prevX) + x1 * 2) / 3;
|
---|
208 | command.y1 = ((command.relative ? 0 : prevY) + y1 * 2) / 3;
|
---|
209 | command.x2 = (command.x + x1 * 2) / 3;
|
---|
210 | command.y2 = (command.y + y1 * 2) / 3;
|
---|
211 | } else {
|
---|
212 | prevQuadX1 = NaN;
|
---|
213 | prevQuadY1 = NaN;
|
---|
214 | }
|
---|
215 |
|
---|
216 | return command;
|
---|
217 | });
|
---|
218 | }
|
---|
219 | export function INFO(
|
---|
220 | f: (command: any, prevXAbs: number, prevYAbs: number,
|
---|
221 | pathStartXAbs: number, pathStartYAbs: number) => any | any[]) {
|
---|
222 | let prevXAbs = 0;
|
---|
223 | let prevYAbs = 0;
|
---|
224 | let pathStartXAbs = NaN;
|
---|
225 | let pathStartYAbs = NaN;
|
---|
226 |
|
---|
227 | return function transform(command: any) {
|
---|
228 | if (isNaN(pathStartXAbs) && !(command.type & SVGPathData.MOVE_TO)) {
|
---|
229 | throw new Error("path must start with moveto");
|
---|
230 | }
|
---|
231 |
|
---|
232 | const result = f(command, prevXAbs, prevYAbs, pathStartXAbs, pathStartYAbs);
|
---|
233 |
|
---|
234 | if (command.type & SVGPathData.CLOSE_PATH) {
|
---|
235 | prevXAbs = pathStartXAbs;
|
---|
236 | prevYAbs = pathStartYAbs;
|
---|
237 | }
|
---|
238 |
|
---|
239 | if ("undefined" !== typeof command.x) {
|
---|
240 | prevXAbs = (command.relative ? prevXAbs + command.x : command.x);
|
---|
241 | }
|
---|
242 | if ("undefined" !== typeof command.y) {
|
---|
243 | prevYAbs = (command.relative ? prevYAbs + command.y : command.y);
|
---|
244 | }
|
---|
245 |
|
---|
246 | if (command.type & SVGPathData.MOVE_TO) {
|
---|
247 | pathStartXAbs = prevXAbs;
|
---|
248 | pathStartYAbs = prevYAbs;
|
---|
249 | }
|
---|
250 |
|
---|
251 | return result;
|
---|
252 | };
|
---|
253 | }
|
---|
254 | /*
|
---|
255 | * remove 0-length segments
|
---|
256 | */
|
---|
257 | export function SANITIZE(EPS = 0) {
|
---|
258 | assertNumbers(EPS);
|
---|
259 | let prevCurveC2X = NaN;
|
---|
260 | let prevCurveC2Y = NaN;
|
---|
261 | let prevQuadCX = NaN;
|
---|
262 | let prevQuadCY = NaN;
|
---|
263 |
|
---|
264 | return INFO((command, prevX, prevY, pathStartX, pathStartY) => {
|
---|
265 | const abs = Math.abs;
|
---|
266 | let skip = false;
|
---|
267 | let x1Rel = 0;
|
---|
268 | let y1Rel = 0;
|
---|
269 |
|
---|
270 | if (command.type & SVGPathData.SMOOTH_CURVE_TO) {
|
---|
271 | x1Rel = isNaN(prevCurveC2X) ? 0 : prevX - prevCurveC2X;
|
---|
272 | y1Rel = isNaN(prevCurveC2Y) ? 0 : prevY - prevCurveC2Y;
|
---|
273 | }
|
---|
274 | if (command.type & (SVGPathData.CURVE_TO | SVGPathData.SMOOTH_CURVE_TO)) {
|
---|
275 | prevCurveC2X = command.relative ? prevX + command.x2 : command.x2;
|
---|
276 | prevCurveC2Y = command.relative ? prevY + command.y2 : command.y2;
|
---|
277 | } else {
|
---|
278 | prevCurveC2X = NaN;
|
---|
279 | prevCurveC2Y = NaN;
|
---|
280 | }
|
---|
281 | if (command.type & SVGPathData.SMOOTH_QUAD_TO) {
|
---|
282 | prevQuadCX = isNaN(prevQuadCX) ? prevX : 2 * prevX - prevQuadCX;
|
---|
283 | prevQuadCY = isNaN(prevQuadCY) ? prevY : 2 * prevY - prevQuadCY;
|
---|
284 | } else if (command.type & SVGPathData.QUAD_TO) {
|
---|
285 | prevQuadCX = command.relative ? prevX + command.x1 : command.x1;
|
---|
286 | prevQuadCY = command.relative ? prevY + command.y1 : command.y2;
|
---|
287 | } else {
|
---|
288 | prevQuadCX = NaN;
|
---|
289 | prevQuadCY = NaN;
|
---|
290 | }
|
---|
291 |
|
---|
292 | if (command.type & SVGPathData.LINE_COMMANDS ||
|
---|
293 | command.type & SVGPathData.ARC && (0 === command.rX || 0 === command.rY || !command.lArcFlag) ||
|
---|
294 | command.type & SVGPathData.CURVE_TO || command.type & SVGPathData.SMOOTH_CURVE_TO ||
|
---|
295 | command.type & SVGPathData.QUAD_TO || command.type & SVGPathData.SMOOTH_QUAD_TO) {
|
---|
296 | const xRel = "undefined" === typeof command.x ? 0 :
|
---|
297 | (command.relative ? command.x : command.x - prevX);
|
---|
298 | const yRel = "undefined" === typeof command.y ? 0 :
|
---|
299 | (command.relative ? command.y : command.y - prevY);
|
---|
300 |
|
---|
301 | x1Rel = !isNaN(prevQuadCX) ? prevQuadCX - prevX :
|
---|
302 | "undefined" === typeof command.x1 ? x1Rel :
|
---|
303 | command.relative ? command.x :
|
---|
304 | command.x1 - prevX;
|
---|
305 | y1Rel = !isNaN(prevQuadCY) ? prevQuadCY - prevY :
|
---|
306 | "undefined" === typeof command.y1 ? y1Rel :
|
---|
307 | command.relative ? command.y :
|
---|
308 | command.y1 - prevY;
|
---|
309 |
|
---|
310 | const x2Rel = "undefined" === typeof command.x2 ? 0 :
|
---|
311 | (command.relative ? command.x : command.x2 - prevX);
|
---|
312 | const y2Rel = "undefined" === typeof command.y2 ? 0 :
|
---|
313 | (command.relative ? command.y : command.y2 - prevY);
|
---|
314 |
|
---|
315 | if (abs(xRel) <= EPS && abs(yRel) <= EPS &&
|
---|
316 | abs(x1Rel) <= EPS && abs(y1Rel) <= EPS &&
|
---|
317 | abs(x2Rel) <= EPS && abs(y2Rel) <= EPS) {
|
---|
318 | skip = true;
|
---|
319 | }
|
---|
320 | }
|
---|
321 |
|
---|
322 | if (command.type & SVGPathData.CLOSE_PATH) {
|
---|
323 | if (abs(prevX - pathStartX) <= EPS && abs(prevY - pathStartY) <= EPS) {
|
---|
324 | skip = true;
|
---|
325 | }
|
---|
326 | }
|
---|
327 |
|
---|
328 | return skip ? [] : command;
|
---|
329 | });
|
---|
330 | }
|
---|
331 | // SVG Transforms : http://www.w3.org/TR/SVGTiny12/coords.html#TransformList
|
---|
332 | // Matrix : http://apike.ca/prog_svg_transform.html
|
---|
333 | // a c e
|
---|
334 | // b d f
|
---|
335 | export function MATRIX(a: number, b: number, c: number, d: number, e: number, f: number) {
|
---|
336 | assertNumbers(a, b, c, d, e, f);
|
---|
337 |
|
---|
338 | return INFO((command, prevX, prevY, pathStartX) => {
|
---|
339 | const origX1 = command.x1;
|
---|
340 | const origX2 = command.x2;
|
---|
341 | // if isNaN(pathStartX), then this is the first command, which is ALWAYS an
|
---|
342 | // absolute MOVE_TO, regardless what the relative flag says
|
---|
343 | const comRel = command.relative && !isNaN(pathStartX);
|
---|
344 | const x = "undefined" !== typeof command.x ? command.x : (comRel ? 0 : prevX);
|
---|
345 | const y = "undefined" !== typeof command.y ? command.y : (comRel ? 0 : prevY);
|
---|
346 |
|
---|
347 | if (command.type & SVGPathData.HORIZ_LINE_TO && 0 !== b) {
|
---|
348 | command.type = SVGPathData.LINE_TO;
|
---|
349 | command.y = command.relative ? 0 : prevY;
|
---|
350 | }
|
---|
351 | if (command.type & SVGPathData.VERT_LINE_TO && 0 !== c) {
|
---|
352 | command.type = SVGPathData.LINE_TO;
|
---|
353 | command.x = command.relative ? 0 : prevX;
|
---|
354 | }
|
---|
355 |
|
---|
356 | if ("undefined" !== typeof command.x) {
|
---|
357 | command.x = (command.x * a) + (y * c) + (comRel ? 0 : e);
|
---|
358 | }
|
---|
359 | if ("undefined" !== typeof command.y) {
|
---|
360 | command.y = (x * b) + command.y * d + (comRel ? 0 : f);
|
---|
361 | }
|
---|
362 | if ("undefined" !== typeof command.x1) {
|
---|
363 | command.x1 = command.x1 * a + command.y1 * c + (comRel ? 0 : e);
|
---|
364 | }
|
---|
365 | if ("undefined" !== typeof command.y1) {
|
---|
366 | command.y1 = origX1 * b + command.y1 * d + (comRel ? 0 : f);
|
---|
367 | }
|
---|
368 | if ("undefined" !== typeof command.x2) {
|
---|
369 | command.x2 = command.x2 * a + command.y2 * c + (comRel ? 0 : e);
|
---|
370 | }
|
---|
371 | if ("undefined" !== typeof command.y2) {
|
---|
372 | command.y2 = origX2 * b + command.y2 * d + (comRel ? 0 : f);
|
---|
373 | }
|
---|
374 | function sqr(x: number) { return x * x; }
|
---|
375 | const det = a * d - b * c;
|
---|
376 |
|
---|
377 | if ("undefined" !== typeof command.xRot) {
|
---|
378 | // Skip if this is a pure translation
|
---|
379 | if (1 !== a || 0 !== b || 0 !== c || 1 !== d) {
|
---|
380 | // Special case for singular matrix
|
---|
381 | if (0 === det) {
|
---|
382 | // In the singular case, the arc is compressed to a line. The actual geometric image of the original
|
---|
383 | // curve under this transform possibly extends beyond the starting and/or ending points of the segment, but
|
---|
384 | // for simplicity we ignore this detail and just replace this command with a single line segment.
|
---|
385 | delete command.rX;
|
---|
386 | delete command.rY;
|
---|
387 | delete command.xRot;
|
---|
388 | delete command.lArcFlag;
|
---|
389 | delete command.sweepFlag;
|
---|
390 | command.type = SVGPathData.LINE_TO;
|
---|
391 | } else {
|
---|
392 | // Convert to radians
|
---|
393 | const xRot = command.xRot * Math.PI / 180;
|
---|
394 |
|
---|
395 | // Convert rotated ellipse to general conic form
|
---|
396 | // x0^2/rX^2 + y0^2/rY^2 - 1 = 0
|
---|
397 | // x0 = x*cos(xRot) + y*sin(xRot)
|
---|
398 | // y0 = -x*sin(xRot) + y*cos(xRot)
|
---|
399 | // --> A*x^2 + B*x*y + C*y^2 - 1 = 0, where
|
---|
400 | const sinRot = Math.sin(xRot);
|
---|
401 | const cosRot = Math.cos(xRot);
|
---|
402 | const xCurve = 1 / sqr(command.rX);
|
---|
403 | const yCurve = 1 / sqr(command.rY);
|
---|
404 | const A = sqr(cosRot) * xCurve + sqr(sinRot) * yCurve;
|
---|
405 | const B = 2 * sinRot * cosRot * (xCurve - yCurve);
|
---|
406 | const C = sqr(sinRot) * xCurve + sqr(cosRot) * yCurve;
|
---|
407 |
|
---|
408 | // Apply matrix to A*x^2 + B*x*y + C*y^2 - 1 = 0
|
---|
409 | // x1 = a*x + c*y
|
---|
410 | // y1 = b*x + d*y
|
---|
411 | // (we can ignore e and f, since pure translations don"t affect the shape of the ellipse)
|
---|
412 | // --> A1*x1^2 + B1*x1*y1 + C1*y1^2 - det^2 = 0, where
|
---|
413 | const A1 = A * d * d - B * b * d + C * b * b;
|
---|
414 | const B1 = B * (a * d + b * c) - 2 * (A * c * d + C * a * b);
|
---|
415 | const C1 = A * c * c - B * a * c + C * a * a;
|
---|
416 |
|
---|
417 | // Unapply newXRot to get back to axis-aligned ellipse equation
|
---|
418 | // x1 = x2*cos(newXRot) - y2*sin(newXRot)
|
---|
419 | // y1 = x2*sin(newXRot) + y2*cos(newXRot)
|
---|
420 | // A1*x1^2 + B1*x1*y1 + C1*y1^2 - det^2 =
|
---|
421 | // x2^2*(A1*cos(newXRot)^2 + B1*sin(newXRot)*cos(newXRot) + C1*sin(newXRot)^2)
|
---|
422 | // + x2*y2*(2*(C1 - A1)*sin(newXRot)*cos(newXRot) + B1*(cos(newXRot)^2 - sin(newXRot)^2))
|
---|
423 | // + y2^2*(A1*sin(newXRot)^2 - B1*sin(newXRot)*cos(newXRot) + C1*cos(newXRot)^2)
|
---|
424 | // (which must have the same zeroes as)
|
---|
425 | // x2^2/newRX^2 + y2^2/newRY^2 - 1
|
---|
426 | // (so we have)
|
---|
427 | // 2*(C1 - A1)*sin(newXRot)*cos(newXRot) + B1*(cos(newXRot)^2 - sin(newXRot)^2) = 0
|
---|
428 | // (A1 - C1)*sin(2*newXRot) = B1*cos(2*newXRot)
|
---|
429 | // 2*newXRot = atan2(B1, A1 - C1)
|
---|
430 | const newXRot = ((Math.atan2(B1, A1 - C1) + Math.PI) % Math.PI) / 2;
|
---|
431 | // For any integer n, (atan2(B1, A1 - C1) + n*pi)/2 is a solution to the above; incrementing n just swaps
|
---|
432 | // the x and y radii computed below (since that"s what rotating an ellipse by pi/2 does). Choosing the
|
---|
433 | // rotation between 0 and pi/2 eliminates the ambiguity and leads to more predictable output.
|
---|
434 |
|
---|
435 | // Finally, we get newRX and newRY from the same-zeroes relationship that gave us newXRot
|
---|
436 | const newSinRot = Math.sin(newXRot);
|
---|
437 | const newCosRot = Math.cos(newXRot);
|
---|
438 |
|
---|
439 | command.rX = Math.abs(det) /
|
---|
440 | Math.sqrt(A1 * sqr(newCosRot) + B1 * newSinRot * newCosRot + C1 * sqr(newSinRot));
|
---|
441 | command.rY = Math.abs(det) /
|
---|
442 | Math.sqrt(A1 * sqr(newSinRot) - B1 * newSinRot * newCosRot + C1 * sqr(newCosRot));
|
---|
443 | command.xRot = newXRot * 180 / Math.PI;
|
---|
444 | }
|
---|
445 | }
|
---|
446 | }
|
---|
447 | // sweepFlag needs to be inverted when mirroring shapes
|
---|
448 | // see http://www.itk.ilstu.edu/faculty/javila/SVG/SVG_drawing1/elliptical_curve.htm
|
---|
449 | // m 65,10 a 50,25 0 1 0 50,25
|
---|
450 | // M 65,60 A 50,25 0 1 1 115,35
|
---|
451 | if ("undefined" !== typeof command.sweepFlag && 0 > det) {
|
---|
452 | command.sweepFlag = +!command.sweepFlag;
|
---|
453 | }
|
---|
454 | return command;
|
---|
455 | });
|
---|
456 | }
|
---|
457 | export function ROTATE(a: number, x = 0, y = 0) {
|
---|
458 | assertNumbers(a, x, y);
|
---|
459 | const sin = Math.sin(a);
|
---|
460 | const cos = Math.cos(a);
|
---|
461 |
|
---|
462 | return MATRIX(cos, sin, -sin, cos, x - x * cos + y * sin, y - x * sin - y * cos);
|
---|
463 | }
|
---|
464 | export function TRANSLATE(dX: number, dY = 0) {
|
---|
465 | assertNumbers(dX, dY);
|
---|
466 | return MATRIX(1, 0, 0, 1, dX, dY);
|
---|
467 | }
|
---|
468 | export function SCALE(dX: number, dY = dX) {
|
---|
469 | assertNumbers(dX, dY);
|
---|
470 | return MATRIX(dX, 0, 0, dY, 0, 0);
|
---|
471 | }
|
---|
472 | export function SKEW_X(a: number) {
|
---|
473 | assertNumbers(a);
|
---|
474 | return MATRIX(1, 0, Math.atan(a), 1, 0, 0);
|
---|
475 | }
|
---|
476 | export function SKEW_Y(a: number) {
|
---|
477 | assertNumbers(a);
|
---|
478 | return MATRIX(1, Math.atan(a), 0, 1, 0, 0);
|
---|
479 | }
|
---|
480 | export function X_AXIS_SYMMETRY(xOffset = 0) {
|
---|
481 | assertNumbers(xOffset);
|
---|
482 | return MATRIX(-1, 0, 0, 1, xOffset, 0);
|
---|
483 | }
|
---|
484 | export function Y_AXIS_SYMMETRY(yOffset = 0) {
|
---|
485 | assertNumbers(yOffset);
|
---|
486 | return MATRIX(1, 0, 0, -1, 0, yOffset);
|
---|
487 | }
|
---|
488 | // Convert arc commands to curve commands
|
---|
489 | export function A_TO_C() {
|
---|
490 | return INFO((command, prevX, prevY) => {
|
---|
491 | if (SVGPathData.ARC === command.type) {
|
---|
492 | return a2c(command, command.relative ? 0 : prevX, command.relative ? 0 : prevY);
|
---|
493 | }
|
---|
494 | return command;
|
---|
495 | });
|
---|
496 | }
|
---|
497 | // @see annotateArcCommand
|
---|
498 | export function ANNOTATE_ARCS() {
|
---|
499 | return INFO((c, x1, y1) => {
|
---|
500 | if (c.relative) {
|
---|
501 | x1 = 0;
|
---|
502 | y1 = 0;
|
---|
503 | }
|
---|
504 | if (SVGPathData.ARC === c.type) {
|
---|
505 | annotateArcCommand(c, x1, y1);
|
---|
506 | }
|
---|
507 | return c;
|
---|
508 | });
|
---|
509 | }
|
---|
510 | export function CLONE() {
|
---|
511 | return (c: SVGCommand) => {
|
---|
512 | const result = {} as SVGCommand;
|
---|
513 | // tslint:disable-next-line
|
---|
514 | for (const key in c) {
|
---|
515 | result[key as keyof SVGCommand] = c[key as keyof SVGCommand];
|
---|
516 | }
|
---|
517 | return result;
|
---|
518 | };
|
---|
519 | }
|
---|
520 | // @see annotateArcCommand
|
---|
521 | export function CALCULATE_BOUNDS() {
|
---|
522 | const clone = CLONE();
|
---|
523 | const toAbs = TO_ABS();
|
---|
524 | const qtToC = QT_TO_C();
|
---|
525 | const normST = NORMALIZE_ST();
|
---|
526 | const f: TransformFunction & {minX: number, maxX: number, minY: number, maxY: number} =
|
---|
527 | INFO((command, prevXAbs, prevYAbs) => {
|
---|
528 | const c = normST(qtToC(toAbs(clone(command))));
|
---|
529 | function fixX(absX: number) {
|
---|
530 | if (absX > f.maxX) { f.maxX = absX; }
|
---|
531 | if (absX < f.minX) { f.minX = absX; }
|
---|
532 | }
|
---|
533 | function fixY(absY: number) {
|
---|
534 | if (absY > f.maxY) { f.maxY = absY; }
|
---|
535 | if (absY < f.minY) { f.minY = absY; }
|
---|
536 | }
|
---|
537 | if (c.type & SVGPathData.DRAWING_COMMANDS) {
|
---|
538 | fixX(prevXAbs);
|
---|
539 | fixY(prevYAbs);
|
---|
540 | }
|
---|
541 | if (c.type & SVGPathData.HORIZ_LINE_TO) {
|
---|
542 | fixX(c.x);
|
---|
543 | }
|
---|
544 | if (c.type & SVGPathData.VERT_LINE_TO) {
|
---|
545 | fixY(c.y);
|
---|
546 | }
|
---|
547 | if (c.type & SVGPathData.LINE_TO) {
|
---|
548 | fixX(c.x);
|
---|
549 | fixY(c.y);
|
---|
550 | }
|
---|
551 | if (c.type & SVGPathData.CURVE_TO) {
|
---|
552 | // add start and end points
|
---|
553 | fixX(c.x);
|
---|
554 | fixY(c.y);
|
---|
555 | const xDerivRoots = bezierRoot(prevXAbs, c.x1, c.x2, c.x);
|
---|
556 |
|
---|
557 | for (const derivRoot of xDerivRoots) {
|
---|
558 | if (0 < derivRoot && 1 > derivRoot) {
|
---|
559 | fixX(bezierAt(prevXAbs, c.x1, c.x2, c.x, derivRoot));
|
---|
560 | }
|
---|
561 | }
|
---|
562 | const yDerivRoots = bezierRoot(prevYAbs, c.y1, c.y2, c.y);
|
---|
563 |
|
---|
564 | for (const derivRoot of yDerivRoots) {
|
---|
565 | if (0 < derivRoot && 1 > derivRoot) {
|
---|
566 | fixY(bezierAt(prevYAbs, c.y1, c.y2, c.y, derivRoot));
|
---|
567 | }
|
---|
568 | }
|
---|
569 | }
|
---|
570 | if (c.type & SVGPathData.ARC) {
|
---|
571 | // add start and end points
|
---|
572 | fixX(c.x);
|
---|
573 | fixY(c.y);
|
---|
574 | annotateArcCommand(c, prevXAbs, prevYAbs);
|
---|
575 | // p = cos(phi) * xv + sin(phi) * yv
|
---|
576 | // dp = -sin(phi) * xv + cos(phi) * yv = 0
|
---|
577 | const xRotRad = c.xRot / 180 * Math.PI;
|
---|
578 | // points on ellipse for phi = 0° and phi = 90°
|
---|
579 | const x0 = Math.cos(xRotRad) * c.rX;
|
---|
580 | const y0 = Math.sin(xRotRad) * c.rX;
|
---|
581 | const x90 = -Math.sin(xRotRad) * c.rY;
|
---|
582 | const y90 = Math.cos(xRotRad) * c.rY;
|
---|
583 |
|
---|
584 | // annotateArcCommand returns phi1 and phi2 such that -180° < phi1 < 180° and phi2 is smaller or greater
|
---|
585 | // depending on the sweep flag. Calculate phiMin, phiMax such that -180° < phiMin < 180° and phiMin < phiMax
|
---|
586 | const [phiMin, phiMax] = c.phi1 < c.phi2 ?
|
---|
587 | [c.phi1, c.phi2] :
|
---|
588 | (-180 > c.phi2 ? [c.phi2 + 360, c.phi1 + 360] : [c.phi2, c.phi1]);
|
---|
589 | const normalizeXiEta = ([xi, eta]: [number, number]) => {
|
---|
590 | const phiRad = Math.atan2(eta, xi);
|
---|
591 | const phi = phiRad * 180 / Math.PI;
|
---|
592 |
|
---|
593 | return phi < phiMin ? phi + 360 : phi;
|
---|
594 | };
|
---|
595 | // xi = cos(phi), eta = sin(phi)
|
---|
596 |
|
---|
597 | const xDerivRoots = intersectionUnitCircleLine(x90, -x0, 0).map(normalizeXiEta);
|
---|
598 | for (const derivRoot of xDerivRoots) {
|
---|
599 | if (derivRoot > phiMin && derivRoot < phiMax) {
|
---|
600 | fixX(arcAt(c.cX, x0, x90, derivRoot));
|
---|
601 | }
|
---|
602 | }
|
---|
603 |
|
---|
604 | const yDerivRoots = intersectionUnitCircleLine(y90, -y0, 0).map(normalizeXiEta);
|
---|
605 | for (const derivRoot of yDerivRoots) {
|
---|
606 | if (derivRoot > phiMin && derivRoot < phiMax) {
|
---|
607 | fixY(arcAt(c.cY, y0, y90, derivRoot));
|
---|
608 | }
|
---|
609 | }
|
---|
610 | }
|
---|
611 | return command;
|
---|
612 | }) as any;
|
---|
613 |
|
---|
614 | f.minX = Infinity;
|
---|
615 | f.maxX = -Infinity;
|
---|
616 | f.minY = Infinity;
|
---|
617 | f.maxY = -Infinity;
|
---|
618 | return f;
|
---|
619 | }
|
---|
620 | }
|
---|