react-weather-effects 0.1.0-beta.0 → 0.1.0

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.d.ts CHANGED
@@ -30,14 +30,37 @@ declare function WeatherCanvas({ children, mode, wind, dpr, quality, reducedMoti
30
30
 
31
31
  interface RainProps {
32
32
  intensity?: number;
33
+ /** Intensity fade duration in milliseconds; 0 (default) applies changes immediately. */
34
+ transitionDuration?: number;
33
35
  angle?: number;
34
36
  color?: string;
35
37
  speed?: number;
38
+ /**
39
+ * Multiplier for raindrop trail length.
40
+ * Defaults to 1. Values < 1 produce fine drizzle; values > 1 produce longer downpour streaks.
41
+ */
42
+ dropLength?: number;
43
+ length?: number;
44
+ /**
45
+ * Multiplier for raindrop stroke thickness.
46
+ * Defaults to 1. Values < 1 produce delicate droplets; values > 1 produce thick heavy drops.
47
+ */
48
+ dropThickness?: number;
49
+ thickness?: number;
50
+ /**
51
+ * Enable animated splashes along the bottom edge of the canvas.
52
+ * Ideal for contained cards, heroes, or window widgets where raindrops hit the ground.
53
+ * Defaults to false.
54
+ */
55
+ splashes?: boolean;
56
+ splash?: boolean;
36
57
  }
37
- declare function Rain({ intensity, angle, color, speed, }: RainProps): null;
58
+ declare function Rain({ intensity, transitionDuration, angle, color, speed, dropLength, length, dropThickness, thickness, splashes, splash, }: RainProps): null;
38
59
 
39
60
  interface SnowProps {
40
61
  intensity?: number;
62
+ /** Intensity fade duration in milliseconds; 0 (default) applies changes immediately. */
63
+ transitionDuration?: number;
41
64
  speed?: number;
42
65
  color?: string;
43
66
  /**
@@ -49,16 +72,30 @@ interface SnowProps {
49
72
  minSize?: number;
50
73
  /** Radius in CSS pixels; defaults to 18 for sprites, 4 for built-in flakes. */
51
74
  maxSize?: number;
75
+ /** Enable animated sideways sway and fall flutter. Defaults to true. */
52
76
  wobble?: boolean;
77
+ /**
78
+ * Enable sprite rotation / spinning.
79
+ * Defaults to `wobble` (enabled when wobble is enabled).
80
+ * Set to `false` to let sprites drift without rotating, or `true` with `wobble={false}` to rotate without swaying.
81
+ */
82
+ rotation?: boolean;
83
+ /**
84
+ * Multiplier for sprite rotation / spinning speed.
85
+ * Defaults to 1. Set to 0 to freeze rotation.
86
+ */
87
+ rotationSpeed?: number;
53
88
  }
54
- declare function Snow({ intensity, speed, color, sprite, minSize, maxSize, wobble, }: SnowProps): null;
89
+ declare function Snow({ intensity, transitionDuration, speed, color, sprite, minSize, maxSize, wobble, rotation, rotationSpeed, }: SnowProps): null;
55
90
 
56
91
  interface FogProps {
57
92
  density?: number;
93
+ /** Density fade duration in milliseconds; 0 (default) applies changes immediately. */
94
+ transitionDuration?: number;
58
95
  speed?: number;
59
96
  color?: string;
60
97
  layers?: number;
61
98
  }
62
- declare function Fog({ density, speed, color, layers, }: FogProps): null;
99
+ declare function Fog({ density, transitionDuration, speed, color, layers, }: FogProps): null;
63
100
 
64
101
  export { type DPRWeight, Fog, type FogProps, type MotionMode, type Quality, Rain, type RainProps, type ReducedMotionProp, type RenderBudget, Snow, type SnowProps, WeatherCanvas, type WeatherCanvasProps };
package/dist/index.mjs CHANGED
@@ -173,6 +173,7 @@ function WeatherCanvas({
173
173
  const frameState = {
174
174
  ctx,
175
175
  dt,
176
+ isAnimating: shouldAnimateRef.current,
176
177
  width: dimensions.width,
177
178
  height: dimensions.height,
178
179
  dpr: currentDpr,
@@ -485,6 +486,51 @@ function growParticlePool(pool, count) {
485
486
  next.set(pool);
486
487
  return next;
487
488
  }
489
+ function resolveDuration(duration) {
490
+ return Number.isFinite(duration) ? Math.max(0, duration) : 0;
491
+ }
492
+ function createTransition(target, duration) {
493
+ return { target, duration: resolveDuration(duration), remaining: 0 };
494
+ }
495
+ function stepTransition(transition, target, duration, state) {
496
+ const nextDuration = resolveDuration(duration);
497
+ if (target !== transition.target || nextDuration !== transition.duration) {
498
+ transition.target = target;
499
+ transition.duration = nextDuration;
500
+ transition.remaining = nextDuration;
501
+ }
502
+ if (!state.isAnimating || nextDuration === 0) {
503
+ transition.remaining = 0;
504
+ return 1;
505
+ }
506
+ if (transition.remaining === 0) return 1;
507
+ const elapsed = state.dt * 1e3;
508
+ const step = elapsed + 1e-6 >= transition.remaining ? 1 : elapsed / transition.remaining;
509
+ transition.remaining = step === 1 ? 0 : transition.remaining - elapsed;
510
+ return step;
511
+ }
512
+ function createParticleTransition(count, capacity, duration) {
513
+ const visibility = new Float32Array(capacity);
514
+ visibility.fill(1, 0, count);
515
+ return { transition: createTransition(count, duration), visibility, count };
516
+ }
517
+ function updateParticleTransition(particles, targetCount, duration, state) {
518
+ const capacity = state.budget.maxParticles;
519
+ if (particles.visibility.length < capacity) {
520
+ const visibility = new Float32Array(capacity);
521
+ visibility.set(particles.visibility);
522
+ particles.visibility = visibility;
523
+ }
524
+ if (particles.count > capacity) particles.visibility.fill(0, capacity, particles.count);
525
+ const count = Math.min(capacity, Math.max(particles.count, targetCount));
526
+ const step = stepTransition(particles.transition, targetCount, duration, state);
527
+ for (let i = 0; i < count; i++) {
528
+ const target = i < targetCount ? 1 : 0;
529
+ particles.visibility[i] = step === 1 ? target : particles.visibility[i] + (target - particles.visibility[i]) * step;
530
+ }
531
+ particles.count = step === 1 ? targetCount : count;
532
+ }
533
+ var SPLASH_STRIDE = 7;
488
534
  function seedParticle(pool, i, state, settings, isInitial) {
489
535
  const base = i * PARTICLE_STRIDE;
490
536
  const angleRad = settings.angle * Math.PI / 180;
@@ -508,7 +554,7 @@ function seedParticleRange(pool, startIndex, endIndex, state, settings) {
508
554
  seedParticle(pool, i, state, settings, true);
509
555
  }
510
556
  }
511
- function updatePool(pool, count, state, settings) {
557
+ function updatePool(pool, count, state, settings, visibility, splashPool, maxSplashes, splashState) {
512
558
  const dt = state.dt;
513
559
  const width = state.width;
514
560
  const height = state.height;
@@ -517,6 +563,15 @@ function updatePool(pool, count, state, settings) {
517
563
  const sinAngle = Math.sin(angleRad);
518
564
  const cosAngle = Math.cos(angleRad);
519
565
  const response = 1 - Math.exp(-dt * 5);
566
+ if (splashState.active) {
567
+ splashState.active = false;
568
+ for (let s = 0; s < maxSplashes; s++) {
569
+ const sBase = s * SPLASH_STRIDE;
570
+ if (splashPool[sBase + 5] === 0 || splashPool[sBase + 2] >= 1) continue;
571
+ splashPool[sBase + 2] = Math.min(1, splashPool[sBase + 2] + dt * splashPool[sBase + 3]);
572
+ if (splashPool[sBase + 2] < 1) splashState.active = true;
573
+ }
574
+ }
520
575
  for (let i = 0; i < count; i++) {
521
576
  const base = i * PARTICLE_STRIDE;
522
577
  let x = pool[base];
@@ -529,8 +584,28 @@ function updatePool(pool, count, state, settings) {
529
584
  const targetVy = cosAngle * speed + state.wind.y * (60 + depth * 50);
530
585
  vx += (targetVx - vx) * response;
531
586
  vy += (targetVy - vy) * response;
587
+ const prevY = y;
532
588
  x += vx * dt;
533
589
  y += vy * dt;
590
+ if (settings.splashes && prevY < height && y >= height && visibility[i] > 0.05) {
591
+ const elapsed = (y - height) / vy;
592
+ const impactX = x - vx * elapsed;
593
+ if (impactX >= 0 && impactX <= width) {
594
+ const sBase = splashState.head * SPLASH_STRIDE;
595
+ splashState.head = (splashState.head + 1) % maxSplashes;
596
+ if (splashPool[sBase + 5] === 0 || splashPool[sBase + 2] >= 1) {
597
+ const duration = 0.3 + depth * 0.18;
598
+ splashState.active = true;
599
+ splashPool[sBase] = impactX;
600
+ splashPool[sBase + 1] = Math.max(-40, Math.min(40, vx * 0.04));
601
+ splashPool[sBase + 2] = elapsed / duration;
602
+ splashPool[sBase + 3] = 1 / duration;
603
+ splashPool[sBase + 4] = (2.8 + depth * 4.6) * Math.sqrt(Math.max(0.4, Math.min(3, settings.dropThickness)));
604
+ splashPool[sBase + 5] = (0.25 + depth * 0.45) * visibility[i];
605
+ splashPool[sBase + 6] = (depth + i * 0.618) % 1;
606
+ }
607
+ }
608
+ }
534
609
  if (x > width + 100 || x < -100) {
535
610
  x = ((x + 100) % wrapWidth + wrapWidth) % wrapWidth - 100;
536
611
  }
@@ -544,65 +619,162 @@ function updatePool(pool, count, state, settings) {
544
619
  }
545
620
  }
546
621
  }
547
- function drawPool(pool, count, state, settings) {
548
- if (count === 0) return;
622
+ function drawPool(pool, count, state, settings, visibility, splashPool, maxSplashes) {
623
+ if (count === 0 && (!settings.splashes || maxSplashes === 0)) return;
549
624
  const ctx = state.ctx;
550
625
  ctx.strokeStyle = settings.color;
551
626
  ctx.lineCap = "round";
552
- for (let i = 0; i < count; i++) {
553
- const base = i * PARTICLE_STRIDE;
554
- const x = pool[base];
555
- const y = pool[base + 1];
556
- const vx = pool[base + 2];
557
- const vy = pool[base + 3];
558
- const depth = pool[base + 6];
559
- const trailFactor = 0.012 + depth * 0.025;
560
- const fade = Math.max(0, Math.min(1, (y + 25) / 45, (state.height + 35 - y) / 80));
561
- for (let segment = 0; segment < 3; segment++) {
562
- const start = segment / 3 * trailFactor;
563
- const end = (segment + 1) / 3 * trailFactor;
564
- ctx.globalAlpha = pool[base + 4] * fade * Math.pow(1 - segment / 3, 2);
565
- ctx.lineWidth = (0.6 + depth * 1.1) * (1 - segment * 0.2);
627
+ if (count > 0) {
628
+ for (let i = 0; i < count; i++) {
629
+ if (visibility[i] === 0) continue;
630
+ const base = i * PARTICLE_STRIDE;
631
+ const x = pool[base];
632
+ const y = pool[base + 1];
633
+ const vx = pool[base + 2];
634
+ const vy = pool[base + 3];
635
+ const depth = pool[base + 6];
636
+ const trailFactor = (0.012 + depth * 0.025) * settings.dropLength;
637
+ const fade = Math.max(0, Math.min(1, (y + 25) / 45, (state.height + 35 - y) / 80));
638
+ for (let segment = 0; segment < 3; segment++) {
639
+ const start = segment / 3 * trailFactor;
640
+ const end = (segment + 1) / 3 * trailFactor;
641
+ ctx.globalAlpha = pool[base + 4] * fade * visibility[i] * Math.pow(1 - segment / 3, 2);
642
+ ctx.lineWidth = (0.6 + depth * 1.1) * (1 - segment * 0.2) * settings.dropThickness;
643
+ ctx.beginPath();
644
+ ctx.moveTo(x - vx * start, y - vy * start);
645
+ ctx.lineTo(x - vx * end, y - vy * end);
646
+ ctx.stroke();
647
+ }
648
+ }
649
+ }
650
+ if (settings.splashes) {
651
+ ctx.fillStyle = settings.color;
652
+ const thickness = Math.max(0.4, Math.min(3, settings.dropThickness));
653
+ for (let s = 0; s < maxSplashes; s++) {
654
+ const sBase = s * SPLASH_STRIDE;
655
+ const progress = splashPool[sBase + 2];
656
+ if (progress >= 1 || splashPool[sBase + 5] === 0) continue;
657
+ const x = splashPool[sBase];
658
+ const y = state.height;
659
+ const size = splashPool[sBase + 4];
660
+ const alpha = splashPool[sBase + 5];
661
+ const elapsed = progress / splashPool[sBase + 3];
662
+ const drift = splashPool[sBase + 1] * elapsed;
663
+ ctx.globalAlpha = alpha * 0.45 * Math.pow(1 - progress, 1.5);
664
+ ctx.lineWidth = Math.max(0.35, 0.6 * thickness * (1 - progress * 0.4));
665
+ const rx = size * (0.25 + 1.9 * (1 - Math.pow(1 - progress, 3)));
666
+ const ry = Math.max(0.25, rx * 0.19);
566
667
  ctx.beginPath();
567
- ctx.moveTo(x - vx * start, y - vy * start);
568
- ctx.lineTo(x - vx * end, y - vy * end);
668
+ ctx.ellipse(x, y, rx, ry, 0, Math.PI, Math.PI * 2);
569
669
  ctx.stroke();
670
+ for (let bead = 0; bead < 5; bead++) {
671
+ const variation = (splashPool[sBase + 6] + bead * 0.37) % 1;
672
+ const beadProgress = progress / (0.72 + variation * 0.28);
673
+ if (beadProgress >= 1) continue;
674
+ const direction = (bead - 2) * 0.5;
675
+ const peak = size * (0.9 + (1 - Math.abs(direction)) * 1.1 + variation * 0.4);
676
+ const beadX = x + drift + direction * size * (1.5 + variation * 0.7) * beadProgress;
677
+ const beadY = y - peak * 4 * beadProgress * (1 - beadProgress);
678
+ const radius = Math.max(0.3, (0.4 + thickness * 0.35) * (1 - beadProgress * 0.45));
679
+ ctx.globalAlpha = alpha * Math.pow(1 - beadProgress, 1.25);
680
+ ctx.beginPath();
681
+ ctx.arc(beadX, beadY, radius, 0, Math.PI * 2);
682
+ ctx.fill();
683
+ }
570
684
  }
571
685
  }
572
686
  ctx.globalAlpha = 1;
573
687
  }
574
688
  function Rain({
575
689
  intensity = 0.7,
690
+ transitionDuration = 0,
576
691
  angle = 10,
577
692
  color = "rgba(190, 215, 245, 0.8)",
578
- speed = 1
693
+ speed = 1,
694
+ dropLength,
695
+ length,
696
+ dropThickness,
697
+ thickness,
698
+ splashes,
699
+ splash
579
700
  }) {
580
701
  const ticker = useContext(TickerCtx);
581
702
  if (!ticker) {
582
703
  throw new Error("<Rain /> must be used inside <WeatherCanvas>");
583
704
  }
584
- const settingsRef = useRef({ intensity, angle, color, speed });
705
+ const effectiveDropLength = dropLength ?? length ?? 1;
706
+ const effectiveDropThickness = dropThickness ?? thickness ?? 1;
707
+ const effectiveSplashes = splashes ?? splash ?? false;
708
+ const settingsRef = useRef({
709
+ intensity,
710
+ transitionDuration,
711
+ angle,
712
+ color,
713
+ speed,
714
+ dropLength: effectiveDropLength,
715
+ dropThickness: effectiveDropThickness,
716
+ splashes: effectiveSplashes
717
+ });
585
718
  useEffect(() => {
586
- settingsRef.current = { intensity, angle, color, speed };
719
+ settingsRef.current = {
720
+ intensity,
721
+ transitionDuration,
722
+ angle,
723
+ color,
724
+ speed,
725
+ dropLength: effectiveDropLength,
726
+ dropThickness: effectiveDropThickness,
727
+ splashes: effectiveSplashes
728
+ };
587
729
  ticker.invalidate();
588
- }, [ticker, intensity, angle, color, speed]);
730
+ }, [
731
+ ticker,
732
+ intensity,
733
+ transitionDuration,
734
+ angle,
735
+ color,
736
+ speed,
737
+ effectiveDropLength,
738
+ effectiveDropThickness,
739
+ effectiveSplashes
740
+ ]);
589
741
  useEffect(() => {
590
- let pool = createParticlePool(ticker.getBudget().maxParticles);
742
+ const capacity = ticker.getBudget().maxParticles;
743
+ let pool = createParticlePool(capacity);
744
+ const particles = createParticleTransition(
745
+ Math.round(Math.max(0, Math.min(1, settingsRef.current.intensity)) * capacity),
746
+ capacity,
747
+ settingsRef.current.transitionDuration
748
+ );
591
749
  let initializedCount = 0;
750
+ const maxSplashes = Math.min(128, Math.max(32, Math.round(capacity * 0.5)));
751
+ const splashPool = new Float32Array(maxSplashes * SPLASH_STRIDE);
752
+ const splashState = { head: 0, active: false };
592
753
  const unregister = ticker.register(LAYER.MIDGROUND, (state) => {
593
754
  const settings = settingsRef.current;
594
- const count = Math.round(
755
+ const targetCount = Math.round(
595
756
  Math.max(0, Math.min(1, settings.intensity)) * state.budget.maxParticles
596
757
  );
758
+ updateParticleTransition(particles, targetCount, settings.transitionDuration, state);
759
+ const count = particles.count;
597
760
  pool = growParticlePool(pool, state.budget.maxParticles);
598
761
  if (count > initializedCount) {
599
762
  seedParticleRange(pool, initializedCount, count, state, settings);
600
763
  initializedCount = count;
601
764
  }
602
765
  if (state.dt > 0) {
603
- updatePool(pool, count, state, settings);
766
+ updatePool(
767
+ pool,
768
+ count,
769
+ state,
770
+ settings,
771
+ particles.visibility,
772
+ splashPool,
773
+ maxSplashes,
774
+ splashState
775
+ );
604
776
  }
605
- drawPool(pool, count, state, settings);
777
+ drawPool(pool, count, state, settings, particles.visibility, splashPool, maxSplashes);
606
778
  });
607
779
  return unregister;
608
780
  }, [ticker]);
@@ -668,12 +840,13 @@ function seedSnowflake(pool, i, state, isInitial) {
668
840
  y = -20 - state.random() * 50;
669
841
  }
670
842
  }
843
+ const phase = state.random() * Math.PI * 2;
671
844
  pool[base] = x;
672
845
  pool[base + 1] = y;
673
- pool[base + 2] = 0;
846
+ pool[base + 2] = phase;
674
847
  pool[base + 3] = fallSpeed;
675
848
  pool[base + 4] = 0.3 + depth * 0.6;
676
- pool[base + 5] = state.random() * Math.PI * 2;
849
+ pool[base + 5] = phase;
677
850
  pool[base + 6] = depth;
678
851
  }
679
852
  function seedSnowRange(pool, startIndex, endIndex, state) {
@@ -691,6 +864,7 @@ function updateSnow(pool, count, state, settings) {
691
864
  const base = i * PARTICLE_STRIDE;
692
865
  let x = pool[base];
693
866
  let y = pool[base + 1];
867
+ let angle = pool[base + 2];
694
868
  let phase = pool[base + 5];
695
869
  const baseVy = pool[base + 3];
696
870
  const depth = pool[base + 6];
@@ -698,6 +872,11 @@ function updateSnow(pool, count, state, settings) {
698
872
  phase += dt * (0.65 + depth * 1.4) * settings.speed;
699
873
  pool[base + 5] = phase;
700
874
  }
875
+ if (settings.rotation && settings.rotationSpeed !== 0) {
876
+ const spinDir = i % 2 === 0 ? 1 : -1;
877
+ const spinRate = (0.65 + depth * 1.4) * spinDir;
878
+ angle += dt * spinRate * settings.rotationSpeed;
879
+ }
701
880
  const sway = settings.wobble ? (Math.sin(phase) * (10 + depth * 22) + Math.sin(phase * 0.5) * 8) * settings.speed : 0;
702
881
  const vx = sway + state.wind.x * (50 + depth * 80) * gust;
703
882
  const flutter = settings.wobble ? 1 + Math.sin(phase * 0.8) * 0.12 : 1;
@@ -712,16 +891,17 @@ function updateSnow(pool, count, state, settings) {
712
891
  } else {
713
892
  pool[base] = x;
714
893
  pool[base + 1] = y;
715
- pool[base + 2] = vx;
894
+ pool[base + 2] = angle;
716
895
  }
717
896
  }
718
897
  }
719
- function drawSnow(pool, count, state, settings, texture, sprite) {
898
+ function drawSnow(pool, count, state, settings, texture, sprite, visibility) {
720
899
  if (count === 0) return;
721
900
  const ctx = state.ctx;
722
901
  const aspect = sprite ? sprite.aspect : 1;
723
902
  ctx.fillStyle = settings.color;
724
903
  for (let i = 0; i < count; i++) {
904
+ if (visibility[i] === 0) continue;
725
905
  const base = i * PARTICLE_STRIDE;
726
906
  const x = pool[base];
727
907
  const y = pool[base + 1];
@@ -729,14 +909,14 @@ function drawSnow(pool, count, state, settings, texture, sprite) {
729
909
  const r = Math.max(0, settings.minSize + depth * (settings.maxSize - settings.minSize));
730
910
  if (r === 0) continue;
731
911
  const fade = Math.max(0, Math.min(1, (y + 20) / 45, (state.height + 20 - y) / 55));
732
- ctx.globalAlpha = pool[base + 4] * fade;
912
+ ctx.globalAlpha = pool[base + 4] * fade * visibility[i];
733
913
  if (sprite) {
734
914
  const halfWidth = r * Math.min(1, aspect);
735
915
  const halfHeight = r / Math.max(1, aspect);
736
916
  ctx.save();
737
917
  try {
738
918
  ctx.translate(x, y);
739
- if (settings.wobble) ctx.rotate(Math.sin(pool[base + 5]) * 0.35);
919
+ if (settings.rotation) ctx.rotate(pool[base + 2]);
740
920
  ctx.drawImage(sprite.texture, -halfWidth, -halfHeight, halfWidth * 2, halfHeight * 2);
741
921
  } finally {
742
922
  ctx.restore();
@@ -754,31 +934,60 @@ function drawSnow(pool, count, state, settings, texture, sprite) {
754
934
  }
755
935
  function Snow({
756
936
  intensity = 0.5,
937
+ transitionDuration = 0,
757
938
  speed = 1,
758
939
  color = "rgba(255, 255, 255, 0.85)",
759
940
  sprite,
760
941
  minSize = sprite ? 6 : 1.5,
761
942
  maxSize = sprite ? 18 : 4,
762
- wobble = true
943
+ wobble = true,
944
+ rotation,
945
+ rotationSpeed
763
946
  }) {
764
947
  const ticker = useContext(TickerCtx);
765
948
  if (!ticker) {
766
949
  throw new Error("<Snow /> must be used inside <WeatherCanvas>");
767
950
  }
951
+ const effectiveRotation = rotation ?? (rotationSpeed !== void 0 ? true : wobble);
952
+ const effectiveRotationSpeed = rotationSpeed ?? 1;
768
953
  const settingsRef = useRef({
769
954
  intensity,
955
+ transitionDuration,
770
956
  speed,
771
957
  color,
772
958
  minSize,
773
959
  maxSize,
774
- wobble
960
+ wobble,
961
+ rotation: effectiveRotation,
962
+ rotationSpeed: effectiveRotationSpeed
775
963
  });
776
964
  const textureRef = useRef(null);
777
965
  const spriteRef = useRef(null);
778
966
  useEffect(() => {
779
- settingsRef.current = { intensity, speed, color, minSize, maxSize, wobble };
967
+ settingsRef.current = {
968
+ intensity,
969
+ transitionDuration,
970
+ speed,
971
+ color,
972
+ minSize,
973
+ maxSize,
974
+ wobble,
975
+ rotation: effectiveRotation,
976
+ rotationSpeed: effectiveRotationSpeed
977
+ };
780
978
  ticker.invalidate();
781
- }, [ticker, intensity, speed, color, minSize, maxSize, wobble]);
979
+ }, [
980
+ ticker,
981
+ intensity,
982
+ transitionDuration,
983
+ speed,
984
+ color,
985
+ minSize,
986
+ maxSize,
987
+ wobble,
988
+ effectiveRotation,
989
+ effectiveRotationSpeed
990
+ ]);
782
991
  useEffect(() => {
783
992
  textureRef.current = createSnowTexture(color);
784
993
  ticker.invalidate();
@@ -821,13 +1030,21 @@ function Snow({
821
1030
  };
822
1031
  }, [ticker, sprite]);
823
1032
  useEffect(() => {
824
- let pool = createParticlePool(ticker.getBudget().maxParticles);
1033
+ const capacity = ticker.getBudget().maxParticles;
1034
+ let pool = createParticlePool(capacity);
1035
+ const particles = createParticleTransition(
1036
+ Math.round(Math.max(0, Math.min(1, settingsRef.current.intensity)) * capacity),
1037
+ capacity,
1038
+ settingsRef.current.transitionDuration
1039
+ );
825
1040
  let initializedCount = 0;
826
1041
  const unregister = ticker.register(LAYER.MIDGROUND, (state) => {
827
1042
  const settings = settingsRef.current;
828
- const count = Math.round(
1043
+ const targetCount = Math.round(
829
1044
  Math.max(0, Math.min(1, settings.intensity)) * state.budget.maxParticles
830
1045
  );
1046
+ updateParticleTransition(particles, targetCount, settings.transitionDuration, state);
1047
+ const count = particles.count;
831
1048
  pool = growParticlePool(pool, state.budget.maxParticles);
832
1049
  if (count > initializedCount) {
833
1050
  seedSnowRange(pool, initializedCount, count, state);
@@ -844,7 +1061,7 @@ function Snow({
844
1061
  spriteRef.current = createSnowSprite(cachedSprite.image, size) ?? cachedSprite;
845
1062
  }
846
1063
  }
847
- drawSnow(pool, count, state, settings, textureRef.current, spriteRef.current);
1064
+ drawSnow(pool, count, state, settings, textureRef.current, spriteRef.current, particles.visibility);
848
1065
  });
849
1066
  return unregister;
850
1067
  }, [ticker]);
@@ -886,6 +1103,7 @@ function renderRadialGradient(surface, size, color) {
886
1103
  }
887
1104
  function Fog({
888
1105
  density = 0.4,
1106
+ transitionDuration = 0,
889
1107
  speed = 1,
890
1108
  color = "rgba(220, 225, 235, 0.35)",
891
1109
  layers = 3
@@ -894,11 +1112,11 @@ function Fog({
894
1112
  if (!ticker) {
895
1113
  throw new Error("<Fog /> must be used inside <WeatherCanvas>");
896
1114
  }
897
- const settingsRef = useRef({ density, speed, color, layers });
1115
+ const settingsRef = useRef({ density, transitionDuration, speed, color, layers });
898
1116
  useEffect(() => {
899
- settingsRef.current = { density, speed, color, layers };
1117
+ settingsRef.current = { density, transitionDuration, speed, color, layers };
900
1118
  ticker.invalidate();
901
- }, [ticker, density, speed, color, layers]);
1119
+ }, [ticker, density, transitionDuration, speed, color, layers]);
902
1120
  const textureRef = useRef(null);
903
1121
  const imageBitmapRef = useRef(null);
904
1122
  useEffect(() => {
@@ -945,13 +1163,18 @@ function Fog({
945
1163
  }, [ticker, color]);
946
1164
  useEffect(() => {
947
1165
  const puffs = [];
1166
+ let currentDensity = Math.max(0, Math.min(1, settingsRef.current.density));
1167
+ const transition = createTransition(currentDensity, settingsRef.current.transitionDuration);
948
1168
  const unregister = ticker.register(LAYER.BACKGROUND, (state) => {
949
1169
  const texture = textureRef.current;
950
1170
  if (!texture) return;
951
1171
  const settings = settingsRef.current;
952
1172
  const numLayers = Math.round(Math.max(1, Math.min(6, settings.layers)));
953
1173
  const count = numLayers * 3;
954
- const baseAlpha = Math.max(0, Math.min(1, settings.density));
1174
+ const targetDensity = Math.max(0, Math.min(1, settings.density));
1175
+ const step = stepTransition(transition, targetDensity, settings.transitionDuration, state);
1176
+ currentDensity = step === 1 ? targetDensity : currentDensity + (targetDensity - currentDensity) * step;
1177
+ const baseAlpha = currentDensity;
955
1178
  if (baseAlpha === 0) return;
956
1179
  for (let i = puffs.length; i < count; i++) {
957
1180
  const layer = Math.floor(i / 3);