diff --git a/SpineViewer/Exporter/Implementations/ExportArgs/GifExportArgs.cs b/SpineViewer/Exporter/Implementations/ExportArgs/GifExportArgs.cs
new file mode 100644
index 0000000..ca7785f
--- /dev/null
+++ b/SpineViewer/Exporter/Implementations/ExportArgs/GifExportArgs.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SpineViewer.Exporter.Implementations.ExportArgs
+{
+ ///
+ /// GIF 导出参数
+ ///
+ [ExportImplementation(ExportType.GIF)]
+ public class GifExportArgs : VideoExportArgs
+ {
+ public GifExportArgs(Size resolution, SFML.Graphics.View view, bool renderSelectedOnly) : base(resolution, view, renderSelectedOnly)
+ {
+ // GIF 的帧率不能太高, 超过 50 帧反而会变慢
+ FPS = 25;
+ }
+
+ ///
+ /// 调色板最大颜色数量
+ ///
+ [Category("GIF 参数"), DisplayName("调色板最大颜色数量"), Description("设置调色板使用的最大颜色数量, 越多则色彩保留程度越高")]
+ public uint MaxColors { get => maxColors; set => maxColors = Math.Clamp(value, 2, 256); }
+ private uint maxColors = 256;
+
+ ///
+ /// 透明度阈值
+ ///
+ [Category("GIF 参数"), DisplayName("透明度阈值"), Description("小于该值的像素点会被认为是透明像素")]
+ public byte AlphaThreshold { get => alphaThreshold; set => alphaThreshold = value; }
+ private byte alphaThreshold = 128;
+
+ ///
+ /// 获取构造好的 FFMpegCore 自定义参数
+ ///
+ [Browsable(false)]
+ public string FFMpegCoreCustomArguments
+ {
+ get
+ {
+ var v = $"[0:v] split [s0][s1]";
+ var s0 = $"[s0] palettegen=reserve_transparent=1:max_colors={MaxColors} [p]";
+ var s1 = $"[s1][p] paletteuse=dither=bayer:alpha_threshold={AlphaThreshold}";
+ return $"-filter_complex \"{v};{s0};{s1}\"";
+ }
+ }
+ }
+}
diff --git a/SpineViewer/Exporter/Implementations/Exporter/GifExporter.cs b/SpineViewer/Exporter/Implementations/Exporter/GifExporter.cs
new file mode 100644
index 0000000..3053675
--- /dev/null
+++ b/SpineViewer/Exporter/Implementations/Exporter/GifExporter.cs
@@ -0,0 +1,76 @@
+using FFMpegCore.Pipes;
+using FFMpegCore;
+using SpineViewer.Exporter.Implementations.ExportArgs;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using FFMpegCore.Arguments;
+
+namespace SpineViewer.Exporter.Implementations.Exporter
+{
+ ///
+ /// GIF 动图导出器
+ ///
+ [ExportImplementation(ExportType.GIF)]
+ public class GifExporter : VideoExporter
+ {
+ public GifExporter(GifExportArgs exportArgs) : base(exportArgs) { }
+
+ protected override void ExportSingle(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
+ {
+ var args = (GifExportArgs)ExportArgs;
+
+ // 导出单个时必定提供输出文件夹
+ var filename = $"{timestamp}_{args.FPS:f0}_{args.MaxColors}_{args.AlphaThreshold}.gif";
+ var savePath = Path.Combine(args.OutputDir, filename);
+
+ var videoFramesSource = new RawVideoPipeSource(GetFrames(spinesToRender, worker)) { FrameRate = args.FPS };
+ try
+ {
+ FFMpegArguments
+ .FromPipeInput(videoFramesSource)
+ .OutputToFile(savePath, true, options => options
+ .ForceFormat("gif")
+ .WithCustomArgument(args.FFMpegCoreCustomArguments))
+ .ProcessSynchronously();
+ }
+ catch (Exception ex)
+ {
+ Program.Logger.Error(ex.ToString());
+ Program.Logger.Error("Failed to export gif {}", savePath);
+ }
+ }
+
+ protected override void ExportIndividual(Spine.Spine[] spinesToRender, BackgroundWorker? worker = null)
+ {
+ var args = (GifExportArgs)ExportArgs;
+ foreach (var spine in spinesToRender)
+ {
+ if (worker?.CancellationPending == true) break; // 取消的日志在 GetFrames 里输出
+
+ // 如果提供了输出文件夹, 则全部导出到输出文件夹, 否则导出到各自的文件夹下
+ var filename = $"{spine.Name}_{timestamp}_{args.FPS:f0}_{args.MaxColors}_{args.AlphaThreshold}.gif";
+ var savePath = Path.Combine(args.OutputDir ?? spine.AssetsDir, filename);
+
+ var videoFramesSource = new RawVideoPipeSource(GetFrames(spine, worker)) { FrameRate = args.FPS };
+ try
+ {
+ FFMpegArguments
+ .FromPipeInput(videoFramesSource)
+ .OutputToFile(savePath, true, options => options
+ .ForceFormat("gif")
+ .WithCustomArgument(args.FFMpegCoreCustomArguments))
+ .ProcessSynchronously();
+ }
+ catch (Exception ex)
+ {
+ Program.Logger.Error(ex.ToString());
+ Program.Logger.Error("Failed to export gif {} {}", savePath, spine.SkelPath);
+ }
+ }
+ }
+ }
+}