Files
InfoGenie/InfoGenie-frontend/public/toolbox/图片处理/GIF拆帧/index.html
2026-03-28 20:59:52 +08:00

419 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>帧拆 - GIF & 图片拆帧工具</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/gif-frames@1.0.1?main=bundled"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css">
<style>
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&amp;display=swap');
body {
font-family: 'Noto Sans SC', system-ui, sans-serif;
}
.upload-zone {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.upload-zone:hover {
transform: translateY(-4px);
box-shadow: 0 20px 25px -5px rgb(16 185 129 / 0.1), 0 8px 10px -6px rgb(16 185 129 / 0.1);
}
.frame-card {
transition: all 0.2s cubic-bezier(0.4, 0.0, 0.2, 1);
}
.frame-card:hover {
transform: scale(1.03);
box-shadow: 0 10px 15px -3px rgb(16 185 129 / 0.15);
}
.tail-container {
max-width: 1280px;
margin: 0 auto;
}
</style>
</head>
<body class="bg-gradient-to-br from-emerald-50 via-white to-teal-50 min-h-screen">
<div class="tail-container p-4 md:p-8">
<!-- 头部 -->
<header class="flex items-center justify-between mb-10">
<div class="flex items-center gap-3">
<div class="w-11 h-11 bg-emerald-500 rounded-2xl flex items-center justify-center text-white text-3xl shadow-lg">
🖼️
</div>
<div>
<h1 class="text-3xl font-bold text-emerald-900 tracking-tight">帧拆</h1>
<p class="text-emerald-600 text-sm -mt-1">GIF & 图片拆帧工具</p>
</div>
</div>
<div class="flex items-center gap-4 text-sm">
<div onclick="resetAll()"
class="flex items-center gap-2 px-5 py-2.5 bg-white hover:bg-emerald-50 border border-emerald-200 rounded-3xl text-emerald-700 font-medium cursor-pointer transition-colors">
<i class="fa-solid fa-rotate"></i>
<span>重新开始</span>
</div>
</div>
</header>
<div class="max-w-5xl mx-auto">
<!-- 上传区域 -->
<div id="upload-section"
class="upload-zone bg-white border-4 border-dashed border-emerald-200 rounded-3xl p-12 text-center cursor-pointer hover:border-emerald-400">
<div class="mx-auto w-20 h-20 bg-emerald-100 rounded-2xl flex items-center justify-center mb-6">
<i class="fa-solid fa-cloud-arrow-up text-5xl text-emerald-500"></i>
</div>
<h2 class="text-2xl font-semibold text-emerald-900 mb-2">拖拽图片或 GIF 到此处</h2>
<p class="text-emerald-600 mb-6">或点击上传 • 支持 JPG、PNG、GIF、WebP</p>
<button onclick="document.getElementById('file-input').click()"
class="px-8 py-4 bg-emerald-600 hover:bg-emerald-700 text-white font-medium rounded-3xl shadow-md flex items-center gap-3 mx-auto transition-all">
<i class="fa-solid fa-upload"></i>
选择文件
</button>
<input type="file" id="file-input" accept="image/*" class="hidden" onchange="handleFileSelect(event)">
</div>
<!-- 主内容区 -->
<div id="main-section" class="hidden">
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8">
<!-- 左侧:原始预览 -->
<div class="lg:col-span-5 bg-white rounded-3xl shadow-sm p-6 border border-emerald-100">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-2">
<i class="fa-solid fa-image text-emerald-500"></i>
<h3 class="font-semibold text-emerald-900">原始图片</h3>
</div>
<span id="file-info" class="text-xs text-emerald-500 font-mono"></span>
</div>
<div class="aspect-video bg-emerald-50 rounded-2xl overflow-hidden flex items-center justify-center border border-emerald-100">
<img id="original-preview"
class="max-h-full max-w-full object-contain"
alt="原始图片">
</div>
<div class="mt-6 text-center">
<button onclick="startExtract()"
id="extract-btn"
class="w-full py-4 bg-gradient-to-r from-emerald-500 to-teal-500 hover:from-emerald-600 hover:to-teal-600 text-white font-semibold rounded-3xl shadow-lg flex items-center justify-center gap-3 transition-all">
<i class="fa-solid fa-scissors"></i>
开始拆解成普通图片帧
</button>
</div>
</div>
<!-- 右侧:帧结果 -->
<div class="lg:col-span-7 bg-white rounded-3xl shadow-sm p-6 border border-emerald-100">
<div class="flex items-center justify-between mb-6">
<div class="flex items-center gap-3">
<i class="fa-solid fa-layer-group text-emerald-500"></i>
<h3 class="font-semibold text-emerald-900">拆解结果</h3>
<span id="frame-count-badge"
class="px-3 py-1 bg-emerald-100 text-emerald-700 text-xs font-medium rounded-3xl">0 帧</span>
</div>
<button onclick="downloadAllZip()"
id="download-all-btn"
class="hidden px-6 py-2 bg-white border border-emerald-300 hover:border-emerald-400 text-emerald-700 rounded-3xl text-sm font-medium flex items-center gap-2">
<i class="fa-solid fa-download"></i>
下载全部 (ZIP)
</button>
</div>
<!-- 加载中 -->
<div id="loading" class="hidden py-20 flex flex-col items-center justify-center">
<div class="w-14 h-14 border-4 border-emerald-200 border-t-emerald-500 rounded-full animate-spin"></div>
<p class="mt-6 text-emerald-600 font-medium">正在拆解 GIF 帧...</p>
<p class="text-xs text-emerald-400 mt-1">大文件可能需要几秒</p>
</div>
<!-- 帧网格 -->
<div id="frames-grid"
class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4 min-h-[400px]">
<!-- JS 动态插入 -->
</div>
</div>
</div>
</div>
</div>
<!-- 底部说明 -->
<div class="mt-16 text-center text-emerald-400 text-xs flex items-center justify-center gap-6">
<div class="flex items-center gap-1">
<i class="fa-solid fa-shield-halved"></i>
<span>完全本地处理 • 隐私安全</span>
</div>
<div>适配电脑 & 手机</div>
<div class="flex items-center gap-1">
<i class="fa-solid fa-leaf"></i>
<span>简洁清新设计</span>
</div>
</div>
</div>
<script>
// Tailwind 配置(可选美化)
function initTailwind() {
// 已通过 CDN 处理
}
let currentFile = null;
let currentPreviewUrl = null;
let frameDataUrls = [];
const uploadSection = document.getElementById('upload-section');
const mainSection = document.getElementById('main-section');
const originalPreview = document.getElementById('original-preview');
const fileInfo = document.getElementById('file-info');
const framesGrid = document.getElementById('frames-grid');
const frameCountBadge = document.getElementById('frame-count-badge');
const downloadAllBtn = document.getElementById('download-all-btn');
const loadingEl = document.getElementById('loading');
const extractBtn = document.getElementById('extract-btn');
// 拖拽支持
function setupDragDrop() {
const zone = uploadSection;
zone.addEventListener('dragover', (e) => {
e.preventDefault();
zone.style.borderColor = '#10b981';
zone.style.backgroundColor = '#ecfdf5';
});
zone.addEventListener('dragleave', () => {
zone.style.borderColor = '#a7f3d0';
zone.style.backgroundColor = '';
});
zone.addEventListener('drop', (e) => {
e.preventDefault();
zone.style.borderColor = '#a7f3d0';
zone.style.backgroundColor = '';
const file = e.dataTransfer.files[0];
if (file) handleFile(file);
});
}
function handleFileSelect(e) {
const file = e.target.files[0];
if (file) handleFile(file);
}
function handleFile(file) {
if (!file.type.startsWith('image/')) {
alert('请上传图片或 GIF 文件!');
return;
}
currentFile = file;
currentPreviewUrl = URL.createObjectURL(file);
// 显示主界面
uploadSection.classList.add('hidden');
mainSection.classList.remove('hidden');
// 预览原始图片
originalPreview.src = currentPreviewUrl;
// 文件信息
const sizeMB = (file.size / (1024 * 1024)).toFixed(2);
fileInfo.textContent = `${file.name}${sizeMB} MB`;
// 重置帧
resetFrames();
// 自动高亮按钮
extractBtn.classList.add('ring-4', 'ring-emerald-200');
setTimeout(() => {
extractBtn.classList.remove('ring-4', 'ring-emerald-200');
}, 1500);
}
function resetFrames() {
framesGrid.innerHTML = '';
frameDataUrls = [];
frameCountBadge.textContent = '0 帧';
downloadAllBtn.classList.add('hidden');
}
async function startExtract() {
if (!currentFile) return;
resetFrames();
loadingEl.classList.remove('hidden');
extractBtn.disabled = true;
extractBtn.innerHTML = `<i class="fa-solid fa-spinner fa-spin"></i> 拆解中...`;
try {
if (currentFile.type === 'image/gif') {
await extractGifFrames();
} else {
await extractSingleImage();
}
} catch (err) {
console.error(err);
alert('拆解失败,请尝试其他图片。\n错误' + err.message);
} finally {
loadingEl.classList.add('hidden');
extractBtn.disabled = false;
extractBtn.innerHTML = `<i class="fa-solid fa-scissors"></i> 重新拆解`;
}
}
// 提取单张普通图片
async function extractSingleImage() {
const img = new Image();
img.onload = function() {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const dataUrl = canvas.toDataURL('image/png', 1.0);
frameDataUrls = [{
index: 0,
url: dataUrl,
isSingle: true
}];
addFrameToUI(0, dataUrl, true);
updateUIAfterExtract(1);
};
img.src = currentPreviewUrl;
}
// 使用 gif-frames 提取 GIF 帧
async function extractGifFrames() {
const objectUrl = URL.createObjectURL(currentFile);
try {
const frames = await gifFrames({
url: objectUrl,
frames: 'all',
outputType: 'canvas',
cumulative: true // 合成完整帧,显示效果更好
});
frameDataUrls = [];
for (let i = 0; i < frames.length; i++) {
const frame = frames[i];
const canvas = frame.getImage();
const dataUrl = canvas.toDataURL('image/png', 1.0);
frameDataUrls.push({
index: i,
url: dataUrl
});
addFrameToUI(i, dataUrl, false);
}
updateUIAfterExtract(frames.length);
} finally {
URL.revokeObjectURL(objectUrl);
}
}
function addFrameToUI(index, dataUrl, isSingle) {
const col = document.createElement('div');
col.className = `frame-card bg-white rounded-2xl overflow-hidden border border-emerald-100 shadow-sm`;
const num = (index + 1).toString().padStart(2, '0');
const label = isSingle ? '唯一帧' : `${num}`;
col.innerHTML = `
<div class="relative">
<img src="${dataUrl}"
class="w-full aspect-square object-contain bg-emerald-50 p-3">
<div class="absolute top-3 left-3 bg-white/90 backdrop-blur px-3 py-1 text-[10px] font-mono font-bold text-emerald-700 rounded-2xl shadow">
${label}
</div>
</div>
<div class="p-3 flex justify-between items-center">
<button onclick="downloadSingleFrame(${index})"
class="flex-1 py-2 text-xs font-medium text-emerald-700 hover:bg-emerald-50 rounded-2xl flex items-center justify-center gap-1">
<i class="fa-solid fa-download"></i>
下载 PNG
</button>
</div>
`;
framesGrid.appendChild(col);
}
function updateUIAfterExtract(count) {
frameCountBadge.textContent = `${count}`;
downloadAllBtn.classList.remove('hidden');
}
function downloadSingleFrame(index) {
const frame = frameDataUrls[index];
if (!frame) return;
const link = document.createElement('a');
link.href = frame.url;
link.download = `帧_${(index + 1).toString().padStart(3, '0')}.png`;
link.click();
}
async function downloadAllZip() {
if (frameDataUrls.length === 0) return;
const zip = new JSZip();
const folderName = currentFile.name.replace(/\.[^/.]+$/, "") || "frames";
for (let i = 0; i < frameDataUrls.length; i++) {
const frame = frameDataUrls[i];
const base64Data = frame.url.split(',')[1];
const fileName = `帧_${(i + 1).toString().padStart(3, '0')}.png`;
zip.file(fileName, base64Data, { base64: true });
}
const zipBlob = await zip.generateAsync({ type: "blob" });
const url = URL.createObjectURL(zipBlob);
const a = document.createElement('a');
a.href = url;
a.download = `${folderName}_所有帧.zip`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
function resetAll() {
// 清理内存
if (currentPreviewUrl) URL.revokeObjectURL(currentPreviewUrl);
currentFile = null;
currentPreviewUrl = null;
frameDataUrls = [];
uploadSection.classList.remove('hidden');
mainSection.classList.add('hidden');
framesGrid.innerHTML = '';
frameCountBadge.textContent = '0 帧';
downloadAllBtn.classList.add('hidden');
fileInfo.textContent = '';
}
// 初始化
window.onload = function() {
setupDragDrop();
initTailwind();
// 示例提示(第一次加载)
setTimeout(() => {
console.log('%c帧拆工具已就绪 🎉\n完全本地运行无需网络除首次加载CDN', 'color:#10b981; font-size:13px');
}, 800);
};
</script>
</body>
</html>