feat: enhance UI consistency, unified navigation middleware, and visual inset guides in Crop Tool (v1.2.0)
This commit is contained in:
@@ -91,6 +91,8 @@ SpriteTool 是一個整合式的 Web 應用程式,集合了四個專業級工
|
||||
**功能特性:**
|
||||
- **色彩去除** - 自訂底色(預設 #ff00ff 粉紅色)及去除閾值
|
||||
- **即時預覽** - 調整 threshold 時實時顯示去底色效果
|
||||
- **預覽底色檢查** - 可修改預覽背景顏色以檢查邊界精度(純檢查用,不影響導出)
|
||||
- **內縮虛線框** - 在預覽上顯示每個格子的內縮邊界(粉紅色虛線框),視覺化檢查內縮效果
|
||||
- **智慧裁切** - 根據行列數和內縮距離精確分割每格
|
||||
- **縮放還原** - 內縮後自動放大回原格尺寸
|
||||
- **批量輸出** - 導出個別格子 PNG + 組合完整圖
|
||||
@@ -278,6 +280,8 @@ SpriteTool/
|
||||
## 🎨 UI/UX 特性
|
||||
|
||||
- **深色主題** - 護眼的深灰配色
|
||||
- **統一導覽體驗** - 透過 FastAPI 中間件在所有工具頁面自動注入導覽列
|
||||
- **標準化佈局** - 所有工具採用一致的寬螢幕佈局(Max-width 7xl)與標題樣式
|
||||
- **響應式設計** - 適配桌面和平板
|
||||
- **即時預覽** - 無延遲的視覺反饋
|
||||
- **拖拽交互** - 直觀的圖像操作
|
||||
@@ -333,4 +337,4 @@ SpriteTool/
|
||||
---
|
||||
|
||||
**最後更新**: 2026 年 5 月
|
||||
**版本**: 1.1.0
|
||||
**版本**: 1.2.0
|
||||
|
||||
@@ -89,6 +89,8 @@ Remove Chroma Key background and perform intelligent inset cropping and enlargem
|
||||
**Key Features:**
|
||||
- **Color Removal** - Custom chroma key (default #ff00ff pink) and threshold adjustment.
|
||||
- **Real-time Preview** - Instantly view background removal effects while adjusting threshold.
|
||||
- **Preview Background Customization** - Change preview background color to check edge precision (inspection only, does not affect export).
|
||||
- **Inset Guide Lines** - Display dashed lines showing each cell's inset boundary (pink dashed outline) for visual verification of inset effects.
|
||||
- **Smart Cropping** - Precisely divide cells based on rows/columns and inset distance.
|
||||
- **Enlarge After Crop** - Automatically enlarge back to original cell size after inset cropping.
|
||||
- **Batch Export** - Export individual cell PNGs + combined full image.
|
||||
@@ -273,6 +275,8 @@ SpriteTool/
|
||||
## 🎨 UI/UX Features
|
||||
|
||||
- **Dark Theme** - Eye-friendly dark gray color scheme.
|
||||
- **Unified Navigation** - Navigation bar is automatically injected into all tool pages via FastAPI middleware.
|
||||
- **Standardized Layout** - Consistent wide-screen layout (Max-width 7xl) and header styling across all tools.
|
||||
- **Responsive Design** - Optimized for desktop and tablets.
|
||||
- **Instant Preview** - Lag-free visual feedback.
|
||||
- **Drag-and-Drop Interaction** - Intuitive image manipulation.
|
||||
@@ -328,4 +332,4 @@ Issues and Pull Requests are welcome!
|
||||
---
|
||||
|
||||
**Last Updated**: May 2026
|
||||
**Version**: 1.1.0
|
||||
**Version**: 1.2.0
|
||||
|
||||
@@ -61,10 +61,13 @@ HTML = """<!DOCTYPE html>
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-slate-900 text-white min-h-screen p-6">
|
||||
<div class="max-w-7xl mx-auto space-y-4">
|
||||
<body class="bg-slate-900 text-white">
|
||||
<div class="max-w-7xl mx-auto space-y-6 p-6 pt-20">
|
||||
|
||||
<h1 class="text-2xl font-bold border-b border-slate-600 pb-3">Inset Crop Tool</h1>
|
||||
<header class="border-b border-slate-700 pb-2">
|
||||
<h1 class="text-xl font-bold">✂️ Inset Crop</h1>
|
||||
<p class="text-slate-400 text-sm mt-2">移除背景色並內縮邊界,自動切割 Sprite Sheet 為個別方塊</p>
|
||||
</header>
|
||||
|
||||
<!-- ── Top: Controls ── -->
|
||||
<div class="bg-slate-800 rounded-2xl p-5">
|
||||
@@ -100,25 +103,15 @@ HTML = """<!DOCTYPE html>
|
||||
|
||||
<div class="w-px self-stretch bg-slate-600"></div>
|
||||
|
||||
<!-- 裁切 -->
|
||||
<!-- 預覽底色 -->
|
||||
<div class="space-y-1">
|
||||
<p class="text-xs font-semibold text-blue-300">裁切設定</p>
|
||||
<p class="text-xs font-semibold text-cyan-300">預覽底色(檢查用)</p>
|
||||
<div class="flex gap-2 items-center">
|
||||
<div>
|
||||
<label class="block text-xs text-slate-400 mb-1">Columns</label>
|
||||
<input id="colsInput" type="number" value="4" min="1"
|
||||
class="w-20 p-2 rounded-lg bg-slate-700 border border-slate-600 text-white text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-slate-400 mb-1">Rows</label>
|
||||
<input id="rowsInput" type="number" value="6" min="1"
|
||||
class="w-20 p-2 rounded-lg bg-slate-700 border border-slate-600 text-white text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-slate-400 mb-1">內縮 (px)</label>
|
||||
<input id="insetInput" type="number" value="2" min="0"
|
||||
class="w-20 p-2 rounded-lg bg-slate-700 border border-slate-600 text-white text-sm">
|
||||
</div>
|
||||
<input id="previewColorPicker" type="color" value="#333333"
|
||||
class="w-10 h-9 rounded cursor-pointer border-0 p-0 bg-transparent">
|
||||
<input id="previewColorHex" type="text" value="#333333" maxlength="7"
|
||||
class="w-24 p-2 rounded-lg bg-slate-700 border border-slate-600 text-white font-mono text-sm">
|
||||
<span class="text-xs text-slate-400">僅用於檢查,不影響匯出</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -141,26 +134,53 @@ HTML = """<!DOCTYPE html>
|
||||
|
||||
<div class="bg-slate-800 rounded-2xl p-4 space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-sm font-semibold text-pink-300">去底色預覽</p>
|
||||
<p class="text-sm font-semibold text-cyan-300">去底色預覽(底色檢查用 + 內縮虛線)</p>
|
||||
<span id="previewLoading" class="hidden text-xs text-slate-400 animate-pulse">更新中…</span>
|
||||
</div>
|
||||
<div class="checker flex items-center justify-center rounded-lg min-h-80 overflow-auto">
|
||||
<div id="previewBg" class="relative flex items-center justify-center rounded-lg min-h-80 overflow-auto" style="background-color: #333333;">
|
||||
<img id="bgImg" class="hidden max-w-full object-contain" style="image-rendering:pixelated">
|
||||
<canvas id="insetCanvas" class="hidden absolute top-0 left-0" style="image-rendering:pixelated;"></canvas>
|
||||
<p id="bgPH" class="text-slate-500 text-sm">預覽將在此顯示</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ── Bottom: Download ── -->
|
||||
<div class="bg-slate-800 rounded-2xl p-4 flex items-center gap-4">
|
||||
<button id="downloadBtn" disabled onclick="doProcess()"
|
||||
class="px-10 py-3 bg-blue-600 hover:bg-blue-500
|
||||
disabled:bg-slate-600 disabled:cursor-not-allowed
|
||||
rounded-xl font-bold transition text-white">
|
||||
⬇ 確認並下載 ZIP
|
||||
</button>
|
||||
<div id="status" class="hidden text-sm text-slate-400"></div>
|
||||
<!-- ── Bottom: Crop Settings & Download ── -->
|
||||
<div class="bg-slate-800 rounded-2xl p-4 flex items-center justify-between gap-6">
|
||||
|
||||
<!-- 左邊:裁切設定 -->
|
||||
<div class="space-y-1">
|
||||
<p class="text-xs font-semibold text-blue-300">裁切設定</p>
|
||||
<div class="flex gap-2 items-center">
|
||||
<div>
|
||||
<label class="block text-xs text-slate-400 mb-1">Columns</label>
|
||||
<input id="colsInput" type="number" value="4" min="1"
|
||||
class="w-20 p-2 rounded-lg bg-slate-700 border border-slate-600 text-white text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-slate-400 mb-1">Rows</label>
|
||||
<input id="rowsInput" type="number" value="6" min="1"
|
||||
class="w-20 p-2 rounded-lg bg-slate-700 border border-slate-600 text-white text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-slate-400 mb-1">內縮 (px)</label>
|
||||
<input id="insetInput" type="number" value="2" min="0"
|
||||
class="w-20 p-2 rounded-lg bg-slate-700 border border-slate-600 text-white text-sm">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右邊:下載按鈕 -->
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<button id="downloadBtn" disabled onclick="doProcess()"
|
||||
class="px-10 py-3 bg-blue-600 hover:bg-blue-500
|
||||
disabled:bg-slate-600 disabled:cursor-not-allowed
|
||||
rounded-xl font-bold transition text-white whitespace-nowrap">
|
||||
⬇ 確認並下載 ZIP
|
||||
</button>
|
||||
<div id="status" class="hidden text-sm text-slate-400"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -175,8 +195,12 @@ const origImg = document.getElementById('origImg');
|
||||
const origPH = document.getElementById('origPH');
|
||||
const bgImg = document.getElementById('bgImg');
|
||||
const bgPH = document.getElementById('bgPH');
|
||||
const insetCanvas = document.getElementById('insetCanvas');
|
||||
const previewBg = document.getElementById('previewBg');
|
||||
const colorPicker = document.getElementById('colorPicker');
|
||||
const colorHex = document.getElementById('colorHex');
|
||||
const previewColorPicker = document.getElementById('previewColorPicker');
|
||||
const previewColorHex = document.getElementById('previewColorHex');
|
||||
const threshRange = document.getElementById('threshRange');
|
||||
const threshNum = document.getElementById('threshNum');
|
||||
const threshLabel = document.getElementById('threshLabel');
|
||||
@@ -185,6 +209,19 @@ const statusDiv = document.getElementById('status');
|
||||
const downloadBtn = document.getElementById('downloadBtn');
|
||||
const previewLoading = document.getElementById('previewLoading');
|
||||
|
||||
// ── Preview color picker <-> hex ─────────────
|
||||
previewColorPicker.addEventListener('input', () => {
|
||||
previewColorHex.value = previewColorPicker.value;
|
||||
previewBg.style.backgroundColor = previewColorPicker.value;
|
||||
});
|
||||
previewColorHex.addEventListener('change', () => {
|
||||
const v = previewColorHex.value.trim();
|
||||
if (/^#[0-9a-fA-F]{6}$/.test(v)) {
|
||||
previewColorPicker.value = v;
|
||||
previewBg.style.backgroundColor = v;
|
||||
}
|
||||
});
|
||||
|
||||
// ── Color picker <-> hex ─────────────────────
|
||||
colorPicker.addEventListener('input', () => {
|
||||
colorHex.value = colorPicker.value;
|
||||
@@ -224,7 +261,59 @@ fileInput.addEventListener('change', () => {
|
||||
|
||||
origImg.addEventListener('load', updateInfo);
|
||||
['colsInput','rowsInput','insetInput'].forEach(id =>
|
||||
document.getElementById(id).addEventListener('input', updateInfo));
|
||||
document.getElementById(id).addEventListener('input', () => {
|
||||
updateInfo();
|
||||
drawInsetGrid();
|
||||
}));
|
||||
|
||||
// ── Draw inset grid on canvas ────────────────
|
||||
function drawInsetGrid() {
|
||||
if (!bgImg.naturalWidth || bgImg.classList.contains('hidden')) return;
|
||||
|
||||
const cols = +document.getElementById('colsInput').value || 1;
|
||||
const rows = +document.getElementById('rowsInput').value || 1;
|
||||
const inset = +document.getElementById('insetInput').value || 0;
|
||||
|
||||
const imgW = bgImg.naturalWidth;
|
||||
const imgH = bgImg.naturalHeight;
|
||||
const tileW = Math.floor(imgW / cols);
|
||||
const tileH = Math.floor(imgH / rows);
|
||||
|
||||
// 設置Canvas的繪製解析度(邏輯像素)
|
||||
insetCanvas.width = imgW;
|
||||
insetCanvas.height = imgH;
|
||||
|
||||
// 計算bgImg的實際顯示大小
|
||||
const displayW = bgImg.offsetWidth;
|
||||
const displayH = bgImg.offsetHeight;
|
||||
const offsetLeft = bgImg.offsetLeft;
|
||||
const offsetTop = bgImg.offsetTop;
|
||||
|
||||
// 設置Canvas的CSS顯示大小和位置以匹配bgImg
|
||||
insetCanvas.style.width = displayW + 'px';
|
||||
insetCanvas.style.height = displayH + 'px';
|
||||
insetCanvas.style.left = offsetLeft + 'px';
|
||||
insetCanvas.style.top = offsetTop + 'px';
|
||||
|
||||
const ctx = insetCanvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, imgW, imgH);
|
||||
ctx.strokeStyle = 'rgba(255, 100, 200, 0.7)'; // 粉紅色虛線
|
||||
ctx.setLineDash([4, 4]);
|
||||
ctx.lineWidth = 1.5;
|
||||
|
||||
// 對每個格子繪製內縮邊界框
|
||||
for (let r = 0; r < rows; r++) {
|
||||
for (let c = 0; c < cols; c++) {
|
||||
const x0 = c * tileW;
|
||||
const y0 = r * tileH;
|
||||
|
||||
// 繪製內縮矩形框
|
||||
ctx.strokeRect(x0 + inset, y0 + inset, tileW - inset * 2, tileH - inset * 2);
|
||||
}
|
||||
}
|
||||
|
||||
insetCanvas.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function updateInfo() {
|
||||
if (!currentFile || !origImg.naturalWidth) return;
|
||||
@@ -266,6 +355,9 @@ async function doPreview() {
|
||||
bgImg.src = previewObjectUrl;
|
||||
bgImg.classList.remove('hidden');
|
||||
bgPH.classList.add('hidden');
|
||||
bgImg.onload = () => {
|
||||
drawInsetGrid();
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
|
||||
@@ -19,13 +19,14 @@ HTML = '''<!DOCTYPE html>
|
||||
::-webkit-scrollbar-thumb { background: #475569; border-radius: 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-slate-900 text-white p-6">
|
||||
<div class="max-w-4xl mx-auto space-y-6">
|
||||
<body class="bg-slate-900 text-white">
|
||||
<div class="max-w-7xl mx-auto space-y-6 p-6 pt-20">
|
||||
<header class="border-b border-slate-700 pb-2">
|
||||
<h1 class="text-xl font-bold">🔄 Flipper</h1>
|
||||
<p class="text-slate-400 text-sm mt-2">將 Sprite Sheet 中每個格子水平翻轉(左右鏡像),並輸出新圖片</p>
|
||||
</header>
|
||||
|
||||
<div class="bg-slate-800 p-6 rounded-2xl border border-slate-700">
|
||||
<h1 class="text-2xl font-bold text-blue-400 mb-1">🔄 Sprite Flipper</h1>
|
||||
<p class="text-slate-400 text-sm mb-6">將 Sprite Sheet 中每個格子水平翻轉(左右鏡像),並輸出新圖片。</p>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<div>
|
||||
<label class="block text-xs text-slate-400 mb-1">Columns(列數)</label>
|
||||
|
||||
@@ -24,11 +24,11 @@ HTML = '''<!DOCTYPE html>
|
||||
::-webkit-scrollbar-thumb { background: #475569; border-radius: 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-slate-900 text-white p-4">
|
||||
<div class="max-w-4xl mx-auto space-y-6">
|
||||
<header class="border-b border-slate-700 pb-4">
|
||||
<h1 class="text-3xl font-bold text-blue-400">Shiny Monster Maker Pro</h1>
|
||||
<p class="text-slate-400">全維度色違調整工具 (HSV 模式)</p>
|
||||
<body class="bg-slate-900 text-white">
|
||||
<div class="max-w-7xl mx-auto space-y-6 p-4 pt-20">
|
||||
<header class="border-b border-slate-700 pb-2">
|
||||
<h1 class="text-xl font-bold">✨ Shiny Maker</h1>
|
||||
<p class="text-slate-400 text-sm mt-2">全維度色違調整工具,支援 HSV 色相、飽和度、亮度變化</p>
|
||||
</header>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
|
||||
@@ -14,10 +14,12 @@ HTML = """
|
||||
<title>Sprite Merger</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body class="bg-slate-900 text-white min-h-screen p-8">
|
||||
<div class="max-w-xl mx-auto space-y-6">
|
||||
<h1 class="text-2xl font-bold text-blue-400">🧩 Sprite Merger</h1>
|
||||
<p class="text-slate-400 text-sm">將來源圖片 merge 進主圖的指定 tile 座標</p>
|
||||
<body class="bg-slate-900 text-white">
|
||||
<div class="max-w-7xl mx-auto space-y-6 p-8 pt-20">
|
||||
<header class="border-b border-slate-700 pb-2">
|
||||
<h1 class="text-xl font-bold">🧩 Merger</h1>
|
||||
<p class="text-slate-400 text-sm mt-2">將來源圖片 merge 進主圖的指定 tile 座標</p>
|
||||
</header>
|
||||
|
||||
<form id="mergeForm" class="space-y-4">
|
||||
<div>
|
||||
|
||||
@@ -21,10 +21,15 @@ HTML = '''<!DOCTYPE html>
|
||||
::-webkit-scrollbar-thumb { background: #475569; border-radius: 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-slate-900 text-white p-4">
|
||||
<div class="max-w-7xl mx-auto grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||
<body class="bg-slate-900 text-white">
|
||||
<div class="max-w-7xl mx-auto p-4 pt-20 space-y-6">
|
||||
<header class="border-b border-slate-700 pb-2">
|
||||
<h1 class="text-xl font-bold">📏 Grid Tool</h1>
|
||||
<p class="text-slate-400 text-sm mt-2">定義網格區域,批量選取多個 Tile 範圍</p>
|
||||
</header>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||
<div class="bg-slate-800 p-4 rounded-2xl space-y-2">
|
||||
<h1 class="text-xl font-bold">Sprite Tool Suite</h1>
|
||||
<div class="bg-blue-900/50 p-3 rounded-lg border border-blue-700/50">
|
||||
<p class="text-sm font-semibold text-blue-200">Total Tiles: <span id="totalCount" class="text-xl text-white">0</span></p>
|
||||
</div>
|
||||
|
||||
@@ -22,11 +22,15 @@ HTML = '''<!DOCTYPE html>
|
||||
::-webkit-scrollbar-thumb { background: #475569; border-radius: 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-slate-900 text-white p-4">
|
||||
<div class="max-w-7xl mx-auto grid grid-cols-1 lg:grid-cols-4 gap-4">
|
||||
<div class="lg:col-span-1 bg-slate-800 p-4 rounded-2xl space-y-4">
|
||||
<h1 class="text-xl font-bold border-b border-slate-700 pb-2">Sprite Picker</h1>
|
||||
<body class="bg-slate-900 text-white">
|
||||
<div class="max-w-7xl mx-auto p-4 pt-20 space-y-6">
|
||||
<header class="border-b border-slate-700 pb-2">
|
||||
<h1 class="text-xl font-bold">🎯 Picker Tool</h1>
|
||||
<p class="text-slate-400 text-sm mt-2">選取並導出單一 Sprite,支援調整尺寸與排列</p>
|
||||
</header>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-4 gap-4">
|
||||
<div class="lg:col-span-1 bg-slate-800 p-4 rounded-2xl space-y-4">
|
||||
<div class="bg-blue-900/30 p-3 rounded-lg border border-blue-700/50">
|
||||
<p class="text-xs text-blue-200 mb-1">Total Selected</p>
|
||||
<span id="totalCount" class="text-2xl font-bold">0</span>
|
||||
|
||||
9
tool.py
9
tool.py
@@ -32,11 +32,7 @@ class NavbarMiddleware(BaseHTTPMiddleware):
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
response = await call_next(request)
|
||||
|
||||
# 排除首頁,避免重複注入
|
||||
if request.url.path == "/":
|
||||
return response
|
||||
|
||||
# 針對所有子工具的 HTML 回傳進行注入
|
||||
# 針對所有 HTML 回傳進行注入
|
||||
if "text/html" in response.headers.get("content-type", ""):
|
||||
body = b""
|
||||
async for chunk in response.body_iterator:
|
||||
@@ -73,8 +69,7 @@ async def index():
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body class="bg-slate-900 text-white">
|
||||
{NAVBAR_HTML}
|
||||
<div class="flex flex-col items-center justify-center min-h-[80vh] space-y-6">
|
||||
<div class="flex flex-col items-center justify-center min-h-[80vh] space-y-6 pt-20">
|
||||
<h1 class="text-4xl font-bold text-blue-400">Game Developer Tool Suite</h1>
|
||||
<p class="text-slate-400">請從上方導覽列選擇要使用的工具</p>
|
||||
<div class="grid grid-cols-4 gap-6 pt-10">
|
||||
|
||||
Reference in New Issue
Block a user