@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.js CHANGED
@@ -202,6 +202,12 @@ var getElementRects = (accumulationMap) => {
202
202
  };
203
203
 
204
204
  // src/utils/snowfall/physics.ts
205
+ var OPACITY_BUCKETS = [0.3, 0.5, 0.7, 0.9];
206
+ var quantizeOpacity = (opacity) => {
207
+ return OPACITY_BUCKETS.reduce(
208
+ (prev, curr) => Math.abs(curr - opacity) < Math.abs(prev - opacity) ? curr : prev
209
+ );
210
+ };
205
211
  var createSnowflake = (worldWidth, config, isBackground = false) => {
206
212
  const x = Math.random() * worldWidth;
207
213
  const dna = Math.random();
@@ -237,13 +243,20 @@ var createSnowflake = (worldWidth, config, isBackground = false) => {
237
243
  wobbleScale: 0.02
238
244
  };
239
245
  const radius = profile.sizeMin + sizeRatio * profile.sizeRange;
246
+ const glowRadius = radius * 1.5;
247
+ const rawOpacity = profile.opacityBase + sizeRatio * profile.opacityScale;
248
+ const opacity = quantizeOpacity(rawOpacity);
249
+ const rawGlowOpacity = opacity * 0.2;
250
+ const glowOpacity = quantizeOpacity(rawGlowOpacity);
240
251
  return {
241
252
  x,
242
253
  y: window.scrollY - 5,
243
254
  radius,
255
+ glowRadius,
244
256
  speed: radius * profile.speedScale + noise.speed * profile.noiseSpeedScale + profile.speedBase,
245
257
  wind: (noise.wind - 0.5) * profile.windScale,
246
- opacity: profile.opacityBase + sizeRatio * profile.opacityScale,
258
+ opacity,
259
+ glowOpacity,
247
260
  wobble: noise.wobblePhase * TAU,
248
261
  wobbleSpeed: noise.wobbleSpeed * profile.wobbleScale + profile.wobbleBase,
249
262
  sizeRatio,
@@ -332,6 +345,8 @@ var initializeAccumulation = (accumulationMap, config) => {
332
345
  leftSide: existing?.leftSide.length === height ? existing.leftSide : new Array(height).fill(0),
333
346
  rightSide: existing?.rightSide.length === height ? existing.rightSide : new Array(height).fill(0),
334
347
  maxSideHeight: isBottom ? 0 : config.MAX_DEPTH.SIDE,
348
+ leftMax: existing?.leftSide.length === height ? existing.leftMax : 0,
349
+ rightMax: existing?.rightSide.length === height ? existing.rightMax : 0,
335
350
  borderRadius,
336
351
  curveOffsets: calculateCurveOffsets(width, borderRadius, isBottom),
337
352
  sideGravityMultipliers: calculateGravityMultipliers(height),
@@ -339,9 +354,10 @@ var initializeAccumulation = (accumulationMap, config) => {
339
354
  });
340
355
  });
341
356
  };
342
- var accumulateSide = (sideArray, rectHeight, localY, maxSideHeight, borderRadius, config) => {
357
+ var accumulateSide = (sideArray, rectHeight, localY, maxSideHeight, borderRadius, config, currentMax) => {
343
358
  const spread = 4;
344
359
  const addHeight = config.ACCUMULATION.SIDE_RATE * (0.8 + Math.random() * 0.4);
360
+ let newMax = currentMax;
345
361
  for (let dy = -spread; dy <= spread; dy++) {
346
362
  const y = localY + dy;
347
363
  if (y >= 0 && y < sideArray.length) {
@@ -350,9 +366,12 @@ var accumulateSide = (sideArray, rectHeight, localY, maxSideHeight, borderRadius
350
366
  if (borderRadius > 0 && (inTop || inBottom)) continue;
351
367
  const normalizedDist = Math.abs(dy) / spread;
352
368
  const falloff = (Math.cos(normalizedDist * Math.PI) + 1) / 2;
353
- sideArray[y] = Math.min(maxSideHeight, sideArray[y] + addHeight * falloff);
369
+ const newHeight = Math.min(maxSideHeight, sideArray[y] + addHeight * falloff);
370
+ sideArray[y] = newHeight;
371
+ if (newHeight > newMax) newMax = newHeight;
354
372
  }
355
373
  }
374
+ return newMax;
356
375
  };
357
376
  var updateSnowflakes = (snowflakes, elementRects, config, dt, worldWidth, worldHeight) => {
358
377
  const scrollX = window.scrollX;
@@ -378,13 +397,13 @@ var updateSnowflakes = (snowflakes, elementRects, config, dt, worldWidth, worldH
378
397
  const isCorner = borderRadius > 0 && (isInTopCorner || isInBottomCorner);
379
398
  if (flakeViewportX >= rect.left - 5 && flakeViewportX < rect.left + 3) {
380
399
  if (!isCorner) {
381
- accumulateSide(acc.leftSide, rect.height, localY, acc.maxSideHeight, borderRadius, config);
400
+ acc.leftMax = accumulateSide(acc.leftSide, rect.height, localY, acc.maxSideHeight, borderRadius, config, acc.leftMax);
382
401
  landed = true;
383
402
  }
384
403
  }
385
404
  if (!landed && flakeViewportX > rect.right - 3 && flakeViewportX <= rect.right + 5) {
386
405
  if (!isCorner) {
387
- accumulateSide(acc.rightSide, rect.height, localY, acc.maxSideHeight, borderRadius, config);
406
+ acc.rightMax = accumulateSide(acc.rightSide, rect.height, localY, acc.maxSideHeight, borderRadius, config, acc.rightMax);
388
407
  landed = true;
389
408
  }
390
409
  }
@@ -451,10 +470,20 @@ var meltAndSmoothAccumulation = (elementRects, config, dt) => {
451
470
  for (let i = 0; i < acc.heights.length; i++) {
452
471
  if (acc.heights[i] > 0) acc.heights[i] = Math.max(0, acc.heights[i] - meltRate);
453
472
  }
473
+ let leftMax = 0;
474
+ let rightMax = 0;
454
475
  for (let i = 0; i < acc.leftSide.length; i++) {
455
- if (acc.leftSide[i] > 0) acc.leftSide[i] = Math.max(0, acc.leftSide[i] - meltRate);
456
- if (acc.rightSide[i] > 0) acc.rightSide[i] = Math.max(0, acc.rightSide[i] - meltRate);
476
+ if (acc.leftSide[i] > 0) {
477
+ acc.leftSide[i] = Math.max(0, acc.leftSide[i] - meltRate);
478
+ if (acc.leftSide[i] > leftMax) leftMax = acc.leftSide[i];
479
+ }
480
+ if (acc.rightSide[i] > 0) {
481
+ acc.rightSide[i] = Math.max(0, acc.rightSide[i] - meltRate);
482
+ if (acc.rightSide[i] > rightMax) rightMax = acc.rightSide[i];
483
+ }
457
484
  }
485
+ acc.leftMax = leftMax;
486
+ acc.rightMax = rightMax;
458
487
  }
459
488
  };
460
489
 
@@ -465,43 +494,40 @@ var drawSnowflakes = (ctx, flakes) => {
465
494
  if (flakes.length === 0) return;
466
495
  ctx.fillStyle = "#FFFFFF";
467
496
  for (const flake of flakes) {
497
+ if (!flake.isBackground) {
498
+ ctx.globalAlpha = flake.glowOpacity;
499
+ ctx.beginPath();
500
+ ctx.arc(flake.x, flake.y, flake.glowRadius, 0, TAU);
501
+ ctx.fill();
502
+ }
468
503
  ctx.globalAlpha = flake.opacity;
469
504
  ctx.beginPath();
470
505
  ctx.arc(flake.x, flake.y, flake.radius, 0, TAU);
471
506
  ctx.fill();
472
507
  }
473
- for (const flake of flakes) {
474
- if (flake.isBackground) continue;
475
- ctx.globalAlpha = flake.opacity * 0.2;
476
- ctx.beginPath();
477
- ctx.arc(flake.x, flake.y, flake.radius * 1.5, 0, TAU);
478
- ctx.fill();
479
- }
480
508
  ctx.globalAlpha = 1;
481
509
  };
482
- var drawAccumulations = (ctx, elementRects) => {
510
+ var drawAccumulations = (ctx, elementRects, scrollX, scrollY) => {
483
511
  ctx.fillStyle = ACC_FILL_STYLE;
484
512
  ctx.shadowColor = ACC_SHADOW_COLOR;
485
513
  ctx.shadowBlur = 4;
486
514
  ctx.shadowOffsetY = -1;
487
515
  ctx.globalAlpha = 1;
488
- const currentScrollX = window.scrollX;
489
- const currentScrollY = window.scrollY;
490
516
  ctx.beginPath();
491
517
  for (const item of elementRects) {
492
518
  const { rect, acc } = item;
493
519
  if (!acc.heights.some((h) => h > 0.1)) continue;
494
- const dx = currentScrollX;
495
- const dy = currentScrollY;
496
520
  const isBottom = acc.type === VAL_BOTTOM;
497
521
  const baseY = isBottom ? rect.bottom - 1 : rect.top + 1;
522
+ const worldLeft = rect.left + scrollX;
523
+ const worldBaseY = baseY + scrollY;
498
524
  let first = true;
499
525
  const step = 2;
500
526
  const len = acc.heights.length;
501
527
  for (let x = 0; x < len; x += step) {
502
528
  const height = acc.heights[x] || 0;
503
- const px = rect.left + x + dx;
504
- const py = baseY - height + (acc.curveOffsets[x] || 0) + dy;
529
+ const px = worldLeft + x;
530
+ const py = worldBaseY - height + (acc.curveOffsets[x] || 0);
505
531
  if (first) {
506
532
  ctx.moveTo(px, py);
507
533
  first = false;
@@ -512,60 +538,59 @@ var drawAccumulations = (ctx, elementRects) => {
512
538
  if ((len - 1) % step !== 0) {
513
539
  const x = len - 1;
514
540
  const height = acc.heights[x] || 0;
515
- const px = rect.left + x + dx;
516
- const py = baseY - height + (acc.curveOffsets[x] || 0) + dy;
541
+ const px = worldLeft + x;
542
+ const py = worldBaseY - height + (acc.curveOffsets[x] || 0);
517
543
  ctx.lineTo(px, py);
518
544
  }
519
545
  for (let x = len - 1; x >= 0; x -= step) {
520
- const px = rect.left + x + dx;
521
- const py = baseY + (acc.curveOffsets[x] || 0) + dy;
546
+ const px = worldLeft + x;
547
+ const py = worldBaseY + (acc.curveOffsets[x] || 0);
522
548
  ctx.lineTo(px, py);
523
549
  }
524
550
  const startX = 0;
525
- const startPx = rect.left + startX + dx;
526
- const startPy = baseY + (acc.curveOffsets[startX] || 0) + dy;
551
+ const startPx = worldLeft + startX;
552
+ const startPy = worldBaseY + (acc.curveOffsets[startX] || 0);
527
553
  ctx.lineTo(startPx, startPy);
528
554
  }
529
555
  ctx.fill();
530
556
  ctx.shadowBlur = 0;
531
557
  ctx.shadowOffsetY = 0;
532
558
  };
533
- var drawSideAccumulations = (ctx, elementRects) => {
559
+ var drawSideAccumulations = (ctx, elementRects, scrollX, scrollY) => {
534
560
  ctx.fillStyle = ACC_FILL_STYLE;
535
561
  ctx.shadowColor = ACC_SHADOW_COLOR;
536
562
  ctx.shadowBlur = 3;
537
563
  ctx.globalAlpha = 1;
538
- const currentScrollX = window.scrollX;
539
- const currentScrollY = window.scrollY;
540
564
  ctx.beginPath();
541
565
  const drawSide = (sideArray, isLeft, multipliers, rect, dx, dy) => {
542
566
  const baseX = isLeft ? rect.left : rect.right;
543
- ctx.moveTo(baseX + dx, rect.top + dy);
567
+ const worldBaseX = baseX + dx;
568
+ const worldTop = rect.top + dy;
569
+ const worldBottom = rect.bottom + dy;
570
+ ctx.moveTo(worldBaseX, worldTop);
544
571
  for (let y = 0; y < sideArray.length; y += 2) {
545
572
  const width = sideArray[y] || 0;
546
573
  const nextY = Math.min(y + 2, sideArray.length - 1);
547
574
  const nextWidth = sideArray[nextY] || 0;
548
575
  const gravityMultiplier = multipliers[y] || 0;
549
- const py = rect.top + y + dy;
550
- const px = (isLeft ? baseX - width * gravityMultiplier : baseX + width * gravityMultiplier) + dx;
551
- const ny = rect.top + nextY + dy;
576
+ const py = worldTop + y;
577
+ const px = isLeft ? worldBaseX - width * gravityMultiplier : worldBaseX + width * gravityMultiplier;
578
+ const ny = worldTop + nextY;
552
579
  const nGravityMultiplier = multipliers[nextY] || 0;
553
- const nx = (isLeft ? baseX - nextWidth * nGravityMultiplier : baseX + nextWidth * nGravityMultiplier) + dx;
580
+ const nx = isLeft ? worldBaseX - nextWidth * nGravityMultiplier : worldBaseX + nextWidth * nGravityMultiplier;
554
581
  ctx.lineTo(px, py);
555
582
  ctx.lineTo(nx, ny);
556
583
  }
557
- ctx.lineTo(baseX + dx, rect.bottom + dy);
584
+ ctx.lineTo(worldBaseX, worldBottom);
558
585
  };
559
586
  for (const item of elementRects) {
560
587
  const { rect, acc } = item;
561
588
  if (acc.maxSideHeight === 0) continue;
562
- const hasLeftSnow = acc.leftSide.some((h) => h > 0.3);
563
- const hasRightSnow = acc.rightSide.some((h) => h > 0.3);
589
+ const hasLeftSnow = acc.leftMax > 0.3;
590
+ const hasRightSnow = acc.rightMax > 0.3;
564
591
  if (!hasLeftSnow && !hasRightSnow) continue;
565
- const dx = currentScrollX;
566
- const dy = currentScrollY;
567
- if (hasLeftSnow) drawSide(acc.leftSide, true, acc.sideGravityMultipliers, rect, dx, dy);
568
- if (hasRightSnow) drawSide(acc.rightSide, false, acc.sideGravityMultipliers, rect, dx, dy);
592
+ if (hasLeftSnow) drawSide(acc.leftSide, true, acc.sideGravityMultipliers, rect, scrollX, scrollY);
593
+ if (hasRightSnow) drawSide(acc.rightSide, false, acc.sideGravityMultipliers, rect, scrollX, scrollY);
569
594
  }
570
595
  ctx.fill();
571
596
  ctx.shadowBlur = 0;
@@ -584,6 +609,7 @@ function Snowfall() {
584
609
  const snowflakesRef = (0, import_react2.useRef)([]);
585
610
  const accumulationRef = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
586
611
  const animationIdRef = (0, import_react2.useRef)(0);
612
+ const dprRef = (0, import_react2.useRef)(1);
587
613
  const fpsFrames = (0, import_react2.useRef)([]);
588
614
  const metricsRef = (0, import_react2.useRef)({
589
615
  scanTime: 0,
@@ -617,6 +643,7 @@ function Snowfall() {
617
643
  const newWidth = window.innerWidth;
618
644
  const newHeight = window.innerHeight;
619
645
  const dpr = window.devicePixelRatio || 1;
646
+ dprRef.current = dpr;
620
647
  canvasRef.current.width = newWidth * dpr;
621
648
  canvasRef.current.height = newHeight * dpr;
622
649
  canvasRef.current.style.width = `${newWidth}px`;
@@ -672,7 +699,7 @@ function Snowfall() {
672
699
  const dt = deltaTime / 16.67;
673
700
  const frameStartTime = performance.now();
674
701
  const clearStart = performance.now();
675
- const dpr = window.devicePixelRatio || 1;
702
+ const dpr = dprRef.current;
676
703
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
677
704
  ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
678
705
  const scrollX = window.scrollX;
@@ -697,11 +724,22 @@ function Snowfall() {
697
724
  const drawStart = performance.now();
698
725
  drawSnowflakes(ctx, snowflakes);
699
726
  if (isEnabledRef.current && snowflakes.length < physicsConfigRef.current.MAX_FLAKES) {
700
- const isBackground = Math.random() < 0.4;
701
- snowflakes.push(createSnowflake(document.documentElement.scrollWidth, physicsConfigRef.current, isBackground));
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);
702
742
  }
703
- drawAccumulations(ctx, elementRects);
704
- drawSideAccumulations(ctx, elementRects);
705
743
  metricsRef.current.drawTime = performance.now() - drawStart;
706
744
  metricsRef.current.frameTime = performance.now() - frameStartTime;
707
745
  if (currentTime - lastMetricsUpdate > 500) {