@altpsyche/maths 0.9.5 → 0.11.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 +81 -3
- package/dist/figure/annotate.d.ts +3 -0
- package/dist/figure/annotate.js +4 -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/field.d.ts +64 -0
- package/dist/figure/field.js +69 -0
- 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 +153 -0
- package/dist/figure/space.js +242 -0
- package/dist/figure/streamline.d.ts +49 -0
- package/dist/figure/streamline.js +95 -0
- package/dist/index.d.ts +15 -1
- package/dist/index.js +8 -1
- package/dist/values/mat4.d.ts +118 -0
- package/dist/values/mat4.js +193 -0
- package/package.json +1 -1
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The path a point follows through a field of vectors, walked by Runge-Kutta 4.
|
|
3
|
+
*
|
|
4
|
+
* The points come back in graph units and nothing here draws them, the way the
|
|
5
|
+
* curve where a plane cuts a surface comes back as points in space. What a
|
|
6
|
+
* figure does with them is its own.
|
|
7
|
+
*
|
|
8
|
+
* The step is a distance rather than a time, so the field is read as a
|
|
9
|
+
* direction and its magnitude decides nothing about how far the run moves. A
|
|
10
|
+
* step in the field's own time crowds the points where the field is weak and
|
|
11
|
+
* spreads them where it is strong, and a curve drawn from those is faceted
|
|
12
|
+
* exactly where it turns hardest.
|
|
13
|
+
*
|
|
14
|
+
* The step is fixed and never adaptive. An adaptive step hands back a different
|
|
15
|
+
* number of points as the field changes, which is a count no gate can hold and
|
|
16
|
+
* a path no morph can pair up against another.
|
|
17
|
+
*/
|
|
18
|
+
import { type Interval } from '../values/interval.js';
|
|
19
|
+
import { type Vec2 } from '../values/vec2.js';
|
|
20
|
+
export interface StreamlineOptions {
|
|
21
|
+
/** How far each step moves, in graph units. */
|
|
22
|
+
step: number;
|
|
23
|
+
/** How many steps the run takes at most, in each direction it is run. */
|
|
24
|
+
steps?: number;
|
|
25
|
+
/** The region the run is held inside. It has no edges where this is left out,
|
|
26
|
+
* and only the step cap and a vanishing field stop it. */
|
|
27
|
+
within?: {
|
|
28
|
+
x: Interval;
|
|
29
|
+
y: Interval;
|
|
30
|
+
};
|
|
31
|
+
/** Which way the run goes from its seed. Both puts the backward half first,
|
|
32
|
+
* so the points read from one end of the curve to the other. */
|
|
33
|
+
direction?: 'forward' | 'backward' | 'both';
|
|
34
|
+
/** The magnitude below which the field is taken to have vanished, in graph
|
|
35
|
+
* units. */
|
|
36
|
+
least?: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The streamline of a field through a seed point, in graph units.
|
|
40
|
+
*
|
|
41
|
+
* The run stops on one of three rules: it leaves the region, it reaches its step
|
|
42
|
+
* cap, or the field where it stands is too small to point anywhere. A seed
|
|
43
|
+
* outside the region comes back as that seed alone, which is a curve with
|
|
44
|
+
* nothing to draw rather than a run that starts by escaping.
|
|
45
|
+
*
|
|
46
|
+
* A run that leaves the region stops at the last point inside it and is not cut
|
|
47
|
+
* at the edge, so it ends within one step of the boundary.
|
|
48
|
+
*/
|
|
49
|
+
export declare function streamlineOf(of: (at: Vec2) => Vec2, from: Vec2, options: StreamlineOptions): Vec2[];
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The path a point follows through a field of vectors, walked by Runge-Kutta 4.
|
|
3
|
+
*
|
|
4
|
+
* The points come back in graph units and nothing here draws them, the way the
|
|
5
|
+
* curve where a plane cuts a surface comes back as points in space. What a
|
|
6
|
+
* figure does with them is its own.
|
|
7
|
+
*
|
|
8
|
+
* The step is a distance rather than a time, so the field is read as a
|
|
9
|
+
* direction and its magnitude decides nothing about how far the run moves. A
|
|
10
|
+
* step in the field's own time crowds the points where the field is weak and
|
|
11
|
+
* spreads them where it is strong, and a curve drawn from those is faceted
|
|
12
|
+
* exactly where it turns hardest.
|
|
13
|
+
*
|
|
14
|
+
* The step is fixed and never adaptive. An adaptive step hands back a different
|
|
15
|
+
* number of points as the field changes, which is a count no gate can hold and
|
|
16
|
+
* a path no morph can pair up against another.
|
|
17
|
+
*/
|
|
18
|
+
import { interval } from '../values/interval.js';
|
|
19
|
+
import { vec2 } from '../values/vec2.js';
|
|
20
|
+
const STEPS = 200;
|
|
21
|
+
const LEAST = 1e-9;
|
|
22
|
+
function holds(within, at) {
|
|
23
|
+
if (!Number.isFinite(at.x) || !Number.isFinite(at.y))
|
|
24
|
+
return false;
|
|
25
|
+
if (!within)
|
|
26
|
+
return true;
|
|
27
|
+
return interval.holds(interval.ordered(within.x), at.x) && interval.holds(interval.ordered(within.y), at.y);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* One run from the seed, as the points after it, with the seed left out so the
|
|
31
|
+
* two directions can be joined without repeating it.
|
|
32
|
+
*
|
|
33
|
+
* A stage whose field has vanished stops the run there rather than being taken
|
|
34
|
+
* as a direction, since dividing by a magnitude near nothing turns rounding
|
|
35
|
+
* error into a direction of its own.
|
|
36
|
+
*/
|
|
37
|
+
function run(of, from, step, steps, within, least) {
|
|
38
|
+
const direction = (at) => {
|
|
39
|
+
const vector = of(at);
|
|
40
|
+
const magnitude = Math.hypot(vector.x, vector.y);
|
|
41
|
+
if (!Number.isFinite(magnitude) || magnitude <= least)
|
|
42
|
+
return undefined;
|
|
43
|
+
return vec2(vector.x / magnitude, vector.y / magnitude);
|
|
44
|
+
};
|
|
45
|
+
const points = [];
|
|
46
|
+
let at = from;
|
|
47
|
+
for (let taken = 0; taken < steps; taken += 1) {
|
|
48
|
+
const first = direction(at);
|
|
49
|
+
if (!first)
|
|
50
|
+
break;
|
|
51
|
+
const second = direction(vec2.add(at, vec2.scale(first, step / 2)));
|
|
52
|
+
if (!second)
|
|
53
|
+
break;
|
|
54
|
+
const third = direction(vec2.add(at, vec2.scale(second, step / 2)));
|
|
55
|
+
if (!third)
|
|
56
|
+
break;
|
|
57
|
+
const fourth = direction(vec2.add(at, vec2.scale(third, step)));
|
|
58
|
+
if (!fourth)
|
|
59
|
+
break;
|
|
60
|
+
const along = vec2((first.x + 2 * second.x + 2 * third.x + fourth.x) / 6, (first.y + 2 * second.y + 2 * third.y + fourth.y) / 6);
|
|
61
|
+
const next = vec2.add(at, vec2.scale(along, step));
|
|
62
|
+
if (!holds(within, next))
|
|
63
|
+
break;
|
|
64
|
+
points.push(next);
|
|
65
|
+
at = next;
|
|
66
|
+
}
|
|
67
|
+
return points;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The streamline of a field through a seed point, in graph units.
|
|
71
|
+
*
|
|
72
|
+
* The run stops on one of three rules: it leaves the region, it reaches its step
|
|
73
|
+
* cap, or the field where it stands is too small to point anywhere. A seed
|
|
74
|
+
* outside the region comes back as that seed alone, which is a curve with
|
|
75
|
+
* nothing to draw rather than a run that starts by escaping.
|
|
76
|
+
*
|
|
77
|
+
* A run that leaves the region stops at the last point inside it and is not cut
|
|
78
|
+
* at the edge, so it ends within one step of the boundary.
|
|
79
|
+
*/
|
|
80
|
+
export function streamlineOf(of, from, options) {
|
|
81
|
+
const steps = Math.max(0, Math.round(options.steps ?? STEPS));
|
|
82
|
+
const least = options.least ?? LEAST;
|
|
83
|
+
const direction = options.direction ?? 'forward';
|
|
84
|
+
if (!holds(options.within, from))
|
|
85
|
+
return [from];
|
|
86
|
+
const forward = direction === 'backward' ? [] : run(of, from, options.step, steps, options.within, least);
|
|
87
|
+
if (direction === 'forward')
|
|
88
|
+
return [from, ...forward];
|
|
89
|
+
const back = (at) => {
|
|
90
|
+
const vector = of(at);
|
|
91
|
+
return vec2(-vector.x, -vector.y);
|
|
92
|
+
};
|
|
93
|
+
const backward = run(back, from, options.step, steps, options.within, least);
|
|
94
|
+
return [...backward.reverse(), from, ...forward];
|
|
95
|
+
}
|
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';
|
|
@@ -40,8 +42,20 @@ export { boundsOf, boundsOfMarks, centreOf } from './figure/bounds.js';
|
|
|
40
42
|
export type { Bounds } from './figure/bounds.js';
|
|
41
43
|
export { areaUnder, plot, riemannBars, slopeOf, tangentAt } from './figure/plot.js';
|
|
42
44
|
export type { AreaOptions, BarsOptions, PlotOptions, TangentOptions } from './figure/plot.js';
|
|
45
|
+
export { vectorField } from './figure/field.js';
|
|
46
|
+
export { streamlineOf } from './figure/streamline.js';
|
|
47
|
+
export type { StreamlineOptions } from './figure/streamline.js';
|
|
48
|
+
export type { VectorFieldOptions } from './figure/field.js';
|
|
43
49
|
export { axes, numberLine, numberPlane } from './figure/axis.js';
|
|
44
50
|
export type { AxesOptions, NumberLineOptions, NumberPlaneOptions } from './figure/axis.js';
|
|
51
|
+
export { camera3, orthographic, perspective } from './figure/camera.js';
|
|
52
|
+
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 { axes3 } from './figure/axis3.js';
|
|
56
|
+
export type { Axes3Options } from './figure/axis3.js';
|
|
57
|
+
export { sectionOf } from './figure/section.js';
|
|
58
|
+
export type { Plane, SectionOptions } from './figure/section.js';
|
|
45
59
|
export { coordsOf, pointOf, scaleOf, scaled, unscaled } from './figure/scale.js';
|
|
46
60
|
export type { Coords, Scale } from './figure/scale.js';
|
|
47
61
|
export { labelFor, tickStep, ticksOn } from './figure/ticks.js';
|
|
@@ -55,7 +69,7 @@ export type { PlayOptions, Span, StaggerOptions } from './figure/timeline.js';
|
|
|
55
69
|
export { lengthOf, pointAlong } from './figure/length.js';
|
|
56
70
|
export { trimPath } from './figure/trim.js';
|
|
57
71
|
export { alignPaths, lerpPath } from './figure/morph.js';
|
|
58
|
-
export { at, durationOf, loops, sameMarks } from './figure/figure.js';
|
|
72
|
+
export { at, durationOf, loops, sameMarks, viewAt } from './figure/figure.js';
|
|
59
73
|
export type { Figure, Values } from './figure/figure.js';
|
|
60
74
|
export { pathData, paintSvg, svgElements, svgMarkup } from './paint/svg.js';
|
|
61
75
|
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';
|
|
@@ -25,7 +26,13 @@ export { curveCrossings } from './figure/intersect.js';
|
|
|
25
26
|
export { byAspect, fractionOf, matchingAspect, resolveExtent, viewMatrix } from './figure/extent.js';
|
|
26
27
|
export { boundsOf, boundsOfMarks, centreOf } from './figure/bounds.js';
|
|
27
28
|
export { areaUnder, plot, riemannBars, slopeOf, tangentAt } from './figure/plot.js';
|
|
29
|
+
export { vectorField } from './figure/field.js';
|
|
30
|
+
export { streamlineOf } from './figure/streamline.js';
|
|
28
31
|
export { axes, numberLine, numberPlane } from './figure/axis.js';
|
|
32
|
+
export { camera3, orthographic, perspective } from './figure/camera.js';
|
|
33
|
+
export { arrow3, dot3, fieldArrows3, polyline3, space, surface3, surfaceCells, text3, vectorField3 } from './figure/space.js';
|
|
34
|
+
export { axes3 } from './figure/axis3.js';
|
|
35
|
+
export { sectionOf } from './figure/section.js';
|
|
29
36
|
export { coordsOf, pointOf, scaleOf, scaled, unscaled } from './figure/scale.js';
|
|
30
37
|
export { labelFor, tickStep, ticksOn } from './figure/ticks.js';
|
|
31
38
|
export { flatten, group, shape, text } from './figure/node.js';
|
|
@@ -34,7 +41,7 @@ export { Timeline } from './figure/timeline.js';
|
|
|
34
41
|
export { lengthOf, pointAlong } from './figure/length.js';
|
|
35
42
|
export { trimPath } from './figure/trim.js';
|
|
36
43
|
export { alignPaths, lerpPath } from './figure/morph.js';
|
|
37
|
-
export { at, durationOf, loops, sameMarks } from './figure/figure.js';
|
|
44
|
+
export { at, durationOf, loops, sameMarks, viewAt } from './figure/figure.js';
|
|
38
45
|
export { pathData, paintSvg, svgElements, svgMarkup } from './paint/svg.js';
|
|
39
46
|
export { paintCanvas } from './paint/canvas.js';
|
|
40
47
|
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 {};
|
|
@@ -0,0 +1,193 @@
|
|
|
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 { vec3 } from './vec3.js';
|
|
17
|
+
const IDENTITY = [
|
|
18
|
+
1, 0, 0, 0,
|
|
19
|
+
0, 1, 0, 0,
|
|
20
|
+
0, 0, 1, 0,
|
|
21
|
+
0, 0, 0, 1,
|
|
22
|
+
];
|
|
23
|
+
/** Column-major product, so `multiply(a, b)` applies `b` to a point first and
|
|
24
|
+
* then `a`, which is the order a projection sits outside a view. */
|
|
25
|
+
function multiply(a, b) {
|
|
26
|
+
const [a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15] = a;
|
|
27
|
+
const [b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15] = b;
|
|
28
|
+
return [
|
|
29
|
+
a0 * b0 + a4 * b1 + a8 * b2 + a12 * b3,
|
|
30
|
+
a1 * b0 + a5 * b1 + a9 * b2 + a13 * b3,
|
|
31
|
+
a2 * b0 + a6 * b1 + a10 * b2 + a14 * b3,
|
|
32
|
+
a3 * b0 + a7 * b1 + a11 * b2 + a15 * b3,
|
|
33
|
+
a0 * b4 + a4 * b5 + a8 * b6 + a12 * b7,
|
|
34
|
+
a1 * b4 + a5 * b5 + a9 * b6 + a13 * b7,
|
|
35
|
+
a2 * b4 + a6 * b5 + a10 * b6 + a14 * b7,
|
|
36
|
+
a3 * b4 + a7 * b5 + a11 * b6 + a15 * b7,
|
|
37
|
+
a0 * b8 + a4 * b9 + a8 * b10 + a12 * b11,
|
|
38
|
+
a1 * b8 + a5 * b9 + a9 * b10 + a13 * b11,
|
|
39
|
+
a2 * b8 + a6 * b9 + a10 * b10 + a14 * b11,
|
|
40
|
+
a3 * b8 + a7 * b9 + a11 * b10 + a15 * b11,
|
|
41
|
+
a0 * b12 + a4 * b13 + a8 * b14 + a12 * b15,
|
|
42
|
+
a1 * b12 + a5 * b13 + a9 * b14 + a13 * b15,
|
|
43
|
+
a2 * b12 + a6 * b13 + a10 * b14 + a14 * b15,
|
|
44
|
+
a3 * b12 + a7 * b13 + a11 * b14 + a15 * b15,
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
/** The last column carries the offset, so this moves a point and leaves a
|
|
48
|
+
* direction where it was. */
|
|
49
|
+
function translation(v) {
|
|
50
|
+
return [
|
|
51
|
+
1, 0, 0, 0,
|
|
52
|
+
0, 1, 0, 0,
|
|
53
|
+
0, 0, 1, 0,
|
|
54
|
+
v.x, v.y, v.z, 1,
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
function scaling(v) {
|
|
58
|
+
return [
|
|
59
|
+
v.x, 0, 0, 0,
|
|
60
|
+
0, v.y, 0, 0,
|
|
61
|
+
0, 0, v.z, 0,
|
|
62
|
+
0, 0, 0, 1,
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
/** Turns y towards z, so a positive angle is anticlockwise seen from the far
|
|
66
|
+
* end of the x axis looking back at the origin. */
|
|
67
|
+
function rotationX(radians) {
|
|
68
|
+
const c = Math.cos(radians);
|
|
69
|
+
const s = Math.sin(radians);
|
|
70
|
+
return [
|
|
71
|
+
1, 0, 0, 0,
|
|
72
|
+
0, c, s, 0,
|
|
73
|
+
0, -s, c, 0,
|
|
74
|
+
0, 0, 0, 1,
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
/** Turns z towards x, which is the odd one out: the pair of axes runs z then x
|
|
78
|
+
* rather than x then z, and writing it the other way flips every y rotation. */
|
|
79
|
+
function rotationY(radians) {
|
|
80
|
+
const c = Math.cos(radians);
|
|
81
|
+
const s = Math.sin(radians);
|
|
82
|
+
return [
|
|
83
|
+
c, 0, -s, 0,
|
|
84
|
+
0, 1, 0, 0,
|
|
85
|
+
s, 0, c, 0,
|
|
86
|
+
0, 0, 0, 1,
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
/** Turns x towards y, which is the flat rotation `Mat3` gives with a z left
|
|
90
|
+
* alone. */
|
|
91
|
+
function rotationZ(radians) {
|
|
92
|
+
const c = Math.cos(radians);
|
|
93
|
+
const s = Math.sin(radians);
|
|
94
|
+
return [
|
|
95
|
+
c, s, 0, 0,
|
|
96
|
+
-s, c, 0, 0,
|
|
97
|
+
0, 0, 1, 0,
|
|
98
|
+
0, 0, 0, 1,
|
|
99
|
+
];
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The view matrix of an eye at `eye` looking at `target`, with `up` saying which
|
|
103
|
+
* way is up.
|
|
104
|
+
*
|
|
105
|
+
* View space looks down its own negative z, so the target comes out on the
|
|
106
|
+
* negative z axis at the distance between the eye and the target. An `up` lying
|
|
107
|
+
* along the line of sight has no sideways direction in it and gives a matrix of
|
|
108
|
+
* zeroes, which is the pose a caller has to avoid rather than one this can fix.
|
|
109
|
+
*/
|
|
110
|
+
function lookAt(eye, target, up) {
|
|
111
|
+
const forward = vec3.normalize(vec3.sub(target, eye));
|
|
112
|
+
const right = vec3.normalize(vec3.cross(forward, up));
|
|
113
|
+
const above = vec3.cross(right, forward);
|
|
114
|
+
return [
|
|
115
|
+
right.x, above.x, -forward.x, 0,
|
|
116
|
+
right.y, above.y, -forward.y, 0,
|
|
117
|
+
right.z, above.z, -forward.z, 0,
|
|
118
|
+
-vec3.dot(right, eye), -vec3.dot(above, eye), vec3.dot(forward, eye), 1,
|
|
119
|
+
];
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The projection of an eye that sees things smaller the further off they are.
|
|
123
|
+
*
|
|
124
|
+
* The third column puts the negated view-space z into the fourth coordinate, so
|
|
125
|
+
* a point twice as far away comes back with twice the divisor and lands half as
|
|
126
|
+
* far from the middle of the frame.
|
|
127
|
+
*/
|
|
128
|
+
function perspective({ fov, aspect, near, far }) {
|
|
129
|
+
const focal = 1 / Math.tan(fov / 2);
|
|
130
|
+
return [
|
|
131
|
+
focal / aspect, 0, 0, 0,
|
|
132
|
+
0, focal, 0, 0,
|
|
133
|
+
0, 0, (far + near) / (near - far), -1,
|
|
134
|
+
0, 0, (2 * far * near) / (near - far), 0,
|
|
135
|
+
];
|
|
136
|
+
}
|
|
137
|
+
/** The projection of an eye that sees everything at the size it is, which maps
|
|
138
|
+
* the named box onto the frame and leaves the fourth coordinate at one. */
|
|
139
|
+
function orthographic({ left, right, bottom, top, near, far }) {
|
|
140
|
+
return [
|
|
141
|
+
2 / (right - left), 0, 0, 0,
|
|
142
|
+
0, 2 / (top - bottom), 0, 0,
|
|
143
|
+
0, 0, -2 / (far - near), 0,
|
|
144
|
+
-(right + left) / (right - left),
|
|
145
|
+
-(top + bottom) / (top - bottom),
|
|
146
|
+
-(far + near) / (far - near),
|
|
147
|
+
1,
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Applies the matrix to a point, taking the translation with it and dividing by
|
|
152
|
+
* the fourth coordinate the matrix gives it.
|
|
153
|
+
*
|
|
154
|
+
* That divide is why a point through `multiply(a, b)` matches the same point
|
|
155
|
+
* through `b` and then through `a` for matrices that do not touch the fourth
|
|
156
|
+
* coordinate and not for a perspective matrix: dividing halfway throws away the
|
|
157
|
+
* fourth coordinate the outer matrix still needed. A fourth coordinate of zero is
|
|
158
|
+
* a point on the plane through the eye, which has no place on the page at all, so
|
|
159
|
+
* the divide is skipped and the caller is left to notice.
|
|
160
|
+
*/
|
|
161
|
+
function transformPoint(m, v) {
|
|
162
|
+
const w = m[3] * v.x + m[7] * v.y + m[11] * v.z + m[15];
|
|
163
|
+
const divisor = w === 0 ? 1 : w;
|
|
164
|
+
return {
|
|
165
|
+
x: (m[0] * v.x + m[4] * v.y + m[8] * v.z + m[12]) / divisor,
|
|
166
|
+
y: (m[1] * v.x + m[5] * v.y + m[9] * v.z + m[13]) / divisor,
|
|
167
|
+
z: (m[2] * v.x + m[6] * v.y + m[10] * v.z + m[14]) / divisor,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/** Applies the rotation and scale and neither the translation nor the divide,
|
|
171
|
+
* which is what a direction wants: moving the world must not move where an arrow
|
|
172
|
+
* points, and a direction has no distance for a perspective to shrink. */
|
|
173
|
+
function transformDirection(m, v) {
|
|
174
|
+
return {
|
|
175
|
+
x: m[0] * v.x + m[4] * v.y + m[8] * v.z,
|
|
176
|
+
y: m[1] * v.x + m[5] * v.y + m[9] * v.z,
|
|
177
|
+
z: m[2] * v.x + m[6] * v.y + m[10] * v.z,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
export const mat4 = {
|
|
181
|
+
IDENTITY,
|
|
182
|
+
multiply,
|
|
183
|
+
translation,
|
|
184
|
+
scaling,
|
|
185
|
+
rotationX,
|
|
186
|
+
rotationY,
|
|
187
|
+
rotationZ,
|
|
188
|
+
lookAt,
|
|
189
|
+
perspective,
|
|
190
|
+
orthographic,
|
|
191
|
+
transformPoint,
|
|
192
|
+
transformDirection,
|
|
193
|
+
};
|
package/package.json
CHANGED