@altpsyche/maths 0.12.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.
Files changed (48) hide show
  1. package/README.md +126 -353
  2. package/dist/figure/animation.d.ts +6 -4
  3. package/dist/figure/animation.js +20 -4
  4. package/dist/figure/annotate.d.ts +3 -2
  5. package/dist/figure/annotate.js +3 -2
  6. package/dist/figure/axis.js +5 -8
  7. package/dist/figure/axis3.d.ts +8 -0
  8. package/dist/figure/axis3.js +13 -0
  9. package/dist/figure/boolean.js +2 -2
  10. package/dist/figure/equation.d.ts +1 -1
  11. package/dist/figure/equation.js +2 -2
  12. package/dist/figure/field.d.ts +6 -6
  13. package/dist/figure/field.js +5 -6
  14. package/dist/figure/field3.d.ts +50 -0
  15. package/dist/figure/field3.js +60 -0
  16. package/dist/figure/figure.d.ts +4 -4
  17. package/dist/figure/figure.js +4 -4
  18. package/dist/figure/frames.d.ts +1 -1
  19. package/dist/figure/frames.js +4 -4
  20. package/dist/figure/grid.d.ts +19 -0
  21. package/dist/figure/grid.js +32 -0
  22. package/dist/figure/inside.d.ts +2 -2
  23. package/dist/figure/inside.js +3 -4
  24. package/dist/figure/length.d.ts +1 -3
  25. package/dist/figure/length.js +7 -10
  26. package/dist/figure/mark.d.ts +7 -2
  27. package/dist/figure/node.js +2 -3
  28. package/dist/figure/path-data.js +2 -3
  29. package/dist/figure/path.d.ts +1 -1
  30. package/dist/figure/path.js +3 -3
  31. package/dist/figure/plot.d.ts +1 -1
  32. package/dist/figure/plot.js +9 -13
  33. package/dist/figure/scale.d.ts +2 -2
  34. package/dist/figure/scale.js +3 -3
  35. package/dist/figure/section.d.ts +5 -2
  36. package/dist/figure/section.js +9 -16
  37. package/dist/figure/space.d.ts +8 -91
  38. package/dist/figure/space.js +7 -110
  39. package/dist/figure/surface3.d.ts +62 -0
  40. package/dist/figure/surface3.js +58 -0
  41. package/dist/index.d.ts +18 -12
  42. package/dist/index.js +10 -7
  43. package/dist/paint/number.js +1 -2
  44. package/dist/paint/svg.d.ts +29 -4
  45. package/dist/paint/svg.js +49 -14
  46. package/dist/values/colour.d.ts +43 -0
  47. package/dist/values/colour.js +110 -0
  48. package/package.json +1 -1
package/README.md CHANGED
@@ -1,402 +1,175 @@
1
1
  # @altpsyche/maths
2
2
 
3
- The mathematics the figures on [altpsyche.dev](https://altpsyche.dev) are drawn from.
3
+ The mathematics behind the figures on [altpsyche.dev](https://altpsyche.dev).
4
4
 
5
- A **figure** is a picture that moves and explains itself. Asking one for a time gives back a
6
- flat list of **marks**, and a **painter** turns marks into something a reader can see.
5
+ ## The model
7
6
 
8
- ```ts
9
- import { Timeline, at, circle, draw, group, shape, svgMarkup, vec2, viewMatrix } from '@altpsyche/maths';
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.
10
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
- };
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.
17
15
 
18
- svgMarkup(at(figure, 0.5), viewMatrix(figure.extent, 'contain', 640, 360), 640, 360);
16
+ ```ts
17
+ marksAt(figure, t): readonly Mark[]
19
18
  ```
20
19
 
21
- ## Axes and a plotted function
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.
22
27
 
23
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.">
24
29
 
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.
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.
31
32
 
32
- ```ts
33
- import { axes, coordsOf, group, interval, numberPlane, plot, scaleOf, shape } from '@altpsyche/maths';
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
- ]);
33
+ ## Install
34
+
35
+ ```sh
36
+ npm install @altpsyche/maths
45
37
  ```
46
38
 
47
- A **scale** is the run of numbers an axis counts through and where that run lands in figure units.
48
- Two of them are a **coords**, and `pointOf` reads a pair of graph numbers as a point. The steps
49
- between ticks are one, two or five times a power of ten, because those are the numbers a reader
50
- adds up in their head.
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.
51
41
 
52
- `plot` samples a function at a fixed count and joins the samples with cubics that leave each one at
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.
42
+ ## A complete figure
57
43
 
58
- `areaUnder` closes the region between a curve and a level line, `riemannBars` draws the bars the
59
- region is the limit of at the left edge, the right edge or the middle of each one, and `tangentAt`
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.
44
+ ```ts
45
+ import { Timeline, circle, draw, group, marksAt, shape, svgMarkup, vec2, viewAt } from '@altpsyche/maths';
62
46
 
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.">
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
+ };
64
53
 
65
- Four times of one figure, side by side: the picture arrived, the beat at the stationary point, half
66
- way up, and the top. A moving picture in a README needs a GIF and this package has no encoder, so the
67
- strip shows the motion in a still.
54
+ svgMarkup(marksAt(figure, 0.5), viewAt(figure, 0.5, 640, 360), 640, 360);
55
+ ```
68
56
 
69
- ## Equations
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.
70
60
 
71
- `equationFromTex` typesets an expression with MathJax and reads the SVG the typesetter wrote back as
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.
61
+ ## Geometry
75
62
 
76
- ```ts
77
- import { equationFromTex, equationNode, vec2 } from '@altpsyche/maths';
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.
78
66
 
79
- const rule = await equationFromTex('\\frac{dy}{dx} = 2x');
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.
80
72
 
81
- equationNode('rule', rule, {
82
- at: vec2(-4.27, 1.74),
83
- width: 1.2,
84
- height: 0.6,
85
- fill: { colour: '#1b1b1b' },
86
- });
87
- ```
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.">
88
74
 
89
- An equation is paths on the page as well as in a recording, which is what stops one expression having
90
- two pictures free to disagree. A glyph is an outline rather than a letter, so no font has to be
91
- installed anywhere and the LaTeX is the label the picture carries for a reader.
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.
92
80
 
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.
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.
96
85
 
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.
86
+ ## Graphs
103
87
 
104
- ```ts
105
- import { equationFromTex, equationNode, group, morphEquation, vec2 } from '@altpsyche/maths';
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.
106
91
 
107
- const box = { at: vec2(-4.86, 1.74), align: 'start', width: 1.2, height: 0.6, fill: { colour: '#1b1b1b' } } as const;
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.
108
97
 
109
- group('rule', [
110
- equationNode('at-rest', await equationFromTex('\\frac{dy}{dx} = 0'), box),
111
- equationNode('moving', await equationFromTex('\\frac{dy}{dx} = 2x'), box),
112
- ]);
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.
113
101
 
114
- morphEquation('rule/at-rest', 'rule/moving');
115
- ```
102
+ ## Space
116
103
 
117
- Both expressions are in the scene at every time and the animation moves one onto the other, because a
118
- mark that arrived part way through a span would turn up in a comparison between two frames as
119
- something that changed. A paired glyph is drawn once rather than cross-faded, so the glyph being left
120
- carries the walk and its partner stays at nothing until it is being stood on exactly. Hang both
121
- expressions from the same edge with `align`, or the part they share slides sideways as the difference
122
- arrives.
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.">
123
105
 
124
- Three things stop a typeset expression rather than being drawn, and each names what it found. A TeX
125
- error carries the typesetter's own message. A character the font has no outline for arrives as text,
126
- which would draw with whatever font a browser had and draw nothing at all in a recording. An
127
- undefined macro is not an error at all, because MathJax draws the macro's own name in red, so a typo
128
- would otherwise ship as a red word inside the picture.
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.
129
110
 
130
- ## Braces and a number that counts
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.
131
117
 
132
- `bracePath` draws a curly brace from one point to another with its tip standing off the line between
133
- them, and `brace` is that path with a word beyond the tip. It is one open subpath of six pieces: a
134
- curl out of each end, a run along at the curl's own height, and two curls meeting at the tip. The tip
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.
118
+ ## Time and motion
139
119
 
140
- The label is anchored and never measured, because nothing about a figure's layout may depend on how
141
- wide some text is.
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.">
142
121
 
143
- `countTo` writes the value a count has reached into a text mark. How the value is written is the
144
- caller's, so a count of a length and a count of a population can round differently and this holds no
145
- opinion about either.
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.
146
127
 
147
- ```ts
148
- import { brace, countTo, labelFor, pointOf, vec2 } from '@altpsyche/maths';
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.
149
133
 
150
- brace('rise', pointOf(coords, 3, 9), pointOf(coords, 3, 0), labelFor(9, 0.01), {
151
- depth: 0.3,
152
- padding: 0.28,
153
- stroke: pen,
154
- fill: ink,
155
- size: 0.3,
156
- });
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.
157
137
 
158
- countTo('rise/word', 0, 9, (value) => labelFor(value, 0.01));
159
- ```
138
+ ## Painters
160
139
 
161
- A number driven by the clock is not the same as a reading driven by a track, and the difference
162
- matters. The slope in the demo is a value of the walk, so it is worked out by the scene from the
163
- track and cannot drift from the dot. The rise is counted by the clock, which is honest only because
164
- the dot has stopped by the time it counts: two clocks running at once would be free to disagree.
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.
165
146
 
166
- ## Two shapes combined
147
+ ## Restrictions
167
148
 
168
- <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.">
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.
169
152
 
170
- `unionOf` is everything either path covers, `intersectionOf` is only what both cover, and
171
- `differenceOf` is the first with the second taken out of it. Each input is closed loops that do not
172
- cross themselves, and a loop left open is closed by a straight run back to where it started before
173
- anything else happens.
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.
174
156
 
175
- ```ts
176
- import { circle, differenceOf, intersectionOf, unionOf, vec2 } from '@altpsyche/maths';
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.
177
160
 
178
- const first = circle(vec2(0, 0), 0.9);
179
- const second = circle(vec2(-0.6, 0), 0.36);
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.
180
164
 
181
- unionOf(first, second);
182
- intersectionOf(first, second);
183
- differenceOf(first, second);
184
- ```
165
+ ## Further reading
185
166
 
186
- A result may have a hole even though an input may not. A disc with a smaller disc taken out of it is
187
- a ring, which is an outer loop and an inner loop wound the opposite way, and the nonzero rule the
188
- mark already carries leaves the middle empty.
189
-
190
- The work happens in four steps, and each of them is a call of its own. `curveCrossings` says where
191
- two cubics cross, by halving both curves and following only the halves whose boxes still overlap,
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.
309
-
310
- ## A view that follows
311
-
312
- `Extent` carries a `centre`, which is where the middle of the frame sits in figure units, and an
313
- extent may be a function of the shape of the surface and of the time. `viewAt(figure, seconds, width,
314
- height)` hands a painter its matrix at a time in one call, so a figure whose extent moves cannot be
315
- asked for its extent at one time and its marks at another.
316
-
317
- The flat demo's view follows its dot across. The dot stays within 1.2 figure units of the middle of
318
- the frame, where before it crossed 2.76. `fractionOf` reads the centre off the extent it is handed, so
319
- the reading and the typeset rule stay where they are on the surface while the grid slides under them:
320
- they drift 1.14e-15 figure units over the whole walk.
321
-
322
- ## The animations
323
-
324
- `fadeIn`, `fadeOut`, `fadeTo`, `draw`, `morph`, `morphEquation`, `countTo`, `moveBy`, `rotate`,
325
- `scale`, `growFrom`, `moveAlong`, `indicate`, `flash` and `circumscribe`. A `Timeline` plays them in order, plays several `together`, or
326
- `stagger`s a row so its parts arrive one after another.
327
-
328
- `boundsOf` is the box a turn and a growth are worked about, found from where each piece of the curve
329
- turns back on itself rather than from the points the curve is written from.
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%.
335
-
336
- Every picture here is written by `svgMarkup`, which needs no browser, so `npm run demos` regenerates
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
- ## 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
-
388
- ## The way in
389
-
390
- `pathFromData` reads an SVG `d` attribute as a path, which is the inverse of what the SVG painter
391
- writes. Without it the only shapes that exist are the ones the builders here make, so a glyph from
392
- a typesetter or an outline from a drawing program could not be trimmed, aligned or walked into
393
- another shape, and those are the operations this package is for. Every command is read, elliptical
394
- arcs included, and a command it does not know stops the read rather than being skipped.
395
-
396
- ## The line through the package
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.
397
170
 
398
- **Values and timing** are below it: vectors, a transform, the four curves a change can travel
399
- along, and a value walked between keys. That half changes almost never. **Figures and painters**
400
- are above it. Nothing below the line imports anything above it, and a test says so.
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.
401
174
 
402
- 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
- /** Held for the length of the span and then let go. */
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
- * The colour is swapped for the length of the span rather than walked into. A
110
- * colour here is any CSS colour written as text, and walking between two of them
111
- * needs a reader for every form one can take, which does not exist here yet.
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 {
@@ -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
- * The colour is swapped for the length of the span rather than walked into. A
263
- * colour here is any CSS colour written as text, and walking between two of them
264
- * needs a reader for every form one can take, which does not exist here yet.
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
- return swelled.map((mark) => (touches(mark.id, target) ? painted(mark, colour) : mark));
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
  /**