@hdcodedev/snowfall 1.0.4 → 1.0.6

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.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
@@ -51,15 +52,21 @@ var DEFAULT_PHYSICS = {
51
52
  FLAKE_SIZE: {
52
53
  MIN: 0.5,
53
54
  MAX: 1.6
54
- }
55
+ },
56
+ MAX_SURFACES: 15
55
57
  };
56
58
  var SnowfallContext = (0, import_react.createContext)(void 0);
57
- function SnowfallProvider({ children }) {
59
+ function SnowfallProvider({ children, initialDebug = false }) {
58
60
  const [isEnabled, setIsEnabled] = (0, import_react.useState)(true);
59
61
  const [physicsConfig, setPhysicsConfig] = (0, import_react.useState)(DEFAULT_PHYSICS);
62
+ const [debugMode, setDebugMode] = (0, import_react.useState)(initialDebug);
63
+ const [metrics, setMetrics] = (0, import_react.useState)(null);
60
64
  const toggleSnow = () => {
61
65
  setIsEnabled((prev) => !prev);
62
66
  };
67
+ const toggleDebug = () => {
68
+ setDebugMode((prev) => !prev);
69
+ };
63
70
  const updatePhysicsConfig = (config) => {
64
71
  setPhysicsConfig((prev) => ({
65
72
  ...prev,
@@ -86,7 +93,11 @@ function SnowfallProvider({ children }) {
86
93
  toggleSnow,
87
94
  physicsConfig,
88
95
  updatePhysicsConfig,
89
- resetPhysics
96
+ resetPhysics,
97
+ debugMode,
98
+ toggleDebug,
99
+ metrics,
100
+ setMetrics
90
101
  }, children });
91
102
  }
92
103
  function useSnowfall() {
@@ -103,13 +114,15 @@ var VAL_IGNORE = "ignore";
103
114
  var VAL_TOP = "top";
104
115
  var VAL_BOTTOM = "bottom";
105
116
  var TAG_HEADER = "header";
117
+ var TAG_FOOTER = "footer";
106
118
  var ROLE_BANNER = "banner";
119
+ var ROLE_CONTENTINFO = "contentinfo";
107
120
 
108
121
  // src/utils/snowfall/dom.ts
109
122
  var BOTTOM_TAGS = [TAG_HEADER];
110
123
  var BOTTOM_ROLES = [ROLE_BANNER];
111
- var AUTO_DETECT_TAGS = ["header", "footer", "article", "section", "aside", "nav"];
112
- var AUTO_DETECT_ROLES = ['[role="banner"]', '[role="contentinfo"]', '[role="main"]'];
124
+ var AUTO_DETECT_TAGS = [TAG_HEADER, TAG_FOOTER, "article", "section", "aside", "nav"];
125
+ var AUTO_DETECT_ROLES = [`[role="${ROLE_BANNER}"]`, `[role="${ROLE_CONTENTINFO}"]`, '[role="main"]'];
113
126
  var AUTO_DETECT_CLASSES = [
114
127
  ".card",
115
128
  '[class*="card"]',
@@ -125,21 +138,21 @@ var getElementType = (el) => {
125
138
  if (role && BOTTOM_ROLES.includes(role)) return VAL_BOTTOM;
126
139
  return VAL_TOP;
127
140
  };
128
- var shouldAccumulate = (el) => {
141
+ var shouldAccumulate = (el, precomputedStyle) => {
129
142
  if (el.getAttribute(ATTR_SNOWFALL) === VAL_IGNORE) return false;
130
143
  if (el.hasAttribute(ATTR_SNOWFALL)) return true;
131
- const styles = window.getComputedStyle(el);
132
- const rect = el.getBoundingClientRect();
144
+ const styles = precomputedStyle || window.getComputedStyle(el);
133
145
  const isVisible = styles.display !== "none" && styles.visibility !== "hidden" && parseFloat(styles.opacity) > 0.1;
134
146
  if (!isVisible) return false;
135
147
  const bgColor = styles.backgroundColor;
136
148
  const hasBackground = bgColor !== "rgba(0, 0, 0, 0)" && bgColor !== "transparent";
137
- const hasBorder = parseFloat(styles.borderWidth) > 0 || styles.borderStyle !== "none";
149
+ const hasBorder = parseFloat(styles.borderWidth) > 0 && styles.borderColor !== "transparent" && styles.borderColor !== "rgba(0, 0, 0, 0)" && styles.borderStyle !== "none";
138
150
  const hasBoxShadow = styles.boxShadow !== "none";
139
- const hasBorderRadius = parseFloat(styles.borderRadius) > 0;
140
- return hasBackground || hasBorder || hasBoxShadow || hasBorderRadius;
151
+ const hasFilter = styles.filter !== "none" && styles.filter.includes("drop-shadow");
152
+ const hasBackdropFilter = styles.backdropFilter !== "none";
153
+ return hasBackground || hasBorder || hasBoxShadow || hasFilter || hasBackdropFilter;
141
154
  };
142
- var getAccumulationSurfaces = () => {
155
+ var getAccumulationSurfaces = (maxSurfaces = 5) => {
143
156
  const surfaces = [];
144
157
  const seen = /* @__PURE__ */ new Set();
145
158
  const candidates = document.querySelectorAll(
@@ -150,44 +163,31 @@ var getAccumulationSurfaces = () => {
150
163
  ...AUTO_DETECT_CLASSES
151
164
  ].join(", ")
152
165
  );
153
- candidates.forEach((el) => {
154
- if (seen.has(el)) return;
155
- const rect = el.getBoundingClientRect();
166
+ for (const el of candidates) {
167
+ if (surfaces.length >= maxSurfaces) break;
168
+ if (seen.has(el)) continue;
156
169
  const manualOverride = el.getAttribute(ATTR_SNOWFALL);
157
- if (manualOverride === VAL_IGNORE) return;
170
+ if (manualOverride === VAL_IGNORE) continue;
158
171
  const isManuallyIncluded = manualOverride !== null;
159
172
  const styles = window.getComputedStyle(el);
160
173
  const isVisible = styles.display !== "none" && styles.visibility !== "hidden" && parseFloat(styles.opacity) > 0.1;
161
- if (!isVisible && !isManuallyIncluded) return;
174
+ if (!isVisible && !isManuallyIncluded) continue;
175
+ const rect = el.getBoundingClientRect();
162
176
  const hasSize = rect.width >= 100 && rect.height >= 50;
163
- if (!hasSize && !isManuallyIncluded) return;
177
+ if (!hasSize && !isManuallyIncluded) continue;
164
178
  const isFullPageWrapper = rect.top <= 10 && rect.height >= window.innerHeight * 0.9;
165
179
  const isBottomTag = BOTTOM_TAGS.includes(el.tagName.toLowerCase());
166
180
  const isBottomRole = BOTTOM_ROLES.includes(el.getAttribute("role") || "");
167
181
  const isBottomSurface = isBottomTag || isBottomRole || manualOverride === VAL_BOTTOM;
168
- if (isFullPageWrapper && !isBottomSurface && !isManuallyIncluded) return;
169
- let isFixed = false;
170
- let currentEl = el;
171
- while (currentEl && currentEl !== document.body) {
172
- const style = window.getComputedStyle(currentEl);
173
- if (style.position === "fixed" || style.position === "sticky") {
174
- isFixed = true;
175
- break;
176
- }
177
- currentEl = currentEl.parentElement;
178
- }
179
- if (shouldAccumulate(el)) {
182
+ if (isFullPageWrapper && !isBottomSurface && !isManuallyIncluded) continue;
183
+ if (shouldAccumulate(el, styles)) {
180
184
  let type = getElementType(el);
181
- if (manualOverride === VAL_BOTTOM) {
182
- type = VAL_BOTTOM;
183
- } else if (manualOverride === VAL_TOP) {
184
- type = VAL_TOP;
185
- }
186
- surfaces.push({ el, type, isFixed });
185
+ if (manualOverride === VAL_BOTTOM) type = VAL_BOTTOM;
186
+ else if (manualOverride === VAL_TOP) type = VAL_TOP;
187
+ surfaces.push({ el, type });
187
188
  seen.add(el);
188
189
  }
189
- });
190
- console.log(`[Snowfall] Auto-detection found ${surfaces.length} surfaces`);
190
+ }
191
191
  return surfaces;
192
192
  };
193
193
  var getElementRects = (accumulationMap) => {
@@ -195,31 +195,18 @@ var getElementRects = (accumulationMap) => {
195
195
  for (const [el, acc] of accumulationMap.entries()) {
196
196
  if (!el.isConnected) continue;
197
197
  const rect = el.getBoundingClientRect();
198
- const absoluteRect = {
199
- left: rect.left + window.scrollX,
200
- right: rect.right + window.scrollX,
201
- top: rect.top + window.scrollY,
202
- bottom: rect.bottom + window.scrollY,
203
- width: rect.width,
204
- height: rect.height,
205
- x: rect.x,
206
- // Note: these are strictly viewport relative in DOMRect usually,
207
- // but we just need consistent absolute coords for physics
208
- y: rect.y,
209
- toJSON: rect.toJSON
210
- };
211
- elementRects.push({ el, rect: absoluteRect, acc });
198
+ elementRects.push({ el, rect, acc });
212
199
  }
213
200
  return elementRects;
214
201
  };
215
202
 
216
203
  // src/utils/snowfall/physics.ts
217
- var createSnowflake = (canvasWidth, config, isBackground = false) => {
204
+ var createSnowflake = (worldWidth, config, isBackground = false) => {
218
205
  if (isBackground) {
219
206
  const sizeRatio = Math.random();
220
207
  const radius = config.FLAKE_SIZE.MIN * 0.6 + sizeRatio * (config.FLAKE_SIZE.MAX - config.FLAKE_SIZE.MIN) * 0.4;
221
208
  return {
222
- x: Math.random() * canvasWidth,
209
+ x: Math.random() * worldWidth,
223
210
  y: window.scrollY - 5,
224
211
  radius,
225
212
  speed: radius * 0.3 + Math.random() * 0.2 + 0.2,
@@ -234,7 +221,7 @@ var createSnowflake = (canvasWidth, config, isBackground = false) => {
234
221
  const sizeRatio = Math.random();
235
222
  const radius = config.FLAKE_SIZE.MIN + sizeRatio * (config.FLAKE_SIZE.MAX - config.FLAKE_SIZE.MIN);
236
223
  return {
237
- x: Math.random() * canvasWidth,
224
+ x: Math.random() * worldWidth,
238
225
  y: window.scrollY - 5,
239
226
  radius,
240
227
  speed: radius * 0.5 + Math.random() * 0.3 + 0.5,
@@ -248,20 +235,19 @@ var createSnowflake = (canvasWidth, config, isBackground = false) => {
248
235
  }
249
236
  };
250
237
  var initializeAccumulation = (accumulationMap, config) => {
251
- const elements = getAccumulationSurfaces();
238
+ const elements = getAccumulationSurfaces(config.MAX_SURFACES);
252
239
  for (const [el] of accumulationMap.entries()) {
253
240
  if (!el.isConnected) {
254
241
  accumulationMap.delete(el);
255
242
  }
256
243
  }
257
- elements.forEach(({ el, type, isFixed }) => {
244
+ elements.forEach(({ el, type }) => {
258
245
  const existing = accumulationMap.get(el);
259
246
  const rect = el.getBoundingClientRect();
260
247
  const width = Math.ceil(rect.width);
261
248
  const isBottom = type === VAL_BOTTOM;
262
249
  if (existing && existing.heights.length === width) {
263
250
  existing.type = type;
264
- existing.isFixed = isFixed;
265
251
  if (existing.borderRadius !== void 0) {
266
252
  const styleBuffer = window.getComputedStyle(el);
267
253
  existing.borderRadius = parseFloat(styleBuffer.borderTopLeftRadius) || 0;
@@ -299,8 +285,7 @@ var initializeAccumulation = (accumulationMap, config) => {
299
285
  rightSide: existing?.rightSide.length === height ? existing.rightSide : new Array(height).fill(0),
300
286
  maxSideHeight: isBottom ? 0 : config.MAX_DEPTH.SIDE,
301
287
  borderRadius,
302
- type,
303
- isFixed
288
+ type
304
289
  });
305
290
  });
306
291
  };
@@ -319,30 +304,35 @@ var accumulateSide = (sideArray, rectHeight, localY, maxSideHeight, borderRadius
319
304
  }
320
305
  }
321
306
  };
322
- var updateSnowflakes = (snowflakes, elementRects, config, dt, canvasWidth, canvasHeight) => {
307
+ var updateSnowflakes = (snowflakes, elementRects, config, dt, worldWidth, worldHeight) => {
308
+ const scrollX = window.scrollX;
309
+ const scrollY = window.scrollY;
323
310
  for (let i = snowflakes.length - 1; i >= 0; i--) {
324
311
  const flake = snowflakes[i];
325
312
  flake.wobble += flake.wobbleSpeed * dt;
326
313
  flake.x += (flake.wind + Math.sin(flake.wobble) * 0.5) * dt;
327
314
  flake.y += (flake.speed + Math.cos(flake.wobble * 0.5) * 0.1) * dt;
328
315
  let landed = false;
329
- for (const { rect, acc } of elementRects) {
316
+ for (const item of elementRects) {
317
+ const { rect, acc } = item;
330
318
  const isBottom = acc.type === VAL_BOTTOM;
319
+ const flakeViewportX = flake.x - scrollX;
320
+ const flakeViewportY = flake.y - scrollY;
321
+ const isInVerticalBounds = flakeViewportY >= rect.top && flakeViewportY <= rect.bottom;
331
322
  if (!landed && acc.maxSideHeight > 0 && !isBottom) {
332
- const isInVerticalBounds = flake.y >= rect.top && flake.y <= rect.bottom;
333
323
  if (isInVerticalBounds) {
334
- const localY = Math.floor(flake.y - rect.top);
324
+ const localY = Math.floor(flakeViewportY - rect.top);
335
325
  const borderRadius = acc.borderRadius;
336
326
  const isInTopCorner = localY < borderRadius;
337
327
  const isInBottomCorner = localY > rect.height - borderRadius;
338
328
  const isCorner = borderRadius > 0 && (isInTopCorner || isInBottomCorner);
339
- if (flake.x >= rect.left - 5 && flake.x < rect.left + 3) {
329
+ if (flakeViewportX >= rect.left - 5 && flakeViewportX < rect.left + 3) {
340
330
  if (!isCorner) {
341
331
  accumulateSide(acc.leftSide, rect.height, localY, acc.maxSideHeight, borderRadius, config);
342
332
  landed = true;
343
333
  }
344
334
  }
345
- if (!landed && flake.x > rect.right - 3 && flake.x <= rect.right + 5) {
335
+ if (!landed && flakeViewportX > rect.right - 3 && flakeViewportX <= rect.right + 5) {
346
336
  if (!isCorner) {
347
337
  accumulateSide(acc.rightSide, rect.height, localY, acc.maxSideHeight, borderRadius, config);
348
338
  landed = true;
@@ -351,12 +341,12 @@ var updateSnowflakes = (snowflakes, elementRects, config, dt, canvasWidth, canva
351
341
  if (landed) break;
352
342
  }
353
343
  }
354
- if (flake.x >= rect.left && flake.x <= rect.right) {
355
- const localX = Math.floor(flake.x - rect.left);
344
+ if (flakeViewportX >= rect.left && flakeViewportX <= rect.right) {
345
+ const localX = Math.floor(flakeViewportX - rect.left);
356
346
  const currentHeight = acc.heights[localX] || 0;
357
347
  const maxHeight = acc.maxHeights[localX] || 5;
358
348
  const surfaceY = isBottom ? rect.bottom - currentHeight : rect.top - currentHeight;
359
- if (flake.y >= surfaceY && flake.y < surfaceY + 10 && currentHeight < maxHeight) {
349
+ if (flakeViewportY >= surfaceY && flakeViewportY < surfaceY + 10 && currentHeight < maxHeight) {
360
350
  const shouldAccumulate2 = isBottom ? Math.random() < 0.15 : true;
361
351
  if (shouldAccumulate2) {
362
352
  const baseSpread = Math.ceil(flake.radius);
@@ -391,7 +381,7 @@ var updateSnowflakes = (snowflakes, elementRects, config, dt, canvasWidth, canva
391
381
  }
392
382
  }
393
383
  }
394
- if (landed || flake.y > canvasHeight + 10 || flake.x < -20 || flake.x > canvasWidth + 20) {
384
+ if (landed || flake.y > worldHeight + 10 || flake.x < -20 || flake.x > worldWidth + 20) {
395
385
  snowflakes.splice(i, 1);
396
386
  }
397
387
  }
@@ -429,7 +419,7 @@ var drawSnowflake = (ctx, flake) => {
429
419
  ctx.fillStyle = `rgba(255, 255, 255, ${flake.opacity * 0.2})`;
430
420
  ctx.fill();
431
421
  };
432
- var drawAccumulations = (ctx, fixedCtx, elementRects) => {
422
+ var drawAccumulations = (ctx, elementRects) => {
433
423
  const setupCtx = (c) => {
434
424
  c.fillStyle = "rgba(255, 255, 255, 0.95)";
435
425
  c.shadowColor = "rgba(200, 230, 255, 0.6)";
@@ -437,15 +427,13 @@ var drawAccumulations = (ctx, fixedCtx, elementRects) => {
437
427
  c.shadowOffsetY = -1;
438
428
  };
439
429
  setupCtx(ctx);
440
- if (fixedCtx) setupCtx(fixedCtx);
441
- const scrollX = window.scrollX;
442
- const scrollY = window.scrollY;
443
- for (const { rect, acc } of elementRects) {
430
+ const currentScrollX = window.scrollX;
431
+ const currentScrollY = window.scrollY;
432
+ for (const item of elementRects) {
433
+ const { rect, acc } = item;
444
434
  if (!acc.heights.some((h) => h > 0.1)) continue;
445
- const useFixed = acc.isFixed && fixedCtx;
446
- const targetCtx = useFixed ? fixedCtx : ctx;
447
- const dx = useFixed ? -scrollX : 0;
448
- const dy = useFixed ? -scrollY : 0;
435
+ const dx = currentScrollX;
436
+ const dy = currentScrollY;
449
437
  const isBottom = acc.type === VAL_BOTTOM;
450
438
  const baseY = isBottom ? rect.bottom - 1 : rect.top + 1;
451
439
  const borderRadius = acc.borderRadius;
@@ -461,7 +449,7 @@ var drawAccumulations = (ctx, fixedCtx, elementRects) => {
461
449
  }
462
450
  return offset;
463
451
  };
464
- targetCtx.beginPath();
452
+ ctx.beginPath();
465
453
  let first = true;
466
454
  const step = 2;
467
455
  const len = acc.heights.length;
@@ -470,10 +458,10 @@ var drawAccumulations = (ctx, fixedCtx, elementRects) => {
470
458
  const px = rect.left + x + dx;
471
459
  const py = baseY - height + getCurveOffset(x) + dy;
472
460
  if (first) {
473
- targetCtx.moveTo(px, py);
461
+ ctx.moveTo(px, py);
474
462
  first = false;
475
463
  } else {
476
- targetCtx.lineTo(px, py);
464
+ ctx.lineTo(px, py);
477
465
  }
478
466
  }
479
467
  if ((len - 1) % step !== 0) {
@@ -481,50 +469,44 @@ var drawAccumulations = (ctx, fixedCtx, elementRects) => {
481
469
  const height = acc.heights[x] || 0;
482
470
  const px = rect.left + x + dx;
483
471
  const py = baseY - height + getCurveOffset(x) + dy;
484
- targetCtx.lineTo(px, py);
472
+ ctx.lineTo(px, py);
485
473
  }
486
474
  for (let x = len - 1; x >= 0; x -= step) {
487
475
  const px = rect.left + x + dx;
488
476
  const py = baseY + getCurveOffset(x) + dy;
489
- targetCtx.lineTo(px, py);
477
+ ctx.lineTo(px, py);
490
478
  }
491
479
  const startX = 0;
492
480
  const startPx = rect.left + startX + dx;
493
481
  const startPy = baseY + getCurveOffset(startX) + dy;
494
- targetCtx.lineTo(startPx, startPy);
495
- targetCtx.closePath();
496
- targetCtx.fill();
482
+ ctx.lineTo(startPx, startPy);
483
+ ctx.closePath();
484
+ ctx.fill();
497
485
  }
498
486
  ctx.shadowBlur = 0;
499
487
  ctx.shadowOffsetY = 0;
500
- if (fixedCtx) {
501
- fixedCtx.shadowBlur = 0;
502
- fixedCtx.shadowOffsetY = 0;
503
- }
504
488
  };
505
- var drawSideAccumulations = (ctx, fixedCtx, elementRects) => {
489
+ var drawSideAccumulations = (ctx, elementRects) => {
506
490
  const setupCtx = (c) => {
507
491
  c.fillStyle = "rgba(255, 255, 255, 0.95)";
508
492
  c.shadowColor = "rgba(200, 230, 255, 0.6)";
509
493
  c.shadowBlur = 3;
510
494
  };
511
495
  setupCtx(ctx);
512
- if (fixedCtx) setupCtx(fixedCtx);
513
- const scrollX = window.scrollX;
514
- const scrollY = window.scrollY;
515
- for (const { rect, acc } of elementRects) {
496
+ const currentScrollX = window.scrollX;
497
+ const currentScrollY = window.scrollY;
498
+ for (const item of elementRects) {
499
+ const { rect, acc } = item;
516
500
  if (acc.maxSideHeight === 0) continue;
517
501
  const hasLeftSnow = acc.leftSide.some((h) => h > 0.3);
518
502
  const hasRightSnow = acc.rightSide.some((h) => h > 0.3);
519
503
  if (!hasLeftSnow && !hasRightSnow) continue;
520
- const useFixed = acc.isFixed && fixedCtx;
521
- const targetCtx = useFixed ? fixedCtx : ctx;
522
- const dx = useFixed ? -scrollX : 0;
523
- const dy = useFixed ? -scrollY : 0;
504
+ const dx = currentScrollX;
505
+ const dy = currentScrollY;
524
506
  const drawSide = (sideArray, isLeft) => {
525
- targetCtx.beginPath();
507
+ ctx.beginPath();
526
508
  const baseX = isLeft ? rect.left : rect.right;
527
- targetCtx.moveTo(baseX + dx, rect.top + dy);
509
+ ctx.moveTo(baseX + dx, rect.top + dy);
528
510
  for (let y = 0; y < sideArray.length; y += 2) {
529
511
  const width = sideArray[y] || 0;
530
512
  const nextY = Math.min(y + 2, sideArray.length - 1);
@@ -537,33 +519,42 @@ var drawSideAccumulations = (ctx, fixedCtx, elementRects) => {
537
519
  const nRatio = nextY / sideArray.length;
538
520
  const nGravityMultiplier = Math.pow(nRatio, 1.5);
539
521
  const nx = (isLeft ? baseX - nextWidth * nGravityMultiplier : baseX + nextWidth * nGravityMultiplier) + dx;
540
- targetCtx.lineTo(px, py);
541
- targetCtx.lineTo(nx, ny);
522
+ ctx.lineTo(px, py);
523
+ ctx.lineTo(nx, ny);
542
524
  }
543
- targetCtx.lineTo(baseX + dx, rect.bottom + dy);
544
- targetCtx.closePath();
545
- targetCtx.fill();
525
+ ctx.lineTo(baseX + dx, rect.bottom + dy);
526
+ ctx.closePath();
527
+ ctx.fill();
546
528
  };
547
529
  if (hasLeftSnow) drawSide(acc.leftSide, true);
548
530
  if (hasRightSnow) drawSide(acc.rightSide, false);
549
531
  }
550
532
  ctx.shadowBlur = 0;
551
- if (fixedCtx) fixedCtx.shadowBlur = 0;
552
533
  };
553
534
 
554
535
  // src/Snowfall.tsx
555
536
  var import_jsx_runtime2 = require("react/jsx-runtime");
556
537
  function Snowfall() {
557
- const { isEnabled, physicsConfig } = useSnowfall();
538
+ const { isEnabled, physicsConfig, setMetrics } = useSnowfall();
558
539
  const isEnabledRef = (0, import_react2.useRef)(isEnabled);
559
540
  const physicsConfigRef = (0, import_react2.useRef)(physicsConfig);
541
+ const setMetricsRef = (0, import_react2.useRef)(setMetrics);
560
542
  const [isMounted, setIsMounted] = (0, import_react2.useState)(false);
561
543
  const [isVisible, setIsVisible] = (0, import_react2.useState)(false);
562
544
  const canvasRef = (0, import_react2.useRef)(null);
563
- const fixedCanvasRef = (0, import_react2.useRef)(null);
564
545
  const snowflakesRef = (0, import_react2.useRef)([]);
565
546
  const accumulationRef = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
566
547
  const animationIdRef = (0, import_react2.useRef)(0);
548
+ const fpsFrames = (0, import_react2.useRef)([]);
549
+ const metricsRef = (0, import_react2.useRef)({
550
+ scanTime: 0,
551
+ rectUpdateTime: 0,
552
+ frameTime: 0,
553
+ rafGap: 0,
554
+ clearTime: 0,
555
+ physicsTime: 0,
556
+ drawTime: 0
557
+ });
567
558
  (0, import_react2.useEffect)(() => {
568
559
  setIsMounted(true);
569
560
  }, []);
@@ -573,40 +564,58 @@ function Snowfall() {
573
564
  (0, import_react2.useEffect)(() => {
574
565
  physicsConfigRef.current = physicsConfig;
575
566
  }, [physicsConfig]);
567
+ (0, import_react2.useEffect)(() => {
568
+ setMetricsRef.current = setMetrics;
569
+ }, [setMetrics]);
576
570
  (0, import_react2.useEffect)(() => {
577
571
  if (!isMounted) return;
578
572
  const canvas = canvasRef.current;
579
- const fixedCanvas = fixedCanvasRef.current;
580
- if (!canvas || !fixedCanvas) return;
573
+ if (!canvas) return;
581
574
  const ctx = canvas.getContext("2d");
582
- const fixedCtx = fixedCanvas.getContext("2d");
583
- if (!ctx || !fixedCtx) return;
575
+ if (!ctx) return;
584
576
  const resizeCanvas = () => {
585
- if (canvasRef.current && fixedCanvasRef.current) {
586
- const newHeight = Math.max(document.documentElement.scrollHeight, window.innerHeight);
587
- const newWidth = Math.max(document.documentElement.scrollWidth, window.innerWidth);
588
- if (canvasRef.current.height !== newHeight || canvasRef.current.width !== newWidth) {
589
- canvasRef.current.width = newWidth;
590
- canvasRef.current.height = newHeight;
591
- }
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
- }
577
+ if (canvasRef.current) {
578
+ const newWidth = window.innerWidth;
579
+ const newHeight = window.innerHeight;
580
+ const dpr = window.devicePixelRatio || 1;
581
+ canvasRef.current.width = newWidth * dpr;
582
+ canvasRef.current.height = newHeight * dpr;
583
+ canvasRef.current.style.width = `${newWidth}px`;
584
+ canvasRef.current.style.height = `${newHeight}px`;
596
585
  }
597
586
  };
598
587
  resizeCanvas();
599
- const resizeObserver = new ResizeObserver(() => {
588
+ const windowResizeObserver = new ResizeObserver(() => {
600
589
  resizeCanvas();
601
590
  });
602
- resizeObserver.observe(document.body);
591
+ windowResizeObserver.observe(document.body);
592
+ const surfaceObserver = new ResizeObserver((entries) => {
593
+ let needsUpdate = false;
594
+ for (const entry of entries) {
595
+ if (entry.target.isConnected) {
596
+ needsUpdate = true;
597
+ break;
598
+ }
599
+ }
600
+ if (needsUpdate) {
601
+ initAccumulationWrapper();
602
+ }
603
+ });
603
604
  snowflakesRef.current = [];
604
605
  const initAccumulationWrapper = () => {
606
+ const scanStart = performance.now();
605
607
  initializeAccumulation(accumulationRef.current, physicsConfigRef.current);
608
+ surfaceObserver.disconnect();
609
+ for (const [el] of accumulationRef.current) {
610
+ surfaceObserver.observe(el);
611
+ }
612
+ metricsRef.current.scanTime = performance.now() - scanStart;
606
613
  };
607
614
  initAccumulationWrapper();
608
615
  setIsVisible(true);
609
616
  let lastTime = 0;
617
+ let lastMetricsUpdate = 0;
618
+ let elementRects = [];
610
619
  const animate = (currentTime) => {
611
620
  if (lastTime === 0) {
612
621
  lastTime = currentTime;
@@ -614,23 +623,64 @@ function Snowfall() {
614
623
  return;
615
624
  }
616
625
  const deltaTime = Math.min(currentTime - lastTime, 50);
626
+ const now = performance.now();
627
+ fpsFrames.current.push(now);
628
+ fpsFrames.current = fpsFrames.current.filter((t) => now - t < 1e3);
629
+ metricsRef.current.rafGap = currentTime - lastTime;
617
630
  lastTime = currentTime;
618
631
  const dt = deltaTime / 16.67;
619
- ctx.clearRect(0, 0, canvas.width, canvas.height);
620
- fixedCtx.clearRect(0, 0, fixedCanvas.width, fixedCanvas.height);
632
+ const frameStartTime = performance.now();
633
+ const clearStart = performance.now();
634
+ const dpr = window.devicePixelRatio || 1;
635
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
636
+ ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
637
+ const scrollX = window.scrollX;
638
+ const scrollY = window.scrollY;
639
+ ctx.translate(-scrollX, -scrollY);
640
+ metricsRef.current.clearTime = performance.now() - clearStart;
621
641
  const snowflakes = snowflakesRef.current;
622
- const elementRects = getElementRects(accumulationRef.current);
642
+ const rectStart = performance.now();
643
+ elementRects = getElementRects(accumulationRef.current);
644
+ metricsRef.current.rectUpdateTime = performance.now() - rectStart;
645
+ const physicsStart = performance.now();
623
646
  meltAndSmoothAccumulation(elementRects, physicsConfigRef.current, dt);
624
- updateSnowflakes(snowflakes, elementRects, physicsConfigRef.current, dt, canvas.width, canvas.height);
647
+ updateSnowflakes(
648
+ snowflakes,
649
+ elementRects,
650
+ physicsConfigRef.current,
651
+ dt,
652
+ document.documentElement.scrollWidth,
653
+ document.documentElement.scrollHeight
654
+ );
655
+ metricsRef.current.physicsTime = performance.now() - physicsStart;
656
+ const drawStart = performance.now();
625
657
  for (const flake of snowflakes) {
626
658
  drawSnowflake(ctx, flake);
627
659
  }
628
660
  if (isEnabledRef.current && snowflakes.length < physicsConfigRef.current.MAX_FLAKES) {
629
661
  const isBackground = Math.random() < 0.4;
630
- snowflakes.push(createSnowflake(canvas.width, physicsConfigRef.current, isBackground));
662
+ snowflakes.push(createSnowflake(document.documentElement.scrollWidth, physicsConfigRef.current, isBackground));
663
+ }
664
+ drawAccumulations(ctx, elementRects);
665
+ drawSideAccumulations(ctx, elementRects);
666
+ metricsRef.current.drawTime = performance.now() - drawStart;
667
+ metricsRef.current.frameTime = performance.now() - frameStartTime;
668
+ if (currentTime - lastMetricsUpdate > 500) {
669
+ setMetricsRef.current({
670
+ fps: fpsFrames.current.length,
671
+ frameTime: metricsRef.current.frameTime,
672
+ scanTime: metricsRef.current.scanTime,
673
+ rectUpdateTime: metricsRef.current.rectUpdateTime,
674
+ surfaceCount: accumulationRef.current.size,
675
+ flakeCount: snowflakes.length,
676
+ maxFlakes: physicsConfigRef.current.MAX_FLAKES,
677
+ rafGap: metricsRef.current.rafGap,
678
+ clearTime: metricsRef.current.clearTime,
679
+ physicsTime: metricsRef.current.physicsTime,
680
+ drawTime: metricsRef.current.drawTime
681
+ });
682
+ lastMetricsUpdate = currentTime;
631
683
  }
632
- drawAccumulations(ctx, fixedCtx, elementRects);
633
- drawSideAccumulations(ctx, fixedCtx, elementRects);
634
684
  animationIdRef.current = requestAnimationFrame(animate);
635
685
  };
636
686
  animationIdRef.current = requestAnimationFrame(animate);
@@ -640,53 +690,267 @@ function Snowfall() {
640
690
  initAccumulationWrapper();
641
691
  };
642
692
  window.addEventListener("resize", handleResize);
643
- const checkInterval = setInterval(initAccumulationWrapper, 3e3);
693
+ const checkInterval = setInterval(initAccumulationWrapper, 5e3);
644
694
  return () => {
645
695
  cancelAnimationFrame(animationIdRef.current);
646
696
  window.removeEventListener("resize", handleResize);
647
697
  clearInterval(checkInterval);
648
- resizeObserver.disconnect();
698
+ windowResizeObserver.disconnect();
699
+ surfaceObserver.disconnect();
649
700
  };
650
701
  }, [isMounted]);
651
702
  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"
703
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
704
+ "canvas",
705
+ {
706
+ ref: canvasRef,
707
+ style: {
708
+ position: "fixed",
709
+ // FIXED position to eliminate scroll jitter
710
+ top: 0,
711
+ left: 0,
712
+ pointerEvents: "none",
713
+ zIndex: 9999,
714
+ opacity: isVisible ? 1 : 0,
715
+ transition: "opacity 0.3s ease-in",
716
+ willChange: "transform"
717
+ },
718
+ "aria-hidden": "true"
719
+ }
720
+ ) });
721
+ }
722
+
723
+ // src/DebugPanel.tsx
724
+ var import_react3 = require("react");
725
+ var import_jsx_runtime3 = require("react/jsx-runtime");
726
+ function DebugPanel({ defaultOpen = true }) {
727
+ const { debugMode, toggleDebug, metrics } = useSnowfall();
728
+ const [isMinimized, setIsMinimized] = (0, import_react3.useState)(!defaultOpen);
729
+ const [copied, setCopied] = (0, import_react3.useState)(false);
730
+ (0, import_react3.useEffect)(() => {
731
+ const handleKeyDown = (e) => {
732
+ if (e.shiftKey && e.key === "D") {
733
+ toggleDebug();
683
734
  }
684
- )
685
- ] });
735
+ };
736
+ window.addEventListener("keydown", handleKeyDown);
737
+ return () => window.removeEventListener("keydown", handleKeyDown);
738
+ }, [toggleDebug]);
739
+ const copyToClipboard = () => {
740
+ if (metrics) {
741
+ const data = {
742
+ ...metrics,
743
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
744
+ userAgent: navigator.userAgent,
745
+ canvasSize: {
746
+ width: window.innerWidth,
747
+ height: window.innerHeight
748
+ }
749
+ };
750
+ navigator.clipboard.writeText(JSON.stringify(data, null, 2));
751
+ setCopied(true);
752
+ setTimeout(() => setCopied(false), 2e3);
753
+ }
754
+ };
755
+ if (!debugMode) return null;
756
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
757
+ "div",
758
+ {
759
+ "data-snowfall": "top",
760
+ style: {
761
+ position: "fixed",
762
+ bottom: "80px",
763
+ left: "24px",
764
+ backgroundColor: "rgba(15, 23, 42, 0.75)",
765
+ backdropFilter: "blur(16px)",
766
+ WebkitBackdropFilter: "blur(16px)",
767
+ color: "#e2e8f0",
768
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
769
+ fontSize: "12px",
770
+ padding: isMinimized ? "12px" : "20px",
771
+ borderRadius: "16px",
772
+ zIndex: 1e4,
773
+ minWidth: isMinimized ? "auto" : "300px",
774
+ maxWidth: "100%",
775
+ border: "1px solid rgba(255, 255, 255, 0.1)",
776
+ boxShadow: "0 20px 25px -5px rgba(0, 0, 0, 0.2), 0 8px 10px -6px rgba(0, 0, 0, 0.2)",
777
+ transition: "all 0.2s ease"
778
+ },
779
+ children: [
780
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: {
781
+ display: "flex",
782
+ justifyContent: "space-between",
783
+ alignItems: "center",
784
+ marginBottom: isMinimized ? 0 : "16px",
785
+ gap: "16px"
786
+ }, children: [
787
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { fontWeight: "600", color: "#fff", display: "flex", alignItems: "center", gap: "8px" }, children: [
788
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontSize: "14px" }, children: "\u2744\uFE0F" }),
789
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: {
790
+ background: "linear-gradient(to right, #60a5fa, #22d3ee)",
791
+ WebkitBackgroundClip: "text",
792
+ WebkitTextFillColor: "transparent",
793
+ fontWeight: "700"
794
+ }, children: "DEBUG" })
795
+ ] }),
796
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
797
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
798
+ "button",
799
+ {
800
+ onClick: () => setIsMinimized(!isMinimized),
801
+ style: {
802
+ background: "rgba(255, 255, 255, 0.1)",
803
+ border: "1px solid rgba(255, 255, 255, 0.1)",
804
+ color: "#fff",
805
+ cursor: "pointer",
806
+ width: "24px",
807
+ height: "24px",
808
+ display: "flex",
809
+ alignItems: "center",
810
+ justifyContent: "center",
811
+ borderRadius: "6px",
812
+ fontSize: "10px"
813
+ },
814
+ children: isMinimized ? "\u25B2" : "\u25BC"
815
+ }
816
+ ),
817
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
818
+ "button",
819
+ {
820
+ onClick: toggleDebug,
821
+ style: {
822
+ background: "rgba(239, 68, 68, 0.15)",
823
+ border: "1px solid rgba(239, 68, 68, 0.2)",
824
+ color: "#f87171",
825
+ cursor: "pointer",
826
+ width: "24px",
827
+ height: "24px",
828
+ display: "flex",
829
+ alignItems: "center",
830
+ justifyContent: "center",
831
+ borderRadius: "6px",
832
+ fontSize: "12px"
833
+ },
834
+ children: "\u2715"
835
+ }
836
+ )
837
+ ] })
838
+ ] }),
839
+ !isMinimized && metrics && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
840
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { marginBottom: "12px", paddingBottom: "12px", borderBottom: "1px solid rgba(255,255,255,0.08)" }, children: [
841
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { color: "#94a3b8", marginBottom: "8px", fontSize: "11px", fontWeight: "600", letterSpacing: "0.5px", textTransform: "uppercase" }, children: "Performance" }),
842
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", justifyContent: "space-between", marginBottom: "4px" }, children: [
843
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "FPS" }),
844
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontWeight: "bold", color: metrics.fps < 30 ? "#f87171" : metrics.fps < 50 ? "#facc15" : "#4ade80" }, children: metrics.fps.toFixed(1) })
845
+ ] }),
846
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", justifyContent: "space-between", marginBottom: "4px" }, children: [
847
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "Frame Time" }),
848
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { fontFamily: "monospace" }, children: [
849
+ metrics.frameTime.toFixed(2),
850
+ "ms"
851
+ ] })
852
+ ] }),
853
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", justifyContent: "space-between" }, children: [
854
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { color: metrics.rafGap && metrics.rafGap > 20 ? "#fbbf24" : "inherit" }, children: "rAF Gap" }),
855
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { fontFamily: "monospace" }, children: [
856
+ metrics.rafGap?.toFixed(1) || 0,
857
+ "ms"
858
+ ] })
859
+ ] })
860
+ ] }),
861
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { marginBottom: "12px", paddingBottom: "12px", borderBottom: "1px solid rgba(255,255,255,0.08)" }, children: [
862
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { color: "#94a3b8", marginBottom: "8px", fontSize: "11px", fontWeight: "600", letterSpacing: "0.5px", textTransform: "uppercase" }, children: "Detailed Timings" }),
863
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: "4px 12px" }, children: [
864
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", justifyContent: "space-between" }, children: [
865
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "Clear" }),
866
+ " ",
867
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { fontFamily: "monospace" }, children: [
868
+ metrics.clearTime?.toFixed(2) || 0,
869
+ "ms"
870
+ ] })
871
+ ] }),
872
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", justifyContent: "space-between" }, children: [
873
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "Physics" }),
874
+ " ",
875
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { fontFamily: "monospace" }, children: [
876
+ metrics.physicsTime?.toFixed(2) || 0,
877
+ "ms"
878
+ ] })
879
+ ] }),
880
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", justifyContent: "space-between" }, children: [
881
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "Draw" }),
882
+ " ",
883
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { fontFamily: "monospace" }, children: [
884
+ metrics.drawTime?.toFixed(2) || 0,
885
+ "ms"
886
+ ] })
887
+ ] }),
888
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", justifyContent: "space-between" }, children: [
889
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "Scan" }),
890
+ " ",
891
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { fontFamily: "monospace" }, children: [
892
+ metrics.scanTime.toFixed(2),
893
+ "ms"
894
+ ] })
895
+ ] }),
896
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", justifyContent: "space-between", gridColumn: "span 2" }, children: [
897
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "Rect Update" }),
898
+ " ",
899
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { fontFamily: "monospace" }, children: [
900
+ metrics.rectUpdateTime.toFixed(2),
901
+ "ms"
902
+ ] })
903
+ ] })
904
+ ] })
905
+ ] }),
906
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { marginBottom: "16px" }, children: [
907
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { color: "#94a3b8", marginBottom: "8px", fontSize: "11px", fontWeight: "600", letterSpacing: "0.5px", textTransform: "uppercase" }, children: "Counts" }),
908
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", justifyContent: "space-between", marginBottom: "4px" }, children: [
909
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "Snowflakes" }),
910
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { fontFamily: "monospace" }, children: [
911
+ metrics.flakeCount,
912
+ " / ",
913
+ metrics.maxFlakes
914
+ ] })
915
+ ] }),
916
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", justifyContent: "space-between" }, children: [
917
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "Surfaces" }),
918
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontFamily: "monospace" }, children: metrics.surfaceCount })
919
+ ] })
920
+ ] }),
921
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
922
+ "button",
923
+ {
924
+ onClick: copyToClipboard,
925
+ style: {
926
+ width: "100%",
927
+ padding: "10px",
928
+ background: copied ? "rgba(34, 197, 94, 0.2)" : "rgba(255, 255, 255, 0.05)",
929
+ border: copied ? "1px solid rgba(34, 197, 94, 0.5)" : "1px solid rgba(255, 255, 255, 0.1)",
930
+ color: copied ? "#4ade80" : "#fff",
931
+ cursor: "pointer",
932
+ borderRadius: "8px",
933
+ fontSize: "11px",
934
+ fontWeight: "600",
935
+ transition: "all 0.2s",
936
+ display: "flex",
937
+ alignItems: "center",
938
+ justifyContent: "center",
939
+ gap: "6px"
940
+ },
941
+ children: copied ? "\u2713 COPIED" : "\u{1F4CB} COPY METRICS"
942
+ }
943
+ ),
944
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { marginTop: "12px", fontSize: "10px", color: "#64748b", textAlign: "center" }, children: "Shift+D to toggle" })
945
+ ] })
946
+ ]
947
+ }
948
+ );
686
949
  }
687
950
  // Annotate the CommonJS export names for ESM import in node:
688
951
  0 && (module.exports = {
689
952
  DEFAULT_PHYSICS,
953
+ DebugPanel,
690
954
  Snowfall,
691
955
  SnowfallProvider,
692
956
  useSnowfall