@altpsyche/maths 0.9.5 → 0.10.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 +45 -1
- package/dist/figure/axis3.d.ts +45 -0
- package/dist/figure/axis3.js +75 -0
- package/dist/figure/camera.d.ts +100 -0
- package/dist/figure/camera.js +74 -0
- package/dist/figure/extent.d.ts +7 -3
- package/dist/figure/extent.js +8 -5
- package/dist/figure/figure.d.ts +11 -1
- package/dist/figure/figure.js +13 -0
- package/dist/figure/section.d.ts +37 -0
- package/dist/figure/section.js +184 -0
- package/dist/figure/space.d.ts +103 -0
- package/dist/figure/space.js +162 -0
- package/dist/index.d.ts +11 -1
- package/dist/index.js +6 -1
- package/dist/values/mat4.d.ts +118 -0
- package/dist/values/mat4.js +193 -0
- package/package.json +1 -1
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The curve where a plane cuts a surface, found on the grid the surface is
|
|
3
|
+
* already drawn from.
|
|
4
|
+
*
|
|
5
|
+
* This is marching squares, with the plane's signed distance as the value at each
|
|
6
|
+
* grid point. A crossing point is found by walking along a cell edge to where
|
|
7
|
+
* that distance reaches nothing, and because signed distance to a plane changes
|
|
8
|
+
* evenly along a straight line, every point this finds lies on the plane exactly.
|
|
9
|
+
* It lies on the chord between two samples of the surface rather than on the
|
|
10
|
+
* surface itself, which is the whole of the error and is why halving the cell
|
|
11
|
+
* size quarters it.
|
|
12
|
+
*/
|
|
13
|
+
import { interval } from '../values/interval.js';
|
|
14
|
+
import { vec3 } from '../values/vec3.js';
|
|
15
|
+
import { TOLERANCE } from './tolerance.js';
|
|
16
|
+
/** Which edge of a cell each pair of corners is, going round from the corner at
|
|
17
|
+
* the low end of both parameters. */
|
|
18
|
+
const EDGES = [
|
|
19
|
+
[0, 1],
|
|
20
|
+
[1, 2],
|
|
21
|
+
[2, 3],
|
|
22
|
+
[3, 0],
|
|
23
|
+
];
|
|
24
|
+
function keyFor(i, j, edge) {
|
|
25
|
+
if (edge === 0)
|
|
26
|
+
return `h${i},${j}`;
|
|
27
|
+
if (edge === 1)
|
|
28
|
+
return `v${i + 1},${j}`;
|
|
29
|
+
if (edge === 2)
|
|
30
|
+
return `h${i},${j + 1}`;
|
|
31
|
+
return `v${i},${j}`;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The chains a set of segments makes, each walked from one loose end to the other
|
|
35
|
+
* and then round whatever loops are left.
|
|
36
|
+
*/
|
|
37
|
+
function chainsOf(segments, count) {
|
|
38
|
+
const next = new Map();
|
|
39
|
+
for (const [from, to] of segments) {
|
|
40
|
+
if (!next.has(from))
|
|
41
|
+
next.set(from, []);
|
|
42
|
+
if (!next.has(to))
|
|
43
|
+
next.set(to, []);
|
|
44
|
+
next.get(from).push(to);
|
|
45
|
+
next.get(to).push(from);
|
|
46
|
+
}
|
|
47
|
+
const used = segments.map(() => false);
|
|
48
|
+
const at = new Map();
|
|
49
|
+
segments.forEach(([from, to], index) => {
|
|
50
|
+
if (!at.has(from))
|
|
51
|
+
at.set(from, []);
|
|
52
|
+
if (!at.has(to))
|
|
53
|
+
at.set(to, []);
|
|
54
|
+
at.get(from).push(index);
|
|
55
|
+
at.get(to).push(index);
|
|
56
|
+
});
|
|
57
|
+
const walkFrom = (start) => {
|
|
58
|
+
const chain = [start];
|
|
59
|
+
let here = start;
|
|
60
|
+
for (;;) {
|
|
61
|
+
const step = (at.get(here) ?? []).find((index) => !used[index]);
|
|
62
|
+
if (step === undefined)
|
|
63
|
+
return chain;
|
|
64
|
+
used[step] = true;
|
|
65
|
+
const [from, to] = segments[step];
|
|
66
|
+
here = from === here ? to : from;
|
|
67
|
+
chain.push(here);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const chains = [];
|
|
71
|
+
// Loose ends first, so a chain that runs off the edge of the grid is walked
|
|
72
|
+
// from its end rather than being started in the middle and coming out as two.
|
|
73
|
+
for (let point = 0; point < count; point += 1) {
|
|
74
|
+
if ((next.get(point)?.length ?? 0) === 1) {
|
|
75
|
+
const chain = walkFrom(point);
|
|
76
|
+
if (chain.length > 1)
|
|
77
|
+
chains.push(chain);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
81
|
+
if (used[index])
|
|
82
|
+
continue;
|
|
83
|
+
const chain = walkFrom(segments[index][0]);
|
|
84
|
+
if (chain.length > 1)
|
|
85
|
+
chains.push(chain);
|
|
86
|
+
}
|
|
87
|
+
return chains;
|
|
88
|
+
}
|
|
89
|
+
/** Chains whose ends meet joined into one, which is what closes a curve that the
|
|
90
|
+
* grid split at the seam where a parameter wraps round. */
|
|
91
|
+
function joinEnds(runs, tolerance) {
|
|
92
|
+
const meets = (a, b) => vec3.magnitude(vec3.sub(a, b)) <= tolerance;
|
|
93
|
+
const open = runs.slice();
|
|
94
|
+
const done = [];
|
|
95
|
+
while (open.length > 0) {
|
|
96
|
+
let run = open.shift();
|
|
97
|
+
for (;;) {
|
|
98
|
+
if (run.length > 2 && meets(run[0], run[run.length - 1]))
|
|
99
|
+
break;
|
|
100
|
+
const found = open.findIndex((other) => meets(run[run.length - 1], other[0]) ||
|
|
101
|
+
meets(run[run.length - 1], other[other.length - 1]) ||
|
|
102
|
+
meets(run[0], other[0]) ||
|
|
103
|
+
meets(run[0], other[other.length - 1]));
|
|
104
|
+
if (found === -1)
|
|
105
|
+
break;
|
|
106
|
+
const other = open.splice(found, 1)[0];
|
|
107
|
+
if (meets(run[run.length - 1], other[0]))
|
|
108
|
+
run = [...run, ...other.slice(1)];
|
|
109
|
+
else if (meets(run[run.length - 1], other[other.length - 1]))
|
|
110
|
+
run = [...run, ...other.slice(0, -1).reverse()];
|
|
111
|
+
else if (meets(run[0], other[other.length - 1]))
|
|
112
|
+
run = [...other.slice(0, -1), ...run];
|
|
113
|
+
else
|
|
114
|
+
run = [...other.slice(1).reverse(), ...run];
|
|
115
|
+
}
|
|
116
|
+
// A run whose two ends meet is given its first point again, so a caller draws
|
|
117
|
+
// a loop by drawing the points it is handed and needs no flag.
|
|
118
|
+
if (run.length > 2 && meets(run[0], run[run.length - 1]))
|
|
119
|
+
run = [...run.slice(0, -1), run[0]];
|
|
120
|
+
done.push(run);
|
|
121
|
+
}
|
|
122
|
+
return done;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The runs of points where a plane cuts a surface, in space.
|
|
126
|
+
*
|
|
127
|
+
* A run whose two ends meet comes back with its first point repeated at the end,
|
|
128
|
+
* so drawing the points as they are given draws the loop closed.
|
|
129
|
+
*/
|
|
130
|
+
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;
|
|
133
|
+
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
|
+
}
|
|
145
|
+
const points = [];
|
|
146
|
+
const found = new Map();
|
|
147
|
+
const segments = [];
|
|
148
|
+
for (let i = 0; i < steps.u; i += 1) {
|
|
149
|
+
for (let j = 0; j < steps.v; j += 1) {
|
|
150
|
+
const corners = [sample[i][j], sample[i + 1][j], sample[i + 1][j + 1], sample[i][j + 1]];
|
|
151
|
+
const gaps = [gap[i][j], gap[i + 1][j], gap[i + 1][j + 1], gap[i][j + 1]];
|
|
152
|
+
const crossed = [];
|
|
153
|
+
for (let edge = 0; edge < 4; edge += 1) {
|
|
154
|
+
const [from, to] = EDGES[edge];
|
|
155
|
+
if (gaps[from] >= 0 === gaps[to] >= 0)
|
|
156
|
+
continue;
|
|
157
|
+
const key = keyFor(i, j, edge);
|
|
158
|
+
let index = found.get(key);
|
|
159
|
+
if (index === undefined) {
|
|
160
|
+
const along = gaps[from] / (gaps[from] - gaps[to]);
|
|
161
|
+
index = points.push(vec3.lerp(corners[from], corners[to], along)) - 1;
|
|
162
|
+
found.set(key, index);
|
|
163
|
+
}
|
|
164
|
+
crossed.push(edge);
|
|
165
|
+
}
|
|
166
|
+
if (crossed.length === 2) {
|
|
167
|
+
segments.push([found.get(keyFor(i, j, crossed[0])), found.get(keyFor(i, j, crossed[1]))]);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (crossed.length !== 4)
|
|
171
|
+
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.
|
|
175
|
+
const middle = (gaps[0] + gaps[1] + gaps[2] + gaps[3]) / 4;
|
|
176
|
+
const pairs = middle >= 0 === gaps[0] >= 0 ? [[0, 1], [2, 3]] : [[3, 0], [1, 2]];
|
|
177
|
+
for (const [first, second] of pairs) {
|
|
178
|
+
segments.push([found.get(keyFor(i, j, first)), found.get(keyFor(i, j, second))]);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const runs = chainsOf(segments, points.length).map((chain) => chain.map((index) => points[index]));
|
|
183
|
+
return joinEnds(runs, tolerance);
|
|
184
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marks placed at points in space, which a camera turns into the flat nodes the
|
|
3
|
+
* rest of this package already draws.
|
|
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.
|
|
9
|
+
*/
|
|
10
|
+
import { type Vec3 } from '../values/vec3.js';
|
|
11
|
+
import type { Vec2 } from '../values/vec2.js';
|
|
12
|
+
import { type GroupNode, type Node, type Style, type TextOptions } from './node.js';
|
|
13
|
+
import { type Interval } from '../values/interval.js';
|
|
14
|
+
import type { Fill, Stroke } from './mark.js';
|
|
15
|
+
import type { Camera3 } from './camera.js';
|
|
16
|
+
export type Polyline3Options = Style & {
|
|
17
|
+
/** Whether the last point joins back to the first. A run that the near plane
|
|
18
|
+
* cut comes back open however this is set, since closing it would draw an edge
|
|
19
|
+
* that is nowhere in the world. */
|
|
20
|
+
close?: boolean;
|
|
21
|
+
};
|
|
22
|
+
/** A run of straight segments through points in space. */
|
|
23
|
+
export declare function polyline3(name: string, points: readonly Vec3[], camera: Camera3, options?: Polyline3Options): GroupNode;
|
|
24
|
+
/** A disc marking a point in space. Its radius is in figure units and does not
|
|
25
|
+
* shrink with distance, because a dot marks where something is rather than how
|
|
26
|
+
* big it is. */
|
|
27
|
+
export declare function dot3(name: string, at: Vec3, radius: number, fill: Fill, camera: Camera3): GroupNode;
|
|
28
|
+
export type Text3Options = TextOptions & {
|
|
29
|
+
/** How far the label stands off the point it names, in figure units, applied
|
|
30
|
+
* after the point is placed. */
|
|
31
|
+
offset?: Vec2;
|
|
32
|
+
};
|
|
33
|
+
/** A label at a point in space. The letters stay upright and stay the size they
|
|
34
|
+
* are given, since a label is read rather than seen in perspective. */
|
|
35
|
+
export declare function text3(name: string, at: Vec3, content: string, size: number, camera: Camera3, options?: Text3Options): GroupNode;
|
|
36
|
+
/** A drawn piece and the points in space it was drawn from, which are what say
|
|
37
|
+
* how far off it is. */
|
|
38
|
+
export type SpaceItem = {
|
|
39
|
+
points: readonly Vec3[];
|
|
40
|
+
node: Node;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* A group whose children are ordered back to front, so the near piece is painted
|
|
44
|
+
* over the far one.
|
|
45
|
+
*
|
|
46
|
+
* This is the painter's algorithm, and what it cannot do is worth knowing before
|
|
47
|
+
* it is used: two pieces that pass through each other, and three that overlap in
|
|
48
|
+
* a ring, have no one order at all, and no comparison of depths can find one. The
|
|
49
|
+
* answer for those is smaller pieces, which is why a surface is cut into cells.
|
|
50
|
+
*
|
|
51
|
+
* Two pieces at the same depth keep the order the author gave them, since the
|
|
52
|
+
* sort is stable, and a picture that changed which of two touching faces was on
|
|
53
|
+
* top between frames would flicker.
|
|
54
|
+
*/
|
|
55
|
+
export declare function space(name: string, items: readonly SpaceItem[], camera: Camera3): GroupNode;
|
|
56
|
+
export type Surface3Options = {
|
|
57
|
+
/** The run of the first parameter, nothing to one unless named. */
|
|
58
|
+
u?: Interval;
|
|
59
|
+
/** The run of the second parameter, nothing to one unless named. */
|
|
60
|
+
v?: Interval;
|
|
61
|
+
/** How many cells each way. */
|
|
62
|
+
resolution?: number | {
|
|
63
|
+
u: number;
|
|
64
|
+
v: number;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* The colour a cell is filled with, given how squarely it faces the light: one
|
|
68
|
+
* where it faces the light head on, a half where it is edge on, and nothing
|
|
69
|
+
* where it faces straight away.
|
|
70
|
+
*
|
|
71
|
+
* The author supplies this rather than naming two colours to mix, because
|
|
72
|
+
* mixing two colours means reading them, and a colour here is any CSS colour
|
|
73
|
+
* written as text with nothing that parses one.
|
|
74
|
+
*/
|
|
75
|
+
shade: (amount: number) => Fill;
|
|
76
|
+
/** Which way the light comes from, over the shoulder of an eye on the positive
|
|
77
|
+
* z axis unless named. */
|
|
78
|
+
light?: Vec3;
|
|
79
|
+
/** Whether a cell facing away from the eye is left out. Off by default, because
|
|
80
|
+
* a count that changes as the camera turns is a count no gate can hold. */
|
|
81
|
+
cull?: boolean;
|
|
82
|
+
stroke?: Stroke;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* The cells a surface is made of, before they are put in an order.
|
|
86
|
+
*
|
|
87
|
+
* Cells rather than one shape is what makes the depth sort work at all: a surface
|
|
88
|
+
* that folds over itself has no one place in a painting order, and pieces small
|
|
89
|
+
* enough to be flat do.
|
|
90
|
+
*
|
|
91
|
+
* A scene holding a surface and a plane that cuts through it has to sort all of
|
|
92
|
+
* their cells together, since two surfaces sorted apart are two groups and the
|
|
93
|
+
* second is painted over the first whichever way round they stand. Each cell
|
|
94
|
+
* carries the name it was given ahead of its own place in the grid, so an
|
|
95
|
+
* animation can still name a whole surface once its cells are mixed with
|
|
96
|
+
* another's.
|
|
97
|
+
*/
|
|
98
|
+
export declare function surfaceCells(name: string, of: (u: number, v: number) => Vec3, camera: Camera3, options: Surface3Options): SpaceItem[];
|
|
99
|
+
/**
|
|
100
|
+
* A surface given by a function of two parameters, drawn as a grid of
|
|
101
|
+
* four-cornered cells ordered back to front.
|
|
102
|
+
*/
|
|
103
|
+
export declare function surface3(name: string, of: (u: number, v: number) => Vec3, camera: Camera3, options: Surface3Options): GroupNode;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marks placed at points in space, which a camera turns into the flat nodes the
|
|
3
|
+
* rest of this package already draws.
|
|
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.
|
|
9
|
+
*/
|
|
10
|
+
import { vec3 } from '../values/vec3.js';
|
|
11
|
+
import { circle, polygon, polyline } from './path.js';
|
|
12
|
+
import { group, shape, text } from './node.js';
|
|
13
|
+
import { interval } from '../values/interval.js';
|
|
14
|
+
/**
|
|
15
|
+
* Where along a segment the near plane is crossed.
|
|
16
|
+
*
|
|
17
|
+
* Depth changes evenly along a segment, because lining the world up with the eye
|
|
18
|
+
* moves and turns a point and never divides it, so the crossing is one division
|
|
19
|
+
* rather than a search.
|
|
20
|
+
*/
|
|
21
|
+
function crossingAt(from, to, near) {
|
|
22
|
+
return (near - from) / (to - from);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The pieces of a run of points in space that the eye can see, cut where they
|
|
26
|
+
* cross the near plane.
|
|
27
|
+
*
|
|
28
|
+
* A point nearer the eye than the near plane has no place on the page: under a
|
|
29
|
+
* perspective the divide flips its sign and puts it on the wrong side of the
|
|
30
|
+
* frame, so the segment is cut rather than drawn to it.
|
|
31
|
+
*/
|
|
32
|
+
function visibleRuns(points, camera) {
|
|
33
|
+
const near = camera.projection.near;
|
|
34
|
+
const seen = points.map((point) => camera.project(point));
|
|
35
|
+
const uncut = seen.every((point) => point.inFront);
|
|
36
|
+
const runs = [];
|
|
37
|
+
let current = [];
|
|
38
|
+
for (let i = 0; i < points.length; i += 1) {
|
|
39
|
+
const here = seen[i];
|
|
40
|
+
if (here.inFront) {
|
|
41
|
+
if (i > 0 && !seen[i - 1].inFront) {
|
|
42
|
+
const along = crossingAt(seen[i - 1].depth, here.depth, near);
|
|
43
|
+
current.push(camera.project(vec3.lerp(points[i - 1], points[i], along)).at);
|
|
44
|
+
}
|
|
45
|
+
current.push(here.at);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (i > 0 && seen[i - 1].inFront) {
|
|
49
|
+
const along = crossingAt(seen[i - 1].depth, here.depth, near);
|
|
50
|
+
current.push(camera.project(vec3.lerp(points[i - 1], points[i], along)).at);
|
|
51
|
+
}
|
|
52
|
+
if (current.length > 1)
|
|
53
|
+
runs.push({ points: current, whole: false });
|
|
54
|
+
current = [];
|
|
55
|
+
}
|
|
56
|
+
if (current.length > 1)
|
|
57
|
+
runs.push({ points: current, whole: uncut });
|
|
58
|
+
return runs;
|
|
59
|
+
}
|
|
60
|
+
/** A run of straight segments through points in space. */
|
|
61
|
+
export function polyline3(name, points, camera, options = {}) {
|
|
62
|
+
const { close = false, ...style } = options;
|
|
63
|
+
const runs = visibleRuns(points, camera);
|
|
64
|
+
return group(name, runs.map((run) => shape('run', close && run.whole ? polygon(run.points) : polyline(run.points), style)));
|
|
65
|
+
}
|
|
66
|
+
/** A disc marking a point in space. Its radius is in figure units and does not
|
|
67
|
+
* shrink with distance, because a dot marks where something is rather than how
|
|
68
|
+
* big it is. */
|
|
69
|
+
export function dot3(name, at, radius, fill, camera) {
|
|
70
|
+
const seen = camera.project(at);
|
|
71
|
+
return group(name, seen.inFront ? [shape('disc', circle(seen.at, radius), { fill })] : []);
|
|
72
|
+
}
|
|
73
|
+
/** A label at a point in space. The letters stay upright and stay the size they
|
|
74
|
+
* are given, since a label is read rather than seen in perspective. */
|
|
75
|
+
export function text3(name, at, content, size, camera, options = {}) {
|
|
76
|
+
const { offset, ...style } = options;
|
|
77
|
+
const seen = camera.project(at);
|
|
78
|
+
const anchor = offset ? { x: seen.at.x + offset.x, y: seen.at.y + offset.y } : seen.at;
|
|
79
|
+
return group(name, seen.inFront ? [text('label', anchor, content, size, style)] : []);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The mean of a piece's own depths, so a piece is ordered by where its middle is
|
|
83
|
+
* rather than by whichever corner happens to be nearest.
|
|
84
|
+
*/
|
|
85
|
+
function middleDepth(points, camera) {
|
|
86
|
+
if (points.length === 0)
|
|
87
|
+
return 0;
|
|
88
|
+
let total = 0;
|
|
89
|
+
for (const point of points)
|
|
90
|
+
total += camera.project(point).depth;
|
|
91
|
+
return total / points.length;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* A group whose children are ordered back to front, so the near piece is painted
|
|
95
|
+
* over the far one.
|
|
96
|
+
*
|
|
97
|
+
* This is the painter's algorithm, and what it cannot do is worth knowing before
|
|
98
|
+
* it is used: two pieces that pass through each other, and three that overlap in
|
|
99
|
+
* a ring, have no one order at all, and no comparison of depths can find one. The
|
|
100
|
+
* answer for those is smaller pieces, which is why a surface is cut into cells.
|
|
101
|
+
*
|
|
102
|
+
* Two pieces at the same depth keep the order the author gave them, since the
|
|
103
|
+
* sort is stable, and a picture that changed which of two touching faces was on
|
|
104
|
+
* top between frames would flicker.
|
|
105
|
+
*/
|
|
106
|
+
export function space(name, items, camera) {
|
|
107
|
+
const measured = items.map((item) => ({ node: item.node, depth: middleDepth(item.points, camera) }));
|
|
108
|
+
measured.sort((a, b) => b.depth - a.depth);
|
|
109
|
+
return group(name, measured.map((item) => item.node));
|
|
110
|
+
}
|
|
111
|
+
function resolutionOf(resolution) {
|
|
112
|
+
return typeof resolution === 'number' ? { u: resolution, v: resolution } : resolution;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The cells a surface is made of, before they are put in an order.
|
|
116
|
+
*
|
|
117
|
+
* Cells rather than one shape is what makes the depth sort work at all: a surface
|
|
118
|
+
* that folds over itself has no one place in a painting order, and pieces small
|
|
119
|
+
* enough to be flat do.
|
|
120
|
+
*
|
|
121
|
+
* A scene holding a surface and a plane that cuts through it has to sort all of
|
|
122
|
+
* their cells together, since two surfaces sorted apart are two groups and the
|
|
123
|
+
* second is painted over the first whichever way round they stand. Each cell
|
|
124
|
+
* carries the name it was given ahead of its own place in the grid, so an
|
|
125
|
+
* animation can still name a whole surface once its cells are mixed with
|
|
126
|
+
* another's.
|
|
127
|
+
*/
|
|
128
|
+
export function surfaceCells(name, of, camera, options) {
|
|
129
|
+
const { u = interval(0, 1), v = interval(0, 1), resolution = 24, shade, light = vec3(0, 0, 1), cull = false, stroke } = options;
|
|
130
|
+
const steps = resolutionOf(resolution);
|
|
131
|
+
const toLight = vec3.normalize(light);
|
|
132
|
+
const items = [];
|
|
133
|
+
for (let i = 0; i < steps.u; i += 1) {
|
|
134
|
+
for (let j = 0; j < steps.v; j += 1) {
|
|
135
|
+
const corners = [
|
|
136
|
+
of(interval.at(u, i / steps.u), interval.at(v, j / steps.v)),
|
|
137
|
+
of(interval.at(u, (i + 1) / steps.u), interval.at(v, j / steps.v)),
|
|
138
|
+
of(interval.at(u, (i + 1) / steps.u), interval.at(v, (j + 1) / steps.v)),
|
|
139
|
+
of(interval.at(u, i / steps.u), interval.at(v, (j + 1) / steps.v)),
|
|
140
|
+
];
|
|
141
|
+
const normal = vec3.normalize(vec3.cross(vec3.sub(corners[1], corners[0]), vec3.sub(corners[3], corners[0])));
|
|
142
|
+
if (cull) {
|
|
143
|
+
const middle = corners.reduce((sum, corner) => vec3.add(sum, vec3.scale(corner, 1 / 4)), vec3.ZERO);
|
|
144
|
+
if (vec3.dot(normal, vec3.sub(camera.eye, middle)) <= 0)
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const fill = shade((vec3.dot(normal, toLight) + 1) / 2);
|
|
148
|
+
items.push({
|
|
149
|
+
points: corners,
|
|
150
|
+
node: polyline3(`${name}/${i}-${j}`, corners, camera, { close: true, fill, stroke }),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return items;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* A surface given by a function of two parameters, drawn as a grid of
|
|
158
|
+
* four-cornered cells ordered back to front.
|
|
159
|
+
*/
|
|
160
|
+
export function surface3(name, of, camera, options) {
|
|
161
|
+
return space(name, surfaceCells('cell', of, camera, options), camera);
|
|
162
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export { interval } from './values/interval.js';
|
|
|
18
18
|
export type { Interval } from './values/interval.js';
|
|
19
19
|
export { mat3 } from './values/mat3.js';
|
|
20
20
|
export type { Mat3 } from './values/mat3.js';
|
|
21
|
+
export { mat4 } from './values/mat4.js';
|
|
22
|
+
export type { Mat4, OrthographicOptions, PerspectiveOptions } from './values/mat4.js';
|
|
21
23
|
export { SAME_TIME, keyAt, sampleTrack, sampleTracks, withKey, withoutKey } from './timing/track.js';
|
|
22
24
|
export type { Key, Track, TrackValue, Tracks } from './timing/track.js';
|
|
23
25
|
export { arc, circle, line, polygon, polyline, pointCount, pointOn, rect, slopeOn, splitCurve, straight, transformPath } from './figure/path.js';
|
|
@@ -42,6 +44,14 @@ export { areaUnder, plot, riemannBars, slopeOf, tangentAt } from './figure/plot.
|
|
|
42
44
|
export type { AreaOptions, BarsOptions, PlotOptions, TangentOptions } from './figure/plot.js';
|
|
43
45
|
export { axes, numberLine, numberPlane } from './figure/axis.js';
|
|
44
46
|
export type { AxesOptions, NumberLineOptions, NumberPlaneOptions } from './figure/axis.js';
|
|
47
|
+
export { camera3, orthographic, perspective } from './figure/camera.js';
|
|
48
|
+
export type { Camera3, Camera3Choice, OrthographicChoice, PerspectiveChoice, Projected, Projection } from './figure/camera.js';
|
|
49
|
+
export { dot3, polyline3, space, surface3, surfaceCells, text3 } from './figure/space.js';
|
|
50
|
+
export type { Polyline3Options, SpaceItem, Surface3Options, Text3Options } from './figure/space.js';
|
|
51
|
+
export { axes3 } from './figure/axis3.js';
|
|
52
|
+
export type { Axes3Options } from './figure/axis3.js';
|
|
53
|
+
export { sectionOf } from './figure/section.js';
|
|
54
|
+
export type { Plane, SectionOptions } from './figure/section.js';
|
|
45
55
|
export { coordsOf, pointOf, scaleOf, scaled, unscaled } from './figure/scale.js';
|
|
46
56
|
export type { Coords, Scale } from './figure/scale.js';
|
|
47
57
|
export { labelFor, tickStep, ticksOn } from './figure/ticks.js';
|
|
@@ -55,7 +65,7 @@ export type { PlayOptions, Span, StaggerOptions } from './figure/timeline.js';
|
|
|
55
65
|
export { lengthOf, pointAlong } from './figure/length.js';
|
|
56
66
|
export { trimPath } from './figure/trim.js';
|
|
57
67
|
export { alignPaths, lerpPath } from './figure/morph.js';
|
|
58
|
-
export { at, durationOf, loops, sameMarks } from './figure/figure.js';
|
|
68
|
+
export { at, durationOf, loops, sameMarks, viewAt } from './figure/figure.js';
|
|
59
69
|
export type { Figure, Values } from './figure/figure.js';
|
|
60
70
|
export { pathData, paintSvg, svgElements, svgMarkup } from './paint/svg.js';
|
|
61
71
|
export type { ElementMaker, PaintNode, PaintTarget, SvgElement } from './paint/svg.js';
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,7 @@ export { vec2 } from './values/vec2.js';
|
|
|
13
13
|
export { vec3 } from './values/vec3.js';
|
|
14
14
|
export { interval } from './values/interval.js';
|
|
15
15
|
export { mat3 } from './values/mat3.js';
|
|
16
|
+
export { mat4 } from './values/mat4.js';
|
|
16
17
|
export { SAME_TIME, keyAt, sampleTrack, sampleTracks, withKey, withoutKey } from './timing/track.js';
|
|
17
18
|
export { arc, circle, line, polygon, polyline, pointCount, pointOn, rect, slopeOn, splitCurve, straight, transformPath } from './figure/path.js';
|
|
18
19
|
export { pathFromData } from './figure/path-data.js';
|
|
@@ -26,6 +27,10 @@ export { byAspect, fractionOf, matchingAspect, resolveExtent, viewMatrix } from
|
|
|
26
27
|
export { boundsOf, boundsOfMarks, centreOf } from './figure/bounds.js';
|
|
27
28
|
export { areaUnder, plot, riemannBars, slopeOf, tangentAt } from './figure/plot.js';
|
|
28
29
|
export { axes, numberLine, numberPlane } from './figure/axis.js';
|
|
30
|
+
export { camera3, orthographic, perspective } from './figure/camera.js';
|
|
31
|
+
export { dot3, polyline3, space, surface3, surfaceCells, text3 } from './figure/space.js';
|
|
32
|
+
export { axes3 } from './figure/axis3.js';
|
|
33
|
+
export { sectionOf } from './figure/section.js';
|
|
29
34
|
export { coordsOf, pointOf, scaleOf, scaled, unscaled } from './figure/scale.js';
|
|
30
35
|
export { labelFor, tickStep, ticksOn } from './figure/ticks.js';
|
|
31
36
|
export { flatten, group, shape, text } from './figure/node.js';
|
|
@@ -34,7 +39,7 @@ export { Timeline } from './figure/timeline.js';
|
|
|
34
39
|
export { lengthOf, pointAlong } from './figure/length.js';
|
|
35
40
|
export { trimPath } from './figure/trim.js';
|
|
36
41
|
export { alignPaths, lerpPath } from './figure/morph.js';
|
|
37
|
-
export { at, durationOf, loops, sameMarks } from './figure/figure.js';
|
|
42
|
+
export { at, durationOf, loops, sameMarks, viewAt } from './figure/figure.js';
|
|
38
43
|
export { pathData, paintSvg, svgElements, svgMarkup } from './paint/svg.js';
|
|
39
44
|
export { paintCanvas } from './paint/canvas.js';
|
|
40
45
|
export { arrow, brace, bracePath, callout, dot } from './figure/annotate.js';
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The transform a point in space carries: how a figure's camera turns a place in
|
|
3
|
+
* the world into a place on the page.
|
|
4
|
+
*
|
|
5
|
+
* Sixteen numbers, column-major, matching the engine's layout the way `Mat3`
|
|
6
|
+
* does: the first four are the first column rather than the first row, and the
|
|
7
|
+
* entry at flat index `col * 4 + row` is the one in that column and row. The
|
|
8
|
+
* fourth row exists so that a translation is a multiplication like every other
|
|
9
|
+
* move, and the fourth coordinate a point picks up is what a perspective divide
|
|
10
|
+
* reads.
|
|
11
|
+
*
|
|
12
|
+
* There is no inverse here because nothing needs one: a camera builds its view
|
|
13
|
+
* and its projection forwards and never undoes either. The day something has to
|
|
14
|
+
* go from the page back into the world, that is when an inverse is written.
|
|
15
|
+
*/
|
|
16
|
+
import { type Vec3 } from './vec3.js';
|
|
17
|
+
export type Mat4 = readonly [
|
|
18
|
+
number,
|
|
19
|
+
number,
|
|
20
|
+
number,
|
|
21
|
+
number,
|
|
22
|
+
number,
|
|
23
|
+
number,
|
|
24
|
+
number,
|
|
25
|
+
number,
|
|
26
|
+
number,
|
|
27
|
+
number,
|
|
28
|
+
number,
|
|
29
|
+
number,
|
|
30
|
+
number,
|
|
31
|
+
number,
|
|
32
|
+
number,
|
|
33
|
+
number
|
|
34
|
+
];
|
|
35
|
+
/** Column-major product, so `multiply(a, b)` applies `b` to a point first and
|
|
36
|
+
* then `a`, which is the order a projection sits outside a view. */
|
|
37
|
+
declare function multiply(a: Mat4, b: Mat4): Mat4;
|
|
38
|
+
/** The last column carries the offset, so this moves a point and leaves a
|
|
39
|
+
* direction where it was. */
|
|
40
|
+
declare function translation(v: Vec3): Mat4;
|
|
41
|
+
declare function scaling(v: Vec3): Mat4;
|
|
42
|
+
/** Turns y towards z, so a positive angle is anticlockwise seen from the far
|
|
43
|
+
* end of the x axis looking back at the origin. */
|
|
44
|
+
declare function rotationX(radians: number): Mat4;
|
|
45
|
+
/** Turns z towards x, which is the odd one out: the pair of axes runs z then x
|
|
46
|
+
* rather than x then z, and writing it the other way flips every y rotation. */
|
|
47
|
+
declare function rotationY(radians: number): Mat4;
|
|
48
|
+
/** Turns x towards y, which is the flat rotation `Mat3` gives with a z left
|
|
49
|
+
* alone. */
|
|
50
|
+
declare function rotationZ(radians: number): Mat4;
|
|
51
|
+
/**
|
|
52
|
+
* The view matrix of an eye at `eye` looking at `target`, with `up` saying which
|
|
53
|
+
* way is up.
|
|
54
|
+
*
|
|
55
|
+
* View space looks down its own negative z, so the target comes out on the
|
|
56
|
+
* negative z axis at the distance between the eye and the target. An `up` lying
|
|
57
|
+
* along the line of sight has no sideways direction in it and gives a matrix of
|
|
58
|
+
* zeroes, which is the pose a caller has to avoid rather than one this can fix.
|
|
59
|
+
*/
|
|
60
|
+
declare function lookAt(eye: Vec3, target: Vec3, up: Vec3): Mat4;
|
|
61
|
+
export type PerspectiveOptions = {
|
|
62
|
+
/** The angle the frame covers up and down, in radians. */
|
|
63
|
+
fov: number;
|
|
64
|
+
/** Width over height, so a wide frame shows more sideways rather than less. */
|
|
65
|
+
aspect: number;
|
|
66
|
+
near: number;
|
|
67
|
+
far: number;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* The projection of an eye that sees things smaller the further off they are.
|
|
71
|
+
*
|
|
72
|
+
* The third column puts the negated view-space z into the fourth coordinate, so
|
|
73
|
+
* a point twice as far away comes back with twice the divisor and lands half as
|
|
74
|
+
* far from the middle of the frame.
|
|
75
|
+
*/
|
|
76
|
+
declare function perspective({ fov, aspect, near, far }: PerspectiveOptions): Mat4;
|
|
77
|
+
export type OrthographicOptions = {
|
|
78
|
+
left: number;
|
|
79
|
+
right: number;
|
|
80
|
+
bottom: number;
|
|
81
|
+
top: number;
|
|
82
|
+
near: number;
|
|
83
|
+
far: number;
|
|
84
|
+
};
|
|
85
|
+
/** The projection of an eye that sees everything at the size it is, which maps
|
|
86
|
+
* the named box onto the frame and leaves the fourth coordinate at one. */
|
|
87
|
+
declare function orthographic({ left, right, bottom, top, near, far }: OrthographicOptions): Mat4;
|
|
88
|
+
/**
|
|
89
|
+
* Applies the matrix to a point, taking the translation with it and dividing by
|
|
90
|
+
* the fourth coordinate the matrix gives it.
|
|
91
|
+
*
|
|
92
|
+
* That divide is why a point through `multiply(a, b)` matches the same point
|
|
93
|
+
* through `b` and then through `a` for matrices that do not touch the fourth
|
|
94
|
+
* coordinate and not for a perspective matrix: dividing halfway throws away the
|
|
95
|
+
* fourth coordinate the outer matrix still needed. A fourth coordinate of zero is
|
|
96
|
+
* a point on the plane through the eye, which has no place on the page at all, so
|
|
97
|
+
* the divide is skipped and the caller is left to notice.
|
|
98
|
+
*/
|
|
99
|
+
declare function transformPoint(m: Mat4, v: Vec3): Vec3;
|
|
100
|
+
/** Applies the rotation and scale and neither the translation nor the divide,
|
|
101
|
+
* which is what a direction wants: moving the world must not move where an arrow
|
|
102
|
+
* points, and a direction has no distance for a perspective to shrink. */
|
|
103
|
+
declare function transformDirection(m: Mat4, v: Vec3): Vec3;
|
|
104
|
+
export declare const mat4: {
|
|
105
|
+
IDENTITY: Mat4;
|
|
106
|
+
multiply: typeof multiply;
|
|
107
|
+
translation: typeof translation;
|
|
108
|
+
scaling: typeof scaling;
|
|
109
|
+
rotationX: typeof rotationX;
|
|
110
|
+
rotationY: typeof rotationY;
|
|
111
|
+
rotationZ: typeof rotationZ;
|
|
112
|
+
lookAt: typeof lookAt;
|
|
113
|
+
perspective: typeof perspective;
|
|
114
|
+
orthographic: typeof orthographic;
|
|
115
|
+
transformPoint: typeof transformPoint;
|
|
116
|
+
transformDirection: typeof transformDirection;
|
|
117
|
+
};
|
|
118
|
+
export {};
|