Hover Zoom Minus --

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

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

  1. // ==UserScript==
  2. // @name Hover Zoom Minus --
  3. // @namespace Hover Zoom Minus --
  4. // @version 1.0.4
  5. // @description image popup: zoom, pan, scroll, pin, scale
  6. // @author Ein, Copilot AI
  7. // @match *://*/*
  8. // @license MIT
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. // 1. to use hover over the image(container) to view a popup of the target image
  13. // 2. to zoom in/out use wheel up/down. to reset press the scroll button
  14. // 3. click left mouse to lock popup this will make it move along with the mouse, click again to release (indicated by green border)
  15. // 4. while being locked"Y" the wheel up/down will act as scroll up/down
  16. // 5. double click will lock it on screen preventing it from being hidden
  17. // 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
  18. // 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
  19. // 8. double clicking on blurred background will de-spawn popup
  20. // 9. click on top right corner to scale image
  21. // 10. to turn on/off hover at the bottom of the page
  22.  
  23. (function() {
  24. 'use strict';
  25.  
  26. // Configuration ----------------------------------------------------------
  27.  
  28. // Define regexp of web page you want HoverZoomMinus to run with,
  29. // 1st array value - default status at start of page: '1' for on, '0' for off
  30. // 2nd array value - spawn position for popup: 'center' for center of screen, '' for cursor position
  31. // 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
  32.  
  33. const siteConfig = {
  34. 'reddit.com*': [1, 'center', '0'],
  35. '9gag.com*': [1, 'center', '0'],
  36. 'feedly.com*': [1, 'center', '200'],
  37. '4chan.org*': [1, '', '400'],
  38. 'deviantart.com*': [0, 'center', '300']
  39. };
  40.  
  41. // image container [hover box where popup triggers]
  42. const imgContainers = `
  43. /* ------- reddit */ ._3Oa0THmZ3f5iZXAQ0hBJ0k > div, ._35oEP5zLnhKEbj5BlkTBUA, ._1ti9kvv_PMZEF2phzAjsGW > div, ._28TEYBuEdOuE3kN6UyoKMa div, ._3Oa0THmZ3f5iZXAQ0hBJ0k.WjuR4W-BBrvdtABBeKUMx div, ._3m20hIKOhTTeMgPnfMbVNN,
  44. /* --------- 9gag */ .post-container .post-view > picture,
  45. /* ------- feedly */ .PinableImageContainer, .entryBody,
  46. /* -------- 4chan */ div.post div.file a,
  47. /* --- deviantart */ ._3_LJY, ._2e1g3, ._2SlAD, ._1R2x6
  48. `;
  49. // target img
  50. const imgElements = `
  51. /* ------- reddit */ ._2_tDEnGMLxpM6uOa2kaDB3, ._1dwExqTGJH2jnA-MYGkEL-, ._2_tDEnGMLxpM6uOa2kaDB3._1XWObl-3b9tPy64oaG6fax,
  52. /* --------- 9gag */ .post-container .post-view > picture > img,
  53. /* ------- feedly */ .pinable, .entryBody img,
  54. /* -------- 4chan */ div.post div.file img:nth-child(1), ._3Oa0THmZ3f5iZXAQ0hBJ0k.WjuR4W-BBrvdtABBeKUMx img, div.post div.file .fileThumb img,
  55. /* --- deviantart */ ._3_LJY img, ._2e1g3 img, ._2SlAD img, ._1R2x6 img
  56. `;
  57. // excluded element
  58. const nopeElements = `
  59. /* ------- reddit */ ._2ED-O3JtIcOqp8iIL1G5cg
  60. `;
  61. // AlbumSelector take note that it will only load image those that are already on the DOM tree
  62. // example reddit will not include all until you press tha navigator. so most of the time this will only load a few ones
  63. // 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
  64. let albumSelector = [
  65. /* ------- reddit */ { imgElement: '._1dwExqTGJH2jnA-MYGkEL-', albumElements: '._1apobczT0TzIKMWpza0OhL' },
  66. /* ------- sample */ { imgElement: 'imgElementSelector2', albumElements: 'albumSelector2' },
  67. ];
  68.  
  69. // specialElements were if targeted, will call it's paired function
  70. const specialElements = [
  71. /* -------- 4chan */ { selector: 'div.post div.file .fileThumb img', func: SP1 }
  72. ];
  73.  
  74. function SP1(imageElement) {
  75. let src = imageElement.getAttribute('src');
  76. if (src && src.includes('s.jpg')) {
  77. let newSrc = src.replace('s.jpg', '.jpg');
  78. imageElement.setAttribute('src', newSrc);
  79. }
  80. }
  81.  
  82. //-------------------------------------------------------------------------
  83.  
  84. // Variables
  85. const currentHref = window.location.href;
  86. let enableP, positionP, intervalP, URLmatched;
  87. Object.keys(siteConfig).some((config) => {
  88. const regex = new RegExp(config);
  89. if (currentHref.match(regex)) {
  90. [enableP, positionP, intervalP] = siteConfig[config];
  91. URLmatched = true;
  92. return true;
  93. }
  94. });
  95.  
  96.  
  97. // The HoverZoomMinus Function---------------------------------------------
  98. function HoverZoomMinus() {
  99. let isshowPopupEnabled = true;
  100. isshowPopupEnabled = true;
  101.  
  102. const style = document.createElement('style');
  103. style.type = 'text/css';
  104. style.innerHTML = `
  105. .popup-container {
  106. display: none;
  107. z-index: 1001;
  108. cursor: move;
  109. }
  110. .popup-image {
  111. max-height: calc(90vh - 10px);
  112. display: none;
  113. }
  114. .popup-backdrop {
  115. position: fixed;
  116. top: 0;
  117. left: 0;
  118. width: 100vw;
  119. height: 100vh;
  120. display: none;
  121. z-index: 1000;
  122. }
  123. .LockedX {
  124. height: 40px;
  125. width: calc(100%);
  126. background: #0000;
  127. position: absolute;
  128. bottom: 0;
  129. z-index: 9999;
  130. }
  131. .scaleTR {
  132. height: 40px;
  133. width: 40px;
  134. background: #0000;
  135. position: absolute;
  136. top: 0;
  137. right: 0;
  138. z-index: 9999;
  139. }
  140. `;
  141. document.head.appendChild(style);
  142. const backdrop = document.createElement('div');
  143. backdrop.className = 'popup-backdrop';
  144. document.body.appendChild(backdrop);
  145. const popupContainer = document.createElement('div');
  146. popupContainer.className = 'popup-container';
  147. document.body.appendChild(popupContainer);
  148. const popup = document.createElement('img');
  149. popup.className = 'popup-image';
  150. popupContainer.appendChild(popup);
  151. const LockedX = document.createElement('div');
  152. LockedX.className = 'LockedX';
  153. popupContainer.appendChild(LockedX);
  154. const scaleTR = document.createElement('div');
  155. scaleTR.className = 'scaleTR';
  156. popupContainer.appendChild(scaleTR);
  157.  
  158. //-------------------------------------------------------------------------
  159.  
  160. // Zoom, Pan, Scroll
  161.  
  162. let isLockedY = false;
  163. let isLockedX = false;
  164. let offsetX, offsetY, rectT, rectB, rectH, rectR, rectL, rectW;
  165. let scale = 1;
  166. const ZOOM_SPEED = 0.005;
  167. let clickTimeout;
  168. let popupTimer;
  169.  
  170.  
  171. popupContainer.addEventListener('click', function(event) {
  172. if (clickTimeout) clearTimeout(clickTimeout);
  173. clickTimeout = setTimeout(function() {
  174. if (event.target.matches('.LockedX') || event.target.closest('.LockedX') || event.target.matches('.scaleTR') || event.target.closest('.scaleTR')) {
  175. event.stopPropagation();
  176. } else {
  177. isLockedY = !isLockedY;
  178. isscaleTR = false;
  179. if (isLockedY) {
  180. popupContainer.style.outline = ishidePopupEnabled ? '' : '3px solid #ae0001';
  181. isLockedX = false;
  182. popupContainer.style.borderTop = '';
  183. popupContainer.style.borderBottom = '';
  184. popupContainer.style.borderLeft = '3px solid #00ff00';
  185. popupContainer.style.borderRight = '3px solid #00ff00';
  186. let rect = popupContainer.getBoundingClientRect();
  187. offsetX = event.clientX - rect.left - (rect.width / 2);
  188. offsetY = event.clientY - rect.top - (rect.height / 2);
  189. } else {
  190. popupContainer.style.border = '';
  191. }
  192. }}, 300);
  193. });
  194.  
  195. // Scale
  196. let isscaleTR = false;
  197.  
  198. scaleTR.addEventListener('click', function(event) {
  199. ishidePopupEnabled = false;
  200. isscaleTR = !isscaleTR;
  201. if (isscaleTR) {
  202. isLockedY = false;
  203. let rect = scaleTR.getBoundingClientRect();
  204. offsetX = event.clientX - rect.left - (rect.width / 2);
  205. offsetY = event.clientY - rect.top - (rect.height / 2);
  206. rectT = rect.top;
  207. rectB = rect.bottom;
  208. rectH = rect.height;
  209. rectW = rect.width;
  210. rectL = rect.left;
  211. rectR = rect.right;
  212. } else {
  213. scaleTR.style.border = '';
  214. }
  215.  
  216. });
  217.  
  218. let scaletrY, scaletrX;
  219. scaleTR.addEventListener('mouseenter', function() {
  220. scaleTR.style.height = '100%';
  221. scaleTR.style.width = '100%';
  222. scaleTR.style.borderTop = '3px solid #ff00ff';
  223. scaleTR.style.borderRight = '3px solid #ff00ff';
  224. });
  225. scaleTR.addEventListener('mouseleave', function(event) {
  226. scaleTR.style.height = '40px';
  227. scaleTR.style.width = '40px';
  228. scaleTR.style.border = '';
  229. });
  230.  
  231. document.addEventListener('mousemove', function(event) {
  232. if (isLockedY) {
  233. popupContainer.style.left = (event.clientX - offsetX) + 'px';
  234. popupContainer.style.top = (event.clientY - offsetY) + 'px';
  235. } else if (isscaleTR) {
  236. scaletrY = ((rectT + rectH - event.clientY) / (rectH));
  237. scaletrX = ((event.clientX - rectL) / (rectW));
  238. popupContainer.style.transform = `translate(-50%, -50%) scale(${scaletrX}, ${scaletrY})`
  239. }
  240. });
  241.  
  242. let imgElementsList = [];
  243. function ZoomOrScroll(event) {
  244. event.preventDefault();
  245. if (isLockedY) {
  246. let deltaY = event.deltaY * -ZOOM_SPEED;
  247. let newTop = parseInt(popupContainer.style.top) || 0;
  248. newTop += deltaY * 100;
  249. popupContainer.style.top = newTop + 'px';
  250. offsetY -= deltaY * 100;
  251. } else if (isLockedX && currentZeroImgElement) {
  252. let pair = albumSelector.find(pair => currentZeroImgElement.matches(pair.imgElement));
  253. if (pair) {
  254. let ancestorElement = currentZeroImgElement.closest(pair.albumElements);
  255. if (ancestorElement) {
  256. imgElementsList = Array.from(ancestorElement.querySelectorAll(pair.imgElement));
  257. let zeroIndex = imgElementsList.indexOf(currentZeroImgElement);
  258. let direction = event.deltaY > 0 ? 1 : -1;
  259. let newIndex = (zeroIndex + direction + imgElementsList.length) % imgElementsList.length;
  260. currentZeroImgElement = imgElementsList[newIndex];
  261. popup.src = currentZeroImgElement.src;
  262. }
  263. }
  264. } else {
  265. scale += event.deltaY * -ZOOM_SPEED;
  266. scale = Math.min(Math.max(0.125, scale), 10);
  267. popupContainer.style.transform = `translate(-50%, -50%) scale(${scale})`;
  268. }
  269. }
  270.  
  271. popupContainer.addEventListener('wheel', ZoomOrScroll);
  272.  
  273. //-------------------------------------------------------------------------
  274.  
  275. // show popup
  276. function showPopup(src, mouseX, mouseY) {
  277. if (!isshowPopupEnabled) return;
  278. popup.src = src;
  279. popup.style.display = 'block';
  280. popupContainer.style.display = 'block';
  281. popupContainer.style.position = 'fixed';
  282. popupContainer.style.transform = 'translate(-50%, -50%)';
  283. backdrop.style.display = 'block';
  284. backdrop.style.zIndex = '999';
  285. backdrop.style.backdropFilter = 'blur(10px)';
  286.  
  287. if (positionP === 'center') {
  288. popupContainer.style.top = '50%';
  289. popupContainer.style.left = '50%';
  290. } else {
  291. popupContainer.style.top = `${mouseY}px`;
  292. popupContainer.style.left = `${mouseX}px`;
  293. }
  294. }
  295.  
  296. let currentZeroImgElement;
  297. document.addEventListener('mouseover', function(e) {
  298. if (popupTimer) return;
  299. let target = e.target.closest(imgContainers);
  300. if (target.querySelector(nopeElements)) return;
  301. const imageElement = target.querySelector(imgElements);
  302. specialElements.forEach(pair => {
  303. if (imageElement.matches(pair.selector)) {
  304. pair.func(imageElement);
  305. }
  306. });
  307.  
  308. if (imageElement) {
  309. currentZeroImgElement = imageElement;
  310. if (intervalP === '') {
  311. showPopup(imageElement.src, e.clientX, e.clientY);
  312. } else {
  313. popupTimer = setTimeout(() => {
  314. showPopup(imageElement.src, e.clientX, e.clientY);
  315. popupTimer = null;
  316. }, parseInt(intervalP));
  317. }
  318. }
  319. });
  320. //-------------------------------------------------------------------------
  321.  
  322. // hide popup
  323. function hidePopup() {
  324. if (!ishidePopupEnabled) return;
  325. imgElementsList = [];
  326. isshowPopupEnabled = true;
  327. if (popupTimer) {
  328. clearTimeout(popupTimer);
  329. }
  330. document.body.appendChild(backdrop);
  331. popup.style.display = 'none';
  332. popupContainer.style.display = 'none';
  333. popupContainer.style.left = '50%';
  334. popupContainer.style.top = '50%';
  335. popupContainer.style.position = 'fixed';
  336. popupContainer.style.transform = 'translate(-50%, -50%) scale(1)';
  337. popupContainer.style.border = '';
  338. popupContainer.style.outline = '';
  339. backdrop.style.zIndex = '';
  340. backdrop.style.display = 'none';
  341. backdrop.style.backdropFilter = '';
  342. isLockedY = false;
  343. isLockedX = false;
  344.  
  345. }
  346.  
  347. popupContainer.addEventListener('mouseout', function(event) {
  348. let relatedTarget = event.relatedTarget;
  349. if (relatedTarget && (popupContainer.contains(relatedTarget) || relatedTarget.matches('.imgContainers') || relatedTarget.closest('.imgContainers'))) {
  350. return;
  351. }
  352.  
  353. hidePopup();
  354. if (intervalP !== '') {
  355. popupTimer = setTimeout(() => {
  356. popupTimer = null;
  357. }, parseInt(intervalP));
  358. }
  359. });
  360.  
  361. document.addEventListener('keydown', function(event) {
  362. if (event.key === "Escape") {
  363. event.preventDefault();
  364. hidePopup();
  365. }
  366. });
  367.  
  368. //-------------------------------------------------------------------------
  369.  
  370. // lock popup in screen
  371. let ishidePopupEnabled = true;
  372.  
  373. function togglehidePopup(event) {
  374. ishidePopupEnabled = !ishidePopupEnabled;
  375. popupContainer.style.outline = ishidePopupEnabled ? '' : '3px solid #ae0001';
  376. }
  377.  
  378. popupContainer.addEventListener('dblclick', function(event) {
  379. clearTimeout(clickTimeout);
  380. togglehidePopup();
  381. });
  382. backdrop.addEventListener('dblclick', function(event) {
  383. clearTimeout(clickTimeout);
  384. ishidePopupEnabled = true;
  385. hidePopup();
  386. });
  387.  
  388.  
  389. backdrop.addEventListener('click', function(event) {
  390. if (clickTimeout) clearTimeout(clickTimeout);
  391. clickTimeout = setTimeout(function() {
  392. if (isscaleTR) {
  393. isscaleTR = false;
  394. } else {
  395. backdrop.style.zIndex = '';
  396. backdrop.style.display = 'none';
  397. backdrop.style.backdropFilter = '';
  398. isshowPopupEnabled = false;
  399. }
  400. }, 300);
  401. });
  402.  
  403.  
  404. // Album scrolling
  405. LockedX.addEventListener('mouseenter', function() {
  406. 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%)';
  407. });
  408.  
  409. LockedX.addEventListener('mouseleave', function() {
  410. LockedX.style.background = '#0000';
  411. });
  412.  
  413. LockedX.addEventListener('click', function(event) {
  414. ishidePopupEnabled = false;
  415. isLockedX = !isLockedX;
  416. if (isLockedX) {
  417. isLockedY = false;
  418. popupContainer.style.borderTop = '3px solid #00ff00';
  419. popupContainer.style.borderBottom = '3px solid #00ff00';
  420. popupContainer.style.borderLeft = '';
  421. popupContainer.style.borderRight = '';
  422. popupContainer.style.outline = '';
  423. } else {
  424. popupContainer.style.border = '';
  425. popupContainer.style.outline = '3px solid #ae0001';
  426. }
  427.  
  428. });
  429. }
  430.  
  431. //-------------------------------------------------------------------------
  432.  
  433. // Is to be run -----------------------------------------------------------
  434. if (URLmatched) {
  435. const indicatorBar = document.createElement('div');
  436. indicatorBar.style.cssText = `
  437. position: fixed;
  438. bottom: 0;
  439. left: 50%;
  440. transform: translateX(-50%);
  441. z-index: 9999;
  442. height: 30px;
  443. width: 50vw;
  444. background: #0000;`;
  445. document.body.appendChild(indicatorBar);
  446.  
  447. function toggleIndicator() {
  448. enableP = 1 - enableP;
  449. 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%)';
  450. setTimeout(() => {
  451. indicatorBar.style.background = '#0000';
  452. }, 1000);
  453. if (enableP === 1) {
  454. HoverZoomMinus();
  455. } else {
  456. const existingPopup = document.body.querySelector('.popup-container');
  457. if (existingPopup) document.body.removeChild(existingPopup);
  458. const existingBackdrop = document.body.querySelector('.popup-backdrop');
  459. if (existingBackdrop) document.body.removeChild(existingBackdrop);
  460.  
  461. }
  462. }
  463.  
  464. let hoverTimeout;
  465. indicatorBar.addEventListener('mouseenter', () => {
  466. hoverTimeout = setTimeout(toggleIndicator, 500);
  467. });
  468. indicatorBar.addEventListener('mouseleave', () => {
  469. clearTimeout(hoverTimeout);
  470. indicatorBar.style.background = '#0000';
  471. });
  472. if (enableP === 1) {
  473. HoverZoomMinus();
  474. }
  475. } else {
  476. return;
  477. }
  478.  
  479. })();