routerconsole/jsp/js/progressx.js

/**
 * @module progressx
 * @file ProgressX 1.1, 2025-09-09
 * @description A lightweight progress bar library with theme support, optimized by dr|z3d
 * @author Ryad Pasha
 * @license MIT
 * @see https://github.com/ryadpasha/progressx
 */

/**
 * @function initProgressX
 * @param {Window} window - The window object
 * @param {Document} document - The document object
 * @returns {void}
 */
function initProgressX(window, document) {
  "use strict";

  if (!window.requestAnimationFrame) {
    window.progressx = { config: () => {}, show: () => {}, progress: () => {}, hide: () => {} };
    return;
  }

  let canvas, context, gradient = null;
  let fadeTimer = null;
  let currentProgress = 0, isVisible = false;
  let lastColors = null;
  let frameRequested = false;
  let animationFrameId = null;
  let animationLocked = false;

  const defaultColors = {"0.0": "rgba(220,48,16,.8)", "1.0": "rgba(255,96,0,.8)"};

  const options = {
    autoRun: true,
    barThickness: 3,
    barColors: {
      default: defaultColors,
      classic: {"0.0": "rgba(48,64,192,.7)", "1.0": "rgba(48,64,255,.7)"},
      midnight: {"0.0": "rgba(72,0,72,.8)", "1.0": "rgba(160,0,160,.8)"},
      current: defaultColors
    }
  };

  let currentColorSetName = "default";

  function createCanvas() {
    if (canvas) { return; }
    canvas = document.createElement("canvas");
    canvas.id = "pageloader";
    canvas.style.cssText = "position:fixed;top:0;left:0;right:0;z-index:100001;" +
                           "margin:0;padding:0;display:none;opacity:1";
    injectCanvas();
    context = canvas.getContext("2d");
    window.addEventListener("resize", onResized, { passive: true });
  }

  function injectCanvas() {
    const html = document.documentElement;
    html.firstChild
      ? html.insertBefore(canvas, html.firstChild)
      : html.appendChild(canvas);
  }

  let ticking = false;
  function onResized() {
    if (!ticking) {
      requestAnimationFrame(() => {
        draw();
        ticking = false;
      });
      ticking = true;
    }
  }

  function colorsEqual(a, b) {
    if (!a || !b) { return false; }
    const keysA = Object.keys(a), keysB = Object.keys(b);
    if (keysA.length !== keysB.length) { return false; }
    for (let key of keysA) {
      if (a[key] !== b[key]) { return false; }
    }
    return true;
  }

  function draw() {
    if (!canvas || !context) { return; }

    const width = window.innerWidth;
    const height = options.barThickness;
    const colors = options.barColors[currentColorSetName] || options.barColors.default;
    options.barColors.current = colors;

    if (canvas.width !== width) { canvas.width = width; }
    if (canvas.height !== height) { canvas.height = height; }

    if (!colorsEqual(colors, lastColors)) {
      gradient = context.createLinearGradient(0, 0, width, 0);
      for (const stop in colors) {
        gradient.addColorStop(parseFloat(stop), colors[stop]);
      }
      lastColors = { ...colors };
    }

    context.clearRect(0, 0, width, height);
    context.lineWidth = height;
    context.beginPath();
    context.moveTo(0, height / 2);
    context.lineTo(Math.ceil(currentProgress * width), height / 2);
    context.strokeStyle = gradient;
    context.stroke();
  }

  function runProgressLoop() {
    if (animationLocked) { return; }
    animationLocked = true;

    function animate() {
      const increment = 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2);
      if (currentProgress < 1) {
        progressxProgress(currentProgress + increment, false);
        requestAnimationFrame(animate);
      } else {
        animationLocked = false;
      }
    }

    requestAnimationFrame(animate);
  }

  function progressxShow(colorSet) {
    if (isVisible || animationLocked) { return; }
    isVisible = true;

    if (fadeTimer) {
      cancelAnimationFrame(fadeTimer);
      fadeTimer = null;
    }

    if (animationFrameId) {
      cancelAnimationFrame(animationFrameId);
      animationFrameId = null;
    }

    if (!canvas) { createCanvas(); }
    window.addEventListener("resize", onResized, { passive: true });
    canvas.style.opacity = "1";
    canvas.style.display = "block";
    currentProgress = 0;
    currentColorSetName = colorSet || "default";
    requestAnimationFrame(draw);
    animationLocked = true;

    if (options.autoRun) { runProgressLoop(); }
  }

  function progressxHide() {
    if (!isVisible) { return; }
    isVisible = false;

    let opacity = 1;
    function fadeLoop() {
      if (progressxProgress("+0.1", false) >= 1) {
        opacity -= 0.045;
        canvas.style.opacity = opacity.toString();
        if (opacity <= 0.2) {
          canvas.style.display = "none";
          fadeTimer = null;
          animationLocked = false;
          return;
        }
      }
      fadeTimer = requestAnimationFrame(fadeLoop);
    }

    fadeLoop();
  }

  function progressxProgress(value, animate = true) {
    if (!canvas || !context) return currentProgress;

    const oldValue = currentProgress;

    if (typeof value === "string") {
      const op = value[0], num = parseFloat(value.slice(1));
      value = op === "+" ? currentProgress + num : op === "-" ? currentProgress - num : num;
    }

    const target = Math.min(1, Math.max(0, value));

    if (!animate || target === oldValue) {
      currentProgress = target;
      if (!frameRequested) {
        frameRequested = true;
        requestAnimationFrame(() => {
          draw();
          frameRequested = false;
        });
      }
      return currentProgress;
    }

    if (animationLocked) return currentProgress;

    animationLocked = true;

    if (animationFrameId) cancelAnimationFrame(animationFrameId);

    const duration = 300;
    const start = performance.now();

    const animateProgress = (now) => {
      const elapsed = now - start;
      const progress = Math.min(elapsed / duration, 1);
      const easeOut = 1 - Math.pow(1 - progress, 3);
      currentProgress = oldValue + (target - oldValue) * easeOut;
      draw();

      if (progress < 1) {
        animationFrameId = requestAnimationFrame(animateProgress);
      } else {
        currentProgress = target;
        animationLocked = false;
        animationFrameId = null;
        draw();
      }
    };

    animationFrameId = requestAnimationFrame(animateProgress);
    return currentProgress;
  }

  function progressxConfig(opts) {
    if (!opts) return;
    Object.assign(options, opts);
    if (opts.barColors) {
      Object.assign(options.barColors, opts.barColors);
    }
    options.barColors.current = options.barColors[currentColorSetName] || options.barColors.default;
  }

  window.progressx = { config: progressxConfig, show: progressxShow, progress: progressxProgress, hide: progressxHide };
}

initProgressX(window, document);

window.addEventListener("load", () => { progressx.hide(); });