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.
- package/README.md +9 -2
- package/package.json +10 -11
- package/src/axes/Axis.js +401 -0
- package/src/{AxisLink.js → axes/AxisLink.js} +6 -2
- package/src/{AxisQuantityKindRegistry.js → axes/AxisQuantityKindRegistry.js} +7 -0
- package/src/axes/AxisRegistry.js +179 -0
- package/src/axes/Camera.js +47 -0
- package/src/axes/ColorAxisRegistry.js +101 -0
- package/src/{FilterAxisRegistry.js → axes/FilterAxisRegistry.js} +63 -0
- package/src/axes/TickLabelAtlas.js +99 -0
- package/src/axes/ZoomController.js +463 -0
- package/src/colorscales/BivariateColorscales.js +205 -0
- package/src/colorscales/ColorscaleRegistry.js +144 -0
- package/src/compute/ComputationRegistry.js +179 -0
- package/src/compute/axisFilter.js +59 -0
- package/src/compute/conv.js +286 -0
- package/src/compute/elementwise.js +72 -0
- package/src/compute/fft.js +378 -0
- package/src/compute/filter.js +229 -0
- package/src/compute/hist.js +285 -0
- package/src/compute/kde.js +120 -0
- package/src/compute/scatter2dInterpolate.js +277 -0
- package/src/compute/util.js +196 -0
- package/src/core/ComputePipeline.js +153 -0
- package/src/core/GlBase.js +141 -0
- package/src/core/Layer.js +59 -0
- package/src/core/LayerType.js +433 -0
- package/src/core/Plot.js +1213 -0
- package/src/core/PlotGroup.js +204 -0
- package/src/core/ShaderQueue.js +73 -0
- package/src/data/ColumnData.js +269 -0
- package/src/data/Computation.js +95 -0
- package/src/data/Data.js +270 -0
- package/src/{Colorbar.js → floats/Colorbar.js} +19 -5
- package/src/floats/Colorbar2d.js +77 -0
- package/src/{Filterbar.js → floats/Filterbar.js} +18 -4
- package/src/{FilterbarFloat.js → floats/Float.js} +73 -30
- package/src/{EpsgUtils.js → geo/EpsgUtils.js} +1 -1
- package/src/index.js +47 -22
- package/src/layers/BarsLayer.js +168 -0
- package/src/{ColorbarLayer.js → layers/ColorbarLayer.js} +12 -16
- package/src/layers/ColorbarLayer2d.js +86 -0
- package/src/{FilterbarLayer.js → layers/FilterbarLayer.js} +6 -5
- package/src/layers/LinesLayer.js +185 -0
- package/src/layers/PointsLayer.js +118 -0
- package/src/layers/ScatterShared.js +98 -0
- package/src/{TileLayer.js → layers/TileLayer.js} +24 -20
- package/src/math/mat4.js +100 -0
- package/src/Axis.js +0 -48
- package/src/AxisRegistry.js +0 -54
- package/src/ColorAxisRegistry.js +0 -49
- package/src/ColorscaleRegistry.js +0 -52
- package/src/Data.js +0 -67
- package/src/Float.js +0 -159
- package/src/Layer.js +0 -44
- package/src/LayerType.js +0 -209
- package/src/Plot.js +0 -1073
- package/src/ScatterLayer.js +0 -287
- /package/src/{MatplotlibColorscales.js → colorscales/MatplotlibColorscales.js} +0 -0
- /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())
|