update: 2026-03-28 20:59
This commit is contained in:
418
InfoGenie-frontend/public/toolbox/图片处理/GIF拆帧/index.html
Normal file
418
InfoGenie-frontend/public/toolbox/图片处理/GIF拆帧/index.html
Normal file
@@ -0,0 +1,418 @@
|
||||
<!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&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>
|
||||
Reference in New Issue
Block a user