Hover Zoom Minus --

image popup: zoom, pan, scroll, pin, scale

当前为 2024-03-17 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name             Hover Zoom Minus --
// @namespace        Hover Zoom Minus --
// @version          1.0.5
// @description      image popup: zoom, pan, scroll, pin, scale
// @author           Ein, Copilot AI
// @match            *://*/*
// @license          MIT
// @grant            none
// ==/UserScript==

// 1. to use hover over the image(container) to view a popup of the target image
// 2. to zoom in/out use wheel up/down. to reset press the scroll button
// 3. click left mouse to lock popup this will make it move along with the mouse, click again to release (indicated by green border)
// 4. while being locked"Y" the wheel up/down will act as scroll up/down
// 5. double click will lock it on screen preventing it from being hidden
// 6. hover below the image and click blue bar this will make a 3rd mode for wheel bottom, which will scroll next/previous image under an album
// 7. while locked at screen (indicated by red outline) a single click with the blurred background will unblur it, only one popup per time, so the locked popup will prevent other popup to spawn
// 8. double clicking on blurred background will de-spawn popup
// 9. click  on top right corner to scale image
// 10. to turn on/off hover at the bottom of the page

(function() {
    'use strict';

    // Configuration ----------------------------------------------------------

    // Define regexp of web page you want HoverZoomMinus to run with,
    // 1st array value - default status at start of page: '1' for on, '0' for off
    // 2nd array value - spawn position for popup: 'center' for center of screen, '' for cursor position
    // 3rd array value - allowed interval for spawning popup; i.e. when exited on popup but immediately touches an img container thus making it "blink spawn blink spawn", experiment it with the right number

    const siteConfig = {
        'reddit.com*': [1, 'center', '0'],
        '9gag.com*': [1, 'center', '0'],
        'feedly.com*': [1, 'center', '200'],
        '4chan.org*': [1, '', '400'],
        'deviantart.com*': [0, 'center', '300']
    };

    // image container [hover box where popup triggers]
    const imgContainers = `
    /* ------- reddit */ ._3Oa0THmZ3f5iZXAQ0hBJ0k > div, ._35oEP5zLnhKEbj5BlkTBUA, ._1ti9kvv_PMZEF2phzAjsGW > div, ._28TEYBuEdOuE3kN6UyoKMa div, ._3Oa0THmZ3f5iZXAQ0hBJ0k.WjuR4W-BBrvdtABBeKUMx div, ._3m20hIKOhTTeMgPnfMbVNN,
    /* --------- 9gag */ .post-container .post-view > picture,
    /* ------- feedly */ .PinableImageContainer, .entryBody,
    /* -------- 4chan */ div.post div.file a,
    /* --- deviantart */ ._3_LJY, ._2e1g3, ._2SlAD, ._1R2x6
    `;
    // target img
    const imgElements = `
    /* ------- reddit */ ._2_tDEnGMLxpM6uOa2kaDB3, ._1dwExqTGJH2jnA-MYGkEL-, ._2_tDEnGMLxpM6uOa2kaDB3._1XWObl-3b9tPy64oaG6fax,
    /* --------- 9gag */ .post-container .post-view > picture > img,
    /* ------- feedly */ .pinable, .entryBody img,
    /* -------- 4chan */ div.post div.file img:nth-child(1), ._3Oa0THmZ3f5iZXAQ0hBJ0k.WjuR4W-BBrvdtABBeKUMx img, div.post div.file .fileThumb img,
    /* --- deviantart */ ._3_LJY img, ._2e1g3 img, ._2SlAD img, ._1R2x6 img
    `;
    // excluded element
    const nopeElements = `
    /* ------- reddit */ ._2ED-O3JtIcOqp8iIL1G5cg
    `;
    // AlbumSelector take note that it will only load image those that are already on the DOM tree
    // example reddit will not include all until you press tha navigator. so most of the time this will only load a few ones
    // unless you update it via interacting with reddit's navigator button or maybe you could make a special function for "specialElements" so it will load and include all image in the DOM tree
    let albumSelector = [
        /* ------- reddit */ { imgElement: '._1dwExqTGJH2jnA-MYGkEL-', albumElements: '._1apobczT0TzIKMWpza0OhL' },
        /* ------- sample */ { imgElement: 'imgElementSelector2', albumElements: 'albumSelector2' },
    ];

    // specialElements were if targeted, will call it's paired function
    const specialElements = [
        /* -------- 4chan */ { selector: 'div.post div.file .fileThumb img', func: SP1 }
    ];

    function SP1(imageElement) {
        let src = imageElement.getAttribute('src');
        if (src && src.includes('s.jpg')) {
            let newSrc = src.replace('s.jpg', '.jpg');
            imageElement.setAttribute('src', newSrc);
        }
    }

    //-------------------------------------------------------------------------

    // Variables
    const currentHref = window.location.href;
    let enableP, positionP, intervalP, URLmatched;
    Object.keys(siteConfig).some((config) => {
        const regex = new RegExp(config);
        if (currentHref.match(regex)) {
            [enableP, positionP, intervalP] = siteConfig[config];
            URLmatched = true;
            return true;
        }
    });


    // The HoverZoomMinus Function---------------------------------------------
    function HoverZoomMinus() {
        let isshowPopupEnabled = true;
        isshowPopupEnabled = true;

        const style = document.createElement('style');
        style.type = 'text/css';
        style.innerHTML = `
		.popup-container {
			display: none;
			z-index: 1001;
			cursor: move;
		}
		.popup-image {
			max-height: calc(90vh - 10px);
			display: none;
		}
		.popup-backdrop {
			position: fixed;
			top: 0;
			left: 0;
			width: 100vw;
			height: 100vh;
			display: none;
			z-index: 1000;
		}
		.LockedX {
			height: 40px;
			width: calc(100% - 80px);
			background: #0000;
			position: absolute;
            left: 40px;
			bottom: 0;
			z-index: 9999;
		}
        .scaleBox,
        .scaleCorner {
            height: 40px;
            width: 40px;
            background: #0000;
			position: absolute;
        }
        .scaleBox {
            z-index: 9999;
        }
        .scaleCorner {
			z-index: 9998;
        }
        .scaleBox.TR,
        .scaleCorner.TR2 {
			top: 0;
            right: 0;
         }
        .scaleBox.TL,
        .scaleCorner.TL2 {
			top: 0;
            left: 0;
         }
        .scaleBox.BR,
        .scaleCorner.BR2 {
			bottom: 0;
            right: 0;
         }
        .scaleBox.BL,
        .scaleCorner.BL2 {
			bottom: 0;
            left: 0;
         }`;
        document.head.appendChild(style);
        const backdrop = document.createElement('div');
        backdrop.className = 'popup-backdrop';
        document.body.appendChild(backdrop);
        const popupContainer = document.createElement('div');
        popupContainer.className = 'popup-container';
        document.body.appendChild(popupContainer);
        const popup = document.createElement('img');
        popup.className = 'popup-image';
        popupContainer.appendChild(popup);

        const LockedX = document.createElement('div');
        LockedX.className = 'LockedX';
        popupContainer.appendChild(LockedX);

        const TR = document.createElement('div');
        TR.className = 'scaleBox TR';
        popupContainer.appendChild(TR);
        const TL = document.createElement('div');
        TL.className = 'scaleBox TL';
        popupContainer.appendChild(TL);
        const BR = document.createElement('div');
        BR.className = 'scaleBox BR';
        popupContainer.appendChild(BR);
        const BL = document.createElement('div');
        BL.className = 'scaleBox BL';
        popupContainer.appendChild(BL);

        const TR2 = document.createElement('div');
        TR2.className = 'scaleCorner TR2';
        popupContainer.appendChild(TR2);
        const TL2 = document.createElement('div');
        TL2.className = 'scaleCorner TL2';
        popupContainer.appendChild(TL2);
        const BR2 = document.createElement('div');
        BR2.className = 'scaleCorner BR2';
        popupContainer.appendChild(BR2);
        const BL2 = document.createElement('div');
        BL2.className = 'scaleCorner BL2';
        popupContainer.appendChild(BL2);

        //-------------------------------------------------------------------------

        // Zoom, Pan, Scroll

        let isLockedY = false;
        let isLockedX = false;
        let offsetX, offsetY, offsetXR, offsetYT, offsetXL, offsetYB, rectT, rectH, rectL, rectW, scaleCurrentX, scaleCurrentY;
        let scale = 1;
        const ZOOM_SPEED = 0.005;
        let clickTimeout;
        let popupTimer;

        let isScale, isScaleTR, isScaleBR, isScaleTL, isScaleBL;
        popupContainer.addEventListener('click', function(event) {
            if (clickTimeout) clearTimeout(clickTimeout);
            clickTimeout = setTimeout(function() {
                if (event.target.matches('.LockedX') || event.target.closest('.LockedX') || event.target.matches('.scaleBox') || event.target.closest('.scaleBox')) {
                    event.stopPropagation();
                } else {
                    isLockedY = !isLockedY;
                    isScale = false;
                    TR2.style.border = '';
                    BR2.style.border = '';
                    TL2.style.border = '';
                    BL2.style.border = '';
                    if (isLockedY) {
                        popupContainer.style.outline = ishidePopupEnabled ? '' : '6px solid #ae0001';
                        isLockedX = false;
                        popupContainer.style.borderTop = '';
                        popupContainer.style.borderBottom = '';
                        popupContainer.style.borderLeft = '6px solid #00ff00';
                        popupContainer.style.borderRight = '6px solid #00ff00';
                        let rect = popupContainer.getBoundingClientRect();
                        offsetX = event.clientX - rect.left - (rect.width / 2);
                        offsetY = event.clientY - rect.top - (rect.height / 2);
                    } else {
                        popupContainer.style.border = '';
                    }
                }}, 300);
        });

        document.querySelectorAll('.scaleBox').forEach(element => {
            element.addEventListener('click', function(event) {
                ishidePopupEnabled = false;
                isScale = !isScale;
                if (isScale) {
                    isLockedY = false;
                    const clickedElement = event.target;
                    const popupContainer = clickedElement.parentElement;
                    const currentTransform = window.getComputedStyle(popupContainer).transform;
                    const matrixMatch = currentTransform.match(/^matrix\(([^,]+), [^,]+, [^,]+, ([^,]+), [^,]+, [^,]+\)$/);
                    if (matrixMatch) {
                        scaleCurrentX = parseFloat(matrixMatch[1]);
                        scaleCurrentY = parseFloat(matrixMatch[2]);
                    }
                    let rect = element.getBoundingClientRect();
                    offsetYT = event.clientY - rect.top;
                    offsetXL = event.clientX - rect.left;
                    offsetYB = rect.height + rect.top - event.clientY;
                    offsetXR = rect.width + rect.left - event.clientX;
                    rectT = rect.top;
                    rectH = rect.height;
                    rectW = rect.width;
                    rectL = rect.left;
                } else {
                    TR2.style.border = '';
                    BR2.style.border = '';
                    TL2.style.border = '';
                    BL2.style.border = '';
                    popupContainer.style.outline = '6px solid #ae0001';
                }
            });
        });

        TR.addEventListener('click', function(event) {
            isScaleTR = !isScaleTR;
        });
        TL.addEventListener('click', function(event) {
            isScaleTL = !isScaleTL;
        });
        BL.addEventListener('click', function(event) {
            isScaleBL = !isScaleBL;
        });
        BR.addEventListener('click', function(event) {
            isScaleBR = !isScaleBR;
        });

        let scaleFactorY, scaleFactorX;
        document.querySelectorAll('.scaleBox').forEach(element => {
            element.addEventListener('mouseenter', function(event) {
                element.style.height = '100%';
                element.style.width = '100%';
                TL2.style.borderTop = '6px solid #0000ff';
                TR2.style.borderTop = '6px solid #0000ff';
                TL2.style.borderLeft = '6px solid #0000ff';
                BL2.style.borderLeft = '6px solid #0000ff';
                TR2.style.borderRight = '6px solid #0000ff';
                BR2.style.borderRight = '6px solid #0000ff';
                BR2.style.borderBottom = '6px solid #0000ff';
                BL2.style.borderBottom = '6px solid #0000ff';
            });
        });
        document.querySelectorAll('.scaleBox').forEach(element => {
            element.addEventListener('mouseleave', function(event) {
                element.style.height = '40px';
                element.style.width = '40px';
                if (!isScale) {
                    TL2.style.border = '';
                    TR2.style.border = '';
                    BL2.style.border = '';
                    BR2.style.border = '';
                }
            });
        });


        document.addEventListener('mousemove', function(event) {
            if (isLockedY) {
                popupContainer.style.left = (event.clientX - offsetX) + 'px';
                popupContainer.style.top = (event.clientY - offsetY) + 'px';
            } else if (isScale) {
                TL2.style.borderTop = '6px solid #0000ff';
                TR2.style.borderTop = '6px solid #0000ff';
                TL2.style.borderLeft = '6px solid #0000ff';
                BL2.style.borderLeft = '6px solid #0000ff';
                TR2.style.borderRight = '6px solid #0000ff';
                BR2.style.borderRight = '6px solid #0000ff';
                BR2.style.borderBottom = '6px solid #0000ff';
                BL2.style.borderBottom = '6px solid #0000ff';
                if (isScaleTR) {
                    scaleFactorY = scaleCurrentY * (1 + (2 * (rectT - event.clientY + offsetYT) / (rectH)));
                    scaleFactorX = scaleCurrentX * (-1 + (2 * (-rectL + event.clientX + offsetXR) / (rectW)));
                } else if (isScaleBR) {
                    scaleFactorY = scaleCurrentY * (-1 + (2 * (-rectT + event.clientY + offsetYB) / (rectH)));
                    scaleFactorX = scaleCurrentX * (-1 + (2 * (-rectL + event.clientX + offsetXR) / (rectW)));
                } else if (isScaleTL) {
                    scaleFactorY = scaleCurrentY * (1 + (2 * (rectT - event.clientY + offsetYT) / (rectH)));
                    scaleFactorX = scaleCurrentX * (1 + (2 * (rectL - event.clientX + offsetXL) / (rectW)));
                } else {
                    scaleFactorY = scaleCurrentY * (-1 + (2 * (-rectT + event.clientY + offsetYB) / (rectH)));
                    scaleFactorX = scaleCurrentX * (1 + (2 * (rectL - event.clientX + offsetXL) / (rectW)));
                }
                popupContainer.style.transform = `translate(-50%, -50%) scale(${scaleFactorX}, ${scaleFactorY})`
            }
        });

        let imgElementsList = [];
        function ZoomOrScroll(event) {
            event.preventDefault();
            if (isLockedY) {
                let deltaY = event.deltaY * -ZOOM_SPEED;
                let newTop = parseInt(popupContainer.style.top) || 0;
                newTop += deltaY * 100;
                popupContainer.style.top = newTop + 'px';
                offsetY -= deltaY * 100;
            } else if (isLockedX && currentZeroImgElement) {
                let pair = albumSelector.find(pair => currentZeroImgElement.matches(pair.imgElement));
                if (pair) {
                    let ancestorElement = currentZeroImgElement.closest(pair.albumElements);
                    if (ancestorElement) {
                        imgElementsList = Array.from(ancestorElement.querySelectorAll(pair.imgElement));
                        let zeroIndex = imgElementsList.indexOf(currentZeroImgElement);
                        let direction = event.deltaY > 0 ? 1 : -1;
                        let newIndex = (zeroIndex + direction + imgElementsList.length) % imgElementsList.length;
                        currentZeroImgElement = imgElementsList[newIndex];
                        popup.src = currentZeroImgElement.src;
                    }
                }
            } else {
                scale += event.deltaY * -ZOOM_SPEED;
                scale = Math.min(Math.max(0.125, scale), 10);
                popupContainer.style.transform = `translate(-50%, -50%) scaleX(${scale}) scaleY(${scale})`;
            }
        }

        popupContainer.addEventListener('wheel', ZoomOrScroll);

        //-------------------------------------------------------------------------

        // show popup
        function showPopup(src, mouseX, mouseY) {
            if (!isshowPopupEnabled) return;
            popup.src = src;
            popup.style.display = 'block';
            popupContainer.style.display = 'block';
            popupContainer.style.position = 'fixed';
            popupContainer.style.transform = 'translate(-50%, -50%) scaleX(1) scaleY(1)';
            backdrop.style.display = 'block';
            backdrop.style.zIndex = '999';
            backdrop.style.backdropFilter = 'blur(10px)';

            if (positionP === 'center') {
                popupContainer.style.top = '50%';
                popupContainer.style.left = '50%';
            } else {
                popupContainer.style.top = `${mouseY}px`;
                popupContainer.style.left = `${mouseX}px`;
            }
        }

        let currentZeroImgElement;
        document.addEventListener('mouseover', function(e) {
            if (popupTimer) return;
            let target = e.target.closest(imgContainers);
            if (target.querySelector(nopeElements)) return;
            const imageElement = target.querySelector(imgElements);
            specialElements.forEach(pair => {
                if (imageElement.matches(pair.selector)) {
                    pair.func(imageElement);
                }
            });

            if (imageElement) {
                currentZeroImgElement = imageElement;
                if (intervalP === '') {
                    showPopup(imageElement.src, e.clientX, e.clientY);
                } else {
                    popupTimer = setTimeout(() => {
                        showPopup(imageElement.src, e.clientX, e.clientY);
                        popupTimer = null;
                    }, parseInt(intervalP));
                }
            }
        });
        //-------------------------------------------------------------------------

        // hide popup
        function hidePopup() {
            if (!ishidePopupEnabled) return;
            imgElementsList = [];
            isshowPopupEnabled = true;
            if (popupTimer) {
                clearTimeout(popupTimer);
            }
            document.body.appendChild(backdrop);
            popup.style.display = 'none';
            popupContainer.style.display = 'none';
            popupContainer.style.left = '50%';
            popupContainer.style.top = '50%';
            popupContainer.style.position = 'fixed';
            popupContainer.style.transform = 'translate(-50%, -50%) scaleX(1) scaleY(1)';
            popupContainer.style.border = '';
            popupContainer.style.outline = '';
            backdrop.style.zIndex = '';
            backdrop.style.display = 'none';
            backdrop.style.backdropFilter = '';
            isLockedY = false;
            isLockedX = false;
        }

        popupContainer.addEventListener('mouseout', function(event) {
            let relatedTarget = event.relatedTarget;
            if (relatedTarget && (popupContainer.contains(relatedTarget) || relatedTarget.matches('.imgContainers') || relatedTarget.closest('.imgContainers'))) {
                return;
            }

            hidePopup();
            if (intervalP !== '') {
                popupTimer = setTimeout(() => {
                    popupTimer = null;
                }, parseInt(intervalP));
            }
        });

        document.addEventListener('keydown', function(event) {
            if (event.key === "Escape") {
                event.preventDefault();
                hidePopup();
            }
        });

        //-------------------------------------------------------------------------

        // lock popup in screen
        let ishidePopupEnabled = true;

        function togglehidePopup(event) {
            ishidePopupEnabled = !ishidePopupEnabled;
            popupContainer.style.outline = ishidePopupEnabled ? '' : '6px solid #ae0001';
        }

        popupContainer.addEventListener('dblclick', function(event) {
            clearTimeout(clickTimeout);
            togglehidePopup();
        });
        backdrop.addEventListener('dblclick', function(event) {
            clearTimeout(clickTimeout);
            ishidePopupEnabled = true;
            hidePopup();
        });


        backdrop.addEventListener('click', function(event) {
            if (clickTimeout) clearTimeout(clickTimeout);
            clickTimeout = setTimeout(function() {
                if (isScale) {
                    isScale = false;
                } else {
                    backdrop.style.zIndex = '';
                    backdrop.style.display = 'none';
                    backdrop.style.backdropFilter = '';
                    isshowPopupEnabled = false;
                }
            }, 300);
        });


        // Album scrolling
        LockedX.addEventListener('mouseenter', function() {
            LockedX.style.background = 'linear-gradient(to right, rgba(0, 0, 255, 0) 0%, rgba(0, 0, 255, 0.5) 25%, rgba(0, 0, 255, 0.5) 75%, rgba(0, 0, 255, 0) 100%)';
        });

        LockedX.addEventListener('mouseleave', function() {
            LockedX.style.background = '#0000';
        });

        LockedX.addEventListener('click', function(event) {
            ishidePopupEnabled = false;
            isLockedX = !isLockedX;
            if (isLockedX) {
                isLockedY = false;
                popupContainer.style.borderTop = '6px solid #00ff00';
                popupContainer.style.borderBottom = '6px solid #00ff00';
                popupContainer.style.borderLeft = '';
                popupContainer.style.borderRight = '';
                popupContainer.style.outline = '';
            } else {
                popupContainer.style.border = '';
                popupContainer.style.outline = '6px solid #ae0001';
            }

        });
    }

    //-------------------------------------------------------------------------

    // Is to be run -----------------------------------------------------------
    if (URLmatched) {
        const indicatorBar = document.createElement('div');
        indicatorBar.style.cssText = `
        position: fixed;
        bottom: 0;
        left: 50%;
        transform: translateX(-50%);
        z-index: 9999;
        height: 30px;
        width: 50vw;
        background: #0000;`;
        document.body.appendChild(indicatorBar);

        function toggleIndicator() {
            enableP = 1 - enableP;
            indicatorBar.style.background = enableP ? 'linear-gradient(to right, rgba(50, 190, 152, 0) 0%, rgba(50, 190, 152, 0.5) 25%, rgba(50, 190, 152, 0.5) 75%, rgba(50, 190, 152, 0) 100%)' : 'linear-gradient(to right, rgba(174, 0, 1, 0) 0%, rgba(174, 0, 1, 0.5) 25%, rgba(174, 0, 1, 0.5) 75%, rgba(174, 0, 1, 0) 100%)';
            setTimeout(() => {
                indicatorBar.style.background = '#0000';
            }, 1000);
            if (enableP === 1) {
                HoverZoomMinus();
            } else {
                const existingPopup = document.body.querySelector('.popup-container');
                if (existingPopup) document.body.removeChild(existingPopup);
                const existingBackdrop = document.body.querySelector('.popup-backdrop');
                if (existingBackdrop) document.body.removeChild(existingBackdrop);
            }
        }
        let hoverTimeout;
        indicatorBar.addEventListener('mouseenter', () => {
            hoverTimeout = setTimeout(toggleIndicator, 500);
        });
        indicatorBar.addEventListener('mouseleave', () => {
            clearTimeout(hoverTimeout);
            indicatorBar.style.background = '#0000';
        });
        if (enableP === 1) {
            HoverZoomMinus();
        }
    } else {
        return;
    }

})();