@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/README.md CHANGED
@@ -1,8 +1,13 @@
1
- # @hdcodedev/snowfall
1
+ # [@hdcodedev/snowfall](https://next-snowfall.vercel.app)
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@hdcodedev/snowfall.svg)](https://www.npmjs.com/package/@hdcodedev/snowfall)
4
+
2
5
 
3
6
  A realistic snowfall effect for React with physics-based accumulation on surfaces. Features melting, wind, and smart surface detection.
4
7
 
5
- ![ScreenRecording2025-12-22at21 55 51-ezgif com-optimize-2](https://github.com/user-attachments/assets/e8e6b910-d996-4386-b76e-43d087440708)
8
+ ![ScreenRecording2025-12-23at00 07 30-ezgif com-optimize](https://github.com/user-attachments/assets/49c4a537-7f04-4043-806e-21478f419dd7)
9
+
10
+
6
11
 
7
12
  ## Features
8
13
 
@@ -135,10 +140,6 @@ Hook to access snowfall controls. Must be used within `SnowfallProvider`.
135
140
  }
136
141
  ```
137
142
 
138
- ## Demo
139
-
140
- Check out the [live demo](https://snowfall-2026.vercel.app)
141
-
142
143
  ## Tips
143
144
 
144
145
  - The snowfall canvas has `pointer-events: none`, so it won't interfere with user interactions
package/dist/index.d.mts CHANGED
@@ -23,16 +23,35 @@ interface PhysicsConfig {
23
23
  };
24
24
  }
25
25
  declare const DEFAULT_PHYSICS: PhysicsConfig;
26
+ interface PerformanceMetrics {
27
+ fps: number;
28
+ frameTime: number;
29
+ scanTime: number;
30
+ rectUpdateTime: number;
31
+ surfaceCount: number;
32
+ flakeCount: number;
33
+ maxFlakes: number;
34
+ rafGap: number;
35
+ clearTime: number;
36
+ physicsTime: number;
37
+ drawTime: number;
38
+ }
26
39
  interface SnowfallContextType {
27
40
  isEnabled: boolean;
28
41
  toggleSnow: () => void;
29
42
  physicsConfig: PhysicsConfig;
30
43
  updatePhysicsConfig: (config: Partial<PhysicsConfig>) => void;
31
44
  resetPhysics: () => void;
45
+ debugMode: boolean;
46
+ toggleDebug: () => void;
47
+ metrics: PerformanceMetrics | null;
48
+ setMetrics: (metrics: PerformanceMetrics) => void;
32
49
  }
33
50
  declare function SnowfallProvider({ children }: {
34
51
  children: ReactNode;
35
52
  }): react_jsx_runtime.JSX.Element;
36
53
  declare function useSnowfall(): SnowfallContextType;
37
54
 
38
- export { DEFAULT_PHYSICS, type PhysicsConfig, Snowfall, SnowfallProvider, useSnowfall };
55
+ declare function DebugPanel(): react_jsx_runtime.JSX.Element | null;
56
+
57
+ export { DEFAULT_PHYSICS, DebugPanel, type PerformanceMetrics, type PhysicsConfig, Snowfall, SnowfallProvider, useSnowfall };
package/dist/index.d.ts CHANGED
@@ -23,16 +23,35 @@ interface PhysicsConfig {
23
23
  };
24
24
  }
25
25
  declare const DEFAULT_PHYSICS: PhysicsConfig;
26
+ interface PerformanceMetrics {
27
+ fps: number;
28
+ frameTime: number;
29
+ scanTime: number;
30
+ rectUpdateTime: number;
31
+ surfaceCount: number;
32
+ flakeCount: number;
33
+ maxFlakes: number;
34
+ rafGap: number;
35
+ clearTime: number;
36
+ physicsTime: number;
37
+ drawTime: number;
38
+ }
26
39
  interface SnowfallContextType {
27
40
  isEnabled: boolean;
28
41
  toggleSnow: () => void;
29
42
  physicsConfig: PhysicsConfig;
30
43
  updatePhysicsConfig: (config: Partial<PhysicsConfig>) => void;
31
44
  resetPhysics: () => void;
45
+ debugMode: boolean;
46
+ toggleDebug: () => void;
47
+ metrics: PerformanceMetrics | null;
48
+ setMetrics: (metrics: PerformanceMetrics) => void;
32
49
  }
33
50
  declare function SnowfallProvider({ children }: {
34
51
  children: ReactNode;
35
52
  }): react_jsx_runtime.JSX.Element;
36
53
  declare function useSnowfall(): SnowfallContextType;
37
54
 
38
- export { DEFAULT_PHYSICS, type PhysicsConfig, Snowfall, SnowfallProvider, useSnowfall };
55
+ declare function DebugPanel(): react_jsx_runtime.JSX.Element | null;
56
+
57
+ export { DEFAULT_PHYSICS, DebugPanel, type PerformanceMetrics, type PhysicsConfig, Snowfall, SnowfallProvider, useSnowfall };
package/dist/index.js CHANGED
@@ -22,6 +22,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
22
22
  var index_exports = {};
23
23
  __export(index_exports, {
24
24
  DEFAULT_PHYSICS: () => DEFAULT_PHYSICS,
25
+ DebugPanel: () => DebugPanel,
25
26
  Snowfall: () => Snowfall,
26
27
  SnowfallProvider: () => SnowfallProvider,
27
28
  useSnowfall: () => useSnowfall
@@ -57,9 +58,14 @@ var SnowfallContext = (0, import_react.createContext)(void 0);
57
58
  function SnowfallProvider({ children }) {
58
59
  const [isEnabled, setIsEnabled] = (0, import_react.useState)(true);
59
60
  const [physicsConfig, setPhysicsConfig] = (0, import_react.useState)(DEFAULT_PHYSICS);
61
+ const [debugMode, setDebugMode] = (0, import_react.useState)(false);
62
+ const [metrics, setMetrics] = (0, import_react.useState)(null);
60
63
  const toggleSnow = () => {
61
64
  setIsEnabled((prev) => !prev);
62
65
  };
66
+ const toggleDebug = () => {
67
+ setDebugMode((prev) => !prev);
68
+ };
63
69
  const updatePhysicsConfig = (config) => {
64
70
  setPhysicsConfig((prev) => ({
65
71
  ...prev,
@@ -86,7 +92,11 @@ function SnowfallProvider({ children }) {
86
92
  toggleSnow,
87
93
  physicsConfig,
88
94
  updatePhysicsConfig,
89
- resetPhysics
95
+ resetPhysics,
96
+ debugMode,
97
+ toggleDebug,
98
+ metrics,
99
+ setMetrics
90
100
  }, children });
91
101
  }
92
102
  function useSnowfall() {
@@ -118,6 +128,18 @@ var AUTO_DETECT_CLASSES = [
118
128
  '[class*="shadow-"]',
119
129
  '[class*="rounded-"]'
120
130
  ];
131
+ var styleCache = /* @__PURE__ */ new Map();
132
+ var getCachedStyle = (el) => {
133
+ let cached = styleCache.get(el);
134
+ if (!cached) {
135
+ cached = window.getComputedStyle(el);
136
+ styleCache.set(el, cached);
137
+ }
138
+ return cached;
139
+ };
140
+ var clearStyleCache = () => {
141
+ styleCache.clear();
142
+ };
121
143
  var getElementType = (el) => {
122
144
  const tagName = el.tagName.toLowerCase();
123
145
  if (BOTTOM_TAGS.includes(tagName)) return VAL_BOTTOM;
@@ -140,6 +162,7 @@ var shouldAccumulate = (el) => {
140
162
  return hasBackground || hasBorder || hasBoxShadow || hasBorderRadius;
141
163
  };
142
164
  var getAccumulationSurfaces = () => {
165
+ clearStyleCache();
143
166
  const surfaces = [];
144
167
  const seen = /* @__PURE__ */ new Set();
145
168
  const candidates = document.querySelectorAll(
@@ -152,13 +175,13 @@ var getAccumulationSurfaces = () => {
152
175
  );
153
176
  candidates.forEach((el) => {
154
177
  if (seen.has(el)) return;
155
- const rect = el.getBoundingClientRect();
156
178
  const manualOverride = el.getAttribute(ATTR_SNOWFALL);
157
179
  if (manualOverride === VAL_IGNORE) return;
158
180
  const isManuallyIncluded = manualOverride !== null;
159
- const styles = window.getComputedStyle(el);
181
+ const styles = getCachedStyle(el);
160
182
  const isVisible = styles.display !== "none" && styles.visibility !== "hidden" && parseFloat(styles.opacity) > 0.1;
161
183
  if (!isVisible && !isManuallyIncluded) return;
184
+ const rect = el.getBoundingClientRect();
162
185
  const hasSize = rect.width >= 100 && rect.height >= 50;
163
186
  if (!hasSize && !isManuallyIncluded) return;
164
187
  const isFullPageWrapper = rect.top <= 10 && rect.height >= window.innerHeight * 0.9;
@@ -169,7 +192,7 @@ var getAccumulationSurfaces = () => {
169
192
  let isFixed = false;
170
193
  let currentEl = el;
171
194
  while (currentEl && currentEl !== document.body) {
172
- const style = window.getComputedStyle(currentEl);
195
+ const style = getCachedStyle(currentEl);
173
196
  if (style.position === "fixed" || style.position === "sticky") {
174
197
  isFixed = true;
175
198
  break;
@@ -188,6 +211,7 @@ var getAccumulationSurfaces = () => {
188
211
  }
189
212
  });
190
213
  console.log(`[Snowfall] Auto-detection found ${surfaces.length} surfaces`);
214
+ console.log("[Snowfall] \u2705 Using OPTIMIZED version with Map-based caching & 5s intervals");
191
215
  return surfaces;
192
216
  };
193
217
  var getElementRects = (accumulationMap) => {
@@ -554,16 +578,26 @@ var drawSideAccumulations = (ctx, fixedCtx, elementRects) => {
554
578
  // src/Snowfall.tsx
555
579
  var import_jsx_runtime2 = require("react/jsx-runtime");
556
580
  function Snowfall() {
557
- const { isEnabled, physicsConfig } = useSnowfall();
581
+ const { isEnabled, physicsConfig, setMetrics } = useSnowfall();
558
582
  const isEnabledRef = (0, import_react2.useRef)(isEnabled);
559
583
  const physicsConfigRef = (0, import_react2.useRef)(physicsConfig);
584
+ const setMetricsRef = (0, import_react2.useRef)(setMetrics);
560
585
  const [isMounted, setIsMounted] = (0, import_react2.useState)(false);
561
586
  const [isVisible, setIsVisible] = (0, import_react2.useState)(false);
562
587
  const canvasRef = (0, import_react2.useRef)(null);
563
- const fixedCanvasRef = (0, import_react2.useRef)(null);
564
588
  const snowflakesRef = (0, import_react2.useRef)([]);
565
589
  const accumulationRef = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
566
590
  const animationIdRef = (0, import_react2.useRef)(0);
591
+ const fpsFrames = (0, import_react2.useRef)([]);
592
+ const metricsRef = (0, import_react2.useRef)({
593
+ scanTime: 0,
594
+ rectUpdateTime: 0,
595
+ frameTime: 0,
596
+ rafGap: 0,
597
+ clearTime: 0,
598
+ physicsTime: 0,
599
+ drawTime: 0
600
+ });
567
601
  (0, import_react2.useEffect)(() => {
568
602
  setIsMounted(true);
569
603
  }, []);
@@ -573,26 +607,23 @@ function Snowfall() {
573
607
  (0, import_react2.useEffect)(() => {
574
608
  physicsConfigRef.current = physicsConfig;
575
609
  }, [physicsConfig]);
610
+ (0, import_react2.useEffect)(() => {
611
+ setMetricsRef.current = setMetrics;
612
+ }, [setMetrics]);
576
613
  (0, import_react2.useEffect)(() => {
577
614
  if (!isMounted) return;
578
615
  const canvas = canvasRef.current;
579
- const fixedCanvas = fixedCanvasRef.current;
580
- if (!canvas || !fixedCanvas) return;
616
+ if (!canvas) return;
581
617
  const ctx = canvas.getContext("2d");
582
- const fixedCtx = fixedCanvas.getContext("2d");
583
- if (!ctx || !fixedCtx) return;
618
+ if (!ctx) return;
584
619
  const resizeCanvas = () => {
585
- if (canvasRef.current && fixedCanvasRef.current) {
620
+ if (canvasRef.current) {
586
621
  const newHeight = Math.max(document.documentElement.scrollHeight, window.innerHeight);
587
622
  const newWidth = Math.max(document.documentElement.scrollWidth, window.innerWidth);
588
623
  if (canvasRef.current.height !== newHeight || canvasRef.current.width !== newWidth) {
589
624
  canvasRef.current.width = newWidth;
590
625
  canvasRef.current.height = newHeight;
591
626
  }
592
- if (fixedCanvasRef.current.width !== window.innerWidth || fixedCanvasRef.current.height !== window.innerHeight) {
593
- fixedCanvasRef.current.width = window.innerWidth;
594
- fixedCanvasRef.current.height = window.innerHeight;
595
- }
596
627
  }
597
628
  };
598
629
  resizeCanvas();
@@ -602,11 +633,16 @@ function Snowfall() {
602
633
  resizeObserver.observe(document.body);
603
634
  snowflakesRef.current = [];
604
635
  const initAccumulationWrapper = () => {
636
+ const scanStart = performance.now();
605
637
  initializeAccumulation(accumulationRef.current, physicsConfigRef.current);
638
+ metricsRef.current.scanTime = performance.now() - scanStart;
606
639
  };
607
640
  initAccumulationWrapper();
608
641
  setIsVisible(true);
609
642
  let lastTime = 0;
643
+ let lastRectUpdate = 0;
644
+ let lastMetricsUpdate = 0;
645
+ let cachedElementRects = [];
610
646
  const animate = (currentTime) => {
611
647
  if (lastTime === 0) {
612
648
  lastTime = currentTime;
@@ -614,14 +650,28 @@ function Snowfall() {
614
650
  return;
615
651
  }
616
652
  const deltaTime = Math.min(currentTime - lastTime, 50);
653
+ const now = performance.now();
654
+ fpsFrames.current.push(now);
655
+ fpsFrames.current = fpsFrames.current.filter((t) => now - t < 1e3);
656
+ metricsRef.current.rafGap = currentTime - lastTime;
617
657
  lastTime = currentTime;
618
658
  const dt = deltaTime / 16.67;
659
+ const frameStartTime = performance.now();
660
+ const clearStart = performance.now();
619
661
  ctx.clearRect(0, 0, canvas.width, canvas.height);
620
- fixedCtx.clearRect(0, 0, fixedCanvas.width, fixedCanvas.height);
662
+ metricsRef.current.clearTime = performance.now() - clearStart;
621
663
  const snowflakes = snowflakesRef.current;
622
- const elementRects = getElementRects(accumulationRef.current);
623
- meltAndSmoothAccumulation(elementRects, physicsConfigRef.current, dt);
624
- updateSnowflakes(snowflakes, elementRects, physicsConfigRef.current, dt, canvas.width, canvas.height);
664
+ if (currentTime - lastRectUpdate > 100) {
665
+ const rectStart = performance.now();
666
+ cachedElementRects = getElementRects(accumulationRef.current);
667
+ metricsRef.current.rectUpdateTime = performance.now() - rectStart;
668
+ lastRectUpdate = currentTime;
669
+ }
670
+ const physicsStart = performance.now();
671
+ meltAndSmoothAccumulation(cachedElementRects, physicsConfigRef.current, dt);
672
+ updateSnowflakes(snowflakes, cachedElementRects, physicsConfigRef.current, dt, canvas.width, canvas.height);
673
+ metricsRef.current.physicsTime = performance.now() - physicsStart;
674
+ const drawStart = performance.now();
625
675
  for (const flake of snowflakes) {
626
676
  drawSnowflake(ctx, flake);
627
677
  }
@@ -629,8 +679,26 @@ function Snowfall() {
629
679
  const isBackground = Math.random() < 0.4;
630
680
  snowflakes.push(createSnowflake(canvas.width, physicsConfigRef.current, isBackground));
631
681
  }
632
- drawAccumulations(ctx, fixedCtx, elementRects);
633
- drawSideAccumulations(ctx, fixedCtx, elementRects);
682
+ drawAccumulations(ctx, ctx, cachedElementRects);
683
+ drawSideAccumulations(ctx, ctx, cachedElementRects);
684
+ metricsRef.current.drawTime = performance.now() - drawStart;
685
+ metricsRef.current.frameTime = performance.now() - frameStartTime;
686
+ if (currentTime - lastMetricsUpdate > 500) {
687
+ setMetricsRef.current({
688
+ fps: fpsFrames.current.length,
689
+ frameTime: metricsRef.current.frameTime,
690
+ scanTime: metricsRef.current.scanTime,
691
+ rectUpdateTime: metricsRef.current.rectUpdateTime,
692
+ surfaceCount: accumulationRef.current.size,
693
+ flakeCount: snowflakes.length,
694
+ maxFlakes: physicsConfigRef.current.MAX_FLAKES,
695
+ rafGap: metricsRef.current.rafGap,
696
+ clearTime: metricsRef.current.clearTime,
697
+ physicsTime: metricsRef.current.physicsTime,
698
+ drawTime: metricsRef.current.drawTime
699
+ });
700
+ lastMetricsUpdate = currentTime;
701
+ }
634
702
  animationIdRef.current = requestAnimationFrame(animate);
635
703
  };
636
704
  animationIdRef.current = requestAnimationFrame(animate);
@@ -640,7 +708,7 @@ function Snowfall() {
640
708
  initAccumulationWrapper();
641
709
  };
642
710
  window.addEventListener("resize", handleResize);
643
- const checkInterval = setInterval(initAccumulationWrapper, 3e3);
711
+ const checkInterval = setInterval(initAccumulationWrapper, 5e3);
644
712
  return () => {
645
713
  cancelAnimationFrame(animationIdRef.current);
646
714
  window.removeEventListener("resize", handleResize);
@@ -649,44 +717,202 @@ function Snowfall() {
649
717
  };
650
718
  }, [isMounted]);
651
719
  if (!isMounted) return null;
652
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
653
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
654
- "canvas",
655
- {
656
- ref: canvasRef,
657
- style: {
658
- position: "absolute",
659
- top: 0,
660
- left: 0,
661
- pointerEvents: "none",
662
- zIndex: 9999,
663
- opacity: isVisible ? 1 : 0,
664
- transition: "opacity 0.3s ease-in"
665
- },
666
- "aria-hidden": "true"
667
- }
668
- ),
669
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
670
- "canvas",
671
- {
672
- ref: fixedCanvasRef,
673
- style: {
674
- position: "fixed",
675
- top: 0,
676
- left: 0,
677
- pointerEvents: "none",
678
- zIndex: 9999,
679
- opacity: isVisible ? 1 : 0,
680
- transition: "opacity 0.3s ease-in"
681
- },
682
- "aria-hidden": "true"
720
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
721
+ "canvas",
722
+ {
723
+ ref: canvasRef,
724
+ style: {
725
+ position: "absolute",
726
+ top: 0,
727
+ left: 0,
728
+ pointerEvents: "none",
729
+ zIndex: 9999,
730
+ opacity: isVisible ? 1 : 0,
731
+ transition: "opacity 0.3s ease-in",
732
+ willChange: "transform"
733
+ },
734
+ "aria-hidden": "true"
735
+ }
736
+ ) });
737
+ }
738
+
739
+ // src/DebugPanel.tsx
740
+ var import_react3 = require("react");
741
+ var import_jsx_runtime3 = require("react/jsx-runtime");
742
+ function DebugPanel() {
743
+ const { debugMode, toggleDebug, metrics } = useSnowfall();
744
+ const [isMinimized, setIsMinimized] = (0, import_react3.useState)(false);
745
+ const [copied, setCopied] = (0, import_react3.useState)(false);
746
+ (0, import_react3.useEffect)(() => {
747
+ const handleKeyDown = (e) => {
748
+ if (e.shiftKey && e.key === "D") {
749
+ toggleDebug();
683
750
  }
684
- )
751
+ };
752
+ window.addEventListener("keydown", handleKeyDown);
753
+ return () => window.removeEventListener("keydown", handleKeyDown);
754
+ }, [toggleDebug]);
755
+ const copyToClipboard = () => {
756
+ if (metrics) {
757
+ const data = {
758
+ ...metrics,
759
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
760
+ userAgent: navigator.userAgent,
761
+ canvasSize: {
762
+ width: window.innerWidth,
763
+ height: window.innerHeight
764
+ }
765
+ };
766
+ navigator.clipboard.writeText(JSON.stringify(data, null, 2));
767
+ setCopied(true);
768
+ setTimeout(() => setCopied(false), 2e3);
769
+ }
770
+ };
771
+ if (!debugMode) return null;
772
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: {
773
+ position: "fixed",
774
+ bottom: "20px",
775
+ left: "20px",
776
+ backgroundColor: "rgba(0, 0, 0, 0.9)",
777
+ color: "#0f0",
778
+ fontFamily: "monospace",
779
+ fontSize: "12px",
780
+ padding: isMinimized ? "10px" : "15px",
781
+ borderRadius: "8px",
782
+ zIndex: 1e4,
783
+ minWidth: isMinimized ? "auto" : "320px",
784
+ maxWidth: "400px",
785
+ border: "1px solid #0f0",
786
+ boxShadow: "0 4px 20px rgba(0, 255, 0, 0.3)"
787
+ }, children: [
788
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: {
789
+ display: "flex",
790
+ justifyContent: "space-between",
791
+ alignItems: "center",
792
+ marginBottom: isMinimized ? 0 : "10px"
793
+ }, children: [
794
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { fontWeight: "bold", color: "#0ff" }, children: "\u2744\uFE0F SNOWFALL DEBUG" }),
795
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "10px" }, children: [
796
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
797
+ "button",
798
+ {
799
+ onClick: () => setIsMinimized(!isMinimized),
800
+ style: {
801
+ background: "none",
802
+ border: "1px solid #0f0",
803
+ color: "#0f0",
804
+ cursor: "pointer",
805
+ padding: "2px 8px",
806
+ borderRadius: "4px",
807
+ fontSize: "10px"
808
+ },
809
+ children: isMinimized ? "\u25B2" : "\u25BC"
810
+ }
811
+ ),
812
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
813
+ "button",
814
+ {
815
+ onClick: toggleDebug,
816
+ style: {
817
+ background: "none",
818
+ border: "1px solid #f00",
819
+ color: "#f00",
820
+ cursor: "pointer",
821
+ padding: "2px 8px",
822
+ borderRadius: "4px",
823
+ fontSize: "10px"
824
+ },
825
+ children: "\u2715"
826
+ }
827
+ )
828
+ ] })
829
+ ] }),
830
+ !isMinimized && metrics && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
831
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { marginBottom: "10px", paddingBottom: "10px", borderBottom: "1px solid #333" }, children: [
832
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { color: "#ff0", marginBottom: "5px", fontWeight: "bold" }, children: "\u26A1 PERFORMANCE" }),
833
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
834
+ "FPS: ",
835
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { color: metrics.fps < 30 ? "#f00" : metrics.fps < 50 ? "#ff0" : "#0f0" }, children: metrics.fps.toFixed(1) })
836
+ ] }),
837
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
838
+ "Frame Time: ",
839
+ metrics.frameTime.toFixed(2),
840
+ "ms"
841
+ ] }),
842
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { color: "#f80" }, children: [
843
+ "rAF Gap: ",
844
+ metrics.rafGap?.toFixed(1) || 0,
845
+ "ms ",
846
+ metrics.rafGap && metrics.rafGap > 20 ? "\u26A0\uFE0F THROTTLED!" : ""
847
+ ] })
848
+ ] }),
849
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { marginBottom: "10px", paddingBottom: "10px", borderBottom: "1px solid #333" }, children: [
850
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { color: "#ff0", marginBottom: "5px", fontWeight: "bold" }, children: "\u{1F52C} DETAILED TIMINGS" }),
851
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
852
+ "Clear: ",
853
+ metrics.clearTime?.toFixed(2) || 0,
854
+ "ms"
855
+ ] }),
856
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
857
+ "Physics: ",
858
+ metrics.physicsTime?.toFixed(2) || 0,
859
+ "ms"
860
+ ] }),
861
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
862
+ "Draw: ",
863
+ metrics.drawTime?.toFixed(2) || 0,
864
+ "ms"
865
+ ] }),
866
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
867
+ "Scan: ",
868
+ metrics.scanTime.toFixed(2),
869
+ "ms"
870
+ ] }),
871
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
872
+ "Rect Update: ",
873
+ metrics.rectUpdateTime.toFixed(2),
874
+ "ms"
875
+ ] })
876
+ ] }),
877
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { marginBottom: "10px", paddingBottom: "10px", borderBottom: "1px solid #333" }, children: [
878
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { color: "#ff0", marginBottom: "5px", fontWeight: "bold" }, children: "\u{1F4CA} COUNTS" }),
879
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
880
+ "Snowflakes: ",
881
+ metrics.flakeCount,
882
+ " / ",
883
+ metrics.maxFlakes
884
+ ] }),
885
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
886
+ "Surfaces: ",
887
+ metrics.surfaceCount
888
+ ] })
889
+ ] }),
890
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
891
+ "button",
892
+ {
893
+ onClick: copyToClipboard,
894
+ style: {
895
+ width: "100%",
896
+ padding: "8px",
897
+ background: copied ? "#0a0" : "#000",
898
+ border: copied ? "1px solid #0f0" : "1px solid #555",
899
+ color: "#0f0",
900
+ cursor: "pointer",
901
+ borderRadius: "4px",
902
+ fontSize: "11px",
903
+ fontFamily: "monospace"
904
+ },
905
+ children: copied ? "\u2713 COPIED!" : "\u{1F4CB} COPY METRICS"
906
+ }
907
+ ),
908
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { marginTop: "8px", fontSize: "10px", color: "#666", textAlign: "center" }, children: "Shift+D to toggle" })
909
+ ] })
685
910
  ] });
686
911
  }
687
912
  // Annotate the CommonJS export names for ESM import in node:
688
913
  0 && (module.exports = {
689
914
  DEFAULT_PHYSICS,
915
+ DebugPanel,
690
916
  Snowfall,
691
917
  SnowfallProvider,
692
918
  useSnowfall