@hdcodedev/snowfall 1.0.13 → 1.0.14

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
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  // src/components/Snowfall.tsx
4
- import { useEffect, useRef as useRef4, useState as useState2 } from "react";
4
+ import { useEffect as useEffect2, useRef as useRef4, useState as useState2 } from "react";
5
5
 
6
6
  // src/components/SnowfallProvider.tsx
7
7
  import { createContext, useContext, useState } from "react";
@@ -25,8 +25,9 @@ var DEFAULT_PHYSICS = {
25
25
  MAX: 1.6
26
26
  },
27
27
  MAX_SURFACES: 15,
28
- COLLISION_CHECK_RATE: 0.3
28
+ COLLISION_CHECK_RATE: 0.3,
29
29
  // 30% of snowflakes check collisions per frame
30
+ MAX_RENDER_DPR: 1.25
30
31
  };
31
32
  var SnowfallContext = createContext(void 0);
32
33
  function SnowfallProvider({ children, initialDebug = false, initialEnabled = true }) {
@@ -352,7 +353,7 @@ var updateSnowflakePosition = (flake, dt) => {
352
353
  flake.x += (flake.wind + Math.sin(flake.wobble) * 0.5) * dt;
353
354
  flake.y += (flake.speed + Math.cos(flake.wobble * 0.5) * 0.1) * dt;
354
355
  };
355
- var checkSideCollision = (flake, flakeViewportX, flakeViewportY, rect, acc, config) => {
356
+ var checkSideCollision = (flakeViewportX, flakeViewportY, rect, acc, config) => {
356
357
  const isInVerticalBounds = flakeViewportY >= rect.top && flakeViewportY <= rect.bottom;
357
358
  if (!isInVerticalBounds || acc.maxSideHeight <= 0) {
358
359
  return false;
@@ -432,7 +433,7 @@ var updateSnowflakes = (snowflakes, elementRects, config, dt, worldWidth, worldH
432
433
  const { rect, acc } = item;
433
434
  const isBottom = acc.type === VAL_BOTTOM;
434
435
  if (!landed && !isBottom) {
435
- landed = checkSideCollision(flake, flakeViewportX, flakeViewportY, rect, acc, config);
436
+ landed = checkSideCollision(flakeViewportX, flakeViewportY, rect, acc, config);
436
437
  if (landed) break;
437
438
  }
438
439
  if (!landed) {
@@ -534,11 +535,11 @@ import { useRef as useRef2, useCallback as useCallback2 } from "react";
534
535
  function useSnowfallCanvas() {
535
536
  const canvasRef = useRef2(null);
536
537
  const dprRef = useRef2(1);
537
- const resizeCanvas = useCallback2(() => {
538
+ const resizeCanvas = useCallback2((maxRenderDpr = Number.POSITIVE_INFINITY) => {
538
539
  if (canvasRef.current) {
539
540
  const newWidth = window.innerWidth;
540
541
  const newHeight = window.innerHeight;
541
- const dpr = window.devicePixelRatio || 1;
542
+ const dpr = Math.min(window.devicePixelRatio || 1, Math.max(1, maxRenderDpr));
542
543
  dprRef.current = dpr;
543
544
  canvasRef.current.width = newWidth * dpr;
544
545
  canvasRef.current.height = newHeight * dpr;
@@ -554,25 +555,46 @@ function useSnowfallCanvas() {
554
555
  }
555
556
 
556
557
  // src/hooks/useAnimationLoop.ts
557
- import { useRef as useRef3, useCallback as useCallback3 } from "react";
558
+ import { useEffect, useRef as useRef3, useCallback as useCallback3 } from "react";
558
559
 
559
560
  // src/core/draw.ts
560
561
  var ACC_FILL_STYLE = "rgba(255, 255, 255, 0.95)";
561
562
  var ACC_SHADOW_COLOR = "rgba(200, 230, 255, 0.6)";
563
+ var OPACITY_BUCKETS2 = [0.3, 0.5, 0.7, 0.9];
562
564
  var drawSnowflakes = (ctx, flakes) => {
563
565
  if (flakes.length === 0) return;
564
566
  ctx.fillStyle = "#FFFFFF";
565
- for (const flake of flakes) {
566
- if (!flake.isBackground) {
567
- ctx.globalAlpha = flake.glowOpacity;
568
- ctx.beginPath();
567
+ for (const alpha of OPACITY_BUCKETS2) {
568
+ let hasPath = false;
569
+ for (const flake of flakes) {
570
+ if (flake.isBackground || flake.glowOpacity !== alpha) continue;
571
+ if (!hasPath) {
572
+ ctx.globalAlpha = alpha;
573
+ ctx.beginPath();
574
+ hasPath = true;
575
+ }
576
+ ctx.moveTo(flake.x + flake.glowRadius, flake.y);
569
577
  ctx.arc(flake.x, flake.y, flake.glowRadius, 0, TAU);
578
+ }
579
+ if (hasPath) {
580
+ ctx.fill();
581
+ }
582
+ }
583
+ for (const alpha of OPACITY_BUCKETS2) {
584
+ let hasPath = false;
585
+ for (const flake of flakes) {
586
+ if (flake.opacity !== alpha) continue;
587
+ if (!hasPath) {
588
+ ctx.globalAlpha = alpha;
589
+ ctx.beginPath();
590
+ hasPath = true;
591
+ }
592
+ ctx.moveTo(flake.x + flake.radius, flake.y);
593
+ ctx.arc(flake.x, flake.y, flake.radius, 0, TAU);
594
+ }
595
+ if (hasPath) {
570
596
  ctx.fill();
571
597
  }
572
- ctx.globalAlpha = flake.opacity;
573
- ctx.beginPath();
574
- ctx.arc(flake.x, flake.y, flake.radius, 0, TAU);
575
- ctx.fill();
576
598
  }
577
599
  ctx.globalAlpha = 1;
578
600
  };
@@ -668,9 +690,12 @@ var drawSideAccumulations = (ctx, elementRects, scrollX, scrollY) => {
668
690
  // src/hooks/useAnimationLoop.ts
669
691
  function useAnimationLoop(params) {
670
692
  const animationIdRef = useRef3(0);
693
+ const animateRef = useRef3(() => {
694
+ });
671
695
  const lastTimeRef = useRef3(0);
672
696
  const lastMetricsUpdateRef = useRef3(0);
673
697
  const elementRectsRef = useRef3([]);
698
+ const visibleRectsRef = useRef3([]);
674
699
  const dirtyRectsRef = useRef3(true);
675
700
  const animate = useCallback3((currentTime) => {
676
701
  const {
@@ -688,17 +713,17 @@ function useAnimationLoop(params) {
688
713
  } = params;
689
714
  const canvas = canvasRef.current;
690
715
  if (!canvas) {
691
- animationIdRef.current = requestAnimationFrame(animate);
716
+ animationIdRef.current = requestAnimationFrame(animateRef.current);
692
717
  return;
693
718
  }
694
719
  const ctx = canvas.getContext("2d");
695
720
  if (!ctx) {
696
- animationIdRef.current = requestAnimationFrame(animate);
721
+ animationIdRef.current = requestAnimationFrame(animateRef.current);
697
722
  return;
698
723
  }
699
724
  if (lastTimeRef.current === 0) {
700
725
  lastTimeRef.current = currentTime;
701
- animationIdRef.current = requestAnimationFrame(animate);
726
+ animationIdRef.current = requestAnimationFrame(animateRef.current);
702
727
  return;
703
728
  }
704
729
  const deltaTime = Math.min(currentTime - lastTimeRef.current, 50);
@@ -707,8 +732,8 @@ function useAnimationLoop(params) {
707
732
  metricsRef.current.rafGap = currentTime - lastTimeRef.current;
708
733
  lastTimeRef.current = currentTime;
709
734
  const dt = deltaTime / 16.67;
710
- const frameStartTime = performance.now();
711
- const clearStart = performance.now();
735
+ const frameStartTime = now;
736
+ const clearStart = now;
712
737
  const dpr = dprRef.current;
713
738
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
714
739
  ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
@@ -725,30 +750,42 @@ function useAnimationLoop(params) {
725
750
  }
726
751
  const physicsStart = performance.now();
727
752
  meltAndSmoothAccumulation(elementRectsRef.current, physicsConfigRef.current, dt);
753
+ const docEl = document.documentElement;
754
+ const worldWidth = docEl.scrollWidth;
755
+ const worldHeight = docEl.scrollHeight;
728
756
  updateSnowflakes(
729
757
  snowflakes,
730
758
  elementRectsRef.current,
731
759
  physicsConfigRef.current,
732
760
  dt,
733
- document.documentElement.scrollWidth,
734
- document.documentElement.scrollHeight
761
+ worldWidth,
762
+ worldHeight
735
763
  );
736
764
  metricsRef.current.physicsTime = performance.now() - physicsStart;
737
765
  const drawStart = performance.now();
738
766
  drawSnowflakes(ctx, snowflakes);
739
767
  if (isEnabledRef.current && snowflakes.length < physicsConfigRef.current.MAX_FLAKES) {
768
+ const minFlakeFloor = Math.min(80, physicsConfigRef.current.MAX_FLAKES);
769
+ const shouldForceSpawn = snowflakes.length < minFlakeFloor;
740
770
  const currentFps = getCurrentFps();
741
- const shouldSpawn = currentFps >= 40 || Math.random() < 0.2;
771
+ const isVisible = document.visibilityState === "visible";
772
+ const isUnderFpsThreshold = isVisible && currentFps > 0 && currentFps < 40;
773
+ const shouldSpawn = shouldForceSpawn || !isUnderFpsThreshold || Math.random() < 0.2;
742
774
  if (shouldSpawn) {
743
775
  const isBackground = Math.random() < 0.4;
744
- snowflakes.push(createSnowflake(document.documentElement.scrollWidth, physicsConfigRef.current, isBackground));
776
+ snowflakes.push(createSnowflake(worldWidth, physicsConfigRef.current, isBackground));
745
777
  }
746
778
  }
747
779
  const viewportWidth = window.innerWidth;
748
780
  const viewportHeight = window.innerHeight;
749
- const visibleRects = elementRectsRef.current.filter(
750
- ({ rect }) => rect.right >= 0 && rect.left <= viewportWidth && rect.bottom >= 0 && rect.top <= viewportHeight
751
- );
781
+ const visibleRects = visibleRectsRef.current;
782
+ visibleRects.length = 0;
783
+ for (const item of elementRectsRef.current) {
784
+ const { rect } = item;
785
+ if (rect.right >= 0 && rect.left <= viewportWidth && rect.bottom >= 0 && rect.top <= viewportHeight) {
786
+ visibleRects.push(item);
787
+ }
788
+ }
752
789
  if (visibleRects.length > 0) {
753
790
  drawAccumulations(ctx, visibleRects, scrollX, scrollY);
754
791
  drawSideAccumulations(ctx, visibleRects, scrollX, scrollY);
@@ -763,13 +800,16 @@ function useAnimationLoop(params) {
763
800
  ));
764
801
  lastMetricsUpdateRef.current = currentTime;
765
802
  }
766
- animationIdRef.current = requestAnimationFrame(animate);
803
+ animationIdRef.current = requestAnimationFrame(animateRef.current);
767
804
  }, [params]);
805
+ useEffect(() => {
806
+ animateRef.current = animate;
807
+ }, [animate]);
768
808
  const start = useCallback3(() => {
769
809
  lastTimeRef.current = 0;
770
810
  lastMetricsUpdateRef.current = 0;
771
- animationIdRef.current = requestAnimationFrame(animate);
772
- }, [animate]);
811
+ animationIdRef.current = requestAnimationFrame(animateRef.current);
812
+ }, []);
773
813
  const stop = useCallback3(() => {
774
814
  cancelAnimationFrame(animationIdRef.current);
775
815
  }, []);
@@ -809,26 +849,31 @@ function Snowfall() {
809
849
  buildMetrics,
810
850
  setMetricsRef
811
851
  });
812
- useEffect(() => {
852
+ useEffect2(() => {
813
853
  requestAnimationFrame(() => setIsMounted(true));
814
854
  }, []);
815
- useEffect(() => {
855
+ useEffect2(() => {
816
856
  isEnabledRef.current = isEnabled;
817
857
  }, [isEnabled]);
818
- useEffect(() => {
858
+ useEffect2(() => {
819
859
  physicsConfigRef.current = physicsConfig;
820
- }, [physicsConfig]);
821
- useEffect(() => {
860
+ if (isMounted) {
861
+ resizeCanvas(physicsConfig.MAX_RENDER_DPR);
862
+ }
863
+ }, [isMounted, physicsConfig, resizeCanvas]);
864
+ useEffect2(() => {
822
865
  setMetricsRef.current = setMetrics;
823
866
  }, [setMetrics]);
824
- useEffect(() => {
867
+ useEffect2(() => {
825
868
  if (!isMounted) return;
826
869
  const canvas = canvasRef.current;
827
870
  if (!canvas) return;
828
871
  const ctx = canvas.getContext("2d");
829
872
  if (!ctx) return;
830
- resizeCanvas();
873
+ resizeCanvas(physicsConfigRef.current.MAX_RENDER_DPR);
831
874
  snowflakesRef.current = [];
875
+ let isUnmounted = false;
876
+ let scheduledInitFrame = 0;
832
877
  const surfaceObserver = new ResizeObserver((entries) => {
833
878
  let needsUpdate = false;
834
879
  for (const entry of entries) {
@@ -838,10 +883,11 @@ function Snowfall() {
838
883
  }
839
884
  }
840
885
  if (needsUpdate) {
841
- initAccumulationWrapper();
886
+ scheduleAccumulationInit();
842
887
  }
843
888
  });
844
889
  const initAccumulationWrapper = () => {
890
+ if (isUnmounted) return;
845
891
  const scanStart = performance.now();
846
892
  initializeAccumulation(accumulationRef.current, physicsConfigRef.current);
847
893
  surfaceObserver.disconnect();
@@ -851,22 +897,25 @@ function Snowfall() {
851
897
  metricsRef.current.scanTime = performance.now() - scanStart;
852
898
  markRectsDirty();
853
899
  };
900
+ const scheduleAccumulationInit = () => {
901
+ if (scheduledInitFrame !== 0 || isUnmounted) return;
902
+ scheduledInitFrame = requestAnimationFrame(() => {
903
+ scheduledInitFrame = 0;
904
+ initAccumulationWrapper();
905
+ });
906
+ };
854
907
  initAccumulationWrapper();
855
908
  requestAnimationFrame(() => {
856
- if (isMounted) setIsVisible(true);
909
+ if (!isUnmounted && isMounted) setIsVisible(true);
857
910
  });
858
911
  startAnimation();
859
912
  const handleResize = () => {
860
- resizeCanvas();
913
+ resizeCanvas(physicsConfigRef.current.MAX_RENDER_DPR);
861
914
  accumulationRef.current.clear();
862
915
  initAccumulationWrapper();
863
916
  markRectsDirty();
864
917
  };
865
918
  window.addEventListener("resize", handleResize);
866
- const windowResizeObserver = new ResizeObserver(() => {
867
- resizeCanvas();
868
- });
869
- windowResizeObserver.observe(document.body);
870
919
  const mutationObserver = new MutationObserver((mutations) => {
871
920
  let hasStructuralChange = false;
872
921
  for (const mutation of mutations) {
@@ -876,10 +925,7 @@ function Snowfall() {
876
925
  }
877
926
  }
878
927
  if (hasStructuralChange) {
879
- const scanStart = performance.now();
880
- initializeAccumulation(accumulationRef.current, physicsConfigRef.current);
881
- metricsRef.current.scanTime = performance.now() - scanStart;
882
- markRectsDirty();
928
+ scheduleAccumulationInit();
883
929
  }
884
930
  });
885
931
  mutationObserver.observe(document.body, {
@@ -887,9 +933,12 @@ function Snowfall() {
887
933
  subtree: true
888
934
  });
889
935
  return () => {
936
+ isUnmounted = true;
937
+ if (scheduledInitFrame !== 0) {
938
+ cancelAnimationFrame(scheduledInitFrame);
939
+ }
890
940
  stopAnimation();
891
941
  window.removeEventListener("resize", handleResize);
892
- windowResizeObserver.disconnect();
893
942
  mutationObserver.disconnect();
894
943
  surfaceObserver.disconnect();
895
944
  };
@@ -916,13 +965,13 @@ function Snowfall() {
916
965
  }
917
966
 
918
967
  // src/components/DebugPanel.tsx
919
- import { useEffect as useEffect2, useState as useState3 } from "react";
968
+ import { useEffect as useEffect3, useState as useState3 } from "react";
920
969
  import { Fragment as Fragment2, jsx as jsx3, jsxs } from "react/jsx-runtime";
921
970
  function DebugPanel({ defaultOpen = true }) {
922
971
  const { debugMode, toggleDebug, metrics } = useSnowfall();
923
972
  const [isMinimized, setIsMinimized] = useState3(!defaultOpen);
924
973
  const [copied, setCopied] = useState3(false);
925
- useEffect2(() => {
974
+ useEffect3(() => {
926
975
  const handleKeyDown = (e) => {
927
976
  if (e.shiftKey && e.key === "D") {
928
977
  toggleDebug();