Search code examples
javascripthtmlcsssvg

SVG path, stroke-width is inconsistent. How to make it uniform


I am creating a loading screen using an SVG path. there is a grey path to represent the unloaded bar, and a white path to represent the percentage loaded.

document.addEventListener("DOMContentLoaded", () => {
  const loadingScreen = document.getElementById("loading-screen");
  const loadingPercentage = document.getElementById("loading-percentage");
  const progressPath = document.getElementById("progress-path");

  let progress = 0; // Current progress
  const minDisplayTime = 1750; // Minimum display time in milliseconds
  const fadeDelay = 500; // Duration of fade-out animation (in ms)
  const delayAfterComplete = 750; // Delay after hitting 100% (in ms)
  const startTime = Date.now(); // Record when loading starts
  const totalLength = 360; // Total length of the SVG path

  // Set the initial stroke-dasharray and stroke-dashoffset for the white bar
  progressPath.style.strokeDasharray = totalLength;
  progressPath.style.strokeDashoffset = totalLength;

  // Function to update progress
  function updateProgress() {
    const elapsedTime = Date.now() - startTime;

    // Calculate the progress as a percentage based on time
    if (elapsedTime < minDisplayTime) {
      progress = Math.min((elapsedTime / minDisplayTime) * 100, 100);
    } else {
      progress = 100; // Ensure progress reaches 100% after minimum display time
    }

    // Update percentage text
    loadingPercentage.textContent = `${Math.floor(progress)}%`;

    // Update progress bar position
    const offset = totalLength * (1 - progress / 100);
    progressPath.style.strokeDashoffset = offset;

    console.log(`Loading progress: ${progress}%`);

    // Trigger fade-out when progress hits 100%
    if (progress === 100) {
      const remainingTime = minDisplayTime - elapsedTime; // Ensure minimum display time
      setTimeout(() => {
        // Add a 0.75-second delay after hitting 100% before starting fade-out
        setTimeout(() => {
          loadingScreen.style.opacity = "0"; // Trigger fade-out animation

          // Completely hide the loading screen after the fade-out
          setTimeout(() => {
            loadingScreen.style.display = "none";
          }, fadeDelay); // Match the CSS transition duration
        }, delayAfterComplete); // 0.75-second delay after hitting 100%
      }, Math.max(remainingTime, 0)); // Wait for any remaining time if necessary
    } else {
      // Continue updating progress until it reaches 100%
      requestAnimationFrame(updateProgress);
    }
  }

  // Start the progress update loop
  updateProgress();
});
#loading-screen {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  background-color: #222;
  /* Dark background */
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 9999;
  /* Overlay all content */
  overflow: hidden;
  transition: opacity 0.5s ease;
  /* Fade-out effect */
}

/* Logo and percentage container */
#loading-content {
  position: absolute;
  display: flex;
  flex-direction: column;
  align-items: center;
  z-index: 1;
  /* Above the loading bar */
}

/* Logo styling */
#ws-logo {
  width: 15%;
  max-width: 100px;
  height: auto;
  margin-bottom: 10px;
}

/* Percentage text styling */
#loading-percentage {
  font-size: 1.5em;
  color: #fff;
  font-family: Arial, sans-serif;
}

/* SVG loading bar */
#loading-bar {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  /* Behind content */
  padding: 0;
  /* Ensure no internal spacing */
  margin: 0;
  /* Remove any external margin */
  box-sizing: border-box;
  /* Include border in dimensions */
}

#loading-bar path {
  vector-effect: ;
}

#progress-path {
  transition: stroke-dashoffset 0.2s ease-out;
  /* Smooth animation for the progress bar */
}
<!-- Loading Screen -->
<div id="loading-screen">
  <div id="loading-content">
    <img id="ws-logo" src="ws-logo.png" alt="WS Logo">
    <div id="loading-percentage">0%</div>
  </div>
  <svg id="loading-bar" width="100%" height="100%" viewBox="0 0 100 100" preserveAspectRatio="none">
          <!-- Grey background path -->
          <path
            d="M5 5 L5 95 L95 95 L95 5 Z"
            stroke="#666"
            stroke-width="1"
            stroke-linecap="square"
            fill="none"
          />
          <!-- White progress path -->
          <path
            id="progress-path"
            d="M5 5 L5 95 L95 95 L95 5 Z"
            stroke="#fff"
            stroke-width="1"
            stroke-linecap="square"
            fill="none"
            stroke-dasharray="0"
            stroke-dashoffset="0"
          />
        </svg>
</div>

here is a CodePen with what i've created: https://codepen.io/Ramon-117/pen/QwLrxvb

The concept: the loading bar will resemble a grey box border. As the site loads, the progress bar will grow white starting from the top right corner (0%), down to the bottom left corner (25%), to the bottom right corner (50%), up to the top right corner (75%), and back to the top left corner (100%).

The issue: I have everything working except for a key issue. If the viewheight is larger than the viewwidth, then then the top and bottom widths will be thicker than the left and right and vise-versa.

What I want: I want the stroke to have an even thickness throughout the path no matter the viewport size. I also want to be able to adjust said thickness to account for mobile devices.

What I've tried:

  • stroke-width="1" *doesnt work

  • stroke-width="1px" *doesnt work

  • stroke-width="1%" *doesnt work

  • stroke-width="1vmin" *doesnt work

  • vector-effect:non-scaling-stroke; *breaks the animation

  • vector-effect:non-scaling-stroke; AND vector-effect:non-rotation;

  • dynamically setting the width based on viewport with JavaScript (i dont have this code anymore)


Solution

  • You may simplify this loader by replacing the <path> with a <rect> element as it supports relative units like %.

    This way we don't need a viewBox or preserveAspectRatio – the rect will adapt its dimensions to the parent HTM element.

    Besides, you can simplify the dashoffset calculations by applying the pathLength attribute. This way you can use the percentage based progress variable for the dynamic stroke-dashoffset value.

    document.addEventListener("DOMContentLoaded", () => {
      const loadingScreen = document.getElementById("loading-screen");
      const loadingPercentage = document.getElementById("loading-percentage");
      const progressPath = document.getElementById("progress-path");
    
      let progress = 0; // Current progress
      const minDisplayTime = 1750; // Minimum display time in milliseconds
      const fadeDelay = 500; // Duration of fade-out animation (in ms)
      const delayAfterComplete = 750; // Delay after hitting 100% (in ms)
      const startTime = Date.now(); // Record when loading starts
    
    
      // Function to update progress
      function updateProgress() {
        const elapsedTime = Date.now() - startTime;
    
        // Calculate the progress as a percentage based on time
        if (elapsedTime < minDisplayTime) {
          progress = Math.min((elapsedTime / minDisplayTime) * 100, 100);
        } else {
          progress = 100; // Ensure progress reaches 100% after minimum display time
        }
        
        // Update percentage text
        loadingPercentage.textContent = `${Math.floor(progress)}%`;
    
        // Update progress bar position
        progressPath.style.strokeDashoffset = progress-100;
    
        console.log(`Loading progress: ${progress}%`);
    
        // Trigger fade-out when progress hits 100%
        if (progress === 100) {
          const remainingTime = minDisplayTime - elapsedTime; // Ensure minimum display time
          setTimeout(() => {
            // Add a 0.75-second delay after hitting 100% before starting fade-out
            setTimeout(() => {
              loadingScreen.style.opacity = "0"; // Trigger fade-out animation
    
              // Completely hide the loading screen after the fade-out
              setTimeout(() => {
                loadingScreen.style.display = "none";
              }, fadeDelay); // Match the CSS transition duration
            }, delayAfterComplete); // 0.75-second delay after hitting 100%
          }, Math.max(remainingTime, 0)); // Wait for any remaining time if necessary
        } else {
          // Continue updating progress until it reaches 100%
          requestAnimationFrame(updateProgress);
        }
      }
    
      // Start the progress update loop
      updateProgress();
    });
    #loading-screen {
      position: fixed;
      top: 0;
      left: 0;
      width: 100vw;
      height: 100vh;
      background-color: #222;
      /* Dark background */
      display: flex;
      justify-content: center;
      align-items: center;
      z-index: 9999;
      /* Overlay all content */
      overflow: hidden;
      transition: opacity 0.5s ease;
      /* Fade-out effect */
    }
    
    /* Logo and percentage container */
    #loading-content {
      position: absolute;
      display: flex;
      flex-direction: column;
      align-items: center;
      z-index: 1;
      /* Above the loading bar */
    }
    
    /* Logo styling */
    #ws-logo {
      width: 15%;
      max-width: 100px;
      height: auto;
      margin-bottom: 10px;
    }
    
    /* Percentage text styling */
    #loading-percentage {
      font-size: 1.5em;
      color: #fff;
      font-family: Arial, sans-serif;
    }
    
    /* SVG loading bar */
    #loading-bar {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      z-index: 0;
      /* Behind content */
      padding: 0;
      /* Ensure no internal spacing */
      margin: 0;
      /* Remove any external margin */
      box-sizing: border-box;
      /* Include border in dimensions */
    }
    
    #loading-bar path {
      //vector-effect: ;
    }
    
    
    #progress-path {
      transition: stroke-dashoffset 0.2s ease-out;
      /* Smooth animation for the progress bar */
    }
    <!-- Loading Screen -->
    <div id="loading-screen">
      <div id="loading-content">
        <div id="loading-percentage">0%</div>
      </div>
      <svg id="loading-bar" width="100%" height="100%" >
        <!-- Grey background path -->
    
        <rect width="100%" height="100%" stroke="#666" stroke-width="2%" stroke-linecap="square" fill="none" />
        <rect id="progress-path" width="100%" height="100%" pathLength="100" stroke="#fff" stroke-width="2%" stroke-linecap="square" fill="none" stroke-dasharray="100 100" stroke-dashoffset="-100" />
    
      </svg>
    </div>