YouTube osu! Play-Along Mode (v1.7.7 Default Trail + 5ms Fade Option)

osu! play-along mode with default trail settings (2 images, 150ms fade) and fade slider that goes down to 5ms 💨🖱️✨🎯 Super snappy!

// ==UserScript==
// @name         YouTube osu! Play-Along Mode (v1.7.7 Default Trail + 5ms Fade Option)
// @namespace    https://osu.ppy.sh/
// @version      1.7.8
// @description  osu! play-along mode with default trail settings (2 images, 150ms fade) and fade slider that goes down to 5ms 💨🖱️✨🎯 Super snappy!
// @author       ThunderBirdo+ChatGPT
// @license      MIT
// @match        https://www.youtube.com/*
// @grant        none
// ==/UserScript==

(function () {
  'use strict';

  let popupShown = false;
  let popupDisabled = false;
  let playModeActive = false;
  let customCursorStyle = null;
  let customStyle = null;
  let trailURL = '';
  let trailMax = 2;
  let fadeTime = 150;
  let trailImages = [];
  let trailContainer = null;

  function createPopup() {
    if (document.getElementById('osu-playalong-popup') || popupDisabled) return;

    const popup = document.createElement('div');
    popup.id = 'osu-playalong-popup';
    Object.assign(popup.style, {
      position: 'fixed',
      top: '10px',
      left: '10px',
      background: '#111',
      color: 'white',
      padding: '10px',
      borderRadius: '8px',
      zIndex: '9999',
      fontSize: '16px',
      display: 'flex',
      alignItems: 'center',
      gap: '10px',
    });

    const label = document.createElement('span');
    label.textContent = 'Play Along?';
    const yesBtn = document.createElement('button');
    yesBtn.textContent = 'Yes';
    const noBtn = document.createElement('button');
    noBtn.textContent = 'No';

    yesBtn.addEventListener('click', () => {
      const video = document.querySelector('video');
      if (video) video.pause();
      popup.remove();
      requestAnimationFrame(() => {
        setTimeout(showConfigForm, 100);
      });
    });

    noBtn.addEventListener('click', () => {
      popupDisabled = true;
      popup.remove();
    });

    popup.appendChild(label);
    popup.appendChild(yesBtn);
    popup.appendChild(noBtn);
    document.body.appendChild(popup);
  }

  function showConfigForm() {
    if (document.getElementById('osu-playalong-config')) return;

    const form = document.createElement('div');
    form.id = 'osu-playalong-config';
    Object.assign(form.style, {
      position: 'fixed',
      top: '10px',
      left: '10px',
      background: '#222',
      color: 'white',
      padding: '10px',
      borderRadius: '8px',
      zIndex: '9999',
      fontSize: '14px',
      display: 'flex',
      flexDirection: 'column',
      gap: '6px',
      width: '320px',
    });

    const cursorInput = document.createElement('input');
    cursorInput.type = 'text';
    cursorInput.placeholder = 'Cursor Image URL';
    cursorInput.style.width = '100%';

    const defaultCursorBtn = document.createElement('button');
    defaultCursorBtn.textContent = 'Use Default Cursor';
    defaultCursorBtn.addEventListener('click', () => {
      cursorInput.value = 'https://osuskinner.com/elements/interface/cursor/152/1557.png';
    });

    const trailInput = document.createElement('input');
    trailInput.type = 'text';
    trailInput.placeholder = 'Cursor Trail Image URL';
    trailInput.style.width = '100%';

    const defaultTrailBtn = document.createElement('button');
    defaultTrailBtn.textContent = 'Use Default Trail';
    defaultTrailBtn.addEventListener('click', () => {
      trailInput.value = 'https://osuskinner.com/elements/interface/cursortrail/1562/8236.png';
    });

    const trailLengthInput = document.createElement('input');
    trailLengthInput.type = 'range';
    trailLengthInput.min = '0';
    trailLengthInput.max = '50';
    trailLengthInput.value = trailMax;
    const trailLabel = document.createElement('label');
    trailLabel.textContent = `Trail Length: ${trailMax}`;
    trailLengthInput.addEventListener('input', () => {
      trailLabel.textContent = `Trail Length: ${trailLengthInput.value}`;
    });

    const fadeInput = document.createElement('input');
    fadeInput.type = 'range';
    fadeInput.min = '5';
    fadeInput.max = '2000';
    fadeInput.step = '5';
    fadeInput.value = fadeTime;
    const fadeLabel = document.createElement('label');
    fadeLabel.textContent = `Fade Duration: ${fadeTime}ms`;
    fadeInput.addEventListener('input', () => {
      fadeLabel.textContent = `Fade Duration: ${fadeInput.value}ms`;
    });

    const restartCheckbox = document.createElement('input');
    restartCheckbox.type = 'checkbox';
    const restartLabel = document.createElement('label');
    restartLabel.appendChild(restartCheckbox);
    restartLabel.appendChild(document.createTextNode(' Start from beginning'));

    const rateSlider = document.createElement('input');
    rateSlider.type = 'range';
    rateSlider.min = '0.25';
    rateSlider.max = '2';
    rateSlider.step = '0.05';
    rateSlider.value = '1';

    const rateDisplay = document.createElement('div');
    const updateRateDisplay = () => {
      let v = parseFloat(rateSlider.value);
      let label = v === 0.75 ? 'HT' : v === 1.5 ? 'DT' : v.toFixed(2) + 'x';
      rateDisplay.textContent = `Playback: ${label}`;
    };
    rateSlider.addEventListener('input', updateRateDisplay);
    updateRateDisplay();

    const startBtn = document.createElement('button');
    startBtn.textContent = 'Start';

    startBtn.addEventListener('click', () => {
      const cursorURL = cursorInput.value.trim();
      trailURL = trailInput.value.trim();
      trailMax = parseInt(trailLengthInput.value.trim()) || 0;
      fadeTime = parseInt(fadeInput.value.trim()) || 150;
      const restart = restartCheckbox.checked;
      const rate = parseFloat(rateSlider.value);

      if (!cursorURL) {
        alert('Please enter a cursor image URL.');
        return;
      }

      form.remove();
      startPlayMode(cursorURL, restart, rate);
    });

    form.appendChild(cursorInput);
    form.appendChild(defaultCursorBtn);
    form.appendChild(trailInput);
    form.appendChild(defaultTrailBtn);
    form.appendChild(trailLabel);
    form.appendChild(trailLengthInput);
    form.appendChild(fadeLabel);
    form.appendChild(fadeInput);
    form.appendChild(restartLabel);
    form.appendChild(rateSlider);
    form.appendChild(rateDisplay);
    form.appendChild(startBtn);

    document.body.appendChild(form);
  }

  function startPlayMode(cursorURL, restart, rate) {
    const video = document.querySelector('video');
    const player = document.querySelector('#movie_player');
    if (!video || !player) {
      alert('Video not found.');
      return;
    }

    video.pause();
    if (restart) video.currentTime = 0;
    video.playbackRate = rate;
    playModeActive = true;

    if (customCursorStyle) customCursorStyle.remove();
    customCursorStyle = document.createElement('style');
    customCursorStyle.textContent = `
      * {
        cursor: url("${cursorURL}") 16 16, auto !important;
      }
    `;
    document.head.appendChild(customCursorStyle);

    if (customStyle) customStyle.remove();
    customStyle = document.createElement('style');
    customStyle.textContent = `
      .ytp-chrome-bottom,
      .ytp-gradient-top,
      .ytp-gradient-bottom,
      .ytp-show-cards-title,
      .ytp-title,
      .ytp-pause-overlay,
      .ytp-chrome-top,
      .ytp-bezel {
        display: none !important;
        pointer-events: none !important;
      }
    `;
    document.head.appendChild(customStyle);

    trailContainer = document.querySelector('#movie_player') || document.body;

    if (trailURL && trailMax > 0) {
      document.addEventListener('pointermove', spawnTrailImage);
    }

    window.addEventListener('keydown', blockAllKeys, true);
    window.addEventListener('mousedown', blockClicks, true);
    window.addEventListener('click', blockClicks, true);

    document.addEventListener('keydown', (e) => {
      if (e.key === 'Escape' && playModeActive) {
        exitPlayMode(video);
      }
    });

    document.addEventListener('fullscreenchange', () => {
      if (!document.fullscreenElement && playModeActive) {
        exitPlayMode(video);
      }
    });

    setTimeout(() => {
      if (player.requestFullscreen) {
        player.requestFullscreen().catch(err => console.log('Fullscreen failed:', err));
      }
      video.play().catch(err => console.log('Play failed:', err));
    }, 100);
  }

  function spawnTrailImage(e) {
    if (!trailContainer) return;

    const trail = document.createElement('img');
    trail.src = trailURL;
    trail.style.position = 'fixed';
    trail.style.left = `${e.clientX}px`;
    trail.style.top = `${e.clientY}px`;
    trail.style.width = '32px';
    trail.style.height = '32px';
    trail.style.pointerEvents = 'none';
    trail.style.zIndex = '10000';
    trail.style.transition = `opacity ${fadeTime}ms linear`;
    trail.style.opacity = '1';
    trailContainer.appendChild(trail);
    trailImages.push(trail);

    if (trailImages.length > trailMax) {
      const oldest = trailImages.shift();
      oldest.style.opacity = '0';
      setTimeout(() => oldest.remove(), fadeTime);
    }
  }

  function blockAllKeys(e) {
    if (playModeActive) {
      e.stopImmediatePropagation();
      e.preventDefault();
    }
  }

  function blockClicks(e) {
    if (playModeActive) {
      e.stopImmediatePropagation();
      e.preventDefault();
    }
  }

  function exitPlayMode(video) {
    playModeActive = false;

    if (document.fullscreenElement) document.exitFullscreen();
    if (video) {
      video.pause();
      video.playbackRate = 1;
    }

    if (customCursorStyle) {
      customCursorStyle.remove();
      customCursorStyle = null;
    }

    if (customStyle) {
      customStyle.remove();
      customStyle = null;
    }

    for (const img of trailImages) img.remove();
    trailImages = [];
    trailContainer = null;

    document.removeEventListener('pointermove', spawnTrailImage);
    window.removeEventListener('keydown', blockAllKeys, true);
    window.removeEventListener('mousedown', blockClicks, true);
    window.removeEventListener('click', blockClicks, true);
  }

  function onYouTubeWatchPage() {
    if (!location.href.includes('/watch')) return;
    const checkReady = setInterval(() => {
      const video = document.querySelector('video');
      if (video && !popupShown) {
        popupShown = true;
        createPopup();
        clearInterval(checkReady);
      }
    }, 500);
  }

  window.addEventListener('load', onYouTubeWatchPage);

  const observer = new MutationObserver(() => {
    if (
      location.href.includes('/watch') &&
      !popupDisabled &&
      !document.getElementById('osu-playalong-popup') &&
      !document.getElementById('osu-playalong-config')
    ) {
      popupShown = false;
      onYouTubeWatchPage();
    }
  });

  observer.observe(document.body, { childList: true, subtree: true });
})();
长期地址
遇到问题?请前往 GitHub 提 Issues。