From 99ec2704fe74e072a391f8e2798fd719fd6cda05 Mon Sep 17 00:00:00 2001 From: ww-rm Date: Wed, 3 Sep 2025 21:30:31 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=8D=95=E4=B8=AA=E8=BD=A8?= =?UTF-8?q?=E9=81=93=E7=9A=84=E6=97=B6=E9=97=B4=E5=9B=A0=E5=AD=90=E5=92=8C?= =?UTF-8?q?alpha=E6=B7=B7=E5=90=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Extensions/SpineObjectExtension.cs | 7 +- SpineViewer/Models/SpineObjectModel.cs | 73 ++++++++++-- SpineViewer/Resources/Strings/en.xaml | 6 +- SpineViewer/Resources/Strings/ja.xaml | 6 +- SpineViewer/Resources/Strings/zh.xaml | 6 +- .../MainWindow/SpineObjectTabViewModel.cs | 111 ++++++++++++------ SpineViewer/Views/MainWindow.xaml | 41 +++++-- 7 files changed, 195 insertions(+), 55 deletions(-) diff --git a/SpineViewer/Extensions/SpineObjectExtension.cs b/SpineViewer/Extensions/SpineObjectExtension.cs index 2d25ef8..0ee0602 100644 --- a/SpineViewer/Extensions/SpineObjectExtension.cs +++ b/SpineViewer/Extensions/SpineObjectExtension.cs @@ -21,6 +21,8 @@ namespace SpineViewer.Extensions foreach (var tr in self.AnimationState.IterTracks().Where(t => t is not null)) { var t = spineObject.AnimationState.SetAnimation(tr!.TrackIndex, tr.Animation, tr.Loop); + t.TimeScale = tr.TimeScale; + t.Alpha = tr.Alpha; if (keepTrackTime) t.TrackTime = tr.TrackTime; } @@ -38,7 +40,8 @@ namespace SpineViewer.Extensions foreach (var e in self.AnimationState.IterTracks()) { if (e is not null) - self.AnimationState.SetAnimation(e.TrackIndex, e.Animation, e.Loop); + e.TrackTime = 0; // 直接重置时间能保留原本的 TrackEntry + //self.AnimationState.SetAnimation(e.TrackIndex, e.Animation, e.Loop); } self.Update(0); } @@ -65,7 +68,7 @@ namespace SpineViewer.Extensions /// /// 按给定的帧率获取所有轨道第一个条目动画全时长包围盒大小, 是一个耗时操作, 如果可能的话最好缓存结果 /// - public static Rect GetAnimationBounds(this SpineObject self, float fps = 10) + public static Rect GetAnimationBounds(this SpineObject self, float fps = 30) { using var copy = self.Copy(); var bounds = copy.GetCurrentBounds(); diff --git a/SpineViewer/Models/SpineObjectModel.cs b/SpineViewer/Models/SpineObjectModel.cs index 19e8d0b..49b6acc 100644 --- a/SpineViewer/Models/SpineObjectModel.cs +++ b/SpineViewer/Models/SpineObjectModel.cs @@ -89,7 +89,7 @@ namespace SpineViewer.Models public event EventHandler? SlotAttachmentChanged; - public event EventHandler? AnimationChanged; + public event EventHandler? TrackPropertyChanged; public SpineVersion Version => _spineObject.Version; @@ -132,7 +132,7 @@ namespace SpineViewer.Models public float TimeScale { get { lock (_lock) return _spineObject.AnimationState.TimeScale; } - set { lock (_lock) SetProperty(_spineObject.AnimationState.TimeScale, Math.Clamp(value, -100f, 100f), v => _spineObject.AnimationState.TimeScale = v); } + set { lock (_lock) SetProperty(_spineObject.AnimationState.TimeScale, Math.Clamp(value, 0.01f, 100f), v => _spineObject.AnimationState.TimeScale = v); } } /// @@ -254,15 +254,59 @@ namespace SpineViewer.Models public void SetAnimation(int index, string name) { bool changed = false; + float lastTimeScale = 1f; + float lastAlpha = 1f; lock (_lock) { if (_spineObject.Data.AnimationsByName.ContainsKey(name)) { - _spineObject.AnimationState.SetAnimation(index, name, true); + // 需要记录之前的轨道属性值并还原 + if (_spineObject.AnimationState.GetCurrent(index) is ITrackEntry entry) + { + lastTimeScale = entry.TimeScale; + lastAlpha = entry.Alpha; + } + entry = _spineObject.AnimationState.SetAnimation(index, name, true); + entry.TimeScale = lastTimeScale; + entry.Alpha = lastAlpha; changed = true; } } - if (changed) AnimationChanged?.Invoke(this, new(index, name)); + if (changed) TrackPropertyChanged?.Invoke(this, new(index, nameof(TrackPropertyChangedEventArgs.AnimationName))); + } + + public float GetTrackTimeScale(int index) + { + lock (_lock) return _spineObject.AnimationState.GetCurrent(index)?.TimeScale ?? 1; + } + + public void SetTrackTimeScale(int index, float scale) + { + lock (_lock) + { + if (_spineObject.AnimationState.GetCurrent(index) is ITrackEntry entry) + { + entry.TimeScale = Math.Clamp(scale, 0.01f, 100f); + TrackPropertyChanged?.Invoke(this, new(index, nameof(TrackPropertyChangedEventArgs.TimeScale))); + } + } + } + + public float GetTrackAlpha(int index) + { + lock (_lock) return _spineObject.AnimationState.GetCurrent(index)?.Alpha ?? 1; + } + + public void SetTrackAlpha(int index, float alpha) + { + lock (_lock) + { + if (_spineObject.AnimationState.GetCurrent(index) is ITrackEntry entry) + { + entry.Alpha = Math.Clamp(alpha, 0f, 1f); + TrackPropertyChanged?.Invoke(this, new(index, nameof(TrackPropertyChangedEventArgs.Alpha))); + } + } } public int[] GetTrackIndices() @@ -283,7 +327,7 @@ namespace SpineViewer.Models public void ClearTrack(int index) { lock (_lock) _spineObject.AnimationState.ClearTrack(index); - AnimationChanged?.Invoke(this, new(index, null)); + TrackPropertyChanged?.Invoke(this, new(index, nameof(TrackPropertyChangedEventArgs.AnimationName))); } public void ResetAnimationsTime() @@ -458,7 +502,7 @@ namespace SpineViewer.Models { if (!string.IsNullOrEmpty(name)) _spineObject.AnimationState.SetAnimation(trackIndex, name, true); - AnimationChanged?.Invoke(this, new(trackIndex, name)); + TrackPropertyChanged?.Invoke(this, new(trackIndex, nameof(TrackPropertyChangedEventArgs.AnimationName))); trackIndex++; } @@ -548,10 +592,23 @@ namespace SpineViewer.Models public string? AttachmentName { get; } = attachmentName; } - public class AnimationChangedEventArgs(int trackIndex, string? animationName) : EventArgs + /// + /// 模型动画轨道属性变化事件参数, 需要检索 来确定发生变化的属性是什么 + /// + /// 发生属性变化的轨道索引 + /// 使用 nameof 设置发生改变的属性名 + public class TrackPropertyChangedEventArgs(int trackIndex, string propertyName) : EventArgs { public int TrackIndex { get; } = trackIndex; - public string? AnimationName { get; } = animationName; + + /// + /// 发生变化的属性名, 将会使用 nameof 设置为属性名称字符串 + /// + public string PropertyName { get; } = propertyName; + + public string? AnimationName { get; } + public float TimeScale { get; } = 1f; + public float Alpha { get; } = 1f; } public class SpineObjectLoadOptions diff --git a/SpineViewer/Resources/Strings/en.xaml b/SpineViewer/Resources/Strings/en.xaml index dcad18b..4844a81 100644 --- a/SpineViewer/Resources/Strings/en.xaml +++ b/SpineViewer/Resources/Strings/en.xaml @@ -65,7 +65,7 @@ Premultiply Alpha Physics Time Scale - Time scale for a single model; a negative value plays the animation in reverse. + Time scale for a single model, must be positive. Transform Scale @@ -87,6 +87,10 @@ Add Insert Clear + Time Scale + Time scale for a single track, must be positive. + Alpha Blending + Value range: 0–1. Similar to image blending, controls how animations from higher-index tracks mix into lower-index tracks. Debug Texture diff --git a/SpineViewer/Resources/Strings/ja.xaml b/SpineViewer/Resources/Strings/ja.xaml index 2d16f34..946aa8b 100644 --- a/SpineViewer/Resources/Strings/ja.xaml +++ b/SpineViewer/Resources/Strings/ja.xaml @@ -65,7 +65,7 @@ プレマルチプライドアルファ 物理 時間スケール - 単一モデルの時間スケール。負の値にするとアニメーションを逆再生します。 + 単一モデルの時間スケール。正の値のみ指定可能です。 変換 スケール @@ -87,6 +87,10 @@ 追加 挿入 削除 + 時間スケール + 単一トラックの時間スケール。正の値のみ指定可能です。 + アルファ合成 + 値の範囲:0~1。画像の合成と同様に、高インデックストラックのアニメーションが低インデックストラックにどの程度混合されるかを制御します。 デバッグ テクスチャ diff --git a/SpineViewer/Resources/Strings/zh.xaml b/SpineViewer/Resources/Strings/zh.xaml index b72d87f..8b78d6e 100644 --- a/SpineViewer/Resources/Strings/zh.xaml +++ b/SpineViewer/Resources/Strings/zh.xaml @@ -65,7 +65,7 @@ 预乘Alpha通道 物理 时间因子 - 单个模型的时间因子,取负数时可以倒放动画 + 单个模型的时间因子,只能取正数 变换 缩放 @@ -87,6 +87,10 @@ 添加 插入 删除 + 时间因子 + 单个轨道的时间因子,只能取正数 + Alpha 混合 + 取值范围 0-1,与图像混合类似,可以控制高索引轨道在低索引轨道中的动画混合比例 调试 Texture diff --git a/SpineViewer/ViewModels/MainWindow/SpineObjectTabViewModel.cs b/SpineViewer/ViewModels/MainWindow/SpineObjectTabViewModel.cs index 1e74af2..469ba99 100644 --- a/SpineViewer/ViewModels/MainWindow/SpineObjectTabViewModel.cs +++ b/SpineViewer/ViewModels/MainWindow/SpineObjectTabViewModel.cs @@ -31,7 +31,7 @@ namespace SpineViewer.ViewModels.MainWindow foreach (var obj in _selectedObjects) { obj.PropertyChanged -= SingleModel_PropertyChanged; - obj.AnimationChanged -= SingleModel_AnimationChanged; + obj.TrackPropertyChanged -= SingleModel_TrackPropChanged; } _skins.Clear(); _slots.Clear(); @@ -44,7 +44,7 @@ namespace SpineViewer.ViewModels.MainWindow foreach (var obj in _selectedObjects) { obj.PropertyChanged += SingleModel_PropertyChanged; - obj.AnimationChanged += SingleModel_AnimationChanged; + obj.TrackPropertyChanged += SingleModel_TrackPropChanged; } IEnumerable commonSkinNames = _selectedObjects[0].Skins; @@ -658,24 +658,24 @@ namespace SpineViewer.ViewModels.MainWindow /// /// 监听单个模型动画轨道发生变化, 则重建聚合后的动画列表 /// - /// - /// - private void SingleModel_AnimationChanged(object? sender, AnimationChangedEventArgs e) + private void SingleModel_TrackPropChanged(object? sender, TrackPropertyChangedEventArgs e) { - // XXX: 这里应该有更好的实现, 当 e.AnimationName == null 的时候代表删除轨道需要重新构建列表 - // 但是目前无法识别是否增加了轨道, 因此总是重建列表 - - // 由于某些原因, 直接使用 Clear 会和 UI 逻辑冲突产生报错, 因此需要放到 Dispatcher 里延迟执行 - Application.Current.Dispatcher.BeginInvoke( - () => - { - _animationTracks.Clear(); - IEnumerable commonTrackIndices = _selectedObjects[0].GetTrackIndices(); - foreach (var obj in _selectedObjects.Skip(1)) commonTrackIndices = commonTrackIndices.Intersect(obj.GetTrackIndices()); - foreach (var idx in commonTrackIndices) _animationTracks.Add(new(idx, _selectedObjects)); - } - ); + if (e.PropertyName == nameof(TrackPropertyChangedEventArgs.AnimationName)) + { + // XXX: 这里应该有更好的实现, 当 e.AnimationName == null 的时候代表删除轨道需要重新构建列表 + // 但是目前无法识别是否增加了轨道, 因此总是重建列表 + // 由于某些原因, 直接使用 Clear 会和 UI 逻辑冲突产生报错, 因此需要放到 Dispatcher 里延迟执行 + Application.Current.Dispatcher.BeginInvoke( + () => + { + _animationTracks.Clear(); + IEnumerable commonTrackIndices = _selectedObjects[0].GetTrackIndices(); + foreach (var obj in _selectedObjects.Skip(1)) commonTrackIndices = commonTrackIndices.Intersect(obj.GetTrackIndices()); + foreach (var idx in commonTrackIndices) _animationTracks.Add(new(idx, _selectedObjects)); + } + ); + } } public class SkinViewModel : ObservableObject @@ -848,10 +848,10 @@ namespace SpineViewer.ViewModels.MainWindow // 使用弱引用, 则此 ViewModel 被释放时无需显式退订事件 foreach (var sp in _spines) { - WeakEventManager.AddHandler( + WeakEventManager.AddHandler( sp, - nameof(sp.AnimationChanged), - SingleModel_AnimationChanged + nameof(sp.TrackPropertyChanged), + SingleModel_TrackPropChanged ); } } @@ -860,6 +860,24 @@ namespace SpineViewer.ViewModels.MainWindow public int TrackIndex => _trackIndex; + public float? AnimationDuration + { + get + { + if (_spines.Length <= 0) return null; + var ani = _spines[0].GetAnimation(_trackIndex); + if (ani is null) return null; + var val = _spines[0].GetAnimationDuration(ani); + foreach (var sp in _spines.Skip(1)) + { + var a = sp.GetAnimation(_trackIndex); + if (a is null) return null; + if (sp.GetAnimationDuration(a) != val) return null; + } + return val; + } + } + public string? AnimationName { get @@ -881,27 +899,54 @@ namespace SpineViewer.ViewModels.MainWindow } } - public float? AnimationDuration + public float? TrackTimeScale { get { + // XXX: 空轨道和多选不相同都会返回 null if (_spines.Length <= 0) return null; - var ani = _spines[0].GetAnimation(_trackIndex); - if (ani is null) return null; - var val = _spines[0].GetAnimationDuration(ani); - foreach (var sp in _spines.Skip(1)) - { - var a = sp.GetAnimation(_trackIndex); - if (a is null) return null; - if (sp.GetAnimationDuration(a) != val) return null; - } + var val = _spines[0].GetTrackTimeScale(_trackIndex); + if (_spines.Skip(1).Any(it => it.GetTrackTimeScale(_trackIndex) != val)) return null; return val; } + + set + { + if (_spines.Length <= 0) return; + if (value is null) return; + foreach (var sp in _spines) sp.SetTrackTimeScale(_trackIndex, (float)value); + OnPropertyChanged(); + } } - private void SingleModel_AnimationChanged(object? sender, AnimationChangedEventArgs e) + public float? TrackAlpha { - if (e.TrackIndex == _trackIndex) OnPropertyChanged(nameof(AnimationName)); + get + { + // XXX: 空轨道和多选不相同都会返回 null + if (_spines.Length <= 0) return null; + var val = _spines[0].GetTrackAlpha(_trackIndex); + if (_spines.Skip(1).Any(it => it.GetTrackAlpha(_trackIndex) != val)) return null; + return val; + } + + set + { + if (_spines.Length <= 0) return; + if (value is null) return; + foreach (var sp in _spines) sp.SetTrackAlpha(_trackIndex, (float)value); + OnPropertyChanged(); + } + } + + private void SingleModel_TrackPropChanged(object? sender, TrackPropertyChangedEventArgs e) + { + if (e.TrackIndex == _trackIndex) + { + if (e.PropertyName == nameof(TrackPropertyChangedEventArgs.AnimationName)) OnPropertyChanged(nameof(AnimationName)); + else if (e.PropertyName == nameof(TrackPropertyChangedEventArgs.TimeScale)) OnPropertyChanged(nameof(TrackTimeScale)); + else if (e.PropertyName == nameof(TrackPropertyChangedEventArgs.Alpha)) OnPropertyChanged(nameof(TrackAlpha)); + } } } } diff --git a/SpineViewer/Views/MainWindow.xaml b/SpineViewer/Views/MainWindow.xaml index 0c5d294..fc630dd 100644 --- a/SpineViewer/Views/MainWindow.xaml +++ b/SpineViewer/Views/MainWindow.xaml @@ -472,15 +472,38 @@ - + + - - @@ -735,7 +758,7 @@ - +