@hdcodedev/snowfall 1.0.10 → 1.0.11
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.d.mts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +351 -204
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +340 -193
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
// src/Snowfall.tsx
|
|
4
|
-
import { useEffect, useRef, useState as useState2 } from "react";
|
|
3
|
+
// src/components/Snowfall.tsx
|
|
4
|
+
import { useEffect, useRef as useRef4, useState as useState2 } from "react";
|
|
5
5
|
|
|
6
|
-
// src/SnowfallProvider.tsx
|
|
6
|
+
// src/components/SnowfallProvider.tsx
|
|
7
7
|
import { createContext, useContext, useState } from "react";
|
|
8
8
|
import { jsx } from "react/jsx-runtime";
|
|
9
9
|
var DEFAULT_PHYSICS = {
|
|
@@ -24,7 +24,9 @@ var DEFAULT_PHYSICS = {
|
|
|
24
24
|
MIN: 0.5,
|
|
25
25
|
MAX: 1.6
|
|
26
26
|
},
|
|
27
|
-
MAX_SURFACES: 15
|
|
27
|
+
MAX_SURFACES: 15,
|
|
28
|
+
COLLISION_CHECK_RATE: 0.3
|
|
29
|
+
// 30% of snowflakes check collisions per frame
|
|
28
30
|
};
|
|
29
31
|
var SnowfallContext = createContext(void 0);
|
|
30
32
|
function SnowfallProvider({ children, initialDebug = false }) {
|
|
@@ -79,7 +81,7 @@ function useSnowfall() {
|
|
|
79
81
|
return context;
|
|
80
82
|
}
|
|
81
83
|
|
|
82
|
-
// src/
|
|
84
|
+
// src/core/constants.ts
|
|
83
85
|
var ATTR_SNOWFALL = "data-snowfall";
|
|
84
86
|
var VAL_IGNORE = "ignore";
|
|
85
87
|
var VAL_TOP = "top";
|
|
@@ -90,7 +92,7 @@ var ROLE_BANNER = "banner";
|
|
|
90
92
|
var ROLE_CONTENTINFO = "contentinfo";
|
|
91
93
|
var TAU = Math.PI * 2;
|
|
92
94
|
|
|
93
|
-
// src/
|
|
95
|
+
// src/core/dom.ts
|
|
94
96
|
var BOTTOM_TAGS = [TAG_HEADER];
|
|
95
97
|
var BOTTOM_ROLES = [ROLE_BANNER];
|
|
96
98
|
var AUTO_DETECT_TAGS = [TAG_HEADER, TAG_FOOTER, "article", "section", "aside", "nav"];
|
|
@@ -172,7 +174,7 @@ var getElementRects = (accumulationMap) => {
|
|
|
172
174
|
return elementRects;
|
|
173
175
|
};
|
|
174
176
|
|
|
175
|
-
// src/
|
|
177
|
+
// src/core/physics.ts
|
|
176
178
|
var OPACITY_BUCKETS = [0.3, 0.5, 0.7, 0.9];
|
|
177
179
|
var quantizeOpacity = (opacity) => {
|
|
178
180
|
return OPACITY_BUCKETS.reduce(
|
|
@@ -219,6 +221,7 @@ var createSnowflake = (worldWidth, config, isBackground = false) => {
|
|
|
219
221
|
const opacity = quantizeOpacity(rawOpacity);
|
|
220
222
|
const rawGlowOpacity = opacity * 0.2;
|
|
221
223
|
const glowOpacity = quantizeOpacity(rawGlowOpacity);
|
|
224
|
+
const initialWobble = noise.wobblePhase * TAU;
|
|
222
225
|
return {
|
|
223
226
|
x,
|
|
224
227
|
y: window.scrollY - 5,
|
|
@@ -228,7 +231,7 @@ var createSnowflake = (worldWidth, config, isBackground = false) => {
|
|
|
228
231
|
wind: (noise.wind - 0.5) * profile.windScale,
|
|
229
232
|
opacity,
|
|
230
233
|
glowOpacity,
|
|
231
|
-
wobble:
|
|
234
|
+
wobble: initialWobble,
|
|
232
235
|
wobbleSpeed: noise.wobbleSpeed * profile.wobbleScale + profile.wobbleBase,
|
|
233
236
|
sizeRatio,
|
|
234
237
|
isBackground
|
|
@@ -344,84 +347,101 @@ var accumulateSide = (sideArray, rectHeight, localY, maxSideHeight, borderRadius
|
|
|
344
347
|
}
|
|
345
348
|
return newMax;
|
|
346
349
|
};
|
|
350
|
+
var updateSnowflakePosition = (flake, dt) => {
|
|
351
|
+
flake.wobble += flake.wobbleSpeed * dt;
|
|
352
|
+
flake.x += (flake.wind + Math.sin(flake.wobble) * 0.5) * dt;
|
|
353
|
+
flake.y += (flake.speed + Math.cos(flake.wobble * 0.5) * 0.1) * dt;
|
|
354
|
+
};
|
|
355
|
+
var checkSideCollision = (flake, flakeViewportX, flakeViewportY, rect, acc, config) => {
|
|
356
|
+
const isInVerticalBounds = flakeViewportY >= rect.top && flakeViewportY <= rect.bottom;
|
|
357
|
+
if (!isInVerticalBounds || acc.maxSideHeight <= 0) {
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
const localY = Math.floor(flakeViewportY - rect.top);
|
|
361
|
+
const borderRadius = acc.borderRadius;
|
|
362
|
+
const isInTopCorner = localY < borderRadius;
|
|
363
|
+
const isInBottomCorner = localY > rect.height - borderRadius;
|
|
364
|
+
const isCorner = borderRadius > 0 && (isInTopCorner || isInBottomCorner);
|
|
365
|
+
if (isCorner) {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
if (flakeViewportX >= rect.left - 5 && flakeViewportX < rect.left + 3) {
|
|
369
|
+
acc.leftMax = accumulateSide(acc.leftSide, rect.height, localY, acc.maxSideHeight, borderRadius, config, acc.leftMax);
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
if (flakeViewportX > rect.right - 3 && flakeViewportX <= rect.right + 5) {
|
|
373
|
+
acc.rightMax = accumulateSide(acc.rightSide, rect.height, localY, acc.maxSideHeight, borderRadius, config, acc.rightMax);
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
376
|
+
return false;
|
|
377
|
+
};
|
|
378
|
+
var checkSurfaceCollision = (flake, flakeViewportX, flakeViewportY, rect, acc, isBottom, config) => {
|
|
379
|
+
if (flakeViewportX < rect.left || flakeViewportX > rect.right) {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
const localX = Math.floor(flakeViewportX - rect.left);
|
|
383
|
+
const currentHeight = acc.heights[localX] || 0;
|
|
384
|
+
const maxHeight = acc.maxHeights[localX] || 5;
|
|
385
|
+
const surfaceY = isBottom ? rect.bottom - currentHeight : rect.top - currentHeight;
|
|
386
|
+
if (flakeViewportY < surfaceY || flakeViewportY >= surfaceY + 10 || currentHeight >= maxHeight) {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
const shouldAccumulate2 = isBottom ? Math.random() < 0.15 : true;
|
|
390
|
+
if (shouldAccumulate2) {
|
|
391
|
+
const baseSpread = Math.ceil(flake.radius);
|
|
392
|
+
const spread = baseSpread + Math.floor(Math.random() * 2);
|
|
393
|
+
const accumRate = isBottom ? config.ACCUMULATION.BOTTOM_RATE : config.ACCUMULATION.TOP_RATE;
|
|
394
|
+
const centerOffset = Math.floor(Math.random() * 3) - 1;
|
|
395
|
+
for (let dx = -spread; dx <= spread; dx++) {
|
|
396
|
+
if (Math.random() < 0.15) continue;
|
|
397
|
+
const idx = localX + dx + centerOffset;
|
|
398
|
+
if (idx >= 0 && idx < acc.heights.length) {
|
|
399
|
+
const dist = Math.abs(dx);
|
|
400
|
+
const pixelMax = acc.maxHeights[idx] || 5;
|
|
401
|
+
const normDist = dist / spread;
|
|
402
|
+
const falloff = (Math.cos(normDist * Math.PI) + 1) / 2;
|
|
403
|
+
const baseAdd = 0.3 * falloff;
|
|
404
|
+
const randomFactor = 0.8 + Math.random() * 0.4;
|
|
405
|
+
const addHeight = baseAdd * randomFactor * accumRate;
|
|
406
|
+
if (acc.heights[idx] < pixelMax && addHeight > 0) {
|
|
407
|
+
acc.heights[idx] = Math.min(pixelMax, acc.heights[idx] + addHeight);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (isBottom) {
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return !isBottom;
|
|
416
|
+
};
|
|
417
|
+
var shouldRemoveSnowflake = (flake, landed, worldWidth, worldHeight) => {
|
|
418
|
+
return landed || flake.y > worldHeight + 10 || flake.x < -20 || flake.x > worldWidth + 20;
|
|
419
|
+
};
|
|
347
420
|
var updateSnowflakes = (snowflakes, elementRects, config, dt, worldWidth, worldHeight) => {
|
|
348
421
|
const scrollX = window.scrollX;
|
|
349
422
|
const scrollY = window.scrollY;
|
|
350
423
|
for (let i = snowflakes.length - 1; i >= 0; i--) {
|
|
351
424
|
const flake = snowflakes[i];
|
|
352
|
-
flake
|
|
353
|
-
flake.x += (flake.wind + Math.sin(flake.wobble) * 0.5) * dt;
|
|
354
|
-
flake.y += (flake.speed + Math.cos(flake.wobble * 0.5) * 0.1) * dt;
|
|
425
|
+
updateSnowflakePosition(flake, dt);
|
|
355
426
|
let landed = false;
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const isBottom = acc.type === VAL_BOTTOM;
|
|
427
|
+
const shouldCheckCollision = Math.random() < config.COLLISION_CHECK_RATE;
|
|
428
|
+
if (shouldCheckCollision) {
|
|
359
429
|
const flakeViewportX = flake.x - scrollX;
|
|
360
430
|
const flakeViewportY = flake.y - scrollY;
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
const isInTopCorner = localY < borderRadius;
|
|
367
|
-
const isInBottomCorner = localY > rect.height - borderRadius;
|
|
368
|
-
const isCorner = borderRadius > 0 && (isInTopCorner || isInBottomCorner);
|
|
369
|
-
if (flakeViewportX >= rect.left - 5 && flakeViewportX < rect.left + 3) {
|
|
370
|
-
if (!isCorner) {
|
|
371
|
-
acc.leftMax = accumulateSide(acc.leftSide, rect.height, localY, acc.maxSideHeight, borderRadius, config, acc.leftMax);
|
|
372
|
-
landed = true;
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
if (!landed && flakeViewportX > rect.right - 3 && flakeViewportX <= rect.right + 5) {
|
|
376
|
-
if (!isCorner) {
|
|
377
|
-
acc.rightMax = accumulateSide(acc.rightSide, rect.height, localY, acc.maxSideHeight, borderRadius, config, acc.rightMax);
|
|
378
|
-
landed = true;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
431
|
+
for (const item of elementRects) {
|
|
432
|
+
const { rect, acc } = item;
|
|
433
|
+
const isBottom = acc.type === VAL_BOTTOM;
|
|
434
|
+
if (!landed && !isBottom) {
|
|
435
|
+
landed = checkSideCollision(flake, flakeViewportX, flakeViewportY, rect, acc, config);
|
|
381
436
|
if (landed) break;
|
|
382
437
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
const currentHeight = acc.heights[localX] || 0;
|
|
387
|
-
const maxHeight = acc.maxHeights[localX] || 5;
|
|
388
|
-
const surfaceY = isBottom ? rect.bottom - currentHeight : rect.top - currentHeight;
|
|
389
|
-
if (flakeViewportY >= surfaceY && flakeViewportY < surfaceY + 10 && currentHeight < maxHeight) {
|
|
390
|
-
const shouldAccumulate2 = isBottom ? Math.random() < 0.15 : true;
|
|
391
|
-
if (shouldAccumulate2) {
|
|
392
|
-
const baseSpread = Math.ceil(flake.radius);
|
|
393
|
-
const spread = baseSpread + Math.floor(Math.random() * 2);
|
|
394
|
-
const accumRate = isBottom ? config.ACCUMULATION.BOTTOM_RATE : config.ACCUMULATION.TOP_RATE;
|
|
395
|
-
const centerOffset = Math.floor(Math.random() * 3) - 1;
|
|
396
|
-
for (let dx = -spread; dx <= spread; dx++) {
|
|
397
|
-
if (Math.random() < 0.15) continue;
|
|
398
|
-
const idx = localX + dx + centerOffset;
|
|
399
|
-
if (idx >= 0 && idx < acc.heights.length) {
|
|
400
|
-
const dist = Math.abs(dx);
|
|
401
|
-
const pixelMax = acc.maxHeights[idx] || 5;
|
|
402
|
-
const normDist = dist / spread;
|
|
403
|
-
const falloff = (Math.cos(normDist * Math.PI) + 1) / 2;
|
|
404
|
-
const baseAdd = 0.3 * falloff;
|
|
405
|
-
const randomFactor = 0.8 + Math.random() * 0.4;
|
|
406
|
-
const addHeight = baseAdd * randomFactor * accumRate;
|
|
407
|
-
if (acc.heights[idx] < pixelMax && addHeight > 0) {
|
|
408
|
-
acc.heights[idx] = Math.min(pixelMax, acc.heights[idx] + addHeight);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
if (isBottom) {
|
|
413
|
-
landed = true;
|
|
414
|
-
break;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
if (!isBottom) {
|
|
418
|
-
landed = true;
|
|
419
|
-
break;
|
|
420
|
-
}
|
|
438
|
+
if (!landed) {
|
|
439
|
+
landed = checkSurfaceCollision(flake, flakeViewportX, flakeViewportY, rect, acc, isBottom, config);
|
|
440
|
+
if (landed) break;
|
|
421
441
|
}
|
|
422
442
|
}
|
|
423
443
|
}
|
|
424
|
-
if (
|
|
444
|
+
if (shouldRemoveSnowflake(flake, landed, worldWidth, worldHeight)) {
|
|
425
445
|
snowflakes.splice(i, 1);
|
|
426
446
|
}
|
|
427
447
|
}
|
|
@@ -458,7 +478,85 @@ var meltAndSmoothAccumulation = (elementRects, config, dt) => {
|
|
|
458
478
|
}
|
|
459
479
|
};
|
|
460
480
|
|
|
461
|
-
// src/
|
|
481
|
+
// src/hooks/usePerformanceMetrics.ts
|
|
482
|
+
import { useRef, useCallback } from "react";
|
|
483
|
+
function usePerformanceMetrics() {
|
|
484
|
+
const lastFpsSecondRef = useRef(0);
|
|
485
|
+
const framesInSecondRef = useRef(0);
|
|
486
|
+
const currentFpsRef = useRef(0);
|
|
487
|
+
const metricsRef = useRef({
|
|
488
|
+
scanTime: 0,
|
|
489
|
+
rectUpdateTime: 0,
|
|
490
|
+
frameTime: 0,
|
|
491
|
+
rafGap: 0,
|
|
492
|
+
clearTime: 0,
|
|
493
|
+
physicsTime: 0,
|
|
494
|
+
drawTime: 0
|
|
495
|
+
});
|
|
496
|
+
const updateFps = useCallback((now) => {
|
|
497
|
+
const currentSecond = Math.floor(now / 1e3);
|
|
498
|
+
if (currentSecond !== lastFpsSecondRef.current) {
|
|
499
|
+
currentFpsRef.current = framesInSecondRef.current;
|
|
500
|
+
framesInSecondRef.current = 1;
|
|
501
|
+
lastFpsSecondRef.current = currentSecond;
|
|
502
|
+
} else {
|
|
503
|
+
framesInSecondRef.current++;
|
|
504
|
+
}
|
|
505
|
+
}, []);
|
|
506
|
+
const getCurrentFps = useCallback(() => {
|
|
507
|
+
return currentFpsRef.current || framesInSecondRef.current;
|
|
508
|
+
}, []);
|
|
509
|
+
const buildMetrics = useCallback((surfaceCount, flakeCount, maxFlakes) => {
|
|
510
|
+
return {
|
|
511
|
+
fps: currentFpsRef.current || framesInSecondRef.current,
|
|
512
|
+
frameTime: metricsRef.current.frameTime,
|
|
513
|
+
scanTime: metricsRef.current.scanTime,
|
|
514
|
+
rectUpdateTime: metricsRef.current.rectUpdateTime,
|
|
515
|
+
surfaceCount,
|
|
516
|
+
flakeCount,
|
|
517
|
+
maxFlakes,
|
|
518
|
+
rafGap: metricsRef.current.rafGap,
|
|
519
|
+
clearTime: metricsRef.current.clearTime,
|
|
520
|
+
physicsTime: metricsRef.current.physicsTime,
|
|
521
|
+
drawTime: metricsRef.current.drawTime
|
|
522
|
+
};
|
|
523
|
+
}, []);
|
|
524
|
+
return {
|
|
525
|
+
metricsRef,
|
|
526
|
+
updateFps,
|
|
527
|
+
getCurrentFps,
|
|
528
|
+
buildMetrics
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// src/hooks/useSnowfallCanvas.ts
|
|
533
|
+
import { useRef as useRef2, useCallback as useCallback2 } from "react";
|
|
534
|
+
function useSnowfallCanvas() {
|
|
535
|
+
const canvasRef = useRef2(null);
|
|
536
|
+
const dprRef = useRef2(1);
|
|
537
|
+
const resizeCanvas = useCallback2(() => {
|
|
538
|
+
if (canvasRef.current) {
|
|
539
|
+
const newWidth = window.innerWidth;
|
|
540
|
+
const newHeight = window.innerHeight;
|
|
541
|
+
const dpr = window.devicePixelRatio || 1;
|
|
542
|
+
dprRef.current = dpr;
|
|
543
|
+
canvasRef.current.width = newWidth * dpr;
|
|
544
|
+
canvasRef.current.height = newHeight * dpr;
|
|
545
|
+
canvasRef.current.style.width = `${newWidth}px`;
|
|
546
|
+
canvasRef.current.style.height = `${newHeight}px`;
|
|
547
|
+
}
|
|
548
|
+
}, []);
|
|
549
|
+
return {
|
|
550
|
+
canvasRef,
|
|
551
|
+
dprRef,
|
|
552
|
+
resizeCanvas
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// src/hooks/useAnimationLoop.ts
|
|
557
|
+
import { useRef as useRef3, useCallback as useCallback3 } from "react";
|
|
558
|
+
|
|
559
|
+
// src/core/draw.ts
|
|
462
560
|
var ACC_FILL_STYLE = "rgba(255, 255, 255, 0.95)";
|
|
463
561
|
var ACC_SHADOW_COLOR = "rgba(200, 230, 255, 0.6)";
|
|
464
562
|
var drawSnowflakes = (ctx, flakes) => {
|
|
@@ -567,29 +665,149 @@ var drawSideAccumulations = (ctx, elementRects, scrollX, scrollY) => {
|
|
|
567
665
|
ctx.shadowBlur = 0;
|
|
568
666
|
};
|
|
569
667
|
|
|
570
|
-
// src/
|
|
668
|
+
// src/hooks/useAnimationLoop.ts
|
|
669
|
+
function useAnimationLoop(params) {
|
|
670
|
+
const animationIdRef = useRef3(0);
|
|
671
|
+
const lastTimeRef = useRef3(0);
|
|
672
|
+
const lastMetricsUpdateRef = useRef3(0);
|
|
673
|
+
const elementRectsRef = useRef3([]);
|
|
674
|
+
const dirtyRectsRef = useRef3(true);
|
|
675
|
+
const animate = useCallback3((currentTime) => {
|
|
676
|
+
const {
|
|
677
|
+
canvasRef,
|
|
678
|
+
dprRef,
|
|
679
|
+
snowflakesRef,
|
|
680
|
+
accumulationRef,
|
|
681
|
+
isEnabledRef,
|
|
682
|
+
physicsConfigRef,
|
|
683
|
+
metricsRef,
|
|
684
|
+
updateFps,
|
|
685
|
+
getCurrentFps,
|
|
686
|
+
buildMetrics,
|
|
687
|
+
setMetricsRef
|
|
688
|
+
} = params;
|
|
689
|
+
const canvas = canvasRef.current;
|
|
690
|
+
if (!canvas) {
|
|
691
|
+
animationIdRef.current = requestAnimationFrame(animate);
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
const ctx = canvas.getContext("2d");
|
|
695
|
+
if (!ctx) {
|
|
696
|
+
animationIdRef.current = requestAnimationFrame(animate);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
if (lastTimeRef.current === 0) {
|
|
700
|
+
lastTimeRef.current = currentTime;
|
|
701
|
+
animationIdRef.current = requestAnimationFrame(animate);
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
const deltaTime = Math.min(currentTime - lastTimeRef.current, 50);
|
|
705
|
+
const now = performance.now();
|
|
706
|
+
updateFps(now);
|
|
707
|
+
metricsRef.current.rafGap = currentTime - lastTimeRef.current;
|
|
708
|
+
lastTimeRef.current = currentTime;
|
|
709
|
+
const dt = deltaTime / 16.67;
|
|
710
|
+
const frameStartTime = performance.now();
|
|
711
|
+
const clearStart = performance.now();
|
|
712
|
+
const dpr = dprRef.current;
|
|
713
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
714
|
+
ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
|
|
715
|
+
const scrollX = window.scrollX;
|
|
716
|
+
const scrollY = window.scrollY;
|
|
717
|
+
ctx.translate(-scrollX, -scrollY);
|
|
718
|
+
metricsRef.current.clearTime = performance.now() - clearStart;
|
|
719
|
+
const snowflakes = snowflakesRef.current;
|
|
720
|
+
if (dirtyRectsRef.current) {
|
|
721
|
+
const rectStart = performance.now();
|
|
722
|
+
elementRectsRef.current = getElementRects(accumulationRef.current);
|
|
723
|
+
metricsRef.current.rectUpdateTime = performance.now() - rectStart;
|
|
724
|
+
dirtyRectsRef.current = false;
|
|
725
|
+
}
|
|
726
|
+
const physicsStart = performance.now();
|
|
727
|
+
meltAndSmoothAccumulation(elementRectsRef.current, physicsConfigRef.current, dt);
|
|
728
|
+
updateSnowflakes(
|
|
729
|
+
snowflakes,
|
|
730
|
+
elementRectsRef.current,
|
|
731
|
+
physicsConfigRef.current,
|
|
732
|
+
dt,
|
|
733
|
+
document.documentElement.scrollWidth,
|
|
734
|
+
document.documentElement.scrollHeight
|
|
735
|
+
);
|
|
736
|
+
metricsRef.current.physicsTime = performance.now() - physicsStart;
|
|
737
|
+
const drawStart = performance.now();
|
|
738
|
+
drawSnowflakes(ctx, snowflakes);
|
|
739
|
+
if (isEnabledRef.current && snowflakes.length < physicsConfigRef.current.MAX_FLAKES) {
|
|
740
|
+
const currentFps = getCurrentFps();
|
|
741
|
+
const shouldSpawn = currentFps >= 40 || Math.random() < 0.2;
|
|
742
|
+
if (shouldSpawn) {
|
|
743
|
+
const isBackground = Math.random() < 0.4;
|
|
744
|
+
snowflakes.push(createSnowflake(document.documentElement.scrollWidth, physicsConfigRef.current, isBackground));
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
const viewportWidth = window.innerWidth;
|
|
748
|
+
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
|
+
);
|
|
752
|
+
if (visibleRects.length > 0) {
|
|
753
|
+
drawAccumulations(ctx, visibleRects, scrollX, scrollY);
|
|
754
|
+
drawSideAccumulations(ctx, visibleRects, scrollX, scrollY);
|
|
755
|
+
}
|
|
756
|
+
metricsRef.current.drawTime = performance.now() - drawStart;
|
|
757
|
+
metricsRef.current.frameTime = performance.now() - frameStartTime;
|
|
758
|
+
if (currentTime - lastMetricsUpdateRef.current > 500) {
|
|
759
|
+
setMetricsRef.current(buildMetrics(
|
|
760
|
+
accumulationRef.current.size,
|
|
761
|
+
snowflakes.length,
|
|
762
|
+
physicsConfigRef.current.MAX_FLAKES
|
|
763
|
+
));
|
|
764
|
+
lastMetricsUpdateRef.current = currentTime;
|
|
765
|
+
}
|
|
766
|
+
animationIdRef.current = requestAnimationFrame(animate);
|
|
767
|
+
}, [params]);
|
|
768
|
+
const start = useCallback3(() => {
|
|
769
|
+
lastTimeRef.current = 0;
|
|
770
|
+
lastMetricsUpdateRef.current = 0;
|
|
771
|
+
animationIdRef.current = requestAnimationFrame(animate);
|
|
772
|
+
}, [animate]);
|
|
773
|
+
const stop = useCallback3(() => {
|
|
774
|
+
cancelAnimationFrame(animationIdRef.current);
|
|
775
|
+
}, []);
|
|
776
|
+
const markRectsDirty = useCallback3(() => {
|
|
777
|
+
dirtyRectsRef.current = true;
|
|
778
|
+
}, []);
|
|
779
|
+
return {
|
|
780
|
+
start,
|
|
781
|
+
stop,
|
|
782
|
+
markRectsDirty
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// src/components/Snowfall.tsx
|
|
571
787
|
import { Fragment, jsx as jsx2 } from "react/jsx-runtime";
|
|
572
788
|
function Snowfall() {
|
|
573
789
|
const { isEnabled, physicsConfig, setMetrics } = useSnowfall();
|
|
574
|
-
const isEnabledRef =
|
|
575
|
-
const physicsConfigRef =
|
|
576
|
-
const setMetricsRef =
|
|
790
|
+
const isEnabledRef = useRef4(isEnabled);
|
|
791
|
+
const physicsConfigRef = useRef4(physicsConfig);
|
|
792
|
+
const setMetricsRef = useRef4(setMetrics);
|
|
577
793
|
const [isMounted, setIsMounted] = useState2(false);
|
|
578
794
|
const [isVisible, setIsVisible] = useState2(false);
|
|
579
|
-
const
|
|
580
|
-
const
|
|
581
|
-
const
|
|
582
|
-
const
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
795
|
+
const snowflakesRef = useRef4([]);
|
|
796
|
+
const accumulationRef = useRef4(/* @__PURE__ */ new Map());
|
|
797
|
+
const { canvasRef, dprRef, resizeCanvas } = useSnowfallCanvas();
|
|
798
|
+
const { metricsRef, updateFps, getCurrentFps, buildMetrics } = usePerformanceMetrics();
|
|
799
|
+
const { start: startAnimation, stop: stopAnimation, markRectsDirty } = useAnimationLoop({
|
|
800
|
+
canvasRef,
|
|
801
|
+
dprRef,
|
|
802
|
+
snowflakesRef,
|
|
803
|
+
accumulationRef,
|
|
804
|
+
isEnabledRef,
|
|
805
|
+
physicsConfigRef,
|
|
806
|
+
metricsRef,
|
|
807
|
+
updateFps,
|
|
808
|
+
getCurrentFps,
|
|
809
|
+
buildMetrics,
|
|
810
|
+
setMetricsRef
|
|
593
811
|
});
|
|
594
812
|
useEffect(() => {
|
|
595
813
|
requestAnimationFrame(() => setIsMounted(true));
|
|
@@ -609,23 +827,8 @@ function Snowfall() {
|
|
|
609
827
|
if (!canvas) return;
|
|
610
828
|
const ctx = canvas.getContext("2d");
|
|
611
829
|
if (!ctx) return;
|
|
612
|
-
const resizeCanvas = () => {
|
|
613
|
-
if (canvasRef.current) {
|
|
614
|
-
const newWidth = window.innerWidth;
|
|
615
|
-
const newHeight = window.innerHeight;
|
|
616
|
-
const dpr = window.devicePixelRatio || 1;
|
|
617
|
-
dprRef.current = dpr;
|
|
618
|
-
canvasRef.current.width = newWidth * dpr;
|
|
619
|
-
canvasRef.current.height = newHeight * dpr;
|
|
620
|
-
canvasRef.current.style.width = `${newWidth}px`;
|
|
621
|
-
canvasRef.current.style.height = `${newHeight}px`;
|
|
622
|
-
}
|
|
623
|
-
};
|
|
624
830
|
resizeCanvas();
|
|
625
|
-
|
|
626
|
-
resizeCanvas();
|
|
627
|
-
});
|
|
628
|
-
windowResizeObserver.observe(document.body);
|
|
831
|
+
snowflakesRef.current = [];
|
|
629
832
|
const surfaceObserver = new ResizeObserver((entries) => {
|
|
630
833
|
let needsUpdate = false;
|
|
631
834
|
for (const entry of entries) {
|
|
@@ -638,7 +841,6 @@ function Snowfall() {
|
|
|
638
841
|
initAccumulationWrapper();
|
|
639
842
|
}
|
|
640
843
|
});
|
|
641
|
-
snowflakesRef.current = [];
|
|
642
844
|
const initAccumulationWrapper = () => {
|
|
643
845
|
const scanStart = performance.now();
|
|
644
846
|
initializeAccumulation(accumulationRef.current, physicsConfigRef.current);
|
|
@@ -647,103 +849,48 @@ function Snowfall() {
|
|
|
647
849
|
surfaceObserver.observe(el);
|
|
648
850
|
}
|
|
649
851
|
metricsRef.current.scanTime = performance.now() - scanStart;
|
|
852
|
+
markRectsDirty();
|
|
650
853
|
};
|
|
651
854
|
initAccumulationWrapper();
|
|
652
855
|
requestAnimationFrame(() => {
|
|
653
856
|
if (isMounted) setIsVisible(true);
|
|
654
857
|
});
|
|
655
|
-
|
|
656
|
-
let lastMetricsUpdate = 0;
|
|
657
|
-
let elementRects = [];
|
|
658
|
-
const animate = (currentTime) => {
|
|
659
|
-
if (lastTime === 0) {
|
|
660
|
-
lastTime = currentTime;
|
|
661
|
-
animationIdRef.current = requestAnimationFrame(animate);
|
|
662
|
-
return;
|
|
663
|
-
}
|
|
664
|
-
const deltaTime = Math.min(currentTime - lastTime, 50);
|
|
665
|
-
const now = performance.now();
|
|
666
|
-
fpsFrames.current.push(now);
|
|
667
|
-
fpsFrames.current = fpsFrames.current.filter((t) => now - t < 1e3);
|
|
668
|
-
metricsRef.current.rafGap = currentTime - lastTime;
|
|
669
|
-
lastTime = currentTime;
|
|
670
|
-
const dt = deltaTime / 16.67;
|
|
671
|
-
const frameStartTime = performance.now();
|
|
672
|
-
const clearStart = performance.now();
|
|
673
|
-
const dpr = dprRef.current;
|
|
674
|
-
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
675
|
-
ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
|
|
676
|
-
const scrollX = window.scrollX;
|
|
677
|
-
const scrollY = window.scrollY;
|
|
678
|
-
ctx.translate(-scrollX, -scrollY);
|
|
679
|
-
metricsRef.current.clearTime = performance.now() - clearStart;
|
|
680
|
-
const snowflakes = snowflakesRef.current;
|
|
681
|
-
const rectStart = performance.now();
|
|
682
|
-
elementRects = getElementRects(accumulationRef.current);
|
|
683
|
-
metricsRef.current.rectUpdateTime = performance.now() - rectStart;
|
|
684
|
-
const physicsStart = performance.now();
|
|
685
|
-
meltAndSmoothAccumulation(elementRects, physicsConfigRef.current, dt);
|
|
686
|
-
updateSnowflakes(
|
|
687
|
-
snowflakes,
|
|
688
|
-
elementRects,
|
|
689
|
-
physicsConfigRef.current,
|
|
690
|
-
dt,
|
|
691
|
-
document.documentElement.scrollWidth,
|
|
692
|
-
document.documentElement.scrollHeight
|
|
693
|
-
);
|
|
694
|
-
metricsRef.current.physicsTime = performance.now() - physicsStart;
|
|
695
|
-
const drawStart = performance.now();
|
|
696
|
-
drawSnowflakes(ctx, snowflakes);
|
|
697
|
-
if (isEnabledRef.current && snowflakes.length < physicsConfigRef.current.MAX_FLAKES) {
|
|
698
|
-
const currentFps = fpsFrames.current.length;
|
|
699
|
-
const shouldSpawn = currentFps >= 40 || Math.random() < 0.2;
|
|
700
|
-
if (shouldSpawn) {
|
|
701
|
-
const isBackground = Math.random() < 0.4;
|
|
702
|
-
snowflakes.push(createSnowflake(document.documentElement.scrollWidth, physicsConfigRef.current, isBackground));
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
const viewportWidth = window.innerWidth;
|
|
706
|
-
const viewportHeight = window.innerHeight;
|
|
707
|
-
const visibleRects = elementRects.filter(
|
|
708
|
-
({ rect }) => rect.right >= 0 && rect.left <= viewportWidth && rect.bottom >= 0 && rect.top <= viewportHeight
|
|
709
|
-
);
|
|
710
|
-
if (visibleRects.length > 0) {
|
|
711
|
-
drawAccumulations(ctx, visibleRects, scrollX, scrollY);
|
|
712
|
-
drawSideAccumulations(ctx, visibleRects, scrollX, scrollY);
|
|
713
|
-
}
|
|
714
|
-
metricsRef.current.drawTime = performance.now() - drawStart;
|
|
715
|
-
metricsRef.current.frameTime = performance.now() - frameStartTime;
|
|
716
|
-
if (currentTime - lastMetricsUpdate > 500) {
|
|
717
|
-
setMetricsRef.current({
|
|
718
|
-
fps: fpsFrames.current.length,
|
|
719
|
-
frameTime: metricsRef.current.frameTime,
|
|
720
|
-
scanTime: metricsRef.current.scanTime,
|
|
721
|
-
rectUpdateTime: metricsRef.current.rectUpdateTime,
|
|
722
|
-
surfaceCount: accumulationRef.current.size,
|
|
723
|
-
flakeCount: snowflakes.length,
|
|
724
|
-
maxFlakes: physicsConfigRef.current.MAX_FLAKES,
|
|
725
|
-
rafGap: metricsRef.current.rafGap,
|
|
726
|
-
clearTime: metricsRef.current.clearTime,
|
|
727
|
-
physicsTime: metricsRef.current.physicsTime,
|
|
728
|
-
drawTime: metricsRef.current.drawTime
|
|
729
|
-
});
|
|
730
|
-
lastMetricsUpdate = currentTime;
|
|
731
|
-
}
|
|
732
|
-
animationIdRef.current = requestAnimationFrame(animate);
|
|
733
|
-
};
|
|
734
|
-
animationIdRef.current = requestAnimationFrame(animate);
|
|
858
|
+
startAnimation();
|
|
735
859
|
const handleResize = () => {
|
|
736
860
|
resizeCanvas();
|
|
737
861
|
accumulationRef.current.clear();
|
|
738
862
|
initAccumulationWrapper();
|
|
863
|
+
markRectsDirty();
|
|
739
864
|
};
|
|
740
865
|
window.addEventListener("resize", handleResize);
|
|
741
|
-
const
|
|
866
|
+
const windowResizeObserver = new ResizeObserver(() => {
|
|
867
|
+
resizeCanvas();
|
|
868
|
+
});
|
|
869
|
+
windowResizeObserver.observe(document.body);
|
|
870
|
+
const mutationObserver = new MutationObserver((mutations) => {
|
|
871
|
+
let hasStructuralChange = false;
|
|
872
|
+
for (const mutation of mutations) {
|
|
873
|
+
if (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0) {
|
|
874
|
+
hasStructuralChange = true;
|
|
875
|
+
break;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
if (hasStructuralChange) {
|
|
879
|
+
const scanStart = performance.now();
|
|
880
|
+
initializeAccumulation(accumulationRef.current, physicsConfigRef.current);
|
|
881
|
+
metricsRef.current.scanTime = performance.now() - scanStart;
|
|
882
|
+
markRectsDirty();
|
|
883
|
+
}
|
|
884
|
+
});
|
|
885
|
+
mutationObserver.observe(document.body, {
|
|
886
|
+
childList: true,
|
|
887
|
+
subtree: true
|
|
888
|
+
});
|
|
742
889
|
return () => {
|
|
743
|
-
|
|
890
|
+
stopAnimation();
|
|
744
891
|
window.removeEventListener("resize", handleResize);
|
|
745
|
-
clearInterval(checkInterval);
|
|
746
892
|
windowResizeObserver.disconnect();
|
|
893
|
+
mutationObserver.disconnect();
|
|
747
894
|
surfaceObserver.disconnect();
|
|
748
895
|
};
|
|
749
896
|
}, [isMounted]);
|
|
@@ -768,7 +915,7 @@ function Snowfall() {
|
|
|
768
915
|
) });
|
|
769
916
|
}
|
|
770
917
|
|
|
771
|
-
// src/DebugPanel.tsx
|
|
918
|
+
// src/components/DebugPanel.tsx
|
|
772
919
|
import { useEffect as useEffect2, useState as useState3 } from "react";
|
|
773
920
|
import { Fragment as Fragment2, jsx as jsx3, jsxs } from "react/jsx-runtime";
|
|
774
921
|
function DebugPanel({ defaultOpen = true }) {
|