First commit.

Signed-off-by: Chen Xiao <abigwc@gmail.com>
This commit is contained in:
Chen Xiao
2026-05-08 14:43:16 +08:00
commit 0b64e2de94
10989 changed files with 2253791 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Preet Shihn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+72
View File
@@ -0,0 +1,72 @@
# path-data-parser
I know there are a bunch of SVG path parsers out there, but here's one that I have been using for a few years now. It's small, tree-shakable and provides four simple functions that I have needed in several graphics projects.
## Install
From npm
```
npm install --save path-data-parser
```
The code is shipped as ES6 modules.
## Methods
The module exposes 4 methods
### pasrePath(d: string): Segment[]
This is the main method that parses the SVG path data. You pass in the path string and it returns an array of `Segments`. A segment has a `key` which is the commands, e.g. `M` or `h` or `C`; and `data` which is an array of numbers used by the command
```javascript
Segment {
key: string;
data: number[];
}
```
example:
```javascript
import { parsePath } from 'path-data-parser';
const segments = parsePath('M10 10 h 80 v 80 h -80 C Z');
```
### serialize(segments: Segment[]): string
This is essentially the opposite of the `parsePath` command. It outputs a path string from an array of `Segment` objects.
```javascript
import { parsePath, serialize, absolutize } from 'path-data-parser';
const segments = parsePath('M10 10 h 80 v 80 h -80 Z');
const absoluteSegments = absolutize(segments); // Turns relative commands to absolute
const outputPath = serialize(absoluteSegments);
console.log(outputPath); // M 10 10 H 90 V 90 H 10 Z
```
### absolutize(segments: Segment[]): Segment[]
Translates relative commands to absolute commands. i.e. all commands that use relative positions (lower-case ones), turns into absolute position commands (upper-case ones).
See the `serialize` example above.
### normalize(segments: Segment[]): Segment[]
Normalize takes a list of _absolute segments_ and outputs a list of segments with only four commands: `M, L, C, Z`. So every segment is described as move, line, or a bezier curve (cubic).
This is useful when translating SVG paths to non SVG mediums - Canvas, or some other graphics platform. Most such platforms will support lines and bezier curves. It also simplifies the cases to consider when modifying these segments.
```javascript
import { parsePath, serialize, absolutize, normalize } from 'path-data-parser';
const segments = parsePath(' M 10 80 Q 52.5 10, 95 80 T 180 80');
const absoluteSegments = absolutize(segments);
const normalizedSegments = normalize(absoluteSegments);
// M 10 80 C 38.33 33.33, 66.67 33.33, 95 80 C 123.33 126.67, 151.67 126.67, 180 80
```
## License
[MIT License](https://github.com/pshihn/path-data-parser/blob/master/LICENSE)
+2
View File
@@ -0,0 +1,2 @@
import { Segment } from './parser.js';
export declare function absolutize(segments: Segment[]): Segment[];
+110
View File
@@ -0,0 +1,110 @@
// Translate relative commands to absolute commands
export function absolutize(segments) {
let cx = 0, cy = 0;
let subx = 0, suby = 0;
const out = [];
for (const { key, data } of segments) {
switch (key) {
case 'M':
out.push({ key: 'M', data: [...data] });
[cx, cy] = data;
[subx, suby] = data;
break;
case 'm':
cx += data[0];
cy += data[1];
out.push({ key: 'M', data: [cx, cy] });
subx = cx;
suby = cy;
break;
case 'L':
out.push({ key: 'L', data: [...data] });
[cx, cy] = data;
break;
case 'l':
cx += data[0];
cy += data[1];
out.push({ key: 'L', data: [cx, cy] });
break;
case 'C':
out.push({ key: 'C', data: [...data] });
cx = data[4];
cy = data[5];
break;
case 'c': {
const newdata = data.map((d, i) => (i % 2) ? (d + cy) : (d + cx));
out.push({ key: 'C', data: newdata });
cx = newdata[4];
cy = newdata[5];
break;
}
case 'Q':
out.push({ key: 'Q', data: [...data] });
cx = data[2];
cy = data[3];
break;
case 'q': {
const newdata = data.map((d, i) => (i % 2) ? (d + cy) : (d + cx));
out.push({ key: 'Q', data: newdata });
cx = newdata[2];
cy = newdata[3];
break;
}
case 'A':
out.push({ key: 'A', data: [...data] });
cx = data[5];
cy = data[6];
break;
case 'a':
cx += data[5];
cy += data[6];
out.push({ key: 'A', data: [data[0], data[1], data[2], data[3], data[4], cx, cy] });
break;
case 'H':
out.push({ key: 'H', data: [...data] });
cx = data[0];
break;
case 'h':
cx += data[0];
out.push({ key: 'H', data: [cx] });
break;
case 'V':
out.push({ key: 'V', data: [...data] });
cy = data[0];
break;
case 'v':
cy += data[0];
out.push({ key: 'V', data: [cy] });
break;
case 'S':
out.push({ key: 'S', data: [...data] });
cx = data[2];
cy = data[3];
break;
case 's': {
const newdata = data.map((d, i) => (i % 2) ? (d + cy) : (d + cx));
out.push({ key: 'S', data: newdata });
cx = newdata[2];
cy = newdata[3];
break;
}
case 'T':
out.push({ key: 'T', data: [...data] });
cx = data[0];
cy = data[1];
break;
case 't':
cx += data[0];
cy += data[1];
out.push({ key: 'T', data: [cx, cy] });
break;
case 'Z':
case 'z':
out.push({ key: 'Z', data: [] });
cx = subx;
cy = suby;
break;
}
}
return out;
}
+3
View File
@@ -0,0 +1,3 @@
export { parsePath, serialize } from './parser.js';
export { absolutize } from './absolutize.js';
export { normalize } from './normalize.js';
+3
View File
@@ -0,0 +1,3 @@
export { parsePath, serialize } from './parser.js';
export { absolutize } from './absolutize.js';
export { normalize } from './normalize.js';
+2
View File
@@ -0,0 +1,2 @@
import { Segment } from './parser.js';
export declare function normalize(segments: Segment[]): Segment[];
+219
View File
@@ -0,0 +1,219 @@
// Normalize path to include only M, L, C, and Z commands
export function normalize(segments) {
const out = [];
let lastType = '';
let cx = 0, cy = 0;
let subx = 0, suby = 0;
let lcx = 0, lcy = 0;
for (const { key, data } of segments) {
switch (key) {
case 'M':
out.push({ key: 'M', data: [...data] });
[cx, cy] = data;
[subx, suby] = data;
break;
case 'C':
out.push({ key: 'C', data: [...data] });
cx = data[4];
cy = data[5];
lcx = data[2];
lcy = data[3];
break;
case 'L':
out.push({ key: 'L', data: [...data] });
[cx, cy] = data;
break;
case 'H':
cx = data[0];
out.push({ key: 'L', data: [cx, cy] });
break;
case 'V':
cy = data[0];
out.push({ key: 'L', data: [cx, cy] });
break;
case 'S': {
let cx1 = 0, cy1 = 0;
if (lastType === 'C' || lastType === 'S') {
cx1 = cx + (cx - lcx);
cy1 = cy + (cy - lcy);
}
else {
cx1 = cx;
cy1 = cy;
}
out.push({ key: 'C', data: [cx1, cy1, ...data] });
lcx = data[0];
lcy = data[1];
cx = data[2];
cy = data[3];
break;
}
case 'T': {
const [x, y] = data;
let x1 = 0, y1 = 0;
if (lastType === 'Q' || lastType === 'T') {
x1 = cx + (cx - lcx);
y1 = cy + (cy - lcy);
}
else {
x1 = cx;
y1 = cy;
}
const cx1 = cx + 2 * (x1 - cx) / 3;
const cy1 = cy + 2 * (y1 - cy) / 3;
const cx2 = x + 2 * (x1 - x) / 3;
const cy2 = y + 2 * (y1 - y) / 3;
out.push({ key: 'C', data: [cx1, cy1, cx2, cy2, x, y] });
lcx = x1;
lcy = y1;
cx = x;
cy = y;
break;
}
case 'Q': {
const [x1, y1, x, y] = data;
const cx1 = cx + 2 * (x1 - cx) / 3;
const cy1 = cy + 2 * (y1 - cy) / 3;
const cx2 = x + 2 * (x1 - x) / 3;
const cy2 = y + 2 * (y1 - y) / 3;
out.push({ key: 'C', data: [cx1, cy1, cx2, cy2, x, y] });
lcx = x1;
lcy = y1;
cx = x;
cy = y;
break;
}
case 'A': {
const r1 = Math.abs(data[0]);
const r2 = Math.abs(data[1]);
const angle = data[2];
const largeArcFlag = data[3];
const sweepFlag = data[4];
const x = data[5];
const y = data[6];
if (r1 === 0 || r2 === 0) {
out.push({ key: 'C', data: [cx, cy, x, y, x, y] });
cx = x;
cy = y;
}
else {
if (cx !== x || cy !== y) {
const curves = arcToCubicCurves(cx, cy, x, y, r1, r2, angle, largeArcFlag, sweepFlag);
curves.forEach(function (curve) {
out.push({ key: 'C', data: curve });
});
cx = x;
cy = y;
}
}
break;
}
case 'Z':
out.push({ key: 'Z', data: [] });
cx = subx;
cy = suby;
break;
}
lastType = key;
}
return out;
}
function degToRad(degrees) {
return (Math.PI * degrees) / 180;
}
function rotate(x, y, angleRad) {
const X = x * Math.cos(angleRad) - y * Math.sin(angleRad);
const Y = x * Math.sin(angleRad) + y * Math.cos(angleRad);
return [X, Y];
}
function arcToCubicCurves(x1, y1, x2, y2, r1, r2, angle, largeArcFlag, sweepFlag, recursive) {
const angleRad = degToRad(angle);
let params = [];
let f1 = 0, f2 = 0, cx = 0, cy = 0;
if (recursive) {
[f1, f2, cx, cy] = recursive;
}
else {
[x1, y1] = rotate(x1, y1, -angleRad);
[x2, y2] = rotate(x2, y2, -angleRad);
const x = (x1 - x2) / 2;
const y = (y1 - y2) / 2;
let h = (x * x) / (r1 * r1) + (y * y) / (r2 * r2);
if (h > 1) {
h = Math.sqrt(h);
r1 = h * r1;
r2 = h * r2;
}
const sign = (largeArcFlag === sweepFlag) ? -1 : 1;
const r1Pow = r1 * r1;
const r2Pow = r2 * r2;
const left = r1Pow * r2Pow - r1Pow * y * y - r2Pow * x * x;
const right = r1Pow * y * y + r2Pow * x * x;
const k = sign * Math.sqrt(Math.abs(left / right));
cx = k * r1 * y / r2 + (x1 + x2) / 2;
cy = k * -r2 * x / r1 + (y1 + y2) / 2;
f1 = Math.asin(parseFloat(((y1 - cy) / r2).toFixed(9)));
f2 = Math.asin(parseFloat(((y2 - cy) / r2).toFixed(9)));
if (x1 < cx) {
f1 = Math.PI - f1;
}
if (x2 < cx) {
f2 = Math.PI - f2;
}
if (f1 < 0) {
f1 = Math.PI * 2 + f1;
}
if (f2 < 0) {
f2 = Math.PI * 2 + f2;
}
if (sweepFlag && f1 > f2) {
f1 = f1 - Math.PI * 2;
}
if (!sweepFlag && f2 > f1) {
f2 = f2 - Math.PI * 2;
}
}
let df = f2 - f1;
if (Math.abs(df) > (Math.PI * 120 / 180)) {
const f2old = f2;
const x2old = x2;
const y2old = y2;
if (sweepFlag && f2 > f1) {
f2 = f1 + (Math.PI * 120 / 180) * (1);
}
else {
f2 = f1 + (Math.PI * 120 / 180) * (-1);
}
x2 = cx + r1 * Math.cos(f2);
y2 = cy + r2 * Math.sin(f2);
params = arcToCubicCurves(x2, y2, x2old, y2old, r1, r2, angle, 0, sweepFlag, [f2, f2old, cx, cy]);
}
df = f2 - f1;
const c1 = Math.cos(f1);
const s1 = Math.sin(f1);
const c2 = Math.cos(f2);
const s2 = Math.sin(f2);
const t = Math.tan(df / 4);
const hx = 4 / 3 * r1 * t;
const hy = 4 / 3 * r2 * t;
const m1 = [x1, y1];
const m2 = [x1 + hx * s1, y1 - hy * c1];
const m3 = [x2 + hx * s2, y2 - hy * c2];
const m4 = [x2, y2];
m2[0] = 2 * m1[0] - m2[0];
m2[1] = 2 * m1[1] - m2[1];
if (recursive) {
return [m2, m3, m4].concat(params);
}
else {
params = [m2, m3, m4].concat(params);
const curves = [];
for (let i = 0; i < params.length; i += 3) {
const r1 = rotate(params[i][0], params[i][1], angleRad);
const r2 = rotate(params[i + 1][0], params[i + 1][1], angleRad);
const r3 = rotate(params[i + 2][0], params[i + 2][1], angleRad);
curves.push([r1[0], r1[1], r2[0], r2[1], r3[0], r3[1]]);
}
return curves;
}
}
+6
View File
@@ -0,0 +1,6 @@
export interface Segment {
key: string;
data: number[];
}
export declare function parsePath(d: string): Segment[];
export declare function serialize(segments: Segment[]): string;
+107
View File
@@ -0,0 +1,107 @@
const COMMAND = 0;
const NUMBER = 1;
const EOD = 2;
const PARAMS = { A: 7, a: 7, C: 6, c: 6, H: 1, h: 1, L: 2, l: 2, M: 2, m: 2, Q: 4, q: 4, S: 4, s: 4, T: 2, t: 2, V: 1, v: 1, Z: 0, z: 0 };
function tokenize(d) {
const tokens = new Array();
while (d !== '') {
if (d.match(/^([ \t\r\n,]+)/)) {
d = d.substr(RegExp.$1.length);
}
else if (d.match(/^([aAcChHlLmMqQsStTvVzZ])/)) {
tokens[tokens.length] = { type: COMMAND, text: RegExp.$1 };
d = d.substr(RegExp.$1.length);
}
else if (d.match(/^(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)/)) {
tokens[tokens.length] = { type: NUMBER, text: `${parseFloat(RegExp.$1)}` };
d = d.substr(RegExp.$1.length);
}
else {
return [];
}
}
tokens[tokens.length] = { type: EOD, text: '' };
return tokens;
}
function isType(token, type) {
return token.type === type;
}
export function parsePath(d) {
const segments = [];
const tokens = tokenize(d);
let mode = 'BOD';
let index = 0;
let token = tokens[index];
while (!isType(token, EOD)) {
let paramsCount = 0;
const params = [];
if (mode === 'BOD') {
if (token.text === 'M' || token.text === 'm') {
index++;
paramsCount = PARAMS[token.text];
mode = token.text;
}
else {
return parsePath('M0,0' + d);
}
}
else if (isType(token, NUMBER)) {
paramsCount = PARAMS[mode];
}
else {
index++;
paramsCount = PARAMS[token.text];
mode = token.text;
}
if ((index + paramsCount) < tokens.length) {
for (let i = index; i < index + paramsCount; i++) {
const numbeToken = tokens[i];
if (isType(numbeToken, NUMBER)) {
params[params.length] = +numbeToken.text;
}
else {
throw new Error('Param not a number: ' + mode + ',' + numbeToken.text);
}
}
if (typeof PARAMS[mode] === 'number') {
const segment = { key: mode, data: params };
segments.push(segment);
index += paramsCount;
token = tokens[index];
if (mode === 'M')
mode = 'L';
if (mode === 'm')
mode = 'l';
}
else {
throw new Error('Bad segment: ' + mode);
}
}
else {
throw new Error('Path data ended short');
}
}
return segments;
}
export function serialize(segments) {
const tokens = [];
for (const { key, data } of segments) {
tokens.push(key);
switch (key) {
case 'C':
case 'c':
tokens.push(data[0], `${data[1]},`, data[2], `${data[3]},`, data[4], data[5]);
break;
case 'S':
case 's':
case 'Q':
case 'q':
tokens.push(data[0], `${data[1]},`, data[2], data[3]);
break;
default:
tokens.push(...data);
break;
}
}
return tokens.join(' ');
}
+35
View File
@@ -0,0 +1,35 @@
{
"name": "path-data-parser",
"version": "0.1.0",
"description": "Yet another SVG path parser. This one's tiny",
"main": "lib/index.js",
"module": "lib/index.js",
"types": "lib/index.d.ts",
"scripts": {
"build": "rm -rf lib && tsc",
"lint": "tslint -p tsconfig.json",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/pshihn/path-data-parser.git"
},
"keywords": [
"svg path parser",
"SVG",
"path parser",
"SVGPATH",
"pathdata",
"svg parser"
],
"author": "Preet Shihn",
"license": "MIT",
"bugs": {
"url": "https://github.com/pshihn/path-data-parser/issues"
},
"homepage": "https://github.com/pshihn/path-data-parser#readme",
"devDependencies": {
"tslint": "^6.1.1",
"typescript": "^3.8.3"
}
}
+112
View File
@@ -0,0 +1,112 @@
import { Segment } from './parser.js';
// Translate relative commands to absolute commands
export function absolutize(segments: Segment[]): Segment[] {
let cx = 0, cy = 0;
let subx = 0, suby = 0;
const out: Segment[] = [];
for (const { key, data } of segments) {
switch (key) {
case 'M':
out.push({ key: 'M', data: [...data] });
[cx, cy] = data;
[subx, suby] = data;
break;
case 'm':
cx += data[0];
cy += data[1];
out.push({ key: 'M', data: [cx, cy] });
subx = cx;
suby = cy;
break;
case 'L':
out.push({ key: 'L', data: [...data] });
[cx, cy] = data;
break;
case 'l':
cx += data[0];
cy += data[1];
out.push({ key: 'L', data: [cx, cy] });
break;
case 'C':
out.push({ key: 'C', data: [...data] });
cx = data[4];
cy = data[5];
break;
case 'c': {
const newdata = data.map((d, i) => (i % 2) ? (d + cy) : (d + cx));
out.push({ key: 'C', data: newdata });
cx = newdata[4];
cy = newdata[5];
break;
}
case 'Q':
out.push({ key: 'Q', data: [...data] });
cx = data[2];
cy = data[3];
break;
case 'q': {
const newdata = data.map((d, i) => (i % 2) ? (d + cy) : (d + cx));
out.push({ key: 'Q', data: newdata });
cx = newdata[2];
cy = newdata[3];
break;
}
case 'A':
out.push({ key: 'A', data: [...data] });
cx = data[5];
cy = data[6];
break;
case 'a':
cx += data[5];
cy += data[6];
out.push({ key: 'A', data: [data[0], data[1], data[2], data[3], data[4], cx, cy] });
break;
case 'H':
out.push({ key: 'H', data: [...data] });
cx = data[0];
break;
case 'h':
cx += data[0];
out.push({ key: 'H', data: [cx] });
break;
case 'V':
out.push({ key: 'V', data: [...data] });
cy = data[0];
break;
case 'v':
cy += data[0];
out.push({ key: 'V', data: [cy] });
break;
case 'S':
out.push({ key: 'S', data: [...data] });
cx = data[2];
cy = data[3];
break;
case 's': {
const newdata = data.map((d, i) => (i % 2) ? (d + cy) : (d + cx));
out.push({ key: 'S', data: newdata });
cx = newdata[2];
cy = newdata[3];
break;
}
case 'T':
out.push({ key: 'T', data: [...data] });
cx = data[0];
cy = data[1];
break;
case 't':
cx += data[0];
cy += data[1];
out.push({ key: 'T', data: [cx, cy] });
break;
case 'Z':
case 'z':
out.push({ key: 'Z', data: [] });
cx = subx;
cy = suby;
break;
}
}
return out;
}
+3
View File
@@ -0,0 +1,3 @@
export { parsePath, serialize } from './parser.js';
export { absolutize } from './absolutize.js';
export { normalize } from './normalize.js';
+242
View File
@@ -0,0 +1,242 @@
import { Segment } from './parser.js';
// Normalize path to include only M, L, C, and Z commands
export function normalize(segments: Segment[]): Segment[] {
const out: Segment[] = [];
let lastType = '';
let cx = 0, cy = 0;
let subx = 0, suby = 0;
let lcx = 0, lcy = 0;
for (const { key, data } of segments) {
switch (key) {
case 'M':
out.push({ key: 'M', data: [...data] });
[cx, cy] = data;
[subx, suby] = data;
break;
case 'C':
out.push({ key: 'C', data: [...data] });
cx = data[4];
cy = data[5];
lcx = data[2];
lcy = data[3];
break;
case 'L':
out.push({ key: 'L', data: [...data] });
[cx, cy] = data;
break;
case 'H':
cx = data[0];
out.push({ key: 'L', data: [cx, cy] });
break;
case 'V':
cy = data[0];
out.push({ key: 'L', data: [cx, cy] });
break;
case 'S': {
let cx1 = 0, cy1 = 0;
if (lastType === 'C' || lastType === 'S') {
cx1 = cx + (cx - lcx);
cy1 = cy + (cy - lcy);
} else {
cx1 = cx;
cy1 = cy;
}
out.push({ key: 'C', data: [cx1, cy1, ...data] });
lcx = data[0];
lcy = data[1];
cx = data[2];
cy = data[3];
break;
}
case 'T': {
const [x, y] = data;
let x1 = 0, y1 = 0;
if (lastType === 'Q' || lastType === 'T') {
x1 = cx + (cx - lcx);
y1 = cy + (cy - lcy);
} else {
x1 = cx;
y1 = cy;
}
const cx1 = cx + 2 * (x1 - cx) / 3;
const cy1 = cy + 2 * (y1 - cy) / 3;
const cx2 = x + 2 * (x1 - x) / 3;
const cy2 = y + 2 * (y1 - y) / 3;
out.push({ key: 'C', data: [cx1, cy1, cx2, cy2, x, y] });
lcx = x1;
lcy = y1;
cx = x;
cy = y;
break;
}
case 'Q': {
const [x1, y1, x, y] = data;
const cx1 = cx + 2 * (x1 - cx) / 3;
const cy1 = cy + 2 * (y1 - cy) / 3;
const cx2 = x + 2 * (x1 - x) / 3;
const cy2 = y + 2 * (y1 - y) / 3;
out.push({ key: 'C', data: [cx1, cy1, cx2, cy2, x, y] });
lcx = x1;
lcy = y1;
cx = x;
cy = y;
break;
}
case 'A': {
const r1 = Math.abs(data[0]);
const r2 = Math.abs(data[1]);
const angle = data[2];
const largeArcFlag = data[3];
const sweepFlag = data[4];
const x = data[5];
const y = data[6];
if (r1 === 0 || r2 === 0) {
out.push({ key: 'C', data: [cx, cy, x, y, x, y] });
cx = x;
cy = y;
} else {
if (cx !== x || cy !== y) {
const curves: number[][] = arcToCubicCurves(cx, cy, x, y, r1, r2, angle, largeArcFlag, sweepFlag);
curves.forEach(function (curve) {
out.push({ key: 'C', data: curve });
});
cx = x;
cy = y;
}
}
break;
}
case 'Z':
out.push({ key: 'Z', data: [] });
cx = subx;
cy = suby;
break;
}
lastType = key;
}
return out;
}
function degToRad(degrees: number): number {
return (Math.PI * degrees) / 180;
}
function rotate(x: number, y: number, angleRad: number): [number, number] {
const X = x * Math.cos(angleRad) - y * Math.sin(angleRad);
const Y = x * Math.sin(angleRad) + y * Math.cos(angleRad);
return [X, Y];
}
function arcToCubicCurves(x1: number, y1: number, x2: number, y2: number, r1: number, r2: number, angle: number, largeArcFlag: number, sweepFlag: number, recursive?: number[]): number[][] {
const angleRad = degToRad(angle);
let params: number[][] = [];
let f1 = 0, f2 = 0, cx = 0, cy = 0;
if (recursive) {
[f1, f2, cx, cy] = recursive;
} else {
[x1, y1] = rotate(x1, y1, -angleRad);
[x2, y2] = rotate(x2, y2, -angleRad);
const x = (x1 - x2) / 2;
const y = (y1 - y2) / 2;
let h = (x * x) / (r1 * r1) + (y * y) / (r2 * r2);
if (h > 1) {
h = Math.sqrt(h);
r1 = h * r1;
r2 = h * r2;
}
const sign = (largeArcFlag === sweepFlag) ? -1 : 1;
const r1Pow = r1 * r1;
const r2Pow = r2 * r2;
const left = r1Pow * r2Pow - r1Pow * y * y - r2Pow * x * x;
const right = r1Pow * y * y + r2Pow * x * x;
const k = sign * Math.sqrt(Math.abs(left / right));
cx = k * r1 * y / r2 + (x1 + x2) / 2;
cy = k * -r2 * x / r1 + (y1 + y2) / 2;
f1 = Math.asin(parseFloat(((y1 - cy) / r2).toFixed(9)));
f2 = Math.asin(parseFloat(((y2 - cy) / r2).toFixed(9)));
if (x1 < cx) {
f1 = Math.PI - f1;
}
if (x2 < cx) {
f2 = Math.PI - f2;
}
if (f1 < 0) {
f1 = Math.PI * 2 + f1;
}
if (f2 < 0) {
f2 = Math.PI * 2 + f2;
}
if (sweepFlag && f1 > f2) {
f1 = f1 - Math.PI * 2;
}
if (!sweepFlag && f2 > f1) {
f2 = f2 - Math.PI * 2;
}
}
let df = f2 - f1;
if (Math.abs(df) > (Math.PI * 120 / 180)) {
const f2old = f2;
const x2old = x2;
const y2old = y2;
if (sweepFlag && f2 > f1) {
f2 = f1 + (Math.PI * 120 / 180) * (1);
}
else {
f2 = f1 + (Math.PI * 120 / 180) * (-1);
}
x2 = cx + r1 * Math.cos(f2);
y2 = cy + r2 * Math.sin(f2);
params = arcToCubicCurves(x2, y2, x2old, y2old, r1, r2, angle, 0, sweepFlag, [f2, f2old, cx, cy]);
}
df = f2 - f1;
const c1 = Math.cos(f1);
const s1 = Math.sin(f1);
const c2 = Math.cos(f2);
const s2 = Math.sin(f2);
const t = Math.tan(df / 4);
const hx = 4 / 3 * r1 * t;
const hy = 4 / 3 * r2 * t;
const m1 = [x1, y1];
const m2 = [x1 + hx * s1, y1 - hy * c1];
const m3 = [x2 + hx * s2, y2 - hy * c2];
const m4 = [x2, y2];
m2[0] = 2 * m1[0] - m2[0];
m2[1] = 2 * m1[1] - m2[1];
if (recursive) {
return [m2, m3, m4].concat(params);
}
else {
params = [m2, m3, m4].concat(params);
const curves = [];
for (let i = 0; i < params.length; i += 3) {
const r1 = rotate(params[i][0], params[i][1], angleRad);
const r2 = rotate(params[i + 1][0], params[i + 1][1], angleRad);
const r3 = rotate(params[i + 2][0], params[i + 2][1], angleRad);
curves.push([r1[0], r1[1], r2[0], r2[1], r3[0], r3[1]]);
}
return curves;
}
}
+113
View File
@@ -0,0 +1,113 @@
const COMMAND = 0;
const NUMBER = 1;
const EOD = 2;
type TokenType = 0 | 1 | 2;
interface PathToken {
type: TokenType;
text: string;
}
export interface Segment {
key: string;
data: number[];
}
const PARAMS: { [key: string]: number } = { A: 7, a: 7, C: 6, c: 6, H: 1, h: 1, L: 2, l: 2, M: 2, m: 2, Q: 4, q: 4, S: 4, s: 4, T: 2, t: 2, V: 1, v: 1, Z: 0, z: 0 };
function tokenize(d: string): PathToken[] {
const tokens: PathToken[] = new Array();
while (d !== '') {
if (d.match(/^([ \t\r\n,]+)/)) {
d = d.substr(RegExp.$1.length);
} else if (d.match(/^([aAcChHlLmMqQsStTvVzZ])/)) {
tokens[tokens.length] = { type: COMMAND, text: RegExp.$1 };
d = d.substr(RegExp.$1.length);
} else if (d.match(/^(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)/)) {
tokens[tokens.length] = { type: NUMBER, text: `${parseFloat(RegExp.$1)}` };
d = d.substr(RegExp.$1.length);
} else {
return [];
}
}
tokens[tokens.length] = { type: EOD, text: '' };
return tokens;
}
function isType(token: PathToken, type: number) {
return token.type === type;
}
export function parsePath(d: string): Segment[] {
const segments: Segment[] = [];
const tokens = tokenize(d);
let mode = 'BOD';
let index = 0;
let token = tokens[index];
while (!isType(token, EOD)) {
let paramsCount = 0;
const params: number[] = [];
if (mode === 'BOD') {
if (token.text === 'M' || token.text === 'm') {
index++;
paramsCount = PARAMS[token.text];
mode = token.text;
} else {
return parsePath('M0,0' + d);
}
} else if (isType(token, NUMBER)) {
paramsCount = PARAMS[mode];
} else {
index++;
paramsCount = PARAMS[token.text];
mode = token.text;
}
if ((index + paramsCount) < tokens.length) {
for (let i = index; i < index + paramsCount; i++) {
const numbeToken = tokens[i];
if (isType(numbeToken, NUMBER)) {
params[params.length] = +numbeToken.text;
}
else {
throw new Error('Param not a number: ' + mode + ',' + numbeToken.text);
}
}
if (typeof PARAMS[mode] === 'number') {
const segment: Segment = { key: mode, data: params };
segments.push(segment);
index += paramsCount;
token = tokens[index];
if (mode === 'M') mode = 'L';
if (mode === 'm') mode = 'l';
} else {
throw new Error('Bad segment: ' + mode);
}
} else {
throw new Error('Path data ended short');
}
}
return segments;
}
export function serialize(segments: Segment[]): string {
const tokens: (string | number)[] = [];
for (const { key, data } of segments) {
tokens.push(key);
switch (key) {
case 'C':
case 'c':
tokens.push(data[0], `${data[1]},`, data[2], `${data[3]},`, data[4], data[5]);
break;
case 'S':
case 's':
case 'Q':
case 'q':
tokens.push(data[0], `${data[1]},`, data[2], data[3]);
break;
default:
tokens.push(...data);
break;
}
}
return tokens.join(' ');
}
+23
View File
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "es2017",
"module": "es2015",
"moduleResolution": "node",
"lib": [
"es2017"
],
"declaration": true,
"outDir": "./lib",
"baseUrl": ".",
"strict": true,
"strictNullChecks": true,
"noImplicitAny": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"include": [
"src/**/*.ts"
]
}
+61
View File
@@ -0,0 +1,61 @@
{
"rules": {
"arrow-parens": true,
"class-name": true,
"indent": [
true,
"spaces",
2
],
"prefer-const": true,
"no-duplicate-variable": true,
"no-eval": true,
"no-internal-module": true,
"no-trailing-whitespace": false,
"no-var-keyword": true,
"one-line": [
true,
"check-open-brace",
"check-whitespace"
],
"quotemark": [
true,
"single",
"avoid-escape"
],
"semicolon": [
true,
"always"
],
"trailing-comma": [
true,
"multiline"
],
"triple-equals": [
true,
"allow-null-check"
],
"typedef-whitespace": [
true,
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
}
],
"variable-name": [
true,
"ban-keywords"
],
"whitespace": [
true,
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type"
]
}
}