gladly-plot 0.0.4 → 0.0.6

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 (60) hide show
  1. package/README.md +9 -2
  2. package/package.json +10 -11
  3. package/src/axes/Axis.js +401 -0
  4. package/src/{AxisLink.js → axes/AxisLink.js} +6 -2
  5. package/src/{AxisQuantityKindRegistry.js → axes/AxisQuantityKindRegistry.js} +7 -0
  6. package/src/axes/AxisRegistry.js +179 -0
  7. package/src/axes/Camera.js +47 -0
  8. package/src/axes/ColorAxisRegistry.js +101 -0
  9. package/src/{FilterAxisRegistry.js → axes/FilterAxisRegistry.js} +63 -0
  10. package/src/axes/TickLabelAtlas.js +99 -0
  11. package/src/axes/ZoomController.js +463 -0
  12. package/src/colorscales/BivariateColorscales.js +205 -0
  13. package/src/colorscales/ColorscaleRegistry.js +144 -0
  14. package/src/compute/ComputationRegistry.js +179 -0
  15. package/src/compute/axisFilter.js +59 -0
  16. package/src/compute/conv.js +286 -0
  17. package/src/compute/elementwise.js +72 -0
  18. package/src/compute/fft.js +378 -0
  19. package/src/compute/filter.js +229 -0
  20. package/src/compute/hist.js +285 -0
  21. package/src/compute/kde.js +120 -0
  22. package/src/compute/scatter2dInterpolate.js +277 -0
  23. package/src/compute/util.js +196 -0
  24. package/src/core/ComputePipeline.js +153 -0
  25. package/src/core/GlBase.js +141 -0
  26. package/src/core/Layer.js +59 -0
  27. package/src/core/LayerType.js +433 -0
  28. package/src/core/Plot.js +1213 -0
  29. package/src/core/PlotGroup.js +204 -0
  30. package/src/core/ShaderQueue.js +73 -0
  31. package/src/data/ColumnData.js +269 -0
  32. package/src/data/Computation.js +95 -0
  33. package/src/data/Data.js +270 -0
  34. package/src/{Colorbar.js → floats/Colorbar.js} +19 -5
  35. package/src/floats/Colorbar2d.js +77 -0
  36. package/src/{Filterbar.js → floats/Filterbar.js} +18 -4
  37. package/src/{FilterbarFloat.js → floats/Float.js} +73 -30
  38. package/src/{EpsgUtils.js → geo/EpsgUtils.js} +1 -1
  39. package/src/index.js +47 -22
  40. package/src/layers/BarsLayer.js +168 -0
  41. package/src/{ColorbarLayer.js → layers/ColorbarLayer.js} +12 -16
  42. package/src/layers/ColorbarLayer2d.js +86 -0
  43. package/src/{FilterbarLayer.js → layers/FilterbarLayer.js} +6 -5
  44. package/src/layers/LinesLayer.js +185 -0
  45. package/src/layers/PointsLayer.js +118 -0
  46. package/src/layers/ScatterShared.js +98 -0
  47. package/src/{TileLayer.js → layers/TileLayer.js} +24 -20
  48. package/src/math/mat4.js +100 -0
  49. package/src/Axis.js +0 -48
  50. package/src/AxisRegistry.js +0 -54
  51. package/src/ColorAxisRegistry.js +0 -49
  52. package/src/ColorscaleRegistry.js +0 -52
  53. package/src/Data.js +0 -67
  54. package/src/Float.js +0 -159
  55. package/src/Layer.js +0 -44
  56. package/src/LayerType.js +0 -209
  57. package/src/Plot.js +0 -1073
  58. package/src/ScatterLayer.js +0 -287
  59. /package/src/{MatplotlibColorscales.js → colorscales/MatplotlibColorscales.js} +0 -0
  60. /package/src/{LayerTypeRegistry.js → core/LayerTypeRegistry.js} +0 -0
@@ -0,0 +1,72 @@
1
+ import { registerComputedData, EXPRESSION_REF, resolveExprToColumn } from "./ComputationRegistry.js"
2
+ import { ComputedData } from "../data/Computation.js"
3
+
4
+ const TDR_STEP_MS = 500
5
+
6
+ class ElementwiseData extends ComputedData {
7
+ columns(params) {
8
+ if (!params?.columns) return []
9
+ return params.columns.map(c => c.dst)
10
+ }
11
+
12
+ async compute(regl, params, data, getAxisDomain) {
13
+ const plotProxy = { currentData: data, getAxisDomain }
14
+
15
+ let N = params.dataLength ?? null
16
+ if (N == null) {
17
+ for (const { src } of params.columns) {
18
+ const col = await resolveExprToColumn(src, data, regl, plotProxy)
19
+ if (col?.length !== null) { N = col.length; break }
20
+ }
21
+ }
22
+ if (N == null) throw new Error('ElementwiseData: cannot determine data length; set dataLength param')
23
+
24
+ const result = {}
25
+ const quantityKinds = {}
26
+ let stepStart = performance.now()
27
+
28
+ for (const { dst, src, quantityKind } of params.columns) {
29
+ const col = await resolveExprToColumn(src, data, regl, plotProxy)
30
+ const tex = col.toTexture(regl)
31
+ tex._dataLength = N
32
+ result[dst] = tex
33
+ if (quantityKind) quantityKinds[dst] = quantityKind
34
+
35
+ if (performance.now() - stepStart > TDR_STEP_MS) {
36
+ await new Promise(r => requestAnimationFrame(r))
37
+ stepStart = performance.now()
38
+ }
39
+ }
40
+
41
+ if (Object.keys(quantityKinds).length > 0) result._meta = { quantityKinds }
42
+ return result
43
+ }
44
+
45
+ schema(data) {
46
+ return {
47
+ type: 'object',
48
+ title: 'ElementwiseData',
49
+ properties: {
50
+ dataLength: {
51
+ type: 'integer',
52
+ description: 'Override output length (optional, auto-detected from column refs)'
53
+ },
54
+ columns: {
55
+ type: 'array',
56
+ items: {
57
+ type: 'object',
58
+ properties: {
59
+ dst: { type: 'string', description: 'Output column name' },
60
+ src: EXPRESSION_REF,
61
+ quantityKind: { type: 'string', description: 'Quantity kind for axis matching (optional)' }
62
+ },
63
+ required: ['dst', 'src']
64
+ }
65
+ }
66
+ },
67
+ required: ['columns']
68
+ }
69
+ }
70
+ }
71
+
72
+ registerComputedData('ElementwiseData', new ElementwiseData())
@@ -0,0 +1,378 @@
1
+ import { registerTextureComputation, registerComputedData, EXPRESSION_REF, resolveQuantityKind } from "./ComputationRegistry.js"
2
+ import { TextureComputation, ComputedData } from "../data/Computation.js"
3
+ import { ArrayColumn } from "../data/ColumnData.js"
4
+
5
+ /* ============================================================
6
+ Utilities
7
+ ============================================================ */
8
+
9
+ function nextPow2(n) {
10
+ return 1 << Math.ceil(Math.log2(n));
11
+ }
12
+
13
+ // Internal: complex texture (R=real, G=imag per frequency bin), 1 element per texel.
14
+ // Not exposed via sampleColumn — only used as intermediate within this module.
15
+ function makeComplexTexture(regl, data, N) {
16
+ // data: Float32Array (real), imag assumed 0
17
+ const texData = new Float32Array(N * 4);
18
+ for (let i = 0; i < data.length; i++) {
19
+ texData[i * 4] = data[i];
20
+ }
21
+ return regl.texture({
22
+ data: texData,
23
+ width: N,
24
+ height: 1,
25
+ format: "rgba",
26
+ type: "float",
27
+ min: "nearest",
28
+ mag: "nearest",
29
+ wrap: "clamp"
30
+ });
31
+ }
32
+
33
+ function makeEmptyComplexTexture(regl, N) {
34
+ return regl.texture({
35
+ width: N,
36
+ height: 1,
37
+ format: "rgba",
38
+ type: "float",
39
+ min: "nearest",
40
+ mag: "nearest",
41
+ wrap: "clamp"
42
+ });
43
+ }
44
+
45
+ function makeFBO(regl, tex) {
46
+ return regl.framebuffer({ color: tex, depth: false, stencil: false });
47
+ }
48
+
49
+ /* ============================================================
50
+ Fullscreen quad
51
+ ============================================================ */
52
+
53
+ const quad = {
54
+ attributes: {
55
+ position: [[-1, -1], [1, -1], [-1, 1], [1, 1]]
56
+ },
57
+ count: 4,
58
+ primitive: "triangle strip"
59
+ };
60
+
61
+ /* ============================================================
62
+ FFT shaders
63
+ ============================================================ */
64
+
65
+ function bitReversePass(regl, N) {
66
+ return regl({
67
+ frag: `#version 300 es
68
+ precision highp float;
69
+ uniform sampler2D inputTex;
70
+ uniform int N;
71
+ out vec4 outColor;
72
+
73
+ int bitReverse(int x, int bits) {
74
+ int y = 0;
75
+ for (int i = 0; i < 16; i++) {
76
+ if (i >= bits) break;
77
+ y = (y << 1) | (x & 1);
78
+ x >>= 1;
79
+ }
80
+ return y;
81
+ }
82
+
83
+ void main() {
84
+ int x = int(gl_FragCoord.x);
85
+ int bits = int(log2(float(N)));
86
+ int rx = bitReverse(x, bits);
87
+ vec2 v = texelFetch(inputTex, ivec2(rx, 0), 0).rg;
88
+ outColor = vec4(v, 0.0, 1.0);
89
+ }`,
90
+ vert: `#version 300 es
91
+ in vec2 position;
92
+ void main() {
93
+ gl_Position = vec4(position, 0, 1);
94
+ }`,
95
+ uniforms: {
96
+ inputTex: regl.prop("input"),
97
+ N
98
+ },
99
+ framebuffer: regl.prop("fbo"),
100
+ ...quad
101
+ });
102
+ }
103
+
104
+ function fftStagePass(regl, stage, inverse) {
105
+ return regl({
106
+ frag: `#version 300 es
107
+ precision highp float;
108
+ uniform sampler2D inputTex;
109
+ uniform int stage;
110
+ uniform int N;
111
+ out vec4 outColor;
112
+
113
+ void main() {
114
+ int x = int(gl_FragCoord.x);
115
+ int half = stage >> 1;
116
+ int block = (x / stage) * stage;
117
+ int i = block + (x % half);
118
+ int j = i + half;
119
+
120
+ vec2 a = texelFetch(inputTex, ivec2(i, 0), 0).rg;
121
+ vec2 b = texelFetch(inputTex, ivec2(j, 0), 0).rg;
122
+
123
+ float sign = ${inverse ? "1.0" : "-1.0"};
124
+ float angle = sign * 6.28318530718 * float(x % stage) / float(stage);
125
+ vec2 w = vec2(cos(angle), sin(angle));
126
+
127
+ vec2 t = vec2(
128
+ b.x * w.x - b.y * w.y,
129
+ b.x * w.y + b.y * w.x
130
+ );
131
+
132
+ vec2 outv = (x < j) ? a + t : a - t;
133
+ outColor = vec4(outv, 0.0, 1.0);
134
+ }`,
135
+ vert: `#version 300 es
136
+ in vec2 position;
137
+ void main() {
138
+ gl_Position = vec4(position, 0, 1);
139
+ }`,
140
+ uniforms: {
141
+ inputTex: regl.prop("input"),
142
+ stage,
143
+ N: regl.prop("N")
144
+ },
145
+ framebuffer: regl.prop("fbo"),
146
+ ...quad
147
+ });
148
+ }
149
+
150
+ function scalePass(regl, N) {
151
+ return regl({
152
+ frag: `#version 300 es
153
+ precision highp float;
154
+ uniform sampler2D inputTex;
155
+ out vec4 outColor;
156
+ void main() {
157
+ vec2 v = texelFetch(inputTex, ivec2(int(gl_FragCoord.x),0),0).rg;
158
+ outColor = vec4(v / float(${N}), 0.0, 1.0);
159
+ }`,
160
+ vert: `#version 300 es
161
+ in vec2 position;
162
+ void main() {
163
+ gl_Position = vec4(position, 0, 1);
164
+ }`,
165
+ uniforms: {
166
+ inputTex: regl.prop("input")
167
+ },
168
+ framebuffer: regl.prop("fbo"),
169
+ ...quad
170
+ });
171
+ }
172
+
173
+ // If a single batch of FFT stages takes longer than this, yield before the next batch.
174
+ const TDR_STEP_MS = 500
175
+
176
+ /* ============================================================
177
+ Internal GPU FFT — returns a complex texture (R=real, G=imag)
178
+ with 1 frequency bin per texel. NOT for direct use with sampleColumn.
179
+ ============================================================ */
180
+
181
+ export async function fft1d(regl, realArray, inverse = false) {
182
+ const N = nextPow2(realArray.length);
183
+
184
+ let texA = makeComplexTexture(regl, realArray, N);
185
+ let texB = makeEmptyComplexTexture(regl, N);
186
+ let fboA = makeFBO(regl, texA);
187
+ let fboB = makeFBO(regl, texB);
188
+
189
+ // bit reversal
190
+ bitReversePass(regl, N)({
191
+ input: texA,
192
+ fbo: fboB
193
+ });
194
+ [fboA, fboB] = [fboB, fboA];
195
+
196
+ // FFT stages — yield between batches to avoid triggering the Windows TDR watchdog.
197
+ const stages = Math.log2(N);
198
+ let batchStart = performance.now();
199
+ for (let s = 1; s <= stages; s++) {
200
+ fftStagePass(regl, 1 << s, inverse)({
201
+ input: fboA,
202
+ N,
203
+ fbo: fboB
204
+ });
205
+ [fboA, fboB] = [fboB, fboA];
206
+ if (performance.now() - batchStart > TDR_STEP_MS) {
207
+ await new Promise(r => requestAnimationFrame(r));
208
+ batchStart = performance.now();
209
+ }
210
+ }
211
+
212
+ // scale for inverse
213
+ if (inverse) {
214
+ scalePass(regl, N)({
215
+ input: fboA,
216
+ fbo: fboB
217
+ });
218
+ return fboB.color[0];
219
+ }
220
+
221
+ return fboA.color[0];
222
+ }
223
+
224
+ /* ============================================================
225
+ FFT-based convolution (internal)
226
+ ============================================================ */
227
+
228
+ export async function fftConvolution(regl, signal, kernel) {
229
+ const N = nextPow2(signal.length + kernel.length);
230
+
231
+ const sigTex = await fft1d(regl, signal, false);
232
+ const kerTex = await fft1d(regl, kernel, false);
233
+
234
+ const outTex = makeEmptyComplexTexture(regl, N);
235
+ const outFBO = makeFBO(regl, outTex);
236
+
237
+ // pointwise complex multiply
238
+ regl({
239
+ frag: `#version 300 es
240
+ precision highp float;
241
+ uniform sampler2D A, B;
242
+ out vec4 outColor;
243
+ void main() {
244
+ int x = int(gl_FragCoord.x);
245
+ vec2 a = texelFetch(A, ivec2(x,0),0).rg;
246
+ vec2 b = texelFetch(B, ivec2(x,0),0).rg;
247
+ outColor = vec4(
248
+ a.x*b.x - a.y*b.y,
249
+ a.x*b.y + a.y*b.x,
250
+ 0, 1
251
+ );
252
+ }`,
253
+ vert: `#version 300 es
254
+ in vec2 position;
255
+ void main() {
256
+ gl_Position = vec4(position,0,1);
257
+ }`,
258
+ uniforms: {
259
+ A: sigTex,
260
+ B: kerTex
261
+ },
262
+ framebuffer: outFBO,
263
+ ...quad
264
+ })();
265
+
266
+ // inverse FFT
267
+ return await fft1d(regl, new Float32Array(N), true);
268
+ }
269
+
270
+ /* ============================================================
271
+ extractAndRepack: extract one channel from a complex texture
272
+ (1 element per texel, R=real/G=imag) into a 4-packed RGBA texture.
273
+ channelSwizzle: 'r' for real part, 'g' for imaginary part.
274
+ ============================================================ */
275
+
276
+ function extractAndRepack(regl, complexTex, channelSwizzle, N) {
277
+ const nTexels = Math.ceil(N / 4)
278
+ const w = Math.min(nTexels, regl.limits.maxTextureSize)
279
+ const h = Math.ceil(nTexels / w)
280
+ const outputTex = regl.texture({ width: w, height: h, type: 'float', format: 'rgba' })
281
+ const outputFBO = regl.framebuffer({ color: outputTex, depth: false, stencil: false })
282
+
283
+ regl({
284
+ framebuffer: outputFBO,
285
+ vert: `#version 300 es
286
+ in vec2 position;
287
+ void main() { gl_Position = vec4(position, 0.0, 1.0); }`,
288
+ frag: `#version 300 es
289
+ precision highp float;
290
+ uniform sampler2D complexTex;
291
+ uniform int totalLength;
292
+ out vec4 fragColor;
293
+ void main() {
294
+ int texelI = int(gl_FragCoord.y) * ${w} + int(gl_FragCoord.x);
295
+ int base = texelI * 4;
296
+ float v0 = base + 0 < totalLength ? texelFetch(complexTex, ivec2(base+0, 0), 0).${channelSwizzle} : 0.0;
297
+ float v1 = base + 1 < totalLength ? texelFetch(complexTex, ivec2(base+1, 0), 0).${channelSwizzle} : 0.0;
298
+ float v2 = base + 2 < totalLength ? texelFetch(complexTex, ivec2(base+2, 0), 0).${channelSwizzle} : 0.0;
299
+ float v3 = base + 3 < totalLength ? texelFetch(complexTex, ivec2(base+3, 0), 0).${channelSwizzle} : 0.0;
300
+ fragColor = vec4(v0, v1, v2, v3);
301
+ }`,
302
+ attributes: { position: [[-1,-1],[1,-1],[-1,1],[1,1]] },
303
+ uniforms: { complexTex, totalLength: N },
304
+ count: 4,
305
+ primitive: 'triangle strip'
306
+ })()
307
+
308
+ outputTex._dataLength = N
309
+ return outputTex
310
+ }
311
+
312
+ /* ============================================================
313
+ FftData — ComputedData producing 'real' and 'imag' columns
314
+ ============================================================ */
315
+
316
+ class FftData extends ComputedData {
317
+ columns() { return ['real', 'imag'] }
318
+
319
+ async compute(regl, params, data, getAxisDomain) {
320
+ const inputCol = data.getData(params.input)
321
+ if (!(inputCol instanceof ArrayColumn)) {
322
+ throw new Error(`FftData: input '${params.input}' must be a plain data column`)
323
+ }
324
+ const N = nextPow2(inputCol.array.length)
325
+ const complexTex = await fft1d(regl, inputCol.array, params.inverse ?? false)
326
+ return {
327
+ real: extractAndRepack(regl, complexTex, 'r', N),
328
+ imag: extractAndRepack(regl, complexTex, 'g', N),
329
+ _meta: {
330
+ domains: { real: null, imag: null },
331
+ quantityKinds: { real: null, imag: null }
332
+ }
333
+ }
334
+ }
335
+
336
+ schema(data) {
337
+ const cols = data ? data.columns() : []
338
+ return {
339
+ type: 'object',
340
+ title: 'FftData',
341
+ properties: {
342
+ input: { type: 'string', enum: cols, description: 'Input signal column' },
343
+ inverse: { type: 'boolean', default: false, description: 'Inverse FFT' }
344
+ },
345
+ required: ['input']
346
+ }
347
+ }
348
+ }
349
+
350
+ registerComputedData('FftData', new FftData())
351
+
352
+ /* ============================================================
353
+ FftConvolutionComputation — TextureComputation (real output)
354
+ ============================================================ */
355
+
356
+ class FftConvolutionComputation extends TextureComputation {
357
+ getQuantityKind(params, data) { return resolveQuantityKind(params.signal, data) }
358
+ async compute(regl, params, data, getAxisDomain) {
359
+ const signal = params.signal instanceof ArrayColumn ? params.signal.array : params.signal
360
+ const kernel = params.kernel instanceof ArrayColumn ? params.kernel.array : params.kernel
361
+ const N = nextPow2(signal.length + kernel.length)
362
+ const complexResult = await fftConvolution(regl, signal, kernel)
363
+ return extractAndRepack(regl, complexResult, 'r', N)
364
+ }
365
+ schema(data) {
366
+ return {
367
+ type: 'object',
368
+ title: 'fftConvolution',
369
+ properties: {
370
+ signal: EXPRESSION_REF,
371
+ kernel: EXPRESSION_REF
372
+ },
373
+ required: ['signal', 'kernel']
374
+ }
375
+ }
376
+ }
377
+
378
+ registerTextureComputation('fftConvolution', new FftConvolutionComputation())
@@ -0,0 +1,229 @@
1
+ import { registerTextureComputation, EXPRESSION_REF, resolveQuantityKind } from "./ComputationRegistry.js"
2
+ import { TextureComputation } from "../data/Computation.js"
3
+ import { ArrayColumn, SAMPLE_COLUMN_GLSL } from "../data/ColumnData.js"
4
+
5
+ function subtractTextures(regl, texA, texB) {
6
+ const w = texA.width
7
+ const h = texA.height
8
+ const outputTex = regl.texture({ width: w, height: h, type: 'float', format: 'rgba' })
9
+ const outputFBO = regl.framebuffer({ color: outputTex, depth: false, stencil: false })
10
+
11
+ regl({
12
+ framebuffer: outputFBO,
13
+ vert: `#version 300 es
14
+ precision highp float;
15
+ in vec2 position;
16
+ void main() { gl_Position = vec4(position, 0.0, 1.0); }
17
+ `,
18
+ frag: `#version 300 es
19
+ precision highp float;
20
+ uniform sampler2D texA;
21
+ uniform sampler2D texB;
22
+ out vec4 fragColor;
23
+ void main() {
24
+ ivec2 coord = ivec2(gl_FragCoord.xy);
25
+ fragColor = texelFetch(texA, coord, 0) - texelFetch(texB, coord, 0);
26
+ }
27
+ `,
28
+ attributes: { position: [[-1, -1], [1, -1], [-1, 1], [1, 1]] },
29
+ uniforms: { texA, texB },
30
+ count: 4,
31
+ primitive: 'triangle strip'
32
+ })()
33
+
34
+ if (texA._dataLength !== undefined) outputTex._dataLength = texA._dataLength
35
+ return outputTex
36
+ }
37
+
38
+ /**
39
+ * Generic 1D convolution filter
40
+ * @param {regl} regl - regl context
41
+ * @param {Texture} inputTex - 4-packed GPU texture, _dataLength set
42
+ * @param {Float32Array} kernel - 1D kernel weights
43
+ * @returns {Texture} - filtered output texture (4-packed, same dimensions as input)
44
+ */
45
+ function filter1D(regl, inputTex, kernel) {
46
+ const length = inputTex._dataLength ?? inputTex.width * inputTex.height * 4
47
+ const w = inputTex.width
48
+ const h = inputTex.height
49
+
50
+ // Kernel texture stays R-channel (internal, not exposed via sampleColumn)
51
+ const kernelData = new Float32Array(kernel.length * 4)
52
+ for (let i = 0; i < kernel.length; i++) kernelData[i * 4] = kernel[i]
53
+ const kernelTex = regl.texture({ data: kernelData, shape: [kernel.length, 1], type: 'float', format: 'rgba' })
54
+ const outputTex = regl.texture({ width: w, height: h, type: 'float', format: 'rgba' })
55
+ const outputFBO = regl.framebuffer({ color: outputTex, depth: false, stencil: false })
56
+
57
+ const radius = Math.floor(kernel.length / 2)
58
+
59
+ regl({
60
+ framebuffer: outputFBO,
61
+ vert: `#version 300 es
62
+ precision highp float;
63
+ in vec2 position;
64
+ void main() { gl_Position = vec4(position, 0.0, 1.0); }
65
+ `,
66
+ frag: `#version 300 es
67
+ precision highp float;
68
+ precision highp sampler2D;
69
+ uniform sampler2D inputTex;
70
+ uniform sampler2D kernelTex;
71
+ uniform int radius;
72
+ uniform int totalLength;
73
+ out vec4 fragColor;
74
+ ${SAMPLE_COLUMN_GLSL}
75
+ void main() {
76
+ ivec2 sz = textureSize(inputTex, 0);
77
+ int texelI = int(gl_FragCoord.y) * sz.x + int(gl_FragCoord.x);
78
+ int base = texelI * 4;
79
+ float s0 = 0.0, s1 = 0.0, s2 = 0.0, s3 = 0.0;
80
+ for (int i = -16; i <= 16; i++) {
81
+ if (i + 16 >= radius * 2 + 1) break;
82
+ float kw = texelFetch(kernelTex, ivec2(i + radius, 0), 0).r;
83
+ s0 += sampleColumn(inputTex, float(clamp(base + 0 + i, 0, totalLength - 1))) * kw;
84
+ s1 += sampleColumn(inputTex, float(clamp(base + 1 + i, 0, totalLength - 1))) * kw;
85
+ s2 += sampleColumn(inputTex, float(clamp(base + 2 + i, 0, totalLength - 1))) * kw;
86
+ s3 += sampleColumn(inputTex, float(clamp(base + 3 + i, 0, totalLength - 1))) * kw;
87
+ }
88
+ fragColor = vec4(s0, s1, s2, s3);
89
+ }
90
+ `,
91
+ attributes: { position: [[-1, -1], [1, -1], [-1, 1], [1, 1]] },
92
+ uniforms: { inputTex, kernelTex, radius, totalLength: length },
93
+ count: 4,
94
+ primitive: 'triangle strip'
95
+ })()
96
+
97
+ outputTex._dataLength = length
98
+ return outputTex
99
+ }
100
+
101
+ // Gaussian kernel helper
102
+ function gaussianKernel(size, sigma) {
103
+ const radius = Math.floor(size / 2)
104
+ const kernel = new Float32Array(size)
105
+ let sum = 0
106
+ for (let i = -radius; i <= radius; i++) {
107
+ const v = Math.exp(-0.5 * (i / sigma) ** 2)
108
+ kernel[i + radius] = v
109
+ sum += v
110
+ }
111
+ for (let i = 0; i < size; i++) kernel[i] /= sum
112
+ return kernel
113
+ }
114
+
115
+ /**
116
+ * Low-pass filter
117
+ * @param {regl} regl
118
+ * @param {Texture} inputTex - 4-packed GPU texture
119
+ */
120
+ function lowPass(regl, inputTex, sigma = 3, kernelSize = null) {
121
+ const size = kernelSize || (Math.ceil(sigma * 6) | 1) // ensure odd
122
+ const kernel = gaussianKernel(size, sigma)
123
+ return filter1D(regl, inputTex, kernel)
124
+ }
125
+
126
+ /**
127
+ * High-pass filter: subtract low-pass
128
+ * @param {regl} regl
129
+ * @param {Texture} inputTex - 4-packed GPU texture
130
+ */
131
+ function highPass(regl, inputTex, sigma = 3, kernelSize = null) {
132
+ const low = lowPass(regl, inputTex, sigma, kernelSize)
133
+ return subtractTextures(regl, inputTex, low)
134
+ }
135
+
136
+ /**
137
+ * Band-pass filter: difference of low-pass filters
138
+ * @param {regl} regl
139
+ * @param {Texture} inputTex - 4-packed GPU texture
140
+ */
141
+ function bandPass(regl, inputTex, sigmaLow, sigmaHigh) {
142
+ const lowHigh = lowPass(regl, inputTex, sigmaHigh)
143
+ const lowLow = lowPass(regl, inputTex, sigmaLow)
144
+ return subtractTextures(regl, lowHigh, lowLow)
145
+ }
146
+
147
+ export { filter1D, gaussianKernel, lowPass, highPass, bandPass }
148
+
149
+ class Filter1DComputation extends TextureComputation {
150
+ getQuantityKind(params, data) { return resolveQuantityKind(params.input, data) }
151
+ compute(regl, inputs, getAxisDomain) {
152
+ const inputTex = inputs.input.toTexture(regl)
153
+ const kernelArr = inputs.kernel instanceof ArrayColumn ? inputs.kernel.array : inputs.kernel
154
+ return filter1D(regl, inputTex, kernelArr)
155
+ }
156
+ schema(data) {
157
+ return {
158
+ type: 'object',
159
+ title: 'filter1D',
160
+ properties: {
161
+ input: EXPRESSION_REF,
162
+ kernel: EXPRESSION_REF
163
+ },
164
+ required: ['input', 'kernel']
165
+ }
166
+ }
167
+ }
168
+
169
+ class LowPassComputation extends TextureComputation {
170
+ getQuantityKind(params, data) { return resolveQuantityKind(params.input, data) }
171
+ compute(regl, inputs, getAxisDomain) {
172
+ return lowPass(regl, inputs.input.toTexture(regl), inputs.sigma, inputs.kernelSize)
173
+ }
174
+ schema(data) {
175
+ return {
176
+ type: 'object',
177
+ title: 'lowPass',
178
+ properties: {
179
+ input: EXPRESSION_REF,
180
+ sigma: { type: 'number' },
181
+ kernelSize: { type: 'number' }
182
+ },
183
+ required: ['input']
184
+ }
185
+ }
186
+ }
187
+
188
+ class HighPassComputation extends TextureComputation {
189
+ getQuantityKind(params, data) { return resolveQuantityKind(params.input, data) }
190
+ compute(regl, inputs, getAxisDomain) {
191
+ return highPass(regl, inputs.input.toTexture(regl), inputs.sigma, inputs.kernelSize)
192
+ }
193
+ schema(data) {
194
+ return {
195
+ type: 'object',
196
+ title: 'highPass',
197
+ properties: {
198
+ input: EXPRESSION_REF,
199
+ sigma: { type: 'number' },
200
+ kernelSize: { type: 'number' }
201
+ },
202
+ required: ['input']
203
+ }
204
+ }
205
+ }
206
+
207
+ class BandPassComputation extends TextureComputation {
208
+ getQuantityKind(params, data) { return resolveQuantityKind(params.input, data) }
209
+ compute(regl, inputs, getAxisDomain) {
210
+ return bandPass(regl, inputs.input.toTexture(regl), inputs.sigmaLow, inputs.sigmaHigh)
211
+ }
212
+ schema(data) {
213
+ return {
214
+ type: 'object',
215
+ title: 'bandPass',
216
+ properties: {
217
+ input: EXPRESSION_REF,
218
+ sigmaLow: { type: 'number' },
219
+ sigmaHigh: { type: 'number' }
220
+ },
221
+ required: ['input', 'sigmaLow', 'sigmaHigh']
222
+ }
223
+ }
224
+ }
225
+
226
+ registerTextureComputation('filter1D', new Filter1DComputation())
227
+ registerTextureComputation('lowPass', new LowPassComputation())
228
+ registerTextureComputation('highPass', new HighPassComputation())
229
+ registerTextureComputation('bandPass', new BandPassComputation())