@altpsyche/maths 0.9.4 → 0.10.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 CHANGED
@@ -212,23 +212,95 @@ When the pieces kept will not join into a loop, the operation stops and says how
212
212
  ends of the run it had are. That happens when an input crosses itself, which these do not take. A
213
213
  shape drawn with a gap in it and nothing said about it is the one failure a caller cannot see.
214
214
 
215
+ ## A turn about a point
216
+
217
+ <img src="docs/rotate.svg" width="720" alt="Two panels side by side, each an L-shaped block turned part way round with a dot marking the point it turns about. In the left panel the dot sits at the middle of the block's own box. In the right it sits off to one side, so the block swings round it. A word rides with the block in both panels and stays upright.">
218
+
219
+ `rotate(target, angle)` turns the marks a name reaches, over a span of the timeline. The point it
220
+ turns about is the middle of the box round those marks unless a figure names one, and it is read off
221
+ them as they arrive rather than after the turn has moved them. The box round a turned shape is not the
222
+ turned box, so reading it back afterwards would let the pivot drift and the turn would stop being a
223
+ turn.
224
+
225
+ The left panel takes that default and spins where it stands. The right panel is given a point off to
226
+ one side, so the same shape swings round it instead. The furthest corner of the left shape stays 1.00
227
+ figure units from its pivot at every time and the right one's stays 2.34, which is what makes the two
228
+ read as different motions rather than as the same one twice.
229
+
230
+ A word rides with the shape in both panels and stays upright the whole way round. A mark carries no
231
+ rotation of its own, so turning the words would be work in both painters for a label that is easier to
232
+ read left as it is, which is the same reason a number line takes a direction rather than being turned
233
+ on its side.
234
+
235
+ A turn does not thicken a line. A stroke's width is multiplied by how much the transform stretches a
236
+ length, and a rotation stretches nothing, where `scale` stretches by the factor it grew by.
237
+
238
+ <img src="docs/rotate-strip.svg" width="820" alt="Four frames in two rows, each showing both panels, at nothing, a quarter, a half and three quarters of the way round.">
239
+
240
+ The quarters of the turn. The whole turn is left off the strip because it draws the picture that
241
+ nothing draws: this is the first figure here to declare itself a loop, and `loops(figure)` is the gate
242
+ behind that flag, comparing the marks at the duration against the marks at zero.
243
+
244
+ ## A surface in space
245
+
246
+ <img src="docs/surface.svg" width="720" alt="A saddle-shaped surface drawn as a grid of shaded cells, with a flat pane cutting through it at one height and the two branches of the curve where they meet drawn in orange along the surface. Three axes with their numbers stand behind it and the equation of the surface is typeset in the top left.">
247
+
248
+ A figure's camera is a value the caller holds. `camera3({ eye, target, up, projection })` answers
249
+ where a point in space lands in the figure's own units, how far off it is along the way the camera
250
+ looks, and whether it is in front of the eye at all. `polyline3`, `dot3`, `text3` and `surface3` take
251
+ points in space and hand back the same flat nodes everything else here draws, so `fadeIn` and `draw`
252
+ reach a mark in space with no change to either of them. Nothing in the marks, the tree, the flattening
253
+ or the two painters knows that space exists.
254
+
255
+ `space(name, items, camera)` puts the pieces in the order they are painted, near over far. That is
256
+ the painter's algorithm, and what it cannot do is worth knowing before it is used: two pieces that
257
+ pass through each other have no one order at all. The answer for those is smaller pieces, which is why
258
+ `surfaceCells` cuts a surface into a grid and why the saddle and the pane above are sorted together
259
+ rather than one after the other.
260
+
261
+ `sectionOf` finds the curve where a plane cuts a surface, by marching squares over the grid the
262
+ surface is already drawn from. Every point it finds lies on the plane exactly, because signed distance
263
+ to a plane changes evenly along a straight line. What it does not lie on exactly is the surface: it
264
+ sits on the chord between two samples of it, 4.870e-4 of a unit off at the resolution the demo uses,
265
+ and halving the cell size quarters that.
266
+
267
+ The camera is driven by a track and never by an animation, which is the call the flat demo's walk
268
+ already made: a span's eased fraction and a track's value are unrelated numbers, and a camera on one
269
+ with a surface on the other would be two clocks free to disagree.
270
+
271
+ <img src="docs/surface-strip.svg" width="820" alt="Four frames in two rows, showing the same saddle and pane from four points around one orbit of the eye.">
272
+
273
+ The quarters of one orbit. The eye comes back to where it started, which the gate holds by comparing
274
+ the marks at the end of the entrance against the marks one orbit later, mark for mark by name.
275
+
276
+ ## A view that follows
277
+
278
+ `Extent` carries a `centre`, which is where the middle of the frame sits in figure units, and an
279
+ extent may be a function of the shape of the surface and of the time. `viewAt(figure, seconds, width,
280
+ height)` hands a painter its matrix at a time in one call, so a figure whose extent moves cannot be
281
+ asked for its extent at one time and its marks at another.
282
+
283
+ The flat demo's view follows its dot across. The dot stays within 1.2 figure units of the middle of
284
+ the frame, where before it crossed 2.76. `fractionOf` reads the centre off the extent it is handed, so
285
+ the reading and the typeset rule stay where they are on the surface while the grid slides under them:
286
+ they drift 1.14e-15 figure units over the whole walk.
287
+
215
288
  ## The animations
216
289
 
217
290
  `fadeIn`, `fadeOut`, `fadeTo`, `draw`, `morph`, `morphEquation`, `countTo`, `moveBy`, `rotate`,
218
291
  `scale`, `growFrom`, `moveAlong`, `indicate`, `flash` and `circumscribe`. A `Timeline` plays them in order, plays several `together`, or
219
292
  `stagger`s a row so its parts arrive one after another.
220
293
 
221
- A turn and a growth happen about a point the marks decide for themselves, which is the middle of the
222
- box round them. `boundsOf` is that box, worked out from where each piece of the curve turns back on
223
- itself rather than from the points the curve is written from.
294
+ `boundsOf` is the box a turn and a growth are worked about, found from where each piece of the curve
295
+ turns back on itself rather than from the points the curve is written from.
224
296
 
225
297
  `moveAlong` carries a mark along a path at one speed, measured by the path's length. Even steps in a
226
298
  curve's own parameter are uneven steps along the curve: a step covers more of it where the curve is
227
299
  moving fast, which on a quarter circle is a 6.9% difference between the longest step and the shortest
228
300
  and on the demo's own walk is 82%.
229
301
 
230
- Both pictures are written by `svgMarkup`, which needs no browser, so `npm run demos` regenerates
231
- them and a test compares the bytes against the committed files.
302
+ Every picture here is written by `svgMarkup`, which needs no browser, so `npm run demos` regenerates
303
+ all eight and a test compares the bytes against the committed files.
232
304
 
233
305
  ## What it is built on
234
306
 
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Three number lines in space, with a tick at each of their numbers and the
3
+ * numbers written beside them.
4
+ *
5
+ * A tick reaches along the next axis round, x along y, y along z and z along x,
6
+ * so every tick lies in a plane the axis is part of and no tick needs the camera
7
+ * to decide which way to point. A label is flat text at the projected tick,
8
+ * standing off it in the direction that leads away from the projected origin, so
9
+ * labels fall outside the picture at every pose rather than over it.
10
+ */
11
+ import { type Interval } from '../values/interval.js';
12
+ import { type GroupNode } from './node.js';
13
+ import type { Camera3 } from './camera.js';
14
+ import type { Fill, Stroke } from './mark.js';
15
+ export interface Axes3Options {
16
+ /** The run of each axis in world units, minus one to one unless named. */
17
+ x?: Interval;
18
+ y?: Interval;
19
+ z?: Interval;
20
+ /** The lines and their ticks. */
21
+ stroke: Stroke;
22
+ /** The labels. Nothing is written where this is missing. */
23
+ fill?: Fill;
24
+ /** How big the labels are, in figure units. Nothing is written where this is
25
+ * missing. */
26
+ size?: number;
27
+ /** About how many ticks are wanted on each axis. The step is a round number,
28
+ * so the count that comes back is near this rather than equal to it. */
29
+ ticks?: number;
30
+ /** How far a tick reaches across its axis in world units, half either side. */
31
+ tickLength?: number;
32
+ /** From the projected tick to the label's own anchor, in figure units. */
33
+ gap?: number;
34
+ family?: string;
35
+ weight?: number;
36
+ }
37
+ /**
38
+ * The three axes as a group, one child per axis, each holding its line under
39
+ * `line`, its ticks under `ticks` and its labels under `labels`.
40
+ *
41
+ * Each tick and each label is named after the number it shows rather than by its
42
+ * place in the list, so an animation naming a tick follows that number when the
43
+ * axes are rebuilt over a different range.
44
+ */
45
+ export declare function axes3(name: string, camera: Camera3, options: Axes3Options): GroupNode;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Three number lines in space, with a tick at each of their numbers and the
3
+ * numbers written beside them.
4
+ *
5
+ * A tick reaches along the next axis round, x along y, y along z and z along x,
6
+ * so every tick lies in a plane the axis is part of and no tick needs the camera
7
+ * to decide which way to point. A label is flat text at the projected tick,
8
+ * standing off it in the direction that leads away from the projected origin, so
9
+ * labels fall outside the picture at every pose rather than over it.
10
+ */
11
+ import { interval } from '../values/interval.js';
12
+ import { vec2 } from '../values/vec2.js';
13
+ import { vec3 } from '../values/vec3.js';
14
+ import { group } from './node.js';
15
+ import { ticksOn } from './ticks.js';
16
+ import { polyline3, text3 } from './space.js';
17
+ const ALONG = {
18
+ x: vec3(1, 0, 0),
19
+ y: vec3(0, 1, 0),
20
+ z: vec3(0, 0, 1),
21
+ };
22
+ const ACROSS = { x: ALONG.y, y: ALONG.z, z: ALONG.x };
23
+ /** Which way a label leans away from where the three lines cross, or straight
24
+ * down where the tick lands on that crossing and there is no direction to lean
25
+ * in. */
26
+ function leaning(from, to, gap) {
27
+ const away = vec2.sub(to, from);
28
+ if (vec2.magnitude(away) < 1e-12)
29
+ return vec2(0, -gap);
30
+ return vec2.scale(vec2.normalize(away), gap);
31
+ }
32
+ function oneAxis(which, bounds, camera, options) {
33
+ const along = ALONG[which];
34
+ const across = ACROSS[which];
35
+ const size = options.size ?? 0;
36
+ const tickLength = options.tickLength ?? options.stroke.width * 8;
37
+ const gap = options.gap ?? size * 0.35;
38
+ const { from: low, to: high } = interval.ordered(bounds);
39
+ const at = (value) => vec3.scale(along, value);
40
+ const marked = ticksOn(bounds, options.ticks);
41
+ const half = tickLength / 2;
42
+ const origin = camera.project(vec3.ZERO).at;
43
+ const parts = [polyline3('line', [at(low), at(high)], camera, { stroke: options.stroke })];
44
+ parts.push(group('ticks', marked.map((tick) => polyline3(tick.label, [vec3.sub(at(tick.value), vec3.scale(across, half)), vec3.add(at(tick.value), vec3.scale(across, half))], camera, { stroke: options.stroke }))));
45
+ if (options.fill && size > 0) {
46
+ // The three lines cross at the origin, so only one of them writes the number
47
+ // there and the other two would write it again in the same place.
48
+ const written = marked.filter((tick) => which === 'x' || tick.value !== 0);
49
+ parts.push(group('labels', written.map((tick) => text3(tick.label, at(tick.value), tick.label, size, camera, {
50
+ fill: options.fill,
51
+ family: options.family,
52
+ weight: options.weight,
53
+ align: 'middle',
54
+ baseline: 'middle',
55
+ offset: leaning(origin, camera.project(at(tick.value)).at, gap),
56
+ }))));
57
+ }
58
+ return group(which, parts);
59
+ }
60
+ /**
61
+ * The three axes as a group, one child per axis, each holding its line under
62
+ * `line`, its ticks under `ticks` and its labels under `labels`.
63
+ *
64
+ * Each tick and each label is named after the number it shows rather than by its
65
+ * place in the list, so an animation naming a tick follows that number when the
66
+ * axes are rebuilt over a different range.
67
+ */
68
+ export function axes3(name, camera, options) {
69
+ const span = interval(-1, 1);
70
+ return group(name, [
71
+ oneAxis('x', options.x ?? span, camera, options),
72
+ oneAxis('y', options.y ?? span, camera, options),
73
+ oneAxis('z', options.z ?? span, camera, options),
74
+ ]);
75
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * A figure's own camera: where an eye is, what it looks at, and how a point in
3
+ * space becomes a point in the figure's units.
4
+ *
5
+ * A camera is a value the caller holds, the way a `Scale` is, and nothing inside
6
+ * a figure owns one. That is what keeps the seam: `at(figure, seconds)` gives
7
+ * back marks measured in the figure's own units, so a point in space has to
8
+ * become a point in those units before it is a mark at all. A camera inside the
9
+ * flattening would make every animation say whether it acts on the shape in
10
+ * space or on the picture of it, which is a question `fadeIn` should never be
11
+ * asked.
12
+ *
13
+ * So a camera moves by being rebuilt from values a track samples, and never by an
14
+ * animation. A span's eased fraction and a track's value are unrelated numbers,
15
+ * and a camera on one with a surface on the other is two clocks free to disagree.
16
+ */
17
+ import { type Mat4 } from '../values/mat4.js';
18
+ import { type Vec3 } from '../values/vec3.js';
19
+ import type { Vec2 } from '../values/vec2.js';
20
+ /**
21
+ * How a point that the eye has already lined up becomes a point on the page.
22
+ *
23
+ * `place` takes a point in view space, where the eye is at the origin looking
24
+ * down the negative z axis, and gives its place in figure units measured from the
25
+ * middle of the frame. `near` is how close to the eye a point may come before it
26
+ * has no place on the page at all.
27
+ */
28
+ export type Projection = {
29
+ near: number;
30
+ place: (view: Vec3) => Vec2;
31
+ };
32
+ export type OrthographicChoice = {
33
+ /** How many figure units across the frame one world unit becomes. */
34
+ scale?: number;
35
+ };
36
+ /**
37
+ * An eye that sees everything at the size it is, however far off it is.
38
+ *
39
+ * A parallel projection is a multiplication rather than a divide, so there is no
40
+ * matrix here and no clip box: near and far planes are what a divide needs, and
41
+ * this has none. Nothing shrinks with distance, so a point behind the eye lands
42
+ * where the point in front of it that it lines up with lands, and the near plane
43
+ * is at negative infinity to say that nothing is ever cut away.
44
+ */
45
+ export declare function orthographic({ scale }?: OrthographicChoice): Projection;
46
+ export type PerspectiveChoice = {
47
+ /** The angle the frame covers up and down, in radians. */
48
+ fov?: number;
49
+ /** How tall the frame is in figure units, so handing this the extent's own
50
+ * height makes the picture fill the frame. */
51
+ height?: number;
52
+ near?: number;
53
+ far?: number;
54
+ };
55
+ /**
56
+ * An eye that sees things smaller the further off they are.
57
+ *
58
+ * The matrix is built at an aspect of one and only its x and y are read, so the
59
+ * same shrinking factor is used across and up and a circle facing the eye stays a
60
+ * circle at every shape of surface. How wide the frame is comes from the extent,
61
+ * and how deep a point is comes from view space, where it is a distance rather
62
+ * than the squeezed value a projection writes into z.
63
+ */
64
+ export declare function perspective({ fov, height, near, far, }?: PerspectiveChoice): Projection;
65
+ export type Camera3Choice = {
66
+ eye: Vec3;
67
+ target: Vec3;
68
+ up?: Vec3;
69
+ projection?: Projection;
70
+ };
71
+ /** Where a point in space landed, how far off it is, and whether the eye can see
72
+ * it at all. */
73
+ export type Projected = {
74
+ /** The place in the figure's own units, measured from the middle of the frame. */
75
+ at: Vec2;
76
+ /** How far the point is from the eye along the way the camera looks, which is
77
+ * what a depth sort orders by and not the straight-line distance to the eye. */
78
+ depth: number;
79
+ /** Whether the point is further off than the near plane. A point that is not is
80
+ * still given a place, and that place is meaningless. */
81
+ inFront: boolean;
82
+ };
83
+ export type Camera3 = {
84
+ eye: Vec3;
85
+ target: Vec3;
86
+ up: Vec3;
87
+ projection: Projection;
88
+ /** The matrix that lines the world up with the eye, kept so a caller that has
89
+ * many points to place does not rebuild it per point. */
90
+ view: Mat4;
91
+ project: (point: Vec3) => Projected;
92
+ };
93
+ /**
94
+ * An eye at `eye` looking at `target`, with `up` saying which way is up.
95
+ *
96
+ * An `up` lying along the line of sight has no sideways direction in it and gives
97
+ * a camera that places every point at the middle of the frame, which is a pose
98
+ * the caller has to avoid rather than one this can fix.
99
+ */
100
+ export declare function camera3({ eye, target, up, projection, }: Camera3Choice): Camera3;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * A figure's own camera: where an eye is, what it looks at, and how a point in
3
+ * space becomes a point in the figure's units.
4
+ *
5
+ * A camera is a value the caller holds, the way a `Scale` is, and nothing inside
6
+ * a figure owns one. That is what keeps the seam: `at(figure, seconds)` gives
7
+ * back marks measured in the figure's own units, so a point in space has to
8
+ * become a point in those units before it is a mark at all. A camera inside the
9
+ * flattening would make every animation say whether it acts on the shape in
10
+ * space or on the picture of it, which is a question `fadeIn` should never be
11
+ * asked.
12
+ *
13
+ * So a camera moves by being rebuilt from values a track samples, and never by an
14
+ * animation. A span's eased fraction and a track's value are unrelated numbers,
15
+ * and a camera on one with a surface on the other is two clocks free to disagree.
16
+ */
17
+ import { mat4 } from '../values/mat4.js';
18
+ import { vec3 } from '../values/vec3.js';
19
+ /**
20
+ * An eye that sees everything at the size it is, however far off it is.
21
+ *
22
+ * A parallel projection is a multiplication rather than a divide, so there is no
23
+ * matrix here and no clip box: near and far planes are what a divide needs, and
24
+ * this has none. Nothing shrinks with distance, so a point behind the eye lands
25
+ * where the point in front of it that it lines up with lands, and the near plane
26
+ * is at negative infinity to say that nothing is ever cut away.
27
+ */
28
+ export function orthographic({ scale = 1 } = {}) {
29
+ return {
30
+ near: -Infinity,
31
+ place: (view) => ({ x: view.x * scale, y: view.y * scale }),
32
+ };
33
+ }
34
+ /**
35
+ * An eye that sees things smaller the further off they are.
36
+ *
37
+ * The matrix is built at an aspect of one and only its x and y are read, so the
38
+ * same shrinking factor is used across and up and a circle facing the eye stays a
39
+ * circle at every shape of surface. How wide the frame is comes from the extent,
40
+ * and how deep a point is comes from view space, where it is a distance rather
41
+ * than the squeezed value a projection writes into z.
42
+ */
43
+ export function perspective({ fov = Math.PI / 4, height = 2, near = 0.01, far = 1000, } = {}) {
44
+ const matrix = mat4.perspective({ fov, aspect: 1, near, far });
45
+ return {
46
+ near,
47
+ place: (view) => {
48
+ const clip = mat4.transformPoint(matrix, view);
49
+ return { x: (clip.x * height) / 2, y: (clip.y * height) / 2 };
50
+ },
51
+ };
52
+ }
53
+ /**
54
+ * An eye at `eye` looking at `target`, with `up` saying which way is up.
55
+ *
56
+ * An `up` lying along the line of sight has no sideways direction in it and gives
57
+ * a camera that places every point at the middle of the frame, which is a pose
58
+ * the caller has to avoid rather than one this can fix.
59
+ */
60
+ export function camera3({ eye, target, up = vec3(0, 1, 0), projection = perspective(), }) {
61
+ const view = mat4.lookAt(eye, target, up);
62
+ return {
63
+ eye,
64
+ target,
65
+ up,
66
+ projection,
67
+ view,
68
+ project: (point) => {
69
+ const seen = mat4.transformPoint(view, point);
70
+ const depth = -seen.z;
71
+ return { at: projection.place(seen), depth, inFront: depth > projection.near };
72
+ },
73
+ };
74
+ }
@@ -10,14 +10,18 @@ import { type Vec2 } from '../values/vec2.js';
10
10
  export interface Extent {
11
11
  width: number;
12
12
  height: number;
13
+ /** Where the middle of the frame sits in the figure's own units, the origin
14
+ * unless named. A figure whose view follows something moves this rather than
15
+ * moving everything it draws. */
16
+ centre?: Vec2;
13
17
  }
14
18
  /** Whether the extent is held inside the surface, leaving margins where the
15
19
  * shapes differ, or fills it and runs off two edges. */
16
20
  export type Fit = 'contain' | 'cover';
17
21
  /** An extent that never changes, or one chosen from the shape of the surface it
18
- * is about to be drawn on. */
19
- export type ExtentChoice = Extent | ((aspect: number) => Extent);
20
- export declare function resolveExtent(choice: ExtentChoice, aspect: number): Extent;
22
+ * is about to be drawn on and the time it is drawn at. */
23
+ export type ExtentChoice = Extent | ((aspect: number, seconds: number) => Extent);
24
+ export declare function resolveExtent(choice: ExtentChoice, aspect: number, seconds?: number): Extent;
21
25
  /**
22
26
  * An extent per shape, for a figure whose composition does not survive being
23
27
  * reframed.
@@ -7,8 +7,8 @@
7
7
  */
8
8
  import { mat3 } from '../values/mat3.js';
9
9
  import { vec2 } from '../values/vec2.js';
10
- export function resolveExtent(choice, aspect) {
11
- return typeof choice === 'function' ? choice(aspect) : choice;
10
+ export function resolveExtent(choice, aspect, seconds = 0) {
11
+ return typeof choice === 'function' ? choice(aspect, seconds) : choice;
12
12
  }
13
13
  /**
14
14
  * An extent per shape, for a figure whose composition does not survive being
@@ -49,7 +49,8 @@ export function matchingAspect(height = 2) {
49
49
  * is drawing would need the shader's camera, and nothing can read one.
50
50
  */
51
51
  export function fractionOf(extent, across, up) {
52
- return vec2((across - 0.5) * extent.width, (up - 0.5) * extent.height);
52
+ const centre = extent.centre ?? vec2(0, 0);
53
+ return vec2(centre.x + (across - 0.5) * extent.width, centre.y + (up - 0.5) * extent.height);
53
54
  }
54
55
  /**
55
56
  * The one matrix taking figure units onto a surface, with the extent centred.
@@ -63,7 +64,9 @@ export function viewMatrix(extent, fit, surfaceWidth, surfaceHeight) {
63
64
  const byWidth = surfaceWidth / extent.width;
64
65
  const byHeight = surfaceHeight / extent.height;
65
66
  const scale = fit === 'cover' ? Math.max(byWidth, byHeight) : Math.min(byWidth, byHeight);
66
- const centre = mat3.translation(vec2(surfaceWidth / 2, surfaceHeight / 2));
67
+ const middle = mat3.translation(vec2(surfaceWidth / 2, surfaceHeight / 2));
67
68
  const flip = mat3.scaling(vec2(scale, -scale));
68
- return mat3.multiply(centre, flip);
69
+ const seen = extent.centre ?? vec2(0, 0);
70
+ const follow = mat3.translation(vec2(-seen.x, -seen.y));
71
+ return mat3.multiply(mat3.multiply(middle, flip), follow);
69
72
  }
@@ -12,7 +12,8 @@
12
12
  import { type TrackValue, type Tracks } from '../timing/track.js';
13
13
  import { type Node } from './node.js';
14
14
  import { Timeline } from './timeline.js';
15
- import type { ExtentChoice, Fit } from './extent.js';
15
+ import { type ExtentChoice, type Fit } from './extent.js';
16
+ import type { Mat3 } from '../values/mat3.js';
16
17
  import type { Mark } from './mark.js';
17
18
  /** The values a scene is rebuilt from, sampled out of the figure's tracks. This
18
19
  * is what lets geometry follow a number rather than only be moved about: a
@@ -44,6 +45,15 @@ export interface Figure {
44
45
  export declare function durationOf(figure: Figure): number;
45
46
  /** The marks a figure shows at a time. */
46
47
  export declare function at(figure: Figure, seconds: number): readonly Mark[];
48
+ /**
49
+ * The matrix a painter needs at a time, in one call.
50
+ *
51
+ * A figure whose extent is a function of the clock has to be asked for its
52
+ * extent at the same time its marks were asked for, and a consumer writing that
53
+ * as two calls has two chances to pass different times. What the painter is
54
+ * handed is the matrix, so the extent and the centring stay in here.
55
+ */
56
+ export declare function viewAt(figure: Figure, seconds: number, width: number, height: number): Mat3;
47
57
  /** Whether a figure declaring itself a loop actually is one, which is the gate
48
58
  * behind that flag. The comparison is by tolerance rather than exactly, because
49
59
  * the sine and cosine a figure is built from are not specified to the last bit
@@ -11,6 +11,7 @@
11
11
  */
12
12
  import { sampleTracks } from '../timing/track.js';
13
13
  import { flatten } from './node.js';
14
+ import { resolveExtent, viewMatrix } from './extent.js';
14
15
  export function durationOf(figure) {
15
16
  return figure.duration ?? figure.timeline?.duration ?? 0;
16
17
  }
@@ -21,6 +22,18 @@ export function at(figure, seconds) {
21
22
  const marks = flatten(tree);
22
23
  return figure.timeline ? figure.timeline.at(marks, seconds) : marks;
23
24
  }
25
+ /**
26
+ * The matrix a painter needs at a time, in one call.
27
+ *
28
+ * A figure whose extent is a function of the clock has to be asked for its
29
+ * extent at the same time its marks were asked for, and a consumer writing that
30
+ * as two calls has two chances to pass different times. What the painter is
31
+ * handed is the matrix, so the extent and the centring stay in here.
32
+ */
33
+ export function viewAt(figure, seconds, width, height) {
34
+ const extent = resolveExtent(figure.extent, width / height, seconds);
35
+ return viewMatrix(extent, figure.fit ?? 'contain', width, height);
36
+ }
24
37
  /** Whether a figure declaring itself a loop actually is one, which is the gate
25
38
  * behind that flag. The comparison is by tolerance rather than exactly, because
26
39
  * the sine and cosine a figure is built from are not specified to the last bit
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The curve where a plane cuts a surface, found on the grid the surface is
3
+ * already drawn from.
4
+ *
5
+ * This is marching squares, with the plane's signed distance as the value at each
6
+ * grid point. A crossing point is found by walking along a cell edge to where
7
+ * that distance reaches nothing, and because signed distance to a plane changes
8
+ * evenly along a straight line, every point this finds lies on the plane exactly.
9
+ * It lies on the chord between two samples of the surface rather than on the
10
+ * surface itself, which is the whole of the error and is why halving the cell
11
+ * size quarters it.
12
+ */
13
+ import { type Interval } from '../values/interval.js';
14
+ import { type Vec3 } from '../values/vec3.js';
15
+ export interface Plane {
16
+ /** A point the plane passes through. */
17
+ point: Vec3;
18
+ /** Which way the plane faces. Its length does not matter. */
19
+ normal: Vec3;
20
+ }
21
+ export interface SectionOptions {
22
+ u?: Interval;
23
+ v?: Interval;
24
+ resolution?: number | {
25
+ u: number;
26
+ v: number;
27
+ };
28
+ /** How close two ends come before they are read as the same place. */
29
+ tolerance?: number;
30
+ }
31
+ /**
32
+ * The runs of points where a plane cuts a surface, in space.
33
+ *
34
+ * A run whose two ends meet comes back with its first point repeated at the end,
35
+ * so drawing the points as they are given draws the loop closed.
36
+ */
37
+ export declare function sectionOf(of: (u: number, v: number) => Vec3, plane: Plane, options?: SectionOptions): Vec3[][];