<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>Lottie → 透明GIF 简易转换器</title>
<style>
body {
font-family: Arial;
padding: 20px;
background: #f5f5f5;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.preview-area {
display: flex;
gap: 20px;
margin: 20px 0;
}
#preview {
width: 400px;
height: 400px;
border: 1px solid #ccc;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><rect width="10" height="10" fill="%23f0f0f0"/><rect x="10" y="10" width="10" height="10" fill="%23f0f0f0"/></svg>');
}
.controls {
display: flex;
flex-direction: column;
gap: 10px;
min-width: 200px;
}
button {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.primary { background: #007bff; color: white; }
.secondary { background: #6c757d; color: white; }
button:disabled { opacity: 0.5; cursor: not-allowed; }
.progress {
width: 100%;
height: 20px;
background: #f0f0f0;
border-radius: 10px;
overflow: hidden;
margin: 10px 0;
display: none;
}
.progress-bar {
height: 100%;
background: #007bff;
transition: width 0.3s;
}
.settings {
background: #f8f9fa;
padding: 15px;
border-radius: 4px;
margin: 15px 0;
}
.setting-row {
display: flex;
justify-content: space-between;
align-items: center;
margin: 8px 0;
}
.setting-row input, .setting-row select {
padding: 4px 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
#debug {
margin-top: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 4px;
}
.frame-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
gap: 8px;
margin-top: 15px;
}
.frame-thumb {
width: 80px;
height: 80px;
border: 1px solid #ddd;
border-radius: 4px;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10"><rect width="5" height="5" fill="%23f0f0f0"/><rect x="5" y="5" width="5" height="5" fill="%23f0f0f0"/></svg>');
}
</style>
</head>
<body>
<div class="container">
<h2>Lottie → 透明GIF 简易转换器</h2>
<p>无需安装任何依赖,直接在浏览器中转换</p>
<input type="file" id="jsonFile" accept=".json"/>
<div class="settings">
<h4>转换设置</h4>
<div class="setting-row">
<label>最大帧数:</label>
<input type="number" id="maxFrames" value="48" min="5" max="100">
</div>
<div class="setting-row">
<label>质量:</label>
<select id="quality">
<option value="10">高质量</option>
<option value="15" selected>中等质量</option>
<option value="20">低质量</option>
</select>
</div>
<div class="setting-row">
<label>尺寸缩放:</label>
<select id="scale">
<option value="1">原始尺寸</option>
<option value="0.75" selected>75%</option>
<option value="0.5">50%</option>
</select>
</div>
</div>
<div class="preview-area">
<div id="preview"></div>
<div class="controls">
<button id="btnExport" class="primary" disabled>导出透明GIF</button>
<button id="btnPreview" class="secondary" disabled>预览帧</button>
<div class="progress">
<div class="progress-bar" id="progressBar"></div>
</div>
<div id="progressText"></div>
</div>
</div>
<div id="debug">
<h4>动画信息</h4>
<div id="debugInfo">请先加载 Lottie 文件</div>
<div id="framePreview" class="frame-grid"></div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lottie-web/5.10.2/lottie.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gifshot/0.3.2/gifshot.min.js"></script>
<script>
let anim;
let animationData;
let totalFrames = 0;
let fps = 30;
let duration = 0;
let container = document.getElementById("preview");
let isProcessing = false;
// 文件加载
document.getElementById("jsonFile").onchange = function(e){
if (isProcessing) return;
let reader = new FileReader();
reader.onload = function(){
try {
animationData = JSON.parse(reader.result);
loadAnimation();
} catch (error) {
alert('JSON文件格式错误: ' + error.message);
}
};
reader.readAsText(e.target.files[0]);
};
function loadAnimation() {
if (anim) anim.destroy();
container.innerHTML = '';
anim = lottie.loadAnimation({
container: container,
renderer: 'canvas',
loop: true,
autoplay: true,
animationData: animationData,
rendererSettings: {
preserveAspectRatio: 'xMidYMid meet',
clearCanvas: true,
progressiveLoad: false,
hideOnTransparent: true
}
});
anim.addEventListener("DOMLoaded", ()=>{
totalFrames = Math.floor(animationData.op) || anim.totalFrames;
fps = animationData.fr || anim.frameRate || 30;
duration = totalFrames / fps;
document.getElementById('maxFrames').value = Math.min(48, totalFrames);
updateDebugInfo();
enableButtons();
});
anim.addEventListener("error", (error) => {
console.error('动画加载失败:', error);
alert('动画加载失败,请检查文件格式');
});
}
function enableButtons() {
document.getElementById("btnExport").disabled = false;
document.getElementById("btnPreview").disabled = false;
}
function updateDebugInfo() {
const analysis = analyzeAnimation();
document.getElementById("debugInfo").innerHTML = `
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
<div>
<p><strong>基本信息:</strong></p>
<p>• 尺寸: ${animationData.w}×${animationData.h}px</p>
<p>• 帧数: ${totalFrames} 帧</p>
<p>• 帧率: ${fps} fps</p>
<p>• 时长: ${duration.toFixed(2)} 秒</p>
</div>
<div>
<p><strong>动画特征:</strong></p>
<p>• 图层数: ${analysis.layerCount}</p>
<p>• 包含渐变: ${analysis.hasGradients ? '是' : '否'}</p>
<p>• 包含透明度: ${analysis.hasOpacity ? '是' : '否'}</p>
<p>• 复杂度: ${analysis.complexity}</p>
</div>
</div>
<p><strong>推荐:</strong> 透明GIF格式,限制${Math.min(48, totalFrames)}帧</p>
`;
}
function analyzeAnimation() {
const jsonStr = JSON.stringify(animationData);
return {
layerCount: animationData.layers ? animationData.layers.length : 0,
hasGradients: jsonStr.includes('"ty":"gf"') || jsonStr.includes('"ty":"gr"'),
hasOpacity: jsonStr.includes('"o":') && jsonStr.includes('0'),
hasTransforms: jsonStr.includes('"s":') || jsonStr.includes('"r":'),
complexity: calculateComplexity()
};
}
function calculateComplexity() {
const jsonStr = JSON.stringify(animationData);
let score = 0;
score += (animationData.layers?.length || 0) * 2;
score += (jsonStr.match(/"k":\[/g) || []).length;
score += (jsonStr.match(/"ty":"sh"/g) || []).length * 3;
score += (jsonStr.match(/"ty":"gf"/g) || []).length * 5;
if (score < 50) return 'low';
if (score < 150) return 'medium';
return 'high';
}
function showProgress(show, progress = 0, text = '') {
const progressDiv = document.querySelector('.progress');
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
progressDiv.style.display = show ? 'block' : 'none';
progressBar.style.width = progress + '%';
progressText.textContent = text;
}
// 预览功能
document.getElementById("btnPreview").onclick = function(){
if (isProcessing) return;
const framePreview = document.getElementById("framePreview");
framePreview.innerHTML = '<p>正在生成预览帧...</p>';
const previewCount = Math.min(12, totalFrames);
const canvas = container.querySelector('canvas');
if (!canvas) {
framePreview.innerHTML = '<p>无法获取画布,请重新加载动画</p>';
return;
}
framePreview.innerHTML = '';
for (let i = 0; i < previewCount; i++) {
setTimeout(() => {
const frameTime = (i / fps) * 1000;
anim.goToAndStop(frameTime, false);
setTimeout(() => {
try {
const dataURL = canvas.toDataURL('image/png');
const img = document.createElement('img');
img.src = dataURL;
img.className = 'frame-thumb';
img.title = `帧 ${i + 1}/${previewCount}`;
framePreview.appendChild(img);
} catch (error) {
console.error('预览帧生成失败:', error);
}
}, 100);
}, i * 150);
}
};
// 导出功能
document.getElementById("btnExport").onclick = function(){
if (isProcessing) return;
isProcessing = true;
showProgress(true, 0, '开始捕获帧...');
const maxFrames = Math.min(totalFrames, parseInt(document.getElementById('maxFrames').value));
const scale = parseFloat(document.getElementById('scale').value);
const quality = parseInt(document.getElementById('quality').value);
const canvas = container.querySelector('canvas');
if (!canvas) {
alert('无法获取画布,请重新加载动画');
isProcessing = false;
showProgress(false);
return;
}
const frames = [];
let current = 0;
function captureNext() {
if (current >= maxFrames) {
generateGIF(frames);
return;
}
const progress = Math.round((current / maxFrames) * 80);
showProgress(true, progress, `正在捕获第 ${current + 1}/${maxFrames} 帧`);
const frameTime = (current / fps) * 1000;
anim.goToAndStop(frameTime, false);
setTimeout(() => {
try {
// 创建缩放后的canvas
const outputCanvas = document.createElement('canvas');
const outputWidth = Math.round(animationData.w * scale);
const outputHeight = Math.round(animationData.h * scale);
outputCanvas.width = outputWidth;
outputCanvas.height = outputHeight;
const ctx = outputCanvas.getContext('2d');
ctx.clearRect(0, 0, outputWidth, outputHeight);
ctx.drawImage(canvas, 0, 0, outputWidth, outputHeight);
const dataURL = outputCanvas.toDataURL('image/png');
frames.push(dataURL);
current++;
setTimeout(captureNext, 50);
} catch (error) {
console.error('帧捕获失败:', error);
current++;
setTimeout(captureNext, 50);
}
}, 100);
}
captureNext();
};
function generateGIF(frames) {
if (frames.length === 0) {
alert('没有捕获到有效帧!');
isProcessing = false;
showProgress(false);
return;
}
showProgress(true, 85, '正在生成透明GIF...');
const scale = parseFloat(document.getElementById('scale').value);
const quality = parseInt(document.getElementById('quality').value);
const outputWidth = Math.round(animationData.w * scale);
const outputHeight = Math.round(animationData.h * scale);
try {
gifshot.createGIF({
images: frames,
gifWidth: outputWidth,
gifHeight: outputHeight,
interval: 1000 / fps / 1000,
transparent: true,
numWorkers: 2,
quality: quality,
repeat: 0
}, function(obj) {
isProcessing = false;
showProgress(false);
if (!obj.error) {
const a = document.createElement("a");
a.href = obj.image;
a.download = `lottie_transparent_${frames.length}frames.gif`;
a.click();
alert(`透明GIF生成成功!\n\n文件信息:\n• 帧数: ${frames.length}\n• 尺寸: ${outputWidth}×${outputHeight}px\n• 格式: GIF (透明背景)`);
} else {
console.error('GIF生成失败:', obj.error);
alert('GIF生成失败: ' + obj.error);
}
});
} catch (error) {
isProcessing = false;
showProgress(false);
console.error('GIF生成出错:', error);
alert('GIF生成失败: ' + error.message);
}
}
</script>
</div>
</body>
</html>
2025-12-02
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
推荐阅读更多精彩内容
- # C++11新特性深度解析:可变参数模板与Lambda表达式探究 ## 引言:C++11的现代化转型 C++11...
- # Git企业级工作流实战:基于Git Flow的分支模型与开发流程 ## 引言:企业级Git工作流的重要性 在当...