🏠 返回首頁 

Greasy Fork is available in English.

Twitter: Zoom In Image

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