@altpsyche/maths 0.9.1 → 0.9.3

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
@@ -201,8 +201,16 @@ Four times of one figure. A small disc walks across a larger one: clear of it, t
201
201
  point, crossing it at two, and wholly inside it. Those are the four cases this kind of code gets
202
202
  silently wrong, which is why the demo walks through all of them rather than drawing one.
203
203
 
204
- Two edges that lie on top of each other for a stretch have no one answer, and what comes back for
205
- them is decided by the tolerance rather than by the geometry.
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.
206
214
 
207
215
  ## The animations
208
216
 
@@ -141,9 +141,9 @@ export function fadeTo(target, opacity) {
141
141
  * A mark carried through a transform, geometry and weight together.
142
142
  *
143
143
  * A transform that scales makes the lines inside it thicker and the words
144
- * bigger, the way it makes everything else bigger, which is what `flatten`
145
- * already does for a group that scales. Doing less here would leave a shrinking
146
- * mark with the stroke it started at.
144
+ * bigger, the way it makes everything else bigger, which is what a group that
145
+ * scales already does to the marks under it. Doing less here would leave a
146
+ * shrinking mark with the stroke it started at.
147
147
  */
148
148
  function carried(mark, through) {
149
149
  const scale = mat3.scaleFactor(through);
@@ -28,6 +28,10 @@ export interface NumberLineOptions {
28
28
  /** Leaves the label at zero out, which is what a second axis crossing here
29
29
  * wants, since both would otherwise write the same number in the same place. */
30
30
  skipZero?: boolean;
31
+ /** The number on this line another line crosses it at. The label there is
32
+ * written below and to the left of the crossing rather than under it, since
33
+ * under it is where the other line and its head already are. */
34
+ crossedAt?: number;
31
35
  }
32
36
  /**
33
37
  * One axis as a group: the line, the ticks under `ticks`, the labels under
@@ -61,6 +65,10 @@ export interface NumberPlaneOptions {
61
65
  /** How much of the stroke a minor line is drawn with, since a grid a reader
62
66
  * notices is a grid competing with the curve on top of it. */
63
67
  minorOpacity?: number;
68
+ /** How wide a minor line is against a major one. A minor line that differs
69
+ * only in how strong its ink is reads as the same line, so the grid comes out
70
+ * flat and busy. */
71
+ minorWidth?: number;
64
72
  ticks?: number;
65
73
  }
66
74
  /**
@@ -37,14 +37,17 @@ export function numberLine(name, scale, options) {
37
37
  const gap = options.gap ?? size * 0.35;
38
38
  const { from: low, to: high } = interval.ordered(scale.units);
39
39
  const at = (along, off) => (across ? vec2(along, seat + off) : vec2(seat + off, along));
40
- // The line stops where a head begins rather than running under it, because a
41
- // line drawn to the point shows through a head that is not fully opaque.
42
- const parts = [shape('line', line(at(low + tip, 0), at(high - tip, 0)), { stroke: options.stroke })];
40
+ // The line runs the whole of the axis and each head stands beyond its end,
41
+ // rather than the heads eating into the line. Eating in leaves the outermost
42
+ // tick standing under a head instead of on the line, and the line still stops
43
+ // where a head begins, so a head that is not fully opaque has nothing showing
44
+ // through it.
45
+ const parts = [shape('line', line(at(low, 0), at(high, 0)), { stroke: options.stroke })];
43
46
  if (tip > 0 && options.fill) {
44
47
  const spread = options.spread ?? 0.6;
45
48
  parts.push(group('tips', [
46
- shape('low', head(at(low, 0), at(low + tip, 0), spread), { fill: options.fill }),
47
- shape('high', head(at(high, 0), at(high - tip, 0), spread), { fill: options.fill }),
49
+ shape('low', head(at(low - tip, 0), at(low, 0), spread), { fill: options.fill }),
50
+ shape('high', head(at(high + tip, 0), at(high, 0), spread), { fill: options.fill }),
48
51
  ]));
49
52
  }
50
53
  const step = tickStep(scale.graph, options.ticks);
@@ -57,17 +60,22 @@ export function numberLine(name, scale, options) {
57
60
  if (options.fill && size > 0) {
58
61
  // Placed by an anchor and an alignment and never by how wide the text is,
59
62
  // so a long label moves nothing else in the figure.
60
- const align = across ? 'middle' : 'end';
61
- const baseline = across ? 'hanging' : 'middle';
62
63
  const written = marked.filter((tick) => !(options.skipZero && tick.value === 0));
64
+ const off = half + gap;
63
65
  parts.push(group('labels', written.map((tick) => {
64
66
  const along = interval.remap(tick.value, scale.graph, scale.units);
65
- return text(tick.label, at(along, -(half + gap)), tick.label, size, {
67
+ const crossed = options.crossedAt !== undefined && tick.value === options.crossedAt;
68
+ const anchor = crossed
69
+ ? across
70
+ ? vec2(along - off, seat - off)
71
+ : vec2(seat - off, along - off)
72
+ : at(along, -off);
73
+ return text(tick.label, anchor, tick.label, size, {
66
74
  fill: options.fill,
67
75
  family: options.family,
68
76
  weight: options.weight,
69
- align,
70
- baseline,
77
+ align: crossed || !across ? 'end' : 'middle',
78
+ baseline: crossed || across ? 'hanging' : 'middle',
71
79
  });
72
80
  })));
73
81
  }
@@ -85,7 +93,12 @@ export function axes(name, coords, options) {
85
93
  const holdsOrigin = interval.holds(coords.x.graph, 0) && interval.holds(coords.y.graph, 0);
86
94
  const seat = (scale) => scaled(scale, interval.clampTo(scale.graph, 0));
87
95
  return group(name, [
88
- numberLine('x', coords.x, { ...options, at: seat(coords.y), direction: 'across' }),
96
+ numberLine('x', coords.x, {
97
+ ...options,
98
+ at: seat(coords.y),
99
+ direction: 'across',
100
+ crossedAt: holdsOrigin ? 0 : undefined,
101
+ }),
89
102
  numberLine('y', coords.y, { ...options, at: seat(coords.x), direction: 'up', skipZero: holdsOrigin }),
90
103
  ]);
91
104
  }
@@ -124,7 +137,12 @@ export function numberPlane(name, coords, options) {
124
137
  parts.push(group('minors', [
125
138
  group('x', gridLines(coords, step.x / minors, 'x', step.x)),
126
139
  group('y', gridLines(coords, step.y / minors, 'y', step.y)),
127
- ], { style: { stroke: options.stroke, opacity: options.minorOpacity ?? 0.4 } }));
140
+ ], {
141
+ style: {
142
+ stroke: { ...options.stroke, width: options.stroke.width * (options.minorWidth ?? 0.6) },
143
+ opacity: options.minorOpacity ?? 0.4,
144
+ },
145
+ }));
128
146
  }
129
147
  parts.push(group('majors', [group('x', gridLines(coords, step.x, 'x')), group('y', gridLines(coords, step.y, 'y'))], {
130
148
  style: { stroke: options.stroke },
@@ -24,7 +24,7 @@ import { pointOn, slopeOn } from './path.js';
24
24
  import { cutPath } from './cut.js';
25
25
  import { curveCrossings } from './intersect.js';
26
26
  import { flattenPath, nearestEdge, windingAt } from './inside.js';
27
- const TOLERANCE = 1e-6;
27
+ import { TOLERANCE } from './tolerance.js';
28
28
  /** A subpath left open closed by the straight run back to where it started,
29
29
  * since a path with an open loop has no inside for any of this to ask about. */
30
30
  function closedLoops(path) {
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import { splitCurve } from './path.js';
11
11
  import { measurePath } from './length.js';
12
- const TOLERANCE = 1e-9;
12
+ import { TOLERANCE } from './tolerance.js';
13
13
  /** The fractions for one piece, in order, with the ones that would leave a
14
14
  * piece of nothing dropped. */
15
15
  function wanted(fractions, tolerance) {
@@ -22,7 +22,8 @@
22
22
  * expressions has to be made on.
23
23
  *
24
24
  * Three things stop the walk instead of being drawn, and each names what it
25
- * found. They are written up on `refuse` below.
25
+ * found: a TeX error, a character the font has no outline for, and a macro the
26
+ * typesetter does not know.
26
27
  */
27
28
  import { mat3 } from '../values/mat3.js';
28
29
  import { vec2 } from '../values/vec2.js';
@@ -49,5 +49,9 @@ export declare function nearestEdge(loops: readonly (readonly Vec2[])[], point:
49
49
  *
50
50
  * A point sitting on the edge itself has no answer this can be right about, and
51
51
  * what comes back for one is whichever side the tolerance put it on.
52
+ *
53
+ * The path is flattened again on every call, which is a millisecond every three
54
+ * points against a path of a hundred pieces. Asking about many points wants the
55
+ * flattening made once and the count taken against it.
52
56
  */
53
57
  export declare function containsPoint(path: Path, point: Vec2, options?: FlattenOptions): boolean;
@@ -14,7 +14,7 @@
14
14
  * ray answers.
15
15
  */
16
16
  import { vec2 } from '../values/vec2.js';
17
- const TOLERANCE = 1e-6;
17
+ import { TOLERANCE } from './tolerance.js';
18
18
  /** How many times a piece may be halved, which a tolerance of zero would
19
19
  * otherwise leave unbounded. */
20
20
  const DEPTH = 24;
@@ -136,6 +136,10 @@ export function nearestEdge(loops, point) {
136
136
  *
137
137
  * A point sitting on the edge itself has no answer this can be right about, and
138
138
  * what comes back for one is whichever side the tolerance put it on.
139
+ *
140
+ * The path is flattened again on every call, which is a millisecond every three
141
+ * points against a path of a hundred pieces. Asking about many points wants the
142
+ * flattening made once and the count taken against it.
139
143
  */
140
144
  export function containsPoint(path, point, options = {}) {
141
145
  return windingAt(flattenPath(path, options), point) !== 0;
@@ -13,8 +13,14 @@
13
13
  */
14
14
  import { type Vec2 } from '../values/vec2.js';
15
15
  import type { Cubic } from './path.js';
16
- /** One place two curves meet, as a fraction along each of them and the point
17
- * itself. */
16
+ /**
17
+ * One place two curves meet, as a fraction along each of them and the point.
18
+ *
19
+ * The point is read off the first curve. Where the two cross cleanly the second
20
+ * curve's own fraction lands on the same point to the last few bits, and where
21
+ * they meet without crossing it can land as far off as the tolerance, since
22
+ * Newton has no step to take when the two are heading the same way.
23
+ */
18
24
  export interface Crossing {
19
25
  readonly alongFirst: number;
20
26
  readonly alongSecond: number;
@@ -22,7 +28,8 @@ export interface Crossing {
22
28
  }
23
29
  export interface CrossingOptions {
24
30
  /** How close two pieces come before they count as meeting, in the picture's
25
- * own units. */
31
+ * own units. It decides which meetings are told apart rather than how sharp
32
+ * one is, since Newton's method supplies the sharpness afterwards. */
26
33
  readonly tolerance?: number;
27
34
  }
28
35
  /**
@@ -12,10 +12,7 @@
12
12
  * tolerance to the last few bits.
13
13
  */
14
14
  import { vec2 } from '../values/vec2.js';
15
- /** How close two pieces come before they count as meeting. It decides which
16
- * meetings are told apart rather than how sharp one is, since Newton's method
17
- * supplies the sharpness afterwards. */
18
- const TOLERANCE = 1e-6;
15
+ import { TOLERANCE } from './tolerance.js';
19
16
  /**
20
17
  * How many pairs the search may make before it answers with where it had got to.
21
18
  *
@@ -0,0 +1,15 @@
1
+ /**
2
+ * How close two things come before this package reads them as the same place.
3
+ *
4
+ * It is a distance in the picture's own units rather than a share of anything,
5
+ * so a figure drawn in units of thousands wants its own number passed in. Every
6
+ * call that takes a tolerance takes it as a distance and defaults to this one,
7
+ * so a crossing, a cut, a flattening and a stitch all agree about what counts as
8
+ * one place without a caller having to line four numbers up.
9
+ *
10
+ * A millionth of a figure unit is a ten thousandth of a pixel at the hundred
11
+ * pixels to the unit the demos draw at, so nothing this decides is visible. What
12
+ * it decides is which meetings are told apart, since two crossings closer than
13
+ * this come back as one.
14
+ */
15
+ export declare const TOLERANCE = 0.000001;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * How close two things come before this package reads them as the same place.
3
+ *
4
+ * It is a distance in the picture's own units rather than a share of anything,
5
+ * so a figure drawn in units of thousands wants its own number passed in. Every
6
+ * call that takes a tolerance takes it as a distance and defaults to this one,
7
+ * so a crossing, a cut, a flattening and a stitch all agree about what counts as
8
+ * one place without a caller having to line four numbers up.
9
+ *
10
+ * A millionth of a figure unit is a ten thousandth of a pixel at the hundred
11
+ * pixels to the unit the demos draw at, so nothing this decides is visible. What
12
+ * it decides is which meetings are told apart, since two crossings closer than
13
+ * this come back as one.
14
+ */
15
+ export const TOLERANCE = 1e-6;
package/dist/index.d.ts CHANGED
@@ -23,6 +23,7 @@ export type { Key, Track, TrackValue, Tracks } from './timing/track.js';
23
23
  export { arc, circle, line, polygon, polyline, pointCount, pointOn, rect, slopeOn, splitCurve, straight, transformPath } from './figure/path.js';
24
24
  export type { Cubic, Path, Subpath } from './figure/path.js';
25
25
  export { pathFromData } from './figure/path-data.js';
26
+ export { TOLERANCE } from './figure/tolerance.js';
26
27
  export { areaOf } from './figure/area.js';
27
28
  export { differenceOf, intersectionOf, unionOf } from './figure/boolean.js';
28
29
  export type { BooleanOptions } from './figure/boolean.js';
package/dist/index.js CHANGED
@@ -16,6 +16,7 @@ export { mat3 } from './values/mat3.js';
16
16
  export { SAME_TIME, keyAt, sampleTrack, sampleTracks, withKey, withoutKey } from './timing/track.js';
17
17
  export { arc, circle, line, polygon, polyline, pointCount, pointOn, rect, slopeOn, splitCurve, straight, transformPath } from './figure/path.js';
18
18
  export { pathFromData } from './figure/path-data.js';
19
+ export { TOLERANCE } from './figure/tolerance.js';
19
20
  export { areaOf } from './figure/area.js';
20
21
  export { differenceOf, intersectionOf, unionOf } from './figure/boolean.js';
21
22
  export { containsPoint, flattenPath, nearestEdge, windingAt } from './figure/inside.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@altpsyche/maths",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
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",