@hdcodedev/snowfall 1.0.10 → 1.0.12

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
@@ -29,10 +29,10 @@ __export(index_exports, {
29
29
  });
30
30
  module.exports = __toCommonJS(index_exports);
31
31
 
32
- // src/Snowfall.tsx
33
- var import_react2 = require("react");
32
+ // src/components/Snowfall.tsx
33
+ var import_react5 = require("react");
34
34
 
35
- // src/SnowfallProvider.tsx
35
+ // src/components/SnowfallProvider.tsx
36
36
  var import_react = require("react");
37
37
  var import_jsx_runtime = require("react/jsx-runtime");
38
38
  var DEFAULT_PHYSICS = {
@@ -53,11 +53,13 @@ var DEFAULT_PHYSICS = {
53
53
  MIN: 0.5,
54
54
  MAX: 1.6
55
55
  },
56
- MAX_SURFACES: 15
56
+ MAX_SURFACES: 15,
57
+ COLLISION_CHECK_RATE: 0.3
58
+ // 30% of snowflakes check collisions per frame
57
59
  };
58
60
  var SnowfallContext = (0, import_react.createContext)(void 0);
59
- function SnowfallProvider({ children, initialDebug = false }) {
60
- const [isEnabled, setIsEnabled] = (0, import_react.useState)(true);
61
+ function SnowfallProvider({ children, initialDebug = false, initialEnabled = true }) {
62
+ const [isEnabled, setIsEnabled] = (0, import_react.useState)(initialEnabled);
61
63
  const [physicsConfig, setPhysicsConfig] = (0, import_react.useState)(DEFAULT_PHYSICS);
62
64
  const [debugMode, setDebugMode] = (0, import_react.useState)(initialDebug);
63
65
  const [metrics, setMetrics] = (0, import_react.useState)(null);
@@ -108,7 +110,7 @@ function useSnowfall() {
108
110
  return context;
109
111
  }
110
112
 
111
- // src/utils/snowfall/constants.ts
113
+ // src/core/constants.ts
112
114
  var ATTR_SNOWFALL = "data-snowfall";
113
115
  var VAL_IGNORE = "ignore";
114
116
  var VAL_TOP = "top";
@@ -119,7 +121,7 @@ var ROLE_BANNER = "banner";
119
121
  var ROLE_CONTENTINFO = "contentinfo";
120
122
  var TAU = Math.PI * 2;
121
123
 
122
- // src/utils/snowfall/dom.ts
124
+ // src/core/dom.ts
123
125
  var BOTTOM_TAGS = [TAG_HEADER];
124
126
  var BOTTOM_ROLES = [ROLE_BANNER];
125
127
  var AUTO_DETECT_TAGS = [TAG_HEADER, TAG_FOOTER, "article", "section", "aside", "nav"];
@@ -201,7 +203,7 @@ var getElementRects = (accumulationMap) => {
201
203
  return elementRects;
202
204
  };
203
205
 
204
- // src/utils/snowfall/physics.ts
206
+ // src/core/physics.ts
205
207
  var OPACITY_BUCKETS = [0.3, 0.5, 0.7, 0.9];
206
208
  var quantizeOpacity = (opacity) => {
207
209
  return OPACITY_BUCKETS.reduce(
@@ -248,6 +250,7 @@ var createSnowflake = (worldWidth, config, isBackground = false) => {
248
250
  const opacity = quantizeOpacity(rawOpacity);
249
251
  const rawGlowOpacity = opacity * 0.2;
250
252
  const glowOpacity = quantizeOpacity(rawGlowOpacity);
253
+ const initialWobble = noise.wobblePhase * TAU;
251
254
  return {
252
255
  x,
253
256
  y: window.scrollY - 5,
@@ -257,7 +260,7 @@ var createSnowflake = (worldWidth, config, isBackground = false) => {
257
260
  wind: (noise.wind - 0.5) * profile.windScale,
258
261
  opacity,
259
262
  glowOpacity,
260
- wobble: noise.wobblePhase * TAU,
263
+ wobble: initialWobble,
261
264
  wobbleSpeed: noise.wobbleSpeed * profile.wobbleScale + profile.wobbleBase,
262
265
  sizeRatio,
263
266
  isBackground
@@ -373,84 +376,101 @@ var accumulateSide = (sideArray, rectHeight, localY, maxSideHeight, borderRadius
373
376
  }
374
377
  return newMax;
375
378
  };
379
+ var updateSnowflakePosition = (flake, dt) => {
380
+ flake.wobble += flake.wobbleSpeed * dt;
381
+ flake.x += (flake.wind + Math.sin(flake.wobble) * 0.5) * dt;
382
+ flake.y += (flake.speed + Math.cos(flake.wobble * 0.5) * 0.1) * dt;
383
+ };
384
+ var checkSideCollision = (flake, flakeViewportX, flakeViewportY, rect, acc, config) => {
385
+ const isInVerticalBounds = flakeViewportY >= rect.top && flakeViewportY <= rect.bottom;
386
+ if (!isInVerticalBounds || acc.maxSideHeight <= 0) {
387
+ return false;
388
+ }
389
+ const localY = Math.floor(flakeViewportY - rect.top);
390
+ const borderRadius = acc.borderRadius;
391
+ const isInTopCorner = localY < borderRadius;
392
+ const isInBottomCorner = localY > rect.height - borderRadius;
393
+ const isCorner = borderRadius > 0 && (isInTopCorner || isInBottomCorner);
394
+ if (isCorner) {
395
+ return false;
396
+ }
397
+ if (flakeViewportX >= rect.left - 5 && flakeViewportX < rect.left + 3) {
398
+ acc.leftMax = accumulateSide(acc.leftSide, rect.height, localY, acc.maxSideHeight, borderRadius, config, acc.leftMax);
399
+ return true;
400
+ }
401
+ if (flakeViewportX > rect.right - 3 && flakeViewportX <= rect.right + 5) {
402
+ acc.rightMax = accumulateSide(acc.rightSide, rect.height, localY, acc.maxSideHeight, borderRadius, config, acc.rightMax);
403
+ return true;
404
+ }
405
+ return false;
406
+ };
407
+ var checkSurfaceCollision = (flake, flakeViewportX, flakeViewportY, rect, acc, isBottom, config) => {
408
+ if (flakeViewportX < rect.left || flakeViewportX > rect.right) {
409
+ return false;
410
+ }
411
+ const localX = Math.floor(flakeViewportX - rect.left);
412
+ const currentHeight = acc.heights[localX] || 0;
413
+ const maxHeight = acc.maxHeights[localX] || 5;
414
+ const surfaceY = isBottom ? rect.bottom - currentHeight : rect.top - currentHeight;
415
+ if (flakeViewportY < surfaceY || flakeViewportY >= surfaceY + 10 || currentHeight >= maxHeight) {
416
+ return false;
417
+ }
418
+ const shouldAccumulate2 = isBottom ? Math.random() < 0.15 : true;
419
+ if (shouldAccumulate2) {
420
+ const baseSpread = Math.ceil(flake.radius);
421
+ const spread = baseSpread + Math.floor(Math.random() * 2);
422
+ const accumRate = isBottom ? config.ACCUMULATION.BOTTOM_RATE : config.ACCUMULATION.TOP_RATE;
423
+ const centerOffset = Math.floor(Math.random() * 3) - 1;
424
+ for (let dx = -spread; dx <= spread; dx++) {
425
+ if (Math.random() < 0.15) continue;
426
+ const idx = localX + dx + centerOffset;
427
+ if (idx >= 0 && idx < acc.heights.length) {
428
+ const dist = Math.abs(dx);
429
+ const pixelMax = acc.maxHeights[idx] || 5;
430
+ const normDist = dist / spread;
431
+ const falloff = (Math.cos(normDist * Math.PI) + 1) / 2;
432
+ const baseAdd = 0.3 * falloff;
433
+ const randomFactor = 0.8 + Math.random() * 0.4;
434
+ const addHeight = baseAdd * randomFactor * accumRate;
435
+ if (acc.heights[idx] < pixelMax && addHeight > 0) {
436
+ acc.heights[idx] = Math.min(pixelMax, acc.heights[idx] + addHeight);
437
+ }
438
+ }
439
+ }
440
+ if (isBottom) {
441
+ return true;
442
+ }
443
+ }
444
+ return !isBottom;
445
+ };
446
+ var shouldRemoveSnowflake = (flake, landed, worldWidth, worldHeight) => {
447
+ return landed || flake.y > worldHeight + 10 || flake.x < -20 || flake.x > worldWidth + 20;
448
+ };
376
449
  var updateSnowflakes = (snowflakes, elementRects, config, dt, worldWidth, worldHeight) => {
377
450
  const scrollX = window.scrollX;
378
451
  const scrollY = window.scrollY;
379
452
  for (let i = snowflakes.length - 1; i >= 0; i--) {
380
453
  const flake = snowflakes[i];
381
- flake.wobble += flake.wobbleSpeed * dt;
382
- flake.x += (flake.wind + Math.sin(flake.wobble) * 0.5) * dt;
383
- flake.y += (flake.speed + Math.cos(flake.wobble * 0.5) * 0.1) * dt;
454
+ updateSnowflakePosition(flake, dt);
384
455
  let landed = false;
385
- for (const item of elementRects) {
386
- const { rect, acc } = item;
387
- const isBottom = acc.type === VAL_BOTTOM;
456
+ const shouldCheckCollision = Math.random() < config.COLLISION_CHECK_RATE;
457
+ if (shouldCheckCollision) {
388
458
  const flakeViewportX = flake.x - scrollX;
389
459
  const flakeViewportY = flake.y - scrollY;
390
- const isInVerticalBounds = flakeViewportY >= rect.top && flakeViewportY <= rect.bottom;
391
- if (!landed && acc.maxSideHeight > 0 && !isBottom) {
392
- if (isInVerticalBounds) {
393
- const localY = Math.floor(flakeViewportY - rect.top);
394
- const borderRadius = acc.borderRadius;
395
- const isInTopCorner = localY < borderRadius;
396
- const isInBottomCorner = localY > rect.height - borderRadius;
397
- const isCorner = borderRadius > 0 && (isInTopCorner || isInBottomCorner);
398
- if (flakeViewportX >= rect.left - 5 && flakeViewportX < rect.left + 3) {
399
- if (!isCorner) {
400
- acc.leftMax = accumulateSide(acc.leftSide, rect.height, localY, acc.maxSideHeight, borderRadius, config, acc.leftMax);
401
- landed = true;
402
- }
403
- }
404
- if (!landed && flakeViewportX > rect.right - 3 && flakeViewportX <= rect.right + 5) {
405
- if (!isCorner) {
406
- acc.rightMax = accumulateSide(acc.rightSide, rect.height, localY, acc.maxSideHeight, borderRadius, config, acc.rightMax);
407
- landed = true;
408
- }
409
- }
460
+ for (const item of elementRects) {
461
+ const { rect, acc } = item;
462
+ const isBottom = acc.type === VAL_BOTTOM;
463
+ if (!landed && !isBottom) {
464
+ landed = checkSideCollision(flake, flakeViewportX, flakeViewportY, rect, acc, config);
410
465
  if (landed) break;
411
466
  }
412
- }
413
- if (flakeViewportX >= rect.left && flakeViewportX <= rect.right) {
414
- const localX = Math.floor(flakeViewportX - rect.left);
415
- const currentHeight = acc.heights[localX] || 0;
416
- const maxHeight = acc.maxHeights[localX] || 5;
417
- const surfaceY = isBottom ? rect.bottom - currentHeight : rect.top - currentHeight;
418
- if (flakeViewportY >= surfaceY && flakeViewportY < surfaceY + 10 && currentHeight < maxHeight) {
419
- const shouldAccumulate2 = isBottom ? Math.random() < 0.15 : true;
420
- if (shouldAccumulate2) {
421
- const baseSpread = Math.ceil(flake.radius);
422
- const spread = baseSpread + Math.floor(Math.random() * 2);
423
- const accumRate = isBottom ? config.ACCUMULATION.BOTTOM_RATE : config.ACCUMULATION.TOP_RATE;
424
- const centerOffset = Math.floor(Math.random() * 3) - 1;
425
- for (let dx = -spread; dx <= spread; dx++) {
426
- if (Math.random() < 0.15) continue;
427
- const idx = localX + dx + centerOffset;
428
- if (idx >= 0 && idx < acc.heights.length) {
429
- const dist = Math.abs(dx);
430
- const pixelMax = acc.maxHeights[idx] || 5;
431
- const normDist = dist / spread;
432
- const falloff = (Math.cos(normDist * Math.PI) + 1) / 2;
433
- const baseAdd = 0.3 * falloff;
434
- const randomFactor = 0.8 + Math.random() * 0.4;
435
- const addHeight = baseAdd * randomFactor * accumRate;
436
- if (acc.heights[idx] < pixelMax && addHeight > 0) {
437
- acc.heights[idx] = Math.min(pixelMax, acc.heights[idx] + addHeight);
438
- }
439
- }
440
- }
441
- if (isBottom) {
442
- landed = true;
443
- break;
444
- }
445
- }
446
- if (!isBottom) {
447
- landed = true;
448
- break;
449
- }
467
+ if (!landed) {
468
+ landed = checkSurfaceCollision(flake, flakeViewportX, flakeViewportY, rect, acc, isBottom, config);
469
+ if (landed) break;
450
470
  }
451
471
  }
452
472
  }
453
- if (landed || flake.y > worldHeight + 10 || flake.x < -20 || flake.x > worldWidth + 20) {
473
+ if (shouldRemoveSnowflake(flake, landed, worldWidth, worldHeight)) {
454
474
  snowflakes.splice(i, 1);
455
475
  }
456
476
  }
@@ -487,7 +507,85 @@ var meltAndSmoothAccumulation = (elementRects, config, dt) => {
487
507
  }
488
508
  };
489
509
 
490
- // src/utils/snowfall/draw.ts
510
+ // src/hooks/usePerformanceMetrics.ts
511
+ var import_react2 = require("react");
512
+ function usePerformanceMetrics() {
513
+ const lastFpsSecondRef = (0, import_react2.useRef)(0);
514
+ const framesInSecondRef = (0, import_react2.useRef)(0);
515
+ const currentFpsRef = (0, import_react2.useRef)(0);
516
+ const metricsRef = (0, import_react2.useRef)({
517
+ scanTime: 0,
518
+ rectUpdateTime: 0,
519
+ frameTime: 0,
520
+ rafGap: 0,
521
+ clearTime: 0,
522
+ physicsTime: 0,
523
+ drawTime: 0
524
+ });
525
+ const updateFps = (0, import_react2.useCallback)((now) => {
526
+ const currentSecond = Math.floor(now / 1e3);
527
+ if (currentSecond !== lastFpsSecondRef.current) {
528
+ currentFpsRef.current = framesInSecondRef.current;
529
+ framesInSecondRef.current = 1;
530
+ lastFpsSecondRef.current = currentSecond;
531
+ } else {
532
+ framesInSecondRef.current++;
533
+ }
534
+ }, []);
535
+ const getCurrentFps = (0, import_react2.useCallback)(() => {
536
+ return currentFpsRef.current || framesInSecondRef.current;
537
+ }, []);
538
+ const buildMetrics = (0, import_react2.useCallback)((surfaceCount, flakeCount, maxFlakes) => {
539
+ return {
540
+ fps: currentFpsRef.current || framesInSecondRef.current,
541
+ frameTime: metricsRef.current.frameTime,
542
+ scanTime: metricsRef.current.scanTime,
543
+ rectUpdateTime: metricsRef.current.rectUpdateTime,
544
+ surfaceCount,
545
+ flakeCount,
546
+ maxFlakes,
547
+ rafGap: metricsRef.current.rafGap,
548
+ clearTime: metricsRef.current.clearTime,
549
+ physicsTime: metricsRef.current.physicsTime,
550
+ drawTime: metricsRef.current.drawTime
551
+ };
552
+ }, []);
553
+ return {
554
+ metricsRef,
555
+ updateFps,
556
+ getCurrentFps,
557
+ buildMetrics
558
+ };
559
+ }
560
+
561
+ // src/hooks/useSnowfallCanvas.ts
562
+ var import_react3 = require("react");
563
+ function useSnowfallCanvas() {
564
+ const canvasRef = (0, import_react3.useRef)(null);
565
+ const dprRef = (0, import_react3.useRef)(1);
566
+ const resizeCanvas = (0, import_react3.useCallback)(() => {
567
+ if (canvasRef.current) {
568
+ const newWidth = window.innerWidth;
569
+ const newHeight = window.innerHeight;
570
+ const dpr = window.devicePixelRatio || 1;
571
+ dprRef.current = dpr;
572
+ canvasRef.current.width = newWidth * dpr;
573
+ canvasRef.current.height = newHeight * dpr;
574
+ canvasRef.current.style.width = `${newWidth}px`;
575
+ canvasRef.current.style.height = `${newHeight}px`;
576
+ }
577
+ }, []);
578
+ return {
579
+ canvasRef,
580
+ dprRef,
581
+ resizeCanvas
582
+ };
583
+ }
584
+
585
+ // src/hooks/useAnimationLoop.ts
586
+ var import_react4 = require("react");
587
+
588
+ // src/core/draw.ts
491
589
  var ACC_FILL_STYLE = "rgba(255, 255, 255, 0.95)";
492
590
  var ACC_SHADOW_COLOR = "rgba(200, 230, 255, 0.6)";
493
591
  var drawSnowflakes = (ctx, flakes) => {
@@ -596,65 +694,170 @@ var drawSideAccumulations = (ctx, elementRects, scrollX, scrollY) => {
596
694
  ctx.shadowBlur = 0;
597
695
  };
598
696
 
599
- // src/Snowfall.tsx
697
+ // src/hooks/useAnimationLoop.ts
698
+ function useAnimationLoop(params) {
699
+ const animationIdRef = (0, import_react4.useRef)(0);
700
+ const lastTimeRef = (0, import_react4.useRef)(0);
701
+ const lastMetricsUpdateRef = (0, import_react4.useRef)(0);
702
+ const elementRectsRef = (0, import_react4.useRef)([]);
703
+ const dirtyRectsRef = (0, import_react4.useRef)(true);
704
+ const animate = (0, import_react4.useCallback)((currentTime) => {
705
+ const {
706
+ canvasRef,
707
+ dprRef,
708
+ snowflakesRef,
709
+ accumulationRef,
710
+ isEnabledRef,
711
+ physicsConfigRef,
712
+ metricsRef,
713
+ updateFps,
714
+ getCurrentFps,
715
+ buildMetrics,
716
+ setMetricsRef
717
+ } = params;
718
+ const canvas = canvasRef.current;
719
+ if (!canvas) {
720
+ animationIdRef.current = requestAnimationFrame(animate);
721
+ return;
722
+ }
723
+ const ctx = canvas.getContext("2d");
724
+ if (!ctx) {
725
+ animationIdRef.current = requestAnimationFrame(animate);
726
+ return;
727
+ }
728
+ if (lastTimeRef.current === 0) {
729
+ lastTimeRef.current = currentTime;
730
+ animationIdRef.current = requestAnimationFrame(animate);
731
+ return;
732
+ }
733
+ const deltaTime = Math.min(currentTime - lastTimeRef.current, 50);
734
+ const now = performance.now();
735
+ updateFps(now);
736
+ metricsRef.current.rafGap = currentTime - lastTimeRef.current;
737
+ lastTimeRef.current = currentTime;
738
+ const dt = deltaTime / 16.67;
739
+ const frameStartTime = performance.now();
740
+ const clearStart = performance.now();
741
+ const dpr = dprRef.current;
742
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
743
+ ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
744
+ const scrollX = window.scrollX;
745
+ const scrollY = window.scrollY;
746
+ ctx.translate(-scrollX, -scrollY);
747
+ metricsRef.current.clearTime = performance.now() - clearStart;
748
+ const snowflakes = snowflakesRef.current;
749
+ if (dirtyRectsRef.current) {
750
+ const rectStart = performance.now();
751
+ elementRectsRef.current = getElementRects(accumulationRef.current);
752
+ metricsRef.current.rectUpdateTime = performance.now() - rectStart;
753
+ dirtyRectsRef.current = false;
754
+ }
755
+ const physicsStart = performance.now();
756
+ meltAndSmoothAccumulation(elementRectsRef.current, physicsConfigRef.current, dt);
757
+ updateSnowflakes(
758
+ snowflakes,
759
+ elementRectsRef.current,
760
+ physicsConfigRef.current,
761
+ dt,
762
+ document.documentElement.scrollWidth,
763
+ document.documentElement.scrollHeight
764
+ );
765
+ metricsRef.current.physicsTime = performance.now() - physicsStart;
766
+ const drawStart = performance.now();
767
+ drawSnowflakes(ctx, snowflakes);
768
+ if (isEnabledRef.current && snowflakes.length < physicsConfigRef.current.MAX_FLAKES) {
769
+ const currentFps = getCurrentFps();
770
+ const shouldSpawn = currentFps >= 40 || Math.random() < 0.2;
771
+ if (shouldSpawn) {
772
+ const isBackground = Math.random() < 0.4;
773
+ snowflakes.push(createSnowflake(document.documentElement.scrollWidth, physicsConfigRef.current, isBackground));
774
+ }
775
+ }
776
+ const viewportWidth = window.innerWidth;
777
+ const viewportHeight = window.innerHeight;
778
+ const visibleRects = elementRectsRef.current.filter(
779
+ ({ rect }) => rect.right >= 0 && rect.left <= viewportWidth && rect.bottom >= 0 && rect.top <= viewportHeight
780
+ );
781
+ if (visibleRects.length > 0) {
782
+ drawAccumulations(ctx, visibleRects, scrollX, scrollY);
783
+ drawSideAccumulations(ctx, visibleRects, scrollX, scrollY);
784
+ }
785
+ metricsRef.current.drawTime = performance.now() - drawStart;
786
+ metricsRef.current.frameTime = performance.now() - frameStartTime;
787
+ if (currentTime - lastMetricsUpdateRef.current > 500) {
788
+ setMetricsRef.current(buildMetrics(
789
+ accumulationRef.current.size,
790
+ snowflakes.length,
791
+ physicsConfigRef.current.MAX_FLAKES
792
+ ));
793
+ lastMetricsUpdateRef.current = currentTime;
794
+ }
795
+ animationIdRef.current = requestAnimationFrame(animate);
796
+ }, [params]);
797
+ const start = (0, import_react4.useCallback)(() => {
798
+ lastTimeRef.current = 0;
799
+ lastMetricsUpdateRef.current = 0;
800
+ animationIdRef.current = requestAnimationFrame(animate);
801
+ }, [animate]);
802
+ const stop = (0, import_react4.useCallback)(() => {
803
+ cancelAnimationFrame(animationIdRef.current);
804
+ }, []);
805
+ const markRectsDirty = (0, import_react4.useCallback)(() => {
806
+ dirtyRectsRef.current = true;
807
+ }, []);
808
+ return {
809
+ start,
810
+ stop,
811
+ markRectsDirty
812
+ };
813
+ }
814
+
815
+ // src/components/Snowfall.tsx
600
816
  var import_jsx_runtime2 = require("react/jsx-runtime");
601
817
  function Snowfall() {
602
818
  const { isEnabled, physicsConfig, setMetrics } = useSnowfall();
603
- const isEnabledRef = (0, import_react2.useRef)(isEnabled);
604
- const physicsConfigRef = (0, import_react2.useRef)(physicsConfig);
605
- const setMetricsRef = (0, import_react2.useRef)(setMetrics);
606
- const [isMounted, setIsMounted] = (0, import_react2.useState)(false);
607
- const [isVisible, setIsVisible] = (0, import_react2.useState)(false);
608
- const canvasRef = (0, import_react2.useRef)(null);
609
- const snowflakesRef = (0, import_react2.useRef)([]);
610
- const accumulationRef = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
611
- const animationIdRef = (0, import_react2.useRef)(0);
612
- const dprRef = (0, import_react2.useRef)(1);
613
- const fpsFrames = (0, import_react2.useRef)([]);
614
- const metricsRef = (0, import_react2.useRef)({
615
- scanTime: 0,
616
- rectUpdateTime: 0,
617
- frameTime: 0,
618
- rafGap: 0,
619
- clearTime: 0,
620
- physicsTime: 0,
621
- drawTime: 0
819
+ const isEnabledRef = (0, import_react5.useRef)(isEnabled);
820
+ const physicsConfigRef = (0, import_react5.useRef)(physicsConfig);
821
+ const setMetricsRef = (0, import_react5.useRef)(setMetrics);
822
+ const [isMounted, setIsMounted] = (0, import_react5.useState)(false);
823
+ const [isVisible, setIsVisible] = (0, import_react5.useState)(false);
824
+ const snowflakesRef = (0, import_react5.useRef)([]);
825
+ const accumulationRef = (0, import_react5.useRef)(/* @__PURE__ */ new Map());
826
+ const { canvasRef, dprRef, resizeCanvas } = useSnowfallCanvas();
827
+ const { metricsRef, updateFps, getCurrentFps, buildMetrics } = usePerformanceMetrics();
828
+ const { start: startAnimation, stop: stopAnimation, markRectsDirty } = useAnimationLoop({
829
+ canvasRef,
830
+ dprRef,
831
+ snowflakesRef,
832
+ accumulationRef,
833
+ isEnabledRef,
834
+ physicsConfigRef,
835
+ metricsRef,
836
+ updateFps,
837
+ getCurrentFps,
838
+ buildMetrics,
839
+ setMetricsRef
622
840
  });
623
- (0, import_react2.useEffect)(() => {
841
+ (0, import_react5.useEffect)(() => {
624
842
  requestAnimationFrame(() => setIsMounted(true));
625
843
  }, []);
626
- (0, import_react2.useEffect)(() => {
844
+ (0, import_react5.useEffect)(() => {
627
845
  isEnabledRef.current = isEnabled;
628
846
  }, [isEnabled]);
629
- (0, import_react2.useEffect)(() => {
847
+ (0, import_react5.useEffect)(() => {
630
848
  physicsConfigRef.current = physicsConfig;
631
849
  }, [physicsConfig]);
632
- (0, import_react2.useEffect)(() => {
850
+ (0, import_react5.useEffect)(() => {
633
851
  setMetricsRef.current = setMetrics;
634
852
  }, [setMetrics]);
635
- (0, import_react2.useEffect)(() => {
853
+ (0, import_react5.useEffect)(() => {
636
854
  if (!isMounted) return;
637
855
  const canvas = canvasRef.current;
638
856
  if (!canvas) return;
639
857
  const ctx = canvas.getContext("2d");
640
858
  if (!ctx) return;
641
- const resizeCanvas = () => {
642
- if (canvasRef.current) {
643
- const newWidth = window.innerWidth;
644
- const newHeight = window.innerHeight;
645
- const dpr = window.devicePixelRatio || 1;
646
- dprRef.current = dpr;
647
- canvasRef.current.width = newWidth * dpr;
648
- canvasRef.current.height = newHeight * dpr;
649
- canvasRef.current.style.width = `${newWidth}px`;
650
- canvasRef.current.style.height = `${newHeight}px`;
651
- }
652
- };
653
859
  resizeCanvas();
654
- const windowResizeObserver = new ResizeObserver(() => {
655
- resizeCanvas();
656
- });
657
- windowResizeObserver.observe(document.body);
860
+ snowflakesRef.current = [];
658
861
  const surfaceObserver = new ResizeObserver((entries) => {
659
862
  let needsUpdate = false;
660
863
  for (const entry of entries) {
@@ -667,7 +870,6 @@ function Snowfall() {
667
870
  initAccumulationWrapper();
668
871
  }
669
872
  });
670
- snowflakesRef.current = [];
671
873
  const initAccumulationWrapper = () => {
672
874
  const scanStart = performance.now();
673
875
  initializeAccumulation(accumulationRef.current, physicsConfigRef.current);
@@ -676,103 +878,48 @@ function Snowfall() {
676
878
  surfaceObserver.observe(el);
677
879
  }
678
880
  metricsRef.current.scanTime = performance.now() - scanStart;
881
+ markRectsDirty();
679
882
  };
680
883
  initAccumulationWrapper();
681
884
  requestAnimationFrame(() => {
682
885
  if (isMounted) setIsVisible(true);
683
886
  });
684
- let lastTime = 0;
685
- let lastMetricsUpdate = 0;
686
- let elementRects = [];
687
- const animate = (currentTime) => {
688
- if (lastTime === 0) {
689
- lastTime = currentTime;
690
- animationIdRef.current = requestAnimationFrame(animate);
691
- return;
692
- }
693
- const deltaTime = Math.min(currentTime - lastTime, 50);
694
- const now = performance.now();
695
- fpsFrames.current.push(now);
696
- fpsFrames.current = fpsFrames.current.filter((t) => now - t < 1e3);
697
- metricsRef.current.rafGap = currentTime - lastTime;
698
- lastTime = currentTime;
699
- const dt = deltaTime / 16.67;
700
- const frameStartTime = performance.now();
701
- const clearStart = performance.now();
702
- const dpr = dprRef.current;
703
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
704
- ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
705
- const scrollX = window.scrollX;
706
- const scrollY = window.scrollY;
707
- ctx.translate(-scrollX, -scrollY);
708
- metricsRef.current.clearTime = performance.now() - clearStart;
709
- const snowflakes = snowflakesRef.current;
710
- const rectStart = performance.now();
711
- elementRects = getElementRects(accumulationRef.current);
712
- metricsRef.current.rectUpdateTime = performance.now() - rectStart;
713
- const physicsStart = performance.now();
714
- meltAndSmoothAccumulation(elementRects, physicsConfigRef.current, dt);
715
- updateSnowflakes(
716
- snowflakes,
717
- elementRects,
718
- physicsConfigRef.current,
719
- dt,
720
- document.documentElement.scrollWidth,
721
- document.documentElement.scrollHeight
722
- );
723
- metricsRef.current.physicsTime = performance.now() - physicsStart;
724
- const drawStart = performance.now();
725
- drawSnowflakes(ctx, snowflakes);
726
- if (isEnabledRef.current && snowflakes.length < physicsConfigRef.current.MAX_FLAKES) {
727
- const currentFps = fpsFrames.current.length;
728
- const shouldSpawn = currentFps >= 40 || Math.random() < 0.2;
729
- if (shouldSpawn) {
730
- const isBackground = Math.random() < 0.4;
731
- snowflakes.push(createSnowflake(document.documentElement.scrollWidth, physicsConfigRef.current, isBackground));
732
- }
733
- }
734
- const viewportWidth = window.innerWidth;
735
- const viewportHeight = window.innerHeight;
736
- const visibleRects = elementRects.filter(
737
- ({ rect }) => rect.right >= 0 && rect.left <= viewportWidth && rect.bottom >= 0 && rect.top <= viewportHeight
738
- );
739
- if (visibleRects.length > 0) {
740
- drawAccumulations(ctx, visibleRects, scrollX, scrollY);
741
- drawSideAccumulations(ctx, visibleRects, scrollX, scrollY);
742
- }
743
- metricsRef.current.drawTime = performance.now() - drawStart;
744
- metricsRef.current.frameTime = performance.now() - frameStartTime;
745
- if (currentTime - lastMetricsUpdate > 500) {
746
- setMetricsRef.current({
747
- fps: fpsFrames.current.length,
748
- frameTime: metricsRef.current.frameTime,
749
- scanTime: metricsRef.current.scanTime,
750
- rectUpdateTime: metricsRef.current.rectUpdateTime,
751
- surfaceCount: accumulationRef.current.size,
752
- flakeCount: snowflakes.length,
753
- maxFlakes: physicsConfigRef.current.MAX_FLAKES,
754
- rafGap: metricsRef.current.rafGap,
755
- clearTime: metricsRef.current.clearTime,
756
- physicsTime: metricsRef.current.physicsTime,
757
- drawTime: metricsRef.current.drawTime
758
- });
759
- lastMetricsUpdate = currentTime;
760
- }
761
- animationIdRef.current = requestAnimationFrame(animate);
762
- };
763
- animationIdRef.current = requestAnimationFrame(animate);
887
+ startAnimation();
764
888
  const handleResize = () => {
765
889
  resizeCanvas();
766
890
  accumulationRef.current.clear();
767
891
  initAccumulationWrapper();
892
+ markRectsDirty();
768
893
  };
769
894
  window.addEventListener("resize", handleResize);
770
- const checkInterval = setInterval(initAccumulationWrapper, 5e3);
895
+ const windowResizeObserver = new ResizeObserver(() => {
896
+ resizeCanvas();
897
+ });
898
+ windowResizeObserver.observe(document.body);
899
+ const mutationObserver = new MutationObserver((mutations) => {
900
+ let hasStructuralChange = false;
901
+ for (const mutation of mutations) {
902
+ if (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0) {
903
+ hasStructuralChange = true;
904
+ break;
905
+ }
906
+ }
907
+ if (hasStructuralChange) {
908
+ const scanStart = performance.now();
909
+ initializeAccumulation(accumulationRef.current, physicsConfigRef.current);
910
+ metricsRef.current.scanTime = performance.now() - scanStart;
911
+ markRectsDirty();
912
+ }
913
+ });
914
+ mutationObserver.observe(document.body, {
915
+ childList: true,
916
+ subtree: true
917
+ });
771
918
  return () => {
772
- cancelAnimationFrame(animationIdRef.current);
919
+ stopAnimation();
773
920
  window.removeEventListener("resize", handleResize);
774
- clearInterval(checkInterval);
775
921
  windowResizeObserver.disconnect();
922
+ mutationObserver.disconnect();
776
923
  surfaceObserver.disconnect();
777
924
  };
778
925
  }, [isMounted]);
@@ -797,14 +944,14 @@ function Snowfall() {
797
944
  ) });
798
945
  }
799
946
 
800
- // src/DebugPanel.tsx
801
- var import_react3 = require("react");
947
+ // src/components/DebugPanel.tsx
948
+ var import_react6 = require("react");
802
949
  var import_jsx_runtime3 = require("react/jsx-runtime");
803
950
  function DebugPanel({ defaultOpen = true }) {
804
951
  const { debugMode, toggleDebug, metrics } = useSnowfall();
805
- const [isMinimized, setIsMinimized] = (0, import_react3.useState)(!defaultOpen);
806
- const [copied, setCopied] = (0, import_react3.useState)(false);
807
- (0, import_react3.useEffect)(() => {
952
+ const [isMinimized, setIsMinimized] = (0, import_react6.useState)(!defaultOpen);
953
+ const [copied, setCopied] = (0, import_react6.useState)(false);
954
+ (0, import_react6.useEffect)(() => {
808
955
  const handleKeyDown = (e) => {
809
956
  if (e.shiftKey && e.key === "D") {
810
957
  toggleDebug();