@aeroswarm/weather-layer 0.0.0-stage → 0.1.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/dist/index.js ADDED
@@ -0,0 +1,620 @@
1
+ // src/clouds3d.ts
2
+ var FT_TO_M = 0.3048;
3
+ var KT_TO_MS = 0.514444;
4
+ var DEFAULT_BASE_M = 600;
5
+ var MAX_PER_LAYER = 14;
6
+ function seededRandom(seed) {
7
+ let a = seed >>> 0;
8
+ return () => {
9
+ a = a + 1831565813 >>> 0;
10
+ let t = a;
11
+ t = Math.imul(t ^ t >>> 15, t | 1);
12
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
13
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
14
+ };
15
+ }
16
+ function buildCloudField(state, options = {}) {
17
+ const radius = options.radiusM ?? 12e3;
18
+ const elevation = options.elevationM ?? 0;
19
+ const random = seededRandom(options.seed ?? 1);
20
+ const field = [];
21
+ state.clouds.layers.forEach((layer, layerIndex) => {
22
+ const count = Math.max(1, Math.round(layer.coverage * MAX_PER_LAYER));
23
+ const baseM = layer.baseFt === null ? DEFAULT_BASE_M : layer.baseFt * FT_TO_M;
24
+ for (let i = 0; i < count; i++) {
25
+ const angle = random() * Math.PI * 2;
26
+ const distance = Math.sqrt(random()) * radius;
27
+ const size = (layer.convective ? 1400 : 800) * (0.6 + random() * 0.9);
28
+ field.push({
29
+ id: `cloud-${layerIndex}-${i}`,
30
+ eastM: Math.sin(angle) * distance,
31
+ northM: Math.cos(angle) * distance,
32
+ altM: elevation + baseM + random() * (layer.convective ? 400 : 120),
33
+ sizeM: size,
34
+ opacity: Math.min(0.9, 0.3 + layer.coverage * 0.5) * (layer.convective ? 1.1 : 1),
35
+ convective: layer.convective
36
+ });
37
+ }
38
+ });
39
+ return field;
40
+ }
41
+ function advanceCloudField(field, wind, dtSeconds, options = {}) {
42
+ if (wind.toDeg === null || wind.speedKt <= 0) return field.map((c) => ({ ...c }));
43
+ const radius = options.radiusM ?? 12e3;
44
+ const meters = wind.speedKt * KT_TO_MS * dtSeconds * (options.timeScale ?? 12);
45
+ const east = Math.sin(wind.toDeg * Math.PI / 180) * meters;
46
+ const north = Math.cos(wind.toDeg * Math.PI / 180) * meters;
47
+ return field.map((cloud) => {
48
+ let eastM = cloud.eastM + east;
49
+ let northM = cloud.northM + north;
50
+ if (Math.hypot(eastM, northM) > radius) {
51
+ eastM = -eastM * 0.98;
52
+ northM = -northM * 0.98;
53
+ }
54
+ return { ...cloud, eastM, northM };
55
+ });
56
+ }
57
+ function offsetToLonLat(centerLat, centerLon, eastM, northM) {
58
+ const metersPerDegLat = 111320;
59
+ const metersPerDegLon = metersPerDegLat * Math.cos(centerLat * Math.PI / 180);
60
+ return {
61
+ lat: centerLat + northM / metersPerDegLat,
62
+ lon: centerLon + eastM / metersPerDegLon
63
+ };
64
+ }
65
+
66
+ // src/cloudLayer.ts
67
+ function makeCloudSpriteUrl() {
68
+ const canvas = document.createElement("canvas");
69
+ canvas.width = canvas.height = 128;
70
+ const ctx = canvas.getContext("2d");
71
+ if (!ctx) return "";
72
+ const gradient = ctx.createRadialGradient(64, 64, 4, 64, 64, 62);
73
+ gradient.addColorStop(0, "rgba(255,255,255,0.95)");
74
+ gradient.addColorStop(0.55, "rgba(255,255,255,0.45)");
75
+ gradient.addColorStop(1, "rgba(255,255,255,0)");
76
+ ctx.fillStyle = gradient;
77
+ ctx.fillRect(0, 0, 128, 128);
78
+ return canvas.toDataURL("image/png");
79
+ }
80
+ function createCloudLayer3D(ria, options) {
81
+ const model = new ria.FeatureModel(new ria.MemoryStore(), { reference: ria.reference });
82
+ const painter = new ria.FeaturePainter();
83
+ const spriteUrl = makeCloudSpriteUrl();
84
+ const only3D = options.only3D ?? true;
85
+ painter.getDetailLevelScales = () => [1 / 1e5, 1 / 2e4, 1 / 5e3];
86
+ painter.paintBody = (geoCanvas, feature, shape, _layer, map) => {
87
+ if (only3D && map.reference.identifier !== "EPSG:4978") return;
88
+ const size = Number(feature.properties.sizeM) || 800;
89
+ geoCanvas.drawIcon(shape, {
90
+ url: spriteUrl,
91
+ width: `${size}m`,
92
+ height: `${size}m`,
93
+ opacity: Number(feature.properties.opacity) || 0.5,
94
+ draped: false,
95
+ modulationColor: feature.properties.convective === true ? "#8f98a3" : "#ffffff"
96
+ });
97
+ };
98
+ const layer = new ria.FeatureLayer(model, {
99
+ id: options.id ?? "weather-clouds",
100
+ label: "Awan",
101
+ painter,
102
+ selectable: false,
103
+ visible: true
104
+ });
105
+ let wind = { toDeg: null, speedKt: 0 };
106
+ let field = [];
107
+ let knownIds = /* @__PURE__ */ new Set();
108
+ const render = () => {
109
+ const next = /* @__PURE__ */ new Set();
110
+ for (const cloud of field) {
111
+ const { lat, lon } = offsetToLonLat(
112
+ options.center.lat,
113
+ options.center.lon,
114
+ cloud.eastM,
115
+ cloud.northM
116
+ );
117
+ model.put(
118
+ new ria.Feature(
119
+ ria.createPoint(ria.reference, [lon, lat, cloud.altM]),
120
+ { sizeM: cloud.sizeM, opacity: cloud.opacity, convective: cloud.convective },
121
+ cloud.id
122
+ )
123
+ );
124
+ next.add(cloud.id);
125
+ }
126
+ for (const id of knownIds) if (!next.has(id)) model.remove(id);
127
+ knownIds = next;
128
+ };
129
+ const interval = options.updateIntervalMs ?? 500;
130
+ const timer = setInterval(() => {
131
+ if (!layer.visible || field.length === 0) return;
132
+ field = advanceCloudField(field, wind, interval / 1e3, {
133
+ radiusM: options.radiusM,
134
+ timeScale: options.timeScale
135
+ });
136
+ render();
137
+ }, interval);
138
+ return {
139
+ layer,
140
+ setState(state) {
141
+ wind = { toDeg: state.wind.toDeg, speedKt: state.wind.speedKt };
142
+ field = buildCloudField(state, {
143
+ radiusM: options.radiusM,
144
+ elevationM: options.elevationM
145
+ });
146
+ render();
147
+ },
148
+ setVisible(visible) {
149
+ layer.visible = visible;
150
+ },
151
+ destroy() {
152
+ clearInterval(timer);
153
+ for (const id of knownIds) model.remove(id);
154
+ knownIds = /* @__PURE__ */ new Set();
155
+ field = [];
156
+ }
157
+ };
158
+ }
159
+
160
+ // src/math.ts
161
+ var RAD = Math.PI / 180;
162
+ function screenVector(toDeg, screenOffsetDeg = 0) {
163
+ const angle = (toDeg + screenOffsetDeg) * RAD;
164
+ return { x: Math.sin(angle), y: -Math.cos(angle) };
165
+ }
166
+ var clamp = (value, min = 0, max = 1) => Math.min(max, Math.max(min, value));
167
+ function windParticleCount(width, height, max = 520) {
168
+ return Math.min(max, Math.round(width * height / 2600));
169
+ }
170
+ function rainCount(pool, intensity) {
171
+ return Math.round(pool * clamp(intensity));
172
+ }
173
+ function particleSpeed(speedKt, gustKt = 0) {
174
+ return (speedKt + gustKt) * 7;
175
+ }
176
+ function gustPulse(gustiness, timeMs) {
177
+ if (gustiness <= 0) return 0;
178
+ const wave = 0.5 + 0.5 * Math.sin(timeMs / 700) * Math.sin(timeMs / 1900 + 1);
179
+ return wave * gustiness * 20;
180
+ }
181
+ function lightningInterval(perMinute, random) {
182
+ if (perMinute <= 0) return Number.POSITIVE_INFINITY;
183
+ return 6e4 / perMinute * (0.4 + random * 1.2);
184
+ }
185
+
186
+ // src/renderer.ts
187
+ var CALM = {
188
+ wind: { fromDeg: null, toDeg: null, speedKt: 0, gustiness: 0, turbulent: false },
189
+ clouds: { layers: [], total: 0 },
190
+ visibility: { meters: null, haze: 0 },
191
+ obscuration: null,
192
+ precipitation: null,
193
+ thunderstorm: null
194
+ };
195
+ var DROP_POOL = 700;
196
+ function makePuffSprite() {
197
+ const sprite = document.createElement("canvas");
198
+ sprite.width = sprite.height = 128;
199
+ const ctx = sprite.getContext("2d");
200
+ if (ctx) {
201
+ const gradient = ctx.createRadialGradient(64, 64, 4, 64, 64, 62);
202
+ gradient.addColorStop(0, "rgba(255,255,255,0.95)");
203
+ gradient.addColorStop(0.55, "rgba(255,255,255,0.45)");
204
+ gradient.addColorStop(1, "rgba(255,255,255,0)");
205
+ ctx.fillStyle = gradient;
206
+ ctx.fillRect(0, 0, 128, 128);
207
+ }
208
+ return sprite;
209
+ }
210
+ var WeatherRenderer = class {
211
+ constructor(options) {
212
+ this.options = options;
213
+ this.theme = options.theme ?? "light";
214
+ this.frameInterval = 1e3 / (options.maxFps ?? 30);
215
+ this.reduceMotion = options.reducedMotion ?? (typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches);
216
+ this.windCanvas = this.createCanvas("wind");
217
+ this.fxCanvas = this.createCanvas("fx");
218
+ const wind = this.windCanvas.getContext("2d");
219
+ const fx = this.fxCanvas.getContext("2d");
220
+ if (!wind || !fx) throw new Error("Canvas 2D tidak tersedia");
221
+ this.wind = wind;
222
+ this.fx = fx;
223
+ this.resize();
224
+ }
225
+ options;
226
+ windCanvas;
227
+ fxCanvas;
228
+ wind;
229
+ fx;
230
+ puff = makePuffSprite();
231
+ drops = Array.from({ length: DROP_POOL }, () => ({
232
+ x: Math.random(),
233
+ y: Math.random(),
234
+ len: 0.6 + Math.random() * 0.8,
235
+ speed: 0.7 + Math.random() * 0.6
236
+ }));
237
+ state = CALM;
238
+ theme;
239
+ screenOffset = 0;
240
+ width = 1;
241
+ height = 1;
242
+ dpr = 1;
243
+ particles = [];
244
+ clouds = [];
245
+ raf = 0;
246
+ lastFrame = 0;
247
+ accumulated = 0;
248
+ flash = 0;
249
+ bolt = null;
250
+ nextBolt = 0;
251
+ frameInterval;
252
+ reduceMotion;
253
+ running = false;
254
+ setState(state) {
255
+ this.state = state;
256
+ if (this.reduceMotion) this.renderFrame(performance.now(), 0);
257
+ }
258
+ /** Putaran peta (derajat) yang ditambahkan ke arah geografis angin. */
259
+ setScreenOffset(degrees) {
260
+ this.screenOffset = degrees;
261
+ }
262
+ setTheme(theme) {
263
+ this.theme = theme;
264
+ }
265
+ setVisible(visible) {
266
+ for (const canvas of [this.windCanvas, this.fxCanvas]) {
267
+ canvas.style.display = visible ? "block" : "none";
268
+ }
269
+ if (visible) this.start();
270
+ else this.stop();
271
+ }
272
+ setOpacity(opacity) {
273
+ for (const canvas of [this.windCanvas, this.fxCanvas]) {
274
+ canvas.style.opacity = String(clamp(opacity));
275
+ }
276
+ }
277
+ resize() {
278
+ const rect = this.options.container.getBoundingClientRect();
279
+ this.width = Math.max(1, Math.round(rect.width));
280
+ this.height = Math.max(1, Math.round(rect.height));
281
+ this.dpr = Math.min(2, window.devicePixelRatio || 1);
282
+ for (const canvas of [this.windCanvas, this.fxCanvas]) {
283
+ canvas.width = this.width * this.dpr;
284
+ canvas.height = this.height * this.dpr;
285
+ }
286
+ this.wind.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
287
+ this.fx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
288
+ this.seedParticles();
289
+ this.seedClouds();
290
+ }
291
+ start() {
292
+ if (this.running) return;
293
+ this.running = true;
294
+ this.lastFrame = performance.now();
295
+ const tick = (now) => {
296
+ if (!this.running) return;
297
+ const dt = (now - this.lastFrame) / 1e3;
298
+ this.lastFrame = now;
299
+ this.accumulated += dt * 1e3;
300
+ if (!document.hidden && this.accumulated >= this.frameInterval) {
301
+ this.renderFrame(now, Math.min(0.1, this.accumulated / 1e3));
302
+ this.accumulated = 0;
303
+ }
304
+ this.raf = requestAnimationFrame(tick);
305
+ };
306
+ this.raf = requestAnimationFrame(tick);
307
+ }
308
+ stop() {
309
+ this.running = false;
310
+ cancelAnimationFrame(this.raf);
311
+ }
312
+ destroy() {
313
+ this.stop();
314
+ this.windCanvas.remove();
315
+ this.fxCanvas.remove();
316
+ }
317
+ // ---- internal --------------------------------------------------------
318
+ createCanvas(name) {
319
+ const canvas = document.createElement("canvas");
320
+ canvas.dataset.weatherLayer = name;
321
+ Object.assign(canvas.style, {
322
+ position: "absolute",
323
+ inset: "0",
324
+ width: "100%",
325
+ height: "100%",
326
+ pointerEvents: "none",
327
+ zIndex: String(this.options.zIndex ?? 5)
328
+ });
329
+ this.options.container.appendChild(canvas);
330
+ return canvas;
331
+ }
332
+ seedParticles() {
333
+ const count = windParticleCount(this.width, this.height);
334
+ this.particles = Array.from({ length: count }, () => this.newParticle());
335
+ this.wind.clearRect(0, 0, this.width, this.height);
336
+ }
337
+ newParticle() {
338
+ return {
339
+ x: Math.random() * this.width,
340
+ y: Math.random() * this.height,
341
+ age: Math.floor(Math.random() * 80),
342
+ life: 60 + Math.random() * 90
343
+ };
344
+ }
345
+ seedClouds() {
346
+ this.clouds = Array.from({ length: 16 }, (_, i) => {
347
+ const size = 90 + Math.random() * 120;
348
+ const count = 5 + Math.floor(Math.random() * 4);
349
+ return {
350
+ x: Math.random() * (this.width + 300) - 150,
351
+ y: Math.random() * (this.height + 100) - 50,
352
+ layer: 0.6 + Math.random() * 0.8,
353
+ threshold: i / 16,
354
+ puffs: Array.from({ length: count }, () => ({
355
+ dx: (Math.random() - 0.5) * size * 1.4,
356
+ dy: (Math.random() - 0.5) * size * 0.5,
357
+ r: size * (0.5 + Math.random() * 0.5)
358
+ }))
359
+ };
360
+ });
361
+ }
362
+ renderFrame(now, dt) {
363
+ const { wind, thunderstorm } = this.state;
364
+ const motion = this.reduceMotion ? 0 : 1;
365
+ const toDeg = wind.toDeg;
366
+ const vector = toDeg === null ? null : screenVector(toDeg, this.screenOffset);
367
+ const gust = gustPulse(wind.gustiness, now);
368
+ const speed = particleSpeed(wind.speedKt, gust);
369
+ this.drawWind(now, dt, vector, wind.turbulent, speed, motion);
370
+ const fx = this.fx;
371
+ fx.clearRect(0, 0, this.width, this.height);
372
+ this.drawClouds(dt, vector, wind.speedKt, motion);
373
+ this.drawPrecipitation(dt, vector, wind.speedKt, motion);
374
+ this.drawHaze(now, motion);
375
+ this.drawLightning(now, dt, thunderstorm?.lightningPerMinute ?? 0);
376
+ }
377
+ drawWind(now, dt, vector, turbulent, speed, motion) {
378
+ const ctx = this.wind;
379
+ ctx.globalCompositeOperation = "destination-out";
380
+ ctx.fillStyle = `rgba(0,0,0,${this.reduceMotion ? 1 : 0.1})`;
381
+ ctx.fillRect(0, 0, this.width, this.height);
382
+ ctx.globalCompositeOperation = "source-over";
383
+ if (this.state.wind.speedKt <= 0) return;
384
+ const alpha = Math.min(0.75, 0.22 + this.state.wind.speedKt / 50);
385
+ ctx.lineWidth = 1.2;
386
+ ctx.lineCap = "round";
387
+ ctx.strokeStyle = this.theme === "dark" ? `rgba(120,200,220,${alpha})` : `rgba(31,122,140,${alpha})`;
388
+ ctx.beginPath();
389
+ for (const p of this.particles) {
390
+ const fromX = p.x;
391
+ const fromY = p.y;
392
+ let vx;
393
+ let vy;
394
+ if (turbulent || vector === null) {
395
+ vx = Math.cos(p.age / 12 + p.x) * 0.7;
396
+ vy = Math.sin(p.age / 14 + p.y) * 0.7;
397
+ } else {
398
+ const wobble = Math.sin(p.y / 55 + now / 1400) * 0.35 + Math.cos(p.x / 70 - now / 1700) * 0.25;
399
+ vx = vector.x - wobble * 0.25 * vector.y;
400
+ vy = vector.y + wobble * 0.25 * vector.x;
401
+ }
402
+ p.x += vx * speed * dt * motion;
403
+ p.y += vy * speed * dt * motion;
404
+ p.age++;
405
+ ctx.moveTo(fromX, fromY);
406
+ ctx.lineTo(p.x, p.y);
407
+ if (p.x < 0 || p.x > this.width || p.y < 0 || p.y > this.height || p.age > p.life) {
408
+ Object.assign(p, this.newParticle(), { age: 0 });
409
+ }
410
+ }
411
+ ctx.stroke();
412
+ }
413
+ drawClouds(dt, vector, speedKt, motion) {
414
+ const ctx = this.fx;
415
+ const cover = this.state.clouds.total;
416
+ const precipitation = this.state.precipitation;
417
+ const dark = clamp(
418
+ (precipitation?.intensity ?? 0) * 0.6 + (this.state.thunderstorm?.overhead ? 0.4 : 0)
419
+ );
420
+ if (cover <= 0) return;
421
+ for (const cloud of this.clouds) {
422
+ if (vector) {
423
+ cloud.x += vector.x * speedKt * 1.4 * cloud.layer * dt * motion;
424
+ cloud.y += vector.y * speedKt * 1.4 * cloud.layer * dt * motion;
425
+ }
426
+ if (cloud.x < -250) cloud.x = this.width + 250;
427
+ if (cloud.x > this.width + 250) cloud.x = -250;
428
+ if (cloud.y < -150) cloud.y = this.height + 150;
429
+ if (cloud.y > this.height + 150) cloud.y = -150;
430
+ const visible = clamp((cover * 1.15 - cloud.threshold) * 4);
431
+ if (visible <= 0) continue;
432
+ ctx.globalAlpha = visible * (0.38 + 0.3 * cover) * (1 - dark * 0.15);
433
+ for (const puff of cloud.puffs) {
434
+ ctx.drawImage(
435
+ this.puff,
436
+ cloud.x + puff.dx - puff.r,
437
+ cloud.y + puff.dy - puff.r,
438
+ puff.r * 2,
439
+ puff.r * 2
440
+ );
441
+ }
442
+ }
443
+ ctx.globalAlpha = 1;
444
+ if (dark > 0 || cover > 0.6) {
445
+ ctx.fillStyle = `rgba(38,48,62,${(cover * 0.2 + dark * 0.32).toFixed(3)})`;
446
+ ctx.fillRect(0, 0, this.width, this.height);
447
+ }
448
+ }
449
+ drawPrecipitation(dt, vector, speedKt, motion) {
450
+ const precipitation = this.state.precipitation;
451
+ if (!precipitation) return;
452
+ const ctx = this.fx;
453
+ const count = rainCount(this.drops.length, precipitation.intensity);
454
+ const slant = (vector?.x ?? 0) * Math.min(1, speedKt / 25) * 0.5;
455
+ const snow = precipitation.kind === "snow";
456
+ const fall = snow ? 0.25 : 1.6;
457
+ if (snow) ctx.fillStyle = "rgba(255,255,255,0.8)";
458
+ else {
459
+ ctx.strokeStyle = `rgba(150,185,225,${(0.3 + 0.3 * precipitation.intensity).toFixed(2)})`;
460
+ ctx.lineWidth = precipitation.kind === "drizzle" ? 0.7 : 1.1;
461
+ ctx.beginPath();
462
+ }
463
+ for (let i = 0; i < count; i++) {
464
+ const drop = this.drops[i];
465
+ if (!drop) continue;
466
+ drop.y += drop.speed * dt * fall * motion;
467
+ drop.x += slant * dt * fall * motion;
468
+ if (drop.y > 1) {
469
+ drop.y = -0.05;
470
+ drop.x = Math.random();
471
+ }
472
+ if (drop.x > 1) drop.x = 0;
473
+ if (drop.x < 0) drop.x = 1;
474
+ const x = drop.x * this.width;
475
+ const y = drop.y * this.height;
476
+ if (snow) ctx.fillRect(x, y, 2, 2);
477
+ else {
478
+ const length = 12 * drop.len;
479
+ ctx.moveTo(x, y);
480
+ ctx.lineTo(x - slant * length, y - length);
481
+ }
482
+ }
483
+ if (!snow) ctx.stroke();
484
+ }
485
+ drawHaze(now, motion) {
486
+ const { visibility, obscuration } = this.state;
487
+ const haze = Math.max(visibility.haze, (obscuration?.density ?? 0) * 0.7);
488
+ if (haze <= 0.01) return;
489
+ const ctx = this.fx;
490
+ const dark = this.theme === "dark";
491
+ ctx.fillStyle = `${dark ? "rgba(150,160,168," : "rgba(214,219,223,"}${(haze * 0.78).toFixed(3)})`;
492
+ ctx.fillRect(0, 0, this.width, this.height);
493
+ if (obscuration?.kind === "fog" || (visibility.meters ?? 1e4) < 1500) {
494
+ for (let i = 0; i < 3; i++) {
495
+ const cx = now / 40 * (i + 1) * 0.8 * motion % (this.width * 1.3) + i * this.width * 0.2;
496
+ const cy = this.height * (0.3 + i * 0.25);
497
+ const gradient = ctx.createRadialGradient(cx, cy, 10, cx, cy, this.width * 0.5);
498
+ gradient.addColorStop(0, dark ? "rgba(170,180,188,0.35)" : "rgba(235,238,240,0.5)");
499
+ gradient.addColorStop(1, "rgba(0,0,0,0)");
500
+ ctx.fillStyle = gradient;
501
+ ctx.fillRect(0, 0, this.width, this.height);
502
+ }
503
+ }
504
+ }
505
+ drawLightning(now, dt, perMinute) {
506
+ if (this.reduceMotion) return;
507
+ const ctx = this.fx;
508
+ if (perMinute > 0 && now > this.nextBolt) {
509
+ this.flash = 1;
510
+ this.bolt = this.makeBolt();
511
+ this.nextBolt = now + lightningInterval(perMinute, Math.random());
512
+ }
513
+ if (this.flash <= 0) return;
514
+ ctx.fillStyle = `rgba(255,255,255,${(this.flash * 0.35).toFixed(3)})`;
515
+ ctx.fillRect(0, 0, this.width, this.height);
516
+ if (this.bolt && this.flash > 0.35) {
517
+ ctx.strokeStyle = `rgba(255,255,255,${this.flash.toFixed(2)})`;
518
+ ctx.lineWidth = 2.2;
519
+ ctx.beginPath();
520
+ this.bolt.forEach(([x, y], i) => {
521
+ if (i === 0) ctx.moveTo(x, y);
522
+ else ctx.lineTo(x, y);
523
+ });
524
+ ctx.stroke();
525
+ }
526
+ this.flash -= dt * 3.2;
527
+ }
528
+ makeBolt() {
529
+ let x = this.width * (0.2 + Math.random() * 0.6);
530
+ let y = 0;
531
+ const points = [[x, y]];
532
+ const reach = this.height * (0.5 + Math.random() * 0.3);
533
+ while (y < reach) {
534
+ x += (Math.random() - 0.5) * 36;
535
+ y += 14 + Math.random() * 22;
536
+ points.push([x, y]);
537
+ }
538
+ return points;
539
+ }
540
+ };
541
+
542
+ // src/overlay.ts
543
+ function mapScreenOffset(map) {
544
+ const camera = map.camera;
545
+ if (!camera) return 0;
546
+ try {
547
+ if (map.reference?.identifier === "EPSG:4978") {
548
+ return -(camera.asLookAt?.(1e3).yaw ?? 0);
549
+ }
550
+ return -(camera.asLook2D?.().rotation ?? 0);
551
+ } catch {
552
+ return 0;
553
+ }
554
+ }
555
+ function detectTheme() {
556
+ const explicit = document.documentElement.dataset.theme;
557
+ if (explicit === "dark" || explicit === "light") return explicit;
558
+ return matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
559
+ }
560
+ function createWeatherOverlay(map, options = {}) {
561
+ const container = map.domNode;
562
+ if (getComputedStyle(container).position === "static") {
563
+ container.style.position = "relative";
564
+ }
565
+ const theme = options.theme ?? "auto";
566
+ const renderer = new WeatherRenderer({
567
+ container,
568
+ theme: theme === "auto" ? detectTheme() : theme,
569
+ zIndex: options.zIndex,
570
+ maxFps: options.maxFps
571
+ });
572
+ const offsetOf = options.getScreenOffset ?? mapScreenOffset;
573
+ let width = container.clientWidth;
574
+ let height = container.clientHeight;
575
+ const handle = map.on("MapChange", () => {
576
+ renderer.setScreenOffset(offsetOf(map));
577
+ if (container.clientWidth !== width || container.clientHeight !== height) {
578
+ width = container.clientWidth;
579
+ height = container.clientHeight;
580
+ renderer.resize();
581
+ }
582
+ });
583
+ const themeObserver = theme === "auto" ? new MutationObserver(() => renderer.setTheme(detectTheme())) : null;
584
+ themeObserver?.observe(document.documentElement, {
585
+ attributes: true,
586
+ attributeFilter: ["data-theme"]
587
+ });
588
+ renderer.setScreenOffset(offsetOf(map));
589
+ renderer.start();
590
+ return {
591
+ renderer,
592
+ setState: (state) => renderer.setState(state),
593
+ setVisible: (visible) => renderer.setVisible(visible),
594
+ setOpacity: (opacity) => renderer.setOpacity(opacity),
595
+ destroy() {
596
+ handle.remove();
597
+ themeObserver?.disconnect();
598
+ renderer.destroy();
599
+ }
600
+ };
601
+ }
602
+ export {
603
+ WeatherRenderer,
604
+ advanceCloudField,
605
+ buildCloudField,
606
+ clamp,
607
+ createCloudLayer3D,
608
+ createWeatherOverlay,
609
+ gustPulse,
610
+ lightningInterval,
611
+ makeCloudSpriteUrl,
612
+ mapScreenOffset,
613
+ offsetToLonLat,
614
+ particleSpeed,
615
+ rainCount,
616
+ screenVector,
617
+ seededRandom,
618
+ windParticleCount
619
+ };
620
+ //# sourceMappingURL=index.js.map