CollabVM CRT Mode Button

Adds a toggleable CRT-style effect to the CollabVM display.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         CollabVM CRT Mode Button
// @name:es      Botón de modo CRT de CollabVM
// @match        https://computernewb.com/collab-vm/*
// @grant        none
// @description  Adds a toggleable CRT-style effect to the CollabVM display.
// @description:es  Agrega un efecto estilo CRT alternable a la pantalla CollabVM.
// @license MIT
// @version 0.0.1.30250617163501
// @namespace https://greasyfork.org/users/1484733
// ==/UserScript==

(function () {
  // console.log("[CRT Script] Script loaded!");

function getVmDisplay() {
  // Returns the #vmDisplay element if it exists, otherwise logs an error and returns null.
  const el = document.getElementById("vmDisplay");
  if (!el) {
    console.log("[CRT Script] #vmDisplay not found.");
    return null;
  }
  return el;
}

function ensureCRTEffects(vmDisplay) {
  // Injects CRT effect styles if not already present,
  // and ensures CRT overlay divs exist inside #vmDisplay.
  try {
    if (!document.getElementById("crt-effects-style")) {
      const style = document.createElement("style");
      style.id = "crt-effects-style";
      style.textContent = `
#vmDisplay {
  display: table;
  margin: 24px auto;
  position: relative;
}
#vmDisplay.crt-canvas {
  background: #191919 radial-gradient(ellipse at 60% 40%, #222 75%, #111 100%);
  overflow: hidden;
  will-change: transform;
  line-height: 0;
}
#vmDisplay.crt-canvas canvas {
  display: block;
  margin: 0;
  padding: 0;
  border-radius: 8px;
  /* Slight retro warp */
  filter: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><filter id="w"><feTurbulence type="turbulence" baseFrequency="0.009 0.012" numOctaves="2" result="t"/><feDisplacementMap in2="t" in="SourceGraphic" scale="2" xChannelSelector="R" yChannelSelector="G"/></filter></svg>#w');
  border: none !important;
  outline: none !important;
  box-shadow: none !important;
}
#vmDisplay.crt-canvas::before {
  content: '';
  pointer-events: none;
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  background: repeating-linear-gradient(rgba(0,0,0,0.16) 0px, rgba(0,0,0,0.16) 1.4px, transparent 1.4px, transparent 3px);
  mix-blend-mode: multiply;
  z-index: 10;
}
#vmDisplay.crt-canvas::after {
  content: '';
  pointer-events: none;
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  background: radial-gradient(ellipse at center, transparent 85%, rgba(0,0,0,0.10) 100%);
  z-index: 11;
}
.crt-bulge {
  pointer-events: none;
  position: absolute;
  inset: 0;
  z-index: 2;
  border-radius: 32px;
  background: radial-gradient(ellipse at 60% 45%, rgba(255,255,255,0.17) 0%, rgba(0,0,0,0.39) 100%);
  opacity: 0.24;
  mix-blend-mode: lighten;
}
.crt-scanlines {
  pointer-events: none;
  position: absolute;
  inset: 0;
  z-index: 3;
  background: repeating-linear-gradient(
    to bottom,
    transparent 0px,
    transparent 1.15px,
    rgba(0,0,0,0.20) 1.15px,
    rgba(0,0,0,0.06) 1.8px,
    transparent 2.2px,
    transparent 3.3px
  );
  opacity: 0.19;
  mix-blend-mode: multiply;
}
.crt-dotmask {
  pointer-events: none;
  position: absolute;
  inset: 0;
  z-index: 4;
  background: repeating-linear-gradient(
    to right,
    rgba(255,0,0,0.12) 0px, rgba(255,0,0,0.12) 1px, transparent 1.5px, transparent 3px
  ),
  repeating-linear-gradient(
    to bottom,
    transparent 0px, transparent 2px, rgba(0,255,0,0.12) 2px, transparent 3px
  );
  opacity: 0.15;
  mix-blend-mode: screen;
}
.crt-phosphor {
  pointer-events: none;
  position: absolute;
  inset: 0;
  z-index: 5;
  box-shadow: 0 0 24px 8px #aaffaa, 0 0 64px 14px #00bfff;
  opacity: 0.20;
  filter: blur(1.5px) brightness(1.1);
}
      `;
      document.head.appendChild(style);
    }
    // Ensure CRT overlays exist
    ["crt-bulge", "crt-scanlines", "crt-dotmask", "crt-phosphor"].forEach(cls => {
      if (!vmDisplay.querySelector("." + cls)) {
        const div = document.createElement("div");
        div.className = cls;
        vmDisplay.appendChild(div);
      }
    });
  } catch (e) {
    console.error("[CRT Script] Error in ensureCRTEffects:", e);
  }
}

  let crtFrameCount = 0;
  let crtFlickerValue = 1;
  let crtFlickerFrame;
  let retroEffect = false;

function crtFlicker() {
  // Animates CRT flicker and simulates an antialiased filter.
  try {
    crtFrameCount++;
    if (crtFrameCount % 2 === 0) {
      const target = 0.98 + Math.random() * 0.06;
      crtFlickerValue += (target - crtFlickerValue) * 0.25;
      const vmDisplay = getVmDisplay();
      if (vmDisplay && vmDisplay.classList.contains("crt-canvas")) {
        const canvas = vmDisplay.querySelector("canvas");
        if (canvas) {
          // "FXAA" style softening:
          canvas.style.filter =
            `contrast(1.05) brightness(${crtFlickerValue}) saturate(1.05) blur(0.4px) drop-shadow(0 0 1px #fff5)`;
        }
      }
    }
    crtFlickerFrame = requestAnimationFrame(crtFlicker);
  } catch (e) {
    console.error("[CRT Script] Error in crtFlicker:", e);
  }
}

function startCrtFlicker() {
  // Enables CRT flicker and ensures CRT overlays/styles are present.
  try {
    const vmDisplay = getVmDisplay();
    if (vmDisplay) {
      ensureCRTEffects(vmDisplay);
    }
    crtFrameCount = 0;
    crtFlickerValue = 1;
    crtFlicker();
    console.log("[CRT Script] CRT flicker started.");
  } catch (e) {
    console.error("[CRT Script] Error in startCrtFlicker:", e);
  }
}


function stopCrtFlicker() {
  // Disables the CRT flicker animation and resets frame reference.
  try {
    if (typeof crtFlickerFrame !== "undefined") {
      cancelAnimationFrame(crtFlickerFrame);
      crtFlickerFrame = undefined;
      console.log("[CRT Script] CRT flicker stopped.");
    }
  } catch (e) {
    console.error("[CRT Script] Error in stopCrtFlicker:", e);
  }
}

function addCrtBtn() {
  // Adds the CRT toggle button to the UI and manages its behavior.
  try {
    const btns = document.getElementById("btns");
    if (!btns) {
      // Throttle log spam
      if (!addCrtBtn.lastLog || Date.now() - addCrtBtn.lastLog > 2000) {
        console.log("[CRT Script] #btns not found, will try again.");
        addCrtBtn.lastLog = Date.now();
      }
      return;
    }

    let wrapper = document.getElementById("crtBtnWrapper");
    if (!wrapper) {
      wrapper = document.createElement("div");
      wrapper.id = "crtBtnWrapper";
      wrapper.style.display = "inline-block";

      // Create the CRT button
      const btn = document.createElement("button");
      btn.id = "crtBtn";
      btn.className = "btn btn-secondary";
      btn.style.display = "inline";
      btn.style.margin = "0";
      btn.textContent = retroEffect ? "🖥️ Normal Mode" : "📺 CRT Mode";
      btn.onclick = function () {
        retroEffect = !retroEffect;
        btn.textContent = retroEffect ? "🖥️ Normal Mode" : "📺 CRT Mode";
        const vmDisplay = getVmDisplay();
        if (!vmDisplay) return;
        vmDisplay.classList.toggle("crt-canvas", retroEffect);
        if (retroEffect) {
          startCrtFlicker();
        } else {
          stopCrtFlicker();
          const canvas = vmDisplay.querySelector("canvas");
          if (canvas) canvas.style.removeProperty("filter");
          ["crt-bulge", "crt-scanlines", "crt-dotmask", "crt-phosphor"].forEach(cls => {
            const el = vmDisplay.querySelector(`.${cls}`);
            if (el) el.remove();
          });
        }
      };
      wrapper.appendChild(btn);
      btns.insertBefore(wrapper, btns.firstChild);
    } else {
      // Update button text to match state if already exists
      const btn = wrapper.querySelector("#crtBtn");
      if (btn) btn.textContent = retroEffect ? "🖥️ Normal Mode" : "📺 CRT Mode";
    }
  } catch (e) {
    console.error("[CRT Script] Error in addCrtBtn:", e);
  }
}

addCrtBtn();

})();