The Falling Snow Effect

Exact numbers to be tweaked when testing, but i want to have sub-50 snowflakes when on mobile, and 50-100 when on a PC. pixed based breakpoints would suffice :slight_smile:

Sorry for the delay! I will get you a snippet later tonight hopefully.

The way I would do it is by using media queries and a custom CSS variable that specifies the number of snowflakes for each window size breakdown. In the JavaScript, I would read the value of the custom variable instead of hardcoding to a number like 50.

Love the approach. Looking forward to the snippet <3

Here is a version that does what I described:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Falling Snow</title>

  <style>
    body {
      background-color: #111;
    }
    #snowflakeContainer {
      position: absolute;
      left: 0px;
      top: 0px;
      display: none;
      --snowflakes: 50;
    }
    .snowflake {
      position: fixed;
      background-color: #CCC;
      user-select: none;
      z-index: 1000;
      pointer-events: none;
      border-radius: 50%;
      width: 10px;
      height: 10px;
    }
    @media screen and (min-width: 480px) {
        #snowflakeContainer {
          --snowflakes: 25;
        }
    }
    @media screen and (min-width: 800px) {
      #snowflakeContainer {
          --snowflakes: 50;
      }
    }
    @media screen and (min-width: 1600px) {
      #snowflakeContainer {
          --snowflakes: 100;
      }
    }
  </style>
</head>
<body>
  <div id="snowflakeContainer">
    <span class="snowflake"></span>
  </div>
  
  <script>
    // Array to store our Snowflake objects
    let snowflakes = [];
  
    // Global variables to store our browser's window size
    let browserWidth;
    let browserHeight;
  
    // Specify the number of snowflakes you want visible
    let snowflakeContainer = document.querySelector("#snowflakeContainer");
    let containerStyle = getComputedStyle(snowflakeContainer);
    let numberOfSnowflakes = containerStyle.getPropertyValue("--snowflakes");
    console.log("Number of snowflakes: " + numberOfSnowflakes);
  
    // Flag to reset the position of the snowflakes
    let resetPosition = false;
  
    // Handle accessibility
    let enableAnimations = false;
    let reduceMotionQuery = matchMedia("(prefers-reduced-motion)");
  
    // Handle animation accessibility preferences 
    function setAccessibilityState() {
      if (reduceMotionQuery.matches) {
        enableAnimations = false;
      } else { 
        enableAnimations = true;
      }
    }
    setAccessibilityState();
  
    reduceMotionQuery.addListener(setAccessibilityState);
  
    //
    // It all starts here...
    //
    function setup() {
      if (enableAnimations) {
        window.addEventListener("DOMContentLoaded", generateSnowflakes, false);
        window.addEventListener("resize", setResetFlag, false);
      }
    }
    setup();
  
    //
    // Constructor for our Snowflake object
    //
    class Snowflake {
      // set initial snowflake properties
      constructor(element, speed, xPos, yPos) {
        this.element = element;
        this.speed = speed;
        this.xPos = xPos;
        this.yPos = yPos;
        this.scale = 1;
    
        // declare variables used for snowflake's motion
        this.counter = 0;
        this.sign = Math.random() < 0.5 ? 1 : -1;
    
        // setting an initial opacity and size for our snowflake
        this.element.style.opacity = (.1 + Math.random()) / 3;
      }

      update() {
        // using some trigonometry to determine our x and y position
        this.counter += this.speed / 5000;
        this.xPos += this.sign * this.speed * Math.cos(this.counter) / 40;
        this.yPos += Math.sin(this.counter) / 40 + this.speed / 30;
        this.scale = .5 + Math.abs(10 * Math.cos(this.counter) / 20);
    
        // setting our snowflake's position
        setTransform(Math.round(this.xPos), Math.round(this.yPos), this.scale, this.element);
    
        // if snowflake goes below the browser window, move it back to the top
        if (this.yPos > browserHeight) {
          this.yPos = -50;
        }
      }
    }
  
    //
    // A performant way to set your snowflake's position and size
    //
    function setTransform(xPos, yPos, scale, el) {
      el.style.transform = `translate3d(${xPos}px, ${yPos}px, 0) scale(${scale}, ${scale})`;
    }
  
    //
    // The function responsible for creating the snowflake
    //
    function generateSnowflakes() {
  
      // get our snowflake element from the DOM and store it
      let originalSnowflake = document.querySelector(".snowflake");
  
      // access our snowflake element's parent container
      snowflakeContainer.style.display = "block";
  
      // get our browser's size
      browserWidth = document.documentElement.clientWidth;
      browserHeight = document.documentElement.clientHeight;
  
      // create each individual snowflake
      for (let i = 0; i < numberOfSnowflakes; i++) {
  
        // clone our original snowflake and add it to snowflakeContainer
        let snowflakeClone = originalSnowflake.cloneNode(true);
        snowflakeContainer.appendChild(snowflakeClone);
  
        // set our snowflake's initial position and related properties
        let initialXPos = getPosition(50, browserWidth);
        let initialYPos = getPosition(50, browserHeight);
        let speed = 5 + Math.random() * 40;
  
        // create our Snowflake object
        let snowflakeObject = new Snowflake(snowflakeClone,
          speed,
          initialXPos,
          initialYPos);
        snowflakes.push(snowflakeObject);
      }
  
      // remove the original snowflake because we no longer need it visible
      snowflakeContainer.removeChild(originalSnowflake);
  
      moveSnowflakes();
    }
  
    //
    // Responsible for moving each snowflake by calling its update function
    //
    function moveSnowflakes() {
  
      if (enableAnimations) {
        for (let i = 0; i < snowflakes.length; i++) {
          let snowflake = snowflakes[i];
          snowflake.update();
        }      
      }
  
      // Reset the position of all the snowflakes to a new value
      if (resetPosition) {
        browserWidth = document.documentElement.clientWidth;
        browserHeight = document.documentElement.clientHeight;
  
        for (let i = 0; i < snowflakes.length; i++) {
          let snowflake = snowflakes[i];
  
          snowflake.xPos = getPosition(50, browserWidth);
          snowflake.yPos = getPosition(50, browserHeight);
        }
  
        resetPosition = false;
      }
  
      requestAnimationFrame(moveSnowflakes);
    }
  
    //
    // This function returns a number between (maximum - offset) and (maximum + offset)
    //
    function getPosition(offset, size) {
      return Math.round(-1 * offset + Math.random() * (size + 2 * offset));
    }
  
    //
    // Trigger a reset of all the snowflakes' positions
    //
    function setResetFlag(e) {
      resetPosition = true;
    }
  </script>
</body>
</html>

I made a few minor coding cleanup changes like using classes instead of the older approach. I’ll get the main article updated shortly :slight_smile:

You can tweak the screen size breakpoints and number of snowflakes by just fiddling with the media queries! That should minimize your need to modify the JavaScript directly.

1 Like