snow.js

7.86 KB
12/11/2025 13:10
JS
snow.js
/**
 * Snow Animation for LeafBox Technologies Website
 * Creates a beautiful snowfall effect using Canvas API
 */

class SnowAnimation {
  constructor(canvas) {
    this.canvas = canvas;
    this.ctx = canvas.getContext('2d');
    this.snowflakes = [];
    this.animationId = null;
    this.isActive = false;

    this.init();
  }

  init() {
    this.setupCanvas();
    this.createSnowflakes();
    this.startAnimation();
    this.handleVisibilityChange();

    // Optimize for performance
    this.throttleResize = this.throttle(this.resize.bind(this), 100);
    window.addEventListener('resize', this.throttleResize);
  }

  setupCanvas() {
    this.resize();
    this.canvas.style.pointerEvents = 'none';
    this.canvas.style.position = 'fixed';
    this.canvas.style.top = '0';
    this.canvas.style.left = '0';
    this.canvas.style.width = '100%';
    this.canvas.style.height = '100%';
    this.canvas.style.zIndex = '1';
    this.canvas.style.opacity = '0.6';
  }

  resize() {
    this.canvas.width = window.innerWidth;
    this.canvas.height = window.innerHeight;

    // Recalculate snowflake properties
    this.adjustSnowflakeCount();
  }

  createSnowflakes() {
    this.snowflakes = [];
    const flakeCount = this.getFlakeCount();

    for (let i = 0; i < flakeCount; i++) {
      this.snowflakes.push(this.createSnowflake());
    }
  }

  createSnowflake() {
    return {
      x: Math.random() * this.canvas.width,
      y: Math.random() * this.canvas.height,
      radius: Math.random() * 3 + 1,
      speed: Math.random() * 2 + 0.5,
      opacity: Math.random() * 0.5 + 0.3,
      drift: Math.random() * 0.5 + 0.1,
      angle: Math.random() * Math.PI * 2,
      rotationSpeed: (Math.random() - 0.5) * 0.02,
      sway: Math.random() * Math.PI * 2,
      swaySpeed: Math.random() * 0.02 + 0.005,
      color: this.getRandomSnowColor()
    };
  }

  getRandomSnowColor() {
    const colors = [
      '#B89B5F', // Antique Gold
      '#FDFDFC', // Snowfall White
      '#FFFFFF', // Pure White
      '#E8E8E8'  // Light Gray
    ];
    return colors[Math.floor(Math.random() * colors.length)];
  }

  getFlakeCount() {
    const area = this.canvas.width * this.canvas.height;
    const baseCount = area / 15000; // Adjust density based on screen area

    // Reduce count on mobile for performance
    if (window.innerWidth < 768) {
      return Math.min(baseCount * 0.6, 50);
    }

    return Math.min(baseCount, 150);
  }

  adjustSnowflakeCount() {
    const targetCount = this.getFlakeCount();
    const currentCount = this.snowflakes.length;

    if (currentCount < targetCount) {
      // Add more snowflakes
      for (let i = currentCount; i < targetCount; i++) {
        this.snowflakes.push(this.createSnowflake());
      }
    } else if (currentCount > targetCount) {
      // Remove excess snowflakes
      this.snowflakes = this.snowflakes.slice(0, targetCount);
    }
  }

  updateSnowflake(flake) {
    // Update position
    flake.y += flake.speed;
    flake.x += Math.sin(flake.sway) * flake.drift;
    flake.angle += flake.rotationSpeed;
    flake.sway += flake.swaySpeed;

    // Wrap around screen
    if (flake.y > this.canvas.height) {
      flake.y = -flake.radius;
      flake.x = Math.random() * this.canvas.width;
    }

    if (flake.x > this.canvas.width) {
      flake.x = -flake.radius;
    } else if (flake.x < -flake.radius) {
      flake.x = this.canvas.width + flake.radius;
    }

    // Update opacity based on height for depth effect
    const depthOpacity = 1 - (flake.y / this.canvas.height) * 0.5;
    flake.currentOpacity = flake.opacity * depthOpacity;
  }

  drawSnowflake(flake) {
    this.ctx.save();
    this.ctx.globalAlpha = flake.currentOpacity || flake.opacity;
    this.ctx.fillStyle = flake.color;
    this.ctx.translate(flake.x, flake.y);
    this.ctx.rotate(flake.angle);

    // Draw snowflake shape
    this.drawSnowflakeShape(flake);

    this.ctx.restore();
  }

  drawSnowflakeShape(flake) {
    const ctx = this.ctx;
    const radius = flake.radius;

    ctx.beginPath();

    // Create a simple star-like snowflake
    for (let i = 0; i < 6; i++) {
      const angle = (i * Math.PI) / 3;
      const x = Math.cos(angle) * radius;
      const y = Math.sin(angle) * radius;

      if (i === 0) {
        ctx.moveTo(x, y);
      } else {
        ctx.lineTo(x, y);
      }

      // Add smaller branches
      const branchAngle = angle + Math.PI / 6;
      const branchX = Math.cos(branchAngle) * radius * 0.5;
      const branchY = Math.sin(branchAngle) * radius * 0.5;
      ctx.moveTo(x * 0.7, y * 0.7);
      ctx.lineTo(branchX, branchY);
    }

    ctx.closePath();
    ctx.fill();

    // Add center dot for extra detail
    ctx.beginPath();
    ctx.arc(0, 0, radius * 0.2, 0, Math.PI * 2);
    ctx.fill();
  }

  animate() {
    if (!this.isActive) return;

    // Clear canvas with fade effect for smooth trails
    this.ctx.fillStyle = 'rgba(253, 253, 252, 0.05)';
    this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);

    // Update and draw all snowflakes
    this.snowflakes.forEach(flake => {
      this.updateSnowflake(flake);
      this.drawSnowflake(flake);
    });

    this.animationId = requestAnimationFrame(() => this.animate());
  }

  start() {
    if (this.isActive) return;

    this.isActive = true;
    this.animate();
  }

  stop() {
    this.isActive = false;
    if (this.animationId) {
      cancelAnimationFrame(this.animationId);
      this.animationId = null;
    }
  }

  destroy() {
    this.stop();
    window.removeEventListener('resize', this.throttleResize);

    // Clear canvas
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
  }

  handleVisibilityChange() {
    document.addEventListener('visibilitychange', () => {
      if (document.hidden) {
        this.stop();
      } else {
        this.start();
      }
    });
  }

  // Utility functions
  throttle(func, limit) {
    let inThrottle;
    return function() {
      const args = arguments;
      const context = this;
      if (!inThrottle) {
        func.apply(context, args);
        inThrottle = true;
        setTimeout(() => inThrottle = false, limit);
      }
    }
  }

  // Public methods for external control
  pause() {
    this.stop();
  }

  resume() {
    this.start();
  }

  setIntensity(low, medium, high) {
    // This could be used to adjust snow intensity
    // Currently not implemented but could be useful
  }
}

// Initialize snow animation when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
  const canvas = document.getElementById('snowCanvas');

  if (!canvas) {
    console.warn('Snow canvas not found');
    return;
  }

  // Check for reduced motion preference
  const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  if (prefersReducedMotion) {
    console.log('Reduced motion preferred, skipping snow animation');
    return;
  }

  // Create snow animation instance
  const snowAnimation = new SnowAnimation(canvas);

  // Make it globally accessible for debugging
  window.snowAnimation = snowAnimation;

  // Start animation
  snowAnimation.start();

  // Pause on hover over interactive elements (optional)
  const interactiveElements = document.querySelectorAll('button, a, input, textarea');
  interactiveElements.forEach(element => {
    element.addEventListener('mouseenter', () => {
      snowAnimation.pause();
    });

    element.addEventListener('mouseleave', () => {
      snowAnimation.resume();
    });
  });

  // Performance optimization: reduce snow on slower devices
  if (navigator.hardwareConcurrency && navigator.hardwareConcurrency < 4) {
    console.log('Lower-end device detected, reducing snow animation intensity');
    // Could implement additional optimizations here
  }

  // Cleanup on page unload
  window.addEventListener('beforeunload', () => {
    snowAnimation.destroy();
  });
});

// Export for module systems
if (typeof module !== 'undefined' && module.exports) {
  module.exports = SnowAnimation;
}