@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/README.md +27 -21
- package/dist/index.d.mts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +353 -206
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +342 -195
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
|
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)(
|
|
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/
|
|
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/
|
|
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/
|
|
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:
|
|
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
|
|
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
|
-
|
|
386
|
-
|
|
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
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
|
|
414
|
-
|
|
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 (
|
|
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/
|
|
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/
|
|
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,
|
|
604
|
-
const physicsConfigRef = (0,
|
|
605
|
-
const setMetricsRef = (0,
|
|
606
|
-
const [isMounted, setIsMounted] = (0,
|
|
607
|
-
const [isVisible, setIsVisible] = (0,
|
|
608
|
-
const
|
|
609
|
-
const
|
|
610
|
-
const
|
|
611
|
-
const
|
|
612
|
-
const
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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,
|
|
841
|
+
(0, import_react5.useEffect)(() => {
|
|
624
842
|
requestAnimationFrame(() => setIsMounted(true));
|
|
625
843
|
}, []);
|
|
626
|
-
(0,
|
|
844
|
+
(0, import_react5.useEffect)(() => {
|
|
627
845
|
isEnabledRef.current = isEnabled;
|
|
628
846
|
}, [isEnabled]);
|
|
629
|
-
(0,
|
|
847
|
+
(0, import_react5.useEffect)(() => {
|
|
630
848
|
physicsConfigRef.current = physicsConfig;
|
|
631
849
|
}, [physicsConfig]);
|
|
632
|
-
(0,
|
|
850
|
+
(0, import_react5.useEffect)(() => {
|
|
633
851
|
setMetricsRef.current = setMetrics;
|
|
634
852
|
}, [setMetrics]);
|
|
635
|
-
(0,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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,
|
|
806
|
-
const [copied, setCopied] = (0,
|
|
807
|
-
(0,
|
|
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();
|