update: 2026-03-28 20:59

This commit is contained in:
2026-03-28 20:59:52 +08:00
parent e21d58e603
commit 1c81d4e6ea
611 changed files with 27847 additions and 65061 deletions

View 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&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>