small change

This commit is contained in:
ww-rm
2025-04-11 00:58:25 +08:00
parent b6f9cd0c7c
commit e2a84d8f88
38 changed files with 552 additions and 683 deletions

View File

@@ -51,7 +51,7 @@ namespace SpineViewer.Controls
{
propertyGrid = value;
if (propertyGrid is not null)
propertyGrid.SelectedObject = new PropertyGridWrappers.SpinePreviewerWrapper(this);
propertyGrid.SelectedObject = new SpinePreviewerProperty(this);
}
}
private PropertyGrid? propertyGrid;
@@ -635,4 +635,42 @@ namespace SpineViewer.Controls
//public void ClickForwardStepButton() => button_ForwardStep_Click(button_ForwardStep, EventArgs.Empty);
//public void ClickForwardFastButton() => button_ForwardFast_Click(button_ForwardFast, EventArgs.Empty);
}
/// <summary>
/// 用于在 PropertyGrid 上显示 SpinePreviewe 属性的包装类, 提供用户操作接口
/// </summary>
public class SpinePreviewerProperty(SpinePreviewer previewer)
{
[Browsable(false)]
public SpinePreviewer Previewer { get; } = previewer;
[TypeConverter(typeof(SizeConverter))]
[Category("[0] "), DisplayName("")]
public Size Resolution { get => Previewer.Resolution; set => Previewer.Resolution = value; }
[TypeConverter(typeof(PointFConverter))]
[Category("[0] "), DisplayName("")]
public PointF Center { get => Previewer.Center; set => Previewer.Center = value; }
[Category("[0] "), DisplayName("")]
public float Zoom { get => Previewer.Zoom; set => Previewer.Zoom = value; }
[Category("[0] "), DisplayName("")]
public float Rotation { get => Previewer.Rotation; set => Previewer.Rotation = value; }
[Category("[0] "), DisplayName("")]
public bool FlipX { get => Previewer.FlipX; set => Previewer.FlipX = value; }
[Category("[0] "), DisplayName("")]
public bool FlipY { get => Previewer.FlipY; set => Previewer.FlipY = value; }
[Category("[0] "), DisplayName("")]
public bool RenderSelectedOnly { get => Previewer.RenderSelectedOnly; set => Previewer.RenderSelectedOnly = value; }
[Category("[1] "), DisplayName("")]
public bool ShowAxis { get => Previewer.ShowAxis; set => Previewer.ShowAxis = value; }
[Category("[1] "), DisplayName("")]
public uint MaxFps { get => Previewer.MaxFps; set => Previewer.MaxFps = value; }
}
}

View File

@@ -1,5 +1,4 @@
using SpineViewer.PropertyGridWrappers.Exporter;
using SpineViewer.Utils;
using SpineViewer.Utils;
using System;
using System.Collections;
using System.Collections.Generic;
@@ -8,14 +7,15 @@ using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using SpineViewer.Spine.SpineExporter;
namespace SpineViewer.Dialogs
{
public partial class ExportDialog : Form
{
private readonly ExporterWrapper wrapper;
private readonly ExporterProperty wrapper;
public ExportDialog(ExporterWrapper wrapper)
public ExportDialog(ExporterProperty wrapper)
{
InitializeComponent();
this.wrapper = wrapper;

View File

@@ -1,55 +0,0 @@
using FFMpegCore;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Exporter
{
/// <summary>
/// MP4 导出参数
/// </summary>
public class AvifExporter : FFmpegVideoExporter
{
public AvifExporter()
{
FPS = 24;
}
public override string Format => "avif";
public override string Suffix => ".avif";
/// <summary>
/// 编码器
/// </summary>
public string Codec { get; set; } = "av1_nvenc";
/// <summary>
/// CRF
/// </summary>
public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); }
private int crf = 23;
/// <summary>
/// 像素格式
/// </summary>
public string PixelFormat { get; set; } = "yuv420p";
/// <summary>
/// 循环次数, 0 无限循环, 取值范围 [0, 65535]
/// </summary>
public int Loop { get => loop; set => loop = Math.Clamp(value, 0, 65535); }
private int loop = 0;
public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
base.SetOutputOptions(options);
options.WithVideoCodec(Codec).ForcePixelFormat(PixelFormat).WithConstantRateFactor(CRF).WithCustomArgument($"-loop {Loop}");
}
}
}

View File

@@ -1,36 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Exporter
{
/// <summary>
/// FFmpeg 自定义视频导出参数
/// </summary>
public class CustomExporter : FFmpegVideoExporter
{
public CustomExporter()
{
CustomArgument = "-c:v libx264 -crf 23 -pix_fmt yuv420p"; // 提供一个示例参数
}
public override string Format => CustomFormat;
public override string Suffix => CustomSuffix;
public override string FileNameNoteSuffix => string.Empty;
/// <summary>
/// 文件格式
/// </summary>
public string CustomFormat { get; set; } = "mp4";
/// <summary>
/// 文件名后缀
/// </summary>
public string CustomSuffix { get; set; } = ".mp4";
}
}

View File

@@ -1,49 +0,0 @@
using FFMpegCore;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Exporter
{
/// <summary>
/// MKV 导出参数
/// </summary>
public class MkvExporter : FFmpegVideoExporter
{
public MkvExporter()
{
BackgroundColor = new(0, 255, 0);
}
public override string Format => "matroska";
public override string Suffix => ".mkv";
/// <summary>
/// 编码器
/// </summary>
public string Codec { get; set; } = "libx265";
/// <summary>
/// CRF
/// </summary>
public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); }
private int crf = 23;
/// <summary>
/// 像素格式
/// </summary>
public string PixelFormat { get; set; } = "yuv444p";
public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
base.SetOutputOptions(options);
options.WithVideoCodec(Codec).WithConstantRateFactor(CRF).ForcePixelFormat(PixelFormat);
}
}
}

View File

@@ -1,48 +0,0 @@
using FFMpegCore;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Exporter
{
/// <summary>
/// MOV 导出参数
/// </summary>
public class MovExporter : FFmpegVideoExporter
{
public MovExporter()
{
BackgroundColor = new(0, 255, 0);
}
public override string Format => "mov";
public override string Suffix => ".mov";
/// <summary>
/// 编码器
/// </summary>
public string Codec { get; set; } = "prores_ks";
/// <summary>
/// 预设
/// </summary>
public string Profile { get; set; } = "auto";
/// <summary>
/// 像素格式
/// </summary>
public string PixelFormat { get; set; } = "yuva444p10le";
public override string FileNameNoteSuffix => $"{Codec}_{Profile}_{PixelFormat}";
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
base.SetOutputOptions(options);
options.WithFastStart().WithVideoCodec(Codec).WithCustomArgument($"-profile {Profile}").ForcePixelFormat(PixelFormat);
}
}
}

View File

@@ -1,49 +0,0 @@
using FFMpegCore;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Exporter
{
/// <summary>
/// MP4 导出参数
/// </summary>
public class Mp4Exporter : FFmpegVideoExporter
{
public Mp4Exporter()
{
BackgroundColor = new(0, 255, 0);
}
public override string Format => "mp4";
public override string Suffix => ".mp4";
/// <summary>
/// 编码器
/// </summary>
public string Codec { get; set; } = "libx264";
/// <summary>
/// CRF
/// </summary>
public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); }
private int crf = 23;
/// <summary>
/// 像素格式
/// </summary>
public string PixelFormat { get; set; } = "yuv444p";
public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
base.SetOutputOptions(options);
options.WithFastStart().WithVideoCodec(Codec).WithConstantRateFactor(CRF).ForcePixelFormat(PixelFormat);
}
}
}

View File

@@ -1,50 +0,0 @@
using FFMpegCore;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Exporter
{
/// <summary>
/// WebM 导出参数
/// </summary>
public class WebmExporter : FFmpegVideoExporter
{
public WebmExporter()
{
// 默认用透明黑背景
BackgroundColor = new(0, 0, 0, 0);
}
public override string Format => "webm";
public override string Suffix => ".webm";
/// <summary>
/// 编码器
/// </summary>
public string Codec { get; set; } = "libvpx-vp9";
/// <summary>
/// CRF
/// </summary>
public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); }
private int crf = 23;
/// <summary>
/// 像素格式
/// </summary>
public string PixelFormat { get; set; } = "yuva420p";
public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
base.SetOutputOptions(options);
options.WithVideoCodec(Codec).WithConstantRateFactor(CRF).ForcePixelFormat(PixelFormat);
}
}
}

View File

@@ -1,60 +0,0 @@
using FFMpegCore;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Exporter
{
/// <summary>
/// MP4 导出参数
/// </summary>
public class WebpExporter : FFmpegVideoExporter
{
public WebpExporter()
{
FPS = 24;
}
public override string Format => "webp";
public override string Suffix => ".webp";
/// <summary>
/// 编码器
/// </summary>
public string Codec { get; set; } = "libwebp_anim";
/// <summary>
/// 是否无损
/// </summary>
public bool Lossless { get; set; } = false;
/// <summary>
/// 质量
/// </summary>
public int Quality { get => quality; set => quality = Math.Clamp(value, 0, 100); }
private int quality = 75;
/// <summary>
/// 像素格式
/// </summary>
public string PixelFormat { get; set; } = "yuva420p";
/// <summary>
/// 循环次数, 0 无限循环, 取值范围 [0, 65535]
/// </summary>
public int Loop { get => loop; set => loop = Math.Clamp(value, 0, 65535); }
private int loop = 0;
public override string FileNameNoteSuffix => $"{Codec}_{Quality}_{PixelFormat}";
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
base.SetOutputOptions(options);
options.WithVideoCodec(Codec).ForcePixelFormat(PixelFormat).WithCustomArgument($"-lossless {(Lossless ? 1 : 0)} -quality {Quality} -loop {Loop}");
}
}
}

View File

@@ -2,11 +2,9 @@
using SpineViewer.Spine;
using System.ComponentModel;
using System.Diagnostics;
using SpineViewer.Exporter;
using System.Reflection.Metadata;
using SpineViewer.PropertyGridWrappers.Exporter;
using SpineViewer.Natives;
using SpineViewer.Utils;
using SpineViewer.Spine.SpineExporter;
namespace SpineViewer
{
@@ -14,7 +12,7 @@ namespace SpineViewer
{
private readonly Logger logger = LogManager.GetCurrentClassLogger();
private readonly Dictionary<string, Exporter.Exporter> exporterCache = [];
private readonly Dictionary<string, Exporter> exporterCache = [];
public SpineViewerForm()
{
@@ -98,7 +96,7 @@ namespace SpineViewer
exporter.View = spinePreviewer.GetView();
exporter.RenderSelectedOnly = spinePreviewer.RenderSelectedOnly;
var exportDialog = new Dialogs.ExportDialog(new FrameExporterWrapper((FrameExporter)exporter));
var exportDialog = new Dialogs.ExportDialog(new FrameExporterProperty((FrameExporter)exporter));
if (exportDialog.ShowDialog() != DialogResult.OK)
return;
@@ -118,7 +116,7 @@ namespace SpineViewer
exporter.View = spinePreviewer.GetView();
exporter.RenderSelectedOnly = spinePreviewer.RenderSelectedOnly;
var exportDialog = new Dialogs.ExportDialog(new FrameSequenceExporterWrapper((FrameSequenceExporter)exporter));
var exportDialog = new Dialogs.ExportDialog(new FrameSequenceExporterProperty((FrameSequenceExporter)exporter));
if (exportDialog.ShowDialog() != DialogResult.OK)
return;
@@ -138,7 +136,7 @@ namespace SpineViewer
exporter.View = spinePreviewer.GetView();
exporter.RenderSelectedOnly = spinePreviewer.RenderSelectedOnly;
var exportDialog = new Dialogs.ExportDialog(new GifExporterWrapper((GifExporter)exporter));
var exportDialog = new Dialogs.ExportDialog(new GifExporterProperty((GifExporter)exporter));
if (exportDialog.ShowDialog() != DialogResult.OK)
return;
@@ -158,7 +156,7 @@ namespace SpineViewer
exporter.View = spinePreviewer.GetView();
exporter.RenderSelectedOnly = spinePreviewer.RenderSelectedOnly;
var exportDialog = new Dialogs.ExportDialog(new WebpExporterWrapper((WebpExporter)exporter));
var exportDialog = new Dialogs.ExportDialog(new WebpExporterProperty((WebpExporter)exporter));
if (exportDialog.ShowDialog() != DialogResult.OK)
return;
@@ -178,7 +176,7 @@ namespace SpineViewer
exporter.View = spinePreviewer.GetView();
exporter.RenderSelectedOnly = spinePreviewer.RenderSelectedOnly;
var exportDialog = new Dialogs.ExportDialog(new AvifExporterWrapper((AvifExporter)exporter));
var exportDialog = new Dialogs.ExportDialog(new AvifExporterProperty((AvifExporter)exporter));
if (exportDialog.ShowDialog() != DialogResult.OK)
return;
@@ -198,7 +196,7 @@ namespace SpineViewer
exporter.View = spinePreviewer.GetView();
exporter.RenderSelectedOnly = spinePreviewer.RenderSelectedOnly;
var exportDialog = new Dialogs.ExportDialog(new Mp4ExporterWrapper((Mp4Exporter)exporter));
var exportDialog = new Dialogs.ExportDialog(new Mp4ExporterProperty((Mp4Exporter)exporter));
if (exportDialog.ShowDialog() != DialogResult.OK)
return;
@@ -218,7 +216,7 @@ namespace SpineViewer
exporter.View = spinePreviewer.GetView();
exporter.RenderSelectedOnly = spinePreviewer.RenderSelectedOnly;
var exportDialog = new Dialogs.ExportDialog(new WebmExporterWrapper((WebmExporter)exporter));
var exportDialog = new Dialogs.ExportDialog(new WebmExporterProperty((WebmExporter)exporter));
if (exportDialog.ShowDialog() != DialogResult.OK)
return;
@@ -238,7 +236,7 @@ namespace SpineViewer
exporter.View = spinePreviewer.GetView();
exporter.RenderSelectedOnly = spinePreviewer.RenderSelectedOnly;
var exportDialog = new Dialogs.ExportDialog(new MkvExporterWrapper((MkvExporter)exporter));
var exportDialog = new Dialogs.ExportDialog(new MkvExporterProperty((MkvExporter)exporter));
if (exportDialog.ShowDialog() != DialogResult.OK)
return;
@@ -258,7 +256,7 @@ namespace SpineViewer
exporter.View = spinePreviewer.GetView();
exporter.RenderSelectedOnly = spinePreviewer.RenderSelectedOnly;
var exportDialog = new Dialogs.ExportDialog(new MovExporterWrapper((MovExporter)exporter));
var exportDialog = new Dialogs.ExportDialog(new MovExporterProperty((MovExporter)exporter));
if (exportDialog.ShowDialog() != DialogResult.OK)
return;
@@ -278,7 +276,7 @@ namespace SpineViewer
exporter.View = spinePreviewer.GetView();
exporter.RenderSelectedOnly = spinePreviewer.RenderSelectedOnly;
var exportDialog = new Dialogs.ExportDialog(new CustomExporterWrapper((CustomExporter)exporter));
var exportDialog = new Dialogs.ExportDialog(new CustomExporterProperty((CustomExporter)exporter));
if (exportDialog.ShowDialog() != DialogResult.OK)
return;
@@ -337,7 +335,7 @@ namespace SpineViewer
private void Export_Work(object? sender, DoWorkEventArgs e)
{
var worker = (BackgroundWorker)sender;
var exporter = (Exporter.Exporter)e.Argument;
var exporter = (Exporter)e.Argument;
Invoke(() => TaskbarManager.SetProgressState(Handle, TBPFLAG.TBPF_INDETERMINATE));
spinePreviewer.StopRender();
lock (spineListView.Spines) { exporter.Export(spineListView.Spines.Where(sp => !sp.IsHidden).ToArray(), (BackgroundWorker)sender); }

View File

@@ -1,56 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing.Design;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.PropertyGridWrappers.Exporter
{
public class ExporterWrapper(SpineViewer.Exporter.Exporter exporter)
{
[Browsable(false)]
public virtual SpineViewer.Exporter.Exporter Exporter { get; } = exporter;
/// <summary>
/// 输出文件夹
/// </summary>
[Editor(typeof(FolderNameEditor), typeof(UITypeEditor))]
[Category("[0] "), DisplayName(""), Description("")]
public string? OutputDir { get => Exporter.OutputDir; set => Exporter.OutputDir = value; }
/// <summary>
/// 导出单个
/// </summary>
[Category("[0] "), DisplayName(""), Description("")]
public bool IsExportSingle { get => Exporter.IsExportSingle; set => Exporter.IsExportSingle = value; }
/// <summary>
/// 画面分辨率
/// </summary>
[TypeConverter(typeof(SizeConverter))]
[Category("[0] "), DisplayName(""), Description("")]
public Size Resolution { get => Exporter.Resolution; }
/// <summary>
/// 渲染视窗
/// </summary>
[Category("[0] "), DisplayName(""), Description("")]
public SFML.Graphics.View View { get => Exporter.View; }
/// <summary>
/// 是否仅渲染选中
/// </summary>
[Category("[0] "), DisplayName(""), Description("")]
public bool RenderSelectedOnly { get => Exporter.RenderSelectedOnly; }
/// <summary>
/// 背景颜色
/// </summary>
[Editor(typeof(SFMLColorEditor), typeof(UITypeEditor))]
[TypeConverter(typeof(SFMLColorConverter))]
[Category("[0] "), DisplayName(""), Description("使, #RRGGBBAA")]
public SFML.Graphics.Color BackgroundColor { get => Exporter.BackgroundColor; set => Exporter.BackgroundColor = value; }
}
}

View File

@@ -1,34 +0,0 @@
using SpineViewer.Exporter;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.PropertyGridWrappers.Exporter
{
public class FFmpegVideoExporterWrapper(FFmpegVideoExporter exporter) : VideoExporterWrapper(exporter)
{
[Browsable(false)]
public override FFmpegVideoExporter Exporter => (FFmpegVideoExporter)base.Exporter;
/// <summary>
/// 文件格式
/// </summary>
[Category("[2] FFmpeg "), DisplayName(""), Description("-f, ")]
public virtual string Format => Exporter.Format;
/// <summary>
/// 文件名后缀
/// </summary>
[Category("[2] FFmpeg "), DisplayName(""), Description("")]
public virtual string Suffix => Exporter.Suffix;
/// <summary>
/// 文件名后缀
/// </summary>
[Category("[2] FFmpeg "), DisplayName(""), Description("使 \"ffmpeg -h encoder=<编码器>\" 查看编码器支持的参数\n使用 \"ffmpeg -h muxer=<文件格式>\" 查看文件格式支持的参数")]
public string CustomArgument { get => Exporter.CustomArgument; set => Exporter.CustomArgument = value; }
}
}

View File

@@ -1,37 +0,0 @@
using SpineViewer.Exporter;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing.Imaging;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.PropertyGridWrappers.Exporter
{
public class FrameExporterWrapper(FrameExporter exporter) : ExporterWrapper(exporter)
{
[Browsable(false)]
public override FrameExporter Exporter => (FrameExporter)base.Exporter;
/// <summary>
/// 单帧画面格式
/// </summary>
[TypeConverter(typeof(ImageFormatConverter))]
[Category("[1] "), DisplayName("")]
public ImageFormat ImageFormat { get => Exporter.ImageFormat; set => Exporter.ImageFormat = value; }
/// <summary>
/// 文件名后缀
/// </summary>
[Category("[1] "), DisplayName(""), Description("")]
public string Suffix { get => Exporter.ImageFormat.GetSuffix(); }
/// <summary>
/// DPI
/// </summary>
[TypeConverter(typeof(SizeFConverter))]
[Category("[1] "), DisplayName("DPI"), Description("")]
public SizeF DPI { get => Exporter.DPI; set => Exporter.DPI = value; }
}
}

View File

@@ -1,23 +0,0 @@
using SpineViewer.Exporter;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.PropertyGridWrappers.Exporter
{
public class FrameSequenceExporterWrapper(VideoExporter exporter) : VideoExporterWrapper(exporter)
{
[Browsable(false)]
public override FrameSequenceExporter Exporter => (FrameSequenceExporter)base.Exporter;
/// <summary>
/// 文件名后缀
/// </summary>
[TypeConverter(typeof(StringEnumConverter)), StringEnumConverter.StandardValues(".png", ".jpg", ".tga", ".bmp")]
[Category("[2] "), DisplayName(""), Description("")]
public string Suffix { get => Exporter.Suffix; set => Exporter.Suffix = value; }
}
}

View File

@@ -1,34 +0,0 @@
using SpineViewer.Exporter;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.PropertyGridWrappers.Exporter
{
class GifExporterWrapper(FFmpegVideoExporter exporter) : FFmpegVideoExporterWrapper(exporter)
{
[Browsable(false)]
public override GifExporter Exporter => (GifExporter)base.Exporter;
/// <summary>
/// 调色板最大颜色数量
/// </summary>
[Category("[3] "), DisplayName(""), Description("使, ")]
public uint MaxColors { get => Exporter.MaxColors; set => Exporter.MaxColors = value; }
/// <summary>
/// 透明度阈值
/// </summary>
[Category("[3] "), DisplayName(""), Description("")]
public byte AlphaThreshold { get => Exporter.AlphaThreshold; set => Exporter.AlphaThreshold = value; }
/// <summary>
/// 透明度阈值
/// </summary>
[Category("[3] "), DisplayName(""), Description("-loop, , -1 , 0 , [-1, 65535]")]
public int Loop { get => Exporter.Loop; set => Exporter.Loop = value; }
}
}

View File

@@ -1,34 +0,0 @@
using SpineViewer.Exporter;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.PropertyGridWrappers.Exporter
{
public class VideoExporterWrapper(VideoExporter exporter) : ExporterWrapper(exporter)
{
[Browsable(false)]
public override VideoExporter Exporter => (VideoExporter)base.Exporter;
/// <summary>
/// 导出时长
/// </summary>
[Category("[1] "), DisplayName(""), Description(", 0, 使")]
public float Duration { get => Exporter.Duration; set => Exporter.Duration = value; }
/// <summary>
/// 帧率
/// </summary>
[Category("[1] "), DisplayName(""), Description("")]
public float FPS { get => Exporter.FPS; set => Exporter.FPS = value; }
/// <summary>
/// 保留最后一帧
/// </summary>
[Category("[1] "), DisplayName(""), Description(", , 1")]
public bool KeepLast { get => Exporter.KeepLast; set => Exporter.KeepLast = value; }
}
}

View File

@@ -1,4 +1,5 @@
using SpineViewer.Spine;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;

View File

@@ -1,4 +1,5 @@
using SpineViewer.Spine;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;

View File

@@ -1,4 +1,5 @@
using SpineViewer.Spine;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;

View File

@@ -5,6 +5,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SpineViewer.Spine;
using SpineViewer.Utils;
namespace SpineViewer.PropertyGridWrappers.Spine
{

View File

@@ -1,48 +0,0 @@
using SpineViewer.Controls;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.PropertyGridWrappers
{
/// <summary>
/// 用于在 PropertyGrid 上显示 SpinePreviewe 属性的包装类
/// </summary>
public class SpinePreviewerWrapper(SpinePreviewer previewer)
{
[Browsable(false)]
public SpinePreviewer Previewer { get; } = previewer;
[TypeConverter(typeof(SizeConverter))]
[Category("[0] "), DisplayName("")]
public Size Resolution { get => Previewer.Resolution; set => Previewer.Resolution = value; }
[TypeConverter(typeof(PointFConverter))]
[Category("[0] "), DisplayName("")]
public PointF Center { get => Previewer.Center; set => Previewer.Center = value; }
[Category("[0] "), DisplayName("")]
public float Zoom { get => Previewer.Zoom; set => Previewer.Zoom = value; }
[Category("[0] "), DisplayName("")]
public float Rotation { get => Previewer.Rotation; set => Previewer.Rotation = value; }
[Category("[0] "), DisplayName("")]
public bool FlipX { get => Previewer.FlipX; set => Previewer.FlipX = value; }
[Category("[0] "), DisplayName("")]
public bool FlipY { get => Previewer.FlipY; set => Previewer.FlipY = value; }
[Category("[0] "), DisplayName("")]
public bool RenderSelectedOnly { get => Previewer.RenderSelectedOnly; set => Previewer.RenderSelectedOnly = value; }
[Category("[1] "), DisplayName("")]
public bool ShowAxis { get => Previewer.ShowAxis; set => Previewer.ShowAxis = value; }
[Category("[1] "), DisplayName("")]
public uint MaxFps { get => Previewer.MaxFps; set => Previewer.MaxFps = value; }
}
}

View File

@@ -1,4 +1,5 @@
using SpineViewer.Exporter;
using FFMpegCore;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
@@ -6,9 +7,54 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.PropertyGridWrappers.Exporter
namespace SpineViewer.Spine.SpineExporter
{
public class AvifExporterWrapper(AvifExporter exporter) : FFmpegVideoExporterWrapper(exporter)
/// <summary>
/// MP4 导出参数
/// </summary>
public class AvifExporter : FFmpegVideoExporter
{
public AvifExporter()
{
FPS = 24;
}
public override string Format => "avif";
public override string Suffix => ".avif";
/// <summary>
/// 编码器
/// </summary>
public string Codec { get; set; } = "av1_nvenc";
/// <summary>
/// CRF
/// </summary>
public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); }
private int crf = 23;
/// <summary>
/// 像素格式
/// </summary>
public string PixelFormat { get; set; } = "yuv420p";
/// <summary>
/// 循环次数, 0 无限循环, 取值范围 [0, 65535]
/// </summary>
public int Loop { get => loop; set => loop = Math.Clamp(value, 0, 65535); }
private int loop = 0;
public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
base.SetOutputOptions(options);
options.WithVideoCodec(Codec).ForcePixelFormat(PixelFormat).WithConstantRateFactor(CRF).WithCustomArgument($"-loop {Loop}");
}
}
public class AvifExporterProperty(AvifExporter exporter) : FFmpegVideoExporterProperty(exporter)
{
[Browsable(false)]
public override AvifExporter Exporter => (AvifExporter)base.Exporter;

View File

@@ -1,14 +1,40 @@
using SpineViewer.Exporter;
using System;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.PropertyGridWrappers.Exporter
namespace SpineViewer.Spine.SpineExporter
{
public class CustomExporterWrapper(CustomExporter exporter) : FFmpegVideoExporterWrapper(exporter)
/// <summary>
/// FFmpeg 自定义视频导出参数
/// </summary>
public class CustomExporter : FFmpegVideoExporter
{
public CustomExporter()
{
CustomArgument = "-c:v libx264 -crf 23 -pix_fmt yuv420p"; // 提供一个示例参数
}
public override string Format => CustomFormat;
public override string Suffix => CustomSuffix;
public override string FileNameNoteSuffix => string.Empty;
/// <summary>
/// 文件格式
/// </summary>
public string CustomFormat { get; set; } = "mp4";
/// <summary>
/// 文件名后缀
/// </summary>
public string CustomSuffix { get; set; } = ".mp4";
}
public class CustomExporterProperty(CustomExporter exporter) : FFmpegVideoExporterProperty(exporter)
{
[Browsable(false)]
public override CustomExporter Exporter => (CustomExporter)base.Exporter;

View File

@@ -6,7 +6,7 @@ using System.Drawing.Imaging;
using System.Linq;
using System.Text;
namespace SpineViewer.Exporter
namespace SpineViewer.Spine.SpineExporter
{
/// <summary>
/// SFML.Graphics.Image 帧对象包装类, 将接管给定的 image 对象生命周期

View File

@@ -1,6 +1,5 @@
using NLog;
using SpineViewer.Extensions;
using SpineViewer.PropertyGridWrappers;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
@@ -11,7 +10,7 @@ using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Exporter
namespace SpineViewer.Spine.SpineExporter
{
/// <summary>
/// 导出器基类
@@ -96,12 +95,12 @@ namespace SpineViewer.Exporter
/// <summary>
/// 获取单个模型的单帧画面
/// </summary>
protected SFMLImageVideoFrame GetFrame(Spine.SpineObject spine) => GetFrame([spine]);
protected SFMLImageVideoFrame GetFrame(SpineObject spine) => GetFrame([spine]);
/// <summary>
/// 获取模型列表的单帧画面
/// </summary>
protected SFMLImageVideoFrame GetFrame(Spine.SpineObject[] spinesToRender)
protected SFMLImageVideoFrame GetFrame(SpineObject[] spinesToRender)
{
// RenderTexture 必须临时创建, 随用随取, 防止出现跨线程的情况
using var texPma = GetRenderTexture();
@@ -149,12 +148,12 @@ namespace SpineViewer.Exporter
/// <summary>
/// 每个模型在同一个画面进行导出
/// </summary>
protected abstract void ExportSingle(Spine.SpineObject[] spinesToRender, BackgroundWorker? worker = null);
protected abstract void ExportSingle(SpineObject[] spinesToRender, BackgroundWorker? worker = null);
/// <summary>
/// 每个模型独立导出
/// </summary>
protected abstract void ExportIndividual(Spine.SpineObject[] spinesToRender, BackgroundWorker? worker = null);
protected abstract void ExportIndividual(SpineObject[] spinesToRender, BackgroundWorker? worker = null);
/// <summary>
/// 检查参数是否合法并规范化参数值, 否则返回用户错误原因
@@ -178,7 +177,7 @@ namespace SpineViewer.Exporter
/// <param name="spines">要进行导出的 Spine 列表</param>
/// <param name="worker">用来执行该函数的 worker</param>
/// <exception cref="ArgumentException"></exception>
public virtual void Export(Spine.SpineObject[] spines, BackgroundWorker? worker = null)
public virtual void Export(SpineObject[] spines, BackgroundWorker? worker = null)
{
if (Validate() is string err)
throw new ArgumentException(err);
@@ -191,4 +190,53 @@ namespace SpineViewer.Exporter
logger.LogCurrentProcessMemoryUsage();
}
}
/// <summary>
/// 用于在 PropertyGrid 上提供用户操作接口的包装类
/// </summary>
public class ExporterProperty(Exporter exporter)
{
[Browsable(false)]
public virtual Exporter Exporter { get; } = exporter;
/// <summary>
/// 输出文件夹
/// </summary>
[Editor(typeof(FolderNameEditor), typeof(UITypeEditor))]
[Category("[0] "), DisplayName(""), Description("")]
public string? OutputDir { get => Exporter.OutputDir; set => Exporter.OutputDir = value; }
/// <summary>
/// 导出单个
/// </summary>
[Category("[0] "), DisplayName(""), Description("")]
public bool IsExportSingle { get => Exporter.IsExportSingle; set => Exporter.IsExportSingle = value; }
/// <summary>
/// 画面分辨率
/// </summary>
[TypeConverter(typeof(SizeConverter))]
[Category("[0] "), DisplayName(""), Description("")]
public Size Resolution { get => Exporter.Resolution; }
/// <summary>
/// 渲染视窗
/// </summary>
[Category("[0] "), DisplayName(""), Description("")]
public SFML.Graphics.View View { get => Exporter.View; }
/// <summary>
/// 是否仅渲染选中
/// </summary>
[Category("[0] "), DisplayName(""), Description("")]
public bool RenderSelectedOnly { get => Exporter.RenderSelectedOnly; }
/// <summary>
/// 背景颜色
/// </summary>
[Editor(typeof(SFMLColorEditor), typeof(UITypeEditor))]
[TypeConverter(typeof(SFMLColorConverter))]
[Category("[0] "), DisplayName(""), Description("使, #RRGGBBAA")]
public SFML.Graphics.Color BackgroundColor { get => Exporter.BackgroundColor; set => Exporter.BackgroundColor = value; }
}
}

View File

@@ -8,7 +8,7 @@ using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;
namespace SpineViewer.Exporter
namespace SpineViewer.Spine.SpineExporter
{
/// <summary>
/// 使用 FFmpeg 的视频导出器
@@ -51,7 +51,7 @@ namespace SpineViewer.Exporter
return null;
}
protected override void ExportSingle(Spine.SpineObject[] spinesToRender, BackgroundWorker? worker = null)
protected override void ExportSingle(SpineObject[] spinesToRender, BackgroundWorker? worker = null)
{
var noteSuffix = FileNameNoteSuffix;
if (!string.IsNullOrWhiteSpace(noteSuffix)) noteSuffix = $"_{noteSuffix}";
@@ -76,7 +76,7 @@ namespace SpineViewer.Exporter
}
}
protected override void ExportIndividual(Spine.SpineObject[] spinesToRender, BackgroundWorker? worker = null)
protected override void ExportIndividual(SpineObject[] spinesToRender, BackgroundWorker? worker = null)
{
var noteSuffix = FileNameNoteSuffix;
if (!string.IsNullOrWhiteSpace(noteSuffix)) noteSuffix = $"_{noteSuffix}";
@@ -108,4 +108,28 @@ namespace SpineViewer.Exporter
}
}
}
public class FFmpegVideoExporterProperty(FFmpegVideoExporter exporter) : VideoExporterProperty(exporter)
{
[Browsable(false)]
public override FFmpegVideoExporter Exporter => (FFmpegVideoExporter)base.Exporter;
/// <summary>
/// 文件格式
/// </summary>
[Category("[2] FFmpeg "), DisplayName(""), Description("-f, ")]
public virtual string Format => Exporter.Format;
/// <summary>
/// 文件名后缀
/// </summary>
[Category("[2] FFmpeg "), DisplayName(""), Description("")]
public virtual string Suffix => Exporter.Suffix;
/// <summary>
/// 文件名后缀
/// </summary>
[Category("[2] FFmpeg "), DisplayName(""), Description("使 \"ffmpeg -h encoder=<编码器>\" 查看编码器支持的参数\n使用 \"ffmpeg -h muxer=<文件格式>\" 查看文件格式支持的参数")]
public string CustomArgument { get => Exporter.CustomArgument; set => Exporter.CustomArgument = value; }
}
}

View File

@@ -7,7 +7,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Exporter
namespace SpineViewer.Spine.SpineExporter
{
/// <summary>
/// 单帧画面导出器
@@ -43,7 +43,7 @@ namespace SpineViewer.Exporter
}
private SizeF dpi = new(144, 144);
protected override void ExportSingle(Spine.SpineObject[] spinesToRender, BackgroundWorker? worker = null)
protected override void ExportSingle(SpineObject[] spinesToRender, BackgroundWorker? worker = null)
{
// 导出单个时必定提供输出文件夹
var filename = $"frame_{timestamp}{ImageFormat.GetSuffix()}";
@@ -65,7 +65,7 @@ namespace SpineViewer.Exporter
worker?.ReportProgress(100, $"已处理 1/1");
}
protected override void ExportIndividual(Spine.SpineObject[] spinesToRender, BackgroundWorker? worker = null)
protected override void ExportIndividual(SpineObject[] spinesToRender, BackgroundWorker? worker = null)
{
int total = spinesToRender.Length;
int success = 0;
@@ -104,4 +104,30 @@ namespace SpineViewer.Exporter
logger.Info("{} frames saved successfully", success);
}
}
public class FrameExporterProperty(FrameExporter exporter) : ExporterProperty(exporter)
{
[Browsable(false)]
public override FrameExporter Exporter => (FrameExporter)base.Exporter;
/// <summary>
/// 单帧画面格式
/// </summary>
[TypeConverter(typeof(ImageFormatConverter))]
[Category("[1] "), DisplayName("")]
public ImageFormat ImageFormat { get => Exporter.ImageFormat; set => Exporter.ImageFormat = value; }
/// <summary>
/// 文件名后缀
/// </summary>
[Category("[1] "), DisplayName(""), Description("")]
public string Suffix { get => Exporter.ImageFormat.GetSuffix(); }
/// <summary>
/// DPI
/// </summary>
[TypeConverter(typeof(SizeFConverter))]
[Category("[1] "), DisplayName("DPI"), Description("")]
public SizeF DPI { get => Exporter.DPI; set => Exporter.DPI = value; }
}
}

View File

@@ -1,4 +1,5 @@
using SpineViewer.Spine;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
@@ -6,7 +7,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Exporter
namespace SpineViewer.Spine.SpineExporter
{
/// <summary>
/// 帧序列导出器
@@ -18,7 +19,7 @@ namespace SpineViewer.Exporter
/// </summary>
public string Suffix { get; set; } = ".png";
protected override void ExportSingle(Spine.SpineObject[] spinesToRender, BackgroundWorker? worker = null)
protected override void ExportSingle(SpineObject[] spinesToRender, BackgroundWorker? worker = null)
{
// 导出单个时必定提供输出文件夹,
var saveDir = Path.Combine(OutputDir, $"frames_{timestamp}_{FPS:f0}");
@@ -47,7 +48,7 @@ namespace SpineViewer.Exporter
}
}
protected override void ExportIndividual(Spine.SpineObject[] spinesToRender, BackgroundWorker? worker = null)
protected override void ExportIndividual(SpineObject[] spinesToRender, BackgroundWorker? worker = null)
{
foreach (var spine in spinesToRender)
{
@@ -82,4 +83,17 @@ namespace SpineViewer.Exporter
}
}
}
public class FrameSequenceExporterProperty(VideoExporter exporter) : VideoExporterProperty(exporter)
{
[Browsable(false)]
public override FrameSequenceExporter Exporter => (FrameSequenceExporter)base.Exporter;
/// <summary>
/// 文件名后缀
/// </summary>
[TypeConverter(typeof(StringEnumConverter)), StringEnumConverter.StandardValues(".png", ".jpg", ".tga", ".bmp")]
[Category("[2] "), DisplayName(""), Description("")]
public string Suffix { get => Exporter.Suffix; set => Exporter.Suffix = value; }
}
}

View File

@@ -6,7 +6,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Exporter
namespace SpineViewer.Spine.SpineExporter
{
/// <summary>
/// GIF 导出参数
@@ -52,4 +52,28 @@ namespace SpineViewer.Exporter
options.WithCustomArgument(customArgs);
}
}
class GifExporterProperty(FFmpegVideoExporter exporter) : FFmpegVideoExporterProperty(exporter)
{
[Browsable(false)]
public override GifExporter Exporter => (GifExporter)base.Exporter;
/// <summary>
/// 调色板最大颜色数量
/// </summary>
[Category("[3] "), DisplayName(""), Description("使, ")]
public uint MaxColors { get => Exporter.MaxColors; set => Exporter.MaxColors = value; }
/// <summary>
/// 透明度阈值
/// </summary>
[Category("[3] "), DisplayName(""), Description("")]
public byte AlphaThreshold { get => Exporter.AlphaThreshold; set => Exporter.AlphaThreshold = value; }
/// <summary>
/// 透明度阈值
/// </summary>
[Category("[3] "), DisplayName(""), Description("-loop, , -1 , 0 , [-1, 65535]")]
public int Loop { get => Exporter.Loop; set => Exporter.Loop = value; }
}
}

View File

@@ -1,4 +1,5 @@
using SpineViewer.Exporter;
using FFMpegCore;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
@@ -6,9 +7,48 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.PropertyGridWrappers.Exporter
namespace SpineViewer.Spine.SpineExporter
{
public class MkvExporterWrapper(FFmpegVideoExporter exporter) : FFmpegVideoExporterWrapper(exporter)
/// <summary>
/// MKV 导出参数
/// </summary>
public class MkvExporter : FFmpegVideoExporter
{
public MkvExporter()
{
BackgroundColor = new(0, 255, 0);
}
public override string Format => "matroska";
public override string Suffix => ".mkv";
/// <summary>
/// 编码器
/// </summary>
public string Codec { get; set; } = "libx265";
/// <summary>
/// CRF
/// </summary>
public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); }
private int crf = 23;
/// <summary>
/// 像素格式
/// </summary>
public string PixelFormat { get; set; } = "yuv444p";
public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
base.SetOutputOptions(options);
options.WithVideoCodec(Codec).WithConstantRateFactor(CRF).ForcePixelFormat(PixelFormat);
}
}
public class MkvExporterProperty(FFmpegVideoExporter exporter) : FFmpegVideoExporterProperty(exporter)
{
[Browsable(false)]
public override MkvExporter Exporter => (MkvExporter)base.Exporter;

View File

@@ -1,4 +1,5 @@
using SpineViewer.Exporter;
using FFMpegCore;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
@@ -6,9 +7,47 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.PropertyGridWrappers.Exporter
namespace SpineViewer.Spine.SpineExporter
{
public class MovExporterWrapper(FFmpegVideoExporter exporter) : FFmpegVideoExporterWrapper(exporter)
/// <summary>
/// MOV 导出参数
/// </summary>
public class MovExporter : FFmpegVideoExporter
{
public MovExporter()
{
BackgroundColor = new(0, 255, 0);
}
public override string Format => "mov";
public override string Suffix => ".mov";
/// <summary>
/// 编码器
/// </summary>
public string Codec { get; set; } = "prores_ks";
/// <summary>
/// 预设
/// </summary>
public string Profile { get; set; } = "auto";
/// <summary>
/// 像素格式
/// </summary>
public string PixelFormat { get; set; } = "yuva444p10le";
public override string FileNameNoteSuffix => $"{Codec}_{Profile}_{PixelFormat}";
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
base.SetOutputOptions(options);
options.WithFastStart().WithVideoCodec(Codec).WithCustomArgument($"-profile {Profile}").ForcePixelFormat(PixelFormat);
}
}
public class MovExporterProperty(FFmpegVideoExporter exporter) : FFmpegVideoExporterProperty(exporter)
{
[Browsable(false)]
public override MovExporter Exporter => (MovExporter)base.Exporter;

View File

@@ -1,4 +1,5 @@
using SpineViewer.Exporter;
using FFMpegCore;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
@@ -6,9 +7,48 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.PropertyGridWrappers.Exporter
namespace SpineViewer.Spine.SpineExporter
{
public class Mp4ExporterWrapper(FFmpegVideoExporter exporter) : FFmpegVideoExporterWrapper(exporter)
/// <summary>
/// MP4 导出参数
/// </summary>
public class Mp4Exporter : FFmpegVideoExporter
{
public Mp4Exporter()
{
BackgroundColor = new(0, 255, 0);
}
public override string Format => "mp4";
public override string Suffix => ".mp4";
/// <summary>
/// 编码器
/// </summary>
public string Codec { get; set; } = "libx264";
/// <summary>
/// CRF
/// </summary>
public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); }
private int crf = 23;
/// <summary>
/// 像素格式
/// </summary>
public string PixelFormat { get; set; } = "yuv444p";
public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
base.SetOutputOptions(options);
options.WithFastStart().WithVideoCodec(Codec).WithConstantRateFactor(CRF).ForcePixelFormat(PixelFormat);
}
}
public class Mp4ExporterProperty(FFmpegVideoExporter exporter) : FFmpegVideoExporterProperty(exporter)
{
[Browsable(false)]
public override Mp4Exporter Exporter => (Mp4Exporter)base.Exporter;

View File

@@ -6,7 +6,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Exporter
namespace SpineViewer.Spine.SpineExporter
{
/// <summary>
/// 视频导出基类
@@ -41,7 +41,7 @@ namespace SpineViewer.Exporter
/// <summary>
/// 生成单个模型的帧序列
/// </summary>
protected IEnumerable<SFMLImageVideoFrame> GetFrames(Spine.SpineObject spine, BackgroundWorker? worker = null)
protected IEnumerable<SFMLImageVideoFrame> GetFrames(SpineObject spine, BackgroundWorker? worker = null)
{
// 独立导出时如果 Duration 小于 0 则使用所有轨道上动画时长最大值
var duration = Duration;
@@ -51,7 +51,7 @@ namespace SpineViewer.Exporter
int total = (int)(duration * FPS); // 完整帧的数量
float deltaFinal = duration - delta * total; // 最后一帧时长
int final = (KeepLast && (deltaFinal > 1e-3)) ? 1 : 0;
int final = KeepLast && deltaFinal > 1e-3 ? 1 : 0;
int frameCount = 1 + total + final; // 所有帧的数量 = 起始帧 + 完整帧 + 最后一帧
@@ -90,7 +90,7 @@ namespace SpineViewer.Exporter
/// <summary>
/// 生成多个模型的帧序列
/// </summary>
protected IEnumerable<SFMLImageVideoFrame> GetFrames(Spine.SpineObject[] spinesToRender, BackgroundWorker? worker = null)
protected IEnumerable<SFMLImageVideoFrame> GetFrames(SpineObject[] spinesToRender, BackgroundWorker? worker = null)
{
// 导出单个时必须根据 Duration 决定导出时长
var duration = Duration;
@@ -99,7 +99,7 @@ namespace SpineViewer.Exporter
int total = (int)(duration * FPS); // 完整帧的数量
float deltaFinal = duration - delta * total; // 最后一帧时长
int final = (KeepLast && (deltaFinal > 1e-3)) ? 1 : 0;
int final = KeepLast && deltaFinal > 1e-3 ? 1 : 0;
int frameCount = 1 + total + final; // 所有帧的数量 = 起始帧 + 完整帧 + 最后一帧
@@ -135,11 +135,35 @@ namespace SpineViewer.Exporter
}
}
public override void Export(Spine.SpineObject[] spines, BackgroundWorker? worker = null)
public override void Export(SpineObject[] spines, BackgroundWorker? worker = null)
{
// 导出视频格式需要把模型时间都重置到 0
foreach (var spine in spines) spine.ResetAnimationsTime();
base.Export(spines, worker);
}
}
public class VideoExporterProperty(VideoExporter exporter) : ExporterProperty(exporter)
{
[Browsable(false)]
public override VideoExporter Exporter => (VideoExporter)base.Exporter;
/// <summary>
/// 导出时长
/// </summary>
[Category("[1] "), DisplayName(""), Description(", 0, 使")]
public float Duration { get => Exporter.Duration; set => Exporter.Duration = value; }
/// <summary>
/// 帧率
/// </summary>
[Category("[1] "), DisplayName(""), Description("")]
public float FPS { get => Exporter.FPS; set => Exporter.FPS = value; }
/// <summary>
/// 保留最后一帧
/// </summary>
[Category("[1] "), DisplayName(""), Description(", , 1")]
public bool KeepLast { get => Exporter.KeepLast; set => Exporter.KeepLast = value; }
}
}

View File

@@ -1,4 +1,5 @@
using SpineViewer.Exporter;
using FFMpegCore;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
@@ -6,9 +7,49 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.PropertyGridWrappers.Exporter
namespace SpineViewer.Spine.SpineExporter
{
public class WebmExporterWrapper(WebmExporter exporter) : FFmpegVideoExporterWrapper(exporter)
/// <summary>
/// WebM 导出参数
/// </summary>
public class WebmExporter : FFmpegVideoExporter
{
public WebmExporter()
{
// 默认用透明黑背景
BackgroundColor = new(0, 0, 0, 0);
}
public override string Format => "webm";
public override string Suffix => ".webm";
/// <summary>
/// 编码器
/// </summary>
public string Codec { get; set; } = "libvpx-vp9";
/// <summary>
/// CRF
/// </summary>
public int CRF { get => crf; set => crf = Math.Clamp(value, 0, 63); }
private int crf = 23;
/// <summary>
/// 像素格式
/// </summary>
public string PixelFormat { get; set; } = "yuva420p";
public override string FileNameNoteSuffix => $"{Codec}_{CRF}_{PixelFormat}";
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
base.SetOutputOptions(options);
options.WithVideoCodec(Codec).WithConstantRateFactor(CRF).ForcePixelFormat(PixelFormat);
}
}
public class WebmExporterProperty(WebmExporter exporter) : FFmpegVideoExporterProperty(exporter)
{
[Browsable(false)]
public override WebmExporter Exporter => (WebmExporter)base.Exporter;

View File

@@ -1,4 +1,5 @@
using SpineViewer.Exporter;
using FFMpegCore;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
@@ -6,9 +7,59 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.PropertyGridWrappers.Exporter
namespace SpineViewer.Spine.SpineExporter
{
public class WebpExporterWrapper(WebpExporter exporter) : FFmpegVideoExporterWrapper(exporter)
/// <summary>
/// MP4 导出参数
/// </summary>
public class WebpExporter : FFmpegVideoExporter
{
public WebpExporter()
{
FPS = 24;
}
public override string Format => "webp";
public override string Suffix => ".webp";
/// <summary>
/// 编码器
/// </summary>
public string Codec { get; set; } = "libwebp_anim";
/// <summary>
/// 是否无损
/// </summary>
public bool Lossless { get; set; } = false;
/// <summary>
/// 质量
/// </summary>
public int Quality { get => quality; set => quality = Math.Clamp(value, 0, 100); }
private int quality = 75;
/// <summary>
/// 像素格式
/// </summary>
public string PixelFormat { get; set; } = "yuva420p";
/// <summary>
/// 循环次数, 0 无限循环, 取值范围 [0, 65535]
/// </summary>
public int Loop { get => loop; set => loop = Math.Clamp(value, 0, 65535); }
private int loop = 0;
public override string FileNameNoteSuffix => $"{Codec}_{Quality}_{PixelFormat}";
public override void SetOutputOptions(FFMpegArgumentOptions options)
{
base.SetOutputOptions(options);
options.WithVideoCodec(Codec).ForcePixelFormat(PixelFormat).WithCustomArgument($"-lossless {(Lossless ? 1 : 0)} -quality {Quality} -loop {Loop}");
}
}
public class WebpExporterProperty(WebpExporter exporter) : FFmpegVideoExporterProperty(exporter)
{
[Browsable(false)]
public override WebpExporter Exporter => (WebpExporter)base.Exporter;

View File

@@ -1,5 +1,4 @@
using SpineViewer.Exporter;
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

View File

@@ -10,7 +10,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.PropertyGridWrappers
namespace SpineViewer.Utils
{
public class PointFConverter : ExpandableObjectConverter
{

View File

@@ -9,7 +9,7 @@ using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms.Design;
namespace SpineViewer.PropertyGridWrappers
namespace SpineViewer.Utils
{
/// <summary>
/// 使用 FolderBrowserDialog 的文件夹路径编辑器