2025-12-02

<!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>
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

友情链接更多精彩内容