@motionscript/plot 0.0.0-stage → 0.1.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/LICENSE +201 -0
  3. package/dist/browser/chunks/chunk-37IWXGPH.js +2 -0
  4. package/dist/browser/chunks/chunk-37IWXGPH.js.map +7 -0
  5. package/dist/browser/index.js +84 -0
  6. package/dist/browser/index.js.map +7 -0
  7. package/dist/browser/kit.js +2 -0
  8. package/dist/browser/kit.js.map +7 -0
  9. package/dist/browser/manifest.json +12 -0
  10. package/dist/engine.d.ts +14 -0
  11. package/dist/engine.d.ts.map +1 -0
  12. package/dist/engine.js +14 -0
  13. package/dist/engine.js.map +1 -0
  14. package/dist/graph2d/curve-cache.d.ts +60 -0
  15. package/dist/graph2d/curve-cache.d.ts.map +1 -0
  16. package/dist/graph2d/curve-cache.js +79 -0
  17. package/dist/graph2d/curve-cache.js.map +1 -0
  18. package/dist/graph2d/curve-error.d.ts +12 -0
  19. package/dist/graph2d/curve-error.d.ts.map +1 -0
  20. package/dist/graph2d/curve-error.js +15 -0
  21. package/dist/graph2d/curve-error.js.map +1 -0
  22. package/dist/graph2d/curve.d.ts +104 -0
  23. package/dist/graph2d/curve.d.ts.map +1 -0
  24. package/dist/graph2d/curve.js +531 -0
  25. package/dist/graph2d/curve.js.map +1 -0
  26. package/dist/graph2d/graph2d.d.ts +164 -0
  27. package/dist/graph2d/graph2d.d.ts.map +1 -0
  28. package/dist/graph2d/graph2d.js +404 -0
  29. package/dist/graph2d/graph2d.js.map +1 -0
  30. package/dist/graph2d/index.d.ts +32 -0
  31. package/dist/graph2d/index.d.ts.map +1 -0
  32. package/dist/graph2d/index.js +32 -0
  33. package/dist/graph2d/index.js.map +1 -0
  34. package/dist/graph2d/plane-fill.d.ts +110 -0
  35. package/dist/graph2d/plane-fill.d.ts.map +1 -0
  36. package/dist/graph2d/plane-fill.js +248 -0
  37. package/dist/graph2d/plane-fill.js.map +1 -0
  38. package/dist/graph2d/plane.d.ts +179 -0
  39. package/dist/graph2d/plane.d.ts.map +1 -0
  40. package/dist/graph2d/plane.js +359 -0
  41. package/dist/graph2d/plane.js.map +1 -0
  42. package/dist/graph2d/shared.d.ts +40 -0
  43. package/dist/graph2d/shared.d.ts.map +1 -0
  44. package/dist/graph2d/shared.js +70 -0
  45. package/dist/graph2d/shared.js.map +1 -0
  46. package/dist/graph3d/expression.d.ts +30 -0
  47. package/dist/graph3d/expression.d.ts.map +1 -0
  48. package/dist/graph3d/expression.js +35 -0
  49. package/dist/graph3d/expression.js.map +1 -0
  50. package/dist/graph3d/graph3d.d.ts +186 -0
  51. package/dist/graph3d/graph3d.d.ts.map +1 -0
  52. package/dist/graph3d/graph3d.js +404 -0
  53. package/dist/graph3d/graph3d.js.map +1 -0
  54. package/dist/graph3d/index.d.ts +22 -0
  55. package/dist/graph3d/index.d.ts.map +1 -0
  56. package/dist/graph3d/index.js +22 -0
  57. package/dist/graph3d/index.js.map +1 -0
  58. package/dist/graph3d/shared.d.ts +61 -0
  59. package/dist/graph3d/shared.d.ts.map +1 -0
  60. package/dist/graph3d/shared.js +101 -0
  61. package/dist/graph3d/shared.js.map +1 -0
  62. package/dist/index.d.ts +4 -0
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.js +4 -0
  65. package/dist/index.js.map +1 -0
  66. package/dist/kit/equations.d.ts +56 -0
  67. package/dist/kit/equations.d.ts.map +1 -0
  68. package/dist/kit/equations.js +63 -0
  69. package/dist/kit/equations.js.map +1 -0
  70. package/dist/kit/expression.d.ts +60 -0
  71. package/dist/kit/expression.d.ts.map +1 -0
  72. package/dist/kit/expression.js +268 -0
  73. package/dist/kit/expression.js.map +1 -0
  74. package/dist/kit/index.d.ts +15 -0
  75. package/dist/kit/index.d.ts.map +1 -0
  76. package/dist/kit/index.js +13 -0
  77. package/dist/kit/index.js.map +1 -0
  78. package/dist/nodes.d.ts +19 -0
  79. package/dist/nodes.d.ts.map +1 -0
  80. package/dist/nodes.js +19 -0
  81. package/dist/nodes.js.map +1 -0
  82. package/package.json +68 -3
  83. package/registry.json +34 -0
  84. package/src/engine.ts +13 -0
  85. package/src/graph2d/curve-cache.ts +103 -0
  86. package/src/graph2d/curve-error.ts +19 -0
  87. package/src/graph2d/curve.ts +657 -0
  88. package/src/graph2d/graph2d.ts +586 -0
  89. package/src/graph2d/index.ts +31 -0
  90. package/src/graph2d/plane-fill.ts +308 -0
  91. package/src/graph2d/plane.ts +457 -0
  92. package/src/graph2d/shared.ts +103 -0
  93. package/src/graph3d/expression.ts +51 -0
  94. package/src/graph3d/graph3d.ts +615 -0
  95. package/src/graph3d/index.ts +21 -0
  96. package/src/graph3d/shared.ts +143 -0
  97. package/src/index.ts +3 -0
  98. package/src/kit/equations.ts +102 -0
  99. package/src/kit/expression.ts +323 -0
  100. package/src/kit/index.ts +29 -0
  101. package/src/nodes.ts +19 -0
  102. 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/sdk/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/sdk/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/sdk/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,6BAA6B,CAAA;AAEpC;;;;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"}