@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.mjs CHANGED
@@ -88,6 +88,7 @@ var TAG_HEADER = "header";
88
88
  var TAG_FOOTER = "footer";
89
89
  var ROLE_BANNER = "banner";
90
90
  var ROLE_CONTENTINFO = "contentinfo";
91
+ var TAU = Math.PI * 2;
91
92
 
92
93
  // src/utils/snowfall/dom.ts
93
94
  var BOTTOM_TAGS = [TAG_HEADER];
@@ -172,38 +173,113 @@ var getElementRects = (accumulationMap) => {
172
173
  };
173
174
 
174
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
+ };
175
182
  var createSnowflake = (worldWidth, config, isBackground = false) => {
176
- if (isBackground) {
177
- const sizeRatio = Math.random();
178
- const radius = config.FLAKE_SIZE.MIN * 0.6 + sizeRatio * (config.FLAKE_SIZE.MAX - config.FLAKE_SIZE.MIN) * 0.4;
179
- return {
180
- x: Math.random() * worldWidth,
181
- y: window.scrollY - 5,
182
- radius,
183
- speed: radius * 0.3 + Math.random() * 0.2 + 0.2,
184
- wind: (Math.random() - 0.5) * (config.WIND_STRENGTH * 0.625),
185
- opacity: Math.random() * 0.2 + 0.2,
186
- wobble: Math.random() * Math.PI * 2,
187
- wobbleSpeed: Math.random() * 0.015 + 5e-3,
188
- sizeRatio,
189
- isBackground: true
190
- };
191
- } else {
192
- const sizeRatio = Math.random();
193
- const radius = config.FLAKE_SIZE.MIN + sizeRatio * (config.FLAKE_SIZE.MAX - config.FLAKE_SIZE.MIN);
194
- return {
195
- x: Math.random() * worldWidth,
196
- y: window.scrollY - 5,
197
- radius,
198
- speed: radius * 0.5 + Math.random() * 0.3 + 0.5,
199
- wind: (Math.random() - 0.5) * config.WIND_STRENGTH,
200
- opacity: Math.random() * 0.3 + 0.5,
201
- wobble: Math.random() * Math.PI * 2,
202
- wobbleSpeed: Math.random() * 0.02 + 0.01,
203
- sizeRatio,
204
- isBackground: false
205
- };
183
+ const x = Math.random() * worldWidth;
184
+ const dna = Math.random();
185
+ const noise = {
186
+ speed: dna * 13 % 1,
187
+ wind: dna * 7 % 1,
188
+ wobblePhase: dna * 23 % 1,
189
+ wobbleSpeed: dna * 5 % 1
190
+ };
191
+ const { MIN, MAX } = config.FLAKE_SIZE;
192
+ const sizeRatio = dna;
193
+ const profile = isBackground ? {
194
+ sizeMin: MIN * 0.6,
195
+ sizeRange: (MAX - MIN) * 0.4,
196
+ speedBase: 0.2,
197
+ speedScale: 0.3,
198
+ noiseSpeedScale: 0.2,
199
+ windScale: config.WIND_STRENGTH * 0.625,
200
+ opacityBase: 0.2,
201
+ opacityScale: 0.2,
202
+ wobbleBase: 5e-3,
203
+ wobbleScale: 0.015
204
+ } : {
205
+ sizeMin: MIN,
206
+ sizeRange: MAX - MIN,
207
+ speedBase: 0.5,
208
+ speedScale: 0.5,
209
+ noiseSpeedScale: 0.3,
210
+ windScale: config.WIND_STRENGTH,
211
+ opacityBase: 0.5,
212
+ opacityScale: 0.3,
213
+ wobbleBase: 0.01,
214
+ wobbleScale: 0.02
215
+ };
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);
222
+ return {
223
+ x,
224
+ y: window.scrollY - 5,
225
+ radius,
226
+ glowRadius,
227
+ speed: radius * profile.speedScale + noise.speed * profile.noiseSpeedScale + profile.speedBase,
228
+ wind: (noise.wind - 0.5) * profile.windScale,
229
+ opacity,
230
+ glowOpacity,
231
+ wobble: noise.wobblePhase * TAU,
232
+ wobbleSpeed: noise.wobbleSpeed * profile.wobbleScale + profile.wobbleBase,
233
+ sizeRatio,
234
+ isBackground
235
+ };
236
+ };
237
+ var initializeMaxHeights = (width, baseMax, borderRadius, isBottom = false) => {
238
+ let maxHeights = new Array(width);
239
+ for (let i = 0; i < width; i++) {
240
+ let edgeFactor = 1;
241
+ if (!isBottom && borderRadius > 0) {
242
+ if (i < borderRadius) {
243
+ edgeFactor = Math.pow(i / borderRadius, 1.2);
244
+ } else if (i > width - borderRadius) {
245
+ edgeFactor = Math.pow((width - i) / borderRadius, 1.2);
246
+ }
247
+ }
248
+ maxHeights[i] = baseMax * edgeFactor * (0.85 + Math.random() * 0.15);
206
249
  }
250
+ const smoothPasses = 4;
251
+ for (let p = 0; p < smoothPasses; p++) {
252
+ const smoothed = [...maxHeights];
253
+ for (let i = 1; i < width - 1; i++) {
254
+ smoothed[i] = (maxHeights[i - 1] + maxHeights[i] + maxHeights[i + 1]) / 3;
255
+ }
256
+ maxHeights = smoothed;
257
+ }
258
+ return maxHeights;
259
+ };
260
+ var calculateCurveOffsets = (width, borderRadius, isBottom) => {
261
+ const offsets = new Array(width).fill(0);
262
+ if (borderRadius <= 0 || isBottom) return offsets;
263
+ for (let x = 0; x < width; x++) {
264
+ let offset = 0;
265
+ if (x < borderRadius) {
266
+ const dist = borderRadius - x;
267
+ offset = borderRadius - Math.sqrt(Math.max(0, borderRadius * borderRadius - dist * dist));
268
+ } else if (x > width - borderRadius) {
269
+ const dist = x - (width - borderRadius);
270
+ offset = borderRadius - Math.sqrt(Math.max(0, borderRadius * borderRadius - dist * dist));
271
+ }
272
+ offsets[x] = offset;
273
+ }
274
+ return offsets;
275
+ };
276
+ var calculateGravityMultipliers = (height) => {
277
+ const multipliers = new Array(height);
278
+ for (let i = 0; i < height; i++) {
279
+ const ratio = i / height;
280
+ multipliers[i] = Math.sqrt(ratio);
281
+ }
282
+ return multipliers;
207
283
  };
208
284
  var initializeAccumulation = (accumulationMap, config) => {
209
285
  const elements = getAccumulationSurfaces(config.MAX_SURFACES);
@@ -222,6 +298,10 @@ var initializeAccumulation = (accumulationMap, config) => {
222
298
  if (existing.borderRadius !== void 0) {
223
299
  const styleBuffer = window.getComputedStyle(el);
224
300
  existing.borderRadius = parseFloat(styleBuffer.borderTopLeftRadius) || 0;
301
+ existing.curveOffsets = calculateCurveOffsets(width, existing.borderRadius, isBottom);
302
+ if (existing.leftSide.length === Math.ceil(rect.height) && !existing.sideGravityMultipliers) {
303
+ existing.sideGravityMultipliers = calculateGravityMultipliers(existing.leftSide.length);
304
+ }
225
305
  }
226
306
  return;
227
307
  }
@@ -229,26 +309,7 @@ var initializeAccumulation = (accumulationMap, config) => {
229
309
  const baseMax = isBottom ? config.MAX_DEPTH.BOTTOM : config.MAX_DEPTH.TOP;
230
310
  const styles = window.getComputedStyle(el);
231
311
  const borderRadius = parseFloat(styles.borderTopLeftRadius) || 0;
232
- let maxHeights = new Array(width);
233
- for (let i = 0; i < width; i++) {
234
- let edgeFactor = 1;
235
- if (!isBottom && borderRadius > 0) {
236
- if (i < borderRadius) {
237
- edgeFactor = Math.pow(i / borderRadius, 1.2);
238
- } else if (i > width - borderRadius) {
239
- edgeFactor = Math.pow((width - i) / borderRadius, 1.2);
240
- }
241
- }
242
- maxHeights[i] = baseMax * edgeFactor * (0.85 + Math.random() * 0.15);
243
- }
244
- const smoothPasses = 4;
245
- for (let p = 0; p < smoothPasses; p++) {
246
- const smoothed = [...maxHeights];
247
- for (let i = 1; i < width - 1; i++) {
248
- smoothed[i] = (maxHeights[i - 1] + maxHeights[i] + maxHeights[i + 1]) / 3;
249
- }
250
- maxHeights = smoothed;
251
- }
312
+ const maxHeights = initializeMaxHeights(width, baseMax, borderRadius, isBottom);
252
313
  accumulationMap.set(el, {
253
314
  heights: existing?.heights.length === width ? existing.heights : new Array(width).fill(0),
254
315
  maxHeights,
@@ -256,6 +317,8 @@ var initializeAccumulation = (accumulationMap, config) => {
256
317
  rightSide: existing?.rightSide.length === height ? existing.rightSide : new Array(height).fill(0),
257
318
  maxSideHeight: isBottom ? 0 : config.MAX_DEPTH.SIDE,
258
319
  borderRadius,
320
+ curveOffsets: calculateCurveOffsets(width, borderRadius, isBottom),
321
+ sideGravityMultipliers: calculateGravityMultipliers(height),
259
322
  type
260
323
  });
261
324
  });
@@ -380,54 +443,46 @@ var meltAndSmoothAccumulation = (elementRects, config, dt) => {
380
443
  };
381
444
 
382
445
  // src/utils/snowfall/draw.ts
383
- var drawSnowflake = (ctx, flake) => {
384
- ctx.beginPath();
385
- ctx.arc(flake.x, flake.y, flake.radius, 0, Math.PI * 2);
386
- ctx.fillStyle = `rgba(255, 255, 255, ${flake.opacity})`;
387
- ctx.fill();
388
- ctx.beginPath();
389
- ctx.arc(flake.x, flake.y, flake.radius * 1.5, 0, Math.PI * 2);
390
- ctx.fillStyle = `rgba(255, 255, 255, ${flake.opacity * 0.2})`;
391
- ctx.fill();
446
+ var ACC_FILL_STYLE = "rgba(255, 255, 255, 0.95)";
447
+ var ACC_SHADOW_COLOR = "rgba(200, 230, 255, 0.6)";
448
+ var drawSnowflakes = (ctx, flakes) => {
449
+ if (flakes.length === 0) return;
450
+ ctx.fillStyle = "#FFFFFF";
451
+ for (const flake of flakes) {
452
+ if (!flake.isBackground) {
453
+ ctx.globalAlpha = flake.glowOpacity;
454
+ ctx.beginPath();
455
+ ctx.arc(flake.x, flake.y, flake.glowRadius, 0, TAU);
456
+ ctx.fill();
457
+ }
458
+ ctx.globalAlpha = flake.opacity;
459
+ ctx.beginPath();
460
+ ctx.arc(flake.x, flake.y, flake.radius, 0, TAU);
461
+ ctx.fill();
462
+ }
463
+ ctx.globalAlpha = 1;
392
464
  };
393
- var drawAccumulations = (ctx, elementRects) => {
394
- const setupCtx = (c) => {
395
- c.fillStyle = "rgba(255, 255, 255, 0.95)";
396
- c.shadowColor = "rgba(200, 230, 255, 0.6)";
397
- c.shadowBlur = 4;
398
- c.shadowOffsetY = -1;
399
- };
400
- setupCtx(ctx);
401
- const currentScrollX = window.scrollX;
402
- const currentScrollY = window.scrollY;
465
+ var drawAccumulations = (ctx, elementRects, scrollX, scrollY) => {
466
+ ctx.fillStyle = ACC_FILL_STYLE;
467
+ ctx.shadowColor = ACC_SHADOW_COLOR;
468
+ ctx.shadowBlur = 4;
469
+ ctx.shadowOffsetY = -1;
470
+ ctx.globalAlpha = 1;
471
+ ctx.beginPath();
403
472
  for (const item of elementRects) {
404
473
  const { rect, acc } = item;
405
474
  if (!acc.heights.some((h) => h > 0.1)) continue;
406
- const dx = currentScrollX;
407
- const dy = currentScrollY;
408
475
  const isBottom = acc.type === VAL_BOTTOM;
409
476
  const baseY = isBottom ? rect.bottom - 1 : rect.top + 1;
410
- const borderRadius = acc.borderRadius;
411
- const getCurveOffset = (xPos) => {
412
- if (borderRadius <= 0 || isBottom) return 0;
413
- let offset = 0;
414
- if (xPos < borderRadius) {
415
- const dist = borderRadius - xPos;
416
- offset = borderRadius - Math.sqrt(Math.max(0, borderRadius * borderRadius - dist * dist));
417
- } else if (xPos > rect.width - borderRadius) {
418
- const dist = xPos - (rect.width - borderRadius);
419
- offset = borderRadius - Math.sqrt(Math.max(0, borderRadius * borderRadius - dist * dist));
420
- }
421
- return offset;
422
- };
423
- ctx.beginPath();
477
+ const worldLeft = rect.left + scrollX;
478
+ const worldBaseY = baseY + scrollY;
424
479
  let first = true;
425
480
  const step = 2;
426
481
  const len = acc.heights.length;
427
482
  for (let x = 0; x < len; x += step) {
428
483
  const height = acc.heights[x] || 0;
429
- const px = rect.left + x + dx;
430
- const py = baseY - height + getCurveOffset(x) + dy;
484
+ const px = worldLeft + x;
485
+ const py = worldBaseY - height + (acc.curveOffsets[x] || 0);
431
486
  if (first) {
432
487
  ctx.moveTo(px, py);
433
488
  first = false;
@@ -438,68 +493,61 @@ var drawAccumulations = (ctx, elementRects) => {
438
493
  if ((len - 1) % step !== 0) {
439
494
  const x = len - 1;
440
495
  const height = acc.heights[x] || 0;
441
- const px = rect.left + x + dx;
442
- const py = baseY - height + getCurveOffset(x) + dy;
496
+ const px = worldLeft + x;
497
+ const py = worldBaseY - height + (acc.curveOffsets[x] || 0);
443
498
  ctx.lineTo(px, py);
444
499
  }
445
500
  for (let x = len - 1; x >= 0; x -= step) {
446
- const px = rect.left + x + dx;
447
- const py = baseY + getCurveOffset(x) + dy;
501
+ const px = worldLeft + x;
502
+ const py = worldBaseY + (acc.curveOffsets[x] || 0);
448
503
  ctx.lineTo(px, py);
449
504
  }
450
505
  const startX = 0;
451
- const startPx = rect.left + startX + dx;
452
- const startPy = baseY + getCurveOffset(startX) + dy;
506
+ const startPx = worldLeft + startX;
507
+ const startPy = worldBaseY + (acc.curveOffsets[startX] || 0);
453
508
  ctx.lineTo(startPx, startPy);
454
- ctx.closePath();
455
- ctx.fill();
456
509
  }
510
+ ctx.fill();
457
511
  ctx.shadowBlur = 0;
458
512
  ctx.shadowOffsetY = 0;
459
513
  };
460
- var drawSideAccumulations = (ctx, elementRects) => {
461
- const setupCtx = (c) => {
462
- c.fillStyle = "rgba(255, 255, 255, 0.95)";
463
- c.shadowColor = "rgba(200, 230, 255, 0.6)";
464
- c.shadowBlur = 3;
514
+ var drawSideAccumulations = (ctx, elementRects, scrollX, scrollY) => {
515
+ ctx.fillStyle = ACC_FILL_STYLE;
516
+ ctx.shadowColor = ACC_SHADOW_COLOR;
517
+ ctx.shadowBlur = 3;
518
+ ctx.globalAlpha = 1;
519
+ ctx.beginPath();
520
+ const drawSide = (sideArray, isLeft, multipliers, rect, dx, dy) => {
521
+ const baseX = isLeft ? rect.left : rect.right;
522
+ const worldBaseX = baseX + dx;
523
+ const worldTop = rect.top + dy;
524
+ const worldBottom = rect.bottom + dy;
525
+ ctx.moveTo(worldBaseX, worldTop);
526
+ for (let y = 0; y < sideArray.length; y += 2) {
527
+ const width = sideArray[y] || 0;
528
+ const nextY = Math.min(y + 2, sideArray.length - 1);
529
+ const nextWidth = sideArray[nextY] || 0;
530
+ const gravityMultiplier = multipliers[y] || 0;
531
+ const py = worldTop + y;
532
+ const px = isLeft ? worldBaseX - width * gravityMultiplier : worldBaseX + width * gravityMultiplier;
533
+ const ny = worldTop + nextY;
534
+ const nGravityMultiplier = multipliers[nextY] || 0;
535
+ const nx = isLeft ? worldBaseX - nextWidth * nGravityMultiplier : worldBaseX + nextWidth * nGravityMultiplier;
536
+ ctx.lineTo(px, py);
537
+ ctx.lineTo(nx, ny);
538
+ }
539
+ ctx.lineTo(worldBaseX, worldBottom);
465
540
  };
466
- setupCtx(ctx);
467
- const currentScrollX = window.scrollX;
468
- const currentScrollY = window.scrollY;
469
541
  for (const item of elementRects) {
470
542
  const { rect, acc } = item;
471
543
  if (acc.maxSideHeight === 0) continue;
472
544
  const hasLeftSnow = acc.leftSide.some((h) => h > 0.3);
473
545
  const hasRightSnow = acc.rightSide.some((h) => h > 0.3);
474
546
  if (!hasLeftSnow && !hasRightSnow) continue;
475
- const dx = currentScrollX;
476
- const dy = currentScrollY;
477
- const drawSide = (sideArray, isLeft) => {
478
- ctx.beginPath();
479
- const baseX = isLeft ? rect.left : rect.right;
480
- ctx.moveTo(baseX + dx, rect.top + dy);
481
- for (let y = 0; y < sideArray.length; y += 2) {
482
- const width = sideArray[y] || 0;
483
- const nextY = Math.min(y + 2, sideArray.length - 1);
484
- const nextWidth = sideArray[nextY] || 0;
485
- const heightRatio = y / sideArray.length;
486
- const gravityMultiplier = Math.pow(heightRatio, 1.5);
487
- const py = rect.top + y + dy;
488
- const px = (isLeft ? baseX - width * gravityMultiplier : baseX + width * gravityMultiplier) + dx;
489
- const ny = rect.top + nextY + dy;
490
- const nRatio = nextY / sideArray.length;
491
- const nGravityMultiplier = Math.pow(nRatio, 1.5);
492
- const nx = (isLeft ? baseX - nextWidth * nGravityMultiplier : baseX + nextWidth * nGravityMultiplier) + dx;
493
- ctx.lineTo(px, py);
494
- ctx.lineTo(nx, ny);
495
- }
496
- ctx.lineTo(baseX + dx, rect.bottom + dy);
497
- ctx.closePath();
498
- ctx.fill();
499
- };
500
- if (hasLeftSnow) drawSide(acc.leftSide, true);
501
- if (hasRightSnow) drawSide(acc.rightSide, false);
547
+ if (hasLeftSnow) drawSide(acc.leftSide, true, acc.sideGravityMultipliers, rect, scrollX, scrollY);
548
+ if (hasRightSnow) drawSide(acc.rightSide, false, acc.sideGravityMultipliers, rect, scrollX, scrollY);
502
549
  }
550
+ ctx.fill();
503
551
  ctx.shadowBlur = 0;
504
552
  };
505
553
 
@@ -516,6 +564,7 @@ function Snowfall() {
516
564
  const snowflakesRef = useRef([]);
517
565
  const accumulationRef = useRef(/* @__PURE__ */ new Map());
518
566
  const animationIdRef = useRef(0);
567
+ const dprRef = useRef(1);
519
568
  const fpsFrames = useRef([]);
520
569
  const metricsRef = useRef({
521
570
  scanTime: 0,
@@ -527,7 +576,7 @@ function Snowfall() {
527
576
  drawTime: 0
528
577
  });
529
578
  useEffect(() => {
530
- setIsMounted(true);
579
+ requestAnimationFrame(() => setIsMounted(true));
531
580
  }, []);
532
581
  useEffect(() => {
533
582
  isEnabledRef.current = isEnabled;
@@ -549,6 +598,7 @@ function Snowfall() {
549
598
  const newWidth = window.innerWidth;
550
599
  const newHeight = window.innerHeight;
551
600
  const dpr = window.devicePixelRatio || 1;
601
+ dprRef.current = dpr;
552
602
  canvasRef.current.width = newWidth * dpr;
553
603
  canvasRef.current.height = newHeight * dpr;
554
604
  canvasRef.current.style.width = `${newWidth}px`;
@@ -583,7 +633,9 @@ function Snowfall() {
583
633
  metricsRef.current.scanTime = performance.now() - scanStart;
584
634
  };
585
635
  initAccumulationWrapper();
586
- setIsVisible(true);
636
+ requestAnimationFrame(() => {
637
+ if (isMounted) setIsVisible(true);
638
+ });
587
639
  let lastTime = 0;
588
640
  let lastMetricsUpdate = 0;
589
641
  let elementRects = [];
@@ -602,7 +654,7 @@ function Snowfall() {
602
654
  const dt = deltaTime / 16.67;
603
655
  const frameStartTime = performance.now();
604
656
  const clearStart = performance.now();
605
- const dpr = window.devicePixelRatio || 1;
657
+ const dpr = dprRef.current;
606
658
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
607
659
  ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
608
660
  const scrollX = window.scrollX;
@@ -625,15 +677,24 @@ function Snowfall() {
625
677
  );
626
678
  metricsRef.current.physicsTime = performance.now() - physicsStart;
627
679
  const drawStart = performance.now();
628
- for (const flake of snowflakes) {
629
- drawSnowflake(ctx, flake);
630
- }
680
+ drawSnowflakes(ctx, snowflakes);
631
681
  if (isEnabledRef.current && snowflakes.length < physicsConfigRef.current.MAX_FLAKES) {
632
- const isBackground = Math.random() < 0.4;
633
- snowflakes.push(createSnowflake(document.documentElement.scrollWidth, physicsConfigRef.current, isBackground));
682
+ const currentFps = fpsFrames.current.length;
683
+ const shouldSpawn = currentFps >= 40 || Math.random() < 0.2;
684
+ if (shouldSpawn) {
685
+ const isBackground = Math.random() < 0.4;
686
+ snowflakes.push(createSnowflake(document.documentElement.scrollWidth, physicsConfigRef.current, isBackground));
687
+ }
688
+ }
689
+ const viewportWidth = window.innerWidth;
690
+ const viewportHeight = window.innerHeight;
691
+ const visibleRects = elementRects.filter(
692
+ ({ rect }) => rect.right >= 0 && rect.left <= viewportWidth && rect.bottom >= 0 && rect.top <= viewportHeight
693
+ );
694
+ if (visibleRects.length > 0) {
695
+ drawAccumulations(ctx, visibleRects, scrollX, scrollY);
696
+ drawSideAccumulations(ctx, visibleRects, scrollX, scrollY);
634
697
  }
635
- drawAccumulations(ctx, elementRects);
636
- drawSideAccumulations(ctx, elementRects);
637
698
  metricsRef.current.drawTime = performance.now() - drawStart;
638
699
  metricsRef.current.frameTime = performance.now() - frameStartTime;
639
700
  if (currentTime - lastMetricsUpdate > 500) {