@altpsyche/maths 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +124 -326
  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 +57 -0
  19. package/dist/figure/frames.js +43 -0
  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 +19 -11
  42. package/dist/index.js +10 -6
  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
@@ -2,10 +2,11 @@
2
2
  * The shapes an annotation is made of, composed from marks rather than being
3
3
  * marks of their own.
4
4
  *
5
- * Each one hands back a group, so an arrow is a shaft and a head with ids of
5
+ * A builder hands back a group, so an arrow is a shaft and a head with ids of
6
6
  * their own and an animation naming the arrow reaches both. Making an arrow a
7
7
  * single mark instead would mean one path that is stroked along its shaft and
8
- * filled at its head, and no mark can be both.
8
+ * filled at its head, and no mark can be both. `bracePath` is the one call here
9
+ * that hands back a path, for a figure that wants the outline and not the group.
9
10
  */
10
11
  import { vec2 } from '../values/vec2.js';
11
12
  import { circle, line, polygon, straight } from './path.js';
@@ -12,7 +12,7 @@ import { vec2 } from '../values/vec2.js';
12
12
  import { line, polygon } from './path.js';
13
13
  import { group, shape, text } from './node.js';
14
14
  import { multiplesOn, tickStep, ticksOn } from './ticks.js';
15
- import { scaled } from './scale.js';
15
+ import { toUnits } from './scale.js';
16
16
  /** A head pointing along the line, apex at the end and base back along it. */
17
17
  function head(apex, back, spread) {
18
18
  const along = vec2.normalize(vec2.sub(apex, back));
@@ -37,11 +37,8 @@ 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 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.
40
+ // Each head stands beyond the line's end, or the outermost tick stands under a head instead of on
41
+ // the line. The line stops where a head begins, so a head that is not opaque shows nothing through.
45
42
  const parts = [shape('line', line(at(low, 0), at(high, 0)), { stroke: options.stroke })];
46
43
  if (tip > 0 && options.fill) {
47
44
  const spread = options.spread ?? 0.6;
@@ -91,7 +88,7 @@ export function numberLine(name, scale, options) {
91
88
  */
92
89
  export function axes(name, coords, options) {
93
90
  const holdsOrigin = interval.holds(coords.x.graph, 0) && interval.holds(coords.y.graph, 0);
94
- const seat = (scale) => scaled(scale, interval.clampTo(scale.graph, 0));
91
+ const seat = (scale) => toUnits(scale, interval.clampTo(scale.graph, 0));
95
92
  return group(name, [
96
93
  numberLine('x', coords.x, {
97
94
  ...options,
@@ -116,7 +113,7 @@ function gridLines(coords, step, along, skipping) {
116
113
  return multiplesOn(scale.graph, step)
117
114
  .filter((value) => !(skipping && onStep(value, skipping)))
118
115
  .map((value) => {
119
- const [from, to] = ends(scaled(scale, value));
116
+ const [from, to] = ends(toUnits(scale, value));
120
117
  return shape(String(value), line(from, to), {});
121
118
  });
122
119
  }
@@ -31,6 +31,14 @@ export interface Axes3Options {
31
31
  tickLength?: number;
32
32
  /** From the projected tick to the label's own anchor, in figure units. */
33
33
  gap?: number;
34
+ /** What each axis is called, written past its far end. An axis this does not
35
+ * name carries no name, and nothing is written at all without a `fill` and a
36
+ * `size`. */
37
+ names?: {
38
+ x?: string;
39
+ y?: string;
40
+ z?: string;
41
+ };
34
42
  family?: string;
35
43
  weight?: number;
36
44
  }
@@ -42,6 +42,19 @@ function oneAxis(which, bounds, camera, options) {
42
42
  const origin = camera.project(vec3.ZERO).at;
43
43
  const parts = [polyline3('line', [at(low), at(high)], camera, { stroke: options.stroke })];
44
44
  parts.push(group('ticks', marked.map((tick) => polyline3(tick.label, [vec3.sub(at(tick.value), vec3.scale(across, half)), vec3.add(at(tick.value), vec3.scale(across, half))], camera, { stroke: options.stroke }))));
45
+ const name = options.names?.[which];
46
+ if (options.fill && size > 0 && name !== undefined) {
47
+ // Set beyond the label of the last tick, which stands at the same point and
48
+ // leans the same way, so the two would be written over each other.
49
+ parts.push(text3('name', at(high), name, size, camera, {
50
+ fill: options.fill,
51
+ family: options.family,
52
+ weight: options.weight,
53
+ align: 'middle',
54
+ baseline: 'middle',
55
+ offset: leaning(origin, camera.project(at(high)).at, gap + size),
56
+ }));
57
+ }
45
58
  if (options.fill && size > 0) {
46
59
  // The three lines cross at the origin, so only one of them writes the number
47
60
  // there and the other two would write it again in the same place.
@@ -20,7 +20,7 @@
20
20
  * outside an overlap, and a difference keeps the first path's copy of it.
21
21
  */
22
22
  import { vec2 } from '../values/vec2.js';
23
- import { pointOn, slopeOn } from './path.js';
23
+ import { pointOn, tangentOn } 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';
@@ -160,7 +160,7 @@ function sidesAgainst(pieces, other, tolerance) {
160
160
  const middle = pointOn(piece.from, piece.curve, 0.5);
161
161
  const edge = nearestEdge(other, middle);
162
162
  if (edge && edge.gap <= tolerance) {
163
- return vec2.dot(slopeOn(piece.from, piece.curve, 0.5), edge.heading) >= 0 ? 'along' : 'against';
163
+ return vec2.dot(tangentOn(piece.from, piece.curve, 0.5), edge.heading) >= 0 ? 'along' : 'against';
164
164
  }
165
165
  return windingAt(other, middle) !== 0 ? 'inside' : 'outside';
166
166
  });
@@ -14,7 +14,7 @@ export interface Equation {
14
14
  readonly marks: readonly PathMark[];
15
15
  readonly box: EquationBox;
16
16
  }
17
- export declare function equationMarks(root: EquationElement): Equation;
17
+ export declare function equationOf(root: EquationElement): Equation;
18
18
  /** One expression typeset and read, which is the two halves above in the order
19
19
  * they are always used in. */
20
20
  export declare function equationFromTex(tex: string): Promise<Equation>;
@@ -125,7 +125,7 @@ function boxOf(svg) {
125
125
  const [x = 0, y = 0, width = 0, height = 0] = numbers;
126
126
  return { x, y: -(y + height), width, height };
127
127
  }
128
- export function equationMarks(root) {
128
+ export function equationOf(root) {
129
129
  const svg = svgOf(root);
130
130
  const marks = [];
131
131
  const mark = (path, stack, suffix) => {
@@ -157,7 +157,7 @@ export function equationMarks(root) {
157
157
  /** One expression typeset and read, which is the two halves above in the order
158
158
  * they are always used in. */
159
159
  export async function equationFromTex(tex) {
160
- return equationMarks(await typesetElement(tex));
160
+ return equationOf(await typesetElement(tex));
161
161
  }
162
162
  /**
163
163
  * A typeset expression placed in a figure: one shape per glyph, fitted inside a
@@ -31,12 +31,12 @@ export interface VectorFieldOptions {
31
31
  x: number;
32
32
  y: number;
33
33
  };
34
- /** The run of x sampled, which is the whole width of the graph where it is
35
- * left out. */
36
- overX?: Interval;
37
- /** The run of y sampled, which is the whole height of the graph where it is
38
- * left out. */
39
- overY?: Interval;
34
+ /** The runs sampled, each the whole of the graph that way where it is left
35
+ * out. */
36
+ over?: {
37
+ x?: Interval;
38
+ y?: Interval;
39
+ };
40
40
  /** How long a head is, in figure units. Four times the shaft's width unless
41
41
  * named, which is what an arrow takes when nothing says. */
42
42
  head?: number;
@@ -16,11 +16,9 @@
16
16
  import { interval } from '../values/interval.js';
17
17
  import { vec2 } from '../values/vec2.js';
18
18
  import { pointOf } from './scale.js';
19
+ import { stepsOf } from './grid.js';
19
20
  import { group } from './node.js';
20
21
  import { arrow } from './annotate.js';
21
- function stepsOf(resolution) {
22
- return typeof resolution === 'number' ? { x: resolution, y: resolution } : resolution;
23
- }
24
22
  /**
25
23
  * The arrows of a field over a graph, one group per sample, named by its column
26
24
  * and row so a stagger can reach them one at a time.
@@ -40,9 +38,10 @@ function stepsOf(resolution) {
40
38
  * pointing the other, at the same magnitude.
41
39
  */
42
40
  export function vectorField(name, coords, of, options) {
43
- const steps = stepsOf(options.resolution ?? 12);
44
- const overX = interval.ordered(options.overX ?? coords.x.graph);
45
- const overY = interval.ordered(options.overY ?? coords.y.graph);
41
+ const steps = stepsOf(options.resolution ?? 12, 'x', 'y');
42
+ const over = options.over ?? {};
43
+ const overX = interval.ordered(over.x ?? coords.x.graph);
44
+ const overY = interval.ordered(over.y ?? coords.y.graph);
46
45
  const children = [];
47
46
  for (let column = 0; column < steps.x; column += 1) {
48
47
  for (let row = 0; row < steps.y; row += 1) {
@@ -0,0 +1,50 @@
1
+ /**
2
+ * A field of vectors in space, drawn as an arrow at every sample.
3
+ *
4
+ * An arrow is measured in the world's own units rather than the figure's, unlike
5
+ * the arrows of a flat field, because a length in space is what perspective is
6
+ * for. Its head stays in figure units, since the head is drawn on the page.
7
+ */
8
+ import { type Interval } from '../values/interval.js';
9
+ import { type Vec3 } from '../values/vec3.js';
10
+ import type { Colour } from './mark.js';
11
+ import type { ArrowOptions } from './annotate.js';
12
+ import type { Camera3 } from './camera.js';
13
+ import { type SpaceItem } from './space.js';
14
+ import type { GroupNode } from './node.js';
15
+ export type VectorField3Options = ArrowOptions & {
16
+ /** The box the samples are taken in, nothing to one each way unless named. */
17
+ over?: {
18
+ x?: Interval;
19
+ y?: Interval;
20
+ z?: Interval;
21
+ };
22
+ /** How many samples each way. One number is all three. */
23
+ resolution?: number | {
24
+ x: number;
25
+ y: number;
26
+ z: number;
27
+ };
28
+ /** How long an arrow is, in the world's own units, from the magnitude of the
29
+ * vector at its own sample. */
30
+ lengthOf: (magnitude: number) => number;
31
+ /** What colour an arrow is, from that same magnitude. */
32
+ colourFor: (magnitude: number) => Colour;
33
+ };
34
+ /**
35
+ * The arrows of a field sampled over a box in space, and the points each was
36
+ * drawn from, for a figure that sorts them among pieces of its own.
37
+ *
38
+ * An arrow is measured in the world's own units rather than the figure's, unlike
39
+ * the arrows of a flat field, because a length in space is what perspective is
40
+ * for: a far arrow drawing shorter than a near one of the same magnitude is what
41
+ * says which is far. Its head is still in figure units, since the head is drawn
42
+ * on the page.
43
+ *
44
+ * A sample sits at the middle of its cell and the count is fixed by the
45
+ * resolution, so a gate can hold it as the eye moves. A sample whose vector is
46
+ * nothing draws no arrow there.
47
+ */
48
+ export declare function fieldArrows3(name: string, of: (at: Vec3) => Vec3, camera: Camera3, options: VectorField3Options): SpaceItem[];
49
+ /** A field of vectors in space, drawn as arrows ordered back to front. */
50
+ export declare function vectorField3(name: string, of: (at: Vec3) => Vec3, camera: Camera3, options: VectorField3Options): GroupNode;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * A field of vectors in space, drawn as an arrow at every sample.
3
+ *
4
+ * An arrow is measured in the world's own units rather than the figure's, unlike
5
+ * the arrows of a flat field, because a length in space is what perspective is
6
+ * for. Its head stays in figure units, since the head is drawn on the page.
7
+ */
8
+ import { interval } from '../values/interval.js';
9
+ import { vec3 } from '../values/vec3.js';
10
+ import { stepsOf } from './grid.js';
11
+ import { arrow3, scene3 } from './space.js';
12
+ /**
13
+ * The arrows of a field sampled over a box in space, and the points each was
14
+ * drawn from, for a figure that sorts them among pieces of its own.
15
+ *
16
+ * An arrow is measured in the world's own units rather than the figure's, unlike
17
+ * the arrows of a flat field, because a length in space is what perspective is
18
+ * for: a far arrow drawing shorter than a near one of the same magnitude is what
19
+ * says which is far. Its head is still in figure units, since the head is drawn
20
+ * on the page.
21
+ *
22
+ * A sample sits at the middle of its cell and the count is fixed by the
23
+ * resolution, so a gate can hold it as the eye moves. A sample whose vector is
24
+ * nothing draws no arrow there.
25
+ */
26
+ export function fieldArrows3(name, of, camera, options) {
27
+ const { over = {}, resolution = 6, lengthOf, colourFor, ...rest } = options;
28
+ const box = {
29
+ x: interval.ordered(over.x ?? interval(0, 1)),
30
+ y: interval.ordered(over.y ?? interval(0, 1)),
31
+ z: interval.ordered(over.z ?? interval(0, 1)),
32
+ };
33
+ const steps = stepsOf(resolution, 'x', 'y', 'z');
34
+ const items = [];
35
+ for (let i = 0; i < steps.x; i += 1) {
36
+ for (let j = 0; j < steps.y; j += 1) {
37
+ for (let k = 0; k < steps.z; k += 1) {
38
+ const from = vec3(interval.at(box.x, (i + 0.5) / steps.x), interval.at(box.y, (j + 0.5) / steps.y), interval.at(box.z, (k + 0.5) / steps.z));
39
+ const vector = of(from);
40
+ const magnitude = vec3.magnitude(vector);
41
+ const length = lengthOf(magnitude);
42
+ if (!(magnitude > 0) || !Number.isFinite(length) || !(length > 0))
43
+ continue;
44
+ const to = vec3.add(from, vec3.scale(vector, length / magnitude));
45
+ items.push({
46
+ points: [from, to],
47
+ node: arrow3(`${name}/${i}-${j}-${k}`, from, to, camera, {
48
+ ...rest,
49
+ stroke: { ...rest.stroke, colour: colourFor(magnitude) },
50
+ }),
51
+ });
52
+ }
53
+ }
54
+ }
55
+ return items;
56
+ }
57
+ /** A field of vectors in space, drawn as arrows ordered back to front. */
58
+ export function vectorField3(name, of, camera, options) {
59
+ return scene3(name, fieldArrows3('arrow', of, camera, options), camera);
60
+ }
@@ -19,13 +19,13 @@ import type { Mark } from './mark.js';
19
19
  * is what lets geometry follow a number rather than only be moved about: a
20
20
  * radius that is keyed makes a circle that is genuinely a different circle each
21
21
  * frame. */
22
- export type Values = Record<string, TrackValue>;
22
+ export type TrackValues = Record<string, TrackValue>;
23
23
  export interface Figure {
24
24
  /** How much of the world the figure shows, in its own units. */
25
25
  extent: ExtentChoice;
26
26
  fit?: Fit;
27
27
  /** The tree, either fixed or rebuilt from the clock and the sampled values. */
28
- scene: Node | ((seconds: number, values: Values) => Node);
28
+ scene: Node | ((seconds: number, values: TrackValues) => Node);
29
29
  tracks?: Tracks;
30
30
  timeline?: Timeline;
31
31
  /** Overrides the timeline's own length, for a figure that should hold after
@@ -44,7 +44,7 @@ export interface Figure {
44
44
  }
45
45
  export declare function durationOf(figure: Figure): number;
46
46
  /** The marks a figure shows at a time. */
47
- export declare function at(figure: Figure, seconds: number): readonly Mark[];
47
+ export declare function marksAt(figure: Figure, seconds: number): readonly Mark[];
48
48
  /**
49
49
  * The matrix a painter needs at a time, in one call.
50
50
  *
@@ -58,6 +58,6 @@ export declare function viewAt(figure: Figure, seconds: number, width: number, h
58
58
  * behind that flag. The comparison is by tolerance rather than exactly, because
59
59
  * the sine and cosine a figure is built from are not specified to the last bit
60
60
  * and differ between engines. */
61
- export declare function loops(figure: Figure, tolerance?: number): boolean;
61
+ export declare function isLoop(figure: Figure, tolerance?: number): boolean;
62
62
  /** Two lists holding the same marks in the same order, to a tolerance. */
63
63
  export declare function sameMarks(one: readonly Mark[], two: readonly Mark[], tolerance?: number): boolean;
@@ -16,7 +16,7 @@ export function durationOf(figure) {
16
16
  return figure.duration ?? figure.timeline?.duration ?? 0;
17
17
  }
18
18
  /** The marks a figure shows at a time. */
19
- export function at(figure, seconds) {
19
+ export function marksAt(figure, seconds) {
20
20
  const values = figure.tracks ? sampleTracks(figure.tracks, seconds) : {};
21
21
  const tree = typeof figure.scene === 'function' ? figure.scene(seconds, values) : figure.scene;
22
22
  const marks = flatten(tree);
@@ -38,9 +38,9 @@ export function viewAt(figure, seconds, width, height) {
38
38
  * behind that flag. The comparison is by tolerance rather than exactly, because
39
39
  * the sine and cosine a figure is built from are not specified to the last bit
40
40
  * and differ between engines. */
41
- export function loops(figure, tolerance = 1e-6) {
42
- const start = at(figure, 0);
43
- const end = at(figure, durationOf(figure));
41
+ export function isLoop(figure, tolerance = 1e-6) {
42
+ const start = marksAt(figure, 0);
43
+ const end = marksAt(figure, durationOf(figure));
44
44
  return sameMarks(start, end, tolerance);
45
45
  }
46
46
  /** Two lists holding the same marks in the same order, to a tolerance. */
@@ -0,0 +1,57 @@
1
+ /**
2
+ * A figure walked at a fixed step, a frame at a time.
3
+ *
4
+ * A frame is the marks and the view read at one time, handed over together. A
5
+ * consumer that asked for them separately holds two calls it can pass different
6
+ * times, and a figure whose view moves then paints its marks through the matrix
7
+ * of some other moment.
8
+ *
9
+ * Frames come back one at a time rather than as a list. A ten second figure at
10
+ * sixty frames a second is six hundred frames of every mark it draws, and a
11
+ * recorder encodes a frame and throws it away.
12
+ */
13
+ import { type Figure } from './figure.js';
14
+ import type { Mark } from './mark.js';
15
+ import type { Mat3 } from '../values/mat3.js';
16
+ export interface Frame {
17
+ /** Its place in the walk, counting from nothing. */
18
+ index: number;
19
+ /** The time it was read at, in seconds. */
20
+ seconds: number;
21
+ marks: readonly Mark[];
22
+ /** The matrix a painter needs for these marks, built at this frame's own
23
+ * time. */
24
+ view: Mat3;
25
+ }
26
+ /**
27
+ * How the walk is stepped, as a rate or as a count.
28
+ *
29
+ * The two are different questions. A recorder knows how fast the frames play and
30
+ * needs a step of exactly one over that, or the encoded video drifts from the
31
+ * figure's own clock. A strip knows how many pictures fit across a page and wants
32
+ * them spread over the whole figure.
33
+ */
34
+ export type FrameStep = {
35
+ fps: number;
36
+ frames?: never;
37
+ } | {
38
+ frames: number;
39
+ fps?: never;
40
+ };
41
+ export type FramesOptions = FrameStep & {
42
+ /** The surface the view is built for, in whatever units a painter counts in. */
43
+ width: number;
44
+ height: number;
45
+ };
46
+ /**
47
+ * The times a walk reads, which a recorder needs before it has drawn anything to
48
+ * say how far along it is.
49
+ *
50
+ * A walk stops strictly before the duration. The frame at the duration of a
51
+ * figure that loops is its own first frame, and a recording would show it twice.
52
+ * A figure with no duration is one frame, since a picture that never moves still
53
+ * has a picture.
54
+ */
55
+ export declare function frameTimesOf(figure: Figure, step: FrameStep): number[];
56
+ /** A figure walked at a fixed step, a frame at a time. */
57
+ export declare function framesOf(figure: Figure, options: FramesOptions): Generator<Frame>;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * A figure walked at a fixed step, a frame at a time.
3
+ *
4
+ * A frame is the marks and the view read at one time, handed over together. A
5
+ * consumer that asked for them separately holds two calls it can pass different
6
+ * times, and a figure whose view moves then paints its marks through the matrix
7
+ * of some other moment.
8
+ *
9
+ * Frames come back one at a time rather than as a list. A ten second figure at
10
+ * sixty frames a second is six hundred frames of every mark it draws, and a
11
+ * recorder encodes a frame and throws it away.
12
+ */
13
+ import { marksAt, durationOf, viewAt } from './figure.js';
14
+ /**
15
+ * The times a walk reads, which a recorder needs before it has drawn anything to
16
+ * say how far along it is.
17
+ *
18
+ * A walk stops strictly before the duration. The frame at the duration of a
19
+ * figure that loops is its own first frame, and a recording would show it twice.
20
+ * A figure with no duration is one frame, since a picture that never moves still
21
+ * has a picture.
22
+ */
23
+ export function frameTimesOf(figure, step) {
24
+ const duration = durationOf(figure);
25
+ const count = step.fps === undefined
26
+ ? Math.max(1, Math.round(step.frames))
27
+ : Math.max(1, Math.round(duration * step.fps));
28
+ const gap = step.fps === undefined ? duration / count : 1 / step.fps;
29
+ return Array.from({ length: count }, (_, index) => index * gap);
30
+ }
31
+ /** A figure walked at a fixed step, a frame at a time. */
32
+ export function* framesOf(figure, options) {
33
+ const times = frameTimesOf(figure, options);
34
+ for (let index = 0; index < times.length; index += 1) {
35
+ const seconds = times[index];
36
+ yield {
37
+ index,
38
+ seconds,
39
+ marks: marksAt(figure, seconds),
40
+ view: viewAt(figure, seconds, options.width, options.height),
41
+ };
42
+ }
43
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The sampling a surface and a section share: how many steps a resolution asks
3
+ * for, and where the corners of that grid land.
4
+ *
5
+ * A cell reads its four corners out of the grid rather than working each of them
6
+ * out again. Four cells meet at every inside corner, so computing them per cell
7
+ * asks the surface for the same point four times, and the surface is the
8
+ * caller's own function with no promise about what it costs.
9
+ */
10
+ import { type Interval } from '../values/interval.js';
11
+ import type { Vec3 } from '../values/vec3.js';
12
+ /** A resolution given as one number is that many steps along every named way. */
13
+ export declare function stepsOf<K extends string>(resolution: number | Record<K, number>, ...ways: readonly K[]): Record<K, number>;
14
+ /** The corners of a grid over a surface, taking both edges of each run, so a
15
+ * grid of n by m cells is n + 1 by m + 1 corners. */
16
+ export declare function cornersOf(of: (u: number, v: number) => Vec3, u: Interval, v: Interval, steps: {
17
+ u: number;
18
+ v: number;
19
+ }): Vec3[][];
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The sampling a surface and a section share: how many steps a resolution asks
3
+ * for, and where the corners of that grid land.
4
+ *
5
+ * A cell reads its four corners out of the grid rather than working each of them
6
+ * out again. Four cells meet at every inside corner, so computing them per cell
7
+ * asks the surface for the same point four times, and the surface is the
8
+ * caller's own function with no promise about what it costs.
9
+ */
10
+ import { interval } from '../values/interval.js';
11
+ /** A resolution given as one number is that many steps along every named way. */
12
+ export function stepsOf(resolution, ...ways) {
13
+ if (typeof resolution !== 'number')
14
+ return resolution;
15
+ const steps = {};
16
+ for (const way of ways)
17
+ steps[way] = resolution;
18
+ return steps;
19
+ }
20
+ /** The corners of a grid over a surface, taking both edges of each run, so a
21
+ * grid of n by m cells is n + 1 by m + 1 corners. */
22
+ export function cornersOf(of, u, v, steps) {
23
+ const grid = [];
24
+ for (let i = 0; i <= steps.u; i += 1) {
25
+ const column = [];
26
+ for (let j = 0; j <= steps.v; j += 1) {
27
+ column.push(of(interval.at(u, i / steps.u), interval.at(v, j / steps.v)));
28
+ }
29
+ grid.push(column);
30
+ }
31
+ return grid;
32
+ }
@@ -32,7 +32,7 @@ export declare function flattenPath(path: Path, options?: FlattenOptions): Vec2[
32
32
  export declare function windingAt(loops: readonly (readonly Vec2[])[], point: Vec2): number;
33
33
  /** The nearest straight run of a flattening to a point: how far off it is, and
34
34
  * which way that run goes. */
35
- export interface Edge {
35
+ export interface FlatEdge {
36
36
  readonly gap: number;
37
37
  readonly heading: Vec2;
38
38
  }
@@ -43,7 +43,7 @@ export interface Edge {
43
43
  * near it, which the winding count cannot answer because a point on the edge
44
44
  * itself is the one place the count has no answer for.
45
45
  */
46
- export declare function nearestEdge(loops: readonly (readonly Vec2[])[], point: Vec2): Edge | null;
46
+ export declare function nearestEdge(loops: readonly (readonly Vec2[])[], point: Vec2): FlatEdge | null;
47
47
  /**
48
48
  * Whether a path holds a point, under the nonzero rule.
49
49
  *
@@ -21,10 +21,9 @@ const DEPTH = 24;
21
21
  /**
22
22
  * How many points a whole flattening may hold before it is refused.
23
23
  *
24
- * Halving is the only bound the depth gives, and sixteen million points for one
25
- * piece is a machine out of memory rather than a fine flattening. A circle of
26
- * radius 1 wants 4096 of these at a tolerance of a millionth, so the room here
27
- * is a thousandfold.
24
+ * Halving is the only bound the depth gives, so with no ceiling on the count a
25
+ * fine tolerance is a machine out of memory rather than a fine flattening. The
26
+ * ceiling sits far above what the finest tolerance a figure asks for reaches.
28
27
  */
29
28
  const POINTS = 1_000_000;
30
29
  /** How far the two controls sit from the straight run between the ends.
@@ -33,9 +33,7 @@ export declare function measurePath(path: Path): Measure;
33
33
  * Reading the fraction of the length as the parameter is exact only where the
34
34
  * curve moves at one rate, which is a straight line. Walking the table instead
35
35
  * and interpolating inside the one sample the length lands in makes a cut by
36
- * length even along a curve as well: measured on a quarter circle, cutting at
37
- * twenty even fractions of its length was 4.7e-3 of the whole out at the worst
38
- * of them and is 1.4e-4 out now.
36
+ * length even along a curve as well.
39
37
  */
40
38
  export declare function parameterAt(measured: Measured, wanted: number): number;
41
39
  /** How long a path is, in figure units, across every subpath it holds. It reads
@@ -16,13 +16,12 @@ import { clamp } from '../values/scalar.js';
16
16
  /**
17
17
  * How many samples measure one piece.
18
18
  *
19
- * Sixteen chords read a quarter circle 4 parts in ten thousand short of its true
20
- * length, since a chord cuts the corner off the arc it spans. What the count is
21
- * chosen for is the evenness of a walk rather than the total, and a share of the
22
- * length is a ratio the shortfall largely cancels out of: measured, twenty steps
23
- * along a quarter circle vary by 0.36% in size at this count. Doubling it costs
24
- * twice the work in every cut and every walk and buys a total four times closer,
25
- * which nothing here has asked for.
19
+ * A chord cuts the corner off the arc it spans, so a total read this way is
20
+ * short of the true one. What the count is chosen for is the evenness of a walk
21
+ * rather than the total, and a share of the length is a ratio the shortfall
22
+ * largely cancels out of. Doubling it costs twice the work in every cut and
23
+ * every walk and buys a total four times closer, which nothing here has asked
24
+ * for.
26
25
  */
27
26
  const SAMPLES = 16;
28
27
  function measureCurve(from, curve) {
@@ -58,9 +57,7 @@ export function measurePath(path) {
58
57
  * Reading the fraction of the length as the parameter is exact only where the
59
58
  * curve moves at one rate, which is a straight line. Walking the table instead
60
59
  * and interpolating inside the one sample the length lands in makes a cut by
61
- * length even along a curve as well: measured on a quarter circle, cutting at
62
- * twenty even fractions of its length was 4.7e-3 of the whole out at the worst
63
- * of them and is 1.4e-4 out now.
60
+ * length even along a curve as well.
64
61
  */
65
62
  export function parameterAt(measured, wanted) {
66
63
  const { upTo, total } = measured;
@@ -6,8 +6,13 @@
6
6
  * two-dimensional canvas can both do, rather than the union. A figure reaching
7
7
  * for something only one of them has would look right on the page and lose it
8
8
  * without a word in a recording, which is the worst way to find out. So there
9
- * are no filters, no blend modes, no clipping and no gradients here, and adding
10
- * one means adding it to both painters in the same change.
9
+ * are no filters, no blend modes and no clipping here, and adding one means
10
+ * adding it to both painters in the same change.
11
+ *
12
+ * A gradient is refused for a different reason, since both painters draw one.
13
+ * SVG names a gradient with an element carrying an id and a canvas names it with
14
+ * an object built from the context, and a colour here is text that both take as
15
+ * it stands.
11
16
  */
12
17
  import type { Vec2 } from '../values/vec2.js';
13
18
  import type { Path } from './path.js';
@@ -57,9 +57,8 @@ function walk(node, prefix, transform, style, into) {
57
57
  }
58
58
  const settled = inherited(style, node);
59
59
  const opacity = settled.opacity ?? 1;
60
- // A group that scales makes the lines inside it thicker, the way it makes
61
- // everything else bigger, so the width travels through the same transform the
62
- // geometry did rather than staying at the number the author typed.
60
+ // A group that scales makes the lines inside it thicker, the way it makes everything else bigger,
61
+ // so the width goes through the transform the geometry did rather than staying as it was typed.
63
62
  const scale = mat3.scaleFactor(transform);
64
63
  if (node.kind === 'shape') {
65
64
  if (!settled.fill && !settled.stroke)
@@ -139,9 +139,8 @@ export function pathFromData(d) {
139
139
  let moved = false;
140
140
  let command = '';
141
141
  let index = 0;
142
- // Held per kind because an S reflects a cubic's second control and a T a
143
- // quadratic's only one, and either falls back to the current point when the
144
- // segment before it was neither.
142
+ // Held per kind because an S reflects a cubic's second control and a T a quadratic's only one, and
143
+ // either falls back to the current point when the segment before it was neither.
145
144
  let lastCubic;
146
145
  let lastQuadratic;
147
146
  const take = (count) => {
@@ -50,7 +50,7 @@ export declare function arc(centre: Vec2, radius: number, fromAngle: number, toA
50
50
  export declare function pointOn(from: Vec2, curve: Cubic, along: number): Vec2;
51
51
  /** Which way a piece is heading at a fraction along it, which is the derivative
52
52
  * of a cubic and so a quadratic over the gaps between neighbouring points. */
53
- export declare function slopeOn(from: Vec2, curve: Cubic, along: number): Vec2;
53
+ export declare function tangentOn(from: Vec2, curve: Cubic, along: number): Vec2;
54
54
  /**
55
55
  * One piece cut into two at a fraction, both pieces drawing what the whole
56
56
  * drew, by de Casteljau's construction.