@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.d.mts
CHANGED
|
@@ -23,6 +23,10 @@ interface PhysicsConfig {
|
|
|
23
23
|
};
|
|
24
24
|
MAX_SURFACES: number;
|
|
25
25
|
COLLISION_CHECK_RATE: number;
|
|
26
|
+
/**
|
|
27
|
+
* Optional cap for render DPR. Lower values reduce GPU load on high-DPI displays.
|
|
28
|
+
*/
|
|
29
|
+
MAX_RENDER_DPR?: number;
|
|
26
30
|
}
|
|
27
31
|
declare const DEFAULT_PHYSICS: PhysicsConfig;
|
|
28
32
|
interface PerformanceMetrics {
|
package/dist/index.d.ts
CHANGED
|
@@ -23,6 +23,10 @@ interface PhysicsConfig {
|
|
|
23
23
|
};
|
|
24
24
|
MAX_SURFACES: number;
|
|
25
25
|
COLLISION_CHECK_RATE: number;
|
|
26
|
+
/**
|
|
27
|
+
* Optional cap for render DPR. Lower values reduce GPU load on high-DPI displays.
|
|
28
|
+
*/
|
|
29
|
+
MAX_RENDER_DPR?: number;
|
|
26
30
|
}
|
|
27
31
|
declare const DEFAULT_PHYSICS: PhysicsConfig;
|
|
28
32
|
interface PerformanceMetrics {
|
package/dist/index.js
CHANGED
|
@@ -54,8 +54,9 @@ var DEFAULT_PHYSICS = {
|
|
|
54
54
|
MAX: 1.6
|
|
55
55
|
},
|
|
56
56
|
MAX_SURFACES: 15,
|
|
57
|
-
COLLISION_CHECK_RATE: 0.3
|
|
57
|
+
COLLISION_CHECK_RATE: 0.3,
|
|
58
58
|
// 30% of snowflakes check collisions per frame
|
|
59
|
+
MAX_RENDER_DPR: 1.25
|
|
59
60
|
};
|
|
60
61
|
var SnowfallContext = (0, import_react.createContext)(void 0);
|
|
61
62
|
function SnowfallProvider({ children, initialDebug = false, initialEnabled = true }) {
|
|
@@ -381,7 +382,7 @@ var updateSnowflakePosition = (flake, dt) => {
|
|
|
381
382
|
flake.x += (flake.wind + Math.sin(flake.wobble) * 0.5) * dt;
|
|
382
383
|
flake.y += (flake.speed + Math.cos(flake.wobble * 0.5) * 0.1) * dt;
|
|
383
384
|
};
|
|
384
|
-
var checkSideCollision = (
|
|
385
|
+
var checkSideCollision = (flakeViewportX, flakeViewportY, rect, acc, config) => {
|
|
385
386
|
const isInVerticalBounds = flakeViewportY >= rect.top && flakeViewportY <= rect.bottom;
|
|
386
387
|
if (!isInVerticalBounds || acc.maxSideHeight <= 0) {
|
|
387
388
|
return false;
|
|
@@ -461,7 +462,7 @@ var updateSnowflakes = (snowflakes, elementRects, config, dt, worldWidth, worldH
|
|
|
461
462
|
const { rect, acc } = item;
|
|
462
463
|
const isBottom = acc.type === VAL_BOTTOM;
|
|
463
464
|
if (!landed && !isBottom) {
|
|
464
|
-
landed = checkSideCollision(
|
|
465
|
+
landed = checkSideCollision(flakeViewportX, flakeViewportY, rect, acc, config);
|
|
465
466
|
if (landed) break;
|
|
466
467
|
}
|
|
467
468
|
if (!landed) {
|
|
@@ -563,11 +564,11 @@ var import_react3 = require("react");
|
|
|
563
564
|
function useSnowfallCanvas() {
|
|
564
565
|
const canvasRef = (0, import_react3.useRef)(null);
|
|
565
566
|
const dprRef = (0, import_react3.useRef)(1);
|
|
566
|
-
const resizeCanvas = (0, import_react3.useCallback)(() => {
|
|
567
|
+
const resizeCanvas = (0, import_react3.useCallback)((maxRenderDpr = Number.POSITIVE_INFINITY) => {
|
|
567
568
|
if (canvasRef.current) {
|
|
568
569
|
const newWidth = window.innerWidth;
|
|
569
570
|
const newHeight = window.innerHeight;
|
|
570
|
-
const dpr = window.devicePixelRatio || 1;
|
|
571
|
+
const dpr = Math.min(window.devicePixelRatio || 1, Math.max(1, maxRenderDpr));
|
|
571
572
|
dprRef.current = dpr;
|
|
572
573
|
canvasRef.current.width = newWidth * dpr;
|
|
573
574
|
canvasRef.current.height = newHeight * dpr;
|
|
@@ -588,20 +589,41 @@ var import_react4 = require("react");
|
|
|
588
589
|
// src/core/draw.ts
|
|
589
590
|
var ACC_FILL_STYLE = "rgba(255, 255, 255, 0.95)";
|
|
590
591
|
var ACC_SHADOW_COLOR = "rgba(200, 230, 255, 0.6)";
|
|
592
|
+
var OPACITY_BUCKETS2 = [0.3, 0.5, 0.7, 0.9];
|
|
591
593
|
var drawSnowflakes = (ctx, flakes) => {
|
|
592
594
|
if (flakes.length === 0) return;
|
|
593
595
|
ctx.fillStyle = "#FFFFFF";
|
|
594
|
-
for (const
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
596
|
+
for (const alpha of OPACITY_BUCKETS2) {
|
|
597
|
+
let hasPath = false;
|
|
598
|
+
for (const flake of flakes) {
|
|
599
|
+
if (flake.isBackground || flake.glowOpacity !== alpha) continue;
|
|
600
|
+
if (!hasPath) {
|
|
601
|
+
ctx.globalAlpha = alpha;
|
|
602
|
+
ctx.beginPath();
|
|
603
|
+
hasPath = true;
|
|
604
|
+
}
|
|
605
|
+
ctx.moveTo(flake.x + flake.glowRadius, flake.y);
|
|
598
606
|
ctx.arc(flake.x, flake.y, flake.glowRadius, 0, TAU);
|
|
607
|
+
}
|
|
608
|
+
if (hasPath) {
|
|
609
|
+
ctx.fill();
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
for (const alpha of OPACITY_BUCKETS2) {
|
|
613
|
+
let hasPath = false;
|
|
614
|
+
for (const flake of flakes) {
|
|
615
|
+
if (flake.opacity !== alpha) continue;
|
|
616
|
+
if (!hasPath) {
|
|
617
|
+
ctx.globalAlpha = alpha;
|
|
618
|
+
ctx.beginPath();
|
|
619
|
+
hasPath = true;
|
|
620
|
+
}
|
|
621
|
+
ctx.moveTo(flake.x + flake.radius, flake.y);
|
|
622
|
+
ctx.arc(flake.x, flake.y, flake.radius, 0, TAU);
|
|
623
|
+
}
|
|
624
|
+
if (hasPath) {
|
|
599
625
|
ctx.fill();
|
|
600
626
|
}
|
|
601
|
-
ctx.globalAlpha = flake.opacity;
|
|
602
|
-
ctx.beginPath();
|
|
603
|
-
ctx.arc(flake.x, flake.y, flake.radius, 0, TAU);
|
|
604
|
-
ctx.fill();
|
|
605
627
|
}
|
|
606
628
|
ctx.globalAlpha = 1;
|
|
607
629
|
};
|
|
@@ -697,9 +719,12 @@ var drawSideAccumulations = (ctx, elementRects, scrollX, scrollY) => {
|
|
|
697
719
|
// src/hooks/useAnimationLoop.ts
|
|
698
720
|
function useAnimationLoop(params) {
|
|
699
721
|
const animationIdRef = (0, import_react4.useRef)(0);
|
|
722
|
+
const animateRef = (0, import_react4.useRef)(() => {
|
|
723
|
+
});
|
|
700
724
|
const lastTimeRef = (0, import_react4.useRef)(0);
|
|
701
725
|
const lastMetricsUpdateRef = (0, import_react4.useRef)(0);
|
|
702
726
|
const elementRectsRef = (0, import_react4.useRef)([]);
|
|
727
|
+
const visibleRectsRef = (0, import_react4.useRef)([]);
|
|
703
728
|
const dirtyRectsRef = (0, import_react4.useRef)(true);
|
|
704
729
|
const animate = (0, import_react4.useCallback)((currentTime) => {
|
|
705
730
|
const {
|
|
@@ -717,17 +742,17 @@ function useAnimationLoop(params) {
|
|
|
717
742
|
} = params;
|
|
718
743
|
const canvas = canvasRef.current;
|
|
719
744
|
if (!canvas) {
|
|
720
|
-
animationIdRef.current = requestAnimationFrame(
|
|
745
|
+
animationIdRef.current = requestAnimationFrame(animateRef.current);
|
|
721
746
|
return;
|
|
722
747
|
}
|
|
723
748
|
const ctx = canvas.getContext("2d");
|
|
724
749
|
if (!ctx) {
|
|
725
|
-
animationIdRef.current = requestAnimationFrame(
|
|
750
|
+
animationIdRef.current = requestAnimationFrame(animateRef.current);
|
|
726
751
|
return;
|
|
727
752
|
}
|
|
728
753
|
if (lastTimeRef.current === 0) {
|
|
729
754
|
lastTimeRef.current = currentTime;
|
|
730
|
-
animationIdRef.current = requestAnimationFrame(
|
|
755
|
+
animationIdRef.current = requestAnimationFrame(animateRef.current);
|
|
731
756
|
return;
|
|
732
757
|
}
|
|
733
758
|
const deltaTime = Math.min(currentTime - lastTimeRef.current, 50);
|
|
@@ -736,8 +761,8 @@ function useAnimationLoop(params) {
|
|
|
736
761
|
metricsRef.current.rafGap = currentTime - lastTimeRef.current;
|
|
737
762
|
lastTimeRef.current = currentTime;
|
|
738
763
|
const dt = deltaTime / 16.67;
|
|
739
|
-
const frameStartTime =
|
|
740
|
-
const clearStart =
|
|
764
|
+
const frameStartTime = now;
|
|
765
|
+
const clearStart = now;
|
|
741
766
|
const dpr = dprRef.current;
|
|
742
767
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
743
768
|
ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
|
|
@@ -754,30 +779,42 @@ function useAnimationLoop(params) {
|
|
|
754
779
|
}
|
|
755
780
|
const physicsStart = performance.now();
|
|
756
781
|
meltAndSmoothAccumulation(elementRectsRef.current, physicsConfigRef.current, dt);
|
|
782
|
+
const docEl = document.documentElement;
|
|
783
|
+
const worldWidth = docEl.scrollWidth;
|
|
784
|
+
const worldHeight = docEl.scrollHeight;
|
|
757
785
|
updateSnowflakes(
|
|
758
786
|
snowflakes,
|
|
759
787
|
elementRectsRef.current,
|
|
760
788
|
physicsConfigRef.current,
|
|
761
789
|
dt,
|
|
762
|
-
|
|
763
|
-
|
|
790
|
+
worldWidth,
|
|
791
|
+
worldHeight
|
|
764
792
|
);
|
|
765
793
|
metricsRef.current.physicsTime = performance.now() - physicsStart;
|
|
766
794
|
const drawStart = performance.now();
|
|
767
795
|
drawSnowflakes(ctx, snowflakes);
|
|
768
796
|
if (isEnabledRef.current && snowflakes.length < physicsConfigRef.current.MAX_FLAKES) {
|
|
797
|
+
const minFlakeFloor = Math.min(80, physicsConfigRef.current.MAX_FLAKES);
|
|
798
|
+
const shouldForceSpawn = snowflakes.length < minFlakeFloor;
|
|
769
799
|
const currentFps = getCurrentFps();
|
|
770
|
-
const
|
|
800
|
+
const isVisible = document.visibilityState === "visible";
|
|
801
|
+
const isUnderFpsThreshold = isVisible && currentFps > 0 && currentFps < 40;
|
|
802
|
+
const shouldSpawn = shouldForceSpawn || !isUnderFpsThreshold || Math.random() < 0.2;
|
|
771
803
|
if (shouldSpawn) {
|
|
772
804
|
const isBackground = Math.random() < 0.4;
|
|
773
|
-
snowflakes.push(createSnowflake(
|
|
805
|
+
snowflakes.push(createSnowflake(worldWidth, physicsConfigRef.current, isBackground));
|
|
774
806
|
}
|
|
775
807
|
}
|
|
776
808
|
const viewportWidth = window.innerWidth;
|
|
777
809
|
const viewportHeight = window.innerHeight;
|
|
778
|
-
const visibleRects =
|
|
779
|
-
|
|
780
|
-
)
|
|
810
|
+
const visibleRects = visibleRectsRef.current;
|
|
811
|
+
visibleRects.length = 0;
|
|
812
|
+
for (const item of elementRectsRef.current) {
|
|
813
|
+
const { rect } = item;
|
|
814
|
+
if (rect.right >= 0 && rect.left <= viewportWidth && rect.bottom >= 0 && rect.top <= viewportHeight) {
|
|
815
|
+
visibleRects.push(item);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
781
818
|
if (visibleRects.length > 0) {
|
|
782
819
|
drawAccumulations(ctx, visibleRects, scrollX, scrollY);
|
|
783
820
|
drawSideAccumulations(ctx, visibleRects, scrollX, scrollY);
|
|
@@ -792,13 +829,16 @@ function useAnimationLoop(params) {
|
|
|
792
829
|
));
|
|
793
830
|
lastMetricsUpdateRef.current = currentTime;
|
|
794
831
|
}
|
|
795
|
-
animationIdRef.current = requestAnimationFrame(
|
|
832
|
+
animationIdRef.current = requestAnimationFrame(animateRef.current);
|
|
796
833
|
}, [params]);
|
|
834
|
+
(0, import_react4.useEffect)(() => {
|
|
835
|
+
animateRef.current = animate;
|
|
836
|
+
}, [animate]);
|
|
797
837
|
const start = (0, import_react4.useCallback)(() => {
|
|
798
838
|
lastTimeRef.current = 0;
|
|
799
839
|
lastMetricsUpdateRef.current = 0;
|
|
800
|
-
animationIdRef.current = requestAnimationFrame(
|
|
801
|
-
}, [
|
|
840
|
+
animationIdRef.current = requestAnimationFrame(animateRef.current);
|
|
841
|
+
}, []);
|
|
802
842
|
const stop = (0, import_react4.useCallback)(() => {
|
|
803
843
|
cancelAnimationFrame(animationIdRef.current);
|
|
804
844
|
}, []);
|
|
@@ -846,7 +886,10 @@ function Snowfall() {
|
|
|
846
886
|
}, [isEnabled]);
|
|
847
887
|
(0, import_react5.useEffect)(() => {
|
|
848
888
|
physicsConfigRef.current = physicsConfig;
|
|
849
|
-
|
|
889
|
+
if (isMounted) {
|
|
890
|
+
resizeCanvas(physicsConfig.MAX_RENDER_DPR);
|
|
891
|
+
}
|
|
892
|
+
}, [isMounted, physicsConfig, resizeCanvas]);
|
|
850
893
|
(0, import_react5.useEffect)(() => {
|
|
851
894
|
setMetricsRef.current = setMetrics;
|
|
852
895
|
}, [setMetrics]);
|
|
@@ -856,8 +899,10 @@ function Snowfall() {
|
|
|
856
899
|
if (!canvas) return;
|
|
857
900
|
const ctx = canvas.getContext("2d");
|
|
858
901
|
if (!ctx) return;
|
|
859
|
-
resizeCanvas();
|
|
902
|
+
resizeCanvas(physicsConfigRef.current.MAX_RENDER_DPR);
|
|
860
903
|
snowflakesRef.current = [];
|
|
904
|
+
let isUnmounted = false;
|
|
905
|
+
let scheduledInitFrame = 0;
|
|
861
906
|
const surfaceObserver = new ResizeObserver((entries) => {
|
|
862
907
|
let needsUpdate = false;
|
|
863
908
|
for (const entry of entries) {
|
|
@@ -867,10 +912,11 @@ function Snowfall() {
|
|
|
867
912
|
}
|
|
868
913
|
}
|
|
869
914
|
if (needsUpdate) {
|
|
870
|
-
|
|
915
|
+
scheduleAccumulationInit();
|
|
871
916
|
}
|
|
872
917
|
});
|
|
873
918
|
const initAccumulationWrapper = () => {
|
|
919
|
+
if (isUnmounted) return;
|
|
874
920
|
const scanStart = performance.now();
|
|
875
921
|
initializeAccumulation(accumulationRef.current, physicsConfigRef.current);
|
|
876
922
|
surfaceObserver.disconnect();
|
|
@@ -880,22 +926,25 @@ function Snowfall() {
|
|
|
880
926
|
metricsRef.current.scanTime = performance.now() - scanStart;
|
|
881
927
|
markRectsDirty();
|
|
882
928
|
};
|
|
929
|
+
const scheduleAccumulationInit = () => {
|
|
930
|
+
if (scheduledInitFrame !== 0 || isUnmounted) return;
|
|
931
|
+
scheduledInitFrame = requestAnimationFrame(() => {
|
|
932
|
+
scheduledInitFrame = 0;
|
|
933
|
+
initAccumulationWrapper();
|
|
934
|
+
});
|
|
935
|
+
};
|
|
883
936
|
initAccumulationWrapper();
|
|
884
937
|
requestAnimationFrame(() => {
|
|
885
|
-
if (isMounted) setIsVisible(true);
|
|
938
|
+
if (!isUnmounted && isMounted) setIsVisible(true);
|
|
886
939
|
});
|
|
887
940
|
startAnimation();
|
|
888
941
|
const handleResize = () => {
|
|
889
|
-
resizeCanvas();
|
|
942
|
+
resizeCanvas(physicsConfigRef.current.MAX_RENDER_DPR);
|
|
890
943
|
accumulationRef.current.clear();
|
|
891
944
|
initAccumulationWrapper();
|
|
892
945
|
markRectsDirty();
|
|
893
946
|
};
|
|
894
947
|
window.addEventListener("resize", handleResize);
|
|
895
|
-
const windowResizeObserver = new ResizeObserver(() => {
|
|
896
|
-
resizeCanvas();
|
|
897
|
-
});
|
|
898
|
-
windowResizeObserver.observe(document.body);
|
|
899
948
|
const mutationObserver = new MutationObserver((mutations) => {
|
|
900
949
|
let hasStructuralChange = false;
|
|
901
950
|
for (const mutation of mutations) {
|
|
@@ -905,10 +954,7 @@ function Snowfall() {
|
|
|
905
954
|
}
|
|
906
955
|
}
|
|
907
956
|
if (hasStructuralChange) {
|
|
908
|
-
|
|
909
|
-
initializeAccumulation(accumulationRef.current, physicsConfigRef.current);
|
|
910
|
-
metricsRef.current.scanTime = performance.now() - scanStart;
|
|
911
|
-
markRectsDirty();
|
|
957
|
+
scheduleAccumulationInit();
|
|
912
958
|
}
|
|
913
959
|
});
|
|
914
960
|
mutationObserver.observe(document.body, {
|
|
@@ -916,9 +962,12 @@ function Snowfall() {
|
|
|
916
962
|
subtree: true
|
|
917
963
|
});
|
|
918
964
|
return () => {
|
|
965
|
+
isUnmounted = true;
|
|
966
|
+
if (scheduledInitFrame !== 0) {
|
|
967
|
+
cancelAnimationFrame(scheduledInitFrame);
|
|
968
|
+
}
|
|
919
969
|
stopAnimation();
|
|
920
970
|
window.removeEventListener("resize", handleResize);
|
|
921
|
-
windowResizeObserver.disconnect();
|
|
922
971
|
mutationObserver.disconnect();
|
|
923
972
|
surfaceObserver.disconnect();
|
|
924
973
|
};
|