diff --git a/SpineViewer/Models/LastStateModel.cs b/SpineViewer/Models/LastStateModel.cs
index c61455f..639132a 100644
--- a/SpineViewer/Models/LastStateModel.cs
+++ b/SpineViewer/Models/LastStateModel.cs
@@ -33,6 +33,8 @@ namespace SpineViewer.Models
public float Speed { get; set; } = 1f;
public bool ShowAxis { get; set; } = true;
public Color BackgroundColor { get; set; } = Color.FromRgb(105, 105, 105);
+ public string BackgroundImagePath { get; set; }
+ public Stretch BackgroundImageMode { get; set; } = Stretch.Uniform;
#endregion
diff --git a/SpineViewer/Resources/Strings/en.xaml b/SpineViewer/Resources/Strings/en.xaml
index f77be99..7cdda02 100644
--- a/SpineViewer/Resources/Strings/en.xaml
+++ b/SpineViewer/Resources/Strings/en.xaml
@@ -121,7 +121,8 @@
Render Selected Only
Show Axis
Background Color
- Background Image
+ Background Image Path
+ Background Image Mode
Stop
diff --git a/SpineViewer/Resources/Strings/ja.xaml b/SpineViewer/Resources/Strings/ja.xaml
index 6a84772..5f4ef84 100644
--- a/SpineViewer/Resources/Strings/ja.xaml
+++ b/SpineViewer/Resources/Strings/ja.xaml
@@ -121,7 +121,8 @@
選択のみレンダリング
座標軸を表示
背景色
- 背景画像
+ 背景画像のパス
+ 背景画像のモード
停止
diff --git a/SpineViewer/Resources/Strings/zh.xaml b/SpineViewer/Resources/Strings/zh.xaml
index f138b4c..793fd7e 100644
--- a/SpineViewer/Resources/Strings/zh.xaml
+++ b/SpineViewer/Resources/Strings/zh.xaml
@@ -121,7 +121,8 @@
仅渲染选中
显示坐标轴
背景颜色
- 背景图片
+ 背景图片路径
+ 背景图片模式
停止
diff --git a/SpineViewer/Services/DialogService.cs b/SpineViewer/Services/DialogService.cs
index 6e11135..7587af2 100644
--- a/SpineViewer/Services/DialogService.cs
+++ b/SpineViewer/Services/DialogService.cs
@@ -90,6 +90,22 @@ namespace SpineViewer.Services
return false;
}
+ public static bool ShowOpenSFMLImageDialog(out string? fileName, string initialDirectory = "")
+ {
+ var dialog = new OpenFileDialog()
+ {
+ InitialDirectory = initialDirectory,
+ Filter = "SFML Image|*.png;*.jpg;*.jpeg;*.bmp;*.tga|All|*.*"
+ };
+ if (dialog.ShowDialog() is true)
+ {
+ fileName = dialog.FileName;
+ return true;
+ }
+ fileName = null;
+ return false;
+ }
+
public static bool ShowOpenJsonDialog(out string? fileName, string initialDirectory = "")
{
var dialog = new OpenFileDialog()
diff --git a/SpineViewer/ViewModels/MainWindow/SFMLRendererViewModel.cs b/SpineViewer/ViewModels/MainWindow/SFMLRendererViewModel.cs
index 348c80e..7dc9751 100644
--- a/SpineViewer/ViewModels/MainWindow/SFMLRendererViewModel.cs
+++ b/SpineViewer/ViewModels/MainWindow/SFMLRendererViewModel.cs
@@ -10,8 +10,10 @@ using SpineViewer.Services;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Collections.Specialized;
using System.Diagnostics;
+using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
@@ -23,6 +25,8 @@ namespace SpineViewer.ViewModels.MainWindow
{
public class SFMLRendererViewModel : ObservableObject
{
+ public ImmutableArray StretchOptions { get; } = Enum.GetValues().ToImmutableArray();
+
///
/// 日志器
///
@@ -69,6 +73,13 @@ namespace SpineViewer.ViewModels.MainWindow
private float _forwardDelta = 0;
private readonly object _forwardDeltaLock = new();
+ ///
+ /// 背景图片
+ ///
+ private SFML.Graphics.Sprite? _backgroundImageSprite; // XXX: 暂时未使用 Dispose 释放
+ private SFML.Graphics.Texture? _backgroundImageTexture; // XXX: 暂时未使用 Dispose 释放
+ private readonly object _bgLock = new();
+
///
/// 临时变量, 记录拖放世界源点
///
@@ -169,6 +180,62 @@ namespace SpineViewer.ViewModels.MainWindow
}
private SFML.Graphics.Color _backgroundColor = new(105, 105, 105);
+ public string BackgroundImagePath
+ {
+ get => _backgroundImagePath;
+ set => SetProperty(_backgroundImagePath, value, v =>
+ {
+ if (string.IsNullOrWhiteSpace(v))
+ {
+ lock (_bgLock)
+ {
+ _backgroundImageSprite?.Dispose();
+ _backgroundImageTexture?.Dispose();
+ _backgroundImageTexture = null;
+ _backgroundImageSprite = null;
+ }
+ _backgroundImagePath = v;
+ }
+ else
+ {
+ if (!File.Exists(v))
+ {
+ _logger.Warn("Omit non-existed background image path, {0}", v);
+ return;
+ }
+ SFML.Graphics.Texture tex = null;
+ SFML.Graphics.Sprite sprite = null;
+ try
+ {
+ tex = new(v);
+ sprite = new(tex) { Origin = new(tex.Size.X / 2f, tex.Size.Y / 2f) };
+ lock (_bgLock)
+ {
+ _backgroundImageSprite?.Dispose();
+ _backgroundImageTexture?.Dispose();
+ _backgroundImageTexture = tex;
+ _backgroundImageSprite = sprite;
+ }
+ _backgroundImagePath = v;
+ }
+ catch (Exception ex)
+ {
+ sprite?.Dispose();
+ tex?.Dispose();
+ _logger.Error("Failed to load background image from path: {0}, {1}", v, ex.Message);
+ }
+ }
+ });
+ }
+ private string _backgroundImagePath;
+
+ public Stretch BackgroundImageMode
+ {
+ get => _backgroundImageMode;
+ set => SetProperty(ref _backgroundImageMode, value);
+ }
+ private Stretch _backgroundImageMode = Stretch.Uniform;
+
public bool RenderSelectedOnly
{
get => _renderSelectedOnly;
@@ -189,6 +256,14 @@ namespace SpineViewer.ViewModels.MainWindow
}
private bool _isUpdating = true;
+ public RelayCommand Cmd_SelectBackgroundImage => _cmd_SelectBackgroundImage ??= new(() =>
+ {
+ if (!DialogService.ShowOpenSFMLImageDialog(out var fileName))
+ return;
+ BackgroundImagePath = fileName;
+ });
+ private RelayCommand? _cmd_SelectBackgroundImage;
+
public RelayCommand Cmd_Stop => _cmd_Stop ??= new(() =>
{
IsUpdating = false;
@@ -386,6 +461,38 @@ namespace SpineViewer.ViewModels.MainWindow
_renderer.Clear(_backgroundColor);
+ // 渲染背景
+ lock (_bgLock)
+ {
+ if (_backgroundImageSprite is not null)
+ {
+ using var view = _renderer.GetView();
+ var bg = _backgroundImageSprite;
+ var viewSize = view.Size;
+ var bgSize = bg.Texture.Size;
+ var scaleX = Math.Abs(viewSize.X / bgSize.X);
+ var scaleY = Math.Abs(viewSize.Y / bgSize.Y);
+ var signX = Math.Sign(viewSize.X);
+ var signY = Math.Sign(viewSize.Y);
+ if (_backgroundImageMode == Stretch.None)
+ {
+ scaleX = scaleY = 1f / _renderer.Zoom;
+ }
+ else if (_backgroundImageMode == Stretch.Uniform)
+ {
+ scaleX = scaleY = Math.Min(scaleX, scaleY);
+ }
+ else if (_backgroundImageMode == Stretch.UniformToFill)
+ {
+ scaleX = scaleY = Math.Max(scaleX, scaleY);
+ }
+ bg.Scale = new(signX * scaleX, signY * scaleY);
+ bg.Position = view.Center;
+ bg.Rotation = view.Rotation;
+ _renderer.Draw(bg);
+ }
+ }
+
if (_showAxis)
{
// 画一个很长的坐标轴, 用 1e9 比较合适
diff --git a/SpineViewer/Views/MainWindow.xaml b/SpineViewer/Views/MainWindow.xaml
index 1089639..965473a 100644
--- a/SpineViewer/Views/MainWindow.xaml
+++ b/SpineViewer/Views/MainWindow.xaml
@@ -727,6 +727,8 @@
+
+
@@ -780,7 +782,15 @@
+
+
+
+
+
+
+
+
diff --git a/SpineViewer/Views/MainWindow.xaml.cs b/SpineViewer/Views/MainWindow.xaml.cs
index 7792962..44c5dd1 100644
--- a/SpineViewer/Views/MainWindow.xaml.cs
+++ b/SpineViewer/Views/MainWindow.xaml.cs
@@ -157,6 +157,8 @@ public partial class MainWindow : Window
_vm.SFMLRendererViewModel.Speed = m.Speed;
_vm.SFMLRendererViewModel.ShowAxis = m.ShowAxis;
_vm.SFMLRendererViewModel.BackgroundColor = m.BackgroundColor;
+ _vm.SFMLRendererViewModel.BackgroundImagePath = m.BackgroundImagePath;
+ _vm.SFMLRendererViewModel.BackgroundImageMode = m.BackgroundImageMode;
}
}
@@ -181,6 +183,8 @@ public partial class MainWindow : Window
Speed = _vm.SFMLRendererViewModel.Speed,
ShowAxis = _vm.SFMLRendererViewModel.ShowAxis,
BackgroundColor = _vm.SFMLRendererViewModel.BackgroundColor,
+ BackgroundImagePath = _vm.SFMLRendererViewModel.BackgroundImagePath,
+ BackgroundImageMode = _vm.SFMLRendererViewModel.BackgroundImageMode,
};
JsonHelper.Serialize(m, LastStateFilePath);
@@ -596,5 +600,4 @@ public partial class MainWindow : Window
}
#endregion
-
}
\ No newline at end of file