@hdcodedev/snowfall 1.0.3 → 1.0.5

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.mjs CHANGED
@@ -29,9 +29,14 @@ var SnowfallContext = createContext(void 0);
29
29
  function SnowfallProvider({ children }) {
30
30
  const [isEnabled, setIsEnabled] = useState(true);
31
31
  const [physicsConfig, setPhysicsConfig] = useState(DEFAULT_PHYSICS);
32
+ const [debugMode, setDebugMode] = useState(false);
33
+ const [metrics, setMetrics] = useState(null);
32
34
  const toggleSnow = () => {
33
35
  setIsEnabled((prev) => !prev);
34
36
  };
37
+ const toggleDebug = () => {
38
+ setDebugMode((prev) => !prev);
39
+ };
35
40
  const updatePhysicsConfig = (config) => {
36
41
  setPhysicsConfig((prev) => ({
37
42
  ...prev,
@@ -58,7 +63,11 @@ function SnowfallProvider({ children }) {
58
63
  toggleSnow,
59
64
  physicsConfig,
60
65
  updatePhysicsConfig,
61
- resetPhysics
66
+ resetPhysics,
67
+ debugMode,
68
+ toggleDebug,
69
+ metrics,
70
+ setMetrics
62
71
  }, children });
63
72
  }
64
73
  function useSnowfall() {
@@ -75,13 +84,11 @@ var VAL_IGNORE = "ignore";
75
84
  var VAL_TOP = "top";
76
85
  var VAL_BOTTOM = "bottom";
77
86
  var TAG_HEADER = "header";
78
- var TAG_FOOTER = "footer";
79
87
  var ROLE_BANNER = "banner";
80
- var ROLE_CONTENTINFO = "contentinfo";
81
88
 
82
89
  // src/utils/snowfall/dom.ts
83
- var BOTTOM_TAGS = [TAG_HEADER, TAG_FOOTER];
84
- var BOTTOM_ROLES = [ROLE_BANNER, ROLE_CONTENTINFO];
90
+ var BOTTOM_TAGS = [TAG_HEADER];
91
+ var BOTTOM_ROLES = [ROLE_BANNER];
85
92
  var AUTO_DETECT_TAGS = ["header", "footer", "article", "section", "aside", "nav"];
86
93
  var AUTO_DETECT_ROLES = ['[role="banner"]', '[role="contentinfo"]', '[role="main"]'];
87
94
  var AUTO_DETECT_CLASSES = [
@@ -92,6 +99,18 @@ var AUTO_DETECT_CLASSES = [
92
99
  '[class*="shadow-"]',
93
100
  '[class*="rounded-"]'
94
101
  ];
102
+ var styleCache = /* @__PURE__ */ new Map();
103
+ var getCachedStyle = (el) => {
104
+ let cached = styleCache.get(el);
105
+ if (!cached) {
106
+ cached = window.getComputedStyle(el);
107
+ styleCache.set(el, cached);
108
+ }
109
+ return cached;
110
+ };
111
+ var clearStyleCache = () => {
112
+ styleCache.clear();
113
+ };
95
114
  var getElementType = (el) => {
96
115
  const tagName = el.tagName.toLowerCase();
97
116
  if (BOTTOM_TAGS.includes(tagName)) return VAL_BOTTOM;
@@ -114,6 +133,7 @@ var shouldAccumulate = (el) => {
114
133
  return hasBackground || hasBorder || hasBoxShadow || hasBorderRadius;
115
134
  };
116
135
  var getAccumulationSurfaces = () => {
136
+ clearStyleCache();
117
137
  const surfaces = [];
118
138
  const seen = /* @__PURE__ */ new Set();
119
139
  const candidates = document.querySelectorAll(
@@ -126,13 +146,13 @@ var getAccumulationSurfaces = () => {
126
146
  );
127
147
  candidates.forEach((el) => {
128
148
  if (seen.has(el)) return;
129
- const rect = el.getBoundingClientRect();
130
149
  const manualOverride = el.getAttribute(ATTR_SNOWFALL);
131
150
  if (manualOverride === VAL_IGNORE) return;
132
151
  const isManuallyIncluded = manualOverride !== null;
133
- const styles = window.getComputedStyle(el);
152
+ const styles = getCachedStyle(el);
134
153
  const isVisible = styles.display !== "none" && styles.visibility !== "hidden" && parseFloat(styles.opacity) > 0.1;
135
154
  if (!isVisible && !isManuallyIncluded) return;
155
+ const rect = el.getBoundingClientRect();
136
156
  const hasSize = rect.width >= 100 && rect.height >= 50;
137
157
  if (!hasSize && !isManuallyIncluded) return;
138
158
  const isFullPageWrapper = rect.top <= 10 && rect.height >= window.innerHeight * 0.9;
@@ -143,7 +163,7 @@ var getAccumulationSurfaces = () => {
143
163
  let isFixed = false;
144
164
  let currentEl = el;
145
165
  while (currentEl && currentEl !== document.body) {
146
- const style = window.getComputedStyle(currentEl);
166
+ const style = getCachedStyle(currentEl);
147
167
  if (style.position === "fixed" || style.position === "sticky") {
148
168
  isFixed = true;
149
169
  break;
@@ -162,6 +182,7 @@ var getAccumulationSurfaces = () => {
162
182
  }
163
183
  });
164
184
  console.log(`[Snowfall] Auto-detection found ${surfaces.length} surfaces`);
185
+ console.log("[Snowfall] \u2705 Using OPTIMIZED version with Map-based caching & 5s intervals");
165
186
  return surfaces;
166
187
  };
167
188
  var getElementRects = (accumulationMap) => {
@@ -503,10 +524,14 @@ var drawSideAccumulations = (ctx, fixedCtx, elementRects) => {
503
524
  const width = sideArray[y] || 0;
504
525
  const nextY = Math.min(y + 2, sideArray.length - 1);
505
526
  const nextWidth = sideArray[nextY] || 0;
527
+ const heightRatio = y / sideArray.length;
528
+ const gravityMultiplier = Math.pow(heightRatio, 1.5);
506
529
  const py = rect.top + y + dy;
507
- const px = (isLeft ? baseX - width : baseX + width) + dx;
530
+ const px = (isLeft ? baseX - width * gravityMultiplier : baseX + width * gravityMultiplier) + dx;
508
531
  const ny = rect.top + nextY + dy;
509
- const nx = (isLeft ? baseX - nextWidth : baseX + nextWidth) + dx;
532
+ const nRatio = nextY / sideArray.length;
533
+ const nGravityMultiplier = Math.pow(nRatio, 1.5);
534
+ const nx = (isLeft ? baseX - nextWidth * nGravityMultiplier : baseX + nextWidth * nGravityMultiplier) + dx;
510
535
  targetCtx.lineTo(px, py);
511
536
  targetCtx.lineTo(nx, ny);
512
537
  }
@@ -522,18 +547,28 @@ var drawSideAccumulations = (ctx, fixedCtx, elementRects) => {
522
547
  };
523
548
 
524
549
  // src/Snowfall.tsx
525
- import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
550
+ import { Fragment, jsx as jsx2 } from "react/jsx-runtime";
526
551
  function Snowfall() {
527
- const { isEnabled, physicsConfig } = useSnowfall();
552
+ const { isEnabled, physicsConfig, setMetrics } = useSnowfall();
528
553
  const isEnabledRef = useRef(isEnabled);
529
554
  const physicsConfigRef = useRef(physicsConfig);
555
+ const setMetricsRef = useRef(setMetrics);
530
556
  const [isMounted, setIsMounted] = useState2(false);
531
557
  const [isVisible, setIsVisible] = useState2(false);
532
558
  const canvasRef = useRef(null);
533
- const fixedCanvasRef = useRef(null);
534
559
  const snowflakesRef = useRef([]);
535
560
  const accumulationRef = useRef(/* @__PURE__ */ new Map());
536
561
  const animationIdRef = useRef(0);
562
+ const fpsFrames = useRef([]);
563
+ const metricsRef = useRef({
564
+ scanTime: 0,
565
+ rectUpdateTime: 0,
566
+ frameTime: 0,
567
+ rafGap: 0,
568
+ clearTime: 0,
569
+ physicsTime: 0,
570
+ drawTime: 0
571
+ });
537
572
  useEffect(() => {
538
573
  setIsMounted(true);
539
574
  }, []);
@@ -543,26 +578,23 @@ function Snowfall() {
543
578
  useEffect(() => {
544
579
  physicsConfigRef.current = physicsConfig;
545
580
  }, [physicsConfig]);
581
+ useEffect(() => {
582
+ setMetricsRef.current = setMetrics;
583
+ }, [setMetrics]);
546
584
  useEffect(() => {
547
585
  if (!isMounted) return;
548
586
  const canvas = canvasRef.current;
549
- const fixedCanvas = fixedCanvasRef.current;
550
- if (!canvas || !fixedCanvas) return;
587
+ if (!canvas) return;
551
588
  const ctx = canvas.getContext("2d");
552
- const fixedCtx = fixedCanvas.getContext("2d");
553
- if (!ctx || !fixedCtx) return;
589
+ if (!ctx) return;
554
590
  const resizeCanvas = () => {
555
- if (canvasRef.current && fixedCanvasRef.current) {
591
+ if (canvasRef.current) {
556
592
  const newHeight = Math.max(document.documentElement.scrollHeight, window.innerHeight);
557
593
  const newWidth = Math.max(document.documentElement.scrollWidth, window.innerWidth);
558
594
  if (canvasRef.current.height !== newHeight || canvasRef.current.width !== newWidth) {
559
595
  canvasRef.current.width = newWidth;
560
596
  canvasRef.current.height = newHeight;
561
597
  }
562
- if (fixedCanvasRef.current.width !== window.innerWidth || fixedCanvasRef.current.height !== window.innerHeight) {
563
- fixedCanvasRef.current.width = window.innerWidth;
564
- fixedCanvasRef.current.height = window.innerHeight;
565
- }
566
598
  }
567
599
  };
568
600
  resizeCanvas();
@@ -572,11 +604,16 @@ function Snowfall() {
572
604
  resizeObserver.observe(document.body);
573
605
  snowflakesRef.current = [];
574
606
  const initAccumulationWrapper = () => {
607
+ const scanStart = performance.now();
575
608
  initializeAccumulation(accumulationRef.current, physicsConfigRef.current);
609
+ metricsRef.current.scanTime = performance.now() - scanStart;
576
610
  };
577
611
  initAccumulationWrapper();
578
612
  setIsVisible(true);
579
613
  let lastTime = 0;
614
+ let lastRectUpdate = 0;
615
+ let lastMetricsUpdate = 0;
616
+ let cachedElementRects = [];
580
617
  const animate = (currentTime) => {
581
618
  if (lastTime === 0) {
582
619
  lastTime = currentTime;
@@ -584,14 +621,28 @@ function Snowfall() {
584
621
  return;
585
622
  }
586
623
  const deltaTime = Math.min(currentTime - lastTime, 50);
624
+ const now = performance.now();
625
+ fpsFrames.current.push(now);
626
+ fpsFrames.current = fpsFrames.current.filter((t) => now - t < 1e3);
627
+ metricsRef.current.rafGap = currentTime - lastTime;
587
628
  lastTime = currentTime;
588
629
  const dt = deltaTime / 16.67;
630
+ const frameStartTime = performance.now();
631
+ const clearStart = performance.now();
589
632
  ctx.clearRect(0, 0, canvas.width, canvas.height);
590
- fixedCtx.clearRect(0, 0, fixedCanvas.width, fixedCanvas.height);
633
+ metricsRef.current.clearTime = performance.now() - clearStart;
591
634
  const snowflakes = snowflakesRef.current;
592
- const elementRects = getElementRects(accumulationRef.current);
593
- meltAndSmoothAccumulation(elementRects, physicsConfigRef.current, dt);
594
- updateSnowflakes(snowflakes, elementRects, physicsConfigRef.current, dt, canvas.width, canvas.height);
635
+ if (currentTime - lastRectUpdate > 100) {
636
+ const rectStart = performance.now();
637
+ cachedElementRects = getElementRects(accumulationRef.current);
638
+ metricsRef.current.rectUpdateTime = performance.now() - rectStart;
639
+ lastRectUpdate = currentTime;
640
+ }
641
+ const physicsStart = performance.now();
642
+ meltAndSmoothAccumulation(cachedElementRects, physicsConfigRef.current, dt);
643
+ updateSnowflakes(snowflakes, cachedElementRects, physicsConfigRef.current, dt, canvas.width, canvas.height);
644
+ metricsRef.current.physicsTime = performance.now() - physicsStart;
645
+ const drawStart = performance.now();
595
646
  for (const flake of snowflakes) {
596
647
  drawSnowflake(ctx, flake);
597
648
  }
@@ -599,8 +650,26 @@ function Snowfall() {
599
650
  const isBackground = Math.random() < 0.4;
600
651
  snowflakes.push(createSnowflake(canvas.width, physicsConfigRef.current, isBackground));
601
652
  }
602
- drawAccumulations(ctx, fixedCtx, elementRects);
603
- drawSideAccumulations(ctx, fixedCtx, elementRects);
653
+ drawAccumulations(ctx, ctx, cachedElementRects);
654
+ drawSideAccumulations(ctx, ctx, cachedElementRects);
655
+ metricsRef.current.drawTime = performance.now() - drawStart;
656
+ metricsRef.current.frameTime = performance.now() - frameStartTime;
657
+ if (currentTime - lastMetricsUpdate > 500) {
658
+ setMetricsRef.current({
659
+ fps: fpsFrames.current.length,
660
+ frameTime: metricsRef.current.frameTime,
661
+ scanTime: metricsRef.current.scanTime,
662
+ rectUpdateTime: metricsRef.current.rectUpdateTime,
663
+ surfaceCount: accumulationRef.current.size,
664
+ flakeCount: snowflakes.length,
665
+ maxFlakes: physicsConfigRef.current.MAX_FLAKES,
666
+ rafGap: metricsRef.current.rafGap,
667
+ clearTime: metricsRef.current.clearTime,
668
+ physicsTime: metricsRef.current.physicsTime,
669
+ drawTime: metricsRef.current.drawTime
670
+ });
671
+ lastMetricsUpdate = currentTime;
672
+ }
604
673
  animationIdRef.current = requestAnimationFrame(animate);
605
674
  };
606
675
  animationIdRef.current = requestAnimationFrame(animate);
@@ -610,7 +679,7 @@ function Snowfall() {
610
679
  initAccumulationWrapper();
611
680
  };
612
681
  window.addEventListener("resize", handleResize);
613
- const checkInterval = setInterval(initAccumulationWrapper, 3e3);
682
+ const checkInterval = setInterval(initAccumulationWrapper, 5e3);
614
683
  return () => {
615
684
  cancelAnimationFrame(animationIdRef.current);
616
685
  window.removeEventListener("resize", handleResize);
@@ -619,43 +688,201 @@ function Snowfall() {
619
688
  };
620
689
  }, [isMounted]);
621
690
  if (!isMounted) return null;
622
- return /* @__PURE__ */ jsxs(Fragment, { children: [
623
- /* @__PURE__ */ jsx2(
624
- "canvas",
625
- {
626
- ref: canvasRef,
627
- style: {
628
- position: "absolute",
629
- top: 0,
630
- left: 0,
631
- pointerEvents: "none",
632
- zIndex: 9999,
633
- opacity: isVisible ? 1 : 0,
634
- transition: "opacity 0.3s ease-in"
635
- },
636
- "aria-hidden": "true"
637
- }
638
- ),
639
- /* @__PURE__ */ jsx2(
640
- "canvas",
641
- {
642
- ref: fixedCanvasRef,
643
- style: {
644
- position: "fixed",
645
- top: 0,
646
- left: 0,
647
- pointerEvents: "none",
648
- zIndex: 9999,
649
- opacity: isVisible ? 1 : 0,
650
- transition: "opacity 0.3s ease-in"
651
- },
652
- "aria-hidden": "true"
691
+ return /* @__PURE__ */ jsx2(Fragment, { children: /* @__PURE__ */ jsx2(
692
+ "canvas",
693
+ {
694
+ ref: canvasRef,
695
+ style: {
696
+ position: "absolute",
697
+ top: 0,
698
+ left: 0,
699
+ pointerEvents: "none",
700
+ zIndex: 9999,
701
+ opacity: isVisible ? 1 : 0,
702
+ transition: "opacity 0.3s ease-in",
703
+ willChange: "transform"
704
+ },
705
+ "aria-hidden": "true"
706
+ }
707
+ ) });
708
+ }
709
+
710
+ // src/DebugPanel.tsx
711
+ import { useEffect as useEffect2, useState as useState3 } from "react";
712
+ import { Fragment as Fragment2, jsx as jsx3, jsxs } from "react/jsx-runtime";
713
+ function DebugPanel() {
714
+ const { debugMode, toggleDebug, metrics } = useSnowfall();
715
+ const [isMinimized, setIsMinimized] = useState3(false);
716
+ const [copied, setCopied] = useState3(false);
717
+ useEffect2(() => {
718
+ const handleKeyDown = (e) => {
719
+ if (e.shiftKey && e.key === "D") {
720
+ toggleDebug();
653
721
  }
654
- )
722
+ };
723
+ window.addEventListener("keydown", handleKeyDown);
724
+ return () => window.removeEventListener("keydown", handleKeyDown);
725
+ }, [toggleDebug]);
726
+ const copyToClipboard = () => {
727
+ if (metrics) {
728
+ const data = {
729
+ ...metrics,
730
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
731
+ userAgent: navigator.userAgent,
732
+ canvasSize: {
733
+ width: window.innerWidth,
734
+ height: window.innerHeight
735
+ }
736
+ };
737
+ navigator.clipboard.writeText(JSON.stringify(data, null, 2));
738
+ setCopied(true);
739
+ setTimeout(() => setCopied(false), 2e3);
740
+ }
741
+ };
742
+ if (!debugMode) return null;
743
+ return /* @__PURE__ */ jsxs("div", { style: {
744
+ position: "fixed",
745
+ bottom: "20px",
746
+ left: "20px",
747
+ backgroundColor: "rgba(0, 0, 0, 0.9)",
748
+ color: "#0f0",
749
+ fontFamily: "monospace",
750
+ fontSize: "12px",
751
+ padding: isMinimized ? "10px" : "15px",
752
+ borderRadius: "8px",
753
+ zIndex: 1e4,
754
+ minWidth: isMinimized ? "auto" : "320px",
755
+ maxWidth: "400px",
756
+ border: "1px solid #0f0",
757
+ boxShadow: "0 4px 20px rgba(0, 255, 0, 0.3)"
758
+ }, children: [
759
+ /* @__PURE__ */ jsxs("div", { style: {
760
+ display: "flex",
761
+ justifyContent: "space-between",
762
+ alignItems: "center",
763
+ marginBottom: isMinimized ? 0 : "10px"
764
+ }, children: [
765
+ /* @__PURE__ */ jsx3("div", { style: { fontWeight: "bold", color: "#0ff" }, children: "\u2744\uFE0F SNOWFALL DEBUG" }),
766
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "10px" }, children: [
767
+ /* @__PURE__ */ jsx3(
768
+ "button",
769
+ {
770
+ onClick: () => setIsMinimized(!isMinimized),
771
+ style: {
772
+ background: "none",
773
+ border: "1px solid #0f0",
774
+ color: "#0f0",
775
+ cursor: "pointer",
776
+ padding: "2px 8px",
777
+ borderRadius: "4px",
778
+ fontSize: "10px"
779
+ },
780
+ children: isMinimized ? "\u25B2" : "\u25BC"
781
+ }
782
+ ),
783
+ /* @__PURE__ */ jsx3(
784
+ "button",
785
+ {
786
+ onClick: toggleDebug,
787
+ style: {
788
+ background: "none",
789
+ border: "1px solid #f00",
790
+ color: "#f00",
791
+ cursor: "pointer",
792
+ padding: "2px 8px",
793
+ borderRadius: "4px",
794
+ fontSize: "10px"
795
+ },
796
+ children: "\u2715"
797
+ }
798
+ )
799
+ ] })
800
+ ] }),
801
+ !isMinimized && metrics && /* @__PURE__ */ jsxs(Fragment2, { children: [
802
+ /* @__PURE__ */ jsxs("div", { style: { marginBottom: "10px", paddingBottom: "10px", borderBottom: "1px solid #333" }, children: [
803
+ /* @__PURE__ */ jsx3("div", { style: { color: "#ff0", marginBottom: "5px", fontWeight: "bold" }, children: "\u26A1 PERFORMANCE" }),
804
+ /* @__PURE__ */ jsxs("div", { children: [
805
+ "FPS: ",
806
+ /* @__PURE__ */ jsx3("span", { style: { color: metrics.fps < 30 ? "#f00" : metrics.fps < 50 ? "#ff0" : "#0f0" }, children: metrics.fps.toFixed(1) })
807
+ ] }),
808
+ /* @__PURE__ */ jsxs("div", { children: [
809
+ "Frame Time: ",
810
+ metrics.frameTime.toFixed(2),
811
+ "ms"
812
+ ] }),
813
+ /* @__PURE__ */ jsxs("div", { style: { color: "#f80" }, children: [
814
+ "rAF Gap: ",
815
+ metrics.rafGap?.toFixed(1) || 0,
816
+ "ms ",
817
+ metrics.rafGap && metrics.rafGap > 20 ? "\u26A0\uFE0F THROTTLED!" : ""
818
+ ] })
819
+ ] }),
820
+ /* @__PURE__ */ jsxs("div", { style: { marginBottom: "10px", paddingBottom: "10px", borderBottom: "1px solid #333" }, children: [
821
+ /* @__PURE__ */ jsx3("div", { style: { color: "#ff0", marginBottom: "5px", fontWeight: "bold" }, children: "\u{1F52C} DETAILED TIMINGS" }),
822
+ /* @__PURE__ */ jsxs("div", { children: [
823
+ "Clear: ",
824
+ metrics.clearTime?.toFixed(2) || 0,
825
+ "ms"
826
+ ] }),
827
+ /* @__PURE__ */ jsxs("div", { children: [
828
+ "Physics: ",
829
+ metrics.physicsTime?.toFixed(2) || 0,
830
+ "ms"
831
+ ] }),
832
+ /* @__PURE__ */ jsxs("div", { children: [
833
+ "Draw: ",
834
+ metrics.drawTime?.toFixed(2) || 0,
835
+ "ms"
836
+ ] }),
837
+ /* @__PURE__ */ jsxs("div", { children: [
838
+ "Scan: ",
839
+ metrics.scanTime.toFixed(2),
840
+ "ms"
841
+ ] }),
842
+ /* @__PURE__ */ jsxs("div", { children: [
843
+ "Rect Update: ",
844
+ metrics.rectUpdateTime.toFixed(2),
845
+ "ms"
846
+ ] })
847
+ ] }),
848
+ /* @__PURE__ */ jsxs("div", { style: { marginBottom: "10px", paddingBottom: "10px", borderBottom: "1px solid #333" }, children: [
849
+ /* @__PURE__ */ jsx3("div", { style: { color: "#ff0", marginBottom: "5px", fontWeight: "bold" }, children: "\u{1F4CA} COUNTS" }),
850
+ /* @__PURE__ */ jsxs("div", { children: [
851
+ "Snowflakes: ",
852
+ metrics.flakeCount,
853
+ " / ",
854
+ metrics.maxFlakes
855
+ ] }),
856
+ /* @__PURE__ */ jsxs("div", { children: [
857
+ "Surfaces: ",
858
+ metrics.surfaceCount
859
+ ] })
860
+ ] }),
861
+ /* @__PURE__ */ jsx3(
862
+ "button",
863
+ {
864
+ onClick: copyToClipboard,
865
+ style: {
866
+ width: "100%",
867
+ padding: "8px",
868
+ background: copied ? "#0a0" : "#000",
869
+ border: copied ? "1px solid #0f0" : "1px solid #555",
870
+ color: "#0f0",
871
+ cursor: "pointer",
872
+ borderRadius: "4px",
873
+ fontSize: "11px",
874
+ fontFamily: "monospace"
875
+ },
876
+ children: copied ? "\u2713 COPIED!" : "\u{1F4CB} COPY METRICS"
877
+ }
878
+ ),
879
+ /* @__PURE__ */ jsx3("div", { style: { marginTop: "8px", fontSize: "10px", color: "#666", textAlign: "center" }, children: "Shift+D to toggle" })
880
+ ] })
655
881
  ] });
656
882
  }
657
883
  export {
658
884
  DEFAULT_PHYSICS,
885
+ DebugPanel,
659
886
  Snowfall,
660
887
  SnowfallProvider,
661
888
  useSnowfall