// ==UserScript== // @name 百度贴吧图片点击放大-优化版 // @namespace https://greasyfork.org/users/51104 // @version 1.7.6 // @description 直接在当前页面点击查看原图(包括签名档)。支持图片的多开、拖拽、垂直或水平滚动和缩放旋转。优化:先显示原尺寸图片,再加载高清图并保持大小不变,未放大的时候按照高度限制(可设置)等比例缩放图片大小,防止图片挤占过多空间 // @match *://tieba.baidu.com/p/* // @match *://tieba.baidu.com/f?* // @exclude *://tieba.baidu.com/f?*kw=* // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @author lliwhx // @license MIT // @copyright Copyright © 2016-2022 lliwhx // ==/UserScript== (function (win, doc) { 'use strict'; if(doc.title==='贴吧404')return; var iTarget, preferences, gallery, iMouse, debounce, docElement = doc.documentElement, docWidth = docElement.clientWidth - 5, docHeight = docElement.clientHeight - 5; // 添加过渡动画样式 GM_addStyle(` .BDE_Image,.d_content_img,.j_user_sign{cursor:zoom-in;} .btzi-gallery{position:fixed;top:0;left:0;z-index:19990801;} .btzi-img{ position:absolute; transform-origin:0 0; box-shadow:0 0 7px rgba(0,0,0,.4); transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); will-change: transform; } .btzi-img:hover{z-index:20220801;box-shadow:0 0 7px rgb(0,0,0);} .btzi-img:active{box-shadow:0 0 7px rgba(0,0,0,.4);cursor:move;} .btzi-loading{z-index:20220802;} .btzi-img.animating {pointer-events: none;} .btzi-img.no-transition {transition: none !important;} .btzi-img.loading {border: 2px dashed #ccc;} .btzi-img.loaded {border: 2px solid #4CAF50;} .btzi-img.error {border: 2px solid #f44336;} `); // 打开图片函数 - 添加过渡动画 function open(e) { var i, tSrc, t = e.target; /* className分别指向BDE_Image新图,j_user_sign签名档,d_content_img老图。只有鼠标左键点击以上图片且不是图册贴里的图片时才会放大图片。(可修改) */ if (!e.button && ['BDE_Image', 'j_user_sign', 'd_content_img'].includes(t.className) && t.parentNode.nodeName !== 'A') { iTarget = t; i = doc.createElement('img'); i.className = 'btzi-img'; // 获取原图的位置和尺寸 var rect = t.getBoundingClientRect(); var startX = rect.left; var startY = rect.top; var startWidth = rect.width; var startHeight = rect.height; // 添加加载中提示 i.style.backgroundColor = '#f0f0f0'; // 创建加载提示元素 var loadingText = doc.createElement('div'); loadingText.className = 'btzi-loading'; loadingText.textContent = '加载中...'; loadingText.style.position = 'absolute'; loadingText.style.top = '50%'; loadingText.style.left = '50%'; loadingText.style.transform = 'translate(-50%, -50%)'; loadingText.style.color = '#666'; loadingText.style.fontSize = '14px'; loadingText.style.zIndex = '1'; // 先使用帖子中的图片(中等质量) i.src = t.src; // 获取原始图片尺寸 var naturalWidth = t.naturalWidth || t.width; var naturalHeight = t.naturalHeight || t.height; // 计算最终位置和缩放 var w = naturalWidth, h = naturalHeight, s = (!+preferences.size) && (docWidth - w < 0 || docHeight - h < 0) ? Math.min((docWidth - 5) / w,(docHeight - 5) / h) : 1, x = docWidth - w * s - 5 > 0 ? (docWidth - w * s) / 2 : 5, y = docHeight - h * s - 5 > 0 ? (docHeight - h * s) / 2 : 5; i.iData = { width: w, height: h, x: x, y: y, scale: s, rotate: 0, isHighQuality: false, displayWidth: w * s, displayHeight: h * s }; // 初始位置和尺寸 transform(i, startX, startY, startWidth / w, 0); gallery.appendChild(i); i.appendChild(loadingText); // 添加动画类 i.classList.add('animating'); // 使用requestAnimationFrame确保动画流畅 requestAnimationFrame(() => { // 动画到目标位置和尺寸 transform(i, x, y, s, 0); // 动画结束后移除动画类 setTimeout(() => { i.classList.remove('animating'); }, 300); }); // 图片加载完成后的处理 var onMediumQualityLoaded = function() { // 中质量图加载完成,现在开始加载高质量图 loadingText.textContent = '加载高清图...'; // 添加加载中状态 i.classList.add('loading'); /* 获取高质量图片URL 需要获取到点击图片的abc和123的内容,然后补全成原图的地址。 */ tSrc = /https?:\/\/(\w+)\.baidu\.com\/.+\/(\w+\.[a-zA-Z]{3,4}([^_]*_?))/.exec(t.src); if(tSrc && tSrc[3]){ // 有特殊后缀,可能是高质量图 loadHighQualityImage(i, tSrc[2], t); } else { // 无特殊后缀,尝试获取原图 var originalSrcMatch = t.src.match(/^(.*?)(?:_\d+x\d+)?\.(jpg|jpeg|png|gif|bmp|webp)(?:\?.*)?$/i); if (originalSrcMatch) { // 尝试移除可能的尺寸后缀 var baseUrl = originalSrcMatch[1] + '.' + originalSrcMatch[2]; loadHighQualityImageDirect(i, baseUrl, t); } else { // 无法获取高质量图,移除加载提示 loadingText.remove(); i.style.backgroundColor = ''; // 标记为已加载(虽然没有加载高清图) i.classList.remove('loading'); i.classList.add('loaded'); } } // 移除中质量图的监听器 i.removeEventListener('load', onMediumQualityLoaded); i.removeEventListener('error', onMediumQualityError); }; var onMediumQualityError = function() { // 中质量图加载失败,直接尝试加载高质量图 loadingText.textContent = '加载高清图...'; tSrc = /https?:\/\/(\w+)\.baidu\.com\/.+\/(\w+\.[a-zA-Z]{3,4}([^_]*_?))/.exec(t.src); if(tSrc && tSrc[3]){ loadHighQualityImage(i, tSrc[2], t); } else { loadingText.remove(); i.style.backgroundColor = ''; } i.removeEventListener('load', onMediumQualityLoaded); i.removeEventListener('error', onMediumQualityError); }; // 监听中质量图加载 if (i.complete) { onMediumQualityLoaded(); } else { i.addEventListener('load', onMediumQualityLoaded); i.addEventListener('error', onMediumQualityError); } } } var tiebaTid = /\d+/.exec(location.pathname)[0]; // 加载高质量图片(通过API获取) function loadHighQualityImage(i, tSrc, originalImg) { var xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function() { if (this.readyState == 4) { var loadingElement = i.querySelector('.btzi-loading'); if (this.status == 200) { try { var response = JSON.parse(this.responseText); var highQualityUrl = response?.data?.img?.original?.waterurl; if (highQualityUrl) { loadAndReplaceImage(i, highQualityUrl, originalImg, loadingElement); } else { // API返回但没有高质量图 if (loadingElement) loadingElement.remove(); i.style.backgroundColor = ''; } } catch(e) { console.error('解析高质量图片URL失败:', e); if (loadingElement) loadingElement.remove(); i.style.backgroundColor = ''; } } else { // API请求失败 if (loadingElement) loadingElement.remove(); i.style.backgroundColor = ''; } } }; tSrc = /(\w+)/.exec(tSrc)[0]; xhttp.open("GET", "https://tieba.baidu.com/photo/p?alt=jview&pic_id="+tSrc+"&tid="+tiebaTid, true); xhttp.send(); } // 直接加载高质量图片 function loadHighQualityImageDirect(i, highQualityUrl, originalImg) { var loadingElement = i.querySelector('.btzi-loading'); loadAndReplaceImage(i, highQualityUrl, originalImg, loadingElement); } // 修改 loadAndReplaceImage 函数,添加加载状态标记 function loadAndReplaceImage(i, highQualityUrl, originalImg, loadingElement) { var highQualityImg = new Image(); // 添加加载中状态 i.classList.add('loading'); i.classList.remove('success-flash', 'error'); highQualityImg.onload = function() { // 获取当前图片的显示尺寸和位置 var currentData = i.iData; var currentX = currentData.x; var currentY = currentData.y; var currentScale = currentData.scale; var currentRotate = currentData.rotate; // 计算高清图需要的缩放比例,使其显示尺寸与当前图片相同 var targetDisplayWidth = currentData.displayWidth; var targetDisplayHeight = currentData.displayHeight; // 计算高清图需要的缩放比例 var newScaleX = targetDisplayWidth / this.width; var newScaleY = targetDisplayHeight / this.height; // 使用相同的缩放比例,保持宽高比不变 var newScale = newScaleX; // 完全禁用过渡效果 i.classList.add('no-transition'); // 更新图片数据,保持位置、旋转和显示尺寸不变 i.iData = { width: this.width, height: this.height, x: currentX, y: currentY, scale: newScale, rotate: currentRotate, isHighQuality: true, displayWidth: targetDisplayWidth, displayHeight: targetDisplayHeight }; // 直接替换图片源,保持相同的位置和缩放 i.src = highQualityUrl; transform(i, currentX, currentY, newScale, currentRotate); // 移除加载提示 if (loadingElement) loadingElement.remove(); i.style.backgroundColor = ''; // 移除loading类,添加success-flash类 i.classList.remove('loading'); i.classList.add('success-flash'); // 动画结束后移除success-flash类 setTimeout(() => { i.classList.remove('success-flash'); }, 600); // 清理 highQualityImg.onload = null; highQualityImg.onerror = null; }; highQualityImg.onerror = function() { // 高质量图加载失败,保持当前图片 if (loadingElement) loadingElement.remove(); i.style.backgroundColor = ''; // 更新状态为加载失败 i.classList.remove('loading'); i.classList.add('error'); // 标记为已尝试加载高质量图但失败 i.iData.isHighQuality = false; highQualityImg.onload = null; highQualityImg.onerror = null; }; highQualityImg.src = highQualityUrl; } // 鼠标按下图片函数 function down(e) { var t, data; if (!e.button) { t = e.target; data = t.iData; iTarget = t; // 添加 no-transition 类以禁用过渡效果 t.classList.add('no-transition'); iMouse = { // 获取鼠标按下时的xy坐标和相对图片的xy坐标 clientX: e.clientX, clientY: e.clientY, offsetX: data.x - e.clientX, offsetY: data.y - e.clientY }; t = null; data = null; e.preventDefault(); e.stopPropagation(); doc.addEventListener('mousemove', move); // 鼠标按下时给页面注册鼠标移动和鼠标放开事件 doc.addEventListener('mouseup', up); } } // 鼠标移动图片函数 function move(e) { var t = iTarget, data = t.iData, x = e.clientX + iMouse.offsetX, y = e.clientY + iMouse.offsetY; e.stopPropagation(); transform(t, x, y, data.scale, data.rotate); t = null; x = null; y = null; data = null; } // 固定图片位置函数 function up(e) { var t, data, translate; if (iMouse.clientX === e.clientX && iMouse.clientY === e.clientY) { // 判断鼠标按下和松开的位置一致才能关闭图片 iTarget = null; } else { t = iTarget; data = t.iData; translate = /translate\((-?\d+)px,\s?(-?\d+)px\)/.exec(t.getAttribute('style')); // 获取图片变化后的位置导入图片属性内 data.x = translate[1] | 0; // 取整 data.y = translate[2] | 0; // 移除 no-transition 类以恢复过渡效果 t.classList.remove('no-transition'); t = null; data = null; translate = null; } iMouse = null; doc.removeEventListener('mousemove', move); // 鼠标松开后注销页面鼠标移动和鼠标放开事件 doc.removeEventListener('mouseup', up); } /* 图片关闭函数。(可修改) */ function close(e) { var t = e.target; switch (preferences.closeWindow) { case 'btzi_gallery': // 关闭图片范围为图片时,点击图片关闭该图片 if (!iTarget) { t.iData = null; t.remove(); } break; case 'document': // 当关闭图片方式为页面时,点击要放大的图片以外的区域都会关闭所有图片 if (!iTarget || (t !== iTarget && t !== docElement)) { if(doc.body.classList.contains('btzi-enabled') || t.id === 'btzi_settings_save')break; // 打开用户设置界面,不会关闭图片 gallery.style.display = 'none'; while (gallery.hasChildNodes()) { // 关闭所有图片 gallery.firstChild.iData = null; gallery.firstChild.remove(); } gallery.style.display = ''; } break; } t=null; iTarget = null; } // 鼠标滚轮函数 function wheel(e) { var t = e.target, data = t.iData, x = data.x, y = data.y, s = data.scale, r = data.rotate, p = preferences, eKey = !e.altKey && !e.ctrlKey && !e.shiftKey, wKey = p.wheelKey, zKey = p.zoomKey, rKey = p.rotateKey, wDirection = p.wheelDirection, zDirection = p.zoomDirection, rDirection = p.rotateDirection, deltaXY = (e.deltaY || e.deltaX) > 0 ? 100 : -100, delta, tmp, z; e.preventDefault(); e.stopPropagation(); // 添加 no-transition 类以禁用过渡效果 t.classList.add('no-transition'); if (wKey === 'type' && eKey || wKey !== 'type' && e[wKey]) { // 图片滚轮移动判断 // ... (保持原有的移动逻辑不变) } if (zKey === 'type' && eKey || zKey !== 'type' && e[zKey]) { // 图片缩放判断 // 使用百分比缩放,每次缩放10% delta = deltaXY * zDirection > 0 ? 1.2 : 0.9; z = s * delta; // 限制最小缩放比例 if (z < 0.1) { z = 0.1; } // 计算新的位置,使缩放以鼠标位置为中心 tmp = z / s; data.x = e.clientX - (e.clientX - x) * tmp; data.y = e.clientY - (e.clientY - y) * tmp; data.scale = z; data.displayWidth = data.width * z; data.displayHeight = data.height * z; transform(t, data.x, data.y, z, r); return; } if (rKey === 'type' && eKey || rKey !== 'type' && e[rKey]) { // 图片旋转判断 tmp = data.width; // 对图片内data.width宽data.height高属性进行调换,使旋转后的图片数据正常计算 data.width = data.height; data.height = tmp; delta = deltaXY * rDirection > 0 ? 90 : 270; // 270比-90好计算 z = (r + delta) % 360; // 取余。保证为0,90,180,270度 tmp = 0.01745329 * delta; data.x = e.clientX - (e.clientX - x) * Math.cos(tmp) + (e.clientY - y) * Math.sin(tmp); // 以鼠标位置(e.clientX,e.clientY)为中心,图片坐标(x,y)旋转tmp弧度,计算新坐标 data.y = e.clientY - (e.clientX - x) * Math.sin(tmp) - (e.clientY - y) * Math.cos(tmp); data.rotate = z; transform(t, data.x, data.y, s, z); return; } // 使用 requestAnimationFrame 在下一帧移除 no-transition 类 requestAnimationFrame(() => { t.classList.remove('no-transition'); }); } // 图片动画函数。translate移动,scale缩放,rotate旋转度 function transform(t, x, y, s, r) { t.style.transform = `translate(${x | 0}px, ${y | 0}px) scale(${s}) rotate(${r}deg)`; } // 关闭图片的范围是w图片框架还是doc页面 function frame(w) { return doc.getElementById(w) || doc; } // 浏览器窗口缩放后重新计算窗口大小,保证图片打开后的位置基于变化后的窗口大小 function resize() { clearTimeout(debounce); debounce = setTimeout(function () { docWidth = docElement.clientWidth - 5; docHeight = docElement.clientHeight - 5; }, 500); } /* 定位到贴子内容,用来注册事件。(可修改) */ var postlist = doc.getElementById('j_p_postlist'); /* 阻止贴吧默认打开图片方式函数。(可修改) */ var prevent = function (e) { var t = e.target; if (!e.button && t.className === 'BDE_Image' && t.parentNode.nodeName !== 'A') { // t.parentNode.nodeName !== 'A'图册贴里的图片打开方式不取消,仍为默认方式 e.stopPropagation(); } }, /* 贴子翻页删除图片函数。(可修改) */ callback = function () { gallery.style.display = 'none'; while (gallery.hasChildNodes()) { gallery.firstChild.iData = null; gallery.firstChild.remove(); } gallery.style.display = ''; }, /* 监听贴子是否翻页。(可修改) */ observer = new MutationObserver(callback); observer.observe(postlist, { childList: true }); /* 添加样式。鼠标放到图片上的cursor鼠标样式,放大后图片的btzi-gallery框架位置,以及btzi-img图片定位和hover鼠标经过图片时置顶并显示阴影,active鼠标按下图片后隐藏阴影。(可修改) */ GM_addStyle(` .BDE_Image,.d_content_img,.j_user_sign{cursor:zoom-in;} .btzi-gallery{position:fixed;top:0;left:0;z-index:19990801;} .btzi-img{position:absolute;transform-origin:0 0;box-shadow:0 0 7px rgba(0,0,0,.4);} .btzi-img:hover{z-index:20220801;box-shadow:0 0 7px rgb(0,0,0);} .btzi-img:active{box-shadow:0 0 7px rgba(0,0,0,.4);cursor:move;} .btzi-loading{z-index:20220802;} `); /* 获取btzi-UserSettings用户设置,初次使用设定一个默认值。 open鼠标左键打开图片方式; close鼠标左键关闭图片方式,closeWindow可关闭图片的范围; size图片打开后大小; wheelKey滚动图片组合键,wheelDirection滚动图片滚轮方向; zoomKey缩放图片组合键,zoomDirection缩放图片滚轮方向; rotateKey旋转图片组合键,rotateDirection旋转图片滚轮方向。 */ preferences = JSON.parse(GM_getValue('btzi-UserSettings', '{"open": "click","close": "click","closeWindow":"btzi_gallery","size":"0","wheelKey":"ctrlKey","wheelDirection": "-1","zoomKey":"type","zoomDirection": "-1","rotateKey":"shiftKey","rotateDirection": "1"}')); // 创建存放图片的div框架 gallery = doc.createElement('div'); gallery.className = 'btzi-gallery'; gallery.id = 'btzi_gallery'; gallery.addEventListener('mousedown', down); // 给框架注册鼠标按下事件,可以移动关闭图片 gallery.addEventListener('wheel', wheel); // 给框架注册鼠标滚轮事件,可以滚动图片 doc.body.appendChild(gallery); postlist.addEventListener('click', prevent, true); // 给贴子内容注册阻止原图片打开事件 postlist.addEventListener(preferences.open, open, true); // 给贴子内容注册打开图片事件 frame(preferences.closeWindow).addEventListener(preferences.close, close); // 给关闭图片的范围注册图片关闭图片事件 win.addEventListener('resize', resize); // 给浏览器注册窗口缩放事件 // 用户设置界面函数 var settings = function () { // 用户设置界面样式 GM_addStyle('.btzi-enabled .btzi-modal,.btzi-enabled .btzi-container{display:flex;}.btzi-modal,.btzi-container{position:fixed;top:0;left:0;display:none;width:100%;height:100%;}.btzi-modal{z-index:20211231;background-color:rgba(0,0,0,.7);}.btzi-container{z-index:20220101;justify-content:center;align-items:center;text-align:left;}.btzi-content{width:335px;border-radius:6px;background-color:#fff;}.btzi-header,.btzi-body,.btzi-footer{padding:11px;}.btzi-header{border-bottom:1px solid #e6ecf0;}.btzi-title{padding:0;margin:0;font:400 20px sans-serif;color:#000;text-align:center;}.btzi-group{padding:0;margin:0;margin-bottom:15px;border:0;}.btzi-legend,.btzi-controls,.btzi-select,.btzi-button{font:14px sans-serif;color:#000;}.btzi-legend{padding:5px 0;margin:0;float:left;width:81px;text-align:right;}.btzi-controls{margin-left:93px;clear:none;}.btzi-select{box-sizing:border-box;padding:4px;margin:0;width:180px;height:30px;border:1px solid #e6ecf0;border-radius:3px;appearance:auto;}.btzi-select:focus{outline:#f0f auto;}.btzi-footer{text-align:center;border-top:1px solid #e6ecf0;}.btzi-button{padding:9px 18px;border:0;border-radius:75px;font-weight:700;color:#fff;background:#4ab3f4;cursor:pointer;transition:box-shadow .17s ease-in-out;}.btzi-button:hover,.btzi-button:active{background:#1da1f2;}.btzi-button:focus{box-shadow:0 0 0 2px #fff,0 0 0 4px #a4d9f9;}.btzi-button:active{box-shadow:0 0 0 2px #fff,0 0 0 4px #4ab3f4;}'); var html, form, p, prop, KeyIndex, change; // 用户设置界面html html = '