@altpsyche/maths 0.10.0 → 0.12.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
@@ -20,7 +20,7 @@ svgMarkup(at(figure, 0.5), viewMatrix(figure.extent, 'contain', 640, 360), 640,
20
20
 
21
21
  ## Axes and a plotted function
22
22
 
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 under the typeset rule it comes from.">
23
+ <img src="docs/tangent.svg" width="720" alt="A parabola on a labelled grid over a field of small blue arrows, the region under it shaded to a point on the curve, the tangent at that point drawn, and the slope written as a number under the typeset rule it comes from.">
24
24
 
25
25
  The picture arrives rather than appearing. The grid fades, the axes draw on, their labels come in one
26
26
  after another, the curve draws, the dot grows out of the origin, and the dot is pointed at where the
@@ -60,7 +60,7 @@ region is the limit of at the left edge, the right edge or the middle of each on
60
60
  lays the tangent along the curve, cut where it leaves the graph. `slopeOf` reads the slope itself,
61
61
  which is what the number in the corner is.
62
62
 
63
- <img src="docs/tangent-strip.svg" width="960" alt="Four frames of the same figure side by side, the point walking up the curve, the shaded region growing behind it, the typeset rule in the corner changing from a slope of nothing to one that depends on x, and a brace measuring the rise in the last frame.">
63
+ <img src="docs/tangent-strip.svg" width="960" alt="Four frames of the same figure side by side, the point walking up the curve over the field of slope arrows, the shaded region growing behind it, the typeset rule in the corner changing from a slope of nothing to one that depends on x, and a brace measuring the rise in the last frame.">
64
64
 
65
65
  Four times of one figure, side by side: the picture arrived, the beat at the stationary point, half
66
66
  way up, and the top. A moving picture in a README needs a GIF and this package has no encoder, so the
@@ -243,7 +243,7 @@ behind that flag, comparing the marks at the duration against the marks at zero.
243
243
 
244
244
  ## A surface in space
245
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.">
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. Blue arrows across the pane show the way the saddle falls and three green runs of steepest descent are drawn on it. Three axes with their numbers stand behind it and the equation of the surface is typeset in the top left.">
247
247
 
248
248
  A figure's camera is a value the caller holds. `camera3({ eye, target, up, projection })` answers
249
249
  where a point in space lands in the figure's own units, how far off it is along the way the camera
@@ -268,11 +268,45 @@ The camera is driven by a track and never by an animation, which is the call the
268
268
  already made: a span's eased fraction and a track's value are unrelated numbers, and a camera on one
269
269
  with a surface on the other would be two clocks free to disagree.
270
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.">
271
+ <img src="docs/surface-strip.svg" width="820" alt="Four frames in two rows, showing the same saddle, pane, field arrows and runs of descent from four points around one orbit of the eye.">
272
272
 
273
273
  The quarters of one orbit. The eye comes back to where it started, which the gate holds by comparing
274
274
  the marks at the end of the entrance against the marks one orbit later, mark for mark by name.
275
275
 
276
+ ## Fields and streamlines
277
+
278
+ `vectorField(name, coords, of, options)` samples a grid over a graph and draws an arrow at each
279
+ sample. A field is a function from a place to a vector, so nothing here stores one. How long an arrow
280
+ is and what colour it is are both the author's, taken from the magnitude of the vector at that
281
+ sample: a field drawn at its true lengths is unreadable the moment two samples differ by a factor of
282
+ ten. The count is fixed by the resolution and never by the field, so a gate can hold it. An arrow's
283
+ length is in figure units, like the width of its shaft, and only its direction comes from the
284
+ mapping of its own vector. The flat demo's own axes count at 1.84 and 0.415 figure units to the graph
285
+ unit, and a length in graph units would draw a level arrow there 4.43 times longer than an upright one
286
+ beside it.
287
+
288
+ The arrows above are the slope field of the curve they sit under, read from the curve itself with
289
+ `slopeOf`. `streamlineOf(of, from, options)` walks Runge-Kutta 4 through a field and hands back the
290
+ points, and the run through the origin of that field never leaves the plotted parabola by more than
291
+ 4.689e-10 of a figure unit. Two answers to one question.
292
+
293
+ The step is a distance rather than a time: the field is read as a direction and its magnitude decides
294
+ nothing about how far a step moves, which keeps the points evenly spaced in a field whose strength
295
+ changes across the picture. It is fixed and never adaptive, because an adaptive step hands back a
296
+ different number of points as the field changes, and one path is walked into another by pairing their
297
+ points. Three rules stop a run and each has a measurement: a seed outside the region comes back as one
298
+ point, a field that is nothing everywhere stops at one point rather than at its cap, and a run leaving
299
+ its region stops at the last point inside it. In the field that turns a point about the origin,
300
+ halving the step divides the error along the curve by 15.1 and then 15.6, which is the fourth order
301
+ the integrator is named for.
302
+
303
+ `arrow3` and `fieldArrows3` do the same in space. An arrow there is measured in the world's own units
304
+ rather than the figure's, since a far arrow drawing shorter than a near one of the same magnitude is
305
+ what says which is far, and its head is a flat triangle at the projected tip so it stays readable
306
+ however steeply the arrow points away. The solid demo runs three streamlines of steepest descent down
307
+ its saddle: each is walked in the plane the surface is drawn over and lifted onto it, so every point
308
+ lies on the surface exactly and the height falls at every step.
309
+
276
310
  ## A view that follows
277
311
 
278
312
  `Extent` carries a `centre`, which is where the middle of the frame sits in figure units, and an
@@ -326,6 +360,31 @@ marks onto a two-dimensional canvas, which is what a recording needs, because an
326
360
  one surface. A test holds the two to emitting the same geometry and the same style for every
327
361
  mark.
328
362
 
363
+ ## Frames out
364
+
365
+ `framesOf(figure, options)` walks a figure at a fixed step and hands back a frame at a time. A frame
366
+ is its index, its time, its marks and the view those marks are painted through, read together at one
367
+ moment. A consumer that asks for the marks and the view in two calls has two chances to pass different
368
+ times, and a figure whose view moves then paints its marks through the matrix of some other moment:
369
+ the flat demo's view is carried 312 across its own walk, in the units a 1080 by 600 surface counts in.
370
+
371
+ Frames come back one at a time rather than as a list. Ten seconds at sixty frames a second is six
372
+ hundred frames of every mark a figure draws, and a recorder encodes a frame and throws it away.
373
+ `frameTimes` answers the times up front, since a recorder showing a reader how far along it is needs
374
+ the total before it has drawn anything.
375
+
376
+ The step is given as a rate or as a count, and the two are different questions. A recorder knows how
377
+ fast the frames play and needs a step of exactly one over that, or the encoded video drifts from the
378
+ figure's own clock. A strip knows how many pictures fit across a page and wants them spread over the
379
+ whole figure. A walk stops strictly before the duration either way: the frame at the duration of a
380
+ figure that loops is its own first frame, and a recording would show it twice. The rotation strip
381
+ above is a walk of four frames over a six second turn, and it draws the same bytes as the four times
382
+ that were written out by hand before it.
383
+
384
+ Nothing here writes a file. Every frame of both demos is painted through `paintCanvas` and written by
385
+ `svgMarkup` in the suite, which is the whole claim and needs no browser, and what a consumer does with
386
+ a painted frame is the consumer's own.
387
+
329
388
  ## The way in
330
389
 
331
390
  `pathFromData` reads an SVG `d` attribute as a path, which is the inverse of what the SVG painter
@@ -29,6 +29,9 @@ export interface ArrowOptions {
29
29
  *
30
30
  * The shaft stops where the head begins rather than running under it, because a
31
31
  * shaft drawn to the point shows through a head that is not fully opaque.
32
+ *
33
+ * The head is never longer than the arrow. A head longer than that puts its own
34
+ * base behind the tail, which draws the shaft pointing back the way it came.
32
35
  */
33
36
  export declare function arrow(name: string, from: Vec2, to: Vec2, options: ArrowOptions): GroupNode;
34
37
  /** A filled disc, which is what marks a place a line is pointing at. */
@@ -15,9 +15,12 @@ import { group, shape, text } from './node.js';
15
15
  *
16
16
  * The shaft stops where the head begins rather than running under it, because a
17
17
  * shaft drawn to the point shows through a head that is not fully opaque.
18
+ *
19
+ * The head is never longer than the arrow. A head longer than that puts its own
20
+ * base behind the tail, which draws the shaft pointing back the way it came.
18
21
  */
19
22
  export function arrow(name, from, to, options) {
20
- const head = options.head ?? options.stroke.width * 4;
23
+ const head = Math.min(options.head ?? options.stroke.width * 4, vec2.distance(from, to));
21
24
  const spread = options.spread ?? 0.6;
22
25
  const along = vec2.normalize(vec2.sub(to, from));
23
26
  const base = vec2.sub(to, vec2.scale(along, head));
@@ -0,0 +1,64 @@
1
+ /**
2
+ * A field of vectors sampled over a graph and drawn as one arrow per sample.
3
+ *
4
+ * Nothing here stores a field. A field is a function from a place to a vector,
5
+ * so what this adds is where it is read and how what it reads is drawn.
6
+ *
7
+ * How long an arrow is and what colour it is are both the author's, taken from
8
+ * the vector's own magnitude. A field drawn at its true lengths is unreadable
9
+ * the moment two samples differ by a factor of ten, and choosing the scale needs
10
+ * numbers about the picture this package does not have.
11
+ *
12
+ * The count is fixed by the resolution and never by the field, so a gate can
13
+ * hold it. The one sample that draws nothing is the one whose vector is nothing,
14
+ * and the count is what says where that happened.
15
+ */
16
+ import { type Interval } from '../values/interval.js';
17
+ import { type Vec2 } from '../values/vec2.js';
18
+ import { type Coords } from './scale.js';
19
+ import { type GroupNode } from './node.js';
20
+ import type { Colour } from './mark.js';
21
+ export interface VectorFieldOptions {
22
+ /** How long an arrow is, in figure units, from the magnitude of the vector at
23
+ * its own sample. */
24
+ lengthOf: (magnitude: number) => number;
25
+ /** What colour an arrow is, from that same magnitude. */
26
+ colourFor: (magnitude: number) => Colour;
27
+ /** How wide a shaft is, in figure units, like any other stroke here. */
28
+ width: number;
29
+ /** How many samples across and up. One number is both. */
30
+ resolution?: number | {
31
+ x: number;
32
+ y: number;
33
+ };
34
+ /** The run of x sampled, which is the whole width of the graph where it is
35
+ * left out. */
36
+ overX?: Interval;
37
+ /** The run of y sampled, which is the whole height of the graph where it is
38
+ * left out. */
39
+ overY?: Interval;
40
+ /** How long a head is, in figure units. Four times the shaft's width unless
41
+ * named, which is what an arrow takes when nothing says. */
42
+ head?: number;
43
+ /** How wide a head is across its base, against its length. */
44
+ spread?: number;
45
+ }
46
+ /**
47
+ * The arrows of a field over a graph, one group per sample, named by its column
48
+ * and row so a stagger can reach them one at a time.
49
+ *
50
+ * A sample sits at the middle of its cell rather than on the grid line. An
51
+ * inclusive grid would put the arrows of the outer row and column half outside
52
+ * the graph, where a painter has to clip them.
53
+ *
54
+ * An arrow points where the mapping of its own vector points, so it lies along
55
+ * the curves drawn over the same coordinates. Taking the direction in graph
56
+ * units instead would tilt every arrow wherever the two axes count at different
57
+ * rates.
58
+ *
59
+ * Its length is in figure units, like the width of its shaft and the length of
60
+ * its head. A length in graph units under two axes counting at different rates
61
+ * would draw the arrows pointing one way several times shorter than the arrows
62
+ * pointing the other, at the same magnitude.
63
+ */
64
+ export declare function vectorField(name: string, coords: Coords, of: (at: Vec2) => Vec2, options: VectorFieldOptions): GroupNode;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * A field of vectors sampled over a graph and drawn as one arrow per sample.
3
+ *
4
+ * Nothing here stores a field. A field is a function from a place to a vector,
5
+ * so what this adds is where it is read and how what it reads is drawn.
6
+ *
7
+ * How long an arrow is and what colour it is are both the author's, taken from
8
+ * the vector's own magnitude. A field drawn at its true lengths is unreadable
9
+ * the moment two samples differ by a factor of ten, and choosing the scale needs
10
+ * numbers about the picture this package does not have.
11
+ *
12
+ * The count is fixed by the resolution and never by the field, so a gate can
13
+ * hold it. The one sample that draws nothing is the one whose vector is nothing,
14
+ * and the count is what says where that happened.
15
+ */
16
+ import { interval } from '../values/interval.js';
17
+ import { vec2 } from '../values/vec2.js';
18
+ import { pointOf } from './scale.js';
19
+ import { group } from './node.js';
20
+ import { arrow } from './annotate.js';
21
+ function stepsOf(resolution) {
22
+ return typeof resolution === 'number' ? { x: resolution, y: resolution } : resolution;
23
+ }
24
+ /**
25
+ * The arrows of a field over a graph, one group per sample, named by its column
26
+ * and row so a stagger can reach them one at a time.
27
+ *
28
+ * A sample sits at the middle of its cell rather than on the grid line. An
29
+ * inclusive grid would put the arrows of the outer row and column half outside
30
+ * the graph, where a painter has to clip them.
31
+ *
32
+ * An arrow points where the mapping of its own vector points, so it lies along
33
+ * the curves drawn over the same coordinates. Taking the direction in graph
34
+ * units instead would tilt every arrow wherever the two axes count at different
35
+ * rates.
36
+ *
37
+ * Its length is in figure units, like the width of its shaft and the length of
38
+ * its head. A length in graph units under two axes counting at different rates
39
+ * would draw the arrows pointing one way several times shorter than the arrows
40
+ * pointing the other, at the same magnitude.
41
+ */
42
+ export function vectorField(name, coords, of, options) {
43
+ const steps = stepsOf(options.resolution ?? 12);
44
+ const overX = interval.ordered(options.overX ?? coords.x.graph);
45
+ const overY = interval.ordered(options.overY ?? coords.y.graph);
46
+ const children = [];
47
+ for (let column = 0; column < steps.x; column += 1) {
48
+ for (let row = 0; row < steps.y; row += 1) {
49
+ const x = interval.at(overX, (column + 0.5) / steps.x);
50
+ const y = interval.at(overY, (row + 0.5) / steps.y);
51
+ const vector = of(vec2(x, y));
52
+ const magnitude = Math.hypot(vector.x, vector.y);
53
+ const length = options.lengthOf(magnitude);
54
+ if (!(magnitude > 0) || !Number.isFinite(length) || !(length > 0))
55
+ continue;
56
+ const from = pointOf(coords, x, y);
57
+ const along = vec2.sub(pointOf(coords, x + vector.x, y + vector.y), from);
58
+ if (!(vec2.magnitude(along) > 0))
59
+ continue;
60
+ const to = vec2.add(from, vec2.scale(vec2.normalize(along), length));
61
+ children.push(arrow(`${column}-${row}`, from, to, {
62
+ stroke: { colour: options.colourFor(magnitude), width: options.width },
63
+ head: options.head,
64
+ spread: options.spread,
65
+ }));
66
+ }
67
+ }
68
+ return group(name, children);
69
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * A figure walked at a fixed step, a frame at a time.
3
+ *
4
+ * A frame is the marks and the view read at one time, handed over together. A
5
+ * consumer that asked for them separately holds two calls it can pass different
6
+ * times, and a figure whose view moves then paints its marks through the matrix
7
+ * of some other moment.
8
+ *
9
+ * Frames come back one at a time rather than as a list. A ten second figure at
10
+ * sixty frames a second is six hundred frames of every mark it draws, and a
11
+ * recorder encodes a frame and throws it away.
12
+ */
13
+ import { type Figure } from './figure.js';
14
+ import type { Mark } from './mark.js';
15
+ import type { Mat3 } from '../values/mat3.js';
16
+ export interface Frame {
17
+ /** Its place in the walk, counting from nothing. */
18
+ index: number;
19
+ /** The time it was read at, in seconds. */
20
+ seconds: number;
21
+ marks: readonly Mark[];
22
+ /** The matrix a painter needs for these marks, built at this frame's own
23
+ * time. */
24
+ view: Mat3;
25
+ }
26
+ /**
27
+ * How the walk is stepped, as a rate or as a count.
28
+ *
29
+ * The two are different questions. A recorder knows how fast the frames play and
30
+ * needs a step of exactly one over that, or the encoded video drifts from the
31
+ * figure's own clock. A strip knows how many pictures fit across a page and wants
32
+ * them spread over the whole figure.
33
+ */
34
+ export type FrameStep = {
35
+ fps: number;
36
+ frames?: never;
37
+ } | {
38
+ frames: number;
39
+ fps?: never;
40
+ };
41
+ export type FramesOptions = FrameStep & {
42
+ /** The surface the view is built for, in whatever units a painter counts in. */
43
+ width: number;
44
+ height: number;
45
+ };
46
+ /**
47
+ * The times a walk reads, which a recorder needs before it has drawn anything to
48
+ * say how far along it is.
49
+ *
50
+ * A walk stops strictly before the duration. The frame at the duration of a
51
+ * figure that loops is its own first frame, and a recording would show it twice.
52
+ * A figure with no duration is one frame, since a picture that never moves still
53
+ * has a picture.
54
+ */
55
+ export declare function frameTimes(figure: Figure, step: FrameStep): number[];
56
+ /** A figure walked at a fixed step, a frame at a time. */
57
+ export declare function framesOf(figure: Figure, options: FramesOptions): Generator<Frame>;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * A figure walked at a fixed step, a frame at a time.
3
+ *
4
+ * A frame is the marks and the view read at one time, handed over together. A
5
+ * consumer that asked for them separately holds two calls it can pass different
6
+ * times, and a figure whose view moves then paints its marks through the matrix
7
+ * of some other moment.
8
+ *
9
+ * Frames come back one at a time rather than as a list. A ten second figure at
10
+ * sixty frames a second is six hundred frames of every mark it draws, and a
11
+ * recorder encodes a frame and throws it away.
12
+ */
13
+ import { at, durationOf, viewAt } from './figure.js';
14
+ /**
15
+ * The times a walk reads, which a recorder needs before it has drawn anything to
16
+ * say how far along it is.
17
+ *
18
+ * A walk stops strictly before the duration. The frame at the duration of a
19
+ * figure that loops is its own first frame, and a recording would show it twice.
20
+ * A figure with no duration is one frame, since a picture that never moves still
21
+ * has a picture.
22
+ */
23
+ export function frameTimes(figure, step) {
24
+ const duration = durationOf(figure);
25
+ const count = step.fps === undefined
26
+ ? Math.max(1, Math.round(step.frames))
27
+ : Math.max(1, Math.round(duration * step.fps));
28
+ const gap = step.fps === undefined ? duration / count : 1 / step.fps;
29
+ return Array.from({ length: count }, (_, index) => index * gap);
30
+ }
31
+ /** A figure walked at a fixed step, a frame at a time. */
32
+ export function* framesOf(figure, options) {
33
+ const times = frameTimes(figure, options);
34
+ for (let index = 0; index < times.length; index += 1) {
35
+ const seconds = times[index];
36
+ yield {
37
+ index,
38
+ seconds,
39
+ marks: at(figure, seconds),
40
+ view: viewAt(figure, seconds, options.width, options.height),
41
+ };
42
+ }
43
+ }
@@ -11,7 +11,8 @@ import { type Vec3 } from '../values/vec3.js';
11
11
  import type { Vec2 } from '../values/vec2.js';
12
12
  import { type GroupNode, type Node, type Style, type TextOptions } from './node.js';
13
13
  import { type Interval } from '../values/interval.js';
14
- import type { Fill, Stroke } from './mark.js';
14
+ import type { Colour, Fill, Stroke } from './mark.js';
15
+ import { type ArrowOptions } from './annotate.js';
15
16
  import type { Camera3 } from './camera.js';
16
17
  export type Polyline3Options = Style & {
17
18
  /** Whether the last point joins back to the first. A run that the near plane
@@ -53,6 +54,55 @@ export type SpaceItem = {
53
54
  * top between frames would flicker.
54
55
  */
55
56
  export declare function space(name: string, items: readonly SpaceItem[], camera: Camera3): GroupNode;
57
+ export type Arrow3Options = ArrowOptions;
58
+ /**
59
+ * A line between two points in space with a head at the far end.
60
+ *
61
+ * The head is a flat triangle at the projected tip rather than a shape in
62
+ * space, so it stays the size it was given however far off the arrow is and
63
+ * however steeply it points away. A head built in space turns edge on to the eye
64
+ * and disappears exactly where the arrow is hardest to read.
65
+ *
66
+ * An arrow whose far end is behind the eye is cut at the near plane and drawn
67
+ * with no head, since the place the head belongs is not on the page.
68
+ */
69
+ export declare function arrow3(name: string, from: Vec3, to: Vec3, camera: Camera3, options: Arrow3Options): GroupNode;
70
+ export type VectorField3Options = ArrowOptions & {
71
+ /** The box the samples are taken in, nothing to one each way unless named. */
72
+ over?: {
73
+ x?: Interval;
74
+ y?: Interval;
75
+ z?: Interval;
76
+ };
77
+ /** How many samples each way. One number is all three. */
78
+ resolution?: number | {
79
+ x: number;
80
+ y: number;
81
+ z: number;
82
+ };
83
+ /** How long an arrow is, in the world's own units, from the magnitude of the
84
+ * vector at its own sample. */
85
+ lengthOf: (magnitude: number) => number;
86
+ /** What colour an arrow is, from that same magnitude. */
87
+ colourFor: (magnitude: number) => Colour;
88
+ };
89
+ /**
90
+ * The arrows of a field sampled over a box in space, and the points each was
91
+ * drawn from, for a figure that sorts them among pieces of its own.
92
+ *
93
+ * An arrow is measured in the world's own units rather than the figure's, unlike
94
+ * the arrows of a flat field, because a length in space is what perspective is
95
+ * for: a far arrow drawing shorter than a near one of the same magnitude is what
96
+ * says which is far. Its head is still in figure units, since the head is drawn
97
+ * on the page.
98
+ *
99
+ * A sample sits at the middle of its cell and the count is fixed by the
100
+ * resolution, so a gate can hold it as the eye moves. A sample whose vector is
101
+ * nothing draws no arrow there.
102
+ */
103
+ export declare function fieldArrows3(name: string, of: (at: Vec3) => Vec3, camera: Camera3, options: VectorField3Options): SpaceItem[];
104
+ /** A field of vectors in space, drawn as arrows ordered back to front. */
105
+ export declare function vectorField3(name: string, of: (at: Vec3) => Vec3, camera: Camera3, options: VectorField3Options): GroupNode;
56
106
  export type Surface3Options = {
57
107
  /** The run of the first parameter, nothing to one unless named. */
58
108
  u?: Interval;
@@ -8,9 +8,10 @@
8
8
  * with no children, which flattens to no marks rather than to a mark of nothing.
9
9
  */
10
10
  import { vec3 } from '../values/vec3.js';
11
- import { circle, polygon, polyline } from './path.js';
11
+ import { circle, line, polygon, polyline } from './path.js';
12
12
  import { group, shape, text } from './node.js';
13
13
  import { interval } from '../values/interval.js';
14
+ import { arrow } from './annotate.js';
14
15
  /**
15
16
  * Where along a segment the near plane is crossed.
16
17
  *
@@ -108,6 +109,85 @@ export function space(name, items, camera) {
108
109
  measured.sort((a, b) => b.depth - a.depth);
109
110
  return group(name, measured.map((item) => item.node));
110
111
  }
112
+ /**
113
+ * A line between two points in space with a head at the far end.
114
+ *
115
+ * The head is a flat triangle at the projected tip rather than a shape in
116
+ * space, so it stays the size it was given however far off the arrow is and
117
+ * however steeply it points away. A head built in space turns edge on to the eye
118
+ * and disappears exactly where the arrow is hardest to read.
119
+ *
120
+ * An arrow whose far end is behind the eye is cut at the near plane and drawn
121
+ * with no head, since the place the head belongs is not on the page.
122
+ */
123
+ export function arrow3(name, from, to, camera, options) {
124
+ const start = camera.project(from);
125
+ const end = camera.project(to);
126
+ if (!start.inFront && !end.inFront)
127
+ return group(name, []);
128
+ const cut = () => {
129
+ const along = crossingAt(start.depth, end.depth, camera.projection.near);
130
+ return camera.project(vec3.lerp(from, to, along)).at;
131
+ };
132
+ if (!end.inFront)
133
+ return group(name, [shape('shaft', line(start.at, cut()), { stroke: options.stroke })]);
134
+ const tail = start.inFront ? start.at : cut();
135
+ if (tail.x === end.at.x && tail.y === end.at.y)
136
+ return group(name, []);
137
+ return arrow(name, tail, end.at, options);
138
+ }
139
+ function gridOf(resolution) {
140
+ return typeof resolution === 'number' ? { x: resolution, y: resolution, z: resolution } : resolution;
141
+ }
142
+ /**
143
+ * The arrows of a field sampled over a box in space, and the points each was
144
+ * drawn from, for a figure that sorts them among pieces of its own.
145
+ *
146
+ * An arrow is measured in the world's own units rather than the figure's, unlike
147
+ * the arrows of a flat field, because a length in space is what perspective is
148
+ * for: a far arrow drawing shorter than a near one of the same magnitude is what
149
+ * says which is far. Its head is still in figure units, since the head is drawn
150
+ * on the page.
151
+ *
152
+ * A sample sits at the middle of its cell and the count is fixed by the
153
+ * resolution, so a gate can hold it as the eye moves. A sample whose vector is
154
+ * nothing draws no arrow there.
155
+ */
156
+ export function fieldArrows3(name, of, camera, options) {
157
+ const { over = {}, resolution = 6, lengthOf, colourFor, ...rest } = options;
158
+ const box = {
159
+ x: interval.ordered(over.x ?? interval(0, 1)),
160
+ y: interval.ordered(over.y ?? interval(0, 1)),
161
+ z: interval.ordered(over.z ?? interval(0, 1)),
162
+ };
163
+ const steps = gridOf(resolution);
164
+ const items = [];
165
+ for (let i = 0; i < steps.x; i += 1) {
166
+ for (let j = 0; j < steps.y; j += 1) {
167
+ for (let k = 0; k < steps.z; k += 1) {
168
+ const from = vec3(interval.at(box.x, (i + 0.5) / steps.x), interval.at(box.y, (j + 0.5) / steps.y), interval.at(box.z, (k + 0.5) / steps.z));
169
+ const vector = of(from);
170
+ const magnitude = vec3.magnitude(vector);
171
+ const length = lengthOf(magnitude);
172
+ if (!(magnitude > 0) || !Number.isFinite(length) || !(length > 0))
173
+ continue;
174
+ const to = vec3.add(from, vec3.scale(vector, length / magnitude));
175
+ items.push({
176
+ points: [from, to],
177
+ node: arrow3(`${name}/${i}-${j}-${k}`, from, to, camera, {
178
+ ...rest,
179
+ stroke: { ...rest.stroke, colour: colourFor(magnitude) },
180
+ }),
181
+ });
182
+ }
183
+ }
184
+ }
185
+ return items;
186
+ }
187
+ /** A field of vectors in space, drawn as arrows ordered back to front. */
188
+ export function vectorField3(name, of, camera, options) {
189
+ return space(name, fieldArrows3('arrow', of, camera, options), camera);
190
+ }
111
191
  function resolutionOf(resolution) {
112
192
  return typeof resolution === 'number' ? { u: resolution, v: resolution } : resolution;
113
193
  }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The path a point follows through a field of vectors, walked by Runge-Kutta 4.
3
+ *
4
+ * The points come back in graph units and nothing here draws them, the way the
5
+ * curve where a plane cuts a surface comes back as points in space. What a
6
+ * figure does with them is its own.
7
+ *
8
+ * The step is a distance rather than a time, so the field is read as a
9
+ * direction and its magnitude decides nothing about how far the run moves. A
10
+ * step in the field's own time crowds the points where the field is weak and
11
+ * spreads them where it is strong, and a curve drawn from those is faceted
12
+ * exactly where it turns hardest.
13
+ *
14
+ * The step is fixed and never adaptive. An adaptive step hands back a different
15
+ * number of points as the field changes, which is a count no gate can hold and
16
+ * a path no morph can pair up against another.
17
+ */
18
+ import { type Interval } from '../values/interval.js';
19
+ import { type Vec2 } from '../values/vec2.js';
20
+ export interface StreamlineOptions {
21
+ /** How far each step moves, in graph units. */
22
+ step: number;
23
+ /** How many steps the run takes at most, in each direction it is run. */
24
+ steps?: number;
25
+ /** The region the run is held inside. It has no edges where this is left out,
26
+ * and only the step cap and a vanishing field stop it. */
27
+ within?: {
28
+ x: Interval;
29
+ y: Interval;
30
+ };
31
+ /** Which way the run goes from its seed. Both puts the backward half first,
32
+ * so the points read from one end of the curve to the other. */
33
+ direction?: 'forward' | 'backward' | 'both';
34
+ /** The magnitude below which the field is taken to have vanished, in graph
35
+ * units. */
36
+ least?: number;
37
+ }
38
+ /**
39
+ * The streamline of a field through a seed point, in graph units.
40
+ *
41
+ * The run stops on one of three rules: it leaves the region, it reaches its step
42
+ * cap, or the field where it stands is too small to point anywhere. A seed
43
+ * outside the region comes back as that seed alone, which is a curve with
44
+ * nothing to draw rather than a run that starts by escaping.
45
+ *
46
+ * A run that leaves the region stops at the last point inside it and is not cut
47
+ * at the edge, so it ends within one step of the boundary.
48
+ */
49
+ export declare function streamlineOf(of: (at: Vec2) => Vec2, from: Vec2, options: StreamlineOptions): Vec2[];
@@ -0,0 +1,95 @@
1
+ /**
2
+ * The path a point follows through a field of vectors, walked by Runge-Kutta 4.
3
+ *
4
+ * The points come back in graph units and nothing here draws them, the way the
5
+ * curve where a plane cuts a surface comes back as points in space. What a
6
+ * figure does with them is its own.
7
+ *
8
+ * The step is a distance rather than a time, so the field is read as a
9
+ * direction and its magnitude decides nothing about how far the run moves. A
10
+ * step in the field's own time crowds the points where the field is weak and
11
+ * spreads them where it is strong, and a curve drawn from those is faceted
12
+ * exactly where it turns hardest.
13
+ *
14
+ * The step is fixed and never adaptive. An adaptive step hands back a different
15
+ * number of points as the field changes, which is a count no gate can hold and
16
+ * a path no morph can pair up against another.
17
+ */
18
+ import { interval } from '../values/interval.js';
19
+ import { vec2 } from '../values/vec2.js';
20
+ const STEPS = 200;
21
+ const LEAST = 1e-9;
22
+ function holds(within, at) {
23
+ if (!Number.isFinite(at.x) || !Number.isFinite(at.y))
24
+ return false;
25
+ if (!within)
26
+ return true;
27
+ return interval.holds(interval.ordered(within.x), at.x) && interval.holds(interval.ordered(within.y), at.y);
28
+ }
29
+ /**
30
+ * One run from the seed, as the points after it, with the seed left out so the
31
+ * two directions can be joined without repeating it.
32
+ *
33
+ * A stage whose field has vanished stops the run there rather than being taken
34
+ * as a direction, since dividing by a magnitude near nothing turns rounding
35
+ * error into a direction of its own.
36
+ */
37
+ function run(of, from, step, steps, within, least) {
38
+ const direction = (at) => {
39
+ const vector = of(at);
40
+ const magnitude = Math.hypot(vector.x, vector.y);
41
+ if (!Number.isFinite(magnitude) || magnitude <= least)
42
+ return undefined;
43
+ return vec2(vector.x / magnitude, vector.y / magnitude);
44
+ };
45
+ const points = [];
46
+ let at = from;
47
+ for (let taken = 0; taken < steps; taken += 1) {
48
+ const first = direction(at);
49
+ if (!first)
50
+ break;
51
+ const second = direction(vec2.add(at, vec2.scale(first, step / 2)));
52
+ if (!second)
53
+ break;
54
+ const third = direction(vec2.add(at, vec2.scale(second, step / 2)));
55
+ if (!third)
56
+ break;
57
+ const fourth = direction(vec2.add(at, vec2.scale(third, step)));
58
+ if (!fourth)
59
+ break;
60
+ const along = vec2((first.x + 2 * second.x + 2 * third.x + fourth.x) / 6, (first.y + 2 * second.y + 2 * third.y + fourth.y) / 6);
61
+ const next = vec2.add(at, vec2.scale(along, step));
62
+ if (!holds(within, next))
63
+ break;
64
+ points.push(next);
65
+ at = next;
66
+ }
67
+ return points;
68
+ }
69
+ /**
70
+ * The streamline of a field through a seed point, in graph units.
71
+ *
72
+ * The run stops on one of three rules: it leaves the region, it reaches its step
73
+ * cap, or the field where it stands is too small to point anywhere. A seed
74
+ * outside the region comes back as that seed alone, which is a curve with
75
+ * nothing to draw rather than a run that starts by escaping.
76
+ *
77
+ * A run that leaves the region stops at the last point inside it and is not cut
78
+ * at the edge, so it ends within one step of the boundary.
79
+ */
80
+ export function streamlineOf(of, from, options) {
81
+ const steps = Math.max(0, Math.round(options.steps ?? STEPS));
82
+ const least = options.least ?? LEAST;
83
+ const direction = options.direction ?? 'forward';
84
+ if (!holds(options.within, from))
85
+ return [from];
86
+ const forward = direction === 'backward' ? [] : run(of, from, options.step, steps, options.within, least);
87
+ if (direction === 'forward')
88
+ return [from, ...forward];
89
+ const back = (at) => {
90
+ const vector = of(at);
91
+ return vec2(-vector.x, -vector.y);
92
+ };
93
+ const backward = run(back, from, options.step, steps, options.within, least);
94
+ return [...backward.reverse(), from, ...forward];
95
+ }
package/dist/index.d.ts CHANGED
@@ -42,12 +42,16 @@ export { boundsOf, boundsOfMarks, centreOf } from './figure/bounds.js';
42
42
  export type { Bounds } from './figure/bounds.js';
43
43
  export { areaUnder, plot, riemannBars, slopeOf, tangentAt } from './figure/plot.js';
44
44
  export type { AreaOptions, BarsOptions, PlotOptions, TangentOptions } from './figure/plot.js';
45
+ export { vectorField } from './figure/field.js';
46
+ export { streamlineOf } from './figure/streamline.js';
47
+ export type { StreamlineOptions } from './figure/streamline.js';
48
+ export type { VectorFieldOptions } from './figure/field.js';
45
49
  export { axes, numberLine, numberPlane } from './figure/axis.js';
46
50
  export type { AxesOptions, NumberLineOptions, NumberPlaneOptions } from './figure/axis.js';
47
51
  export { camera3, orthographic, perspective } from './figure/camera.js';
48
52
  export type { Camera3, Camera3Choice, OrthographicChoice, PerspectiveChoice, Projected, Projection } from './figure/camera.js';
49
- export { dot3, polyline3, space, surface3, surfaceCells, text3 } from './figure/space.js';
50
- export type { Polyline3Options, SpaceItem, Surface3Options, Text3Options } from './figure/space.js';
53
+ export { arrow3, dot3, fieldArrows3, polyline3, space, surface3, surfaceCells, text3, vectorField3 } from './figure/space.js';
54
+ export type { Arrow3Options, Polyline3Options, SpaceItem, Surface3Options, Text3Options, VectorField3Options } from './figure/space.js';
51
55
  export { axes3 } from './figure/axis3.js';
52
56
  export type { Axes3Options } from './figure/axis3.js';
53
57
  export { sectionOf } from './figure/section.js';
@@ -66,6 +70,8 @@ export { lengthOf, pointAlong } from './figure/length.js';
66
70
  export { trimPath } from './figure/trim.js';
67
71
  export { alignPaths, lerpPath } from './figure/morph.js';
68
72
  export { at, durationOf, loops, sameMarks, viewAt } from './figure/figure.js';
73
+ export { frameTimes, framesOf } from './figure/frames.js';
74
+ export type { Frame, FrameStep, FramesOptions } from './figure/frames.js';
69
75
  export type { Figure, Values } from './figure/figure.js';
70
76
  export { pathData, paintSvg, svgElements, svgMarkup } from './paint/svg.js';
71
77
  export type { ElementMaker, PaintNode, PaintTarget, SvgElement } from './paint/svg.js';
package/dist/index.js CHANGED
@@ -26,9 +26,11 @@ export { curveCrossings } from './figure/intersect.js';
26
26
  export { byAspect, fractionOf, matchingAspect, resolveExtent, viewMatrix } from './figure/extent.js';
27
27
  export { boundsOf, boundsOfMarks, centreOf } from './figure/bounds.js';
28
28
  export { areaUnder, plot, riemannBars, slopeOf, tangentAt } from './figure/plot.js';
29
+ export { vectorField } from './figure/field.js';
30
+ export { streamlineOf } from './figure/streamline.js';
29
31
  export { axes, numberLine, numberPlane } from './figure/axis.js';
30
32
  export { camera3, orthographic, perspective } from './figure/camera.js';
31
- export { dot3, polyline3, space, surface3, surfaceCells, text3 } from './figure/space.js';
33
+ export { arrow3, dot3, fieldArrows3, polyline3, space, surface3, surfaceCells, text3, vectorField3 } from './figure/space.js';
32
34
  export { axes3 } from './figure/axis3.js';
33
35
  export { sectionOf } from './figure/section.js';
34
36
  export { coordsOf, pointOf, scaleOf, scaled, unscaled } from './figure/scale.js';
@@ -40,6 +42,7 @@ export { lengthOf, pointAlong } from './figure/length.js';
40
42
  export { trimPath } from './figure/trim.js';
41
43
  export { alignPaths, lerpPath } from './figure/morph.js';
42
44
  export { at, durationOf, loops, sameMarks, viewAt } from './figure/figure.js';
45
+ export { frameTimes, framesOf } from './figure/frames.js';
43
46
  export { pathData, paintSvg, svgElements, svgMarkup } from './paint/svg.js';
44
47
  export { paintCanvas } from './paint/canvas.js';
45
48
  export { arrow, brace, bracePath, callout, dot } from './figure/annotate.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@altpsyche/maths",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "The mathematics AltPsyche's figures are drawn from: vectors, matrices, curves, and a value walked over time.",
5
5
  "license": "MIT",
6
6
  "author": "Siva",