@longline/aqua-ui 1.0.299 → 1.0.302
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/aquaui-sprites.svg +1 -1
- package/formatters/CountryFormatter/CountryFormatter.d.ts +25 -3
- package/formatters/CountryFormatter/CountryFormatter.js +24 -2
- package/formatters/DateTimeFormatter/DateTimeFormatter.d.ts +38 -9
- package/formatters/DateTimeFormatter/DateTimeFormatter.js +38 -9
- package/formatters/DivisionFormatter/DivisionFormatter.d.ts +28 -4
- package/formatters/DivisionFormatter/DivisionFormatter.js +25 -3
- package/formatters/FilesizeFormatter/FilesizeFormatter.d.ts +24 -8
- package/formatters/FilesizeFormatter/FilesizeFormatter.js +50 -31
- package/formatters/GIS/CoordinateFormatter.d.ts +58 -0
- package/formatters/GIS/CoordinateFormatter.js +51 -0
- package/formatters/GIS/LatitudeFormatter.d.ts +5 -4
- package/formatters/GIS/LatitudeFormatter.js +7 -11
- package/formatters/GIS/LongitudeFormatter.d.ts +9 -6
- package/formatters/GIS/LongitudeFormatter.js +11 -13
- package/formatters/GIS/index.d.ts +1 -0
- package/formatters/GIS/index.js +1 -0
- package/formatters/HighlightFormatter/HighlightFormatter.d.ts +26 -5
- package/formatters/HighlightFormatter/HighlightFormatter.js +26 -5
- package/formatters/HttpStatusFormatter/HttpStatusFormatter.d.ts +34 -14
- package/formatters/HttpStatusFormatter/HttpStatusFormatter.js +35 -16
- package/formatters/HumanFormatter/HumanFormatter.d.ts +24 -4
- package/formatters/HumanFormatter/HumanFormatter.js +37 -12
- package/formatters/NumberFormatter/NumberFormatter.d.ts +19 -6
- package/formatters/NumberFormatter/NumberFormatter.js +19 -6
- package/formatters/StringFormatter/StringFormatter.d.ts +22 -4
- package/formatters/StringFormatter/StringFormatter.js +21 -3
- package/map/layers/ParticlesLayer/ParticlesLayer.d.ts +97 -1
- package/map/layers/ParticlesLayer/ParticlesLayer.js +239 -38
- package/map/layers/ParticlesLayer/ParticlesVertexShader.d.ts +1 -1
- package/map/layers/ParticlesLayer/ParticlesVertexShader.js +1 -1
- package/map/layers/ParticlesLayer/PositionComputeFragmentShader.d.ts +2 -0
- package/map/layers/ParticlesLayer/PositionComputeFragmentShader.js +2 -0
- package/map/layers/ParticlesLayer/PositionComputeVertexShader.d.ts +2 -0
- package/map/layers/ParticlesLayer/PositionComputeVertexShader.js +2 -0
- package/package.json +1 -1
- package/services/Dialog/AlertDialog.d.ts +1 -1
- package/services/Dialog/ConfirmDialog.d.ts +5 -5
- package/services/Dialog/ConfirmDialog.js +1 -1
- package/services/Dialog/Dialog.d.ts +46 -16
- package/services/Dialog/Dialog.js +98 -16
- package/services/Dialog/DialogBackground.js +1 -0
- package/services/Dialog/DialogContent.d.ts +3 -3
- package/services/Dialog/DialogContent.js +1 -1
- package/services/Dialog/DialogFooter.d.ts +3 -3
- package/services/Dialog/DialogHeader.d.ts +3 -3
- package/services/Dialog/DialogWindow.d.ts +3 -4
- package/services/Dialog/DialogWindow.js +1 -0
- package/services/Dialog/XhrDialog.d.ts +1 -1
- package/services/Dialog/index.d.ts +7 -1
- package/svg/icons/index.d.ts +1 -0
- package/svg/icons/index.js +1 -0
|
@@ -16,15 +16,28 @@ interface INumberFormatterProps {
|
|
|
16
16
|
locale?: string;
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* convert it to a float first. By default, two decimals are printed.
|
|
19
|
+
* Formats numbers with locale-aware thousands separators and configurable
|
|
20
|
+
* decimal precision.
|
|
22
21
|
*
|
|
23
|
-
*
|
|
22
|
+
* Use `NumberFormatter` when displaying numeric values that benefit from
|
|
23
|
+
* grouping (e.g., financial figures, statistics, counts). For large numbers
|
|
24
|
+
* that should be abbreviated (e.g., "1.2M"), use `HumanFormatter` instead.
|
|
25
|
+
*
|
|
26
|
+
* ## Output Examples
|
|
27
|
+
*
|
|
28
|
+
* | Input | Props | Output |
|
|
29
|
+
* |-------|-------|--------|
|
|
30
|
+
* | `35100.2` | (default) | `35,100.20` |
|
|
31
|
+
* | `35100.2` | `decimals={0}` | `35,100` |
|
|
32
|
+
* | `1234.567` | `locale="de-DE"` | `1.234,57` |
|
|
33
|
+
* | `"1000"` | (default) | `1,000.00` |
|
|
34
|
+
* | `NaN` | (default) | *(null)* |
|
|
35
|
+
*
|
|
36
|
+
* ## Usage
|
|
24
37
|
*
|
|
25
38
|
* ```tsx
|
|
26
|
-
* <NumberFormatter value={35100.20}/>
|
|
27
|
-
* <NumberFormatter value={35100.20} decimals={0}/>
|
|
39
|
+
* <NumberFormatter value={35100.20} />
|
|
40
|
+
* <NumberFormatter value={35100.20} decimals={0} />
|
|
28
41
|
* <NumberFormatter value={1234.567} locale="de-DE" />
|
|
29
42
|
* ```
|
|
30
43
|
*/
|
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* convert it to a float first. By default, two decimals are printed.
|
|
3
|
+
* Formats numbers with locale-aware thousands separators and configurable
|
|
4
|
+
* decimal precision.
|
|
6
5
|
*
|
|
7
|
-
*
|
|
6
|
+
* Use `NumberFormatter` when displaying numeric values that benefit from
|
|
7
|
+
* grouping (e.g., financial figures, statistics, counts). For large numbers
|
|
8
|
+
* that should be abbreviated (e.g., "1.2M"), use `HumanFormatter` instead.
|
|
9
|
+
*
|
|
10
|
+
* ## Output Examples
|
|
11
|
+
*
|
|
12
|
+
* | Input | Props | Output |
|
|
13
|
+
* |-------|-------|--------|
|
|
14
|
+
* | `35100.2` | (default) | `35,100.20` |
|
|
15
|
+
* | `35100.2` | `decimals={0}` | `35,100` |
|
|
16
|
+
* | `1234.567` | `locale="de-DE"` | `1.234,57` |
|
|
17
|
+
* | `"1000"` | (default) | `1,000.00` |
|
|
18
|
+
* | `NaN` | (default) | *(null)* |
|
|
19
|
+
*
|
|
20
|
+
* ## Usage
|
|
8
21
|
*
|
|
9
22
|
* ```tsx
|
|
10
|
-
* <NumberFormatter value={35100.20}/>
|
|
11
|
-
* <NumberFormatter value={35100.20} decimals={0}/>
|
|
23
|
+
* <NumberFormatter value={35100.20} />
|
|
24
|
+
* <NumberFormatter value={35100.20} decimals={0} />
|
|
12
25
|
* <NumberFormatter value={1234.567} locale="de-DE" />
|
|
13
26
|
* ```
|
|
14
27
|
*/
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
1
2
|
interface IStringFormatterProps {
|
|
2
3
|
/** String to format */
|
|
3
4
|
value: string;
|
|
@@ -5,16 +6,33 @@ interface IStringFormatterProps {
|
|
|
5
6
|
type: 'capitalize' | 'capitalizeWords' | 'upper' | 'lower' | 'reverse' | 'snake' | 'kebab';
|
|
6
7
|
}
|
|
7
8
|
/**
|
|
8
|
-
*
|
|
9
|
+
* Transforms strings using common text formatting operations.
|
|
10
|
+
*
|
|
11
|
+
* All operations are Unicode-safe, using `toLocaleUpperCase()` and
|
|
12
|
+
* `toLocaleLowerCase()` for proper internationalization.
|
|
13
|
+
*
|
|
14
|
+
* ## Format Types
|
|
15
|
+
*
|
|
16
|
+
* | Type | Input | Output |
|
|
17
|
+
* |------|-------|--------|
|
|
18
|
+
* | `capitalize` | `"hello world"` | `"Hello world"` |
|
|
19
|
+
* | `capitalizeWords` | `"hello world"` | `"Hello World"` |
|
|
20
|
+
* | `upper` | `"Hello World"` | `"HELLO WORLD"` |
|
|
21
|
+
* | `lower` | `"Hello World"` | `"hello world"` |
|
|
22
|
+
* | `reverse` | `"Hello"` | `"olleH"` |
|
|
23
|
+
* | `snake` | `"Hello World"` | `"hello_world"` |
|
|
24
|
+
* | `kebab` | `"Hello World"` | `"hello-world"` |
|
|
25
|
+
*
|
|
26
|
+
* ## Usage
|
|
9
27
|
*
|
|
10
28
|
* ```tsx
|
|
11
29
|
* <StringFormatter value="hello world" type="capitalize" />
|
|
12
30
|
* <StringFormatter value="hello world" type="capitalizeWords" />
|
|
13
|
-
* <StringFormatter value="
|
|
31
|
+
* <StringFormatter value="my variable name" type="snake" />
|
|
14
32
|
* ```
|
|
15
33
|
*/
|
|
16
34
|
declare const StringFormatter: {
|
|
17
|
-
({ value, type }: IStringFormatterProps):
|
|
35
|
+
({ value, type }: IStringFormatterProps): React.JSX.Element;
|
|
18
36
|
displayName: string;
|
|
19
37
|
};
|
|
20
|
-
export { StringFormatter };
|
|
38
|
+
export { StringFormatter, IStringFormatterProps };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
1
2
|
/** Unicode-safe capitalize first letter */
|
|
2
3
|
var capitalize = function (str) {
|
|
3
4
|
if (!str)
|
|
@@ -26,12 +27,29 @@ var formatters = {
|
|
|
26
27
|
kebab: function (s) { return s.trim().toLocaleLowerCase().replace(/\s+/g, '-'); },
|
|
27
28
|
};
|
|
28
29
|
/**
|
|
29
|
-
*
|
|
30
|
+
* Transforms strings using common text formatting operations.
|
|
31
|
+
*
|
|
32
|
+
* All operations are Unicode-safe, using `toLocaleUpperCase()` and
|
|
33
|
+
* `toLocaleLowerCase()` for proper internationalization.
|
|
34
|
+
*
|
|
35
|
+
* ## Format Types
|
|
36
|
+
*
|
|
37
|
+
* | Type | Input | Output |
|
|
38
|
+
* |------|-------|--------|
|
|
39
|
+
* | `capitalize` | `"hello world"` | `"Hello world"` |
|
|
40
|
+
* | `capitalizeWords` | `"hello world"` | `"Hello World"` |
|
|
41
|
+
* | `upper` | `"Hello World"` | `"HELLO WORLD"` |
|
|
42
|
+
* | `lower` | `"Hello World"` | `"hello world"` |
|
|
43
|
+
* | `reverse` | `"Hello"` | `"olleH"` |
|
|
44
|
+
* | `snake` | `"Hello World"` | `"hello_world"` |
|
|
45
|
+
* | `kebab` | `"Hello World"` | `"hello-world"` |
|
|
46
|
+
*
|
|
47
|
+
* ## Usage
|
|
30
48
|
*
|
|
31
49
|
* ```tsx
|
|
32
50
|
* <StringFormatter value="hello world" type="capitalize" />
|
|
33
51
|
* <StringFormatter value="hello world" type="capitalizeWords" />
|
|
34
|
-
* <StringFormatter value="
|
|
52
|
+
* <StringFormatter value="my variable name" type="snake" />
|
|
35
53
|
* ```
|
|
36
54
|
*/
|
|
37
55
|
var StringFormatter = function (_a) {
|
|
@@ -42,7 +60,7 @@ var StringFormatter = function (_a) {
|
|
|
42
60
|
if (typeof value !== 'string')
|
|
43
61
|
return null;
|
|
44
62
|
var formatter = (_b = formatters[type]) !== null && _b !== void 0 ? _b : (function (s) { return s; });
|
|
45
|
-
return formatter(value);
|
|
63
|
+
return React.createElement(React.Fragment, null, formatter(value));
|
|
46
64
|
};
|
|
47
65
|
StringFormatter.displayName = 'StringFormatter';
|
|
48
66
|
export { StringFormatter };
|
|
@@ -46,9 +46,105 @@ interface IParticlesLayerProps {
|
|
|
46
46
|
* @default 99
|
|
47
47
|
*/
|
|
48
48
|
maxZoom?: number;
|
|
49
|
+
/**
|
|
50
|
+
* Minimum visible velocity magnitude for particle trails.
|
|
51
|
+
* Velocities are linearly remapped to [minSpeed, maxSpeed] range.
|
|
52
|
+
* @default 0.02
|
|
53
|
+
*/
|
|
54
|
+
minSpeed?: number;
|
|
55
|
+
/**
|
|
56
|
+
* Maximum visible velocity magnitude for particle trails.
|
|
57
|
+
* Velocities are linearly remapped to [minSpeed, maxSpeed] range.
|
|
58
|
+
* @default 0.15
|
|
59
|
+
*/
|
|
60
|
+
maxSpeed?: number;
|
|
49
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* A WebGL-based Mapbox GL custom layer that visualizes vector flow fields (such as
|
|
64
|
+
* ocean currents or wind patterns) using animated particle trails.
|
|
65
|
+
*
|
|
66
|
+
* ## Overview
|
|
67
|
+
*
|
|
68
|
+
* The component takes an array of data points, each with a geographic position and
|
|
69
|
+
* a velocity vector (U and V components). It renders animated particles that flow
|
|
70
|
+
* along these velocity vectors, creating a visual representation of the flow field.
|
|
71
|
+
*
|
|
72
|
+
* Particle trails are colored using a configurable gradient, where the head of the
|
|
73
|
+
* trail is typically bright and the tail fades out. The animation continuously cycles
|
|
74
|
+
* particles through their trails, creating a flowing effect.
|
|
75
|
+
*
|
|
76
|
+
* ## Data Format
|
|
77
|
+
*
|
|
78
|
+
* Input data should be an array of points with the following structure:
|
|
79
|
+
*
|
|
80
|
+
* ```typescript
|
|
81
|
+
* interface IPoint {
|
|
82
|
+
* latitude: number; // Geographic latitude
|
|
83
|
+
* longitude: number; // Geographic longitude
|
|
84
|
+
* u: number; // Velocity component in X direction
|
|
85
|
+
* v: number; // Velocity component in Y direction
|
|
86
|
+
* }
|
|
87
|
+
* ```
|
|
88
|
+
*
|
|
89
|
+
* The component uses Delaunay triangulation (via Turf.js) to interpolate velocity
|
|
90
|
+
* values between data points, so the data does not need to be on a regular grid.
|
|
91
|
+
*
|
|
92
|
+
* ## Velocity Remapping
|
|
93
|
+
*
|
|
94
|
+
* To ensure all particles have visible trails regardless of the actual velocity
|
|
95
|
+
* magnitudes in the data, velocities are linearly remapped to a visible range:
|
|
96
|
+
*
|
|
97
|
+
* - The `minSpeed` prop sets the minimum trail speed (for the slowest currents)
|
|
98
|
+
* - The `maxSpeed` prop sets the maximum trail speed (for the fastest currents)
|
|
99
|
+
* - All velocities are scaled proportionally within this range
|
|
100
|
+
* - The relative differences between fast and slow currents are preserved
|
|
101
|
+
* - Particles with exactly zero velocity are not rendered
|
|
102
|
+
*
|
|
103
|
+
* ## Performance
|
|
104
|
+
*
|
|
105
|
+
* The component is optimized for performance using several GPU-based techniques:
|
|
106
|
+
*
|
|
107
|
+
* 1. **UV Texture Caching**: The velocity field is rendered to a texture only when
|
|
108
|
+
* the map view changes (pan/zoom), not on every animation frame.
|
|
109
|
+
*
|
|
110
|
+
* 2. **Combined UV Texture**: U and V components are stored in a single texture
|
|
111
|
+
* (U in red channel, V in green channel) to reduce texture fetches.
|
|
112
|
+
*
|
|
113
|
+
* 3. **Position Pre-computation**: Particle positions are pre-computed in a GPU
|
|
114
|
+
* shader pass when the view changes, eliminating expensive per-vertex loops
|
|
115
|
+
* during animation.
|
|
116
|
+
*
|
|
117
|
+
* With default settings (`density=40`, `particles=80`), the component renders
|
|
118
|
+
* 128,000 particles efficiently.
|
|
119
|
+
*
|
|
120
|
+
* ## Example
|
|
121
|
+
*
|
|
122
|
+
* ```tsx
|
|
123
|
+
* <Map>
|
|
124
|
+
* <ParticlesLayer
|
|
125
|
+
* data={oceanCurrentData}
|
|
126
|
+
* density={50}
|
|
127
|
+
* particles={100}
|
|
128
|
+
* gradientStops={[
|
|
129
|
+
* { pos: 0.0, color: '#00ffffff' },
|
|
130
|
+
* { pos: 0.5, color: '#0088ffff' },
|
|
131
|
+
* { pos: 1.0, color: '#0088ff00' },
|
|
132
|
+
* ]}
|
|
133
|
+
* minSpeed={0.01}
|
|
134
|
+
* maxSpeed={0.2}
|
|
135
|
+
* />
|
|
136
|
+
* </Map>
|
|
137
|
+
* ```
|
|
138
|
+
*
|
|
139
|
+
* ## Notes
|
|
140
|
+
*
|
|
141
|
+
* - The component requires a Mapbox GL map context (via `react-map-gl`)
|
|
142
|
+
* - WebGL 1.0 is used for maximum browser compatibility (no extensions required)
|
|
143
|
+
* - The layer renders before the "overlay" layer in the Mapbox style
|
|
144
|
+
* - Changing props triggers a full re-initialization of the WebGL resources
|
|
145
|
+
*/
|
|
50
146
|
declare const ParticlesLayer: {
|
|
51
|
-
({ particles, density, delay, gradientStops, minZoom, maxZoom, ...props }: IParticlesLayerProps): React.JSX.Element;
|
|
147
|
+
({ particles, density, delay, gradientStops, minZoom, maxZoom, minSpeed, maxSpeed, ...props }: IParticlesLayerProps): React.JSX.Element;
|
|
52
148
|
displayName: string;
|
|
53
149
|
};
|
|
54
150
|
export { ParticlesLayer, IParticlesLayerProps };
|
|
@@ -38,6 +38,8 @@ import { UVVertexShader } from './UVVertexShader';
|
|
|
38
38
|
import { UVFragmentShader } from './UVFragmentShader';
|
|
39
39
|
import { ParticlesFragmentShader } from './ParticlesFragmentShader';
|
|
40
40
|
import { ParticlesVertexShader } from './ParticlesVertexShader';
|
|
41
|
+
import { PositionComputeVertexShader } from './PositionComputeVertexShader';
|
|
42
|
+
import { PositionComputeFragmentShader } from './PositionComputeFragmentShader';
|
|
41
43
|
import { RgbColor } from '../../../helper/RgbColor';
|
|
42
44
|
var TEXTURE_WIDTH = 1024;
|
|
43
45
|
var TEXTURE_HEIGHT = 1024;
|
|
@@ -54,14 +56,23 @@ var ParticlesLayerBase = function (props) {
|
|
|
54
56
|
var maxU = React.useRef(0);
|
|
55
57
|
var minV = React.useRef(0);
|
|
56
58
|
var maxV = React.useRef(0);
|
|
57
|
-
|
|
58
|
-
var
|
|
59
|
-
var
|
|
60
|
-
|
|
59
|
+
// Combined UV texture: U in R channel, V in G channel, alpha indicates valid data
|
|
60
|
+
var textureUV = React.useRef(null);
|
|
61
|
+
var fboUV = React.useRef(null);
|
|
62
|
+
// Position texture: stores pre-computed particle positions (X in RG, Y in BA)
|
|
63
|
+
var positionProgram = React.useRef(null);
|
|
64
|
+
var texturePosition = React.useRef(null);
|
|
65
|
+
var fboPosition = React.useRef(null);
|
|
66
|
+
var bufferQuad = React.useRef(null);
|
|
67
|
+
// Dynamic position texture dimensions (computed based on density * density * particles)
|
|
68
|
+
var posTexWidth = React.useRef(0);
|
|
69
|
+
var posTexHeight = React.useRef(0);
|
|
61
70
|
var frame = React.useRef(0);
|
|
62
71
|
var skip = React.useRef(0);
|
|
63
72
|
// requestAnimationFrame ID:
|
|
64
73
|
var animation = React.useRef(null);
|
|
74
|
+
// Last matrix used to render UV textures (for caching):
|
|
75
|
+
var lastMatrix = React.useRef(null);
|
|
65
76
|
// Current time is used to force component rerender when props change:
|
|
66
77
|
var _a = React.useState(0), time = _a[0], setTime = _a[1];
|
|
67
78
|
// Force component rerender when props change:
|
|
@@ -70,11 +81,12 @@ var ParticlesLayerBase = function (props) {
|
|
|
70
81
|
setTime(Date.now());
|
|
71
82
|
frame.current = 0;
|
|
72
83
|
skip.current = 0;
|
|
84
|
+
lastMatrix.current = null; // Reset matrix cache so UV textures are re-rendered
|
|
73
85
|
animation.current = window.requestAnimationFrame(draw);
|
|
74
|
-
}, [props.data, props.gradientStops, props.particles, props.density, props.delay, props.pointSize]);
|
|
86
|
+
}, [props.data, props.gradientStops, props.particles, props.density, props.delay, props.pointSize, props.minSpeed, props.maxSpeed]);
|
|
75
87
|
var draw = function () {
|
|
76
88
|
skip.current++;
|
|
77
|
-
if (skip.current
|
|
89
|
+
if (skip.current > props.delay) {
|
|
78
90
|
skip.current = 0;
|
|
79
91
|
frame.current = (frame.current + 1) % props.particles;
|
|
80
92
|
map.current.triggerRepaint();
|
|
@@ -169,6 +181,53 @@ var ParticlesLayerBase = function (props) {
|
|
|
169
181
|
gl.attachShader(particlesProgram.current, fragmentShader);
|
|
170
182
|
gl.linkProgram(particlesProgram.current);
|
|
171
183
|
};
|
|
184
|
+
var createPositionComputeProgram = function (gl) {
|
|
185
|
+
var vertexShader = gl.createShader(gl.VERTEX_SHADER);
|
|
186
|
+
gl.shaderSource(vertexShader, PositionComputeVertexShader);
|
|
187
|
+
gl.compileShader(vertexShader);
|
|
188
|
+
var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
|
|
189
|
+
gl.shaderSource(fragmentShader, PositionComputeFragmentShader);
|
|
190
|
+
gl.compileShader(fragmentShader);
|
|
191
|
+
positionProgram.current = gl.createProgram();
|
|
192
|
+
gl.attachShader(positionProgram.current, vertexShader);
|
|
193
|
+
gl.attachShader(positionProgram.current, fragmentShader);
|
|
194
|
+
gl.linkProgram(positionProgram.current);
|
|
195
|
+
};
|
|
196
|
+
var createPositionTexture = function (gl) {
|
|
197
|
+
// Compute texture dimensions to fit all particles
|
|
198
|
+
// Total particles = density * density * particles
|
|
199
|
+
var totalParticles = props.density * props.density * props.particles;
|
|
200
|
+
// Find suitable texture dimensions (width should be power of 2 for compatibility)
|
|
201
|
+
// Use a width that balances the texture shape
|
|
202
|
+
var width = 256;
|
|
203
|
+
while (width * width < totalParticles) {
|
|
204
|
+
width *= 2;
|
|
205
|
+
}
|
|
206
|
+
var height = Math.ceil(totalParticles / width);
|
|
207
|
+
posTexWidth.current = width;
|
|
208
|
+
posTexHeight.current = height;
|
|
209
|
+
var texture = gl.createTexture();
|
|
210
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
211
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
|
212
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
213
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
214
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
215
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
216
|
+
return texture;
|
|
217
|
+
};
|
|
218
|
+
var createQuadBuffer = function (gl) {
|
|
219
|
+
// Full-screen quad (two triangles covering clip space)
|
|
220
|
+
bufferQuad.current = gl.createBuffer();
|
|
221
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, bufferQuad.current);
|
|
222
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
|
|
223
|
+
-1, -1,
|
|
224
|
+
1, -1,
|
|
225
|
+
-1, 1,
|
|
226
|
+
-1, 1,
|
|
227
|
+
1, -1,
|
|
228
|
+
1, 1
|
|
229
|
+
]), gl.STATIC_DRAW);
|
|
230
|
+
};
|
|
172
231
|
var createBufferU = function (gl) {
|
|
173
232
|
var min = getGeometryU[0], max = getGeometryU[1], geometry = getGeometryU[2];
|
|
174
233
|
minU.current = min;
|
|
@@ -240,16 +299,20 @@ var ParticlesLayerBase = function (props) {
|
|
|
240
299
|
createBufferU(gl);
|
|
241
300
|
createBufferV(gl);
|
|
242
301
|
createBufferScreen(gl);
|
|
302
|
+
createQuadBuffer(gl);
|
|
243
303
|
createUVProgram(gl);
|
|
244
304
|
createParticlesProgram(gl);
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
305
|
+
createPositionComputeProgram(gl);
|
|
306
|
+
// Single combined texture for both U and V (U in R, V in G)
|
|
307
|
+
textureUV.current = createTexture(gl);
|
|
308
|
+
fboUV.current = createFrameBuffer(gl, textureUV.current);
|
|
309
|
+
// Position texture for pre-computed particle positions
|
|
310
|
+
texturePosition.current = createPositionTexture(gl);
|
|
311
|
+
fboPosition.current = createFrameBuffer(gl, texturePosition.current);
|
|
249
312
|
};
|
|
250
313
|
var renderSpeedBuffer = function (gl, matrix, buffer, numTriangles) {
|
|
251
314
|
gl.useProgram(uvProgram.current);
|
|
252
|
-
// Pass a matrix uniform in:
|
|
315
|
+
// Pass a matrix uniform in:
|
|
253
316
|
gl.uniformMatrix4fv(gl.getUniformLocation(uvProgram.current, 'u_matrix'), false, matrix);
|
|
254
317
|
// Vertices:
|
|
255
318
|
var aPos = gl.getAttribLocation(uvProgram.current, 'a_pos');
|
|
@@ -263,6 +326,43 @@ var ParticlesLayerBase = function (props) {
|
|
|
263
326
|
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
264
327
|
gl.drawArrays(gl.TRIANGLES, 0, numTriangles * 3);
|
|
265
328
|
};
|
|
329
|
+
//
|
|
330
|
+
// Render particle positions to the position texture.
|
|
331
|
+
// This moves the expensive trace loop from the particle vertex shader
|
|
332
|
+
// to a one-time-per-view-change fragment shader pass.
|
|
333
|
+
//
|
|
334
|
+
var renderPositionTexture = function (gl) {
|
|
335
|
+
gl.useProgram(positionProgram.current);
|
|
336
|
+
// Bind position framebuffer
|
|
337
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, fboPosition.current);
|
|
338
|
+
gl.viewport(0, 0, posTexWidth.current, posTexHeight.current);
|
|
339
|
+
gl.clearColor(0, 0, 0, 0);
|
|
340
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
341
|
+
// Disable blending for position computation
|
|
342
|
+
gl.disable(gl.BLEND);
|
|
343
|
+
// UV texture (already rendered)
|
|
344
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
345
|
+
gl.bindTexture(gl.TEXTURE_2D, textureUV.current);
|
|
346
|
+
gl.uniform1i(gl.getUniformLocation(positionProgram.current, 'uvTexture'), 0);
|
|
347
|
+
// UV range uniforms
|
|
348
|
+
gl.uniform1f(gl.getUniformLocation(positionProgram.current, 'minU'), minU.current);
|
|
349
|
+
gl.uniform1f(gl.getUniformLocation(positionProgram.current, 'maxU'), maxU.current);
|
|
350
|
+
gl.uniform1f(gl.getUniformLocation(positionProgram.current, 'minV'), minV.current);
|
|
351
|
+
gl.uniform1f(gl.getUniformLocation(positionProgram.current, 'maxV'), maxV.current);
|
|
352
|
+
// Grid parameters
|
|
353
|
+
gl.uniform1f(gl.getUniformLocation(positionProgram.current, 'u_density'), props.density);
|
|
354
|
+
gl.uniform1f(gl.getUniformLocation(positionProgram.current, 'u_particles'), props.particles);
|
|
355
|
+
gl.uniform1f(gl.getUniformLocation(positionProgram.current, 'u_texWidth'), posTexWidth.current);
|
|
356
|
+
// Velocity magnitude remapping parameters
|
|
357
|
+
gl.uniform1f(gl.getUniformLocation(positionProgram.current, 'u_minVisMag'), props.minSpeed);
|
|
358
|
+
gl.uniform1f(gl.getUniformLocation(positionProgram.current, 'u_maxVisMag'), props.maxSpeed);
|
|
359
|
+
// Render full-screen quad
|
|
360
|
+
var aPos = gl.getAttribLocation(positionProgram.current, 'a_pos');
|
|
361
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, bufferQuad.current);
|
|
362
|
+
gl.enableVertexAttribArray(aPos);
|
|
363
|
+
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
|
|
364
|
+
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
365
|
+
};
|
|
266
366
|
var renderParticles = function (gl, matrix) {
|
|
267
367
|
gl.useProgram(particlesProgram.current);
|
|
268
368
|
// Matrix:
|
|
@@ -281,19 +381,14 @@ var ParticlesLayerBase = function (props) {
|
|
|
281
381
|
gl.uniform1i(gl.getUniformLocation(particlesProgram.current, "u_num_instances"), props.particles);
|
|
282
382
|
// Size of points to be drawn (in pixels).
|
|
283
383
|
gl.uniform1f(gl.getUniformLocation(particlesProgram.current, "u_point_size"), props.pointSize);
|
|
284
|
-
//
|
|
285
|
-
gl.uniform1f(gl.getUniformLocation(particlesProgram.current, "minU"), minU.current);
|
|
286
|
-
gl.uniform1f(gl.getUniformLocation(particlesProgram.current, "maxU"), maxU.current);
|
|
287
|
-
gl.uniform1f(gl.getUniformLocation(particlesProgram.current, "minV"), minV.current);
|
|
288
|
-
gl.uniform1f(gl.getUniformLocation(particlesProgram.current, "maxV"), maxV.current);
|
|
289
|
-
// u-texture:
|
|
384
|
+
// Position texture (pre-computed particle positions):
|
|
290
385
|
gl.activeTexture(gl.TEXTURE0);
|
|
291
|
-
gl.bindTexture(gl.TEXTURE_2D,
|
|
292
|
-
gl.uniform1i(gl.getUniformLocation(particlesProgram.current, "
|
|
293
|
-
//
|
|
294
|
-
gl.
|
|
295
|
-
gl.
|
|
296
|
-
gl.
|
|
386
|
+
gl.bindTexture(gl.TEXTURE_2D, texturePosition.current);
|
|
387
|
+
gl.uniform1i(gl.getUniformLocation(particlesProgram.current, "positionTexture"), 0);
|
|
388
|
+
// Position texture dimensions and grid parameters for coordinate lookup:
|
|
389
|
+
gl.uniform1f(gl.getUniformLocation(particlesProgram.current, "u_posTexWidth"), posTexWidth.current);
|
|
390
|
+
gl.uniform1f(gl.getUniformLocation(particlesProgram.current, "u_posTexHeight"), posTexHeight.current);
|
|
391
|
+
gl.uniform1f(gl.getUniformLocation(particlesProgram.current, "u_density"), props.density);
|
|
297
392
|
// Vertices:
|
|
298
393
|
var aPos = gl.getAttribLocation(particlesProgram.current, 'a_pos'); // on-screen position [-1..1]
|
|
299
394
|
var aIdx = gl.getAttribLocation(particlesProgram.current, 'a_idx'); // particle index
|
|
@@ -309,24 +404,46 @@ var ParticlesLayerBase = function (props) {
|
|
|
309
404
|
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
310
405
|
gl.drawArrays(gl.POINTS, 0, props.density * props.density * props.particles);
|
|
311
406
|
};
|
|
407
|
+
//
|
|
408
|
+
// Check if two matrices are equal.
|
|
409
|
+
//
|
|
410
|
+
var matricesEqual = function (a, b) {
|
|
411
|
+
if (!a || !b)
|
|
412
|
+
return false;
|
|
413
|
+
for (var i = 0; i < 16; i++) {
|
|
414
|
+
if (a[i] !== b[i])
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
return true;
|
|
418
|
+
};
|
|
312
419
|
var onRender = function (gl, matrix, zoom, width, height) {
|
|
313
420
|
if (zoom < props.minZoom)
|
|
314
421
|
return;
|
|
315
422
|
if (zoom > props.maxZoom)
|
|
316
423
|
return;
|
|
317
|
-
|
|
318
|
-
//
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
424
|
+
// Only re-render UV textures when the matrix changes (i.e., when the map
|
|
425
|
+
// view changes due to panning or zooming). This is a significant performance
|
|
426
|
+
// optimization since UV texture rendering involves expensive draw calls.
|
|
427
|
+
var matrixChanged = !matricesEqual(matrix, lastMatrix.current);
|
|
428
|
+
if (matrixChanged) {
|
|
429
|
+
lastMatrix.current = __spreadArray([], matrix, true);
|
|
430
|
+
gl.useProgram(uvProgram.current);
|
|
431
|
+
// Render to combined UV texture (U in R channel, V in G channel)
|
|
432
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, fboUV.current);
|
|
433
|
+
gl.viewport(0, 0, TEXTURE_WIDTH, TEXTURE_HEIGHT);
|
|
434
|
+
gl.clearColor(0, 0, 0, 0); // clear to transparent
|
|
435
|
+
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
|
436
|
+
// Render U to R channel and A channel (alpha indicates valid data)
|
|
437
|
+
gl.colorMask(true, false, false, true);
|
|
438
|
+
renderSpeedBuffer(gl, matrix, bufferU.current, numTrianglesU.current);
|
|
439
|
+
// Render V to G channel only (preserve R and A from U pass)
|
|
440
|
+
gl.colorMask(false, true, false, false);
|
|
441
|
+
renderSpeedBuffer(gl, matrix, bufferV.current, numTrianglesV.current);
|
|
442
|
+
// Reset color mask to default
|
|
443
|
+
gl.colorMask(true, true, true, true);
|
|
444
|
+
// Compute particle positions (moves expensive loop from vertex shader)
|
|
445
|
+
renderPositionTexture(gl);
|
|
446
|
+
}
|
|
330
447
|
// Unbind framebuffer so we're rendering to the screen:
|
|
331
448
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
332
449
|
// Reset the viewport size to equal the map size:
|
|
@@ -336,14 +453,98 @@ var ParticlesLayerBase = function (props) {
|
|
|
336
453
|
};
|
|
337
454
|
return (React.createElement(Layer, { id: null, type: "custom", beforeId: "overlay", key: time, onAdd: onAddLayer, render: function (gl, matrix) { return onRender(gl, matrix, map.current.getZoom(), map.current.getContainer().clientWidth, map.current.getContainer().clientHeight); } }));
|
|
338
455
|
};
|
|
456
|
+
/**
|
|
457
|
+
* A WebGL-based Mapbox GL custom layer that visualizes vector flow fields (such as
|
|
458
|
+
* ocean currents or wind patterns) using animated particle trails.
|
|
459
|
+
*
|
|
460
|
+
* ## Overview
|
|
461
|
+
*
|
|
462
|
+
* The component takes an array of data points, each with a geographic position and
|
|
463
|
+
* a velocity vector (U and V components). It renders animated particles that flow
|
|
464
|
+
* along these velocity vectors, creating a visual representation of the flow field.
|
|
465
|
+
*
|
|
466
|
+
* Particle trails are colored using a configurable gradient, where the head of the
|
|
467
|
+
* trail is typically bright and the tail fades out. The animation continuously cycles
|
|
468
|
+
* particles through their trails, creating a flowing effect.
|
|
469
|
+
*
|
|
470
|
+
* ## Data Format
|
|
471
|
+
*
|
|
472
|
+
* Input data should be an array of points with the following structure:
|
|
473
|
+
*
|
|
474
|
+
* ```typescript
|
|
475
|
+
* interface IPoint {
|
|
476
|
+
* latitude: number; // Geographic latitude
|
|
477
|
+
* longitude: number; // Geographic longitude
|
|
478
|
+
* u: number; // Velocity component in X direction
|
|
479
|
+
* v: number; // Velocity component in Y direction
|
|
480
|
+
* }
|
|
481
|
+
* ```
|
|
482
|
+
*
|
|
483
|
+
* The component uses Delaunay triangulation (via Turf.js) to interpolate velocity
|
|
484
|
+
* values between data points, so the data does not need to be on a regular grid.
|
|
485
|
+
*
|
|
486
|
+
* ## Velocity Remapping
|
|
487
|
+
*
|
|
488
|
+
* To ensure all particles have visible trails regardless of the actual velocity
|
|
489
|
+
* magnitudes in the data, velocities are linearly remapped to a visible range:
|
|
490
|
+
*
|
|
491
|
+
* - The `minSpeed` prop sets the minimum trail speed (for the slowest currents)
|
|
492
|
+
* - The `maxSpeed` prop sets the maximum trail speed (for the fastest currents)
|
|
493
|
+
* - All velocities are scaled proportionally within this range
|
|
494
|
+
* - The relative differences between fast and slow currents are preserved
|
|
495
|
+
* - Particles with exactly zero velocity are not rendered
|
|
496
|
+
*
|
|
497
|
+
* ## Performance
|
|
498
|
+
*
|
|
499
|
+
* The component is optimized for performance using several GPU-based techniques:
|
|
500
|
+
*
|
|
501
|
+
* 1. **UV Texture Caching**: The velocity field is rendered to a texture only when
|
|
502
|
+
* the map view changes (pan/zoom), not on every animation frame.
|
|
503
|
+
*
|
|
504
|
+
* 2. **Combined UV Texture**: U and V components are stored in a single texture
|
|
505
|
+
* (U in red channel, V in green channel) to reduce texture fetches.
|
|
506
|
+
*
|
|
507
|
+
* 3. **Position Pre-computation**: Particle positions are pre-computed in a GPU
|
|
508
|
+
* shader pass when the view changes, eliminating expensive per-vertex loops
|
|
509
|
+
* during animation.
|
|
510
|
+
*
|
|
511
|
+
* With default settings (`density=40`, `particles=80`), the component renders
|
|
512
|
+
* 128,000 particles efficiently.
|
|
513
|
+
*
|
|
514
|
+
* ## Example
|
|
515
|
+
*
|
|
516
|
+
* ```tsx
|
|
517
|
+
* <Map>
|
|
518
|
+
* <ParticlesLayer
|
|
519
|
+
* data={oceanCurrentData}
|
|
520
|
+
* density={50}
|
|
521
|
+
* particles={100}
|
|
522
|
+
* gradientStops={[
|
|
523
|
+
* { pos: 0.0, color: '#00ffffff' },
|
|
524
|
+
* { pos: 0.5, color: '#0088ffff' },
|
|
525
|
+
* { pos: 1.0, color: '#0088ff00' },
|
|
526
|
+
* ]}
|
|
527
|
+
* minSpeed={0.01}
|
|
528
|
+
* maxSpeed={0.2}
|
|
529
|
+
* />
|
|
530
|
+
* </Map>
|
|
531
|
+
* ```
|
|
532
|
+
*
|
|
533
|
+
* ## Notes
|
|
534
|
+
*
|
|
535
|
+
* - The component requires a Mapbox GL map context (via `react-map-gl`)
|
|
536
|
+
* - WebGL 1.0 is used for maximum browser compatibility (no extensions required)
|
|
537
|
+
* - The layer renders before the "overlay" layer in the Mapbox style
|
|
538
|
+
* - Changing props triggers a full re-initialization of the WebGL resources
|
|
539
|
+
*/
|
|
339
540
|
var ParticlesLayer = function (_a) {
|
|
340
541
|
var _b = _a.particles, particles = _b === void 0 ? 80 : _b, _c = _a.density, density = _c === void 0 ? 40 : _c, _d = _a.delay, delay = _d === void 0 ? 0 : _d, _e = _a.gradientStops, gradientStops = _e === void 0 ? [
|
|
341
542
|
{ pos: 0.0, color: '#ffffffff' },
|
|
342
543
|
{ pos: 0.25, color: '#5c5cffff' },
|
|
343
544
|
{ pos: 0.5, color: '#5c5cff00' },
|
|
344
545
|
{ pos: 1.0, color: '#5c5cff00' }, // transparent blue
|
|
345
|
-
] : _e, _f = _a.minZoom, minZoom = _f === void 0 ? 0 : _f, _g = _a.maxZoom, maxZoom = _g === void 0 ? 99 : _g, props = __rest(_a, ["particles", "density", "delay", "gradientStops", "minZoom", "maxZoom"]);
|
|
346
|
-
return React.createElement(ParticlesLayerBase, __assign({ particles: particles, density: density, gradientStops: gradientStops, delay: delay, minZoom: minZoom, maxZoom: maxZoom }, props));
|
|
546
|
+
] : _e, _f = _a.minZoom, minZoom = _f === void 0 ? 0 : _f, _g = _a.maxZoom, maxZoom = _g === void 0 ? 99 : _g, _h = _a.minSpeed, minSpeed = _h === void 0 ? 0.02 : _h, _j = _a.maxSpeed, maxSpeed = _j === void 0 ? 0.15 : _j, props = __rest(_a, ["particles", "density", "delay", "gradientStops", "minZoom", "maxZoom", "minSpeed", "maxSpeed"]);
|
|
547
|
+
return React.createElement(ParticlesLayerBase, __assign({ particles: particles, density: density, gradientStops: gradientStops, delay: delay, minZoom: minZoom, maxZoom: maxZoom, minSpeed: minSpeed, maxSpeed: maxSpeed }, props));
|
|
347
548
|
};
|
|
348
549
|
ParticlesLayer.displayName = 'ParticlesLayer';
|
|
349
550
|
export { ParticlesLayer };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const ParticlesVertexShader = "\n
|
|
1
|
+
declare const ParticlesVertexShader = "\n attribute vec2 a_pos;\n attribute float a_idx;\n attribute float a_offset;\n\n uniform float u_point_size;\n uniform int u_num_instances;\n uniform int u_frame;\n uniform sampler2D positionTexture; // Pre-computed positions (X in RG, Y in BA)\n uniform float u_posTexWidth;\n uniform float u_posTexHeight;\n uniform float u_density;\n uniform float u_gradientStops[12];\n uniform vec4 u_gradientColors[12];\n\n // Output color, calculated by this vertex shader:\n varying vec4 v_color;\n\n //\n // Given a value, find its corresponding color in the gradient stops.\n //\n vec4 getGradientColor(float position) {\n // Find left and right stop:\n float left = -1.0;\n float right = 99.0;\n vec4 leftColor;\n vec4 rightColor;\n\n for(int i = 0; i < 12; i++) {\n if (u_gradientStops[i] <= position) {\n left = u_gradientStops[i];\n leftColor = u_gradientColors[i];\n } else break;\n }\n\n for(int i = 12 - 1; i >= 0; i--) {\n if (u_gradientStops[i] >= position) {\n right = u_gradientStops[i];\n rightColor = u_gradientColors[i];\n } else break;\n }\n\n // Distance between stops:\n float width = right - left;\n // Distance from left stop:\n float dist = position - left;\n // Right stop weight:\n float weight;\n if(dist == 0.0) {\n weight = dist;\n } else {\n weight = dist / width;\n }\n\n float r = leftColor.x + (rightColor.x - leftColor.x) * weight;\n float g = leftColor.y + (rightColor.y - leftColor.y) * weight;\n float b = leftColor.z + (rightColor.z - leftColor.z) * weight;\n float a = leftColor.w + (rightColor.w - leftColor.w) * weight;\n return vec4(r, g, b, a);\n }\n\n // Decode a 16-bit encoded position from two bytes (high, low as 0-1 floats)\n float decodePosition(float hi, float lo) {\n float encoded = hi * 255.0 * 256.0 + lo * 255.0;\n return encoded / 65535.0 * 2.0 - 1.0;\n }\n\n void main() {\n int idx = int(a_idx);\n int offset = int(a_offset);\n int frame = u_frame + offset;\n if(frame >= u_num_instances) frame -= u_num_instances;\n\n // Determine color from frame.\n int start = idx - frame;\n if(start < 0) start += u_num_instances;\n v_color = getGradientColor(float(u_num_instances - start) / float(u_num_instances));\n\n // Compute texture coordinates for this particle's pre-computed position.\n // WebGL 1 doesn't have gl_VertexID, so we compute the particle index from\n // vertex attributes. The vertex buffer layout is:\n // for y in [0, density): for x in [0, density): for i in [0, particles): (pos, idx, offset)\n\n // Convert grid position back to grid index\n float gridX = (a_pos.x + 0.99) * (u_density - 1.0) / 1.98;\n float gridY = (a_pos.y + 0.99) * (u_density - 1.0) / 1.98;\n float gridIndex = floor(gridY + 0.5) * u_density + floor(gridX + 0.5);\n float particleIdx = gridIndex * float(u_num_instances) + a_idx;\n\n // Convert particle index to texture coordinates\n float texX = mod(particleIdx, u_posTexWidth);\n float texY = floor(particleIdx / u_posTexWidth);\n\n // Sample position texture (add 0.5 to sample pixel center)\n vec2 texCoord = vec2((texX + 0.5) / u_posTexWidth, (texY + 0.5) / u_posTexHeight);\n vec4 encodedPos = texture2D(positionTexture, texCoord);\n\n // Decode position (X in RG, Y in BA)\n float posX = decodePosition(encodedPos.r, encodedPos.g);\n float posY = decodePosition(encodedPos.b, encodedPos.a);\n\n // Check for off-screen marker (position > 1.5 means invalid)\n if(posX > 1.5) return;\n\n gl_Position = vec4(posX, posY, 0.0, 1.0);\n gl_PointSize = u_point_size;\n }\n";
|
|
2
2
|
export { ParticlesVertexShader };
|