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, rectH, 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 ? '' : '6px solid #ae0001';
  181. isLockedX = false;
  182. popupContainer.style.borderTop = '';
  183. popupContainer.style.borderBottom = '';
  184. popupContainer.style.borderLeft = '6px solid #00ff00';
  185. popupContainer.style.borderRight = '6px 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. rectH = rect.height;
  208. rectW = rect.width;
  209. rectL = rect.left;
  210. } else {
  211. scaleTR.style.border = '';
  212. }
  213.  
  214. });
  215.  
  216. let scaletrY, scaletrX;
  217. scaleTR.addEventListener('mouseenter', function() {
  218. scaleTR.style.height = 'calc(100% - 12px)';
  219. scaleTR.style.width = 'calc(100% - 12px)';
  220. scaleTR.style.border = '6px solid #0000ff';
  221.  
  222. });
  223. scaleTR.addEventListener('mouseleave', function(event) {
  224. scaleTR.style.height = '40px';
  225. scaleTR.style.width = '40px';
  226. scaleTR.style.borderBottom = '6px solid #0000';
  227. scaleTR.style.borderLeft = '6px solid #0000';
  228. });
  229.  
  230. document.addEventListener('mousemove', function(event) {
  231. if (isLockedY) {
  232. popupContainer.style.left = (event.clientX - offsetX) + 'px';
  233. popupContainer.style.top = (event.clientY - offsetY) + 'px';
  234. } else if (isscaleTR) {
  235. scaletrY = ((rectT + rectH - event.clientY) / (rectH));
  236. scaletrX = ((event.clientX - rectL) / (rectW));
  237. popupContainer.style.transform = `translate(-50%, -50%) scale(${scaletrX}, ${scaletrY})`
  238. scaleTR.style.borderTop = '6px solid #0000ff';
  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. scaleTR.style.borderTop = '';
  345. scaleTR.style.borderRight = '';
  346. scaleTR.style.boxShadow = '';
  347.  
  348. }
  349.  
  350. popupContainer.addEventListener('mouseout', function(event) {
  351. let relatedTarget = event.relatedTarget;
  352. if (relatedTarget && (popupContainer.contains(relatedTarget) || relatedTarget.matches('.imgContainers') || relatedTarget.closest('.imgContainers'))) {
  353. return;
  354. }
  355.  
  356. hidePopup();
  357. if (intervalP !== '') {
  358. popupTimer = setTimeout(() => {
  359. popupTimer = null;
  360. }, parseInt(intervalP));
  361. }
  362. });
  363.  
  364. document.addEventListener('keydown', function(event) {
  365. if (event.key === "Escape") {
  366. event.preventDefault();
  367. hidePopup();
  368. }
  369. });
  370.  
  371. //-------------------------------------------------------------------------
  372.  
  373. // lock popup in screen
  374. let ishidePopupEnabled = true;
  375.  
  376. function togglehidePopup(event) {
  377. ishidePopupEnabled = !ishidePopupEnabled;
  378. popupContainer.style.outline = ishidePopupEnabled ? '' : '6px solid #ae0001';
  379. }
  380.  
  381. popupContainer.addEventListener('dblclick', function(event) {
  382. clearTimeout(clickTimeout);
  383. togglehidePopup();
  384. });
  385. backdrop.addEventListener('dblclick', function(event) {
  386. clearTimeout(clickTimeout);
  387. ishidePopupEnabled = true;
  388. hidePopup();
  389. });
  390.  
  391.  
  392. backdrop.addEventListener('click', function(event) {
  393. if (clickTimeout) clearTimeout(clickTimeout);
  394. clickTimeout = setTimeout(function() {
  395. if (isscaleTR) {
  396. isscaleTR = false;
  397. scaleTR.style.border = '';
  398. scaleTR.style.boxShadow = '';
  399. } else {
  400. backdrop.style.zIndex = '';
  401. backdrop.style.display = 'none';
  402. backdrop.style.backdropFilter = '';
  403. isshowPopupEnabled = false;
  404. }
  405. }, 300);
  406. });
  407.  
  408.  
  409. // Album scrolling
  410. LockedX.addEventListener('mouseenter', function() {
  411. 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%)';
  412. });
  413.  
  414. LockedX.addEventListener('mouseleave', function() {
  415. LockedX.style.background = '#0000';
  416. });
  417.  
  418. LockedX.addEventListener('click', function(event) {
  419. ishidePopupEnabled = false;
  420. isLockedX = !isLockedX;
  421. if (isLockedX) {
  422. isLockedY = false;
  423. popupContainer.style.borderTop = '6px solid #00ff00';
  424. popupContainer.style.borderBottom = '6px solid #00ff00';
  425. popupContainer.style.borderLeft = '';
  426. popupContainer.style.borderRight = '';
  427. popupContainer.style.outline = '';
  428. } else {
  429. popupContainer.style.border = '';
  430. popupContainer.style.outline = '6px solid #ae0001';
  431. }
  432.  
  433. });
  434. }
  435.  
  436. //-------------------------------------------------------------------------
  437.  
  438. // Is to be run -----------------------------------------------------------
  439. if (URLmatched) {
  440. const indicatorBar = document.createElement('div');
  441. indicatorBar.style.cssText = `
  442. position: fixed;
  443. bottom: 0;
  444. left: 50%;
  445. transform: translateX(-50%);
  446. z-index: 9999;
  447. height: 30px;
  448. width: 50vw;
  449. background: #0000;`;
  450. document.body.appendChild(indicatorBar);
  451.  
  452. function toggleIndicator() {
  453. enableP = 1 - enableP;
  454. 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%)';
  455. setTimeout(() => {
  456. indicatorBar.style.background = '#0000';
  457. }, 1000);
  458. if (enableP === 1) {
  459. HoverZoomMinus();
  460. } else {
  461. const existingPopup = document.body.querySelector('.popup-container');
  462. if (existingPopup) document.body.removeChild(existingPopup);
  463. const existingBackdrop = document.body.querySelector('.popup-backdrop');
  464. if (existingBackdrop) document.body.removeChild(existingBackdrop);
  465.  
  466. }
  467. }
  468.  
  469. let hoverTimeout;
  470. indicatorBar.addEventListener('mouseenter', () => {
  471. hoverTimeout = setTimeout(toggleIndicator, 500);
  472. });
  473. indicatorBar.addEventListener('mouseleave', () => {
  474. clearTimeout(hoverTimeout);
  475. indicatorBar.style.background = '#0000';
  476. });
  477. if (enableP === 1) {
  478. HoverZoomMinus();
  479. }
  480. } else {
  481. return;
  482. }
  483.  
  484. })();