Initial project import
This commit is contained in:
90
node_modules/svg-pathdata/src/SVGPathData.ts
generated
vendored
Normal file
90
node_modules/svg-pathdata/src/SVGPathData.ts
generated
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
import { encodeSVGPath } from "./SVGPathDataEncoder";
|
||||
import { SVGPathDataParser } from "./SVGPathDataParser";
|
||||
import { SVGPathDataTransformer } from "./SVGPathDataTransformer";
|
||||
import { TransformableSVG } from "./TransformableSVG";
|
||||
import { SVGCommand } from "./types";
|
||||
|
||||
export class SVGPathData extends TransformableSVG {
|
||||
commands: SVGCommand[];
|
||||
constructor(content: string | SVGCommand[]) {
|
||||
super();
|
||||
if ("string" === typeof content) {
|
||||
this.commands = SVGPathData.parse(content);
|
||||
} else {
|
||||
this.commands = content;
|
||||
}
|
||||
}
|
||||
|
||||
encode() {
|
||||
return SVGPathData.encode(this.commands);
|
||||
}
|
||||
|
||||
getBounds() {
|
||||
const boundsTransform = SVGPathDataTransformer.CALCULATE_BOUNDS();
|
||||
|
||||
this.transform(boundsTransform);
|
||||
return boundsTransform;
|
||||
}
|
||||
|
||||
transform(
|
||||
transformFunction: (input: SVGCommand) => SVGCommand | SVGCommand[],
|
||||
) {
|
||||
const newCommands = [];
|
||||
|
||||
for (const command of this.commands) {
|
||||
const transformedCommand = transformFunction(command);
|
||||
|
||||
if (Array.isArray(transformedCommand)) {
|
||||
newCommands.push(...transformedCommand);
|
||||
} else {
|
||||
newCommands.push(transformedCommand);
|
||||
}
|
||||
}
|
||||
this.commands = newCommands;
|
||||
return this;
|
||||
}
|
||||
|
||||
static encode(commands: SVGCommand[]) {
|
||||
return encodeSVGPath(commands);
|
||||
}
|
||||
|
||||
static parse(path: string) {
|
||||
const parser = new SVGPathDataParser();
|
||||
const commands: SVGCommand[] = [];
|
||||
parser.parse(path, commands);
|
||||
parser.finish(commands);
|
||||
return commands;
|
||||
}
|
||||
|
||||
static readonly CLOSE_PATH: 1 = 1;
|
||||
static readonly MOVE_TO: 2 = 2;
|
||||
static readonly HORIZ_LINE_TO: 4 = 4;
|
||||
static readonly VERT_LINE_TO: 8 = 8;
|
||||
static readonly LINE_TO: 16 = 16;
|
||||
static readonly CURVE_TO: 32 = 32;
|
||||
static readonly SMOOTH_CURVE_TO: 64 = 64;
|
||||
static readonly QUAD_TO: 128 = 128;
|
||||
static readonly SMOOTH_QUAD_TO: 256 = 256;
|
||||
static readonly ARC: 512 = 512;
|
||||
static readonly LINE_COMMANDS = SVGPathData.LINE_TO | SVGPathData.HORIZ_LINE_TO | SVGPathData.VERT_LINE_TO;
|
||||
static readonly DRAWING_COMMANDS = SVGPathData.HORIZ_LINE_TO | SVGPathData.VERT_LINE_TO | SVGPathData.LINE_TO |
|
||||
SVGPathData.CURVE_TO | SVGPathData.SMOOTH_CURVE_TO | SVGPathData.QUAD_TO |
|
||||
SVGPathData.SMOOTH_QUAD_TO | SVGPathData.ARC;
|
||||
}
|
||||
|
||||
export const COMMAND_ARG_COUNTS = {
|
||||
[SVGPathData.MOVE_TO]: 2,
|
||||
[SVGPathData.LINE_TO]: 2,
|
||||
[SVGPathData.HORIZ_LINE_TO]: 1,
|
||||
[SVGPathData.VERT_LINE_TO]: 1,
|
||||
[SVGPathData.CLOSE_PATH]: 0,
|
||||
[SVGPathData.QUAD_TO]: 4,
|
||||
[SVGPathData.SMOOTH_QUAD_TO]: 2,
|
||||
[SVGPathData.CURVE_TO]: 6,
|
||||
[SVGPathData.SMOOTH_CURVE_TO]: 4,
|
||||
[SVGPathData.ARC]: 7,
|
||||
};
|
||||
|
||||
export {encodeSVGPath} from "./SVGPathDataEncoder";
|
||||
export {SVGPathDataParser} from "./SVGPathDataParser";
|
||||
export {SVGPathDataTransformer} from "./SVGPathDataTransformer";
|
||||
62
node_modules/svg-pathdata/src/SVGPathDataEncoder.ts
generated
vendored
Normal file
62
node_modules/svg-pathdata/src/SVGPathDataEncoder.ts
generated
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
import { SVGPathData } from "./SVGPathData";
|
||||
import { SVGCommand } from "./types";
|
||||
|
||||
// Encode SVG PathData
|
||||
// http://www.w3.org/TR/SVG/paths.html#PathDataBNF
|
||||
|
||||
// Private consts : Char groups
|
||||
const WSP = " ";
|
||||
|
||||
export function encodeSVGPath(commands: SVGCommand | SVGCommand[]) {
|
||||
let str = "";
|
||||
|
||||
if (!Array.isArray(commands)) {
|
||||
commands = [commands];
|
||||
}
|
||||
for (let i = 0; i < commands.length; i++) {
|
||||
const command = commands[i];
|
||||
if (command.type === SVGPathData.CLOSE_PATH) {
|
||||
str += "z";
|
||||
} else if (command.type === SVGPathData.HORIZ_LINE_TO) {
|
||||
str += (command.relative ? "h" : "H") +
|
||||
command.x;
|
||||
} else if (command.type === SVGPathData.VERT_LINE_TO) {
|
||||
str += (command.relative ? "v" : "V") +
|
||||
command.y;
|
||||
} else if (command.type === SVGPathData.MOVE_TO) {
|
||||
str += (command.relative ? "m" : "M") +
|
||||
command.x + WSP + command.y;
|
||||
} else if (command.type === SVGPathData.LINE_TO) {
|
||||
str += (command.relative ? "l" : "L") +
|
||||
command.x + WSP + command.y;
|
||||
} else if (command.type === SVGPathData.CURVE_TO) {
|
||||
str += (command.relative ? "c" : "C") +
|
||||
command.x1 + WSP + command.y1 +
|
||||
WSP + command.x2 + WSP + command.y2 +
|
||||
WSP + command.x + WSP + command.y;
|
||||
} else if (command.type === SVGPathData.SMOOTH_CURVE_TO) {
|
||||
str += (command.relative ? "s" : "S") +
|
||||
command.x2 + WSP + command.y2 +
|
||||
WSP + command.x + WSP + command.y;
|
||||
} else if (command.type === SVGPathData.QUAD_TO) {
|
||||
str += (command.relative ? "q" : "Q") +
|
||||
command.x1 + WSP + command.y1 +
|
||||
WSP + command.x + WSP + command.y;
|
||||
} else if (command.type === SVGPathData.SMOOTH_QUAD_TO) {
|
||||
str += (command.relative ? "t" : "T") +
|
||||
command.x + WSP + command.y;
|
||||
} else if (command.type === SVGPathData.ARC) {
|
||||
str += (command.relative ? "a" : "A") +
|
||||
command.rX + WSP + command.rY +
|
||||
WSP + command.xRot +
|
||||
WSP + (+command.lArcFlag) + WSP + (+command.sweepFlag) +
|
||||
WSP + command.x + WSP + command.y;
|
||||
} else {
|
||||
// Unknown command
|
||||
throw new Error(
|
||||
`Unexpected command type "${ (command as any).type}" at index ${i}.`);
|
||||
}
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
290
node_modules/svg-pathdata/src/SVGPathDataParser.ts
generated
vendored
Normal file
290
node_modules/svg-pathdata/src/SVGPathDataParser.ts
generated
vendored
Normal file
@@ -0,0 +1,290 @@
|
||||
// Parse SVG PathData
|
||||
// http://www.w3.org/TR/SVG/paths.html#PathDataBNF
|
||||
import { COMMAND_ARG_COUNTS, SVGPathData } from "./SVGPathData";
|
||||
import { TransformableSVG } from "./TransformableSVG";
|
||||
import { SVGCommand, TransformFunction } from "./types";
|
||||
// Private consts : Char groups
|
||||
const isWhiteSpace = (c: string) =>
|
||||
" " === c || "\t" === c || "\r" === c || "\n" === c;
|
||||
const isDigit = (c: string) =>
|
||||
"0".charCodeAt(0) <= c.charCodeAt(0) && c.charCodeAt(0) <= "9".charCodeAt(0);
|
||||
const COMMANDS = "mMzZlLhHvVcCsSqQtTaA";
|
||||
|
||||
export class SVGPathDataParser extends TransformableSVG {
|
||||
private curNumber: string = "";
|
||||
private curCommandType: SVGCommand["type"] | -1 = -1;
|
||||
private curCommandRelative = false;
|
||||
private canParseCommandOrComma = true;
|
||||
private curNumberHasExp = false;
|
||||
private curNumberHasExpDigits = false;
|
||||
private curNumberHasDecimal = false;
|
||||
private curArgs: number[] = [];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
finish(commands: SVGCommand[] = []) {
|
||||
this.parse(" ", commands);
|
||||
// Adding residual command
|
||||
if (0 !== this.curArgs.length || !this.canParseCommandOrComma) {
|
||||
throw new SyntaxError("Unterminated command at the path end.");
|
||||
}
|
||||
return commands;
|
||||
}
|
||||
|
||||
parse(str: string, commands: SVGCommand[] = []) {
|
||||
const finishCommand = (command: SVGCommand) => {
|
||||
commands.push(command);
|
||||
this.curArgs.length = 0;
|
||||
this.canParseCommandOrComma = true;
|
||||
};
|
||||
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const c = str[i];
|
||||
// White spaces parsing
|
||||
const isAArcFlag = this.curCommandType === SVGPathData.ARC &&
|
||||
(this.curArgs.length === 3 || this.curArgs.length === 4) &&
|
||||
this.curNumber.length === 1 &&
|
||||
(this.curNumber === "0" || this.curNumber === "1");
|
||||
const isEndingDigit = isDigit(c) && (
|
||||
(this.curNumber === "0" && c === "0") ||
|
||||
isAArcFlag
|
||||
);
|
||||
|
||||
if (
|
||||
isDigit(c) &&
|
||||
!isEndingDigit
|
||||
) {
|
||||
this.curNumber += c;
|
||||
this.curNumberHasExpDigits = this.curNumberHasExp;
|
||||
continue;
|
||||
}
|
||||
if ("e" === c || "E" === c) {
|
||||
this.curNumber += c;
|
||||
this.curNumberHasExp = true;
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
("-" === c || "+" === c) &&
|
||||
this.curNumberHasExp &&
|
||||
!this.curNumberHasExpDigits
|
||||
) {
|
||||
this.curNumber += c;
|
||||
continue;
|
||||
}
|
||||
// if we already have a ".", it means we are starting a new number
|
||||
if ("." === c && !this.curNumberHasExp && !this.curNumberHasDecimal && !isAArcFlag) {
|
||||
this.curNumber += c;
|
||||
this.curNumberHasDecimal = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// New number
|
||||
if (this.curNumber && -1 !== this.curCommandType) {
|
||||
const val = Number(this.curNumber);
|
||||
if (isNaN(val)) {
|
||||
throw new SyntaxError(`Invalid number ending at ${i}`);
|
||||
}
|
||||
if (this.curCommandType === SVGPathData.ARC) {
|
||||
if (0 === this.curArgs.length || 1 === this.curArgs.length) {
|
||||
if (0 > val) {
|
||||
throw new SyntaxError(
|
||||
`Expected positive number, got "${val}" at index "${i}"`,
|
||||
);
|
||||
}
|
||||
} else if (3 === this.curArgs.length || 4 === this.curArgs.length) {
|
||||
if ("0" !== this.curNumber && "1" !== this.curNumber) {
|
||||
throw new SyntaxError(
|
||||
`Expected a flag, got "${this.curNumber}" at index "${i}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.curArgs.push(val);
|
||||
if (this.curArgs.length === COMMAND_ARG_COUNTS[this.curCommandType]) {
|
||||
if (SVGPathData.HORIZ_LINE_TO === this.curCommandType) {
|
||||
finishCommand({
|
||||
type: SVGPathData.HORIZ_LINE_TO,
|
||||
relative: this.curCommandRelative,
|
||||
x: val,
|
||||
});
|
||||
} else if (SVGPathData.VERT_LINE_TO === this.curCommandType) {
|
||||
finishCommand({
|
||||
type: SVGPathData.VERT_LINE_TO,
|
||||
relative: this.curCommandRelative,
|
||||
y: val,
|
||||
});
|
||||
// Move to / line to / smooth quadratic curve to commands (x, y)
|
||||
} else if (
|
||||
this.curCommandType === SVGPathData.MOVE_TO ||
|
||||
this.curCommandType === SVGPathData.LINE_TO ||
|
||||
this.curCommandType === SVGPathData.SMOOTH_QUAD_TO
|
||||
) {
|
||||
finishCommand({
|
||||
type: this.curCommandType,
|
||||
relative: this.curCommandRelative,
|
||||
x: this.curArgs[0],
|
||||
y: this.curArgs[1],
|
||||
} as SVGCommand);
|
||||
// Switch to line to state
|
||||
if (SVGPathData.MOVE_TO === this.curCommandType) {
|
||||
this.curCommandType = SVGPathData.LINE_TO;
|
||||
}
|
||||
} else if (this.curCommandType === SVGPathData.CURVE_TO) {
|
||||
finishCommand({
|
||||
type: SVGPathData.CURVE_TO,
|
||||
relative: this.curCommandRelative,
|
||||
x1: this.curArgs[0],
|
||||
y1: this.curArgs[1],
|
||||
x2: this.curArgs[2],
|
||||
y2: this.curArgs[3],
|
||||
x: this.curArgs[4],
|
||||
y: this.curArgs[5],
|
||||
});
|
||||
} else if (this.curCommandType === SVGPathData.SMOOTH_CURVE_TO) {
|
||||
finishCommand({
|
||||
type: SVGPathData.SMOOTH_CURVE_TO,
|
||||
relative: this.curCommandRelative,
|
||||
x2: this.curArgs[0],
|
||||
y2: this.curArgs[1],
|
||||
x: this.curArgs[2],
|
||||
y: this.curArgs[3],
|
||||
});
|
||||
} else if (this.curCommandType === SVGPathData.QUAD_TO) {
|
||||
finishCommand({
|
||||
type: SVGPathData.QUAD_TO,
|
||||
relative: this.curCommandRelative,
|
||||
x1: this.curArgs[0],
|
||||
y1: this.curArgs[1],
|
||||
x: this.curArgs[2],
|
||||
y: this.curArgs[3],
|
||||
});
|
||||
} else if (this.curCommandType === SVGPathData.ARC) {
|
||||
finishCommand({
|
||||
type: SVGPathData.ARC,
|
||||
relative: this.curCommandRelative,
|
||||
rX: this.curArgs[0],
|
||||
rY: this.curArgs[1],
|
||||
xRot: this.curArgs[2],
|
||||
lArcFlag: this.curArgs[3] as 0 | 1,
|
||||
sweepFlag: this.curArgs[4] as 0 | 1,
|
||||
x: this.curArgs[5],
|
||||
y: this.curArgs[6],
|
||||
});
|
||||
}
|
||||
}
|
||||
this.curNumber = "";
|
||||
this.curNumberHasExpDigits = false;
|
||||
this.curNumberHasExp = false;
|
||||
this.curNumberHasDecimal = false;
|
||||
this.canParseCommandOrComma = true;
|
||||
}
|
||||
// Continue if a white space or a comma was detected
|
||||
if (isWhiteSpace(c)) {
|
||||
continue;
|
||||
}
|
||||
if ("," === c && this.canParseCommandOrComma) {
|
||||
// L 0,0, H is not valid:
|
||||
this.canParseCommandOrComma = false;
|
||||
continue;
|
||||
}
|
||||
// if a sign is detected, then parse the new number
|
||||
if ("+" === c || "-" === c || "." === c) {
|
||||
this.curNumber = c;
|
||||
this.curNumberHasDecimal = "." === c;
|
||||
continue;
|
||||
}
|
||||
// if a 0 is detected, then parse the new number
|
||||
if (isEndingDigit) {
|
||||
this.curNumber = c;
|
||||
this.curNumberHasDecimal = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Adding residual command
|
||||
if (0 !== this.curArgs.length) {
|
||||
throw new SyntaxError(`Unterminated command at index ${i}.`);
|
||||
}
|
||||
if (!this.canParseCommandOrComma) {
|
||||
throw new SyntaxError(
|
||||
`Unexpected character "${c}" at index ${i}. Command cannot follow comma`,
|
||||
);
|
||||
}
|
||||
this.canParseCommandOrComma = false;
|
||||
// Detecting the next command
|
||||
if ("z" === c || "Z" === c) {
|
||||
commands.push({
|
||||
type: SVGPathData.CLOSE_PATH,
|
||||
});
|
||||
this.canParseCommandOrComma = true;
|
||||
this.curCommandType = -1;
|
||||
continue;
|
||||
// Horizontal move to command
|
||||
} else if ("h" === c || "H" === c) {
|
||||
this.curCommandType = SVGPathData.HORIZ_LINE_TO;
|
||||
this.curCommandRelative = "h" === c;
|
||||
// Vertical move to command
|
||||
} else if ("v" === c || "V" === c) {
|
||||
this.curCommandType = SVGPathData.VERT_LINE_TO;
|
||||
this.curCommandRelative = "v" === c;
|
||||
// Move to command
|
||||
} else if ("m" === c || "M" === c) {
|
||||
this.curCommandType = SVGPathData.MOVE_TO;
|
||||
this.curCommandRelative = "m" === c;
|
||||
// Line to command
|
||||
} else if ("l" === c || "L" === c) {
|
||||
this.curCommandType = SVGPathData.LINE_TO;
|
||||
this.curCommandRelative = "l" === c;
|
||||
// Curve to command
|
||||
} else if ("c" === c || "C" === c) {
|
||||
this.curCommandType = SVGPathData.CURVE_TO;
|
||||
this.curCommandRelative = "c" === c;
|
||||
// Smooth curve to command
|
||||
} else if ("s" === c || "S" === c) {
|
||||
this.curCommandType = SVGPathData.SMOOTH_CURVE_TO;
|
||||
this.curCommandRelative = "s" === c;
|
||||
// Quadratic bezier curve to command
|
||||
} else if ("q" === c || "Q" === c) {
|
||||
this.curCommandType = SVGPathData.QUAD_TO;
|
||||
this.curCommandRelative = "q" === c;
|
||||
// Smooth quadratic bezier curve to command
|
||||
} else if ("t" === c || "T" === c) {
|
||||
this.curCommandType = SVGPathData.SMOOTH_QUAD_TO;
|
||||
this.curCommandRelative = "t" === c;
|
||||
// Elliptic arc command
|
||||
} else if ("a" === c || "A" === c) {
|
||||
this.curCommandType = SVGPathData.ARC;
|
||||
this.curCommandRelative = "a" === c;
|
||||
} else {
|
||||
throw new SyntaxError(`Unexpected character "${c}" at index ${i}.`);
|
||||
}
|
||||
}
|
||||
return commands;
|
||||
}
|
||||
/**
|
||||
* Return a wrapper around this parser which applies the transformation on parsed commands.
|
||||
*/
|
||||
transform(transform: TransformFunction) {
|
||||
const result = Object.create(this, {
|
||||
parse: {
|
||||
value(chunk: string, commands: SVGCommand[] = []) {
|
||||
const parsedCommands = Object.getPrototypeOf(this).parse.call(
|
||||
this,
|
||||
chunk,
|
||||
);
|
||||
for (const c of parsedCommands) {
|
||||
const cT = transform(c);
|
||||
if (Array.isArray(cT)) {
|
||||
commands.push(...cT);
|
||||
} else {
|
||||
commands.push(cT);
|
||||
}
|
||||
}
|
||||
return commands;
|
||||
},
|
||||
},
|
||||
});
|
||||
return result as this;
|
||||
}
|
||||
}
|
||||
620
node_modules/svg-pathdata/src/SVGPathDataTransformer.ts
generated
vendored
Normal file
620
node_modules/svg-pathdata/src/SVGPathDataTransformer.ts
generated
vendored
Normal file
@@ -0,0 +1,620 @@
|
||||
// Transform SVG PathData
|
||||
// http://www.w3.org/TR/SVG/paths.html#PathDataBNF
|
||||
|
||||
import { a2c, annotateArcCommand, arcAt, assertNumbers, bezierAt, bezierRoot,
|
||||
intersectionUnitCircleLine } from "./mathUtils";
|
||||
import { SVGPathData } from "./SVGPathData";
|
||||
import { SVGCommand, TransformFunction } from "./types";
|
||||
|
||||
export namespace SVGPathDataTransformer {
|
||||
// Predefined transforming functions
|
||||
// Rounds commands values
|
||||
export function ROUND(roundVal = 1e13) {
|
||||
assertNumbers(roundVal);
|
||||
function rf(val: number) { return Math.round(val * roundVal) / roundVal; }
|
||||
return function round(command: any) {
|
||||
if ("undefined" !== typeof command.x1) {
|
||||
command.x1 = rf(command.x1);
|
||||
}
|
||||
if ("undefined" !== typeof command.y1) {
|
||||
command.y1 = rf(command.y1);
|
||||
}
|
||||
|
||||
if ("undefined" !== typeof command.x2) {
|
||||
command.x2 = rf(command.x2);
|
||||
}
|
||||
if ("undefined" !== typeof command.y2) {
|
||||
command.y2 = rf(command.y2);
|
||||
}
|
||||
|
||||
if ("undefined" !== typeof command.x) {
|
||||
command.x = rf(command.x);
|
||||
}
|
||||
if ("undefined" !== typeof command.y) {
|
||||
command.y = rf(command.y);
|
||||
}
|
||||
|
||||
if ("undefined" !== typeof command.rX) {
|
||||
command.rX = rf(command.rX);
|
||||
}
|
||||
if ("undefined" !== typeof command.rY) {
|
||||
command.rY = rf(command.rY);
|
||||
}
|
||||
|
||||
return command;
|
||||
};
|
||||
}
|
||||
// Relative to absolute commands
|
||||
export function TO_ABS() {
|
||||
return INFO((command, prevX, prevY) => {
|
||||
if (command.relative) {
|
||||
// x1/y1 values
|
||||
if ("undefined" !== typeof command.x1) {
|
||||
command.x1 += prevX;
|
||||
}
|
||||
if ("undefined" !== typeof command.y1) {
|
||||
command.y1 += prevY;
|
||||
}
|
||||
// x2/y2 values
|
||||
if ("undefined" !== typeof command.x2) {
|
||||
command.x2 += prevX;
|
||||
}
|
||||
if ("undefined" !== typeof command.y2) {
|
||||
command.y2 += prevY;
|
||||
}
|
||||
// Finally x/y values
|
||||
if ("undefined" !== typeof command.x) {
|
||||
command.x += prevX;
|
||||
}
|
||||
if ("undefined" !== typeof command.y) {
|
||||
command.y += prevY;
|
||||
}
|
||||
command.relative = false;
|
||||
}
|
||||
return command;
|
||||
});
|
||||
}
|
||||
// Absolute to relative commands
|
||||
export function TO_REL() {
|
||||
return INFO((command, prevX, prevY) => {
|
||||
if (!command.relative) {
|
||||
// x1/y1 values
|
||||
if ("undefined" !== typeof command.x1) {
|
||||
command.x1 -= prevX;
|
||||
}
|
||||
if ("undefined" !== typeof command.y1) {
|
||||
command.y1 -= prevY;
|
||||
}
|
||||
// x2/y2 values
|
||||
if ("undefined" !== typeof command.x2) {
|
||||
command.x2 -= prevX;
|
||||
}
|
||||
if ("undefined" !== typeof command.y2) {
|
||||
command.y2 -= prevY;
|
||||
}
|
||||
// Finally x/y values
|
||||
if ("undefined" !== typeof command.x) {
|
||||
command.x -= prevX;
|
||||
}
|
||||
if ("undefined" !== typeof command.y) {
|
||||
command.y -= prevY;
|
||||
}
|
||||
command.relative = true;
|
||||
}
|
||||
return command;
|
||||
});
|
||||
}
|
||||
// Convert H, V, Z and A with rX = 0 to L
|
||||
export function NORMALIZE_HVZ(normalizeZ = true, normalizeH = true, normalizeV = true) {
|
||||
return INFO((command, prevX, prevY, pathStartX, pathStartY) => {
|
||||
if (isNaN(pathStartX) && !(command.type & SVGPathData.MOVE_TO)) {
|
||||
throw new Error("path must start with moveto");
|
||||
}
|
||||
if (normalizeH && command.type & SVGPathData.HORIZ_LINE_TO) {
|
||||
command.type = SVGPathData.LINE_TO;
|
||||
command.y = command.relative ? 0 : prevY;
|
||||
}
|
||||
if (normalizeV && command.type & SVGPathData.VERT_LINE_TO) {
|
||||
command.type = SVGPathData.LINE_TO;
|
||||
command.x = command.relative ? 0 : prevX;
|
||||
}
|
||||
if (normalizeZ && command.type & SVGPathData.CLOSE_PATH) {
|
||||
command.type = SVGPathData.LINE_TO;
|
||||
command.x = command.relative ? pathStartX - prevX : pathStartX;
|
||||
command.y = command.relative ? pathStartY - prevY : pathStartY;
|
||||
}
|
||||
if (command.type & SVGPathData.ARC && (0 === command.rX || 0 === command.rY)) {
|
||||
command.type = SVGPathData.LINE_TO;
|
||||
delete command.rX;
|
||||
delete command.rY;
|
||||
delete command.xRot;
|
||||
delete command.lArcFlag;
|
||||
delete command.sweepFlag;
|
||||
}
|
||||
return command;
|
||||
});
|
||||
}
|
||||
/*
|
||||
* Transforms smooth curves and quads to normal curves and quads (SsTt to CcQq)
|
||||
*/
|
||||
export function NORMALIZE_ST() {
|
||||
let prevCurveC2X = NaN;
|
||||
let prevCurveC2Y = NaN;
|
||||
let prevQuadCX = NaN;
|
||||
let prevQuadCY = NaN;
|
||||
|
||||
return INFO((command, prevX, prevY) => {
|
||||
if (command.type & SVGPathData.SMOOTH_CURVE_TO) {
|
||||
command.type = SVGPathData.CURVE_TO;
|
||||
prevCurveC2X = isNaN(prevCurveC2X) ? prevX : prevCurveC2X;
|
||||
prevCurveC2Y = isNaN(prevCurveC2Y) ? prevY : prevCurveC2Y;
|
||||
command.x1 = command.relative ? prevX - prevCurveC2X : 2 * prevX - prevCurveC2X;
|
||||
command.y1 = command.relative ? prevY - prevCurveC2Y : 2 * prevY - prevCurveC2Y;
|
||||
}
|
||||
if (command.type & SVGPathData.CURVE_TO) {
|
||||
prevCurveC2X = command.relative ? prevX + command.x2 : command.x2;
|
||||
prevCurveC2Y = command.relative ? prevY + command.y2 : command.y2;
|
||||
} else {
|
||||
prevCurveC2X = NaN;
|
||||
prevCurveC2Y = NaN;
|
||||
}
|
||||
if (command.type & SVGPathData.SMOOTH_QUAD_TO) {
|
||||
command.type = SVGPathData.QUAD_TO;
|
||||
prevQuadCX = isNaN(prevQuadCX) ? prevX : prevQuadCX;
|
||||
prevQuadCY = isNaN(prevQuadCY) ? prevY : prevQuadCY;
|
||||
command.x1 = command.relative ? prevX - prevQuadCX : 2 * prevX - prevQuadCX;
|
||||
command.y1 = command.relative ? prevY - prevQuadCY : 2 * prevY - prevQuadCY;
|
||||
}
|
||||
if (command.type & SVGPathData.QUAD_TO) {
|
||||
prevQuadCX = command.relative ? prevX + command.x1 : command.x1;
|
||||
prevQuadCY = command.relative ? prevY + command.y1 : command.y1;
|
||||
} else {
|
||||
prevQuadCX = NaN;
|
||||
prevQuadCY = NaN;
|
||||
}
|
||||
|
||||
return command;
|
||||
});
|
||||
}
|
||||
/*
|
||||
* A quadratic bézier curve can be represented by a cubic bézier curve which has
|
||||
* the same end points as the quadratic and both control points in place of the
|
||||
* quadratic"s one.
|
||||
*
|
||||
* This transformer replaces QqTt commands with Cc commands respectively.
|
||||
* This is useful for reading path data into a system which only has a
|
||||
* representation for cubic curves.
|
||||
*/
|
||||
export function QT_TO_C() {
|
||||
let prevQuadX1 = NaN;
|
||||
let prevQuadY1 = NaN;
|
||||
|
||||
return INFO((command, prevX, prevY) => {
|
||||
if (command.type & SVGPathData.SMOOTH_QUAD_TO) {
|
||||
command.type = SVGPathData.QUAD_TO;
|
||||
prevQuadX1 = isNaN(prevQuadX1) ? prevX : prevQuadX1;
|
||||
prevQuadY1 = isNaN(prevQuadY1) ? prevY : prevQuadY1;
|
||||
command.x1 = command.relative ? prevX - prevQuadX1 : 2 * prevX - prevQuadX1;
|
||||
command.y1 = command.relative ? prevY - prevQuadY1 : 2 * prevY - prevQuadY1;
|
||||
}
|
||||
if (command.type & SVGPathData.QUAD_TO) {
|
||||
prevQuadX1 = command.relative ? prevX + command.x1 : command.x1;
|
||||
prevQuadY1 = command.relative ? prevY + command.y1 : command.y1;
|
||||
const x1 = command.x1;
|
||||
const y1 = command.y1;
|
||||
|
||||
command.type = SVGPathData.CURVE_TO;
|
||||
command.x1 = ((command.relative ? 0 : prevX) + x1 * 2) / 3;
|
||||
command.y1 = ((command.relative ? 0 : prevY) + y1 * 2) / 3;
|
||||
command.x2 = (command.x + x1 * 2) / 3;
|
||||
command.y2 = (command.y + y1 * 2) / 3;
|
||||
} else {
|
||||
prevQuadX1 = NaN;
|
||||
prevQuadY1 = NaN;
|
||||
}
|
||||
|
||||
return command;
|
||||
});
|
||||
}
|
||||
export function INFO(
|
||||
f: (command: any, prevXAbs: number, prevYAbs: number,
|
||||
pathStartXAbs: number, pathStartYAbs: number) => any | any[]) {
|
||||
let prevXAbs = 0;
|
||||
let prevYAbs = 0;
|
||||
let pathStartXAbs = NaN;
|
||||
let pathStartYAbs = NaN;
|
||||
|
||||
return function transform(command: any) {
|
||||
if (isNaN(pathStartXAbs) && !(command.type & SVGPathData.MOVE_TO)) {
|
||||
throw new Error("path must start with moveto");
|
||||
}
|
||||
|
||||
const result = f(command, prevXAbs, prevYAbs, pathStartXAbs, pathStartYAbs);
|
||||
|
||||
if (command.type & SVGPathData.CLOSE_PATH) {
|
||||
prevXAbs = pathStartXAbs;
|
||||
prevYAbs = pathStartYAbs;
|
||||
}
|
||||
|
||||
if ("undefined" !== typeof command.x) {
|
||||
prevXAbs = (command.relative ? prevXAbs + command.x : command.x);
|
||||
}
|
||||
if ("undefined" !== typeof command.y) {
|
||||
prevYAbs = (command.relative ? prevYAbs + command.y : command.y);
|
||||
}
|
||||
|
||||
if (command.type & SVGPathData.MOVE_TO) {
|
||||
pathStartXAbs = prevXAbs;
|
||||
pathStartYAbs = prevYAbs;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
/*
|
||||
* remove 0-length segments
|
||||
*/
|
||||
export function SANITIZE(EPS = 0) {
|
||||
assertNumbers(EPS);
|
||||
let prevCurveC2X = NaN;
|
||||
let prevCurveC2Y = NaN;
|
||||
let prevQuadCX = NaN;
|
||||
let prevQuadCY = NaN;
|
||||
|
||||
return INFO((command, prevX, prevY, pathStartX, pathStartY) => {
|
||||
const abs = Math.abs;
|
||||
let skip = false;
|
||||
let x1Rel = 0;
|
||||
let y1Rel = 0;
|
||||
|
||||
if (command.type & SVGPathData.SMOOTH_CURVE_TO) {
|
||||
x1Rel = isNaN(prevCurveC2X) ? 0 : prevX - prevCurveC2X;
|
||||
y1Rel = isNaN(prevCurveC2Y) ? 0 : prevY - prevCurveC2Y;
|
||||
}
|
||||
if (command.type & (SVGPathData.CURVE_TO | SVGPathData.SMOOTH_CURVE_TO)) {
|
||||
prevCurveC2X = command.relative ? prevX + command.x2 : command.x2;
|
||||
prevCurveC2Y = command.relative ? prevY + command.y2 : command.y2;
|
||||
} else {
|
||||
prevCurveC2X = NaN;
|
||||
prevCurveC2Y = NaN;
|
||||
}
|
||||
if (command.type & SVGPathData.SMOOTH_QUAD_TO) {
|
||||
prevQuadCX = isNaN(prevQuadCX) ? prevX : 2 * prevX - prevQuadCX;
|
||||
prevQuadCY = isNaN(prevQuadCY) ? prevY : 2 * prevY - prevQuadCY;
|
||||
} else if (command.type & SVGPathData.QUAD_TO) {
|
||||
prevQuadCX = command.relative ? prevX + command.x1 : command.x1;
|
||||
prevQuadCY = command.relative ? prevY + command.y1 : command.y2;
|
||||
} else {
|
||||
prevQuadCX = NaN;
|
||||
prevQuadCY = NaN;
|
||||
}
|
||||
|
||||
if (command.type & SVGPathData.LINE_COMMANDS ||
|
||||
command.type & SVGPathData.ARC && (0 === command.rX || 0 === command.rY || !command.lArcFlag) ||
|
||||
command.type & SVGPathData.CURVE_TO || command.type & SVGPathData.SMOOTH_CURVE_TO ||
|
||||
command.type & SVGPathData.QUAD_TO || command.type & SVGPathData.SMOOTH_QUAD_TO) {
|
||||
const xRel = "undefined" === typeof command.x ? 0 :
|
||||
(command.relative ? command.x : command.x - prevX);
|
||||
const yRel = "undefined" === typeof command.y ? 0 :
|
||||
(command.relative ? command.y : command.y - prevY);
|
||||
|
||||
x1Rel = !isNaN(prevQuadCX) ? prevQuadCX - prevX :
|
||||
"undefined" === typeof command.x1 ? x1Rel :
|
||||
command.relative ? command.x :
|
||||
command.x1 - prevX;
|
||||
y1Rel = !isNaN(prevQuadCY) ? prevQuadCY - prevY :
|
||||
"undefined" === typeof command.y1 ? y1Rel :
|
||||
command.relative ? command.y :
|
||||
command.y1 - prevY;
|
||||
|
||||
const x2Rel = "undefined" === typeof command.x2 ? 0 :
|
||||
(command.relative ? command.x : command.x2 - prevX);
|
||||
const y2Rel = "undefined" === typeof command.y2 ? 0 :
|
||||
(command.relative ? command.y : command.y2 - prevY);
|
||||
|
||||
if (abs(xRel) <= EPS && abs(yRel) <= EPS &&
|
||||
abs(x1Rel) <= EPS && abs(y1Rel) <= EPS &&
|
||||
abs(x2Rel) <= EPS && abs(y2Rel) <= EPS) {
|
||||
skip = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (command.type & SVGPathData.CLOSE_PATH) {
|
||||
if (abs(prevX - pathStartX) <= EPS && abs(prevY - pathStartY) <= EPS) {
|
||||
skip = true;
|
||||
}
|
||||
}
|
||||
|
||||
return skip ? [] : command;
|
||||
});
|
||||
}
|
||||
// SVG Transforms : http://www.w3.org/TR/SVGTiny12/coords.html#TransformList
|
||||
// Matrix : http://apike.ca/prog_svg_transform.html
|
||||
// a c e
|
||||
// b d f
|
||||
export function MATRIX(a: number, b: number, c: number, d: number, e: number, f: number) {
|
||||
assertNumbers(a, b, c, d, e, f);
|
||||
|
||||
return INFO((command, prevX, prevY, pathStartX) => {
|
||||
const origX1 = command.x1;
|
||||
const origX2 = command.x2;
|
||||
// if isNaN(pathStartX), then this is the first command, which is ALWAYS an
|
||||
// absolute MOVE_TO, regardless what the relative flag says
|
||||
const comRel = command.relative && !isNaN(pathStartX);
|
||||
const x = "undefined" !== typeof command.x ? command.x : (comRel ? 0 : prevX);
|
||||
const y = "undefined" !== typeof command.y ? command.y : (comRel ? 0 : prevY);
|
||||
|
||||
if (command.type & SVGPathData.HORIZ_LINE_TO && 0 !== b) {
|
||||
command.type = SVGPathData.LINE_TO;
|
||||
command.y = command.relative ? 0 : prevY;
|
||||
}
|
||||
if (command.type & SVGPathData.VERT_LINE_TO && 0 !== c) {
|
||||
command.type = SVGPathData.LINE_TO;
|
||||
command.x = command.relative ? 0 : prevX;
|
||||
}
|
||||
|
||||
if ("undefined" !== typeof command.x) {
|
||||
command.x = (command.x * a) + (y * c) + (comRel ? 0 : e);
|
||||
}
|
||||
if ("undefined" !== typeof command.y) {
|
||||
command.y = (x * b) + command.y * d + (comRel ? 0 : f);
|
||||
}
|
||||
if ("undefined" !== typeof command.x1) {
|
||||
command.x1 = command.x1 * a + command.y1 * c + (comRel ? 0 : e);
|
||||
}
|
||||
if ("undefined" !== typeof command.y1) {
|
||||
command.y1 = origX1 * b + command.y1 * d + (comRel ? 0 : f);
|
||||
}
|
||||
if ("undefined" !== typeof command.x2) {
|
||||
command.x2 = command.x2 * a + command.y2 * c + (comRel ? 0 : e);
|
||||
}
|
||||
if ("undefined" !== typeof command.y2) {
|
||||
command.y2 = origX2 * b + command.y2 * d + (comRel ? 0 : f);
|
||||
}
|
||||
function sqr(x: number) { return x * x; }
|
||||
const det = a * d - b * c;
|
||||
|
||||
if ("undefined" !== typeof command.xRot) {
|
||||
// Skip if this is a pure translation
|
||||
if (1 !== a || 0 !== b || 0 !== c || 1 !== d) {
|
||||
// Special case for singular matrix
|
||||
if (0 === det) {
|
||||
// In the singular case, the arc is compressed to a line. The actual geometric image of the original
|
||||
// curve under this transform possibly extends beyond the starting and/or ending points of the segment, but
|
||||
// for simplicity we ignore this detail and just replace this command with a single line segment.
|
||||
delete command.rX;
|
||||
delete command.rY;
|
||||
delete command.xRot;
|
||||
delete command.lArcFlag;
|
||||
delete command.sweepFlag;
|
||||
command.type = SVGPathData.LINE_TO;
|
||||
} else {
|
||||
// Convert to radians
|
||||
const xRot = command.xRot * Math.PI / 180;
|
||||
|
||||
// Convert rotated ellipse to general conic form
|
||||
// x0^2/rX^2 + y0^2/rY^2 - 1 = 0
|
||||
// x0 = x*cos(xRot) + y*sin(xRot)
|
||||
// y0 = -x*sin(xRot) + y*cos(xRot)
|
||||
// --> A*x^2 + B*x*y + C*y^2 - 1 = 0, where
|
||||
const sinRot = Math.sin(xRot);
|
||||
const cosRot = Math.cos(xRot);
|
||||
const xCurve = 1 / sqr(command.rX);
|
||||
const yCurve = 1 / sqr(command.rY);
|
||||
const A = sqr(cosRot) * xCurve + sqr(sinRot) * yCurve;
|
||||
const B = 2 * sinRot * cosRot * (xCurve - yCurve);
|
||||
const C = sqr(sinRot) * xCurve + sqr(cosRot) * yCurve;
|
||||
|
||||
// Apply matrix to A*x^2 + B*x*y + C*y^2 - 1 = 0
|
||||
// x1 = a*x + c*y
|
||||
// y1 = b*x + d*y
|
||||
// (we can ignore e and f, since pure translations don"t affect the shape of the ellipse)
|
||||
// --> A1*x1^2 + B1*x1*y1 + C1*y1^2 - det^2 = 0, where
|
||||
const A1 = A * d * d - B * b * d + C * b * b;
|
||||
const B1 = B * (a * d + b * c) - 2 * (A * c * d + C * a * b);
|
||||
const C1 = A * c * c - B * a * c + C * a * a;
|
||||
|
||||
// Unapply newXRot to get back to axis-aligned ellipse equation
|
||||
// x1 = x2*cos(newXRot) - y2*sin(newXRot)
|
||||
// y1 = x2*sin(newXRot) + y2*cos(newXRot)
|
||||
// A1*x1^2 + B1*x1*y1 + C1*y1^2 - det^2 =
|
||||
// x2^2*(A1*cos(newXRot)^2 + B1*sin(newXRot)*cos(newXRot) + C1*sin(newXRot)^2)
|
||||
// + x2*y2*(2*(C1 - A1)*sin(newXRot)*cos(newXRot) + B1*(cos(newXRot)^2 - sin(newXRot)^2))
|
||||
// + y2^2*(A1*sin(newXRot)^2 - B1*sin(newXRot)*cos(newXRot) + C1*cos(newXRot)^2)
|
||||
// (which must have the same zeroes as)
|
||||
// x2^2/newRX^2 + y2^2/newRY^2 - 1
|
||||
// (so we have)
|
||||
// 2*(C1 - A1)*sin(newXRot)*cos(newXRot) + B1*(cos(newXRot)^2 - sin(newXRot)^2) = 0
|
||||
// (A1 - C1)*sin(2*newXRot) = B1*cos(2*newXRot)
|
||||
// 2*newXRot = atan2(B1, A1 - C1)
|
||||
const newXRot = ((Math.atan2(B1, A1 - C1) + Math.PI) % Math.PI) / 2;
|
||||
// For any integer n, (atan2(B1, A1 - C1) + n*pi)/2 is a solution to the above; incrementing n just swaps
|
||||
// the x and y radii computed below (since that"s what rotating an ellipse by pi/2 does). Choosing the
|
||||
// rotation between 0 and pi/2 eliminates the ambiguity and leads to more predictable output.
|
||||
|
||||
// Finally, we get newRX and newRY from the same-zeroes relationship that gave us newXRot
|
||||
const newSinRot = Math.sin(newXRot);
|
||||
const newCosRot = Math.cos(newXRot);
|
||||
|
||||
command.rX = Math.abs(det) /
|
||||
Math.sqrt(A1 * sqr(newCosRot) + B1 * newSinRot * newCosRot + C1 * sqr(newSinRot));
|
||||
command.rY = Math.abs(det) /
|
||||
Math.sqrt(A1 * sqr(newSinRot) - B1 * newSinRot * newCosRot + C1 * sqr(newCosRot));
|
||||
command.xRot = newXRot * 180 / Math.PI;
|
||||
}
|
||||
}
|
||||
}
|
||||
// sweepFlag needs to be inverted when mirroring shapes
|
||||
// see http://www.itk.ilstu.edu/faculty/javila/SVG/SVG_drawing1/elliptical_curve.htm
|
||||
// m 65,10 a 50,25 0 1 0 50,25
|
||||
// M 65,60 A 50,25 0 1 1 115,35
|
||||
if ("undefined" !== typeof command.sweepFlag && 0 > det) {
|
||||
command.sweepFlag = +!command.sweepFlag;
|
||||
}
|
||||
return command;
|
||||
});
|
||||
}
|
||||
export function ROTATE(a: number, x = 0, y = 0) {
|
||||
assertNumbers(a, x, y);
|
||||
const sin = Math.sin(a);
|
||||
const cos = Math.cos(a);
|
||||
|
||||
return MATRIX(cos, sin, -sin, cos, x - x * cos + y * sin, y - x * sin - y * cos);
|
||||
}
|
||||
export function TRANSLATE(dX: number, dY = 0) {
|
||||
assertNumbers(dX, dY);
|
||||
return MATRIX(1, 0, 0, 1, dX, dY);
|
||||
}
|
||||
export function SCALE(dX: number, dY = dX) {
|
||||
assertNumbers(dX, dY);
|
||||
return MATRIX(dX, 0, 0, dY, 0, 0);
|
||||
}
|
||||
export function SKEW_X(a: number) {
|
||||
assertNumbers(a);
|
||||
return MATRIX(1, 0, Math.atan(a), 1, 0, 0);
|
||||
}
|
||||
export function SKEW_Y(a: number) {
|
||||
assertNumbers(a);
|
||||
return MATRIX(1, Math.atan(a), 0, 1, 0, 0);
|
||||
}
|
||||
export function X_AXIS_SYMMETRY(xOffset = 0) {
|
||||
assertNumbers(xOffset);
|
||||
return MATRIX(-1, 0, 0, 1, xOffset, 0);
|
||||
}
|
||||
export function Y_AXIS_SYMMETRY(yOffset = 0) {
|
||||
assertNumbers(yOffset);
|
||||
return MATRIX(1, 0, 0, -1, 0, yOffset);
|
||||
}
|
||||
// Convert arc commands to curve commands
|
||||
export function A_TO_C() {
|
||||
return INFO((command, prevX, prevY) => {
|
||||
if (SVGPathData.ARC === command.type) {
|
||||
return a2c(command, command.relative ? 0 : prevX, command.relative ? 0 : prevY);
|
||||
}
|
||||
return command;
|
||||
});
|
||||
}
|
||||
// @see annotateArcCommand
|
||||
export function ANNOTATE_ARCS() {
|
||||
return INFO((c, x1, y1) => {
|
||||
if (c.relative) {
|
||||
x1 = 0;
|
||||
y1 = 0;
|
||||
}
|
||||
if (SVGPathData.ARC === c.type) {
|
||||
annotateArcCommand(c, x1, y1);
|
||||
}
|
||||
return c;
|
||||
});
|
||||
}
|
||||
export function CLONE() {
|
||||
return (c: SVGCommand) => {
|
||||
const result = {} as SVGCommand;
|
||||
// tslint:disable-next-line
|
||||
for (const key in c) {
|
||||
result[key as keyof SVGCommand] = c[key as keyof SVGCommand];
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
// @see annotateArcCommand
|
||||
export function CALCULATE_BOUNDS() {
|
||||
const clone = CLONE();
|
||||
const toAbs = TO_ABS();
|
||||
const qtToC = QT_TO_C();
|
||||
const normST = NORMALIZE_ST();
|
||||
const f: TransformFunction & {minX: number, maxX: number, minY: number, maxY: number} =
|
||||
INFO((command, prevXAbs, prevYAbs) => {
|
||||
const c = normST(qtToC(toAbs(clone(command))));
|
||||
function fixX(absX: number) {
|
||||
if (absX > f.maxX) { f.maxX = absX; }
|
||||
if (absX < f.minX) { f.minX = absX; }
|
||||
}
|
||||
function fixY(absY: number) {
|
||||
if (absY > f.maxY) { f.maxY = absY; }
|
||||
if (absY < f.minY) { f.minY = absY; }
|
||||
}
|
||||
if (c.type & SVGPathData.DRAWING_COMMANDS) {
|
||||
fixX(prevXAbs);
|
||||
fixY(prevYAbs);
|
||||
}
|
||||
if (c.type & SVGPathData.HORIZ_LINE_TO) {
|
||||
fixX(c.x);
|
||||
}
|
||||
if (c.type & SVGPathData.VERT_LINE_TO) {
|
||||
fixY(c.y);
|
||||
}
|
||||
if (c.type & SVGPathData.LINE_TO) {
|
||||
fixX(c.x);
|
||||
fixY(c.y);
|
||||
}
|
||||
if (c.type & SVGPathData.CURVE_TO) {
|
||||
// add start and end points
|
||||
fixX(c.x);
|
||||
fixY(c.y);
|
||||
const xDerivRoots = bezierRoot(prevXAbs, c.x1, c.x2, c.x);
|
||||
|
||||
for (const derivRoot of xDerivRoots) {
|
||||
if (0 < derivRoot && 1 > derivRoot) {
|
||||
fixX(bezierAt(prevXAbs, c.x1, c.x2, c.x, derivRoot));
|
||||
}
|
||||
}
|
||||
const yDerivRoots = bezierRoot(prevYAbs, c.y1, c.y2, c.y);
|
||||
|
||||
for (const derivRoot of yDerivRoots) {
|
||||
if (0 < derivRoot && 1 > derivRoot) {
|
||||
fixY(bezierAt(prevYAbs, c.y1, c.y2, c.y, derivRoot));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (c.type & SVGPathData.ARC) {
|
||||
// add start and end points
|
||||
fixX(c.x);
|
||||
fixY(c.y);
|
||||
annotateArcCommand(c, prevXAbs, prevYAbs);
|
||||
// p = cos(phi) * xv + sin(phi) * yv
|
||||
// dp = -sin(phi) * xv + cos(phi) * yv = 0
|
||||
const xRotRad = c.xRot / 180 * Math.PI;
|
||||
// points on ellipse for phi = 0° and phi = 90°
|
||||
const x0 = Math.cos(xRotRad) * c.rX;
|
||||
const y0 = Math.sin(xRotRad) * c.rX;
|
||||
const x90 = -Math.sin(xRotRad) * c.rY;
|
||||
const y90 = Math.cos(xRotRad) * c.rY;
|
||||
|
||||
// annotateArcCommand returns phi1 and phi2 such that -180° < phi1 < 180° and phi2 is smaller or greater
|
||||
// depending on the sweep flag. Calculate phiMin, phiMax such that -180° < phiMin < 180° and phiMin < phiMax
|
||||
const [phiMin, phiMax] = c.phi1 < c.phi2 ?
|
||||
[c.phi1, c.phi2] :
|
||||
(-180 > c.phi2 ? [c.phi2 + 360, c.phi1 + 360] : [c.phi2, c.phi1]);
|
||||
const normalizeXiEta = ([xi, eta]: [number, number]) => {
|
||||
const phiRad = Math.atan2(eta, xi);
|
||||
const phi = phiRad * 180 / Math.PI;
|
||||
|
||||
return phi < phiMin ? phi + 360 : phi;
|
||||
};
|
||||
// xi = cos(phi), eta = sin(phi)
|
||||
|
||||
const xDerivRoots = intersectionUnitCircleLine(x90, -x0, 0).map(normalizeXiEta);
|
||||
for (const derivRoot of xDerivRoots) {
|
||||
if (derivRoot > phiMin && derivRoot < phiMax) {
|
||||
fixX(arcAt(c.cX, x0, x90, derivRoot));
|
||||
}
|
||||
}
|
||||
|
||||
const yDerivRoots = intersectionUnitCircleLine(y90, -y0, 0).map(normalizeXiEta);
|
||||
for (const derivRoot of yDerivRoots) {
|
||||
if (derivRoot > phiMin && derivRoot < phiMax) {
|
||||
fixY(arcAt(c.cY, y0, y90, derivRoot));
|
||||
}
|
||||
}
|
||||
}
|
||||
return command;
|
||||
}) as any;
|
||||
|
||||
f.minX = Infinity;
|
||||
f.maxX = -Infinity;
|
||||
f.minY = Infinity;
|
||||
f.maxY = -Infinity;
|
||||
return f;
|
||||
}
|
||||
}
|
||||
74
node_modules/svg-pathdata/src/TransformableSVG.ts
generated
vendored
Normal file
74
node_modules/svg-pathdata/src/TransformableSVG.ts
generated
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
import { SVGPathDataTransformer } from "./SVGPathDataTransformer";
|
||||
import { TransformFunction } from "./types";
|
||||
|
||||
export abstract class TransformableSVG {
|
||||
round(x?: number) {
|
||||
return this.transform(SVGPathDataTransformer.ROUND(x));
|
||||
}
|
||||
|
||||
toAbs() {
|
||||
return this.transform(SVGPathDataTransformer.TO_ABS());
|
||||
}
|
||||
|
||||
toRel() {
|
||||
return this.transform(SVGPathDataTransformer.TO_REL());
|
||||
}
|
||||
|
||||
normalizeHVZ(a?: boolean, b?: boolean, c?: boolean) {
|
||||
return this.transform(SVGPathDataTransformer.NORMALIZE_HVZ(a, b, c));
|
||||
}
|
||||
|
||||
normalizeST() {
|
||||
return this.transform(SVGPathDataTransformer.NORMALIZE_ST());
|
||||
}
|
||||
|
||||
qtToC() {
|
||||
return this.transform(SVGPathDataTransformer.QT_TO_C());
|
||||
}
|
||||
|
||||
aToC() {
|
||||
return this.transform(SVGPathDataTransformer.A_TO_C());
|
||||
}
|
||||
|
||||
sanitize(eps?: number) {
|
||||
return this.transform(SVGPathDataTransformer.SANITIZE(eps));
|
||||
}
|
||||
|
||||
translate(x: number, y?: number) {
|
||||
return this.transform(SVGPathDataTransformer.TRANSLATE(x, y));
|
||||
}
|
||||
|
||||
scale(x: number, y?: number) {
|
||||
return this.transform(SVGPathDataTransformer.SCALE(x, y));
|
||||
}
|
||||
|
||||
rotate(a: number, x?: number, y?: number) {
|
||||
return this.transform(SVGPathDataTransformer.ROTATE(a, x, y));
|
||||
}
|
||||
|
||||
matrix(a: number, b: number, c: number, d: number, e: number, f: number) {
|
||||
return this.transform(SVGPathDataTransformer.MATRIX(a, b, c, d, e, f));
|
||||
}
|
||||
|
||||
skewX(a: number) {
|
||||
return this.transform(SVGPathDataTransformer.SKEW_X(a));
|
||||
}
|
||||
|
||||
skewY(a: number) {
|
||||
return this.transform(SVGPathDataTransformer.SKEW_Y(a));
|
||||
}
|
||||
|
||||
xSymmetry(xOffset?: number) {
|
||||
return this.transform(SVGPathDataTransformer.X_AXIS_SYMMETRY(xOffset));
|
||||
}
|
||||
|
||||
ySymmetry(yOffset?: number) {
|
||||
return this.transform(SVGPathDataTransformer.Y_AXIS_SYMMETRY(yOffset));
|
||||
}
|
||||
|
||||
annotateArcs() {
|
||||
return this.transform(SVGPathDataTransformer.ANNOTATE_ARCS());
|
||||
}
|
||||
|
||||
abstract transform(transformFunction: TransformFunction): this;
|
||||
}
|
||||
200
node_modules/svg-pathdata/src/mathUtils.ts
generated
vendored
Normal file
200
node_modules/svg-pathdata/src/mathUtils.ts
generated
vendored
Normal file
@@ -0,0 +1,200 @@
|
||||
import { SVGPathData } from "./SVGPathData";
|
||||
import { CommandA, CommandC } from "./types";
|
||||
|
||||
export function rotate([x, y]: [number, number], rad: number) {
|
||||
return [
|
||||
x * Math.cos(rad) - y * Math.sin(rad),
|
||||
x * Math.sin(rad) + y * Math.cos(rad),
|
||||
];
|
||||
}
|
||||
|
||||
const DEBUG_CHECK_NUMBERS = true;
|
||||
export function assertNumbers(...numbers: number[]) {
|
||||
if (DEBUG_CHECK_NUMBERS) {
|
||||
for (let i = 0; i < numbers.length; i++) {
|
||||
if ("number" !== typeof numbers[i]) {
|
||||
throw new Error(
|
||||
`assertNumbers arguments[${i}] is not a number. ${typeof numbers[i]} == typeof ${numbers[i]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const PI = Math.PI;
|
||||
|
||||
/**
|
||||
* https://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
|
||||
* Fixes rX and rY.
|
||||
* Ensures lArcFlag and sweepFlag are 0 or 1
|
||||
* Adds center coordinates: command.cX, command.cY (relative or absolute, depending on command.relative)
|
||||
* Adds start and end arc parameters (in degrees): command.phi1, command.phi2; phi1 < phi2 iff. c.sweepFlag == true
|
||||
*/
|
||||
export function annotateArcCommand(c: CommandA, x1: number, y1: number) {
|
||||
c.lArcFlag = (0 === c.lArcFlag) ? 0 : 1;
|
||||
c.sweepFlag = (0 === c.sweepFlag) ? 0 : 1;
|
||||
// tslint:disable-next-line
|
||||
let {rX, rY, x, y} = c;
|
||||
|
||||
rX = Math.abs(c.rX);
|
||||
rY = Math.abs(c.rY);
|
||||
const [x1_, y1_] = rotate([(x1 - x) / 2, (y1 - y) / 2], -c.xRot / 180 * PI);
|
||||
const testValue = Math.pow(x1_, 2) / Math.pow(rX, 2) + Math.pow(y1_, 2) / Math.pow(rY, 2);
|
||||
|
||||
if (1 < testValue) {
|
||||
rX *= Math.sqrt(testValue);
|
||||
rY *= Math.sqrt(testValue);
|
||||
}
|
||||
c.rX = rX;
|
||||
c.rY = rY;
|
||||
const c_ScaleTemp = (Math.pow(rX, 2) * Math.pow(y1_, 2) + Math.pow(rY, 2) * Math.pow(x1_, 2));
|
||||
const c_Scale = (c.lArcFlag !== c.sweepFlag ? 1 : -1) *
|
||||
Math.sqrt(Math.max(0, (Math.pow(rX, 2) * Math.pow(rY, 2) - c_ScaleTemp) / c_ScaleTemp));
|
||||
const cx_ = rX * y1_ / rY * c_Scale;
|
||||
const cy_ = -rY * x1_ / rX * c_Scale;
|
||||
const cRot = rotate([cx_, cy_], c.xRot / 180 * PI);
|
||||
|
||||
c.cX = cRot[0] + (x1 + x) / 2;
|
||||
c.cY = cRot[1] + (y1 + y) / 2;
|
||||
c.phi1 = Math.atan2((y1_ - cy_) / rY, (x1_ - cx_) / rX);
|
||||
c.phi2 = Math.atan2((-y1_ - cy_) / rY, (-x1_ - cx_) / rX);
|
||||
if (0 === c.sweepFlag && c.phi2 > c.phi1) {
|
||||
c.phi2 -= 2 * PI;
|
||||
}
|
||||
if (1 === c.sweepFlag && c.phi2 < c.phi1) {
|
||||
c.phi2 += 2 * PI;
|
||||
}
|
||||
c.phi1 *= 180 / PI;
|
||||
c.phi2 *= 180 / PI;
|
||||
}
|
||||
|
||||
/**
|
||||
* Solves a quadratic system of equations of the form
|
||||
* a * x + b * y = c
|
||||
* x² + y² = 1
|
||||
* This can be understood as the intersection of the unit circle with a line.
|
||||
* => y = (c - a x) / b
|
||||
* => x² + (c - a x)² / b² = 1
|
||||
* => x² b² + c² - 2 c a x + a² x² = b²
|
||||
* => (a² + b²) x² - 2 a c x + (c² - b²) = 0
|
||||
*/
|
||||
export function intersectionUnitCircleLine(a: number, b: number, c: number): [number, number][] {
|
||||
assertNumbers(a, b, c);
|
||||
// cf. pqFormula
|
||||
const termSqr = a * a + b * b - c * c;
|
||||
|
||||
if (0 > termSqr) {
|
||||
return [];
|
||||
} else if (0 === termSqr) {
|
||||
return [
|
||||
[
|
||||
(a * c) / (a * a + b * b),
|
||||
(b * c) / (a * a + b * b)]];
|
||||
}
|
||||
const term = Math.sqrt(termSqr);
|
||||
|
||||
return [
|
||||
[
|
||||
(a * c + b * term) / (a * a + b * b),
|
||||
(b * c - a * term) / (a * a + b * b)],
|
||||
[
|
||||
(a * c - b * term) / (a * a + b * b),
|
||||
(b * c + a * term) / (a * a + b * b)]];
|
||||
|
||||
}
|
||||
|
||||
export const DEG = Math.PI / 180;
|
||||
|
||||
export function lerp(a: number, b: number, t: number) {
|
||||
return (1 - t) * a + t * b;
|
||||
}
|
||||
|
||||
export function arcAt(c: number, x1: number, x2: number, phiDeg: number) {
|
||||
return c + Math.cos(phiDeg / 180 * PI) * x1 + Math.sin(phiDeg / 180 * PI) * x2;
|
||||
}
|
||||
|
||||
export function bezierRoot(x0: number, x1: number, x2: number, x3: number) {
|
||||
const EPS = 1e-6;
|
||||
const x01 = x1 - x0;
|
||||
const x12 = x2 - x1;
|
||||
const x23 = x3 - x2;
|
||||
const a = 3 * x01 + 3 * x23 - 6 * x12;
|
||||
const b = (x12 - x01) * 6;
|
||||
const c = 3 * x01;
|
||||
// solve a * t² + b * t + c = 0
|
||||
|
||||
if (Math.abs(a) < EPS) {
|
||||
// equivalent to b * t + c =>
|
||||
return [-c / b];
|
||||
}
|
||||
return pqFormula(b / a, c / a, EPS);
|
||||
|
||||
}
|
||||
|
||||
export function bezierAt(x0: number, x1: number, x2: number, x3: number, t: number) {
|
||||
// console.log(x0, y0, x1, y1, x2, y2, x3, y3, t)
|
||||
const s = 1 - t;
|
||||
const c0 = s * s * s;
|
||||
const c1 = 3 * s * s * t;
|
||||
const c2 = 3 * s * t * t;
|
||||
const c3 = t * t * t;
|
||||
|
||||
return x0 * c0 + x1 * c1 + x2 * c2 + x3 * c3;
|
||||
}
|
||||
|
||||
function pqFormula(p: number, q: number, PRECISION = 1e-6) {
|
||||
// 4 times the discriminant:in
|
||||
const discriminantX4 = p * p / 4 - q;
|
||||
|
||||
if (discriminantX4 < -PRECISION) {
|
||||
return [];
|
||||
} else if (discriminantX4 <= PRECISION) {
|
||||
return [-p / 2];
|
||||
}
|
||||
const root = Math.sqrt(discriminantX4);
|
||||
|
||||
return [-(p / 2) - root, -(p / 2) + root];
|
||||
|
||||
}
|
||||
|
||||
export function a2c(arc: CommandA, x0: number, y0: number): CommandC[] {
|
||||
if (!arc.cX) {
|
||||
annotateArcCommand(arc, x0, y0);
|
||||
}
|
||||
|
||||
const phiMin = Math.min(arc.phi1!, arc.phi2!), phiMax = Math.max(arc.phi1!, arc.phi2!), deltaPhi = phiMax - phiMin;
|
||||
const partCount = Math.ceil(deltaPhi / 90 );
|
||||
|
||||
const result: CommandC[] = new Array(partCount);
|
||||
let prevX = x0, prevY = y0;
|
||||
for (let i = 0; i < partCount; i++) {
|
||||
const phiStart = lerp(arc.phi1!, arc.phi2!, i / partCount);
|
||||
const phiEnd = lerp(arc.phi1!, arc.phi2!, (i + 1) / partCount);
|
||||
const deltaPhi = phiEnd - phiStart;
|
||||
const f = 4 / 3 * Math.tan(deltaPhi * DEG / 4);
|
||||
// x1/y1, x2/y2 and x/y coordinates on the unit circle for phiStart/phiEnd
|
||||
const [x1, y1] = [
|
||||
Math.cos(phiStart * DEG) - f * Math.sin(phiStart * DEG),
|
||||
Math.sin(phiStart * DEG) + f * Math.cos(phiStart * DEG)];
|
||||
const [x, y] = [Math.cos(phiEnd * DEG), Math.sin(phiEnd * DEG)];
|
||||
const [x2, y2] = [x + f * Math.sin(phiEnd * DEG), y - f * Math.cos(phiEnd * DEG)];
|
||||
result[i] = {relative: arc.relative, type: SVGPathData.CURVE_TO } as any;
|
||||
const transform = (x: number, y: number) => {
|
||||
const [xTemp, yTemp] = rotate([x * arc.rX, y * arc.rY], arc.xRot);
|
||||
return [arc.cX! + xTemp, arc.cY! + yTemp];
|
||||
};
|
||||
[result[i].x1, result[i].y1] = transform(x1, y1);
|
||||
[result[i].x2, result[i].y2] = transform(x2, y2);
|
||||
[result[i].x, result[i].y] = transform(x, y);
|
||||
if (arc.relative) {
|
||||
result[i].x1 -= prevX;
|
||||
result[i].y1 -= prevY;
|
||||
result[i].x2 -= prevX;
|
||||
result[i].y2 -= prevY;
|
||||
result[i].x -= prevX;
|
||||
result[i].y -= prevY;
|
||||
}
|
||||
[prevX, prevY] = [result[i].x, result[i].y];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
41
node_modules/svg-pathdata/src/types.ts
generated
vendored
Normal file
41
node_modules/svg-pathdata/src/types.ts
generated
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
import { SVGPathData } from "./SVGPathData";
|
||||
|
||||
export type CommandM = { relative: boolean, type: typeof SVGPathData.MOVE_TO, x: number, y: number };
|
||||
export type CommandL = { relative: boolean, type: typeof SVGPathData.LINE_TO, x: number, y: number };
|
||||
export type CommandH = { relative: boolean, type: typeof SVGPathData.HORIZ_LINE_TO, x: number };
|
||||
export type CommandV = { relative: boolean, type: typeof SVGPathData.VERT_LINE_TO, y: number };
|
||||
export type CommandZ = { type: typeof SVGPathData.CLOSE_PATH };
|
||||
export type CommandQ = {
|
||||
relative: boolean;
|
||||
type: typeof SVGPathData.QUAD_TO;
|
||||
x1: number;
|
||||
y1: number;
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
export type CommandT = { relative: boolean, type: typeof SVGPathData.SMOOTH_QUAD_TO, x: number, y: number };
|
||||
export type CommandC = {
|
||||
relative: boolean,
|
||||
type: typeof SVGPathData.CURVE_TO,
|
||||
x1: number, y1: number,
|
||||
x2: number, y2: number,
|
||||
x: number, y: number };
|
||||
export type CommandS = {
|
||||
relative: boolean;
|
||||
type: typeof SVGPathData.SMOOTH_CURVE_TO;
|
||||
x2: number;
|
||||
y2: number;
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
export type CommandA = {
|
||||
relative: boolean,
|
||||
type: typeof SVGPathData.ARC,
|
||||
rX: number, rY: number,
|
||||
xRot: number, sweepFlag: 0 | 1, lArcFlag: 0 | 1,
|
||||
x: number, y: number
|
||||
cX?: number, cY?: number, phi1?: number, phi2?: number};
|
||||
export type SVGCommand = CommandM | CommandL | CommandH | CommandV | CommandZ | CommandQ |
|
||||
CommandT | CommandC | CommandS | CommandA;
|
||||
|
||||
export type TransformFunction = (input: SVGCommand) => SVGCommand | SVGCommand[];
|
||||
Reference in New Issue
Block a user