particle-layer
v0.0.2
Published
GPU-based wind particle visualization layer for deck.gl
Maintainers
Readme
English | 日本語

Wind Particle Layer
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
Features
- 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
Installation
npm install particle-layerPeer Dependencies
The following packages are required:
npm install @deck.gl/core @deck.gl/layers @luma.gl/core @luma.gl/engine @luma.gl/shadertoolsQuick Start
import { 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]
});API Reference
PngWindLoader
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]
});Options
| 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) |
PNG Image Format
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])WindParticleLayer
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
});Properties
| 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) |
Particle Count Guide
| 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) |
WindColormapLayer
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
});Properties
| 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) |
Color Gradient
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 |
Type Definitions
WindDataSource
An interface for providing wind data. Implement this when creating custom data sources.
interface WindDataSource {
load(): Promise<WindFieldData>;
}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];
}Creating Custom Data Sources
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
});Performance Tuning
Recommended Settings
| 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 |
Tips
- 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
Complete Example
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]
});Development
# Install dependencies
npm install
# Start development server
npm run dev
# Build library
npm run build:lib
# Run tests
npm testRoadmap
The 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
License
MIT
