@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.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];
@@ -173,37 +174,99 @@ var getElementRects = (accumulationMap) => {
173
174
 
174
175
  // src/utils/snowfall/physics.ts
175
176
  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
- };
177
+ const x = Math.random() * worldWidth;
178
+ const dna = Math.random();
179
+ const noise = {
180
+ speed: dna * 13 % 1,
181
+ wind: dna * 7 % 1,
182
+ wobblePhase: dna * 23 % 1,
183
+ wobbleSpeed: dna * 5 % 1
184
+ };
185
+ const { MIN, MAX } = config.FLAKE_SIZE;
186
+ const sizeRatio = dna;
187
+ const profile = isBackground ? {
188
+ sizeMin: MIN * 0.6,
189
+ sizeRange: (MAX - MIN) * 0.4,
190
+ speedBase: 0.2,
191
+ speedScale: 0.3,
192
+ noiseSpeedScale: 0.2,
193
+ windScale: config.WIND_STRENGTH * 0.625,
194
+ opacityBase: 0.2,
195
+ opacityScale: 0.2,
196
+ wobbleBase: 5e-3,
197
+ wobbleScale: 0.015
198
+ } : {
199
+ sizeMin: MIN,
200
+ sizeRange: MAX - MIN,
201
+ speedBase: 0.5,
202
+ speedScale: 0.5,
203
+ noiseSpeedScale: 0.3,
204
+ windScale: config.WIND_STRENGTH,
205
+ opacityBase: 0.5,
206
+ opacityScale: 0.3,
207
+ wobbleBase: 0.01,
208
+ wobbleScale: 0.02
209
+ };
210
+ const radius = profile.sizeMin + sizeRatio * profile.sizeRange;
211
+ return {
212
+ x,
213
+ y: window.scrollY - 5,
214
+ radius,
215
+ speed: radius * profile.speedScale + noise.speed * profile.noiseSpeedScale + profile.speedBase,
216
+ wind: (noise.wind - 0.5) * profile.windScale,
217
+ opacity: profile.opacityBase + sizeRatio * profile.opacityScale,
218
+ wobble: noise.wobblePhase * TAU,
219
+ wobbleSpeed: noise.wobbleSpeed * profile.wobbleScale + profile.wobbleBase,
220
+ sizeRatio,
221
+ isBackground
222
+ };
223
+ };
224
+ var initializeMaxHeights = (width, baseMax, borderRadius, isBottom = false) => {
225
+ let maxHeights = new Array(width);
226
+ for (let i = 0; i < width; i++) {
227
+ let edgeFactor = 1;
228
+ if (!isBottom && borderRadius > 0) {
229
+ if (i < borderRadius) {
230
+ edgeFactor = Math.pow(i / borderRadius, 1.2);
231
+ } else if (i > width - borderRadius) {
232
+ edgeFactor = Math.pow((width - i) / borderRadius, 1.2);
233
+ }
234
+ }
235
+ maxHeights[i] = baseMax * edgeFactor * (0.85 + Math.random() * 0.15);
236
+ }
237
+ const smoothPasses = 4;
238
+ for (let p = 0; p < smoothPasses; p++) {
239
+ const smoothed = [...maxHeights];
240
+ for (let i = 1; i < width - 1; i++) {
241
+ smoothed[i] = (maxHeights[i - 1] + maxHeights[i] + maxHeights[i + 1]) / 3;
242
+ }
243
+ maxHeights = smoothed;
244
+ }
245
+ return maxHeights;
246
+ };
247
+ var calculateCurveOffsets = (width, borderRadius, isBottom) => {
248
+ const offsets = new Array(width).fill(0);
249
+ if (borderRadius <= 0 || isBottom) return offsets;
250
+ for (let x = 0; x < width; x++) {
251
+ let offset = 0;
252
+ if (x < borderRadius) {
253
+ const dist = borderRadius - x;
254
+ offset = borderRadius - Math.sqrt(Math.max(0, borderRadius * borderRadius - dist * dist));
255
+ } else if (x > width - borderRadius) {
256
+ const dist = x - (width - borderRadius);
257
+ offset = borderRadius - Math.sqrt(Math.max(0, borderRadius * borderRadius - dist * dist));
258
+ }
259
+ offsets[x] = offset;
206
260
  }
261
+ return offsets;
262
+ };
263
+ var calculateGravityMultipliers = (height) => {
264
+ const multipliers = new Array(height);
265
+ for (let i = 0; i < height; i++) {
266
+ const ratio = i / height;
267
+ multipliers[i] = Math.sqrt(ratio);
268
+ }
269
+ return multipliers;
207
270
  };
208
271
  var initializeAccumulation = (accumulationMap, config) => {
209
272
  const elements = getAccumulationSurfaces(config.MAX_SURFACES);
@@ -222,6 +285,10 @@ var initializeAccumulation = (accumulationMap, config) => {
222
285
  if (existing.borderRadius !== void 0) {
223
286
  const styleBuffer = window.getComputedStyle(el);
224
287
  existing.borderRadius = parseFloat(styleBuffer.borderTopLeftRadius) || 0;
288
+ existing.curveOffsets = calculateCurveOffsets(width, existing.borderRadius, isBottom);
289
+ if (existing.leftSide.length === Math.ceil(rect.height) && !existing.sideGravityMultipliers) {
290
+ existing.sideGravityMultipliers = calculateGravityMultipliers(existing.leftSide.length);
291
+ }
225
292
  }
226
293
  return;
227
294
  }
@@ -229,26 +296,7 @@ var initializeAccumulation = (accumulationMap, config) => {
229
296
  const baseMax = isBottom ? config.MAX_DEPTH.BOTTOM : config.MAX_DEPTH.TOP;
230
297
  const styles = window.getComputedStyle(el);
231
298
  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
- }
299
+ const maxHeights = initializeMaxHeights(width, baseMax, borderRadius, isBottom);
252
300
  accumulationMap.set(el, {
253
301
  heights: existing?.heights.length === width ? existing.heights : new Array(width).fill(0),
254
302
  maxHeights,
@@ -256,6 +304,8 @@ var initializeAccumulation = (accumulationMap, config) => {
256
304
  rightSide: existing?.rightSide.length === height ? existing.rightSide : new Array(height).fill(0),
257
305
  maxSideHeight: isBottom ? 0 : config.MAX_DEPTH.SIDE,
258
306
  borderRadius,
307
+ curveOffsets: calculateCurveOffsets(width, borderRadius, isBottom),
308
+ sideGravityMultipliers: calculateGravityMultipliers(height),
259
309
  type
260
310
  });
261
311
  });
@@ -380,26 +430,35 @@ var meltAndSmoothAccumulation = (elementRects, config, dt) => {
380
430
  };
381
431
 
382
432
  // 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();
433
+ var ACC_FILL_STYLE = "rgba(255, 255, 255, 0.95)";
434
+ var ACC_SHADOW_COLOR = "rgba(200, 230, 255, 0.6)";
435
+ var drawSnowflakes = (ctx, flakes) => {
436
+ if (flakes.length === 0) return;
437
+ ctx.fillStyle = "#FFFFFF";
438
+ for (const flake of flakes) {
439
+ ctx.globalAlpha = flake.opacity;
440
+ ctx.beginPath();
441
+ ctx.arc(flake.x, flake.y, flake.radius, 0, TAU);
442
+ ctx.fill();
443
+ }
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
+ ctx.globalAlpha = 1;
392
452
  };
393
453
  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);
454
+ ctx.fillStyle = ACC_FILL_STYLE;
455
+ ctx.shadowColor = ACC_SHADOW_COLOR;
456
+ ctx.shadowBlur = 4;
457
+ ctx.shadowOffsetY = -1;
458
+ ctx.globalAlpha = 1;
401
459
  const currentScrollX = window.scrollX;
402
460
  const currentScrollY = window.scrollY;
461
+ ctx.beginPath();
403
462
  for (const item of elementRects) {
404
463
  const { rect, acc } = item;
405
464
  if (!acc.heights.some((h) => h > 0.1)) continue;
@@ -407,27 +466,13 @@ var drawAccumulations = (ctx, elementRects) => {
407
466
  const dy = currentScrollY;
408
467
  const isBottom = acc.type === VAL_BOTTOM;
409
468
  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();
424
469
  let first = true;
425
470
  const step = 2;
426
471
  const len = acc.heights.length;
427
472
  for (let x = 0; x < len; x += step) {
428
473
  const height = acc.heights[x] || 0;
429
474
  const px = rect.left + x + dx;
430
- const py = baseY - height + getCurveOffset(x) + dy;
475
+ const py = baseY - height + (acc.curveOffsets[x] || 0) + dy;
431
476
  if (first) {
432
477
  ctx.moveTo(px, py);
433
478
  first = false;
@@ -439,33 +484,49 @@ var drawAccumulations = (ctx, elementRects) => {
439
484
  const x = len - 1;
440
485
  const height = acc.heights[x] || 0;
441
486
  const px = rect.left + x + dx;
442
- const py = baseY - height + getCurveOffset(x) + dy;
487
+ const py = baseY - height + (acc.curveOffsets[x] || 0) + dy;
443
488
  ctx.lineTo(px, py);
444
489
  }
445
490
  for (let x = len - 1; x >= 0; x -= step) {
446
491
  const px = rect.left + x + dx;
447
- const py = baseY + getCurveOffset(x) + dy;
492
+ const py = baseY + (acc.curveOffsets[x] || 0) + dy;
448
493
  ctx.lineTo(px, py);
449
494
  }
450
495
  const startX = 0;
451
496
  const startPx = rect.left + startX + dx;
452
- const startPy = baseY + getCurveOffset(startX) + dy;
497
+ const startPy = baseY + (acc.curveOffsets[startX] || 0) + dy;
453
498
  ctx.lineTo(startPx, startPy);
454
- ctx.closePath();
455
- ctx.fill();
456
499
  }
500
+ ctx.fill();
457
501
  ctx.shadowBlur = 0;
458
502
  ctx.shadowOffsetY = 0;
459
503
  };
460
504
  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;
465
- };
466
- setupCtx(ctx);
505
+ ctx.fillStyle = ACC_FILL_STYLE;
506
+ ctx.shadowColor = ACC_SHADOW_COLOR;
507
+ ctx.shadowBlur = 3;
508
+ ctx.globalAlpha = 1;
467
509
  const currentScrollX = window.scrollX;
468
510
  const currentScrollY = window.scrollY;
511
+ ctx.beginPath();
512
+ const drawSide = (sideArray, isLeft, multipliers, rect, dx, dy) => {
513
+ const baseX = isLeft ? rect.left : rect.right;
514
+ ctx.moveTo(baseX + dx, rect.top + dy);
515
+ for (let y = 0; y < sideArray.length; y += 2) {
516
+ const width = sideArray[y] || 0;
517
+ const nextY = Math.min(y + 2, sideArray.length - 1);
518
+ const nextWidth = sideArray[nextY] || 0;
519
+ 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;
523
+ const nGravityMultiplier = multipliers[nextY] || 0;
524
+ const nx = (isLeft ? baseX - nextWidth * nGravityMultiplier : baseX + nextWidth * nGravityMultiplier) + dx;
525
+ ctx.lineTo(px, py);
526
+ ctx.lineTo(nx, ny);
527
+ }
528
+ ctx.lineTo(baseX + dx, rect.bottom + dy);
529
+ };
469
530
  for (const item of elementRects) {
470
531
  const { rect, acc } = item;
471
532
  if (acc.maxSideHeight === 0) continue;
@@ -474,32 +535,10 @@ var drawSideAccumulations = (ctx, elementRects) => {
474
535
  if (!hasLeftSnow && !hasRightSnow) continue;
475
536
  const dx = currentScrollX;
476
537
  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);
538
+ if (hasLeftSnow) drawSide(acc.leftSide, true, acc.sideGravityMultipliers, rect, dx, dy);
539
+ if (hasRightSnow) drawSide(acc.rightSide, false, acc.sideGravityMultipliers, rect, dx, dy);
502
540
  }
541
+ ctx.fill();
503
542
  ctx.shadowBlur = 0;
504
543
  };
505
544
 
@@ -527,7 +566,7 @@ function Snowfall() {
527
566
  drawTime: 0
528
567
  });
529
568
  useEffect(() => {
530
- setIsMounted(true);
569
+ requestAnimationFrame(() => setIsMounted(true));
531
570
  }, []);
532
571
  useEffect(() => {
533
572
  isEnabledRef.current = isEnabled;
@@ -583,7 +622,9 @@ function Snowfall() {
583
622
  metricsRef.current.scanTime = performance.now() - scanStart;
584
623
  };
585
624
  initAccumulationWrapper();
586
- setIsVisible(true);
625
+ requestAnimationFrame(() => {
626
+ if (isMounted) setIsVisible(true);
627
+ });
587
628
  let lastTime = 0;
588
629
  let lastMetricsUpdate = 0;
589
630
  let elementRects = [];
@@ -625,9 +666,7 @@ function Snowfall() {
625
666
  );
626
667
  metricsRef.current.physicsTime = performance.now() - physicsStart;
627
668
  const drawStart = performance.now();
628
- for (const flake of snowflakes) {
629
- drawSnowflake(ctx, flake);
630
- }
669
+ drawSnowflakes(ctx, snowflakes);
631
670
  if (isEnabledRef.current && snowflakes.length < physicsConfigRef.current.MAX_FLAKES) {
632
671
  const isBackground = Math.random() < 0.4;
633
672
  snowflakes.push(createSnowflake(document.documentElement.scrollWidth, physicsConfigRef.current, isBackground));