+21
@@ -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
@@ -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
@@ -0,0 +1,2 @@
|
||||
import { Segment } from './parser.js';
|
||||
export declare function absolutize(segments: Segment[]): Segment[];
|
||||
+110
@@ -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
@@ -0,0 +1,3 @@
|
||||
export { parsePath, serialize } from './parser.js';
|
||||
export { absolutize } from './absolutize.js';
|
||||
export { normalize } from './normalize.js';
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export { parsePath, serialize } from './parser.js';
|
||||
export { absolutize } from './absolutize.js';
|
||||
export { normalize } from './normalize.js';
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
import { Segment } from './parser.js';
|
||||
export declare function normalize(segments: Segment[]): Segment[];
|
||||
+219
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,3 @@
|
||||
export { parsePath, serialize } from './parser.js';
|
||||
export { absolutize } from './absolutize.js';
|
||||
export { normalize } from './normalize.js';
|
||||
+242
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user