AutoScroll to Selected Element (Pixels Every Frames) with Progress Bar

Click to pick an element, then auto-scroll by exact pixels every specified frames, with a visual progress bar for remaining frames.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         AutoScroll to Selected Element (Pixels Every Frames) with Progress Bar
// @description  Click to pick an element, then auto-scroll by exact pixels every specified frames, with a visual progress bar for remaining frames.
// @match        *://*/*
// @version 0.0.1.20250614211616
// @namespace https://greasyfork.org/users/1435046
// ==/UserScript==

(function () {
  let pixelsPerStep = 1;
  let framesPerStep = 1;
  let isScrollingActive = false;
  let previousFrameTimestamp = null;
  let selectedTargetElement = null;
  let targetScrollPositionY = null;
  let frameCountSinceLastScroll = 0;
  let averageFrameTimeMilliseconds = 16.67;
  let frameTimeSampleCount = 0;
  let frameTimeSumMilliseconds = 0;
  let previousScrollPositionY = 0;
  let pixelsScrolledLastFrame = 0;
  let initialRemainingFrames = 0;

  const controlPanel = document.createElement("div");
  Object.assign(controlPanel.style, {
    position: "fixed",
    top: "10px",
    right: "10px",
    background: "#333",
    color: "#fff",
    padding: "10px",
    zIndex: 10000,
    fontFamily: "monospace",
    borderRadius: "5px",
    lineHeight: "1.5",
    width: "260px",
    boxSizing: "border-box"
  });
  document.body.appendChild(controlPanel);

  const statusDisplay = document.createElement("div");
  statusDisplay.textContent = "No target selected";
  Object.assign(statusDisplay.style, { height: "20px", whiteSpace: "nowrap", overflow: "hidden" });
  controlPanel.appendChild(statusDisplay);

  const scrollSpeedDisplay = document.createElement("div");
  scrollSpeedDisplay.textContent = `Speed: ${pixelsPerStep} pixels every ${framesPerStep} frames`;
  Object.assign(scrollSpeedDisplay.style, { marginTop: "10px", height: "20px", whiteSpace: "nowrap", overflow: "hidden" });
  controlPanel.appendChild(scrollSpeedDisplay);

  const pixelsInputLabel = document.createElement("label");
  pixelsInputLabel.textContent = "Pixels per step:";
  controlPanel.appendChild(pixelsInputLabel);
  const pixelsInput = document.createElement("input");
  pixelsInput.type = "number";
  pixelsInput.min = "1";
  pixelsInput.value = pixelsPerStep;
  Object.assign(pixelsInput.style, { width: "100%", boxSizing: "border-box", height: "25px" });
  controlPanel.appendChild(pixelsInput);

  const pixelsScrolledLabel = document.createElement("label");
  pixelsScrolledLabel.textContent = "Actual px scrolled last frame:";
  controlPanel.appendChild(pixelsScrolledLabel);
  const pixelsScrolledInput = document.createElement("input");
  pixelsScrolledInput.type = "number";
  pixelsScrolledInput.readOnly = true;
  pixelsScrolledInput.value = "0";
  Object.assign(pixelsScrolledInput.style, { width: "100%", boxSizing: "border-box", height: "25px", backgroundColor: "#555" });
  controlPanel.appendChild(pixelsScrolledInput);

  const framesInputLabel = document.createElement("label");
  framesInputLabel.textContent = "Frames per step:";
  controlPanel.appendChild(framesInputLabel);
  const framesInput = document.createElement("input");
  framesInput.type = "number";
  framesInput.min = "1";
  framesInput.value = framesPerStep;
  Object.assign(framesInput.style, { width: "100%", boxSizing: "border-box", height: "25px" });
  controlPanel.appendChild(framesInput);

  const framesRemainingLabel = document.createElement("label");
  framesRemainingLabel.textContent = "Frames remaining:";
  controlPanel.appendChild(framesRemainingLabel);
  const framesRemainingInput = document.createElement("input");
  framesRemainingInput.type = "number";
  framesRemainingInput.readOnly = true;
  framesRemainingInput.value = "0";
  Object.assign(framesRemainingInput.style, { width: "100%", boxSizing: "border-box", height: "25px", backgroundColor: "#555" });
  controlPanel.appendChild(framesRemainingInput);

  const progressBar = document.createElement("progress");
  progressBar.value = 0;
  progressBar.max = 0;
  Object.assign(progressBar.style, { width: "100%", height: "10px", marginTop: "5px", boxSizing: "border-box" });
  controlPanel.appendChild(progressBar);

  const buttonSelectTarget = document.createElement("button");
  buttonSelectTarget.textContent = "Select Target";
  Object.assign(buttonSelectTarget.style, { display: "block", marginTop: "10px", width: "100%", height: "30px" });
  controlPanel.appendChild(buttonSelectTarget);

  const buttonStartScrolling = document.createElement("button");
  buttonStartScrolling.textContent = "Start Scrolling";
  Object.assign(buttonStartScrolling.style, { display: "block", marginTop: "10px", width: "100%", height: "30px" });
  buttonStartScrolling.disabled = true;
  controlPanel.appendChild(buttonStartScrolling);

  const buttonStopScrolling = document.createElement("button");
  buttonStopScrolling.textContent = "Stop Scrolling";
  Object.assign(buttonStopScrolling.style, { display: "block", marginTop: "10px", width: "100%", height: "30px" });
  buttonStopScrolling.disabled = true;
  controlPanel.appendChild(buttonStopScrolling);

  function updateScrollSpeed() {
    pixelsPerStep = Math.max(1, Number(pixelsInput.value));
    framesPerStep = Math.max(1, Number(framesInput.value));
    scrollSpeedDisplay.textContent = `Speed: ${pixelsPerStep} pixels every ${framesPerStep} frames`;
    updateTimeEstimateDisplay();
  }

  pixelsInput.addEventListener("input", updateScrollSpeed);
  framesInput.addEventListener("input", updateScrollSpeed);

  function getFormattedTimeFromNow(seconds) {
    const now = new Date();
    now.setSeconds(now.getSeconds() + seconds);
    return now.toTimeString().split(" ")[0];
  }

  function computeRemainingFrames() {
    if (targetScrollPositionY === null || pixelsPerStep === 0) return 0;
    const remainingPixels = targetScrollPositionY - window.scrollY;
    return Math.max(0, remainingPixels / pixelsPerStep);
  }

  function computeRemainingTimeInSeconds() {
    const remainingFrames = computeRemainingFrames();
    return (remainingFrames * averageFrameTimeMilliseconds) / 1000;
  }

  function updateTimeEstimateDisplay() {
    if (!selectedTargetElement) {
      statusDisplay.textContent = "No target selected";
      framesRemainingInput.value = "0";
      progressBar.value = 0;
    } else {
      const remainingFrames = computeRemainingFrames();
      const secondsRemaining = computeRemainingTimeInSeconds();
      framesRemainingInput.value = remainingFrames.toFixed(0);
      progressBar.value = remainingFrames;
      const eta = getFormattedTimeFromNow(secondsRemaining);
      const timeLabel = Math.ceil(secondsRemaining);
      statusDisplay.textContent = isScrollingActive ?
        `Time left: ${timeLabel} s (ETA: ${eta})` :
        `Ready: ${timeLabel} s (ETA: ${eta})`;
    }
  }

  function animationStep(timestamp) {
    if (!isScrollingActive) return;
    if (!previousFrameTimestamp) previousFrameTimestamp = timestamp;

    const delta = timestamp - previousFrameTimestamp;
    frameTimeSumMilliseconds += delta;
    frameTimeSampleCount++;
    averageFrameTimeMilliseconds = frameTimeSumMilliseconds / frameTimeSampleCount;

    const currentY = window.scrollY;
    pixelsScrolledLastFrame = currentY - previousScrollPositionY;
    previousScrollPositionY = currentY;
    pixelsScrolledInput.value = pixelsScrolledLastFrame.toFixed(2);

    frameCountSinceLastScroll++;
    if (frameCountSinceLastScroll >= framesPerStep) {
      window.scrollBy(0, pixelsPerStep);
      frameCountSinceLastScroll = 0;
    }

    previousFrameTimestamp = timestamp;
    updateTimeEstimateDisplay();

    if (window.scrollY + 0.5 >= targetScrollPositionY) {
      isScrollingActive = false;
      const arrivalTime = new Date().toTimeString().split(" ")[0];
      statusDisplay.textContent = `Target reached: <${selectedTargetElement.tagName.toLowerCase()}> at ${arrivalTime}`;
      buttonStartScrolling.disabled = false;
      buttonStopScrolling.disabled = true;
      framesRemainingInput.value = "0";
      progressBar.value = 0;
      return;
    }

    requestAnimationFrame(animationStep);
  }

  function startSmoothScrolling() {
    if (!selectedTargetElement) return;
    isScrollingActive = true;
    previousFrameTimestamp = null;
    frameCountSinceLastScroll = 0;
    previousScrollPositionY = window.scrollY;
    pixelsScrolledInput.value = "0";

    initialRemainingFrames = computeRemainingFrames();
    progressBar.max = initialRemainingFrames;
    progressBar.value = initialRemainingFrames;

    buttonStartScrolling.disabled = true;
    buttonStopScrolling.disabled = false;
    updateTimeEstimateDisplay();
    requestAnimationFrame(animationStep);
  }

  function stopSmoothScrolling() {
    isScrollingActive = false;
    previousFrameTimestamp = null;
    frameCountSinceLastScroll = 0;
    pixelsScrolledInput.value = "0";
    framesRemainingInput.value = "0";
    progressBar.value = 0;
    buttonStartScrolling.disabled = false;
    buttonStopScrolling.disabled = true;
    statusDisplay.textContent = "Scrolling stopped";
  }

  function initiateTargetElementSelection() {
    statusDisplay.textContent = "Click on target element";
    document.body.style.cursor = "crosshair";

    function handleClick(event) {
      event.preventDefault();
      event.stopPropagation();

      selectedTargetElement = event.target;
      const rect = selectedTargetElement.getBoundingClientRect();
      const absoluteTop = rect.top + window.pageYOffset;
      const bottomVisibleY = absoluteTop - window.innerHeight + selectedTargetElement.offsetHeight;
      targetScrollPositionY = Math.max(0, bottomVisibleY);

      document.body.style.cursor = "";
      document.removeEventListener("click", handleClick, true);

      buttonSelectTarget.disabled = false;
      buttonStartScrolling.disabled = false;
      buttonStopScrolling.disabled = true;

      statusDisplay.textContent = `Target set: <${selectedTargetElement.tagName.toLowerCase()}> at Y=${targetScrollPositionY.toFixed(2)}`;
      framesRemainingInput.value = computeRemainingFrames().toFixed(0);
      progressBar.max = computeRemainingFrames();
      progressBar.value = computeRemainingFrames();
    }

    document.addEventListener("click", handleClick, true);
    buttonSelectTarget.disabled = true;
  }

  buttonSelectTarget.addEventListener("click", initiateTargetElementSelection);
  buttonStartScrolling.addEventListener("click", startSmoothScrolling);
  buttonStopScrolling.addEventListener("click", stopSmoothScrolling);
  document.addEventListener("keydown", event => {
    if (event.key === "Escape") stopSmoothScrolling();
  });
})();