@altpsyche/maths 0.10.0 → 0.11.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 +38 -4
- package/dist/figure/annotate.d.ts +3 -0
- package/dist/figure/annotate.js +4 -1
- package/dist/figure/field.d.ts +64 -0
- package/dist/figure/field.js +69 -0
- package/dist/figure/space.d.ts +51 -1
- package/dist/figure/space.js +81 -1
- package/dist/figure/streamline.d.ts +49 -0
- package/dist/figure/streamline.js +95 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +3 -1
- package/package.json +1 -1
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
|
|
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
|
|
@@ -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. */
|
package/dist/figure/annotate.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/figure/space.d.ts
CHANGED
|
@@ -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;
|
package/dist/figure/space.js
CHANGED
|
@@ -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';
|
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';
|
package/package.json
CHANGED