Google 圖片搜尋直開

Google 圖片搜尋點擊圖片直接懸浮原圖,可用滾輪放大縮小

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Google 圖片搜尋直開
// @namespace    http://tampermonkey.net/
// @version      1.5
// @description  Google 圖片搜尋點擊圖片直接懸浮原圖,可用滾輪放大縮小
// @author       shanlan(grok-4-fast-reasoning)
// @match        https://www.google.com/search*
// @grant        GM_download
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(function(){
  'use strict';
  document.head.appendChild(Object.assign(document.createElement('style'), {
    textContent: "#tm-image-modal{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.8);z-index:9999}#tm-image-modal canvas{position:absolute;left:0;top:0;width:100vw;height:100vh;border:none;transform-origin:0 0;user-select:none;cursor:move}#tm-image-modal .close{position:absolute;top:10px;right:20px;color:#fff;font-size:30px;font-weight:bold;cursor:pointer;z-index:10000;line-height:1;padding:5px}#tm-image-modal .fit{position:absolute;top:50px;right:20px;color:#fff;font-size:24px;font-weight:bold;cursor:pointer;z-index:10000;line-height:1;padding:5px;background:rgba(0,0,0,0.5);border-radius:5px}#tm-image-modal .center{position:absolute;top:90px;right:20px;color:#fff;font-size:24px;font-weight:bold;cursor:pointer;z-index:10000;line-height:1;padding:5px;background:rgba(0,0,0,0.5);border-radius:5px}#tm-image-modal .reset{position:absolute;top:130px;right:20px;color:#fff;font-size:24px;font-weight:bold;cursor:pointer;z-index:10000;line-height:1;padding:5px;background:rgba(0,0,0,0.5);border-radius:5px}#tm-image-modal .interp{position:absolute;top:170px;right:20px;color:#fff;font-size:24px;font-weight:bold;cursor:pointer;z-index:10000;line-height:1;padding:5px;background:rgba(0,0,0,0.5);border-radius:5px}#tm-image-modal .download{position:absolute;top:210px;right:20px;color:#fff;font-size:24px;font-weight:bold;cursor:pointer;z-index:10000;line-height:1;padding:5px;background:rgba(0,0,0,0.5);border-radius:5px}"
  }));
  const getVisibleSrc = a => {
    for (const img of a.querySelectorAll("img")) {
      const s = getComputedStyle(img);
      if (s.visibility !== "hidden" && img.offsetWidth && img.offsetHeight) return img.src;
    }
  }, openModal = a => {
    const m = document.createElement("div");
    m.id = "tm-image-modal";
    const closeBtn = document.createElement("span");
    closeBtn.innerHTML = "×";
    closeBtn.className = "close";
    const fitBtn = document.createElement("span");
    fitBtn.innerHTML = "恢復預設";
    fitBtn.className = "fit";
    const centerBtn = document.createElement("span");
    centerBtn.innerHTML = "置中";
    centerBtn.className = "center";
    const resetBtn = document.createElement("span");
    let zoomLevel = 1;
    resetBtn.innerHTML = `${zoomLevel}x`;
    resetBtn.className = "reset";
    let interpMode = localStorage.getItem('tm-interp-mode') || 'auto';
    const modes = [
      { value: 'auto', label: '雙立方', enabled: true, quality: 'high' },
      { value: 'bilinear', label: '雙線性', enabled: true, quality: 'low' },
      { value: 'pixelated', label: '像素化', enabled: false, quality: 'low' }
    ];
    if (!modes.find(m => m.value === interpMode)) interpMode = 'auto';
    const interpBtn = document.createElement("span");
    interpBtn.innerHTML = modes.find(m => m.value === interpMode)?.label || '插值';
    interpBtn.className = "interp";
    const downloadBtn = document.createElement("span");
    downloadBtn.innerHTML = "💾";
    downloadBtn.className = "download";
    const canvas = document.createElement("canvas");
    const tempImg = new Image();
    tempImg.src = getVisibleSrc(a);
    let scale = 1, posX = 0, posY = 0, isDragging = false, startX, startY, startPosX, startPosY, rafId;
    let natW = 0, natH = 0;
    const updateDisplay = () => {
      if (!natW || !natH) return;
      const w = window.innerWidth;
      const h = window.innerHeight;
      canvas.width = w;
      canvas.height = h;
      const ctx = canvas.getContext('2d');
      ctx.clearRect(0, 0, w, h);
      const current = modes.find(m => m.value === interpMode);
      ctx.imageSmoothingEnabled = current?.enabled ?? true;
      if (ctx.imageSmoothingQuality !== undefined) ctx.imageSmoothingQuality = current?.quality ?? 'high';
      ctx.save();
      ctx.translate(posX, posY);
      ctx.scale(scale, scale);
      ctx.drawImage(tempImg, 0, 0);
      ctx.lineWidth = 1 / scale;
      ctx.strokeStyle = '#fff';
      ctx.strokeRect(0, 0, natW, natH);
      ctx.restore();
      rafId = null;
    };
    const centerImage = () => {
      if (!natW || !natH) return;
      posX = (window.innerWidth - natW * scale) / 2;
      posY = (window.innerHeight - natH * scale) / 2;
      rafId = requestAnimationFrame(updateDisplay);
    };
    const initFit = () => {
      if (!natW || !natH) return;
      const maxW = window.innerWidth * 0.9, maxH = window.innerHeight * 0.9;
      scale = Math.min(maxW / natW, maxH / natH, 1);
      posX = (window.innerWidth - natW * scale) / 2;
      posY = (window.innerHeight - natH * scale) / 2;
      rafId = requestAnimationFrame(updateDisplay);
    };
    const toggleZoom = () => {
      if (!natW || !natH) return;
      const oldScale = scale;
      zoomLevel = zoomLevel === 1 ? 2 : 1;
      scale = zoomLevel;
      resetBtn.innerHTML = `${zoomLevel}x`;
      const centerX = window.innerWidth / 2;
      const centerY = window.innerHeight / 2;
      posX = centerX - (centerX - posX) * (scale / oldScale);
      posY = centerY - (centerY - posY) * (scale / oldScale);
      rafId = requestAnimationFrame(updateDisplay);
    };
    const downloadImage = () => {
      const urlObj = new URL(tempImg.src);
      let filename = urlObj.pathname.split('/').filter(Boolean).pop() || 'image';
      const queryIndex = filename.indexOf('?');
      if (queryIndex > -1) filename = filename.substring(0, queryIndex);
      if (!filename.includes('.')) {
        const extMatch = tempImg.src.match(/\.(jpg|jpeg|png|gif|webp|bmp|svg|avif)$/i);
        filename += '.' + (extMatch ? extMatch[1].toLowerCase() : 'png');
      }
      GM_download(tempImg.src, filename);
    };
    const restoreFit = () => initFit();
    const closeModal = () => {
      o.disconnect();
      m.remove();
    };
    const updateInterp = () => {
      const idx = modes.findIndex(m => m.value === interpMode);
      interpMode = modes[(idx + 1) % modes.length].value;
      interpBtn.innerHTML = modes.find(m => m.value === interpMode)?.label || '插值';
      localStorage.setItem('tm-interp-mode', interpMode);
      rafId = requestAnimationFrame(updateDisplay);
    };
    m.append(closeBtn, fitBtn, centerBtn, resetBtn, interpBtn, downloadBtn, canvas);
    document.body.appendChild(m);
    const o = new MutationObserver(() => {
      const n = getVisibleSrc(a);
      if (n && n !== tempImg.src) {
        tempImg.src = n;
        scale = 1;
        zoomLevel = 1;
        resetBtn.innerHTML = '1x';
        posX = 0;
        posY = 0;
        if (tempImg.complete) onLoad(); else tempImg.addEventListener('load', onLoad, {once: true});
      }
    });
    o.observe(a, {attributes: true, childList: true, subtree: true});
    closeBtn.addEventListener('click', closeModal);
    fitBtn.addEventListener('click', restoreFit);
    centerBtn.addEventListener('click', centerImage);
    resetBtn.addEventListener('click', toggleZoom);
    interpBtn.addEventListener('click', updateInterp);
    downloadBtn.addEventListener('click', downloadImage);
    m.addEventListener('click', e => {
      if (e.target === m) return closeModal();
      if (e.target !== canvas || !natW || !natH) return;
      const rect = canvas.getBoundingClientRect();
      const clickRelX = e.clientX - rect.left;
      const clickRelY = e.clientY - rect.top;
      const imgLeft = posX;
      const imgTop = posY;
      const imgWidth = natW * scale;
      const imgHeight = natH * scale;
      if (clickRelX < imgLeft || clickRelX > imgLeft + imgWidth || clickRelY < imgTop || clickRelY > imgTop + imgHeight) closeModal();
    });
    canvas.addEventListener('mousedown', e => {
      if (e.button !== 0) return;
      isDragging = true;
      startX = e.clientX;
      startY = e.clientY;
      startPosX = posX;
      startPosY = posY;
      document.body.style.userSelect = 'none';
      e.preventDefault();
      window.getSelection?.removeAllRanges();
    });
    document.addEventListener('mousemove', e => {
      if (!isDragging) return;
      posX = startPosX + (e.clientX - startX);
      posY = startPosY + (e.clientY - startY);
      rafId = requestAnimationFrame(updateDisplay);
    });
    document.addEventListener('mouseup', () => {
      isDragging = false;
      document.body.style.userSelect = '';
      if (rafId) { cancelAnimationFrame(rafId); rafId = null; }
    });
    canvas.addEventListener('wheel', e => {
      e.preventDefault();
      const rect = canvas.getBoundingClientRect();
      const mouseX = e.clientX - rect.left;
      const mouseY = e.clientY - rect.top;
      const imageX = (mouseX - posX) / scale;
      const imageY = (mouseY - posY) / scale;
      const factor = e.deltaY > 0 ? 0.9 : 1.1;
      const newScale = scale * factor;
      if (newScale < 0.01 || newScale > 100) return;
      scale = newScale;
      posX = mouseX - imageX * newScale;
      posY = mouseY - imageY * newScale;
      rafId = requestAnimationFrame(updateDisplay);
    }, { passive: false });
    const onLoad = () => {
      natW = tempImg.naturalWidth;
      natH = tempImg.naturalHeight;
      if (natW === 0 || natH === 0) return;
      initFit();
      rafId = requestAnimationFrame(updateDisplay);
    };
    if (tempImg.complete) onLoad(); else tempImg.addEventListener('load', onLoad, { once: true });
    window.addEventListener('resize', initFit);
  };
  document.addEventListener("click", e => {
    const a = e.target.closest("a.YsLeY");
    if (a && getVisibleSrc(a)) { e.preventDefault(); openModal(a) }
  });
})();