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);
}
}
@@ -0,0 +1,43 @@
ISC License
Copyright (c) 2026 Lucide Icons and Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
---
The following Lucide icons are derived from the Feather project:
airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out
The MIT License (MIT) (for the icons listed above)
Copyright (c) 2013-present Cole Bemis
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,19 @@
<!-- @license lucide-static v1.17.0 - ISC -->
<svg
class="lucide lucide-anvil"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M7 10H6a4 4 0 0 1-4-4 1 1 0 0 1 1-1h4" />
<path d="M7 5a1 1 0 0 1 1-1h13a1 1 0 0 1 1 1 7 7 0 0 1-7 7H8a1 1 0 0 1-1-1z" />
<path d="M9 12v5" />
<path d="M15 12v5" />
<path d="M5 20a3 3 0 0 1 3-3h8a3 3 0 0 1 3 3 1 1 0 0 1-1 1H6a1 1 0 0 1-1-1" />
</svg>

After

Width:  |  Height:  |  Size: 547 B

@@ -0,0 +1,16 @@
<!-- @license lucide-static v1.17.0 - ISC -->
<svg
class="lucide lucide-arrow-left"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m12 19-7-7 7-7" />
<path d="M19 12H5" />
</svg>

After

Width:  |  Height:  |  Size: 343 B

@@ -0,0 +1,16 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m16 3 4 4-4 4" />
<path d="M20 7H4" />
<path d="m8 21-4-4 4-4" />
<path d="M4 17h16" />
</svg>

After

Width:  |  Height:  |  Size: 313 B

@@ -0,0 +1,16 @@
<!-- @license lucide-static v1.17.0 - ISC -->
<svg
class="lucide lucide-arrow-right"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M5 12h14" />
<path d="m12 5 7 7-7 7" />
</svg>

After

Width:  |  Height:  |  Size: 343 B

@@ -0,0 +1,18 @@
<!-- @license lucide-static v1.17.0 - ISC -->
<svg
class="lucide lucide-arrow-up-down"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m21 16-4 4-4-4" />
<path d="M17 20V4" />
<path d="m3 8 4-4 4 4" />
<path d="M7 4v16" />
</svg>

After

Width:  |  Height:  |  Size: 397 B

@@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m18 9-6-6-6 6" />
<path d="M12 3v14" />
<path d="M5 21h14" />
</svg>

After

Width:  |  Height:  |  Size: 285 B

@@ -0,0 +1,17 @@
<!-- @license lucide-static v1.17.0 - ISC -->
<svg
class="lucide lucide-arrow-up-to-line"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M5 3h14" />
<path d="m18 13-6-6-6 6" />
<path d="M12 7v14" />
</svg>

After

Width:  |  Height:  |  Size: 372 B

@@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M10 2v8l3-3 3 3V2" />
<path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20" />
</svg>

After

Width:  |  Height:  |  Size: 343 B

@@ -0,0 +1,17 @@
<!-- @license lucide-static v1.17.0 - ISC -->
<svg
class="lucide lucide-box"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z" />
<path d="m3.3 7 8.7 5 8.7-5" />
<path d="M12 22V12" />
</svg>

After

Width:  |  Height:  |  Size: 475 B

@@ -0,0 +1,24 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M2.97 12.92A2 2 0 0 0 2 14.63v3.24a2 2 0 0 0 .97 1.71l3 1.8a2 2 0 0 0 2.06 0L12 19v-5.5l-5-3-4.03 2.42Z" />
<path d="m7 16.5-4.74-2.85" />
<path d="m7 16.5 5-3" />
<path d="M7 16.5v5.17" />
<path d="M12 13.5V19l3.97 2.38a2 2 0 0 0 2.06 0l3-1.8a2 2 0 0 0 .97-1.71v-3.24a2 2 0 0 0-.97-1.71L17 10.5l-5 3Z" />
<path d="m17 16.5-5-3" />
<path d="m17 16.5 4.74-2.85" />
<path d="M17 16.5v5.17" />
<path d="M7.97 4.42A2 2 0 0 0 7 6.13v4.37l5 3 5-3V6.13a2 2 0 0 0-.97-1.71l-3-1.8a2 2 0 0 0-2.06 0l-3 1.8Z" />
<path d="M12 8 7.26 5.15" />
<path d="m12 8 4.74-2.85" />
<path d="M12 13.5V8" />
</svg>

After

Width:  |  Height:  |  Size: 824 B

@@ -0,0 +1,16 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M7.001 15.085A1.5 1.5 0 0 1 9 16.5" />
<circle cx="18.5" cy="8.5" r="3.5" />
<circle cx="7.5" cy="16.5" r="5.5" />
<circle cx="7.5" cy="4.5" r="2.5" />
</svg>

After

Width:  |  Height:  |  Size: 377 B

@@ -0,0 +1,16 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 5c.67 0 1.35.09 2 .26 1.78-2 5.03-2.84 6.42-2.26 1.4.58-.42 7-.42 7 .57 1.07 1 2.24 1 3.44C21 17.9 16.97 21 12 21s-9-3-9-7.56c0-1.25.5-2.4 1-3.44 0 0-1.89-6.42-.5-7 1.39-.58 4.72.23 6.5 2.23A9.04 9.04 0 0 1 12 5Z" />
<path d="M8 14v.5" />
<path d="M16 14v.5" />
<path d="M11.25 16.25h1.5L12 17l-.75-.75Z" />
</svg>

After

Width:  |  Height:  |  Size: 537 B

Some files were not shown because too many files have changed in this diff Show More