@altpsyche/maths 0.12.0 → 1.0.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 +126 -353
- 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 +1 -1
- package/dist/figure/frames.js +4 -4
- 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 +18 -12
- package/dist/index.js +10 -7
- package/dist/paint/number.js +1 -2
- package/dist/paint/svg.d.ts +40 -4
- package/dist/paint/svg.js +57 -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,402 +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
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
16
|
+
```ts
|
|
17
|
+
marksAt(figure, t): readonly Mark[]
|
|
19
18
|
```
|
|
20
19
|
|
|
21
|
-
|
|
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
|
|
26
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
44
|
+
```ts
|
|
45
|
+
import { Timeline, circle, draw, group, marksAt, shape, svgMarkup, vec2, viewAt } from '@altpsyche/maths';
|
|
62
46
|
|
|
63
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
115
|
-
```
|
|
102
|
+
## Space
|
|
116
103
|
|
|
117
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
148
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
159
|
-
```
|
|
138
|
+
## Painters
|
|
160
139
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
##
|
|
147
|
+
## Restrictions
|
|
167
148
|
|
|
168
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
176
|
-
|
|
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
|
-
|
|
179
|
-
|
|
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
|
-
|
|
182
|
-
intersectionOf(first, second);
|
|
183
|
-
differenceOf(first, second);
|
|
184
|
-
```
|
|
165
|
+
## Further reading
|
|
185
166
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
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
|
/**
|