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/LICENSE +21 -0
- package/README.md +56 -3
- package/dist/index.cjs +267 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -3
- package/dist/index.d.mts +40 -3
- package/dist/index.d.ts +40 -3
- package/dist/index.mjs +267 -44
- package/dist/index.mjs.map +1 -1
- package/package.json +16 -5
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
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
const
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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.
|
|
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
|
|
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 = {
|
|
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
|
-
}, [
|
|
730
|
+
}, [
|
|
731
|
+
ticker,
|
|
732
|
+
intensity,
|
|
733
|
+
transitionDuration,
|
|
734
|
+
angle,
|
|
735
|
+
color,
|
|
736
|
+
speed,
|
|
737
|
+
effectiveDropLength,
|
|
738
|
+
effectiveDropThickness,
|
|
739
|
+
effectiveSplashes
|
|
740
|
+
]);
|
|
589
741
|
useEffect(() => {
|
|
590
|
-
|
|
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
|
|
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(
|
|
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] =
|
|
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] =
|
|
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] =
|
|
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.
|
|
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 = {
|
|
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
|
-
}, [
|
|
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
|
-
|
|
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
|
|
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
|
|
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);
|