增加单个轨道的时间因子和alpha混合

This commit is contained in:
ww-rm
2025-09-03 21:30:31 +08:00
parent dbd2cef766
commit 99ec2704fe
7 changed files with 195 additions and 55 deletions

View File

@@ -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
/// <summary>
/// 按给定的帧率获取所有轨道第一个条目动画全时长包围盒大小, 是一个耗时操作, 如果可能的话最好缓存结果
/// </summary>
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();

View File

@@ -89,7 +89,7 @@ namespace SpineViewer.Models
public event EventHandler<SlotAttachmentChangedEventArgs>? SlotAttachmentChanged;
public event EventHandler<AnimationChangedEventArgs>? AnimationChanged;
public event EventHandler<TrackPropertyChangedEventArgs>? 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); }
}
/// <summary>
@@ -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
/// <summary>
/// 模型动画轨道属性变化事件参数, 需要检索 <c><see cref="PropertyName"/></c> 来确定发生变化的属性是什么
/// </summary>
/// <param name="trackIndex">发生属性变化的轨道索引</param>
/// <param name="propertyName">使用 <c>nameof</c> 设置发生改变的属性名</param>
public class TrackPropertyChangedEventArgs(int trackIndex, string propertyName) : EventArgs
{
public int TrackIndex { get; } = trackIndex;
public string? AnimationName { get; } = animationName;
/// <summary>
/// 发生变化的属性名, 将会使用 <c>nameof</c> 设置为属性名称字符串
/// </summary>
public string PropertyName { get; } = propertyName;
public string? AnimationName { get; }
public float TimeScale { get; } = 1f;
public float Alpha { get; } = 1f;
}
public class SpineObjectLoadOptions

View File

@@ -65,7 +65,7 @@
<s:String x:Key="Str_UsePma">Premultiply Alpha</s:String>
<s:String x:Key="Str_Physics">Physics</s:String>
<s:String x:Key="Str_TimeScale">Time Scale</s:String>
<s:String x:Key="Str_TimeScaleTootltip">Time scale for a single model; a negative value plays the animation in reverse.</s:String>
<s:String x:Key="Str_TimeScaleTootltip">Time scale for a single model, must be positive.</s:String>
<s:String x:Key="Str_Transform">Transform</s:String>
<s:String x:Key="Str_Scale">Scale</s:String>
@@ -87,6 +87,10 @@
<s:String x:Key="Str_AppendTrack">Add</s:String>
<s:String x:Key="Str_InsertTrack">Insert</s:String>
<s:String x:Key="Str_ClearTrack">Clear</s:String>
<s:String x:Key="Str_TrackTimeScale">Time Scale</s:String>
<s:String x:Key="Str_TrackTimeScaleTooltip">Time scale for a single track, must be positive.</s:String>
<s:String x:Key="Str_TrackAlpha">Alpha Blending</s:String>
<s:String x:Key="Str_TrackAlphaTooltip">Value range: 01. Similar to image blending, controls how animations from higher-index tracks mix into lower-index tracks.</s:String>
<s:String x:Key="Str_Debug">Debug</s:String>
<s:String x:Key="Str_DebugTexture">Texture</s:String>

View File

@@ -65,7 +65,7 @@
<s:String x:Key="Str_UsePma">プレマルチプライドアルファ</s:String>
<s:String x:Key="Str_Physics">物理</s:String>
<s:String x:Key="Str_TimeScale">時間スケール</s:String>
<s:String x:Key="Str_TimeScaleTootltip">単一モデルの時間スケール。の値にするとアニメーションを逆再生します。</s:String>
<s:String x:Key="Str_TimeScaleTootltip">単一モデルの時間スケール。の値のみ指定可能です。</s:String>
<s:String x:Key="Str_Transform">変換</s:String>
<s:String x:Key="Str_Scale">スケール</s:String>
@@ -87,6 +87,10 @@
<s:String x:Key="Str_AppendTrack">追加</s:String>
<s:String x:Key="Str_InsertTrack">挿入</s:String>
<s:String x:Key="Str_ClearTrack">削除</s:String>
<s:String x:Key="Str_TrackTimeScale">時間スケール</s:String>
<s:String x:Key="Str_TrackTimeScaleTooltip">単一トラックの時間スケール。正の値のみ指定可能です。</s:String>
<s:String x:Key="Str_TrackAlpha">アルファ合成</s:String>
<s:String x:Key="Str_TrackAlphaTooltip">値の範囲01。画像の合成と同様に、高インデックストラックのアニメーションが低インデックストラックにどの程度混合されるかを制御します。</s:String>
<s:String x:Key="Str_Debug">デバッグ</s:String>
<s:String x:Key="Str_DebugTexture">テクスチャ</s:String>

View File

@@ -65,7 +65,7 @@
<s:String x:Key="Str_UsePma">预乘Alpha通道</s:String>
<s:String x:Key="Str_Physics">物理</s:String>
<s:String x:Key="Str_TimeScale">时间因子</s:String>
<s:String x:Key="Str_TimeScaleTootltip">单个模型的时间因子,取负数时可以倒放动画</s:String>
<s:String x:Key="Str_TimeScaleTootltip">单个模型的时间因子,只能取正数</s:String>
<s:String x:Key="Str_Transform">变换</s:String>
<s:String x:Key="Str_Scale">缩放</s:String>
@@ -87,6 +87,10 @@
<s:String x:Key="Str_AppendTrack">添加</s:String>
<s:String x:Key="Str_InsertTrack">插入</s:String>
<s:String x:Key="Str_ClearTrack">删除</s:String>
<s:String x:Key="Str_TrackTimeScale">时间因子</s:String>
<s:String x:Key="Str_TrackTimeScaleTooltip">单个轨道的时间因子,只能取正数</s:String>
<s:String x:Key="Str_TrackAlpha">Alpha 混合</s:String>
<s:String x:Key="Str_TrackAlphaTooltip">取值范围 0-1与图像混合类似可以控制高索引轨道在低索引轨道中的动画混合比例</s:String>
<s:String x:Key="Str_Debug">调试</s:String>
<s:String x:Key="Str_DebugTexture">Texture</s:String>

View File

@@ -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<string> commonSkinNames = _selectedObjects[0].Skins;
@@ -658,24 +658,24 @@ namespace SpineViewer.ViewModels.MainWindow
/// <summary>
/// 监听单个模型动画轨道发生变化, 则重建聚合后的动画列表
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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<int> 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<int> 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<SpineObjectModel, AnimationChangedEventArgs>.AddHandler(
WeakEventManager<SpineObjectModel, TrackPropertyChangedEventArgs>.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));
}
}
}
}

View File

@@ -472,15 +472,38 @@
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Col0"/>
<ColumnDefinition Width="Auto" SharedSizeGroup="ColTrackIdx"/>
<ColumnDefinition Width="Auto" SharedSizeGroup="ColAniTime"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto" SharedSizeGroup="Col2"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Content="{Binding TrackIndex}" HorizontalContentAlignment="Left" Background="#bfffffff"/>
<ComboBox Grid.Column="1" SelectedValue="{Binding AnimationName}" ItemsSource="{Binding AnimationNames}"/>
<Label Grid.Column="2"
Content="{Binding AnimationDuration}"
ContentStringFormat="{}{0:F3} s"/>
<Label Grid.Column="0" Content="{Binding TrackIndex}" HorizontalContentAlignment="Left" VerticalAlignment="Top" Background="#bfffffff"/>
<Label Grid.Column="1" Content="{Binding AnimationDuration}" VerticalAlignment="Top" ContentStringFormat="{}{0:F3} s"/>
<Expander Grid.Column="2" HorizontalContentAlignment="Stretch">
<Expander.Header>
<!-- hc 的模板自带左侧 10 的 padding, 此处用 -10 的 margin 来抵消去除 -->
<ComboBox Margin="-10 0 0 0" Grid.Column="2" SelectedValue="{Binding AnimationName}" ItemsSource="{Binding AnimationNames}"/>
</Expander.Header>
<Grid Margin="1 0 0 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 时间因子 -->
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource Str_TrackTimeScale}" ToolTip="{DynamicResource Str_TrackTimeScaleTooltip}"/>
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding TrackTimeScale, StringFormat='{}{0:F3}'}" ToolTip="{DynamicResource Str_TrackTimeScaleTooltip}"/>
<!-- Alpha 混合 -->
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_TrackAlpha}" ToolTip="{DynamicResource Str_TrackAlphaTooltip}"/>
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding TrackAlpha, StringFormat='{}{0:F3}'}" ToolTip="{DynamicResource Str_TrackAlphaTooltip}"/>
</Grid>
</Expander>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
@@ -735,7 +758,7 @@
<ToggleButton Grid.Row="7" Grid.Column="1" IsChecked="{Binding FlipY}"/>
<Separator Grid.Row="8" Grid.Column="0" Grid.ColumnSpan="2" Margin="0 5"/>
<!-- 最大帧率 -->
<Label Grid.Row="9" Grid.Column="0" Content="{DynamicResource Str_MaxFps}" ToolTip="{DynamicResource Str_MaxFpsTooltip}"/>
<TextBox Grid.Row="9" Grid.Column="1" Text="{Binding MaxFps}" ToolTip="{DynamicResource Str_MaxFpsTooltip}"/>
@@ -751,7 +774,7 @@
<!-- 背景颜色 -->
<Label Grid.Row="12" Grid.Column="0" Content="{DynamicResource Str_BackgroundColor}" ToolTip="#AARRGGBB"/>
<TextBox Grid.Row="12" Grid.Column="1" Text="{Binding BackgroundColor}" ToolTip="#AARRGGBB"/>
<!-- 背景图案 -->
<!-- 背景图案模式 -->
</Grid>