diff --git a/PCL.Core/UI/Animation/Animatable/ClrAnimatable.cs b/PCL.Core/UI/Animation/Animatable/ClrAnimatable.cs index 90b00404f..41d676a5e 100644 --- a/PCL.Core/UI/Animation/Animatable/ClrAnimatable.cs +++ b/PCL.Core/UI/Animation/Animatable/ClrAnimatable.cs @@ -24,4 +24,5 @@ public ClrAnimatable( object? IAnimatable.GetValue() => GetValue(); void IAnimatable.SetValue(object? value) => SetValue((T)value!); + void IAnimatable.SetValue(TValue value) => SetValue((T)(object)value!); } \ No newline at end of file diff --git a/PCL.Core/UI/Animation/Animatable/EmptyAnimatable.cs b/PCL.Core/UI/Animation/Animatable/EmptyAnimatable.cs new file mode 100644 index 000000000..3d79f9555 --- /dev/null +++ b/PCL.Core/UI/Animation/Animatable/EmptyAnimatable.cs @@ -0,0 +1,23 @@ +namespace PCL.Core.UI.Animation.Animatable; + +public sealed class EmptyAnimatable : IAnimatable +{ + public static EmptyAnimatable Instance { get; } = new(); + + private EmptyAnimatable() { } + + public object? GetValue() + { + return null; + } + + public void SetValue(object value) + { + // 空 + } + + public void SetValue(T value) + { + // 空 + } +} \ No newline at end of file diff --git a/PCL.Core/UI/Animation/Animatable/IAnimatable.cs b/PCL.Core/UI/Animation/Animatable/IAnimatable.cs index 3a9105d01..0af95a4cb 100644 --- a/PCL.Core/UI/Animation/Animatable/IAnimatable.cs +++ b/PCL.Core/UI/Animation/Animatable/IAnimatable.cs @@ -4,4 +4,5 @@ public interface IAnimatable { public object? GetValue(); public void SetValue(object value); + public void SetValue(T value); } \ No newline at end of file diff --git a/PCL.Core/UI/Animation/Animatable/WpfAnimatable.cs b/PCL.Core/UI/Animation/Animatable/WpfAnimatable.cs index 9cd9e2e21..8de6104ac 100644 --- a/PCL.Core/UI/Animation/Animatable/WpfAnimatable.cs +++ b/PCL.Core/UI/Animation/Animatable/WpfAnimatable.cs @@ -1,5 +1,7 @@ using System; +using System.Runtime.CompilerServices; using System.Windows; +using System.Windows.Media; using PCL.Core.UI.Animation.ValueProcessor; namespace PCL.Core.UI.Animation.Animatable; @@ -28,13 +30,76 @@ public sealed class WpfAnimatable(DependencyObject owner, DependencyProperty? pr ArgumentNullException.ThrowIfNull(actualProperty); - return Owner.GetValue(actualProperty); + var value = Owner.GetValue(actualProperty); + return value switch + { + SolidColorBrush brush => (NColor)brush, + Color color => (NColor)color, + ScaleTransform scaleTransform => (NScaleTransform)scaleTransform, + RotateTransform rotateTransform => (NRotateTransform)rotateTransform, + _ => value + }; } public void SetValue(object value) { value = ValueProcessorManager.Filter(value); ArgumentNullException.ThrowIfNull(Property); + + value = value switch + { + NColor color => Property.Name switch + { + "Color" => (Color)color, + _ => (SolidColorBrush)color + }, + NScaleTransform st => (ScaleTransform)st, + NRotateTransform rt => (RotateTransform)rt, + _ => value + }; + Owner.SetValue(Property, value); } + + public void SetValue(T value) + { + value = ValueProcessorManager.Filter(value); + ArgumentNullException.ThrowIfNull(Property); + SetValueCore(value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SetValueCore(T value) + { + ArgumentNullException.ThrowIfNull(Property); + + if (typeof(T) == typeof(NColor)) + { + var color = Unsafe.As(ref value); + + Owner.SetValue( + Property, + Property.Name == "Color" + ? (Color)color + : (SolidColorBrush)color); + + return; + } + + if (typeof(T) == typeof(NScaleTransform)) + { + var st = Unsafe.As(ref value); + Owner.SetValue(Property, (ScaleTransform)st); + return; + } + + if (typeof(T) == typeof(NRotateTransform)) + { + var rt = Unsafe.As(ref value); + Owner.SetValue(Property, (RotateTransform)rt); + return; + } + + Owner.SetValue(Property, value!); + } } \ No newline at end of file diff --git a/PCL.Core/UI/Animation/Core/ActionAnimation.cs b/PCL.Core/UI/Animation/Core/ActionAnimation.cs new file mode 100644 index 000000000..6433731bb --- /dev/null +++ b/PCL.Core/UI/Animation/Core/ActionAnimation.cs @@ -0,0 +1,90 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using PCL.Core.UI.Animation.Animatable; + +namespace PCL.Core.UI.Animation.Core; + +/// +/// 用于在动画系统中执行 Action。 +/// +public class ActionAnimation : AnimationBase +{ + public ActionAnimation() { } + + public ActionAnimation(Action action) => Action = _ => action(); + + public ActionAnimation(Action action) => Action = action; + + public Action Action { get; set; } = null!; + public override int CurrentFrame { get; set; } + public TimeSpan Delay { get; set; } + + private CancellationTokenSource? _cts = new(); + private TaskCompletionSource? _tcs; + private int _called = 0; + + public override async Task RunAsync(IAnimatable target) + { + ArgumentNullException.ThrowIfNull(Action); + + _tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + _cts = new CancellationTokenSource(); + + var clone = (ActionAnimation)MemberwiseClone(); + clone.Status = AnimationStatus.Running; + + // 延迟 + await Task.Delay(Delay); + + Interlocked.Exchange(ref _called, 0); + _ = AnimationService.PushAnimationAsync(clone, target); + return await _tcs.Task.ContinueWith(_ => clone); + } + + public override IAnimation RunFireAndForget(IAnimatable target) + { + ArgumentNullException.ThrowIfNull(Action); + + _cts = new CancellationTokenSource(); + + var clone = (ActionAnimation)MemberwiseClone(); + clone.Status = AnimationStatus.Running; + + _ = Task.Run(async () => + { + // 延迟 + await Task.Delay(Delay); + + Interlocked.Exchange(ref _called, 0); + AnimationService.PushAnimationFireAndForget(clone, target); + }); + + return clone; + } + + public override void Cancel() + { + Status = AnimationStatus.Canceled; + _cts?.Cancel(); + _tcs?.TrySetCanceled(); + } + + public override IAnimationFrame? ComputeNextFrame(IAnimatable target) + { + if (Status is AnimationStatus.Canceled or AnimationStatus.Completed) return null; + + if (Interlocked.CompareExchange(ref _called, 1, 0) == 0) + { + return new ActionAnimationFrame(() => + { + Action(_cts!.Token); + Status = AnimationStatus.Completed; + _tcs?.TrySetResult(); + }); + } + + return null; + } +} \ No newline at end of file diff --git a/PCL.Core/UI/Animation/Core/ActionAnimationFrame.cs b/PCL.Core/UI/Animation/Core/ActionAnimationFrame.cs new file mode 100644 index 000000000..ceb29a36e --- /dev/null +++ b/PCL.Core/UI/Animation/Core/ActionAnimationFrame.cs @@ -0,0 +1,10 @@ +using System; + +namespace PCL.Core.UI.Animation.Core; + +public struct ActionAnimationFrame(Action action) : IAnimationFrame +{ + public Action Action { get; set; } = action; + + public Action GetAction() => Action; +} \ No newline at end of file diff --git a/PCL.Core/UI/Animation/Core/AnimationBase.cs b/PCL.Core/UI/Animation/Core/AnimationBase.cs index 57d55de15..cd0c6c6ed 100644 --- a/PCL.Core/UI/Animation/Core/AnimationBase.cs +++ b/PCL.Core/UI/Animation/Core/AnimationBase.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; using System.Windows; using PCL.Core.UI.Animation.Animatable; @@ -7,13 +8,18 @@ namespace PCL.Core.UI.Animation.Core; public abstract class AnimationBase : DependencyObject, IAnimation { - public abstract bool IsCompleted { get; } + public string Name { get; set; } = string.Empty; + private volatile int _status = (int)AnimationStatus.NotStarted; + public AnimationStatus Status + { + get => (AnimationStatus)_status; + internal set => Interlocked.Exchange(ref _status, (int)value); + } public abstract int CurrentFrame { get; set; } - public abstract Task RunAsync(IAnimatable target); - public abstract void RunFireAndForget(IAnimatable target); + public abstract Task RunAsync(IAnimatable target); + public abstract IAnimation RunFireAndForget(IAnimatable target); public abstract void Cancel(); - public abstract IAnimationFrame? ComputeNextFrame(IAnimatable target); public void RaiseStarted() => Started?.Invoke(this, EventArgs.Empty); diff --git a/PCL.Core/UI/Animation/Core/AnimationFrame.cs b/PCL.Core/UI/Animation/Core/AnimationFrame.cs index 255a2c895..52bc275b7 100644 --- a/PCL.Core/UI/Animation/Core/AnimationFrame.cs +++ b/PCL.Core/UI/Animation/Core/AnimationFrame.cs @@ -5,12 +5,13 @@ namespace PCL.Core.UI.Animation.Core; -public readonly struct AnimationFrame : IAnimationFrame where T : struct -{ - public IAnimatable Target { get; init; } - public T Value { get; init; } - public T StartValue { get; init; } - public T GetAbsoluteValue() => ValueProcessorManager.Add(StartValue, Value); - - object IAnimationFrame.GetAbsoluteValue() => GetAbsoluteValue(); -} \ No newline at end of file +// public readonly struct AnimationFrame(IAnimatable target, T value, T startValue) : IAnimationFrame +// { +// public IAnimatable Target { get; init; } = target; +// public T Value { get; init; } = value; +// public T StartValue { get; init; } = startValue; +// public T GetAbsoluteValue() => ValueProcessorManager.Add(StartValue, Value); +// object IAnimationFrame.StartValue => StartValue!; +// object IAnimationFrame.Value => Value!; +// object IAnimationFrame.GetAbsoluteValue() => GetAbsoluteValue()!; +// } \ No newline at end of file diff --git a/PCL.Core/UI/Animation/Core/AnimationGroup.cs b/PCL.Core/UI/Animation/Core/AnimationGroup.cs index 172d11671..f2227462d 100644 --- a/PCL.Core/UI/Animation/Core/AnimationGroup.cs +++ b/PCL.Core/UI/Animation/Core/AnimationGroup.cs @@ -1,5 +1,6 @@ -using System.Collections.ObjectModel; -using System.Linq; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Threading.Tasks; using System.Windows; using System.Windows.Markup; @@ -14,34 +15,55 @@ namespace PCL.Core.UI.Animation.Core; [ContentProperty(nameof(Children))] public abstract class AnimationGroup : AnimationBase { - public static readonly DependencyProperty ChildrenProperty = DependencyProperty.Register( - nameof(Children), typeof(ObservableCollection), typeof(SequentialAnimationGroup)); + public static readonly DependencyProperty ChildrenProperty = + DependencyProperty.Register( + nameof(Children), + typeof(ObservableCollection), + typeof(AnimationGroup), + new PropertyMetadata(null)); // 移除 OnChildrenChanged 回调,避免运行时冲突 public ObservableCollection Children { get => (ObservableCollection)GetValue(ChildrenProperty); set => SetValue(ChildrenProperty, value); } - + + /// + /// 存储当前正在运行的动画实例。 + /// + protected List ChildrenCore { get; } = []; + protected AnimationGroup() { + // 确保集合初始化,但不进行自动同步 SetCurrentValue(ChildrenProperty, new ObservableCollection()); } - - public override bool IsCompleted => Children.All(child => child.IsCompleted); + public override int CurrentFrame { get; set; } public override void Cancel() { - // 重置值 + Status = AnimationStatus.Canceled; + CurrentFrame = 0; - // 取消所有子动画 - foreach (var child in Children) + lock (ChildrenCore) { - child.Cancel(); + foreach (var child in ChildrenCore) + { + child.Cancel(); + } + // 清理运行实例,断开引用 + ChildrenCore.Clear(); } } + + public void CancelAndClear() + { + Cancel(); + // 如果需要清空定义的 Children 集合,应在 UI 线程操作 + AnimationService.UIAccessProvider.Invoke(() => Children.Clear()); + } public override IAnimationFrame? ComputeNextFrame(IAnimatable target) { @@ -53,29 +75,60 @@ protected static IAnimatable ResolveTarget(IAnimation animation, IAnimatable def if (animation is not DependencyObject aniDependencyObject) return defaultTarget; - DependencyObject? targetObject; - DependencyProperty? targetProperty; + DependencyObject? targetObject = null; + DependencyProperty? targetProperty = null; - // Target + // Target check if (WpfUtils.IsDependencyPropertySet(aniDependencyObject, AnimationExtensions.TargetProperty)) { targetObject = (DependencyObject)aniDependencyObject.GetValue(AnimationExtensions.TargetProperty); } - else + else if (defaultTarget is WpfAnimatable animatable) { - targetObject = ((WpfAnimatable)defaultTarget).Owner; // 默认用父级 + targetObject = animatable.Owner; } - // TargetProperty + // TargetProperty check if (WpfUtils.IsDependencyPropertySet(aniDependencyObject, AnimationExtensions.TargetPropertyProperty)) { targetProperty = (DependencyProperty)aniDependencyObject.GetValue(AnimationExtensions.TargetPropertyProperty); } - else + else if (defaultTarget is WpfAnimatable animatable) { - targetProperty = ((WpfAnimatable)defaultTarget).Property; // 默认用父级 + targetProperty = animatable.Property; } + // 如果都未解析出特定值,直接返回默认目标 + if (targetObject == null || targetProperty == null) + return defaultTarget; + return new WpfAnimatable(targetObject, targetProperty); } + + protected static Task CreateChildAwaiter(IAnimation animation) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + if (animation.Status == AnimationStatus.Completed) + { + tcs.TrySetResult(); + return tcs.Task; + } + + EventHandler? handler = null; + handler = (_, _) => + { + animation.Completed -= handler; + tcs.TrySetResult(); + }; + + animation.Completed += handler; + + if (animation.Status != AnimationStatus.Completed) return tcs.Task; + + animation.Completed -= handler; + tcs.TrySetResult(); + + return tcs.Task; + } } \ No newline at end of file diff --git a/PCL.Core/UI/Animation/Core/AnimationService.cs b/PCL.Core/UI/Animation/Core/AnimationService.cs index 2340ca3af..10c366554 100644 --- a/PCL.Core/UI/Animation/Core/AnimationService.cs +++ b/PCL.Core/UI/Animation/Core/AnimationService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading; using System.Threading.Channels; @@ -17,12 +18,14 @@ namespace PCL.Core.UI.Animation.Core; public sealed class AnimationService : GeneralService { #region Lifecycle - + private static LifecycleContext? _context; private static LifecycleContext Context => _context!; - - private AnimationService() : base("animation", "动画计算以及赋值") { _context = ServiceContext; } - + + private AnimationService() : base("animation", "动画") + { + _context = ServiceContext; + } public override void Start() { @@ -35,34 +38,40 @@ public override void Stop() } #endregion - + private static void _RegisterValueProcessors() { // 在这里注册所有的 ValueProcessor ValueProcessorManager.Register(new DoubleValueProcessor()); ValueProcessorManager.Register(new MatrixValueProcessor()); ValueProcessorManager.Register(new NColorValueProcessor()); + ValueProcessorManager.Register(new NRotateTransformValueProcessor()); + ValueProcessorManager.Register(new NScaleTransformValueProcessor()); ValueProcessorManager.Register(new PointValueProcessor()); ValueProcessorManager.Register(new ThicknessValueProcessor()); } - - private static Channel<(IAnimation, IAnimatable)> _animationChannel = null!; - private static Channel _frameChannel = null!; + + private static Channel<(IAnimation Animation, IAnimatable Target)> _animationChannel = null!; + // private static Channel _frameChannel = null!; + private static Channel<(IAnimationFrame Frame, IAnimation Source)> _frameChannel = null!; + // private static ConcurrentDictionary _frameDictionary = null!; + private static ConcurrentDictionary _namedAnimations = new(); private static IClock _clock = null!; private static AsyncCountResetEvent _resetEvent = null!; private static int _taskCount; private static CancellationTokenSource _cts = null!; public static int Fps { get; set; } = 60; - public static double Scale { get; set; } = 1.0d; + public static double Scale { get; set; } = 0.1d; public static IUIAccessProvider UIAccessProvider { get; private set; } = null!; private static void _Initialize() { - // 初始化 Channel + // 初始化 Channel 与 Dictionary _animationChannel = Channel.CreateUnbounded<(IAnimation, IAnimatable)>(); - _frameChannel = Channel.CreateUnbounded(); + // _frameChannel = Channel.CreateUnbounded(); + _frameChannel = Channel.CreateUnbounded<(IAnimationFrame, IAnimation)>(); // 根据核心数量来确定动画计算 Task 数量 _taskCount = Environment.ProcessorCount; @@ -80,13 +89,17 @@ private static void _Initialize() _ = UIAccessProvider.InvokeAsync(async () => { if (_cts.IsCancellationRequested) return; - - // 取出所有动画帧并赋值 while (await _frameChannel.Reader.WaitToReadAsync()) { - while (_frameChannel.Reader.TryRead(out var frame)) + // 读取数据 + while (_frameChannel.Reader.TryRead(out var item)) { - frame.Target.SetValue(frame.GetAbsoluteValue()); + // 如果动画源已被标记取消,直接丢弃该帧,不进行处理 + if (item.Source.Status == AnimationStatus.Canceled) + continue; + + // 正常处理 + item.Frame.GetAction()(); } await Task.Yield(); @@ -117,6 +130,9 @@ private static void _Uninitialize() // 将 ResetEvent 释放 _resetEvent.Dispose(); + + // 清理 Dictionary + _namedAnimations.Clear(); } private static void ClockOnTick(object? sender, long e) @@ -128,7 +144,7 @@ private static void ClockOnTick(object? sender, long e) private static async Task _AnimationComputeTaskAsync() { // 本地动画列表,确保没有一直无法计算的动画 - var animationList = new List<(IAnimation, IAnimatable)>(8); + var animationList = new List<(IAnimation Animation, IAnimatable Target)>(8); // 持续监听 Channel 中的动画 while (!_cts.IsCancellationRequested) @@ -152,33 +168,59 @@ private static async Task _AnimationComputeTaskAsync() // TODO: 支持缓存动画计算结果 (由 AnimationData 支持) // 从列表中获取动画 - var animation = animationList[i]; + var animationEntry = animationList[i]; - // 如果动画已经完成,则从列表中移除 - if (animation.Item1.IsCompleted) + // 如果动画已经完成或被取消,则从列表中移除 + if (animationEntry.Animation.Status is AnimationStatus.Canceled or AnimationStatus.Completed) { - animation.Item1.RaiseCompleted(); + animationEntry.Animation.RaiseCompleted(); + + if (!string.IsNullOrEmpty(animationEntry.Animation.Name)) + { + // 使用显式接口 + ((ICollection>)_namedAnimations) + .Remove(new KeyValuePair(animationEntry.Animation.Name, animationEntry.Animation)); + } + animationList.RemoveAt(i); continue; } - + // 计算动画的下一帧 - var frame = animation.Item1.ComputeNextFrame(animation.Item2); + var frame = animationEntry.Animation.ComputeNextFrame(animationEntry.Target); // 如果没有计算帧(当动画为 SequentialAnimationGroup 或 ParallelAnimationGroup 这种动画集合时),跳过 if (frame is null) continue; // 将动画帧写入 Channel - _frameChannel.Writer.TryWrite(frame); + _frameChannel.Writer.TryWrite((frame, animationEntry.Animation)); // 增加当前帧计数 - animation.Item1.CurrentFrame++; + animationEntry.Animation.CurrentFrame++; } - + // 等待 Tick 事件的通知 await _resetEvent.WaitAsync(); } } + private static void HandleNamedAnimationConflict(IAnimation animation) + { + if (string.IsNullOrEmpty(animation.Name)) return; + + _namedAnimations.AddOrUpdate( + animation.Name, + animation, // 如果不存在,直接添加 + (_, existingAnimation) => + { + // 如果已存在同名动画,取消旧动画 + existingAnimation.Cancel(); + // 替换为新动画 + return animation; + }); + } + internal static Task PushAnimationAsync(IAnimation animation, IAnimatable target) { + HandleNamedAnimationConflict(animation); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); animation.Completed += (_, _) => tcs.SetResult(); @@ -189,6 +231,16 @@ internal static Task PushAnimationAsync(IAnimation animation, IAnimatable target internal static void PushAnimationFireAndForget(IAnimation animation, IAnimatable target) { + HandleNamedAnimationConflict(animation); + _animationChannel.Writer.TryWrite((animation, target)); } + + public static void CancelAnimationByName(string name) + { + if (_namedAnimations.TryRemove(name, out var animation)) + { + animation.Cancel(); + } + } } \ No newline at end of file diff --git a/PCL.Core/UI/Animation/Core/AnimationStatus.cs b/PCL.Core/UI/Animation/Core/AnimationStatus.cs new file mode 100644 index 000000000..ac2bae8ec --- /dev/null +++ b/PCL.Core/UI/Animation/Core/AnimationStatus.cs @@ -0,0 +1,21 @@ +namespace PCL.Core.UI.Animation.Core; + +public enum AnimationStatus +{ + /// + /// 动画未开始。 + /// + NotStarted, + /// + /// 动画正在运行。 + /// + Running, + /// + /// 动画已完成。 + /// + Completed, + /// + /// 动画已取消。 + /// + Canceled, +} \ No newline at end of file diff --git a/PCL.Core/UI/Animation/Core/FromToAnimationBase.cs b/PCL.Core/UI/Animation/Core/FromToAnimationBase.cs index 88eda2f31..dd6c86b8f 100644 --- a/PCL.Core/UI/Animation/Core/FromToAnimationBase.cs +++ b/PCL.Core/UI/Animation/Core/FromToAnimationBase.cs @@ -1,5 +1,6 @@ using System; using System.Numerics; +using System.Threading; using System.Threading.Tasks; using PCL.Core.UI.Animation.Animatable; using PCL.Core.UI.Animation.Easings; @@ -7,12 +8,14 @@ namespace PCL.Core.UI.Animation.Core; -public class FromToAnimationBase : AnimationBase, IFromToAnimation where T : struct +public class FromToAnimationBase : AnimationBase, IFromToAnimation { public IEasing Easing { get; set; } = new LinearEasing(); - public T? From { get; set; } - public T To { get; set; } - public AnimationValueType ValueType { get; set; } = AnimationValueType.Relative; + + public T From { get; set; } = default!; + + public T? To { get; set; } + public AnimationValueType ValueType { get; set; } = AnimationValueType.Absolute; public TimeSpan Duration { get; set; } public TimeSpan Delay { get; set; } public T? CurrentValue { get; internal set; } @@ -28,26 +31,35 @@ public class FromToAnimationBase : AnimationBase, IFromToAnimation where T : } public int TotalFrames { get; private set; } + + private int _currentFrame; - public override bool IsCompleted => CurrentFrame >= TotalFrames; - public override int CurrentFrame { get; set; } + public override int CurrentFrame + { + get => Interlocked.CompareExchange(ref _currentFrame, 0, 0); + set => Interlocked.Exchange(ref _currentFrame, value); + } - private T _startValue; + private T? _startValue; - public override async Task RunAsync(IAnimatable target) + public override async Task RunAsync(IAnimatable target) { _RunCore(target); + var clone = (FromToAnimationBase)MemberwiseClone(); // 延迟 await Task.Delay(Delay); // 将该动画推送到动画服务 - await AnimationService.PushAnimationAsync((FromToAnimationBase)MemberwiseClone(), target); + await AnimationService.PushAnimationAsync(clone, target); + + return clone; } - public override void RunFireAndForget(IAnimatable target) + public override IAnimation RunFireAndForget(IAnimatable target) { _RunCore(target); + var clone = (FromToAnimationBase)MemberwiseClone(); _ = Task.Run(async () => { @@ -55,14 +67,16 @@ public override void RunFireAndForget(IAnimatable target) await Task.Delay(Delay); // 将该动画推送到动画服务 - AnimationService.PushAnimationFireAndForget((FromToAnimationBase)MemberwiseClone(), target); + AnimationService.PushAnimationFireAndForget(clone, target); }); + + return clone; } private void _RunCore(IAnimatable target) { // 重置当前帧 - CurrentFrame = 0; + _currentFrame = 0; // 空值检查 ArgumentNullException.ThrowIfNull(To); @@ -71,31 +85,45 @@ private void _RunCore(IAnimatable target) _startValue = (T)target.GetValue()!; // 如果 From 为空,则根据动画值类型设置初始值 - From ??= ValueType == AnimationValueType.Relative ? default : _startValue; + if (!ValueProcessorManager.Equal(_startValue, From)) + { + From = ValueType == AnimationValueType.Relative ? ValueProcessorManager.DefaultValue() : _startValue; + } // 计算总帧数 TotalFrames = (int)Math.Round(Duration.TotalSeconds * AnimationService.Fps / AnimationService.Scale); // 进行初始赋值 - target.SetValue( - ValueType == AnimationValueType.Relative ? ValueProcessorManager.Add(From!.Value, _startValue) : From!); + // target.SetValue( + // ValueType == AnimationValueType.Relative ? ValueProcessorManager.Add(From, _startValue)! : From!); + + // 设置状态 + Status = AnimationStatus.Running; } public override void Cancel() { // 确保正常结束 - CurrentFrame = TotalFrames + 1; + Interlocked.Exchange(ref _currentFrame, TotalFrames); + + Status = AnimationStatus.Canceled; } public override IAnimationFrame? ComputeNextFrame(IAnimatable target) { - return new AnimationFrame + if (_currentFrame >= TotalFrames) + { + Status = AnimationStatus.Completed; + return null; + } + + return new FromToAnimationFrame { Target = target, Value = ValueType == AnimationValueType.Relative - ? CurrentValue!.Value - : ValueProcessorManager.Subtract(CurrentValue!.Value, From!.Value), - StartValue = ValueType == AnimationValueType.Relative ? _startValue : From!.Value + ? CurrentValue! + : ValueProcessorManager.Subtract(CurrentValue!, From!), + StartValue = ValueType == AnimationValueType.Relative ? _startValue! : From! }; } } \ No newline at end of file diff --git a/PCL.Core/UI/Animation/Core/FromToAnimationFrame.cs b/PCL.Core/UI/Animation/Core/FromToAnimationFrame.cs new file mode 100644 index 000000000..7068c82d1 --- /dev/null +++ b/PCL.Core/UI/Animation/Core/FromToAnimationFrame.cs @@ -0,0 +1,18 @@ +using System; +using PCL.Core.UI.Animation.Animatable; +using PCL.Core.UI.Animation.ValueProcessor; + +namespace PCL.Core.UI.Animation.Core; + +public readonly struct FromToAnimationFrame(IAnimatable target, T value, T startValue) : IAnimationFrame +{ + public IAnimatable Target { get; init; } = target; + public T Value { get; init; } = value; + public T StartValue { get; init; } = startValue; + public Action GetAction() + { + var target = Target; + var absolute = ValueProcessorManager.Add(StartValue, Value); + return () => target.SetValue(absolute!); + } +} \ No newline at end of file diff --git a/PCL.Core/UI/Animation/Core/IAnimation.cs b/PCL.Core/UI/Animation/Core/IAnimation.cs index 1ce98c8a4..aee661a00 100644 --- a/PCL.Core/UI/Animation/Core/IAnimation.cs +++ b/PCL.Core/UI/Animation/Core/IAnimation.cs @@ -7,9 +7,13 @@ namespace PCL.Core.UI.Animation.Core; public interface IAnimation { /// - /// 动画是否已完成。 + /// 动画名。 /// - bool IsCompleted { get; } + string Name { get; set; } + /// + /// 当前动画状态。 + /// + AnimationStatus Status { get; } /// /// 当前动画帧索引。 /// @@ -19,18 +23,18 @@ public interface IAnimation /// /// 被动画的对象。 /// 返回表示异步动画操作的任务。 - Task RunAsync(IAnimatable target); + Task RunAsync(IAnimatable target); /// /// 一发即忘方式运行动画。 /// /// 被动画的对象。 - void RunFireAndForget(IAnimatable target); + IAnimation RunFireAndForget(IAnimatable target); /// /// 取消动画。 /// void Cancel(); /// - /// 计算下一帧。。 + /// 计算下一帧。 /// /// 被动画的对象。 /// 动画帧。 diff --git a/PCL.Core/UI/Animation/Core/IAnimationFrame.cs b/PCL.Core/UI/Animation/Core/IAnimationFrame.cs index 7a93143db..ca044dda7 100644 --- a/PCL.Core/UI/Animation/Core/IAnimationFrame.cs +++ b/PCL.Core/UI/Animation/Core/IAnimationFrame.cs @@ -1,10 +1,10 @@ -using System.Windows; +using System; +using System.Windows; using PCL.Core.UI.Animation.Animatable; namespace PCL.Core.UI.Animation.Core; public interface IAnimationFrame { - IAnimatable Target { get; } - object GetAbsoluteValue(); + Action GetAction(); } \ No newline at end of file diff --git a/PCL.Core/UI/Animation/Core/ParallelAnimationGroup.cs b/PCL.Core/UI/Animation/Core/ParallelAnimationGroup.cs index fb4a6d3fb..99f5454ab 100644 --- a/PCL.Core/UI/Animation/Core/ParallelAnimationGroup.cs +++ b/PCL.Core/UI/Animation/Core/ParallelAnimationGroup.cs @@ -1,4 +1,6 @@ -using System.Linq; +using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using PCL.Core.UI.Animation.Animatable; @@ -9,24 +11,62 @@ namespace PCL.Core.UI.Animation.Core; /// public sealed class ParallelAnimationGroup : AnimationGroup { - public override Task RunAsync(IAnimatable target) + private TaskCompletionSource? _cancelTcs; + + public override async Task RunAsync(IAnimatable target) { - // 取出所有子动画的任务,并等待它们全部完成 - var tasks = Children.Select(child => - { - var childTarget = ResolveTarget(child, target); - return child.RunAsync(childTarget); - }); + Status = AnimationStatus.Running; + AnimationService.PushAnimationFireAndForget(this, target); - return Task.WhenAll(tasks); - } + _cancelTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - public override void RunFireAndForget(IAnimatable target) - { - foreach (var child in Children) + var childrenSnapshot = Children.ToList(); + var childWaitTasks = new List(); + + lock (ChildrenCore) { - var childTarget = ResolveTarget(child, target); - child.RunFireAndForget(childTarget); + if (Status == AnimationStatus.Canceled) return this; + + // 立即启动所有子动画,并收集它们的完成 Task + foreach (var child in childrenSnapshot) + { + var childTarget = ResolveTarget(child, target); + + // 立即拿到实例 + var instance = child.RunFireAndForget(childTarget); + ChildrenCore.Add(instance); + + // 收集 Task + childWaitTasks.Add(CreateChildAwaiter(instance)); + } } + + try + { + // 等待到所有子动画完成或组被取消 + await Task.WhenAny(Task.WhenAll(childWaitTasks), _cancelTcs.Task); + } + finally + { + if (Status != AnimationStatus.Canceled) + { + Status = AnimationStatus.Completed; + } + _cancelTcs = null; + } + + return this; + } + + public override void Cancel() + { + base.Cancel(); + _cancelTcs?.TrySetResult(); + } + + public override IAnimation RunFireAndForget(IAnimatable target) + { + _ = RunAsync(target); + return this; } } \ No newline at end of file diff --git a/PCL.Core/UI/Animation/Core/RunAnimation.cs b/PCL.Core/UI/Animation/Core/RunAnimation.cs index 771d17229..a3e7043b5 100644 --- a/PCL.Core/UI/Animation/Core/RunAnimation.cs +++ b/PCL.Core/UI/Animation/Core/RunAnimation.cs @@ -1,7 +1,5 @@ using System; -using System.ComponentModel; using System.Windows; -using System.Windows.Data; using System.Windows.Markup; using Microsoft.Xaml.Behaviors; using PCL.Core.UI.Animation.Animatable; @@ -10,12 +8,12 @@ namespace PCL.Core.UI.Animation.Core; [ContentProperty(nameof(Animation))] -public class RunAnimation : TriggerAction +public class RunAnimationAction : TriggerAction { public static readonly DependencyProperty AnimationProperty = DependencyProperty.Register( nameof(Animation), typeof(IAnimation), - typeof(RunAnimation), + typeof(RunAnimationAction), new PropertyMetadata(default(IAnimation))); public IAnimation Animation @@ -27,7 +25,7 @@ public IAnimation Animation public static readonly DependencyProperty TargetPropertyProperty = DependencyProperty.Register( nameof(TargetProperty), typeof(DependencyProperty), - typeof(RunAnimation), + typeof(RunAnimationAction), new PropertyMetadata(default(DependencyProperty))); public DependencyProperty TargetProperty diff --git a/PCL.Core/UI/Animation/Core/SequentialAnimationGroup.cs b/PCL.Core/UI/Animation/Core/SequentialAnimationGroup.cs index 6a3792015..cd440b24e 100644 --- a/PCL.Core/UI/Animation/Core/SequentialAnimationGroup.cs +++ b/PCL.Core/UI/Animation/Core/SequentialAnimationGroup.cs @@ -1,4 +1,6 @@ -using System.Threading.Tasks; +using System; +using System.Linq; +using System.Threading.Tasks; using PCL.Core.UI.Animation.Animatable; namespace PCL.Core.UI.Animation.Core; @@ -8,18 +10,68 @@ namespace PCL.Core.UI.Animation.Core; /// public sealed class SequentialAnimationGroup : AnimationGroup { - public override async Task RunAsync(IAnimatable target) + private TaskCompletionSource? _cancelTcs; + + public override async Task RunAsync(IAnimatable target) { - foreach (var child in Children) + Status = AnimationStatus.Running; + AnimationService.PushAnimationFireAndForget(this, target); + + _cancelTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var childrenSnapshot = Children.ToList(); + + lock (ChildrenCore) { - var childTarget = ResolveTarget(child, target); - await child.RunAsync(childTarget); + if (Status == AnimationStatus.Canceled) return this; } + + try + { + foreach (var child in childrenSnapshot) + { + // 检查取消信号 + if (Status == AnimationStatus.Canceled) break; + + var childTarget = ResolveTarget(child, target); + Task childWaiter; + + lock (ChildrenCore) + { + var runChild = child.RunFireAndForget(childTarget); + ChildrenCore.Add(runChild); + + childWaiter = CreateChildAwaiter(runChild); + } + + // 等待到当前子动画完成或组被取消 + await Task.WhenAny(childWaiter, _cancelTcs.Task); + + // 如果是取消触发的醒来,直接跳出循环 + if (Status == AnimationStatus.Canceled) break; + } + } + finally + { + if (Status != AnimationStatus.Canceled) + { + Status = AnimationStatus.Completed; + } + _cancelTcs = null; + } + + return this; + } + + public override void Cancel() + { + base.Cancel(); + _cancelTcs?.TrySetResult(); } - public override void RunFireAndForget(IAnimatable target) + public override IAnimation RunFireAndForget(IAnimatable target) { - // 由于顺序执行的特性,这里直接调用异步方法并且不等待其完成,无法享受 FireAndForget 的好处。 _ = RunAsync(target); + return this; } } \ No newline at end of file diff --git a/PCL.Core/UI/Animation/DoubleFromToAnimation.cs b/PCL.Core/UI/Animation/DoubleFromToAnimation.cs index b2331996e..c21cf34c2 100644 --- a/PCL.Core/UI/Animation/DoubleFromToAnimation.cs +++ b/PCL.Core/UI/Animation/DoubleFromToAnimation.cs @@ -1,4 +1,5 @@ -using PCL.Core.UI.Animation.Animatable; +using System.Diagnostics; +using PCL.Core.UI.Animation.Animatable; using PCL.Core.UI.Animation.Core; namespace PCL.Core.UI.Animation; diff --git a/PCL.Core/UI/Animation/Easings/BackEaseIn.cs b/PCL.Core/UI/Animation/Easings/BackEaseIn.cs index 928367b99..64def1a66 100644 --- a/PCL.Core/UI/Animation/Easings/BackEaseIn.cs +++ b/PCL.Core/UI/Animation/Easings/BackEaseIn.cs @@ -4,6 +4,8 @@ namespace PCL.Core.UI.Animation.Easings; public class BackEaseIn : Easing { + public static BackEaseIn Shared { get; } = new(); + protected override double EaseCore(double progress) { return progress * (progress * progress - Math.Sin(progress * Math.PI)); diff --git a/PCL.Core/UI/Animation/Easings/BackEaseInOut.cs b/PCL.Core/UI/Animation/Easings/BackEaseInOut.cs index 07a79eced..e9b373ec8 100644 --- a/PCL.Core/UI/Animation/Easings/BackEaseInOut.cs +++ b/PCL.Core/UI/Animation/Easings/BackEaseInOut.cs @@ -4,6 +4,8 @@ namespace PCL.Core.UI.Animation.Easings; public class BackEaseInOut : Easing { + public static BackEaseInOut Shared { get; } = new(); + protected override double EaseCore(double progress) { if (progress < 0.5) diff --git a/PCL.Core/UI/Animation/Easings/BackEaseOut.cs b/PCL.Core/UI/Animation/Easings/BackEaseOut.cs index 60578e15c..d59a978e6 100644 --- a/PCL.Core/UI/Animation/Easings/BackEaseOut.cs +++ b/PCL.Core/UI/Animation/Easings/BackEaseOut.cs @@ -4,6 +4,8 @@ namespace PCL.Core.UI.Animation.Easings; public class BackEaseOut : Easing { + public static BackEaseOut Shared { get; } = new(); + protected override double EaseCore(double progress) { var p = 1 - progress; diff --git a/PCL.Core/UI/Animation/Easings/BackEaseWithPowerIn.cs b/PCL.Core/UI/Animation/Easings/BackEaseWithPowerIn.cs new file mode 100644 index 000000000..9974cd6f1 --- /dev/null +++ b/PCL.Core/UI/Animation/Easings/BackEaseWithPowerIn.cs @@ -0,0 +1,14 @@ +using System; + +namespace PCL.Core.UI.Animation.Easings; + +public class BackEaseWithPowerIn(EasePower power = EasePower.Middle) : Easing +{ + private readonly double _p = 3.0 - (double)power * 0.5; + + protected override double EaseCore(double progress) + { + var t = Math.Clamp(progress, 0.0, 1.0); + return Math.Pow(t, _p) * Math.Cos(1.5 * Math.PI * (1.0 - t)); + } +} \ No newline at end of file diff --git a/PCL.Core/UI/Animation/Easings/BackEaseWithPowerInOut.cs b/PCL.Core/UI/Animation/Easings/BackEaseWithPowerInOut.cs new file mode 100644 index 000000000..ac337ba9c --- /dev/null +++ b/PCL.Core/UI/Animation/Easings/BackEaseWithPowerInOut.cs @@ -0,0 +1,25 @@ +using System; + +namespace PCL.Core.UI.Animation.Easings; + +public class BackEaseWithPowerInOut(EasePower power = EasePower.Middle) : Easing +{ + private readonly double _p = 3.0 - (double)power * 0.5; + + protected override double EaseCore(double progress) + { + var t = Math.Clamp(progress, 0.0, 1.0); + + if (t < 0.5) + { + var f = 2.0 * t; + return 0.5 * (Math.Pow(f, _p) * Math.Cos(1.5 * Math.PI * (1.0 - f))); + } + else + { + var f = 2.0 * (t - 0.5); + var inv = 1.0 - f; + return 0.5 * (1.0 - Math.Pow(inv, _p) * Math.Cos(1.5 * Math.PI * f)) + 0.5; + } + } +} \ No newline at end of file diff --git a/PCL.Core/UI/Animation/Easings/BackEaseWithPowerOut.cs b/PCL.Core/UI/Animation/Easings/BackEaseWithPowerOut.cs new file mode 100644 index 000000000..6343f6372 --- /dev/null +++ b/PCL.Core/UI/Animation/Easings/BackEaseWithPowerOut.cs @@ -0,0 +1,15 @@ +using System; + +namespace PCL.Core.UI.Animation.Easings; + +public class BackEaseWithPowerOut(EasePower power = EasePower.Middle) : Easing +{ + private readonly double _p = 3.0 - (double)power * 0.5; + + protected override double EaseCore(double progress) + { + var t = Math.Clamp(progress, 0.0, 1.0); + var inv = 1.0 - t; + return 1.0 - Math.Pow(inv, _p) * Math.Cos(1.5 * Math.PI * t); + } +} \ No newline at end of file diff --git a/PCL.Core/UI/Animation/Easings/BounceEaseIn.cs b/PCL.Core/UI/Animation/Easings/BounceEaseIn.cs index e5491fdca..1558c6c7a 100644 --- a/PCL.Core/UI/Animation/Easings/BounceEaseIn.cs +++ b/PCL.Core/UI/Animation/Easings/BounceEaseIn.cs @@ -4,6 +4,8 @@ namespace PCL.Core.UI.Animation.Easings; public class BounceEaseIn : Easing { + public static BounceEaseIn Shared { get; } = new(); + protected override double EaseCore(double progress) { return 1 - EaseUtils.Bounce(1 - progress); diff --git a/PCL.Core/UI/Animation/Easings/BounceEaseInOut.cs b/PCL.Core/UI/Animation/Easings/BounceEaseInOut.cs index 261fa2d87..8a40dac82 100644 --- a/PCL.Core/UI/Animation/Easings/BounceEaseInOut.cs +++ b/PCL.Core/UI/Animation/Easings/BounceEaseInOut.cs @@ -4,6 +4,8 @@ namespace PCL.Core.UI.Animation.Easings; public class BounceEaseInOut : Easing { + public static BounceEaseInOut Shared { get; } = new(); + protected override double EaseCore(double progress) { if (progress < 0.5) diff --git a/PCL.Core/UI/Animation/Easings/BounceEaseOut.cs b/PCL.Core/UI/Animation/Easings/BounceEaseOut.cs index 412351e6b..e1496db8d 100644 --- a/PCL.Core/UI/Animation/Easings/BounceEaseOut.cs +++ b/PCL.Core/UI/Animation/Easings/BounceEaseOut.cs @@ -4,6 +4,8 @@ namespace PCL.Core.UI.Animation.Easings; public class BounceEaseOut : Easing { + public static BounceEaseOut Shared { get; } = new(); + protected override double EaseCore(double progress) { return EaseUtils.Bounce(progress); diff --git a/PCL.Core/UI/Animation/Easings/CircularEaseIn.cs b/PCL.Core/UI/Animation/Easings/CircularEaseIn.cs index 8a29c2227..d9fa653a7 100644 --- a/PCL.Core/UI/Animation/Easings/CircularEaseIn.cs +++ b/PCL.Core/UI/Animation/Easings/CircularEaseIn.cs @@ -4,6 +4,8 @@ namespace PCL.Core.UI.Animation.Easings; public class CircularEaseIn : Easing { + public static CircularEaseIn Shared { get; } = new(); + protected override double EaseCore(double progress) { return 1 - Math.Sqrt(1d - progress * progress); diff --git a/PCL.Core/UI/Animation/Easings/CircularEaseInOut.cs b/PCL.Core/UI/Animation/Easings/CircularEaseInOut.cs index edcaed1a5..1b33af137 100644 --- a/PCL.Core/UI/Animation/Easings/CircularEaseInOut.cs +++ b/PCL.Core/UI/Animation/Easings/CircularEaseInOut.cs @@ -4,6 +4,8 @@ namespace PCL.Core.UI.Animation.Easings; public class CircularEaseInOut : Easing { + public static CircularEaseInOut Shared { get; } = new(); + protected override double EaseCore(double progress) { if (progress < 0.5) diff --git a/PCL.Core/UI/Animation/Easings/CircularEaseOut.cs b/PCL.Core/UI/Animation/Easings/CircularEaseOut.cs index 799554933..6869c3f2e 100644 --- a/PCL.Core/UI/Animation/Easings/CircularEaseOut.cs +++ b/PCL.Core/UI/Animation/Easings/CircularEaseOut.cs @@ -4,6 +4,8 @@ namespace PCL.Core.UI.Animation.Easings; public class CircularEaseOut : Easing { + public static CircularEaseOut Shared { get; } = new(); + protected override double EaseCore(double progress) { return Math.Sqrt((2d - progress) * progress); diff --git a/PCL.Core/UI/Animation/Easings/CombinedEasing.cs b/PCL.Core/UI/Animation/Easings/CombinedEasing.cs new file mode 100644 index 000000000..29bdf8d65 --- /dev/null +++ b/PCL.Core/UI/Animation/Easings/CombinedEasing.cs @@ -0,0 +1,21 @@ +using System; + +namespace PCL.Core.UI.Animation.Easings; + +public class CombinedEasing(IEasing ease1, IEasing ease2, double split = 0.5) : Easing +{ + private readonly IEasing _ease1 = ease1 ?? throw new ArgumentNullException(nameof(ease1)); + private readonly IEasing _ease2 = ease2 ?? throw new ArgumentNullException(nameof(ease2)); + + private readonly double _split = Math.Clamp(split, 0.00001, 0.99999); + + protected override double EaseCore(double t) + { + if (t < _split) + { + return _split * _ease1.Ease(t / _split); + } + + return (1.0 - _split) * _ease2.Ease((t - _split) / (1.0 - _split)) + _split; + } +} \ No newline at end of file diff --git a/PCL.Core/UI/Animation/Easings/CompositeEasing.cs b/PCL.Core/UI/Animation/Easings/CompositeEasing.cs new file mode 100644 index 000000000..28ad1a657 --- /dev/null +++ b/PCL.Core/UI/Animation/Easings/CompositeEasing.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace PCL.Core.UI.Animation.Easings; + +/// +/// 复合缓动,支持多个缓动混合,每个缓动可独立设置时长,延迟和权重。 +/// +public class CompositeEasing : Easing +{ + private readonly List<(IEasing easing, TimeSpan duration, TimeSpan delay, double weight)> _easings; + private readonly TimeSpan _totalDuration; + + /// + /// 初始化复合缓动。 + /// + /// 参数元组:(缓动逻辑, 持续时长) + public CompositeEasing(params (IEasing easing, TimeSpan duration)[] easings) + : this(easings.Select(e => (e.easing, e.duration, TimeSpan.Zero, 1.0 / (easings.Length > 0 ? easings.Length : 1))).ToArray()) + { + } + + /// + /// 初始化复合缓动。 + /// + /// 参数元组:(缓动逻辑, 持续时长, 延迟时间) + public CompositeEasing(params (IEasing easing, TimeSpan duration, TimeSpan delay)[] easings) + : this(easings.Select(e => (e.easing, e.duration, e.delay, 1.0 / (easings.Length > 0 ? easings.Length : 1))).ToArray()) + { + } + + /// + /// 初始化复合缓动。 + /// + /// 参数元组:(缓动逻辑, 持续时长, 权重) + public CompositeEasing(params (IEasing easing, TimeSpan duration, double weight)[] easings) + : this(easings.Select(e => (e.easing, e.duration, TimeSpan.Zero, e.weight)).ToArray()) + { + } + + /// + /// 初始化复合缓动。 + /// + /// 参数元组:(缓动逻辑, 持续时长, 延迟时间, 权重) + public CompositeEasing(params (IEasing easing, TimeSpan duration, TimeSpan delay, double weight)[] easings) + { + if (easings == null || easings.Length == 0) + throw new ArgumentException("至少需要一个缓动", nameof(easings)); + + _easings = new List<(IEasing, TimeSpan, TimeSpan, double)>(easings.Length); + + long maxTicks = 0; + + foreach (var (easing, duration, delay, weight) in easings) + { + if (easing is null) throw new ArgumentNullException(nameof(easing)); + if (duration <= TimeSpan.Zero) throw new ArgumentException("duration 必须大于 zero"); + + _easings.Add((easing, duration, delay, weight)); + + long endTicks = (delay + duration).Ticks; + if (endTicks > maxTicks) + maxTicks = endTicks; + } + + _totalDuration = TimeSpan.FromTicks(maxTicks); + } + + public TimeSpan TotalDuration => _totalDuration; + + protected override double EaseCore(double progress) + { + // 计算当前绝对时间 + var elapsed = _totalDuration * progress; + + var value = 0.0; + + foreach (var (easing, duration, delay, weight) in _easings) + { + if (weight == 0) continue; + + // 计算相对于该缓动的时间 + var localElapsed = elapsed - delay; + + double easingValue; + + // 还没开始 + if (localElapsed <= TimeSpan.Zero) + { + easingValue = 0.0; + } + // 已经结束 + else if (localElapsed >= duration) + { + // 保持最终状态 + easingValue = easing.Ease(1.0); + } + // 正在运行 + else + { + // 避免除法浮点数计算问题 + var localProgress = localElapsed.TotalSeconds / duration.TotalSeconds; + easingValue = easing.Ease(localProgress); + } + + value += easingValue * weight; + } + + return value; + } +} \ No newline at end of file diff --git a/PCL.Core/UI/Animation/Easings/CubicEaseIn.cs b/PCL.Core/UI/Animation/Easings/CubicEaseIn.cs index 6db012f1b..7d7b37a9c 100644 --- a/PCL.Core/UI/Animation/Easings/CubicEaseIn.cs +++ b/PCL.Core/UI/Animation/Easings/CubicEaseIn.cs @@ -2,6 +2,8 @@ public class CubicEaseIn : Easing { + public static CubicEaseIn Shared { get; } = new(); + protected override double EaseCore(double progress) { return progress * progress * progress; diff --git a/PCL.Core/UI/Animation/Easings/CubicEaseInOut.cs b/PCL.Core/UI/Animation/Easings/CubicEaseInOut.cs index 284be5a90..4ba98d1bf 100644 --- a/PCL.Core/UI/Animation/Easings/CubicEaseInOut.cs +++ b/PCL.Core/UI/Animation/Easings/CubicEaseInOut.cs @@ -2,6 +2,8 @@ public class CubicEaseInOut : Easing { + public static CubicEaseInOut Shared { get; } = new(); + protected override double EaseCore(double progress) { if (progress < 0.5) diff --git a/PCL.Core/UI/Animation/Easings/CubicEaseOut.cs b/PCL.Core/UI/Animation/Easings/CubicEaseOut.cs index 35a607769..600cd3ad0 100644 --- a/PCL.Core/UI/Animation/Easings/CubicEaseOut.cs +++ b/PCL.Core/UI/Animation/Easings/CubicEaseOut.cs @@ -2,6 +2,8 @@ public class CubicEaseOut : Easing { + public static CubicEaseOut Shared { get; } = new(); + protected override double EaseCore(double progress) { var f = progress - 1; diff --git a/PCL.Core/UI/Animation/Easings/EasePower.cs b/PCL.Core/UI/Animation/Easings/EasePower.cs new file mode 100644 index 000000000..b6eabfc6a --- /dev/null +++ b/PCL.Core/UI/Animation/Easings/EasePower.cs @@ -0,0 +1,9 @@ +namespace PCL.Core.UI.Animation.Easings; + +public enum EasePower +{ + Weak = 2, + Middle = 3, + Strong = 4, + ExtraStrong = 5 +} \ No newline at end of file diff --git a/PCL.Core/UI/Animation/Easings/Easing.cs b/PCL.Core/UI/Animation/Easings/Easing.cs index dfd7ee7ea..7246ca493 100644 --- a/PCL.Core/UI/Animation/Easings/Easing.cs +++ b/PCL.Core/UI/Animation/Easings/Easing.cs @@ -19,6 +19,6 @@ public double Ease(double progress) public double Ease(int currentFrame, int totalFrames) { - return totalFrames <= 0 ? 0.0 : Ease((double)currentFrame / totalFrames); + return totalFrames <= 1 ? 1.0 : Ease((double)currentFrame / (totalFrames - 1)); } } \ No newline at end of file diff --git a/PCL.Core/UI/Animation/Easings/ElasticEaseIn.cs b/PCL.Core/UI/Animation/Easings/ElasticEaseIn.cs index c423fa224..054b073f4 100644 --- a/PCL.Core/UI/Animation/Easings/ElasticEaseIn.cs +++ b/PCL.Core/UI/Animation/Easings/ElasticEaseIn.cs @@ -5,6 +5,8 @@ namespace PCL.Core.UI.Animation.Easings; public class ElasticEaseIn : Easing { + public static ElasticEaseIn Shared { get; } = new(); + protected override double EaseCore(double progress) { return Math.Sin(EaseUtils.ElasticPiTimes6Point5 * progress) * diff --git a/PCL.Core/UI/Animation/Easings/ElasticEaseInOut.cs b/PCL.Core/UI/Animation/Easings/ElasticEaseInOut.cs index c7cc60fd0..333e01956 100644 --- a/PCL.Core/UI/Animation/Easings/ElasticEaseInOut.cs +++ b/PCL.Core/UI/Animation/Easings/ElasticEaseInOut.cs @@ -5,6 +5,8 @@ namespace PCL.Core.UI.Animation.Easings; public class ElasticEaseInOut : Easing { + public static ElasticEaseInOut Shared { get; } = new(); + protected override double EaseCore(double progress) { if (progress < 0.5d) diff --git a/PCL.Core/UI/Animation/Easings/ElasticEaseOut.cs b/PCL.Core/UI/Animation/Easings/ElasticEaseOut.cs index efd0025a9..906c0490f 100644 --- a/PCL.Core/UI/Animation/Easings/ElasticEaseOut.cs +++ b/PCL.Core/UI/Animation/Easings/ElasticEaseOut.cs @@ -5,6 +5,8 @@ namespace PCL.Core.UI.Animation.Easings; public class ElasticEaseOut : Easing { + public static ElasticEaseOut Shared { get; } = new(); + protected override double EaseCore(double progress) { return Math.Sin(-EaseUtils.ElasticPiTimes6Point5 * (progress + 1d)) * diff --git a/PCL.Core/UI/Animation/Easings/ExponentialEaseIn.cs b/PCL.Core/UI/Animation/Easings/ExponentialEaseIn.cs index fc5061b12..a06f16dd3 100644 --- a/PCL.Core/UI/Animation/Easings/ExponentialEaseIn.cs +++ b/PCL.Core/UI/Animation/Easings/ExponentialEaseIn.cs @@ -4,6 +4,8 @@ namespace PCL.Core.UI.Animation.Easings; public class ExponentialEaseIn : Easing { + public static ExponentialEaseIn Shared { get; } = new(); + protected override double EaseCore(double progress) { return progress == 0 ? progress : Math.Pow(2, 10 * (progress - 1)); diff --git a/PCL.Core/UI/Animation/Easings/ExponentialEaseInOut.cs b/PCL.Core/UI/Animation/Easings/ExponentialEaseInOut.cs index bb6528114..b1a71cee8 100644 --- a/PCL.Core/UI/Animation/Easings/ExponentialEaseInOut.cs +++ b/PCL.Core/UI/Animation/Easings/ExponentialEaseInOut.cs @@ -4,6 +4,8 @@ namespace PCL.Core.UI.Animation.Easings; public class ExponentialEaseInOut : Easing { + public static ExponentialEaseInOut Shared { get; } = new(); + protected override double EaseCore(double progress) { if (progress < 0.5) diff --git a/PCL.Core/UI/Animation/Easings/ExponentialEaseOut.cs b/PCL.Core/UI/Animation/Easings/ExponentialEaseOut.cs index c5958041d..5109bbfe4 100644 --- a/PCL.Core/UI/Animation/Easings/ExponentialEaseOut.cs +++ b/PCL.Core/UI/Animation/Easings/ExponentialEaseOut.cs @@ -4,6 +4,8 @@ namespace PCL.Core.UI.Animation.Easings; public class ExponentialEaseOut : Easing { + public static ExponentialEaseOut Shared { get; } = new(); + protected override double EaseCore(double progress) { return Math.Abs(progress - 1.0) < 1e-4 ? progress : 1 - Math.Pow(2, -10 * progress); diff --git a/PCL.Core/UI/Animation/Easings/LinearEasing.cs b/PCL.Core/UI/Animation/Easings/LinearEasing.cs index ef3751526..24f7ffff3 100644 --- a/PCL.Core/UI/Animation/Easings/LinearEasing.cs +++ b/PCL.Core/UI/Animation/Easings/LinearEasing.cs @@ -2,6 +2,8 @@ public class LinearEasing : Easing { + public static LinearEasing Shared { get; } = new(); + protected override double EaseCore(double progress) { return progress; diff --git a/PCL.Core/UI/Animation/Easings/QuadEaseIn.cs b/PCL.Core/UI/Animation/Easings/QuadEaseIn.cs index 9baf5363b..bef1756aa 100644 --- a/PCL.Core/UI/Animation/Easings/QuadEaseIn.cs +++ b/PCL.Core/UI/Animation/Easings/QuadEaseIn.cs @@ -2,6 +2,8 @@ public class QuadEaseIn : Easing { + public static QuadEaseIn Shared { get; } = new(); + protected override double EaseCore(double progress) { return progress * progress; diff --git a/PCL.Core/UI/Animation/Easings/QuadEaseInOut.cs b/PCL.Core/UI/Animation/Easings/QuadEaseInOut.cs index 6f79dc8d9..f07d3db8d 100644 --- a/PCL.Core/UI/Animation/Easings/QuadEaseInOut.cs +++ b/PCL.Core/UI/Animation/Easings/QuadEaseInOut.cs @@ -2,6 +2,8 @@ public class QuadEaseInOut : Easing { + public static QuadEaseInOut Shared { get; } = new(); + protected override double EaseCore(double progress) { if (progress < 0.5) diff --git a/PCL.Core/UI/Animation/Easings/QuadEaseOut.cs b/PCL.Core/UI/Animation/Easings/QuadEaseOut.cs index 85477f80d..c2cc52492 100644 --- a/PCL.Core/UI/Animation/Easings/QuadEaseOut.cs +++ b/PCL.Core/UI/Animation/Easings/QuadEaseOut.cs @@ -2,6 +2,8 @@ public class QuadEaseOut : Easing { + public static QuadEaseOut Shared { get; } = new(); + protected override double EaseCore(double progress) { return 1 - (1 - progress) * (1 - progress); diff --git a/PCL.Core/UI/Animation/Easings/QuarticEaseIn.cs b/PCL.Core/UI/Animation/Easings/QuarticEaseIn.cs index 23f180db2..3897d2974 100644 --- a/PCL.Core/UI/Animation/Easings/QuarticEaseIn.cs +++ b/PCL.Core/UI/Animation/Easings/QuarticEaseIn.cs @@ -2,6 +2,8 @@ public class QuarticEaseIn : Easing { + public static QuarticEaseIn Shared { get; } = new(); + protected override double EaseCore(double progress) { var p2 = progress * progress; diff --git a/PCL.Core/UI/Animation/Easings/QuarticEaseInOut.cs b/PCL.Core/UI/Animation/Easings/QuarticEaseInOut.cs index 70a42bc7d..082780172 100644 --- a/PCL.Core/UI/Animation/Easings/QuarticEaseInOut.cs +++ b/PCL.Core/UI/Animation/Easings/QuarticEaseInOut.cs @@ -2,6 +2,8 @@ public class QuarticEaseInOut : Easing { + public static QuarticEaseInOut Shared { get; } = new(); + protected override double EaseCore(double progress) { if (progress < 0.5d) diff --git a/PCL.Core/UI/Animation/Easings/QuarticEaseOut.cs b/PCL.Core/UI/Animation/Easings/QuarticEaseOut.cs index 454e70662..80323f890 100644 --- a/PCL.Core/UI/Animation/Easings/QuarticEaseOut.cs +++ b/PCL.Core/UI/Animation/Easings/QuarticEaseOut.cs @@ -2,6 +2,8 @@ public class QuarticEaseOut : Easing { + public static QuarticEaseOut Shared { get; } = new(); + protected override double EaseCore(double progress) { var f = progress - 1; diff --git a/PCL.Core/UI/Animation/Easings/QuinticEaseIn.cs b/PCL.Core/UI/Animation/Easings/QuinticEaseIn.cs index 842fe0e16..67feef4d7 100644 --- a/PCL.Core/UI/Animation/Easings/QuinticEaseIn.cs +++ b/PCL.Core/UI/Animation/Easings/QuinticEaseIn.cs @@ -2,6 +2,8 @@ public class QuinticEaseIn : Easing { + public static QuinticEaseIn Shared { get; } = new(); + protected override double EaseCore(double progress) { var p2 = progress * progress; diff --git a/PCL.Core/UI/Animation/Easings/QuinticEaseInOut.cs b/PCL.Core/UI/Animation/Easings/QuinticEaseInOut.cs index 1ef718985..06bf35f2c 100644 --- a/PCL.Core/UI/Animation/Easings/QuinticEaseInOut.cs +++ b/PCL.Core/UI/Animation/Easings/QuinticEaseInOut.cs @@ -2,6 +2,8 @@ public class QuinticEaseInOut : Easing { + public static QuinticEaseInOut Shared { get; } = new(); + protected override double EaseCore(double progress) { if (progress < 0.5) diff --git a/PCL.Core/UI/Animation/Easings/QuinticEaseOut.cs b/PCL.Core/UI/Animation/Easings/QuinticEaseOut.cs index b0c7d7e0d..d36313966 100644 --- a/PCL.Core/UI/Animation/Easings/QuinticEaseOut.cs +++ b/PCL.Core/UI/Animation/Easings/QuinticEaseOut.cs @@ -2,6 +2,8 @@ public class QuinticEaseOut : Easing { + public static QuinticEaseOut Shared { get; } = new(); + protected override double EaseCore(double progress) { var f = progress - 1d; diff --git a/PCL.Core/UI/Animation/Easings/SineEaseIn.cs b/PCL.Core/UI/Animation/Easings/SineEaseIn.cs index 2e9f96f6a..66d823c8f 100644 --- a/PCL.Core/UI/Animation/Easings/SineEaseIn.cs +++ b/PCL.Core/UI/Animation/Easings/SineEaseIn.cs @@ -4,6 +4,8 @@ namespace PCL.Core.UI.Animation.Easings; public class SineEaseIn : Easing { + public static SineEaseIn Shared { get; } = new(); + protected override double EaseCore(double progress) { return 1 - Math.Cos(progress * Math.PI / 2); diff --git a/PCL.Core/UI/Animation/Easings/SineEaseInOut.cs b/PCL.Core/UI/Animation/Easings/SineEaseInOut.cs index f6d2de93b..b4aeb6e5b 100644 --- a/PCL.Core/UI/Animation/Easings/SineEaseInOut.cs +++ b/PCL.Core/UI/Animation/Easings/SineEaseInOut.cs @@ -4,6 +4,8 @@ namespace PCL.Core.UI.Animation.Easings; public class SineEaseInOut : Easing { + public static SineEaseInOut Shared { get; } = new(); + protected override double EaseCore(double progress) { return -(Math.Cos(Math.PI * progress) - 1) / 2; diff --git a/PCL.Core/UI/Animation/Easings/SineEaseOut.cs b/PCL.Core/UI/Animation/Easings/SineEaseOut.cs index b4c114b03..7aa4116b1 100644 --- a/PCL.Core/UI/Animation/Easings/SineEaseOut.cs +++ b/PCL.Core/UI/Animation/Easings/SineEaseOut.cs @@ -4,6 +4,8 @@ namespace PCL.Core.UI.Animation.Easings; public class SineEaseOut : Easing { + public static SineEaseOut Shared { get; } = new(); + protected override double EaseCore(double progress) { return Math.Sin(progress * Math.PI / 2); diff --git a/PCL.Core/UI/Animation/MatrixFromToAnimation.cs b/PCL.Core/UI/Animation/MatrixFromToAnimation.cs index bf8d71990..8b58ad0bf 100644 --- a/PCL.Core/UI/Animation/MatrixFromToAnimation.cs +++ b/PCL.Core/UI/Animation/MatrixFromToAnimation.cs @@ -14,9 +14,9 @@ public class MatrixFromToAnimation : FromToAnimationBase // 计算当前值 CurrentValue = ValueType == AnimationValueType.Relative - ? ValueProcessorManager.Add(From!.Value, ValueProcessorManager.Scale(To, easedProgress)) - : ValueProcessorManager.Add(From!.Value, - ValueProcessorManager.Scale(ValueProcessorManager.Subtract(To, From!.Value), easedProgress)); + ? ValueProcessorManager.Add(From, ValueProcessorManager.Scale(To, easedProgress)) + : ValueProcessorManager.Add(From, + ValueProcessorManager.Scale(ValueProcessorManager.Subtract(To, From), easedProgress)); return base.ComputeNextFrame(target); } diff --git a/PCL.Core/UI/Animation/NRotateTransformFromToAnimation.cs b/PCL.Core/UI/Animation/NRotateTransformFromToAnimation.cs new file mode 100644 index 000000000..00d805910 --- /dev/null +++ b/PCL.Core/UI/Animation/NRotateTransformFromToAnimation.cs @@ -0,0 +1,23 @@ +using System.Windows.Media; +using PCL.Core.UI.Animation.Animatable; +using PCL.Core.UI.Animation.Core; +using PCL.Core.UI.Animation.ValueProcessor; + +namespace PCL.Core.UI.Animation; + +public class NRotateTransformFromToAnimation : FromToAnimationBase +{ + public override IAnimationFrame? ComputeNextFrame(IAnimatable target) + { + // 应用缓动函数 + var easedProgress = Easing.Ease(CurrentFrame, TotalFrames); + + // 计算当前值 + CurrentValue = ValueType == AnimationValueType.Relative + ? ValueProcessorManager.Add(From, ValueProcessorManager.Scale(To, easedProgress)) + : ValueProcessorManager.Add(From, + ValueProcessorManager.Scale(ValueProcessorManager.Subtract(To, From), easedProgress)); + + return base.ComputeNextFrame(target); + } +} \ No newline at end of file diff --git a/PCL.Core/UI/Animation/NScaleTransformFromToAnimation.cs b/PCL.Core/UI/Animation/NScaleTransformFromToAnimation.cs new file mode 100644 index 000000000..1763fb022 --- /dev/null +++ b/PCL.Core/UI/Animation/NScaleTransformFromToAnimation.cs @@ -0,0 +1,23 @@ +using System.Windows.Media; +using PCL.Core.UI.Animation.Animatable; +using PCL.Core.UI.Animation.Core; +using PCL.Core.UI.Animation.ValueProcessor; + +namespace PCL.Core.UI.Animation; + +public class NScaleTransformFromToAnimation : FromToAnimationBase +{ + public override IAnimationFrame? ComputeNextFrame(IAnimatable target) + { + // 应用缓动函数 + var easedProgress = Easing.Ease(CurrentFrame, TotalFrames); + + // 计算当前值 + CurrentValue = ValueType == AnimationValueType.Relative + ? ValueProcessorManager.Add(From, ValueProcessorManager.Scale(To, easedProgress)) + : ValueProcessorManager.Add(From, + ValueProcessorManager.Scale(ValueProcessorManager.Subtract(To, From), easedProgress)); + + return base.ComputeNextFrame(target); + } +} \ No newline at end of file diff --git a/PCL.Core/UI/Animation/PointFromToAnimation.cs b/PCL.Core/UI/Animation/PointFromToAnimation.cs index f44329550..bff0ec0ba 100644 --- a/PCL.Core/UI/Animation/PointFromToAnimation.cs +++ b/PCL.Core/UI/Animation/PointFromToAnimation.cs @@ -14,9 +14,9 @@ public class PointFromToAnimation : FromToAnimationBase // 计算当前值 CurrentValue = ValueType == AnimationValueType.Relative - ? ValueProcessorManager.Add(From!.Value, ValueProcessorManager.Scale(To, easedProgress)) - : ValueProcessorManager.Add(From!.Value, - ValueProcessorManager.Scale(ValueProcessorManager.Subtract(To, From!.Value), easedProgress)); + ? ValueProcessorManager.Add(From, ValueProcessorManager.Scale(To, easedProgress)) + : ValueProcessorManager.Add(From, + ValueProcessorManager.Scale(ValueProcessorManager.Subtract(To, From), easedProgress)); return base.ComputeNextFrame(target); } diff --git a/PCL.Core/UI/Animation/ThicknessFromToAnimation.cs b/PCL.Core/UI/Animation/ThicknessFromToAnimation.cs index a7a328c9e..cbdea1cdd 100644 --- a/PCL.Core/UI/Animation/ThicknessFromToAnimation.cs +++ b/PCL.Core/UI/Animation/ThicknessFromToAnimation.cs @@ -14,9 +14,9 @@ public class ThicknessFromToAnimation : FromToAnimationBase // 计算当前值 CurrentValue = ValueType == AnimationValueType.Relative - ? ValueProcessorManager.Add(From!.Value, ValueProcessorManager.Scale(To, easedProgress)) - : ValueProcessorManager.Add(From!.Value, - ValueProcessorManager.Scale(ValueProcessorManager.Subtract(To, From!.Value), easedProgress)); + ? ValueProcessorManager.Add(From, ValueProcessorManager.Scale(To, easedProgress)) + : ValueProcessorManager.Add(From, + ValueProcessorManager.Scale(ValueProcessorManager.Subtract(To, From), easedProgress)); return base.ComputeNextFrame(target); } diff --git a/PCL.Core/UI/Animation/ValueProcessor/DoubleValueProcessor.cs b/PCL.Core/UI/Animation/ValueProcessor/DoubleValueProcessor.cs index 20ea409e3..701335aa1 100644 --- a/PCL.Core/UI/Animation/ValueProcessor/DoubleValueProcessor.cs +++ b/PCL.Core/UI/Animation/ValueProcessor/DoubleValueProcessor.cs @@ -4,10 +4,15 @@ namespace PCL.Core.UI.Animation.ValueProcessor; public class DoubleValueProcessor : IValueProcessor { - public double Filter(double value) => Math.Max(0, value); - + public double Filter(double value) => value; + public double Add(double value1, double value2) => value1 + value2; - + public double Subtract(double value1, double value2) => value1 - value2; + public double Scale(double value, double factor) => value * factor; + + public double DefaultValue() => 0; + + public bool Equal(double value1, double value2) => Math.Abs(value1 - value2) < 1e-6; } \ No newline at end of file diff --git a/PCL.Core/UI/Animation/ValueProcessor/IValueProcessor.cs b/PCL.Core/UI/Animation/ValueProcessor/IValueProcessor.cs index 6694c816f..9fe576a78 100644 --- a/PCL.Core/UI/Animation/ValueProcessor/IValueProcessor.cs +++ b/PCL.Core/UI/Animation/ValueProcessor/IValueProcessor.cs @@ -35,4 +35,18 @@ public interface IValueProcessor /// 缩放因子。 /// 返回缩放后的值。 T Scale(T value, double factor); + + /// + /// 获取某种类型的初始值。 + /// + /// 初始值。 + T DefaultValue(); + + /// + /// 比较两个值是否相等。 + /// + /// 第一个值。 + /// 第二个值。 + /// + bool Equal(T value1, T value2); } \ No newline at end of file diff --git a/PCL.Core/UI/Animation/ValueProcessor/MatrixValueProcessor.cs b/PCL.Core/UI/Animation/ValueProcessor/MatrixValueProcessor.cs index 11edf954b..1d9092901 100644 --- a/PCL.Core/UI/Animation/ValueProcessor/MatrixValueProcessor.cs +++ b/PCL.Core/UI/Animation/ValueProcessor/MatrixValueProcessor.cs @@ -29,4 +29,8 @@ public Matrix Scale(Matrix value, double factor) value.M21 * factor, value.M22 * factor, value.OffsetX * factor, value.OffsetY * factor); } + + public Matrix DefaultValue() => new(); + + public bool Equal(Matrix value1, Matrix value2) => value1 == value2; } \ No newline at end of file diff --git a/PCL.Core/UI/Animation/ValueProcessor/NColorValueProcessor.cs b/PCL.Core/UI/Animation/ValueProcessor/NColorValueProcessor.cs index 132804392..7b0d791ff 100644 --- a/PCL.Core/UI/Animation/ValueProcessor/NColorValueProcessor.cs +++ b/PCL.Core/UI/Animation/ValueProcessor/NColorValueProcessor.cs @@ -11,9 +11,14 @@ public NColor Filter(NColor value) return value; } - + public NColor Add(NColor value1, NColor value2) => value1 + value2; - + public NColor Subtract(NColor value1, NColor value2) => value1 - value2; + public NColor Scale(NColor value, double factor) => value * (float)factor; + + public NColor DefaultValue() => new(); + + public bool Equal(NColor value1, NColor value2) => value1 == value2; } \ No newline at end of file diff --git a/PCL.Core/UI/Animation/ValueProcessor/NRotateTransformValueProcessor.cs b/PCL.Core/UI/Animation/ValueProcessor/NRotateTransformValueProcessor.cs new file mode 100644 index 000000000..d3ad30b28 --- /dev/null +++ b/PCL.Core/UI/Animation/ValueProcessor/NRotateTransformValueProcessor.cs @@ -0,0 +1,16 @@ +namespace PCL.Core.UI.Animation.ValueProcessor; + +public class NRotateTransformValueProcessor : IValueProcessor +{ + public NRotateTransform Filter(NRotateTransform value) => value; + + public NRotateTransform Add(NRotateTransform value1, NRotateTransform value2) => value1 + value2; + + public NRotateTransform Subtract(NRotateTransform value1, NRotateTransform value2) => value1 - value2; + + public NRotateTransform Scale(NRotateTransform value, double factor) => value * (float)factor; + + public NRotateTransform DefaultValue() => new(); + + public bool Equal(NRotateTransform value1, NRotateTransform value2) => value1 == value2; +} \ No newline at end of file diff --git a/PCL.Core/UI/Animation/ValueProcessor/NScaleTransformValueProcessor.cs b/PCL.Core/UI/Animation/ValueProcessor/NScaleTransformValueProcessor.cs new file mode 100644 index 000000000..535b3c5e0 --- /dev/null +++ b/PCL.Core/UI/Animation/ValueProcessor/NScaleTransformValueProcessor.cs @@ -0,0 +1,16 @@ +namespace PCL.Core.UI.Animation.ValueProcessor; + +public class NScaleTransformValueProcessor : IValueProcessor +{ + public NScaleTransform Filter(NScaleTransform value) => value; + + public NScaleTransform Add(NScaleTransform value1, NScaleTransform value2) => value1 + value2; + + public NScaleTransform Subtract(NScaleTransform value1, NScaleTransform value2) => value1 - value2; + + public NScaleTransform Scale(NScaleTransform value, double factor) => value * (float)factor; + + public NScaleTransform DefaultValue() => new(); + + public bool Equal(NScaleTransform value1, NScaleTransform value2) => value1 == value2; +} \ No newline at end of file diff --git a/PCL.Core/UI/Animation/ValueProcessor/PointValueProcessor.cs b/PCL.Core/UI/Animation/ValueProcessor/PointValueProcessor.cs index 8e2995354..fb367f583 100644 --- a/PCL.Core/UI/Animation/ValueProcessor/PointValueProcessor.cs +++ b/PCL.Core/UI/Animation/ValueProcessor/PointValueProcessor.cs @@ -11,4 +11,8 @@ public class PointValueProcessor : IValueProcessor public Point Subtract(Point value1, Point value2) => new(value1.X - value2.X, value1.Y - value2.Y); public Point Scale(Point value, double factor) => new(value.X * factor, value.Y * factor); + + public Point DefaultValue() => new(); + + public bool Equal(Point value1, Point value2) => value1 == value2; } \ No newline at end of file diff --git a/PCL.Core/UI/Animation/ValueProcessor/ThicknessValueProcessor.cs b/PCL.Core/UI/Animation/ValueProcessor/ThicknessValueProcessor.cs index 4f972420e..396a2e084 100644 --- a/PCL.Core/UI/Animation/ValueProcessor/ThicknessValueProcessor.cs +++ b/PCL.Core/UI/Animation/ValueProcessor/ThicknessValueProcessor.cs @@ -4,7 +4,6 @@ namespace PCL.Core.UI.Animation.ValueProcessor; public class ThicknessValueProcessor : IValueProcessor { - // Thickness 不需要过滤 public Thickness Filter(Thickness value) => value; public Thickness Add(Thickness value1, Thickness value2) @@ -30,4 +29,8 @@ public Thickness Scale(Thickness value, double factor) value.Right * factor, value.Bottom * factor); } + + public Thickness DefaultValue() => new(); + + public bool Equal(Thickness value1, Thickness value2) => value1 == value2; } \ No newline at end of file diff --git a/PCL.Core/UI/Animation/ValueProcessor/ValueProcessorManager.cs b/PCL.Core/UI/Animation/ValueProcessor/ValueProcessorManager.cs index 964fffd2e..8eb213f76 100644 --- a/PCL.Core/UI/Animation/ValueProcessor/ValueProcessorManager.cs +++ b/PCL.Core/UI/Animation/ValueProcessor/ValueProcessorManager.cs @@ -5,7 +5,9 @@ namespace PCL.Core.UI.Animation.ValueProcessor; public static class ValueProcessorManager { - private static readonly Dictionary> _Filters = new(); + private static readonly Dictionary> Filters = new(); + private static readonly Dictionary> Adders = new(); + private static readonly Dictionary> Scalers = new(); private static class Cache { @@ -17,7 +19,9 @@ public static void Register(IValueProcessor processor) ArgumentNullException.ThrowIfNull(processor); Cache.Processor = processor; - _Filters[typeof(T)] = o => processor.Filter((T)o)!; + Filters[typeof(T)] = o => processor.Filter((T)o)!; + Adders[typeof(T)] = (o1, o2) => processor.Add((T)o1, (T)o2)!; + Scalers[typeof(T)] = (o, f) => processor.Scale((T)o, f)!; } public static T Filter(T value) @@ -29,7 +33,7 @@ public static T Filter(T value) public static object Filter(object value) { var t = value.GetType(); - return _Filters.TryGetValue(t, out var func) + return Filters.TryGetValue(t, out var func) ? func(value) : value; } @@ -41,6 +45,15 @@ public static T Add(T value1, T value2) return p.Add(value1, value2); } + public static object Add(object value1, object value2) + { + var t = value1.GetType(); + if (t != value2.GetType()) + throw new InvalidOperationException($"类型不一致:{t} vs {value2.GetType()}"); + + return Adders.TryGetValue(t, out var func) ? func(value1, value2) : value2; + } + public static T Subtract(T value1, T value2) { var p = Cache.Processor @@ -54,4 +67,24 @@ public static T Scale(T value, double factor) ?? throw new InvalidOperationException($"类型未注册:{typeof(T)}"); return p.Scale(value, factor); } + + public static object Scale(object value, double factor) + { + var t = value.GetType(); + return Scalers.TryGetValue(t, out var func) ? func(value, factor) : value; + } + + public static T DefaultValue() + { + var p = Cache.Processor + ?? throw new InvalidOperationException($"类型未注册:{typeof(T)}"); + return p.DefaultValue(); + } + + public static bool Equal(T value1, T value2) + { + var p = Cache.Processor + ?? throw new InvalidOperationException($"类型未注册:{typeof(T)}"); + return p.Equal(value1, value2); + } } \ No newline at end of file diff --git a/PCL.Core/UI/NColor.cs b/PCL.Core/UI/NColor.cs index ccb73c33f..d4202035b 100644 --- a/PCL.Core/UI/NColor.cs +++ b/PCL.Core/UI/NColor.cs @@ -1,6 +1,7 @@ using System; using System.Numerics; using System.Windows.Media; +using PCL.Core.App.IoC; namespace PCL.Core.UI; @@ -49,11 +50,10 @@ public NColor() public NColor(float r, float g, float b, float a = 255f) { - _color = new Vector4(Math.Clamp(r, 0, 255), Math.Clamp(g, 0, 255), Math.Clamp(b, 0, 255), - Math.Clamp(a, 0, 255)); + _color = new Vector4(r, g, b, a); } - public NColor(Color color) : this(color.R, color.G, color.B, a: color.A) + public NColor(Color color) : this(color.R, color.G, color.B, color.A) { } @@ -61,14 +61,34 @@ public NColor(System.Drawing.Color color) : this(color.R, color.G, color.B, colo { } - public NColor(string hex) + public NColor(string str) { - if (string.IsNullOrWhiteSpace(hex)) - throw new ArgumentException("颜色字符串不能为空。", nameof(hex)); + try + { + var resource = Lifecycle.CurrentApplication.FindResource(str); + switch (resource) + { + case Color color: + _color = new Vector4(color.R, color.G, color.B, color.A); + return; + case SolidColorBrush brush: + var brushColor = brush.Color; + _color = new Vector4(brushColor.R, brushColor.G, brushColor.B, brushColor.A); + return; + } + } + catch + { + // 忽略 + } + + + if (string.IsNullOrWhiteSpace(str)) + throw new ArgumentException("颜色字符串不能为空。", nameof(str)); - var trimmedString = hex.Trim(); + var trimmedString = str.Trim(); if (!trimmedString.StartsWith('#')) - throw new ArgumentException("颜色字符串必须以 '#' 开头。", nameof(hex)); + throw new ArgumentException("颜色字符串必须以 '#' 开头。", nameof(str)); trimmedString = trimmedString[1..]; @@ -104,7 +124,7 @@ public NColor(string hex) break; default: - throw new ArgumentException($"无效的颜色字符串长度:{trimmedString.Length}。", nameof(hex)); + throw new ArgumentException($"无效的颜色字符串长度:{trimmedString.Length}。", nameof(str)); } _color = new Vector4(r, g, b, a); @@ -126,40 +146,21 @@ public NColor(Brush brush) : this((SolidColorBrush)brush) { } + private NColor(Vector4 v) => _color = v; + #endregion #region 运算符重载 - public static NColor operator +(NColor a, NColor b) - { - return new NColor(a.R + b.R, a.G + b.G, a.B + b.B, a.A + b.A); - } + public static NColor operator +(NColor a, NColor b) => new(a._color + b._color); + public static NColor operator -(NColor a, NColor b) => new(a._color - b._color); + public static NColor operator *(NColor a, float b) => new(a._color * b); - public static NColor operator -(NColor a, NColor b) - { - return new NColor(a.R - b.R, a.G - b.G, a.B - b.B, a.A - b.A); - } + public static NColor operator /(NColor a, float b) => + b == 0 ? throw new DivideByZeroException("除数不能为零。") : new NColor(a._color / b); - public static NColor operator *(NColor a, float b) - { - return new NColor(a.R * b, a.G * b, a.B * b, a.A * b); - } - - public static NColor operator /(NColor a, float b) - { - if (b == 0) throw new DivideByZeroException("除数不能为零。"); - return new NColor(a.R / b, a.G / b, a.B / b, a.A / b); - } - - public static bool operator ==(NColor a, NColor b) - { - return a._color == b._color; - } - - public static bool operator !=(NColor a, NColor b) - { - return a._color != b._color; - } + public static bool operator ==(NColor a, NColor b) => a._color == b._color; + public static bool operator !=(NColor a, NColor b) => a._color != b._color; #endregion @@ -231,4 +232,21 @@ private static double _Hue(double v1, double v2, double vH) } #endregion + + #region 隐式转换 + + public static implicit operator Color(NColor color) => + Color.FromArgb( + (byte)Math.Clamp(color.A, 0, 255), + (byte)Math.Clamp(color.R, 0, 255), + (byte)Math.Clamp(color.G, 0, 255), + (byte)Math.Clamp(color.B, 0, 255)); + public static implicit operator Brush(NColor color) => new SolidColorBrush(color); + public static implicit operator SolidColorBrush(NColor color) => new(color); + + public static implicit operator NColor(Color color) => new(color); + public static implicit operator NColor(Brush brush) => new(brush); + public static implicit operator NColor(SolidColorBrush brush) => new(brush); + + #endregion } \ No newline at end of file diff --git a/PCL.Core/UI/NRotateTransform.cs b/PCL.Core/UI/NRotateTransform.cs new file mode 100644 index 000000000..08fa12950 --- /dev/null +++ b/PCL.Core/UI/NRotateTransform.cs @@ -0,0 +1,114 @@ +using System; +using System.Numerics; +using System.Windows.Media; +using PCL.Core.UI.Animation.Core; + +namespace PCL.Core.UI; + +public struct NRotateTransform : + IEquatable, + IAdditionOperators, + ISubtractionOperators, + IMultiplyOperators, + IDivisionOperators +{ + private Vector3 _rotate; + + public float Angle + { + get => _rotate.X; + set => _rotate.X = value; + } + + public float CenterX + { + get => _rotate.Y; + set => _rotate.Y = value; + } + + public float CenterY + { + get => _rotate.Z; + set => _rotate.Z = value; + } + + #region 构造函数 + + public NRotateTransform() + { + _rotate = new Vector3(0, 0, 0); + } + + public NRotateTransform(float angle, float centerX = 0f, float centerY = 0f) + { + _rotate = new Vector3(angle, centerX, centerY); + } + + public NRotateTransform(RotateTransform scaleTransform) + { + var uiAccessProvider = AnimationService.UIAccessProvider; + if (uiAccessProvider.CheckAccess()) + { + _rotate = GetVector(scaleTransform); + } + else + { + Vector3 localScale = default; + uiAccessProvider.Invoke(() => localScale = GetVector(scaleTransform)); + _rotate = localScale; + } + + return; + + Vector3 GetVector(RotateTransform rt) + { + return new Vector3((float)rt.Angle, (float)rt.CenterX, (float)rt.CenterY); + } + } + + #endregion + + #region 运算符重载 + + public static NRotateTransform operator +(NRotateTransform a, NRotateTransform b) => new(a.Angle + b.Angle, a.CenterX + b.CenterX, a.CenterY + b.CenterY); + public static NRotateTransform operator -(NRotateTransform a, NRotateTransform b) => new(a.Angle - b.Angle, a.CenterX - b.CenterX, a.CenterY - b.CenterY); + public static NRotateTransform operator *(NRotateTransform a, float b) => new(a.Angle * b, a.CenterX * b, a.CenterY * b); + + public static NRotateTransform operator /(NRotateTransform a, float b) => + b == 0 ? throw new DivideByZeroException("除数不能为零。") : new NRotateTransform(a.Angle / b, a.CenterX / b, a.CenterY / b); + + public static bool operator ==(NRotateTransform a, NRotateTransform b) => a._rotate == b._rotate; + public static bool operator !=(NRotateTransform a, NRotateTransform b) => a._rotate != b._rotate; + + #endregion + + #region IEquatable + + public bool Equals(NRotateTransform other) + { + return _rotate.Equals(other._rotate); + } + + public override bool Equals(object? obj) + { + if (obj is NRotateTransform color) + return Equals(color); + return false; + } + + public override int GetHashCode() + { + return HashCode.Combine(Angle, CenterX, CenterY); + } + + #endregion + + #region 隐式转换 + + public static implicit operator RotateTransform(NRotateTransform rt) => + new(rt.Angle, rt.CenterX, rt.CenterY); + + public static implicit operator NRotateTransform(RotateTransform rt) => new(rt); + + #endregion +} \ No newline at end of file diff --git a/PCL.Core/UI/NScaleTransform.cs b/PCL.Core/UI/NScaleTransform.cs new file mode 100644 index 000000000..45cb05166 --- /dev/null +++ b/PCL.Core/UI/NScaleTransform.cs @@ -0,0 +1,120 @@ +using System; +using System.Numerics; +using System.Windows.Media; +using PCL.Core.UI.Animation.Core; + +namespace PCL.Core.UI; + +public struct NScaleTransform : + IEquatable, + IAdditionOperators, + ISubtractionOperators, + IMultiplyOperators, + IDivisionOperators +{ + private Vector4 _scale; + + public float ScaleX + { + get => _scale.X; + set => _scale.X = value; + } + + public float ScaleY + { + get => _scale.Y; + set => _scale.Y = value; + } + + public float CenterX + { + get => _scale.Z; + set => _scale.Z = value; + } + + public float CenterY + { + get => _scale.W; + set => _scale.W = value; + } + + #region 构造函数 + + public NScaleTransform() + { + _scale = new Vector4(1, 1, 0, 0); + } + + public NScaleTransform(float scaleX, float scaleY, float centerX = 0f, float centerY = 0f) + { + _scale = new Vector4(scaleX, scaleY, centerX, centerY); + } + + public NScaleTransform(ScaleTransform scaleTransform) + { + var uiAccessProvider = AnimationService.UIAccessProvider; + if (uiAccessProvider.CheckAccess()) + { + _scale = GetVector(scaleTransform); + } + else + { + Vector4 localScale = default; + uiAccessProvider.Invoke(() => localScale = GetVector(scaleTransform)); + _scale = localScale; + } + + return; + + Vector4 GetVector(ScaleTransform st) + { + return new Vector4((float)st.ScaleX, (float)st.ScaleY, (float)st.CenterX, (float)st.CenterY); + } + } + + #endregion + + #region 运算符重载 + + public static NScaleTransform operator +(NScaleTransform a, NScaleTransform b) => new(a.ScaleX + b.ScaleX, a.ScaleY + b.ScaleY, a.CenterX + b.CenterX, a.CenterY + b.CenterY); + public static NScaleTransform operator -(NScaleTransform a, NScaleTransform b) => new(a.ScaleX - b.ScaleX, a.ScaleY - b.ScaleY, a.CenterX - b.CenterX, a.CenterY - b.CenterY); + public static NScaleTransform operator *(NScaleTransform a, float b) => new(a.ScaleX * b, a.ScaleY * b, a.CenterX * b, a.CenterY * b); + + public static NScaleTransform operator /(NScaleTransform a, float b) => + b == 0 ? throw new DivideByZeroException("除数不能为零。") : new NScaleTransform(a.ScaleX / b, a.ScaleY / b, a.CenterX / b, a.CenterY / b); + + public static bool operator ==(NScaleTransform a, NScaleTransform b) => a._scale == b._scale; + public static bool operator !=(NScaleTransform a, NScaleTransform b) => a._scale != b._scale; + + #endregion + + #region IEquatable + + public bool Equals(NScaleTransform other) + { + return _scale.Equals(other._scale); + } + + public override bool Equals(object? obj) + { + if (obj is NScaleTransform color) + return Equals(color); + return false; + } + + public override int GetHashCode() + { + return HashCode.Combine(ScaleX, ScaleY, CenterX, CenterY); + } + + #endregion + + #region 隐式转换 + + public static implicit operator ScaleTransform(NScaleTransform st) => + new(st.ScaleX, st.ScaleY, st.CenterX, st.CenterY); + + public static implicit operator NScaleTransform(ScaleTransform st) => new(st); + + #endregion +} \ No newline at end of file