@altpsyche/maths 0.11.0 → 0.13.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 +124 -326
- package/dist/figure/animation.d.ts +6 -4
- package/dist/figure/animation.js +20 -4
- package/dist/figure/annotate.d.ts +3 -2
- package/dist/figure/annotate.js +3 -2
- package/dist/figure/axis.js +5 -8
- package/dist/figure/axis3.d.ts +8 -0
- package/dist/figure/axis3.js +13 -0
- package/dist/figure/boolean.js +2 -2
- package/dist/figure/equation.d.ts +1 -1
- package/dist/figure/equation.js +2 -2
- package/dist/figure/field.d.ts +6 -6
- package/dist/figure/field.js +5 -6
- package/dist/figure/field3.d.ts +50 -0
- package/dist/figure/field3.js +60 -0
- package/dist/figure/figure.d.ts +4 -4
- package/dist/figure/figure.js +4 -4
- package/dist/figure/frames.d.ts +57 -0
- package/dist/figure/frames.js +43 -0
- package/dist/figure/grid.d.ts +19 -0
- package/dist/figure/grid.js +32 -0
- package/dist/figure/inside.d.ts +2 -2
- package/dist/figure/inside.js +3 -4
- package/dist/figure/length.d.ts +1 -3
- package/dist/figure/length.js +7 -10
- package/dist/figure/mark.d.ts +7 -2
- package/dist/figure/node.js +2 -3
- package/dist/figure/path-data.js +2 -3
- package/dist/figure/path.d.ts +1 -1
- package/dist/figure/path.js +3 -3
- package/dist/figure/plot.d.ts +1 -1
- package/dist/figure/plot.js +9 -13
- package/dist/figure/scale.d.ts +2 -2
- package/dist/figure/scale.js +3 -3
- package/dist/figure/section.d.ts +5 -2
- package/dist/figure/section.js +9 -16
- package/dist/figure/space.d.ts +8 -91
- package/dist/figure/space.js +7 -110
- package/dist/figure/surface3.d.ts +62 -0
- package/dist/figure/surface3.js +58 -0
- package/dist/index.d.ts +19 -11
- package/dist/index.js +10 -6
- package/dist/paint/number.js +1 -2
- package/dist/paint/svg.d.ts +29 -4
- package/dist/paint/svg.js +49 -14
- package/dist/values/colour.d.ts +43 -0
- package/dist/values/colour.js +110 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,377 +1,175 @@
|
|
|
1
1
|
# @altpsyche/maths
|
|
2
2
|
|
|
3
|
-
The mathematics the figures on [altpsyche.dev](https://altpsyche.dev)
|
|
3
|
+
The mathematics behind the figures on [altpsyche.dev](https://altpsyche.dev).
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
flat list of **marks**, and a **painter** turns marks into something a reader can see.
|
|
5
|
+
## The model
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const figure = {
|
|
12
|
-
extent: { width: 16, height: 9 },
|
|
13
|
-
still: 1,
|
|
14
|
-
scene: group('fig', [shape('ring', circle(vec2(0, 0), 3), { stroke: { colour: '#fff', width: 0.05 } })]),
|
|
15
|
-
timeline: Timeline.empty().play(draw('fig/ring'), 1),
|
|
16
|
-
};
|
|
7
|
+
A **figure** is a description of a picture over time. It carries an **extent** and a **scene**. The
|
|
8
|
+
extent is a width and a height in the figure's own units. The scene is a tree of nodes holding
|
|
9
|
+
paths, text, transforms and styles. A figure has no canvas, no clock and no state.
|
|
17
10
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
25
|
-
The picture arrives rather than appearing. The grid fades, the axes draw on, their labels come in one
|
|
26
|
-
after another, the curve draws, the dot grows out of the origin, and the dot is pointed at where the
|
|
27
|
-
slope is nothing. Then it walks the curve at one speed and flashes at the top, and a brace measures
|
|
28
|
-
how far it climbed with its number counting up to the rise. The number in the corner is a value of
|
|
29
|
-
the typeset rule under it, and that rule reads no slope while the dot is held at the stationary point
|
|
30
|
-
and walks into the one that depends on x as the dot leaves.
|
|
11
|
+
Evaluating a figure at time t yields a flat array of **marks**. A mark is one drawn item: an outline
|
|
12
|
+
with a fill, a stroke, or both, or a piece of text. Its geometry is expressed in figure units with
|
|
13
|
+
every transform already applied, and its style is resolved rather than inherited. A mark holds no
|
|
14
|
+
reference to an output device.
|
|
31
15
|
|
|
32
16
|
```ts
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const coords = coordsOf(
|
|
36
|
-
scaleOf(interval(-1, 4), interval(-4.6, 4.6)),
|
|
37
|
-
scaleOf(interval(-1, 9), interval(-2.4, 2.4))
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
group('graph', [
|
|
41
|
-
numberPlane('grid', coords, { stroke: faint, minors: 4 }),
|
|
42
|
-
axes('axes', coords, { stroke: pen, fill: ink, size: 0.26, tip: 0.18 }),
|
|
43
|
-
shape('curve', plot(coords, (x) => x * x), { stroke: drawn }),
|
|
44
|
-
]);
|
|
17
|
+
marksAt(figure, t): readonly Mark[]
|
|
45
18
|
```
|
|
46
19
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
20
|
+
`marksAt` is a pure function of t. Two evaluations at one time produce identical arrays. A page
|
|
21
|
+
playing forward, a reader dragging a scrub bar backward and a recorder stepping at a fixed rate
|
|
22
|
+
therefore read one figure.
|
|
23
|
+
|
|
24
|
+
A **painter** consumes marks. `viewMatrix` builds the single affine transform taking figure units to
|
|
25
|
+
a surface of a given size. It inverts the y axis, since a figure counts upward and both painters
|
|
26
|
+
count downward from the top. No conversion to device units occurs anywhere else.
|
|
51
27
|
|
|
52
|
-
|
|
53
|
-
the slope the function has there. It cuts the curve where the curve leaves the graph, so a pole
|
|
54
|
-
breaks in two instead of drawing a line up the picture, and the cut end sits on the edge rather
|
|
55
|
-
than a sample short of it. The curve above is cut at x = 3, where the parabola meets the 9 its y
|
|
56
|
-
axis stops at.
|
|
28
|
+
<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.">
|
|
57
29
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
lays the tangent along the curve, cut where it leaves the graph. `slopeOf` reads the slope itself,
|
|
61
|
-
which is what the number in the corner is.
|
|
30
|
+
The figure above is evaluated at t = 7.86 s of a 10.25 s duration. Its extent is 10.8 by 6 units and
|
|
31
|
+
its marks number 202 at every time in the run.
|
|
62
32
|
|
|
63
|
-
|
|
33
|
+
## Install
|
|
64
34
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
35
|
+
```sh
|
|
36
|
+
npm install @altpsyche/maths
|
|
37
|
+
```
|
|
68
38
|
|
|
69
|
-
|
|
39
|
+
Node 20 or newer, ESM, `sideEffects: false`. The single runtime dependency is MathJax, reached by a
|
|
40
|
+
dynamic import inside `typesetElement`, so a figure containing no equations never loads its 41 MB.
|
|
70
41
|
|
|
71
|
-
|
|
72
|
-
marks, one per glyph. `equationNode` places those marks in a figure, fitted inside a box and centred
|
|
73
|
-
on a point. It fits the width as well as the height, because an expression six times wider than it is
|
|
74
|
-
tall runs off the sides of a figure the moment the height alone decides its size.
|
|
42
|
+
## A complete figure
|
|
75
43
|
|
|
76
44
|
```ts
|
|
77
|
-
import {
|
|
45
|
+
import { Timeline, circle, draw, group, marksAt, shape, svgMarkup, vec2, viewAt } from '@altpsyche/maths';
|
|
78
46
|
|
|
79
|
-
const
|
|
47
|
+
const figure = {
|
|
48
|
+
extent: { width: 16, height: 9 },
|
|
49
|
+
still: 1,
|
|
50
|
+
scene: group('fig', [shape('ring', circle(vec2(0, 0), 3), { stroke: { colour: '#fff', width: 0.05 } })]),
|
|
51
|
+
timeline: Timeline.empty().play(draw('fig/ring'), 1),
|
|
52
|
+
};
|
|
80
53
|
|
|
81
|
-
|
|
82
|
-
at: vec2(-4.27, 1.74),
|
|
83
|
-
width: 1.2,
|
|
84
|
-
height: 0.6,
|
|
85
|
-
fill: { colour: '#1b1b1b' },
|
|
86
|
-
});
|
|
54
|
+
svgMarkup(marksAt(figure, 0.5), viewAt(figure, 0.5, 640, 360), 640, 360);
|
|
87
55
|
```
|
|
88
56
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
MathJax is the one runtime dependency and the typesetting call is what loads it. Importing the door
|
|
94
|
-
reaches none of it, so a consumer who draws figures and typesets nothing pays nothing. What that
|
|
95
|
-
costs is that typesetting answers with a promise.
|
|
96
|
-
|
|
97
|
-
`matchGlyphs` says which glyph of one expression is which glyph of the other, and `morphEquation`
|
|
98
|
-
walks one into the next: the shared sub-expressions stay put and only the difference moves. Two marks
|
|
99
|
-
match on the part of the leaf name after the first dash, which the typesetter's own naming gives, so
|
|
100
|
-
a glyph is `3-1D465` and a fraction bar is `4-rule`. The pairing is the longest common subsequence of
|
|
101
|
-
the two token sequences, which pairs each occurrence of a repeated glyph once and refuses a pair that
|
|
102
|
-
would cross another pair on the way over.
|
|
57
|
+
At t = 0.5 the timeline has applied `draw` at half its span, and the mark carries half the ring's
|
|
58
|
+
arc length. `svgMarkup` returns a complete SVG document as a string, with a view box and no width or
|
|
59
|
+
height of its own.
|
|
103
60
|
|
|
104
|
-
|
|
105
|
-
import { equationFromTex, equationNode, group, morphEquation, vec2 } from '@altpsyche/maths';
|
|
61
|
+
## Geometry
|
|
106
62
|
|
|
107
|
-
|
|
63
|
+
All geometry is cubic Bézier. A **path** is a sequence of subpaths; a subpath is a start point
|
|
64
|
+
followed by cubic segments, closed or open. A segment stores its two control points and its endpoint
|
|
65
|
+
and not its start, which is why the functions evaluating one take that start as an argument.
|
|
108
66
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
67
|
+
`circle` and `arc` emit segments spanning at most a quarter turn. For a segment of angle θ the
|
|
68
|
+
control distance is (4/3)·tan(θ/4). That bounds the radial error at 2.7 × 10⁻⁴ times the radius, and
|
|
69
|
+
the suite holds the drawn edge between 2.6 × 10⁻⁴ and 2.8 × 10⁻⁴. `splitCurve` subdivides by de
|
|
70
|
+
Casteljau's construction and `pathFromData` reads the elliptical arc form of an SVG `d` attribute by
|
|
71
|
+
the conversion the specification itself gives.
|
|
113
72
|
|
|
114
|
-
|
|
115
|
-
```
|
|
73
|
+
<img src="docs/boolean.svg" width="720" alt="Two discs drawn three times side by side: everything either one covers, only what both cover, and the first with the second taken out of it.">
|
|
116
74
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
arrives.
|
|
75
|
+
`unionOf`, `intersectionOf` and `differenceOf` operate on closed loops of cubics. Crossings are
|
|
76
|
+
located per segment pair and refined by Newton's method, the surviving pieces are stitched, and the
|
|
77
|
+
result may contain a hole neither operand had. When the kept pieces fail to close, the call throws
|
|
78
|
+
and the message states the piece count and the distance between the two open ends. Proximity is a
|
|
79
|
+
distance in figure units, `TOLERANCE = 1e-6` by default, rather than a fraction of anything.
|
|
123
80
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
would otherwise ship as a red word inside the picture.
|
|
81
|
+
`areaOf` returns the signed area, positive for anticlockwise winding, summed over subpaths.
|
|
82
|
+
`containsPoint` flattens to polylines and applies the nonzero winding rule. `lengthOf` and
|
|
83
|
+
`pointAlong` measure by arc length, and `lengthOf` reads slightly short by the chord error of its
|
|
84
|
+
sampling.
|
|
129
85
|
|
|
130
|
-
##
|
|
86
|
+
## Graphs
|
|
131
87
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
is a corner rather than a smooth turn, which is what a brace has and what says which point of it is
|
|
136
|
-
doing the pointing. The tip stands at the depth asked for whatever the span, and only the curl
|
|
137
|
-
narrows when the span is short, so two close points get a shallower brace rather than one whose
|
|
138
|
-
halves cross.
|
|
88
|
+
A **scale** is a pair of intervals: the numbers an axis counts through, and where those numbers land
|
|
89
|
+
in figure units. `coordsOf` pairs two scales, and the mapping is a value the caller holds rather
|
|
90
|
+
than state read back out of a drawn group.
|
|
139
91
|
|
|
140
|
-
|
|
141
|
-
|
|
92
|
+
`plot` samples a function and emits one Hermite cubic per interval. The control points sit a third
|
|
93
|
+
of the way along in x and carry the sample's own slope, so the curve passes through both samples at
|
|
94
|
+
both slopes. `slopeOf` uses the central difference, whose error is O(h²) for the same two
|
|
95
|
+
evaluations a one-sided difference costs. `tangentAt` clips the tangent line to the graph
|
|
96
|
+
analytically rather than by sampling, since a line crosses each edge once.
|
|
142
97
|
|
|
143
|
-
`
|
|
144
|
-
|
|
145
|
-
|
|
98
|
+
`streamlineOf` integrates a field by fourth-order Runge-Kutta with a step in arc length rather than
|
|
99
|
+
in time, which keeps the points evenly spaced. Halving the step divides the error along the curve by
|
|
100
|
+
15.1 and then 15.6, against the factor of 16 the order predicts.
|
|
146
101
|
|
|
147
|
-
|
|
148
|
-
import { brace, countTo, labelFor, pointOf, vec2 } from '@altpsyche/maths';
|
|
102
|
+
## Space
|
|
149
103
|
|
|
150
|
-
|
|
151
|
-
depth: 0.3,
|
|
152
|
-
padding: 0.28,
|
|
153
|
-
stroke: pen,
|
|
154
|
-
fill: ink,
|
|
155
|
-
size: 0.3,
|
|
156
|
-
});
|
|
104
|
+
<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.">
|
|
157
105
|
|
|
158
|
-
|
|
159
|
-
|
|
106
|
+
`camera3` holds an eye, a target, an up vector, a view matrix and a projection. `perspective` and
|
|
107
|
+
`orthographic` supply the projection; the orthographic case is a scale rather than a divide and
|
|
108
|
+
therefore has no near plane. Every builder that works in space projects to figure units and returns
|
|
109
|
+
the same node types a graph returns, so one animation reaches both.
|
|
160
110
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
111
|
+
`scene3` orders children back to front by the depth of their sample points, which is the painter's
|
|
112
|
+
algorithm. Mutually piercing pieces and cyclic overlaps admit no correct order. The answer is
|
|
113
|
+
smaller pieces: `surface3` cuts a surface into four-cornered cells, so two surfaces sort against
|
|
114
|
+
each other rather than as two groups. The sort is stable, so pieces at equal depth hold the order
|
|
115
|
+
the author gave and a picture does not flicker between frames. `sectionOf` returns the runs of
|
|
116
|
+
points where a plane cuts a parametric surface, closing a run whose ends meet.
|
|
165
117
|
|
|
166
|
-
##
|
|
118
|
+
## Time and motion
|
|
167
119
|
|
|
168
|
-
<img src="docs/
|
|
120
|
+
<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.">
|
|
169
121
|
|
|
170
|
-
|
|
171
|
-
`
|
|
172
|
-
|
|
173
|
-
|
|
122
|
+
An **animation** maps marks and a fraction of a span to marks. Fifteen of them are supplied, among
|
|
123
|
+
them `draw`, `fadeIn`, `moveAlong`, `rotate`, `morph` and `morphEquation`, which pairs the glyphs of
|
|
124
|
+
two typeset expressions and moves only the difference. Every animation is the identity at the start
|
|
125
|
+
of its span. Every mark it introduces exists at every fraction, at zero opacity where it is not yet
|
|
126
|
+
visible, so a frame-to-frame comparison never reports an arrival.
|
|
174
127
|
|
|
175
|
-
|
|
176
|
-
|
|
128
|
+
`Timeline` sequences animations by `play`, `together` and `stagger`, each span carrying its own
|
|
129
|
+
easing curve. `Timeline.at` applies a finished span in full and an unstarted one at zero, making the
|
|
130
|
+
timeline a function of time rather than a record of what has played. Tracks are the second source of
|
|
131
|
+
values: `sampleTrack` reads a keyed value at a time, holding the nearest key outside the keyed
|
|
132
|
+
range.
|
|
177
133
|
|
|
178
|
-
|
|
179
|
-
|
|
134
|
+
`framesOf` walks a figure at a fixed rate or count and yields one frame at a time. A frame carries
|
|
135
|
+
its index, its time, its marks and the view matrix built at that same time. The walk stops strictly
|
|
136
|
+
before the duration, so a looping figure never emits its first frame twice.
|
|
180
137
|
|
|
181
|
-
|
|
182
|
-
intersectionOf(first, second);
|
|
183
|
-
differenceOf(first, second);
|
|
184
|
-
```
|
|
138
|
+
## Painters
|
|
185
139
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
then sharpening what it finds by Newton's method. `cutPath` puts a cut wherever something crosses,
|
|
193
|
-
so that afterwards every piece is wholly inside the other path or wholly outside it. `containsPoint`
|
|
194
|
-
decides which of those a piece is, by counting how many times the other path winds round its middle.
|
|
195
|
-
`areaOf` says how much a path encloses, in closed form rather than by sampling, which is what every
|
|
196
|
-
claim above is checked against.
|
|
197
|
-
|
|
198
|
-
<img src="docs/boolean-strip.svg" width="820" alt="Four moments in two rows, each showing the three panels, as the small disc walks from clear of the large one, through touching it at one point, through overlapping it, to sitting wholly inside it.">
|
|
199
|
-
|
|
200
|
-
Four times of one figure, in two rows. A small disc walks across a larger one: clear of it, touching
|
|
201
|
-
it at one point, crossing it at two, and wholly inside it. Those are the four cases this kind of code
|
|
202
|
-
gets silently wrong, which is why the demo walks through all of them rather than drawing one.
|
|
203
|
-
|
|
204
|
-
Two shapes that share an edge are combined by which way each of them runs over it. Two paths walking
|
|
205
|
-
a shared stretch the same way have their solid on the same side of it, so the stretch is on the edge
|
|
206
|
-
of a union and of an overlap and is kept once. Walking it opposite ways puts their solids on opposite
|
|
207
|
-
sides, so the stretch is inside a union and outside an overlap, and a difference keeps the first
|
|
208
|
-
path's copy of it. Two rectangles sharing an edge unite into one rectangle, and a shape combined with
|
|
209
|
-
itself gives itself back.
|
|
210
|
-
|
|
211
|
-
When the pieces kept will not join into a loop, the operation stops and says how far apart the two
|
|
212
|
-
ends of the run it had are. That happens when an input crosses itself, which these do not take. A
|
|
213
|
-
shape drawn with a gap in it and nothing said about it is the one failure a caller cannot see.
|
|
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. 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
|
-
|
|
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, pane, field arrows and runs of descent 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
|
-
## 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.
|
|
140
|
+
`svgMarkup` returns a document as a string and `svgElements` returns the elements as data.
|
|
141
|
+
`paintSvg` replaces the children of an element already in a document, and `paintCanvas` draws into a
|
|
142
|
+
two-dimensional context. Coordinates are written to three decimal places. That is finer than any
|
|
143
|
+
screen or encoder resolves, and coarse enough that the last bits of a double never reach the output.
|
|
144
|
+
One test paints a single mark list both ways and holds the two to the same geometry within a
|
|
145
|
+
thousandth of a pixel, and to the same style exactly.
|
|
309
146
|
|
|
310
|
-
##
|
|
147
|
+
## Restrictions
|
|
311
148
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
asked for its extent at one time and its marks at another.
|
|
149
|
+
A mark may request only what both painters implement: no filters, no blend modes, no clipping. A
|
|
150
|
+
figure using an SVG filter would render correctly on a page and lose the effect silently in a
|
|
151
|
+
recording.
|
|
316
152
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
they drift 1.14e-15 figure units over the whole walk.
|
|
321
|
-
|
|
322
|
-
## The animations
|
|
153
|
+
Gradients are excluded for a different reason, since both painters draw them. SVG names a gradient
|
|
154
|
+
with an element carrying a document-unique identifier, and a canvas with an object built from the
|
|
155
|
+
context. A colour here is text that both accept unchanged.
|
|
323
156
|
|
|
324
|
-
|
|
325
|
-
`
|
|
326
|
-
|
|
157
|
+
A stroke has one width along its length. Colour enters as text, `'#1b1b1b'` or `'rgb(27, 27, 27)'`;
|
|
158
|
+
`colourOf` parses hex and `rgb()` for interpolation in sRGB and rejects every other form rather than
|
|
159
|
+
guessing. Nothing reads the page, and `getComputedStyle` appears nowhere in the tree.
|
|
327
160
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
`moveAlong` carries a mark along a path at one speed, measured by the path's length. Even steps in a
|
|
332
|
-
curve's own parameter are uneven steps along the curve: a step covers more of it where the curve is
|
|
333
|
-
moving fast, which on a quarter circle is a 6.9% difference between the longest step and the shortest
|
|
334
|
-
and on the demo's own walk is 82%.
|
|
161
|
+
No screenshot gates this package. Every assertion reads a mark list or a number, so the suite of 618
|
|
162
|
+
tests runs in Node without a browser. Comparisons are by tolerance rather than by hash, because
|
|
163
|
+
`Math.sin`, `Math.cos` and `Math.pow` are not specified to the last bit and differ between engines.
|
|
335
164
|
|
|
336
|
-
|
|
337
|
-
all eight and a test compares the bytes against the committed files.
|
|
338
|
-
|
|
339
|
-
## What it is built on
|
|
340
|
-
|
|
341
|
-
**A figure at a time is data.** `at(figure, seconds)` is the whole public surface, and it is a
|
|
342
|
-
pure function: ask for four seconds and it gives the picture at four seconds whatever it gave
|
|
343
|
-
before. A page playing forward, a reader dragging a scrub bar backwards and a recorder walking
|
|
344
|
-
a fixed step are three consumers of one answer.
|
|
345
|
-
|
|
346
|
-
**Everything is a cubic**, a straight line included. That is what lets one shape be walked into
|
|
347
|
-
another point by point, with no case where a line has to become an arc.
|
|
348
|
-
|
|
349
|
-
**A mark may only ask for what both painters can do**, rather than the union of them. There are
|
|
350
|
-
no filters, no blend modes, no clipping and no gradients, because a figure reaching for
|
|
351
|
-
something only SVG has would look right on a page and lose it without a word in a recording.
|
|
352
|
-
|
|
353
|
-
**A figure never reads the page.** Colours arrive as a palette the caller hands in.
|
|
354
|
-
|
|
355
|
-
## The two painters
|
|
356
|
-
|
|
357
|
-
`svgMarkup` and `paintSvg` write SVG, which is what a figure on a page is: the text is text, CSS
|
|
358
|
-
reaches it, and the markup can be written with no browser at all. `paintCanvas` paints the same
|
|
359
|
-
marks onto a two-dimensional canvas, which is what a recording needs, because an encoder takes
|
|
360
|
-
one surface. A test holds the two to emitting the same geometry and the same style for every
|
|
361
|
-
mark.
|
|
362
|
-
|
|
363
|
-
## The way in
|
|
165
|
+
## Further reading
|
|
364
166
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
another shape, and those are the operations this package is for. Every command is read, elliptical
|
|
369
|
-
arcs included, and a command it does not know stops the read rather than being skipped.
|
|
167
|
+
[docs/GUIDE.md](docs/GUIDE.md) teaches the package in order. [docs/REFERENCE.md](docs/REFERENCE.md)
|
|
168
|
+
carries one entry for each of the 227 names at the door. [DESIGN.md](DESIGN.md) states why the
|
|
169
|
+
design is what it is and what it will not become.
|
|
370
170
|
|
|
371
|
-
|
|
171
|
+
`index.ts` is the entire public surface, and nothing outside the package reaches a file inside it by
|
|
172
|
+
path. A line divides the package: values and timing below it, figures and painters above, and
|
|
173
|
+
nothing below the line imports anything above it. A test holds each of those.
|
|
372
174
|
|
|
373
|
-
|
|
374
|
-
along, and a value walked between keys. That half changes almost never. **Figures and painters**
|
|
375
|
-
are above it. Nothing below the line imports anything above it, and a test says so.
|
|
376
|
-
|
|
377
|
-
One door. MIT.
|
|
175
|
+
MIT.
|
|
@@ -100,15 +100,17 @@ export declare function growFrom(target: string, from?: Vec2): Animation;
|
|
|
100
100
|
export interface IndicateOptions extends AboutOptions {
|
|
101
101
|
/** How big it gets at the middle of the span. */
|
|
102
102
|
factor?: number;
|
|
103
|
-
/**
|
|
103
|
+
/** Walked towards over the span and back again, so the mark ends in the colour
|
|
104
|
+
* it started in. */
|
|
104
105
|
colour?: Colour;
|
|
105
106
|
}
|
|
106
107
|
/**
|
|
107
108
|
* Swelled and settled, to point at something without moving it.
|
|
108
109
|
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
110
|
+
* Each of the mark's own colours is walked towards the colour named and back
|
|
111
|
+
* again, so the swell and the colour reach their furthest at the same moment. A
|
|
112
|
+
* colour `colourOf` cannot read is held at the far end rather than mixed towards
|
|
113
|
+
* a guess.
|
|
112
114
|
*/
|
|
113
115
|
export declare function indicate(target: string, options?: IndicateOptions): Animation;
|
|
114
116
|
export interface FlashOptions {
|
package/dist/figure/animation.js
CHANGED
|
@@ -14,6 +14,7 @@ import { mat3 } from '../values/mat3.js';
|
|
|
14
14
|
import { vec2 } from '../values/vec2.js';
|
|
15
15
|
import { lerp } from '../values/scalar.js';
|
|
16
16
|
import { smoothstep } from '../values/ease.js';
|
|
17
|
+
import { lerpColour } from '../values/colour.js';
|
|
17
18
|
import { circle, line, polygon, transformPath } from './path.js';
|
|
18
19
|
import { trimPath } from './trim.js';
|
|
19
20
|
import { lerpPath } from './morph.js';
|
|
@@ -256,12 +257,26 @@ function painted(mark, colour) {
|
|
|
256
257
|
stroke: mark.stroke ? { ...mark.stroke, colour } : undefined,
|
|
257
258
|
};
|
|
258
259
|
}
|
|
260
|
+
/** A mark walked a fraction of the way towards a colour, each of its own colours
|
|
261
|
+
* from where that colour stands. A colour neither end can be read from is held at
|
|
262
|
+
* the far end rather than mixed towards a guess. */
|
|
263
|
+
function paintedTowards(mark, colour, along) {
|
|
264
|
+
const towards = (from) => from === undefined ? colour : (lerpColour(from, colour, along) ?? colour);
|
|
265
|
+
if (mark.kind === 'text')
|
|
266
|
+
return { ...mark, fill: { ...mark.fill, colour: towards(mark.fill.colour) } };
|
|
267
|
+
return {
|
|
268
|
+
...mark,
|
|
269
|
+
fill: mark.fill ? { ...mark.fill, colour: towards(mark.fill.colour) } : undefined,
|
|
270
|
+
stroke: mark.stroke ? { ...mark.stroke, colour: towards(mark.stroke.colour) } : undefined,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
259
273
|
/**
|
|
260
274
|
* Swelled and settled, to point at something without moving it.
|
|
261
275
|
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
276
|
+
* Each of the mark's own colours is walked towards the colour named and back
|
|
277
|
+
* again, so the swell and the colour reach their furthest at the same moment. A
|
|
278
|
+
* colour `colourOf` cannot read is held at the far end rather than mixed towards
|
|
279
|
+
* a guess.
|
|
265
280
|
*/
|
|
266
281
|
export function indicate(target, options = {}) {
|
|
267
282
|
const peak = options.factor ?? 1.2;
|
|
@@ -276,7 +291,8 @@ export function indicate(target, options = {}) {
|
|
|
276
291
|
return swelled;
|
|
277
292
|
if (!swelled.some((mark) => touches(mark.id, target)))
|
|
278
293
|
return swelled;
|
|
279
|
-
|
|
294
|
+
const towards = thereAndBack(along);
|
|
295
|
+
return swelled.map((mark) => touches(mark.id, target) ? paintedTowards(mark, colour, towards) : mark);
|
|
280
296
|
};
|
|
281
297
|
}
|
|
282
298
|
/**
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
* The shapes an annotation is made of, composed from marks rather than being
|
|
3
3
|
* marks of their own.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* A builder hands back a group, so an arrow is a shaft and a head with ids of
|
|
6
6
|
* their own and an animation naming the arrow reaches both. Making an arrow a
|
|
7
7
|
* single mark instead would mean one path that is stroked along its shaft and
|
|
8
|
-
* filled at its head, and no mark can be both.
|
|
8
|
+
* filled at its head, and no mark can be both. `bracePath` is the one call here
|
|
9
|
+
* that hands back a path, for a figure that wants the outline and not the group.
|
|
9
10
|
*/
|
|
10
11
|
import { type Vec2 } from '../values/vec2.js';
|
|
11
12
|
import { type Path } from './path.js';
|