IG Zoom Lens

在Instagram上悬停在图像上时添加一个圆形缩放镜头,支持平移和通过鼠标滚动调整缩放。

安装此脚本
作者推荐脚本

您可能也喜欢NetSkip

安装此脚本
// ==UserScript==
// @name               IG Zoom Lens
// @name:pt-BR         IG Zoom Lens
// @name:zh-CN         IG Zoom Lens
// @name:zh-TW         IG Zoom Lens
// @name:en            IG Zoom Lens
// @name:es            IG Zoom Lens
// @name:ja            IG Zoom Lens
// @name:ko            IG Zoom Lens
// @name:de            IG Zoom Lens
// @name:fr            IG Zoom Lens
// @namespace          http://github.com/0H4S
// @version            1.0
// @description        Adds a circular zoom lens when hovering over images on Instagram, with panning support and zoom adjustment via mouse scroll.
// @description:pt-BR  Adiciona uma lente de zoom circular ao passar o mouse sobre imagens no Instagram, com suporte a panning e ajuste de zoom via scroll do mouse.
// @description:zh-CN  在Instagram上悬停在图像上时添加一个圆形缩放镜头,支持平移和通过鼠标滚动调整缩放。
// @description:zh-TW  在Instagram上懸停在圖像上時添加一個圓形縮放鏡頭,支持平移和通過鼠標滾動調整縮放。
// @description:en     Adds a circular zoom lens when hovering over images on Instagram, with panning support and zoom adjustment via mouse scroll.
// @description:es     Añade una lente de zoom circular al pasar el ratón sobre las imágenes en Instagram, con soporte para paneo y ajuste de zoom mediante la rueda del ratón.
// @description:ja     Instagramの画像にマウスをホバーすると円形のズームレンズが追加され、パンニングサポートとマウススクロールによるズーム調整が可能になります。
// @description:ko     인스타그램 이미지 위에 마우스를 올리면 원형 줌 렌즈가 추가되며, 팬닝 지원 및 마우스 스크롤을 통한 줌 조정이 가능합니다.
// @description:de     Fügt eine kreisförmige Zoom-Linse hinzu, wenn Sie mit der Maus über Bilder auf Instagram fahren, mit Unterstützung für Panning und Zoom-Anpassung über das Mausrad.
// @description:fr     Ajoute une lentille de zoom circulaire lors du survol des images sur Instagram, avec prise en charge du panoramique et ajustement du zoom via la molette de la souris.
// @author             OHAS
// @license            CC-BY-NC-ND-4.0
// @copyright          2025 OHAS. All Rights Reserved.
// @icon               https://i.imgur.com/HH9zLZE.png
// @match              https://www.instagram.com/*
// @require            https://update.greasyfork.org/scripts/549920/Script%20Notifier.js
// @connect            gist.githubusercontent.com
// @grant              GM_getValue
// @grant              GM_setValue
// @grant              GM_registerMenuCommand
// @grant              GM_xmlhttpRequest
// @compatible         chrome
// @compatible         firefox
// @compatible         edge
// ==/UserScript==

(function() {
    'use strict';

    if (window.top !== window.self) {
        return;
    }

    const SCRIPT_CONFIG = {
        notificationsUrl: 'https://gist.githubusercontent.com/0H4S/aa1f90cc844db6c516379befd9ff354c/raw/ig_zoom_lens_notifications.json',
        scriptVersion: '1.0',
    };

    const notifier = new ScriptNotifier(SCRIPT_CONFIG);
    notifier.run();

    const DEFAULT_LENS_SIZE = 200;
    const MIN_LENS_SIZE = 100;
    const MAX_LENS_SIZE = 500;
    const LENS_SIZE_STEP = 20;
    const LENS_STORAGE_KEY = 'LensSize';

    const MIN_ZOOM = 3;
    const MAX_ZOOM = 30;
    const ZOOM_STEP = 0.5;
    const ZOOM_STORAGE_KEY = 'ZoomLevel';

    let currentLensSize = GM_getValue(LENS_STORAGE_KEY, DEFAULT_LENS_SIZE);
    let currentZoomLevel = GM_getValue(ZOOM_STORAGE_KEY, MIN_ZOOM);

    let currentImage = null;
    let isZoomActive = false;
    let isPanning = false;

    let panStartClientX = 0;
    let panStartClientY = 0;
    let initialBgPosX = 0;
    let initialBgPosY = 0;
    let lastMouseEvent = null;

    function addCursorStyles() {
        const style = document.createElement('style');
        style.textContent = ` .ig-zoom-active-image { cursor: none !important; } `;
        document.head.appendChild(style);
    }

    addCursorStyles();

    function createZoomLens() {
        const lens = document.createElement('div');
        lens.id = 'ig-zoom-lens';
        lens.style.position = 'fixed';
        lens.style.width = `${currentLensSize}px`;
        lens.style.height = `${currentLensSize}px`;
        lens.style.borderRadius = '50%';
        lens.style.border = '3px solid white';
        lens.style.boxShadow = '0 0 0 1px rgba(0,0,0,0.15), 0 4px 15px rgba(0,0,0,0.3)';
        lens.style.pointerEvents = 'none';
        lens.style.zIndex = '9999';
        lens.style.overflow = 'hidden';
        lens.style.transition = 'opacity 0.25s, transform 0.25s, border-color 0.15s linear';
        lens.style.backgroundRepeat = 'no-repeat';
        lens.style.backgroundSize = `${currentZoomLevel * 100}%`;
        lens.style.display = 'none';
        document.body.appendChild(lens);
        return lens;
    }

    const zoomLens = createZoomLens();

    function updateLensSize() {
        if (!zoomLens) return;
        zoomLens.style.width = `${currentLensSize}px`;
        zoomLens.style.height = `${currentLensSize}px`;
        if (isZoomActive && lastMouseEvent && currentImage) {
            updateZoomPosition(lastMouseEvent, currentImage);
        }
    }

    function provideVisualFeedback() {
        zoomLens.style.display = 'block';
        zoomLens.style.borderColor = '#3897f0';
        setTimeout(() => {
            zoomLens.style.borderColor = isPanning ? '#ff2323ff' : 'white';
            if (!isZoomActive) {
                zoomLens.style.display = 'none';
            }
        }, 200);
    }

    function increaseLensSize() {
        currentLensSize = Math.min(MAX_LENS_SIZE, currentLensSize + LENS_SIZE_STEP);
        GM_setValue(LENS_STORAGE_KEY, currentLensSize);
        updateLensSize();
        provideVisualFeedback();
    }

    function decreaseLensSize() {
        currentLensSize = Math.max(MIN_LENS_SIZE, currentLensSize - LENS_SIZE_STEP);
        GM_setValue(LENS_STORAGE_KEY, currentLensSize);
        updateLensSize();
        provideVisualFeedback();
    }

    function resetLensSize() {
        currentLensSize = DEFAULT_LENS_SIZE;
        GM_setValue(LENS_STORAGE_KEY, currentLensSize);
        updateLensSize();
        provideVisualFeedback();
    }

    document.addEventListener('keydown', function(e) {
        if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) {
            return;
        }

        if (e.key === '+' || e.key === '=') {
            e.preventDefault();
            increaseLensSize();
        } else if (e.key === '-') {
            e.preventDefault();
            decreaseLensSize();
        } else if (e.key === '0') {
            e.preventDefault();
            resetLensSize();
        }
    });

    function getHighResImageSrc(img) {
        if (img.srcset) {
            const sources = img.srcset.split(',').map(s => {
                const parts = s.trim().split(' ');
                return { url: parts[0], width: parseInt(parts[1]) || 0 };
            });
            sources.sort((a, b) => b.width - a.width);
            if (sources.length > 0 && sources[0].url) return sources[0].url;
        }
        return img.src;
    }

    function hideZoom() {
        if (!isZoomActive) return;
        isZoomActive = false;
        isPanning = false;
        if (currentImage) {
            currentImage.classList.remove('ig-zoom-active-image');
        }
        currentImage = null;
        zoomLens.style.borderColor = 'white';
        zoomLens.style.opacity = '0';
        zoomLens.style.transform = 'scale(0.8)';
        setTimeout(() => {
            if (!isZoomActive) {
                zoomLens.style.display = 'none';
            }
        }, 250);
    }

    function showZoom(img, e) {
        if (isZoomActive && currentImage === img) return;
        const src = getHighResImageSrc(img);
        if (!src || src.includes('profile_pic')) return;
        currentImage = img;
        isZoomActive = true;
        img.classList.add('ig-zoom-active-image');
        zoomLens.style.backgroundImage = `url(${src})`;
        zoomLens.style.backgroundSize = `${currentZoomLevel * 100}%`;
        zoomLens.style.display = 'block';
        setTimeout(() => {
            if (isZoomActive) {
                zoomLens.style.opacity = '1';
                zoomLens.style.transform = 'scale(1)';
            }
        }, 10);
        updateZoomPosition(e, img);
    }

    function updateZoomPosition(e, img) {
        if (!isZoomActive || currentImage !== img) return;
        lastMouseEvent = e;

        if (isPanning) {
            const deltaX = e.clientX - panStartClientX;
            const deltaY = e.clientY - panStartClientY;
            const deltaPercentX = (deltaX / currentLensSize) * 50;
            const deltaPercentY = (deltaY / currentLensSize) * 50;
            const newBgPosX = initialBgPosX + deltaPercentX;
            const newBgPosY = initialBgPosY + deltaPercentY;
            zoomLens.style.backgroundPosition = `${newBgPosX}% ${newBgPosY}%`;
            return;
        }

        const rect = img.getBoundingClientRect();
        const x = e.clientX - rect.left;
        const y = e.clientY - rect.top;

        if (x < 0 || y < 0 || x > rect.width || y > rect.height) {
            hideZoom();
            return;
        }

        const xPercent = (x / rect.width) * 100;
        const yPercent = (y / rect.height) * 100;

        zoomLens.style.left = `${e.clientX - currentLensSize / 2}px`;
        zoomLens.style.top = `${e.clientY - currentLensSize / 2}px`;
        zoomLens.style.backgroundPosition = `${xPercent}% ${yPercent}%`;
    }

    function applyZoomEffect(img) {
        if (img.dataset.zoomApplied) return;
        img.dataset.zoomApplied = 'true';

        img.addEventListener('mouseenter', function(e) { showZoom(img, e); });
        img.addEventListener('mousemove', function(e) { updateZoomPosition(e, img); });
        img.addEventListener('mouseleave', function() { if (!isPanning) { hideZoom(); } });

        img.addEventListener('mousedown', function(e) {
            if (e.button === 0 && isZoomActive) {
                e.preventDefault();
                isPanning = true;
                zoomLens.style.transition = 'none';
                zoomLens.style.borderColor = '#ff2323ff';
                panStartClientX = e.clientX;
                panStartClientY = e.clientY;
                const bgPos = zoomLens.style.backgroundPosition.split(' ');
                initialBgPosX = parseFloat(bgPos[0]) || 0;
                initialBgPosY = parseFloat(bgPos[1]) || 0;
            }
        });

        img.addEventListener('wheel', function(e) {
            if (!isZoomActive) return;
            e.preventDefault();
            const delta = e.deltaY < 0 ? ZOOM_STEP : -ZOOM_STEP;
            const newZoomLevel = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, currentZoomLevel + delta));
            if (newZoomLevel !== currentZoomLevel) {
                currentZoomLevel = newZoomLevel;
                GM_setValue(ZOOM_STORAGE_KEY, currentZoomLevel);
                zoomLens.style.backgroundSize = `${currentZoomLevel * 100}%`;
                if (!isPanning) {
                    updateZoomPosition(e, img);
                }
            }
        });
    }

    document.addEventListener('mouseup', function() {
        if (isPanning) {
            isPanning = false;
            zoomLens.style.transition = 'opacity 0.25s, transform 0.25s, border-color 0.15s linear';
            zoomLens.style.borderColor = 'white';
        }
    });

    function isOverNavigationButton(element) {
        if (!element) return false;
        let current = element;
        while (current && current !== document.body) {
            if (current.classList.contains('_afxw') || current.classList.contains('_afxv')) {
                return true;
            }
            current = current.parentElement;
        }
        return false;
    }

    document.addEventListener('mousemove', function(e) {
        lastMouseEvent = e;
        if (isPanning && currentImage) {
            updateZoomPosition(e, currentImage);
            return;
        }
        if (isOverNavigationButton(e.target) && isZoomActive) {
            hideZoom();
            return;
        }
        if (!e.target.closest('img') && isZoomActive) {
            hideZoom();
        }
    });

    function applyToAllImages() {
        const images = document.querySelectorAll('img.x5yr21d:not([src*="profile_pic"]):not([src*="s150x150"]):not([data-zoom-applied])');
        images.forEach(img => {
            if (img.offsetWidth > 150 || img.offsetHeight > 150) {
                applyZoomEffect(img);
            } else {
                img.onload = () => {
                    if (img.offsetWidth > 150 || img.offsetHeight > 150) {
                        applyZoomEffect(img);
                    }
                };
            }
        });
    }

    setTimeout(applyToAllImages, 1000);

    const observer = new MutationObserver(function(mutations) {
        let shouldReapply = false;
        mutations.forEach(mutation => {
            if (mutation.addedNodes.length) {
                mutation.addedNodes.forEach(node => {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        if (node.matches && node.matches('img.x5yr21d') || node.querySelector && node.querySelector('img.x5yr21d')) {
                            shouldReapply = true;
                        }
                    }
                });
            }
            if (mutation.type === 'attributes' && mutation.attributeName === 'src' && mutation.target.matches('img.x5yr21d')) {
                mutation.target.removeAttribute('data-zoom-applied');
                shouldReapply = true;
            }
        });
        if (shouldReapply) {
            setTimeout(applyToAllImages, 100);
        }
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true,
        attributes: true,
        attributeFilter: ['src'],
        characterData: false
    });

})();