// ==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;
}
})();