├── .gitattributes
├── examples
├── css.d.ts
├── rubber_duck.png
├── maplibre.css
├── maplibre_basic.css
├── vortex.css
├── maplibre_basic.html
├── maplibre_basic_sprites.html
├── vortex.html
├── vortex_sprites.html
├── vortex.ts
├── maplibre.html
├── vortex-sprites.ts
├── maplibre_basic.ts
├── maplibre_basic-sprites.ts
├── wave.svg
├── maplibre.ts
├── vortex-base.ts
├── wms.ts
└── options.ts
├── .gitignore
├── tests
├── assets
│ ├── glsl.d.ts
│ ├── example.vert.glsl
│ └── example.frag.glsl
├── utils.ts
├── shader.test.ts
└── shader-program.test.ts
├── src
├── shaders
│ ├── is_missing_velocity.glsl
│ ├── placeholder.frag.glsl
│ ├── texture.vert.glsl
│ ├── render.frag.glsl
│ ├── texture.frag.glsl
│ ├── final.vert.glsl
│ ├── final.frag.glsl
│ ├── render.vert.glsl
│ └── particle.vert.glsl
├── render
│ ├── index.ts
│ ├── texture.ts
│ ├── final.ts
│ ├── particles.ts
│ └── propagator.ts
├── index.ts
├── utils
│ ├── shader.ts
│ ├── geometry.ts
│ ├── speedcurve.ts
│ ├── textures.ts
│ ├── colormap.ts
│ ├── wms.ts
│ └── shader-program.ts
├── layer.ts
└── visualiser.ts
├── .prettierrc
├── .github
└── workflows
│ ├── build-lint.yml
│ ├── test.yml
│ ├── npm-publish.yml
│ └── github-pages.yml
├── vitest.config.ts
├── eslint.config.js
├── vite-examples.config.ts
├── LICENSE
├── tsconfig.json
├── vite-library.config.ts
├── index.html
├── package.json
└── README.md
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
2 |
--------------------------------------------------------------------------------
/examples/css.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.css'
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode/
2 | dist/
3 | node_modules/
4 |
--------------------------------------------------------------------------------
/tests/assets/glsl.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.glsl?raw' {
2 | const value: string
3 | export default value
4 | }
5 |
--------------------------------------------------------------------------------
/examples/rubber_duck.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Deltares/webgl-streamline-visualizer/main/examples/rubber_duck.png
--------------------------------------------------------------------------------
/src/shaders/is_missing_velocity.glsl:
--------------------------------------------------------------------------------
1 | bool is_missing_velocity(vec4 raw) {
2 | // There is no velocity if r = g = 255.
3 | return raw.r == 1.0 && raw.g == 1.0;
4 | }
5 |
--------------------------------------------------------------------------------
/src/shaders/placeholder.frag.glsl:
--------------------------------------------------------------------------------
1 | #version 300 es
2 | precision highp float;
3 |
4 | out vec4 color;
5 |
6 | void main() {
7 | color = vec4(0.0, 0.0, 0.0, 0.0);
8 | }
9 |
--------------------------------------------------------------------------------
/src/render/index.ts:
--------------------------------------------------------------------------------
1 | export { ParticlePropagator } from './propagator'
2 | export { ParticleRenderer } from './particles'
3 | export { TextureRenderer } from './texture'
4 | export { FinalRenderer, StreamlineStyle } from './final'
5 |
--------------------------------------------------------------------------------
/src/shaders/texture.vert.glsl:
--------------------------------------------------------------------------------
1 | #version 300 es
2 | precision highp float;
3 |
4 | in vec4 a_position;
5 | in vec2 a_tex_coord;
6 |
7 | out vec2 v_tex_coord;
8 |
9 | void main() {
10 | v_tex_coord = a_tex_coord;
11 | gl_Position = a_position;
12 | }
13 |
--------------------------------------------------------------------------------
/src/shaders/render.frag.glsl:
--------------------------------------------------------------------------------
1 | #version 300 es
2 | precision highp float;
3 |
4 | uniform sampler2D u_particle_texture;
5 |
6 | in vec2 v_tex_coord;
7 |
8 | out vec4 color;
9 |
10 | void main() {
11 | color = texture(u_particle_texture, v_tex_coord);
12 | }
13 |
--------------------------------------------------------------------------------
/tests/utils.ts:
--------------------------------------------------------------------------------
1 | export function createWebGl2Context(): WebGL2RenderingContext {
2 | const canvas = new OffscreenCanvas(100, 100)
3 | const gl = canvas.getContext('webgl2')
4 | if (!gl) {
5 | throw new Error('Failed to create WebGL2 context.')
6 | }
7 | return gl
8 | }
9 |
--------------------------------------------------------------------------------
/tests/assets/example.vert.glsl:
--------------------------------------------------------------------------------
1 | #version 300 es
2 |
3 | uniform float u_factor;
4 |
5 | in vec4 a_position;
6 | in vec2 a_tex_coord;
7 |
8 | out vec2 v_tex_coord;
9 |
10 | void main() {
11 | v_tex_coord = u_factor * a_tex_coord;
12 | gl_Position = a_position;
13 | }
14 |
--------------------------------------------------------------------------------
/examples/maplibre.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: sans-serif;
4 | font-size: 16px;
5 | }
6 |
7 | main {
8 | width: 800px;
9 | margin: 40px auto;
10 | display: flex;
11 | flex-direction: column;
12 | row-gap: 20px;
13 | }
14 |
15 | div#container {
16 | width: 800px;
17 | height: 600px;
18 | }
--------------------------------------------------------------------------------
/examples/maplibre_basic.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: sans-serif;
4 | font-size: 16px;
5 | }
6 |
7 | main {
8 | width: 800px;
9 | margin: 40px auto;
10 | display: flex;
11 | flex-direction: column;
12 | row-gap: 20px;
13 | }
14 |
15 | div#container {
16 | width: 800px;
17 | height: 600px;
18 | }
--------------------------------------------------------------------------------
/src/shaders/texture.frag.glsl:
--------------------------------------------------------------------------------
1 | #version 300 es
2 | precision highp float;
3 |
4 | uniform sampler2D u_texture;
5 | uniform float u_fade_amount;
6 |
7 | in vec2 v_tex_coord;
8 |
9 | out vec4 color;
10 |
11 | void main() {
12 | color = texture(u_texture, v_tex_coord);
13 | color.a = max(color.a - u_fade_amount, 0.0);
14 | }
15 |
--------------------------------------------------------------------------------
/tests/assets/example.frag.glsl:
--------------------------------------------------------------------------------
1 | #version 300 es
2 |
3 | precision highp float;
4 |
5 | uniform sampler2D u_texture;
6 | uniform float u_fade_amount;
7 |
8 | in vec2 v_tex_coord;
9 |
10 | out vec4 color;
11 |
12 | void main() {
13 | color = texture(u_texture, v_tex_coord);
14 | color.a = max(color.a - u_fade_amount, 0.0);
15 | }
16 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 80,
3 | "tabWidth": 2,
4 | "semi": false,
5 | "singleQuote": true,
6 | "quoteProps": "as-needed",
7 | "trailingComma": "none",
8 | "bracketSpacing": true,
9 | "bracketSameLine": false,
10 | "arrowParens": "avoid",
11 | "vueIndentScriptAndStyle": false,
12 | "endOfLine": "lf",
13 | "singleAttributePerLine": true
14 | }
--------------------------------------------------------------------------------
/examples/vortex.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: sans-serif;
4 | font-size: 16px;
5 | }
6 |
7 | main {
8 | width: 800px;
9 | margin: 40px auto;
10 | }
11 |
12 | canvas {
13 | width: 800px;
14 | height: 600px;
15 | }
16 |
17 | div.container {
18 | width: 100%;
19 | display: flex;
20 | flex-direction: column;
21 | align-items: center;
22 | row-gap: 10px;
23 | }
--------------------------------------------------------------------------------
/examples/maplibre_basic.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Basic MapLibre demo
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/examples/maplibre_basic_sprites.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Basic MapLibre demo
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/.github/workflows/build-lint.yml:
--------------------------------------------------------------------------------
1 | name: Build and lint
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | build-lint:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v5
16 | - uses: actions/setup-node@v6
17 | with:
18 | node-version: 22
19 | cache: 'npm'
20 | - run: npm ci
21 | - run: npm run build
22 | - run: npm run lint
23 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export { WMSStreamlineLayer, type WMSStreamlineLayerOptions } from './layer'
2 | export { StreamlineStyle } from './render'
3 | export {
4 | StreamlineVisualiser,
5 | TrailParticleShape,
6 | type TrailParticleOptions,
7 | type StreamlineVisualiserOptions
8 | } from './visualiser'
9 | export {
10 | fetchWMSAvailableTimesAndElevations,
11 | fetchWMSColormap,
12 | fetchWMSVelocityField
13 | } from './utils/wms'
14 | export { type BoundingBoxScaling } from './render/final'
15 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Run tests
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | build-lint:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v5
16 | - uses: actions/setup-node@v6
17 | with:
18 | node-version: 22
19 | cache: 'npm'
20 | - run: npm ci
21 | - run: npx playwright install chromium
22 | - run: npm run test-once
23 |
--------------------------------------------------------------------------------
/examples/vortex.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vortex demo
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/examples/vortex_sprites.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vortex with sprites demo
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { resolve } from 'path'
2 | import { defineConfig } from 'vitest/config'
3 | import { playwright} from '@vitest/browser-playwright'
4 |
5 | export default defineConfig({
6 | resolve: {
7 | alias: {
8 | '@': resolve(__dirname, 'src')
9 | }
10 | },
11 | test: {
12 | browser: {
13 | enabled: true,
14 | screenshotFailures: false,
15 | provider: playwright(),
16 | instances: [ { browser: 'chromium' }],
17 | headless: true,
18 | api: 5174
19 | }
20 | }
21 | })
22 |
--------------------------------------------------------------------------------
/.github/workflows/npm-publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish to NPM
2 |
3 | permissions:
4 | id-token: write # Required for OIDC
5 | contents: read
6 |
7 | on:
8 | release:
9 | types: [published]
10 |
11 | jobs:
12 | publish:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v5
16 | - uses: actions/setup-node@v6
17 | with:
18 | node-version: 22
19 | - name: Update npm # 11.5.0 required for OIDC authentication
20 | run: npm install -g npm@latest
21 | - run: npm ci
22 | - run: npm run build
23 | - run: npm publish
24 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import eslint from '@eslint/js'
2 | import { defineConfig, globalIgnores } from 'eslint/config'
3 | import globals from 'globals'
4 | import tseslint from 'typescript-eslint'
5 | import eslintConfigPrettier from 'eslint-config-prettier/flat'
6 |
7 | export default defineConfig(
8 | globalIgnores(['coverage/', 'dist/', 'doc/']),
9 | eslint.configs.recommended,
10 | tseslint.configs.recommended,
11 | eslintConfigPrettier,
12 | {
13 | languageOptions: {
14 | globals: globals.browser,
15 | parserOptions: {
16 | projectService: { allowDefaultProject: ['*.js'] },
17 | },
18 | },
19 | },
20 | )
21 |
--------------------------------------------------------------------------------
/src/shaders/final.vert.glsl:
--------------------------------------------------------------------------------
1 | #version 300 es
2 | precision highp float;
3 |
4 | uniform vec2 u_bbox_scale;
5 | uniform vec2 u_bbox_offset;
6 |
7 | in vec4 a_position;
8 | in vec2 a_tex_coord;
9 |
10 | out vec2 v_tex_coord;
11 | out vec2 v_flipped_tex_coord;
12 |
13 | void main() {
14 | v_tex_coord = a_tex_coord;
15 | // Vertically flipped texture coordinate for velocity field data.
16 | v_flipped_tex_coord = vec2(
17 | a_tex_coord.x,
18 | 1.0 - a_tex_coord.y
19 | );
20 |
21 | gl_Position = a_position;
22 |
23 | // Scale bounding box.
24 | gl_Position.xy = gl_Position.xy * u_bbox_scale + u_bbox_offset;
25 | }
26 |
--------------------------------------------------------------------------------
/examples/vortex.ts:
--------------------------------------------------------------------------------
1 | import './vortex.css'
2 |
3 | import { initialiseControl, initialiseVisualiser } from './vortex-base'
4 | import { StreamlineStyle, type StreamlineVisualiserOptions } from '@/index'
5 |
6 | const numParticles = 10000
7 | const options: StreamlineVisualiserOptions = {
8 | style: StreamlineStyle.LightParticlesOnMagnitude,
9 | particleSize: 3,
10 | speedFactor: 0.4,
11 | fadeAmountPerSecond: 3,
12 | maxDisplacement: 1,
13 | maxAge: 2.0
14 | }
15 |
16 | initialiseVisualiser(numParticles, options)
17 | .then(visualiser => {
18 | initialiseControl(visualiser)
19 | })
20 | .catch(error => console.error(`Failed to initialise visualiser: ${error}`))
21 |
--------------------------------------------------------------------------------
/examples/maplibre.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 | MapLibre demo
10 |
11 |
12 |
13 |
14 |
15 |
16 |
20 |
21 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/tests/shader.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from 'vitest'
2 |
3 | import { VertexShader, FragmentShader } from '@/utils/shader'
4 |
5 | import { createWebGl2Context } from './utils'
6 |
7 | test('tests shader constructor', () => {
8 | const gl = createWebGl2Context()
9 |
10 | const vertexShader = new VertexShader(gl, 'void main() {}')
11 | expect(vertexShader).toBeDefined()
12 | expect(vertexShader.shader).toBeDefined()
13 |
14 | const fragmentShader = new FragmentShader(gl, 'void main() {}')
15 | expect(fragmentShader).toBeDefined()
16 | expect(fragmentShader.shader).toBeDefined()
17 |
18 | // Compiling should run without errors (errors are checked at link time).
19 | vertexShader.compile()
20 | fragmentShader.compile()
21 | })
22 |
--------------------------------------------------------------------------------
/examples/vortex-sprites.ts:
--------------------------------------------------------------------------------
1 | import './vortex.css'
2 | import spriteUrl from './wave.svg'
3 |
4 | import { initialiseControl, initialiseVisualiser } from './vortex-base'
5 | import { StreamlineStyle, type StreamlineVisualiserOptions } from '@/index'
6 |
7 | const numParticles = 500
8 | const options: StreamlineVisualiserOptions = {
9 | style: StreamlineStyle.LightParticlesOnMagnitude,
10 | particleSize: 24,
11 | speedFactor: 0.4,
12 | fadeAmountPerSecond: 3,
13 | maxDisplacement: 1,
14 | maxAge: 2,
15 | growthRate: 1,
16 | spriteUrl: new URL(spriteUrl, window.location.origin)
17 | }
18 |
19 | initialiseVisualiser(numParticles, options)
20 | .then(visualiser => {
21 | initialiseControl(visualiser)
22 | })
23 | .catch(error => console.error(`Failed to initialise visualiser: ${error}`))
24 |
--------------------------------------------------------------------------------
/vite-examples.config.ts:
--------------------------------------------------------------------------------
1 | import { resolve } from 'path'
2 | import { defineConfig } from 'vite'
3 | import viteGlslPlugin from 'vite-plugin-glsl'
4 |
5 | function resolveRelativePath(relative: string): string {
6 | return resolve(__dirname, relative)
7 | }
8 |
9 | export default defineConfig({
10 | base: '/webgl-streamline-visualizer/', // Base path for GitHub Pages
11 | plugins: [
12 | viteGlslPlugin({
13 | include: ['**/*.frag.glsl', '**/*.vert.glsl'],
14 | minify: true
15 | })
16 | ],
17 | build: {
18 | rollupOptions: {
19 | input: {
20 | index: resolveRelativePath('index.html'),
21 | vortex: resolveRelativePath('examples/vortex.html'),
22 | vortex_sprite: resolveRelativePath('examples/vortex_sprites.html'),
23 | maplibre: resolveRelativePath('examples/maplibre.html')
24 | }
25 | }
26 | },
27 | resolve: {
28 | alias: {
29 | '@': resolveRelativePath('./src')
30 | }
31 | }
32 | })
33 |
--------------------------------------------------------------------------------
/examples/maplibre_basic.ts:
--------------------------------------------------------------------------------
1 | import './maplibre_basic.css'
2 | import 'maplibre-gl/dist/maplibre-gl.css'
3 |
4 | import { Map } from 'maplibre-gl'
5 | import {
6 | StreamlineStyle,
7 | WMSStreamlineLayer,
8 | type WMSStreamlineLayerOptions
9 | } from '@/index'
10 |
11 | const defaultCentre: [number, number] = [0, 0]
12 | const defaultZoom = 0.2
13 | const style = 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json'
14 | const map = new Map({
15 | container: 'container',
16 | style,
17 | center: defaultCentre,
18 | zoom: defaultZoom
19 | })
20 |
21 | await map.once('load')
22 |
23 | const options: WMSStreamlineLayerOptions = {
24 | baseUrl: 'https://example.com/FewsWebServices/wms',
25 | layer: 'layer_name',
26 | streamlineStyle: StreamlineStyle.LightParticlesOnMagnitude,
27 | numParticles: 12000,
28 | particleSize: 4,
29 | speedFactor: 0.1,
30 | fadeAmountPerSecond: 3,
31 | speedExponent: 0.7
32 | }
33 |
34 | const layer = new WMSStreamlineLayer('streamlines', options)
35 | map.addLayer(layer)
36 |
37 | await layer.initialise()
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2025 Deltares
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the “Software”), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7 | the Software, and to permit persons to whom the Software is furnished to do so,
8 | subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "module": "ESNext",
6 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
7 | "skipLibCheck": true,
8 |
9 | "paths": {
10 | "@/*": ["./src/*"]
11 | },
12 |
13 | /* Bundler mode */
14 | "moduleResolution": "bundler",
15 | "allowImportingTsExtensions": true,
16 | "isolatedModules": true,
17 | "moduleDetection": "force",
18 | "noEmit": true,
19 |
20 | /* Linting */
21 | "strict": true,
22 | "allowUnreachableCode": false,
23 | "allowUnusedLabels": false,
24 | "noFallthroughCasesInSwitch": true,
25 | "noImplicitOverride": true,
26 | "noImplicitReturns": true,
27 | "noPropertyAccessFromIndexSignature": true,
28 | "noUncheckedIndexedAccess": true,
29 | "noUncheckedSideEffectImports": true,
30 | "noUnusedLocals": true,
31 | "noUnusedParameters": true,
32 | "verbatimModuleSyntax": true,
33 |
34 | "types": ["vite-plugin-glsl/ext", "vite/client"]
35 | },
36 | "include": ["examples", "src", "tests", "*.config.ts"]
37 | }
38 |
--------------------------------------------------------------------------------
/examples/maplibre_basic-sprites.ts:
--------------------------------------------------------------------------------
1 | import './maplibre_basic.css'
2 | import 'maplibre-gl/dist/maplibre-gl.css'
3 | import spriteUrl from './wave.svg'
4 |
5 | import { Map } from 'maplibre-gl'
6 | import {
7 | StreamlineStyle,
8 | WMSStreamlineLayer,
9 | type WMSStreamlineLayerOptions
10 | } from '@/index'
11 |
12 | const defaultCentre: [number, number] = [0, 0]
13 | const defaultZoom = 0.2
14 | const style = 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json'
15 | const map = new Map({
16 | container: 'container',
17 | style,
18 | center: defaultCentre,
19 | zoom: defaultZoom
20 | })
21 |
22 | await map.once('load')
23 |
24 | const options: WMSStreamlineLayerOptions = {
25 | baseUrl:
26 | 'https://rwsos-dataservices-ont.avi.deltares.nl/durban/FewsWebServices/wms',
27 | layer: 'swan_hs',
28 | streamlineStyle: StreamlineStyle.LightParticlesOnMagnitude,
29 | numParticles: 500,
30 | particleSize: 24,
31 | speedFactor: 0.05,
32 | fadeAmountPerSecond: 2,
33 | speedExponent: 0.7,
34 | growthRate: 1,
35 | maxAge: 2,
36 | spriteUrl: new URL(spriteUrl, window.location.origin)
37 | }
38 |
39 | const layer = new WMSStreamlineLayer('streamlines', options)
40 | map.addLayer(layer)
41 |
42 | await layer.initialise()
43 |
--------------------------------------------------------------------------------
/.github/workflows/github-pages.yml:
--------------------------------------------------------------------------------
1 | name: Deploy to GitHub Pages
2 |
3 | on:
4 | push:
5 | branches: [main, examples-gh-pages]
6 | # enables manual deployment from the Actions tab
7 | workflow_dispatch:
8 |
9 | permissions:
10 | contents: read
11 | pages: write
12 | id-token: write
13 |
14 | # Allow one concurrent deployment
15 | concurrency:
16 | group: 'pages'
17 | cancel-in-progress: true
18 |
19 | jobs:
20 | # Single deploy job since we're just deploying
21 | deploy:
22 | environment:
23 | name: github-pages
24 | url: ${{ steps.deployment.outputs.page_url }}
25 | runs-on: ubuntu-latest
26 | steps:
27 | - name: Checkout
28 | uses: actions/checkout@v5
29 | - name: Set up Node
30 | uses: actions/setup-node@v6
31 | with:
32 | node-version: lts/*
33 | cache: 'npm'
34 | - name: Install dependencies
35 | run: npm ci
36 | - name: Build
37 | run: npm run build-examples
38 | - name: Setup Pages
39 | uses: actions/configure-pages@v5
40 | - name: Upload artifact
41 | uses: actions/upload-pages-artifact@v4
42 | with:
43 | # Upload dist folder
44 | path: './dist'
45 | - name: Deploy to GitHub Pages
46 | id: deployment
47 | uses: actions/deploy-pages@v4
48 |
--------------------------------------------------------------------------------
/vite-library.config.ts:
--------------------------------------------------------------------------------
1 | import { resolve } from 'path'
2 | import { defineConfig } from 'vite'
3 | import viteGlslPlugin from 'vite-plugin-glsl'
4 | import rollupPluginTypescript from '@rollup/plugin-typescript'
5 |
6 | function resolveRelativePath(relative: string): string {
7 | return resolve(__dirname, relative)
8 | }
9 |
10 | export default defineConfig({
11 | plugins: [
12 | viteGlslPlugin({
13 | include: ['**/*.frag.glsl', '**/*.vert.glsl'],
14 | minify: true
15 | })
16 | ],
17 | build: {
18 | lib: {
19 | entry: resolveRelativePath('src/index.ts'),
20 | // Only build ES module, this library is only relevant for use in the
21 | // browser.
22 | formats: ['es'],
23 | name: 'webgl-streamline-visualizer',
24 | fileName: 'webgl-streamline-visualizer'
25 | },
26 | rollupOptions: {
27 | // Do not bundle MapLibre; applications using this streamlines library as a
28 | // map layer should already have it anyway.
29 | external: ['maplibre-gl'],
30 | plugins: [
31 | rollupPluginTypescript({
32 | allowImportingTsExtensions: false,
33 | declaration: true,
34 | declarationDir: resolveRelativePath('dist'),
35 | rootDir: resolveRelativePath('src')
36 | })
37 | ]
38 | }
39 | },
40 | resolve: {
41 | alias: {
42 | '@': resolveRelativePath('./src')
43 | }
44 | }
45 | })
46 |
--------------------------------------------------------------------------------
/src/utils/shader.ts:
--------------------------------------------------------------------------------
1 | type ShaderType =
2 | | WebGL2RenderingContext['VERTEX_SHADER']
3 | | WebGL2RenderingContext['FRAGMENT_SHADER']
4 |
5 | class Shader {
6 | readonly gl: WebGL2RenderingContext
7 | readonly shader: WebGLShader
8 |
9 | private hasCompileAttempt: boolean
10 |
11 | constructor(gl: WebGL2RenderingContext, type: ShaderType, source: string) {
12 | const shader = gl.createShader(type)
13 | if (!shader) {
14 | throw new Error('Failed to create WebGL shader.')
15 | }
16 | gl.shaderSource(shader, source)
17 |
18 | this.gl = gl
19 | this.shader = shader
20 | this.hasCompileAttempt = false
21 | }
22 |
23 | destruct(): void {
24 | this.gl.deleteShader(this.shader)
25 | }
26 |
27 | compile(): void {
28 | // Do not try to compile more than once.
29 | if (this.hasCompileAttempt) return
30 |
31 | this.gl.compileShader(this.shader)
32 | this.hasCompileAttempt = true
33 | // Do not check errors here, as checking the compile status blocks the main
34 | // thread until compilation is complete. Instead, the compile status (and
35 | // any possible errors) will be checked when this shader is linked into a
36 | // shader program.
37 | }
38 | }
39 |
40 | export class VertexShader extends Shader {
41 | constructor(gl: WebGL2RenderingContext, source: string) {
42 | super(gl, gl.VERTEX_SHADER, source)
43 | }
44 | }
45 |
46 | export class FragmentShader extends Shader {
47 | constructor(gl: WebGL2RenderingContext, source: string) {
48 | super(gl, gl.FRAGMENT_SHADER, source)
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/utils/geometry.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ShaderProgram,
3 | bindAttribute,
4 | createAndFillStaticBuffer
5 | } from './shader-program'
6 |
7 | export function createRectangleVertexArray(
8 | program: ShaderProgram,
9 | xMin: number,
10 | xMax: number,
11 | yMin: number,
12 | yMax: number,
13 | doFlipV: boolean,
14 | positionAttribute: string,
15 | vertexCoordAttribute: string
16 | ): [WebGLBuffer, WebGLBuffer, WebGLVertexArrayObject] {
17 | const gl = program.gl
18 | const vertexArray = gl.createVertexArray()
19 | if (vertexArray === null) {
20 | throw new Error('Failed to create vertex array.')
21 | }
22 | gl.bindVertexArray(vertexArray)
23 |
24 | const positions = [xMax, yMax, xMin, yMax, xMax, yMin, xMin, yMin]
25 | // Texture coordinate v can either run in the same direction as the clip space
26 | // y-axis, or in the opposite direction.
27 | const texCoords = doFlipV
28 | ? [1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0] // flipped V
29 | : [1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0] // unflipped V
30 |
31 | const positionBuffer = createAndFillStaticBuffer(
32 | gl,
33 | new Float32Array(positions)
34 | )
35 | const texCoordBuffer = createAndFillStaticBuffer(
36 | gl,
37 | new Float32Array(texCoords)
38 | )
39 |
40 | bindAttribute(
41 | gl,
42 | positionBuffer,
43 | program.getAttributeLocation(positionAttribute),
44 | 2
45 | )
46 | bindAttribute(
47 | gl,
48 | texCoordBuffer,
49 | program.getAttributeLocation(vertexCoordAttribute),
50 | 2
51 | )
52 |
53 | gl.bindVertexArray(null)
54 |
55 | return [positionBuffer, texCoordBuffer, vertexArray]
56 | }
57 |
--------------------------------------------------------------------------------
/src/utils/speedcurve.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Speed curve of the form factor * speed ^ exponent.
3 | */
4 | export class SpeedCurve {
5 | private _exponent: number
6 | private _factor: number
7 | private _baseFactor: number
8 |
9 | constructor(exponent: number, factor: number, baseFactor: number) {
10 | this._exponent = exponent
11 | this._factor = factor
12 | this._baseFactor = baseFactor
13 | }
14 |
15 | /** Exponent of the curve. */
16 | get exponent(): number {
17 | return this._exponent
18 | }
19 |
20 | /** Factor applied after exponentiation. */
21 | get factor(): number {
22 | return this._factor
23 | }
24 |
25 | /** Base speed factor
26 | *
27 | * This is the multiplication factor that would be applied if the exponent
28 | * would be 1.
29 | */
30 | get baseFactor(): number {
31 | return this._baseFactor
32 | }
33 | /**
34 | * Returns a speed curve from an exponent and a speed.
35 | *
36 | * The specified speed is used to compute the factor of the curve. At this
37 | * speed, the transformed speed is equal to the original speed.
38 | *
39 | * @param exponent exponent applied to the speed.
40 | * @param factor factor applied to the transformed speed.
41 | * @param speed speed where the transformation does not change the speed
42 | */
43 | static fromExponentFactorAndSpeed(
44 | exponent: number,
45 | factor: number,
46 | speed: number
47 | ): SpeedCurve {
48 | const transformedSpeed = Math.pow(speed, exponent)
49 | const finalFactor = (factor * speed) / transformedSpeed
50 | return new SpeedCurve(exponent, finalFactor, factor)
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 | WebGL Streamline Visualizer Examples
10 |
45 |
46 |
47 |
48 |
49 | WebGL Streamline Visualizer Examples
50 |
51 | Explore the following examples to see how the WebGL Streamline
52 | Visualizer can be used:
53 |
54 |
55 | -
56 | Vortex Demo - A standalone example
57 | that generates velocity data using TypeScript functions.
58 |
59 | -
60 | Vortex with Sprites Demo -
61 | Similar to the vortex demo but with sprite-based visualization.
62 |
63 | -
64 | MapLibre Demo - Integrates the
65 | visualizer with a MapLibre map and a WMS service.
66 |
67 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@deltares/webgl-streamline-visualizer",
3 | "description": "Particle-based flow field visualiser based on WebGL",
4 | "license": "MIT",
5 | "private": false,
6 | "version": "4.5.0",
7 | "type": "module",
8 | "files": [
9 | "dist"
10 | ],
11 | "module": "./dist/webgl-streamline-visualizer.js",
12 | "exports": {
13 | ".": {
14 | "types": "./dist/index.d.ts",
15 | "import": "./dist/webgl-streamline-visualizer.js"
16 | }
17 | },
18 | "homepage": "https://deltares.github.io/webgl-streamline-visualizer/",
19 | "scripts": {
20 | "dev": "vite --config vite-examples.config.ts",
21 | "build": "tsc && vite build --config vite-library.config.ts",
22 | "build-examples": "tsc && vite build --config vite-examples.config.ts",
23 | "lint": "eslint \"./src/**/*.ts\" \"./examples/**/*.ts\"",
24 | "preview": "vite preview",
25 | "test": "vitest",
26 | "test-once": "vitest run"
27 | },
28 | "devDependencies": {
29 | "@deltares/fews-wms-requests": "^4.0.1",
30 | "@eslint/js": "^9.39.1",
31 | "@rollup/plugin-typescript": "^12.3.0",
32 | "@types/lodash-es": "^4.17.12",
33 | "@types/node": "^22.14.1",
34 | "@vitest/browser": "^4.0.8",
35 | "@vitest/browser-playwright": "^4.0.8",
36 | "eslint": "^9.39.1",
37 | "eslint-config-prettier": "^10.1.8",
38 | "eslint-plugin-prettier": "^5.5.4",
39 | "playwright": "^1.56.1",
40 | "typescript": "^5.8.3",
41 | "typescript-eslint": "^8.46.4",
42 | "vite": "^7.2.2",
43 | "vite-plugin-glsl": "^1.5.4",
44 | "vitest": "^4.0.8"
45 | },
46 | "dependencies": {
47 | "geotiff": "^2.1.3",
48 | "lodash-es": "^4.17.21",
49 | "maplibre-gl": "^5.12.0",
50 | "tslib": "^2.8.1"
51 | },
52 | "volta": {
53 | "node": "22.14.0"
54 | },
55 | "repository": {
56 | "type": "git",
57 | "url": "git+https://github.com/Deltares/webgl-streamline-visualizer.git"
58 | },
59 | "bugs": {
60 | "url": "https://github.com/Deltares/webgl-streamline-visualizer/issues"
61 | },
62 | "publishConfig": {
63 | "access": "public",
64 | "registry": "https://registry.npmjs.org"
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/examples/wave.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
82 |
--------------------------------------------------------------------------------
/examples/maplibre.ts:
--------------------------------------------------------------------------------
1 | import './maplibre.css'
2 | import 'maplibre-gl/dist/maplibre-gl.css'
3 |
4 | import { Map } from 'maplibre-gl'
5 |
6 | import type { WMSStreamlineLayerOptions } from '@/layer'
7 | import {
8 | StreamlineStyle,
9 | WMSStreamlineLayer,
10 | type StreamlineVisualiserOptions
11 | } from '@/index'
12 |
13 | import { VisualiserOptionsControl } from './options'
14 | import './wms.ts'
15 | import type { FewsWmsOptionsControl } from './wms.ts'
16 |
17 | async function createMap(): Promise