@hdcodedev/snowfall 1.0.6 → 1.0.7

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];
@@ -202,37 +203,99 @@ var getElementRects = (accumulationMap) => {
202
203
 
203
204
  // src/utils/snowfall/physics.ts
204
205
  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
- };
206
+ const x = Math.random() * worldWidth;
207
+ const dna = Math.random();
208
+ const noise = {
209
+ speed: dna * 13 % 1,
210
+ wind: dna * 7 % 1,
211
+ wobblePhase: dna * 23 % 1,
212
+ wobbleSpeed: dna * 5 % 1
213
+ };
214
+ const { MIN, MAX } = config.FLAKE_SIZE;
215
+ const sizeRatio = dna;
216
+ const profile = isBackground ? {
217
+ sizeMin: MIN * 0.6,
218
+ sizeRange: (MAX - MIN) * 0.4,
219
+ speedBase: 0.2,
220
+ speedScale: 0.3,
221
+ noiseSpeedScale: 0.2,
222
+ windScale: config.WIND_STRENGTH * 0.625,
223
+ opacityBase: 0.2,
224
+ opacityScale: 0.2,
225
+ wobbleBase: 5e-3,
226
+ wobbleScale: 0.015
227
+ } : {
228
+ sizeMin: MIN,
229
+ sizeRange: MAX - MIN,
230
+ speedBase: 0.5,
231
+ speedScale: 0.5,
232
+ noiseSpeedScale: 0.3,
233
+ windScale: config.WIND_STRENGTH,
234
+ opacityBase: 0.5,
235
+ opacityScale: 0.3,
236
+ wobbleBase: 0.01,
237
+ wobbleScale: 0.02
238
+ };
239
+ const radius = profile.sizeMin + sizeRatio * profile.sizeRange;
240
+ return {
241
+ x,
242
+ y: window.scrollY - 5,
243
+ radius,
244
+ speed: radius * profile.speedScale + noise.speed * profile.noiseSpeedScale + profile.speedBase,
245
+ wind: (noise.wind - 0.5) * profile.windScale,
246
+ opacity: profile.opacityBase + sizeRatio * profile.opacityScale,
247
+ wobble: noise.wobblePhase * TAU,
248
+ wobbleSpeed: noise.wobbleSpeed * profile.wobbleScale + profile.wobbleBase,
249
+ sizeRatio,
250
+ isBackground
251
+ };
252
+ };
253
+ var initializeMaxHeights = (width, baseMax, borderRadius, isBottom = false) => {
254
+ let maxHeights = new Array(width);
255
+ for (let i = 0; i < width; i++) {
256
+ let edgeFactor = 1;
257
+ if (!isBottom && borderRadius > 0) {
258
+ if (i < borderRadius) {
259
+ edgeFactor = Math.pow(i / borderRadius, 1.2);
260
+ } else if (i > width - borderRadius) {
261
+ edgeFactor = Math.pow((width - i) / borderRadius, 1.2);
262
+ }
263
+ }
264
+ maxHeights[i] = baseMax * edgeFactor * (0.85 + Math.random() * 0.15);
265
+ }
266
+ const smoothPasses = 4;
267
+ for (let p = 0; p < smoothPasses; p++) {
268
+ const smoothed = [...maxHeights];
269
+ for (let i = 1; i < width - 1; i++) {
270
+ smoothed[i] = (maxHeights[i - 1] + maxHeights[i] + maxHeights[i + 1]) / 3;
271
+ }
272
+ maxHeights = smoothed;
273
+ }
274
+ return maxHeights;
275
+ };
276
+ var calculateCurveOffsets = (width, borderRadius, isBottom) => {
277
+ const offsets = new Array(width).fill(0);
278
+ if (borderRadius <= 0 || isBottom) return offsets;
279
+ for (let x = 0; x < width; x++) {
280
+ let offset = 0;
281
+ if (x < borderRadius) {
282
+ const dist = borderRadius - x;
283
+ offset = borderRadius - Math.sqrt(Math.max(0, borderRadius * borderRadius - dist * dist));
284
+ } else if (x > width - borderRadius) {
285
+ const dist = x - (width - borderRadius);
286
+ offset = borderRadius - Math.sqrt(Math.max(0, borderRadius * borderRadius - dist * dist));
287
+ }
288
+ offsets[x] = offset;
235
289
  }
290
+ return offsets;
291
+ };
292
+ var calculateGravityMultipliers = (height) => {
293
+ const multipliers = new Array(height);
294
+ for (let i = 0; i < height; i++) {
295
+ const ratio = i / height;
296
+ multipliers[i] = Math.sqrt(ratio);
297
+ }
298
+ return multipliers;
236
299
  };
237
300
  var initializeAccumulation = (accumulationMap, config) => {
238
301
  const elements = getAccumulationSurfaces(config.MAX_SURFACES);
@@ -251,6 +314,10 @@ var initializeAccumulation = (accumulationMap, config) => {
251
314
  if (existing.borderRadius !== void 0) {
252
315
  const styleBuffer = window.getComputedStyle(el);
253
316
  existing.borderRadius = parseFloat(styleBuffer.borderTopLeftRadius) || 0;
317
+ existing.curveOffsets = calculateCurveOffsets(width, existing.borderRadius, isBottom);
318
+ if (existing.leftSide.length === Math.ceil(rect.height) && !existing.sideGravityMultipliers) {
319
+ existing.sideGravityMultipliers = calculateGravityMultipliers(existing.leftSide.length);
320
+ }
254
321
  }
255
322
  return;
256
323
  }
@@ -258,26 +325,7 @@ var initializeAccumulation = (accumulationMap, config) => {
258
325
  const baseMax = isBottom ? config.MAX_DEPTH.BOTTOM : config.MAX_DEPTH.TOP;
259
326
  const styles = window.getComputedStyle(el);
260
327
  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
- }
328
+ const maxHeights = initializeMaxHeights(width, baseMax, borderRadius, isBottom);
281
329
  accumulationMap.set(el, {
282
330
  heights: existing?.heights.length === width ? existing.heights : new Array(width).fill(0),
283
331
  maxHeights,
@@ -285,6 +333,8 @@ var initializeAccumulation = (accumulationMap, config) => {
285
333
  rightSide: existing?.rightSide.length === height ? existing.rightSide : new Array(height).fill(0),
286
334
  maxSideHeight: isBottom ? 0 : config.MAX_DEPTH.SIDE,
287
335
  borderRadius,
336
+ curveOffsets: calculateCurveOffsets(width, borderRadius, isBottom),
337
+ sideGravityMultipliers: calculateGravityMultipliers(height),
288
338
  type
289
339
  });
290
340
  });
@@ -409,26 +459,35 @@ var meltAndSmoothAccumulation = (elementRects, config, dt) => {
409
459
  };
410
460
 
411
461
  // 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();
462
+ var ACC_FILL_STYLE = "rgba(255, 255, 255, 0.95)";
463
+ var ACC_SHADOW_COLOR = "rgba(200, 230, 255, 0.6)";
464
+ var drawSnowflakes = (ctx, flakes) => {
465
+ if (flakes.length === 0) return;
466
+ ctx.fillStyle = "#FFFFFF";
467
+ for (const flake of flakes) {
468
+ ctx.globalAlpha = flake.opacity;
469
+ ctx.beginPath();
470
+ ctx.arc(flake.x, flake.y, flake.radius, 0, TAU);
471
+ ctx.fill();
472
+ }
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
+ ctx.globalAlpha = 1;
421
481
  };
422
482
  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);
483
+ ctx.fillStyle = ACC_FILL_STYLE;
484
+ ctx.shadowColor = ACC_SHADOW_COLOR;
485
+ ctx.shadowBlur = 4;
486
+ ctx.shadowOffsetY = -1;
487
+ ctx.globalAlpha = 1;
430
488
  const currentScrollX = window.scrollX;
431
489
  const currentScrollY = window.scrollY;
490
+ ctx.beginPath();
432
491
  for (const item of elementRects) {
433
492
  const { rect, acc } = item;
434
493
  if (!acc.heights.some((h) => h > 0.1)) continue;
@@ -436,27 +495,13 @@ var drawAccumulations = (ctx, elementRects) => {
436
495
  const dy = currentScrollY;
437
496
  const isBottom = acc.type === VAL_BOTTOM;
438
497
  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();
453
498
  let first = true;
454
499
  const step = 2;
455
500
  const len = acc.heights.length;
456
501
  for (let x = 0; x < len; x += step) {
457
502
  const height = acc.heights[x] || 0;
458
503
  const px = rect.left + x + dx;
459
- const py = baseY - height + getCurveOffset(x) + dy;
504
+ const py = baseY - height + (acc.curveOffsets[x] || 0) + dy;
460
505
  if (first) {
461
506
  ctx.moveTo(px, py);
462
507
  first = false;
@@ -468,33 +513,49 @@ var drawAccumulations = (ctx, elementRects) => {
468
513
  const x = len - 1;
469
514
  const height = acc.heights[x] || 0;
470
515
  const px = rect.left + x + dx;
471
- const py = baseY - height + getCurveOffset(x) + dy;
516
+ const py = baseY - height + (acc.curveOffsets[x] || 0) + dy;
472
517
  ctx.lineTo(px, py);
473
518
  }
474
519
  for (let x = len - 1; x >= 0; x -= step) {
475
520
  const px = rect.left + x + dx;
476
- const py = baseY + getCurveOffset(x) + dy;
521
+ const py = baseY + (acc.curveOffsets[x] || 0) + dy;
477
522
  ctx.lineTo(px, py);
478
523
  }
479
524
  const startX = 0;
480
525
  const startPx = rect.left + startX + dx;
481
- const startPy = baseY + getCurveOffset(startX) + dy;
526
+ const startPy = baseY + (acc.curveOffsets[startX] || 0) + dy;
482
527
  ctx.lineTo(startPx, startPy);
483
- ctx.closePath();
484
- ctx.fill();
485
528
  }
529
+ ctx.fill();
486
530
  ctx.shadowBlur = 0;
487
531
  ctx.shadowOffsetY = 0;
488
532
  };
489
533
  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;
494
- };
495
- setupCtx(ctx);
534
+ ctx.fillStyle = ACC_FILL_STYLE;
535
+ ctx.shadowColor = ACC_SHADOW_COLOR;
536
+ ctx.shadowBlur = 3;
537
+ ctx.globalAlpha = 1;
496
538
  const currentScrollX = window.scrollX;
497
539
  const currentScrollY = window.scrollY;
540
+ ctx.beginPath();
541
+ const drawSide = (sideArray, isLeft, multipliers, rect, dx, dy) => {
542
+ const baseX = isLeft ? rect.left : rect.right;
543
+ ctx.moveTo(baseX + dx, rect.top + dy);
544
+ for (let y = 0; y < sideArray.length; y += 2) {
545
+ const width = sideArray[y] || 0;
546
+ const nextY = Math.min(y + 2, sideArray.length - 1);
547
+ const nextWidth = sideArray[nextY] || 0;
548
+ 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;
552
+ const nGravityMultiplier = multipliers[nextY] || 0;
553
+ const nx = (isLeft ? baseX - nextWidth * nGravityMultiplier : baseX + nextWidth * nGravityMultiplier) + dx;
554
+ ctx.lineTo(px, py);
555
+ ctx.lineTo(nx, ny);
556
+ }
557
+ ctx.lineTo(baseX + dx, rect.bottom + dy);
558
+ };
498
559
  for (const item of elementRects) {
499
560
  const { rect, acc } = item;
500
561
  if (acc.maxSideHeight === 0) continue;
@@ -503,32 +564,10 @@ var drawSideAccumulations = (ctx, elementRects) => {
503
564
  if (!hasLeftSnow && !hasRightSnow) continue;
504
565
  const dx = currentScrollX;
505
566
  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);
567
+ if (hasLeftSnow) drawSide(acc.leftSide, true, acc.sideGravityMultipliers, rect, dx, dy);
568
+ if (hasRightSnow) drawSide(acc.rightSide, false, acc.sideGravityMultipliers, rect, dx, dy);
531
569
  }
570
+ ctx.fill();
532
571
  ctx.shadowBlur = 0;
533
572
  };
534
573
 
@@ -556,7 +595,7 @@ function Snowfall() {
556
595
  drawTime: 0
557
596
  });
558
597
  (0, import_react2.useEffect)(() => {
559
- setIsMounted(true);
598
+ requestAnimationFrame(() => setIsMounted(true));
560
599
  }, []);
561
600
  (0, import_react2.useEffect)(() => {
562
601
  isEnabledRef.current = isEnabled;
@@ -612,7 +651,9 @@ function Snowfall() {
612
651
  metricsRef.current.scanTime = performance.now() - scanStart;
613
652
  };
614
653
  initAccumulationWrapper();
615
- setIsVisible(true);
654
+ requestAnimationFrame(() => {
655
+ if (isMounted) setIsVisible(true);
656
+ });
616
657
  let lastTime = 0;
617
658
  let lastMetricsUpdate = 0;
618
659
  let elementRects = [];
@@ -654,9 +695,7 @@ function Snowfall() {
654
695
  );
655
696
  metricsRef.current.physicsTime = performance.now() - physicsStart;
656
697
  const drawStart = performance.now();
657
- for (const flake of snowflakes) {
658
- drawSnowflake(ctx, flake);
659
- }
698
+ drawSnowflakes(ctx, snowflakes);
660
699
  if (isEnabledRef.current && snowflakes.length < physicsConfigRef.current.MAX_FLAKES) {
661
700
  const isBackground = Math.random() < 0.4;
662
701
  snowflakes.push(createSnowflake(document.documentElement.scrollWidth, physicsConfigRef.current, isBackground));