@altpsyche/maths 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +126 -353
  2. package/dist/figure/animation.d.ts +6 -4
  3. package/dist/figure/animation.js +20 -4
  4. package/dist/figure/annotate.d.ts +3 -2
  5. package/dist/figure/annotate.js +3 -2
  6. package/dist/figure/axis.js +5 -8
  7. package/dist/figure/axis3.d.ts +8 -0
  8. package/dist/figure/axis3.js +13 -0
  9. package/dist/figure/boolean.js +2 -2
  10. package/dist/figure/equation.d.ts +1 -1
  11. package/dist/figure/equation.js +2 -2
  12. package/dist/figure/field.d.ts +6 -6
  13. package/dist/figure/field.js +5 -6
  14. package/dist/figure/field3.d.ts +50 -0
  15. package/dist/figure/field3.js +60 -0
  16. package/dist/figure/figure.d.ts +4 -4
  17. package/dist/figure/figure.js +4 -4
  18. package/dist/figure/frames.d.ts +1 -1
  19. package/dist/figure/frames.js +4 -4
  20. package/dist/figure/grid.d.ts +19 -0
  21. package/dist/figure/grid.js +32 -0
  22. package/dist/figure/inside.d.ts +2 -2
  23. package/dist/figure/inside.js +3 -4
  24. package/dist/figure/length.d.ts +1 -3
  25. package/dist/figure/length.js +7 -10
  26. package/dist/figure/mark.d.ts +7 -2
  27. package/dist/figure/node.js +2 -3
  28. package/dist/figure/path-data.js +2 -3
  29. package/dist/figure/path.d.ts +1 -1
  30. package/dist/figure/path.js +3 -3
  31. package/dist/figure/plot.d.ts +1 -1
  32. package/dist/figure/plot.js +9 -13
  33. package/dist/figure/scale.d.ts +2 -2
  34. package/dist/figure/scale.js +3 -3
  35. package/dist/figure/section.d.ts +5 -2
  36. package/dist/figure/section.js +9 -16
  37. package/dist/figure/space.d.ts +8 -91
  38. package/dist/figure/space.js +7 -110
  39. package/dist/figure/surface3.d.ts +62 -0
  40. package/dist/figure/surface3.js +58 -0
  41. package/dist/index.d.ts +18 -12
  42. package/dist/index.js +10 -7
  43. package/dist/paint/number.js +1 -2
  44. package/dist/paint/svg.d.ts +29 -4
  45. package/dist/paint/svg.js +49 -14
  46. package/dist/values/colour.d.ts +43 -0
  47. package/dist/values/colour.js +110 -0
  48. package/package.json +1 -1
@@ -17,15 +17,14 @@
17
17
  */
18
18
  import { interval } from '../values/interval.js';
19
19
  import { vec2 } from '../values/vec2.js';
20
- import { pointOf, scaled } from './scale.js';
20
+ import { pointOf, toUnits } from './scale.js';
21
21
  import { group, shape } from './node.js';
22
22
  import { line, polygon, straight } from './path.js';
23
23
  /**
24
24
  * How many pieces a curve is cut into when a figure does not say.
25
25
  *
26
- * A sine over two turns at this count leaves the drawn curve within 3.3e-4
27
- * figure units of the true one, which is under a tenth of a pixel on the largest
28
- * surface anything here is drawn at.
26
+ * The count is what draws a sine over two turns under a tenth of a pixel from
27
+ * the true one on the largest surface anything here is drawn at.
29
28
  */
30
29
  const SAMPLES = 96;
31
30
  /**
@@ -34,10 +33,8 @@ const SAMPLES = 96;
34
33
  *
35
34
  * An end with evenly spaced neighbours takes the three-point one-sided
36
35
  * difference, which is second order like the middle and exact for a quadratic.
37
- * An end that was cut at the edge of the graph is not evenly spaced, so it takes
38
- * the two-point difference instead: measured on a parabola at 64 samples, the
39
- * three-point ends leave an uncut curve exact where the two-point ends leave it
40
- * 3.7e-4 figure units out.
36
+ * An end that was cut at the edge of the graph is not evenly spaced, so the
37
+ * three-point form does not hold there and it takes the two-point difference.
41
38
  */
42
39
  function slopes(xs, ys) {
43
40
  const last = xs.length - 1;
@@ -95,7 +92,7 @@ function crossing(of, drawable, bounds, inside, outside) {
95
92
  * that makes the cubic pass through both samples at both slopes.
96
93
  */
97
94
  export function plot(coords, of, options = {}) {
98
- const samples = Math.max(1, Math.round(options.samples ?? SAMPLES));
95
+ const samples = Math.max(1, Math.round(options.resolution ?? SAMPLES));
99
96
  const { from, to } = interval.ordered(options.over ?? coords.x.graph);
100
97
  if (!(to > from))
101
98
  return [];
@@ -165,7 +162,7 @@ export function plot(coords, of, options = {}) {
165
162
  * samplings that agree to within a sample.
166
163
  */
167
164
  export function areaUnder(coords, of, over, options = {}) {
168
- const foot = scaled(coords.y, interval.clampTo(coords.y.graph, options.baseline ?? 0));
165
+ const foot = toUnits(coords.y, interval.clampTo(coords.y.graph, options.baseline ?? 0));
169
166
  return plot(coords, of, { ...options, over }).map((top) => {
170
167
  const last = top.curves.length > 0 ? top.curves[top.curves.length - 1].to : top.start;
171
168
  const under = vec2(last.x, foot);
@@ -199,9 +196,8 @@ export function riemannBars(name, coords, of, options = {}) {
199
196
  if (!Number.isFinite(y))
200
197
  continue;
201
198
  const top = interval.clampTo(coords.y.graph, y);
202
- // Built from its four corners rather than from a corner and a size. A bar
203
- // whose top is held on the graph's own edge has that edge as an exact
204
- // number, and adding a height back on to the near corner overshoots it.
199
+ // Built from four corners rather than a corner and a size: a bar whose top is held on the
200
+ // graph's own edge has that edge exactly, and adding a height back on overshoots it.
205
201
  const corner = pointOf(coords, left, foot);
206
202
  const far = pointOf(coords, right, top);
207
203
  children.push(shape(String(bar), polygon([corner, vec2(far.x, corner.y), far, vec2(corner.x, far.y)]), {}));
@@ -19,10 +19,10 @@ export interface Scale {
19
19
  }
20
20
  export declare function scaleOf(graph: Interval, units: Interval): Scale;
21
21
  /** A number on the axis, as a place in the figure's own units. */
22
- export declare function scaled(scale: Scale, value: number): number;
22
+ export declare function toUnits(scale: Scale, value: number): number;
23
23
  /** A place in the figure's own units, as a number on the axis, which is what a
24
24
  * reader pointing at the picture is asking for. */
25
- export declare function unscaled(scale: Scale, place: number): number;
25
+ export declare function toGraph(scale: Scale, place: number): number;
26
26
  export interface Coords {
27
27
  readonly x: Scale;
28
28
  readonly y: Scale;
@@ -15,12 +15,12 @@ export function scaleOf(graph, units) {
15
15
  return { graph, units };
16
16
  }
17
17
  /** A number on the axis, as a place in the figure's own units. */
18
- export function scaled(scale, value) {
18
+ export function toUnits(scale, value) {
19
19
  return interval.remap(value, scale.graph, scale.units);
20
20
  }
21
21
  /** A place in the figure's own units, as a number on the axis, which is what a
22
22
  * reader pointing at the picture is asking for. */
23
- export function unscaled(scale, place) {
23
+ export function toGraph(scale, place) {
24
24
  return interval.remap(place, scale.units, scale.graph);
25
25
  }
26
26
  export function coordsOf(x, y) {
@@ -28,5 +28,5 @@ export function coordsOf(x, y) {
28
28
  }
29
29
  /** A pair of graph numbers as a point in the figure's own units. */
30
30
  export function pointOf(coords, x, y) {
31
- return vec2(scaled(coords.x, x), scaled(coords.y, y));
31
+ return vec2(toUnits(coords.x, x), toUnits(coords.y, y));
32
32
  }
@@ -19,8 +19,11 @@ export interface Plane {
19
19
  normal: Vec3;
20
20
  }
21
21
  export interface SectionOptions {
22
- u?: Interval;
23
- v?: Interval;
22
+ /** The runs of the two parameters, nothing to one each unless named. */
23
+ over?: {
24
+ u?: Interval;
25
+ v?: Interval;
26
+ };
24
27
  resolution?: number | {
25
28
  u: number;
26
29
  v: number;
@@ -13,6 +13,7 @@
13
13
  import { interval } from '../values/interval.js';
14
14
  import { vec3 } from '../values/vec3.js';
15
15
  import { TOLERANCE } from './tolerance.js';
16
+ import { cornersOf, stepsOf } from './grid.js';
16
17
  /** Which edge of a cell each pair of corners is, going round from the corner at
17
18
  * the low end of both parameters. */
18
19
  const EDGES = [
@@ -128,20 +129,13 @@ function joinEnds(runs, tolerance) {
128
129
  * so drawing the points as they are given draws the loop closed.
129
130
  */
130
131
  export function sectionOf(of, plane, options = {}) {
131
- const { u = interval(0, 1), v = interval(0, 1), resolution = 24, tolerance = TOLERANCE } = options;
132
- const steps = typeof resolution === 'number' ? { u: resolution, v: resolution } : resolution;
132
+ const { over = {}, resolution = 24, tolerance = TOLERANCE } = options;
133
+ const u = over.u ?? interval(0, 1);
134
+ const v = over.v ?? interval(0, 1);
135
+ const steps = stepsOf(resolution, 'u', 'v');
133
136
  const facing = vec3.normalize(plane.normal);
134
- const sample = [];
135
- const gap = [];
136
- for (let i = 0; i <= steps.u; i += 1) {
137
- sample.push([]);
138
- gap.push([]);
139
- for (let j = 0; j <= steps.v; j += 1) {
140
- const point = of(interval.at(u, i / steps.u), interval.at(v, j / steps.v));
141
- sample[i].push(point);
142
- gap[i].push(vec3.dot(facing, vec3.sub(point, plane.point)));
143
- }
144
- }
137
+ const sample = cornersOf(of, u, v, steps);
138
+ const gap = sample.map((column) => column.map((point) => vec3.dot(facing, vec3.sub(point, plane.point))));
145
139
  const points = [];
146
140
  const found = new Map();
147
141
  const segments = [];
@@ -169,9 +163,8 @@ export function sectionOf(of, plane, options = {}) {
169
163
  }
170
164
  if (crossed.length !== 4)
171
165
  continue;
172
- // A cell whose corners alternate in sign has two ways to be joined and the
173
- // grid cannot tell them apart. The middle of the cell decides: the pair of
174
- // corners it agrees with is the pair the curve runs around.
166
+ // A cell whose corners alternate in sign has two ways to be joined, so the middle of the cell
167
+ // decides: the pair of corners it agrees with is the pair the curve runs around.
175
168
  const middle = (gaps[0] + gaps[1] + gaps[2] + gaps[3]) / 4;
176
169
  const pairs = middle >= 0 === gaps[0] >= 0 ? [[0, 1], [2, 3]] : [[3, 0], [1, 2]];
177
170
  for (const [first, second] of pairs) {
@@ -2,16 +2,17 @@
2
2
  * Marks placed at points in space, which a camera turns into the flat nodes the
3
3
  * rest of this package already draws.
4
4
  *
5
- * Every builder here hands back a group, because a shape in space is not always
6
- * one shape on the page: a line running past the eye comes back as the pieces of
7
- * it the eye can see, and a shape wholly behind the eye comes back as a group
8
- * with no children, which flattens to no marks rather than to a mark of nothing.
5
+ * A builder that draws one thing hands back a group, because a shape in space is
6
+ * not always one shape on the page: a line running past the eye comes back as
7
+ * the pieces of it the eye can see, and a shape wholly behind the eye comes back
8
+ * as a group with no children, which flattens to no marks rather than to a mark
9
+ * of nothing. A builder that draws pieces for a scene to sort hands back those
10
+ * pieces with the points they came from, which is what says how far off each is.
9
11
  */
10
12
  import { type Vec3 } from '../values/vec3.js';
11
13
  import type { Vec2 } from '../values/vec2.js';
14
+ import type { Fill } from './mark.js';
12
15
  import { type GroupNode, type Node, type Style, type TextOptions } from './node.js';
13
- import { type Interval } from '../values/interval.js';
14
- import type { Colour, Fill, Stroke } from './mark.js';
15
16
  import { type ArrowOptions } from './annotate.js';
16
17
  import type { Camera3 } from './camera.js';
17
18
  export type Polyline3Options = Style & {
@@ -53,7 +54,7 @@ export type SpaceItem = {
53
54
  * sort is stable, and a picture that changed which of two touching faces was on
54
55
  * top between frames would flicker.
55
56
  */
56
- export declare function space(name: string, items: readonly SpaceItem[], camera: Camera3): GroupNode;
57
+ export declare function scene3(name: string, items: readonly SpaceItem[], camera: Camera3): GroupNode;
57
58
  export type Arrow3Options = ArrowOptions;
58
59
  /**
59
60
  * A line between two points in space with a head at the far end.
@@ -67,87 +68,3 @@ export type Arrow3Options = ArrowOptions;
67
68
  * with no head, since the place the head belongs is not on the page.
68
69
  */
69
70
  export declare function arrow3(name: string, from: Vec3, to: Vec3, camera: Camera3, options: Arrow3Options): GroupNode;
70
- export type VectorField3Options = ArrowOptions & {
71
- /** The box the samples are taken in, nothing to one each way unless named. */
72
- over?: {
73
- x?: Interval;
74
- y?: Interval;
75
- z?: Interval;
76
- };
77
- /** How many samples each way. One number is all three. */
78
- resolution?: number | {
79
- x: number;
80
- y: number;
81
- z: number;
82
- };
83
- /** How long an arrow is, in the world's own units, from the magnitude of the
84
- * vector at its own sample. */
85
- lengthOf: (magnitude: number) => number;
86
- /** What colour an arrow is, from that same magnitude. */
87
- colourFor: (magnitude: number) => Colour;
88
- };
89
- /**
90
- * The arrows of a field sampled over a box in space, and the points each was
91
- * drawn from, for a figure that sorts them among pieces of its own.
92
- *
93
- * An arrow is measured in the world's own units rather than the figure's, unlike
94
- * the arrows of a flat field, because a length in space is what perspective is
95
- * for: a far arrow drawing shorter than a near one of the same magnitude is what
96
- * says which is far. Its head is still in figure units, since the head is drawn
97
- * on the page.
98
- *
99
- * A sample sits at the middle of its cell and the count is fixed by the
100
- * resolution, so a gate can hold it as the eye moves. A sample whose vector is
101
- * nothing draws no arrow there.
102
- */
103
- export declare function fieldArrows3(name: string, of: (at: Vec3) => Vec3, camera: Camera3, options: VectorField3Options): SpaceItem[];
104
- /** A field of vectors in space, drawn as arrows ordered back to front. */
105
- export declare function vectorField3(name: string, of: (at: Vec3) => Vec3, camera: Camera3, options: VectorField3Options): GroupNode;
106
- export type Surface3Options = {
107
- /** The run of the first parameter, nothing to one unless named. */
108
- u?: Interval;
109
- /** The run of the second parameter, nothing to one unless named. */
110
- v?: Interval;
111
- /** How many cells each way. */
112
- resolution?: number | {
113
- u: number;
114
- v: number;
115
- };
116
- /**
117
- * The colour a cell is filled with, given how squarely it faces the light: one
118
- * where it faces the light head on, a half where it is edge on, and nothing
119
- * where it faces straight away.
120
- *
121
- * The author supplies this rather than naming two colours to mix, because
122
- * mixing two colours means reading them, and a colour here is any CSS colour
123
- * written as text with nothing that parses one.
124
- */
125
- shade: (amount: number) => Fill;
126
- /** Which way the light comes from, over the shoulder of an eye on the positive
127
- * z axis unless named. */
128
- light?: Vec3;
129
- /** Whether a cell facing away from the eye is left out. Off by default, because
130
- * a count that changes as the camera turns is a count no gate can hold. */
131
- cull?: boolean;
132
- stroke?: Stroke;
133
- };
134
- /**
135
- * The cells a surface is made of, before they are put in an order.
136
- *
137
- * Cells rather than one shape is what makes the depth sort work at all: a surface
138
- * that folds over itself has no one place in a painting order, and pieces small
139
- * enough to be flat do.
140
- *
141
- * A scene holding a surface and a plane that cuts through it has to sort all of
142
- * their cells together, since two surfaces sorted apart are two groups and the
143
- * second is painted over the first whichever way round they stand. Each cell
144
- * carries the name it was given ahead of its own place in the grid, so an
145
- * animation can still name a whole surface once its cells are mixed with
146
- * another's.
147
- */
148
- export declare function surfaceCells(name: string, of: (u: number, v: number) => Vec3, camera: Camera3, options: Surface3Options): SpaceItem[];
149
- /**
150
- * A surface given by a function of two parameters, drawn as a grid of
151
- * four-cornered cells ordered back to front.
152
- */
153
- export declare function surface3(name: string, of: (u: number, v: number) => Vec3, camera: Camera3, options: Surface3Options): GroupNode;
@@ -2,15 +2,16 @@
2
2
  * Marks placed at points in space, which a camera turns into the flat nodes the
3
3
  * rest of this package already draws.
4
4
  *
5
- * Every builder here hands back a group, because a shape in space is not always
6
- * one shape on the page: a line running past the eye comes back as the pieces of
7
- * it the eye can see, and a shape wholly behind the eye comes back as a group
8
- * with no children, which flattens to no marks rather than to a mark of nothing.
5
+ * A builder that draws one thing hands back a group, because a shape in space is
6
+ * not always one shape on the page: a line running past the eye comes back as
7
+ * the pieces of it the eye can see, and a shape wholly behind the eye comes back
8
+ * as a group with no children, which flattens to no marks rather than to a mark
9
+ * of nothing. A builder that draws pieces for a scene to sort hands back those
10
+ * pieces with the points they came from, which is what says how far off each is.
9
11
  */
10
12
  import { vec3 } from '../values/vec3.js';
11
13
  import { circle, line, polygon, polyline } from './path.js';
12
14
  import { group, shape, text } from './node.js';
13
- import { interval } from '../values/interval.js';
14
15
  import { arrow } from './annotate.js';
15
16
  /**
16
17
  * Where along a segment the near plane is crossed.
@@ -104,7 +105,7 @@ function middleDepth(points, camera) {
104
105
  * sort is stable, and a picture that changed which of two touching faces was on
105
106
  * top between frames would flicker.
106
107
  */
107
- export function space(name, items, camera) {
108
+ export function scene3(name, items, camera) {
108
109
  const measured = items.map((item) => ({ node: item.node, depth: middleDepth(item.points, camera) }));
109
110
  measured.sort((a, b) => b.depth - a.depth);
110
111
  return group(name, measured.map((item) => item.node));
@@ -136,107 +137,3 @@ export function arrow3(name, from, to, camera, options) {
136
137
  return group(name, []);
137
138
  return arrow(name, tail, end.at, options);
138
139
  }
139
- function gridOf(resolution) {
140
- return typeof resolution === 'number' ? { x: resolution, y: resolution, z: resolution } : resolution;
141
- }
142
- /**
143
- * The arrows of a field sampled over a box in space, and the points each was
144
- * drawn from, for a figure that sorts them among pieces of its own.
145
- *
146
- * An arrow is measured in the world's own units rather than the figure's, unlike
147
- * the arrows of a flat field, because a length in space is what perspective is
148
- * for: a far arrow drawing shorter than a near one of the same magnitude is what
149
- * says which is far. Its head is still in figure units, since the head is drawn
150
- * on the page.
151
- *
152
- * A sample sits at the middle of its cell and the count is fixed by the
153
- * resolution, so a gate can hold it as the eye moves. A sample whose vector is
154
- * nothing draws no arrow there.
155
- */
156
- export function fieldArrows3(name, of, camera, options) {
157
- const { over = {}, resolution = 6, lengthOf, colourFor, ...rest } = options;
158
- const box = {
159
- x: interval.ordered(over.x ?? interval(0, 1)),
160
- y: interval.ordered(over.y ?? interval(0, 1)),
161
- z: interval.ordered(over.z ?? interval(0, 1)),
162
- };
163
- const steps = gridOf(resolution);
164
- const items = [];
165
- for (let i = 0; i < steps.x; i += 1) {
166
- for (let j = 0; j < steps.y; j += 1) {
167
- for (let k = 0; k < steps.z; k += 1) {
168
- 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));
169
- const vector = of(from);
170
- const magnitude = vec3.magnitude(vector);
171
- const length = lengthOf(magnitude);
172
- if (!(magnitude > 0) || !Number.isFinite(length) || !(length > 0))
173
- continue;
174
- const to = vec3.add(from, vec3.scale(vector, length / magnitude));
175
- items.push({
176
- points: [from, to],
177
- node: arrow3(`${name}/${i}-${j}-${k}`, from, to, camera, {
178
- ...rest,
179
- stroke: { ...rest.stroke, colour: colourFor(magnitude) },
180
- }),
181
- });
182
- }
183
- }
184
- }
185
- return items;
186
- }
187
- /** A field of vectors in space, drawn as arrows ordered back to front. */
188
- export function vectorField3(name, of, camera, options) {
189
- return space(name, fieldArrows3('arrow', of, camera, options), camera);
190
- }
191
- function resolutionOf(resolution) {
192
- return typeof resolution === 'number' ? { u: resolution, v: resolution } : resolution;
193
- }
194
- /**
195
- * The cells a surface is made of, before they are put in an order.
196
- *
197
- * Cells rather than one shape is what makes the depth sort work at all: a surface
198
- * that folds over itself has no one place in a painting order, and pieces small
199
- * enough to be flat do.
200
- *
201
- * A scene holding a surface and a plane that cuts through it has to sort all of
202
- * their cells together, since two surfaces sorted apart are two groups and the
203
- * second is painted over the first whichever way round they stand. Each cell
204
- * carries the name it was given ahead of its own place in the grid, so an
205
- * animation can still name a whole surface once its cells are mixed with
206
- * another's.
207
- */
208
- export function surfaceCells(name, of, camera, options) {
209
- const { u = interval(0, 1), v = interval(0, 1), resolution = 24, shade, light = vec3(0, 0, 1), cull = false, stroke } = options;
210
- const steps = resolutionOf(resolution);
211
- const toLight = vec3.normalize(light);
212
- const items = [];
213
- for (let i = 0; i < steps.u; i += 1) {
214
- for (let j = 0; j < steps.v; j += 1) {
215
- const corners = [
216
- of(interval.at(u, i / steps.u), interval.at(v, j / steps.v)),
217
- of(interval.at(u, (i + 1) / steps.u), interval.at(v, j / steps.v)),
218
- of(interval.at(u, (i + 1) / steps.u), interval.at(v, (j + 1) / steps.v)),
219
- of(interval.at(u, i / steps.u), interval.at(v, (j + 1) / steps.v)),
220
- ];
221
- const normal = vec3.normalize(vec3.cross(vec3.sub(corners[1], corners[0]), vec3.sub(corners[3], corners[0])));
222
- if (cull) {
223
- const middle = corners.reduce((sum, corner) => vec3.add(sum, vec3.scale(corner, 1 / 4)), vec3.ZERO);
224
- if (vec3.dot(normal, vec3.sub(camera.eye, middle)) <= 0)
225
- continue;
226
- }
227
- const fill = shade((vec3.dot(normal, toLight) + 1) / 2);
228
- items.push({
229
- points: corners,
230
- node: polyline3(`${name}/${i}-${j}`, corners, camera, { close: true, fill, stroke }),
231
- });
232
- }
233
- }
234
- return items;
235
- }
236
- /**
237
- * A surface given by a function of two parameters, drawn as a grid of
238
- * four-cornered cells ordered back to front.
239
- */
240
- export function surface3(name, of, camera, options) {
241
- return space(name, surfaceCells('cell', of, camera, options), camera);
242
- }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * A surface given by a function of two parameters, drawn as a grid of cells.
3
+ *
4
+ * Cells rather than one shape is what makes the depth sort work at all: a
5
+ * surface that folds over itself has no one place in a painting order, and
6
+ * pieces small enough to be flat do.
7
+ */
8
+ import { type Interval } from '../values/interval.js';
9
+ import { type Vec3 } from '../values/vec3.js';
10
+ import type { Fill, Stroke } from './mark.js';
11
+ import type { Camera3 } from './camera.js';
12
+ import { type SpaceItem } from './space.js';
13
+ import type { GroupNode } from './node.js';
14
+ export type Surface3Options = {
15
+ /** The runs of the two parameters, nothing to one each unless named. */
16
+ over?: {
17
+ u?: Interval;
18
+ v?: Interval;
19
+ };
20
+ /** How many cells each way. */
21
+ resolution?: number | {
22
+ u: number;
23
+ v: number;
24
+ };
25
+ /**
26
+ * The colour a cell is filled with, given how squarely it faces the light: one
27
+ * where it faces the light head on, a half where it is edge on, and nothing
28
+ * where it faces straight away.
29
+ *
30
+ * The author supplies this rather than naming two colours to mix, because
31
+ * mixing two colours means reading them, and a colour here is any CSS colour
32
+ * written as text with nothing that parses one.
33
+ */
34
+ shade: (amount: number) => Fill;
35
+ /** Which way the light comes from, over the shoulder of an eye on the positive
36
+ * z axis unless named. */
37
+ light?: Vec3;
38
+ /** Whether a cell facing away from the eye is left out. Off by default, because
39
+ * a count that changes as the camera turns is a count no gate can hold. */
40
+ cull?: boolean;
41
+ stroke?: Stroke;
42
+ };
43
+ /**
44
+ * The cells a surface is made of, before they are put in an order.
45
+ *
46
+ * Cells rather than one shape is what makes the depth sort work at all: a surface
47
+ * that folds over itself has no one place in a painting order, and pieces small
48
+ * enough to be flat do.
49
+ *
50
+ * A scene holding a surface and a plane that cuts through it has to sort all of
51
+ * their cells together, since two surfaces sorted apart are two groups and the
52
+ * second is painted over the first whichever way round they stand. Each cell
53
+ * carries the name it was given ahead of its own place in the grid, so an
54
+ * animation can still name a whole surface once its cells are mixed with
55
+ * another's.
56
+ */
57
+ export declare function surfaceCells(name: string, of: (u: number, v: number) => Vec3, camera: Camera3, options: Surface3Options): SpaceItem[];
58
+ /**
59
+ * A surface given by a function of two parameters, drawn as a grid of
60
+ * four-cornered cells ordered back to front.
61
+ */
62
+ export declare function surface3(name: string, of: (u: number, v: number) => Vec3, camera: Camera3, options: Surface3Options): GroupNode;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * A surface given by a function of two parameters, drawn as a grid of cells.
3
+ *
4
+ * Cells rather than one shape is what makes the depth sort work at all: a
5
+ * surface that folds over itself has no one place in a painting order, and
6
+ * pieces small enough to be flat do.
7
+ */
8
+ import { interval } from '../values/interval.js';
9
+ import { vec3 } from '../values/vec3.js';
10
+ import { cornersOf, stepsOf } from './grid.js';
11
+ import { polyline3, scene3 } from './space.js';
12
+ /**
13
+ * The cells a surface is made of, before they are put in an order.
14
+ *
15
+ * Cells rather than one shape is what makes the depth sort work at all: a surface
16
+ * that folds over itself has no one place in a painting order, and pieces small
17
+ * enough to be flat do.
18
+ *
19
+ * A scene holding a surface and a plane that cuts through it has to sort all of
20
+ * their cells together, since two surfaces sorted apart are two groups and the
21
+ * second is painted over the first whichever way round they stand. Each cell
22
+ * carries the name it was given ahead of its own place in the grid, so an
23
+ * animation can still name a whole surface once its cells are mixed with
24
+ * another's.
25
+ */
26
+ export function surfaceCells(name, of, camera, options) {
27
+ const { over = {}, resolution = 24, shade, light = vec3(0, 0, 1), cull = false, stroke } = options;
28
+ const u = over.u ?? interval(0, 1);
29
+ const v = over.v ?? interval(0, 1);
30
+ const steps = stepsOf(resolution, 'u', 'v');
31
+ const grid = cornersOf(of, u, v, steps);
32
+ const toLight = vec3.normalize(light);
33
+ const items = [];
34
+ for (let i = 0; i < steps.u; i += 1) {
35
+ for (let j = 0; j < steps.v; j += 1) {
36
+ const corners = [grid[i][j], grid[i + 1][j], grid[i + 1][j + 1], grid[i][j + 1]];
37
+ const normal = vec3.normalize(vec3.cross(vec3.sub(corners[1], corners[0]), vec3.sub(corners[3], corners[0])));
38
+ if (cull) {
39
+ const middle = corners.reduce((sum, corner) => vec3.add(sum, vec3.scale(corner, 1 / 4)), vec3.ZERO);
40
+ if (vec3.dot(normal, vec3.sub(camera.eye, middle)) <= 0)
41
+ continue;
42
+ }
43
+ const fill = shade((vec3.dot(normal, toLight) + 1) / 2);
44
+ items.push({
45
+ points: corners,
46
+ node: polyline3(`${name}/${i}-${j}`, corners, camera, { close: true, fill, stroke }),
47
+ });
48
+ }
49
+ }
50
+ return items;
51
+ }
52
+ /**
53
+ * A surface given by a function of two parameters, drawn as a grid of
54
+ * four-cornered cells ordered back to front.
55
+ */
56
+ export function surface3(name, of, camera, options) {
57
+ return scene3(name, surfaceCells('cell', of, camera, options), camera);
58
+ }
package/dist/index.d.ts CHANGED
@@ -8,6 +8,8 @@
8
8
  * Nothing below the line may import anything above it.
9
9
  */
10
10
  export { clamp, inverseLerp, lerp, remap } from './values/scalar.js';
11
+ export { colourOf, colourText, lerpColour } from './values/colour.js';
12
+ export type { Rgba } from './values/colour.js';
11
13
  export { curveFor, easeIn, easeOut, linear, smoothstep } from './values/ease.js';
12
14
  export type { Curve } from './values/ease.js';
13
15
  export { vec2 } from './values/vec2.js';
@@ -19,10 +21,10 @@ export type { Interval } from './values/interval.js';
19
21
  export { mat3 } from './values/mat3.js';
20
22
  export type { Mat3 } from './values/mat3.js';
21
23
  export { mat4 } from './values/mat4.js';
22
- export type { Mat4, OrthographicOptions, PerspectiveOptions } from './values/mat4.js';
24
+ export type { Mat4 } from './values/mat4.js';
23
25
  export { SAME_TIME, keyAt, sampleTrack, sampleTracks, withKey, withoutKey } from './timing/track.js';
24
26
  export type { Key, Track, TrackValue, Tracks } from './timing/track.js';
25
- export { arc, circle, line, polygon, polyline, pointCount, pointOn, rect, slopeOn, splitCurve, straight, transformPath } from './figure/path.js';
27
+ export { arc, circle, line, polygon, polyline, pointCount, pointOn, rect, splitCurve, straight, tangentOn, transformPath } from './figure/path.js';
26
28
  export type { Cubic, Path, Subpath } from './figure/path.js';
27
29
  export { pathFromData } from './figure/path-data.js';
28
30
  export { TOLERANCE } from './figure/tolerance.js';
@@ -30,7 +32,7 @@ export { areaOf } from './figure/area.js';
30
32
  export { differenceOf, intersectionOf, unionOf } from './figure/boolean.js';
31
33
  export type { BooleanOptions } from './figure/boolean.js';
32
34
  export { containsPoint, flattenPath, nearestEdge, windingAt } from './figure/inside.js';
33
- export type { Edge, FlattenOptions } from './figure/inside.js';
35
+ export type { FlatEdge, FlattenOptions } from './figure/inside.js';
34
36
  export { cutPath } from './figure/cut.js';
35
37
  export type { Cut, CutOptions } from './figure/cut.js';
36
38
  export { curveCrossings } from './figure/intersect.js';
@@ -50,13 +52,17 @@ export { axes, numberLine, numberPlane } from './figure/axis.js';
50
52
  export type { AxesOptions, NumberLineOptions, NumberPlaneOptions } from './figure/axis.js';
51
53
  export { camera3, orthographic, perspective } from './figure/camera.js';
52
54
  export type { Camera3, Camera3Choice, OrthographicChoice, PerspectiveChoice, Projected, Projection } from './figure/camera.js';
53
- export { arrow3, dot3, fieldArrows3, polyline3, space, surface3, surfaceCells, text3, vectorField3 } from './figure/space.js';
54
- export type { Arrow3Options, Polyline3Options, SpaceItem, Surface3Options, Text3Options, VectorField3Options } from './figure/space.js';
55
+ export { arrow3, dot3, polyline3, scene3, text3 } from './figure/space.js';
56
+ export type { Arrow3Options, Polyline3Options, SpaceItem, Text3Options } from './figure/space.js';
57
+ export { fieldArrows3, vectorField3 } from './figure/field3.js';
58
+ export type { VectorField3Options } from './figure/field3.js';
59
+ export { surface3, surfaceCells } from './figure/surface3.js';
60
+ export type { Surface3Options } from './figure/surface3.js';
55
61
  export { axes3 } from './figure/axis3.js';
56
62
  export type { Axes3Options } from './figure/axis3.js';
57
63
  export { sectionOf } from './figure/section.js';
58
64
  export type { Plane, SectionOptions } from './figure/section.js';
59
- export { coordsOf, pointOf, scaleOf, scaled, unscaled } from './figure/scale.js';
65
+ export { coordsOf, pointOf, scaleOf, toGraph, toUnits } from './figure/scale.js';
60
66
  export type { Coords, Scale } from './figure/scale.js';
61
67
  export { labelFor, tickStep, ticksOn } from './figure/ticks.js';
62
68
  export type { Tick } from './figure/ticks.js';
@@ -69,19 +75,19 @@ export type { PlayOptions, Span, StaggerOptions } from './figure/timeline.js';
69
75
  export { lengthOf, pointAlong } from './figure/length.js';
70
76
  export { trimPath } from './figure/trim.js';
71
77
  export { alignPaths, lerpPath } from './figure/morph.js';
72
- export { at, durationOf, loops, sameMarks, viewAt } from './figure/figure.js';
73
- export { frameTimes, framesOf } from './figure/frames.js';
78
+ export { durationOf, isLoop, marksAt, sameMarks, viewAt } from './figure/figure.js';
79
+ export { frameTimesOf, framesOf } from './figure/frames.js';
74
80
  export type { Frame, FrameStep, FramesOptions } from './figure/frames.js';
75
- export type { Figure, Values } from './figure/figure.js';
76
- export { pathData, paintSvg, svgElements, svgMarkup } from './paint/svg.js';
77
- export type { ElementMaker, PaintNode, PaintTarget, SvgElement } from './paint/svg.js';
81
+ export type { Figure, TrackValues } from './figure/figure.js';
82
+ export { paintSvg, pathToData, svgElements, svgMarkup } from './paint/svg.js';
83
+ export type { ElementMaker, PaintNode, PaintTarget, SvgElement, SvgMarkupOptions, SvgTheme } from './paint/svg.js';
78
84
  export { paintCanvas } from './paint/canvas.js';
79
85
  export type { CanvasLike } from './paint/canvas.js';
80
86
  export { arrow, brace, bracePath, callout, dot } from './figure/annotate.js';
81
87
  export type { ArrowOptions, BraceOptions, BracedOptions, CalloutOptions } from './figure/annotate.js';
82
88
  export { typesetElement } from './figure/typeset.js';
83
89
  export type { EquationElement } from './figure/typeset.js';
84
- export { equationFromTex, equationMarks, equationNode } from './figure/equation.js';
90
+ export { equationFromTex, equationNode, equationOf } from './figure/equation.js';
85
91
  export type { Equation, EquationBox, EquationOptions } from './figure/equation.js';
86
92
  export { glyphToken, matchGlyphs } from './figure/equation-match.js';
87
93
  export type { GlyphMatch } from './figure/equation-match.js';