您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在 novelai.net 网站上添加一个浮动的图片工具箱,用于按特定规则裁剪和拼接图片。
// ==UserScript== // @name NovelAI Image Helper // @name:zh-CN NovelAI 局部放大重绘 // @version 3.1 // @description Adds a floating image toolkit on novelai.net for cropping and splicing images with specific constraints. // @description:zh-CN 在 novelai.net 网站上添加一个浮动的图片工具箱,用于按特定规则裁剪和拼接图片。 // @author axing // @match https://novelai.net/* // @grant GM_addStyle // @require https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.1/cropper.min.js // @namespace https://greasyfork.org/users/1285713 // ==/UserScript== (function() { 'use strict'; // 1. 注入 Cropper.js 的 CSS const cropperCSS = document.createElement('link'); cropperCSS.rel = 'stylesheet'; cropperCSS.href = 'https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.1/cropper.min.css'; document.head.appendChild(cropperCSS); // 2. 创建 HTML 结构 const panel = document.createElement('div'); panel.id = 'nai-helper-panel'; panel.innerHTML = ` <div id="nai-helper-header"> 🖼️ 图片助手 (可拖动) <button id="nai-helper-btn-minimize" title="最小化">_</button> </div> <div id="nai-helper-content"> <div class="nai-helper-image-container" id="nai-helper-container-a"> <p>图片A (原图): 将图片拖拽至此</p> <img id="nai-helper-image-a" style="display:none; max-width: 100%;"> <canvas id="nai-helper-canvas-a" style="display:none;"></canvas> </div> <div id="nai-helper-status-bar">请先将图片A拖拽至上方区域</div> <div class="nai-helper-controls"> <div class="nai-helper-control-group"> <label for="nai-helper-scale-input">缩放倍率:</label> <input type="number" id="nai-helper-scale-input" value="1.5" step="0.1" min="0.1"> <button id="nai-helper-btn-crop">✂️ 裁剪</button> </div> <div class="nai-helper-control-group"> <button id="nai-helper-btn-splice">🧩 拼接</button> <button id="nai-helper-btn-download" style="display:none;">📥 下载结果</button> </div> </div> <div class="nai-helper-image-row"> <div class="nai-helper-image-container-small" id="nai-helper-container-b"> <p>图片B (自行复制粘贴)</p> <canvas id="nai-helper-canvas-b"></canvas> </div> <div class="nai-helper-image-container-small" id="nai-helper-container-c"> <p>图片C (拼接目标)</p> <img id="nai-helper-image-c" style="display:none; max-width: 100%;"> <input type="file" id="nai-helper-file-c" class="nai-helper-file-input" accept="image/*"> </div> </div> </div> `; document.body.appendChild(panel); const restoreBtn = document.createElement('button'); restoreBtn.id = 'nai-helper-restore-btn'; restoreBtn.title = '展开图片助手'; restoreBtn.textContent = '🖼️'; document.body.appendChild(restoreBtn); // 3. 添加 CSS 样式 GM_addStyle(` #nai-helper-panel { position: fixed; width: 420px; background-color: #2c2c2c; border: 1px solid #555; border-radius: 8px; z-index: 99999; color: #eee; font-family: sans-serif; box-shadow: 0 4px 12px rgba(0,0,0,0.5); } #nai-helper-restore-btn { position: fixed; width: 48px; height: 48px; background-color: rgba(60, 70, 90, 0.9); border: 2px solid rgba(255, 255, 255, 0.4); border-radius: 50%; z-index: 99999; color: #eee; font-size: 24px; line-height: 44px; text-align: center; cursor: move; display: none; box-shadow: 0 3px 10px rgba(0,0,0,0.4); transition: transform 0.2s ease, background-color 0.2s ease; } #nai-helper-restore-btn:hover { background-color: rgba(80, 90, 110, 0.95); transform: scale(1.1); } #nai-helper-header { position: relative; padding: 10px; cursor: move; background-color: #3a3a3a; border-bottom: 1px solid #555; border-radius: 8px 8px 0 0; text-align: center; font-weight: bold; } #nai-helper-btn-minimize { position: absolute; top: 50%; left: 10px; /* <-- 修改处 */ transform: translateY(-50%); width: 24px; height: 24px; background: #505050; border: 1px solid #666; color: #ddd; font-weight: bold; line-height: 22px; text-align: center; cursor: pointer; border-radius: 4px; } #nai-helper-btn-minimize:hover { background: #656565; } #nai-helper-content { padding: 15px; display: flex; flex-direction: column; gap: 15px; } .nai-helper-image-container, .nai-helper-image-container-small { background-color: #1e1e1e; border: 2px dashed #444; border-radius: 5px; padding: 10px; text-align: center; position: relative; min-height: 100px; display: flex; align-items: center; justify-content: center; flex-direction: column; transition: border-color 0.2s; } .nai-helper-image-container p, .nai-helper-image-container-small p { color: #888; margin-bottom: 5px; } .nai-helper-image-container-small { min-height: 80px; } .nai-helper-image-row { display: flex; gap: 10px; } .nai-helper-image-row > div { flex: 1; } #nai-helper-panel img, #nai-helper-panel canvas { max-width: 100%; border-radius: 4px; } .nai-helper-file-input { position: absolute; top: 0; left: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; } .nai-helper-controls { display: flex; flex-direction: column; gap: 10px; } .nai-helper-control-group { display: flex; align-items: center; gap: 10px; } .nai-helper-control-group label { flex-shrink: 0; } #nai-helper-panel button { padding: 0; height: 30px; border: none; background-color: #5a5a5a; color: white; border-radius: 5px; cursor: pointer; flex-grow: 1; } #nai-helper-panel button:hover { background-color: #6a6a6a; } #nai-helper-panel input[type="number"] { width: 60px; padding: 5px; background-color: #1e1e1e; border: 1px solid #555; color: white; border-radius: 3px; } #nai-helper-status-bar { background-color: #1a1a1a; padding: 8px; text-align: center; border-radius: 4px; font-size: 0.9em; color: #aaa; } .cropper-view-box { outline-color: rgba(0, 173, 255, 0.75); } `); // 4. 变量和状态管理 const header = document.getElementById('nai-helper-header'); const btnMinimize = document.getElementById('nai-helper-btn-minimize'); const imgA = document.getElementById('nai-helper-image-a'); const canvasA = document.getElementById('nai-helper-canvas-a'); const containerA = document.getElementById('nai-helper-container-a'); const canvasB = document.getElementById('nai-helper-canvas-b'); const ctxB = canvasB.getContext('2d'); const imgC = document.getElementById('nai-helper-image-c'); const fileC = document.getElementById('nai-helper-file-c'); const containerC = document.getElementById('nai-helper-container-c'); const btnCrop = document.getElementById('nai-helper-btn-crop'); const btnSplice = document.getElementById('nai-helper-btn-splice'); const btnDownload = document.getElementById('nai-helper-btn-download'); const scaleInput = document.getElementById('nai-helper-scale-input'); const statusBar = document.getElementById('nai-helper-status-bar'); let cropper = null; let originalA_URL = null; let cropData = null; const roundTo64 = (num) => Math.round(num / 64) * 64; // 5. 功能实现 // 计算并设置初始位置 const initialTop = 50; const initialRight = 20; const initialLeft = window.innerWidth - panel.offsetWidth - initialRight; panel.style.top = `${initialTop}px`; panel.style.left = `${initialLeft}px`; restoreBtn.style.top = `${initialTop}px`; restoreBtn.style.left = `${initialLeft}px`; // 设置初始状态为最小化 panel.style.display = 'none'; restoreBtn.style.display = 'block'; makeDraggable(panel, header); makeDraggable(restoreBtn); btnMinimize.addEventListener('click', (e) => { e.stopPropagation(); const panelRect = panel.getBoundingClientRect(); restoreBtn.style.top = `${panelRect.top}px`; restoreBtn.style.left = `${panelRect.left}px`; panel.style.display = 'none'; restoreBtn.style.display = 'block'; }); restoreBtn.addEventListener('click', () => { const iconRect = restoreBtn.getBoundingClientRect(); panel.style.top = `${iconRect.top}px`; panel.style.left = `${iconRect.left}px`; panel.style.display = 'block'; restoreBtn.style.display = 'none'; }); setupImageLoader(containerA, null, (imgElement, dataURL) => { if (cropper) cropper.destroy(); imgA.src = dataURL; originalA_URL = dataURL; imgA.style.display = 'block'; canvasA.style.display = 'none'; btnDownload.style.display = 'none'; imgA.onload = () => { cropper = new Cropper(imgA, { viewMode: 1, dragMode: 'move', background: false, autoCropArea: 0.8, crop() { let data = cropper.getData(true); let snappedWidth = roundTo64(data.width); let snappedHeight = roundTo64(data.height); if (snappedWidth < 64) snappedWidth = 64; if (snappedHeight < 64) snappedHeight = 64; statusBar.textContent = `选区尺寸: ${snappedWidth} x ${snappedHeight}`; }, cropend() { let data = cropper.getData(true); let newX = roundTo64(data.x); let newY = roundTo64(data.y); let newWidth = roundTo64(data.width); let newHeight = roundTo64(data.height); if (newWidth < 64) newWidth = 64; if (newHeight < 64) newHeight = 64; cropper.setData({ x: newX, y: newY, width: newWidth, height: newHeight }); }, }); }; }); setupImageLoader(containerC, fileC, (imgElement, dataURL) => { imgC.src = dataURL; imgC.style.display = 'block'; statusBar.textContent = "图片C已加载。可以进行 [拼接] 操作"; }); btnCrop.addEventListener('click', () => { if (!cropper) return alert("请先加载图片A。"); cropData = cropper.getData(true); const scale = parseFloat(scaleInput.value) || 1.0; if (cropData.width === 0 || cropData.height === 0) return alert("裁剪区域无效。"); const croppedCanvas = cropper.getCroppedCanvas({ width: cropData.width, height: cropData.height }); const scaledWidth = roundTo64(cropData.width * scale); const scaledHeight = roundTo64(cropData.height * scale); canvasB.width = scaledWidth; canvasB.height = scaledHeight; ctxB.clearRect(0, 0, scaledWidth, scaledHeight); ctxB.drawImage(croppedCanvas, 0, 0, scaledWidth, scaledHeight); statusBar.textContent = `裁剪成功! 原尺寸: ${cropData.width}x${cropData.height}, 缩放后: ${scaledWidth}x${scaledHeight}`; }); btnSplice.addEventListener('click', () => { if (!originalA_URL || !cropData || !imgC.src) return alert("请确保图片A、B、C都已就绪。"); const originalImage = new Image(); originalImage.onload = () => { canvasA.width = originalImage.naturalWidth; canvasA.height = originalImage.naturalHeight; const ctxA = canvasA.getContext('2d'); ctxA.drawImage(originalImage, 0, 0); const tempCanvasC = document.createElement('canvas'); tempCanvasC.width = canvasB.width; tempCanvasC.height = canvasB.height; tempCanvasC.getContext('2d').drawImage(imgC, 0, 0, canvasB.width, canvasB.height); ctxA.drawImage(tempCanvasC, cropData.x, cropData.y, cropData.width, cropData.height); imgA.style.display = 'none'; if (cropper) cropper.destroy(); cropper = null; canvasA.style.display = 'block'; btnDownload.style.display = 'inline-block'; statusBar.textContent = "拼接完成!可以下载结果。"; }; originalImage.src = originalA_URL; }); btnDownload.addEventListener('click', () => { if (canvasA.style.display === 'none') return alert("没有可下载的拼接结果。"); const link = document.createElement('a'); link.download = 'spliced_image.png'; link.href = canvasA.toDataURL('image/png'); link.click(); }); function makeDraggable(element, handle) { let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; const dragHandle = handle || element; dragHandle.onmousedown = dragMouseDown; function dragMouseDown(e) { if (handle && e.target.id === 'nai-helper-btn-minimize') return; // Don't drag if clicking the minimize button e.preventDefault(); e.stopPropagation(); pos3 = e.clientX; pos4 = e.clientY; document.onmouseup = closeDragElement; document.onmousemove = elementDrag; } function elementDrag(e) { e.preventDefault(); e.stopPropagation(); pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY; pos3 = e.clientX; pos4 = e.clientY; element.style.top = `${element.offsetTop - pos2}px`; element.style.left = `${element.offsetLeft - pos1}px`; element.style.right = 'auto'; } function closeDragElement() { document.onmouseup = null; document.onmousemove = null; } } function setupImageLoader(container, fileInput, callback) { const dropZoneText = container.querySelector('p'); const handleFile = (file) => { if (file && file.type.startsWith('image/')) { const reader = new FileReader(); reader.onload = (e) => { const imgElement = container.querySelector('img'); callback(imgElement, e.target.result); if (container.id !== 'nai-helper-container-c' && dropZoneText) { dropZoneText.style.display = 'none'; } }; reader.readAsDataURL(file); } }; container.addEventListener('dragover', (e) => { e.preventDefault(); e.stopPropagation(); container.style.borderColor = '#00adff'; }); container.addEventListener('dragleave', (e) => { e.stopPropagation(); container.style.borderColor = '#444'; }); container.addEventListener('drop', (e) => { e.preventDefault(); e.stopPropagation(); container.style.borderColor = '#444'; if (e.dataTransfer.files.length > 0) handleFile(e.dataTransfer.files[0]); }); if (fileInput) { fileInput.addEventListener('change', (e) => { if (e.target.files.length > 0) handleFile(e.target.files[0]); }); } } })();