// ==UserScript==
// @name Hover Zoom Minus --
// @namespace Hover Zoom Minus --
// @version 1.0
// @description popup and hover zoom pan scroll any image on any website
// @author Ein, with help from 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 follow the mouse, click again to release
// 4. while locked the wheel up/down will act as scroll up/down
// 5. to turn on/off hover at the bottom of the page
// 6. easy to configure
(function() {
'use strict';
// Configuration ----------------------------------------------------------
// Define your list of sites you want HoverZoomMinus to run with,
// 1st array value - 1 will default it to turn on, 0 defaults it to turn off
// 2nd array value - "center" to always spawn it in center otherwise '' [blank] spawns near the mouse
// 3rd array value - allowed interval between respawing of popup. ie when exited on popup but imedietly touches an img container thus making it blinks or unsuable
const siteList = {
/*---- reddit */ 'new.reddit.com': [1, 'center','0'],
/*------ 9gag */ '9gag.com': [1, 'center', '100'],
/*---- feedly */ 'feedly.com': [1, 'center', '200'],
/*----- 4chan */ 'boards.4chan.org': [1, '', '400'],
/* deviantart */ 'www.deviantart.com': [0, 'center', '300'],
};
// image container [hover area that triggers the popup]
const imgContainers = `
/* --- reddit */ ._3Oa0THmZ3f5iZXAQ0hBJ0k > div, ._35oEP5zLnhKEbj5BlkTBUA, ._1ti9kvv_PMZEF2phzAjsGW > div, ._28TEYBuEdOuE3kN6UyoKMa div, ._3Oa0THmZ3f5iZXAQ0hBJ0k.WjuR4W-BBrvdtABBeKUMx div,
/* ----- 9gag */ .post-container .post-view > picture,
/* --- feedly */ .PinableImageContainer, .entryBody,
/* ---- 4chan */ div.post div.file a,
/* deviantart */ ._3_LJY, ._2e1g3, ._2SlAD
`;
// target img
const imgElements = `
/* --- reddit */ ._2_tDEnGMLxpM6uOa2kaDB3, ._1dwExqTGJH2jnA-MYGkEL-,
/* ----- 9gag */ .post-container .post-view > picture > img,
/* --- feedly */ .pinable, .entryBody img,
/* ---- 4chan */ div.post div.file img:nth-child(2), div.post div.file img:nth-child(1), ._3Oa0THmZ3f5iZXAQ0hBJ0k.WjuR4W-BBrvdtABBeKUMx img,
/* deviantart */ ._3_LJY img._2e1g3 img, ._2SlAD img
`;
// excluded element
const nopeElements = `
/* ------- reddit */ ._2ED-O3JtIcOqp8iIL1G5cg
`;
//-------------------------------------------------------------------------
// The HoverZoomMinus Function---------------------------------------------
function HoverZoomMinus() {
// Style for the popup image
const style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = `
.popup-image {
max-height: calc(90vh - 10px);
z-index: 1000;
display: none;
cursor: move;}`;
document.head.appendChild(style);
// Creating the popup image and backdrop
const popup = document.createElement('img');
popup.className = 'popup-image';
document.body.appendChild(popup);
const backdrop = document.createElement('div');
backdrop.className = 'popup-backdrop';
backdrop.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100 vw;
height: 100 vh;
display: none;`;
document.body.appendChild(backdrop);
//-------------------------------------------------------------------------
// Function to show the popup
function showPopup(src, mouseX, mouseY) {
popup.src = src;
popup.style.display = 'block';
popup.style.position = 'fixed';
popup.style.transform = 'translate(-50%, -50%)';
backdrop.style.display = 'block';
backdrop.style.zIndex = '999';
backdrop.style.backdropFilter = 'blur(10px)';
const currentDomain = window.location.hostname;
const position = siteList[currentDomain] ? siteList[currentDomain][1] : '';
if (position === 'center') {
popup.style.top = '50%';
popup.style.left = '50%';
} else {
popup.style.top = `${mouseY}px`;
popup.style.left = `${mouseX}px`;
}
}
//-------------------------------------------------------------------------
// Function combined Zoom and pan
let isPanning = false;
let offsetX, offsetY;
let scale = 1;
const ZOOM_SPEED = 0.005;
popup.addEventListener('click', function(event) {
isPanning = !isPanning;
if (isPanning) {
let rect = popup.getBoundingClientRect();
offsetX = event.clientX - rect.left - (rect.width / 2);
offsetY = event.clientY - rect.top - (rect.height / 2);
}
});
document.addEventListener('mousemove', function(event) {
if (isPanning) {
popup.style.left = (event.clientX - offsetX) + 'px';
popup.style.top = (event.clientY - offsetY) + 'px';
}
});
function zoomOrPan(event) {
event.preventDefault();
if (isPanning) {
let deltaY = event.deltaY * -ZOOM_SPEED;
let newTop = parseInt(popup.style.top) || 0;
newTop += deltaY * 100;
popup.style.top = newTop + 'px';
offsetY -= deltaY * 100;
} else {
scale += event.deltaY * -ZOOM_SPEED;
scale = Math.min(Math.max(0.125, scale), 4);
popup.style.transform = `translate(-50%, -50%) scale(${scale})`;
}
}
popup.addEventListener('wheel', zoomOrPan);
//-------------------------------------------------------------------------
// Function to show popup
let popupTimer;
document.addEventListener('mouseover', function(e) {
if (popupTimer) return;
let target = e.target.closest(imgContainers);
if (!target) return;
if (target.querySelector(nopeElements)) return;
let currentContainer = target;
const imageElement = currentContainer.querySelector(imgElements);
if (imageElement) {
const currentDomain = window.location.hostname;
const domainConfig = siteList[currentDomain];
const position = siteList[currentDomain] ? siteList[currentDomain][1] : '';
const delayTimer = domainConfig ? domainConfig[2] : '';
if (delayTimer === '') {
if (position === 'center') {
showPopup(imageElement.src);
} else {
showPopup(imageElement.src, e.clientX, e.clientY);
}
} else {
popupTimer = setTimeout(() => {
if (position === 'center') {
showPopup(imageElement.src);
} else {
showPopup(imageElement.src, e.clientX, e.clientY);
}
popupTimer = null;
}, parseInt(delayTimer));
}
}
});
//-------------------------------------------------------------------------
// Function to hide popup
function hidePopup() {
if (popupTimer) {
clearTimeout(popupTimer);
}
document.body.appendChild(backdrop);
popup.style.display = 'none';
popup.style.left = '50%';
popup.style.top = '50%';
popup.style.position = 'fixed';
popup.style.transform = 'translate(-50%, -50%) scale(1)';
backdrop.style.zIndex = '';
backdrop.style.display = 'none';
backdrop.style.backdropFilter = '';
isPanning = false;
}
document.addEventListener('mouseout', function(e) {
let target = e.target.closest('.imgContainers');
let relatedTarget = e.relatedTarget;
if (target && (!relatedTarget || (!relatedTarget.matches('.popup-image') && !relatedTarget.closest('.imgContainers')))) {
hidePopup();
const currentDomain = window.location.hostname;
const delayTimer = siteList[currentDomain] ? siteList[currentDomain][2] : '';
if (delayTimer !== '') {
popupTimer = setTimeout(() => {
popupTimer = null;
}, parseInt(delayTimer));
}
}
});
popup.addEventListener('mouseout', function() {
hidePopup();
const currentDomain = window.location.hostname;
const delayTimer = siteList[currentDomain] ? siteList[currentDomain][2] : '';
if (delayTimer !== '') {
popupTimer = setTimeout(() => {
popupTimer = null;
}, parseInt(delayTimer));
}
});
document.addEventListener('keydown', function(event) {
if (event.key === "Escape") {
event.preventDefault();
hidePopup();
}
});
}
//-------------------------------------------------------------------------
// Is HoverZoomMinus to be run
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() {
indicatorValue = 1 - indicatorValue;
indicatorBar.style.background = indicatorValue ? 'rgba(50, 190, 152, 0.5)' : 'rgba(174, 0, 1, 0.5)';
setTimeout(() => {
indicatorBar.style.background = '#0000';
}, 1000);
if (indicatorValue === 1) {
HoverZoomMinus();
} else {
const existingPopup = document.body.querySelector('.popup-image');
if (existingPopup) document.body.removeChild(existingPopup);
const existingBackdrop = document.body.querySelector('.popup-backdrop');
if (existingBackdrop) document.body.removeChild(existingBackdrop);
}
}
let indicatorValue;
const currentDomain = window.location.hostname;
if (siteList.hasOwnProperty(currentDomain)) {
indicatorValue = siteList[currentDomain][0];
let hoverTimeout;
indicatorBar.addEventListener('mouseenter', () => {
hoverTimeout = setTimeout(toggleIndicator, 500);
});
indicatorBar.addEventListener('mouseleave', () => {
clearTimeout(hoverTimeout);
indicatorBar.style.background = '#0000';
});
if (indicatorValue === 1) {
HoverZoomMinus();
}
} else { return; }
})();