@altpsyche/maths 0.2.0 → 0.3.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 +8 -0
- package/dist/figure/path-data.d.ts +29 -0
- package/dist/figure/path-data.js +283 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/paint/svg.d.ts +14 -9
- package/dist/paint/svg.js +5 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,6 +42,14 @@ marks onto a two-dimensional canvas, which is what a recording needs, because an
|
|
|
42
42
|
one surface. A test holds the two to emitting the same geometry and the same style for every
|
|
43
43
|
mark.
|
|
44
44
|
|
|
45
|
+
## The way in
|
|
46
|
+
|
|
47
|
+
`pathFromData` reads an SVG `d` attribute as a path, which is the inverse of what the SVG painter
|
|
48
|
+
writes. Without it the only shapes that exist are the ones the builders here make, so a glyph from
|
|
49
|
+
a typesetter or an outline from a drawing program could not be trimmed, aligned or walked into
|
|
50
|
+
another shape, and those are the operations this package is for. Every command is read, elliptical
|
|
51
|
+
arcs included, and a command it does not know stops the read rather than being skipped.
|
|
52
|
+
|
|
45
53
|
## The line through the package
|
|
46
54
|
|
|
47
55
|
**Values and timing** are below it: vectors, a transform, the four curves a change can travel
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An SVG path string read as geometry, which is the way in to everything above.
|
|
3
|
+
*
|
|
4
|
+
* `pathData` writes a path out and this reads one back. Without it the only
|
|
5
|
+
* paths that exist are the ones the builders beside this file make, so a glyph
|
|
6
|
+
* from a typesetter, an icon from a designer or anything else a drawing program
|
|
7
|
+
* exported cannot be trimmed, aligned or walked into another shape, and those
|
|
8
|
+
* are the operations this package is for.
|
|
9
|
+
*
|
|
10
|
+
* Everything becomes a cubic, the way it does everywhere here. A straight run
|
|
11
|
+
* takes the controls a third and two thirds along, a quadratic elevates exactly,
|
|
12
|
+
* and an elliptical arc is cut into pieces of at most a quarter turn each. So no
|
|
13
|
+
* segment read here is an approximation of the one the string described, apart
|
|
14
|
+
* from the arc, which no Bézier can be exactly.
|
|
15
|
+
*
|
|
16
|
+
* A command it does not know stops the read rather than being skipped. Skipping
|
|
17
|
+
* leaves a shape with a piece missing, and a piece missing from a letter or an
|
|
18
|
+
* outline reads as a mistake in the drawing rather than in the reading of it.
|
|
19
|
+
*/
|
|
20
|
+
import { type Path } from './path.js';
|
|
21
|
+
/**
|
|
22
|
+
* The path a `d` attribute describes.
|
|
23
|
+
*
|
|
24
|
+
* Both cases of every command are read, so a relative run is resolved against
|
|
25
|
+
* where the last one ended. A command letter followed by more numbers than it
|
|
26
|
+
* takes repeats, which is the shorthand the grammar allows and which a moveto
|
|
27
|
+
* repeats as a lineto.
|
|
28
|
+
*/
|
|
29
|
+
export declare function pathFromData(d: string): Path;
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An SVG path string read as geometry, which is the way in to everything above.
|
|
3
|
+
*
|
|
4
|
+
* `pathData` writes a path out and this reads one back. Without it the only
|
|
5
|
+
* paths that exist are the ones the builders beside this file make, so a glyph
|
|
6
|
+
* from a typesetter, an icon from a designer or anything else a drawing program
|
|
7
|
+
* exported cannot be trimmed, aligned or walked into another shape, and those
|
|
8
|
+
* are the operations this package is for.
|
|
9
|
+
*
|
|
10
|
+
* Everything becomes a cubic, the way it does everywhere here. A straight run
|
|
11
|
+
* takes the controls a third and two thirds along, a quadratic elevates exactly,
|
|
12
|
+
* and an elliptical arc is cut into pieces of at most a quarter turn each. So no
|
|
13
|
+
* segment read here is an approximation of the one the string described, apart
|
|
14
|
+
* from the arc, which no Bézier can be exactly.
|
|
15
|
+
*
|
|
16
|
+
* A command it does not know stops the read rather than being skipped. Skipping
|
|
17
|
+
* leaves a shape with a piece missing, and a piece missing from a letter or an
|
|
18
|
+
* outline reads as a mistake in the drawing rather than in the reading of it.
|
|
19
|
+
*/
|
|
20
|
+
import { vec2 } from '../values/vec2.js';
|
|
21
|
+
import { mat3 } from '../values/mat3.js';
|
|
22
|
+
import { straight } from './path.js';
|
|
23
|
+
/** Every command in the path grammar. */
|
|
24
|
+
const DRAWN = 'MLHVCSQTAZ';
|
|
25
|
+
/** A letter, a number, a run of separators, or one character that is none of
|
|
26
|
+
* those. The last group is what turns an unexpected character into a refusal
|
|
27
|
+
* rather than leaving it in the string unread. */
|
|
28
|
+
const SCANNER = /([A-Za-z])|([+-]?(?:\d*\.\d+|\d+\.?)(?:[eE][+-]?\d+)?)|([,\s]+)|([\s\S])/g;
|
|
29
|
+
function tokenize(d) {
|
|
30
|
+
const tokens = [];
|
|
31
|
+
SCANNER.lastIndex = 0;
|
|
32
|
+
for (let match = SCANNER.exec(d); match; match = SCANNER.exec(d)) {
|
|
33
|
+
if (match[1])
|
|
34
|
+
tokens.push({ command: match[1] });
|
|
35
|
+
else if (match[2] !== undefined)
|
|
36
|
+
tokens.push({ number: Number(match[2]) });
|
|
37
|
+
else if (match[3] === undefined)
|
|
38
|
+
throw new Error(`path data holds "${match[4]}", which is neither a command nor a number`);
|
|
39
|
+
}
|
|
40
|
+
return tokens;
|
|
41
|
+
}
|
|
42
|
+
/** A quadratic written as a cubic, both controls two thirds of the way from an
|
|
43
|
+
* end towards the single control. The two describe the same curve. */
|
|
44
|
+
const elevate = (from, control, to) => ({
|
|
45
|
+
control1: vec2.lerp(from, control, 2 / 3),
|
|
46
|
+
control2: vec2.lerp(to, control, 2 / 3),
|
|
47
|
+
to,
|
|
48
|
+
});
|
|
49
|
+
/** The control a smooth segment uses, which is the previous one turned through
|
|
50
|
+
* the point the two segments share. */
|
|
51
|
+
const reflect = (control, about) => vec2.sub(vec2.scale(about, 2), control);
|
|
52
|
+
/** How far a cubic's controls sit from the ends of a piece of a unit arc, along
|
|
53
|
+
* the tangent. It is the same derivation `arc` uses and it holds for any sweep
|
|
54
|
+
* short enough, which is why the sweep is cut into quarters first. */
|
|
55
|
+
const reachFor = (sweep) => (4 / 3) * Math.tan(sweep / 4);
|
|
56
|
+
/**
|
|
57
|
+
* An SVG elliptical arc as cubics, from the endpoint form the string carries.
|
|
58
|
+
*
|
|
59
|
+
* SVG gives an arc as where it ends plus two radii, a rotation and two flags,
|
|
60
|
+
* and every one of the four arcs that fit those endpoints is selected by the
|
|
61
|
+
* flags. The centre and the two angles are recovered first, which is the
|
|
62
|
+
* conversion in the SVG specification, and then the sweep is walked in pieces
|
|
63
|
+
* of at most a quarter turn on a unit circle. The ellipse is an affine map of
|
|
64
|
+
* that circle, and an affine map takes a cubic to a cubic, so the control
|
|
65
|
+
* points can be mapped straight through rather than derived again.
|
|
66
|
+
*/
|
|
67
|
+
function arcCurves(from, radii, rotation, largeArc, sweep, to) {
|
|
68
|
+
// A radius of zero is a straight line by the specification, and so is an arc
|
|
69
|
+
// whose endpoints are the same point, which the specification drops entirely.
|
|
70
|
+
if (radii.x === 0 || radii.y === 0)
|
|
71
|
+
return [straight(from, to)];
|
|
72
|
+
const angle = (rotation * Math.PI) / 180;
|
|
73
|
+
const cos = Math.cos(angle);
|
|
74
|
+
const sin = Math.sin(angle);
|
|
75
|
+
const half = vec2.scale(vec2.sub(from, to), 0.5);
|
|
76
|
+
const turned = vec2(cos * half.x + sin * half.y, -sin * half.x + cos * half.y);
|
|
77
|
+
// Radii too small to reach both endpoints are scaled up until they just do,
|
|
78
|
+
// which the specification asks for rather than treating as an error.
|
|
79
|
+
let rx = Math.abs(radii.x);
|
|
80
|
+
let ry = Math.abs(radii.y);
|
|
81
|
+
const short = (turned.x * turned.x) / (rx * rx) + (turned.y * turned.y) / (ry * ry);
|
|
82
|
+
if (short > 1) {
|
|
83
|
+
rx *= Math.sqrt(short);
|
|
84
|
+
ry *= Math.sqrt(short);
|
|
85
|
+
}
|
|
86
|
+
const denominator = rx * rx * turned.y * turned.y + ry * ry * turned.x * turned.x;
|
|
87
|
+
const numerator = Math.max(0, rx * rx * ry * ry - denominator);
|
|
88
|
+
const spread = (largeArc === sweep ? -1 : 1) * Math.sqrt(numerator / denominator);
|
|
89
|
+
const centreTurned = vec2((spread * rx * turned.y) / ry, (-spread * ry * turned.x) / rx);
|
|
90
|
+
const centre = vec2.add(vec2(cos * centreTurned.x - sin * centreTurned.y, sin * centreTurned.x + cos * centreTurned.y), vec2.scale(vec2.add(from, to), 0.5));
|
|
91
|
+
const at = (point) => vec2((point.x - centreTurned.x) / rx, (point.y - centreTurned.y) / ry);
|
|
92
|
+
const opening = at(turned);
|
|
93
|
+
const closing = at(vec2.scale(turned, -1));
|
|
94
|
+
const first = vec2.angle(opening);
|
|
95
|
+
let swept = vec2.angle(closing) - first;
|
|
96
|
+
if (!sweep && swept > 0)
|
|
97
|
+
swept -= 2 * Math.PI;
|
|
98
|
+
if (sweep && swept < 0)
|
|
99
|
+
swept += 2 * Math.PI;
|
|
100
|
+
// The whole ellipse, placed and turned, as one matrix. Every point of the
|
|
101
|
+
// unit arc goes through it, controls included.
|
|
102
|
+
const place = mat3.multiply(mat3.translation(centre), mat3.multiply(mat3.rotation(angle), mat3.scaling(vec2(rx, ry))));
|
|
103
|
+
const pieces = Math.max(1, Math.ceil(Math.abs(swept) / (Math.PI / 2)));
|
|
104
|
+
const step = swept / pieces;
|
|
105
|
+
const reach = reachFor(step);
|
|
106
|
+
const curves = [];
|
|
107
|
+
for (let piece = 0; piece < pieces; piece++) {
|
|
108
|
+
const a0 = first + step * piece;
|
|
109
|
+
const a1 = a0 + step;
|
|
110
|
+
const p0 = vec2(Math.cos(a0), Math.sin(a0));
|
|
111
|
+
const p1 = vec2(Math.cos(a1), Math.sin(a1));
|
|
112
|
+
const t0 = vec2(-Math.sin(a0), Math.cos(a0));
|
|
113
|
+
const t1 = vec2(-Math.sin(a1), Math.cos(a1));
|
|
114
|
+
curves.push({
|
|
115
|
+
control1: mat3.transformPoint(place, vec2.add(p0, vec2.scale(t0, reach))),
|
|
116
|
+
control2: mat3.transformPoint(place, vec2.sub(p1, vec2.scale(t1, reach))),
|
|
117
|
+
// The endpoint the string named rather than the one the angles give back,
|
|
118
|
+
// so a run of arcs cannot drift away from where it said it ends.
|
|
119
|
+
to: piece === pieces - 1 ? to : mat3.transformPoint(place, p1),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return curves;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The path a `d` attribute describes.
|
|
126
|
+
*
|
|
127
|
+
* Both cases of every command are read, so a relative run is resolved against
|
|
128
|
+
* where the last one ended. A command letter followed by more numbers than it
|
|
129
|
+
* takes repeats, which is the shorthand the grammar allows and which a moveto
|
|
130
|
+
* repeats as a lineto.
|
|
131
|
+
*/
|
|
132
|
+
export function pathFromData(d) {
|
|
133
|
+
const tokens = tokenize(d);
|
|
134
|
+
const subpaths = [];
|
|
135
|
+
let curves = [];
|
|
136
|
+
let start = vec2.ZERO;
|
|
137
|
+
let at = vec2.ZERO;
|
|
138
|
+
let open = false;
|
|
139
|
+
let moved = false;
|
|
140
|
+
let command = '';
|
|
141
|
+
let index = 0;
|
|
142
|
+
// Held per kind because an S reflects a cubic's second control and a T a
|
|
143
|
+
// quadratic's only one, and either falls back to the current point when the
|
|
144
|
+
// segment before it was neither.
|
|
145
|
+
let lastCubic;
|
|
146
|
+
let lastQuadratic;
|
|
147
|
+
const take = (count) => {
|
|
148
|
+
const values = [];
|
|
149
|
+
while (values.length < count) {
|
|
150
|
+
const token = tokens[index++];
|
|
151
|
+
if (!token || !('number' in token))
|
|
152
|
+
throw new Error(`path command "${command}" wants ${count} numbers and the run ends short`);
|
|
153
|
+
values.push(token.number);
|
|
154
|
+
}
|
|
155
|
+
return values;
|
|
156
|
+
};
|
|
157
|
+
const flush = (closed) => {
|
|
158
|
+
if (open)
|
|
159
|
+
subpaths.push({ start, curves, closed });
|
|
160
|
+
open = false;
|
|
161
|
+
};
|
|
162
|
+
const segment = (...added) => {
|
|
163
|
+
if (!moved)
|
|
164
|
+
throw new Error('path data draws before it moves to a starting point');
|
|
165
|
+
// A segment after a close begins again where the closed subpath began,
|
|
166
|
+
// which is the point the close left as the current one.
|
|
167
|
+
if (!open) {
|
|
168
|
+
start = at;
|
|
169
|
+
curves = [];
|
|
170
|
+
open = true;
|
|
171
|
+
}
|
|
172
|
+
curves.push(...added);
|
|
173
|
+
at = added[added.length - 1]?.to ?? at;
|
|
174
|
+
};
|
|
175
|
+
while (index < tokens.length) {
|
|
176
|
+
const token = tokens[index];
|
|
177
|
+
if (token && 'command' in token) {
|
|
178
|
+
command = token.command;
|
|
179
|
+
index++;
|
|
180
|
+
if (!DRAWN.includes(command.toUpperCase()))
|
|
181
|
+
throw new Error(`path data holds command "${command}", which is not one of "${DRAWN}"`);
|
|
182
|
+
}
|
|
183
|
+
else if (!command) {
|
|
184
|
+
throw new Error('path data opens on a number rather than a command');
|
|
185
|
+
}
|
|
186
|
+
const relative = command === command.toLowerCase();
|
|
187
|
+
const absolute = (x, y) => (relative ? vec2(at.x + x, at.y + y) : vec2(x, y));
|
|
188
|
+
switch (command.toUpperCase()) {
|
|
189
|
+
case 'M': {
|
|
190
|
+
const [x = 0, y = 0] = take(2);
|
|
191
|
+
flush(false);
|
|
192
|
+
at = absolute(x, y);
|
|
193
|
+
start = at;
|
|
194
|
+
curves = [];
|
|
195
|
+
open = true;
|
|
196
|
+
moved = true;
|
|
197
|
+
lastCubic = undefined;
|
|
198
|
+
lastQuadratic = undefined;
|
|
199
|
+
// A second pair under one moveto is a line rather than a second move,
|
|
200
|
+
// and the repetition carries the case the moveto was written in.
|
|
201
|
+
command = relative ? 'l' : 'L';
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
case 'L': {
|
|
205
|
+
const [x = 0, y = 0] = take(2);
|
|
206
|
+
segment(straight(at, absolute(x, y)));
|
|
207
|
+
lastCubic = undefined;
|
|
208
|
+
lastQuadratic = undefined;
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
case 'H': {
|
|
212
|
+
const [x = 0] = take(1);
|
|
213
|
+
segment(straight(at, relative ? vec2(at.x + x, at.y) : vec2(x, at.y)));
|
|
214
|
+
lastCubic = undefined;
|
|
215
|
+
lastQuadratic = undefined;
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
case 'V': {
|
|
219
|
+
const [y = 0] = take(1);
|
|
220
|
+
segment(straight(at, relative ? vec2(at.x, at.y + y) : vec2(at.x, y)));
|
|
221
|
+
lastCubic = undefined;
|
|
222
|
+
lastQuadratic = undefined;
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
case 'C': {
|
|
226
|
+
const [x1 = 0, y1 = 0, x2 = 0, y2 = 0, x = 0, y = 0] = take(6);
|
|
227
|
+
const control2 = absolute(x2, y2);
|
|
228
|
+
segment({ control1: absolute(x1, y1), control2, to: absolute(x, y) });
|
|
229
|
+
lastCubic = control2;
|
|
230
|
+
lastQuadratic = undefined;
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
case 'S': {
|
|
234
|
+
const [x2 = 0, y2 = 0, x = 0, y = 0] = take(4);
|
|
235
|
+
const control1 = lastCubic ? reflect(lastCubic, at) : at;
|
|
236
|
+
const control2 = absolute(x2, y2);
|
|
237
|
+
segment({ control1, control2, to: absolute(x, y) });
|
|
238
|
+
lastCubic = control2;
|
|
239
|
+
lastQuadratic = undefined;
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
case 'Q': {
|
|
243
|
+
const [qx = 0, qy = 0, x = 0, y = 0] = take(4);
|
|
244
|
+
const control = absolute(qx, qy);
|
|
245
|
+
segment(elevate(at, control, absolute(x, y)));
|
|
246
|
+
lastCubic = undefined;
|
|
247
|
+
lastQuadratic = control;
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
case 'T': {
|
|
251
|
+
const [x = 0, y = 0] = take(2);
|
|
252
|
+
const control = lastQuadratic ? reflect(lastQuadratic, at) : at;
|
|
253
|
+
segment(elevate(at, control, absolute(x, y)));
|
|
254
|
+
lastCubic = undefined;
|
|
255
|
+
lastQuadratic = control;
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
case 'A': {
|
|
259
|
+
const [rx = 0, ry = 0, turn = 0, large = 0, sweep = 0, x = 0, y = 0] = take(7);
|
|
260
|
+
const to = absolute(x, y);
|
|
261
|
+
// An arc that ends where it began is dropped rather than drawn, which
|
|
262
|
+
// is the specification's own wording: there is no such ellipse to pick.
|
|
263
|
+
if (to.x !== at.x || to.y !== at.y)
|
|
264
|
+
segment(...arcCurves(at, vec2(rx, ry), turn, large !== 0, sweep !== 0, to));
|
|
265
|
+
lastCubic = undefined;
|
|
266
|
+
lastQuadratic = undefined;
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
case 'Z': {
|
|
270
|
+
flush(true);
|
|
271
|
+
at = start;
|
|
272
|
+
lastCubic = undefined;
|
|
273
|
+
lastQuadratic = undefined;
|
|
274
|
+
// A close ends the run of one command, so a number after it has nothing
|
|
275
|
+
// to repeat and is refused rather than read as a line.
|
|
276
|
+
command = '';
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
flush(false);
|
|
282
|
+
return subpaths;
|
|
283
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export { SAME_TIME, keyAt, sampleTrack, sampleTracks, withKey, withoutKey } from
|
|
|
20
20
|
export type { Key, Track, TrackValue, Tracks } from './timing/track.js';
|
|
21
21
|
export { arc, circle, line, polygon, polyline, pointCount, pointOn, rect, straight, transformPath } from './figure/path.js';
|
|
22
22
|
export type { Cubic, Path, Subpath } from './figure/path.js';
|
|
23
|
+
export { pathFromData } from './figure/path-data.js';
|
|
23
24
|
export type { Colour, Fill, Mark, PathMark, Stroke, TextMark } from './figure/mark.js';
|
|
24
25
|
export { byAspect, fractionOf, matchingAspect, resolveExtent, viewMatrix } from './figure/extent.js';
|
|
25
26
|
export type { Extent, ExtentChoice, Fit } from './figure/extent.js';
|
package/dist/index.js
CHANGED
|
@@ -14,6 +14,7 @@ export { vec3 } from './values/vec3.js';
|
|
|
14
14
|
export { mat3 } from './values/mat3.js';
|
|
15
15
|
export { SAME_TIME, keyAt, sampleTrack, sampleTracks, withKey, withoutKey } from './timing/track.js';
|
|
16
16
|
export { arc, circle, line, polygon, polyline, pointCount, pointOn, rect, straight, transformPath } from './figure/path.js';
|
|
17
|
+
export { pathFromData } from './figure/path-data.js';
|
|
17
18
|
export { byAspect, fractionOf, matchingAspect, resolveExtent, viewMatrix } from './figure/extent.js';
|
|
18
19
|
export { flatten, group, shape, text } from './figure/node.js';
|
|
19
20
|
export { fadeIn, fadeOut, fadeTo, draw, morph, moveBy } from './figure/animation.js';
|
package/dist/paint/svg.d.ts
CHANGED
|
@@ -37,21 +37,26 @@ export declare function svgElements(marks: readonly Mark[], view: Mat3): SvgElem
|
|
|
37
37
|
export declare function svgMarkup(marks: readonly Mark[], view: Mat3, width: number, height: number): string;
|
|
38
38
|
/**
|
|
39
39
|
* Only what a painter needs from a document, named here rather than taken from
|
|
40
|
-
* the DOM types
|
|
40
|
+
* the DOM types, so this package declares no browser library at all: it can be
|
|
41
|
+
* checked and tested without one, and a caller can hand in a stand-in.
|
|
41
42
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
43
|
+
* The element a maker makes is the element the target is handed, and that type
|
|
44
|
+
* travels through rather than being flattened to the two members named below. An
|
|
45
|
+
* element in a real document takes whole nodes and text where the painter's own
|
|
46
|
+
* type takes neither, so a target written in terms of the painter's type is a
|
|
47
|
+
* target no real element can be: what a document offers and what the painter
|
|
48
|
+
* would ask for are each missing something the other has, and neither signature
|
|
49
|
+
* is assignable to the other in either direction.
|
|
45
50
|
*/
|
|
46
51
|
export interface PaintNode {
|
|
47
52
|
setAttribute(name: string, value: string): void;
|
|
48
53
|
textContent: string | null;
|
|
49
54
|
}
|
|
50
|
-
export interface PaintTarget {
|
|
51
|
-
replaceChildren(...nodes:
|
|
55
|
+
export interface PaintTarget<Made extends PaintNode = PaintNode> {
|
|
56
|
+
replaceChildren(...nodes: Made[]): void;
|
|
52
57
|
}
|
|
53
|
-
export interface ElementMaker {
|
|
54
|
-
createElementNS(namespace: string, tag: string):
|
|
58
|
+
export interface ElementMaker<Made extends PaintNode = PaintNode> {
|
|
59
|
+
createElementNS(namespace: string, tag: string): Made;
|
|
55
60
|
}
|
|
56
61
|
/**
|
|
57
62
|
* The marks put into an element that is already on the page.
|
|
@@ -61,4 +66,4 @@ export interface ElementMaker {
|
|
|
61
66
|
* and matching them up first would cost more than it saved while adding a way for
|
|
62
67
|
* two frames to disagree.
|
|
63
68
|
*/
|
|
64
|
-
export declare function paintSvg(into: PaintTarget
|
|
69
|
+
export declare function paintSvg<Made extends PaintNode>(into: PaintTarget<NoInfer<Made>>, marks: readonly Mark[], view: Mat3, maker: ElementMaker<Made>): void;
|
package/dist/paint/svg.js
CHANGED
|
@@ -114,7 +114,11 @@ const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
|
|
|
114
114
|
* and matching them up first would cost more than it saved while adding a way for
|
|
115
115
|
* two frames to disagree.
|
|
116
116
|
*/
|
|
117
|
-
export function paintSvg(
|
|
117
|
+
export function paintSvg(
|
|
118
|
+
// The maker alone says what kind of element this is. Read from the target as
|
|
119
|
+
// well, a real element would offer the whole union its own call accepts, text
|
|
120
|
+
// included, and that union is not a thing this painter can set an attribute on.
|
|
121
|
+
into, marks, view, maker) {
|
|
118
122
|
const elements = svgElements(marks, view);
|
|
119
123
|
into.replaceChildren(...elements.map((element) => {
|
|
120
124
|
const node = maker.createElementNS(SVG_NAMESPACE, element.tag);
|
package/package.json
CHANGED