[79a0317] | 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 | }
|
---|