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