Greasy Fork is available in English.
Zoom in the image which is under the cursor.
// ==UserScript== // @name Twitter: Zoom In Image // @name:zh-TW Twitter 放大圖片 // @name:zh-CN Twitter 放大图像 // @name:ja Twitter 画像を拡大 // @name:ko Twitter 이미지 확대 // @name:ru Twitter Увеличить изображение // @version 1.2.5 // @description Zoom in the image which is under the cursor. // @description:zh-TW 放大滑鼠游標下的圖片。 // @description:zh-CN 放大滑鼠光标下的图像。 // @description:ja カーソルの下にある画像を拡大します。 // @description:ko 커서 아래에있는 이미지를 확대합니다. // @description:ru Увеличьте изображение под курсором. // @author Hayao-Gai // @namespace https://github.com/HayaoGai // @icon https://i.imgur.com/M9oO8K9.png // @match https://twitter.com/* // @grant none // ==/UserScript== /* jshint esversion: 6 */ (function() { 'use strict'; const svgLoading = `<svg width="100%" height="100%" viewBox="0 0 32 32"><circle cx="16" cy="16" fill="none" r="14" stroke-width="4" style="opacity: 0.2;"></circle><circle cx="16" cy="16" fill="none" r="14" stroke-width="4" style="stroke-dasharray: 80; stroke-dashoffset: 60;"></circle></svg>`; const textStyle = ` .zoomin-loading { position: fixed; width: 26px; height: 26px; display: none; } .zoomin-loading-show { display: flex !important; } .zoomin-canvas { border-radius: 8px; position: fixed; background-color: #e0e0e0; pointer-events: none; opacity: 0; } .zoomin-canvas-show { transition: opacity 0.4s; opacity: 1 !important; } .zoomin-zoom { border-radius: 8px; position: fixed; pointer-events: none; opacity: 0; } .zoomin-zoom-show { transition: opacity 0.4s; opacity: 1 !important; }`; let currentUrl = document.location.href; let updating = false, showing = false; let loading, canvas, zoom, currentImage; css(); init(10); locationChange(); window.addEventListener("scroll", update); function init(times) { for (let i = 0; i < times; i++) { setTimeout(removeBlock, 500 * i); setTimeout(createLoading, 500 * i); setTimeout(createCanvas, 500 * i); setTimeout(createZoom, 500 * i); setTimeout(eventListener, 500 * i); setTimeout(sensitiveContent, 500 * i); } } // create function removeBlock() { // remove the div block on every avatar. document.querySelectorAll(".r-1twgtwe").forEach(block => block.remove()); } function removeLoading() { if (!loading) return; loading.remove(); loading = null; } function createLoading() { // check svg if (!getColor()) return; // check exist if (!!loading) return; // create loading = document.createElement("div"); loading.className = "zoomin-loading css-1dbjc4n r-17bb2tj r-1muvv40 r-127358a r-1ldzwu0"; loading.innerHTML = svgLoading; loading.querySelectorAll("circle").forEach(circle => { circle.style.stroke = getColor(); }); document.body.appendChild(loading); } function createCanvas() { // check exist if (!!canvas) return; // create canvas = document.createElement("div"); canvas.classList.add("zoomin-canvas"); document.body.appendChild(canvas); } function createZoom() { // check exist if (!!zoom) return; // create zoom = document.createElement("img"); zoom.classList.add("zoomin-zoom"); document.body.appendChild(zoom); } // event function eventListener() { // situation 1: disable if you go into the image page. if (currentUrl.includes("photo/1")) return; // situation 2: return if loading, canvas or zoom doesn't exist. if (!loading || !canvas || !zoom) return; // add thumbnail mouse event. document.querySelectorAll(".r-4gszlv:not(zoomin-listener)").forEach(thumbnail => { thumbnail.classList.add("zoomin-listener"); // return if the video thumbnail is exist. if (!thumbnail.closest(".r-1777fci") || (thumbnail.closest(".r-1777fci") && !thumbnail.closest(".r-1777fci").querySelector("[data-testid='playButton']"))) { const image = thumbnail.parentElement.querySelector("img"); image.addEventListener("mousemove", showImage); image.addEventListener("mouseleave", hideImage); } }); } function sensitiveContent() { document.querySelectorAll(".r-42olwf.r-1vsu8ta:not(.zoomin-view)").forEach(view => { view.classList.add("zoomin-view"); view.addEventListener("click", () => init(3)); }); } function showImage() { // avoid calling this function multiple times. if (showing) return; showing = true; currentImage = this; // get image original size url. const origin = getOrigin(currentImage.src); if (!origin) return; zoom.setAttribute("src", origin); // show loading icon. loading.style.left = getLeft(); loading.style.top = getTop(); loading.classList.add("zoomin-loading-show"); // detail zoomDetail(); } function hideImage() { showing = false; loading.classList.remove("zoomin-loading-show"); canvas.classList.remove("zoomin-canvas-show"); zoom.classList.remove("zoomin-zoom-show"); zoom.removeAttribute("src"); } function zoomDetail() { // wait until get the image size. if (!zoom.naturalWidth) { setTimeout(zoomDetail, 100); return; } // hide loading icon. loading.classList.remove("zoomin-loading-show"); // fit zoom original size for browser. const w = zoom.naturalWidth; const h = zoom.naturalHeight; const clientW = document.documentElement.clientWidth; const clientH = document.documentElement.clientHeight; const situation1 = w > clientW; const situation2 = h > clientH; if (situation1 && situation2) { const rate = clientH / h; const new_w = w * rate; const new_h = clientH; if (new_w > clientW) { const rate2 = clientW / new_w; const new_w2 = clientW; const new_h2 = new_h * rate2; setSize(canvas, new_w2, new_h2); setSize(zoom, new_w2 - 10, new_h2 - 10); } else { setSize(canvas, new_w, new_h); setSize(zoom, new_w - 10, new_h - 10); } } else if (situation1) { const rate3 = clientW / w; const new_h3 = h * rate3; setSize(canvas, clientW, new_h3); setSize(zoom, clientW - 10, new_h3 - 10); } else if (situation2) { const rate4 = clientH / h; const new_w4 = w * rate4; setSize(canvas, new_w4, clientH); setSize(zoom, new_w4 - 10, clientH - 10); } else { setSize(canvas, w + 10, h + 10); setSize(zoom, w, h); } // position const cWidth = parseInt(canvas.style.width); const cHeight = parseInt(canvas.style.height); const zWidth = parseInt(zoom.style.width); const zHeight = parseInt(zoom.style.height); let cLeft = clientW / 2 - cWidth / 2; let cTop = clientH / 2 - cHeight / 2; if (cLeft < 0) cLeft = 0; if (cTop < 0) cTop = 0; let zLeft = clientW / 2 - zWidth / 2; let zTop = clientH / 2 - zHeight / 2; if (zLeft < 0) zLeft = 0; if (zTop < 0) zTop = 0; canvas.classList.add("zoomin-canvas-show"); canvas.style.left = `${cLeft}px`; canvas.style.top = `${cTop}px`; zoom.classList.add("zoomin-zoom-show"); zoom.style.left = `${zLeft}px`; zoom.style.top = `${zTop}px`; } // method function getColor() { let color = ""; document.querySelectorAll("svg.r-50lct3").forEach(svg => { if (!!color) return; color = getComputedStyle(svg).color; }); return color; } function getOrigin(url) { // situation 1: post if (url.includes("media") || url.includes("card")) { const search = url.split("&name="); const last = search[search.length - 1]; return url.replace(last, "orig"); } // situation 2: banner else if (url.includes("banner")) { const search = url.split("/"); const last = search[search.length - 1]; return url.replace(last, "1500x500"); } // situation 3: avatar else if (url.includes("profile")) { const search1 = url.split("_"); const search2 = url.split("."); const last1 = search1[search1.length - 1]; const last2 = search2[search2.length - 1]; return url.replace(`_${last1}`, `.${last2}`); } // situation 3: video else { return null; } } function getLeft() { return `${document.documentElement.clientWidth / 2 - 13}px`; } function getTop() { return `${document.documentElement.clientHeight / 2 - 13}px`; } function setSize(element, width, height) { element.style.width = `${width}px`; element.style.height = `${height}px`; } // others function css() { const style = document.createElement("style"); style.type = "text/css"; style.innerHTML = textStyle; document.head.appendChild(style); } function update() { if (updating) return; updating = true; init(3); setTimeout(() => { updating = false; }, 1000); } function locationChange() { const observer = new MutationObserver(mutations => { mutations.forEach(() => { if (currentUrl !== document.location.href) { currentUrl = document.location.href; hideImage(); removeLoading(); init(10); } }); }); const target = document.body; const config = { childList: true, subtree: true }; observer.observe(target, config); } })();