feat: 项目初始化 + 3D方块世界原型 + AI助搭系统
CI / Go Backend (push) Canceled after 0s

初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器

HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块

Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚

AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
xyou
2026-08-08 14:07:56 +08:00
parent 9500c4c80a
commit f70b061d1a
1972 changed files with 159760 additions and 6 deletions
@@ -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;
}
@@ -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);
}
}