@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,308 @@
1
+ /**
2
+ * The ruled plane as a **paint** rather than as geometry.
3
+ *
4
+ * The grid used to be one filled rect per line — up to four hundred per axis per
5
+ * order, chained into one `Graphics` so at least the *draws* stayed constant.
6
+ * That is the arrangement `ChartCanvas` makes, and for a chart it is right: a
7
+ * chart's axes are ruled once and then sit still. A grapher's are not. Every
8
+ * rect moves on every frame of a pan, so the renderer's shape cache never hits;
9
+ * worse, the rect *count* changes as the ladder steps, and the cache is keyed by
10
+ * shape index, so a grid that gained a line shifted the key of every curve drawn
11
+ * after it and threw those away too.
12
+ *
13
+ * A shader has none of that. The plane is one shape whatever the zoom, so the
14
+ * curves' cache keys are stable, the wasm path behind it is built once, and the
15
+ * cost of ruling the plane stops depending on how much of it is ruled — which is
16
+ * the whole reason the old {@link MAX_TICKS} cap existed.
17
+ *
18
+ * ## Why not the built-in `grid` fill
19
+ *
20
+ * `Fills.grid` measures its pitch in *divisions of the shape* — deliberately, so
21
+ * a grid of eight stays a grid of eight when the shape resizes. A graph's pitch
22
+ * is decided by the tick ladder and is a different number of pixels on each
23
+ * axis, which one scalar `divisions` cannot say; and it carries one colour,
24
+ * where a graph has two orders in two colours plus its axes.
25
+ *
26
+ * ## Everything variable is a uniform
27
+ *
28
+ * `getOrCompileSkSL` caches by *source string* and never evicts, so a source
29
+ * built per frame is a new program per frame. {@link PLANE_SOURCE} is therefore a
30
+ * module constant and every switch — the two orders, the axes, the paper — is
31
+ * expressed by a uniform that can go to zero.
32
+ *
33
+ * ## The phase is the whole trick
34
+ *
35
+ * Uniforms reach the shader as **fp32**. Handing it "where x = 0 is" would be
36
+ * fine at the default zoom and 1e15 pixels at a deep one off the origin, which
37
+ * is not a number fp32 holds at all. So {@link planeUniforms} does the reduction
38
+ * in double precision on the way in: it splits the centre into a whole number of
39
+ * steps plus a remainder *before* anything is multiplied by the scale, and hands
40
+ * over a phase inside a single cell. Every number the shader sees is bounded by
41
+ * the node's own box.
42
+ */
43
+
44
+ import type { NormalizedColor } from "@motionscript/sdk"
45
+
46
+ import type { ResolvedPlane, TickLadder } from "./plane"
47
+
48
+ /**
49
+ * The plane's paint program.
50
+ *
51
+ * Coverage-based rather than a hard test, and the ramp is one *device* pixel
52
+ * wide (`1 / u_scale`, the idiom every built-in pattern fill uses), so a ruling
53
+ * is a crisp hairline whether the camera is zoomed in on the node or the scene
54
+ * is being exported at four times scale.
55
+ *
56
+ * Uniforms are declared widest-first out of habit rather than necessity — a
57
+ * shader fill's uniforms are bound by *name* through reflection, unlike the
58
+ * built-in fills, whose flat positional array makes declaration order load
59
+ * bearing. `u_origin` and `u_scale` are supplied by the renderer.
60
+ */
61
+ export const PLANE_SOURCE = `
62
+ uniform vec4 u_paper;
63
+ uniform vec4 u_minorColor;
64
+ uniform vec4 u_majorColor;
65
+ uniform vec4 u_axisColor;
66
+ uniform vec2 u_origin;
67
+ uniform vec2 u_pitch;
68
+ uniform vec2 u_phase;
69
+ uniform vec2 u_divisions;
70
+ uniform vec2 u_axisAt;
71
+ uniform float u_majorHalf;
72
+ uniform float u_minorHalf;
73
+ uniform float u_axisHalf;
74
+ uniform float u_scale;
75
+
76
+ // Coverage of a line \`d\` px away, of half-width \`w\`, ramped over \`feather\`.
77
+ //
78
+ // The zero test is not a shortcut. The ramp is centred on the line, so a line of
79
+ // no width still lands half a pixel of coverage on the pixels it runs through —
80
+ // an order you switched off, drawn faintly. Answering it here rather than in the
81
+ // ramp is what lets one compiled program serve showGrid, showMinorGrid, showAxes
82
+ // and a grid width of zero.
83
+ float ink(float d, float w, float feather) {
84
+ if (w <= 0.0) return 0.0;
85
+ return clamp((w - d) / feather + 0.5, 0.0, 1.0);
86
+ }
87
+
88
+ // Distance from \`t\` to the nearest ruling. SkSL's mod is floor-based, so this
89
+ // is right on either side of the phase without a sign test.
90
+ float toRuling(float t, float pitch, float phase) {
91
+ float m = mod(t - phase, pitch);
92
+ return min(m, pitch - m);
93
+ }
94
+
95
+ // A straight colour under a coverage, premultiplied.
96
+ vec4 lay(vec4 color, float cover) {
97
+ float a = color.a * cover;
98
+ return vec4(color.rgb * a, a);
99
+ }
100
+
101
+ // \`src\` over \`dst\`, both premultiplied.
102
+ vec4 stack(vec4 src, vec4 dst) {
103
+ return src + dst * (1.0 - src.a);
104
+ }
105
+
106
+ vec4 main(vec2 fragCoord) {
107
+ vec2 p = fragCoord - u_origin;
108
+ float feather = 1.0 / max(u_scale, 0.0001);
109
+
110
+ // Floored well above zero: a mod by zero paints garbage rather than nothing.
111
+ vec2 major = max(u_pitch, vec2(0.01));
112
+ vec2 minor = major / max(u_divisions, vec2(1.0));
113
+
114
+ vec2 dMajor = vec2(toRuling(p.x, major.x, u_phase.x),
115
+ toRuling(p.y, major.y, u_phase.y));
116
+ vec2 dMinor = vec2(toRuling(p.x, minor.x, u_phase.x),
117
+ toRuling(p.y, minor.y, u_phase.y));
118
+
119
+ float majorCover = max(ink(dMajor.x, u_majorHalf, feather),
120
+ ink(dMajor.y, u_majorHalf, feather));
121
+
122
+ // Every numbered ruling is also a faint one — the minor pitch divides the
123
+ // major exactly — so the faint order is cut away inside the numbered one's
124
+ // whole footprint, its ramp included. Drawing both would leave a hairline of
125
+ // the faint colour either side of every numbered line, where the numbered
126
+ // one's own ramp has not yet reached full coverage. The old ladder dropped
127
+ // coincident minors from a list; a shader has no list, so it subtracts.
128
+ float minorCover = max(
129
+ ink(dMinor.x, u_minorHalf, feather)
130
+ * (1.0 - ink(dMajor.x, u_majorHalf + feather, feather)),
131
+ ink(dMinor.y, u_minorHalf, feather)
132
+ * (1.0 - ink(dMajor.y, u_majorHalf + feather, feather)));
133
+
134
+ float axisCover = max(ink(abs(p.x - u_axisAt.x), u_axisHalf, feather),
135
+ ink(abs(p.y - u_axisAt.y), u_axisHalf, feather));
136
+
137
+ vec4 c = lay(u_paper, 1.0);
138
+ c = stack(lay(u_minorColor, minorCover), c);
139
+ c = stack(lay(u_majorColor, majorCover), c);
140
+ c = stack(lay(u_axisColor, axisCover), c);
141
+ return c;
142
+ }
143
+ `
144
+
145
+ /** What {@link planeUniforms} needs to know about how the plane is styled. */
146
+ export interface PlaneStyle {
147
+ /** The paper's colour. */
148
+ paper: NormalizedColor
149
+ /** How opaque the paper is, 0–1. At 0 the graph draws over what is behind it. */
150
+ paperOpacity: number
151
+ showGrid: boolean
152
+ showMinorGrid: boolean
153
+ showAxes: boolean
154
+ colorGrid: NormalizedColor
155
+ colorMinorGrid: NormalizedColor
156
+ colorAxis: NormalizedColor
157
+ /** Width of a numbered ruling. The faint ones are drawn at two thirds of it. */
158
+ lineWidth: number
159
+ /** Width of the two lines through the origin. */
160
+ axisWidth: number
161
+ }
162
+
163
+ /**
164
+ * The uniform record {@link PLANE_SOURCE} is fed.
165
+ *
166
+ * A type alias rather than an interface so it satisfies the renderer's
167
+ * `Record<string, number | number[]>` without a cast — interfaces have no
168
+ * implicit index signature.
169
+ */
170
+ export type PlaneUniforms = {
171
+ u_paper: number[]
172
+ u_minorColor: number[]
173
+ u_majorColor: number[]
174
+ u_axisColor: number[]
175
+ u_pitch: number[]
176
+ u_phase: number[]
177
+ u_divisions: number[]
178
+ u_axisAt: number[]
179
+ u_majorHalf: number
180
+ u_minorHalf: number
181
+ u_axisHalf: number
182
+ }
183
+
184
+ /**
185
+ * Where an axis is parked when it is not on screen, in box pixels.
186
+ *
187
+ * Finite on purpose: an infinity here would reach the coverage ramp as a `NaN`,
188
+ * and a `NaN` compares false against every clamp, which paints the axis colour
189
+ * over the entire plane rather than nowhere on it.
190
+ */
191
+ const AXIS_OFF = -1e7
192
+
193
+ /** Two thirds the width of a numbered ruling — the faint order's weight. */
194
+ const MINOR_WEIGHT = 2 / 3
195
+
196
+ /** Past this an integer index is no longer exact, so the reduction is meaningless. */
197
+ const MAX_INDEX = Number.MAX_SAFE_INTEGER
198
+
199
+ /**
200
+ * Whether the plane has anything to paint at all — no paper, no rulings and no
201
+ * axes is a draw the node can skip outright.
202
+ */
203
+ export function planeVisible(style: PlaneStyle): boolean {
204
+ if (style.paper[3] * clamp01(style.paperOpacity) > 0.001) return true
205
+ if (style.showGrid && style.lineWidth > 0) return true
206
+ return style.showAxes && style.axisWidth > 0
207
+ }
208
+
209
+ /**
210
+ * The uniforms that put {@link PLANE_SOURCE} over a resolved plane.
211
+ *
212
+ * Everything handed over is bounded by the node's box — a pitch of tens of
213
+ * pixels, a phase inside one cell, an axis position inside the frame or parked
214
+ * at {@link AXIS_OFF}. That is deliberate and it is what makes the deep zoom
215
+ * work: see the module note.
216
+ */
217
+ export function planeUniforms(
218
+ plane: ResolvedPlane,
219
+ style: PlaneStyle,
220
+ xTicks: TickLadder,
221
+ yTicks: TickLadder
222
+ ): PlaneUniforms {
223
+ const ruled = style.showGrid && style.lineWidth > 0
224
+ const majorHalf = ruled ? style.lineWidth / 2 : 0
225
+
226
+ return {
227
+ u_paper: withAlpha(style.paper, clamp01(style.paperOpacity)),
228
+ u_minorColor: rgba(style.colorMinorGrid),
229
+ u_majorColor: rgba(style.colorGrid),
230
+ u_axisColor: rgba(style.colorAxis),
231
+ u_pitch: [
232
+ pitchOf(xTicks.step, plane.scaleX),
233
+ pitchOf(yTicks.step, plane.scaleY),
234
+ ],
235
+ // The shader's y runs down the box and the graph's runs up it, which is the
236
+ // whole of the difference between these two: one signed scale each.
237
+ u_phase: [
238
+ phaseOf(plane.centerX, xTicks.step, plane.width / 2, -plane.scaleX),
239
+ phaseOf(plane.centerY, yTicks.step, plane.height / 2, plane.scaleY),
240
+ ],
241
+ u_divisions: [
242
+ Math.max(1, Math.round(xTicks.divisions)),
243
+ Math.max(1, Math.round(yTicks.divisions)),
244
+ ],
245
+ u_axisAt: [
246
+ // Only ever evaluated when the origin is inside the range, which bounds
247
+ // `centre × scale` by half the box — the same reduction `phaseOf` makes,
248
+ // for free.
249
+ style.showAxes && plane.xMin <= 0 && 0 <= plane.xMax
250
+ ? plane.width / 2 - plane.centerX * plane.scaleX
251
+ : AXIS_OFF,
252
+ style.showAxes && plane.yMin <= 0 && 0 <= plane.yMax
253
+ ? plane.height / 2 + plane.centerY * plane.scaleY
254
+ : AXIS_OFF,
255
+ ],
256
+ u_majorHalf: majorHalf,
257
+ u_minorHalf: ruled && style.showMinorGrid ? majorHalf * MINOR_WEIGHT : 0,
258
+ u_axisHalf: style.showAxes ? Math.max(0, style.axisWidth) / 2 : 0,
259
+ }
260
+ }
261
+
262
+ /** A ladder step in pixels, floored where the shader floors it. */
263
+ function pitchOf(step: number, scale: number): number {
264
+ const pitch = Math.abs(step * scale)
265
+ return Number.isFinite(pitch) && pitch > 0.01 ? pitch : 0.01
266
+ }
267
+
268
+ /**
269
+ * Where the first ruling sits inside the box, in pixels from its leading edge —
270
+ * a number in `[0, pitch)`.
271
+ *
272
+ * The centre is split into a whole number of steps plus a remainder *before* it
273
+ * is ever multiplied by the scale, so the only quantity that ever carries the
274
+ * centre's own magnitude is an integer count that then cancels. Subtracting a
275
+ * tick value from the centre directly is exact at a span of 20 and has no
276
+ * significant digits left at 1e-9 around x = 1e6 — where the lines would still
277
+ * be drawn, just not evenly spaced.
278
+ *
279
+ * `signedScale` is negative for x and positive for y; see {@link planeUniforms}.
280
+ */
281
+ function phaseOf(
282
+ center: number,
283
+ step: number,
284
+ halfBox: number,
285
+ signedScale: number
286
+ ): number {
287
+ const pitch = pitchOf(step, signedScale)
288
+ const index = Math.round(center / step)
289
+ if (!Number.isFinite(index) || Math.abs(index) > MAX_INDEX) return 0
290
+
291
+ const remainder = center - index * step
292
+ const anchor = halfBox + remainder * signedScale
293
+ if (!Number.isFinite(anchor)) return 0
294
+ return anchor - Math.floor(anchor / pitch) * pitch
295
+ }
296
+
297
+ function rgba(color: NormalizedColor): number[] {
298
+ return [color[0], color[1], color[2], color[3]]
299
+ }
300
+
301
+ function withAlpha(color: NormalizedColor, opacity: number): number[] {
302
+ return [color[0], color[1], color[2], color[3] * opacity]
303
+ }
304
+
305
+ function clamp01(value: number): number {
306
+ if (!Number.isFinite(value)) return 1
307
+ return value < 0 ? 0 : value > 1 ? 1 : value
308
+ }