@hdcodedev/snowfall 1.0.4 → 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() {
@@ -90,6 +99,18 @@ var AUTO_DETECT_CLASSES = [
90
99
  '[class*="shadow-"]',
91
100
  '[class*="rounded-"]'
92
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
+ };
93
114
  var getElementType = (el) => {
94
115
  const tagName = el.tagName.toLowerCase();
95
116
  if (BOTTOM_TAGS.includes(tagName)) return VAL_BOTTOM;
@@ -112,6 +133,7 @@ var shouldAccumulate = (el) => {
112
133
  return hasBackground || hasBorder || hasBoxShadow || hasBorderRadius;
113
134
  };
114
135
  var getAccumulationSurfaces = () => {
136
+ clearStyleCache();
115
137
  const surfaces = [];
116
138
  const seen = /* @__PURE__ */ new Set();
117
139
  const candidates = document.querySelectorAll(
@@ -124,13 +146,13 @@ var getAccumulationSurfaces = () => {
124
146
  );
125
147
  candidates.forEach((el) => {
126
148
  if (seen.has(el)) return;
127
- const rect = el.getBoundingClientRect();
128
149
  const manualOverride = el.getAttribute(ATTR_SNOWFALL);
129
150
  if (manualOverride === VAL_IGNORE) return;
130
151
  const isManuallyIncluded = manualOverride !== null;
131
- const styles = window.getComputedStyle(el);
152
+ const styles = getCachedStyle(el);
132
153
  const isVisible = styles.display !== "none" && styles.visibility !== "hidden" && parseFloat(styles.opacity) > 0.1;
133
154
  if (!isVisible && !isManuallyIncluded) return;
155
+ const rect = el.getBoundingClientRect();
134
156
  const hasSize = rect.width >= 100 && rect.height >= 50;
135
157
  if (!hasSize && !isManuallyIncluded) return;
136
158
  const isFullPageWrapper = rect.top <= 10 && rect.height >= window.innerHeight * 0.9;
@@ -141,7 +163,7 @@ var getAccumulationSurfaces = () => {
141
163
  let isFixed = false;
142
164
  let currentEl = el;
143
165
  while (currentEl && currentEl !== document.body) {
144
- const style = window.getComputedStyle(currentEl);
166
+ const style = getCachedStyle(currentEl);
145
167
  if (style.position === "fixed" || style.position === "sticky") {
146
168
  isFixed = true;
147
169
  break;
@@ -160,6 +182,7 @@ var getAccumulationSurfaces = () => {
160
182
  }
161
183
  });
162
184
  console.log(`[Snowfall] Auto-detection found ${surfaces.length} surfaces`);
185
+ console.log("[Snowfall] \u2705 Using OPTIMIZED version with Map-based caching & 5s intervals");
163
186
  return surfaces;
164
187
  };
165
188
  var getElementRects = (accumulationMap) => {
@@ -524,18 +547,28 @@ var drawSideAccumulations = (ctx, fixedCtx, elementRects) => {
524
547
  };
525
548
 
526
549
  // src/Snowfall.tsx
527
- import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
550
+ import { Fragment, jsx as jsx2 } from "react/jsx-runtime";
528
551
  function Snowfall() {
529
- const { isEnabled, physicsConfig } = useSnowfall();
552
+ const { isEnabled, physicsConfig, setMetrics } = useSnowfall();
530
553
  const isEnabledRef = useRef(isEnabled);
531
554
  const physicsConfigRef = useRef(physicsConfig);
555
+ const setMetricsRef = useRef(setMetrics);
532
556
  const [isMounted, setIsMounted] = useState2(false);
533
557
  const [isVisible, setIsVisible] = useState2(false);
534
558
  const canvasRef = useRef(null);
535
- const fixedCanvasRef = useRef(null);
536
559
  const snowflakesRef = useRef([]);
537
560
  const accumulationRef = useRef(/* @__PURE__ */ new Map());
538
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
+ });
539
572
  useEffect(() => {
540
573
  setIsMounted(true);
541
574
  }, []);
@@ -545,26 +578,23 @@ function Snowfall() {
545
578
  useEffect(() => {
546
579
  physicsConfigRef.current = physicsConfig;
547
580
  }, [physicsConfig]);
581
+ useEffect(() => {
582
+ setMetricsRef.current = setMetrics;
583
+ }, [setMetrics]);
548
584
  useEffect(() => {
549
585
  if (!isMounted) return;
550
586
  const canvas = canvasRef.current;
551
- const fixedCanvas = fixedCanvasRef.current;
552
- if (!canvas || !fixedCanvas) return;
587
+ if (!canvas) return;
553
588
  const ctx = canvas.getContext("2d");
554
- const fixedCtx = fixedCanvas.getContext("2d");
555
- if (!ctx || !fixedCtx) return;
589
+ if (!ctx) return;
556
590
  const resizeCanvas = () => {
557
- if (canvasRef.current && fixedCanvasRef.current) {
591
+ if (canvasRef.current) {
558
592
  const newHeight = Math.max(document.documentElement.scrollHeight, window.innerHeight);
559
593
  const newWidth = Math.max(document.documentElement.scrollWidth, window.innerWidth);
560
594
  if (canvasRef.current.height !== newHeight || canvasRef.current.width !== newWidth) {
561
595
  canvasRef.current.width = newWidth;
562
596
  canvasRef.current.height = newHeight;
563
597
  }
564
- if (fixedCanvasRef.current.width !== window.innerWidth || fixedCanvasRef.current.height !== window.innerHeight) {
565
- fixedCanvasRef.current.width = window.innerWidth;
566
- fixedCanvasRef.current.height = window.innerHeight;
567
- }
568
598
  }
569
599
  };
570
600
  resizeCanvas();
@@ -574,11 +604,16 @@ function Snowfall() {
574
604
  resizeObserver.observe(document.body);
575
605
  snowflakesRef.current = [];
576
606
  const initAccumulationWrapper = () => {
607
+ const scanStart = performance.now();
577
608
  initializeAccumulation(accumulationRef.current, physicsConfigRef.current);
609
+ metricsRef.current.scanTime = performance.now() - scanStart;
578
610
  };
579
611
  initAccumulationWrapper();
580
612
  setIsVisible(true);
581
613
  let lastTime = 0;
614
+ let lastRectUpdate = 0;
615
+ let lastMetricsUpdate = 0;
616
+ let cachedElementRects = [];
582
617
  const animate = (currentTime) => {
583
618
  if (lastTime === 0) {
584
619
  lastTime = currentTime;
@@ -586,14 +621,28 @@ function Snowfall() {
586
621
  return;
587
622
  }
588
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;
589
628
  lastTime = currentTime;
590
629
  const dt = deltaTime / 16.67;
630
+ const frameStartTime = performance.now();
631
+ const clearStart = performance.now();
591
632
  ctx.clearRect(0, 0, canvas.width, canvas.height);
592
- fixedCtx.clearRect(0, 0, fixedCanvas.width, fixedCanvas.height);
633
+ metricsRef.current.clearTime = performance.now() - clearStart;
593
634
  const snowflakes = snowflakesRef.current;
594
- const elementRects = getElementRects(accumulationRef.current);
595
- meltAndSmoothAccumulation(elementRects, physicsConfigRef.current, dt);
596
- 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();
597
646
  for (const flake of snowflakes) {
598
647
  drawSnowflake(ctx, flake);
599
648
  }
@@ -601,8 +650,26 @@ function Snowfall() {
601
650
  const isBackground = Math.random() < 0.4;
602
651
  snowflakes.push(createSnowflake(canvas.width, physicsConfigRef.current, isBackground));
603
652
  }
604
- drawAccumulations(ctx, fixedCtx, elementRects);
605
- 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
+ }
606
673
  animationIdRef.current = requestAnimationFrame(animate);
607
674
  };
608
675
  animationIdRef.current = requestAnimationFrame(animate);
@@ -612,7 +679,7 @@ function Snowfall() {
612
679
  initAccumulationWrapper();
613
680
  };
614
681
  window.addEventListener("resize", handleResize);
615
- const checkInterval = setInterval(initAccumulationWrapper, 3e3);
682
+ const checkInterval = setInterval(initAccumulationWrapper, 5e3);
616
683
  return () => {
617
684
  cancelAnimationFrame(animationIdRef.current);
618
685
  window.removeEventListener("resize", handleResize);
@@ -621,43 +688,201 @@ function Snowfall() {
621
688
  };
622
689
  }, [isMounted]);
623
690
  if (!isMounted) return null;
624
- return /* @__PURE__ */ jsxs(Fragment, { children: [
625
- /* @__PURE__ */ jsx2(
626
- "canvas",
627
- {
628
- ref: canvasRef,
629
- style: {
630
- position: "absolute",
631
- top: 0,
632
- left: 0,
633
- pointerEvents: "none",
634
- zIndex: 9999,
635
- opacity: isVisible ? 1 : 0,
636
- transition: "opacity 0.3s ease-in"
637
- },
638
- "aria-hidden": "true"
639
- }
640
- ),
641
- /* @__PURE__ */ jsx2(
642
- "canvas",
643
- {
644
- ref: fixedCanvasRef,
645
- style: {
646
- position: "fixed",
647
- top: 0,
648
- left: 0,
649
- pointerEvents: "none",
650
- zIndex: 9999,
651
- opacity: isVisible ? 1 : 0,
652
- transition: "opacity 0.3s ease-in"
653
- },
654
- "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();
655
721
  }
656
- )
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
+ ] })
657
881
  ] });
658
882
  }
659
883
  export {
660
884
  DEFAULT_PHYSICS,
885
+ DebugPanel,
661
886
  Snowfall,
662
887
  SnowfallProvider,
663
888
  useSnowfall