初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
|
||||
namespace PCL.Core.UI.Animation.Animatable;
|
||||
|
||||
public sealed class ClrAnimatable<TOwner, T> : IAnimatable
|
||||
{
|
||||
private readonly TOwner _owner;
|
||||
private readonly Func<TOwner, T> _getter;
|
||||
private readonly Action<TOwner, T> _setter;
|
||||
|
||||
public ClrAnimatable(
|
||||
TOwner owner,
|
||||
Func<TOwner, T> getter,
|
||||
Action<TOwner, T> setter)
|
||||
{
|
||||
_owner = owner ?? throw new ArgumentNullException(nameof(owner));
|
||||
_getter = getter ?? throw new ArgumentNullException(nameof(getter));
|
||||
_setter = setter ?? throw new ArgumentNullException(nameof(setter));
|
||||
}
|
||||
|
||||
public T GetValue() => _getter(_owner);
|
||||
|
||||
public void SetValue(T value) => _setter(_owner, value);
|
||||
|
||||
object? IAnimatable.GetValue() => GetValue();
|
||||
void IAnimatable.SetValue(object? value) => SetValue((T)value!);
|
||||
void IAnimatable.SetValue<TValue>(TValue value) => SetValue((T)(object)value!);
|
||||
}
|
||||
@@ -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>(T value)
|
||||
{
|
||||
// 空
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace PCL.Core.UI.Animation.Animatable;
|
||||
|
||||
public interface IAnimatable
|
||||
{
|
||||
public object? GetValue();
|
||||
public void SetValue(object value);
|
||||
public void SetValue<T>(T value);
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
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;
|
||||
|
||||
public sealed class WpfAnimatable(DependencyObject owner, DependencyProperty? property) : IAnimatable
|
||||
{
|
||||
public DependencyObject Owner { get; set; } = owner;
|
||||
public DependencyProperty? Property { get; set; } = property;
|
||||
|
||||
public object? GetValue()
|
||||
{
|
||||
DependencyProperty? actualProperty;
|
||||
|
||||
if (Property == FrameworkElement.WidthProperty)
|
||||
{
|
||||
actualProperty = FrameworkElement.ActualWidthProperty;
|
||||
}
|
||||
else if (Property == FrameworkElement.HeightProperty)
|
||||
{
|
||||
actualProperty = FrameworkElement.ActualHeightProperty;
|
||||
}
|
||||
else
|
||||
{
|
||||
actualProperty = Property;
|
||||
}
|
||||
|
||||
ArgumentNullException.ThrowIfNull(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>(T value)
|
||||
{
|
||||
value = ValueProcessorManager.Filter(value);
|
||||
ArgumentNullException.ThrowIfNull(Property);
|
||||
_SetValueCore(value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void _SetValueCore<T>(T value)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(Property);
|
||||
|
||||
if (typeof(T) == typeof(NColor))
|
||||
{
|
||||
var color = Unsafe.As<T, NColor>(ref value);
|
||||
|
||||
Owner.SetValue(
|
||||
Property,
|
||||
Property.Name == "Color"
|
||||
? (Color)color
|
||||
: (SolidColorBrush)color);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof(T) == typeof(NScaleTransform))
|
||||
{
|
||||
var st = Unsafe.As<T, NScaleTransform>(ref value);
|
||||
Owner.SetValue(Property, (ScaleTransform)st);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof(T) == typeof(NRotateTransform))
|
||||
{
|
||||
var rt = Unsafe.As<T, NRotateTransform>(ref value);
|
||||
Owner.SetValue(Property, (RotateTransform)rt);
|
||||
return;
|
||||
}
|
||||
|
||||
Owner.SetValue(Property, value!);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PCL.Core.UI.Animation.Clock;
|
||||
|
||||
public interface IClock
|
||||
{
|
||||
/// <summary>
|
||||
/// 当前是否在运行。
|
||||
/// </summary>
|
||||
bool IsRunning { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 时钟频率 (FPS),如果为 <see cref="int.MaxValue"/> 则表示每次循环都触发 Tick 事件。
|
||||
/// </summary>
|
||||
int Fps { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 启动时钟。
|
||||
/// </summary>
|
||||
void Start();
|
||||
|
||||
/// <summary>
|
||||
/// 停止时钟。
|
||||
/// </summary>
|
||||
void Stop();
|
||||
|
||||
/// <summary>
|
||||
/// 每一帧触发的事件。
|
||||
/// </summary>
|
||||
event EventHandler<long>? Tick;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace PCL.Core.UI.Animation.Clock;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public interface IUIClock : IClock
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using PCL.Core.Utils;
|
||||
|
||||
namespace PCL.Core.UI.Animation.Clock;
|
||||
|
||||
/// <summary>
|
||||
/// 一个基于 Stopwatch 的时钟实现。
|
||||
/// </summary>
|
||||
/// <param name="fps">帧率。</param>
|
||||
public class StopwatchClock(int fps = 60) : IClock, IDisposable
|
||||
{
|
||||
private CancellationTokenSource? _cts;
|
||||
|
||||
private long _lastStamp;
|
||||
private long _lastFrame;
|
||||
|
||||
public event EventHandler<long>? Tick;
|
||||
|
||||
public int Fps { get; set; } = fps;
|
||||
|
||||
public bool IsRunning => _cts is not null && !_cts.IsCancellationRequested;
|
||||
|
||||
~StopwatchClock()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (IsRunning) return;
|
||||
|
||||
_cts = new CancellationTokenSource();
|
||||
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
_lastStamp = FrameUtils.NowStamp();
|
||||
|
||||
while (!_cts.IsCancellationRequested)
|
||||
{
|
||||
if (Fps == int.MaxValue)
|
||||
{
|
||||
_lastFrame++;
|
||||
Tick?.Invoke(this, _lastFrame);
|
||||
}
|
||||
else
|
||||
{
|
||||
var frame = FrameUtils.StampToFrameIndex(_lastStamp, Fps);
|
||||
if (frame == _lastFrame) continue;
|
||||
_lastFrame = frame;
|
||||
|
||||
Tick?.Invoke(this, frame);
|
||||
}
|
||||
}
|
||||
}, _cts.Token);
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
if (_cts is null) return;
|
||||
_cts.Cancel();
|
||||
_cts = null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cts?.Cancel();
|
||||
_cts?.Dispose();
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace PCL.Core.UI.Animation.Clock;
|
||||
|
||||
public sealed partial class WinMMClock(int fps = 60) : IClock, IDisposable
|
||||
{
|
||||
private uint _timerId;
|
||||
private long _frameIndex;
|
||||
private TimeProc? _callback;
|
||||
|
||||
public event EventHandler<long>? Tick;
|
||||
|
||||
public int Fps { get; set; } = fps;
|
||||
|
||||
public bool IsRunning { get; private set; }
|
||||
|
||||
~WinMMClock()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (IsRunning) return;
|
||||
IsRunning = true;
|
||||
|
||||
_frameIndex = 0;
|
||||
|
||||
// 计算帧间隔(毫秒)
|
||||
var delay = (uint)Math.Max(1, 1000.0 / Fps);
|
||||
|
||||
// 定义回调函数
|
||||
_callback = (_, _, _, _, _) =>
|
||||
{
|
||||
_frameIndex++;
|
||||
Tick?.Invoke(this, _frameIndex);
|
||||
};
|
||||
|
||||
// 设置定时器
|
||||
_timerId = _TimeSetEvent(
|
||||
delay,
|
||||
0,
|
||||
_callback,
|
||||
IntPtr.Zero,
|
||||
TimePeriodic | TimeCallbackFunction
|
||||
);
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
if (!IsRunning) return;
|
||||
IsRunning = false;
|
||||
|
||||
// 停止定时器
|
||||
if (_timerId != 0)
|
||||
{
|
||||
_TimeKillEvent(_timerId);
|
||||
_timerId = 0;
|
||||
}
|
||||
_callback = null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
[LibraryImport("winmm.dll", EntryPoint = "timeSetEvent", SetLastError = true)]
|
||||
private static partial uint _TimeSetEvent(uint uDelay, uint uResolution, TimeProc lpTimeProc, IntPtr dwUser, uint fuEvent);
|
||||
|
||||
[LibraryImport("winmm.dll", EntryPoint = "timeKillEvent", SetLastError = true)]
|
||||
private static partial void _TimeKillEvent(uint uTimerId);
|
||||
|
||||
private delegate void TimeProc(uint id, uint msg, IntPtr user, IntPtr dw1, IntPtr dw2);
|
||||
|
||||
private const uint TimePeriodic = 0x0001;
|
||||
private const uint TimeCallbackFunction = 0x0000;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Media;
|
||||
using PCL.Core.Utils;
|
||||
|
||||
namespace PCL.Core.UI.Animation.Clock;
|
||||
|
||||
/// <summary>
|
||||
/// 一个基于 WPF CompositionTarget.Rendering 事件的时钟实现。
|
||||
/// 该时钟引发的所有事件均在 UI 线程上执行。
|
||||
/// </summary>
|
||||
public class WpfCompositionTargetRenderingClock(int fps = 60) : IUIClock, IDisposable
|
||||
{
|
||||
private CancellationTokenSource? _cts;
|
||||
|
||||
private TimeSpan _lastTime = TimeSpan.Zero;
|
||||
private long _lastFrame;
|
||||
|
||||
public event EventHandler<long>? Tick;
|
||||
|
||||
public int Fps { get; set; } = fps;
|
||||
|
||||
public bool IsRunning => _cts is not null && !_cts.IsCancellationRequested;
|
||||
|
||||
~WpfCompositionTargetRenderingClock()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (IsRunning) return;
|
||||
|
||||
_cts = new CancellationTokenSource();
|
||||
|
||||
CompositionTarget.Rendering += _OnCompositionTargetOnRendering;
|
||||
}
|
||||
|
||||
private void _OnCompositionTargetOnRendering(object? _, EventArgs args)
|
||||
{
|
||||
if (args is not RenderingEventArgs renderingEventArgs) return;
|
||||
|
||||
if (_cts!.IsCancellationRequested)
|
||||
{
|
||||
CompositionTarget.Rendering -= _OnCompositionTargetOnRendering;
|
||||
return;
|
||||
}
|
||||
|
||||
if (Fps == int.MaxValue)
|
||||
{
|
||||
_lastFrame++;
|
||||
Tick?.Invoke(this, _lastFrame);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_lastTime == TimeSpan.Zero)
|
||||
{
|
||||
_lastTime = renderingEventArgs.RenderingTime;
|
||||
}
|
||||
|
||||
var frame = FrameUtils.TimeSpanToFrameIndex(_lastTime, renderingEventArgs.RenderingTime, Fps);
|
||||
if (frame == _lastFrame) return;
|
||||
_lastFrame = frame;
|
||||
|
||||
_lastTime = renderingEventArgs.RenderingTime;
|
||||
|
||||
Tick?.Invoke(this, frame);
|
||||
}
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
CompositionTarget.Rendering -= _OnCompositionTargetOnRendering;
|
||||
|
||||
if (_cts is null) return;
|
||||
_cts.Cancel();
|
||||
_cts = null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
CompositionTarget.Rendering -= _OnCompositionTargetOnRendering;
|
||||
|
||||
_cts?.Cancel();
|
||||
_cts?.Dispose();
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using PCL.Core.UI.Animation.Animatable;
|
||||
|
||||
namespace PCL.Core.UI.Animation.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 用于在动画系统中执行 Action。
|
||||
/// </summary>
|
||||
public class ActionAnimation : AnimationBase
|
||||
{
|
||||
public ActionAnimation() { }
|
||||
|
||||
public ActionAnimation(Action action) => Action = _ => action();
|
||||
|
||||
public ActionAnimation(Action<CancellationToken> action) => Action = action;
|
||||
|
||||
public Action<CancellationToken> 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<IAnimation> 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<IAnimation>(_ => 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using PCL.Core.UI.Animation.Animatable;
|
||||
|
||||
namespace PCL.Core.UI.Animation.Core;
|
||||
|
||||
public abstract class AnimationBase : DependencyObject, IAnimation
|
||||
{
|
||||
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<IAnimation> 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);
|
||||
public void RaiseCompleted() => Completed?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
public event EventHandler? Started;
|
||||
public event EventHandler? Completed;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Frozen;
|
||||
|
||||
namespace PCL.Core.UI.Animation.Core;
|
||||
|
||||
public record AnimationData<T> : IAnimationData
|
||||
{
|
||||
public IAnimation Animation { get; init; } = null!;
|
||||
public FrozenDictionary<int, T> Values { get; init; } = null!;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using PCL.Core.UI.Animation.Easings;
|
||||
|
||||
namespace PCL.Core.UI.Animation.Core;
|
||||
|
||||
public static class AnimationExtensions
|
||||
{
|
||||
#region 附加属性
|
||||
|
||||
public static readonly DependencyProperty TargetProperty = DependencyProperty.RegisterAttached(
|
||||
"Target", typeof(DependencyObject), typeof(AnimationExtensions), new PropertyMetadata(default(DependencyObject)));
|
||||
|
||||
public static void SetTarget(DependencyObject element, DependencyObject value)
|
||||
{
|
||||
if (element is not IAnimation)
|
||||
throw new InvalidOperationException("AnimationExtensions.Target 只能附加到 IAnimation 实例上。");
|
||||
|
||||
element.SetValue(TargetProperty, value);
|
||||
}
|
||||
|
||||
public static DependencyObject GetTarget(DependencyObject element)
|
||||
{
|
||||
return (DependencyObject)element.GetValue(TargetProperty);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty TargetPropertyProperty = DependencyProperty.RegisterAttached(
|
||||
"TargetProperty", typeof(DependencyProperty), typeof(AnimationExtensions), new PropertyMetadata(default(DependencyProperty)));
|
||||
|
||||
public static void SetTargetProperty(DependencyObject element, DependencyProperty value)
|
||||
{
|
||||
if (element is not IAnimation)
|
||||
throw new InvalidOperationException("AnimationExtensions.TargetProperty 只能附加到 IAnimation 实例上。");
|
||||
|
||||
element.SetValue(TargetPropertyProperty, value);
|
||||
}
|
||||
|
||||
public static DependencyProperty GetTargetProperty(DependencyObject element)
|
||||
{
|
||||
return (DependencyProperty)element.GetValue(TargetPropertyProperty);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public static void Animate(this DependencyObject target, TimeSpan? duration = null, TimeSpan? delay = null,
|
||||
IEasing? easing = null, AnimationValueType valueType = AnimationValueType.Relative, int iterationCount = 1,
|
||||
double? width = null,
|
||||
double? height = null,
|
||||
double? opacity = null,
|
||||
double? radius = null,
|
||||
TranslateTransform? translate = null,
|
||||
double? translateX = null,
|
||||
double? translateY = null,
|
||||
RotateTransform? rotate = null,
|
||||
double? rotateAngle = null,
|
||||
ScaleTransform? scale = null,
|
||||
double? scaleX = null,
|
||||
double? scaleY = null,
|
||||
SkewTransform? skew = null,
|
||||
double? skewX = null,
|
||||
double? skewY = null,
|
||||
Thickness? margin = null,
|
||||
double? marginLeft = null,
|
||||
double? marginTop = null,
|
||||
double? marginRight = null,
|
||||
double? marginBottom = null,
|
||||
Thickness? padding = null,
|
||||
double? paddingLeft = null,
|
||||
double? paddingTop = null,
|
||||
double? paddingRight = null,
|
||||
double? paddingBottom = null,
|
||||
NColor? background = null,
|
||||
NColor? foreground = null)
|
||||
{
|
||||
// TODO: 实现快速调用动画逻辑
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using PCL.Core.UI.Animation.Animatable;
|
||||
using PCL.Core.UI.Animation.ValueProcessor;
|
||||
|
||||
namespace PCL.Core.UI.Animation.Core;
|
||||
|
||||
// public readonly struct AnimationFrame<T>(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()!;
|
||||
// }
|
||||
@@ -0,0 +1,134 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Markup;
|
||||
using PCL.Core.UI.Animation.Animatable;
|
||||
using PCL.Core.Utils;
|
||||
|
||||
namespace PCL.Core.UI.Animation.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 动画组的基类。
|
||||
/// </summary>
|
||||
[ContentProperty(nameof(Children))]
|
||||
public abstract class AnimationGroup : AnimationBase
|
||||
{
|
||||
public static readonly DependencyProperty ChildrenProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(Children),
|
||||
typeof(ObservableCollection<IAnimation>),
|
||||
typeof(AnimationGroup),
|
||||
new PropertyMetadata(null)); // 移除 OnChildrenChanged 回调,避免运行时冲突
|
||||
|
||||
public ObservableCollection<IAnimation> Children
|
||||
{
|
||||
get => (ObservableCollection<IAnimation>)GetValue(ChildrenProperty);
|
||||
set => SetValue(ChildrenProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 存储当前正在运行的动画实例。
|
||||
/// </summary>
|
||||
protected List<IAnimation> ChildrenCore { get; } = [];
|
||||
|
||||
protected AnimationGroup()
|
||||
{
|
||||
// 确保集合初始化,但不进行自动同步
|
||||
SetCurrentValue(ChildrenProperty, new ObservableCollection<IAnimation>());
|
||||
}
|
||||
|
||||
public override int CurrentFrame { get; set; }
|
||||
|
||||
public override void Cancel()
|
||||
{
|
||||
Status = AnimationStatus.Canceled;
|
||||
|
||||
CurrentFrame = 0;
|
||||
|
||||
lock (ChildrenCore)
|
||||
{
|
||||
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)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
protected static IAnimatable ResolveTarget(IAnimation animation, IAnimatable defaultTarget)
|
||||
{
|
||||
if (animation is not DependencyObject aniDependencyObject)
|
||||
return defaultTarget;
|
||||
|
||||
DependencyObject? targetObject = null;
|
||||
DependencyProperty? targetProperty = null;
|
||||
|
||||
// Target check
|
||||
if (WpfUtils.IsDependencyPropertySet(aniDependencyObject, AnimationExtensions.TargetProperty))
|
||||
{
|
||||
targetObject = (DependencyObject)aniDependencyObject.GetValue(AnimationExtensions.TargetProperty);
|
||||
}
|
||||
else if (defaultTarget is WpfAnimatable animatable)
|
||||
{
|
||||
targetObject = animatable.Owner;
|
||||
}
|
||||
|
||||
// TargetProperty check
|
||||
if (WpfUtils.IsDependencyPropertySet(aniDependencyObject, AnimationExtensions.TargetPropertyProperty))
|
||||
{
|
||||
targetProperty = (DependencyProperty)aniDependencyObject.GetValue(AnimationExtensions.TargetPropertyProperty);
|
||||
}
|
||||
else if (defaultTarget is WpfAnimatable animatable)
|
||||
{
|
||||
targetProperty = animatable.Property;
|
||||
}
|
||||
|
||||
// 如果都未解析出特定值,直接返回默认目标
|
||||
if (targetObject is null || targetProperty is 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using PCL.Core.App.IoC;
|
||||
using PCL.Core.UI.Animation.Animatable;
|
||||
using PCL.Core.UI.Animation.Clock;
|
||||
using PCL.Core.UI.Animation.UIAccessProvider;
|
||||
using PCL.Core.UI.Animation.ValueProcessor;
|
||||
using PCL.Core.Utils.Threading;
|
||||
|
||||
namespace PCL.Core.UI.Animation.Core;
|
||||
|
||||
[LifecycleService(LifecycleState.WindowCreating)]
|
||||
public sealed class AnimationService : GeneralService
|
||||
{
|
||||
#region Lifecycle
|
||||
|
||||
private static LifecycleContext? _context;
|
||||
private static LifecycleContext Context => _context!;
|
||||
|
||||
private AnimationService() : base("animation", "动画")
|
||||
{
|
||||
_context = ServiceContext;
|
||||
}
|
||||
|
||||
public override void Start()
|
||||
{
|
||||
_Initialize();
|
||||
}
|
||||
|
||||
public override void Stop()
|
||||
{
|
||||
_Uninitialize();
|
||||
}
|
||||
|
||||
#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 Animation, IAnimatable Target)> _animationChannel = null!;
|
||||
// private static Channel<IAnimationFrame> _frameChannel = null!;
|
||||
private static Channel<(IAnimationFrame Frame, IAnimation Source)> _frameChannel = null!;
|
||||
// private static ConcurrentDictionary<IAnimatable, IAnimationFrame> _frameDictionary = null!;
|
||||
private static ConcurrentDictionary<string, IAnimation> _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; } = 0.1d;
|
||||
|
||||
public static IUIAccessProvider UIAccessProvider { get; private set; } = null!;
|
||||
|
||||
private static void _Initialize()
|
||||
{
|
||||
// 初始化 Channel 与 Dictionary
|
||||
_animationChannel = Channel.CreateUnbounded<(IAnimation, IAnimatable)>();
|
||||
// _frameChannel = Channel.CreateUnbounded<IAnimationFrame>();
|
||||
_frameChannel = Channel.CreateUnbounded<(IAnimationFrame, IAnimation)>();
|
||||
|
||||
// 根据核心数量来确定动画计算 Task 数量
|
||||
_taskCount = Environment.ProcessorCount;
|
||||
Context.Info($"以最多 {_taskCount} 个线程初始化动画计算 Task");
|
||||
|
||||
// 初始化 CancellationTokenSource 与 ResetEvent
|
||||
_cts = new CancellationTokenSource();
|
||||
_resetEvent = new AsyncCountResetEvent();
|
||||
|
||||
// 注册 ValueProcessor
|
||||
_RegisterValueProcessors();
|
||||
|
||||
// 初始化 UI 线程访问提供器并启动赋值 Task
|
||||
UIAccessProvider = new WpfUIAccessProvider(Lifecycle.CurrentApplication.Dispatcher);
|
||||
_ = UIAccessProvider.InvokeAsync(async () =>
|
||||
{
|
||||
if (_cts.IsCancellationRequested) return;
|
||||
while (await _frameChannel.Reader.WaitToReadAsync())
|
||||
{
|
||||
// 读取数据
|
||||
while (_frameChannel.Reader.TryRead(out var item))
|
||||
{
|
||||
// 如果动画源已被标记取消,直接丢弃该帧,不进行处理
|
||||
if (item.Source.Status == AnimationStatus.Canceled)
|
||||
continue;
|
||||
|
||||
// 正常处理
|
||||
item.Frame.GetAction()();
|
||||
}
|
||||
|
||||
await Task.Yield();
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化 Clock 并注册 Tick 事件
|
||||
_clock = new WinMMClock(Fps);
|
||||
_clock.Tick += ClockOnTick;
|
||||
_clock.Start();
|
||||
|
||||
// 运行动画计算 Task
|
||||
for (var i = 0; i < _taskCount; i++)
|
||||
{
|
||||
_ = Task.Run(_AnimationComputeTaskAsync);
|
||||
}
|
||||
}
|
||||
|
||||
private static void _Uninitialize()
|
||||
{
|
||||
// 取消动画计算 Task
|
||||
_cts.Cancel();
|
||||
_cts.Dispose();
|
||||
|
||||
// 停止 Clock 并注销 Tick 事件
|
||||
_clock.Tick -= ClockOnTick;
|
||||
_clock.Stop();
|
||||
|
||||
// 将 ResetEvent 释放
|
||||
_resetEvent.Dispose();
|
||||
|
||||
// 清理 Dictionary
|
||||
_namedAnimations.Clear();
|
||||
}
|
||||
|
||||
private static void ClockOnTick(object? sender, long e)
|
||||
{
|
||||
// 通知所有等待的动画计算 Task 进行下一帧计算
|
||||
_resetEvent.Set(_taskCount);
|
||||
}
|
||||
|
||||
private static async Task _AnimationComputeTaskAsync()
|
||||
{
|
||||
// 本地动画列表,确保没有一直无法计算的动画
|
||||
var animationList = new List<(IAnimation Animation, IAnimatable Target)>(8);
|
||||
|
||||
// 持续监听 Channel 中的动画
|
||||
while (!_cts.IsCancellationRequested)
|
||||
{
|
||||
// 读取所有可用的动画到本地列表
|
||||
while (_animationChannel.Reader.TryRead(out var animation))
|
||||
{
|
||||
// 将动画添加到本地列表
|
||||
animationList.Add(animation);
|
||||
}
|
||||
|
||||
// 如果没有动画,直接等下一帧
|
||||
if (animationList.Count == 0)
|
||||
{
|
||||
await _resetEvent.WaitAsync();
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var i = animationList.Count - 1; i >= 0; i--)
|
||||
{
|
||||
// TODO: 支持缓存动画计算结果 (由 AnimationData 支持)
|
||||
|
||||
// 从列表中获取动画
|
||||
var animationEntry = animationList[i];
|
||||
|
||||
// 如果动画已经完成或被取消,则从列表中移除
|
||||
if (animationEntry.Animation.Status is AnimationStatus.Canceled or AnimationStatus.Completed)
|
||||
{
|
||||
animationEntry.Animation.RaiseCompleted();
|
||||
|
||||
if (!string.IsNullOrEmpty(animationEntry.Animation.Name))
|
||||
{
|
||||
// 使用显式接口
|
||||
((ICollection<KeyValuePair<string, IAnimation>>)_namedAnimations)
|
||||
.Remove(new KeyValuePair<string, IAnimation>(animationEntry.Animation.Name, animationEntry.Animation));
|
||||
}
|
||||
|
||||
animationList.RemoveAt(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 计算动画的下一帧
|
||||
var frame = animationEntry.Animation.ComputeNextFrame(animationEntry.Target);
|
||||
// 如果没有计算帧(当动画为 SequentialAnimationGroup 或 ParallelAnimationGroup 这种动画集合时),跳过
|
||||
if (frame is null) continue;
|
||||
// 将动画帧写入 Channel
|
||||
_frameChannel.Writer.TryWrite((frame, animationEntry.Animation));
|
||||
// 增加当前帧计数
|
||||
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();
|
||||
|
||||
_animationChannel.Writer.TryWrite((animation, target));
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace PCL.Core.UI.Animation.Core;
|
||||
|
||||
public enum AnimationStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// 动画未开始。
|
||||
/// </summary>
|
||||
NotStarted,
|
||||
/// <summary>
|
||||
/// 动画正在运行。
|
||||
/// </summary>
|
||||
Running,
|
||||
/// <summary>
|
||||
/// 动画已完成。
|
||||
/// </summary>
|
||||
Completed,
|
||||
/// <summary>
|
||||
/// 动画已取消。
|
||||
/// </summary>
|
||||
Canceled,
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace PCL.Core.UI.Animation.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 用于指定动画值的解释方式。。
|
||||
/// </summary>
|
||||
public enum AnimationValueType
|
||||
{
|
||||
/// <summary>
|
||||
/// 绝对值。
|
||||
/// </summary>
|
||||
Absolute,
|
||||
/// <summary>
|
||||
/// 相对值。
|
||||
/// </summary>
|
||||
Relative
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using PCL.Core.UI.Animation.Animatable;
|
||||
using PCL.Core.UI.Animation.Easings;
|
||||
using PCL.Core.UI.Animation.ValueProcessor;
|
||||
|
||||
namespace PCL.Core.UI.Animation.Core;
|
||||
|
||||
public class FromToAnimationBase<T> : AnimationBase, IFromToAnimation
|
||||
{
|
||||
public IEasing Easing { get; set; } = new LinearEasing();
|
||||
|
||||
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; }
|
||||
|
||||
object? IFromToAnimation.CurrentValue
|
||||
{
|
||||
get => CurrentValue;
|
||||
set
|
||||
{
|
||||
if (value is not T typed) throw new InvalidCastException($"无法将 {value!.GetType()} 转换为 {typeof(T)}");
|
||||
CurrentValue = typed;
|
||||
}
|
||||
}
|
||||
|
||||
public int TotalFrames { get; private set; }
|
||||
|
||||
private int _currentFrame;
|
||||
|
||||
public override int CurrentFrame
|
||||
{
|
||||
get => Interlocked.CompareExchange(ref _currentFrame, 0, 0);
|
||||
set => Interlocked.Exchange(ref _currentFrame, value);
|
||||
}
|
||||
|
||||
private T? _startValue;
|
||||
|
||||
public override async Task<IAnimation> RunAsync(IAnimatable target)
|
||||
{
|
||||
_RunCore(target);
|
||||
var clone = (FromToAnimationBase<T>)MemberwiseClone();
|
||||
|
||||
// 延迟
|
||||
await Task.Delay(Delay);
|
||||
|
||||
// 将该动画推送到动画服务
|
||||
await AnimationService.PushAnimationAsync(clone, target);
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
public override IAnimation RunFireAndForget(IAnimatable target)
|
||||
{
|
||||
_RunCore(target);
|
||||
var clone = (FromToAnimationBase<T>)MemberwiseClone();
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
// 延迟
|
||||
await Task.Delay(Delay);
|
||||
|
||||
// 将该动画推送到动画服务
|
||||
AnimationService.PushAnimationFireAndForget(clone, target);
|
||||
});
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
private void _RunCore(IAnimatable target)
|
||||
{
|
||||
// 重置当前帧
|
||||
_currentFrame = 0;
|
||||
|
||||
// 空值检查
|
||||
ArgumentNullException.ThrowIfNull(To);
|
||||
|
||||
// 记录初始值
|
||||
_startValue = (T)target.GetValue()!;
|
||||
|
||||
// 如果 From 为空,则根据动画值类型设置初始值
|
||||
if (!ValueProcessorManager.Equal(_startValue, From))
|
||||
{
|
||||
From = ValueType == AnimationValueType.Relative ? ValueProcessorManager.DefaultValue<T>() : _startValue;
|
||||
}
|
||||
|
||||
// 计算总帧数
|
||||
TotalFrames = (int)Math.Round(Duration.TotalSeconds * AnimationService.Fps / AnimationService.Scale);
|
||||
|
||||
// 进行初始赋值
|
||||
// target.SetValue(
|
||||
// ValueType == AnimationValueType.Relative ? ValueProcessorManager.Add(From, _startValue)! : From!);
|
||||
|
||||
// 设置状态
|
||||
Status = AnimationStatus.Running;
|
||||
}
|
||||
|
||||
public override void Cancel()
|
||||
{
|
||||
// 确保正常结束
|
||||
Interlocked.Exchange(ref _currentFrame, TotalFrames);
|
||||
|
||||
Status = AnimationStatus.Canceled;
|
||||
}
|
||||
|
||||
public override IAnimationFrame? ComputeNextFrame(IAnimatable target)
|
||||
{
|
||||
if (_currentFrame >= TotalFrames)
|
||||
{
|
||||
Status = AnimationStatus.Completed;
|
||||
return null;
|
||||
}
|
||||
|
||||
return new FromToAnimationFrame<T>
|
||||
{
|
||||
Target = target,
|
||||
Value = ValueType == AnimationValueType.Relative
|
||||
? CurrentValue!
|
||||
: ValueProcessorManager.Subtract(CurrentValue!, From!),
|
||||
StartValue = ValueType == AnimationValueType.Relative ? _startValue! : From!
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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<T>(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!);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using PCL.Core.UI.Animation.Animatable;
|
||||
|
||||
namespace PCL.Core.UI.Animation.Core;
|
||||
|
||||
public interface IAnimation
|
||||
{
|
||||
/// <summary>
|
||||
/// 动画名。
|
||||
/// </summary>
|
||||
string Name { get; set; }
|
||||
/// <summary>
|
||||
/// 当前动画状态。
|
||||
/// </summary>
|
||||
AnimationStatus Status { get; }
|
||||
/// <summary>
|
||||
/// 当前动画帧索引。
|
||||
/// </summary>
|
||||
int CurrentFrame { get; set; }
|
||||
/// <summary>
|
||||
/// 异步方式运行动画。
|
||||
/// </summary>
|
||||
/// <param name="target">被动画的对象。</param>
|
||||
/// <returns>返回表示异步动画操作的任务。</returns>
|
||||
Task<IAnimation> RunAsync(IAnimatable target);
|
||||
/// <summary>
|
||||
/// 一发即忘方式运行动画。
|
||||
/// </summary>
|
||||
/// <param name="target">被动画的对象。</param>
|
||||
IAnimation RunFireAndForget(IAnimatable target);
|
||||
/// <summary>
|
||||
/// 取消动画。
|
||||
/// </summary>
|
||||
void Cancel();
|
||||
/// <summary>
|
||||
/// 计算下一帧。
|
||||
/// </summary>
|
||||
/// <param name="target">被动画的对象。</param>
|
||||
/// <returns>动画帧。</returns>
|
||||
IAnimationFrame? ComputeNextFrame(IAnimatable target);
|
||||
/// <summary>
|
||||
/// 触发动画开始事件。
|
||||
/// </summary>
|
||||
void RaiseStarted();
|
||||
/// <summary>
|
||||
/// 触发动画完成事件。
|
||||
/// </summary>
|
||||
void RaiseCompleted();
|
||||
/// <summary>
|
||||
/// 动画开始时触发的事件。
|
||||
/// </summary>
|
||||
event EventHandler Started;
|
||||
/// <summary>
|
||||
/// 动画完成时触发的事件。
|
||||
/// </summary>
|
||||
event EventHandler Completed;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace PCL.Core.UI.Animation.Core;
|
||||
|
||||
public interface IAnimationData
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using PCL.Core.UI.Animation.Animatable;
|
||||
|
||||
namespace PCL.Core.UI.Animation.Core;
|
||||
|
||||
public interface IAnimationFrame
|
||||
{
|
||||
Action GetAction();
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace PCL.Core.UI.Animation.Core;
|
||||
|
||||
public interface IFromToAnimation : IAnimation
|
||||
{
|
||||
object? CurrentValue { get; internal set; }
|
||||
int TotalFrames { get; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace PCL.Core.UI.Animation.Core;
|
||||
|
||||
public interface IImplicitAnimation
|
||||
{
|
||||
// TODO: 实现隐式动画
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace PCL.Core.UI.Animation.Core;
|
||||
|
||||
public interface IKeyFrameAnimation
|
||||
{
|
||||
// TODO: 实现关键帧动画
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using PCL.Core.UI.Animation.Animatable;
|
||||
|
||||
namespace PCL.Core.UI.Animation.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 同时执行的动画集合。
|
||||
/// </summary>
|
||||
public sealed class ParallelAnimationGroup : AnimationGroup
|
||||
{
|
||||
private TaskCompletionSource? _cancelTcs;
|
||||
|
||||
public override async Task<IAnimation> RunAsync(IAnimatable target)
|
||||
{
|
||||
Status = AnimationStatus.Running;
|
||||
AnimationService.PushAnimationFireAndForget(this, target);
|
||||
|
||||
_cancelTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
var childrenSnapshot = Children.ToList();
|
||||
var childWaitTasks = new List<Task>();
|
||||
|
||||
lock (ChildrenCore)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Markup;
|
||||
using Microsoft.Xaml.Behaviors;
|
||||
using PCL.Core.UI.Animation.Animatable;
|
||||
using PCL.Core.Utils;
|
||||
|
||||
namespace PCL.Core.UI.Animation.Core;
|
||||
|
||||
[ContentProperty(nameof(Animation))]
|
||||
public class RunAnimationAction : TriggerAction<DependencyObject>
|
||||
{
|
||||
public static readonly DependencyProperty AnimationProperty = DependencyProperty.Register(
|
||||
nameof(Animation),
|
||||
typeof(IAnimation),
|
||||
typeof(RunAnimationAction),
|
||||
new PropertyMetadata(default(IAnimation)));
|
||||
|
||||
public IAnimation Animation
|
||||
{
|
||||
get => (IAnimation)GetValue(AnimationProperty);
|
||||
set => SetValue(AnimationProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty TargetPropertyProperty = DependencyProperty.Register(
|
||||
nameof(TargetProperty),
|
||||
typeof(DependencyProperty),
|
||||
typeof(RunAnimationAction),
|
||||
new PropertyMetadata(default(DependencyProperty)));
|
||||
|
||||
public DependencyProperty TargetProperty
|
||||
{
|
||||
get => (DependencyProperty)GetValue(TargetPropertyProperty);
|
||||
set => SetValue(TargetPropertyProperty, value);
|
||||
}
|
||||
|
||||
protected override void Invoke(object parameter)
|
||||
{
|
||||
DependencyObject? targetObject;
|
||||
DependencyProperty? targetProperty;
|
||||
|
||||
var aniDependencyObject = (DependencyObject)Animation;
|
||||
|
||||
// 判断对象
|
||||
if (WpfUtils.IsDependencyPropertySet(aniDependencyObject, AnimationExtensions.TargetProperty))
|
||||
{
|
||||
targetObject = (DependencyObject)aniDependencyObject.GetValue(AnimationExtensions.TargetProperty);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (AssociatedObject is not null)
|
||||
{
|
||||
targetObject = AssociatedObject;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 按理来说不可能出现这种情况,但是还是抛个异常吧
|
||||
throw new InvalidOperationException("未指定动画的目标对象。");
|
||||
}
|
||||
}
|
||||
|
||||
// 判断属性
|
||||
if (WpfUtils.IsDependencyPropertySet(aniDependencyObject, AnimationExtensions.TargetPropertyProperty))
|
||||
{
|
||||
targetProperty =
|
||||
(DependencyProperty)aniDependencyObject.GetValue(AnimationExtensions.TargetPropertyProperty);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (WpfUtils.IsDependencyPropertySet(this, TargetPropertyProperty))
|
||||
{
|
||||
targetProperty = TargetProperty;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Animation is not AnimationGroup)
|
||||
{
|
||||
// 这里就有可能出现这种情况
|
||||
throw new InvalidOperationException("未指定动画的目标属性。");
|
||||
}
|
||||
|
||||
// AnimationGroup 可以没有目标属性
|
||||
targetProperty = null;
|
||||
}
|
||||
}
|
||||
|
||||
Animation.RunFireAndForget(new WpfAnimatable(targetObject, targetProperty));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using PCL.Core.UI.Animation.Animatable;
|
||||
|
||||
namespace PCL.Core.UI.Animation.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 按顺序执行的动画集合。
|
||||
/// </summary>
|
||||
public sealed class SequentialAnimationGroup : AnimationGroup
|
||||
{
|
||||
private TaskCompletionSource? _cancelTcs;
|
||||
|
||||
public override async Task<IAnimation> RunAsync(IAnimatable target)
|
||||
{
|
||||
Status = AnimationStatus.Running;
|
||||
AnimationService.PushAnimationFireAndForget(this, target);
|
||||
|
||||
_cancelTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
var childrenSnapshot = Children.ToList();
|
||||
|
||||
lock (ChildrenCore)
|
||||
{
|
||||
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 IAnimation RunFireAndForget(IAnimatable target)
|
||||
{
|
||||
_ = RunAsync(target);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using PCL.Core.UI.Animation.Animatable;
|
||||
using PCL.Core.UI.Animation.Core;
|
||||
|
||||
namespace PCL.Core.UI.Animation;
|
||||
|
||||
public sealed class DoubleFromToAnimation : FromToAnimationBase<double>
|
||||
{
|
||||
public override IAnimationFrame? ComputeNextFrame(IAnimatable target)
|
||||
{
|
||||
// 应用缓动函数
|
||||
var easedProgress = Easing.Ease(CurrentFrame, TotalFrames);
|
||||
|
||||
// 计算当前值
|
||||
CurrentValue = ValueType == AnimationValueType.Relative
|
||||
? From + To * easedProgress
|
||||
: From + (To - From) * easedProgress;
|
||||
|
||||
return base.ComputeNextFrame(target);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
|
||||
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)
|
||||
{
|
||||
var f = 2 * progress;
|
||||
return 0.5 * f * (f * f - Math.Sin(f * Math.PI));
|
||||
}
|
||||
else
|
||||
{
|
||||
var f = 1 - (2 * progress - 1);
|
||||
return 0.5 * (1 - f * (f * f - Math.Sin(f * Math.PI))) + 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
|
||||
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;
|
||||
return 1 - p * (p * p - Math.Sin(p * Math.PI));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using PCL.Core.Utils;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using PCL.Core.Utils;
|
||||
|
||||
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)
|
||||
{
|
||||
return 0.5 * (1 - EaseUtils.Bounce(1 - progress * 2));
|
||||
}
|
||||
|
||||
return 0.5 * EaseUtils.Bounce(progress * 2 - 1) + 0.5;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using PCL.Core.Utils;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
|
||||
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)
|
||||
{
|
||||
return 0.5 * (1 - Math.Sqrt(1 - 4 * progress * progress));
|
||||
}
|
||||
|
||||
var t = 2 * progress;
|
||||
return 0.5 * (Math.Sqrt((3 - t) * (t - 1)) + 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace PCL.Core.UI.Animation.Easings;
|
||||
|
||||
/// <summary>
|
||||
/// 复合缓动,支持多个缓动混合,每个缓动可独立设置时长,延迟和权重。
|
||||
/// </summary>
|
||||
public class CompositeEasing : Easing
|
||||
{
|
||||
private readonly List<(IEasing easing, TimeSpan duration, TimeSpan delay, double weight)> _easings;
|
||||
private readonly TimeSpan _totalDuration;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化复合缓动。
|
||||
/// </summary>
|
||||
/// <param name="easings">参数元组:(缓动逻辑, 持续时长)</param>
|
||||
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())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化复合缓动。
|
||||
/// </summary>
|
||||
/// <param name="easings">参数元组:(缓动逻辑, 持续时长, 延迟时间)</param>
|
||||
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())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化复合缓动。
|
||||
/// </summary>
|
||||
/// <param name="easings">参数元组:(缓动逻辑, 持续时长, 权重)</param>
|
||||
public CompositeEasing(params (IEasing easing, TimeSpan duration, double weight)[] easings)
|
||||
: this(easings.Select(e => (e.easing, e.duration, TimeSpan.Zero, e.weight)).ToArray())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化复合缓动。
|
||||
/// </summary>
|
||||
/// <param name="easings">参数元组:(缓动逻辑, 持续时长, 延迟时间, 权重)</param>
|
||||
public CompositeEasing(params (IEasing easing, TimeSpan duration, TimeSpan delay, double weight)[] easings)
|
||||
{
|
||||
if (easings is 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace PCL.Core.UI.Animation.Easings;
|
||||
|
||||
public class CubicEaseIn : Easing
|
||||
{
|
||||
public static CubicEaseIn Shared { get; } = new();
|
||||
|
||||
protected override double EaseCore(double progress)
|
||||
{
|
||||
return progress * progress * progress;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace PCL.Core.UI.Animation.Easings;
|
||||
|
||||
public class CubicEaseInOut : Easing
|
||||
{
|
||||
public static CubicEaseInOut Shared { get; } = new();
|
||||
|
||||
protected override double EaseCore(double progress)
|
||||
{
|
||||
if (progress < 0.5)
|
||||
{
|
||||
return 4 * progress * progress * progress;
|
||||
}
|
||||
|
||||
var f = 2 * (progress - 1);
|
||||
return 0.5 * f * f * f + 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace PCL.Core.UI.Animation.Easings;
|
||||
|
||||
public class CubicEaseOut : Easing
|
||||
{
|
||||
public static CubicEaseOut Shared { get; } = new();
|
||||
|
||||
protected override double EaseCore(double progress)
|
||||
{
|
||||
var f = progress - 1;
|
||||
return f * f * f + 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace PCL.Core.UI.Animation.Easings;
|
||||
|
||||
public enum EasePower
|
||||
{
|
||||
Weak = 2,
|
||||
Middle = 3,
|
||||
Strong = 4,
|
||||
ExtraStrong = 5
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace PCL.Core.UI.Animation.Easings;
|
||||
|
||||
/// <summary>
|
||||
/// 所有缓动类的基类。
|
||||
/// </summary>
|
||||
public abstract class Easing : IEasing
|
||||
{
|
||||
protected abstract double EaseCore(double progress);
|
||||
|
||||
public double Ease(double progress)
|
||||
{
|
||||
return progress switch
|
||||
{
|
||||
<= 0.0 => 0.0,
|
||||
>= 1.0 => 1.0,
|
||||
_ => EaseCore(progress)
|
||||
};
|
||||
}
|
||||
|
||||
public double Ease(int currentFrame, int totalFrames)
|
||||
{
|
||||
return totalFrames <= 1 ? 1.0 : Ease((double)currentFrame / (totalFrames - 1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using PCL.Core.Utils;
|
||||
|
||||
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) *
|
||||
Math.Exp(EaseUtils.ElasticLn2Times10 * (progress - 1d));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using PCL.Core.Utils;
|
||||
|
||||
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)
|
||||
{
|
||||
var t = progress * 2d;
|
||||
return 0.5d * Math.Sin(EaseUtils.ElasticPiTimes6Point5 * t) *
|
||||
Math.Exp(EaseUtils.ElasticLn2Times10 * (t - 1d));
|
||||
}
|
||||
else
|
||||
{
|
||||
var t = progress * 2d - 1d;
|
||||
return 0.5d * (Math.Sin(-EaseUtils.ElasticPiTimes6Point5 * (t + 1d)) *
|
||||
Math.Exp(-EaseUtils.ElasticLn2Times10 * t) + 2d);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using PCL.Core.Utils;
|
||||
|
||||
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)) *
|
||||
Math.Exp(-EaseUtils.ElasticLn2Times10 * progress) + 1d;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
|
||||
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)
|
||||
{
|
||||
return 0.5 * Math.Pow(2, 20 * progress - 10);
|
||||
}
|
||||
|
||||
return -0.5 * Math.Pow(2, -20 * progress + 10) + 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.ComponentModel;
|
||||
using PCL.Core.UI.Converters;
|
||||
|
||||
namespace PCL.Core.UI.Animation.Easings;
|
||||
|
||||
/// <summary>
|
||||
/// 定义了缓动类的接口。
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(EasingConverter))]
|
||||
public interface IEasing
|
||||
{
|
||||
/// <summary>
|
||||
/// 返回指定进度的过渡值。
|
||||
/// </summary>
|
||||
/// <param name="progress">从 0.0 到 1.0 的进度值。</param>
|
||||
/// <returns>过渡值。</returns>
|
||||
double Ease(double progress);
|
||||
|
||||
/// <summary>
|
||||
/// 返回指定动画帧的过渡值。
|
||||
/// </summary>
|
||||
/// <param name="currentFrame">当前动画帧。</param>
|
||||
/// <param name="totalFrames">总动画帧数量。</param>
|
||||
/// <returns>过渡值。</returns>
|
||||
double Ease(int currentFrame, int totalFrames);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace PCL.Core.UI.Animation.Easings;
|
||||
|
||||
public class LinearEasing : Easing
|
||||
{
|
||||
public static LinearEasing Shared { get; } = new();
|
||||
|
||||
protected override double EaseCore(double progress)
|
||||
{
|
||||
return progress;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace PCL.Core.UI.Animation.Easings;
|
||||
|
||||
public class QuadEaseIn : Easing
|
||||
{
|
||||
public static QuadEaseIn Shared { get; } = new();
|
||||
|
||||
protected override double EaseCore(double progress)
|
||||
{
|
||||
return progress * progress;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace PCL.Core.UI.Animation.Easings;
|
||||
|
||||
public class QuadEaseInOut : Easing
|
||||
{
|
||||
public static QuadEaseInOut Shared { get; } = new();
|
||||
|
||||
protected override double EaseCore(double progress)
|
||||
{
|
||||
if (progress < 0.5)
|
||||
{
|
||||
return 2 * progress * progress;
|
||||
}
|
||||
|
||||
return progress * (4 - 2 * progress) - 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace PCL.Core.UI.Animation.Easings;
|
||||
|
||||
public class QuadEaseOut : Easing
|
||||
{
|
||||
public static QuadEaseOut Shared { get; } = new();
|
||||
|
||||
protected override double EaseCore(double progress)
|
||||
{
|
||||
return 1 - (1 - progress) * (1 - progress);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace PCL.Core.UI.Animation.Easings;
|
||||
|
||||
public class QuarticEaseIn : Easing
|
||||
{
|
||||
public static QuarticEaseIn Shared { get; } = new();
|
||||
|
||||
protected override double EaseCore(double progress)
|
||||
{
|
||||
var p2 = progress * progress;
|
||||
return p2 * p2;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace PCL.Core.UI.Animation.Easings;
|
||||
|
||||
public class QuarticEaseInOut : Easing
|
||||
{
|
||||
public static QuarticEaseInOut Shared { get; } = new();
|
||||
|
||||
protected override double EaseCore(double progress)
|
||||
{
|
||||
if (progress < 0.5d)
|
||||
{
|
||||
var p2 = progress * progress;
|
||||
return 8 * p2 * p2;
|
||||
}
|
||||
|
||||
var f = progress - 1;
|
||||
var f2 = f * f;
|
||||
return -8 * f2 * f2 + 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace PCL.Core.UI.Animation.Easings;
|
||||
|
||||
public class QuarticEaseOut : Easing
|
||||
{
|
||||
public static QuarticEaseOut Shared { get; } = new();
|
||||
|
||||
protected override double EaseCore(double progress)
|
||||
{
|
||||
var f = progress - 1;
|
||||
var f2 = f * f;
|
||||
return -f2 * f2 + 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace PCL.Core.UI.Animation.Easings;
|
||||
|
||||
public class QuinticEaseIn : Easing
|
||||
{
|
||||
public static QuinticEaseIn Shared { get; } = new();
|
||||
|
||||
protected override double EaseCore(double progress)
|
||||
{
|
||||
var p2 = progress * progress;
|
||||
return p2 * p2 * progress;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace PCL.Core.UI.Animation.Easings;
|
||||
|
||||
public class QuinticEaseInOut : Easing
|
||||
{
|
||||
public static QuinticEaseInOut Shared { get; } = new();
|
||||
|
||||
protected override double EaseCore(double progress)
|
||||
{
|
||||
if (progress < 0.5)
|
||||
{
|
||||
var p2 = progress * progress;
|
||||
return 16 * p2 * p2 * progress;
|
||||
}
|
||||
|
||||
var f = 2 * progress - 2;
|
||||
var f2 = f * f;
|
||||
return 0.5 * f2 * f2 * f + 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace PCL.Core.UI.Animation.Easings;
|
||||
|
||||
public class QuinticEaseOut : Easing
|
||||
{
|
||||
public static QuinticEaseOut Shared { get; } = new();
|
||||
|
||||
protected override double EaseCore(double progress)
|
||||
{
|
||||
var f = progress - 1d;
|
||||
var f2 = f * f;
|
||||
return f2 * f2 * f + 1d;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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 MatrixFromToAnimation : FromToAnimationBase<Matrix>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using PCL.Core.UI.Animation.Animatable;
|
||||
using PCL.Core.UI.Animation.Core;
|
||||
|
||||
namespace PCL.Core.UI.Animation;
|
||||
|
||||
public class NColorFromToAnimation : FromToAnimationBase<NColor>
|
||||
{
|
||||
public override IAnimationFrame? ComputeNextFrame(IAnimatable target)
|
||||
{
|
||||
// 应用缓动函数
|
||||
var easedProgress = Easing.Ease(CurrentFrame, TotalFrames);
|
||||
|
||||
// 计算当前值
|
||||
CurrentValue = ValueType == AnimationValueType.Relative
|
||||
? From + To * (float)easedProgress
|
||||
: From + (To - From) * (float)easedProgress;
|
||||
|
||||
return base.ComputeNextFrame(target);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
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<NRotateTransform>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
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<NScaleTransform>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.Windows;
|
||||
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 PointFromToAnimation : FromToAnimationBase<Point>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.Windows;
|
||||
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 ThicknessFromToAnimation : FromToAnimationBase<Thickness>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PCL.Core.UI.Animation.UIAccessProvider;
|
||||
|
||||
/// <summary>
|
||||
/// 跨框架的 UI 线程访问接口。
|
||||
/// </summary>
|
||||
public interface IUIAccessProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否在 UI 线程。
|
||||
/// </summary>
|
||||
bool CheckAccess();
|
||||
|
||||
/// <summary>
|
||||
/// 在 UI 线程同步执行。
|
||||
/// 如果当前就在 UI 线程,则直接执行。
|
||||
/// </summary>
|
||||
void Invoke(Action action);
|
||||
|
||||
/// <summary>
|
||||
/// 在 UI 线程异步执行。
|
||||
/// 如果当前就在 UI 线程,则直接执行。
|
||||
/// </summary>
|
||||
Task InvokeAsync(Action action);
|
||||
|
||||
/// <summary>
|
||||
/// 在 UI 线程异步执行,并返回结果。
|
||||
/// </summary>
|
||||
Task<T> InvokeAsync<T>(Func<T> func);
|
||||
|
||||
/// <summary>
|
||||
/// 在 UI 线程异步执行 Task。
|
||||
/// </summary>
|
||||
Task InvokeAsync(Func<Task> func);
|
||||
|
||||
/// <summary>
|
||||
/// 在 UI 线程异步执行 Task,并返回结果。
|
||||
/// </summary>
|
||||
Task<T> InvokeAsync<T>(Func<Task<T>> func);
|
||||
|
||||
/// <summary>
|
||||
/// UI 渲染时执行的事件。
|
||||
/// </summary>
|
||||
event EventHandler FrameTick;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace PCL.Core.UI.Animation.UIAccessProvider;
|
||||
|
||||
public sealed class WpfUIAccessProvider(Dispatcher dispatcher) : IUIAccessProvider
|
||||
{
|
||||
private readonly Dispatcher _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
|
||||
|
||||
public bool CheckAccess() => _dispatcher.CheckAccess();
|
||||
|
||||
public void Invoke(Action action)
|
||||
{
|
||||
if (_dispatcher.CheckAccess())
|
||||
action();
|
||||
else
|
||||
_dispatcher.Invoke(action, DispatcherPriority.Send);
|
||||
}
|
||||
|
||||
public Task InvokeAsync(Action action)
|
||||
{
|
||||
if (!_dispatcher.CheckAccess()) return _dispatcher.InvokeAsync(action, DispatcherPriority.Send).Task;
|
||||
action();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<T> InvokeAsync<T>(Func<T> func)
|
||||
{
|
||||
return _dispatcher.CheckAccess() ? Task.FromResult(func()) : _dispatcher.InvokeAsync(func, DispatcherPriority.Send).Task;
|
||||
}
|
||||
|
||||
public Task InvokeAsync(Func<Task> func)
|
||||
{
|
||||
return _dispatcher.CheckAccess() ? func() : _dispatcher.InvokeAsync(func, DispatcherPriority.Send).Task.Unwrap();
|
||||
}
|
||||
|
||||
public Task<T> InvokeAsync<T>(Func<Task<T>> func)
|
||||
{
|
||||
return _dispatcher.CheckAccess() ? func() : _dispatcher.InvokeAsync(func, DispatcherPriority.Send).Task.Unwrap();
|
||||
}
|
||||
|
||||
public event EventHandler FrameTick
|
||||
{
|
||||
add => Invoke(() => CompositionTarget.Rendering += value);
|
||||
remove => Invoke(() => CompositionTarget.Rendering -= value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
|
||||
namespace PCL.Core.UI.Animation.ValueProcessor;
|
||||
|
||||
public class DoubleValueProcessor : IValueProcessor<double>
|
||||
{
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
namespace PCL.Core.UI.Animation.ValueProcessor;
|
||||
|
||||
/// <summary>
|
||||
/// 数值处理器。
|
||||
/// </summary>
|
||||
public interface IValueProcessor<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// 过滤值。
|
||||
/// </summary>
|
||||
/// <param name="value">值。</param>
|
||||
/// <returns>返回过滤后的值。</returns>
|
||||
T Filter(T value);
|
||||
|
||||
/// <summary>
|
||||
/// 将两个值相加。
|
||||
/// </summary>
|
||||
/// <param name="value1">第一个值。</param>
|
||||
/// <param name="value2">第二个值。</param>
|
||||
/// <returns>返回相加后的值。</returns>
|
||||
T Add(T value1, T value2);
|
||||
|
||||
/// <summary>
|
||||
/// 将两个值相减。
|
||||
/// </summary>
|
||||
/// <param name="value1">第一个值。</param>
|
||||
/// <param name="value2">第二个值。</param>
|
||||
/// <returns>返回相减后的值。</returns>
|
||||
T Subtract(T value1, T value2);
|
||||
|
||||
/// <summary>
|
||||
/// 将值按比例因子进行缩放。
|
||||
/// </summary>
|
||||
/// <param name="value">需要缩放的值。</param>
|
||||
/// <param name="factor">缩放因子。</param>
|
||||
/// <returns>返回缩放后的值。</returns>
|
||||
T Scale(T value, double factor);
|
||||
|
||||
/// <summary>
|
||||
/// 获取某种类型的初始值。
|
||||
/// </summary>
|
||||
/// <returns>初始值。</returns>
|
||||
T DefaultValue();
|
||||
|
||||
/// <summary>
|
||||
/// 比较两个值是否相等。
|
||||
/// </summary>
|
||||
/// <param name="value1">第一个值。</param>
|
||||
/// <param name="value2">第二个值。</param>
|
||||
/// <returns></returns>
|
||||
bool Equal(T value1, T value2);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace PCL.Core.UI.Animation.ValueProcessor;
|
||||
|
||||
public class MatrixValueProcessor : IValueProcessor<Matrix>
|
||||
{
|
||||
public Matrix Filter(Matrix value) => value;
|
||||
|
||||
public Matrix Add(Matrix value1, Matrix value2)
|
||||
{
|
||||
return new Matrix(
|
||||
value1.M11 + value2.M11, value1.M12 + value2.M12,
|
||||
value1.M21 + value2.M21, value1.M22 + value2.M22,
|
||||
value1.OffsetX + value2.OffsetX, value1.OffsetY + value2.OffsetY);
|
||||
}
|
||||
|
||||
public Matrix Subtract(Matrix value1, Matrix value2)
|
||||
{
|
||||
return new Matrix(
|
||||
value1.M11 - value2.M11, value1.M12 - value2.M12,
|
||||
value1.M21 - value2.M21, value1.M22 - value2.M22,
|
||||
value1.OffsetX - value2.OffsetX, value1.OffsetY - value2.OffsetY);
|
||||
}
|
||||
|
||||
public Matrix Scale(Matrix value, double factor)
|
||||
{
|
||||
return new Matrix(
|
||||
value.M11 * factor, value.M12 * 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;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace PCL.Core.UI.Animation.ValueProcessor;
|
||||
|
||||
public class NColorValueProcessor : IValueProcessor<NColor>
|
||||
{
|
||||
public NColor Filter(NColor value)
|
||||
{
|
||||
if (value.A < 0) value.A = 0;
|
||||
if (value.R < 0) value.R = 0;
|
||||
if (value.G < 0) value.G = 0;
|
||||
if (value.B < 0) value.B = 0;
|
||||
|
||||
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;
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
namespace PCL.Core.UI.Animation.ValueProcessor;
|
||||
|
||||
public class NRotateTransformValueProcessor : IValueProcessor<NRotateTransform>
|
||||
{
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace PCL.Core.UI.Animation.ValueProcessor;
|
||||
|
||||
public class NScaleTransformValueProcessor : IValueProcessor<NScaleTransform>
|
||||
{
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace PCL.Core.UI.Animation.ValueProcessor;
|
||||
|
||||
public class PointValueProcessor : IValueProcessor<Point>
|
||||
{
|
||||
public Point Filter(Point value) => value;
|
||||
|
||||
public Point Add(Point value1, Point value2) => new(value1.X + value2.X, value1.Y + value2.Y);
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace PCL.Core.UI.Animation.ValueProcessor;
|
||||
|
||||
public class ThicknessValueProcessor : IValueProcessor<Thickness>
|
||||
{
|
||||
public Thickness Filter(Thickness value) => value;
|
||||
|
||||
public Thickness Add(Thickness value1, Thickness value2)
|
||||
{
|
||||
return new Thickness(value1.Left + value2.Left,
|
||||
value1.Top + value2.Top,
|
||||
value1.Right + value2.Right,
|
||||
value1.Bottom + value2.Bottom);
|
||||
}
|
||||
|
||||
public Thickness Subtract(Thickness value1, Thickness value2)
|
||||
{
|
||||
return new Thickness(value1.Left - value2.Left,
|
||||
value1.Top - value2.Top,
|
||||
value1.Right - value2.Right,
|
||||
value1.Bottom - value2.Bottom);
|
||||
}
|
||||
|
||||
public Thickness Scale(Thickness value, double factor)
|
||||
{
|
||||
return new Thickness(value.Left * factor,
|
||||
value.Top * factor,
|
||||
value.Right * factor,
|
||||
value.Bottom * factor);
|
||||
}
|
||||
|
||||
public Thickness DefaultValue() => new();
|
||||
|
||||
public bool Equal(Thickness value1, Thickness value2) => value1 == value2;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace PCL.Core.UI.Animation.ValueProcessor;
|
||||
|
||||
public static class ValueProcessorManager
|
||||
{
|
||||
private static readonly Dictionary<Type, Func<object, object>> _Filters = new();
|
||||
private static readonly Dictionary<Type, Func<object, object, object>> _Adders = new();
|
||||
private static readonly Dictionary<Type, Func<object, double, object>> _Scalers = new();
|
||||
|
||||
private static class Cache<T>
|
||||
{
|
||||
public static IValueProcessor<T>? Processor;
|
||||
}
|
||||
|
||||
public static void Register<T>(IValueProcessor<T> processor)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(processor);
|
||||
Cache<T>.Processor = processor;
|
||||
|
||||
_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>(T value)
|
||||
{
|
||||
var p = Cache<T>.Processor;
|
||||
return p is null ? value : p.Filter(value);
|
||||
}
|
||||
|
||||
public static object Filter(object value)
|
||||
{
|
||||
var t = value.GetType();
|
||||
return _Filters.TryGetValue(t, out var func)
|
||||
? func(value)
|
||||
: value;
|
||||
}
|
||||
|
||||
public static T Add<T>(T value1, T value2)
|
||||
{
|
||||
var p = Cache<T>.Processor
|
||||
?? throw new InvalidOperationException($"类型未注册:{typeof(T)}");
|
||||
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>(T value1, T value2)
|
||||
{
|
||||
var p = Cache<T>.Processor
|
||||
?? throw new InvalidOperationException($"类型未注册:{typeof(T)}");
|
||||
return p.Subtract(value1, value2);
|
||||
}
|
||||
|
||||
public static T Scale<T>(T value, double factor)
|
||||
{
|
||||
var p = Cache<T>.Processor
|
||||
?? 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<T>()
|
||||
{
|
||||
var p = Cache<T>.Processor
|
||||
?? throw new InvalidOperationException($"类型未注册:{typeof(T)}");
|
||||
return p.DefaultValue();
|
||||
}
|
||||
|
||||
public static bool Equal<T>(T value1, T value2)
|
||||
{
|
||||
var p = Cache<T>.Processor
|
||||
?? throw new InvalidOperationException($"类型未注册:{typeof(T)}");
|
||||
return p.Equal(value1, value2);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user