@hema-to/regl-scatterplot 1.14.1-hemato.0

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.
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Calculates exponential moving average of 2D points
3
+ * @param {[number, number][]} values - Array of numbers to average
4
+ * @param {number} halfLife - Number of steps after which weight becomes half
5
+ * @param {number} windowSize - Maximum number of previous values to consider
6
+ * @returns {number} The exponential moving average
7
+ */
8
+ export const exponentialMovingAverage = (values, halfLife, windowSize) => {
9
+ if (values.length === 0) {
10
+ return 0;
11
+ }
12
+
13
+ if (values.length === 1) {
14
+ return values[0];
15
+ }
16
+
17
+ // Calculate decay factor from `halfLife` such that weight = 0.5 when the
18
+ // step is `halfLife`
19
+ const decayBase = 2 ** (-1 / halfLife);
20
+
21
+ // Limit to window size
22
+ const startIdx = Math.max(0, values.length - windowSize);
23
+ const relevantValues = values.slice(startIdx);
24
+
25
+ let weightedSumX = 0;
26
+ let weightedSumY = 0;
27
+ let weightSum = 0;
28
+
29
+ // Calculate weighted sum starting from most recent value
30
+ for (let i = relevantValues.length - 1; i >= 0; i--) {
31
+ const steps = relevantValues.length - 1 - i;
32
+ const weight = decayBase ** steps;
33
+
34
+ weightedSumX += relevantValues[i][0] * weight;
35
+ weightedSumY += relevantValues[i][1] * weight;
36
+ weightSum += weight;
37
+ }
38
+
39
+ return [weightedSumX / weightSum, weightedSumY / weightSum];
40
+ };
@@ -0,0 +1,10 @@
1
+ const FRAGMENT_SHADER = `precision highp float;
2
+
3
+ varying vec4 color;
4
+
5
+ void main() {
6
+ gl_FragColor = color;
7
+ }
8
+ `;
9
+
10
+ export default FRAGMENT_SHADER;
@@ -0,0 +1,21 @@
1
+ const SHADER = `precision highp float;
2
+
3
+ uniform sampler2D startStateTex;
4
+ uniform sampler2D endStateTex;
5
+ uniform float t;
6
+
7
+ varying vec2 particleTextureIndex;
8
+
9
+ void main() {
10
+ // Interpolate x, y, and value
11
+ vec3 start = texture2D(startStateTex, particleTextureIndex).xyw;
12
+ vec3 end = texture2D(endStateTex, particleTextureIndex).xyw;
13
+ vec3 curr = start * (1.0 - t) + end * t;
14
+
15
+ // The category cannot be interpolated
16
+ float endCategory = texture2D(endStateTex, particleTextureIndex).z;
17
+
18
+ gl_FragColor = vec4(curr.xy, endCategory, curr.z);
19
+ }`;
20
+
21
+ export default SHADER;
@@ -0,0 +1,13 @@
1
+ const SHADER = `precision highp float;
2
+
3
+ attribute vec2 position;
4
+ varying vec2 particleTextureIndex;
5
+
6
+ void main() {
7
+ // map normalized device coords to texture coords
8
+ particleTextureIndex = 0.5 * (1.0 + position);
9
+
10
+ gl_Position = vec4(position, 0, 1);
11
+ }`;
12
+
13
+ export default SHADER;
package/src/point.fs ADDED
@@ -0,0 +1,22 @@
1
+ const FRAGMENT_SHADER = `
2
+ precision highp float;
3
+
4
+ uniform float antiAliasing;
5
+
6
+ varying vec4 color;
7
+ varying float finalPointSize;
8
+
9
+ float linearstep(float edge0, float edge1, float x) {
10
+ return clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);
11
+ }
12
+
13
+ void main() {
14
+ vec2 c = gl_PointCoord * 2.0 - 1.0;
15
+ float sdf = length(c) * finalPointSize;
16
+ float alpha = linearstep(finalPointSize + antiAliasing, finalPointSize - antiAliasing, sdf);
17
+
18
+ gl_FragColor = vec4(color.rgb, alpha * color.a);
19
+ }
20
+ `;
21
+
22
+ export default FRAGMENT_SHADER;
package/src/point.vs ADDED
@@ -0,0 +1,124 @@
1
+ const createVertexShader = (globalState) => `
2
+ precision highp float;
3
+
4
+ uniform sampler2D colorTex;
5
+ uniform float colorTexRes;
6
+ uniform float colorTexEps;
7
+ uniform sampler2D stateTex;
8
+ uniform float stateTexRes;
9
+ uniform float stateTexEps;
10
+ uniform float devicePixelRatio;
11
+ uniform sampler2D encodingTex;
12
+ uniform float encodingTexRes;
13
+ uniform float encodingTexEps;
14
+ uniform float pointSizeExtra;
15
+ uniform float pointOpacityMax;
16
+ uniform float pointOpacityScale;
17
+ uniform float numPoints;
18
+ uniform float globalState;
19
+ uniform float isColoredByZ;
20
+ uniform float isColoredByW;
21
+ uniform float isOpacityByZ;
22
+ uniform float isOpacityByW;
23
+ uniform float isOpacityByDensity;
24
+ uniform float isSizedByZ;
25
+ uniform float isSizedByW;
26
+ uniform float isPixelAligned;
27
+ uniform float colorMultiplicator;
28
+ uniform float opacityMultiplicator;
29
+ uniform float opacityDensity;
30
+ uniform float sizeMultiplicator;
31
+ uniform float numColorStates;
32
+ uniform float pointScale;
33
+ uniform float drawingBufferWidth;
34
+ uniform float drawingBufferHeight;
35
+ uniform mat4 modelViewProjection;
36
+
37
+ attribute vec2 stateIndex;
38
+
39
+ varying vec4 color;
40
+ varying float finalPointSize;
41
+
42
+ void main() {
43
+ vec4 state = texture2D(stateTex, stateIndex);
44
+
45
+ if (isPixelAligned < 0.5) {
46
+ gl_Position = modelViewProjection * vec4(state.x, state.y, 0.0, 1.0);
47
+ } else {
48
+ vec4 clipSpacePosition = modelViewProjection * vec4(state.x, state.y, 0.0, 1.0);
49
+ vec2 ndcPosition = clipSpacePosition.xy / clipSpacePosition.w;
50
+ vec2 pixelPos = 0.5 * (ndcPosition + 1.0) * vec2(drawingBufferWidth, drawingBufferHeight);
51
+ pixelPos = floor(pixelPos + 0.5); // Snap to nearest pixel
52
+ vec2 snappedPosition = (pixelPos / vec2(drawingBufferWidth, drawingBufferHeight)) * 2.0 - 1.0;
53
+ gl_Position = vec4(snappedPosition, 0.0, 1.0);
54
+ }
55
+
56
+
57
+ // Determine color index
58
+ float colorIndexZ = isColoredByZ * floor(state.z * colorMultiplicator);
59
+ float colorIndexW = isColoredByW * floor(state.w * colorMultiplicator);
60
+
61
+ // Multiply by the number of color states per color
62
+ // I.e., normal, active, hover, background, etc.
63
+ float colorIndex = (colorIndexZ + colorIndexW) * numColorStates;
64
+
65
+ // Half a "pixel" or "texel" in texture coordinates
66
+ float colorLinearIndex = colorIndex + globalState;
67
+
68
+ // Need to add cEps here to avoid floating point issue that can lead to
69
+ // dramatic changes in which color is loaded as floor(3/2.9999) = 1 but
70
+ // floor(3/3.0001) = 0!
71
+ float colorRowIndex = floor((colorLinearIndex + colorTexEps) / colorTexRes);
72
+
73
+ vec2 colorTexIndex = vec2(
74
+ (colorLinearIndex / colorTexRes) - colorRowIndex + colorTexEps,
75
+ colorRowIndex / colorTexRes + colorTexEps
76
+ );
77
+
78
+ color = texture2D(colorTex, colorTexIndex);
79
+
80
+ // Retrieve point size
81
+ float pointSizeIndexZ = isSizedByZ * floor(state.z * sizeMultiplicator);
82
+ float pointSizeIndexW = isSizedByW * floor(state.w * sizeMultiplicator);
83
+ float pointSizeIndex = pointSizeIndexZ + pointSizeIndexW;
84
+
85
+ float pointSizeRowIndex = floor((pointSizeIndex + encodingTexEps) / encodingTexRes);
86
+ vec2 pointSizeTexIndex = vec2(
87
+ (pointSizeIndex / encodingTexRes) - pointSizeRowIndex + encodingTexEps,
88
+ pointSizeRowIndex / encodingTexRes + encodingTexEps
89
+ );
90
+ float pointSize = texture2D(encodingTex, pointSizeTexIndex).x;
91
+
92
+ // Retrieve opacity
93
+ ${
94
+ (() => {
95
+ // Drawing the inner border of selected points
96
+ if (globalState === 3) return '';
97
+
98
+ // Draw points with opacity encoding or dynamic opacity
99
+ return `
100
+ if (isOpacityByDensity < 0.5) {
101
+ float opacityIndexZ = isOpacityByZ * floor(state.z * opacityMultiplicator);
102
+ float opacityIndexW = isOpacityByW * floor(state.w * opacityMultiplicator);
103
+ float opacityIndex = opacityIndexZ + opacityIndexW;
104
+
105
+ float opacityRowIndex = floor((opacityIndex + encodingTexEps) / encodingTexRes);
106
+ vec2 opacityTexIndex = vec2(
107
+ (opacityIndex / encodingTexRes) - opacityRowIndex + encodingTexEps,
108
+ opacityRowIndex / encodingTexRes + encodingTexEps
109
+ );
110
+ color.a = texture2D(encodingTex, opacityTexIndex)[${1 + globalState}];
111
+ } else {
112
+ color.a = min(1.0, opacityDensity + globalState);
113
+ }
114
+ `;
115
+ })()
116
+ }
117
+
118
+ color.a = min(pointOpacityMax, color.a) * pointOpacityScale;
119
+ finalPointSize = (pointSize * pointScale) + pointSizeExtra;
120
+ gl_PointSize = finalPointSize;
121
+ }
122
+ `;
123
+
124
+ export default createVertexShader;
@@ -0,0 +1,252 @@
1
+ import { CLEAR_OPTIONS, DEFAULT_GAMMA } from './constants.js';
2
+ import { checkReglExtensions, createRegl } from './utils.js';
3
+
4
+ export const createRenderer = (
5
+ /** @type {Partial<import('./types').RendererOptions>} */ options = {},
6
+ ) => {
7
+ let {
8
+ regl,
9
+ canvas = document.createElement('canvas'),
10
+ gamma = DEFAULT_GAMMA,
11
+ } = options;
12
+
13
+ let isDestroyed = false;
14
+
15
+ if (!regl) {
16
+ regl = createRegl(canvas);
17
+ }
18
+
19
+ const isSupportingAllGlExtensions = checkReglExtensions(regl);
20
+
21
+ const fboRes = [canvas.width, canvas.height];
22
+ const fbo = regl.framebuffer({
23
+ width: fboRes[0],
24
+ height: fboRes[1],
25
+ colorFormat: 'rgba',
26
+ colorType: 'float',
27
+ });
28
+
29
+ /**
30
+ * Render the float32 framebuffer to the internal canvas
31
+ *
32
+ * From https://observablehq.com/@rreusser/selecting-the-right-opacity-for-2d-point-clouds
33
+ */
34
+ const renderToCanvas = regl({
35
+ vert: `
36
+ precision highp float;
37
+ attribute vec2 xy;
38
+ void main () {
39
+ gl_Position = vec4(xy, 0, 1);
40
+ }`,
41
+ frag: `
42
+ precision highp float;
43
+ uniform vec2 srcRes;
44
+ uniform sampler2D src;
45
+ uniform float gamma;
46
+
47
+ vec3 approxLinearToSRGB (vec3 rgb, float gamma) {
48
+ return pow(clamp(rgb, vec3(0), vec3(1)), vec3(1.0 / gamma));
49
+ }
50
+
51
+ void main () {
52
+ vec4 color = texture2D(src, gl_FragCoord.xy / srcRes);
53
+ gl_FragColor = vec4(approxLinearToSRGB(color.rgb, gamma), color.a);
54
+ }`,
55
+ attributes: {
56
+ xy: [-4, -4, 4, -4, 0, 4],
57
+ },
58
+ uniforms: {
59
+ src: () => fbo,
60
+ srcRes: () => fboRes,
61
+ gamma: () => gamma,
62
+ },
63
+ count: 3,
64
+ depth: { enable: false },
65
+ blend: {
66
+ enable: true,
67
+ func: {
68
+ // biome-ignore lint/style/useNamingConvention: Regl internal
69
+ srcRGB: 'one',
70
+ srcAlpha: 'one',
71
+ // biome-ignore lint/style/useNamingConvention: Regl internal
72
+ dstRGB: 'one minus src alpha',
73
+ dstAlpha: 'one minus src alpha',
74
+ },
75
+ },
76
+ });
77
+
78
+ /**
79
+ * Copy the pixels from the internal canvas onto the target canvas
80
+ */
81
+ const copyTo = (targetCanvas) => {
82
+ const ctx = targetCanvas.getContext('2d');
83
+ ctx.clearRect(0, 0, targetCanvas.width, targetCanvas.height);
84
+ ctx.drawImage(
85
+ canvas,
86
+ (canvas.width - targetCanvas.width) / 2,
87
+ (canvas.height - targetCanvas.height) / 2,
88
+ targetCanvas.width,
89
+ targetCanvas.height,
90
+ 0,
91
+ 0,
92
+ targetCanvas.width,
93
+ targetCanvas.height,
94
+ );
95
+ };
96
+
97
+ /**
98
+ * The render function
99
+ */
100
+ const render = (
101
+ /** @type {(): void} */ draw,
102
+ /** @type {HTMLCanvasElement} */ targetCanvas,
103
+ ) => {
104
+ // Clear internal canvas
105
+ regl.clear(CLEAR_OPTIONS);
106
+ fbo.use(() => {
107
+ // Clear framebuffer
108
+ regl.clear(CLEAR_OPTIONS);
109
+ draw();
110
+ });
111
+ renderToCanvas();
112
+ copyTo(targetCanvas);
113
+ };
114
+
115
+ /**
116
+ * Update Regl's viewport, drawingBufferWidth, and drawingBufferHeight
117
+ *
118
+ * @description Call this method after the viewport has changed, e.g., width
119
+ * or height have been altered
120
+ */
121
+ const refresh = () => {
122
+ regl.poll();
123
+ };
124
+
125
+ const drawFns = new Set();
126
+
127
+ /**
128
+ * Register an draw function that is going to be invoked on every animation
129
+ * frame.
130
+ */
131
+ const onFrame = (/** @type {(): void} */ draw) => {
132
+ drawFns.add(draw);
133
+ return () => {
134
+ drawFns.delete(draw);
135
+ };
136
+ };
137
+
138
+ const frame = regl.frame(() => {
139
+ const iterator = drawFns.values();
140
+ let result = iterator.next();
141
+ while (!result.done) {
142
+ result.value(); // The draw function
143
+ result = iterator.next();
144
+ }
145
+ });
146
+
147
+ const resize = (
148
+ /** @type {number} */ customWidth,
149
+ /** @type {number} */ customHeight,
150
+ ) => {
151
+ // We need to limit the width and height by the screen size to prevent
152
+ // a bug in VSCode where the window height is said to be taller than the
153
+ // screen height. The problem with too large dimensions is that at some
154
+ // point WebGL will break down because there's an upper limit on how large
155
+ // any buffer and texture can be. It also harms the performance quite a bit.
156
+ //
157
+ // By restricting the widht/height to the screen size we should have a safe
158
+ // upper limit for the canvas size.
159
+ //
160
+ // @see
161
+ // https://github.com/microsoft/vscode/issues/225808
162
+ // https://github.com/flekschas/jupyter-scatter/issues/37
163
+ const width =
164
+ customWidth === undefined
165
+ ? Math.min(window.innerWidth, window.screen.availWidth)
166
+ : customWidth;
167
+ const height =
168
+ customHeight === undefined
169
+ ? Math.min(window.innerHeight, window.screen.availHeight)
170
+ : customHeight;
171
+ canvas.width = width * window.devicePixelRatio;
172
+ canvas.height = height * window.devicePixelRatio;
173
+ fboRes[0] = canvas.width;
174
+ fboRes[1] = canvas.height;
175
+ fbo.resize(...fboRes);
176
+ };
177
+
178
+ const resizeHandler = () => {
179
+ resize();
180
+ };
181
+
182
+ if (!options.canvas) {
183
+ window.addEventListener('resize', resizeHandler);
184
+ window.addEventListener('orientationchange', resizeHandler);
185
+ resize();
186
+ }
187
+
188
+ /**
189
+ * Destroy the renderer to free resources and cancel animation frames
190
+ */
191
+ const destroy = () => {
192
+ isDestroyed = true;
193
+ window.removeEventListener('resize', resizeHandler);
194
+ window.removeEventListener('orientationchange', resizeHandler);
195
+ frame.cancel();
196
+ canvas = undefined;
197
+ regl.destroy();
198
+ regl = undefined;
199
+ };
200
+
201
+ return {
202
+ /**
203
+ * Get the associated canvas element
204
+ * @return {HTMLCanvasElement} The associated canvas element
205
+ */
206
+ get canvas() {
207
+ return canvas;
208
+ },
209
+ /**
210
+ * Get the associated Regl instance
211
+ * @return {import('regl').Regl} The associated Regl instance
212
+ */
213
+ get regl() {
214
+ return regl;
215
+ },
216
+ /**
217
+ * Get the gamma value
218
+ * @return {number} The gamma value
219
+ */
220
+ get gamma() {
221
+ return gamma;
222
+ },
223
+ /**
224
+ * Set gamma to a new value
225
+ * @param {number} newGamma - The new gamma value
226
+ */
227
+ set gamma(newGamma) {
228
+ gamma = +newGamma;
229
+ },
230
+ /**
231
+ * Get whether the browser supports all necessary WebGL features
232
+ * @return {boolean} If `true` the browser supports all necessary WebGL features
233
+ */
234
+ get isSupported() {
235
+ return isSupportingAllGlExtensions;
236
+ },
237
+ /**
238
+ * Get whether the renderer (and its Regl instance) is destroyed
239
+ * @return {boolean} If `true` the renderer is destroyed
240
+ */
241
+ get isDestroyed() {
242
+ return isDestroyed;
243
+ },
244
+ render,
245
+ resize,
246
+ onFrame,
247
+ refresh,
248
+ destroy,
249
+ };
250
+ };
251
+
252
+ export default createRenderer;