@altpsyche/maths 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,9 +24,10 @@ svgMarkup(at(figure, 0.5), viewMatrix(figure.extent, 'contain', 640, 360), 640,
24
24
 
25
25
  The picture arrives rather than appearing. The grid fades, the axes draw on, their labels come in one
26
26
  after another, the curve draws, the dot grows out of the origin, and the dot is pointed at where the
27
- slope is nothing. Then it walks the curve at one speed and flashes at the top. The number in the
28
- corner is a value of the typeset rule under it, and that rule reads no slope while the dot is held at
29
- the stationary point and walks into the one that depends on x as the dot leaves.
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
31
 
31
32
  ```ts
32
33
  import { axes, coordsOf, group, interval, numberPlane, plot, scaleOf, shape } from '@altpsyche/maths';
@@ -59,7 +60,7 @@ region is the limit of at the left edge, the right edge or the middle of each on
59
60
  lays the tangent along the curve, cut where it leaves the graph. `slopeOf` reads the slope itself,
60
61
  which is what the number in the corner is.
61
62
 
62
- <img src="docs/tangent-strip.svg" width="960" alt="Four frames of the same figure side by side, the point walking up the curve, the shaded region growing behind it, and the typeset rule in the corner changing from a slope of nothing to one that depends on x.">
63
+ <img src="docs/tangent-strip.svg" width="960" alt="Four frames of the same figure side by side, the point walking up the curve, the shaded region growing behind it, the typeset rule in the corner changing from a slope of nothing to one that depends on x, and a brace measuring the rise in the last frame.">
63
64
 
64
65
  Four times of one figure, side by side: the picture arrived, the beat at the stationary point, half
65
66
  way up, and the top. A moving picture in a README needs a GIF and this package has no encoder, so the
@@ -126,10 +127,46 @@ which would draw with whatever font a browser had and draw nothing at all in a r
126
127
  undefined macro is not an error at all, because MathJax draws the macro's own name in red, so a typo
127
128
  would otherwise ship as a red word inside the picture.
128
129
 
130
+ ## Braces and a number that counts
131
+
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.
139
+
140
+ The label is anchored and never measured, because nothing about a figure's layout may depend on how
141
+ wide some text is.
142
+
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.
146
+
147
+ ```ts
148
+ import { brace, countTo, labelFor, pointOf, vec2 } from '@altpsyche/maths';
149
+
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
+ });
157
+
158
+ countTo('rise/word', 0, 9, (value) => labelFor(value, 0.01));
159
+ ```
160
+
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.
165
+
129
166
  ## The animations
130
167
 
131
- `fadeIn`, `fadeOut`, `fadeTo`, `draw`, `morph`, `morphEquation`, `moveBy`, `rotate`, `scale`,
132
- `growFrom`, `moveAlong`, `indicate`, `flash` and `circumscribe`. A `Timeline` plays them in order, plays several `together`, or
168
+ `fadeIn`, `fadeOut`, `fadeTo`, `draw`, `morph`, `morphEquation`, `countTo`, `moveBy`, `rotate`,
169
+ `scale`, `growFrom`, `moveAlong`, `indicate`, `flash` and `circumscribe`. A `Timeline` plays them in order, plays several `together`, or
133
170
  `stagger`s a row so its parts arrive one after another.
134
171
 
135
172
  A turn and a growth happen about a point the marks decide for themselves, which is the middle of the
@@ -43,6 +43,17 @@ export declare function morph(target: string, into: Path): Animation;
43
43
  * expression still fading in when a morph starts does not jump to solid.
44
44
  */
45
45
  export declare function morphEquation(from: string, to: string): Animation;
46
+ /**
47
+ * A number ticking from one value to another, written into a text mark.
48
+ *
49
+ * How the value is written is the caller's, so this holds no opinion about
50
+ * decimal places: a count of a length and a count of a population want different
51
+ * rounding and neither is this function's to choose.
52
+ *
53
+ * The scene writes the value the count ends at, which is what this writes at the
54
+ * end of its span, so the two never disagree about what the number settles on.
55
+ */
56
+ export declare function countTo(target: string, from: number, to: number, write: (value: number) => string): Animation;
46
57
  /** A mark's own opacity walked to a value, for a figure that wants a thing dimmed
47
58
  * rather than gone. */
48
59
  export declare function fadeTo(target: string, opacity: number): Animation;
@@ -119,6 +119,19 @@ export function morphEquation(from, to) {
119
119
  return marks.map((mark) => changed.get(mark.id) ?? mark);
120
120
  };
121
121
  }
122
+ /**
123
+ * A number ticking from one value to another, written into a text mark.
124
+ *
125
+ * How the value is written is the caller's, so this holds no opinion about
126
+ * decimal places: a count of a length and a count of a population want different
127
+ * rounding and neither is this function's to choose.
128
+ *
129
+ * The scene writes the value the count ends at, which is what this writes at the
130
+ * end of its span, so the two never disagree about what the number settles on.
131
+ */
132
+ export function countTo(target, from, to, write) {
133
+ return over(target, (mark, along) => (mark.kind === 'text' ? { ...mark, text: write(lerp(from, to, along)) } : mark));
134
+ }
122
135
  /** A mark's own opacity walked to a value, for a figure that wants a thing dimmed
123
136
  * rather than gone. */
124
137
  export function fadeTo(target, opacity) {
@@ -8,6 +8,7 @@
8
8
  * filled at its head, and no mark can be both.
9
9
  */
10
10
  import { type Vec2 } from '../values/vec2.js';
11
+ import { type Path } from './path.js';
11
12
  import { type GroupNode, type TextOptions } from './node.js';
12
13
  import type { Fill, Stroke } from './mark.js';
13
14
  export interface ArrowOptions {
@@ -32,6 +33,52 @@ export interface ArrowOptions {
32
33
  export declare function arrow(name: string, from: Vec2, to: Vec2, options: ArrowOptions): GroupNode;
33
34
  /** A filled disc, which is what marks a place a line is pointing at. */
34
35
  export declare function dot(name: string, at: Vec2, radius: number, fill: Fill): GroupNode;
36
+ export interface BraceOptions {
37
+ /** How far the tip stands off the line between the two points, in figure
38
+ * units. A negative depth puts the brace on the other side of that line. */
39
+ depth: number;
40
+ /**
41
+ * How wide the curl at each end and at the tip is, in figure units. Half the
42
+ * depth unless named, which is the curl a quarter circle gives, and never more
43
+ * than a quarter of the span, since two curls wider than that would cross.
44
+ */
45
+ curl?: number;
46
+ }
47
+ /**
48
+ * A curly brace from one point to the other, with its tip standing off the line
49
+ * between them.
50
+ *
51
+ * It is one open subpath of six pieces: a curl out of each end, a run along at
52
+ * the curl's own height, and two curls meeting at the tip. The tip is a corner
53
+ * rather than a smooth turn, which is what a brace has and what says which point
54
+ * of it is being pointed at.
55
+ *
56
+ * The tip stands at the depth asked for whatever the span, and only the curl
57
+ * narrows when the span is short, so a brace between two close points is a
58
+ * shallower shape rather than one whose halves cross.
59
+ */
60
+ export declare function bracePath(from: Vec2, to: Vec2, options: BraceOptions): Path;
61
+ export interface BracedOptions extends BraceOptions {
62
+ stroke: Stroke;
63
+ fill: Fill;
64
+ size: number;
65
+ /** How far beyond the tip the label's anchor sits, in figure units. */
66
+ padding?: number;
67
+ align?: TextOptions['align'];
68
+ baseline?: TextOptions['baseline'];
69
+ family?: string;
70
+ weight?: number;
71
+ }
72
+ /**
73
+ * A brace with a word on it, placed beyond the tip on the far side from the two
74
+ * points.
75
+ *
76
+ * The label is anchored and never measured. Nothing about a figure's layout may
77
+ * depend on how wide some text is, because the width depends on which fonts the
78
+ * machine has and a box sized to fit a label would be a different box on two
79
+ * machines.
80
+ */
81
+ export declare function brace(name: string, from: Vec2, to: Vec2, content: string, options: BracedOptions): GroupNode;
35
82
  export interface CalloutOptions {
36
83
  stroke: Stroke;
37
84
  fill: Fill;
@@ -8,7 +8,7 @@
8
8
  * filled at its head, and no mark can be both.
9
9
  */
10
10
  import { vec2 } from '../values/vec2.js';
11
- import { circle, line, polygon } from './path.js';
11
+ import { circle, line, polygon, straight } from './path.js';
12
12
  import { group, shape, text } from './node.js';
13
13
  /**
14
14
  * A line with a head at the far end.
@@ -32,6 +32,80 @@ export function arrow(name, from, to, options) {
32
32
  export function dot(name, at, radius, fill) {
33
33
  return group(name, [shape('disc', circle(at, radius), { fill })]);
34
34
  }
35
+ /** The control distance a quarter circle wants, which is what makes each curl of
36
+ * a brace an arc rather than a corner rounded by eye. */
37
+ const QUARTER = (4 / 3) * (Math.SQRT2 - 1);
38
+ /**
39
+ * A curly brace from one point to the other, with its tip standing off the line
40
+ * between them.
41
+ *
42
+ * It is one open subpath of six pieces: a curl out of each end, a run along at
43
+ * the curl's own height, and two curls meeting at the tip. The tip is a corner
44
+ * rather than a smooth turn, which is what a brace has and what says which point
45
+ * of it is being pointed at.
46
+ *
47
+ * The tip stands at the depth asked for whatever the span, and only the curl
48
+ * narrows when the span is short, so a brace between two close points is a
49
+ * shallower shape rather than one whose halves cross.
50
+ */
51
+ export function bracePath(from, to, options) {
52
+ const span = vec2.distance(from, to);
53
+ if (span === 0)
54
+ return [];
55
+ const along = vec2.normalize(vec2.sub(to, from));
56
+ const out = vec2.perpendicular(along);
57
+ const depth = options.depth;
58
+ const curl = Math.min(Math.abs(options.curl ?? depth / 2), span / 4);
59
+ const rise = Math.sign(depth || 1) * curl;
60
+ const at = (forward, off) => vec2.add(from, vec2.add(vec2.scale(along, forward), vec2.scale(out, off)));
61
+ const shoulder = at(curl, rise);
62
+ const beforeTip = at(span / 2 - curl, rise);
63
+ const tip = at(span / 2, depth);
64
+ const afterTip = at(span / 2 + curl, rise);
65
+ const beyond = at(span - curl, rise);
66
+ const reach = QUARTER * curl;
67
+ const lift = QUARTER * (depth - rise);
68
+ const piece = (control1, control2, end) => ({ control1, control2, to: end });
69
+ return [
70
+ {
71
+ start: from,
72
+ curves: [
73
+ piece(vec2.add(from, vec2.scale(out, rise * QUARTER)), vec2.sub(shoulder, vec2.scale(along, reach)), shoulder),
74
+ straight(shoulder, beforeTip),
75
+ piece(vec2.add(beforeTip, vec2.scale(along, reach)), vec2.sub(tip, vec2.scale(out, lift)), tip),
76
+ piece(vec2.sub(tip, vec2.scale(out, lift)), vec2.sub(afterTip, vec2.scale(along, reach)), afterTip),
77
+ straight(afterTip, beyond),
78
+ piece(vec2.add(beyond, vec2.scale(along, reach)), vec2.add(to, vec2.scale(out, rise * QUARTER)), to),
79
+ ],
80
+ closed: false,
81
+ },
82
+ ];
83
+ }
84
+ /**
85
+ * A brace with a word on it, placed beyond the tip on the far side from the two
86
+ * points.
87
+ *
88
+ * The label is anchored and never measured. Nothing about a figure's layout may
89
+ * depend on how wide some text is, because the width depends on which fonts the
90
+ * machine has and a box sized to fit a label would be a different box on two
91
+ * machines.
92
+ */
93
+ export function brace(name, from, to, content, options) {
94
+ const path = bracePath(from, to, options);
95
+ const padding = options.padding ?? Math.abs(options.depth) / 2;
96
+ const out = vec2.perpendicular(vec2.normalize(vec2.sub(to, from)));
97
+ const middle = vec2.lerp(from, to, 0.5);
98
+ const at = vec2.add(middle, vec2.scale(out, options.depth + Math.sign(options.depth || 1) * padding));
99
+ const style = { fill: options.fill, family: options.family, weight: options.weight };
100
+ return group(name, [
101
+ shape('brace', path, { stroke: options.stroke }),
102
+ text('word', at, content, options.size, {
103
+ ...style,
104
+ align: options.align ?? 'middle',
105
+ baseline: options.baseline ?? 'middle',
106
+ }),
107
+ ]);
108
+ }
35
109
  /**
36
110
  * A word attached to a place: a disc on the place, a line out to where there is
37
111
  * room, and the word at the end of it.
package/dist/index.d.ts CHANGED
@@ -38,7 +38,7 @@ export { labelFor, tickStep, ticksOn } from './figure/ticks.js';
38
38
  export type { Tick } from './figure/ticks.js';
39
39
  export { flatten, group, shape, text } from './figure/node.js';
40
40
  export type { GroupNode, Node, ShapeNode, Style, TextNode, TextOptions } from './figure/node.js';
41
- export { circumscribe, fadeIn, fadeOut, fadeTo, draw, flash, growFrom, indicate, morph, morphEquation, moveAlong, moveBy, rotate, scale } from './figure/animation.js';
41
+ export { circumscribe, countTo, fadeIn, fadeOut, fadeTo, draw, flash, growFrom, indicate, morph, morphEquation, moveAlong, moveBy, rotate, scale } from './figure/animation.js';
42
42
  export type { AboutOptions, Animation, CircumscribeOptions, FlashOptions, IndicateOptions, ScaleOptions } from './figure/animation.js';
43
43
  export { Timeline } from './figure/timeline.js';
44
44
  export type { PlayOptions, Span, StaggerOptions } from './figure/timeline.js';
@@ -51,8 +51,8 @@ export { pathData, paintSvg, svgElements, svgMarkup } from './paint/svg.js';
51
51
  export type { ElementMaker, PaintNode, PaintTarget, SvgElement } from './paint/svg.js';
52
52
  export { paintCanvas } from './paint/canvas.js';
53
53
  export type { CanvasLike } from './paint/canvas.js';
54
- export { arrow, callout, dot } from './figure/annotate.js';
55
- export type { ArrowOptions, CalloutOptions } from './figure/annotate.js';
54
+ export { arrow, brace, bracePath, callout, dot } from './figure/annotate.js';
55
+ export type { ArrowOptions, BraceOptions, BracedOptions, CalloutOptions } from './figure/annotate.js';
56
56
  export { typesetElement } from './figure/typeset.js';
57
57
  export type { EquationElement } from './figure/typeset.js';
58
58
  export { equationFromTex, equationMarks, equationNode } from './figure/equation.js';
package/dist/index.js CHANGED
@@ -23,7 +23,7 @@ export { axes, numberLine, numberPlane } from './figure/axis.js';
23
23
  export { coordsOf, pointOf, scaleOf, scaled, unscaled } from './figure/scale.js';
24
24
  export { labelFor, tickStep, ticksOn } from './figure/ticks.js';
25
25
  export { flatten, group, shape, text } from './figure/node.js';
26
- export { circumscribe, fadeIn, fadeOut, fadeTo, draw, flash, growFrom, indicate, morph, morphEquation, moveAlong, moveBy, rotate, scale } from './figure/animation.js';
26
+ export { circumscribe, countTo, fadeIn, fadeOut, fadeTo, draw, flash, growFrom, indicate, morph, morphEquation, moveAlong, moveBy, rotate, scale } from './figure/animation.js';
27
27
  export { Timeline } from './figure/timeline.js';
28
28
  export { lengthOf, pointAlong } from './figure/length.js';
29
29
  export { trimPath } from './figure/trim.js';
@@ -31,7 +31,7 @@ export { alignPaths, lerpPath } from './figure/morph.js';
31
31
  export { at, durationOf, loops, sameMarks } from './figure/figure.js';
32
32
  export { pathData, paintSvg, svgElements, svgMarkup } from './paint/svg.js';
33
33
  export { paintCanvas } from './paint/canvas.js';
34
- export { arrow, callout, dot } from './figure/annotate.js';
34
+ export { arrow, brace, bracePath, callout, dot } from './figure/annotate.js';
35
35
  export { typesetElement } from './figure/typeset.js';
36
36
  export { equationFromTex, equationMarks, equationNode } from './figure/equation.js';
37
37
  export { glyphToken, matchGlyphs } from './figure/equation-match.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@altpsyche/maths",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "The mathematics AltPsyche's figures are drawn from: vectors, matrices, curves, and a value walked over time.",
5
5
  "license": "MIT",
6
6
  "author": "Siva",