ntk 2.1.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -110
- package/lib/app.js +48 -15
- package/lib/cleanup.js +14 -0
- package/lib/drawable.js +10 -16
- package/lib/events_map.js +81 -87
- package/lib/fontconfig.js +122 -14
- package/lib/glyphset.js +36 -18
- package/lib/image.js +138 -0
- package/lib/index.js +110 -52
- package/lib/path.js +734 -0
- package/lib/picture.js +76 -38
- package/lib/pixmap.js +34 -25
- package/lib/rasterize.js +178 -0
- package/lib/renderingcontext_2d.js +994 -517
- package/lib/renderingcontext_opengl.js +38 -31
- package/lib/renderingcontext_x11.js +56 -64
- package/lib/text/font.js +196 -0
- package/lib/text/fontmanager.js +233 -0
- package/lib/text/fontsource.js +179 -0
- package/lib/text/glyphs.js +456 -0
- package/lib/text/layout.js +356 -0
- package/lib/text/shape.js +141 -0
- package/lib/trapezoid.js +131 -0
- package/lib/widgets/css.js +676 -0
- package/lib/widgets/highlight.js +97 -0
- package/lib/widgets/htmlview.js +879 -0
- package/lib/widgets/markdown.js +148 -0
- package/lib/widgets/markdownview.js +487 -0
- package/lib/widgets/mermaid.js +720 -0
- package/lib/widgets/svgview.js +528 -0
- package/lib/widgets/tex.js +681 -0
- package/lib/window.js +568 -246
- package/package.json +35 -32
- package/.npmignore +0 -15
- package/examples/example-await.js +0 -23
- package/examples/example-co.js +0 -13
- package/examples/example.js +0 -14
- package/examples/fill-styles.js +0 -39
- package/examples/glclock.js +0 -100
- package/examples/glxpixmap.js +0 -12
- package/examples/gradient.js +0 -41
- package/examples/package.json +0 -17
- package/examples/simpletex.js +0 -72
- package/examples/teapot.js +0 -104
- package/examples/teapot.json +0 -6
- package/examples/text-rendering.js +0 -44
- package/lib/fontface.js +0 -109
- package/lib/widgets/webview.js +0 -33
package/lib/path.js
ADDED
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
// Vector path support shared by the 2d context and the SVG/TeX widgets:
|
|
2
|
+
//
|
|
3
|
+
// - affine matrix helpers ([a, b, c, d, e, f], canvas convention:
|
|
4
|
+
// x' = a*x + c*y + e, y' = b*x + d*y + f)
|
|
5
|
+
// - an SVG path-data parser (full grammar incl. elliptical arcs, which are
|
|
6
|
+
// converted to cubics) producing normalized M/L/C/Q/Z commands
|
|
7
|
+
// - Path2D: the HTML canvas retained-path class
|
|
8
|
+
// - flattenPath(): commands -> polylines, with adaptive (error-bounded)
|
|
9
|
+
// bezier subdivision in device space
|
|
10
|
+
|
|
11
|
+
const TAU = Math.PI * 2;
|
|
12
|
+
|
|
13
|
+
// --------------------------------------------------------------------------
|
|
14
|
+
// matrices
|
|
15
|
+
|
|
16
|
+
export const MAT_IDENTITY = Object.freeze([1, 0, 0, 1, 0, 0]);
|
|
17
|
+
|
|
18
|
+
/** m · n (n applied first, then m — canvas ctx.transform() order) */
|
|
19
|
+
export function matMultiply(m, n) {
|
|
20
|
+
return [
|
|
21
|
+
m[0] * n[0] + m[2] * n[1],
|
|
22
|
+
m[1] * n[0] + m[3] * n[1],
|
|
23
|
+
m[0] * n[2] + m[2] * n[3],
|
|
24
|
+
m[1] * n[2] + m[3] * n[3],
|
|
25
|
+
m[0] * n[4] + m[2] * n[5] + m[4],
|
|
26
|
+
m[1] * n[4] + m[3] * n[5] + m[5]
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function matApply(m, x, y) {
|
|
31
|
+
return [m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5]];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function matInvert(m) {
|
|
35
|
+
const det = m[0] * m[3] - m[1] * m[2];
|
|
36
|
+
if (!det || !Number.isFinite(det)) return null;
|
|
37
|
+
const a = m[3] / det;
|
|
38
|
+
const b = -m[1] / det;
|
|
39
|
+
const c = -m[2] / det;
|
|
40
|
+
const d = m[0] / det;
|
|
41
|
+
return [a, b, c, d, -(a * m[4] + c * m[5]), -(b * m[4] + d * m[5])];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function matIsIdentity(m) {
|
|
45
|
+
return m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1 && m[4] === 0 && m[5] === 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// --------------------------------------------------------------------------
|
|
49
|
+
// elliptical arcs -> cubics
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Cubic bezier segments approximating an elliptical arc: center (cx, cy),
|
|
53
|
+
* radii (rx, ry), rotated by phi, from angle a0 sweeping by da (signed,
|
|
54
|
+
* |da| <= 2π). Returns { start: {x, y}, cmds: [{type:'C', ...}, ...] };
|
|
55
|
+
* segments are split to <= 90° so the approximation error stays tiny.
|
|
56
|
+
*/
|
|
57
|
+
export function ellipseCubics(cx, cy, rx, ry, phi, a0, da) {
|
|
58
|
+
const cosPhi = Math.cos(phi);
|
|
59
|
+
const sinPhi = Math.sin(phi);
|
|
60
|
+
const point = (a) => {
|
|
61
|
+
const x = rx * Math.cos(a);
|
|
62
|
+
const y = ry * Math.sin(a);
|
|
63
|
+
return [cx + cosPhi * x - sinPhi * y, cy + sinPhi * x + cosPhi * y];
|
|
64
|
+
};
|
|
65
|
+
const derivative = (a) => {
|
|
66
|
+
const x = -rx * Math.sin(a);
|
|
67
|
+
const y = ry * Math.cos(a);
|
|
68
|
+
return [cosPhi * x - sinPhi * y, sinPhi * x + cosPhi * y];
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const [sx, sy] = point(a0);
|
|
72
|
+
const cmds = [];
|
|
73
|
+
const n = Math.max(1, Math.ceil(Math.abs(da) / (Math.PI / 2)));
|
|
74
|
+
let a = a0;
|
|
75
|
+
const step = da / n;
|
|
76
|
+
for (let i = 0; i < n; i++) {
|
|
77
|
+
const b = a + step;
|
|
78
|
+
const k = (4 / 3) * Math.tan((b - a) / 4);
|
|
79
|
+
const [x0, y0] = point(a);
|
|
80
|
+
const [x3, y3] = point(b);
|
|
81
|
+
const [dx0, dy0] = derivative(a);
|
|
82
|
+
const [dx3, dy3] = derivative(b);
|
|
83
|
+
cmds.push({
|
|
84
|
+
type: 'C',
|
|
85
|
+
x1: x0 + k * dx0,
|
|
86
|
+
y1: y0 + k * dy0,
|
|
87
|
+
x2: x3 - k * dx3,
|
|
88
|
+
y2: y3 - k * dy3,
|
|
89
|
+
x: x3,
|
|
90
|
+
y: y3
|
|
91
|
+
});
|
|
92
|
+
a = b;
|
|
93
|
+
}
|
|
94
|
+
return { start: { x: sx, y: sy }, cmds };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Canvas arc/ellipse semantics (start/end angle + counterclockwise flag,
|
|
99
|
+
* with the spec's sweep normalization) -> { start, cmds } cubics.
|
|
100
|
+
*/
|
|
101
|
+
export function ellipseSegments(x, y, rx, ry, rotation, a0, a1, counterclockwise = false) {
|
|
102
|
+
let da = a1 - a0;
|
|
103
|
+
if (!counterclockwise) {
|
|
104
|
+
if (da >= TAU) da = TAU;
|
|
105
|
+
else {
|
|
106
|
+
da %= TAU;
|
|
107
|
+
if (da < 0) da += TAU;
|
|
108
|
+
}
|
|
109
|
+
} else if (-da >= TAU) {
|
|
110
|
+
da = -TAU;
|
|
111
|
+
} else {
|
|
112
|
+
da %= TAU;
|
|
113
|
+
if (da > 0) da -= TAU;
|
|
114
|
+
}
|
|
115
|
+
return ellipseCubics(x, y, rx, ry, rotation, a0, da);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// SVG endpoint parameterization (spec appendix F.6.5) -> center form -> cubics
|
|
119
|
+
function svgArcCommands(x0, y0, rx, ry, xRotationDeg, largeArc, sweep, x, y) {
|
|
120
|
+
if (x0 === x && y0 === y) return [];
|
|
121
|
+
rx = Math.abs(rx);
|
|
122
|
+
ry = Math.abs(ry);
|
|
123
|
+
if (!rx || !ry) return [{ type: 'L', x, y }];
|
|
124
|
+
|
|
125
|
+
const phi = (xRotationDeg * Math.PI) / 180;
|
|
126
|
+
const cosPhi = Math.cos(phi);
|
|
127
|
+
const sinPhi = Math.sin(phi);
|
|
128
|
+
const dx = (x0 - x) / 2;
|
|
129
|
+
const dy = (y0 - y) / 2;
|
|
130
|
+
const x1p = cosPhi * dx + sinPhi * dy;
|
|
131
|
+
const y1p = -sinPhi * dx + cosPhi * dy;
|
|
132
|
+
|
|
133
|
+
const lambda = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry);
|
|
134
|
+
if (lambda > 1) {
|
|
135
|
+
const s = Math.sqrt(lambda);
|
|
136
|
+
rx *= s;
|
|
137
|
+
ry *= s;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const rx2 = rx * rx;
|
|
141
|
+
const ry2 = ry * ry;
|
|
142
|
+
const num = rx2 * ry2 - rx2 * y1p * y1p - ry2 * x1p * x1p;
|
|
143
|
+
const den = rx2 * y1p * y1p + ry2 * x1p * x1p;
|
|
144
|
+
let coef = Math.sqrt(Math.max(0, num / den));
|
|
145
|
+
if (largeArc === sweep) coef = -coef;
|
|
146
|
+
const cxp = (coef * rx * y1p) / ry;
|
|
147
|
+
const cyp = (-coef * ry * x1p) / rx;
|
|
148
|
+
const cx = cosPhi * cxp - sinPhi * cyp + (x0 + x) / 2;
|
|
149
|
+
const cy = sinPhi * cxp + cosPhi * cyp + (y0 + y) / 2;
|
|
150
|
+
|
|
151
|
+
const angle = (ux, uy, vx, vy) => {
|
|
152
|
+
const sign = ux * vy - uy * vx < 0 ? -1 : 1;
|
|
153
|
+
const d = (ux * vx + uy * vy) / Math.sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy));
|
|
154
|
+
return sign * Math.acos(Math.min(1, Math.max(-1, d)));
|
|
155
|
+
};
|
|
156
|
+
const a0 = angle(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry);
|
|
157
|
+
let da = angle((x1p - cxp) / rx, (y1p - cyp) / ry, (-x1p - cxp) / rx, (-y1p - cyp) / ry);
|
|
158
|
+
if (!sweep && da > 0) da -= TAU;
|
|
159
|
+
if (sweep && da < 0) da += TAU;
|
|
160
|
+
|
|
161
|
+
const { cmds } = ellipseCubics(cx, cy, rx, ry, phi, a0, da);
|
|
162
|
+
if (cmds.length) {
|
|
163
|
+
// land exactly on the endpoint (kill accumulated fp error)
|
|
164
|
+
cmds[cmds.length - 1].x = x;
|
|
165
|
+
cmds[cmds.length - 1].y = y;
|
|
166
|
+
}
|
|
167
|
+
return cmds;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// --------------------------------------------------------------------------
|
|
171
|
+
// SVG path data
|
|
172
|
+
|
|
173
|
+
const CMD_ARGC = { M: 2, L: 2, H: 1, V: 1, C: 6, S: 4, Q: 4, T: 2, A: 7, Z: 0 };
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Parse SVG path data (`d` attribute) into normalized commands:
|
|
177
|
+
* `{type:'M',x,y}`, `{type:'L',x,y}`, `{type:'C',x1,y1,x2,y2,x,y}`,
|
|
178
|
+
* `{type:'Q',x1,y1,x,y}`, `{type:'Z'}`. H/V/S/T/A and all relative forms
|
|
179
|
+
* are resolved; elliptical arcs are converted to cubics.
|
|
180
|
+
*
|
|
181
|
+
* The scanner is position-based (not a global number regex) so compact
|
|
182
|
+
* arc flags like `a1 1 0 011 0` parse correctly.
|
|
183
|
+
*/
|
|
184
|
+
export function parseSvgPath(d) {
|
|
185
|
+
const cmds = [];
|
|
186
|
+
let i = 0;
|
|
187
|
+
const n = d.length;
|
|
188
|
+
|
|
189
|
+
const isWs = (ch) => ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r' || ch === '\f';
|
|
190
|
+
const skipSep = () => {
|
|
191
|
+
while (i < n && (isWs(d[i]) || d[i] === ',')) i++;
|
|
192
|
+
};
|
|
193
|
+
const readNumber = () => {
|
|
194
|
+
skipSep();
|
|
195
|
+
const start = i;
|
|
196
|
+
if (d[i] === '+' || d[i] === '-') i++;
|
|
197
|
+
while (i < n && d[i] >= '0' && d[i] <= '9') i++;
|
|
198
|
+
if (d[i] === '.') {
|
|
199
|
+
i++;
|
|
200
|
+
while (i < n && d[i] >= '0' && d[i] <= '9') i++;
|
|
201
|
+
}
|
|
202
|
+
if (d[i] === 'e' || d[i] === 'E') {
|
|
203
|
+
i++;
|
|
204
|
+
if (d[i] === '+' || d[i] === '-') i++;
|
|
205
|
+
while (i < n && d[i] >= '0' && d[i] <= '9') i++;
|
|
206
|
+
}
|
|
207
|
+
if (i === start) return null;
|
|
208
|
+
const v = parseFloat(d.slice(start, i));
|
|
209
|
+
return Number.isFinite(v) ? v : null;
|
|
210
|
+
};
|
|
211
|
+
const readFlag = () => {
|
|
212
|
+
skipSep();
|
|
213
|
+
if (d[i] === '0' || d[i] === '1') return d[i++] === '1';
|
|
214
|
+
return null;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
let cmd = null;
|
|
218
|
+
let x = 0;
|
|
219
|
+
let y = 0;
|
|
220
|
+
let sx = 0;
|
|
221
|
+
let sy = 0;
|
|
222
|
+
let px = null; // previous cubic/quad control point, for S/T reflection
|
|
223
|
+
let py = null;
|
|
224
|
+
|
|
225
|
+
while (i < n) {
|
|
226
|
+
skipSep();
|
|
227
|
+
if (i >= n) break;
|
|
228
|
+
const ch = d[i];
|
|
229
|
+
if (/[a-zA-Z]/.test(ch)) {
|
|
230
|
+
if (!(ch.toUpperCase() in CMD_ARGC)) break; // invalid command: stop
|
|
231
|
+
cmd = ch;
|
|
232
|
+
i++;
|
|
233
|
+
} else if (cmd === null) {
|
|
234
|
+
break; // leading garbage
|
|
235
|
+
}
|
|
236
|
+
const rel = cmd >= 'a';
|
|
237
|
+
const C = cmd.toUpperCase();
|
|
238
|
+
|
|
239
|
+
if (C === 'Z') {
|
|
240
|
+
cmds.push({ type: 'Z' });
|
|
241
|
+
x = sx;
|
|
242
|
+
y = sy;
|
|
243
|
+
px = py = null;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// one argument group; number-led repeats loop back via the while
|
|
248
|
+
const before = i;
|
|
249
|
+
let curved = false;
|
|
250
|
+
switch (C) {
|
|
251
|
+
case 'M': {
|
|
252
|
+
const ax = readNumber();
|
|
253
|
+
const ay = readNumber();
|
|
254
|
+
if (ax === null || ay === null) return cmds;
|
|
255
|
+
x = rel ? x + ax : ax;
|
|
256
|
+
y = rel ? y + ay : ay;
|
|
257
|
+
sx = x;
|
|
258
|
+
sy = y;
|
|
259
|
+
cmds.push({ type: 'M', x, y });
|
|
260
|
+
cmd = rel ? 'l' : 'L'; // subsequent pairs are linetos
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
case 'L': {
|
|
264
|
+
const ax = readNumber();
|
|
265
|
+
const ay = readNumber();
|
|
266
|
+
if (ax === null || ay === null) return cmds;
|
|
267
|
+
x = rel ? x + ax : ax;
|
|
268
|
+
y = rel ? y + ay : ay;
|
|
269
|
+
cmds.push({ type: 'L', x, y });
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
case 'H': {
|
|
273
|
+
const ax = readNumber();
|
|
274
|
+
if (ax === null) return cmds;
|
|
275
|
+
x = rel ? x + ax : ax;
|
|
276
|
+
cmds.push({ type: 'L', x, y });
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
case 'V': {
|
|
280
|
+
const ay = readNumber();
|
|
281
|
+
if (ay === null) return cmds;
|
|
282
|
+
y = rel ? y + ay : ay;
|
|
283
|
+
cmds.push({ type: 'L', x, y });
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
case 'C':
|
|
287
|
+
case 'S': {
|
|
288
|
+
let x1;
|
|
289
|
+
let y1;
|
|
290
|
+
if (C === 'C') {
|
|
291
|
+
const a = readNumber();
|
|
292
|
+
const b = readNumber();
|
|
293
|
+
if (a === null || b === null) return cmds;
|
|
294
|
+
x1 = rel ? x + a : a;
|
|
295
|
+
y1 = rel ? y + b : b;
|
|
296
|
+
} else {
|
|
297
|
+
x1 = px !== null ? 2 * x - px : x;
|
|
298
|
+
y1 = py !== null ? 2 * y - py : y;
|
|
299
|
+
}
|
|
300
|
+
const c = readNumber();
|
|
301
|
+
const dnum = readNumber();
|
|
302
|
+
const e = readNumber();
|
|
303
|
+
const f = readNumber();
|
|
304
|
+
if (c === null || dnum === null || e === null || f === null) return cmds;
|
|
305
|
+
const x2 = rel ? x + c : c;
|
|
306
|
+
const y2 = rel ? y + dnum : dnum;
|
|
307
|
+
x = rel ? x + e : e;
|
|
308
|
+
y = rel ? y + f : f;
|
|
309
|
+
cmds.push({ type: 'C', x1, y1, x2, y2, x, y });
|
|
310
|
+
px = x2;
|
|
311
|
+
py = y2;
|
|
312
|
+
curved = true;
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
case 'Q':
|
|
316
|
+
case 'T': {
|
|
317
|
+
let x1;
|
|
318
|
+
let y1;
|
|
319
|
+
if (C === 'Q') {
|
|
320
|
+
const a = readNumber();
|
|
321
|
+
const b = readNumber();
|
|
322
|
+
if (a === null || b === null) return cmds;
|
|
323
|
+
x1 = rel ? x + a : a;
|
|
324
|
+
y1 = rel ? y + b : b;
|
|
325
|
+
} else {
|
|
326
|
+
x1 = px !== null ? 2 * x - px : x;
|
|
327
|
+
y1 = py !== null ? 2 * y - py : y;
|
|
328
|
+
}
|
|
329
|
+
const e = readNumber();
|
|
330
|
+
const f = readNumber();
|
|
331
|
+
if (e === null || f === null) return cmds;
|
|
332
|
+
x = rel ? x + e : e;
|
|
333
|
+
y = rel ? y + f : f;
|
|
334
|
+
cmds.push({ type: 'Q', x1, y1, x, y });
|
|
335
|
+
px = x1;
|
|
336
|
+
py = y1;
|
|
337
|
+
curved = true;
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
case 'A': {
|
|
341
|
+
const rx = readNumber();
|
|
342
|
+
const ry = readNumber();
|
|
343
|
+
const rot = readNumber();
|
|
344
|
+
const large = readFlag();
|
|
345
|
+
const sweep = readFlag();
|
|
346
|
+
const e = readNumber();
|
|
347
|
+
const f = readNumber();
|
|
348
|
+
if (rx === null || ry === null || rot === null || large === null || sweep === null || e === null || f === null) {
|
|
349
|
+
return cmds;
|
|
350
|
+
}
|
|
351
|
+
const nx = rel ? x + e : e;
|
|
352
|
+
const ny = rel ? y + f : f;
|
|
353
|
+
for (const c of svgArcCommands(x, y, rx, ry, rot, large, sweep, nx, ny)) cmds.push(c);
|
|
354
|
+
x = nx;
|
|
355
|
+
y = ny;
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (!curved) px = py = null;
|
|
360
|
+
if (i === before) break; // no progress: malformed input
|
|
361
|
+
}
|
|
362
|
+
return cmds;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// --------------------------------------------------------------------------
|
|
366
|
+
// Path2D
|
|
367
|
+
|
|
368
|
+
function normalizeRadii(w, h, radii) {
|
|
369
|
+
if (radii === undefined) radii = 0;
|
|
370
|
+
if (typeof radii === 'number') radii = [radii];
|
|
371
|
+
const take = (v) => (typeof v === 'number' ? { x: v, y: v } : { x: v.x ?? 0, y: v.y ?? 0 });
|
|
372
|
+
let tl;
|
|
373
|
+
let tr;
|
|
374
|
+
let br;
|
|
375
|
+
let bl;
|
|
376
|
+
if (radii.length === 1) tl = tr = br = bl = take(radii[0]);
|
|
377
|
+
else if (radii.length === 2) {
|
|
378
|
+
tl = br = take(radii[0]);
|
|
379
|
+
tr = bl = take(radii[1]);
|
|
380
|
+
} else if (radii.length === 3) {
|
|
381
|
+
tl = take(radii[0]);
|
|
382
|
+
tr = bl = take(radii[1]);
|
|
383
|
+
br = take(radii[2]);
|
|
384
|
+
} else {
|
|
385
|
+
tl = take(radii[0]);
|
|
386
|
+
tr = take(radii[1]);
|
|
387
|
+
br = take(radii[2]);
|
|
388
|
+
bl = take(radii[3]);
|
|
389
|
+
}
|
|
390
|
+
// scale down so opposite corners never overlap
|
|
391
|
+
const s = Math.min(
|
|
392
|
+
1,
|
|
393
|
+
w / (tl.x + tr.x) || 1,
|
|
394
|
+
h / (tr.y + br.y) || 1,
|
|
395
|
+
w / (br.x + bl.x) || 1,
|
|
396
|
+
h / (bl.y + tl.y) || 1
|
|
397
|
+
);
|
|
398
|
+
const f = (r) => ({ x: Math.max(0, r.x * s), y: Math.max(0, r.y * s) });
|
|
399
|
+
return [f(tl), f(tr), f(br), f(bl)];
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Retained path, HTML canvas compatible. Coordinates are in the space the
|
|
404
|
+
* path is built in; the 2d context applies its current transform when the
|
|
405
|
+
* path is filled/stroked/clipped (per the canvas spec).
|
|
406
|
+
*
|
|
407
|
+
* new Path2D() empty
|
|
408
|
+
* new Path2D(otherPath) copy
|
|
409
|
+
* new Path2D('M0 0L10 0...') from SVG path data
|
|
410
|
+
*/
|
|
411
|
+
export class Path2D {
|
|
412
|
+
constructor(init) {
|
|
413
|
+
/** normalized commands (M/L/C/Q/Z); treat as read-only */
|
|
414
|
+
this._cmds = [];
|
|
415
|
+
this._x = null; // current point
|
|
416
|
+
this._y = null;
|
|
417
|
+
this._sx = null; // subpath start
|
|
418
|
+
this._sy = null;
|
|
419
|
+
if (init instanceof Path2D) {
|
|
420
|
+
this._cmds = init._cmds.map((c) => ({ ...c }));
|
|
421
|
+
this._x = init._x;
|
|
422
|
+
this._y = init._y;
|
|
423
|
+
this._sx = init._sx;
|
|
424
|
+
this._sy = init._sy;
|
|
425
|
+
} else if (typeof init === 'string') {
|
|
426
|
+
this._append(parseSvgPath(init));
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
_track(c) {
|
|
431
|
+
if (c.type === 'M') {
|
|
432
|
+
this._sx = c.x;
|
|
433
|
+
this._sy = c.y;
|
|
434
|
+
this._x = c.x;
|
|
435
|
+
this._y = c.y;
|
|
436
|
+
} else if (c.type === 'Z') {
|
|
437
|
+
this._x = this._sx;
|
|
438
|
+
this._y = this._sy;
|
|
439
|
+
} else {
|
|
440
|
+
this._x = c.x;
|
|
441
|
+
this._y = c.y;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
_append(cmds) {
|
|
446
|
+
for (const c of cmds) {
|
|
447
|
+
this._cmds.push(c);
|
|
448
|
+
this._track(c);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// append segment commands, connecting to the current subpath: a leading
|
|
453
|
+
// implicit start point becomes lineTo when a current point exists
|
|
454
|
+
_appendSegments(start, cmds) {
|
|
455
|
+
if (this._x === null) this.moveTo(start.x, start.y);
|
|
456
|
+
else if (this._x !== start.x || this._y !== start.y) this.lineTo(start.x, start.y);
|
|
457
|
+
this._append(cmds);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
moveTo(x, y) {
|
|
461
|
+
this._append([{ type: 'M', x, y }]);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
lineTo(x, y) {
|
|
465
|
+
if (this._x === null) return this.moveTo(x, y);
|
|
466
|
+
this._append([{ type: 'L', x, y }]);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
closePath() {
|
|
470
|
+
if (this._cmds.length) this._append([{ type: 'Z' }]);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
bezierCurveTo(x1, y1, x2, y2, x, y) {
|
|
474
|
+
if (this._x === null) this.moveTo(x1, y1);
|
|
475
|
+
this._append([{ type: 'C', x1, y1, x2, y2, x, y }]);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
quadraticCurveTo(x1, y1, x, y) {
|
|
479
|
+
if (this._x === null) this.moveTo(x1, y1);
|
|
480
|
+
this._append([{ type: 'Q', x1, y1, x, y }]);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
arc(x, y, r, a0, a1, counterclockwise = false) {
|
|
484
|
+
this.ellipse(x, y, r, r, 0, a0, a1, counterclockwise);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
ellipse(x, y, rx, ry, rotation, a0, a1, counterclockwise = false) {
|
|
488
|
+
if (rx < 0 || ry < 0) throw new RangeError('ellipse: negative radius');
|
|
489
|
+
const { start, cmds } = ellipseSegments(x, y, rx, ry, rotation, a0, a1, counterclockwise);
|
|
490
|
+
this._appendSegments(start, cmds);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
arcTo(x1, y1, x2, y2, r) {
|
|
494
|
+
if (r < 0) throw new RangeError('arcTo: negative radius');
|
|
495
|
+
if (this._x === null) return this.moveTo(x1, y1);
|
|
496
|
+
const x0 = this._x;
|
|
497
|
+
const y0 = this._y;
|
|
498
|
+
const ax = x0 - x1;
|
|
499
|
+
const ay = y0 - y1;
|
|
500
|
+
const bx = x2 - x1;
|
|
501
|
+
const by = y2 - y1;
|
|
502
|
+
const cross = ax * by - ay * bx;
|
|
503
|
+
const la = Math.hypot(ax, ay);
|
|
504
|
+
const lb = Math.hypot(bx, by);
|
|
505
|
+
if (!r || !la || !lb || Math.abs(cross) < 1e-9) return this.lineTo(x1, y1);
|
|
506
|
+
|
|
507
|
+
const theta = Math.acos(Math.min(1, Math.max(-1, (ax * bx + ay * by) / (la * lb))));
|
|
508
|
+
const dist = r / Math.tan(theta / 2);
|
|
509
|
+
const t1x = x1 + (ax / la) * dist;
|
|
510
|
+
const t1y = y1 + (ay / la) * dist;
|
|
511
|
+
const t2x = x1 + (bx / lb) * dist;
|
|
512
|
+
const t2y = y1 + (by / lb) * dist;
|
|
513
|
+
// center: offset from the tangent point, perpendicular to P0->P1
|
|
514
|
+
const sign = cross < 0 ? -1 : 1;
|
|
515
|
+
const cx = t1x - (ay / la) * r * sign;
|
|
516
|
+
const cy = t1y + (ax / la) * r * sign;
|
|
517
|
+
const startAngle = Math.atan2(t1y - cy, t1x - cx);
|
|
518
|
+
const endAngle = Math.atan2(t2y - cy, t2x - cx);
|
|
519
|
+
this.lineTo(t1x, t1y);
|
|
520
|
+
this.ellipse(cx, cy, r, r, 0, startAngle, endAngle, cross < 0);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
rect(x, y, w, h) {
|
|
524
|
+
this._append([
|
|
525
|
+
{ type: 'M', x, y },
|
|
526
|
+
{ type: 'L', x: x + w, y },
|
|
527
|
+
{ type: 'L', x: x + w, y: y + h },
|
|
528
|
+
{ type: 'L', x, y: y + h },
|
|
529
|
+
{ type: 'Z' },
|
|
530
|
+
{ type: 'M', x, y } // spec: rect() leaves the current point at (x, y)
|
|
531
|
+
]);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
roundRect(x, y, w, h, radii) {
|
|
535
|
+
if (typeof radii === 'number' && radii === 0) return this.rect(x, y, w, h);
|
|
536
|
+
const [tl, tr, br, bl] = normalizeRadii(Math.abs(w), Math.abs(h), radii);
|
|
537
|
+
if (w < 0 || h < 0) {
|
|
538
|
+
// degenerate: fall back to a plain rect on flipped geometry
|
|
539
|
+
return this.rect(x, y, w, h);
|
|
540
|
+
}
|
|
541
|
+
this.moveTo(x + tl.x, y);
|
|
542
|
+
this.lineTo(x + w - tr.x, y);
|
|
543
|
+
if (tr.x || tr.y) this.ellipse(x + w - tr.x, y + tr.y, tr.x, tr.y, 0, -Math.PI / 2, 0);
|
|
544
|
+
this.lineTo(x + w, y + h - br.y);
|
|
545
|
+
if (br.x || br.y) this.ellipse(x + w - br.x, y + h - br.y, br.x, br.y, 0, 0, Math.PI / 2);
|
|
546
|
+
this.lineTo(x + bl.x, y + h);
|
|
547
|
+
if (bl.x || bl.y) this.ellipse(x + bl.x, y + h - bl.y, bl.x, bl.y, 0, Math.PI / 2, Math.PI);
|
|
548
|
+
this.lineTo(x, y + tl.y);
|
|
549
|
+
if (tl.x || tl.y) this.ellipse(x + tl.x, y + tl.y, tl.x, tl.y, 0, Math.PI, Math.PI * 1.5);
|
|
550
|
+
this.closePath();
|
|
551
|
+
this.moveTo(x, y);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** append another path, optionally transformed ([a,b,c,d,e,f] or {a..f}) */
|
|
555
|
+
addPath(path, transform) {
|
|
556
|
+
if (!(path instanceof Path2D)) throw new TypeError('addPath: expected a Path2D');
|
|
557
|
+
const m = transform
|
|
558
|
+
? Array.isArray(transform)
|
|
559
|
+
? transform
|
|
560
|
+
: [transform.a, transform.b, transform.c, transform.d, transform.e, transform.f]
|
|
561
|
+
: null;
|
|
562
|
+
this._append(m ? transformCommands(path._cmds, m) : path._cmds.map((c) => ({ ...c })));
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/** deep-copy commands with an affine transform applied to every point */
|
|
567
|
+
export function transformCommands(cmds, m) {
|
|
568
|
+
const out = [];
|
|
569
|
+
for (const c of cmds) {
|
|
570
|
+
switch (c.type) {
|
|
571
|
+
case 'M':
|
|
572
|
+
case 'L': {
|
|
573
|
+
const [x, y] = matApply(m, c.x, c.y);
|
|
574
|
+
out.push({ type: c.type, x, y });
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
577
|
+
case 'C': {
|
|
578
|
+
const [x1, y1] = matApply(m, c.x1, c.y1);
|
|
579
|
+
const [x2, y2] = matApply(m, c.x2, c.y2);
|
|
580
|
+
const [x, y] = matApply(m, c.x, c.y);
|
|
581
|
+
out.push({ type: 'C', x1, y1, x2, y2, x, y });
|
|
582
|
+
break;
|
|
583
|
+
}
|
|
584
|
+
case 'Q': {
|
|
585
|
+
const [x1, y1] = matApply(m, c.x1, c.y1);
|
|
586
|
+
const [x, y] = matApply(m, c.x, c.y);
|
|
587
|
+
out.push({ type: 'Q', x1, y1, x, y });
|
|
588
|
+
break;
|
|
589
|
+
}
|
|
590
|
+
default:
|
|
591
|
+
out.push({ type: 'Z' });
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return out;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// --------------------------------------------------------------------------
|
|
598
|
+
// flattening
|
|
599
|
+
|
|
600
|
+
function addCubic(pts, x0, y0, x1, y1, x2, y2, x3, y3, tol2, depth) {
|
|
601
|
+
const dx = x3 - x0;
|
|
602
|
+
const dy = y3 - y0;
|
|
603
|
+
const chord2 = dx * dx + dy * dy;
|
|
604
|
+
// cross products = (distance of control point from the chord) * |chord|
|
|
605
|
+
const d1 = (x1 - x0) * dy - (y1 - y0) * dx;
|
|
606
|
+
const d2 = (x2 - x0) * dy - (y2 - y0) * dx;
|
|
607
|
+
const err = (Math.abs(d1) + Math.abs(d2)) ** 2;
|
|
608
|
+
let flat;
|
|
609
|
+
if (chord2 < 1e-9) {
|
|
610
|
+
// degenerate chord: flat only if the control points are also close
|
|
611
|
+
const c1 = (x1 - x0) ** 2 + (y1 - y0) ** 2;
|
|
612
|
+
const c2 = (x2 - x0) ** 2 + (y2 - y0) ** 2;
|
|
613
|
+
flat = Math.max(c1, c2) <= tol2;
|
|
614
|
+
} else {
|
|
615
|
+
// (dist1 + dist2)² <= tol² — absolute flatness in output pixels
|
|
616
|
+
flat = err <= tol2 * chord2;
|
|
617
|
+
}
|
|
618
|
+
if (flat || depth >= 18) {
|
|
619
|
+
pts.push(x3, y3);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
// de Casteljau split at t = 0.5
|
|
623
|
+
const ax = (x0 + x1) / 2;
|
|
624
|
+
const ay = (y0 + y1) / 2;
|
|
625
|
+
const bx = (x1 + x2) / 2;
|
|
626
|
+
const by = (y1 + y2) / 2;
|
|
627
|
+
const cx = (x2 + x3) / 2;
|
|
628
|
+
const cy = (y2 + y3) / 2;
|
|
629
|
+
const abx = (ax + bx) / 2;
|
|
630
|
+
const aby = (ay + by) / 2;
|
|
631
|
+
const bcx = (bx + cx) / 2;
|
|
632
|
+
const bcy = (by + cy) / 2;
|
|
633
|
+
const mx = (abx + bcx) / 2;
|
|
634
|
+
const my = (aby + bcy) / 2;
|
|
635
|
+
addCubic(pts, x0, y0, ax, ay, abx, aby, mx, my, tol2, depth + 1);
|
|
636
|
+
addCubic(pts, mx, my, bcx, bcy, cx, cy, x3, y3, tol2, depth + 1);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Flatten normalized commands into polylines. `m` (optional affine) is
|
|
641
|
+
* applied to control points before subdivision, so the flatness tolerance
|
|
642
|
+
* `tol` (default 0.25) is measured in output/device pixels.
|
|
643
|
+
*
|
|
644
|
+
* @returns {Array<{pts: number[], closed: boolean}>} flat [x0,y0,x1,y1,…]
|
|
645
|
+
*/
|
|
646
|
+
export function flattenPath(cmds, m = null, tol = 0.25) {
|
|
647
|
+
if (m && matIsIdentity(m)) m = null;
|
|
648
|
+
const tol2 = tol * tol;
|
|
649
|
+
const polys = [];
|
|
650
|
+
let pts = null;
|
|
651
|
+
const p = (x, y) => (m ? matApply(m, x, y) : [x, y]);
|
|
652
|
+
|
|
653
|
+
for (const c of cmds) {
|
|
654
|
+
switch (c.type) {
|
|
655
|
+
case 'M': {
|
|
656
|
+
if (pts && pts.pts.length >= 4) polys.push(pts);
|
|
657
|
+
const [x, y] = p(c.x, c.y);
|
|
658
|
+
pts = { pts: [x, y], closed: false };
|
|
659
|
+
break;
|
|
660
|
+
}
|
|
661
|
+
case 'L': {
|
|
662
|
+
if (!pts) break;
|
|
663
|
+
const [x, y] = p(c.x, c.y);
|
|
664
|
+
pts.pts.push(x, y);
|
|
665
|
+
break;
|
|
666
|
+
}
|
|
667
|
+
case 'C': {
|
|
668
|
+
if (!pts) break;
|
|
669
|
+
const x0 = pts.pts[pts.pts.length - 2];
|
|
670
|
+
const y0 = pts.pts[pts.pts.length - 1];
|
|
671
|
+
const [x1, y1] = p(c.x1, c.y1);
|
|
672
|
+
const [x2, y2] = p(c.x2, c.y2);
|
|
673
|
+
const [x, y] = p(c.x, c.y);
|
|
674
|
+
addCubic(pts.pts, x0, y0, x1, y1, x2, y2, x, y, tol2, 0);
|
|
675
|
+
break;
|
|
676
|
+
}
|
|
677
|
+
case 'Q': {
|
|
678
|
+
if (!pts) break;
|
|
679
|
+
const x0 = pts.pts[pts.pts.length - 2];
|
|
680
|
+
const y0 = pts.pts[pts.pts.length - 1];
|
|
681
|
+
const [qx, qy] = p(c.x1, c.y1);
|
|
682
|
+
const [x, y] = p(c.x, c.y);
|
|
683
|
+
// degree elevation: quadratic -> cubic
|
|
684
|
+
addCubic(
|
|
685
|
+
pts.pts,
|
|
686
|
+
x0, y0,
|
|
687
|
+
x0 + (2 / 3) * (qx - x0), y0 + (2 / 3) * (qy - y0),
|
|
688
|
+
x + (2 / 3) * (qx - x), y + (2 / 3) * (qy - y),
|
|
689
|
+
x, y,
|
|
690
|
+
tol2, 0
|
|
691
|
+
);
|
|
692
|
+
break;
|
|
693
|
+
}
|
|
694
|
+
case 'Z': {
|
|
695
|
+
if (pts) {
|
|
696
|
+
if (pts.pts.length >= 4) {
|
|
697
|
+
pts.closed = true;
|
|
698
|
+
polys.push(pts);
|
|
699
|
+
}
|
|
700
|
+
// a new subpath continues from the subpath start
|
|
701
|
+
pts = { pts: [pts.pts[0], pts.pts[1]], closed: false };
|
|
702
|
+
}
|
|
703
|
+
break;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
if (pts && pts.pts.length >= 4) polys.push(pts);
|
|
708
|
+
return polys;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/** point-in-path test on flattened polylines (device space) */
|
|
712
|
+
export function polysContain(polys, x, y, rule = 'nonzero') {
|
|
713
|
+
let winding = 0;
|
|
714
|
+
let crossings = 0;
|
|
715
|
+
for (const poly of polys) {
|
|
716
|
+
const pts = poly.pts;
|
|
717
|
+
const n = pts.length / 2;
|
|
718
|
+
for (let i = 0; i < n; i++) {
|
|
719
|
+
const j = (i + 1) % n;
|
|
720
|
+
const y0 = pts[i * 2 + 1];
|
|
721
|
+
const y1 = pts[j * 2 + 1];
|
|
722
|
+
if (y0 === y1) continue;
|
|
723
|
+
if (y < Math.min(y0, y1) || y >= Math.max(y0, y1)) continue;
|
|
724
|
+
const x0 = pts[i * 2];
|
|
725
|
+
const x1 = pts[j * 2];
|
|
726
|
+
const xc = x0 + ((y - y0) * (x1 - x0)) / (y1 - y0);
|
|
727
|
+
if (xc > x) {
|
|
728
|
+
crossings++;
|
|
729
|
+
winding += y1 > y0 ? 1 : -1;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return rule === 'evenodd' ? (crossings & 1) === 1 : winding !== 0;
|
|
734
|
+
}
|