From 0abe063899c1cd4e63d9cc0cf9bc4879d4df5076 Mon Sep 17 00:00:00 2001 From: ww-rm Date: Thu, 2 Oct 2025 15:18:40 +0800 Subject: [PATCH 01/15] =?UTF-8?q?=E8=B0=83=E6=95=B4=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Spine/{Interfaces => }/SpineExtension.cs | 9 +++++---- SpineViewer/Extensions/SpineObjectExtension.cs | 1 - SpineViewer/Models/PreferenceModel.cs | 2 +- .../ViewModels/MainWindow/ExplorerListViewModel.cs | 1 - SpineViewer/ViewModels/MainWindow/PreferenceViewModel.cs | 2 +- SpineViewerCLI/SpineViewerCLI.cs | 1 - 6 files changed, 7 insertions(+), 9 deletions(-) rename Spine/{Interfaces => }/SpineExtension.cs (97%) diff --git a/Spine/Interfaces/SpineExtension.cs b/Spine/SpineExtension.cs similarity index 97% rename from Spine/Interfaces/SpineExtension.cs rename to Spine/SpineExtension.cs index 218c8fc..efecba4 100644 --- a/Spine/Interfaces/SpineExtension.cs +++ b/Spine/SpineExtension.cs @@ -1,4 +1,5 @@ using NLog; +using Spine.Interfaces; using Spine.Interfaces.Attachments; using System; using System.Collections.Generic; @@ -6,7 +7,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace Spine.Interfaces +namespace Spine { /// /// 命中测试等级枚举值 @@ -136,7 +137,7 @@ namespace Spine.Interfaces if (HitTestLevel == HitTestLevel.None || HitTestLevel == HitTestLevel.Bounds) { self.GetBounds(out var bx, out var by, out var bw, out var bh); - return x >= bx && x <= (bx + bw) && y >= by && y <= (by + bh); + return x >= bx && x <= bx + bw && y >= by && y <= by + bh; } else if (HitTestLevel == HitTestLevel.Meshes || HitTestLevel == HitTestLevel.Pixels) { @@ -179,7 +180,7 @@ namespace Spine.Interfaces float c2 = Cross(x2, y2, x0, y0); // 判断是否全部同号 (或为 0, 点在边上) - if ((c0 >= 0 && c1 >= 0 && c2 >= 0) || (c0 <= 0 && c1 <= 0 && c2 <= 0)) + if (c0 >= 0 && c1 >= 0 && c2 >= 0 || c0 <= 0 && c1 <= 0 && c2 <= 0) { if (HitTestLevel == HitTestLevel.Meshes) return true; @@ -229,7 +230,7 @@ namespace Spine.Interfaces if (HitTestLevel == HitTestLevel.None) { self.GetBounds(out var bx, out var by, out var bw, out var bh); - return x >= bx && x <= (bx + bw) && y >= by && y <= (by + bh); + return x >= bx && x <= bx + bw && y >= by && y <= by + bh; } bool hit = false; diff --git a/SpineViewer/Extensions/SpineObjectExtension.cs b/SpineViewer/Extensions/SpineObjectExtension.cs index ca24299..9d8d703 100644 --- a/SpineViewer/Extensions/SpineObjectExtension.cs +++ b/SpineViewer/Extensions/SpineObjectExtension.cs @@ -1,5 +1,4 @@ using Spine; -using Spine.Interfaces; using System; using System.Collections.Generic; using System.Linq; diff --git a/SpineViewer/Models/PreferenceModel.cs b/SpineViewer/Models/PreferenceModel.cs index a4336b2..2ae88ec 100644 --- a/SpineViewer/Models/PreferenceModel.cs +++ b/SpineViewer/Models/PreferenceModel.cs @@ -1,6 +1,6 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using Spine.Interfaces; +using Spine; using SpineViewer.Services; using System; using System.Collections.Generic; diff --git a/SpineViewer/ViewModels/MainWindow/ExplorerListViewModel.cs b/SpineViewer/ViewModels/MainWindow/ExplorerListViewModel.cs index a4860de..0f85b90 100644 --- a/SpineViewer/ViewModels/MainWindow/ExplorerListViewModel.cs +++ b/SpineViewer/ViewModels/MainWindow/ExplorerListViewModel.cs @@ -20,7 +20,6 @@ using System.Windows; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Shell; -using Spine.Interfaces; namespace SpineViewer.ViewModels.MainWindow { diff --git a/SpineViewer/ViewModels/MainWindow/PreferenceViewModel.cs b/SpineViewer/ViewModels/MainWindow/PreferenceViewModel.cs index fa461c4..b2f5530 100644 --- a/SpineViewer/ViewModels/MainWindow/PreferenceViewModel.cs +++ b/SpineViewer/ViewModels/MainWindow/PreferenceViewModel.cs @@ -2,8 +2,8 @@ using CommunityToolkit.Mvvm.Input; using Microsoft.Win32; using NLog; +using Spine; using Spine.Implementations; -using Spine.Interfaces; using SpineViewer.Models; using SpineViewer.Natives; using SpineViewer.Services; diff --git a/SpineViewerCLI/SpineViewerCLI.cs b/SpineViewerCLI/SpineViewerCLI.cs index 6397632..2c91c5e 100644 --- a/SpineViewerCLI/SpineViewerCLI.cs +++ b/SpineViewerCLI/SpineViewerCLI.cs @@ -4,7 +4,6 @@ using SFML.Graphics; using SFML.System; using Spine; using Spine.Exporters; -using Spine.Interfaces; namespace SpineViewerCLI { From 2204eb6c7576eaa65804e53fb00f4adebeab8455 Mon Sep 17 00:00:00 2001 From: ww-rm Date: Thu, 2 Oct 2025 22:18:05 +0800 Subject: [PATCH 02/15] =?UTF-8?q?=E5=A2=9E=E5=8A=A0apng=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E5=B9=B6=E4=B8=94=E8=B0=83=E6=95=B4=E9=83=A8=E5=88=86=E5=B8=83?= =?UTF-8?q?=E5=B1=80=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Spine/Exporters/FFmpegVideoExporter.cs | 25 +- SpineViewer/Resources/Strings/en.xaml | 4 +- SpineViewer/Resources/Strings/ja.xaml | 4 +- SpineViewer/Resources/Strings/zh.xaml | 4 +- .../Exporters/FFmpegVideoExporterViewModel.cs | 43 +- SpineViewer/Views/AboutDialog.xaml | 42 +- SpineViewer/Views/DiagnosticsDialog.xaml | 7 +- .../CustomFFmpegExporterDialog.xaml | 286 +++++---- .../FFmpegVideoExporterDialog.xaml | 262 +++++--- .../ExporterDialogs/FrameExporterDialog.xaml | 153 +++-- .../FrameSequenceExporterDialog.xaml | 175 ++++-- SpineViewer/Views/MainWindow.xaml | 566 +++++++++++------- SpineViewer/Views/PreferenceDialog.xaml | 48 +- 13 files changed, 1033 insertions(+), 586 deletions(-) diff --git a/Spine/Exporters/FFmpegVideoExporter.cs b/Spine/Exporters/FFmpegVideoExporter.cs index e3fd2c3..ea04666 100644 --- a/Spine/Exporters/FFmpegVideoExporter.cs +++ b/Spine/Exporters/FFmpegVideoExporter.cs @@ -28,6 +28,7 @@ namespace Spine.Exporters { Gif, Webp, + Apng, Mp4, Webm, Mkv, @@ -41,31 +42,37 @@ namespace Spine.Exporters private VideoFormat _format = VideoFormat.Mp4; /// - /// 动图是否循环 [Gif/Webp] + /// [Gif/Webp/Apng] 动图是否循环 /// public bool Loop { get => _loop; set => _loop = value; } private bool _loop = true; /// - /// 质量 [Webp] + /// [Webp] 质量 /// public int Quality { get => _quality; set => _quality = Math.Clamp(value, 0, 100); } private int _quality = 75; /// - /// 无损压缩 [Webp] + /// [Webp] 无损压缩 /// public bool Lossless { get => _lossless; set => _lossless = value; } private bool _lossless = false; /// - /// CRF [Mp4/Webm/Mkv] + /// [Apng] 预测器算法, 取值范围 0-5, 分别对应 none, sub, up, avg, paeth, mixed + /// + public int ApngPred { get => _apngPred; set => _apngPred = Math.Clamp(value, 0, 5); } + private int _apngPred = 5; + + /// + /// [Mp4/Webm/Mkv] CRF /// public int Crf { get => _crf; set => _crf = Math.Clamp(value, 0, 63); } private int _crf = 23; /// - /// prores_ks 编码器的配置等级, -1 是自动, 越高质量越好, 只有 4 及以上才有透明通道 [Mov] + /// [Mov] prores_ks 编码器的配置等级, -1 是自动, 越高质量越好, 只有 4 及以上才有透明通道 /// public int Profile { get => _profile; set => _profile = Math.Clamp(value, -1, 5); } private int _profile = 5; @@ -93,6 +100,7 @@ namespace Spine.Exporters { VideoFormat.Gif => SetGifOptions, VideoFormat.Webp => SetWebpOptions, + VideoFormat.Apng => SetApngOptions, VideoFormat.Mp4 => SetMp4Options, VideoFormat.Webm => SetWebmOptions, VideoFormat.Mkv => SetMkvOptions, @@ -132,6 +140,13 @@ namespace Spine.Exporters .WithCustomArgument(customArgs); } + private void SetApngOptions(FFMpegArgumentOptions options) + { + var customArgs = $"-vf unpremultiply=inplace=1 -plays {(_loop ? 0 : 1)} -pred {_apngPred}"; + options.ForceFormat("apng").WithVideoCodec("apng").ForcePixelFormat("rgba") + .WithCustomArgument(customArgs); + } + private void SetMp4Options(FFMpegArgumentOptions options) { // XXX: windows 默认播放器在播放 MP4 格式时对于 libx264 编码器只支持 yuv420p 的像素格式 diff --git a/SpineViewer/Resources/Strings/en.xaml b/SpineViewer/Resources/Strings/en.xaml index be3fd86..0ddc1af 100644 --- a/SpineViewer/Resources/Strings/en.xaml +++ b/SpineViewer/Resources/Strings/en.xaml @@ -197,11 +197,13 @@ Video Format Loop Play - [Gif/Webp] Whether the animation loops + [Gif/Webp/Apng] Whether the animation loops Quality Parameter [Webp] Quality parameter, range 0-100, higher value means better quality Lossless Compression [Webp] Lossless compression, quality parameter will be ignored + Predictor Method + [Apng] Pred parameter, value range 0-5, corresponding to different encoding strategies: none, sub, up, avg, paeth, and mixed. It affects encoding time and file size. CRF Parameter [Mp4/Webm/Mkv] CRF parameter, range 0-63, lower value means higher quality Profile Parameter diff --git a/SpineViewer/Resources/Strings/ja.xaml b/SpineViewer/Resources/Strings/ja.xaml index 6790f47..39ce6bc 100644 --- a/SpineViewer/Resources/Strings/ja.xaml +++ b/SpineViewer/Resources/Strings/ja.xaml @@ -197,11 +197,13 @@ ビデオフォーマット ループ再生 - [Gif/Webp] アニメーションをループ再生するかどうか + [Gif/Webp/Apng] アニメーションをループ再生するかどうか 品質パラメータ [Webp] 品質パラメータ、範囲は0-100。値が高いほど品質が良い 無損失圧縮 [Webp] 無損失圧縮、品質パラメータは無視されます + 予測器方式 + [Apng] Pred パラメータ。値の範囲は 0~5 で、それぞれ none、sub、up、avg、paeth、mixed の異なるエンコード方式に対応します。 エンコード時間とファイルサイズに影響します。 CRF パラメータ [Mp4/Webm/Mkv] CRF パラメータ、範囲0-63。値が小さいほど品質が高い プロファイルパラメータ diff --git a/SpineViewer/Resources/Strings/zh.xaml b/SpineViewer/Resources/Strings/zh.xaml index 200e717..9eef84f 100644 --- a/SpineViewer/Resources/Strings/zh.xaml +++ b/SpineViewer/Resources/Strings/zh.xaml @@ -197,11 +197,13 @@ 视频格式 循环播放 - [Gif/Webp] 动图是否循环播放 + [Gif/Webp/Apng] 动图是否循环播放 质量参数 [Webp] 质量参数,取值范围 0-100,越高质量越好 无损压缩 [Webp] 无损压缩,会忽略质量参数 + 预测器方法 + [Apng] Pred 参数,取值范围 0-5,分别对应 none、sub、up、avg、paeth、mixed 几种不同的编码策略, 影响编码时间和文件大小 CRF 参数 [Mp4/Webm/Mkv] CRF 参数,取值范围 0-63,越小质量越高 Profile 参数 diff --git a/SpineViewer/ViewModels/Exporters/FFmpegVideoExporterViewModel.cs b/SpineViewer/ViewModels/Exporters/FFmpegVideoExporterViewModel.cs index ed9d098..c11b986 100644 --- a/SpineViewer/ViewModels/Exporters/FFmpegVideoExporterViewModel.cs +++ b/SpineViewer/ViewModels/Exporters/FFmpegVideoExporterViewModel.cs @@ -20,24 +20,64 @@ namespace SpineViewer.ViewModels.Exporters { public static ImmutableArray VideoFormatOptions { get; } = Enum.GetValues().ToImmutableArray(); - public FFmpegVideoExporter.VideoFormat Format { get => _format; set => SetProperty(ref _format, value); } + public FFmpegVideoExporter.VideoFormat Format + { + get => _format; + set + { + if (SetProperty(ref _format, value)) + { + OnPropertyChanged(nameof(EnableParamLoop)); + OnPropertyChanged(nameof(EnableParamQuality)); + OnPropertyChanged(nameof(EnableParamLossless)); + OnPropertyChanged(nameof(EnableParamApngPred)); + OnPropertyChanged(nameof(EnableParamCrf)); + OnPropertyChanged(nameof(EnableParamProfile)); + } + } + } protected FFmpegVideoExporter.VideoFormat _format = FFmpegVideoExporter.VideoFormat.Mp4; public bool Loop { get => _loop; set => SetProperty(ref _loop, value); } protected bool _loop = true; + public bool EnableParamLoop => + _format == FFmpegVideoExporter.VideoFormat.Gif || + _format == FFmpegVideoExporter.VideoFormat.Webp || + _format == FFmpegVideoExporter.VideoFormat.Apng; + public int Quality { get => _quality; set => SetProperty(ref _quality, Math.Clamp(value, 0, 100)); } protected int _quality = 75; + public bool EnableParamQuality => + _format == FFmpegVideoExporter.VideoFormat.Webp; + public bool Lossless { get => _lossless; set => SetProperty(ref _lossless, value); } protected bool _lossless = false; + public bool EnableParamLossless => + _format == FFmpegVideoExporter.VideoFormat.Webp; + + public int ApngPred { get => _apngPred; set => SetProperty(ref _apngPred, Math.Clamp(value, 0, 5)); } + protected int _apngPred = 5; + + public bool EnableParamApngPred => + _format == FFmpegVideoExporter.VideoFormat.Apng; + public int Crf { get => _crf; set => SetProperty(ref _crf, Math.Clamp(value, 0, 63)); } protected int _crf = 23; + public bool EnableParamCrf => + _format == FFmpegVideoExporter.VideoFormat.Mp4 || + _format == FFmpegVideoExporter.VideoFormat.Webm || + _format == FFmpegVideoExporter.VideoFormat.Mkv; + public int Profile { get => _profile; set => SetProperty(ref _profile, Math.Clamp(value, -1, 5)); } protected int _profile = 5; + public bool EnableParamProfile => + _format == FFmpegVideoExporter.VideoFormat.Mov; + private string FormatSuffix => $".{_format.ToString().ToLower()}"; protected override void Export(SpineObjectModel[] models) @@ -63,6 +103,7 @@ namespace SpineViewer.ViewModels.Exporters Loop = _loop, Quality = _quality, Lossless = _lossless, + ApngPred = _apngPred, Crf = _crf, Profile = _profile, }; diff --git a/SpineViewer/Views/AboutDialog.xaml b/SpineViewer/Views/AboutDialog.xaml index 7fa3eac..3be6fbb 100644 --- a/SpineViewer/Views/AboutDialog.xaml +++ b/SpineViewer/Views/AboutDialog.xaml @@ -26,25 +26,31 @@ - - - - - - - - - -