@hdcodedev/snowfall 1.0.5 → 1.0.7

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
@@ -52,13 +52,14 @@ var DEFAULT_PHYSICS = {
52
52
  FLAKE_SIZE: {
53
53
  MIN: 0.5,
54
54
  MAX: 1.6
55
- }
55
+ },
56
+ MAX_SURFACES: 15
56
57
  };
57
58
  var SnowfallContext = (0, import_react.createContext)(void 0);
58
- function SnowfallProvider({ children }) {
59
+ function SnowfallProvider({ children, initialDebug = false }) {
59
60
  const [isEnabled, setIsEnabled] = (0, import_react.useState)(true);
60
61
  const [physicsConfig, setPhysicsConfig] = (0, import_react.useState)(DEFAULT_PHYSICS);
61
- const [debugMode, setDebugMode] = (0, import_react.useState)(false);
62
+ const [debugMode, setDebugMode] = (0, import_react.useState)(initialDebug);
62
63
  const [metrics, setMetrics] = (0, import_react.useState)(null);
63
64
  const toggleSnow = () => {
64
65
  setIsEnabled((prev) => !prev);
@@ -113,13 +114,16 @@ var VAL_IGNORE = "ignore";
113
114
  var VAL_TOP = "top";
114
115
  var VAL_BOTTOM = "bottom";
115
116
  var TAG_HEADER = "header";
117
+ var TAG_FOOTER = "footer";
116
118
  var ROLE_BANNER = "banner";
119
+ var ROLE_CONTENTINFO = "contentinfo";
120
+ var TAU = Math.PI * 2;
117
121
 
118
122
  // src/utils/snowfall/dom.ts
119
123
  var BOTTOM_TAGS = [TAG_HEADER];
120
124
  var BOTTOM_ROLES = [ROLE_BANNER];
121
- var AUTO_DETECT_TAGS = ["header", "footer", "article", "section", "aside", "nav"];
122
- var AUTO_DETECT_ROLES = ['[role="banner"]', '[role="contentinfo"]', '[role="main"]'];
125
+ var AUTO_DETECT_TAGS = [TAG_HEADER, TAG_FOOTER, "article", "section", "aside", "nav"];
126
+ var AUTO_DETECT_ROLES = [`[role="${ROLE_BANNER}"]`, `[role="${ROLE_CONTENTINFO}"]`, '[role="main"]'];
123
127
  var AUTO_DETECT_CLASSES = [
124
128
  ".card",
125
129
  '[class*="card"]',
@@ -128,18 +132,6 @@ var AUTO_DETECT_CLASSES = [
128
132
  '[class*="shadow-"]',
129
133
  '[class*="rounded-"]'
130
134
  ];
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
- };
143
135
  var getElementType = (el) => {
144
136
  const tagName = el.tagName.toLowerCase();
145
137
  if (BOTTOM_TAGS.includes(tagName)) return VAL_BOTTOM;
@@ -147,22 +139,21 @@ var getElementType = (el) => {
147
139
  if (role && BOTTOM_ROLES.includes(role)) return VAL_BOTTOM;
148
140
  return VAL_TOP;
149
141
  };
150
- var shouldAccumulate = (el) => {
142
+ var shouldAccumulate = (el, precomputedStyle) => {
151
143
  if (el.getAttribute(ATTR_SNOWFALL) === VAL_IGNORE) return false;
152
144
  if (el.hasAttribute(ATTR_SNOWFALL)) return true;
153
- const styles = window.getComputedStyle(el);
154
- const rect = el.getBoundingClientRect();
145
+ const styles = precomputedStyle || window.getComputedStyle(el);
155
146
  const isVisible = styles.display !== "none" && styles.visibility !== "hidden" && parseFloat(styles.opacity) > 0.1;
156
147
  if (!isVisible) return false;
157
148
  const bgColor = styles.backgroundColor;
158
149
  const hasBackground = bgColor !== "rgba(0, 0, 0, 0)" && bgColor !== "transparent";
159
- const hasBorder = parseFloat(styles.borderWidth) > 0 || styles.borderStyle !== "none";
150
+ const hasBorder = parseFloat(styles.borderWidth) > 0 && styles.borderColor !== "transparent" && styles.borderColor !== "rgba(0, 0, 0, 0)" && styles.borderStyle !== "none";
160
151
  const hasBoxShadow = styles.boxShadow !== "none";
161
- const hasBorderRadius = parseFloat(styles.borderRadius) > 0;
162
- return hasBackground || hasBorder || hasBoxShadow || hasBorderRadius;
152
+ const hasFilter = styles.filter !== "none" && styles.filter.includes("drop-shadow");
153
+ const hasBackdropFilter = styles.backdropFilter !== "none";
154
+ return hasBackground || hasBorder || hasBoxShadow || hasFilter || hasBackdropFilter;
163
155
  };
164
- var getAccumulationSurfaces = () => {
165
- clearStyleCache();
156
+ var getAccumulationSurfaces = (maxSurfaces = 5) => {
166
157
  const surfaces = [];
167
158
  const seen = /* @__PURE__ */ new Set();
168
159
  const candidates = document.querySelectorAll(
@@ -173,45 +164,31 @@ var getAccumulationSurfaces = () => {
173
164
  ...AUTO_DETECT_CLASSES
174
165
  ].join(", ")
175
166
  );
176
- candidates.forEach((el) => {
177
- if (seen.has(el)) return;
167
+ for (const el of candidates) {
168
+ if (surfaces.length >= maxSurfaces) break;
169
+ if (seen.has(el)) continue;
178
170
  const manualOverride = el.getAttribute(ATTR_SNOWFALL);
179
- if (manualOverride === VAL_IGNORE) return;
171
+ if (manualOverride === VAL_IGNORE) continue;
180
172
  const isManuallyIncluded = manualOverride !== null;
181
- const styles = getCachedStyle(el);
173
+ const styles = window.getComputedStyle(el);
182
174
  const isVisible = styles.display !== "none" && styles.visibility !== "hidden" && parseFloat(styles.opacity) > 0.1;
183
- if (!isVisible && !isManuallyIncluded) return;
175
+ if (!isVisible && !isManuallyIncluded) continue;
184
176
  const rect = el.getBoundingClientRect();
185
177
  const hasSize = rect.width >= 100 && rect.height >= 50;
186
- if (!hasSize && !isManuallyIncluded) return;
178
+ if (!hasSize && !isManuallyIncluded) continue;
187
179
  const isFullPageWrapper = rect.top <= 10 && rect.height >= window.innerHeight * 0.9;
188
180
  const isBottomTag = BOTTOM_TAGS.includes(el.tagName.toLowerCase());
189
181
  const isBottomRole = BOTTOM_ROLES.includes(el.getAttribute("role") || "");
190
182
  const isBottomSurface = isBottomTag || isBottomRole || manualOverride === VAL_BOTTOM;
191
- if (isFullPageWrapper && !isBottomSurface && !isManuallyIncluded) return;
192
- let isFixed = false;
193
- let currentEl = el;
194
- while (currentEl && currentEl !== document.body) {
195
- const style = getCachedStyle(currentEl);
196
- if (style.position === "fixed" || style.position === "sticky") {
197
- isFixed = true;
198
- break;
199
- }
200
- currentEl = currentEl.parentElement;
201
- }
202
- if (shouldAccumulate(el)) {
183
+ if (isFullPageWrapper && !isBottomSurface && !isManuallyIncluded) continue;
184
+ if (shouldAccumulate(el, styles)) {
203
185
  let type = getElementType(el);
204
- if (manualOverride === VAL_BOTTOM) {
205
- type = VAL_BOTTOM;
206
- } else if (manualOverride === VAL_TOP) {
207
- type = VAL_TOP;
208
- }
209
- surfaces.push({ el, type, isFixed });
186
+ if (manualOverride === VAL_BOTTOM) type = VAL_BOTTOM;
187
+ else if (manualOverride === VAL_TOP) type = VAL_TOP;
188
+ surfaces.push({ el, type });
210
189
  seen.add(el);
211
190
  }
212
- });
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
192
  return surfaces;
216
193
  };
217
194
  var getElementRects = (accumulationMap) => {
@@ -219,76 +196,128 @@ var getElementRects = (accumulationMap) => {
219
196
  for (const [el, acc] of accumulationMap.entries()) {
220
197
  if (!el.isConnected) continue;
221
198
  const rect = el.getBoundingClientRect();
222
- const absoluteRect = {
223
- left: rect.left + window.scrollX,
224
- right: rect.right + window.scrollX,
225
- top: rect.top + window.scrollY,
226
- bottom: rect.bottom + window.scrollY,
227
- width: rect.width,
228
- height: rect.height,
229
- x: rect.x,
230
- // Note: these are strictly viewport relative in DOMRect usually,
231
- // but we just need consistent absolute coords for physics
232
- y: rect.y,
233
- toJSON: rect.toJSON
234
- };
235
- elementRects.push({ el, rect: absoluteRect, acc });
199
+ elementRects.push({ el, rect, acc });
236
200
  }
237
201
  return elementRects;
238
202
  };
239
203
 
240
204
  // src/utils/snowfall/physics.ts
241
- var createSnowflake = (canvasWidth, config, isBackground = false) => {
242
- if (isBackground) {
243
- const sizeRatio = Math.random();
244
- const radius = config.FLAKE_SIZE.MIN * 0.6 + sizeRatio * (config.FLAKE_SIZE.MAX - config.FLAKE_SIZE.MIN) * 0.4;
245
- return {
246
- x: Math.random() * canvasWidth,
247
- y: window.scrollY - 5,
248
- radius,
249
- speed: radius * 0.3 + Math.random() * 0.2 + 0.2,
250
- wind: (Math.random() - 0.5) * (config.WIND_STRENGTH * 0.625),
251
- opacity: Math.random() * 0.2 + 0.2,
252
- wobble: Math.random() * Math.PI * 2,
253
- wobbleSpeed: Math.random() * 0.015 + 5e-3,
254
- sizeRatio,
255
- isBackground: true
256
- };
257
- } else {
258
- const sizeRatio = Math.random();
259
- const radius = config.FLAKE_SIZE.MIN + sizeRatio * (config.FLAKE_SIZE.MAX - config.FLAKE_SIZE.MIN);
260
- return {
261
- x: Math.random() * canvasWidth,
262
- y: window.scrollY - 5,
263
- radius,
264
- speed: radius * 0.5 + Math.random() * 0.3 + 0.5,
265
- wind: (Math.random() - 0.5) * config.WIND_STRENGTH,
266
- opacity: Math.random() * 0.3 + 0.5,
267
- wobble: Math.random() * Math.PI * 2,
268
- wobbleSpeed: Math.random() * 0.02 + 0.01,
269
- sizeRatio,
270
- isBackground: false
271
- };
205
+ var createSnowflake = (worldWidth, config, isBackground = false) => {
206
+ const x = Math.random() * worldWidth;
207
+ const dna = Math.random();
208
+ const noise = {
209
+ speed: dna * 13 % 1,
210
+ wind: dna * 7 % 1,
211
+ wobblePhase: dna * 23 % 1,
212
+ wobbleSpeed: dna * 5 % 1
213
+ };
214
+ const { MIN, MAX } = config.FLAKE_SIZE;
215
+ const sizeRatio = dna;
216
+ const profile = isBackground ? {
217
+ sizeMin: MIN * 0.6,
218
+ sizeRange: (MAX - MIN) * 0.4,
219
+ speedBase: 0.2,
220
+ speedScale: 0.3,
221
+ noiseSpeedScale: 0.2,
222
+ windScale: config.WIND_STRENGTH * 0.625,
223
+ opacityBase: 0.2,
224
+ opacityScale: 0.2,
225
+ wobbleBase: 5e-3,
226
+ wobbleScale: 0.015
227
+ } : {
228
+ sizeMin: MIN,
229
+ sizeRange: MAX - MIN,
230
+ speedBase: 0.5,
231
+ speedScale: 0.5,
232
+ noiseSpeedScale: 0.3,
233
+ windScale: config.WIND_STRENGTH,
234
+ opacityBase: 0.5,
235
+ opacityScale: 0.3,
236
+ wobbleBase: 0.01,
237
+ wobbleScale: 0.02
238
+ };
239
+ const radius = profile.sizeMin + sizeRatio * profile.sizeRange;
240
+ return {
241
+ x,
242
+ y: window.scrollY - 5,
243
+ radius,
244
+ speed: radius * profile.speedScale + noise.speed * profile.noiseSpeedScale + profile.speedBase,
245
+ wind: (noise.wind - 0.5) * profile.windScale,
246
+ opacity: profile.opacityBase + sizeRatio * profile.opacityScale,
247
+ wobble: noise.wobblePhase * TAU,
248
+ wobbleSpeed: noise.wobbleSpeed * profile.wobbleScale + profile.wobbleBase,
249
+ sizeRatio,
250
+ isBackground
251
+ };
252
+ };
253
+ var initializeMaxHeights = (width, baseMax, borderRadius, isBottom = false) => {
254
+ let maxHeights = new Array(width);
255
+ for (let i = 0; i < width; i++) {
256
+ let edgeFactor = 1;
257
+ if (!isBottom && borderRadius > 0) {
258
+ if (i < borderRadius) {
259
+ edgeFactor = Math.pow(i / borderRadius, 1.2);
260
+ } else if (i > width - borderRadius) {
261
+ edgeFactor = Math.pow((width - i) / borderRadius, 1.2);
262
+ }
263
+ }
264
+ maxHeights[i] = baseMax * edgeFactor * (0.85 + Math.random() * 0.15);
265
+ }
266
+ const smoothPasses = 4;
267
+ for (let p = 0; p < smoothPasses; p++) {
268
+ const smoothed = [...maxHeights];
269
+ for (let i = 1; i < width - 1; i++) {
270
+ smoothed[i] = (maxHeights[i - 1] + maxHeights[i] + maxHeights[i + 1]) / 3;
271
+ }
272
+ maxHeights = smoothed;
273
+ }
274
+ return maxHeights;
275
+ };
276
+ var calculateCurveOffsets = (width, borderRadius, isBottom) => {
277
+ const offsets = new Array(width).fill(0);
278
+ if (borderRadius <= 0 || isBottom) return offsets;
279
+ for (let x = 0; x < width; x++) {
280
+ let offset = 0;
281
+ if (x < borderRadius) {
282
+ const dist = borderRadius - x;
283
+ offset = borderRadius - Math.sqrt(Math.max(0, borderRadius * borderRadius - dist * dist));
284
+ } else if (x > width - borderRadius) {
285
+ const dist = x - (width - borderRadius);
286
+ offset = borderRadius - Math.sqrt(Math.max(0, borderRadius * borderRadius - dist * dist));
287
+ }
288
+ offsets[x] = offset;
289
+ }
290
+ return offsets;
291
+ };
292
+ var calculateGravityMultipliers = (height) => {
293
+ const multipliers = new Array(height);
294
+ for (let i = 0; i < height; i++) {
295
+ const ratio = i / height;
296
+ multipliers[i] = Math.sqrt(ratio);
272
297
  }
298
+ return multipliers;
273
299
  };
274
300
  var initializeAccumulation = (accumulationMap, config) => {
275
- const elements = getAccumulationSurfaces();
301
+ const elements = getAccumulationSurfaces(config.MAX_SURFACES);
276
302
  for (const [el] of accumulationMap.entries()) {
277
303
  if (!el.isConnected) {
278
304
  accumulationMap.delete(el);
279
305
  }
280
306
  }
281
- elements.forEach(({ el, type, isFixed }) => {
307
+ elements.forEach(({ el, type }) => {
282
308
  const existing = accumulationMap.get(el);
283
309
  const rect = el.getBoundingClientRect();
284
310
  const width = Math.ceil(rect.width);
285
311
  const isBottom = type === VAL_BOTTOM;
286
312
  if (existing && existing.heights.length === width) {
287
313
  existing.type = type;
288
- existing.isFixed = isFixed;
289
314
  if (existing.borderRadius !== void 0) {
290
315
  const styleBuffer = window.getComputedStyle(el);
291
316
  existing.borderRadius = parseFloat(styleBuffer.borderTopLeftRadius) || 0;
317
+ existing.curveOffsets = calculateCurveOffsets(width, existing.borderRadius, isBottom);
318
+ if (existing.leftSide.length === Math.ceil(rect.height) && !existing.sideGravityMultipliers) {
319
+ existing.sideGravityMultipliers = calculateGravityMultipliers(existing.leftSide.length);
320
+ }
292
321
  }
293
322
  return;
294
323
  }
@@ -296,26 +325,7 @@ var initializeAccumulation = (accumulationMap, config) => {
296
325
  const baseMax = isBottom ? config.MAX_DEPTH.BOTTOM : config.MAX_DEPTH.TOP;
297
326
  const styles = window.getComputedStyle(el);
298
327
  const borderRadius = parseFloat(styles.borderTopLeftRadius) || 0;
299
- let maxHeights = new Array(width);
300
- for (let i = 0; i < width; i++) {
301
- let edgeFactor = 1;
302
- if (!isBottom && borderRadius > 0) {
303
- if (i < borderRadius) {
304
- edgeFactor = Math.pow(i / borderRadius, 1.2);
305
- } else if (i > width - borderRadius) {
306
- edgeFactor = Math.pow((width - i) / borderRadius, 1.2);
307
- }
308
- }
309
- maxHeights[i] = baseMax * edgeFactor * (0.85 + Math.random() * 0.15);
310
- }
311
- const smoothPasses = 4;
312
- for (let p = 0; p < smoothPasses; p++) {
313
- const smoothed = [...maxHeights];
314
- for (let i = 1; i < width - 1; i++) {
315
- smoothed[i] = (maxHeights[i - 1] + maxHeights[i] + maxHeights[i + 1]) / 3;
316
- }
317
- maxHeights = smoothed;
318
- }
328
+ const maxHeights = initializeMaxHeights(width, baseMax, borderRadius, isBottom);
319
329
  accumulationMap.set(el, {
320
330
  heights: existing?.heights.length === width ? existing.heights : new Array(width).fill(0),
321
331
  maxHeights,
@@ -323,8 +333,9 @@ var initializeAccumulation = (accumulationMap, config) => {
323
333
  rightSide: existing?.rightSide.length === height ? existing.rightSide : new Array(height).fill(0),
324
334
  maxSideHeight: isBottom ? 0 : config.MAX_DEPTH.SIDE,
325
335
  borderRadius,
326
- type,
327
- isFixed
336
+ curveOffsets: calculateCurveOffsets(width, borderRadius, isBottom),
337
+ sideGravityMultipliers: calculateGravityMultipliers(height),
338
+ type
328
339
  });
329
340
  });
330
341
  };
@@ -343,30 +354,35 @@ var accumulateSide = (sideArray, rectHeight, localY, maxSideHeight, borderRadius
343
354
  }
344
355
  }
345
356
  };
346
- var updateSnowflakes = (snowflakes, elementRects, config, dt, canvasWidth, canvasHeight) => {
357
+ var updateSnowflakes = (snowflakes, elementRects, config, dt, worldWidth, worldHeight) => {
358
+ const scrollX = window.scrollX;
359
+ const scrollY = window.scrollY;
347
360
  for (let i = snowflakes.length - 1; i >= 0; i--) {
348
361
  const flake = snowflakes[i];
349
362
  flake.wobble += flake.wobbleSpeed * dt;
350
363
  flake.x += (flake.wind + Math.sin(flake.wobble) * 0.5) * dt;
351
364
  flake.y += (flake.speed + Math.cos(flake.wobble * 0.5) * 0.1) * dt;
352
365
  let landed = false;
353
- for (const { rect, acc } of elementRects) {
366
+ for (const item of elementRects) {
367
+ const { rect, acc } = item;
354
368
  const isBottom = acc.type === VAL_BOTTOM;
369
+ const flakeViewportX = flake.x - scrollX;
370
+ const flakeViewportY = flake.y - scrollY;
371
+ const isInVerticalBounds = flakeViewportY >= rect.top && flakeViewportY <= rect.bottom;
355
372
  if (!landed && acc.maxSideHeight > 0 && !isBottom) {
356
- const isInVerticalBounds = flake.y >= rect.top && flake.y <= rect.bottom;
357
373
  if (isInVerticalBounds) {
358
- const localY = Math.floor(flake.y - rect.top);
374
+ const localY = Math.floor(flakeViewportY - rect.top);
359
375
  const borderRadius = acc.borderRadius;
360
376
  const isInTopCorner = localY < borderRadius;
361
377
  const isInBottomCorner = localY > rect.height - borderRadius;
362
378
  const isCorner = borderRadius > 0 && (isInTopCorner || isInBottomCorner);
363
- if (flake.x >= rect.left - 5 && flake.x < rect.left + 3) {
379
+ if (flakeViewportX >= rect.left - 5 && flakeViewportX < rect.left + 3) {
364
380
  if (!isCorner) {
365
381
  accumulateSide(acc.leftSide, rect.height, localY, acc.maxSideHeight, borderRadius, config);
366
382
  landed = true;
367
383
  }
368
384
  }
369
- if (!landed && flake.x > rect.right - 3 && flake.x <= rect.right + 5) {
385
+ if (!landed && flakeViewportX > rect.right - 3 && flakeViewportX <= rect.right + 5) {
370
386
  if (!isCorner) {
371
387
  accumulateSide(acc.rightSide, rect.height, localY, acc.maxSideHeight, borderRadius, config);
372
388
  landed = true;
@@ -375,12 +391,12 @@ var updateSnowflakes = (snowflakes, elementRects, config, dt, canvasWidth, canva
375
391
  if (landed) break;
376
392
  }
377
393
  }
378
- if (flake.x >= rect.left && flake.x <= rect.right) {
379
- const localX = Math.floor(flake.x - rect.left);
394
+ if (flakeViewportX >= rect.left && flakeViewportX <= rect.right) {
395
+ const localX = Math.floor(flakeViewportX - rect.left);
380
396
  const currentHeight = acc.heights[localX] || 0;
381
397
  const maxHeight = acc.maxHeights[localX] || 5;
382
398
  const surfaceY = isBottom ? rect.bottom - currentHeight : rect.top - currentHeight;
383
- if (flake.y >= surfaceY && flake.y < surfaceY + 10 && currentHeight < maxHeight) {
399
+ if (flakeViewportY >= surfaceY && flakeViewportY < surfaceY + 10 && currentHeight < maxHeight) {
384
400
  const shouldAccumulate2 = isBottom ? Math.random() < 0.15 : true;
385
401
  if (shouldAccumulate2) {
386
402
  const baseSpread = Math.ceil(flake.radius);
@@ -415,7 +431,7 @@ var updateSnowflakes = (snowflakes, elementRects, config, dt, canvasWidth, canva
415
431
  }
416
432
  }
417
433
  }
418
- if (landed || flake.y > canvasHeight + 10 || flake.x < -20 || flake.x > canvasWidth + 20) {
434
+ if (landed || flake.y > worldHeight + 10 || flake.x < -20 || flake.x > worldWidth + 20) {
419
435
  snowflakes.splice(i, 1);
420
436
  }
421
437
  }
@@ -443,136 +459,116 @@ var meltAndSmoothAccumulation = (elementRects, config, dt) => {
443
459
  };
444
460
 
445
461
  // src/utils/snowfall/draw.ts
446
- var drawSnowflake = (ctx, flake) => {
447
- ctx.beginPath();
448
- ctx.arc(flake.x, flake.y, flake.radius, 0, Math.PI * 2);
449
- ctx.fillStyle = `rgba(255, 255, 255, ${flake.opacity})`;
450
- ctx.fill();
451
- ctx.beginPath();
452
- ctx.arc(flake.x, flake.y, flake.radius * 1.5, 0, Math.PI * 2);
453
- ctx.fillStyle = `rgba(255, 255, 255, ${flake.opacity * 0.2})`;
454
- ctx.fill();
462
+ var ACC_FILL_STYLE = "rgba(255, 255, 255, 0.95)";
463
+ var ACC_SHADOW_COLOR = "rgba(200, 230, 255, 0.6)";
464
+ var drawSnowflakes = (ctx, flakes) => {
465
+ if (flakes.length === 0) return;
466
+ ctx.fillStyle = "#FFFFFF";
467
+ for (const flake of flakes) {
468
+ ctx.globalAlpha = flake.opacity;
469
+ ctx.beginPath();
470
+ ctx.arc(flake.x, flake.y, flake.radius, 0, TAU);
471
+ ctx.fill();
472
+ }
473
+ for (const flake of flakes) {
474
+ if (flake.isBackground) continue;
475
+ ctx.globalAlpha = flake.opacity * 0.2;
476
+ ctx.beginPath();
477
+ ctx.arc(flake.x, flake.y, flake.radius * 1.5, 0, TAU);
478
+ ctx.fill();
479
+ }
480
+ ctx.globalAlpha = 1;
455
481
  };
456
- var drawAccumulations = (ctx, fixedCtx, elementRects) => {
457
- const setupCtx = (c) => {
458
- c.fillStyle = "rgba(255, 255, 255, 0.95)";
459
- c.shadowColor = "rgba(200, 230, 255, 0.6)";
460
- c.shadowBlur = 4;
461
- c.shadowOffsetY = -1;
462
- };
463
- setupCtx(ctx);
464
- if (fixedCtx) setupCtx(fixedCtx);
465
- const scrollX = window.scrollX;
466
- const scrollY = window.scrollY;
467
- for (const { rect, acc } of elementRects) {
482
+ var drawAccumulations = (ctx, elementRects) => {
483
+ ctx.fillStyle = ACC_FILL_STYLE;
484
+ ctx.shadowColor = ACC_SHADOW_COLOR;
485
+ ctx.shadowBlur = 4;
486
+ ctx.shadowOffsetY = -1;
487
+ ctx.globalAlpha = 1;
488
+ const currentScrollX = window.scrollX;
489
+ const currentScrollY = window.scrollY;
490
+ ctx.beginPath();
491
+ for (const item of elementRects) {
492
+ const { rect, acc } = item;
468
493
  if (!acc.heights.some((h) => h > 0.1)) continue;
469
- const useFixed = acc.isFixed && fixedCtx;
470
- const targetCtx = useFixed ? fixedCtx : ctx;
471
- const dx = useFixed ? -scrollX : 0;
472
- const dy = useFixed ? -scrollY : 0;
494
+ const dx = currentScrollX;
495
+ const dy = currentScrollY;
473
496
  const isBottom = acc.type === VAL_BOTTOM;
474
497
  const baseY = isBottom ? rect.bottom - 1 : rect.top + 1;
475
- const borderRadius = acc.borderRadius;
476
- const getCurveOffset = (xPos) => {
477
- if (borderRadius <= 0 || isBottom) return 0;
478
- let offset = 0;
479
- if (xPos < borderRadius) {
480
- const dist = borderRadius - xPos;
481
- offset = borderRadius - Math.sqrt(Math.max(0, borderRadius * borderRadius - dist * dist));
482
- } else if (xPos > rect.width - borderRadius) {
483
- const dist = xPos - (rect.width - borderRadius);
484
- offset = borderRadius - Math.sqrt(Math.max(0, borderRadius * borderRadius - dist * dist));
485
- }
486
- return offset;
487
- };
488
- targetCtx.beginPath();
489
498
  let first = true;
490
499
  const step = 2;
491
500
  const len = acc.heights.length;
492
501
  for (let x = 0; x < len; x += step) {
493
502
  const height = acc.heights[x] || 0;
494
503
  const px = rect.left + x + dx;
495
- const py = baseY - height + getCurveOffset(x) + dy;
504
+ const py = baseY - height + (acc.curveOffsets[x] || 0) + dy;
496
505
  if (first) {
497
- targetCtx.moveTo(px, py);
506
+ ctx.moveTo(px, py);
498
507
  first = false;
499
508
  } else {
500
- targetCtx.lineTo(px, py);
509
+ ctx.lineTo(px, py);
501
510
  }
502
511
  }
503
512
  if ((len - 1) % step !== 0) {
504
513
  const x = len - 1;
505
514
  const height = acc.heights[x] || 0;
506
515
  const px = rect.left + x + dx;
507
- const py = baseY - height + getCurveOffset(x) + dy;
508
- targetCtx.lineTo(px, py);
516
+ const py = baseY - height + (acc.curveOffsets[x] || 0) + dy;
517
+ ctx.lineTo(px, py);
509
518
  }
510
519
  for (let x = len - 1; x >= 0; x -= step) {
511
520
  const px = rect.left + x + dx;
512
- const py = baseY + getCurveOffset(x) + dy;
513
- targetCtx.lineTo(px, py);
521
+ const py = baseY + (acc.curveOffsets[x] || 0) + dy;
522
+ ctx.lineTo(px, py);
514
523
  }
515
524
  const startX = 0;
516
525
  const startPx = rect.left + startX + dx;
517
- const startPy = baseY + getCurveOffset(startX) + dy;
518
- targetCtx.lineTo(startPx, startPy);
519
- targetCtx.closePath();
520
- targetCtx.fill();
526
+ const startPy = baseY + (acc.curveOffsets[startX] || 0) + dy;
527
+ ctx.lineTo(startPx, startPy);
521
528
  }
529
+ ctx.fill();
522
530
  ctx.shadowBlur = 0;
523
531
  ctx.shadowOffsetY = 0;
524
- if (fixedCtx) {
525
- fixedCtx.shadowBlur = 0;
526
- fixedCtx.shadowOffsetY = 0;
527
- }
528
532
  };
529
- var drawSideAccumulations = (ctx, fixedCtx, elementRects) => {
530
- const setupCtx = (c) => {
531
- c.fillStyle = "rgba(255, 255, 255, 0.95)";
532
- c.shadowColor = "rgba(200, 230, 255, 0.6)";
533
- c.shadowBlur = 3;
533
+ var drawSideAccumulations = (ctx, elementRects) => {
534
+ ctx.fillStyle = ACC_FILL_STYLE;
535
+ ctx.shadowColor = ACC_SHADOW_COLOR;
536
+ ctx.shadowBlur = 3;
537
+ ctx.globalAlpha = 1;
538
+ const currentScrollX = window.scrollX;
539
+ const currentScrollY = window.scrollY;
540
+ ctx.beginPath();
541
+ const drawSide = (sideArray, isLeft, multipliers, rect, dx, dy) => {
542
+ const baseX = isLeft ? rect.left : rect.right;
543
+ ctx.moveTo(baseX + dx, rect.top + dy);
544
+ for (let y = 0; y < sideArray.length; y += 2) {
545
+ const width = sideArray[y] || 0;
546
+ const nextY = Math.min(y + 2, sideArray.length - 1);
547
+ const nextWidth = sideArray[nextY] || 0;
548
+ const gravityMultiplier = multipliers[y] || 0;
549
+ const py = rect.top + y + dy;
550
+ const px = (isLeft ? baseX - width * gravityMultiplier : baseX + width * gravityMultiplier) + dx;
551
+ const ny = rect.top + nextY + dy;
552
+ const nGravityMultiplier = multipliers[nextY] || 0;
553
+ const nx = (isLeft ? baseX - nextWidth * nGravityMultiplier : baseX + nextWidth * nGravityMultiplier) + dx;
554
+ ctx.lineTo(px, py);
555
+ ctx.lineTo(nx, ny);
556
+ }
557
+ ctx.lineTo(baseX + dx, rect.bottom + dy);
534
558
  };
535
- setupCtx(ctx);
536
- if (fixedCtx) setupCtx(fixedCtx);
537
- const scrollX = window.scrollX;
538
- const scrollY = window.scrollY;
539
- for (const { rect, acc } of elementRects) {
559
+ for (const item of elementRects) {
560
+ const { rect, acc } = item;
540
561
  if (acc.maxSideHeight === 0) continue;
541
562
  const hasLeftSnow = acc.leftSide.some((h) => h > 0.3);
542
563
  const hasRightSnow = acc.rightSide.some((h) => h > 0.3);
543
564
  if (!hasLeftSnow && !hasRightSnow) continue;
544
- const useFixed = acc.isFixed && fixedCtx;
545
- const targetCtx = useFixed ? fixedCtx : ctx;
546
- const dx = useFixed ? -scrollX : 0;
547
- const dy = useFixed ? -scrollY : 0;
548
- const drawSide = (sideArray, isLeft) => {
549
- targetCtx.beginPath();
550
- const baseX = isLeft ? rect.left : rect.right;
551
- targetCtx.moveTo(baseX + dx, rect.top + dy);
552
- for (let y = 0; y < sideArray.length; y += 2) {
553
- const width = sideArray[y] || 0;
554
- const nextY = Math.min(y + 2, sideArray.length - 1);
555
- const nextWidth = sideArray[nextY] || 0;
556
- const heightRatio = y / sideArray.length;
557
- const gravityMultiplier = Math.pow(heightRatio, 1.5);
558
- const py = rect.top + y + dy;
559
- const px = (isLeft ? baseX - width * gravityMultiplier : baseX + width * gravityMultiplier) + dx;
560
- const ny = rect.top + nextY + dy;
561
- const nRatio = nextY / sideArray.length;
562
- const nGravityMultiplier = Math.pow(nRatio, 1.5);
563
- const nx = (isLeft ? baseX - nextWidth * nGravityMultiplier : baseX + nextWidth * nGravityMultiplier) + dx;
564
- targetCtx.lineTo(px, py);
565
- targetCtx.lineTo(nx, ny);
566
- }
567
- targetCtx.lineTo(baseX + dx, rect.bottom + dy);
568
- targetCtx.closePath();
569
- targetCtx.fill();
570
- };
571
- if (hasLeftSnow) drawSide(acc.leftSide, true);
572
- if (hasRightSnow) drawSide(acc.rightSide, false);
565
+ const dx = currentScrollX;
566
+ const dy = currentScrollY;
567
+ if (hasLeftSnow) drawSide(acc.leftSide, true, acc.sideGravityMultipliers, rect, dx, dy);
568
+ if (hasRightSnow) drawSide(acc.rightSide, false, acc.sideGravityMultipliers, rect, dx, dy);
573
569
  }
570
+ ctx.fill();
574
571
  ctx.shadowBlur = 0;
575
- if (fixedCtx) fixedCtx.shadowBlur = 0;
576
572
  };
577
573
 
578
574
  // src/Snowfall.tsx
@@ -599,7 +595,7 @@ function Snowfall() {
599
595
  drawTime: 0
600
596
  });
601
597
  (0, import_react2.useEffect)(() => {
602
- setIsMounted(true);
598
+ requestAnimationFrame(() => setIsMounted(true));
603
599
  }, []);
604
600
  (0, import_react2.useEffect)(() => {
605
601
  isEnabledRef.current = isEnabled;
@@ -618,31 +614,49 @@ function Snowfall() {
618
614
  if (!ctx) return;
619
615
  const resizeCanvas = () => {
620
616
  if (canvasRef.current) {
621
- const newHeight = Math.max(document.documentElement.scrollHeight, window.innerHeight);
622
- const newWidth = Math.max(document.documentElement.scrollWidth, window.innerWidth);
623
- if (canvasRef.current.height !== newHeight || canvasRef.current.width !== newWidth) {
624
- canvasRef.current.width = newWidth;
625
- canvasRef.current.height = newHeight;
626
- }
617
+ const newWidth = window.innerWidth;
618
+ const newHeight = window.innerHeight;
619
+ const dpr = window.devicePixelRatio || 1;
620
+ canvasRef.current.width = newWidth * dpr;
621
+ canvasRef.current.height = newHeight * dpr;
622
+ canvasRef.current.style.width = `${newWidth}px`;
623
+ canvasRef.current.style.height = `${newHeight}px`;
627
624
  }
628
625
  };
629
626
  resizeCanvas();
630
- const resizeObserver = new ResizeObserver(() => {
627
+ const windowResizeObserver = new ResizeObserver(() => {
631
628
  resizeCanvas();
632
629
  });
633
- resizeObserver.observe(document.body);
630
+ windowResizeObserver.observe(document.body);
631
+ const surfaceObserver = new ResizeObserver((entries) => {
632
+ let needsUpdate = false;
633
+ for (const entry of entries) {
634
+ if (entry.target.isConnected) {
635
+ needsUpdate = true;
636
+ break;
637
+ }
638
+ }
639
+ if (needsUpdate) {
640
+ initAccumulationWrapper();
641
+ }
642
+ });
634
643
  snowflakesRef.current = [];
635
644
  const initAccumulationWrapper = () => {
636
645
  const scanStart = performance.now();
637
646
  initializeAccumulation(accumulationRef.current, physicsConfigRef.current);
647
+ surfaceObserver.disconnect();
648
+ for (const [el] of accumulationRef.current) {
649
+ surfaceObserver.observe(el);
650
+ }
638
651
  metricsRef.current.scanTime = performance.now() - scanStart;
639
652
  };
640
653
  initAccumulationWrapper();
641
- setIsVisible(true);
654
+ requestAnimationFrame(() => {
655
+ if (isMounted) setIsVisible(true);
656
+ });
642
657
  let lastTime = 0;
643
- let lastRectUpdate = 0;
644
658
  let lastMetricsUpdate = 0;
645
- let cachedElementRects = [];
659
+ let elementRects = [];
646
660
  const animate = (currentTime) => {
647
661
  if (lastTime === 0) {
648
662
  lastTime = currentTime;
@@ -658,29 +672,36 @@ function Snowfall() {
658
672
  const dt = deltaTime / 16.67;
659
673
  const frameStartTime = performance.now();
660
674
  const clearStart = performance.now();
661
- ctx.clearRect(0, 0, canvas.width, canvas.height);
675
+ const dpr = window.devicePixelRatio || 1;
676
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
677
+ ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
678
+ const scrollX = window.scrollX;
679
+ const scrollY = window.scrollY;
680
+ ctx.translate(-scrollX, -scrollY);
662
681
  metricsRef.current.clearTime = performance.now() - clearStart;
663
682
  const snowflakes = snowflakesRef.current;
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
- }
683
+ const rectStart = performance.now();
684
+ elementRects = getElementRects(accumulationRef.current);
685
+ metricsRef.current.rectUpdateTime = performance.now() - rectStart;
670
686
  const physicsStart = performance.now();
671
- meltAndSmoothAccumulation(cachedElementRects, physicsConfigRef.current, dt);
672
- updateSnowflakes(snowflakes, cachedElementRects, physicsConfigRef.current, dt, canvas.width, canvas.height);
687
+ meltAndSmoothAccumulation(elementRects, physicsConfigRef.current, dt);
688
+ updateSnowflakes(
689
+ snowflakes,
690
+ elementRects,
691
+ physicsConfigRef.current,
692
+ dt,
693
+ document.documentElement.scrollWidth,
694
+ document.documentElement.scrollHeight
695
+ );
673
696
  metricsRef.current.physicsTime = performance.now() - physicsStart;
674
697
  const drawStart = performance.now();
675
- for (const flake of snowflakes) {
676
- drawSnowflake(ctx, flake);
677
- }
698
+ drawSnowflakes(ctx, snowflakes);
678
699
  if (isEnabledRef.current && snowflakes.length < physicsConfigRef.current.MAX_FLAKES) {
679
700
  const isBackground = Math.random() < 0.4;
680
- snowflakes.push(createSnowflake(canvas.width, physicsConfigRef.current, isBackground));
701
+ snowflakes.push(createSnowflake(document.documentElement.scrollWidth, physicsConfigRef.current, isBackground));
681
702
  }
682
- drawAccumulations(ctx, ctx, cachedElementRects);
683
- drawSideAccumulations(ctx, ctx, cachedElementRects);
703
+ drawAccumulations(ctx, elementRects);
704
+ drawSideAccumulations(ctx, elementRects);
684
705
  metricsRef.current.drawTime = performance.now() - drawStart;
685
706
  metricsRef.current.frameTime = performance.now() - frameStartTime;
686
707
  if (currentTime - lastMetricsUpdate > 500) {
@@ -713,7 +734,8 @@ function Snowfall() {
713
734
  cancelAnimationFrame(animationIdRef.current);
714
735
  window.removeEventListener("resize", handleResize);
715
736
  clearInterval(checkInterval);
716
- resizeObserver.disconnect();
737
+ windowResizeObserver.disconnect();
738
+ surfaceObserver.disconnect();
717
739
  };
718
740
  }, [isMounted]);
719
741
  if (!isMounted) return null;
@@ -722,7 +744,8 @@ function Snowfall() {
722
744
  {
723
745
  ref: canvasRef,
724
746
  style: {
725
- position: "absolute",
747
+ position: "fixed",
748
+ // FIXED position to eliminate scroll jitter
726
749
  top: 0,
727
750
  left: 0,
728
751
  pointerEvents: "none",
@@ -739,9 +762,9 @@ function Snowfall() {
739
762
  // src/DebugPanel.tsx
740
763
  var import_react3 = require("react");
741
764
  var import_jsx_runtime3 = require("react/jsx-runtime");
742
- function DebugPanel() {
765
+ function DebugPanel({ defaultOpen = true }) {
743
766
  const { debugMode, toggleDebug, metrics } = useSnowfall();
744
- const [isMinimized, setIsMinimized] = (0, import_react3.useState)(false);
767
+ const [isMinimized, setIsMinimized] = (0, import_react3.useState)(!defaultOpen);
745
768
  const [copied, setCopied] = (0, import_react3.useState)(false);
746
769
  (0, import_react3.useEffect)(() => {
747
770
  const handleKeyDown = (e) => {
@@ -769,145 +792,199 @@ function DebugPanel() {
769
792
  }
770
793
  };
771
794
  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
795
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
796
+ "div",
797
+ {
798
+ "data-snowfall": "top",
799
+ style: {
800
+ position: "fixed",
801
+ bottom: "80px",
802
+ left: "24px",
803
+ backgroundColor: "rgba(15, 23, 42, 0.75)",
804
+ backdropFilter: "blur(16px)",
805
+ WebkitBackdropFilter: "blur(16px)",
806
+ color: "#e2e8f0",
807
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
808
+ fontSize: "12px",
809
+ padding: isMinimized ? "12px" : "20px",
810
+ borderRadius: "16px",
811
+ zIndex: 1e4,
812
+ minWidth: isMinimized ? "auto" : "300px",
813
+ maxWidth: "100%",
814
+ border: "1px solid rgba(255, 255, 255, 0.1)",
815
+ boxShadow: "0 20px 25px -5px rgba(0, 0, 0, 0.2), 0 8px 10px -6px rgba(0, 0, 0, 0.2)",
816
+ transition: "all 0.2s ease"
817
+ },
818
+ children: [
819
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: {
820
+ display: "flex",
821
+ justifyContent: "space-between",
822
+ alignItems: "center",
823
+ marginBottom: isMinimized ? 0 : "16px",
824
+ gap: "16px"
825
+ }, children: [
826
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { fontWeight: "600", color: "#fff", display: "flex", alignItems: "center", gap: "8px" }, children: [
827
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontSize: "14px" }, children: "\u2744\uFE0F" }),
828
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: {
829
+ background: "linear-gradient(to right, #60a5fa, #22d3ee)",
830
+ WebkitBackgroundClip: "text",
831
+ WebkitTextFillColor: "transparent",
832
+ fontWeight: "700"
833
+ }, children: "DEBUG" })
834
+ ] }),
835
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "8px" }, children: [
836
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
837
+ "button",
838
+ {
839
+ onClick: () => setIsMinimized(!isMinimized),
840
+ style: {
841
+ background: "rgba(255, 255, 255, 0.1)",
842
+ border: "1px solid rgba(255, 255, 255, 0.1)",
843
+ color: "#fff",
844
+ cursor: "pointer",
845
+ width: "24px",
846
+ height: "24px",
847
+ display: "flex",
848
+ alignItems: "center",
849
+ justifyContent: "center",
850
+ borderRadius: "6px",
851
+ fontSize: "10px"
852
+ },
853
+ children: isMinimized ? "\u25B2" : "\u25BC"
854
+ }
855
+ ),
856
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
857
+ "button",
858
+ {
859
+ onClick: toggleDebug,
860
+ style: {
861
+ background: "rgba(239, 68, 68, 0.15)",
862
+ border: "1px solid rgba(239, 68, 68, 0.2)",
863
+ color: "#f87171",
864
+ cursor: "pointer",
865
+ width: "24px",
866
+ height: "24px",
867
+ display: "flex",
868
+ alignItems: "center",
869
+ justifyContent: "center",
870
+ borderRadius: "6px",
871
+ fontSize: "12px"
872
+ },
873
+ children: "\u2715"
874
+ }
875
+ )
876
+ ] })
884
877
  ] }),
885
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
886
- "Surfaces: ",
887
- metrics.surfaceCount
878
+ !isMinimized && metrics && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
879
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { marginBottom: "12px", paddingBottom: "12px", borderBottom: "1px solid rgba(255,255,255,0.08)" }, children: [
880
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { color: "#94a3b8", marginBottom: "8px", fontSize: "11px", fontWeight: "600", letterSpacing: "0.5px", textTransform: "uppercase" }, children: "Performance" }),
881
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", justifyContent: "space-between", marginBottom: "4px" }, children: [
882
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "FPS" }),
883
+ /* @__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) })
884
+ ] }),
885
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", justifyContent: "space-between", marginBottom: "4px" }, children: [
886
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "Frame Time" }),
887
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { fontFamily: "monospace" }, children: [
888
+ metrics.frameTime.toFixed(2),
889
+ "ms"
890
+ ] })
891
+ ] }),
892
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", justifyContent: "space-between" }, children: [
893
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { color: metrics.rafGap && metrics.rafGap > 20 ? "#fbbf24" : "inherit" }, children: "rAF Gap" }),
894
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { fontFamily: "monospace" }, children: [
895
+ metrics.rafGap?.toFixed(1) || 0,
896
+ "ms"
897
+ ] })
898
+ ] })
899
+ ] }),
900
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { marginBottom: "12px", paddingBottom: "12px", borderBottom: "1px solid rgba(255,255,255,0.08)" }, children: [
901
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { color: "#94a3b8", marginBottom: "8px", fontSize: "11px", fontWeight: "600", letterSpacing: "0.5px", textTransform: "uppercase" }, children: "Detailed Timings" }),
902
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: "4px 12px" }, children: [
903
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", justifyContent: "space-between" }, children: [
904
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "Clear" }),
905
+ " ",
906
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { fontFamily: "monospace" }, children: [
907
+ metrics.clearTime?.toFixed(2) || 0,
908
+ "ms"
909
+ ] })
910
+ ] }),
911
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", justifyContent: "space-between" }, children: [
912
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "Physics" }),
913
+ " ",
914
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { fontFamily: "monospace" }, children: [
915
+ metrics.physicsTime?.toFixed(2) || 0,
916
+ "ms"
917
+ ] })
918
+ ] }),
919
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", justifyContent: "space-between" }, children: [
920
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "Draw" }),
921
+ " ",
922
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { fontFamily: "monospace" }, children: [
923
+ metrics.drawTime?.toFixed(2) || 0,
924
+ "ms"
925
+ ] })
926
+ ] }),
927
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", justifyContent: "space-between" }, children: [
928
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "Scan" }),
929
+ " ",
930
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { fontFamily: "monospace" }, children: [
931
+ metrics.scanTime.toFixed(2),
932
+ "ms"
933
+ ] })
934
+ ] }),
935
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", justifyContent: "space-between", gridColumn: "span 2" }, children: [
936
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "Rect Update" }),
937
+ " ",
938
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { fontFamily: "monospace" }, children: [
939
+ metrics.rectUpdateTime.toFixed(2),
940
+ "ms"
941
+ ] })
942
+ ] })
943
+ ] })
944
+ ] }),
945
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { marginBottom: "16px" }, children: [
946
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { color: "#94a3b8", marginBottom: "8px", fontSize: "11px", fontWeight: "600", letterSpacing: "0.5px", textTransform: "uppercase" }, children: "Counts" }),
947
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", justifyContent: "space-between", marginBottom: "4px" }, children: [
948
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "Snowflakes" }),
949
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { style: { fontFamily: "monospace" }, children: [
950
+ metrics.flakeCount,
951
+ " / ",
952
+ metrics.maxFlakes
953
+ ] })
954
+ ] }),
955
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", justifyContent: "space-between" }, children: [
956
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "Surfaces" }),
957
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontFamily: "monospace" }, children: metrics.surfaceCount })
958
+ ] })
959
+ ] }),
960
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
961
+ "button",
962
+ {
963
+ onClick: copyToClipboard,
964
+ style: {
965
+ width: "100%",
966
+ padding: "10px",
967
+ background: copied ? "rgba(34, 197, 94, 0.2)" : "rgba(255, 255, 255, 0.05)",
968
+ border: copied ? "1px solid rgba(34, 197, 94, 0.5)" : "1px solid rgba(255, 255, 255, 0.1)",
969
+ color: copied ? "#4ade80" : "#fff",
970
+ cursor: "pointer",
971
+ borderRadius: "8px",
972
+ fontSize: "11px",
973
+ fontWeight: "600",
974
+ transition: "all 0.2s",
975
+ display: "flex",
976
+ alignItems: "center",
977
+ justifyContent: "center",
978
+ gap: "6px"
979
+ },
980
+ children: copied ? "\u2713 COPIED" : "\u{1F4CB} COPY METRICS"
981
+ }
982
+ ),
983
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { marginTop: "12px", fontSize: "10px", color: "#64748b", textAlign: "center" }, children: "Shift+D to toggle" })
888
984
  ] })
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
- ] })
910
- ] });
985
+ ]
986
+ }
987
+ );
911
988
  }
912
989
  // Annotate the CommonJS export names for ESM import in node:
913
990
  0 && (module.exports = {