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,144 @@
1
+ const colorscales = new Map()
2
+ const colorscales2d = new Map()
3
+
4
+ export function registerColorscale(name, glslFn) {
5
+ colorscales.set(name, glslFn)
6
+ }
7
+
8
+ export function register2DColorscale(name, glslFn) {
9
+ colorscales2d.set(name, glslFn)
10
+ }
11
+
12
+ export function getRegisteredColorscales() {
13
+ return colorscales
14
+ }
15
+
16
+ export function getRegistered2DColorscales() {
17
+ return colorscales2d
18
+ }
19
+
20
+ export function getColorscaleIndex(name) {
21
+ let idx = 0
22
+ for (const key of colorscales.keys()) {
23
+ if (key === name) return idx
24
+ idx++
25
+ }
26
+ // 2D colorscales use negative indices: -(idx + 1)
27
+ let idx2d = 0
28
+ for (const key of colorscales2d.keys()) {
29
+ if (key === name) return -(idx2d + 1)
30
+ idx2d++
31
+ }
32
+ return 0
33
+ }
34
+
35
+ export function get2DColorscaleIndex(name) {
36
+ let idx = 0
37
+ for (const key of colorscales2d.keys()) {
38
+ if (key === name) return -(idx + 1)
39
+ idx++
40
+ }
41
+ return null
42
+ }
43
+
44
+ export function buildColorGlsl() {
45
+ if (colorscales.size === 0 && colorscales2d.size === 0) return ''
46
+
47
+ const parts = []
48
+
49
+ // 1D colorscale functions
50
+ for (const glslFn of colorscales.values()) {
51
+ parts.push(glslFn)
52
+ }
53
+
54
+ // 2D colorscale functions (fn signature: vec4 colorscale_2d_<name>(vec2 t))
55
+ for (const glslFn of colorscales2d.values()) {
56
+ parts.push(glslFn)
57
+ }
58
+
59
+ // map_color — 1D dispatch, operates in already-transformed (possibly log) space
60
+ parts.push('vec4 map_color(int cs, vec2 range, float value) {')
61
+ parts.push(' float t = clamp((value - range.x) / (range.y - range.x), 0.0, 1.0);')
62
+ let idx = 0
63
+ for (const name of colorscales.keys()) {
64
+ parts.push(` if (cs == ${idx}) return colorscale_${name}(t);`)
65
+ idx++
66
+ }
67
+ parts.push(' return vec4(0.5, 0.5, 0.5, 1.0);')
68
+ parts.push('}')
69
+
70
+ // map_color_2d — 2D dispatch, takes normalized vec2 t in [0,1]x[0,1]
71
+ parts.push('vec4 map_color_2d(int cs, vec2 t) {')
72
+ let idx2d = 0
73
+ for (const name of colorscales2d.keys()) {
74
+ parts.push(` if (cs == ${idx2d}) return colorscale_2d_${name}(t);`)
75
+ idx2d++
76
+ }
77
+ parts.push(' return vec4(0.5, 0.5, 0.5, 1.0);')
78
+ parts.push('}')
79
+
80
+ // map_color_s — 1D with scale type, alpha blending, and picking
81
+ parts.push('vec4 map_color_s(int cs, vec2 range, float v, float scaleType, float useAlpha) {')
82
+ parts.push(' float vt = scaleType > 0.5 ? log(v) : v;')
83
+ parts.push(' float r0 = scaleType > 0.5 ? log(range.x) : range.x;')
84
+ parts.push(' float r1 = scaleType > 0.5 ? log(range.y) : range.y;')
85
+ parts.push(' float t = clamp((vt - r0) / (r1 - r0), 0.0, 1.0);')
86
+ parts.push(' vec4 color = map_color(cs, vec2(r0, r1), vt);')
87
+ parts.push(' if (useAlpha > 0.5) color.a = t;')
88
+ parts.push(' return gladly_apply_color(color);')
89
+ parts.push('}')
90
+
91
+ // gladly_map_color_raw — like map_color_s but without picking/apply, used internally by map_color_s_2d
92
+ parts.push('vec4 gladly_map_color_raw(int cs, vec2 range, float v, float scaleType) {')
93
+ parts.push(' float vt = scaleType > 0.5 ? log(v) : v;')
94
+ parts.push(' float r0 = scaleType > 0.5 ? log(range.x) : range.x;')
95
+ parts.push(' float r1 = scaleType > 0.5 ? log(range.y) : range.y;')
96
+ parts.push(' return map_color(cs, vec2(r0, r1), vt);')
97
+ parts.push('}')
98
+
99
+ // gladly_normalize_color — normalize a data value to [0,1] for 2D colorscale lookup
100
+ parts.push('float gladly_normalize_color(vec2 range, float v, float scaleType) {')
101
+ parts.push(' float vt = scaleType > 0.5 ? log(v) : v;')
102
+ parts.push(' float r0 = scaleType > 0.5 ? log(range.x) : range.x;')
103
+ parts.push(' float r1 = scaleType > 0.5 ? log(range.y) : range.y;')
104
+ parts.push(' return clamp((vt - r0) / (r1 - r0), 0.0, 1.0);')
105
+ parts.push('}')
106
+
107
+ // map_color_s_2d — blend two 1D colorscales, or dispatch to a true 2D colorscale.
108
+ // A true 2D colorscale is selected when cs_a < 0 && cs_a == cs_b (both axes share the
109
+ // same 2D colorscale, identified by the negative index -(idx+1)).
110
+ // useAlpha_a / useAlpha_b: if > 0.5, the normalised value for that axis modulates alpha.
111
+ parts.push('vec4 map_color_s_2d(int cs_a, vec2 range_a, float v_a, float type_a, float useAlpha_a,')
112
+ parts.push(' int cs_b, vec2 range_b, float v_b, float type_b, float useAlpha_b) {')
113
+
114
+ parts.push(' bool a_nan = isnan(v_a);')
115
+ parts.push(' bool b_nan = isnan(v_b);')
116
+ parts.push(' vec4 c;')
117
+
118
+ parts.push(' if (cs_a < 0 && cs_a == cs_b) {')
119
+ parts.push(' float ta = a_nan ? 0.0 : gladly_normalize_color(range_a, v_a, type_a);')
120
+ parts.push(' float tb = b_nan ? 0.0 : gladly_normalize_color(range_b, v_b, type_b);')
121
+ parts.push(' c = map_color_2d(-(cs_a + 1), vec2(ta, tb));')
122
+ parts.push(' } else if (cs_a >= 0) {')
123
+ parts.push(' if (!a_nan) c = gladly_map_color_raw(cs_a, range_a, v_a, type_a);')
124
+ parts.push(' else if (!b_nan) c = gladly_map_color_raw(cs_b, range_b, v_b, type_b);')
125
+ parts.push(' else c = vec4(0.0);')
126
+ parts.push(' } else {')
127
+ parts.push(' // fallback (cs_a < 0 but not equal to cs_b)')
128
+ parts.push(' if (!a_nan && !b_nan) {')
129
+ parts.push(' vec4 ca = gladly_map_color_raw(cs_a, range_a, v_a, type_a);')
130
+ parts.push(' vec4 cb = gladly_map_color_raw(cs_b, range_b, v_b, type_b);')
131
+ parts.push(' c = (ca + cb) / 2.0;')
132
+ parts.push(' } else if (!a_nan) c = gladly_map_color_raw(cs_a, range_a, v_a, type_a);')
133
+ parts.push(' else if (!b_nan) c = gladly_map_color_raw(cs_b, range_b, v_b, type_b);')
134
+ parts.push(' else c = vec4(0.0);')
135
+ parts.push(' }')
136
+
137
+ parts.push(' if (!a_nan && useAlpha_a > 0.5) c.a = gladly_normalize_color(range_a, v_a, type_a);')
138
+ parts.push(' if (!b_nan && useAlpha_b > 0.5) c.a *= gladly_normalize_color(range_b, v_b, type_b);')
139
+
140
+ parts.push(' return gladly_apply_color(c);')
141
+ parts.push('}')
142
+
143
+ return parts.join('\n')
144
+ }
@@ -0,0 +1,179 @@
1
+ import { ColumnData } from '../data/ColumnData.js'
2
+
3
+ const textureComputations = new Map()
4
+ const glslComputations = new Map()
5
+ const computedDataRegistry = new Map()
6
+
7
+ // ─── Registration ─────────────────────────────────────────────────────────────
8
+ export function registerTextureComputation(name, computation) {
9
+ textureComputations.set(name, computation)
10
+ }
11
+
12
+ export function registerGlslComputation(name, computation) {
13
+ glslComputations.set(name, computation)
14
+ }
15
+
16
+ export function registerComputedData(name, instance) {
17
+ computedDataRegistry.set(name, instance)
18
+ }
19
+
20
+ export function getComputedData(name) {
21
+ return computedDataRegistry.get(name)
22
+ }
23
+
24
+ export function getRegisteredComputedData() {
25
+ return computedDataRegistry
26
+ }
27
+
28
+ // ─── resolveExprToColumn ─────────────────────────────────────────────────────
29
+ // Turns any expression (string col name, { compName: params }, ColumnData) into ColumnData.
30
+ export async function resolveExprToColumn(expr, data, regl, plot) {
31
+ if (expr instanceof ColumnData) return expr
32
+
33
+ if (typeof expr === 'string') {
34
+ const col = data?.getData(expr)
35
+ if (!col) throw new Error(`Column '${expr}' not found in data`)
36
+ return col
37
+ }
38
+
39
+ if (typeof expr === 'object' && expr !== null) {
40
+ const keys = Object.keys(expr)
41
+ if (keys.length === 1) {
42
+ const compName = keys[0]
43
+ const params = expr[compName]
44
+
45
+ if (textureComputations.has(compName)) {
46
+ const comp = textureComputations.get(compName)
47
+ const resolvedInputs = await resolveParams(params, data, regl, plot)
48
+ return await comp.createColumn(regl, resolvedInputs, plot)
49
+ }
50
+
51
+ if (glslComputations.has(compName)) {
52
+ const comp = glslComputations.get(compName)
53
+ const resolvedInputs = await resolveParams(params, data, regl, plot)
54
+ return comp.createColumn(resolvedInputs)
55
+ }
56
+ }
57
+ }
58
+
59
+ throw new Error(`Cannot resolve expression to column: ${JSON.stringify(expr)}`)
60
+ }
61
+
62
+ // Resolve a params dict recursively: column refs -> ColumnData, scalars pass through.
63
+ async function resolveParams(params, data, regl, plot) {
64
+ if (params === null || params === undefined) return params
65
+ if (typeof params === 'number' || typeof params === 'boolean') return params
66
+ if (params instanceof ColumnData) return params
67
+ if (params instanceof Float32Array) return params
68
+
69
+ if (typeof params === 'string') {
70
+ const col = data?.getData(params)
71
+ return col ?? params // fall back to string value if not a known column
72
+ }
73
+
74
+ if (typeof params === 'object') {
75
+ const keys = Object.keys(params)
76
+ if (keys.length === 1 &&
77
+ (textureComputations.has(keys[0]) || glslComputations.has(keys[0]))) {
78
+ return await resolveExprToColumn(params, data, regl, plot)
79
+ }
80
+ const resolved = {}
81
+ for (const [k, v] of Object.entries(params)) {
82
+ resolved[k] = await resolveParams(v, data, regl, plot)
83
+ }
84
+ return resolved
85
+ }
86
+
87
+ return params
88
+ }
89
+
90
+ // ─── resolveAttributeExpr ────────────────────────────────────────────────────
91
+ // Entry point from LayerType.createDrawCommand. Returns:
92
+ // { kind: 'buffer', value: Float32Array } — fixed geometry
93
+ // { kind: 'computed', glslExpr, textures, col } — data column
94
+ export async function resolveAttributeExpr(regl, expr, attrShaderName, plot) {
95
+ if (expr instanceof Float32Array) {
96
+ return { kind: 'buffer', value: expr }
97
+ }
98
+
99
+ const data = plot ? plot.currentData : null
100
+ const col = (expr instanceof ColumnData)
101
+ ? expr
102
+ : await resolveExprToColumn(expr, data, regl, plot)
103
+
104
+ const safePath = attrShaderName.replace(/[^a-zA-Z0-9_]/g, '_')
105
+ const { glslExpr, textures } = col.resolve(safePath, regl)
106
+ return { kind: 'computed', glslExpr, textures, col }
107
+ }
108
+
109
+ // ─── resolveQuantityKind ─────────────────────────────────────────────────────
110
+ export function resolveQuantityKind(expr, data) {
111
+ if (expr instanceof ColumnData) return expr.quantityKind
112
+ if (typeof expr === 'string') {
113
+ return (data ? data.getQuantityKind(expr) : null) ?? expr
114
+ }
115
+ if (expr && typeof expr === 'object') {
116
+ const keys = Object.keys(expr)
117
+ if (keys.length === 1) {
118
+ const compName = keys[0]
119
+ const comp = textureComputations.get(compName) ?? glslComputations.get(compName)
120
+ if (comp) return comp.getQuantityKind(expr[compName], data)
121
+ }
122
+ }
123
+ return null
124
+ }
125
+
126
+ // ─── Schema builders ──────────────────────────────────────────────────────────
127
+ export const EXPRESSION_REF = { '$ref': '#/$defs/expression' }
128
+ export const EXPRESSION_REF_OPT = { '$ref': '#/$defs/expression_opt' }
129
+
130
+ export function buildTransformSchema(data) {
131
+ const defs = {}
132
+ for (const [name, comp] of computedDataRegistry) {
133
+ defs[`params_computeddata_${name}`] = comp.schema(data)
134
+ }
135
+ defs.transform_expression = {
136
+ anyOf: [...computedDataRegistry].map(([name]) => ({
137
+ type: 'object',
138
+ title: name,
139
+ properties: { [name]: { '$ref': `#/$defs/params_computeddata_${name}` } },
140
+ required: [name],
141
+ additionalProperties: false
142
+ }))
143
+ }
144
+ return { '$defs': defs }
145
+ }
146
+
147
+ export function computationSchema(data) {
148
+ const cols = data ? data.columns() : []
149
+ const defs = {}
150
+
151
+ for (const [name, comp] of textureComputations) {
152
+ defs[`params_${name}`] = comp.schema(data)
153
+ }
154
+ for (const [name, comp] of glslComputations) {
155
+ defs[`params_${name}`] = comp.schema(data)
156
+ }
157
+
158
+ defs.expression = {
159
+ anyOf: [
160
+ ...cols.map(col => ({ type: 'string', const: col, enum: [col], title: col, readOnly: true })),
161
+ ...[...textureComputations, ...glslComputations].map(([name]) => ({
162
+ type: 'object',
163
+ title: name,
164
+ properties: { [name]: { '$ref': `#/$defs/params_${name}` } },
165
+ required: [name],
166
+ additionalProperties: false
167
+ }))
168
+ ]
169
+ }
170
+
171
+ defs.expression_opt = {
172
+ anyOf: [
173
+ { type: 'string', const: 'none', enum: ['none'], title: 'none', readOnly: true },
174
+ ...defs.expression.anyOf
175
+ ]
176
+ }
177
+
178
+ return { '$defs': defs, '$ref': '#/$defs/expression' }
179
+ }
@@ -0,0 +1,59 @@
1
+ import { registerTextureComputation, EXPRESSION_REF } from "./ComputationRegistry.js"
2
+ import { TextureComputation } from "../data/Computation.js"
3
+ import { ArrayColumn, uploadToTexture } from "../data/ColumnData.js"
4
+ import makeHistogram from "./hist.js"
5
+
6
+ // Texture computation that filters the input data by a filter axis range and
7
+ // then builds a histogram texture from the surviving values.
8
+ //
9
+ // params:
10
+ // input — ColumnData of values normalised to [0, 1] (for histogram bins); must be ArrayColumn
11
+ // filterValues — ColumnData of raw filter-column values (same length as input); must be ArrayColumn
12
+ // filterAxisId — string: axis ID / quantity kind whose domain drives the filter
13
+ // bins — optional number of histogram bins
14
+ //
15
+ // getAxisDomain(filterAxisId) returns [min|null, max|null] where null means
16
+ // unbounded. The computation is re-run automatically whenever the domain changes.
17
+ class FilteredHistogramComputation extends TextureComputation {
18
+ compute(regl, inputs, getAxisDomain) {
19
+ const inputCol = inputs.input
20
+ const filterCol = inputs.filterValues
21
+ if (!(inputCol instanceof ArrayColumn)) throw new Error('filteredHistogram: input must be ArrayColumn')
22
+ if (!(filterCol instanceof ArrayColumn)) throw new Error('filteredHistogram: filterValues must be ArrayColumn')
23
+
24
+ const inputArr = inputCol.array
25
+ const filterArr = filterCol.array
26
+ const { filterAxisId, bins } = inputs
27
+ const domain = getAxisDomain(filterAxisId) // [min|null, max|null] or null
28
+ const filterMin = domain?.[0] ?? null
29
+ const filterMax = domain?.[1] ?? null
30
+
31
+ // Build a compact array of the normalised values that pass the filter.
32
+ const filtered = []
33
+ for (let i = 0; i < inputArr.length; i++) {
34
+ const fv = filterArr[i]
35
+ if (filterMin !== null && fv < filterMin) continue
36
+ if (filterMax !== null && fv > filterMax) continue
37
+ filtered.push(inputArr[i])
38
+ }
39
+
40
+ const filteredTex = uploadToTexture(regl, new Float32Array(filtered))
41
+ return makeHistogram(regl, filteredTex, { bins })
42
+ }
43
+
44
+ schema(data) {
45
+ return {
46
+ type: 'object',
47
+ title: 'filteredHistogram',
48
+ properties: {
49
+ input: EXPRESSION_REF,
50
+ filterValues: EXPRESSION_REF,
51
+ filterAxisId: { type: 'string' },
52
+ bins: { type: 'number' }
53
+ },
54
+ required: ['input', 'filterValues', 'filterAxisId']
55
+ }
56
+ }
57
+ }
58
+
59
+ registerTextureComputation('filteredHistogram', new FilteredHistogramComputation())
@@ -0,0 +1,286 @@
1
+ import { fftConvolution } from "./fft.js"
2
+ import { registerTextureComputation, EXPRESSION_REF, resolveQuantityKind } from "./ComputationRegistry.js"
3
+ import { TextureComputation } from "../data/Computation.js"
4
+
5
+ /*
6
+ ============================================================
7
+ Utilities
8
+ ============================================================
9
+ */
10
+
11
+ const MAX_KERNEL_LOOP = 1024;
12
+
13
+ function nextPow2(n) {
14
+ return 1 << Math.ceil(Math.log2(n));
15
+ }
16
+
17
+ // Internal: builds a 1-value-per-texel R-channel texture (not exposed via sampleColumn)
18
+ function make1DTexture(regl, data, width) {
19
+ return regl.texture({
20
+ data,
21
+ width,
22
+ height: 1,
23
+ format: "rgba",
24
+ type: "float",
25
+ wrap: "clamp",
26
+ min: "nearest",
27
+ mag: "nearest"
28
+ });
29
+ }
30
+
31
+ // Internal: creates an R-channel FBO of given width
32
+ function makeFBO(regl, width) {
33
+ return regl.framebuffer({
34
+ color: regl.texture({
35
+ width,
36
+ height: 1,
37
+ format: "rgba",
38
+ type: "float"
39
+ }),
40
+ depth: false,
41
+ stencil: false,
42
+ });
43
+ }
44
+
45
+ // Repack a 1-value-per-texel R-channel texture into a 4-packed RGBA texture.
46
+ // The source texture has elements at (i % srcW, i / srcW).r
47
+ // The output has elements packed 4 per texel, _dataLength set.
48
+ function repackToQuadTexture(regl, sourceTex, N) {
49
+ const nTexels = Math.ceil(N / 4)
50
+ const w = Math.min(nTexels, regl.limits.maxTextureSize)
51
+ const h = Math.ceil(nTexels / w)
52
+ const outputTex = regl.texture({ width: w, height: h, type: 'float', format: 'rgba' })
53
+ const outputFBO = regl.framebuffer({ color: outputTex, depth: false, stencil: false })
54
+
55
+ regl({
56
+ framebuffer: outputFBO,
57
+ vert: `#version 300 es
58
+ in vec2 position;
59
+ void main() { gl_Position = vec4(position, 0.0, 1.0); }`,
60
+ frag: `#version 300 es
61
+ precision highp float;
62
+ uniform sampler2D sourceTex;
63
+ uniform int totalLength;
64
+ out vec4 fragColor;
65
+ void main() {
66
+ ivec2 srcSz = textureSize(sourceTex, 0);
67
+ int texelI = int(gl_FragCoord.y) * ${w} + int(gl_FragCoord.x);
68
+ int base = texelI * 4;
69
+ float v0 = base + 0 < totalLength ? texelFetch(sourceTex, ivec2((base+0) % srcSz.x, (base+0) / srcSz.x), 0).r : 0.0;
70
+ float v1 = base + 1 < totalLength ? texelFetch(sourceTex, ivec2((base+1) % srcSz.x, (base+1) / srcSz.x), 0).r : 0.0;
71
+ float v2 = base + 2 < totalLength ? texelFetch(sourceTex, ivec2((base+2) % srcSz.x, (base+2) / srcSz.x), 0).r : 0.0;
72
+ float v3 = base + 3 < totalLength ? texelFetch(sourceTex, ivec2((base+3) % srcSz.x, (base+3) / srcSz.x), 0).r : 0.0;
73
+ fragColor = vec4(v0, v1, v2, v3);
74
+ }`,
75
+ attributes: { position: [[-1,-1],[1,-1],[-1,1],[1,1]] },
76
+ uniforms: { sourceTex, totalLength: N },
77
+ count: 4,
78
+ primitive: 'triangle strip'
79
+ })()
80
+
81
+ outputTex._dataLength = N
82
+ return outputTex
83
+ }
84
+
85
+ /*
86
+ ============================================================
87
+ 1) Single-pass convolution (kernel ≤ 1024)
88
+ ============================================================
89
+ */
90
+
91
+ function singlePassConvolution(regl) {
92
+ return regl({
93
+ frag: `#version 300 es
94
+ precision highp float;
95
+ uniform sampler2D signal, kernel;
96
+ uniform int N, K;
97
+ out vec4 outColor;
98
+
99
+ void main() {
100
+ int x = int(gl_FragCoord.x);
101
+ float sum = 0.0;
102
+
103
+ for (int i = 0; i < ${MAX_KERNEL_LOOP}; i++) {
104
+ if (i >= K) break;
105
+ int xi = x - i;
106
+ if (xi < 0 || xi >= N) continue;
107
+
108
+ float s = texelFetch(signal, ivec2(xi, 0), 0).r;
109
+ float k = texelFetch(kernel, ivec2(i, 0), 0).r;
110
+ sum += s * k;
111
+ }
112
+
113
+ outColor = vec4(sum, 0, 0, 1);
114
+ }`,
115
+ vert: `#version 300 es
116
+ in vec2 position;
117
+ void main() {
118
+ gl_Position = vec4(position, 0, 1);
119
+ }`,
120
+ attributes: {
121
+ position: [[-1,-1],[1,-1],[-1,1],[1,1]]
122
+ },
123
+ uniforms: {
124
+ signal: regl.prop("signal"),
125
+ kernel: regl.prop("kernel"),
126
+ N: regl.prop("N"),
127
+ K: regl.prop("K")
128
+ },
129
+ framebuffer: regl.prop("fbo"),
130
+ count: 4,
131
+ primitive: "triangle strip"
132
+ });
133
+ }
134
+
135
+ /*
136
+ ============================================================
137
+ 2) Two-pass chunked convolution (arbitrary kernel size)
138
+ ============================================================
139
+ */
140
+
141
+ function chunkedConvolution(regl) {
142
+ const partialPass = regl({
143
+ frag: `#version 300 es
144
+ precision highp float;
145
+ uniform sampler2D signal, kernel2D;
146
+ uniform int N, chunkOffset;
147
+ out vec4 outColor;
148
+
149
+ void main() {
150
+ int x = int(gl_FragCoord.x);
151
+ float sum = 0.0;
152
+
153
+ for (int i = 0; i < ${MAX_KERNEL_LOOP}; i++) {
154
+ int kIndex = chunkOffset + i;
155
+ int xi = x - kIndex;
156
+ if (xi < 0 || xi >= N) continue;
157
+
158
+ float s = texelFetch(signal, ivec2(xi, 0), 0).r;
159
+ float k = texelFetch(kernel2D, ivec2(i, 0), 0).r;
160
+ sum += s * k;
161
+ }
162
+
163
+ outColor = vec4(sum, 0, 0, 1);
164
+ }`,
165
+ vert: `#version 300 es
166
+ in vec2 position;
167
+ void main() {
168
+ gl_Position = vec4(position, 0, 1);
169
+ }`,
170
+ attributes: {
171
+ position: [[-1,-1],[1,-1],[-1,1],[1,1]]
172
+ },
173
+ uniforms: {
174
+ signal: regl.prop("signal"),
175
+ kernel2D: regl.prop("kernel"),
176
+ N: regl.prop("N"),
177
+ chunkOffset: regl.prop("offset")
178
+ },
179
+ framebuffer: regl.prop("fbo"),
180
+ blend: {
181
+ enable: true,
182
+ func: { src: "one", dst: "one" }
183
+ },
184
+ count: 4,
185
+ primitive: "triangle strip"
186
+ });
187
+
188
+ return function run({ signalTex, kernel, N }) {
189
+ const chunks = Math.ceil(kernel.length / MAX_KERNEL_LOOP);
190
+ const fbo = makeFBO(regl, N);
191
+
192
+ regl.clear({ framebuffer: fbo, color: [0,0,0,0] });
193
+
194
+ for (let c = 0; c < chunks; c++) {
195
+ const slice = kernel.slice(
196
+ c * MAX_KERNEL_LOOP,
197
+ (c + 1) * MAX_KERNEL_LOOP
198
+ );
199
+
200
+ const kernelTex = make1DTexture(regl, slice, MAX_KERNEL_LOOP);
201
+
202
+ partialPass({
203
+ signal: signalTex,
204
+ kernel: kernelTex,
205
+ N,
206
+ offset: c * MAX_KERNEL_LOOP,
207
+ fbo
208
+ });
209
+ }
210
+
211
+ return fbo.color[0];
212
+ };
213
+ }
214
+
215
+ /*
216
+ ============================================================
217
+ Adaptive wrapper
218
+ ============================================================
219
+ */
220
+
221
+ export default function adaptiveConvolution(regl, signalArray, kernelArray) {
222
+ const single = singlePassConvolution(regl);
223
+ const chunked = chunkedConvolution(regl);
224
+
225
+ const N = signalArray.length;
226
+ const K = kernelArray.length;
227
+
228
+ const signalTex = make1DTexture(regl, signalArray, N);
229
+
230
+ let result;
231
+
232
+ // Case 1: single pass
233
+ if (K <= MAX_KERNEL_LOOP) {
234
+ const kernelTex = make1DTexture(regl, kernelArray, K);
235
+ const fbo = makeFBO(regl, N);
236
+
237
+ single({
238
+ signal: signalTex,
239
+ kernel: kernelTex,
240
+ N,
241
+ K,
242
+ fbo
243
+ });
244
+
245
+ result = fbo.color[0];
246
+ }
247
+
248
+ // Case 2: chunked
249
+ else if (K <= 8192) {
250
+ result = chunked({
251
+ signalTex,
252
+ kernel: kernelArray,
253
+ N
254
+ });
255
+ }
256
+
257
+ // Case 3: FFT
258
+ else {
259
+ result = fftConvolution(regl, signalArray, kernelArray);
260
+ }
261
+
262
+ // Repack internal R-channel result into 4-packed output for sampleColumn
263
+ return repackToQuadTexture(regl, result, N);
264
+ }
265
+
266
+ class ConvolutionComputation extends TextureComputation {
267
+ getQuantityKind(params, data) { return resolveQuantityKind(params.signal, data) }
268
+ compute(regl, params, data, getAxisDomain) {
269
+ const signal = typeof params.signal === 'string' ? data.getData(params.signal) : params.signal
270
+ const kernel = typeof params.kernel === 'string' ? data.getData(params.kernel) : params.kernel
271
+ return adaptiveConvolution(regl, signal, kernel)
272
+ }
273
+ schema(data) {
274
+ return {
275
+ type: 'object',
276
+ title: 'convolution',
277
+ properties: {
278
+ signal: EXPRESSION_REF,
279
+ kernel: EXPRESSION_REF
280
+ },
281
+ required: ['signal', 'kernel']
282
+ }
283
+ }
284
+ }
285
+
286
+ registerTextureComputation('convolution', new ConvolutionComputation())