English | 日本語
Experimental Repository This library is currently experimental. Only PNG image loading is supported at this time. Support for other formats such as GeoTIFF is under development.
A library for visualizing vector fields. Enables particle animations and colormap displays on deck.gl.
Demo: https://wind-particle.nokonoko1203.com
- Particle Animation - Renders particles flowing along vector fields
- Colormap Display - Gradient color display based on vector magnitude
- Data Loading from PNG/WebP - Easily load vector field data from image files
- Customizable - Adjustable particle density, speed, lifespan, and more
npm install particle-layerThe following packages are required:
npm install @deck.gl/core @deck.gl/layers @luma.gl/core @luma.gl/engine @luma.gl/shadertoolsimport { Deck, MapView } from '@deck.gl/core';
import { WindParticleLayer, WindColormapLayer, PngWindLoader } from 'particle-layer';
// 1. Create a wind data loader
const loader = new PngWindLoader({
image: '/wind-data.png',
uRange: [-30.0, 30.0], // U component range (m/s)
vRange: [-30.0, 30.0], // V component range (m/s)
bounds: [138, 34, 142, 37] // [west, south, east, north] (degrees)
});
// 2. Create a wind colormap layer (optional)
const colormapLayer = new WindColormapLayer({
id: 'wind-colormap',
dataSource: loader,
maxSpeedMps: 30.0
});
// 3. Create a wind particle layer
const particleLayer = new WindParticleLayer({
id: 'wind-particles',
dataSource: loader,
stateTexSize: 64, // 64² = 4,096 particles
maxAge: 5, // Particle lifespan (seconds)
speedFactor: 2000, // Speed multiplier
randomSpeedMps: 0.1 // Random speed (m/s)
});
// 4. Add to deck.gl
const deck = new Deck({
parent: document.getElementById('map'),
_animate: true,
views: new MapView({ repeat: true }),
initialViewState: {
longitude: 140,
latitude: 35.5,
zoom: 7
},
controller: true,
layers: [colormapLayer, particleLayer]
});A loader that reads wind data from PNG/WebP images.
import { PngWindLoader } from 'particle-layer';
const loader = new PngWindLoader({
image: '/path/to/wind.png',
uRange: [-30.0, 30.0],
vRange: [-30.0, 30.0],
bounds: [138, 34, 142, 37]
});| Property | Type | Description |
|---|---|---|
image |
string |
URL of PNG/WebP image |
uRange |
[number, number] |
U component (eastward velocity) range [min, max] (m/s) |
vRange |
[number, number] |
V component (northward velocity) range [min, max] (m/s) |
bounds |
[number, number, number, number] |
Geographic bounds [west, south, east, north] (degrees) |
Wind data must be encoded in PNG images using the following format:
- Red Channel (R): U component (eastward velocity)
- Pixel value 0 →
uRange[0] - Pixel value 255 →
uRange[1]
- Pixel value 0 →
- Green Channel (G): V component (northward velocity)
- Pixel value 0 →
vRange[0] - Pixel value 255 →
vRange[1]
- Pixel value 0 →
- Blue/Alpha Channels: Unused
Decoding Formula:
u = uRange[0] + (R / 255) * (uRange[1] - uRange[0])
v = vRange[0] + (G / 255) * (vRange[1] - vRange[0])
A GPU-based wind particle animation layer.
import { WindParticleLayer } from 'particle-layer';
const layer = new WindParticleLayer({
id: 'wind-particles',
dataSource: loader,
stateTexSize: 64,
maxAge: 5,
speedFactor: 2000,
randomSpeedMps: 0.1,
integrationSteps: 16
});| Property | Type | Default | Description |
|---|---|---|---|
dataSource |
WindDataSource |
Required | Data source providing wind data |
stateTexSize |
number |
256 |
Particle grid size. Number of particles is stateTexSize² |
maxAge |
number |
30 |
Maximum particle lifespan (seconds) |
speedFactor |
number |
10000.0 |
Speed multiplier. Higher values make particles move faster |
randomSpeedMps |
number |
0.0 |
Random speed variation (m/s) |
integrationSteps |
number |
16 |
Integration steps per frame. More steps result in smoother motion |
maxSpeedMps |
number |
30.0 |
Maximum wind speed for point size scaling (m/s) |
minPointSize |
number |
0.1 |
Minimum particle point size (pixels) |
maxPointSize |
number |
3.0 |
Maximum particle point size (pixels) |
altitude |
number |
0 |
Altitude offset for 3D visualization (meters) |
stateTexSize |
Particle Count | Use Case |
|---|---|---|
| 32 | 1,024 | Lightweight demo |
| 64 | 4,096 | Standard use |
| 128 | 16,384 | High density display |
| 256 | 65,536 | Detailed visualization |
| 512 | 262,144 | Maximum density (high-end GPU recommended) |
A layer that displays a colormap based on wind speed.
import { WindColormapLayer } from 'particle-layer';
const layer = new WindColormapLayer({
id: 'wind-colormap',
dataSource: loader,
maxSpeedMps: 30.0
});| Property | Type | Default | Description |
|---|---|---|---|
dataSource |
WindDataSource |
Required | Data source providing wind data |
maxSpeedMps |
number |
30.0 |
Maximum wind speed for colormap (m/s) |
colormapAlpha |
number |
140 |
Colormap opacity (0-255) |
The following color gradient is applied based on wind speed:
| Speed Ratio | Color | Description |
|---|---|---|
| 0.0 | Dark Blue | Calm |
| 0.1 | Blue | Light Air |
| 0.2 | Cyan | Light Breeze |
| 0.3 | Teal | Gentle Breeze |
| 0.4 | Green | Moderate Breeze |
| 0.5 | Yellow-Green | Fresh Breeze |
| 0.6 | Yellow | Strong Breeze |
| 0.7 | Orange | Near Gale |
| 0.8 | Dark Orange | Gale |
| 0.9 | Red | Strong Gale |
| 1.0 | Purple | Storm |
An interface for providing wind data. Implement this when creating custom data sources.
interface WindDataSource {
load(): Promise<WindFieldData>;
}The structure of wind field data.
interface WindFieldData {
/** U/V interleaved array [u0, v0, u1, v1, ...] (m/s) */
uvMps: Float32Array;
/** Grid width (pixels) */
width: number;
/** Grid height (pixels) */
height: number;
/** Geographic bounds [west, south, east, north] (degrees) */
bounds: [number, number, number, number];
}You can create custom data sources by implementing the WindDataSource interface.
import type { WindDataSource, WindFieldData } from 'particle-layer';
class MyCustomLoader implements WindDataSource {
async load(): Promise<WindFieldData> {
// Fetch data from API
const response = await fetch('/api/wind-data');
const data = await response.json();
// Convert to WindFieldData format
const uvMps = new Float32Array(data.u.length * 2);
for (let i = 0; i < data.u.length; i++) {
uvMps[i * 2] = data.u[i]; // U component
uvMps[i * 2 + 1] = data.v[i]; // V component
}
return {
uvMps,
width: data.width,
height: data.height,
bounds: data.bounds
};
}
}
// Usage
const loader = new MyCustomLoader();
const layer = new WindParticleLayer({
id: 'wind',
dataSource: loader
});| Scenario | stateTexSize |
integrationSteps |
maxAge |
|---|---|---|---|
| Mobile | 32-64 | 8 | 3-5 |
| Desktop | 64-128 | 16 | 5-10 |
| High-end PC | 256-512 | 16-32 | 10-30 |
- Adjust particle count: Reduce
stateTexSizefor better performance - Reduce integration steps: Lower
integrationStepsto reduce computational load - Shorten lifespan: Decrease
maxAgefor more frequent particle regeneration and livelier motion - Adjust speed multiplier: Tune
speedFactorbased on zoom level for better visual results
import { Deck, MapView } from '@deck.gl/core';
import { TileLayer } from '@deck.gl/geo-layers';
import { BitmapLayer } from '@deck.gl/layers';
import { WindParticleLayer, WindColormapLayer, PngWindLoader } from 'particle-layer';
// Define geographic bounds
const BOUNDS: [number, number, number, number] = [138, 34, 142, 37];
// Create wind data loader
const loader = new PngWindLoader({
image: '/wind-data.png',
uRange: [-30.0, 30.0],
vRange: [-30.0, 30.0],
bounds: BOUNDS
});
// Basemap layer (OpenStreetMap)
const basemapLayer = new TileLayer({
id: 'basemap',
data: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
minZoom: 0,
maxZoom: 19,
tileSize: 256,
renderSubLayers: (props) => {
const { boundingBox } = props.tile;
return new BitmapLayer(props, {
data: undefined,
image: props.data,
bounds: [boundingBox[0][0], boundingBox[0][1], boundingBox[1][0], boundingBox[1][1]]
});
}
});
// Wind colormap layer
const windColormapLayer = new WindColormapLayer({
id: 'wind-colormap',
dataSource: loader,
maxSpeedMps: 30.0
});
// Wind particle layer
const windParticleLayer = new WindParticleLayer({
id: 'wind-particles',
dataSource: loader,
stateTexSize: 64,
maxAge: 5,
speedFactor: 2000,
randomSpeedMps: 0.1
});
// Initialize deck.gl
const deck = new Deck({
parent: document.getElementById('map'),
_animate: true, // Enable animation (required)
views: new MapView({ repeat: true }),
initialViewState: {
longitude: 140,
latitude: 35.5,
zoom: 7,
pitch: 0,
bearing: 0
},
controller: true,
layers: [basemapLayer, windColormapLayer, windParticleLayer]
});# Install dependencies
npm install
# Start development server
npm run dev
# Build library
npm run build:lib
# Run tests
npm testThe following features are planned for future development:
- GeoTIFF Loader - Direct loading of wind data from GeoTIFF format
- 3D Particle Visualization - Three-dimensional particle representation with altitude information
MIT
