@hdcodedev/snowfall 1.0.7 → 1.0.9

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
@@ -173,6 +173,12 @@ var getElementRects = (accumulationMap) => {
173
173
  };
174
174
 
175
175
  // src/utils/snowfall/physics.ts
176
+ var OPACITY_BUCKETS = [0.3, 0.5, 0.7, 0.9];
177
+ var quantizeOpacity = (opacity) => {
178
+ return OPACITY_BUCKETS.reduce(
179
+ (prev, curr) => Math.abs(curr - opacity) < Math.abs(prev - opacity) ? curr : prev
180
+ );
181
+ };
176
182
  var createSnowflake = (worldWidth, config, isBackground = false) => {
177
183
  const x = Math.random() * worldWidth;
178
184
  const dna = Math.random();
@@ -208,13 +214,20 @@ var createSnowflake = (worldWidth, config, isBackground = false) => {
208
214
  wobbleScale: 0.02
209
215
  };
210
216
  const radius = profile.sizeMin + sizeRatio * profile.sizeRange;
217
+ const glowRadius = radius * 1.5;
218
+ const rawOpacity = profile.opacityBase + sizeRatio * profile.opacityScale;
219
+ const opacity = quantizeOpacity(rawOpacity);
220
+ const rawGlowOpacity = opacity * 0.2;
221
+ const glowOpacity = quantizeOpacity(rawGlowOpacity);
211
222
  return {
212
223
  x,
213
224
  y: window.scrollY - 5,
214
225
  radius,
226
+ glowRadius,
215
227
  speed: radius * profile.speedScale + noise.speed * profile.noiseSpeedScale + profile.speedBase,
216
228
  wind: (noise.wind - 0.5) * profile.windScale,
217
- opacity: profile.opacityBase + sizeRatio * profile.opacityScale,
229
+ opacity,
230
+ glowOpacity,
218
231
  wobble: noise.wobblePhase * TAU,
219
232
  wobbleSpeed: noise.wobbleSpeed * profile.wobbleScale + profile.wobbleBase,
220
233
  sizeRatio,
@@ -303,6 +316,8 @@ var initializeAccumulation = (accumulationMap, config) => {
303
316
  leftSide: existing?.leftSide.length === height ? existing.leftSide : new Array(height).fill(0),
304
317
  rightSide: existing?.rightSide.length === height ? existing.rightSide : new Array(height).fill(0),
305
318
  maxSideHeight: isBottom ? 0 : config.MAX_DEPTH.SIDE,
319
+ leftMax: existing?.leftSide.length === height ? existing.leftMax : 0,
320
+ rightMax: existing?.rightSide.length === height ? existing.rightMax : 0,
306
321
  borderRadius,
307
322
  curveOffsets: calculateCurveOffsets(width, borderRadius, isBottom),
308
323
  sideGravityMultipliers: calculateGravityMultipliers(height),
@@ -310,9 +325,10 @@ var initializeAccumulation = (accumulationMap, config) => {
310
325
  });
311
326
  });
312
327
  };
313
- var accumulateSide = (sideArray, rectHeight, localY, maxSideHeight, borderRadius, config) => {
328
+ var accumulateSide = (sideArray, rectHeight, localY, maxSideHeight, borderRadius, config, currentMax) => {
314
329
  const spread = 4;
315
330
  const addHeight = config.ACCUMULATION.SIDE_RATE * (0.8 + Math.random() * 0.4);
331
+ let newMax = currentMax;
316
332
  for (let dy = -spread; dy <= spread; dy++) {
317
333
  const y = localY + dy;
318
334
  if (y >= 0 && y < sideArray.length) {
@@ -321,9 +337,12 @@ var accumulateSide = (sideArray, rectHeight, localY, maxSideHeight, borderRadius
321
337
  if (borderRadius > 0 && (inTop || inBottom)) continue;
322
338
  const normalizedDist = Math.abs(dy) / spread;
323
339
  const falloff = (Math.cos(normalizedDist * Math.PI) + 1) / 2;
324
- sideArray[y] = Math.min(maxSideHeight, sideArray[y] + addHeight * falloff);
340
+ const newHeight = Math.min(maxSideHeight, sideArray[y] + addHeight * falloff);
341
+ sideArray[y] = newHeight;
342
+ if (newHeight > newMax) newMax = newHeight;
325
343
  }
326
344
  }
345
+ return newMax;
327
346
  };
328
347
  var updateSnowflakes = (snowflakes, elementRects, config, dt, worldWidth, worldHeight) => {
329
348
  const scrollX = window.scrollX;
@@ -349,13 +368,13 @@ var updateSnowflakes = (snowflakes, elementRects, config, dt, worldWidth, worldH
349
368
  const isCorner = borderRadius > 0 && (isInTopCorner || isInBottomCorner);
350
369
  if (flakeViewportX >= rect.left - 5 && flakeViewportX < rect.left + 3) {
351
370
  if (!isCorner) {
352
- accumulateSide(acc.leftSide, rect.height, localY, acc.maxSideHeight, borderRadius, config);
371
+ acc.leftMax = accumulateSide(acc.leftSide, rect.height, localY, acc.maxSideHeight, borderRadius, config, acc.leftMax);
353
372
  landed = true;
354
373
  }
355
374
  }
356
375
  if (!landed && flakeViewportX > rect.right - 3 && flakeViewportX <= rect.right + 5) {
357
376
  if (!isCorner) {
358
- accumulateSide(acc.rightSide, rect.height, localY, acc.maxSideHeight, borderRadius, config);
377
+ acc.rightMax = accumulateSide(acc.rightSide, rect.height, localY, acc.maxSideHeight, borderRadius, config, acc.rightMax);
359
378
  landed = true;
360
379
  }
361
380
  }
@@ -422,10 +441,20 @@ var meltAndSmoothAccumulation = (elementRects, config, dt) => {
422
441
  for (let i = 0; i < acc.heights.length; i++) {
423
442
  if (acc.heights[i] > 0) acc.heights[i] = Math.max(0, acc.heights[i] - meltRate);
424
443
  }
444
+ let leftMax = 0;
445
+ let rightMax = 0;
425
446
  for (let i = 0; i < acc.leftSide.length; i++) {
426
- if (acc.leftSide[i] > 0) acc.leftSide[i] = Math.max(0, acc.leftSide[i] - meltRate);
427
- if (acc.rightSide[i] > 0) acc.rightSide[i] = Math.max(0, acc.rightSide[i] - meltRate);
447
+ if (acc.leftSide[i] > 0) {
448
+ acc.leftSide[i] = Math.max(0, acc.leftSide[i] - meltRate);
449
+ if (acc.leftSide[i] > leftMax) leftMax = acc.leftSide[i];
450
+ }
451
+ if (acc.rightSide[i] > 0) {
452
+ acc.rightSide[i] = Math.max(0, acc.rightSide[i] - meltRate);
453
+ if (acc.rightSide[i] > rightMax) rightMax = acc.rightSide[i];
454
+ }
428
455
  }
456
+ acc.leftMax = leftMax;
457
+ acc.rightMax = rightMax;
429
458
  }
430
459
  };
431
460
 
@@ -436,43 +465,40 @@ var drawSnowflakes = (ctx, flakes) => {
436
465
  if (flakes.length === 0) return;
437
466
  ctx.fillStyle = "#FFFFFF";
438
467
  for (const flake of flakes) {
468
+ if (!flake.isBackground) {
469
+ ctx.globalAlpha = flake.glowOpacity;
470
+ ctx.beginPath();
471
+ ctx.arc(flake.x, flake.y, flake.glowRadius, 0, TAU);
472
+ ctx.fill();
473
+ }
439
474
  ctx.globalAlpha = flake.opacity;
440
475
  ctx.beginPath();
441
476
  ctx.arc(flake.x, flake.y, flake.radius, 0, TAU);
442
477
  ctx.fill();
443
478
  }
444
- for (const flake of flakes) {
445
- if (flake.isBackground) continue;
446
- ctx.globalAlpha = flake.opacity * 0.2;
447
- ctx.beginPath();
448
- ctx.arc(flake.x, flake.y, flake.radius * 1.5, 0, TAU);
449
- ctx.fill();
450
- }
451
479
  ctx.globalAlpha = 1;
452
480
  };
453
- var drawAccumulations = (ctx, elementRects) => {
481
+ var drawAccumulations = (ctx, elementRects, scrollX, scrollY) => {
454
482
  ctx.fillStyle = ACC_FILL_STYLE;
455
483
  ctx.shadowColor = ACC_SHADOW_COLOR;
456
484
  ctx.shadowBlur = 4;
457
485
  ctx.shadowOffsetY = -1;
458
486
  ctx.globalAlpha = 1;
459
- const currentScrollX = window.scrollX;
460
- const currentScrollY = window.scrollY;
461
487
  ctx.beginPath();
462
488
  for (const item of elementRects) {
463
489
  const { rect, acc } = item;
464
490
  if (!acc.heights.some((h) => h > 0.1)) continue;
465
- const dx = currentScrollX;
466
- const dy = currentScrollY;
467
491
  const isBottom = acc.type === VAL_BOTTOM;
468
492
  const baseY = isBottom ? rect.bottom - 1 : rect.top + 1;
493
+ const worldLeft = rect.left + scrollX;
494
+ const worldBaseY = baseY + scrollY;
469
495
  let first = true;
470
496
  const step = 2;
471
497
  const len = acc.heights.length;
472
498
  for (let x = 0; x < len; x += step) {
473
499
  const height = acc.heights[x] || 0;
474
- const px = rect.left + x + dx;
475
- const py = baseY - height + (acc.curveOffsets[x] || 0) + dy;
500
+ const px = worldLeft + x;
501
+ const py = worldBaseY - height + (acc.curveOffsets[x] || 0);
476
502
  if (first) {
477
503
  ctx.moveTo(px, py);
478
504
  first = false;
@@ -483,60 +509,59 @@ var drawAccumulations = (ctx, elementRects) => {
483
509
  if ((len - 1) % step !== 0) {
484
510
  const x = len - 1;
485
511
  const height = acc.heights[x] || 0;
486
- const px = rect.left + x + dx;
487
- const py = baseY - height + (acc.curveOffsets[x] || 0) + dy;
512
+ const px = worldLeft + x;
513
+ const py = worldBaseY - height + (acc.curveOffsets[x] || 0);
488
514
  ctx.lineTo(px, py);
489
515
  }
490
516
  for (let x = len - 1; x >= 0; x -= step) {
491
- const px = rect.left + x + dx;
492
- const py = baseY + (acc.curveOffsets[x] || 0) + dy;
517
+ const px = worldLeft + x;
518
+ const py = worldBaseY + (acc.curveOffsets[x] || 0);
493
519
  ctx.lineTo(px, py);
494
520
  }
495
521
  const startX = 0;
496
- const startPx = rect.left + startX + dx;
497
- const startPy = baseY + (acc.curveOffsets[startX] || 0) + dy;
522
+ const startPx = worldLeft + startX;
523
+ const startPy = worldBaseY + (acc.curveOffsets[startX] || 0);
498
524
  ctx.lineTo(startPx, startPy);
499
525
  }
500
526
  ctx.fill();
501
527
  ctx.shadowBlur = 0;
502
528
  ctx.shadowOffsetY = 0;
503
529
  };
504
- var drawSideAccumulations = (ctx, elementRects) => {
530
+ var drawSideAccumulations = (ctx, elementRects, scrollX, scrollY) => {
505
531
  ctx.fillStyle = ACC_FILL_STYLE;
506
532
  ctx.shadowColor = ACC_SHADOW_COLOR;
507
533
  ctx.shadowBlur = 3;
508
534
  ctx.globalAlpha = 1;
509
- const currentScrollX = window.scrollX;
510
- const currentScrollY = window.scrollY;
511
535
  ctx.beginPath();
512
536
  const drawSide = (sideArray, isLeft, multipliers, rect, dx, dy) => {
513
537
  const baseX = isLeft ? rect.left : rect.right;
514
- ctx.moveTo(baseX + dx, rect.top + dy);
538
+ const worldBaseX = baseX + dx;
539
+ const worldTop = rect.top + dy;
540
+ const worldBottom = rect.bottom + dy;
541
+ ctx.moveTo(worldBaseX, worldTop);
515
542
  for (let y = 0; y < sideArray.length; y += 2) {
516
543
  const width = sideArray[y] || 0;
517
544
  const nextY = Math.min(y + 2, sideArray.length - 1);
518
545
  const nextWidth = sideArray[nextY] || 0;
519
546
  const gravityMultiplier = multipliers[y] || 0;
520
- const py = rect.top + y + dy;
521
- const px = (isLeft ? baseX - width * gravityMultiplier : baseX + width * gravityMultiplier) + dx;
522
- const ny = rect.top + nextY + dy;
547
+ const py = worldTop + y;
548
+ const px = isLeft ? worldBaseX - width * gravityMultiplier : worldBaseX + width * gravityMultiplier;
549
+ const ny = worldTop + nextY;
523
550
  const nGravityMultiplier = multipliers[nextY] || 0;
524
- const nx = (isLeft ? baseX - nextWidth * nGravityMultiplier : baseX + nextWidth * nGravityMultiplier) + dx;
551
+ const nx = isLeft ? worldBaseX - nextWidth * nGravityMultiplier : worldBaseX + nextWidth * nGravityMultiplier;
525
552
  ctx.lineTo(px, py);
526
553
  ctx.lineTo(nx, ny);
527
554
  }
528
- ctx.lineTo(baseX + dx, rect.bottom + dy);
555
+ ctx.lineTo(worldBaseX, worldBottom);
529
556
  };
530
557
  for (const item of elementRects) {
531
558
  const { rect, acc } = item;
532
559
  if (acc.maxSideHeight === 0) continue;
533
- const hasLeftSnow = acc.leftSide.some((h) => h > 0.3);
534
- const hasRightSnow = acc.rightSide.some((h) => h > 0.3);
560
+ const hasLeftSnow = acc.leftMax > 0.3;
561
+ const hasRightSnow = acc.rightMax > 0.3;
535
562
  if (!hasLeftSnow && !hasRightSnow) continue;
536
- const dx = currentScrollX;
537
- const dy = currentScrollY;
538
- if (hasLeftSnow) drawSide(acc.leftSide, true, acc.sideGravityMultipliers, rect, dx, dy);
539
- if (hasRightSnow) drawSide(acc.rightSide, false, acc.sideGravityMultipliers, rect, dx, dy);
563
+ if (hasLeftSnow) drawSide(acc.leftSide, true, acc.sideGravityMultipliers, rect, scrollX, scrollY);
564
+ if (hasRightSnow) drawSide(acc.rightSide, false, acc.sideGravityMultipliers, rect, scrollX, scrollY);
540
565
  }
541
566
  ctx.fill();
542
567
  ctx.shadowBlur = 0;
@@ -555,6 +580,7 @@ function Snowfall() {
555
580
  const snowflakesRef = useRef([]);
556
581
  const accumulationRef = useRef(/* @__PURE__ */ new Map());
557
582
  const animationIdRef = useRef(0);
583
+ const dprRef = useRef(1);
558
584
  const fpsFrames = useRef([]);
559
585
  const metricsRef = useRef({
560
586
  scanTime: 0,
@@ -588,6 +614,7 @@ function Snowfall() {
588
614
  const newWidth = window.innerWidth;
589
615
  const newHeight = window.innerHeight;
590
616
  const dpr = window.devicePixelRatio || 1;
617
+ dprRef.current = dpr;
591
618
  canvasRef.current.width = newWidth * dpr;
592
619
  canvasRef.current.height = newHeight * dpr;
593
620
  canvasRef.current.style.width = `${newWidth}px`;
@@ -643,7 +670,7 @@ function Snowfall() {
643
670
  const dt = deltaTime / 16.67;
644
671
  const frameStartTime = performance.now();
645
672
  const clearStart = performance.now();
646
- const dpr = window.devicePixelRatio || 1;
673
+ const dpr = dprRef.current;
647
674
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
648
675
  ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
649
676
  const scrollX = window.scrollX;
@@ -668,11 +695,22 @@ function Snowfall() {
668
695
  const drawStart = performance.now();
669
696
  drawSnowflakes(ctx, snowflakes);
670
697
  if (isEnabledRef.current && snowflakes.length < physicsConfigRef.current.MAX_FLAKES) {
671
- const isBackground = Math.random() < 0.4;
672
- snowflakes.push(createSnowflake(document.documentElement.scrollWidth, physicsConfigRef.current, isBackground));
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);
673
713
  }
674
- drawAccumulations(ctx, elementRects);
675
- drawSideAccumulations(ctx, elementRects);
676
714
  metricsRef.current.drawTime = performance.now() - drawStart;
677
715
  metricsRef.current.frameTime = performance.now() - frameStartTime;
678
716
  if (currentTime - lastMetricsUpdate > 500) {