-
Notifications
You must be signed in to change notification settings - Fork 1.6k
/
Copy pathSVGPathRebuilder.ts
137 lines (120 loc) · 3.96 KB
/
SVGPathRebuilder.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import { PathRebuilder } from '../core/PathProxy';
import { isAroundZero } from './helper';
const mathSin = Math.sin;
const mathCos = Math.cos;
const PI = Math.PI;
const PI2 = Math.PI * 2;
const degree = 180 / PI;
export default class SVGPathRebuilder implements PathRebuilder {
private _d: (string | number)[]
private _str: string
private _invalid: boolean
// If is start of subpath
private _start: boolean
private _p: number
reset(precision?: number) {
this._start = true;
this._d = [];
this._str = '';
this._p = Math.pow(10, precision || 4);
}
moveTo(x: number, y: number) {
this._add('M', x, y);
}
lineTo(x: number, y: number) {
this._add('L', x, y);
}
bezierCurveTo(x: number, y: number, x2: number, y2: number, x3: number, y3: number) {
this._add('C', x, y, x2, y2, x3, y3);
}
quadraticCurveTo(x: number, y: number, x2: number, y2: number) {
this._add('Q', x, y, x2, y2);
}
arc(cx: number, cy: number, r: number, startAngle: number, endAngle: number, anticlockwise: boolean) {
this.ellipse(cx, cy, r, r, 0, startAngle, endAngle, anticlockwise);
}
ellipse(
cx: number, cy: number,
rx: number, ry: number,
psi: number,
startAngle: number,
endAngle: number,
anticlockwise: boolean
) {
let dTheta = endAngle - startAngle;
const clockwise = !anticlockwise;
const dThetaPositive = Math.abs(dTheta);
const isCircle = isAroundZero(dThetaPositive - PI2)
|| (clockwise ? dTheta >= PI2 : -dTheta >= PI2);
// Mapping to 0~2PI
const unifiedTheta = dTheta > 0 ? dTheta % PI2 : (dTheta % PI2 + PI2);
let large = false;
if (isCircle) {
large = true;
}
else if (isAroundZero(dThetaPositive)) {
large = false;
}
else {
large = (unifiedTheta >= PI) === !!clockwise;
}
const x0 = cx + rx * mathCos(startAngle);
const y0 = cy + ry * mathSin(startAngle);
if (this._start && !isCircle) {
// Move to (x0, y0) only when CMD.A comes at the
// first position of a shape.
// For instance, when drawing a ring, CMD.A comes
// after CMD.M, so it's unnecessary to move to
// (x0, y0).
this._add('M', x0, y0);
}
const xRot = Math.round(psi * degree);
if (isCircle) {
this._add('M', cx - rx, cy);
this._add('a', rx, ry, xRot, 1, +clockwise, rx * 2, 0);
this._add('a', rx, ry, xRot, 1, +clockwise, -rx * 2, 0);
}
else {
const x = cx + rx * mathCos(endAngle);
const y = cy + ry * mathSin(endAngle);
// FIXME Ellipse
this._add('A', rx, ry, xRot, +large, +clockwise, x, y);
}
}
rect(x: number, y: number, w: number, h: number) {
this._add('M', x, y);
// Use relative coordinates to reduce the size.
this._add('l', w, 0);
this._add('l', 0, h);
this._add('l', -w, 0);
// this._add('L', x, y);
this._add('Z');
}
closePath() {
// Not use Z as first command
if (this._d.length > 0) {
this._add('Z');
}
}
_add(cmd: string, a?: number, b?: number, c?: number, d?: number, e?: number, f?: number, g?: number, h?: number) {
const vals = [];
const p = this._p;
for (let i = 1; i < arguments.length; i++) {
const val = arguments[i];
if (isNaN(val)) {
this._invalid = true;
return;
}
vals.push(Math.round(val * p) / p);
}
this._d.push(cmd + vals.join(' '));
this._start = cmd === 'Z';
}
generateStr() {
this._str = this._invalid ? '' : this._d.join('');
this._d = [];
}
getStr() {
return this._str;
}
}