@hdcodedev/snowfall 1.0.6 → 1.0.8

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
@@ -117,6 +117,7 @@ var TAG_HEADER = "header";
117
117
  var TAG_FOOTER = "footer";
118
118
  var ROLE_BANNER = "banner";
119
119
  var ROLE_CONTENTINFO = "contentinfo";
120
+ var TAU = Math.PI * 2;
120
121
 
121
122
  // src/utils/snowfall/dom.ts
122
123
  var BOTTOM_TAGS = [TAG_HEADER];
@@ -201,38 +202,113 @@ var getElementRects = (accumulationMap) => {
201
202
  };
202
203
 
203
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
+ };
204
211
  var createSnowflake = (worldWidth, config, isBackground = false) => {
205
- if (isBackground) {
206
- const sizeRatio = Math.random();
207
- const radius = config.FLAKE_SIZE.MIN * 0.6 + sizeRatio * (config.FLAKE_SIZE.MAX - config.FLAKE_SIZE.MIN) * 0.4;
208
- return {
209
- x: Math.random() * worldWidth,
210
- y: window.scrollY - 5,
211
- radius,
212
- speed: radius * 0.3 + Math.random() * 0.2 + 0.2,
213
- wind: (Math.random() - 0.5) * (config.WIND_STRENGTH * 0.625),
214
- opacity: Math.random() * 0.2 + 0.2,
215
- wobble: Math.random() * Math.PI * 2,
216
- wobbleSpeed: Math.random() * 0.015 + 5e-3,
217
- sizeRatio,
218
- isBackground: true
219
- };
220
- } else {
221
- const sizeRatio = Math.random();
222
- const radius = config.FLAKE_SIZE.MIN + sizeRatio * (config.FLAKE_SIZE.MAX - config.FLAKE_SIZE.MIN);
223
- return {
224
- x: Math.random() * worldWidth,
225
- y: window.scrollY - 5,
226
- radius,
227
- speed: radius * 0.5 + Math.random() * 0.3 + 0.5,
228
- wind: (Math.random() - 0.5) * config.WIND_STRENGTH,
229
- opacity: Math.random() * 0.3 + 0.5,
230
- wobble: Math.random() * Math.PI * 2,
231
- wobbleSpeed: Math.random() * 0.02 + 0.01,
232
- sizeRatio,
233
- isBackground: false
234
- };
212
+ const x = Math.random() * worldWidth;
213
+ const dna = Math.random();
214
+ const noise = {
215
+ speed: dna * 13 % 1,
216
+ wind: dna * 7 % 1,
217
+ wobblePhase: dna * 23 % 1,
218
+ wobbleSpeed: dna * 5 % 1
219
+ };
220
+ const { MIN, MAX } = config.FLAKE_SIZE;
221
+ const sizeRatio = dna;
222
+ const profile = isBackground ? {
223
+ sizeMin: MIN * 0.6,
224
+ sizeRange: (MAX - MIN) * 0.4,
225
+ speedBase: 0.2,
226
+ speedScale: 0.3,
227
+ noiseSpeedScale: 0.2,
228
+ windScale: config.WIND_STRENGTH * 0.625,
229
+ opacityBase: 0.2,
230
+ opacityScale: 0.2,
231
+ wobbleBase: 5e-3,
232
+ wobbleScale: 0.015
233
+ } : {
234
+ sizeMin: MIN,
235
+ sizeRange: MAX - MIN,
236
+ speedBase: 0.5,
237
+ speedScale: 0.5,
238
+ noiseSpeedScale: 0.3,
239
+ windScale: config.WIND_STRENGTH,
240
+ opacityBase: 0.5,
241
+ opacityScale: 0.3,
242
+ wobbleBase: 0.01,
243
+ wobbleScale: 0.02
244
+ };
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);
251
+ return {
252
+ x,
253
+ y: window.scrollY - 5,
254
+ radius,
255
+ glowRadius,
256
+ speed: radius * profile.speedScale + noise.speed * profile.noiseSpeedScale + profile.speedBase,
257
+ wind: (noise.wind - 0.5) * profile.windScale,
258
+ opacity,
259
+ glowOpacity,
260
+ wobble: noise.wobblePhase * TAU,
261
+ wobbleSpeed: noise.wobbleSpeed * profile.wobbleScale + profile.wobbleBase,
262
+ sizeRatio,
263
+ isBackground
264
+ };
265
+ };
266
+ var initializeMaxHeights = (width, baseMax, borderRadius, isBottom = false) => {
267
+ let maxHeights = new Array(width);
268
+ for (let i = 0; i < width; i++) {
269
+ let edgeFactor = 1;
270
+ if (!isBottom && borderRadius > 0) {
271
+ if (i < borderRadius) {
272
+ edgeFactor = Math.pow(i / borderRadius, 1.2);
273
+ } else if (i > width - borderRadius) {
274
+ edgeFactor = Math.pow((width - i) / borderRadius, 1.2);
275
+ }
276
+ }
277
+ maxHeights[i] = baseMax * edgeFactor * (0.85 + Math.random() * 0.15);
235
278
  }
279
+ const smoothPasses = 4;
280
+ for (let p = 0; p < smoothPasses; p++) {
281
+ const smoothed = [...maxHeights];
282
+ for (let i = 1; i < width - 1; i++) {
283
+ smoothed[i] = (maxHeights[i - 1] + maxHeights[i] + maxHeights[i + 1]) / 3;
284
+ }
285
+ maxHeights = smoothed;
286
+ }
287
+ return maxHeights;
288
+ };
289
+ var calculateCurveOffsets = (width, borderRadius, isBottom) => {
290
+ const offsets = new Array(width).fill(0);
291
+ if (borderRadius <= 0 || isBottom) return offsets;
292
+ for (let x = 0; x < width; x++) {
293
+ let offset = 0;
294
+ if (x < borderRadius) {
295
+ const dist = borderRadius - x;
296
+ offset = borderRadius - Math.sqrt(Math.max(0, borderRadius * borderRadius - dist * dist));
297
+ } else if (x > width - borderRadius) {
298
+ const dist = x - (width - borderRadius);
299
+ offset = borderRadius - Math.sqrt(Math.max(0, borderRadius * borderRadius - dist * dist));
300
+ }
301
+ offsets[x] = offset;
302
+ }
303
+ return offsets;
304
+ };
305
+ var calculateGravityMultipliers = (height) => {
306
+ const multipliers = new Array(height);
307
+ for (let i = 0; i < height; i++) {
308
+ const ratio = i / height;
309
+ multipliers[i] = Math.sqrt(ratio);
310
+ }
311
+ return multipliers;
236
312
  };
237
313
  var initializeAccumulation = (accumulationMap, config) => {
238
314
  const elements = getAccumulationSurfaces(config.MAX_SURFACES);
@@ -251,6 +327,10 @@ var initializeAccumulation = (accumulationMap, config) => {
251
327
  if (existing.borderRadius !== void 0) {
252
328
  const styleBuffer = window.getComputedStyle(el);
253
329
  existing.borderRadius = parseFloat(styleBuffer.borderTopLeftRadius) || 0;
330
+ existing.curveOffsets = calculateCurveOffsets(width, existing.borderRadius, isBottom);
331
+ if (existing.leftSide.length === Math.ceil(rect.height) && !existing.sideGravityMultipliers) {
332
+ existing.sideGravityMultipliers = calculateGravityMultipliers(existing.leftSide.length);
333
+ }
254
334
  }
255
335
  return;
256
336
  }
@@ -258,26 +338,7 @@ var initializeAccumulation = (accumulationMap, config) => {
258
338
  const baseMax = isBottom ? config.MAX_DEPTH.BOTTOM : config.MAX_DEPTH.TOP;
259
339
  const styles = window.getComputedStyle(el);
260
340
  const borderRadius = parseFloat(styles.borderTopLeftRadius) || 0;
261
- let maxHeights = new Array(width);
262
- for (let i = 0; i < width; i++) {
263
- let edgeFactor = 1;
264
- if (!isBottom && borderRadius > 0) {
265
- if (i < borderRadius) {
266
- edgeFactor = Math.pow(i / borderRadius, 1.2);
267
- } else if (i > width - borderRadius) {
268
- edgeFactor = Math.pow((width - i) / borderRadius, 1.2);
269
- }
270
- }
271
- maxHeights[i] = baseMax * edgeFactor * (0.85 + Math.random() * 0.15);
272
- }
273
- const smoothPasses = 4;
274
- for (let p = 0; p < smoothPasses; p++) {
275
- const smoothed = [...maxHeights];
276
- for (let i = 1; i < width - 1; i++) {
277
- smoothed[i] = (maxHeights[i - 1] + maxHeights[i] + maxHeights[i + 1]) / 3;
278
- }
279
- maxHeights = smoothed;
280
- }
341
+ const maxHeights = initializeMaxHeights(width, baseMax, borderRadius, isBottom);
281
342
  accumulationMap.set(el, {
282
343
  heights: existing?.heights.length === width ? existing.heights : new Array(width).fill(0),
283
344
  maxHeights,
@@ -285,6 +346,8 @@ var initializeAccumulation = (accumulationMap, config) => {
285
346
  rightSide: existing?.rightSide.length === height ? existing.rightSide : new Array(height).fill(0),
286
347
  maxSideHeight: isBottom ? 0 : config.MAX_DEPTH.SIDE,
287
348
  borderRadius,
349
+ curveOffsets: calculateCurveOffsets(width, borderRadius, isBottom),
350
+ sideGravityMultipliers: calculateGravityMultipliers(height),
288
351
  type
289
352
  });
290
353
  });
@@ -409,54 +472,46 @@ var meltAndSmoothAccumulation = (elementRects, config, dt) => {
409
472
  };
410
473
 
411
474
  // src/utils/snowfall/draw.ts
412
- var drawSnowflake = (ctx, flake) => {
413
- ctx.beginPath();
414
- ctx.arc(flake.x, flake.y, flake.radius, 0, Math.PI * 2);
415
- ctx.fillStyle = `rgba(255, 255, 255, ${flake.opacity})`;
416
- ctx.fill();
417
- ctx.beginPath();
418
- ctx.arc(flake.x, flake.y, flake.radius * 1.5, 0, Math.PI * 2);
419
- ctx.fillStyle = `rgba(255, 255, 255, ${flake.opacity * 0.2})`;
420
- ctx.fill();
475
+ var ACC_FILL_STYLE = "rgba(255, 255, 255, 0.95)";
476
+ var ACC_SHADOW_COLOR = "rgba(200, 230, 255, 0.6)";
477
+ var drawSnowflakes = (ctx, flakes) => {
478
+ if (flakes.length === 0) return;
479
+ ctx.fillStyle = "#FFFFFF";
480
+ for (const flake of flakes) {
481
+ if (!flake.isBackground) {
482
+ ctx.globalAlpha = flake.glowOpacity;
483
+ ctx.beginPath();
484
+ ctx.arc(flake.x, flake.y, flake.glowRadius, 0, TAU);
485
+ ctx.fill();
486
+ }
487
+ ctx.globalAlpha = flake.opacity;
488
+ ctx.beginPath();
489
+ ctx.arc(flake.x, flake.y, flake.radius, 0, TAU);
490
+ ctx.fill();
491
+ }
492
+ ctx.globalAlpha = 1;
421
493
  };
422
- var drawAccumulations = (ctx, elementRects) => {
423
- const setupCtx = (c) => {
424
- c.fillStyle = "rgba(255, 255, 255, 0.95)";
425
- c.shadowColor = "rgba(200, 230, 255, 0.6)";
426
- c.shadowBlur = 4;
427
- c.shadowOffsetY = -1;
428
- };
429
- setupCtx(ctx);
430
- const currentScrollX = window.scrollX;
431
- const currentScrollY = window.scrollY;
494
+ var drawAccumulations = (ctx, elementRects, scrollX, scrollY) => {
495
+ ctx.fillStyle = ACC_FILL_STYLE;
496
+ ctx.shadowColor = ACC_SHADOW_COLOR;
497
+ ctx.shadowBlur = 4;
498
+ ctx.shadowOffsetY = -1;
499
+ ctx.globalAlpha = 1;
500
+ ctx.beginPath();
432
501
  for (const item of elementRects) {
433
502
  const { rect, acc } = item;
434
503
  if (!acc.heights.some((h) => h > 0.1)) continue;
435
- const dx = currentScrollX;
436
- const dy = currentScrollY;
437
504
  const isBottom = acc.type === VAL_BOTTOM;
438
505
  const baseY = isBottom ? rect.bottom - 1 : rect.top + 1;
439
- const borderRadius = acc.borderRadius;
440
- const getCurveOffset = (xPos) => {
441
- if (borderRadius <= 0 || isBottom) return 0;
442
- let offset = 0;
443
- if (xPos < borderRadius) {
444
- const dist = borderRadius - xPos;
445
- offset = borderRadius - Math.sqrt(Math.max(0, borderRadius * borderRadius - dist * dist));
446
- } else if (xPos > rect.width - borderRadius) {
447
- const dist = xPos - (rect.width - borderRadius);
448
- offset = borderRadius - Math.sqrt(Math.max(0, borderRadius * borderRadius - dist * dist));
449
- }
450
- return offset;
451
- };
452
- ctx.beginPath();
506
+ const worldLeft = rect.left + scrollX;
507
+ const worldBaseY = baseY + scrollY;
453
508
  let first = true;
454
509
  const step = 2;
455
510
  const len = acc.heights.length;
456
511
  for (let x = 0; x < len; x += step) {
457
512
  const height = acc.heights[x] || 0;
458
- const px = rect.left + x + dx;
459
- const py = baseY - height + getCurveOffset(x) + dy;
513
+ const px = worldLeft + x;
514
+ const py = worldBaseY - height + (acc.curveOffsets[x] || 0);
460
515
  if (first) {
461
516
  ctx.moveTo(px, py);
462
517
  first = false;
@@ -467,68 +522,61 @@ var drawAccumulations = (ctx, elementRects) => {
467
522
  if ((len - 1) % step !== 0) {
468
523
  const x = len - 1;
469
524
  const height = acc.heights[x] || 0;
470
- const px = rect.left + x + dx;
471
- const py = baseY - height + getCurveOffset(x) + dy;
525
+ const px = worldLeft + x;
526
+ const py = worldBaseY - height + (acc.curveOffsets[x] || 0);
472
527
  ctx.lineTo(px, py);
473
528
  }
474
529
  for (let x = len - 1; x >= 0; x -= step) {
475
- const px = rect.left + x + dx;
476
- const py = baseY + getCurveOffset(x) + dy;
530
+ const px = worldLeft + x;
531
+ const py = worldBaseY + (acc.curveOffsets[x] || 0);
477
532
  ctx.lineTo(px, py);
478
533
  }
479
534
  const startX = 0;
480
- const startPx = rect.left + startX + dx;
481
- const startPy = baseY + getCurveOffset(startX) + dy;
535
+ const startPx = worldLeft + startX;
536
+ const startPy = worldBaseY + (acc.curveOffsets[startX] || 0);
482
537
  ctx.lineTo(startPx, startPy);
483
- ctx.closePath();
484
- ctx.fill();
485
538
  }
539
+ ctx.fill();
486
540
  ctx.shadowBlur = 0;
487
541
  ctx.shadowOffsetY = 0;
488
542
  };
489
- var drawSideAccumulations = (ctx, elementRects) => {
490
- const setupCtx = (c) => {
491
- c.fillStyle = "rgba(255, 255, 255, 0.95)";
492
- c.shadowColor = "rgba(200, 230, 255, 0.6)";
493
- c.shadowBlur = 3;
543
+ var drawSideAccumulations = (ctx, elementRects, scrollX, scrollY) => {
544
+ ctx.fillStyle = ACC_FILL_STYLE;
545
+ ctx.shadowColor = ACC_SHADOW_COLOR;
546
+ ctx.shadowBlur = 3;
547
+ ctx.globalAlpha = 1;
548
+ ctx.beginPath();
549
+ const drawSide = (sideArray, isLeft, multipliers, rect, dx, dy) => {
550
+ const baseX = isLeft ? rect.left : rect.right;
551
+ const worldBaseX = baseX + dx;
552
+ const worldTop = rect.top + dy;
553
+ const worldBottom = rect.bottom + dy;
554
+ ctx.moveTo(worldBaseX, worldTop);
555
+ for (let y = 0; y < sideArray.length; y += 2) {
556
+ const width = sideArray[y] || 0;
557
+ const nextY = Math.min(y + 2, sideArray.length - 1);
558
+ const nextWidth = sideArray[nextY] || 0;
559
+ const gravityMultiplier = multipliers[y] || 0;
560
+ const py = worldTop + y;
561
+ const px = isLeft ? worldBaseX - width * gravityMultiplier : worldBaseX + width * gravityMultiplier;
562
+ const ny = worldTop + nextY;
563
+ const nGravityMultiplier = multipliers[nextY] || 0;
564
+ const nx = isLeft ? worldBaseX - nextWidth * nGravityMultiplier : worldBaseX + nextWidth * nGravityMultiplier;
565
+ ctx.lineTo(px, py);
566
+ ctx.lineTo(nx, ny);
567
+ }
568
+ ctx.lineTo(worldBaseX, worldBottom);
494
569
  };
495
- setupCtx(ctx);
496
- const currentScrollX = window.scrollX;
497
- const currentScrollY = window.scrollY;
498
570
  for (const item of elementRects) {
499
571
  const { rect, acc } = item;
500
572
  if (acc.maxSideHeight === 0) continue;
501
573
  const hasLeftSnow = acc.leftSide.some((h) => h > 0.3);
502
574
  const hasRightSnow = acc.rightSide.some((h) => h > 0.3);
503
575
  if (!hasLeftSnow && !hasRightSnow) continue;
504
- const dx = currentScrollX;
505
- const dy = currentScrollY;
506
- const drawSide = (sideArray, isLeft) => {
507
- ctx.beginPath();
508
- const baseX = isLeft ? rect.left : rect.right;
509
- ctx.moveTo(baseX + dx, rect.top + dy);
510
- for (let y = 0; y < sideArray.length; y += 2) {
511
- const width = sideArray[y] || 0;
512
- const nextY = Math.min(y + 2, sideArray.length - 1);
513
- const nextWidth = sideArray[nextY] || 0;
514
- const heightRatio = y / sideArray.length;
515
- const gravityMultiplier = Math.pow(heightRatio, 1.5);
516
- const py = rect.top + y + dy;
517
- const px = (isLeft ? baseX - width * gravityMultiplier : baseX + width * gravityMultiplier) + dx;
518
- const ny = rect.top + nextY + dy;
519
- const nRatio = nextY / sideArray.length;
520
- const nGravityMultiplier = Math.pow(nRatio, 1.5);
521
- const nx = (isLeft ? baseX - nextWidth * nGravityMultiplier : baseX + nextWidth * nGravityMultiplier) + dx;
522
- ctx.lineTo(px, py);
523
- ctx.lineTo(nx, ny);
524
- }
525
- ctx.lineTo(baseX + dx, rect.bottom + dy);
526
- ctx.closePath();
527
- ctx.fill();
528
- };
529
- if (hasLeftSnow) drawSide(acc.leftSide, true);
530
- if (hasRightSnow) drawSide(acc.rightSide, false);
576
+ if (hasLeftSnow) drawSide(acc.leftSide, true, acc.sideGravityMultipliers, rect, scrollX, scrollY);
577
+ if (hasRightSnow) drawSide(acc.rightSide, false, acc.sideGravityMultipliers, rect, scrollX, scrollY);
531
578
  }
579
+ ctx.fill();
532
580
  ctx.shadowBlur = 0;
533
581
  };
534
582
 
@@ -545,6 +593,7 @@ function Snowfall() {
545
593
  const snowflakesRef = (0, import_react2.useRef)([]);
546
594
  const accumulationRef = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
547
595
  const animationIdRef = (0, import_react2.useRef)(0);
596
+ const dprRef = (0, import_react2.useRef)(1);
548
597
  const fpsFrames = (0, import_react2.useRef)([]);
549
598
  const metricsRef = (0, import_react2.useRef)({
550
599
  scanTime: 0,
@@ -556,7 +605,7 @@ function Snowfall() {
556
605
  drawTime: 0
557
606
  });
558
607
  (0, import_react2.useEffect)(() => {
559
- setIsMounted(true);
608
+ requestAnimationFrame(() => setIsMounted(true));
560
609
  }, []);
561
610
  (0, import_react2.useEffect)(() => {
562
611
  isEnabledRef.current = isEnabled;
@@ -578,6 +627,7 @@ function Snowfall() {
578
627
  const newWidth = window.innerWidth;
579
628
  const newHeight = window.innerHeight;
580
629
  const dpr = window.devicePixelRatio || 1;
630
+ dprRef.current = dpr;
581
631
  canvasRef.current.width = newWidth * dpr;
582
632
  canvasRef.current.height = newHeight * dpr;
583
633
  canvasRef.current.style.width = `${newWidth}px`;
@@ -612,7 +662,9 @@ function Snowfall() {
612
662
  metricsRef.current.scanTime = performance.now() - scanStart;
613
663
  };
614
664
  initAccumulationWrapper();
615
- setIsVisible(true);
665
+ requestAnimationFrame(() => {
666
+ if (isMounted) setIsVisible(true);
667
+ });
616
668
  let lastTime = 0;
617
669
  let lastMetricsUpdate = 0;
618
670
  let elementRects = [];
@@ -631,7 +683,7 @@ function Snowfall() {
631
683
  const dt = deltaTime / 16.67;
632
684
  const frameStartTime = performance.now();
633
685
  const clearStart = performance.now();
634
- const dpr = window.devicePixelRatio || 1;
686
+ const dpr = dprRef.current;
635
687
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
636
688
  ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
637
689
  const scrollX = window.scrollX;
@@ -654,15 +706,24 @@ function Snowfall() {
654
706
  );
655
707
  metricsRef.current.physicsTime = performance.now() - physicsStart;
656
708
  const drawStart = performance.now();
657
- for (const flake of snowflakes) {
658
- drawSnowflake(ctx, flake);
659
- }
709
+ drawSnowflakes(ctx, snowflakes);
660
710
  if (isEnabledRef.current && snowflakes.length < physicsConfigRef.current.MAX_FLAKES) {
661
- const isBackground = Math.random() < 0.4;
662
- snowflakes.push(createSnowflake(document.documentElement.scrollWidth, physicsConfigRef.current, isBackground));
711
+ const currentFps = fpsFrames.current.length;
712
+ const shouldSpawn = currentFps >= 40 || Math.random() < 0.2;
713
+ if (shouldSpawn) {
714
+ const isBackground = Math.random() < 0.4;
715
+ snowflakes.push(createSnowflake(document.documentElement.scrollWidth, physicsConfigRef.current, isBackground));
716
+ }
717
+ }
718
+ const viewportWidth = window.innerWidth;
719
+ const viewportHeight = window.innerHeight;
720
+ const visibleRects = elementRects.filter(
721
+ ({ rect }) => rect.right >= 0 && rect.left <= viewportWidth && rect.bottom >= 0 && rect.top <= viewportHeight
722
+ );
723
+ if (visibleRects.length > 0) {
724
+ drawAccumulations(ctx, visibleRects, scrollX, scrollY);
725
+ drawSideAccumulations(ctx, visibleRects, scrollX, scrollY);
663
726
  }
664
- drawAccumulations(ctx, elementRects);
665
- drawSideAccumulations(ctx, elementRects);
666
727
  metricsRef.current.drawTime = performance.now() - drawStart;
667
728
  metricsRef.current.frameTime = performance.now() - frameStartTime;
668
729
  if (currentTime - lastMetricsUpdate > 500) {