@motionscript/plot 0.0.0-stage → 0.1.0-alpha.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/CHANGELOG.md +5 -0
- package/LICENSE +201 -0
- package/dist/browser/chunks/chunk-RCFWYW6O.js +2 -0
- package/dist/browser/chunks/chunk-RCFWYW6O.js.map +7 -0
- package/dist/browser/index.js +84 -0
- package/dist/browser/index.js.map +7 -0
- package/dist/browser/kit.js +2 -0
- package/dist/browser/kit.js.map +7 -0
- package/dist/browser/manifest.json +12 -0
- package/dist/engine.d.ts +14 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +14 -0
- package/dist/engine.js.map +1 -0
- package/dist/graph2d/curve-cache.d.ts +60 -0
- package/dist/graph2d/curve-cache.d.ts.map +1 -0
- package/dist/graph2d/curve-cache.js +79 -0
- package/dist/graph2d/curve-cache.js.map +1 -0
- package/dist/graph2d/curve-error.d.ts +12 -0
- package/dist/graph2d/curve-error.d.ts.map +1 -0
- package/dist/graph2d/curve-error.js +15 -0
- package/dist/graph2d/curve-error.js.map +1 -0
- package/dist/graph2d/curve.d.ts +104 -0
- package/dist/graph2d/curve.d.ts.map +1 -0
- package/dist/graph2d/curve.js +531 -0
- package/dist/graph2d/curve.js.map +1 -0
- package/dist/graph2d/graph2d.d.ts +164 -0
- package/dist/graph2d/graph2d.d.ts.map +1 -0
- package/dist/graph2d/graph2d.js +404 -0
- package/dist/graph2d/graph2d.js.map +1 -0
- package/dist/graph2d/index.d.ts +32 -0
- package/dist/graph2d/index.d.ts.map +1 -0
- package/dist/graph2d/index.js +32 -0
- package/dist/graph2d/index.js.map +1 -0
- package/dist/graph2d/plane-fill.d.ts +110 -0
- package/dist/graph2d/plane-fill.d.ts.map +1 -0
- package/dist/graph2d/plane-fill.js +248 -0
- package/dist/graph2d/plane-fill.js.map +1 -0
- package/dist/graph2d/plane.d.ts +179 -0
- package/dist/graph2d/plane.d.ts.map +1 -0
- package/dist/graph2d/plane.js +359 -0
- package/dist/graph2d/plane.js.map +1 -0
- package/dist/graph2d/shared.d.ts +40 -0
- package/dist/graph2d/shared.d.ts.map +1 -0
- package/dist/graph2d/shared.js +70 -0
- package/dist/graph2d/shared.js.map +1 -0
- package/dist/graph3d/expression.d.ts +30 -0
- package/dist/graph3d/expression.d.ts.map +1 -0
- package/dist/graph3d/expression.js +35 -0
- package/dist/graph3d/expression.js.map +1 -0
- package/dist/graph3d/graph3d.d.ts +186 -0
- package/dist/graph3d/graph3d.d.ts.map +1 -0
- package/dist/graph3d/graph3d.js +404 -0
- package/dist/graph3d/graph3d.js.map +1 -0
- package/dist/graph3d/index.d.ts +22 -0
- package/dist/graph3d/index.d.ts.map +1 -0
- package/dist/graph3d/index.js +22 -0
- package/dist/graph3d/index.js.map +1 -0
- package/dist/graph3d/shared.d.ts +61 -0
- package/dist/graph3d/shared.d.ts.map +1 -0
- package/dist/graph3d/shared.js +101 -0
- package/dist/graph3d/shared.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/kit/equations.d.ts +56 -0
- package/dist/kit/equations.d.ts.map +1 -0
- package/dist/kit/equations.js +63 -0
- package/dist/kit/equations.js.map +1 -0
- package/dist/kit/expression.d.ts +60 -0
- package/dist/kit/expression.d.ts.map +1 -0
- package/dist/kit/expression.js +268 -0
- package/dist/kit/expression.js.map +1 -0
- package/dist/kit/index.d.ts +15 -0
- package/dist/kit/index.d.ts.map +1 -0
- package/dist/kit/index.js +13 -0
- package/dist/kit/index.js.map +1 -0
- package/dist/nodes.d.ts +19 -0
- package/dist/nodes.d.ts.map +1 -0
- package/dist/nodes.js +19 -0
- package/dist/nodes.js.map +1 -0
- package/package.json +68 -3
- package/registry.json +34 -0
- package/src/engine.ts +13 -0
- package/src/graph2d/curve-cache.ts +103 -0
- package/src/graph2d/curve-error.ts +19 -0
- package/src/graph2d/curve.ts +657 -0
- package/src/graph2d/graph2d.ts +586 -0
- package/src/graph2d/index.ts +31 -0
- package/src/graph2d/plane-fill.ts +308 -0
- package/src/graph2d/plane.ts +457 -0
- package/src/graph2d/shared.ts +103 -0
- package/src/graph3d/expression.ts +51 -0
- package/src/graph3d/graph3d.ts +614 -0
- package/src/graph3d/index.ts +21 -0
- package/src/graph3d/shared.ts +143 -0
- package/src/index.ts +3 -0
- package/src/kit/equations.ts +102 -0
- package/src/kit/expression.ts +323 -0
- package/src/kit/index.ts +29 -0
- package/src/nodes.ts +19 -0
- package/README.md +0 -4
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What part of the plane a 2D graph is looking at, and the furniture that makes
|
|
3
|
+
* it readable: the graph→pixel mapping, the tick ladder, and how a number on an
|
|
4
|
+
* axis is written.
|
|
5
|
+
*
|
|
6
|
+
* ## The view is four numbers, and none of them is a matrix
|
|
7
|
+
*
|
|
8
|
+
* `centerX`/`centerY` say where you are, `xSpan`/`ySpan` say how much you can
|
|
9
|
+
* see. Four independently tweenable numbers rather than a transform, for exactly
|
|
10
|
+
* the reason the 3D graph's camera is three: the studio renders a *timeline*, so
|
|
11
|
+
* every frame has to be reproducible from stored values under scrubbing and
|
|
12
|
+
* export, and "pan across while zooming in" has to be one command rather than a
|
|
13
|
+
* hand-built parallel over matrix entries.
|
|
14
|
+
*
|
|
15
|
+
* ## `ySpan: 0` means "keep the grid square"
|
|
16
|
+
*
|
|
17
|
+
* The vertical span is the one number that usually shouldn't be typed. A grapher
|
|
18
|
+
* whose units are square is one where a circle is round and a 45° line looks
|
|
19
|
+
* like one, and the span that achieves that depends on the node's *box* — which
|
|
20
|
+
* the author changes by dragging a handle, not by editing this field. So zero
|
|
21
|
+
* means "derive it", the same convention a node's own width and height use for
|
|
22
|
+
* auto-sizing, and any positive value takes over.
|
|
23
|
+
*
|
|
24
|
+
* Nothing is lost by it: the moment a gesture or a command scales the axes
|
|
25
|
+
* apart, the derived value is written down and the field is an ordinary number
|
|
26
|
+
* from then on — the same handover the canvas makes when a resize drag turns a
|
|
27
|
+
* filling axis into a fixed one.
|
|
28
|
+
*/
|
|
29
|
+
import type { PixelRect, PlaneMap } from "./curve.js";
|
|
30
|
+
/** Where a 2D graph is looking, as the four numbers its props hold. */
|
|
31
|
+
export interface PlaneView {
|
|
32
|
+
/** Graph x at the centre of the node's box. */
|
|
33
|
+
centerX: number;
|
|
34
|
+
/** Graph y at the centre of the node's box. */
|
|
35
|
+
centerY: number;
|
|
36
|
+
/** Width of the visible window, in graph units. */
|
|
37
|
+
xSpan: number;
|
|
38
|
+
/** Height of the visible window in graph units, or 0 to keep units square. */
|
|
39
|
+
ySpan: number;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Bounds on a span.
|
|
43
|
+
*
|
|
44
|
+
* A hundred decades inside the double's own range, so nothing downstream —
|
|
45
|
+
* `width / span`, `step * scale`, `y * scale` — can overflow or fall into the
|
|
46
|
+
* denormals on the way through.
|
|
47
|
+
*
|
|
48
|
+
* Deliberately far wider than anything usable, because what actually limits how
|
|
49
|
+
* far a plane can be zoomed is not a floor on the span but a **ratio**:
|
|
50
|
+
* `x - centerX` keeps about sixteen significant digits, so a window narrower
|
|
51
|
+
* than `|centerX| · 4.3e-13` has fewer distinct representable x values across it
|
|
52
|
+
* than it has pixels, and the curve staircases whatever these constants say.
|
|
53
|
+
* That limit belongs to the *gesture* — see `planeSpanLimits` — which is the
|
|
54
|
+
* thing a person can be stopped at. A scene that deliberately authored a deeper
|
|
55
|
+
* zoom still renders, badly and visibly, rather than being silently clamped to
|
|
56
|
+
* something it never asked for.
|
|
57
|
+
*/
|
|
58
|
+
export declare const MIN_SPAN = 1e-200;
|
|
59
|
+
export declare const MAX_SPAN = 1e+200;
|
|
60
|
+
/** A {@link PlaneView} resolved against a box: everything in concrete numbers. */
|
|
61
|
+
export interface ResolvedPlane extends PlaneMap {
|
|
62
|
+
/** The box, in node-local pixels. */
|
|
63
|
+
width: number;
|
|
64
|
+
height: number;
|
|
65
|
+
/** Visible graph range, low to high. */
|
|
66
|
+
xMin: number;
|
|
67
|
+
xMax: number;
|
|
68
|
+
yMin: number;
|
|
69
|
+
yMax: number;
|
|
70
|
+
/** The vertical span actually in force — the derived one when `ySpan` is 0. */
|
|
71
|
+
ySpan: number;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Resolves a view against the node's box: what a graph unit is worth in pixels,
|
|
75
|
+
* and which part of the plane that puts on screen.
|
|
76
|
+
*
|
|
77
|
+
* The single place `ySpan: 0` is turned into a number, so nothing downstream —
|
|
78
|
+
* the grid, the curves, the gesture — has to know the convention exists.
|
|
79
|
+
*/
|
|
80
|
+
export declare function resolvePlane(view: PlaneView, width: number, height: number): ResolvedPlane;
|
|
81
|
+
/** Holds a span inside the range a double can still draw a grid over. */
|
|
82
|
+
export declare function clampSpan(span: number): number;
|
|
83
|
+
/** The node's box as a pixel rectangle, y-up and centred on the origin. */
|
|
84
|
+
export declare function planeBox(plane: ResolvedPlane): PixelRect;
|
|
85
|
+
/** Grows a rectangle by `margin` on every side. */
|
|
86
|
+
export declare function inflate(rect: PixelRect, margin: number): PixelRect;
|
|
87
|
+
/** Graph x → node-local pixels. */
|
|
88
|
+
export declare function toPx(plane: PlaneMap, x: number): number;
|
|
89
|
+
/** Graph y → node-local pixels, y-up. */
|
|
90
|
+
export declare function toPy(plane: PlaneMap, y: number): number;
|
|
91
|
+
/** Slides the view by a delta in **graph units**. Spans are untouched. */
|
|
92
|
+
export declare function panPlane(view: PlaneView, dx: number, dy: number): PlaneView;
|
|
93
|
+
/**
|
|
94
|
+
* Scales the view about a fixed point in **graph units** — the gesture every map
|
|
95
|
+
* makes: whatever was under the cursor stays under the cursor.
|
|
96
|
+
*
|
|
97
|
+
* `factorX`/`factorY` are how much bigger the *window* gets, so a factor above 1
|
|
98
|
+
* zooms out. Passing the same factor twice keeps a square grid square, and is
|
|
99
|
+
* the only case that leaves an auto {@link PlaneView.ySpan} auto: scaling one
|
|
100
|
+
* axis on its own is the statement "this axis is mine now", so the derived span
|
|
101
|
+
* is written down and stops following the box.
|
|
102
|
+
*/
|
|
103
|
+
export declare function zoomPlane(view: PlaneView, plane: ResolvedPlane, factorX: number, factorY: number, anchorX: number, anchorY: number): PlaneView;
|
|
104
|
+
/** One axis's tick ladder: where the numbers go and how finely to rule between. */
|
|
105
|
+
export interface TickLadder {
|
|
106
|
+
/** Distance between labelled ticks, in graph units. */
|
|
107
|
+
step: number;
|
|
108
|
+
/** Distance between the faint lines between them. */
|
|
109
|
+
minorStep: number;
|
|
110
|
+
/**
|
|
111
|
+
* Faint lines inside one labelled step — `step / minorStep`, as a whole
|
|
112
|
+
* number.
|
|
113
|
+
*
|
|
114
|
+
* The faint lines have no list of their own any more: they are generated by
|
|
115
|
+
* the plane's shader from a pitch and a phase, so what the ladder has to hand
|
|
116
|
+
* over is how many of them fit rather than where each one is. Building and
|
|
117
|
+
* filtering a few hundred numbers per axis per frame for a paint that never
|
|
118
|
+
* reads them was most of what the old ladder cost.
|
|
119
|
+
*/
|
|
120
|
+
divisions: number;
|
|
121
|
+
/** Labelled positions across the visible range, ascending. */
|
|
122
|
+
major: number[];
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The tick ladder for a range, at a scale, given the closest two labels may sit.
|
|
126
|
+
*
|
|
127
|
+
* Steps climb the 1–2–5 ladder, which is the one every plotting tool uses
|
|
128
|
+
* because those are the numbers people can subdivide in their heads. Minor lines
|
|
129
|
+
* subdivide a step into 5 — or into 4 when the step is a 2, so that the faint
|
|
130
|
+
* lines land on halves rather than on fifths of a two.
|
|
131
|
+
*/
|
|
132
|
+
export declare function tickLadder(min: number, max: number, pxPerUnit: number, minSpacingPx: number): TickLadder;
|
|
133
|
+
/**
|
|
134
|
+
* A ladder whose numbers are guaranteed not to overlap, however long they are.
|
|
135
|
+
*
|
|
136
|
+
* The base pitch assumes a label of about five characters, which is what an
|
|
137
|
+
* axis anywhere near the origin actually carries. Zoom to `x = 1.23456789` at a
|
|
138
|
+
* span of 1e-6 and every number is fourteen, so the ladder is rebuilt once at a
|
|
139
|
+
* pitch wide enough to hold the widest one it just produced. One extra pass and
|
|
140
|
+
* not a loop: the second ladder is coarser than the first, so its labels are no
|
|
141
|
+
* longer, and a third pass could only ever agree with the second.
|
|
142
|
+
*/
|
|
143
|
+
export declare function labelLadder(min: number, max: number, pxPerUnit: number, fontSize: number): TickLadder;
|
|
144
|
+
/**
|
|
145
|
+
* Room the widest number on `ladder` needs beside its neighbour, in font sizes.
|
|
146
|
+
*
|
|
147
|
+
* Also what the node aligns its numbers within — a right-aligned `-1200` and a
|
|
148
|
+
* right-aligned `5` end in the same place only if the box holds the longer of
|
|
149
|
+
* the two.
|
|
150
|
+
*/
|
|
151
|
+
export declare function labelWidthEm(ladder: TickLadder): number;
|
|
152
|
+
/**
|
|
153
|
+
* How wide a number on this ladder can be, in characters.
|
|
154
|
+
*
|
|
155
|
+
* The longest is not always at an extreme — `-0.5` is longer than `10` — so this
|
|
156
|
+
* scans, which costs at most {@link MAX_TICKS} formats and is a rounding error
|
|
157
|
+
* beside the labels the node is about to shape anyway.
|
|
158
|
+
*/
|
|
159
|
+
export declare function widestLabel(ladder: TickLadder): number;
|
|
160
|
+
/**
|
|
161
|
+
* A tick value as it is written on the axis.
|
|
162
|
+
*
|
|
163
|
+
* The precision comes from the **step**, not from the value: what a number on an
|
|
164
|
+
* axis has to do is tell you which tick it is, so a ladder of 0.2s reads
|
|
165
|
+
* `0.2 0.4 0.6` and the same values on a ladder of 1s would be rounded away
|
|
166
|
+
* rather than shown as `0.2` next to `1`.
|
|
167
|
+
*
|
|
168
|
+
* That is also the rule for choosing the exponent, and the reason the two fixed
|
|
169
|
+
* magnitude thresholds this used to carry are gone. `1e6` as a ceiling is wrong
|
|
170
|
+
* about *both* directions at once: pan to `x = 1e7` at the default zoom and a
|
|
171
|
+
* whole axis of distinct ticks reads `1e7`, `1e7`, `1e7`; while at a span of
|
|
172
|
+
* 1e-9 around `x = 1` no threshold on the magnitude helps at all, because the
|
|
173
|
+
* values are all near 1 and it is the *step* that has run out of room. What
|
|
174
|
+
* decides it is how many significant digits it takes to tell two neighbouring
|
|
175
|
+
* ticks apart: the exponent is only shorter when that count is small and the
|
|
176
|
+
* number is far from 1.
|
|
177
|
+
*/
|
|
178
|
+
export declare function formatTick(value: number, step: number): string;
|
|
179
|
+
//# sourceMappingURL=plane.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plane.d.ts","sourceRoot":"","sources":["../../src/graph2d/plane.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What part of the plane a 2D graph is looking at, and the furniture that makes
|
|
3
|
+
* it readable: the graph→pixel mapping, the tick ladder, and how a number on an
|
|
4
|
+
* axis is written.
|
|
5
|
+
*
|
|
6
|
+
* ## The view is four numbers, and none of them is a matrix
|
|
7
|
+
*
|
|
8
|
+
* `centerX`/`centerY` say where you are, `xSpan`/`ySpan` say how much you can
|
|
9
|
+
* see. Four independently tweenable numbers rather than a transform, for exactly
|
|
10
|
+
* the reason the 3D graph's camera is three: the studio renders a *timeline*, so
|
|
11
|
+
* every frame has to be reproducible from stored values under scrubbing and
|
|
12
|
+
* export, and "pan across while zooming in" has to be one command rather than a
|
|
13
|
+
* hand-built parallel over matrix entries.
|
|
14
|
+
*
|
|
15
|
+
* ## `ySpan: 0` means "keep the grid square"
|
|
16
|
+
*
|
|
17
|
+
* The vertical span is the one number that usually shouldn't be typed. A grapher
|
|
18
|
+
* whose units are square is one where a circle is round and a 45° line looks
|
|
19
|
+
* like one, and the span that achieves that depends on the node's *box* — which
|
|
20
|
+
* the author changes by dragging a handle, not by editing this field. So zero
|
|
21
|
+
* means "derive it", the same convention a node's own width and height use for
|
|
22
|
+
* auto-sizing, and any positive value takes over.
|
|
23
|
+
*
|
|
24
|
+
* Nothing is lost by it: the moment a gesture or a command scales the axes
|
|
25
|
+
* apart, the derived value is written down and the field is an ordinary number
|
|
26
|
+
* from then on — the same handover the canvas makes when a resize drag turns a
|
|
27
|
+
* filling axis into a fixed one.
|
|
28
|
+
*/
|
|
29
|
+
import { niceStep } from "@motionscript/core/component";
|
|
30
|
+
/**
|
|
31
|
+
* Bounds on a span.
|
|
32
|
+
*
|
|
33
|
+
* A hundred decades inside the double's own range, so nothing downstream —
|
|
34
|
+
* `width / span`, `step * scale`, `y * scale` — can overflow or fall into the
|
|
35
|
+
* denormals on the way through.
|
|
36
|
+
*
|
|
37
|
+
* Deliberately far wider than anything usable, because what actually limits how
|
|
38
|
+
* far a plane can be zoomed is not a floor on the span but a **ratio**:
|
|
39
|
+
* `x - centerX` keeps about sixteen significant digits, so a window narrower
|
|
40
|
+
* than `|centerX| · 4.3e-13` has fewer distinct representable x values across it
|
|
41
|
+
* than it has pixels, and the curve staircases whatever these constants say.
|
|
42
|
+
* That limit belongs to the *gesture* — see `planeSpanLimits` — which is the
|
|
43
|
+
* thing a person can be stopped at. A scene that deliberately authored a deeper
|
|
44
|
+
* zoom still renders, badly and visibly, rather than being silently clamped to
|
|
45
|
+
* something it never asked for.
|
|
46
|
+
*/
|
|
47
|
+
export const MIN_SPAN = 1e-200;
|
|
48
|
+
export const MAX_SPAN = 1e200;
|
|
49
|
+
/**
|
|
50
|
+
* Resolves a view against the node's box: what a graph unit is worth in pixels,
|
|
51
|
+
* and which part of the plane that puts on screen.
|
|
52
|
+
*
|
|
53
|
+
* The single place `ySpan: 0` is turned into a number, so nothing downstream —
|
|
54
|
+
* the grid, the curves, the gesture — has to know the convention exists.
|
|
55
|
+
*/
|
|
56
|
+
export function resolvePlane(view, width, height) {
|
|
57
|
+
const w = Math.max(1, width);
|
|
58
|
+
const h = Math.max(1, height);
|
|
59
|
+
const xSpan = clampSpan(view.xSpan);
|
|
60
|
+
const scaleX = w / xSpan;
|
|
61
|
+
// Square units: a graph unit is the same number of pixels either way, so the
|
|
62
|
+
// visible height is however many of them fit in the box.
|
|
63
|
+
const ySpan = view.ySpan > 0 ? clampSpan(view.ySpan) : h / scaleX;
|
|
64
|
+
const scaleY = h / ySpan;
|
|
65
|
+
return {
|
|
66
|
+
centerX: view.centerX,
|
|
67
|
+
centerY: view.centerY,
|
|
68
|
+
scaleX,
|
|
69
|
+
scaleY,
|
|
70
|
+
width: w,
|
|
71
|
+
height: h,
|
|
72
|
+
xMin: view.centerX - xSpan / 2,
|
|
73
|
+
xMax: view.centerX + xSpan / 2,
|
|
74
|
+
yMin: view.centerY - ySpan / 2,
|
|
75
|
+
yMax: view.centerY + ySpan / 2,
|
|
76
|
+
ySpan,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** Holds a span inside the range a double can still draw a grid over. */
|
|
80
|
+
export function clampSpan(span) {
|
|
81
|
+
if (!Number.isFinite(span) || span <= 0)
|
|
82
|
+
return MIN_SPAN;
|
|
83
|
+
return Math.min(MAX_SPAN, Math.max(MIN_SPAN, span));
|
|
84
|
+
}
|
|
85
|
+
/** The node's box as a pixel rectangle, y-up and centred on the origin. */
|
|
86
|
+
export function planeBox(plane) {
|
|
87
|
+
return {
|
|
88
|
+
left: -plane.width / 2,
|
|
89
|
+
right: plane.width / 2,
|
|
90
|
+
bottom: -plane.height / 2,
|
|
91
|
+
top: plane.height / 2,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** Grows a rectangle by `margin` on every side. */
|
|
95
|
+
export function inflate(rect, margin) {
|
|
96
|
+
return {
|
|
97
|
+
left: rect.left - margin,
|
|
98
|
+
right: rect.right + margin,
|
|
99
|
+
bottom: rect.bottom - margin,
|
|
100
|
+
top: rect.top + margin,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/** Graph x → node-local pixels. */
|
|
104
|
+
export function toPx(plane, x) {
|
|
105
|
+
return (x - plane.centerX) * plane.scaleX;
|
|
106
|
+
}
|
|
107
|
+
/** Graph y → node-local pixels, y-up. */
|
|
108
|
+
export function toPy(plane, y) {
|
|
109
|
+
return (y - plane.centerY) * plane.scaleY;
|
|
110
|
+
}
|
|
111
|
+
// --- Moving the view -------------------------------------------------------
|
|
112
|
+
/** Slides the view by a delta in **graph units**. Spans are untouched. */
|
|
113
|
+
export function panPlane(view, dx, dy) {
|
|
114
|
+
return {
|
|
115
|
+
...view,
|
|
116
|
+
centerX: view.centerX + dx,
|
|
117
|
+
centerY: view.centerY + dy,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Scales the view about a fixed point in **graph units** — the gesture every map
|
|
122
|
+
* makes: whatever was under the cursor stays under the cursor.
|
|
123
|
+
*
|
|
124
|
+
* `factorX`/`factorY` are how much bigger the *window* gets, so a factor above 1
|
|
125
|
+
* zooms out. Passing the same factor twice keeps a square grid square, and is
|
|
126
|
+
* the only case that leaves an auto {@link PlaneView.ySpan} auto: scaling one
|
|
127
|
+
* axis on its own is the statement "this axis is mine now", so the derived span
|
|
128
|
+
* is written down and stops following the box.
|
|
129
|
+
*/
|
|
130
|
+
export function zoomPlane(view, plane, factorX, factorY, anchorX, anchorY) {
|
|
131
|
+
const uniform = factorX === factorY && view.ySpan <= 0;
|
|
132
|
+
const xSpan = clampSpan(plane.xMax - plane.xMin) * factorX;
|
|
133
|
+
const ySpan = plane.ySpan * factorY;
|
|
134
|
+
return {
|
|
135
|
+
// The anchor keeps its distance from each edge as a *fraction* of the span,
|
|
136
|
+
// which is what "stays under the cursor" means once the span has changed.
|
|
137
|
+
//
|
|
138
|
+
// Written as a *displacement from the current centre* rather than as
|
|
139
|
+
// `anchor + (centre - anchor) · factor`, which is the same expression
|
|
140
|
+
// rearranged and much better conditioned. The anchor is up to half a span
|
|
141
|
+
// away from the centre, so at a wide view the direct form subtracts two
|
|
142
|
+
// enormous numbers to recover a small one and loses it: at a span of 1e200 a
|
|
143
|
+
// centre of 4 comes back as 0, and at a stop — where the factor is exactly 1
|
|
144
|
+
// and the centre must not move at all — it came back as 0 rather than as 4.
|
|
145
|
+
// This form multiplies the big quantity by `1 - factor`, which is what is
|
|
146
|
+
// actually small, and is bit-exact when the wheel is doing nothing.
|
|
147
|
+
centerX: view.centerX + (anchorX - view.centerX) * (1 - factorX),
|
|
148
|
+
centerY: view.centerY + (anchorY - view.centerY) * (1 - factorY),
|
|
149
|
+
xSpan: clampSpan(xSpan),
|
|
150
|
+
ySpan: uniform ? 0 : clampSpan(ySpan),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Cap on how many *numbers* one axis may carry.
|
|
155
|
+
*
|
|
156
|
+
* The step is chosen from how close two labels may sit, so this is unreachable
|
|
157
|
+
* in normal use — a 1920-px axis at the tightest legal pitch holds about
|
|
158
|
+
* seventeen. It is there because `xSpan` is an animatable number and a tween
|
|
159
|
+
* passing through an absurd value must not try to shape a thousand strings for
|
|
160
|
+
* one frame. Half what it was, because it no longer bounds the grid: the shader
|
|
161
|
+
* rules the plane at O(1) however far it is zoomed.
|
|
162
|
+
*/
|
|
163
|
+
const MAX_TICKS = 200;
|
|
164
|
+
/**
|
|
165
|
+
* Largest tick index the ladder will build from.
|
|
166
|
+
*
|
|
167
|
+
* Past `2^53` an integer is no longer exactly representable, so `first + i`
|
|
168
|
+
* repeats values and `(first + i) * step` produces garbage. At that view the
|
|
169
|
+
* ticks genuinely have no distinct values, and an empty ladder says so.
|
|
170
|
+
*/
|
|
171
|
+
const MAX_INDEX = Number.MAX_SAFE_INTEGER;
|
|
172
|
+
/**
|
|
173
|
+
* The decade a positive number sits in: `1` for 42, `-2` for 0.03.
|
|
174
|
+
*
|
|
175
|
+
* The nudge is not cosmetic. `Math.log10(1e-7)` is `-7.000000000000001` in V8,
|
|
176
|
+
* so a bare `floor` answers -8 for an exact power of ten — which put the leading
|
|
177
|
+
* digit of a 1e-7 step at 10 and handed {@link minorDivisions} the wrong answer
|
|
178
|
+
* on several perfectly ordinary zooms.
|
|
179
|
+
*/
|
|
180
|
+
function decadeOf(value) {
|
|
181
|
+
return Math.floor(Math.log10(value) + 1e-12);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* The tick ladder for a range, at a scale, given the closest two labels may sit.
|
|
185
|
+
*
|
|
186
|
+
* Steps climb the 1–2–5 ladder, which is the one every plotting tool uses
|
|
187
|
+
* because those are the numbers people can subdivide in their heads. Minor lines
|
|
188
|
+
* subdivide a step into 5 — or into 4 when the step is a 2, so that the faint
|
|
189
|
+
* lines land on halves rather than on fifths of a two.
|
|
190
|
+
*/
|
|
191
|
+
export function tickLadder(min, max, pxPerUnit, minSpacingPx) {
|
|
192
|
+
const span = max - min;
|
|
193
|
+
if (!(span > 0) || !(pxPerUnit > 0)) {
|
|
194
|
+
return { step: 1, minorStep: 1, divisions: 1, major: [] };
|
|
195
|
+
}
|
|
196
|
+
const step = niceStep(Math.max(minSpacingPx, 1) / pxPerUnit);
|
|
197
|
+
const divisions = minorDivisions(step);
|
|
198
|
+
return {
|
|
199
|
+
step,
|
|
200
|
+
minorStep: step / divisions,
|
|
201
|
+
divisions,
|
|
202
|
+
major: multiplesWithin(min, max, step),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* A digit's advance in the built-in face, as a fraction of the font size.
|
|
207
|
+
*
|
|
208
|
+
* Approximate on purpose: the node draws its numbers through the renderer's
|
|
209
|
+
* shaper and this arithmetic runs in `scene-core`, which has no font metrics and
|
|
210
|
+
* shouldn't grow any. It only has to be close enough to decide whether two
|
|
211
|
+
* labels would collide, and it errs wide.
|
|
212
|
+
*/
|
|
213
|
+
const DIGIT_EM = 0.58;
|
|
214
|
+
/** Blank either side of a number, in font sizes. */
|
|
215
|
+
const LABEL_MARGIN = 0.8;
|
|
216
|
+
/**
|
|
217
|
+
* Room an x-axis number needs before the ladder steps up, as multiples of its
|
|
218
|
+
* font size — the pitch {@link labelLadder} widens *from*.
|
|
219
|
+
*
|
|
220
|
+
* Wider than the five characters it nominally buys, and left that way: it is
|
|
221
|
+
* what every graph anywhere near the origin has always been ruled at, and
|
|
222
|
+
* tightening it here would re-space every existing scene to fix a problem only
|
|
223
|
+
* deep zoom has.
|
|
224
|
+
*/
|
|
225
|
+
const BASE_LABEL_PITCH = 5;
|
|
226
|
+
/**
|
|
227
|
+
* A ladder whose numbers are guaranteed not to overlap, however long they are.
|
|
228
|
+
*
|
|
229
|
+
* The base pitch assumes a label of about five characters, which is what an
|
|
230
|
+
* axis anywhere near the origin actually carries. Zoom to `x = 1.23456789` at a
|
|
231
|
+
* span of 1e-6 and every number is fourteen, so the ladder is rebuilt once at a
|
|
232
|
+
* pitch wide enough to hold the widest one it just produced. One extra pass and
|
|
233
|
+
* not a loop: the second ladder is coarser than the first, so its labels are no
|
|
234
|
+
* longer, and a third pass could only ever agree with the second.
|
|
235
|
+
*/
|
|
236
|
+
export function labelLadder(min, max, pxPerUnit, fontSize) {
|
|
237
|
+
const base = fontSize * BASE_LABEL_PITCH;
|
|
238
|
+
const ladder = tickLadder(min, max, pxPerUnit, base);
|
|
239
|
+
const needed = fontSize * labelWidthEm(ladder);
|
|
240
|
+
return needed > base ? tickLadder(min, max, pxPerUnit, needed) : ladder;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Room the widest number on `ladder` needs beside its neighbour, in font sizes.
|
|
244
|
+
*
|
|
245
|
+
* Also what the node aligns its numbers within — a right-aligned `-1200` and a
|
|
246
|
+
* right-aligned `5` end in the same place only if the box holds the longer of
|
|
247
|
+
* the two.
|
|
248
|
+
*/
|
|
249
|
+
export function labelWidthEm(ladder) {
|
|
250
|
+
return DIGIT_EM * widestLabel(ladder) + LABEL_MARGIN;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* How wide a number on this ladder can be, in characters.
|
|
254
|
+
*
|
|
255
|
+
* The longest is not always at an extreme — `-0.5` is longer than `10` — so this
|
|
256
|
+
* scans, which costs at most {@link MAX_TICKS} formats and is a rounding error
|
|
257
|
+
* beside the labels the node is about to shape anyway.
|
|
258
|
+
*/
|
|
259
|
+
export function widestLabel(ladder) {
|
|
260
|
+
let widest = 0;
|
|
261
|
+
for (const value of ladder.major) {
|
|
262
|
+
const length = formatTick(value, ladder.step).length;
|
|
263
|
+
if (length > widest)
|
|
264
|
+
widest = length;
|
|
265
|
+
}
|
|
266
|
+
return widest;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* How many faint lines a step is divided into. A 1 or a 5 takes five, a 2 takes
|
|
270
|
+
* four — see {@link tickLadder}.
|
|
271
|
+
*/
|
|
272
|
+
function minorDivisions(step) {
|
|
273
|
+
const magnitude = Math.pow(10, decadeOf(step));
|
|
274
|
+
const leading = Math.round(step / magnitude);
|
|
275
|
+
return leading === 2 ? 4 : 5;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Every multiple of `step` within `[min, max]`, ascending.
|
|
279
|
+
*
|
|
280
|
+
* Built from an integer index times the step rather than by accumulation, so the
|
|
281
|
+
* hundredth tick is still exactly a hundred steps out — repeated addition drifts
|
|
282
|
+
* enough to put a `0.30000000000000004` on an axis.
|
|
283
|
+
*/
|
|
284
|
+
function multiplesWithin(min, max, step) {
|
|
285
|
+
// The nudge absorbs the case where a bound *is* a multiple and lands a hair
|
|
286
|
+
// outside it after the division, which would drop the tick on the edge.
|
|
287
|
+
const first = Math.ceil(min / step - 1e-9);
|
|
288
|
+
const last = Math.floor(max / step + 1e-9);
|
|
289
|
+
if (!Number.isFinite(first) || !Number.isFinite(last))
|
|
290
|
+
return [];
|
|
291
|
+
if (Math.abs(first) > MAX_INDEX || Math.abs(last) > MAX_INDEX)
|
|
292
|
+
return [];
|
|
293
|
+
const count = last - first + 1;
|
|
294
|
+
if (count <= 0 || count > MAX_TICKS)
|
|
295
|
+
return [];
|
|
296
|
+
const out = new Array(count);
|
|
297
|
+
for (let i = 0; i < count; i++)
|
|
298
|
+
out[i] = (first + i) * step;
|
|
299
|
+
return out;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Largest number of digits worth writing out in full, before the exponent is the
|
|
303
|
+
* shorter answer.
|
|
304
|
+
*/
|
|
305
|
+
const MAX_DIGITS = 14;
|
|
306
|
+
/** `toFixed` and `toExponential` both refuse an argument past 100. */
|
|
307
|
+
const MAX_FRACTION = 100;
|
|
308
|
+
/**
|
|
309
|
+
* A tick value as it is written on the axis.
|
|
310
|
+
*
|
|
311
|
+
* The precision comes from the **step**, not from the value: what a number on an
|
|
312
|
+
* axis has to do is tell you which tick it is, so a ladder of 0.2s reads
|
|
313
|
+
* `0.2 0.4 0.6` and the same values on a ladder of 1s would be rounded away
|
|
314
|
+
* rather than shown as `0.2` next to `1`.
|
|
315
|
+
*
|
|
316
|
+
* That is also the rule for choosing the exponent, and the reason the two fixed
|
|
317
|
+
* magnitude thresholds this used to carry are gone. `1e6` as a ceiling is wrong
|
|
318
|
+
* about *both* directions at once: pan to `x = 1e7` at the default zoom and a
|
|
319
|
+
* whole axis of distinct ticks reads `1e7`, `1e7`, `1e7`; while at a span of
|
|
320
|
+
* 1e-9 around `x = 1` no threshold on the magnitude helps at all, because the
|
|
321
|
+
* values are all near 1 and it is the *step* that has run out of room. What
|
|
322
|
+
* decides it is how many significant digits it takes to tell two neighbouring
|
|
323
|
+
* ticks apart: the exponent is only shorter when that count is small and the
|
|
324
|
+
* number is far from 1.
|
|
325
|
+
*/
|
|
326
|
+
export function formatTick(value, step) {
|
|
327
|
+
if (value === 0)
|
|
328
|
+
return "0";
|
|
329
|
+
if (!Number.isFinite(value))
|
|
330
|
+
return "";
|
|
331
|
+
if (!(step > 0) || !Number.isFinite(step))
|
|
332
|
+
return String(value);
|
|
333
|
+
const decade = decadeOf(Math.abs(value));
|
|
334
|
+
// At least one: a value smaller than its own step isn't a tick of this ladder,
|
|
335
|
+
// but it is still a number somebody may ask this to write.
|
|
336
|
+
const digits = Math.max(1, decade - decadeOf(step) + 1);
|
|
337
|
+
// Far from 1 and cheap to write in scientific form — `2e7`, `1.5e-9`.
|
|
338
|
+
if ((decade >= 6 || decade <= -5) && digits <= 6) {
|
|
339
|
+
return trimExponent(value.toExponential(digits - 1));
|
|
340
|
+
}
|
|
341
|
+
if (digits <= MAX_DIGITS) {
|
|
342
|
+
const decimals = Math.min(MAX_FRACTION, Math.max(0, -decadeOf(step)));
|
|
343
|
+
const text = value.toFixed(decimals);
|
|
344
|
+
// `toFixed` keeps the zeros a nice step can't produce (0.50 for a 0.1 ladder
|
|
345
|
+
// is only ever written that way by the formatter).
|
|
346
|
+
return decimals > 0 ? text.replace(/\.?0+$/, "") : text;
|
|
347
|
+
}
|
|
348
|
+
// More digits than anybody can read off an axis. The exponent at least keeps
|
|
349
|
+
// the ticks distinguishable from one another.
|
|
350
|
+
const precision = Math.min(17, Math.max(0, digits - 1));
|
|
351
|
+
return trimExponent(value.toExponential(precision));
|
|
352
|
+
}
|
|
353
|
+
/** `1.20e-7` → `1.2e-7`, and `1.00e+21` → `1e21`. */
|
|
354
|
+
function trimExponent(text) {
|
|
355
|
+
const [mantissa, exponent] = text.split("e");
|
|
356
|
+
const trimmed = mantissa.replace(/\.?0+$/, "");
|
|
357
|
+
return `${trimmed}e${Number(exponent)}`;
|
|
358
|
+
}
|
|
359
|
+
//# sourceMappingURL=plane.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plane.js","sourceRoot":"","sources":["../../src/graph2d/plane.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What part of the plane a 2D graph is looking at, and the furniture that makes\n * it readable: the graph→pixel mapping, the tick ladder, and how a number on an\n * axis is written.\n *\n * ## The view is four numbers, and none of them is a matrix\n *\n * `centerX`/`centerY` say where you are, `xSpan`/`ySpan` say how much you can\n * see. Four independently tweenable numbers rather than a transform, for exactly\n * the reason the 3D graph's camera is three: the studio renders a *timeline*, so\n * every frame has to be reproducible from stored values under scrubbing and\n * export, and \"pan across while zooming in\" has to be one command rather than a\n * hand-built parallel over matrix entries.\n *\n * ## `ySpan: 0` means \"keep the grid square\"\n *\n * The vertical span is the one number that usually shouldn't be typed. A grapher\n * whose units are square is one where a circle is round and a 45° line looks\n * like one, and the span that achieves that depends on the node's *box* — which\n * the author changes by dragging a handle, not by editing this field. So zero\n * means \"derive it\", the same convention a node's own width and height use for\n * auto-sizing, and any positive value takes over.\n *\n * Nothing is lost by it: the moment a gesture or a command scales the axes\n * apart, the derived value is written down and the field is an ordinary number\n * from then on — the same handover the canvas makes when a resize drag turns a\n * filling axis into a fixed one.\n */\n\nimport { niceStep } from \"@motionscript/core/component\"\nimport type { PixelRect, PlaneMap } from \"./curve\"\n\n/** Where a 2D graph is looking, as the four numbers its props hold. */\nexport interface PlaneView {\n /** Graph x at the centre of the node's box. */\n centerX: number\n /** Graph y at the centre of the node's box. */\n centerY: number\n /** Width of the visible window, in graph units. */\n xSpan: number\n /** Height of the visible window in graph units, or 0 to keep units square. */\n ySpan: number\n}\n\n/**\n * Bounds on a span.\n *\n * A hundred decades inside the double's own range, so nothing downstream —\n * `width / span`, `step * scale`, `y * scale` — can overflow or fall into the\n * denormals on the way through.\n *\n * Deliberately far wider than anything usable, because what actually limits how\n * far a plane can be zoomed is not a floor on the span but a **ratio**:\n * `x - centerX` keeps about sixteen significant digits, so a window narrower\n * than `|centerX| · 4.3e-13` has fewer distinct representable x values across it\n * than it has pixels, and the curve staircases whatever these constants say.\n * That limit belongs to the *gesture* — see `planeSpanLimits` — which is the\n * thing a person can be stopped at. A scene that deliberately authored a deeper\n * zoom still renders, badly and visibly, rather than being silently clamped to\n * something it never asked for.\n */\nexport const MIN_SPAN = 1e-200\nexport const MAX_SPAN = 1e200\n\n/** A {@link PlaneView} resolved against a box: everything in concrete numbers. */\nexport interface ResolvedPlane extends PlaneMap {\n /** The box, in node-local pixels. */\n width: number\n height: number\n /** Visible graph range, low to high. */\n xMin: number\n xMax: number\n yMin: number\n yMax: number\n /** The vertical span actually in force — the derived one when `ySpan` is 0. */\n ySpan: number\n}\n\n/**\n * Resolves a view against the node's box: what a graph unit is worth in pixels,\n * and which part of the plane that puts on screen.\n *\n * The single place `ySpan: 0` is turned into a number, so nothing downstream —\n * the grid, the curves, the gesture — has to know the convention exists.\n */\nexport function resolvePlane(\n view: PlaneView,\n width: number,\n height: number\n): ResolvedPlane {\n const w = Math.max(1, width)\n const h = Math.max(1, height)\n const xSpan = clampSpan(view.xSpan)\n const scaleX = w / xSpan\n // Square units: a graph unit is the same number of pixels either way, so the\n // visible height is however many of them fit in the box.\n const ySpan = view.ySpan > 0 ? clampSpan(view.ySpan) : h / scaleX\n const scaleY = h / ySpan\n\n return {\n centerX: view.centerX,\n centerY: view.centerY,\n scaleX,\n scaleY,\n width: w,\n height: h,\n xMin: view.centerX - xSpan / 2,\n xMax: view.centerX + xSpan / 2,\n yMin: view.centerY - ySpan / 2,\n yMax: view.centerY + ySpan / 2,\n ySpan,\n }\n}\n\n/** Holds a span inside the range a double can still draw a grid over. */\nexport function clampSpan(span: number): number {\n if (!Number.isFinite(span) || span <= 0) return MIN_SPAN\n return Math.min(MAX_SPAN, Math.max(MIN_SPAN, span))\n}\n\n/** The node's box as a pixel rectangle, y-up and centred on the origin. */\nexport function planeBox(plane: ResolvedPlane): PixelRect {\n return {\n left: -plane.width / 2,\n right: plane.width / 2,\n bottom: -plane.height / 2,\n top: plane.height / 2,\n }\n}\n\n/** Grows a rectangle by `margin` on every side. */\nexport function inflate(rect: PixelRect, margin: number): PixelRect {\n return {\n left: rect.left - margin,\n right: rect.right + margin,\n bottom: rect.bottom - margin,\n top: rect.top + margin,\n }\n}\n\n/** Graph x → node-local pixels. */\nexport function toPx(plane: PlaneMap, x: number): number {\n return (x - plane.centerX) * plane.scaleX\n}\n\n/** Graph y → node-local pixels, y-up. */\nexport function toPy(plane: PlaneMap, y: number): number {\n return (y - plane.centerY) * plane.scaleY\n}\n\n// --- Moving the view -------------------------------------------------------\n\n/** Slides the view by a delta in **graph units**. Spans are untouched. */\nexport function panPlane(\n view: PlaneView,\n dx: number,\n dy: number\n): PlaneView {\n return {\n ...view,\n centerX: view.centerX + dx,\n centerY: view.centerY + dy,\n }\n}\n\n/**\n * Scales the view about a fixed point in **graph units** — the gesture every map\n * makes: whatever was under the cursor stays under the cursor.\n *\n * `factorX`/`factorY` are how much bigger the *window* gets, so a factor above 1\n * zooms out. Passing the same factor twice keeps a square grid square, and is\n * the only case that leaves an auto {@link PlaneView.ySpan} auto: scaling one\n * axis on its own is the statement \"this axis is mine now\", so the derived span\n * is written down and stops following the box.\n */\nexport function zoomPlane(\n view: PlaneView,\n plane: ResolvedPlane,\n factorX: number,\n factorY: number,\n anchorX: number,\n anchorY: number\n): PlaneView {\n const uniform = factorX === factorY && view.ySpan <= 0\n const xSpan = clampSpan(plane.xMax - plane.xMin) * factorX\n const ySpan = plane.ySpan * factorY\n return {\n // The anchor keeps its distance from each edge as a *fraction* of the span,\n // which is what \"stays under the cursor\" means once the span has changed.\n //\n // Written as a *displacement from the current centre* rather than as\n // `anchor + (centre - anchor) · factor`, which is the same expression\n // rearranged and much better conditioned. The anchor is up to half a span\n // away from the centre, so at a wide view the direct form subtracts two\n // enormous numbers to recover a small one and loses it: at a span of 1e200 a\n // centre of 4 comes back as 0, and at a stop — where the factor is exactly 1\n // and the centre must not move at all — it came back as 0 rather than as 4.\n // This form multiplies the big quantity by `1 - factor`, which is what is\n // actually small, and is bit-exact when the wheel is doing nothing.\n centerX: view.centerX + (anchorX - view.centerX) * (1 - factorX),\n centerY: view.centerY + (anchorY - view.centerY) * (1 - factorY),\n xSpan: clampSpan(xSpan),\n ySpan: uniform ? 0 : clampSpan(ySpan),\n }\n}\n\n// --- Ticks -----------------------------------------------------------------\n\n/** One axis's tick ladder: where the numbers go and how finely to rule between. */\nexport interface TickLadder {\n /** Distance between labelled ticks, in graph units. */\n step: number\n /** Distance between the faint lines between them. */\n minorStep: number\n /**\n * Faint lines inside one labelled step — `step / minorStep`, as a whole\n * number.\n *\n * The faint lines have no list of their own any more: they are generated by\n * the plane's shader from a pitch and a phase, so what the ladder has to hand\n * over is how many of them fit rather than where each one is. Building and\n * filtering a few hundred numbers per axis per frame for a paint that never\n * reads them was most of what the old ladder cost.\n */\n divisions: number\n /** Labelled positions across the visible range, ascending. */\n major: number[]\n}\n\n/**\n * Cap on how many *numbers* one axis may carry.\n *\n * The step is chosen from how close two labels may sit, so this is unreachable\n * in normal use — a 1920-px axis at the tightest legal pitch holds about\n * seventeen. It is there because `xSpan` is an animatable number and a tween\n * passing through an absurd value must not try to shape a thousand strings for\n * one frame. Half what it was, because it no longer bounds the grid: the shader\n * rules the plane at O(1) however far it is zoomed.\n */\nconst MAX_TICKS = 200\n\n/**\n * Largest tick index the ladder will build from.\n *\n * Past `2^53` an integer is no longer exactly representable, so `first + i`\n * repeats values and `(first + i) * step` produces garbage. At that view the\n * ticks genuinely have no distinct values, and an empty ladder says so.\n */\nconst MAX_INDEX = Number.MAX_SAFE_INTEGER\n\n/**\n * The decade a positive number sits in: `1` for 42, `-2` for 0.03.\n *\n * The nudge is not cosmetic. `Math.log10(1e-7)` is `-7.000000000000001` in V8,\n * so a bare `floor` answers -8 for an exact power of ten — which put the leading\n * digit of a 1e-7 step at 10 and handed {@link minorDivisions} the wrong answer\n * on several perfectly ordinary zooms.\n */\nfunction decadeOf(value: number): number {\n return Math.floor(Math.log10(value) + 1e-12)\n}\n\n/**\n * The tick ladder for a range, at a scale, given the closest two labels may sit.\n *\n * Steps climb the 1–2–5 ladder, which is the one every plotting tool uses\n * because those are the numbers people can subdivide in their heads. Minor lines\n * subdivide a step into 5 — or into 4 when the step is a 2, so that the faint\n * lines land on halves rather than on fifths of a two.\n */\nexport function tickLadder(\n min: number,\n max: number,\n pxPerUnit: number,\n minSpacingPx: number\n): TickLadder {\n const span = max - min\n if (!(span > 0) || !(pxPerUnit > 0)) {\n return { step: 1, minorStep: 1, divisions: 1, major: [] }\n }\n\n const step = niceStep(Math.max(minSpacingPx, 1) / pxPerUnit)\n const divisions = minorDivisions(step)\n\n return {\n step,\n minorStep: step / divisions,\n divisions,\n major: multiplesWithin(min, max, step),\n }\n}\n\n/**\n * A digit's advance in the built-in face, as a fraction of the font size.\n *\n * Approximate on purpose: the node draws its numbers through the renderer's\n * shaper and this arithmetic runs in `scene-core`, which has no font metrics and\n * shouldn't grow any. It only has to be close enough to decide whether two\n * labels would collide, and it errs wide.\n */\nconst DIGIT_EM = 0.58\n/** Blank either side of a number, in font sizes. */\nconst LABEL_MARGIN = 0.8\n/**\n * Room an x-axis number needs before the ladder steps up, as multiples of its\n * font size — the pitch {@link labelLadder} widens *from*.\n *\n * Wider than the five characters it nominally buys, and left that way: it is\n * what every graph anywhere near the origin has always been ruled at, and\n * tightening it here would re-space every existing scene to fix a problem only\n * deep zoom has.\n */\nconst BASE_LABEL_PITCH = 5\n\n/**\n * A ladder whose numbers are guaranteed not to overlap, however long they are.\n *\n * The base pitch assumes a label of about five characters, which is what an\n * axis anywhere near the origin actually carries. Zoom to `x = 1.23456789` at a\n * span of 1e-6 and every number is fourteen, so the ladder is rebuilt once at a\n * pitch wide enough to hold the widest one it just produced. One extra pass and\n * not a loop: the second ladder is coarser than the first, so its labels are no\n * longer, and a third pass could only ever agree with the second.\n */\nexport function labelLadder(\n min: number,\n max: number,\n pxPerUnit: number,\n fontSize: number\n): TickLadder {\n const base = fontSize * BASE_LABEL_PITCH\n const ladder = tickLadder(min, max, pxPerUnit, base)\n const needed = fontSize * labelWidthEm(ladder)\n return needed > base ? tickLadder(min, max, pxPerUnit, needed) : ladder\n}\n\n/**\n * Room the widest number on `ladder` needs beside its neighbour, in font sizes.\n *\n * Also what the node aligns its numbers within — a right-aligned `-1200` and a\n * right-aligned `5` end in the same place only if the box holds the longer of\n * the two.\n */\nexport function labelWidthEm(ladder: TickLadder): number {\n return DIGIT_EM * widestLabel(ladder) + LABEL_MARGIN\n}\n\n/**\n * How wide a number on this ladder can be, in characters.\n *\n * The longest is not always at an extreme — `-0.5` is longer than `10` — so this\n * scans, which costs at most {@link MAX_TICKS} formats and is a rounding error\n * beside the labels the node is about to shape anyway.\n */\nexport function widestLabel(ladder: TickLadder): number {\n let widest = 0\n for (const value of ladder.major) {\n const length = formatTick(value, ladder.step).length\n if (length > widest) widest = length\n }\n return widest\n}\n\n/**\n * How many faint lines a step is divided into. A 1 or a 5 takes five, a 2 takes\n * four — see {@link tickLadder}.\n */\nfunction minorDivisions(step: number): number {\n const magnitude = Math.pow(10, decadeOf(step))\n const leading = Math.round(step / magnitude)\n return leading === 2 ? 4 : 5\n}\n\n/**\n * Every multiple of `step` within `[min, max]`, ascending.\n *\n * Built from an integer index times the step rather than by accumulation, so the\n * hundredth tick is still exactly a hundred steps out — repeated addition drifts\n * enough to put a `0.30000000000000004` on an axis.\n */\nfunction multiplesWithin(min: number, max: number, step: number): number[] {\n // The nudge absorbs the case where a bound *is* a multiple and lands a hair\n // outside it after the division, which would drop the tick on the edge.\n const first = Math.ceil(min / step - 1e-9)\n const last = Math.floor(max / step + 1e-9)\n if (!Number.isFinite(first) || !Number.isFinite(last)) return []\n if (Math.abs(first) > MAX_INDEX || Math.abs(last) > MAX_INDEX) return []\n\n const count = last - first + 1\n if (count <= 0 || count > MAX_TICKS) return []\n\n const out: number[] = new Array(count)\n for (let i = 0; i < count; i++) out[i] = (first + i) * step\n return out\n}\n\n/**\n * Largest number of digits worth writing out in full, before the exponent is the\n * shorter answer.\n */\nconst MAX_DIGITS = 14\n/** `toFixed` and `toExponential` both refuse an argument past 100. */\nconst MAX_FRACTION = 100\n\n/**\n * A tick value as it is written on the axis.\n *\n * The precision comes from the **step**, not from the value: what a number on an\n * axis has to do is tell you which tick it is, so a ladder of 0.2s reads\n * `0.2 0.4 0.6` and the same values on a ladder of 1s would be rounded away\n * rather than shown as `0.2` next to `1`.\n *\n * That is also the rule for choosing the exponent, and the reason the two fixed\n * magnitude thresholds this used to carry are gone. `1e6` as a ceiling is wrong\n * about *both* directions at once: pan to `x = 1e7` at the default zoom and a\n * whole axis of distinct ticks reads `1e7`, `1e7`, `1e7`; while at a span of\n * 1e-9 around `x = 1` no threshold on the magnitude helps at all, because the\n * values are all near 1 and it is the *step* that has run out of room. What\n * decides it is how many significant digits it takes to tell two neighbouring\n * ticks apart: the exponent is only shorter when that count is small and the\n * number is far from 1.\n */\nexport function formatTick(value: number, step: number): string {\n if (value === 0) return \"0\"\n if (!Number.isFinite(value)) return \"\"\n if (!(step > 0) || !Number.isFinite(step)) return String(value)\n\n const decade = decadeOf(Math.abs(value))\n // At least one: a value smaller than its own step isn't a tick of this ladder,\n // but it is still a number somebody may ask this to write.\n const digits = Math.max(1, decade - decadeOf(step) + 1)\n\n // Far from 1 and cheap to write in scientific form — `2e7`, `1.5e-9`.\n if ((decade >= 6 || decade <= -5) && digits <= 6) {\n return trimExponent(value.toExponential(digits - 1))\n }\n\n if (digits <= MAX_DIGITS) {\n const decimals = Math.min(MAX_FRACTION, Math.max(0, -decadeOf(step)))\n const text = value.toFixed(decimals)\n // `toFixed` keeps the zeros a nice step can't produce (0.50 for a 0.1 ladder\n // is only ever written that way by the formatter).\n return decimals > 0 ? text.replace(/\\.?0+$/, \"\") : text\n }\n\n // More digits than anybody can read off an axis. The exponent at least keeps\n // the ticks distinguishable from one another.\n const precision = Math.min(17, Math.max(0, digits - 1))\n return trimExponent(value.toExponential(precision))\n}\n\n/** `1.20e-7` → `1.2e-7`, and `1.00e+21` → `1e21`. */\nfunction trimExponent(text: string): string {\n const [mantissa, exponent] = text.split(\"e\")\n const trimmed = mantissa.replace(/\\.?0+$/, \"\")\n return `${trimmed}e${Number(exponent)}`\n}\n"]}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The mappers and tweens the {@link Graph2D} node is built from — the same shape
|
|
3
|
+
* `graph3d/impl/shared.ts` has, and mostly the same short list, because a
|
|
4
|
+
* `@property` is only as good as the `mapper`/`tween` it is declared with.
|
|
5
|
+
*
|
|
6
|
+
* Almost everything here is a re-export. That is the point: the match-by-id
|
|
7
|
+
* equation tween lives in `nodes/graph-kit` because both graphs animate a list of
|
|
8
|
+
* equations the same way, and colour normalisation lives in `nodes/view3d-kit`
|
|
9
|
+
* because the 3D nodes needed it first. What is genuinely this node's own is one
|
|
10
|
+
* function: turning a two-argument compiled expression into the one-argument
|
|
11
|
+
* curve the sampler asks for, without losing the identity the tween compares.
|
|
12
|
+
*/
|
|
13
|
+
import { type GraphEquationResolved } from "../kit/index.js";
|
|
14
|
+
import type { CurveFunction } from "./curve.js";
|
|
15
|
+
/**
|
|
16
|
+
* Colour normalisation and its tween, plus the two number tweens that fix what a
|
|
17
|
+
* plain lerp gets wrong.
|
|
18
|
+
*
|
|
19
|
+
* They live in `view3d-kit` because the 3D nodes needed them first, and there is
|
|
20
|
+
* nothing three-dimensional about any of them — a `NormalizedColor` is an RGBA
|
|
21
|
+
* tuple, which is both interpolatable and still a valid `Color` to hand back to
|
|
22
|
+
* `Graphics`. Renamed on the way through so the call sites don't read as though
|
|
23
|
+
* this node draws in perspective.
|
|
24
|
+
*/
|
|
25
|
+
export { resolveColor3D as resolveColor, lerpColor3D as lerpColor, lerpCount, snapFlag, snapValue, } from "@motionscript/core/component";
|
|
26
|
+
/**
|
|
27
|
+
* One curve after {@link Graph2D}'s mapper has run: compiled, resolved and fully
|
|
28
|
+
* defaulted, so the per-frame draw never re-derives anything and the tween never
|
|
29
|
+
* tests for an absent field.
|
|
30
|
+
*/
|
|
31
|
+
export type CurveResolved = GraphEquationResolved<CurveFunction>;
|
|
32
|
+
export declare function compileCurveCached(source: string): CurveFunction | null;
|
|
33
|
+
/**
|
|
34
|
+
* Tween for the curve list — the shared match-by-id walk, with the one thing a
|
|
35
|
+
* curve does differently: mid-morph its height is sampled from both functions
|
|
36
|
+
* and blended, so it deforms into the new one rather than cross-fading through
|
|
37
|
+
* a frame where both are drawn.
|
|
38
|
+
*/
|
|
39
|
+
export declare function lerpCurves(from: CurveResolved[], to: CurveResolved[], t: number): CurveResolved[];
|
|
40
|
+
//# sourceMappingURL=shared.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/graph2d/shared.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAIL,KAAK,qBAAqB,EAC3B,MAAM,QAAQ,CAAA;AACf,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAE5C;;;;;;;;;GASG;AACH,OAAO,EACL,cAAc,IAAI,YAAY,EAC9B,WAAW,IAAI,SAAS,EACxB,SAAS,EACT,QAAQ,EACR,SAAS,GACV,MAAM,8BAA8B,CAAA;AAErC;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,qBAAqB,CAAC,aAAa,CAAC,CAAA;AAoBhE,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAYvE;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,aAAa,EAAE,EACrB,EAAE,EAAE,aAAa,EAAE,EACnB,CAAC,EAAE,MAAM,GACR,aAAa,EAAE,CAEjB"}
|