@hdcodedev/snowfall 1.0.13 → 1.0.14
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.mts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +91 -42
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +100 -51
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
// src/components/Snowfall.tsx
|
|
4
|
-
import { useEffect, useRef as useRef4, useState as useState2 } from "react";
|
|
4
|
+
import { useEffect as useEffect2, useRef as useRef4, useState as useState2 } from "react";
|
|
5
5
|
|
|
6
6
|
// src/components/SnowfallProvider.tsx
|
|
7
7
|
import { createContext, useContext, useState } from "react";
|
|
@@ -25,8 +25,9 @@ var DEFAULT_PHYSICS = {
|
|
|
25
25
|
MAX: 1.6
|
|
26
26
|
},
|
|
27
27
|
MAX_SURFACES: 15,
|
|
28
|
-
COLLISION_CHECK_RATE: 0.3
|
|
28
|
+
COLLISION_CHECK_RATE: 0.3,
|
|
29
29
|
// 30% of snowflakes check collisions per frame
|
|
30
|
+
MAX_RENDER_DPR: 1.25
|
|
30
31
|
};
|
|
31
32
|
var SnowfallContext = createContext(void 0);
|
|
32
33
|
function SnowfallProvider({ children, initialDebug = false, initialEnabled = true }) {
|
|
@@ -352,7 +353,7 @@ var updateSnowflakePosition = (flake, dt) => {
|
|
|
352
353
|
flake.x += (flake.wind + Math.sin(flake.wobble) * 0.5) * dt;
|
|
353
354
|
flake.y += (flake.speed + Math.cos(flake.wobble * 0.5) * 0.1) * dt;
|
|
354
355
|
};
|
|
355
|
-
var checkSideCollision = (
|
|
356
|
+
var checkSideCollision = (flakeViewportX, flakeViewportY, rect, acc, config) => {
|
|
356
357
|
const isInVerticalBounds = flakeViewportY >= rect.top && flakeViewportY <= rect.bottom;
|
|
357
358
|
if (!isInVerticalBounds || acc.maxSideHeight <= 0) {
|
|
358
359
|
return false;
|
|
@@ -432,7 +433,7 @@ var updateSnowflakes = (snowflakes, elementRects, config, dt, worldWidth, worldH
|
|
|
432
433
|
const { rect, acc } = item;
|
|
433
434
|
const isBottom = acc.type === VAL_BOTTOM;
|
|
434
435
|
if (!landed && !isBottom) {
|
|
435
|
-
landed = checkSideCollision(
|
|
436
|
+
landed = checkSideCollision(flakeViewportX, flakeViewportY, rect, acc, config);
|
|
436
437
|
if (landed) break;
|
|
437
438
|
}
|
|
438
439
|
if (!landed) {
|
|
@@ -534,11 +535,11 @@ import { useRef as useRef2, useCallback as useCallback2 } from "react";
|
|
|
534
535
|
function useSnowfallCanvas() {
|
|
535
536
|
const canvasRef = useRef2(null);
|
|
536
537
|
const dprRef = useRef2(1);
|
|
537
|
-
const resizeCanvas = useCallback2(() => {
|
|
538
|
+
const resizeCanvas = useCallback2((maxRenderDpr = Number.POSITIVE_INFINITY) => {
|
|
538
539
|
if (canvasRef.current) {
|
|
539
540
|
const newWidth = window.innerWidth;
|
|
540
541
|
const newHeight = window.innerHeight;
|
|
541
|
-
const dpr = window.devicePixelRatio || 1;
|
|
542
|
+
const dpr = Math.min(window.devicePixelRatio || 1, Math.max(1, maxRenderDpr));
|
|
542
543
|
dprRef.current = dpr;
|
|
543
544
|
canvasRef.current.width = newWidth * dpr;
|
|
544
545
|
canvasRef.current.height = newHeight * dpr;
|
|
@@ -554,25 +555,46 @@ function useSnowfallCanvas() {
|
|
|
554
555
|
}
|
|
555
556
|
|
|
556
557
|
// src/hooks/useAnimationLoop.ts
|
|
557
|
-
import { useRef as useRef3, useCallback as useCallback3 } from "react";
|
|
558
|
+
import { useEffect, useRef as useRef3, useCallback as useCallback3 } from "react";
|
|
558
559
|
|
|
559
560
|
// src/core/draw.ts
|
|
560
561
|
var ACC_FILL_STYLE = "rgba(255, 255, 255, 0.95)";
|
|
561
562
|
var ACC_SHADOW_COLOR = "rgba(200, 230, 255, 0.6)";
|
|
563
|
+
var OPACITY_BUCKETS2 = [0.3, 0.5, 0.7, 0.9];
|
|
562
564
|
var drawSnowflakes = (ctx, flakes) => {
|
|
563
565
|
if (flakes.length === 0) return;
|
|
564
566
|
ctx.fillStyle = "#FFFFFF";
|
|
565
|
-
for (const
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
567
|
+
for (const alpha of OPACITY_BUCKETS2) {
|
|
568
|
+
let hasPath = false;
|
|
569
|
+
for (const flake of flakes) {
|
|
570
|
+
if (flake.isBackground || flake.glowOpacity !== alpha) continue;
|
|
571
|
+
if (!hasPath) {
|
|
572
|
+
ctx.globalAlpha = alpha;
|
|
573
|
+
ctx.beginPath();
|
|
574
|
+
hasPath = true;
|
|
575
|
+
}
|
|
576
|
+
ctx.moveTo(flake.x + flake.glowRadius, flake.y);
|
|
569
577
|
ctx.arc(flake.x, flake.y, flake.glowRadius, 0, TAU);
|
|
578
|
+
}
|
|
579
|
+
if (hasPath) {
|
|
580
|
+
ctx.fill();
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
for (const alpha of OPACITY_BUCKETS2) {
|
|
584
|
+
let hasPath = false;
|
|
585
|
+
for (const flake of flakes) {
|
|
586
|
+
if (flake.opacity !== alpha) continue;
|
|
587
|
+
if (!hasPath) {
|
|
588
|
+
ctx.globalAlpha = alpha;
|
|
589
|
+
ctx.beginPath();
|
|
590
|
+
hasPath = true;
|
|
591
|
+
}
|
|
592
|
+
ctx.moveTo(flake.x + flake.radius, flake.y);
|
|
593
|
+
ctx.arc(flake.x, flake.y, flake.radius, 0, TAU);
|
|
594
|
+
}
|
|
595
|
+
if (hasPath) {
|
|
570
596
|
ctx.fill();
|
|
571
597
|
}
|
|
572
|
-
ctx.globalAlpha = flake.opacity;
|
|
573
|
-
ctx.beginPath();
|
|
574
|
-
ctx.arc(flake.x, flake.y, flake.radius, 0, TAU);
|
|
575
|
-
ctx.fill();
|
|
576
598
|
}
|
|
577
599
|
ctx.globalAlpha = 1;
|
|
578
600
|
};
|
|
@@ -668,9 +690,12 @@ var drawSideAccumulations = (ctx, elementRects, scrollX, scrollY) => {
|
|
|
668
690
|
// src/hooks/useAnimationLoop.ts
|
|
669
691
|
function useAnimationLoop(params) {
|
|
670
692
|
const animationIdRef = useRef3(0);
|
|
693
|
+
const animateRef = useRef3(() => {
|
|
694
|
+
});
|
|
671
695
|
const lastTimeRef = useRef3(0);
|
|
672
696
|
const lastMetricsUpdateRef = useRef3(0);
|
|
673
697
|
const elementRectsRef = useRef3([]);
|
|
698
|
+
const visibleRectsRef = useRef3([]);
|
|
674
699
|
const dirtyRectsRef = useRef3(true);
|
|
675
700
|
const animate = useCallback3((currentTime) => {
|
|
676
701
|
const {
|
|
@@ -688,17 +713,17 @@ function useAnimationLoop(params) {
|
|
|
688
713
|
} = params;
|
|
689
714
|
const canvas = canvasRef.current;
|
|
690
715
|
if (!canvas) {
|
|
691
|
-
animationIdRef.current = requestAnimationFrame(
|
|
716
|
+
animationIdRef.current = requestAnimationFrame(animateRef.current);
|
|
692
717
|
return;
|
|
693
718
|
}
|
|
694
719
|
const ctx = canvas.getContext("2d");
|
|
695
720
|
if (!ctx) {
|
|
696
|
-
animationIdRef.current = requestAnimationFrame(
|
|
721
|
+
animationIdRef.current = requestAnimationFrame(animateRef.current);
|
|
697
722
|
return;
|
|
698
723
|
}
|
|
699
724
|
if (lastTimeRef.current === 0) {
|
|
700
725
|
lastTimeRef.current = currentTime;
|
|
701
|
-
animationIdRef.current = requestAnimationFrame(
|
|
726
|
+
animationIdRef.current = requestAnimationFrame(animateRef.current);
|
|
702
727
|
return;
|
|
703
728
|
}
|
|
704
729
|
const deltaTime = Math.min(currentTime - lastTimeRef.current, 50);
|
|
@@ -707,8 +732,8 @@ function useAnimationLoop(params) {
|
|
|
707
732
|
metricsRef.current.rafGap = currentTime - lastTimeRef.current;
|
|
708
733
|
lastTimeRef.current = currentTime;
|
|
709
734
|
const dt = deltaTime / 16.67;
|
|
710
|
-
const frameStartTime =
|
|
711
|
-
const clearStart =
|
|
735
|
+
const frameStartTime = now;
|
|
736
|
+
const clearStart = now;
|
|
712
737
|
const dpr = dprRef.current;
|
|
713
738
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
714
739
|
ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
|
|
@@ -725,30 +750,42 @@ function useAnimationLoop(params) {
|
|
|
725
750
|
}
|
|
726
751
|
const physicsStart = performance.now();
|
|
727
752
|
meltAndSmoothAccumulation(elementRectsRef.current, physicsConfigRef.current, dt);
|
|
753
|
+
const docEl = document.documentElement;
|
|
754
|
+
const worldWidth = docEl.scrollWidth;
|
|
755
|
+
const worldHeight = docEl.scrollHeight;
|
|
728
756
|
updateSnowflakes(
|
|
729
757
|
snowflakes,
|
|
730
758
|
elementRectsRef.current,
|
|
731
759
|
physicsConfigRef.current,
|
|
732
760
|
dt,
|
|
733
|
-
|
|
734
|
-
|
|
761
|
+
worldWidth,
|
|
762
|
+
worldHeight
|
|
735
763
|
);
|
|
736
764
|
metricsRef.current.physicsTime = performance.now() - physicsStart;
|
|
737
765
|
const drawStart = performance.now();
|
|
738
766
|
drawSnowflakes(ctx, snowflakes);
|
|
739
767
|
if (isEnabledRef.current && snowflakes.length < physicsConfigRef.current.MAX_FLAKES) {
|
|
768
|
+
const minFlakeFloor = Math.min(80, physicsConfigRef.current.MAX_FLAKES);
|
|
769
|
+
const shouldForceSpawn = snowflakes.length < minFlakeFloor;
|
|
740
770
|
const currentFps = getCurrentFps();
|
|
741
|
-
const
|
|
771
|
+
const isVisible = document.visibilityState === "visible";
|
|
772
|
+
const isUnderFpsThreshold = isVisible && currentFps > 0 && currentFps < 40;
|
|
773
|
+
const shouldSpawn = shouldForceSpawn || !isUnderFpsThreshold || Math.random() < 0.2;
|
|
742
774
|
if (shouldSpawn) {
|
|
743
775
|
const isBackground = Math.random() < 0.4;
|
|
744
|
-
snowflakes.push(createSnowflake(
|
|
776
|
+
snowflakes.push(createSnowflake(worldWidth, physicsConfigRef.current, isBackground));
|
|
745
777
|
}
|
|
746
778
|
}
|
|
747
779
|
const viewportWidth = window.innerWidth;
|
|
748
780
|
const viewportHeight = window.innerHeight;
|
|
749
|
-
const visibleRects =
|
|
750
|
-
|
|
751
|
-
)
|
|
781
|
+
const visibleRects = visibleRectsRef.current;
|
|
782
|
+
visibleRects.length = 0;
|
|
783
|
+
for (const item of elementRectsRef.current) {
|
|
784
|
+
const { rect } = item;
|
|
785
|
+
if (rect.right >= 0 && rect.left <= viewportWidth && rect.bottom >= 0 && rect.top <= viewportHeight) {
|
|
786
|
+
visibleRects.push(item);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
752
789
|
if (visibleRects.length > 0) {
|
|
753
790
|
drawAccumulations(ctx, visibleRects, scrollX, scrollY);
|
|
754
791
|
drawSideAccumulations(ctx, visibleRects, scrollX, scrollY);
|
|
@@ -763,13 +800,16 @@ function useAnimationLoop(params) {
|
|
|
763
800
|
));
|
|
764
801
|
lastMetricsUpdateRef.current = currentTime;
|
|
765
802
|
}
|
|
766
|
-
animationIdRef.current = requestAnimationFrame(
|
|
803
|
+
animationIdRef.current = requestAnimationFrame(animateRef.current);
|
|
767
804
|
}, [params]);
|
|
805
|
+
useEffect(() => {
|
|
806
|
+
animateRef.current = animate;
|
|
807
|
+
}, [animate]);
|
|
768
808
|
const start = useCallback3(() => {
|
|
769
809
|
lastTimeRef.current = 0;
|
|
770
810
|
lastMetricsUpdateRef.current = 0;
|
|
771
|
-
animationIdRef.current = requestAnimationFrame(
|
|
772
|
-
}, [
|
|
811
|
+
animationIdRef.current = requestAnimationFrame(animateRef.current);
|
|
812
|
+
}, []);
|
|
773
813
|
const stop = useCallback3(() => {
|
|
774
814
|
cancelAnimationFrame(animationIdRef.current);
|
|
775
815
|
}, []);
|
|
@@ -809,26 +849,31 @@ function Snowfall() {
|
|
|
809
849
|
buildMetrics,
|
|
810
850
|
setMetricsRef
|
|
811
851
|
});
|
|
812
|
-
|
|
852
|
+
useEffect2(() => {
|
|
813
853
|
requestAnimationFrame(() => setIsMounted(true));
|
|
814
854
|
}, []);
|
|
815
|
-
|
|
855
|
+
useEffect2(() => {
|
|
816
856
|
isEnabledRef.current = isEnabled;
|
|
817
857
|
}, [isEnabled]);
|
|
818
|
-
|
|
858
|
+
useEffect2(() => {
|
|
819
859
|
physicsConfigRef.current = physicsConfig;
|
|
820
|
-
|
|
821
|
-
|
|
860
|
+
if (isMounted) {
|
|
861
|
+
resizeCanvas(physicsConfig.MAX_RENDER_DPR);
|
|
862
|
+
}
|
|
863
|
+
}, [isMounted, physicsConfig, resizeCanvas]);
|
|
864
|
+
useEffect2(() => {
|
|
822
865
|
setMetricsRef.current = setMetrics;
|
|
823
866
|
}, [setMetrics]);
|
|
824
|
-
|
|
867
|
+
useEffect2(() => {
|
|
825
868
|
if (!isMounted) return;
|
|
826
869
|
const canvas = canvasRef.current;
|
|
827
870
|
if (!canvas) return;
|
|
828
871
|
const ctx = canvas.getContext("2d");
|
|
829
872
|
if (!ctx) return;
|
|
830
|
-
resizeCanvas();
|
|
873
|
+
resizeCanvas(physicsConfigRef.current.MAX_RENDER_DPR);
|
|
831
874
|
snowflakesRef.current = [];
|
|
875
|
+
let isUnmounted = false;
|
|
876
|
+
let scheduledInitFrame = 0;
|
|
832
877
|
const surfaceObserver = new ResizeObserver((entries) => {
|
|
833
878
|
let needsUpdate = false;
|
|
834
879
|
for (const entry of entries) {
|
|
@@ -838,10 +883,11 @@ function Snowfall() {
|
|
|
838
883
|
}
|
|
839
884
|
}
|
|
840
885
|
if (needsUpdate) {
|
|
841
|
-
|
|
886
|
+
scheduleAccumulationInit();
|
|
842
887
|
}
|
|
843
888
|
});
|
|
844
889
|
const initAccumulationWrapper = () => {
|
|
890
|
+
if (isUnmounted) return;
|
|
845
891
|
const scanStart = performance.now();
|
|
846
892
|
initializeAccumulation(accumulationRef.current, physicsConfigRef.current);
|
|
847
893
|
surfaceObserver.disconnect();
|
|
@@ -851,22 +897,25 @@ function Snowfall() {
|
|
|
851
897
|
metricsRef.current.scanTime = performance.now() - scanStart;
|
|
852
898
|
markRectsDirty();
|
|
853
899
|
};
|
|
900
|
+
const scheduleAccumulationInit = () => {
|
|
901
|
+
if (scheduledInitFrame !== 0 || isUnmounted) return;
|
|
902
|
+
scheduledInitFrame = requestAnimationFrame(() => {
|
|
903
|
+
scheduledInitFrame = 0;
|
|
904
|
+
initAccumulationWrapper();
|
|
905
|
+
});
|
|
906
|
+
};
|
|
854
907
|
initAccumulationWrapper();
|
|
855
908
|
requestAnimationFrame(() => {
|
|
856
|
-
if (isMounted) setIsVisible(true);
|
|
909
|
+
if (!isUnmounted && isMounted) setIsVisible(true);
|
|
857
910
|
});
|
|
858
911
|
startAnimation();
|
|
859
912
|
const handleResize = () => {
|
|
860
|
-
resizeCanvas();
|
|
913
|
+
resizeCanvas(physicsConfigRef.current.MAX_RENDER_DPR);
|
|
861
914
|
accumulationRef.current.clear();
|
|
862
915
|
initAccumulationWrapper();
|
|
863
916
|
markRectsDirty();
|
|
864
917
|
};
|
|
865
918
|
window.addEventListener("resize", handleResize);
|
|
866
|
-
const windowResizeObserver = new ResizeObserver(() => {
|
|
867
|
-
resizeCanvas();
|
|
868
|
-
});
|
|
869
|
-
windowResizeObserver.observe(document.body);
|
|
870
919
|
const mutationObserver = new MutationObserver((mutations) => {
|
|
871
920
|
let hasStructuralChange = false;
|
|
872
921
|
for (const mutation of mutations) {
|
|
@@ -876,10 +925,7 @@ function Snowfall() {
|
|
|
876
925
|
}
|
|
877
926
|
}
|
|
878
927
|
if (hasStructuralChange) {
|
|
879
|
-
|
|
880
|
-
initializeAccumulation(accumulationRef.current, physicsConfigRef.current);
|
|
881
|
-
metricsRef.current.scanTime = performance.now() - scanStart;
|
|
882
|
-
markRectsDirty();
|
|
928
|
+
scheduleAccumulationInit();
|
|
883
929
|
}
|
|
884
930
|
});
|
|
885
931
|
mutationObserver.observe(document.body, {
|
|
@@ -887,9 +933,12 @@ function Snowfall() {
|
|
|
887
933
|
subtree: true
|
|
888
934
|
});
|
|
889
935
|
return () => {
|
|
936
|
+
isUnmounted = true;
|
|
937
|
+
if (scheduledInitFrame !== 0) {
|
|
938
|
+
cancelAnimationFrame(scheduledInitFrame);
|
|
939
|
+
}
|
|
890
940
|
stopAnimation();
|
|
891
941
|
window.removeEventListener("resize", handleResize);
|
|
892
|
-
windowResizeObserver.disconnect();
|
|
893
942
|
mutationObserver.disconnect();
|
|
894
943
|
surfaceObserver.disconnect();
|
|
895
944
|
};
|
|
@@ -916,13 +965,13 @@ function Snowfall() {
|
|
|
916
965
|
}
|
|
917
966
|
|
|
918
967
|
// src/components/DebugPanel.tsx
|
|
919
|
-
import { useEffect as
|
|
968
|
+
import { useEffect as useEffect3, useState as useState3 } from "react";
|
|
920
969
|
import { Fragment as Fragment2, jsx as jsx3, jsxs } from "react/jsx-runtime";
|
|
921
970
|
function DebugPanel({ defaultOpen = true }) {
|
|
922
971
|
const { debugMode, toggleDebug, metrics } = useSnowfall();
|
|
923
972
|
const [isMinimized, setIsMinimized] = useState3(!defaultOpen);
|
|
924
973
|
const [copied, setCopied] = useState3(false);
|
|
925
|
-
|
|
974
|
+
useEffect3(() => {
|
|
926
975
|
const handleKeyDown = (e) => {
|
|
927
976
|
if (e.shiftKey && e.key === "D") {
|
|
928
977
|
toggleDebug();
|