您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Creates an adaptive canvas to enable zoom & pan for images on Danbooru (when viewing original) using scroll & drag, and click to set zoom to 1x/reset zoom
当前为
// ==UserScript== // @name Danbooru Smart Canvas // @namespace https://sleazyfork.org/ // @version 1 // @description Creates an adaptive canvas to enable zoom & pan for images on Danbooru (when viewing original) using scroll & drag, and click to set zoom to 1x/reset zoom // @author Broodyr // @license MIT // @match https://danbooru.donmai.us/posts/* // @run-at document-end // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // ==/UserScript== (function() { 'use strict'; // === Constants === const MAX_ZOOM_SCALE = 3; // 3 = 300% max zoom const ZOOM_FACTOR = 1.1; // 1.1 = 10% per scroll const RESET_CLICK_THRESHOLD = 5; // Max mouse movement (px) for a click to count as a reset const RESET_CLICK_DURATION = 200; // Max click duration (ms) for a click to count as a reset const TRANSITION = 'none'; // CSS transition for image panning/zooming //const TRANSITION = 'transform 0.1s ease-out'; // Causes slightly glitchy panning atm const CONTAINER_BG_COLOR = 'rgba(0, 0, 0, 0.05)'; // Background color for the image container ('rgba(0, 0, 0, 0)' for none) // Script options let isPanningEnabled = true; let panningToggleCommandId = null; if (typeof GM_getValue === 'function' && typeof GM_setValue === 'function') { isPanningEnabled = GM_getValue('panningEnabledUserSetting', true); } // Global observer to watch for full image if sample is shown let imgObserver = null; // Global references for resize handling let currentWrapper = null; let currentImageContainer = null; let currentImg = null; let currentZoomController = null; let currentZoomDisplayElement = null; function togglePanning() { isPanningEnabled = !isPanningEnabled; if (typeof GM_setValue === 'function') { GM_setValue('panningEnabledUserSetting', isPanningEnabled); } registerPanningMenuCommand(); if (!isPanningEnabled && currentWrapper) { currentWrapper.style.cursor = 'default'; } } function registerPanningMenuCommand() { if (typeof GM_registerMenuCommand === 'function') { const commandText = isPanningEnabled ? 'Disable Panning' : 'Enable Panning'; if (panningToggleCommandId !== null && typeof GM_unregisterMenuCommand === 'function') { try { GM_unregisterMenuCommand(panningToggleCommandId); } catch(e) { console.warn("Smart Canvas: Could not unregister old menu command:", e); } } panningToggleCommandId = GM_registerMenuCommand(commandText, togglePanning); } } // Adjusts layout and starting scale on window resize function handleResize() { if (!currentWrapper || !currentImageContainer || !currentImg) { return; } const header = document.querySelector('header#top'); const headerHeight = header ? header.offsetHeight : 0; const availableHeight = window.innerHeight - headerHeight - 32; currentImageContainer.style.maxHeight = availableHeight + 'px'; currentWrapper.style.height = availableHeight + 'px'; const newMinScale = Math.min( currentWrapper.clientWidth / currentImg.naturalWidth, currentWrapper.clientHeight / currentImg.naturalHeight ); const newStartScale = Math.min(newMinScale, 1); if (currentZoomController) { currentZoomController.destroy(); } currentZoomController = ZoomPanController(currentImg, currentWrapper, currentZoomDisplayElement, newStartScale); currentZoomController.resetZoom(); } // Manages image zoom and pan function ZoomPanController(img, wrapper, zoomDisplayElement, startScale = 1) { let scale = startScale; let posX = 0; let posY = 0; let isDragging = false; let lastX = 0; let lastY = 0; let clickStartTime = 0; let clickStartX = 0; let clickStartY = 0; img.style.transition = TRANSITION; // Keep image within wrapper boundaries function constrainPosition() { const containerWidth = wrapper.clientWidth; const containerHeight = wrapper.clientHeight; const imgWidth = img.offsetWidth * scale; const imgHeight = img.offsetHeight * scale; if (imgWidth <= containerWidth) { posX = Math.max(0, Math.min(posX, containerWidth - imgWidth)); } else { posX = Math.min(0, Math.max(posX, containerWidth - imgWidth)); } if (imgHeight <= containerHeight) { posY = Math.max(0, Math.min(posY, containerHeight - imgHeight)); } else { posY = Math.min(0, Math.max(posY, containerHeight - imgHeight)); } } function updateZoomDisplay() { if (zoomDisplayElement) { zoomDisplayElement.textContent = `x${scale.toFixed(1)}`; if (Math.abs(scale - 1.0) <= 0.001) { //zoomDisplayElement.style.boxShadow = '0 0 6px 3px rgba(0, 155, 230, 1)'; zoomDisplayElement.style.backgroundColor = 'rgba(0, 155, 230, 0.4)'; zoomDisplayElement.style.color = 'black'; zoomDisplayElement.style.fontWeight = 'bold'; zoomDisplayElement.style.textShadow = '0 0 8px white'; } else { //zoomDisplayElement.style.boxShadow = 'none'; zoomDisplayElement.style.backgroundColor = 'rgba(0, 0, 0, 0.3)'; zoomDisplayElement.style.color = 'white'; zoomDisplayElement.style.fontWeight = 'normal'; zoomDisplayElement.style.textShadow = 'unset'; } } } function applyTransform() { constrainPosition(); img.style.transform = `translate(${posX}px, ${posY}px) scale(${scale})`; img.style.transformOrigin = '0 0'; updateZoomDisplay(); } function resetZoom() { scale = startScale; const containerWidth = wrapper.clientWidth; const containerHeight = wrapper.clientHeight; const imgWidth = img.offsetWidth * scale; const imgHeight = img.offsetHeight * scale; posX = (containerWidth - imgWidth) / 2; posY = (containerHeight - imgHeight) / 2; applyTransform(); wrapper.style.cursor = 'default'; } // Event Handlers const handleWheel = (e) => { const rect = wrapper.getBoundingClientRect(); const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; const imgX = (mouseX - posX) / scale; const imgY = (mouseY - posY) / scale; const delta = -e.deltaY; let newScale = delta > 0 ? scale * ZOOM_FACTOR : scale / ZOOM_FACTOR; newScale = Math.max(startScale, Math.min(newScale, MAX_ZOOM_SCALE)); if (newScale !== scale) { e.preventDefault(); if (newScale == startScale) { resetZoom(); } else { scale = newScale; posX = mouseX - imgX * scale; posY = mouseY - imgY * scale; applyTransform(); if (isPanningEnabled) { wrapper.style.cursor = scale > startScale ? 'grab' : 'default'; } else { wrapper.style.cursor = 'default'; } } } }; const handleMouseDown = (e) => { if (e.button === 0) { clickStartTime = Date.now(); clickStartX = e.clientX; clickStartY = e.clientY; if (isPanningEnabled && scale > startScale) { isDragging = true; lastX = e.clientX; lastY = e.clientY; e.preventDefault(); wrapper.style.cursor = 'grabbing'; } } }; const handleMouseUp = (e) => { if (e.button === 0) { if (isDragging) isDragging = false; const isClick = ( Date.now() - clickStartTime < RESET_CLICK_DURATION && Math.abs(e.clientX - clickStartX) < RESET_CLICK_THRESHOLD && Math.abs(e.clientY - clickStartY) < RESET_CLICK_THRESHOLD ); if (isClick) { // If current scale is not ~1.00, set to 1.0, else reset image if (Math.abs(scale - 1.0) > 0.001) { const rect = wrapper.getBoundingClientRect(); const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; const imgX = (mouseX - posX) / scale; const imgY = (mouseY - posY) / scale; scale = 1.0; // Center if image fits in frame, else base position on mouse if (Math.abs(startScale - 1.0) <= 0.001) { posX = (wrapper.clientWidth - img.naturalWidth) / 2; posY = (wrapper.clientHeight - img.naturalHeight) / 2; } else { posX = mouseX - imgX * scale; posY = mouseY - imgY * scale; } applyTransform(); if (isPanningEnabled) { wrapper.style.cursor = scale > startScale ? 'grab' : 'default'; } else { wrapper.style.cursor = 'default'; } } else { resetZoom(); } } else { if (isPanningEnabled) { wrapper.style.cursor = scale > startScale ? 'grab' : 'default'; } else { wrapper.style.cursor = 'default'; } } } }; const handleMouseMove = (e) => { if (isDragging && scale > startScale) { const deltaX = e.clientX - lastX; const deltaY = e.clientY - lastY; posX += deltaX; posY += deltaY; lastX = e.clientX; lastY = e.clientY; applyTransform(); } }; wrapper.addEventListener('wheel', handleWheel); wrapper.addEventListener('mousedown', handleMouseDown); document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); function destroy() { wrapper.removeEventListener('wheel', handleWheel); wrapper.removeEventListener('mousedown', handleMouseDown); document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); } return { resetZoom, applyTransform, destroy }; } // Sets up the image, wrapper, and controller function initializeZoomPan() { const imageContainer = document.querySelector('section.image-container'); const img = document.querySelector('img#image'); if (!img || !imageContainer) return; currentImageContainer = imageContainer; currentImg = img; // Don't run if Danbooru's resize notice is active (image is sample) const resizeNotice = document.getElementById('image-resize-notice'); if (resizeNotice && getComputedStyle(resizeNotice).display !== 'none') return; const header = document.querySelector('header#top'); const headerHeight = header ? header.offsetHeight : 0; const availableHeight = window.innerHeight - headerHeight - 32; // Doesn't accommodate 'related posts' bar or notices, but figure a little scrolling is better than a smaller canvas imageContainer.style.maxHeight = availableHeight + 'px'; imageContainer.style.width = '100%'; imageContainer.style.overflow = 'hidden'; imageContainer.style.margin = '0 0 16px 0'; if (img.parentElement !== imageContainer) { imageContainer.appendChild(img); } const wrapper = document.createElement('div'); currentWrapper = wrapper; wrapper.className = 'danbooru-zoom-wrapper'; wrapper.style.position = 'relative'; wrapper.style.display = 'block'; wrapper.style.overflow = 'hidden'; wrapper.style.width = '100%'; wrapper.style.height = availableHeight + 'px'; wrapper.style.backgroundColor = CONTAINER_BG_COLOR; img.parentNode.insertBefore(wrapper, img); wrapper.appendChild(img); const zoomDisplayElement = document.createElement('div'); zoomDisplayElement.className = 'zoom-factor-display'; zoomDisplayElement.style.position = 'absolute'; zoomDisplayElement.style.top = '10px'; zoomDisplayElement.style.right = '10px'; zoomDisplayElement.style.padding = '5px 10px'; zoomDisplayElement.style.backgroundColor = 'rgba(0, 0, 0, 0.3)'; zoomDisplayElement.style.color = 'white'; zoomDisplayElement.style.borderRadius = '8px'; zoomDisplayElement.style.fontSize = '12px'; zoomDisplayElement.style.fontFamily = 'sans-serif'; zoomDisplayElement.style.zIndex = '100'; zoomDisplayElement.style.backdropFilter = 'blur(5px)'; zoomDisplayElement.style.webkitBackdropFilter = 'blur(5px)'; // For Safari zoomDisplayElement.style.border = '1px solid rgba(255, 255, 255, 0.1)'; wrapper.appendChild(zoomDisplayElement); currentZoomDisplayElement = zoomDisplayElement; img.style.position = 'absolute'; img.style.left = '0'; img.style.top = '0'; img.style.maxWidth = 'none'; img.style.maxHeight = 'none'; img.style.objectFit = ''; // Calculate initial scale and initialize controller function setupZoom() { img.style.width = img.naturalWidth + 'px'; img.style.height = img.naturalHeight + 'px'; const minScale = Math.min( wrapper.clientWidth / img.naturalWidth, wrapper.clientHeight / img.naturalHeight ); const startScale = Math.min(minScale, 1); const controller = ZoomPanController(img, wrapper, currentZoomDisplayElement, startScale); currentZoomController = controller; controller.resetZoom(); } if (img.complete) { setupZoom(); } else { img.addEventListener('load', setupZoom, { once: true }); } } // Initializes zoom and sets up observer for image changes function setupZoomAndObserver() { // Disconnect old observer if it exists if (imgObserver) { imgObserver.disconnect(); imgObserver = null; } initializeZoomPan(); // Observe 'src' attribute changes on the image to re-initialize when full image is loaded const resizeNotice = document.getElementById('image-resize-notice'); const img = document.querySelector('img#image'); if (img) { imgObserver = new MutationObserver(() => { if (!resizeNotice || getComputedStyle(resizeNotice).display === 'none') { setupZoomAndObserver(); } }); imgObserver.observe(img, { attributes: true, attributeFilter: ['src'] }); } } // Initial run setupZoomAndObserver(); // Handle window resize window.addEventListener('resize', handleResize); registerPanningMenuCommand(); })();