@altpsyche/maths 0.4.1 → 0.5.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 +22 -2
- package/dist/figure/animation.d.ts +94 -1
- package/dist/figure/animation.js +229 -1
- package/dist/figure/bounds.d.ts +31 -0
- package/dist/figure/bounds.js +112 -0
- package/dist/figure/length.d.ts +53 -0
- package/dist/figure/length.js +117 -0
- package/dist/figure/timeline.d.ts +15 -0
- package/dist/figure/timeline.js +16 -0
- package/dist/figure/trim.d.ts +1 -1
- package/dist/figure/trim.js +7 -35
- package/dist/index.d.ts +6 -3
- package/dist/index.js +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,6 +22,10 @@ svgMarkup(at(figure, 0.5), viewMatrix(figure.extent, 'contain', 640, 360), 640,
|
|
|
22
22
|
|
|
23
23
|
<img src="docs/tangent.svg" width="720" alt="A parabola on a labelled grid, the region under it shaded to a point on the curve, the tangent at that point drawn, and the slope written as a number.">
|
|
24
24
|
|
|
25
|
+
The picture arrives rather than appearing. The grid fades, the axes draw on, their labels come in one
|
|
26
|
+
after another, the curve draws, the dot grows out of the origin, and the dot is pointed at where the
|
|
27
|
+
slope is nothing. Then it walks the curve at one speed and flashes at the top.
|
|
28
|
+
|
|
25
29
|
```ts
|
|
26
30
|
import { axes, coordsOf, group, interval, numberPlane, plot, scaleOf, shape } from '@altpsyche/maths';
|
|
27
31
|
|
|
@@ -55,8 +59,24 @@ which is what the number in the corner is.
|
|
|
55
59
|
|
|
56
60
|
<img src="docs/tangent-strip.svg" width="960" alt="Four frames of the same figure side by side, the point walking up the curve and the shaded region growing behind it.">
|
|
57
61
|
|
|
58
|
-
Four times of one figure, side by side
|
|
59
|
-
|
|
62
|
+
Four times of one figure, side by side: the picture arrived, the beat at the stationary point, half
|
|
63
|
+
way up, and the top. A moving picture in a README needs a GIF and this package has no encoder, so the
|
|
64
|
+
strip shows the motion in a still.
|
|
65
|
+
|
|
66
|
+
## The animations
|
|
67
|
+
|
|
68
|
+
`fadeIn`, `fadeOut`, `fadeTo`, `draw`, `morph`, `moveBy`, `rotate`, `scale`, `growFrom`, `moveAlong`,
|
|
69
|
+
`indicate`, `flash` and `circumscribe`. A `Timeline` plays them in order, plays several `together`, or
|
|
70
|
+
`stagger`s a row so its parts arrive one after another.
|
|
71
|
+
|
|
72
|
+
A turn and a growth happen about a point the marks decide for themselves, which is the middle of the
|
|
73
|
+
box round them. `boundsOf` is that box, worked out from where each piece of the curve turns back on
|
|
74
|
+
itself rather than from the points the curve is written from.
|
|
75
|
+
|
|
76
|
+
`moveAlong` carries a mark along a path at one speed, measured by the path's length. Even steps in a
|
|
77
|
+
curve's own parameter are uneven steps along the curve: a step covers more of it where the curve is
|
|
78
|
+
moving fast, which on a quarter circle is a 6.9% difference between the longest step and the shortest
|
|
79
|
+
and on the demo's own walk is 82%.
|
|
60
80
|
|
|
61
81
|
Both pictures are written by `svgMarkup`, which needs no browser, so `npm run demos` regenerates
|
|
62
82
|
them and a test compares the bytes against the committed files.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Vec2 } from '../values/vec2.js';
|
|
2
2
|
import { type Path } from './path.js';
|
|
3
|
-
import type { Mark } from './mark.js';
|
|
3
|
+
import type { Colour, Mark, Stroke } from './mark.js';
|
|
4
4
|
export type Animation = (marks: readonly Mark[], along: number) => readonly Mark[];
|
|
5
5
|
/** From nothing to whatever opacity the mark already had, so a mark that is
|
|
6
6
|
* half faded by design does not become solid on the way in. */
|
|
@@ -27,3 +27,96 @@ export declare function morph(target: string, into: Path): Animation;
|
|
|
27
27
|
/** A mark's own opacity walked to a value, for a figure that wants a thing dimmed
|
|
28
28
|
* rather than gone. */
|
|
29
29
|
export declare function fadeTo(target: string, opacity: number): Animation;
|
|
30
|
+
export interface AboutOptions {
|
|
31
|
+
/** The point the change happens about. The middle of the box round the marks
|
|
32
|
+
* being changed unless a figure names one. */
|
|
33
|
+
pivot?: Vec2;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Turned about a point, by an angle in radians.
|
|
37
|
+
*
|
|
38
|
+
* A text mark's anchor moves and its words stay upright. A mark carries no
|
|
39
|
+
* rotation of its own, so turning the words would mean adding one to what both
|
|
40
|
+
* painters have to do, and a label that stays readable while the thing it names
|
|
41
|
+
* turns is what a figure wants anyway, which is the same reason a number line
|
|
42
|
+
* takes a direction rather than being turned on its side.
|
|
43
|
+
*/
|
|
44
|
+
export declare function rotate(target: string, angle: number, options?: AboutOptions): Animation;
|
|
45
|
+
export interface ScaleOptions extends AboutOptions {
|
|
46
|
+
/** What it is scaled by at the start of the span, which is its own size. */
|
|
47
|
+
from?: number;
|
|
48
|
+
}
|
|
49
|
+
/** Grown or shrunk about a point, from one factor to another. */
|
|
50
|
+
export declare function scale(target: string, to: number, options?: ScaleOptions): Animation;
|
|
51
|
+
/**
|
|
52
|
+
* Carried along a path at a steady pace, by length rather than by piece.
|
|
53
|
+
*
|
|
54
|
+
* What it moves is the offset from the path's own start, so a mark placed at that
|
|
55
|
+
* start travels the path and a mark placed elsewhere travels the same shape from
|
|
56
|
+
* where it stands. That is what `moveBy` does with a straight offset, and it is
|
|
57
|
+
* what makes this nothing at the beginning of its span like every other change
|
|
58
|
+
* here.
|
|
59
|
+
*/
|
|
60
|
+
export declare function moveAlong(target: string, path: Path): Animation;
|
|
61
|
+
/**
|
|
62
|
+
* Grown from nothing at a point, which is a growth starting at no size.
|
|
63
|
+
*
|
|
64
|
+
* Left out, the point is the middle of the box round the marks, so a thing grows
|
|
65
|
+
* out of where it already is. At the end of the span it is the marks themselves
|
|
66
|
+
* rather than the marks rebuilt through a transform of one, so a growth that has
|
|
67
|
+
* finished leaves the geometry the author wrote.
|
|
68
|
+
*/
|
|
69
|
+
export declare function growFrom(target: string, from?: Vec2): Animation;
|
|
70
|
+
export interface IndicateOptions extends AboutOptions {
|
|
71
|
+
/** How big it gets at the middle of the span. */
|
|
72
|
+
factor?: number;
|
|
73
|
+
/** Held for the length of the span and then let go. */
|
|
74
|
+
colour?: Colour;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Swelled and settled, to point at something without moving it.
|
|
78
|
+
*
|
|
79
|
+
* The colour is swapped for the length of the span rather than walked into. A
|
|
80
|
+
* colour here is any CSS colour written as text, and walking between two of them
|
|
81
|
+
* needs a reader for every form one can take, which does not exist here yet.
|
|
82
|
+
*/
|
|
83
|
+
export declare function indicate(target: string, options?: IndicateOptions): Animation;
|
|
84
|
+
export interface FlashOptions {
|
|
85
|
+
stroke: Stroke;
|
|
86
|
+
/** Where it flashes from. The middle of the box round the marks unless named. */
|
|
87
|
+
at?: Vec2;
|
|
88
|
+
rays?: number;
|
|
89
|
+
/** How far the far end of a ray reaches at the widest, in figure units. Twice
|
|
90
|
+
* the distance from the middle of the box to its corner unless named, so the
|
|
91
|
+
* rays sit outside the thing they are pointing at. */
|
|
92
|
+
reach?: number;
|
|
93
|
+
/** Where the near end of a ray sits, as a share of the reach. */
|
|
94
|
+
inner?: number;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Rays out from a point and gone, for a moment a figure wants a reader to look
|
|
98
|
+
* at.
|
|
99
|
+
*
|
|
100
|
+
* The rays are in the list at every fraction of the span, at nothing at both
|
|
101
|
+
* ends, rather than appended part way through. A mark that arrives between one
|
|
102
|
+
* frame and the next turns up in a comparison between two frames as something
|
|
103
|
+
* that changed, and a figure's marks are compared frame to frame by every gate
|
|
104
|
+
* here.
|
|
105
|
+
*/
|
|
106
|
+
export declare function flash(target: string, options: FlashOptions): Animation;
|
|
107
|
+
export interface CircumscribeOptions {
|
|
108
|
+
stroke: Stroke;
|
|
109
|
+
/** A box round the thing, or the ellipse through the same four sides. */
|
|
110
|
+
around?: 'box' | 'ellipse';
|
|
111
|
+
/** How far outside the box it sits, in figure units. */
|
|
112
|
+
padding?: number;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* A shape drawn round something and then let go.
|
|
116
|
+
*
|
|
117
|
+
* The first half of the span draws it on and the second half fades it, so one
|
|
118
|
+
* span is the whole gesture rather than two a figure has to line up. The shape
|
|
119
|
+
* is in the list at every fraction, with nothing drawn at the beginning and
|
|
120
|
+
* nothing showing at the end, for the reason a flash keeps its rays.
|
|
121
|
+
*/
|
|
122
|
+
export declare function circumscribe(target: string, options: CircumscribeOptions): Animation;
|
package/dist/figure/animation.js
CHANGED
|
@@ -13,9 +13,13 @@
|
|
|
13
13
|
import { mat3 } from '../values/mat3.js';
|
|
14
14
|
import { vec2 } from '../values/vec2.js';
|
|
15
15
|
import { lerp } from '../values/scalar.js';
|
|
16
|
-
import {
|
|
16
|
+
import { smoothstep } from '../values/ease.js';
|
|
17
|
+
import { circle, line, polygon, transformPath } from './path.js';
|
|
17
18
|
import { trimPath } from './trim.js';
|
|
18
19
|
import { lerpPath } from './morph.js';
|
|
20
|
+
import { pointAlong } from './length.js';
|
|
21
|
+
import { boundsOfMarks, centreOf } from './bounds.js';
|
|
22
|
+
import { interval } from '../values/interval.js';
|
|
19
23
|
/**
|
|
20
24
|
* Which marks an animation touches.
|
|
21
25
|
*
|
|
@@ -80,3 +84,227 @@ export function morph(target, into) {
|
|
|
80
84
|
export function fadeTo(target, opacity) {
|
|
81
85
|
return over(target, (mark, along) => ({ ...mark, opacity: lerp(mark.opacity ?? 1, opacity, along) }));
|
|
82
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* A mark carried through a transform, geometry and weight together.
|
|
89
|
+
*
|
|
90
|
+
* A transform that scales makes the lines inside it thicker and the words
|
|
91
|
+
* bigger, the way it makes everything else bigger, which is what `flatten`
|
|
92
|
+
* already does for a group that scales. Doing less here would leave a shrinking
|
|
93
|
+
* mark with the stroke it started at.
|
|
94
|
+
*/
|
|
95
|
+
function carried(mark, through) {
|
|
96
|
+
const scale = mat3.scaleFactor(through);
|
|
97
|
+
if (mark.kind === 'text') {
|
|
98
|
+
return { ...mark, at: mat3.transformPoint(through, mark.at), size: mark.size * scale };
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
...mark,
|
|
102
|
+
path: transformPath(mark.path, through),
|
|
103
|
+
stroke: mark.stroke ? { ...mark.stroke, width: mark.stroke.width * scale } : undefined,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* A change built round a point the marks themselves decide.
|
|
108
|
+
*
|
|
109
|
+
* The pivot is read off the marks as they arrive, which is before this change has
|
|
110
|
+
* moved them, so it is the same point at every time and a turn of a whole circle
|
|
111
|
+
* lands where it began. Reading it back off the marks after the change would let
|
|
112
|
+
* it drift, because the box round a turned shape is not the turned box.
|
|
113
|
+
*/
|
|
114
|
+
function about(target, options, step) {
|
|
115
|
+
return (marks, along) => {
|
|
116
|
+
const touched = marks.filter((mark) => touches(mark.id, target));
|
|
117
|
+
if (touched.length === 0)
|
|
118
|
+
return marks;
|
|
119
|
+
const box = boundsOfMarks(touched);
|
|
120
|
+
const pivot = options.pivot ?? (box ? centreOf(box) : vec2(0, 0));
|
|
121
|
+
const through = step(along, pivot);
|
|
122
|
+
if (through === null)
|
|
123
|
+
return marks;
|
|
124
|
+
return marks.map((mark) => (touches(mark.id, target) ? carried(mark, through) : mark));
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/** The transform for a change about a point: back to the origin, the change,
|
|
128
|
+
* then back where it was. */
|
|
129
|
+
function around(pivot, change) {
|
|
130
|
+
return mat3.multiply(mat3.multiply(mat3.translation(pivot), change), mat3.translation(vec2.scale(pivot, -1)));
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Turned about a point, by an angle in radians.
|
|
134
|
+
*
|
|
135
|
+
* A text mark's anchor moves and its words stay upright. A mark carries no
|
|
136
|
+
* rotation of its own, so turning the words would mean adding one to what both
|
|
137
|
+
* painters have to do, and a label that stays readable while the thing it names
|
|
138
|
+
* turns is what a figure wants anyway, which is the same reason a number line
|
|
139
|
+
* takes a direction rather than being turned on its side.
|
|
140
|
+
*/
|
|
141
|
+
export function rotate(target, angle, options = {}) {
|
|
142
|
+
return about(target, options, (along, pivot) => {
|
|
143
|
+
const turned = angle * along;
|
|
144
|
+
return turned === 0 ? null : around(pivot, mat3.rotation(turned));
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/** Grown or shrunk about a point, from one factor to another. */
|
|
148
|
+
export function scale(target, to, options = {}) {
|
|
149
|
+
const start = options.from ?? 1;
|
|
150
|
+
return about(target, options, (along, pivot) => {
|
|
151
|
+
const factor = lerp(start, to, along);
|
|
152
|
+
return factor === 1 ? null : around(pivot, mat3.scaling(vec2(factor, factor)));
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Carried along a path at a steady pace, by length rather than by piece.
|
|
157
|
+
*
|
|
158
|
+
* What it moves is the offset from the path's own start, so a mark placed at that
|
|
159
|
+
* start travels the path and a mark placed elsewhere travels the same shape from
|
|
160
|
+
* where it stands. That is what `moveBy` does with a straight offset, and it is
|
|
161
|
+
* what makes this nothing at the beginning of its span like every other change
|
|
162
|
+
* here.
|
|
163
|
+
*/
|
|
164
|
+
export function moveAlong(target, path) {
|
|
165
|
+
const start = pointAlong(path, 0);
|
|
166
|
+
return (marks, along) => {
|
|
167
|
+
if (start === null || along === 0)
|
|
168
|
+
return marks;
|
|
169
|
+
const reached = pointAlong(path, along);
|
|
170
|
+
if (reached === null)
|
|
171
|
+
return marks;
|
|
172
|
+
const step = mat3.translation(vec2.sub(reached, start));
|
|
173
|
+
return marks.map((mark) => (touches(mark.id, target) ? carried(mark, step) : mark));
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Grown from nothing at a point, which is a growth starting at no size.
|
|
178
|
+
*
|
|
179
|
+
* Left out, the point is the middle of the box round the marks, so a thing grows
|
|
180
|
+
* out of where it already is. At the end of the span it is the marks themselves
|
|
181
|
+
* rather than the marks rebuilt through a transform of one, so a growth that has
|
|
182
|
+
* finished leaves the geometry the author wrote.
|
|
183
|
+
*/
|
|
184
|
+
export function growFrom(target, from) {
|
|
185
|
+
return scale(target, 1, { from: 0, pivot: from });
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Out and back over a span, flat at the beginning, the peak and the end.
|
|
189
|
+
*
|
|
190
|
+
* Each half is the smoothstep the tracks and the timeline already pace changes
|
|
191
|
+
* with, so a pulse leaves from rest, turns without a corner, and settles.
|
|
192
|
+
*/
|
|
193
|
+
function thereAndBack(along) {
|
|
194
|
+
return along < 0.5 ? smoothstep(along * 2) : smoothstep(2 - along * 2);
|
|
195
|
+
}
|
|
196
|
+
/** A mark's own colours replaced, fill and stroke together. */
|
|
197
|
+
function painted(mark, colour) {
|
|
198
|
+
if (mark.kind === 'text')
|
|
199
|
+
return { ...mark, fill: { ...mark.fill, colour } };
|
|
200
|
+
return {
|
|
201
|
+
...mark,
|
|
202
|
+
fill: mark.fill ? { ...mark.fill, colour } : undefined,
|
|
203
|
+
stroke: mark.stroke ? { ...mark.stroke, colour } : undefined,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Swelled and settled, to point at something without moving it.
|
|
208
|
+
*
|
|
209
|
+
* The colour is swapped for the length of the span rather than walked into. A
|
|
210
|
+
* colour here is any CSS colour written as text, and walking between two of them
|
|
211
|
+
* needs a reader for every form one can take, which does not exist here yet.
|
|
212
|
+
*/
|
|
213
|
+
export function indicate(target, options = {}) {
|
|
214
|
+
const peak = options.factor ?? 1.2;
|
|
215
|
+
const swell = about(target, options, (along, pivot) => {
|
|
216
|
+
const factor = lerp(1, peak, thereAndBack(along));
|
|
217
|
+
return factor === 1 ? null : around(pivot, mat3.scaling(vec2(factor, factor)));
|
|
218
|
+
});
|
|
219
|
+
const colour = options.colour;
|
|
220
|
+
return (marks, along) => {
|
|
221
|
+
const swelled = swell(marks, along);
|
|
222
|
+
if (colour === undefined || along <= 0 || along >= 1)
|
|
223
|
+
return swelled;
|
|
224
|
+
if (!swelled.some((mark) => touches(mark.id, target)))
|
|
225
|
+
return swelled;
|
|
226
|
+
return swelled.map((mark) => (touches(mark.id, target) ? painted(mark, colour) : mark));
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Rays out from a point and gone, for a moment a figure wants a reader to look
|
|
231
|
+
* at.
|
|
232
|
+
*
|
|
233
|
+
* The rays are in the list at every fraction of the span, at nothing at both
|
|
234
|
+
* ends, rather than appended part way through. A mark that arrives between one
|
|
235
|
+
* frame and the next turns up in a comparison between two frames as something
|
|
236
|
+
* that changed, and a figure's marks are compared frame to frame by every gate
|
|
237
|
+
* here.
|
|
238
|
+
*/
|
|
239
|
+
export function flash(target, options) {
|
|
240
|
+
const count = Math.max(1, Math.round(options.rays ?? 12));
|
|
241
|
+
const inner = options.inner ?? 0.5;
|
|
242
|
+
return (marks, along) => {
|
|
243
|
+
const touched = marks.filter((mark) => touches(mark.id, target));
|
|
244
|
+
if (touched.length === 0)
|
|
245
|
+
return marks;
|
|
246
|
+
const box = boundsOfMarks(touched);
|
|
247
|
+
if (box === null)
|
|
248
|
+
return marks;
|
|
249
|
+
const centre = options.at ?? centreOf(box);
|
|
250
|
+
const corner = Math.hypot(interval.span(box.x) / 2, interval.span(box.y) / 2);
|
|
251
|
+
const reach = options.reach ?? corner * 2;
|
|
252
|
+
const opacity = thereAndBack(along);
|
|
253
|
+
const rays = [];
|
|
254
|
+
for (let ray = 0; ray < count; ray++) {
|
|
255
|
+
const angle = (2 * Math.PI * ray) / count;
|
|
256
|
+
const direction = vec2(Math.cos(angle), Math.sin(angle));
|
|
257
|
+
const near = vec2.add(centre, vec2.scale(direction, reach * inner * along));
|
|
258
|
+
const far = vec2.add(centre, vec2.scale(direction, reach * along));
|
|
259
|
+
rays.push({
|
|
260
|
+
kind: 'path',
|
|
261
|
+
id: `${target}/flash/${ray}`,
|
|
262
|
+
path: line(near, far),
|
|
263
|
+
stroke: options.stroke,
|
|
264
|
+
opacity,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
return [...marks, ...rays];
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* A shape drawn round something and then let go.
|
|
272
|
+
*
|
|
273
|
+
* The first half of the span draws it on and the second half fades it, so one
|
|
274
|
+
* span is the whole gesture rather than two a figure has to line up. The shape
|
|
275
|
+
* is in the list at every fraction, with nothing drawn at the beginning and
|
|
276
|
+
* nothing showing at the end, for the reason a flash keeps its rays.
|
|
277
|
+
*/
|
|
278
|
+
export function circumscribe(target, options) {
|
|
279
|
+
const padding = options.padding ?? 0;
|
|
280
|
+
return (marks, along) => {
|
|
281
|
+
const touched = marks.filter((mark) => touches(mark.id, target));
|
|
282
|
+
if (touched.length === 0)
|
|
283
|
+
return marks;
|
|
284
|
+
const box = boundsOfMarks(touched);
|
|
285
|
+
if (box === null)
|
|
286
|
+
return marks;
|
|
287
|
+
const centre = centreOf(box);
|
|
288
|
+
const across = interval.span(box.x) / 2 + padding;
|
|
289
|
+
const up = interval.span(box.y) / 2 + padding;
|
|
290
|
+
const whole = (options.around ?? 'box') === 'ellipse'
|
|
291
|
+
? transformPath(circle(vec2(0, 0), 1), mat3.multiply(mat3.translation(centre), mat3.scaling(vec2(across, up))))
|
|
292
|
+
: polygon([
|
|
293
|
+
vec2(centre.x - across, centre.y - up),
|
|
294
|
+
vec2(centre.x + across, centre.y - up),
|
|
295
|
+
vec2(centre.x + across, centre.y + up),
|
|
296
|
+
vec2(centre.x - across, centre.y + up),
|
|
297
|
+
]);
|
|
298
|
+
const drawing = along <= 0.5;
|
|
299
|
+
return [
|
|
300
|
+
...marks,
|
|
301
|
+
{
|
|
302
|
+
kind: 'path',
|
|
303
|
+
id: `${target}/circumscribed`,
|
|
304
|
+
path: drawing ? trimPath(whole, along * 2) : whole,
|
|
305
|
+
stroke: options.stroke,
|
|
306
|
+
opacity: drawing ? 1 : 2 - along * 2,
|
|
307
|
+
},
|
|
308
|
+
];
|
|
309
|
+
};
|
|
310
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The smallest box holding a path, worked out rather than sampled.
|
|
3
|
+
*
|
|
4
|
+
* A curve's furthest point is usually not one of the points it is written from,
|
|
5
|
+
* so the box round the control points is bigger than the box round the curve.
|
|
6
|
+
* The derivative of a cubic is a quadratic, so where each piece turns back on
|
|
7
|
+
* itself is the root of a quadratic, and the box is those turns together with
|
|
8
|
+
* the ends of every piece.
|
|
9
|
+
*/
|
|
10
|
+
import { type Interval } from '../values/interval.js';
|
|
11
|
+
import { type Path } from './path.js';
|
|
12
|
+
import { type Vec2 } from '../values/vec2.js';
|
|
13
|
+
import type { Mark } from './mark.js';
|
|
14
|
+
export interface Bounds {
|
|
15
|
+
readonly x: Interval;
|
|
16
|
+
readonly y: Interval;
|
|
17
|
+
}
|
|
18
|
+
/** The box round a path, or nothing where the path holds no points. */
|
|
19
|
+
export declare function boundsOf(path: Path): Bounds | null;
|
|
20
|
+
/**
|
|
21
|
+
* The box round a list of marks, or nothing where the list is empty.
|
|
22
|
+
*
|
|
23
|
+
* A text mark reaches only as far as its own anchor. How wide some text is
|
|
24
|
+
* depends on which fonts the machine has, so a box that took it in would be a
|
|
25
|
+
* different box on two machines, and nothing about a figure's layout may turn on
|
|
26
|
+
* that.
|
|
27
|
+
*/
|
|
28
|
+
export declare function boundsOfMarks(marks: readonly Mark[]): Bounds | null;
|
|
29
|
+
/** The middle of a box, which is what a turn or a growth happens about when a
|
|
30
|
+
* figure names no other point. */
|
|
31
|
+
export declare function centreOf(bounds: Bounds): Vec2;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The smallest box holding a path, worked out rather than sampled.
|
|
3
|
+
*
|
|
4
|
+
* A curve's furthest point is usually not one of the points it is written from,
|
|
5
|
+
* so the box round the control points is bigger than the box round the curve.
|
|
6
|
+
* The derivative of a cubic is a quadratic, so where each piece turns back on
|
|
7
|
+
* itself is the root of a quadratic, and the box is those turns together with
|
|
8
|
+
* the ends of every piece.
|
|
9
|
+
*/
|
|
10
|
+
import { interval } from '../values/interval.js';
|
|
11
|
+
import { pointOn } from './path.js';
|
|
12
|
+
import { vec2 } from '../values/vec2.js';
|
|
13
|
+
/** Below this a squared term counts as absent and the derivative is read as a
|
|
14
|
+
* straight line, which is what a cubic written as a straight segment is. */
|
|
15
|
+
const FLAT = 1e-12;
|
|
16
|
+
/**
|
|
17
|
+
* Where one piece turns back on itself along one axis, as fractions of its own
|
|
18
|
+
* length strictly between its two ends.
|
|
19
|
+
*
|
|
20
|
+
* The ends are added by the caller, since they belong to the piece rather than
|
|
21
|
+
* to its turns, and a piece with no turn at all still has them.
|
|
22
|
+
*/
|
|
23
|
+
function turns(from, control1, control2, to) {
|
|
24
|
+
const square = -from + 3 * control1 - 3 * control2 + to;
|
|
25
|
+
const linear = 2 * from - 4 * control1 + 2 * control2;
|
|
26
|
+
const constant = control1 - from;
|
|
27
|
+
const inside = (at) => (at > 0 && at < 1 ? [at] : []);
|
|
28
|
+
if (Math.abs(square) < FLAT) {
|
|
29
|
+
if (Math.abs(linear) < FLAT)
|
|
30
|
+
return [];
|
|
31
|
+
return inside(-constant / linear);
|
|
32
|
+
}
|
|
33
|
+
const under = linear * linear - 4 * square * constant;
|
|
34
|
+
if (under < 0)
|
|
35
|
+
return [];
|
|
36
|
+
const root = Math.sqrt(under);
|
|
37
|
+
return [...inside((-linear + root) / (2 * square)), ...inside((-linear - root) / (2 * square))];
|
|
38
|
+
}
|
|
39
|
+
function grown(bounds, value) {
|
|
40
|
+
if (value < bounds.low)
|
|
41
|
+
bounds.low = value;
|
|
42
|
+
if (value > bounds.high)
|
|
43
|
+
bounds.high = value;
|
|
44
|
+
}
|
|
45
|
+
function reach(into, point) {
|
|
46
|
+
grown(into.x, point.x);
|
|
47
|
+
grown(into.y, point.y);
|
|
48
|
+
}
|
|
49
|
+
function pieceReach(into, from, curve) {
|
|
50
|
+
reach(into, curve.to);
|
|
51
|
+
for (const at of turns(from.x, curve.control1.x, curve.control2.x, curve.to.x)) {
|
|
52
|
+
reach(into, pointOn(from, curve, at));
|
|
53
|
+
}
|
|
54
|
+
for (const at of turns(from.y, curve.control1.y, curve.control2.y, curve.to.y)) {
|
|
55
|
+
reach(into, pointOn(from, curve, at));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function empty() {
|
|
59
|
+
return {
|
|
60
|
+
x: { low: Infinity, high: -Infinity },
|
|
61
|
+
y: { low: Infinity, high: -Infinity },
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function settled(box) {
|
|
65
|
+
if (box.x.low > box.x.high)
|
|
66
|
+
return null;
|
|
67
|
+
return { x: interval(box.x.low, box.x.high), y: interval(box.y.low, box.y.high) };
|
|
68
|
+
}
|
|
69
|
+
/** The box round a path, or nothing where the path holds no points. */
|
|
70
|
+
export function boundsOf(path) {
|
|
71
|
+
const box = empty();
|
|
72
|
+
for (const subpath of path) {
|
|
73
|
+
reach(box, subpath.start);
|
|
74
|
+
let from = subpath.start;
|
|
75
|
+
for (const curve of subpath.curves) {
|
|
76
|
+
pieceReach(box, from, curve);
|
|
77
|
+
from = curve.to;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return settled(box);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The box round a list of marks, or nothing where the list is empty.
|
|
84
|
+
*
|
|
85
|
+
* A text mark reaches only as far as its own anchor. How wide some text is
|
|
86
|
+
* depends on which fonts the machine has, so a box that took it in would be a
|
|
87
|
+
* different box on two machines, and nothing about a figure's layout may turn on
|
|
88
|
+
* that.
|
|
89
|
+
*/
|
|
90
|
+
export function boundsOfMarks(marks) {
|
|
91
|
+
const box = empty();
|
|
92
|
+
for (const mark of marks) {
|
|
93
|
+
if (mark.kind === 'text') {
|
|
94
|
+
reach(box, mark.at);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
for (const subpath of mark.path) {
|
|
98
|
+
reach(box, subpath.start);
|
|
99
|
+
let from = subpath.start;
|
|
100
|
+
for (const curve of subpath.curves) {
|
|
101
|
+
pieceReach(box, from, curve);
|
|
102
|
+
from = curve.to;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return settled(box);
|
|
107
|
+
}
|
|
108
|
+
/** The middle of a box, which is what a turn or a growth happens about when a
|
|
109
|
+
* figure names no other point. */
|
|
110
|
+
export function centreOf(bounds) {
|
|
111
|
+
return vec2(interval.at(bounds.x, 0.5), interval.at(bounds.y, 0.5));
|
|
112
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How long a path is, measured piece by piece, and where a length falls inside
|
|
3
|
+
* one piece.
|
|
4
|
+
*
|
|
5
|
+
* A cubic's parameter is not its length: the same step in parameter covers more
|
|
6
|
+
* of the curve where the curve is moving fast. So anything that walks a path at
|
|
7
|
+
* a steady pace needs the two related, and this is the table that relates them.
|
|
8
|
+
*
|
|
9
|
+
* The table is sampled rather than solved. A cubic's length has no closed form,
|
|
10
|
+
* so every library that measures one either samples it or integrates it
|
|
11
|
+
* numerically, and the samples are wanted anyway.
|
|
12
|
+
*/
|
|
13
|
+
import { type Vec2 } from '../values/vec2.js';
|
|
14
|
+
import { type Path } from './path.js';
|
|
15
|
+
export interface Measured {
|
|
16
|
+
/** The length reached at each sample parameter, starting at nothing and
|
|
17
|
+
* ending at the whole, so the piece's own parameter can be read back out of a
|
|
18
|
+
* length. */
|
|
19
|
+
readonly upTo: readonly number[];
|
|
20
|
+
readonly total: number;
|
|
21
|
+
}
|
|
22
|
+
export interface Measure {
|
|
23
|
+
/** Every piece of every subpath, in the order a walk crosses them. */
|
|
24
|
+
readonly per: readonly (readonly Measured[])[];
|
|
25
|
+
readonly total: number;
|
|
26
|
+
}
|
|
27
|
+
/** Every piece's length and the whole, which is what a walk by length needs
|
|
28
|
+
* before it can find which piece a place falls in. */
|
|
29
|
+
export declare function measurePath(path: Path): Measure;
|
|
30
|
+
/**
|
|
31
|
+
* The parameter a length into one piece.
|
|
32
|
+
*
|
|
33
|
+
* Reading the fraction of the length as the parameter is exact only where the
|
|
34
|
+
* curve moves at one rate, which is a straight line. Walking the table instead
|
|
35
|
+
* and interpolating inside the one sample the length lands in makes a cut by
|
|
36
|
+
* length even along a curve as well: measured on a quarter circle, cutting at
|
|
37
|
+
* twenty even fractions of its length was 4.7e-3 of the whole out at the worst
|
|
38
|
+
* of them and is 1.4e-4 out now.
|
|
39
|
+
*/
|
|
40
|
+
export declare function parameterAt(measured: Measured, wanted: number): number;
|
|
41
|
+
/** How long a path is, in figure units, across every subpath it holds. It reads
|
|
42
|
+
* a little short of the truth, by the chord error the sample count above
|
|
43
|
+
* states. */
|
|
44
|
+
export declare function lengthOf(path: Path): number;
|
|
45
|
+
/**
|
|
46
|
+
* The point a fraction of the way along a path, measured by length rather than
|
|
47
|
+
* by piece or by parameter.
|
|
48
|
+
*
|
|
49
|
+
* A fraction outside nothing to one is held at the nearer end, so a walk that
|
|
50
|
+
* overshoots stops at the end of the path rather than carrying on past it. A
|
|
51
|
+
* path with no points has no such place and hands back nothing.
|
|
52
|
+
*/
|
|
53
|
+
export declare function pointAlong(path: Path, fraction: number): Vec2 | null;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How long a path is, measured piece by piece, and where a length falls inside
|
|
3
|
+
* one piece.
|
|
4
|
+
*
|
|
5
|
+
* A cubic's parameter is not its length: the same step in parameter covers more
|
|
6
|
+
* of the curve where the curve is moving fast. So anything that walks a path at
|
|
7
|
+
* a steady pace needs the two related, and this is the table that relates them.
|
|
8
|
+
*
|
|
9
|
+
* The table is sampled rather than solved. A cubic's length has no closed form,
|
|
10
|
+
* so every library that measures one either samples it or integrates it
|
|
11
|
+
* numerically, and the samples are wanted anyway.
|
|
12
|
+
*/
|
|
13
|
+
import { vec2 } from '../values/vec2.js';
|
|
14
|
+
import { pointOn } from './path.js';
|
|
15
|
+
import { clamp } from '../values/scalar.js';
|
|
16
|
+
/**
|
|
17
|
+
* How many samples measure one piece.
|
|
18
|
+
*
|
|
19
|
+
* Sixteen chords read a quarter circle 4 parts in ten thousand short of its true
|
|
20
|
+
* length, since a chord cuts the corner off the arc it spans. What the count is
|
|
21
|
+
* chosen for is the evenness of a walk rather than the total, and a share of the
|
|
22
|
+
* length is a ratio the shortfall largely cancels out of: measured, twenty steps
|
|
23
|
+
* along a quarter circle vary by 0.36% in size at this count. Doubling it costs
|
|
24
|
+
* twice the work in every cut and every walk and buys a total four times closer,
|
|
25
|
+
* which nothing here has asked for.
|
|
26
|
+
*/
|
|
27
|
+
const SAMPLES = 16;
|
|
28
|
+
function measureCurve(from, curve) {
|
|
29
|
+
const upTo = [0];
|
|
30
|
+
let total = 0;
|
|
31
|
+
let previous = from;
|
|
32
|
+
for (let at = 1; at <= SAMPLES; at++) {
|
|
33
|
+
const point = pointOn(from, curve, at / SAMPLES);
|
|
34
|
+
total += vec2.distance(previous, point);
|
|
35
|
+
upTo.push(total);
|
|
36
|
+
previous = point;
|
|
37
|
+
}
|
|
38
|
+
return { upTo, total };
|
|
39
|
+
}
|
|
40
|
+
/** Every piece's length and the whole, which is what a walk by length needs
|
|
41
|
+
* before it can find which piece a place falls in. */
|
|
42
|
+
export function measurePath(path) {
|
|
43
|
+
let total = 0;
|
|
44
|
+
const per = path.map((subpath) => {
|
|
45
|
+
let from = subpath.start;
|
|
46
|
+
return subpath.curves.map((curve) => {
|
|
47
|
+
const measured = measureCurve(from, curve);
|
|
48
|
+
from = curve.to;
|
|
49
|
+
total += measured.total;
|
|
50
|
+
return measured;
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
return { per, total };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The parameter a length into one piece.
|
|
57
|
+
*
|
|
58
|
+
* Reading the fraction of the length as the parameter is exact only where the
|
|
59
|
+
* curve moves at one rate, which is a straight line. Walking the table instead
|
|
60
|
+
* and interpolating inside the one sample the length lands in makes a cut by
|
|
61
|
+
* length even along a curve as well: measured on a quarter circle, cutting at
|
|
62
|
+
* twenty even fractions of its length was 4.7e-3 of the whole out at the worst
|
|
63
|
+
* of them and is 1.4e-4 out now.
|
|
64
|
+
*/
|
|
65
|
+
export function parameterAt(measured, wanted) {
|
|
66
|
+
const { upTo, total } = measured;
|
|
67
|
+
if (!(total > 0) || wanted <= 0)
|
|
68
|
+
return 0;
|
|
69
|
+
if (wanted >= total)
|
|
70
|
+
return 1;
|
|
71
|
+
let sample = 1;
|
|
72
|
+
while (sample < upTo.length - 1 && upTo[sample] < wanted)
|
|
73
|
+
sample++;
|
|
74
|
+
const low = upTo[sample - 1];
|
|
75
|
+
const high = upTo[sample];
|
|
76
|
+
const within = high > low ? (wanted - low) / (high - low) : 0;
|
|
77
|
+
return (sample - 1 + within) / SAMPLES;
|
|
78
|
+
}
|
|
79
|
+
/** How long a path is, in figure units, across every subpath it holds. It reads
|
|
80
|
+
* a little short of the truth, by the chord error the sample count above
|
|
81
|
+
* states. */
|
|
82
|
+
export function lengthOf(path) {
|
|
83
|
+
return measurePath(path).total;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The point a fraction of the way along a path, measured by length rather than
|
|
87
|
+
* by piece or by parameter.
|
|
88
|
+
*
|
|
89
|
+
* A fraction outside nothing to one is held at the nearer end, so a walk that
|
|
90
|
+
* overshoots stops at the end of the path rather than carrying on past it. A
|
|
91
|
+
* path with no points has no such place and hands back nothing.
|
|
92
|
+
*/
|
|
93
|
+
export function pointAlong(path, fraction) {
|
|
94
|
+
if (path.length === 0)
|
|
95
|
+
return null;
|
|
96
|
+
const { per, total } = measurePath(path);
|
|
97
|
+
if (!(total > 0))
|
|
98
|
+
return path[0].start;
|
|
99
|
+
const wanted = total * clamp(fraction, 0, 1);
|
|
100
|
+
let walked = 0;
|
|
101
|
+
for (let at = 0; at < path.length; at++) {
|
|
102
|
+
const subpath = path[at];
|
|
103
|
+
let from = subpath.start;
|
|
104
|
+
for (let piece = 0; piece < subpath.curves.length; piece++) {
|
|
105
|
+
const curve = subpath.curves[piece];
|
|
106
|
+
const measured = per[at][piece];
|
|
107
|
+
if (walked + measured.total >= wanted)
|
|
108
|
+
return pointOn(from, curve, parameterAt(measured, wanted - walked));
|
|
109
|
+
walked += measured.total;
|
|
110
|
+
from = curve.to;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Reached only when the lengths added up a hair short of the whole, so the
|
|
114
|
+
// place asked for is the far end.
|
|
115
|
+
const last = path[path.length - 1];
|
|
116
|
+
return last.curves.length > 0 ? last.curves[last.curves.length - 1].to : last.start;
|
|
117
|
+
}
|
|
@@ -22,6 +22,12 @@ export interface PlayOptions {
|
|
|
22
22
|
* animation with the one before it. */
|
|
23
23
|
after?: number;
|
|
24
24
|
}
|
|
25
|
+
export interface StaggerOptions extends PlayOptions {
|
|
26
|
+
/** Seconds between one change starting and the next. A quarter of each
|
|
27
|
+
* change's own length unless a figure says otherwise, so a row overlaps rather
|
|
28
|
+
* than running one at a time. */
|
|
29
|
+
gap?: number;
|
|
30
|
+
}
|
|
25
31
|
/**
|
|
26
32
|
* The ordered list, built by naming one thing after another.
|
|
27
33
|
*
|
|
@@ -37,6 +43,15 @@ export declare class Timeline {
|
|
|
37
43
|
play(animation: Animation, seconds: number, options?: PlayOptions): Timeline;
|
|
38
44
|
/** Several changes over one span, which is how two things move at once. */
|
|
39
45
|
together(animations: readonly Animation[], seconds: number, options?: PlayOptions): Timeline;
|
|
46
|
+
/**
|
|
47
|
+
* A row of changes, each starting a gap after the one before and each running
|
|
48
|
+
* the same length.
|
|
49
|
+
*
|
|
50
|
+
* Written out by hand this is one play a change with a negative wait between
|
|
51
|
+
* them, and getting that arithmetic right at every entry is what a row of six
|
|
52
|
+
* things arriving one after another used to cost.
|
|
53
|
+
*/
|
|
54
|
+
stagger(animations: readonly Animation[], seconds: number, options?: StaggerOptions): Timeline;
|
|
40
55
|
wait(seconds: number): Timeline;
|
|
41
56
|
/**
|
|
42
57
|
* The marks as every span leaves them at a time.
|
package/dist/figure/timeline.js
CHANGED
|
@@ -37,6 +37,22 @@ export class Timeline {
|
|
|
37
37
|
});
|
|
38
38
|
return built;
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* A row of changes, each starting a gap after the one before and each running
|
|
42
|
+
* the same length.
|
|
43
|
+
*
|
|
44
|
+
* Written out by hand this is one play a change with a negative wait between
|
|
45
|
+
* them, and getting that arithmetic right at every entry is what a row of six
|
|
46
|
+
* things arriving one after another used to cost.
|
|
47
|
+
*/
|
|
48
|
+
stagger(animations, seconds, options = {}) {
|
|
49
|
+
const gap = Math.max(0, options.gap ?? seconds / 4);
|
|
50
|
+
let built = this;
|
|
51
|
+
animations.forEach((animation, at) => {
|
|
52
|
+
built = built.play(animation, seconds, at === 0 ? options : { ...options, after: gap - seconds });
|
|
53
|
+
});
|
|
54
|
+
return built;
|
|
55
|
+
}
|
|
40
56
|
wait(seconds) {
|
|
41
57
|
return new Timeline(this.spans, this.duration + seconds);
|
|
42
58
|
}
|
package/dist/figure/trim.d.ts
CHANGED
package/dist/figure/trim.js
CHANGED
|
@@ -4,14 +4,11 @@
|
|
|
4
4
|
* The cut is by length rather than by segment, so a path whose segments differ in
|
|
5
5
|
* size is drawn at one steady pace. Parameterising by segment instead is cheaper
|
|
6
6
|
* and reads wrong: a long side would be crossed in the same time as a short one
|
|
7
|
-
* and the pen would visibly speed up and slow down.
|
|
7
|
+
* and the pen would visibly speed up and slow down. The length inside a segment
|
|
8
|
+
* is read off that segment's own table for the same reason.
|
|
8
9
|
*/
|
|
9
10
|
import { vec2 } from '../values/vec2.js';
|
|
10
|
-
import {
|
|
11
|
-
/** How many samples measure one segment. Sixteen holds the length of a quarter
|
|
12
|
-
* circle to better than a part in ten thousand, which is finer than the curve's
|
|
13
|
-
* own error against a true arc. */
|
|
14
|
-
const SAMPLES = 16;
|
|
11
|
+
import { measurePath, parameterAt } from './length.js';
|
|
15
12
|
/**
|
|
16
13
|
* A cubic cut at a fraction of its own parameter, keeping the first piece.
|
|
17
14
|
*
|
|
@@ -26,32 +23,6 @@ function splitCubic(from, curve, along) {
|
|
|
26
23
|
const e = vec2.lerp(b, c, along);
|
|
27
24
|
return { control1: a, control2: d, to: vec2.lerp(d, e, along) };
|
|
28
25
|
}
|
|
29
|
-
/** A segment's length, measured by walking it in straight steps. */
|
|
30
|
-
function segmentLength(from, curve) {
|
|
31
|
-
let total = 0;
|
|
32
|
-
let previous = from;
|
|
33
|
-
for (let at = 1; at <= SAMPLES; at++) {
|
|
34
|
-
const point = pointOn(from, curve, at / SAMPLES);
|
|
35
|
-
total += vec2.distance(previous, point);
|
|
36
|
-
previous = point;
|
|
37
|
-
}
|
|
38
|
-
return total;
|
|
39
|
-
}
|
|
40
|
-
/** Every segment's length and the total, which is what a cut by length needs
|
|
41
|
-
* before it can find which segment the cut falls in. */
|
|
42
|
-
function lengths(path) {
|
|
43
|
-
let total = 0;
|
|
44
|
-
const per = path.map((subpath) => {
|
|
45
|
-
let from = subpath.start;
|
|
46
|
-
return subpath.curves.map((curve) => {
|
|
47
|
-
const length = segmentLength(from, curve);
|
|
48
|
-
from = curve.to;
|
|
49
|
-
total += length;
|
|
50
|
-
return length;
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
return { per, total };
|
|
54
|
-
}
|
|
55
26
|
/**
|
|
56
27
|
* The path up to a fraction of its total length.
|
|
57
28
|
*
|
|
@@ -65,7 +36,7 @@ export function trimPath(path, fraction) {
|
|
|
65
36
|
return path;
|
|
66
37
|
if (fraction <= 0)
|
|
67
38
|
return [];
|
|
68
|
-
const { per, total } =
|
|
39
|
+
const { per, total } = measurePath(path);
|
|
69
40
|
if (total === 0)
|
|
70
41
|
return path;
|
|
71
42
|
const wanted = total * fraction;
|
|
@@ -78,14 +49,15 @@ export function trimPath(path, fraction) {
|
|
|
78
49
|
let cut = false;
|
|
79
50
|
for (let piece = 0; piece < subpath.curves.length; piece++) {
|
|
80
51
|
const curve = subpath.curves[piece];
|
|
81
|
-
const
|
|
52
|
+
const measured = per[at][piece];
|
|
53
|
+
const length = measured.total;
|
|
82
54
|
if (walked + length <= wanted || length === 0) {
|
|
83
55
|
curves.push(curve);
|
|
84
56
|
walked += length;
|
|
85
57
|
from = curve.to;
|
|
86
58
|
continue;
|
|
87
59
|
}
|
|
88
|
-
curves.push(splitCubic(from, curve, (wanted - walked)
|
|
60
|
+
curves.push(splitCubic(from, curve, parameterAt(measured, wanted - walked)));
|
|
89
61
|
cut = true;
|
|
90
62
|
break;
|
|
91
63
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -26,6 +26,8 @@ export { pathFromData } from './figure/path-data.js';
|
|
|
26
26
|
export type { Colour, Fill, Mark, PathMark, Stroke, TextMark } from './figure/mark.js';
|
|
27
27
|
export { byAspect, fractionOf, matchingAspect, resolveExtent, viewMatrix } from './figure/extent.js';
|
|
28
28
|
export type { Extent, ExtentChoice, Fit } from './figure/extent.js';
|
|
29
|
+
export { boundsOf, boundsOfMarks, centreOf } from './figure/bounds.js';
|
|
30
|
+
export type { Bounds } from './figure/bounds.js';
|
|
29
31
|
export { areaUnder, plot, riemannBars, slopeOf, tangentAt } from './figure/plot.js';
|
|
30
32
|
export type { AreaOptions, BarsOptions, PlotOptions, TangentOptions } from './figure/plot.js';
|
|
31
33
|
export { axes, numberLine, numberPlane } from './figure/axis.js';
|
|
@@ -36,10 +38,11 @@ export { labelFor, tickStep, ticksOn } from './figure/ticks.js';
|
|
|
36
38
|
export type { Tick } from './figure/ticks.js';
|
|
37
39
|
export { flatten, group, shape, text } from './figure/node.js';
|
|
38
40
|
export type { GroupNode, Node, ShapeNode, Style, TextNode, TextOptions } from './figure/node.js';
|
|
39
|
-
export { fadeIn, fadeOut, fadeTo, draw, morph, moveBy } from './figure/animation.js';
|
|
40
|
-
export type { Animation } from './figure/animation.js';
|
|
41
|
+
export { circumscribe, fadeIn, fadeOut, fadeTo, draw, flash, growFrom, indicate, morph, moveAlong, moveBy, rotate, scale } from './figure/animation.js';
|
|
42
|
+
export type { AboutOptions, Animation, CircumscribeOptions, FlashOptions, IndicateOptions, ScaleOptions } from './figure/animation.js';
|
|
41
43
|
export { Timeline } from './figure/timeline.js';
|
|
42
|
-
export type { PlayOptions, Span } from './figure/timeline.js';
|
|
44
|
+
export type { PlayOptions, Span, StaggerOptions } from './figure/timeline.js';
|
|
45
|
+
export { lengthOf, pointAlong } from './figure/length.js';
|
|
43
46
|
export { trimPath } from './figure/trim.js';
|
|
44
47
|
export { alignPaths, lerpPath } from './figure/morph.js';
|
|
45
48
|
export { at, durationOf, loops, sameMarks } from './figure/figure.js';
|
package/dist/index.js
CHANGED
|
@@ -17,13 +17,15 @@ export { SAME_TIME, keyAt, sampleTrack, sampleTracks, withKey, withoutKey } from
|
|
|
17
17
|
export { arc, circle, line, polygon, polyline, pointCount, pointOn, rect, straight, transformPath } from './figure/path.js';
|
|
18
18
|
export { pathFromData } from './figure/path-data.js';
|
|
19
19
|
export { byAspect, fractionOf, matchingAspect, resolveExtent, viewMatrix } from './figure/extent.js';
|
|
20
|
+
export { boundsOf, boundsOfMarks, centreOf } from './figure/bounds.js';
|
|
20
21
|
export { areaUnder, plot, riemannBars, slopeOf, tangentAt } from './figure/plot.js';
|
|
21
22
|
export { axes, numberLine, numberPlane } from './figure/axis.js';
|
|
22
23
|
export { coordsOf, pointOf, scaleOf, scaled, unscaled } from './figure/scale.js';
|
|
23
24
|
export { labelFor, tickStep, ticksOn } from './figure/ticks.js';
|
|
24
25
|
export { flatten, group, shape, text } from './figure/node.js';
|
|
25
|
-
export { fadeIn, fadeOut, fadeTo, draw, morph, moveBy } from './figure/animation.js';
|
|
26
|
+
export { circumscribe, fadeIn, fadeOut, fadeTo, draw, flash, growFrom, indicate, morph, moveAlong, moveBy, rotate, scale } from './figure/animation.js';
|
|
26
27
|
export { Timeline } from './figure/timeline.js';
|
|
28
|
+
export { lengthOf, pointAlong } from './figure/length.js';
|
|
27
29
|
export { trimPath } from './figure/trim.js';
|
|
28
30
|
export { alignPaths, lerpPath } from './figure/morph.js';
|
|
29
31
|
export { at, durationOf, loops, sameMarks } from './figure/figure.js';
|
package/package.json
CHANGED