@geoql/maplibre-gl-snow 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026-current Vinayak Kulkarni
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,159 @@
1
+ # @geoql/maplibre-gl-snow
2
+
3
+ WebGPU-accelerated snow particle layer for [MapLibre GL JS](https://maplibre.org/).
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@geoql/maplibre-gl-snow.svg)](https://www.npmjs.com/package/@geoql/maplibre-gl-snow)
6
+ [![npm bundle size](https://img.shields.io/bundlephobia/minzip/@geoql/maplibre-gl-snow)](https://bundlephobia.com/package/@geoql/maplibre-gl-snow)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
+
9
+ [![oxlint](https://img.shields.io/badge/linter-oxlint-7c5dfa?logo=oxc)](https://oxc.rs)
10
+ [![tsdown](https://img.shields.io/badge/bundler-tsdown-3178c6)](https://tsdown.dev/)
11
+ [![typescript](https://img.shields.io/npm/dependency-version/@geoql/maplibre-gl-snow/dev/typescript?logo=TypeScript)](https://www.typescriptlang.org/)
12
+ [![WebGPU](https://img.shields.io/badge/WebGPU-Yes-brightgreen)](https://developer.mozilla.org/en-US/docs/Web/API/WebGPU_API)
13
+
14
+ > [**Live Demo**](https://geoql.github.io/maplibre-gl-snow/)
15
+
16
+ Renders 100,000+ animated snow particles using Three.js WebGPU renderer with TSL compute shaders. Particles are georeferenced in Mercator coordinates — snow always fills the viewport regardless of zoom level. Supports wind direction, intensity, fog overlay, and more.
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ # npm
22
+ npm install @geoql/maplibre-gl-snow maplibre-gl three
23
+
24
+ # pnpm
25
+ pnpm add @geoql/maplibre-gl-snow maplibre-gl three
26
+
27
+ # yarn
28
+ yarn add @geoql/maplibre-gl-snow maplibre-gl three
29
+
30
+ # bun
31
+ bun add @geoql/maplibre-gl-snow maplibre-gl three
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ```typescript
37
+ import maplibregl from 'maplibre-gl';
38
+ import { MaplibreSnowLayer } from '@geoql/maplibre-gl-snow';
39
+ import 'maplibre-gl/dist/maplibre-gl.css';
40
+
41
+ const map = new maplibregl.Map({
42
+ container: 'map',
43
+ style: 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json',
44
+ center: [-74.006, 40.7128],
45
+ zoom: 13,
46
+ pitch: 55,
47
+ maxPitch: 85,
48
+ });
49
+
50
+ map.on('load', () => {
51
+ const snow = new MaplibreSnowLayer({
52
+ density: 0.5,
53
+ intensity: 0.5,
54
+ flakeSize: 4,
55
+ opacity: 0.8,
56
+ direction: [0, 50], // [azimuth degrees, horizontal speed px/s]
57
+ fog: true,
58
+ fogOpacity: 0.08,
59
+ });
60
+
61
+ map.addLayer(snow);
62
+ });
63
+ ```
64
+
65
+ ## Options
66
+
67
+ ```typescript
68
+ interface MaplibreSnowOptions {
69
+ id?: string;
70
+ density?: number;
71
+ intensity?: number;
72
+ flakeSize?: number;
73
+ opacity?: number;
74
+ direction?: [number, number];
75
+ fog?: boolean;
76
+ fogOpacity?: number;
77
+ }
78
+ ```
79
+
80
+ | Option | Type | Default | Description |
81
+ | ------------ | ------------------ | --------- | -------------------------------------------------- |
82
+ | `id` | `string` | `'snow'` | Unique layer ID |
83
+ | `density` | `number` (0–1) | `0.5` | Particle density — maps to 10k–200k particles |
84
+ | `intensity` | `number` (0–1) | `0.5` | Fall speed multiplier |
85
+ | `flakeSize` | `number` | `4` | Base flake size in CSS pixels |
86
+ | `opacity` | `number` (0–1) | `0.8` | Global opacity multiplier |
87
+ | `direction` | `[number, number]` | `[0, 50]` | Wind as `[azimuth degrees, horizontal speed px/s]` |
88
+ | `fog` | `boolean` | `true` | Enable atmospheric fog overlay |
89
+ | `fogOpacity` | `number` (0–1) | `0.08` | Fog opacity |
90
+
91
+ ## API
92
+
93
+ ```typescript
94
+ const snow = new MaplibreSnowLayer(options);
95
+
96
+ // Update settings at runtime
97
+ snow.setDensity(0.8);
98
+ snow.setIntensity(0.7);
99
+ snow.setFlakeSize(6);
100
+ snow.setOpacity(0.6);
101
+ snow.setDirection([45, 80]); // wind from NE at 80 px/s
102
+ snow.setFog(false);
103
+ snow.setFogOpacity(0.12);
104
+ ```
105
+
106
+ ## How It Works
107
+
108
+ The layer implements MapLibre's `CustomLayerInterface` with a two-canvas architecture:
109
+
110
+ 1. **WebGPU overlay** — Three.js `WebGPURenderer` on a separate `<canvas>` positioned absolutely over the MapLibre canvas. `pointer-events: none` ensures clicks pass through to the map.
111
+ 2. **TSL compute shaders** — 100k particles stored in GPU `instancedArray` buffers. Compute shaders handle:
112
+ - `computeInit` — spawns particles in a zoom-adaptive volume centered on the viewport
113
+ - `computeUpdate` — applies gravity, wind drift, and respawns particles that fall below ground
114
+ 3. **Georeferenced particles** — positions stored as `(mercX, mercY, mercAlt)` in Mercator [0,1] space. Spawn volume adapts to zoom level so snow always fills the viewport.
115
+ 4. **Camera sync** — uses MapLibre's projection matrix directly. A `PerspectiveCamera` with `updateProjectionMatrix` no-op'd prevents Three.js from overwriting the matrix.
116
+ 5. **Animation** — MapLibre drives the frame loop via `triggerRepaint()`, calling our `render()` callback which runs compute + render each frame.
117
+
118
+ ## Browser Support
119
+
120
+ Requires a WebGPU-capable browser (Chrome 113+, Edge 113+, Firefox Nightly with `dom.webgpu.enabled`, or Safari 17.4+ with WebGPU enabled).
121
+
122
+ > **Note:** This library is **WebGPU-only**. There is no WebGL fallback. If WebGPU is unavailable, the layer silently does nothing.
123
+
124
+ ## Exports
125
+
126
+ ```typescript
127
+ // Main class
128
+ export { MaplibreSnowLayer } from '@geoql/maplibre-gl-snow';
129
+
130
+ // Default export (same class)
131
+ export { default } from '@geoql/maplibre-gl-snow';
132
+
133
+ // Types
134
+ export type { MaplibreSnowOptions } from '@geoql/maplibre-gl-snow';
135
+ ```
136
+
137
+ ## Requirements
138
+
139
+ - MapLibre GL JS >= 3.0.0
140
+ - Three.js >= 0.183.0 (WebGPU-enabled build)
141
+ - Node.js >= 24.0.0
142
+
143
+ ## Contributing
144
+
145
+ 1. Fork and create a feature branch from `main`
146
+ 2. Make changes following [conventional commits](https://www.conventionalcommits.org/)
147
+ 3. Ensure commits are signed ([why?](https://withblue.ink/2020/05/17/how-and-why-to-sign-git-commits.html))
148
+ 4. Submit a PR
149
+
150
+ ```bash
151
+ bun install
152
+ bun run build
153
+ bun run lint
154
+ bun run typecheck
155
+ ```
156
+
157
+ ## License
158
+
159
+ [MIT](./LICENSE)
@@ -0,0 +1,54 @@
1
+ import { CustomRenderMethodInput, Map } from "maplibre-gl";
2
+
3
+ //#region src/index.d.ts
4
+ interface MaplibreSnowOptions {
5
+ /** Unique layer ID (default: 'snow') */
6
+ id?: string;
7
+ /** Particle density 0–1 (default: 0.5) */
8
+ density?: number;
9
+ /** Fall speed intensity 0–1 (default: 0.5) */
10
+ intensity?: number;
11
+ /** Base flake size in CSS pixels (default: 4) */
12
+ flakeSize?: number;
13
+ /** Global opacity 0–1 (default: 0.8) */
14
+ opacity?: number;
15
+ /** Wind as [azimuth degrees, horizontal speed px/s] (default: [0, 50]) */
16
+ direction?: [number, number];
17
+ /** Enable atmospheric fog overlay (default: true) */
18
+ fog?: boolean;
19
+ /** Fog opacity 0–1 (default: 0.08) */
20
+ fogOpacity?: number;
21
+ }
22
+ declare class MaplibreSnowLayer {
23
+ id: string;
24
+ readonly type: "custom";
25
+ readonly renderingMode: "3d";
26
+ private map;
27
+ private overlayCanvas;
28
+ private overlayDiv;
29
+ private gpu;
30
+ private resizeObserver;
31
+ private _density;
32
+ private _intensity;
33
+ private _flakeSize;
34
+ private _opacity;
35
+ private _direction;
36
+ private _fog;
37
+ private _fogOpacity;
38
+ private _lastFrameTime;
39
+ private _fps;
40
+ constructor(options?: MaplibreSnowOptions);
41
+ onAdd(map: Map, _gl: WebGL2RenderingContext): void;
42
+ render(_gl: WebGL2RenderingContext, args: CustomRenderMethodInput): void;
43
+ onRemove(_map: Map, _gl: WebGL2RenderingContext): void;
44
+ setDensity(density: number): void;
45
+ setIntensity(intensity: number): void;
46
+ setFlakeSize(size: number): void;
47
+ setOpacity(opacity: number): void;
48
+ setDirection(direction: [number, number]): void;
49
+ setFog(enabled: boolean): void;
50
+ setFogOpacity(opacity: number): void;
51
+ }
52
+ //#endregion
53
+ export { MaplibreSnowLayer, MaplibreSnowOptions };
54
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";;;UAkCiB,mBAAA;EAgBf;EAdA,EAAA;EAcU;EAZV,OAAA;EAuYqB;EArYrB,SAAA;EA6ZqB;EA3ZrB,SAAA;EA0a6B;EAxa7B,OAAA;EAie0C;EA/d1C,SAAA;EAwfiC;EAtfjC,GAAA;EAsfuD;EApfvD,UAAA;AAAA;AAAA,cA2XI,iBAAA;EACJ,EAAA;EAAA,SACS,IAAA;EAAA,SACA,aAAA;EAAA,QAED,GAAA;EAAA,QACA,aAAA;EAAA,QACA,UAAA;EAAA,QACA,GAAA;EAAA,QACA,cAAA;EAAA,QAGA,QAAA;EAAA,QACA,UAAA;EAAA,QACA,UAAA;EAAA,QACA,QAAA;EAAA,QACA,UAAA;EAAA,QACA,IAAA;EAAA,QACA,WAAA;EAAA,QAGA,cAAA;EAAA,QACA,IAAA;cAEI,OAAA,GAAS,mBAAA;EAerB,KAAA,CAAM,GAAA,EAAK,GAAA,EAAa,GAAA,EAAK,sBAAA;EAyD7B,MAAA,CAAO,GAAA,EAAK,sBAAA,EAAwB,IAAA,EAAM,uBAAA;EAyB1C,QAAA,CAAS,IAAA,EAAM,GAAA,EAAa,GAAA,EAAK,sBAAA;EAkBjC,UAAA,CAAW,OAAA;EAKX,YAAA,CAAa,SAAA;EAIb,YAAA,CAAa,IAAA;EAIb,UAAA,CAAW,OAAA;EAKX,YAAA,CAAa,SAAA;EAIb,MAAA,CAAO,OAAA;EAKP,aAAA,CAAc,OAAA;AAAA"}
package/dist/index.mjs ADDED
@@ -0,0 +1,372 @@
1
+ import * as THREE from "three/webgpu";
2
+ import { Fn, If, color, float, hash, instanceIndex, instancedArray, positionLocal, screenUV, uint, uniform, vec4 } from "three/tsl";
3
+
4
+ //#region src/index.ts
5
+ /**
6
+ * @geoql/maplibre-gl-snow
7
+ *
8
+ * WebGPU-accelerated snow particle layer for MapLibre GL JS.
9
+ * Uses Three.js WebGPU renderer with TSL compute shaders.
10
+ *
11
+ * Architecture:
12
+ * - Two-canvas overlay: MapLibre (WebGL2) + Three.js WebGPU overlay
13
+ * - Particles georeferenced as (mercX, mercY, altMerc) in mercator [0,1] space
14
+ * - Camera syncs to MapLibre's mercator projection matrix directly
15
+ * - MapLibre drives the frame loop via triggerRepaint()
16
+ */
17
+ const DEFAULT_PARTICLE_COUNT = 1e5;
18
+ const MIN_PARTICLE_COUNT = 1e4;
19
+ const MAX_PARTICLE_COUNT = 2e5;
20
+ function lngLatToMercator(lng, lat) {
21
+ const sinLat = Math.sin(lat * Math.PI / 180);
22
+ return {
23
+ x: (lng + 180) / 360,
24
+ y: .5 - Math.log((1 + sinLat) / (1 - sinLat)) / (4 * Math.PI)
25
+ };
26
+ }
27
+ var SnowGPU = class {
28
+ renderer = null;
29
+ scene = null;
30
+ camera = null;
31
+ particleCount = DEFAULT_PARTICLE_COUNT;
32
+ posBuffer = null;
33
+ velBuffer = null;
34
+ snowMesh = null;
35
+ computeInit = null;
36
+ computeUpdate = null;
37
+ uCenter = uniform(new THREE.Vector2(.5, .5));
38
+ uHalfSpan = uniform(.005);
39
+ uAltSpan = uniform(.0025);
40
+ uRadius = uniform(1e-6);
41
+ uFallSpeed = uniform(0);
42
+ uWindX = uniform(0);
43
+ uWindY = uniform(0);
44
+ uOpacity = uniform(.8);
45
+ uColor = uniform(new THREE.Color(1, 1, 1));
46
+ fogMesh = null;
47
+ uFogOpacity = uniform(.08);
48
+ _fogEnabled = true;
49
+ initialized = false;
50
+ resizeObserver = null;
51
+ async init(overlayCanvas) {
52
+ if (!navigator.gpu) {
53
+ console.warn("[maplibre-gl-snow] WebGPU not supported");
54
+ return false;
55
+ }
56
+ try {
57
+ this.renderer = new THREE.WebGPURenderer({
58
+ canvas: overlayCanvas,
59
+ antialias: false,
60
+ alpha: true
61
+ });
62
+ this.renderer.setClearColor(0, 0);
63
+ this.renderer.setPixelRatio(window.devicePixelRatio);
64
+ await this.renderer.init();
65
+ this.scene = new THREE.Scene();
66
+ this.camera = new THREE.PerspectiveCamera(60, 1, .001, 1e3);
67
+ this.camera.matrixAutoUpdate = false;
68
+ this.camera.matrixWorld.identity();
69
+ this.camera.matrixWorldInverse.identity();
70
+ this.camera.updateProjectionMatrix = () => {};
71
+ this._buildParticleSystem();
72
+ this._buildFogOverlay();
73
+ this.initialized = true;
74
+ return true;
75
+ } catch (err) {
76
+ console.error("[maplibre-gl-snow] WebGPU init failed:", err);
77
+ return false;
78
+ }
79
+ }
80
+ _buildParticleSystem() {
81
+ if (!this.renderer || !this.scene) return;
82
+ const N = this.particleCount;
83
+ this.posBuffer = instancedArray(N, "vec3");
84
+ this.velBuffer = instancedArray(N, "vec4");
85
+ const randUint = () => uint(Math.floor(Math.random() * 16777215));
86
+ const posBuffer = this.posBuffer;
87
+ const velBuffer = this.velBuffer;
88
+ const uCenter = this.uCenter;
89
+ const uHalfSpan = this.uHalfSpan;
90
+ const uAltSpan = this.uAltSpan;
91
+ const uFallSpeed = this.uFallSpeed;
92
+ this.computeInit = Fn(() => {
93
+ const pos = posBuffer.element(instanceIndex);
94
+ const vel = velBuffer.element(instanceIndex);
95
+ const rx = hash(instanceIndex);
96
+ const ry = hash(instanceIndex.add(randUint()));
97
+ const rz = hash(instanceIndex.add(randUint()));
98
+ const rs = hash(instanceIndex.add(randUint()));
99
+ pos.x = uCenter.x.add(rx.mul(uHalfSpan.mul(2)).sub(uHalfSpan));
100
+ pos.y = uCenter.y.add(ry.mul(uHalfSpan.mul(2)).sub(uHalfSpan));
101
+ pos.z = rz.mul(uAltSpan);
102
+ vel.x = float(0);
103
+ vel.y = float(0);
104
+ vel.z = rs.mul(.5).add(.5);
105
+ vel.w = rs;
106
+ })().compute(N);
107
+ const uWindX = this.uWindX;
108
+ const uWindY = this.uWindY;
109
+ this.computeUpdate = Fn(() => {
110
+ const pos = posBuffer.element(instanceIndex);
111
+ const vel = velBuffer.element(instanceIndex);
112
+ pos.x = pos.x.add(uWindX);
113
+ pos.y = pos.y.add(uWindY);
114
+ pos.z = pos.z.sub(uFallSpeed.mul(vel.z));
115
+ If(pos.z.lessThan(float(0)), () => {
116
+ const rx2 = hash(instanceIndex.add(uint(Math.floor(Math.random() * 16777215))));
117
+ const ry2 = hash(instanceIndex.add(uint(Math.floor(Math.random() * 16777215))));
118
+ pos.x = uCenter.x.add(rx2.mul(uHalfSpan.mul(2)).sub(uHalfSpan));
119
+ pos.y = uCenter.y.add(ry2.mul(uHalfSpan.mul(2)).sub(uHalfSpan));
120
+ pos.z = uAltSpan;
121
+ });
122
+ })().compute(N);
123
+ const geometry = new THREE.SphereGeometry(1, 12, 12);
124
+ const material = new THREE.MeshBasicNodeMaterial({
125
+ transparent: true,
126
+ depthWrite: false,
127
+ depthTest: false
128
+ });
129
+ const uRadius = this.uRadius;
130
+ const uColor = this.uColor;
131
+ const uOpacity = this.uOpacity;
132
+ material.positionNode = positionLocal.mul(uRadius).add(posBuffer.toAttribute());
133
+ material.colorNode = vec4(uColor, uOpacity);
134
+ this.snowMesh = new THREE.Mesh(geometry, material);
135
+ this.snowMesh.count = N;
136
+ this.snowMesh.frustumCulled = false;
137
+ this.scene.add(this.snowMesh);
138
+ }
139
+ _buildFogOverlay() {
140
+ if (!this.scene) return;
141
+ const geo = new THREE.PlaneGeometry(2, 2);
142
+ const mat = new THREE.MeshBasicNodeMaterial({
143
+ transparent: true,
144
+ depthWrite: false,
145
+ depthTest: false
146
+ });
147
+ const uFogOpacity = this.uFogOpacity;
148
+ const vignette = screenUV.distance(float(.5)).mul(2).saturate();
149
+ mat.colorNode = vec4(color(13690103), vignette.mul(uFogOpacity));
150
+ this.fogMesh = new THREE.Mesh(geo, mat);
151
+ this.fogMesh.frustumCulled = false;
152
+ this.fogMesh.renderOrder = 999;
153
+ this.fogMesh.visible = this._fogEnabled;
154
+ this.scene.add(this.fogMesh);
155
+ }
156
+ frame(projMatrix) {
157
+ if (!this.renderer || !this.scene || !this.camera || !this.initialized) return;
158
+ this.camera.projectionMatrix.fromArray(projMatrix);
159
+ this.camera.projectionMatrixInverse.copy(this.camera.projectionMatrix).invert();
160
+ if (this.computeUpdate) this.renderer.compute(this.computeUpdate);
161
+ this.renderer.render(this.scene, this.camera);
162
+ }
163
+ _initRan = false;
164
+ runInit() {
165
+ if (this._initRan || !this.renderer || !this.computeInit) return;
166
+ this.renderer.compute(this.computeInit);
167
+ this._initRan = true;
168
+ }
169
+ updateSpatial(mercX, mercY, zoom, canvasCSSWidth) {
170
+ const halfSpan = canvasCSSWidth * (1 / (512 * Math.pow(2, zoom))) * 1.2;
171
+ const altSpan = halfSpan * .5;
172
+ this.uCenter.value.set(mercX, mercY);
173
+ this.uHalfSpan.value = halfSpan;
174
+ this.uAltSpan.value = altSpan;
175
+ }
176
+ updateFlakeRadius(flakeSizePx, zoom, dpr) {
177
+ const pxToMerc = 1 / (512 * Math.pow(2, zoom));
178
+ this.uRadius.value = flakeSizePx * pxToMerc * dpr * .5;
179
+ }
180
+ updateWind(azimuthDeg, speedPxPerSec, zoom, fps) {
181
+ const pxToMerc = 1 / (512 * Math.pow(2, zoom));
182
+ const azRad = azimuthDeg * Math.PI / 180;
183
+ const mercSpeedPerFrame = speedPxPerSec * pxToMerc / fps;
184
+ this.uWindX.value = Math.sin(azRad) * mercSpeedPerFrame;
185
+ this.uWindY.value = Math.cos(azRad) * mercSpeedPerFrame;
186
+ }
187
+ updateFallSpeed(intensity, zoom, fps) {
188
+ const pxToMerc = 1 / (512 * Math.pow(2, zoom));
189
+ this.uFallSpeed.value = 40 * intensity * pxToMerc / fps;
190
+ }
191
+ setDensity(density) {
192
+ const newCount = Math.round(MIN_PARTICLE_COUNT + Math.max(0, Math.min(1, density)) * (MAX_PARTICLE_COUNT - MIN_PARTICLE_COUNT));
193
+ if (newCount === this.particleCount) return;
194
+ this.particleCount = newCount;
195
+ this._rebuildParticles();
196
+ }
197
+ _rebuildParticles() {
198
+ if (!this.scene) return;
199
+ if (this.snowMesh) {
200
+ this.scene.remove(this.snowMesh);
201
+ this.snowMesh.geometry.dispose();
202
+ this.snowMesh.material.dispose();
203
+ }
204
+ this._initRan = false;
205
+ this._buildParticleSystem();
206
+ }
207
+ setOpacity(value) {
208
+ this.uOpacity.value = Math.max(0, Math.min(1, value));
209
+ }
210
+ setColor(r, g, b) {
211
+ this.uColor.value.setRGB(r, g, b);
212
+ }
213
+ setFog(enabled) {
214
+ this._fogEnabled = enabled;
215
+ if (this.fogMesh) this.fogMesh.visible = enabled;
216
+ }
217
+ setFogOpacity(value) {
218
+ this.uFogOpacity.value = Math.max(0, Math.min(1, value));
219
+ }
220
+ resize(cssWidth, cssHeight) {
221
+ if (!this.renderer) return;
222
+ this.renderer.setSize(cssWidth, cssHeight);
223
+ }
224
+ dispose() {
225
+ if (this.snowMesh) {
226
+ this.snowMesh.geometry.dispose();
227
+ this.snowMesh.material.dispose();
228
+ }
229
+ if (this.fogMesh) {
230
+ this.fogMesh.geometry.dispose();
231
+ this.fogMesh.material.dispose();
232
+ }
233
+ this.renderer?.dispose();
234
+ this.resizeObserver?.disconnect();
235
+ }
236
+ get ready() {
237
+ return this.initialized;
238
+ }
239
+ };
240
+ var MaplibreSnowLayer = class {
241
+ id;
242
+ type = "custom";
243
+ renderingMode = "3d";
244
+ map = null;
245
+ overlayCanvas = null;
246
+ overlayDiv = null;
247
+ gpu = null;
248
+ resizeObserver = null;
249
+ _density;
250
+ _intensity;
251
+ _flakeSize;
252
+ _opacity;
253
+ _direction;
254
+ _fog;
255
+ _fogOpacity;
256
+ _lastFrameTime = 0;
257
+ _fps = 60;
258
+ constructor(options = {}) {
259
+ this.id = options.id ?? "snow";
260
+ this._density = options.density ?? .5;
261
+ this._intensity = options.intensity ?? .5;
262
+ this._flakeSize = options.flakeSize ?? 4;
263
+ this._opacity = options.opacity ?? .8;
264
+ this._direction = options.direction ?? [0, 50];
265
+ this._fog = options.fog ?? true;
266
+ this._fogOpacity = options.fogOpacity ?? .08;
267
+ }
268
+ onAdd(map, _gl) {
269
+ this.map = map;
270
+ const container = map.getContainer();
271
+ container.style.position = "relative";
272
+ this.overlayDiv = document.createElement("div");
273
+ Object.assign(this.overlayDiv.style, {
274
+ position: "absolute",
275
+ top: "0",
276
+ left: "0",
277
+ width: "100%",
278
+ height: "100%",
279
+ pointerEvents: "none",
280
+ zIndex: "10"
281
+ });
282
+ this.overlayCanvas = document.createElement("canvas");
283
+ Object.assign(this.overlayCanvas.style, {
284
+ width: "100%",
285
+ height: "100%",
286
+ display: "block"
287
+ });
288
+ this.overlayDiv.appendChild(this.overlayCanvas);
289
+ container.appendChild(this.overlayDiv);
290
+ const dpr = window.devicePixelRatio;
291
+ this.overlayCanvas.width = container.clientWidth * dpr;
292
+ this.overlayCanvas.height = container.clientHeight * dpr;
293
+ this.resizeObserver = new ResizeObserver(() => {
294
+ if (!this.overlayCanvas || !this.map) return;
295
+ const c = this.map.getContainer();
296
+ const d = window.devicePixelRatio;
297
+ this.overlayCanvas.width = c.clientWidth * d;
298
+ this.overlayCanvas.height = c.clientHeight * d;
299
+ this.gpu?.resize(c.clientWidth, c.clientHeight);
300
+ });
301
+ this.resizeObserver.observe(container);
302
+ this.gpu = new SnowGPU();
303
+ this.gpu.init(this.overlayCanvas).then((ok) => {
304
+ if (!ok) return;
305
+ this.gpu.setDensity(this._density);
306
+ this.gpu.setOpacity(this._opacity);
307
+ this.gpu.setFog(this._fog);
308
+ this.gpu.setFogOpacity(this._fogOpacity);
309
+ });
310
+ }
311
+ render(_gl, args) {
312
+ if (!this.gpu?.ready || !this.map) return;
313
+ const now = performance.now();
314
+ const dt = now - this._lastFrameTime;
315
+ if (this._lastFrameTime > 0 && dt > 0) {
316
+ const instantFps = 1e3 / dt;
317
+ this._fps = this._fps * .9 + instantFps * .1;
318
+ }
319
+ this._lastFrameTime = now;
320
+ const fps = Math.max(10, Math.min(120, this._fps));
321
+ const zoom = this.map.getZoom();
322
+ const center = this.map.getCenter();
323
+ const dpr = window.devicePixelRatio;
324
+ const cssW = this.map.getContainer().clientWidth;
325
+ const merc = lngLatToMercator(center.lng, center.lat);
326
+ this.gpu.updateSpatial(merc.x, merc.y, zoom, cssW);
327
+ this.gpu.updateFlakeRadius(this._flakeSize, zoom, dpr);
328
+ this.gpu.updateWind(this._direction[0], this._direction[1], zoom, fps);
329
+ this.gpu.updateFallSpeed(this._intensity, zoom, fps);
330
+ this.gpu.runInit();
331
+ this.gpu.frame(new Float32Array(args.defaultProjectionData.mainMatrix));
332
+ this.map.triggerRepaint();
333
+ }
334
+ onRemove(_map, _gl) {
335
+ this.resizeObserver?.disconnect();
336
+ this.gpu?.dispose();
337
+ if (this.overlayDiv?.parentElement) this.overlayDiv.parentElement.removeChild(this.overlayDiv);
338
+ this.map = null;
339
+ this.overlayDiv = null;
340
+ this.overlayCanvas = null;
341
+ this.gpu = null;
342
+ }
343
+ setDensity(density) {
344
+ this._density = density;
345
+ this.gpu?.setDensity(density);
346
+ }
347
+ setIntensity(intensity) {
348
+ this._intensity = Math.max(0, Math.min(1, intensity));
349
+ }
350
+ setFlakeSize(size) {
351
+ this._flakeSize = size;
352
+ }
353
+ setOpacity(opacity) {
354
+ this._opacity = opacity;
355
+ this.gpu?.setOpacity(opacity);
356
+ }
357
+ setDirection(direction) {
358
+ this._direction = direction;
359
+ }
360
+ setFog(enabled) {
361
+ this._fog = enabled;
362
+ this.gpu?.setFog(enabled);
363
+ }
364
+ setFogOpacity(opacity) {
365
+ this._fogOpacity = opacity;
366
+ this.gpu?.setFogOpacity(opacity);
367
+ }
368
+ };
369
+
370
+ //#endregion
371
+ export { MaplibreSnowLayer };
372
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * @geoql/maplibre-gl-snow\n *\n * WebGPU-accelerated snow particle layer for MapLibre GL JS.\n * Uses Three.js WebGPU renderer with TSL compute shaders.\n *\n * Architecture:\n * - Two-canvas overlay: MapLibre (WebGL2) + Three.js WebGPU overlay\n * - Particles georeferenced as (mercX, mercY, altMerc) in mercator [0,1] space\n * - Camera syncs to MapLibre's mercator projection matrix directly\n * - MapLibre drives the frame loop via triggerRepaint()\n */\n\nimport * as THREE from 'three/webgpu';\nimport {\n Fn,\n vec4,\n float,\n uint,\n instanceIndex,\n instancedArray,\n positionLocal,\n uniform,\n hash,\n If,\n color,\n screenUV,\n} from 'three/tsl';\nimport type { CustomRenderMethodInput, Map as MaplibreMap } from 'maplibre-gl';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface MaplibreSnowOptions {\n /** Unique layer ID (default: 'snow') */\n id?: string;\n /** Particle density 0–1 (default: 0.5) */\n density?: number;\n /** Fall speed intensity 0–1 (default: 0.5) */\n intensity?: number;\n /** Base flake size in CSS pixels (default: 4) */\n flakeSize?: number;\n /** Global opacity 0–1 (default: 0.8) */\n opacity?: number;\n /** Wind as [azimuth degrees, horizontal speed px/s] (default: [0, 50]) */\n direction?: [number, number];\n /** Enable atmospheric fog overlay (default: true) */\n fog?: boolean;\n /** Fog opacity 0–1 (default: 0.08) */\n fogOpacity?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_PARTICLE_COUNT = 100_000;\nconst MIN_PARTICLE_COUNT = 10_000;\nconst MAX_PARTICLE_COUNT = 200_000;\n\n// ---------------------------------------------------------------------------\n// Mercator helpers (no maplibre-gl import needed)\n// ---------------------------------------------------------------------------\n\nfunction lngLatToMercator(lng: number, lat: number): { x: number; y: number } {\n const sinLat = Math.sin((lat * Math.PI) / 180);\n return {\n x: (lng + 180) / 360,\n y: 0.5 - Math.log((1 + sinLat) / (1 - sinLat)) / (4 * Math.PI),\n };\n}\n\n// ---------------------------------------------------------------------------\n// WebGPU Particle System\n// ---------------------------------------------------------------------------\n\nclass SnowGPU {\n private renderer: THREE.WebGPURenderer | null = null;\n private scene: THREE.Scene | null = null;\n private camera: THREE.PerspectiveCamera | null = null;\n\n // Particle state\n private particleCount = DEFAULT_PARTICLE_COUNT;\n private posBuffer: ReturnType<typeof instancedArray> | null = null;\n private velBuffer: ReturnType<typeof instancedArray> | null = null;\n private snowMesh: THREE.Mesh | null = null;\n private computeInit: ReturnType<ReturnType<typeof Fn>['compute']> | null =\n null;\n private computeUpdate: ReturnType<ReturnType<typeof Fn>['compute']> | null =\n null;\n\n // Uniforms (updated every frame from render callback)\n private uCenter = uniform(new THREE.Vector2(0.5, 0.5));\n private uHalfSpan = uniform(0.005);\n private uAltSpan = uniform(0.0025);\n private uRadius = uniform(1e-6);\n private uFallSpeed = uniform(0.0);\n private uWindX = uniform(0.0);\n private uWindY = uniform(0.0);\n private uOpacity = uniform(0.8);\n private uColor = uniform(new THREE.Color(1, 1, 1));\n\n // Fog overlay\n private fogMesh: THREE.Mesh | null = null;\n private uFogOpacity = uniform(0.08);\n private _fogEnabled = true;\n\n private initialized = false;\n private resizeObserver: ResizeObserver | null = null;\n\n async init(overlayCanvas: HTMLCanvasElement): Promise<boolean> {\n if (!(navigator as unknown as { gpu?: unknown }).gpu) {\n console.warn('[maplibre-gl-snow] WebGPU not supported');\n return false;\n }\n\n try {\n this.renderer = new THREE.WebGPURenderer({\n canvas: overlayCanvas,\n antialias: false,\n alpha: true,\n });\n this.renderer.setClearColor(0x000000, 0);\n this.renderer.setPixelRatio(window.devicePixelRatio);\n await this.renderer.init();\n\n this.scene = new THREE.Scene();\n\n // PerspectiveCamera — projection matrix is driven by MapLibre, not Three.js internals.\n // Override updateProjectionMatrix to no-op so the renderer does not\n // clobber our manually-set projection matrix from MapLibre's mercator projection.\n this.camera = new THREE.PerspectiveCamera(60, 1, 0.001, 1000);\n this.camera.matrixAutoUpdate = false;\n this.camera.matrixWorld.identity();\n this.camera.matrixWorldInverse.identity();\n // Prevent Three.js renderer from overwriting our projection matrix\n this.camera.updateProjectionMatrix = () => {};\n\n this._buildParticleSystem();\n this._buildFogOverlay();\n\n this.initialized = true;\n return true;\n } catch (err) {\n console.error('[maplibre-gl-snow] WebGPU init failed:', err);\n return false;\n }\n }\n\n // -------------------------------------------------------------------------\n // Particle system construction\n // -------------------------------------------------------------------------\n\n private _buildParticleSystem(): void {\n if (!this.renderer || !this.scene) return;\n\n const N = this.particleCount;\n\n // Storage buffers\n // posBuffer: vec3(mercX, mercY, mercAlt) — mercator [0,1] space\n // velBuffer: vec4(vx, vy, valt, seed)\n this.posBuffer = instancedArray(N, 'vec3');\n this.velBuffer = instancedArray(N, 'vec4');\n\n const randUint = () => uint(Math.floor(Math.random() * 0xffffff));\n\n // ----- computeInit -----\n const posBuffer = this.posBuffer;\n const velBuffer = this.velBuffer;\n const uCenter = this.uCenter;\n const uHalfSpan = this.uHalfSpan;\n const uAltSpan = this.uAltSpan;\n const uFallSpeed = this.uFallSpeed;\n\n const initFn = Fn(() => {\n const pos = posBuffer.element(instanceIndex);\n const vel = velBuffer.element(instanceIndex);\n\n const rx = hash(instanceIndex);\n const ry = hash(instanceIndex.add(randUint()));\n const rz = hash(instanceIndex.add(randUint()));\n const rs = hash(instanceIndex.add(randUint()));\n\n // Spawn in a box centered at uCenter\n // Spawn in a box centered at uCenter, scattered vertically\n pos.x = uCenter.x.add(rx.mul(uHalfSpan.mul(2.0)).sub(uHalfSpan));\n pos.y = uCenter.y.add(ry.mul(uHalfSpan.mul(2.0)).sub(uHalfSpan));\n pos.z = rz.mul(uAltSpan); // altitude [0, altSpan]\n // vel.z is a per-particle random multiplier [0.5, 1.0].\n // Actual fall delta = uFallSpeed * vel.z, computed live in update shader.\n vel.x = float(0.0);\n vel.y = float(0.0);\n vel.z = rs.mul(0.5).add(0.5); // random speed multiplier, NOT absolute speed\n vel.w = rs; // random seed for drift\n });\n\n this.computeInit = initFn().compute(N);\n\n // ----- computeUpdate -----\n const uWindX = this.uWindX;\n const uWindY = this.uWindY;\n\n const updateFn = Fn(() => {\n const pos = posBuffer.element(instanceIndex);\n const vel = velBuffer.element(instanceIndex);\n // Fall: actual delta = uFallSpeed * per-particle multiplier (vel.z)\n // Wind: uWindX / uWindY are already in merc-units/frame\n pos.x = pos.x.add(uWindX);\n pos.y = pos.y.add(uWindY);\n pos.z = pos.z.sub(uFallSpeed.mul(vel.z));\n\n // Only respawn when particle has fallen below ground (pos.z < 0).\n // No horizontal OOB check — that caused jitter when uCenter moved each frame.\n If(pos.z.lessThan(float(0.0)), () => {\n const rx2 = hash(instanceIndex.add(uint(Math.floor(Math.random() * 0xffffff))));\n const ry2 = hash(instanceIndex.add(uint(Math.floor(Math.random() * 0xffffff))));\n pos.x = uCenter.x.add(rx2.mul(uHalfSpan.mul(2.0)).sub(uHalfSpan));\n pos.y = uCenter.y.add(ry2.mul(uHalfSpan.mul(2.0)).sub(uHalfSpan));\n pos.z = uAltSpan; // respawn at top\n });\n });\n\n this.computeUpdate = updateFn().compute(N);\n\n // ----- Snow flake mesh -----\n const geometry = new THREE.SphereGeometry(1, 12, 12);\n\n const material = new THREE.MeshBasicNodeMaterial({\n transparent: true,\n depthWrite: false,\n depthTest: false,\n });\n\n const uRadius = this.uRadius;\n const uColor = this.uColor;\n const uOpacity = this.uOpacity;\n\n // positionNode: scale unit sphere by uRadius, offset by particle mercator position\n material.positionNode = positionLocal\n .mul(uRadius)\n .add(posBuffer.toAttribute());\n\n material.colorNode = vec4(uColor, uOpacity);\n\n this.snowMesh = new THREE.Mesh(geometry, material);\n this.snowMesh.count = N;\n this.snowMesh.frustumCulled = false;\n\n this.scene.add(this.snowMesh);\n }\n\n private _buildFogOverlay(): void {\n if (!this.scene) return;\n\n // Fullscreen quad using NDC-space plane\n const geo = new THREE.PlaneGeometry(2, 2);\n const mat = new THREE.MeshBasicNodeMaterial({\n transparent: true,\n depthWrite: false,\n depthTest: false,\n });\n\n const uFogOpacity = this.uFogOpacity;\n\n // Radial vignette fog (blue-white tint)\n const vignette = screenUV.distance(float(0.5)).mul(2.0).saturate();\n mat.colorNode = vec4(\n color(0xd0e4f7), // cool blue-white\n vignette.mul(uFogOpacity),\n );\n\n this.fogMesh = new THREE.Mesh(geo, mat);\n this.fogMesh.frustumCulled = false;\n this.fogMesh.renderOrder = 999;\n this.fogMesh.visible = this._fogEnabled;\n\n this.scene.add(this.fogMesh);\n }\n\n // -------------------------------------------------------------------------\n // Called by MaplibreSnowLayer.render() every frame\n // -------------------------------------------------------------------------\n\n frame(projMatrix: Float32Array): void {\n if (!this.renderer || !this.scene || !this.camera || !this.initialized)\n return;\n\n // Apply MapLibre's mercator projection matrix directly\n this.camera.projectionMatrix.fromArray(projMatrix);\n this.camera.projectionMatrixInverse\n .copy(this.camera.projectionMatrix)\n .invert();\n\n // Run compute\n if (this.computeUpdate) {\n this.renderer.compute(this.computeUpdate);\n }\n\n // Render\n this.renderer.render(this.scene, this.camera);\n }\n\n // Called once on first render to initialize particle positions\n private _initRan = false;\n runInit(): void {\n if (this._initRan || !this.renderer || !this.computeInit) return;\n this.renderer.compute(this.computeInit);\n this._initRan = true;\n }\n\n // -------------------------------------------------------------------------\n // Uniform updates (called from MaplibreSnowLayer)\n // -------------------------------------------------------------------------\n\n updateSpatial(\n mercX: number,\n mercY: number,\n zoom: number,\n canvasCSSWidth: number,\n ): void {\n // Pixels per mercator unit at this zoom\n const pxToMerc = 1 / (512 * Math.pow(2, zoom));\n const halfSpan = canvasCSSWidth * pxToMerc * 1.2; // 120% of viewport width\n const altSpan = halfSpan * 0.5;\n\n this.uCenter.value.set(mercX, mercY);\n this.uHalfSpan.value = halfSpan;\n this.uAltSpan.value = altSpan;\n // Flake radius: stored separately, updated via setFlakeSize\n // (radius doesn't change per-frame unless user changes it)\n }\n\n updateFlakeRadius(flakeSizePx: number, zoom: number, dpr: number): void {\n const pxToMerc = 1 / (512 * Math.pow(2, zoom));\n this.uRadius.value = flakeSizePx * pxToMerc * dpr * 0.5;\n }\n\n updateWind(\n azimuthDeg: number,\n speedPxPerSec: number,\n zoom: number,\n fps: number,\n ): void {\n const pxToMerc = 1 / (512 * Math.pow(2, zoom));\n const azRad = (azimuthDeg * Math.PI) / 180;\n const mercSpeedPerFrame = (speedPxPerSec * pxToMerc) / fps;\n this.uWindX.value = Math.sin(azRad) * mercSpeedPerFrame;\n // mercY increases downward (southward), so negate cosine for northward component\n this.uWindY.value = Math.cos(azRad) * mercSpeedPerFrame;\n }\n\n updateFallSpeed(intensity: number, zoom: number, fps: number): void {\n const pxToMerc = 1 / (512 * Math.pow(2, zoom));\n // base fall = 40 px/s * intensity, converted to merc/frame\n this.uFallSpeed.value = (40 * intensity * pxToMerc) / fps;\n }\n\n // -------------------------------------------------------------------------\n // Public API setters\n // -------------------------------------------------------------------------\n\n setDensity(density: number): void {\n const newCount = Math.round(\n MIN_PARTICLE_COUNT +\n Math.max(0, Math.min(1, density)) *\n (MAX_PARTICLE_COUNT - MIN_PARTICLE_COUNT),\n );\n if (newCount === this.particleCount) return;\n this.particleCount = newCount;\n this._rebuildParticles();\n }\n\n private _rebuildParticles(): void {\n if (!this.scene) return;\n if (this.snowMesh) {\n this.scene.remove(this.snowMesh);\n this.snowMesh.geometry.dispose();\n (this.snowMesh.material as THREE.Material).dispose();\n }\n this._initRan = false;\n this._buildParticleSystem();\n // Re-run init on next frame\n }\n\n setOpacity(value: number): void {\n this.uOpacity.value = Math.max(0, Math.min(1, value));\n }\n\n setColor(r: number, g: number, b: number): void {\n this.uColor.value.setRGB(r, g, b);\n }\n\n setFog(enabled: boolean): void {\n this._fogEnabled = enabled;\n if (this.fogMesh) this.fogMesh.visible = enabled;\n }\n\n setFogOpacity(value: number): void {\n this.uFogOpacity.value = Math.max(0, Math.min(1, value));\n }\n\n resize(cssWidth: number, cssHeight: number): void {\n if (!this.renderer) return;\n this.renderer.setSize(cssWidth, cssHeight);\n }\n\n dispose(): void {\n if (this.snowMesh) {\n this.snowMesh.geometry.dispose();\n (this.snowMesh.material as THREE.Material).dispose();\n }\n if (this.fogMesh) {\n this.fogMesh.geometry.dispose();\n (this.fogMesh.material as THREE.Material).dispose();\n }\n this.renderer?.dispose();\n this.resizeObserver?.disconnect();\n }\n\n get ready(): boolean {\n return this.initialized;\n }\n}\n\n// ---------------------------------------------------------------------------\n// MapLibre Custom Layer\n// ---------------------------------------------------------------------------\n\nclass MaplibreSnowLayer {\n id: string;\n readonly type = 'custom' as const;\n readonly renderingMode = '3d' as const;\n\n private map: MaplibreMap | null = null;\n private overlayCanvas: HTMLCanvasElement | null = null;\n private overlayDiv: HTMLDivElement | null = null;\n private gpu: SnowGPU | null = null;\n private resizeObserver: ResizeObserver | null = null;\n\n // Options\n private _density: number;\n private _intensity: number;\n private _flakeSize: number;\n private _opacity: number;\n private _direction: [number, number];\n private _fog: boolean;\n private _fogOpacity: number;\n\n // Frame timing\n private _lastFrameTime = 0;\n private _fps = 60;\n\n constructor(options: MaplibreSnowOptions = {}) {\n this.id = options.id ?? 'snow';\n this._density = options.density ?? 0.5;\n this._intensity = options.intensity ?? 0.5;\n this._flakeSize = options.flakeSize ?? 4;\n this._opacity = options.opacity ?? 0.8;\n this._direction = options.direction ?? [0, 50];\n this._fog = options.fog ?? true;\n this._fogOpacity = options.fogOpacity ?? 0.08;\n }\n\n // -------------------------------------------------------------------------\n // MapLibre lifecycle\n // -------------------------------------------------------------------------\n\n onAdd(map: MaplibreMap, _gl: WebGL2RenderingContext): void {\n this.map = map;\n\n const container = map.getContainer();\n container.style.position = 'relative';\n\n // Overlay div\n this.overlayDiv = document.createElement('div');\n Object.assign(this.overlayDiv.style, {\n position: 'absolute',\n top: '0',\n left: '0',\n width: '100%',\n height: '100%',\n pointerEvents: 'none',\n zIndex: '10',\n });\n\n // Overlay canvas\n this.overlayCanvas = document.createElement('canvas');\n Object.assign(this.overlayCanvas.style, {\n width: '100%',\n height: '100%',\n display: 'block',\n });\n\n this.overlayDiv.appendChild(this.overlayCanvas);\n container.appendChild(this.overlayDiv);\n\n // Physical canvas size\n const dpr = window.devicePixelRatio;\n this.overlayCanvas.width = container.clientWidth * dpr;\n this.overlayCanvas.height = container.clientHeight * dpr;\n\n // Resize observer\n this.resizeObserver = new ResizeObserver(() => {\n if (!this.overlayCanvas || !this.map) return;\n const c = this.map.getContainer();\n const d = window.devicePixelRatio;\n this.overlayCanvas.width = c.clientWidth * d;\n this.overlayCanvas.height = c.clientHeight * d;\n this.gpu?.resize(c.clientWidth, c.clientHeight);\n });\n this.resizeObserver.observe(container);\n\n // Init WebGPU async\n this.gpu = new SnowGPU();\n this.gpu.init(this.overlayCanvas).then((ok) => {\n if (!ok) return;\n // Apply initial options after init\n this.gpu!.setDensity(this._density);\n this.gpu!.setOpacity(this._opacity);\n this.gpu!.setFog(this._fog);\n this.gpu!.setFogOpacity(this._fogOpacity);\n });\n }\n\n render(_gl: WebGL2RenderingContext, args: CustomRenderMethodInput): void {\n if (!this.gpu?.ready || !this.map) return;\n const now = performance.now();\n const dt = now - this._lastFrameTime;\n if (this._lastFrameTime > 0 && dt > 0) {\n const instantFps = 1000 / dt;\n this._fps = this._fps * 0.9 + instantFps * 0.1;\n }\n this._lastFrameTime = now;\n const fps = Math.max(10, Math.min(120, this._fps));\n const zoom = this.map.getZoom();\n const center = this.map.getCenter();\n const dpr = window.devicePixelRatio;\n const cssW = this.map.getContainer().clientWidth;\n const merc = lngLatToMercator(center.lng, center.lat);\n this.gpu.updateSpatial(merc.x, merc.y, zoom, cssW);\n this.gpu.updateFlakeRadius(this._flakeSize, zoom, dpr);\n this.gpu.updateWind(this._direction[0], this._direction[1], zoom, fps);\n this.gpu.updateFallSpeed(this._intensity, zoom, fps);\n this.gpu.runInit();\n this.gpu.frame(new Float32Array(args.defaultProjectionData.mainMatrix));\n this.map.triggerRepaint();\n }\n\n\n onRemove(_map: MaplibreMap, _gl: WebGL2RenderingContext): void {\n this.resizeObserver?.disconnect();\n this.gpu?.dispose();\n\n if (this.overlayDiv?.parentElement) {\n this.overlayDiv.parentElement.removeChild(this.overlayDiv);\n }\n\n this.map = null;\n this.overlayDiv = null;\n this.overlayCanvas = null;\n this.gpu = null;\n }\n\n // -------------------------------------------------------------------------\n // Public API\n // -------------------------------------------------------------------------\n\n setDensity(density: number): void {\n this._density = density;\n this.gpu?.setDensity(density);\n }\n\n setIntensity(intensity: number): void {\n this._intensity = Math.max(0, Math.min(1, intensity));\n }\n\n setFlakeSize(size: number): void {\n this._flakeSize = size;\n }\n\n setOpacity(opacity: number): void {\n this._opacity = opacity;\n this.gpu?.setOpacity(opacity);\n }\n\n setDirection(direction: [number, number]): void {\n this._direction = direction;\n }\n\n setFog(enabled: boolean): void {\n this._fog = enabled;\n this.gpu?.setFog(enabled);\n }\n\n setFogOpacity(opacity: number): void {\n this._fogOpacity = opacity;\n this.gpu?.setFogOpacity(opacity);\n }\n}\n\nexport { MaplibreSnowLayer };\n"],"mappings":";;;;;;;;;;;;;;;;AAyDA,MAAM,yBAAyB;AAC/B,MAAM,qBAAqB;AAC3B,MAAM,qBAAqB;AAM3B,SAAS,iBAAiB,KAAa,KAAuC;CAC5E,MAAM,SAAS,KAAK,IAAK,MAAM,KAAK,KAAM,IAAI;AAC9C,QAAO;EACL,IAAI,MAAM,OAAO;EACjB,GAAG,KAAM,KAAK,KAAK,IAAI,WAAW,IAAI,QAAQ,IAAI,IAAI,KAAK;EAC5D;;AAOH,IAAM,UAAN,MAAc;CACZ,AAAQ,WAAwC;CAChD,AAAQ,QAA4B;CACpC,AAAQ,SAAyC;CAGjD,AAAQ,gBAAgB;CACxB,AAAQ,YAAsD;CAC9D,AAAQ,YAAsD;CAC9D,AAAQ,WAA8B;CACtC,AAAQ,cACN;CACF,AAAQ,gBACN;CAGF,AAAQ,UAAU,QAAQ,IAAI,MAAM,QAAQ,IAAK,GAAI,CAAC;CACtD,AAAQ,YAAY,QAAQ,KAAM;CAClC,AAAQ,WAAW,QAAQ,MAAO;CAClC,AAAQ,UAAU,QAAQ,KAAK;CAC/B,AAAQ,aAAa,QAAQ,EAAI;CACjC,AAAQ,SAAS,QAAQ,EAAI;CAC7B,AAAQ,SAAS,QAAQ,EAAI;CAC7B,AAAQ,WAAW,QAAQ,GAAI;CAC/B,AAAQ,SAAS,QAAQ,IAAI,MAAM,MAAM,GAAG,GAAG,EAAE,CAAC;CAGlD,AAAQ,UAA6B;CACrC,AAAQ,cAAc,QAAQ,IAAK;CACnC,AAAQ,cAAc;CAEtB,AAAQ,cAAc;CACtB,AAAQ,iBAAwC;CAEhD,MAAM,KAAK,eAAoD;AAC7D,MAAI,CAAE,UAA2C,KAAK;AACpD,WAAQ,KAAK,0CAA0C;AACvD,UAAO;;AAGT,MAAI;AACF,QAAK,WAAW,IAAI,MAAM,eAAe;IACvC,QAAQ;IACR,WAAW;IACX,OAAO;IACR,CAAC;AACF,QAAK,SAAS,cAAc,GAAU,EAAE;AACxC,QAAK,SAAS,cAAc,OAAO,iBAAiB;AACpD,SAAM,KAAK,SAAS,MAAM;AAE1B,QAAK,QAAQ,IAAI,MAAM,OAAO;AAK9B,QAAK,SAAS,IAAI,MAAM,kBAAkB,IAAI,GAAG,MAAO,IAAK;AAC7D,QAAK,OAAO,mBAAmB;AAC/B,QAAK,OAAO,YAAY,UAAU;AAClC,QAAK,OAAO,mBAAmB,UAAU;AAEzC,QAAK,OAAO,+BAA+B;AAE3C,QAAK,sBAAsB;AAC3B,QAAK,kBAAkB;AAEvB,QAAK,cAAc;AACnB,UAAO;WACA,KAAK;AACZ,WAAQ,MAAM,0CAA0C,IAAI;AAC5D,UAAO;;;CAQX,AAAQ,uBAA6B;AACnC,MAAI,CAAC,KAAK,YAAY,CAAC,KAAK,MAAO;EAEnC,MAAM,IAAI,KAAK;AAKf,OAAK,YAAY,eAAe,GAAG,OAAO;AAC1C,OAAK,YAAY,eAAe,GAAG,OAAO;EAE1C,MAAM,iBAAiB,KAAK,KAAK,MAAM,KAAK,QAAQ,GAAG,SAAS,CAAC;EAGjE,MAAM,YAAY,KAAK;EACvB,MAAM,YAAY,KAAK;EACvB,MAAM,UAAU,KAAK;EACrB,MAAM,YAAY,KAAK;EACvB,MAAM,WAAW,KAAK;EACtB,MAAM,aAAa,KAAK;AAwBxB,OAAK,cAtBU,SAAS;GACtB,MAAM,MAAM,UAAU,QAAQ,cAAc;GAC5C,MAAM,MAAM,UAAU,QAAQ,cAAc;GAE5C,MAAM,KAAK,KAAK,cAAc;GAC9B,MAAM,KAAK,KAAK,cAAc,IAAI,UAAU,CAAC,CAAC;GAC9C,MAAM,KAAK,KAAK,cAAc,IAAI,UAAU,CAAC,CAAC;GAC9C,MAAM,KAAK,KAAK,cAAc,IAAI,UAAU,CAAC,CAAC;AAI9C,OAAI,IAAI,QAAQ,EAAE,IAAI,GAAG,IAAI,UAAU,IAAI,EAAI,CAAC,CAAC,IAAI,UAAU,CAAC;AAChE,OAAI,IAAI,QAAQ,EAAE,IAAI,GAAG,IAAI,UAAU,IAAI,EAAI,CAAC,CAAC,IAAI,UAAU,CAAC;AAChE,OAAI,IAAI,GAAG,IAAI,SAAS;AAGxB,OAAI,IAAI,MAAM,EAAI;AAClB,OAAI,IAAI,MAAM,EAAI;AAClB,OAAI,IAAI,GAAG,IAAI,GAAI,CAAC,IAAI,GAAI;AAC5B,OAAI,IAAI;IACR,EAEyB,CAAC,QAAQ,EAAE;EAGtC,MAAM,SAAS,KAAK;EACpB,MAAM,SAAS,KAAK;AAsBpB,OAAK,gBApBY,SAAS;GACxB,MAAM,MAAM,UAAU,QAAQ,cAAc;GAC5C,MAAM,MAAM,UAAU,QAAQ,cAAc;AAG5C,OAAI,IAAI,IAAI,EAAE,IAAI,OAAO;AACzB,OAAI,IAAI,IAAI,EAAE,IAAI,OAAO;AACzB,OAAI,IAAI,IAAI,EAAE,IAAI,WAAW,IAAI,IAAI,EAAE,CAAC;AAIxC,MAAG,IAAI,EAAE,SAAS,MAAM,EAAI,CAAC,QAAQ;IACnC,MAAM,MAAM,KAAK,cAAc,IAAI,KAAK,KAAK,MAAM,KAAK,QAAQ,GAAG,SAAS,CAAC,CAAC,CAAC;IAC/E,MAAM,MAAM,KAAK,cAAc,IAAI,KAAK,KAAK,MAAM,KAAK,QAAQ,GAAG,SAAS,CAAC,CAAC,CAAC;AAC/E,QAAI,IAAI,QAAQ,EAAE,IAAI,IAAI,IAAI,UAAU,IAAI,EAAI,CAAC,CAAC,IAAI,UAAU,CAAC;AACjE,QAAI,IAAI,QAAQ,EAAE,IAAI,IAAI,IAAI,UAAU,IAAI,EAAI,CAAC,CAAC,IAAI,UAAU,CAAC;AACjE,QAAI,IAAI;KACR;IACF,EAE6B,CAAC,QAAQ,EAAE;EAG1C,MAAM,WAAW,IAAI,MAAM,eAAe,GAAG,IAAI,GAAG;EAEpD,MAAM,WAAW,IAAI,MAAM,sBAAsB;GAC/C,aAAa;GACb,YAAY;GACZ,WAAW;GACZ,CAAC;EAEF,MAAM,UAAU,KAAK;EACrB,MAAM,SAAS,KAAK;EACpB,MAAM,WAAW,KAAK;AAGtB,WAAS,eAAe,cACrB,IAAI,QAAQ,CACZ,IAAI,UAAU,aAAa,CAAC;AAE/B,WAAS,YAAY,KAAK,QAAQ,SAAS;AAE3C,OAAK,WAAW,IAAI,MAAM,KAAK,UAAU,SAAS;AAClD,OAAK,SAAS,QAAQ;AACtB,OAAK,SAAS,gBAAgB;AAE9B,OAAK,MAAM,IAAI,KAAK,SAAS;;CAG/B,AAAQ,mBAAyB;AAC/B,MAAI,CAAC,KAAK,MAAO;EAGjB,MAAM,MAAM,IAAI,MAAM,cAAc,GAAG,EAAE;EACzC,MAAM,MAAM,IAAI,MAAM,sBAAsB;GAC1C,aAAa;GACb,YAAY;GACZ,WAAW;GACZ,CAAC;EAEF,MAAM,cAAc,KAAK;EAGzB,MAAM,WAAW,SAAS,SAAS,MAAM,GAAI,CAAC,CAAC,IAAI,EAAI,CAAC,UAAU;AAClE,MAAI,YAAY,KACd,MAAM,SAAS,EACf,SAAS,IAAI,YAAY,CAC1B;AAED,OAAK,UAAU,IAAI,MAAM,KAAK,KAAK,IAAI;AACvC,OAAK,QAAQ,gBAAgB;AAC7B,OAAK,QAAQ,cAAc;AAC3B,OAAK,QAAQ,UAAU,KAAK;AAE5B,OAAK,MAAM,IAAI,KAAK,QAAQ;;CAO9B,MAAM,YAAgC;AACpC,MAAI,CAAC,KAAK,YAAY,CAAC,KAAK,SAAS,CAAC,KAAK,UAAU,CAAC,KAAK,YACzD;AAGF,OAAK,OAAO,iBAAiB,UAAU,WAAW;AAClD,OAAK,OAAO,wBACT,KAAK,KAAK,OAAO,iBAAiB,CAClC,QAAQ;AAGX,MAAI,KAAK,cACP,MAAK,SAAS,QAAQ,KAAK,cAAc;AAI3C,OAAK,SAAS,OAAO,KAAK,OAAO,KAAK,OAAO;;CAI/C,AAAQ,WAAW;CACnB,UAAgB;AACd,MAAI,KAAK,YAAY,CAAC,KAAK,YAAY,CAAC,KAAK,YAAa;AAC1D,OAAK,SAAS,QAAQ,KAAK,YAAY;AACvC,OAAK,WAAW;;CAOlB,cACE,OACA,OACA,MACA,gBACM;EAGN,MAAM,WAAW,kBADA,KAAK,MAAM,KAAK,IAAI,GAAG,KAAK,KACA;EAC7C,MAAM,UAAU,WAAW;AAE3B,OAAK,QAAQ,MAAM,IAAI,OAAO,MAAM;AACpC,OAAK,UAAU,QAAQ;AACvB,OAAK,SAAS,QAAQ;;CAKxB,kBAAkB,aAAqB,MAAc,KAAmB;EACtE,MAAM,WAAW,KAAK,MAAM,KAAK,IAAI,GAAG,KAAK;AAC7C,OAAK,QAAQ,QAAQ,cAAc,WAAW,MAAM;;CAGtD,WACE,YACA,eACA,MACA,KACM;EACN,MAAM,WAAW,KAAK,MAAM,KAAK,IAAI,GAAG,KAAK;EAC7C,MAAM,QAAS,aAAa,KAAK,KAAM;EACvC,MAAM,oBAAqB,gBAAgB,WAAY;AACvD,OAAK,OAAO,QAAQ,KAAK,IAAI,MAAM,GAAG;AAEtC,OAAK,OAAO,QAAQ,KAAK,IAAI,MAAM,GAAG;;CAGxC,gBAAgB,WAAmB,MAAc,KAAmB;EAClE,MAAM,WAAW,KAAK,MAAM,KAAK,IAAI,GAAG,KAAK;AAE7C,OAAK,WAAW,QAAS,KAAK,YAAY,WAAY;;CAOxD,WAAW,SAAuB;EAChC,MAAM,WAAW,KAAK,MACpB,qBACE,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,QAAQ,CAAC,IAC9B,qBAAqB,oBAC3B;AACD,MAAI,aAAa,KAAK,cAAe;AACrC,OAAK,gBAAgB;AACrB,OAAK,mBAAmB;;CAG1B,AAAQ,oBAA0B;AAChC,MAAI,CAAC,KAAK,MAAO;AACjB,MAAI,KAAK,UAAU;AACjB,QAAK,MAAM,OAAO,KAAK,SAAS;AAChC,QAAK,SAAS,SAAS,SAAS;AAChC,GAAC,KAAK,SAAS,SAA4B,SAAS;;AAEtD,OAAK,WAAW;AAChB,OAAK,sBAAsB;;CAI7B,WAAW,OAAqB;AAC9B,OAAK,SAAS,QAAQ,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,MAAM,CAAC;;CAGvD,SAAS,GAAW,GAAW,GAAiB;AAC9C,OAAK,OAAO,MAAM,OAAO,GAAG,GAAG,EAAE;;CAGnC,OAAO,SAAwB;AAC7B,OAAK,cAAc;AACnB,MAAI,KAAK,QAAS,MAAK,QAAQ,UAAU;;CAG3C,cAAc,OAAqB;AACjC,OAAK,YAAY,QAAQ,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,MAAM,CAAC;;CAG1D,OAAO,UAAkB,WAAyB;AAChD,MAAI,CAAC,KAAK,SAAU;AACpB,OAAK,SAAS,QAAQ,UAAU,UAAU;;CAG5C,UAAgB;AACd,MAAI,KAAK,UAAU;AACjB,QAAK,SAAS,SAAS,SAAS;AAChC,GAAC,KAAK,SAAS,SAA4B,SAAS;;AAEtD,MAAI,KAAK,SAAS;AAChB,QAAK,QAAQ,SAAS,SAAS;AAC/B,GAAC,KAAK,QAAQ,SAA4B,SAAS;;AAErD,OAAK,UAAU,SAAS;AACxB,OAAK,gBAAgB,YAAY;;CAGnC,IAAI,QAAiB;AACnB,SAAO,KAAK;;;AAQhB,IAAM,oBAAN,MAAwB;CACtB;CACA,AAAS,OAAO;CAChB,AAAS,gBAAgB;CAEzB,AAAQ,MAA0B;CAClC,AAAQ,gBAA0C;CAClD,AAAQ,aAAoC;CAC5C,AAAQ,MAAsB;CAC9B,AAAQ,iBAAwC;CAGhD,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ;CAGR,AAAQ,iBAAiB;CACzB,AAAQ,OAAO;CAEf,YAAY,UAA+B,EAAE,EAAE;AAC7C,OAAK,KAAK,QAAQ,MAAM;AACxB,OAAK,WAAW,QAAQ,WAAW;AACnC,OAAK,aAAa,QAAQ,aAAa;AACvC,OAAK,aAAa,QAAQ,aAAa;AACvC,OAAK,WAAW,QAAQ,WAAW;AACnC,OAAK,aAAa,QAAQ,aAAa,CAAC,GAAG,GAAG;AAC9C,OAAK,OAAO,QAAQ,OAAO;AAC3B,OAAK,cAAc,QAAQ,cAAc;;CAO3C,MAAM,KAAkB,KAAmC;AACzD,OAAK,MAAM;EAEX,MAAM,YAAY,IAAI,cAAc;AACpC,YAAU,MAAM,WAAW;AAG3B,OAAK,aAAa,SAAS,cAAc,MAAM;AAC/C,SAAO,OAAO,KAAK,WAAW,OAAO;GACnC,UAAU;GACV,KAAK;GACL,MAAM;GACN,OAAO;GACP,QAAQ;GACR,eAAe;GACf,QAAQ;GACT,CAAC;AAGF,OAAK,gBAAgB,SAAS,cAAc,SAAS;AACrD,SAAO,OAAO,KAAK,cAAc,OAAO;GACtC,OAAO;GACP,QAAQ;GACR,SAAS;GACV,CAAC;AAEF,OAAK,WAAW,YAAY,KAAK,cAAc;AAC/C,YAAU,YAAY,KAAK,WAAW;EAGtC,MAAM,MAAM,OAAO;AACnB,OAAK,cAAc,QAAQ,UAAU,cAAc;AACnD,OAAK,cAAc,SAAS,UAAU,eAAe;AAGrD,OAAK,iBAAiB,IAAI,qBAAqB;AAC7C,OAAI,CAAC,KAAK,iBAAiB,CAAC,KAAK,IAAK;GACtC,MAAM,IAAI,KAAK,IAAI,cAAc;GACjC,MAAM,IAAI,OAAO;AACjB,QAAK,cAAc,QAAQ,EAAE,cAAc;AAC3C,QAAK,cAAc,SAAS,EAAE,eAAe;AAC7C,QAAK,KAAK,OAAO,EAAE,aAAa,EAAE,aAAa;IAC/C;AACF,OAAK,eAAe,QAAQ,UAAU;AAGtC,OAAK,MAAM,IAAI,SAAS;AACxB,OAAK,IAAI,KAAK,KAAK,cAAc,CAAC,MAAM,OAAO;AAC7C,OAAI,CAAC,GAAI;AAET,QAAK,IAAK,WAAW,KAAK,SAAS;AACnC,QAAK,IAAK,WAAW,KAAK,SAAS;AACnC,QAAK,IAAK,OAAO,KAAK,KAAK;AAC3B,QAAK,IAAK,cAAc,KAAK,YAAY;IACzC;;CAGJ,OAAO,KAA6B,MAAqC;AACvE,MAAI,CAAC,KAAK,KAAK,SAAS,CAAC,KAAK,IAAK;EACnC,MAAM,MAAM,YAAY,KAAK;EAC7B,MAAM,KAAK,MAAM,KAAK;AACtB,MAAI,KAAK,iBAAiB,KAAK,KAAK,GAAG;GACrC,MAAM,aAAa,MAAO;AAC1B,QAAK,OAAO,KAAK,OAAO,KAAM,aAAa;;AAE7C,OAAK,iBAAiB;EACtB,MAAM,MAAM,KAAK,IAAI,IAAI,KAAK,IAAI,KAAK,KAAK,KAAK,CAAC;EAClD,MAAM,OAAO,KAAK,IAAI,SAAS;EAC/B,MAAM,SAAS,KAAK,IAAI,WAAW;EACnC,MAAM,MAAM,OAAO;EACnB,MAAM,OAAO,KAAK,IAAI,cAAc,CAAC;EACrC,MAAM,OAAO,iBAAiB,OAAO,KAAK,OAAO,IAAI;AACrD,OAAK,IAAI,cAAc,KAAK,GAAG,KAAK,GAAG,MAAM,KAAK;AAClD,OAAK,IAAI,kBAAkB,KAAK,YAAY,MAAM,IAAI;AACtD,OAAK,IAAI,WAAW,KAAK,WAAW,IAAI,KAAK,WAAW,IAAI,MAAM,IAAI;AACtE,OAAK,IAAI,gBAAgB,KAAK,YAAY,MAAM,IAAI;AACpD,OAAK,IAAI,SAAS;AAClB,OAAK,IAAI,MAAM,IAAI,aAAa,KAAK,sBAAsB,WAAW,CAAC;AACvE,OAAK,IAAI,gBAAgB;;CAI3B,SAAS,MAAmB,KAAmC;AAC7D,OAAK,gBAAgB,YAAY;AACjC,OAAK,KAAK,SAAS;AAEnB,MAAI,KAAK,YAAY,cACnB,MAAK,WAAW,cAAc,YAAY,KAAK,WAAW;AAG5D,OAAK,MAAM;AACX,OAAK,aAAa;AAClB,OAAK,gBAAgB;AACrB,OAAK,MAAM;;CAOb,WAAW,SAAuB;AAChC,OAAK,WAAW;AAChB,OAAK,KAAK,WAAW,QAAQ;;CAG/B,aAAa,WAAyB;AACpC,OAAK,aAAa,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,UAAU,CAAC;;CAGvD,aAAa,MAAoB;AAC/B,OAAK,aAAa;;CAGpB,WAAW,SAAuB;AAChC,OAAK,WAAW;AAChB,OAAK,KAAK,WAAW,QAAQ;;CAG/B,aAAa,WAAmC;AAC9C,OAAK,aAAa;;CAGpB,OAAO,SAAwB;AAC7B,OAAK,OAAO;AACZ,OAAK,KAAK,OAAO,QAAQ;;CAG3B,cAAc,SAAuB;AACnC,OAAK,cAAc;AACnB,OAAK,KAAK,cAAc,QAAQ"}
package/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "@geoql/maplibre-gl-snow",
3
+ "version": "0.0.1",
4
+ "private": false,
5
+ "description": "A MapLibre GL JS custom layer for rendering falling snow particles via Three.js WebGPU compute shaders",
6
+ "keywords": [
7
+ "layer",
8
+ "maplibre",
9
+ "particles",
10
+ "snow",
11
+ "three.js",
12
+ "weather",
13
+ "webgpu"
14
+ ],
15
+ "homepage": "https://github.com/geoql/maplibre-gl-snow#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/geoql/maplibre-gl-snow/issues"
18
+ },
19
+ "license": "MIT",
20
+ "author": {
21
+ "name": "Vinayak Kulkarni",
22
+ "email": "inbox.vinayak@gmail.com",
23
+ "url": "https://vinayakkulkarni.dev"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/geoql/maplibre-gl-snow.git"
28
+ },
29
+ "files": [
30
+ "dist"
31
+ ],
32
+ "type": "module",
33
+ "sideEffects": false,
34
+ "main": "./dist/index.mjs",
35
+ "module": "./dist/index.mjs",
36
+ "types": "./dist/index.d.mts",
37
+ "exports": {
38
+ ".": {
39
+ "types": "./dist/index.d.mts",
40
+ "import": "./dist/index.mjs"
41
+ }
42
+ },
43
+ "scripts": {
44
+ "build": "tsdown",
45
+ "dev": "tsdown --watch",
46
+ "test": "echo 'test!'",
47
+ "prepare": "is-ci || husky",
48
+ "lint": "oxlint --deny-warnings",
49
+ "lint:fix": "oxlint --fix",
50
+ "format": "oxfmt --check",
51
+ "format:fix": "oxfmt --write",
52
+ "typecheck": "tsc --noEmit",
53
+ "publish:npm": "npm publish --access public",
54
+ "publish:jsr": "bunx jsr publish"
55
+ },
56
+ "devDependencies": {
57
+ "@commitlint/cli": "^20.4.2",
58
+ "@commitlint/config-conventional": "^20.4.2",
59
+ "@commitlint/types": "^20.4.0",
60
+ "@types/node": "^25.3.0",
61
+ "husky": "^9.1.7",
62
+ "is-ci": "^4.1.0",
63
+ "lint-staged": "^16.2.7",
64
+ "maplibre-gl": "^5.19.0",
65
+ "oxfmt": "^0.35.0",
66
+ "oxlint": "^1.50.0",
67
+ "three": "^0.183.1",
68
+ "tsdown": "^0.20.3",
69
+ "typescript": "^5.9.3"
70
+ },
71
+ "peerDependencies": {
72
+ "maplibre-gl": ">=3.0.0",
73
+ "three": ">=0.183.0"
74
+ },
75
+ "engines": {
76
+ "node": ">=24.0.0"
77
+ },
78
+ "packageManager": "bun@1.3.9"
79
+ }